diff --git a/src/frontends/tui/commands.test.ts b/src/frontends/tui/commands.test.ts new file mode 100644 index 0000000..917b408 --- /dev/null +++ b/src/frontends/tui/commands.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from 'vitest'; +import { parseCommand, getHelpText } from './commands.js'; + +describe('parseCommand', () => { + it('parses /quit command', () => { + expect(parseCommand('/quit')).toEqual({ type: 'quit' }); + expect(parseCommand('/exit')).toEqual({ type: 'quit' }); + }); + + it('parses /reset command', () => { + expect(parseCommand('/reset')).toEqual({ type: 'reset' }); + expect(parseCommand('/clear')).toEqual({ type: 'reset' }); + }); + + it('parses /help command', () => { + expect(parseCommand('/help')).toEqual({ type: 'help' }); + expect(parseCommand('/?')).toEqual({ type: 'help' }); + }); + + it('parses /status command', () => { + expect(parseCommand('/status')).toEqual({ type: 'status' }); + }); + + it('parses /fullscreen command', () => { + expect(parseCommand('/fullscreen')).toEqual({ type: 'fullscreen' }); + expect(parseCommand('/fs')).toEqual({ type: 'fullscreen' }); + }); + + it('parses /model command without argument', () => { + expect(parseCommand('/model')).toEqual({ type: 'model' }); + }); + + it('parses /model command with argument', () => { + expect(parseCommand('/model local')).toEqual({ type: 'model', name: 'local' }); + expect(parseCommand('/model opus')).toEqual({ type: 'model', name: 'opus' }); + }); + + it('parses /transfer command', () => { + expect(parseCommand('/transfer telegram')).toEqual({ type: 'transfer', target: 'telegram' }); + }); + + it('parses regular message', () => { + expect(parseCommand('Hello Flynn')).toEqual({ type: 'message', content: 'Hello Flynn' }); + }); + + it('returns null for empty input', () => { + expect(parseCommand('')).toBeNull(); + expect(parseCommand(' ')).toBeNull(); + }); +}); + +describe('getHelpText', () => { + it('returns help text with all commands', () => { + const help = getHelpText(); + expect(help).toContain('/help'); + expect(help).toContain('/model'); + expect(help).toContain('/reset'); + expect(help).toContain('/quit'); + }); +}); diff --git a/src/frontends/tui/commands.ts b/src/frontends/tui/commands.ts new file mode 100644 index 0000000..015f4e7 --- /dev/null +++ b/src/frontends/tui/commands.ts @@ -0,0 +1,85 @@ +export type Command = + | { type: 'quit' } + | { type: 'reset' } + | { type: 'help' } + | { type: 'status' } + | { type: 'fullscreen' } + | { type: 'model'; name?: string } + | { type: 'transfer'; target: string } + | { type: 'message'; content: string }; + +export function parseCommand(input: string): Command | null { + const trimmed = input.trim(); + if (!trimmed) return null; + + // Quit + if (trimmed === '/quit' || trimmed === '/exit') { + return { type: 'quit' }; + } + + // Reset + if (trimmed === '/reset' || trimmed === '/clear') { + return { type: 'reset' }; + } + + // Help + if (trimmed === '/help' || trimmed === '/?') { + return { type: 'help' }; + } + + // Status + if (trimmed === '/status') { + return { type: 'status' }; + } + + // Fullscreen + if (trimmed === '/fullscreen' || trimmed === '/fs') { + return { type: 'fullscreen' }; + } + + // Model (with optional argument) + if (trimmed === '/model') { + return { type: 'model' }; + } + if (trimmed.startsWith('/model ')) { + const name = trimmed.slice('/model '.length).trim(); + return { type: 'model', name }; + } + + // Transfer + if (trimmed.startsWith('/transfer ')) { + const target = trimmed.slice('/transfer '.length).trim(); + return { type: 'transfer', target }; + } + + // Regular message + return { type: 'message', content: trimmed }; +} + +export function getHelpText(): string { + return ` +Commands: + /help, /? Show this help + /model [name] Show or switch model (local, default, fast, complex) + /reset, /clear Clear conversation history + /status Show session info and token usage + /fullscreen, /fs Switch to fullscreen mode + /transfer Transfer session to another frontend + /quit, /exit Exit TUI +`.trim(); +} + +export type ModelAlias = 'local' | 'default' | 'fast' | 'complex' | 'opus' | 'sonnet' | 'ollama'; + +export function resolveModelAlias(alias: string): 'local' | 'default' | 'fast' | 'complex' { + const map: Record = { + local: 'local', + ollama: 'local', + default: 'default', + opus: 'default', + fast: 'fast', + sonnet: 'fast', + complex: 'complex', + }; + return map[alias.toLowerCase()] ?? 'default'; +}