feat(cli): TUI slash command parsing + local commands (P8-009) (#176)

Co-authored-by: Jason Woltje <[email protected]>
Co-committed-by: Jason Woltje <[email protected]>
This commit is contained in:
2026-03-16 01:58:56 +00:00
committed by jason.woltje
parent 5a1991924c
commit f0741e045f
11 changed files with 328 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
export { parseSlashCommand } from './parse.js';
export { commandRegistry, CommandRegistry } from './registry.js';
export { executeHelp } from './local/help.js';
export { executeStatus } from './local/status.js';
export type { StatusContext } from './local/status.js';
@@ -0,0 +1,19 @@
import type { ParsedCommand } from '@mosaic/types';
import { commandRegistry } from '../registry.js';
export function executeHelp(_parsed: ParsedCommand): string {
const commands = commandRegistry.getAll();
const lines = ['Available commands:', ''];
for (const cmd of commands) {
const aliases =
cmd.aliases.length > 0 ? ` (${cmd.aliases.map((a) => `/${a}`).join(', ')})` : '';
const argsStr =
cmd.args && cmd.args.length > 0
? ' ' + cmd.args.map((a) => (a.optional ? `[${a.name}]` : `<${a.name}>`)).join(' ')
: '';
lines.push(` /${cmd.name}${argsStr}${aliases}${cmd.description}`);
}
return lines.join('\n').trimEnd();
}
@@ -0,0 +1,20 @@
import type { ParsedCommand } from '@mosaic/types';
export interface StatusContext {
connected: boolean;
model: string | null;
provider: string | null;
sessionId: string | null;
tokenCount: number;
}
export function executeStatus(_parsed: ParsedCommand, ctx: StatusContext): string {
const lines = [
`Connection: ${ctx.connected ? 'connected' : 'disconnected'}`,
`Model: ${ctx.model ?? 'unknown'}`,
`Provider: ${ctx.provider ?? 'unknown'}`,
`Session: ${ctx.sessionId ?? 'none'}`,
`Tokens (session): ${ctx.tokenCount}`,
];
return lines.join('\n');
}
+11
View File
@@ -0,0 +1,11 @@
import type { ParsedCommand } from '@mosaic/types';
export function parseSlashCommand(input: string): ParsedCommand | null {
const match = input.match(/^\/([a-z][a-z0-9:_-]*)\s*(.*)?$/i);
if (!match) return null;
return {
command: match[1]!,
args: match[2]?.trim() || null,
raw: input,
};
}
+98
View File
@@ -0,0 +1,98 @@
import type { CommandDef, CommandManifest } from '@mosaic/types';
// Local-only commands (work even when gateway is disconnected)
const LOCAL_COMMANDS: CommandDef[] = [
{
name: 'help',
description: 'Show available commands',
aliases: ['h'],
args: undefined,
execution: 'local',
available: true,
scope: 'core',
},
{
name: 'stop',
description: 'Cancel current streaming response',
aliases: [],
args: undefined,
execution: 'local',
available: true,
scope: 'core',
},
{
name: 'cost',
description: 'Show token usage and cost for current session',
aliases: [],
args: undefined,
execution: 'local',
available: true,
scope: 'core',
},
{
name: 'status',
description: 'Show connection and session status',
aliases: ['s'],
args: undefined,
execution: 'local',
available: true,
scope: 'core',
},
{
name: 'clear',
description: 'Clear the current conversation display',
aliases: [],
args: undefined,
execution: 'local',
available: true,
scope: 'core',
},
];
const ALIASES: Record<string, string> = {
n: 'new',
m: 'model',
t: 'thinking',
a: 'agent',
s: 'status',
h: 'help',
pref: 'preferences',
};
export class CommandRegistry {
private gatewayManifest: CommandManifest | null = null;
updateManifest(manifest: CommandManifest): void {
this.gatewayManifest = manifest;
}
resolveAlias(command: string): string {
return ALIASES[command] ?? command;
}
find(command: string): CommandDef | null {
const resolved = this.resolveAlias(command);
// Search local first, then gateway manifest
const local = LOCAL_COMMANDS.find((c) => c.name === resolved || c.aliases.includes(resolved));
if (local) return local;
if (this.gatewayManifest) {
return (
this.gatewayManifest.commands.find(
(c) => c.name === resolved || c.aliases.includes(resolved),
) ?? null
);
}
return null;
}
getAll(): CommandDef[] {
const gateway = this.gatewayManifest?.commands ?? [];
return [...LOCAL_COMMANDS, ...gateway];
}
getLocalCommands(): CommandDef[] {
return LOCAL_COMMANDS;
}
}
export const commandRegistry = new CommandRegistry();