332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
export type Command =
|
|
| { type: 'quit' }
|
|
| { type: 'reset' }
|
|
| { type: 'help' }
|
|
| { type: 'status' }
|
|
| { type: 'fullscreen' }
|
|
| { type: 'compact' }
|
|
| { type: 'usage' }
|
|
| { type: 'verbose' }
|
|
| { type: 'model'; name?: string; providerModel?: string }
|
|
| { type: 'backend'; provider?: string }
|
|
| { type: 'login'; provider?: string }
|
|
| { type: 'transfer'; target: string }
|
|
| { type: 'pair'; action?: 'generate' | 'list' | 'revoke'; args?: string }
|
|
| { type: 'queue'; action?: 'show' | 'set' | 'reset'; args?: string }
|
|
| { type: 'elevate'; args?: 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' || trimmed === '/new') {
|
|
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' };
|
|
}
|
|
|
|
// Compact
|
|
if (trimmed === '/compact') {
|
|
return { type: 'compact' };
|
|
}
|
|
|
|
// Usage
|
|
if (trimmed === '/usage') {
|
|
return { type: 'usage' };
|
|
}
|
|
|
|
// Verbose
|
|
if (trimmed === '/verbose') {
|
|
return { type: 'verbose' };
|
|
}
|
|
|
|
// Model (with optional argument)
|
|
if (trimmed === '/model') {
|
|
return { type: 'model' };
|
|
}
|
|
if (trimmed.startsWith('/model ')) {
|
|
const args = trimmed.slice('/model '.length).trim();
|
|
const parts = args.split(/\s+/);
|
|
|
|
// /model <tier> <provider/model> - change tier's provider/model
|
|
if (parts.length === 2 && parts[1].includes('/')) {
|
|
return { type: 'model', name: parts[0], providerModel: parts[1] };
|
|
}
|
|
|
|
// /model <name> - single word (backward compatibility)
|
|
const name = parts[0];
|
|
return { type: 'model', name };
|
|
}
|
|
|
|
// Backend (with optional argument)
|
|
if (trimmed === '/backend') {
|
|
return { type: 'backend' };
|
|
}
|
|
if (trimmed.startsWith('/backend ')) {
|
|
const provider = trimmed.slice('/backend '.length).trim();
|
|
return { type: 'backend', provider };
|
|
}
|
|
|
|
// Transfer
|
|
if (trimmed.startsWith('/transfer ')) {
|
|
const target = trimmed.slice('/transfer '.length).trim();
|
|
return { type: 'transfer', target };
|
|
}
|
|
|
|
// Login
|
|
if (trimmed === '/login') {
|
|
return { type: 'login' };
|
|
}
|
|
if (trimmed.startsWith('/login ')) {
|
|
const provider = trimmed.slice('/login '.length).trim();
|
|
return { type: 'login', provider: provider || undefined };
|
|
}
|
|
|
|
// Pair
|
|
if (trimmed === '/pair' || trimmed === '/pair list') {
|
|
return { type: 'pair', action: 'list' };
|
|
}
|
|
if (trimmed === '/pair generate' || trimmed.startsWith('/pair generate ')) {
|
|
const label = trimmed.slice('/pair generate'.length).trim() || undefined;
|
|
return { type: 'pair', action: 'generate', args: label };
|
|
}
|
|
if (trimmed.startsWith('/pair revoke ')) {
|
|
const args = trimmed.slice('/pair revoke '.length).trim();
|
|
return { type: 'pair', action: 'revoke', args };
|
|
}
|
|
|
|
// Queue
|
|
if (trimmed === '/queue' || trimmed === '/queue show') {
|
|
return { type: 'queue', action: 'show' };
|
|
}
|
|
if (trimmed === '/queue reset') {
|
|
return { type: 'queue', action: 'reset' };
|
|
}
|
|
if (trimmed.startsWith('/queue set ')) {
|
|
const args = trimmed.slice('/queue set '.length).trim();
|
|
return { type: 'queue', action: 'set', args };
|
|
}
|
|
if (trimmed.startsWith('/queue ')) {
|
|
const args = trimmed.slice('/queue '.length).trim();
|
|
return { type: 'queue', action: 'set', args };
|
|
}
|
|
|
|
// Elevate
|
|
if (trimmed === '/elevate') {
|
|
return { type: 'elevate' };
|
|
}
|
|
if (trimmed.startsWith('/elevate ')) {
|
|
const args = trimmed.slice('/elevate '.length).trim();
|
|
return { type: 'elevate', args };
|
|
}
|
|
|
|
// Regular message
|
|
return { type: 'message', content: trimmed };
|
|
}
|
|
|
|
export function getHelpText(): string {
|
|
return `
|
|
Commands:
|
|
/help, /? Show this help
|
|
/model [name] Show or switch model tier (local, default, fast, complex)
|
|
/model <tier> <p/m> Change tier's provider/model (e.g. /model default anthropic/claude-sonnet-4)
|
|
/backend [provider] Show or switch local backend (ollama, llamacpp)
|
|
/login [provider] Authenticate with GitHub, OpenAI, Anthropic, or Z.AI
|
|
/pair List pending pairing codes and approved senders
|
|
/pair generate [label] Generate a new DM pairing code
|
|
/pair revoke <ch> <id> Revoke an approved sender
|
|
/queue Show queue policy for this session
|
|
/queue set <k> <v> Set queue override (mode/cap/overflow/debounce_ms/summarize_overflow)
|
|
/queue reset Clear queue overrides for this session
|
|
/elevate [args] Show or manage elevated mode
|
|
/reset, /clear, /new Clear conversation history
|
|
/compact Compact conversation history
|
|
/usage Show token usage and estimated cost
|
|
/verbose Toggle verbose mode (show raw streaming and tool output)
|
|
/status Show session info and token usage
|
|
/fullscreen, /fs Switch to fullscreen mode
|
|
/transfer <dest> Transfer session to another frontend
|
|
/quit, /exit Exit TUI
|
|
`.trim();
|
|
}
|
|
|
|
import { MODEL_PROVIDERS } from '../../config/index.js';
|
|
|
|
export type ModelAlias = 'local' | 'default' | 'fast' | 'complex' | 'opus' | 'sonnet' | 'haiku' | 'ollama';
|
|
|
|
// List of all slash commands for autocompletion
|
|
export const SLASH_COMMANDS = [
|
|
'/help',
|
|
'/model',
|
|
'/backend',
|
|
'/reset',
|
|
'/clear',
|
|
'/new',
|
|
'/compact',
|
|
'/usage',
|
|
'/verbose',
|
|
'/status',
|
|
'/fullscreen',
|
|
'/fs',
|
|
'/login',
|
|
'/pair',
|
|
'/queue',
|
|
'/elevate',
|
|
'/transfer',
|
|
'/quit',
|
|
'/exit',
|
|
];
|
|
|
|
// Command descriptions for tooltips
|
|
export const COMMAND_TOOLTIPS: Record<string, string> = {
|
|
'/help': 'Show available commands',
|
|
'/model': 'Show or switch model (local, default, fast, complex)',
|
|
'/backend': 'Show or switch local backend (ollama, llamacpp)',
|
|
'/reset': 'Clear conversation history',
|
|
'/clear': 'Clear conversation history',
|
|
'/new': 'Start a new conversation',
|
|
'/compact': 'Compact conversation history to save context space',
|
|
'/usage': 'Show token usage and estimated cost',
|
|
'/verbose': 'Toggle verbose mode (show raw streaming and tool output)',
|
|
'/status': 'Show session info and token usage',
|
|
'/fullscreen': 'Switch to fullscreen mode',
|
|
'/fs': 'Switch to fullscreen mode',
|
|
'/login': 'Authenticate with GitHub/OpenAI/Anthropic (OAuth/token or API key) or Z.AI (API key store)',
|
|
'/pair': 'Generate/list/revoke DM pairing codes',
|
|
'/queue': 'Show or update per-session queue policy',
|
|
'/elevate': 'Show or manage elevated mode',
|
|
'/transfer': 'Transfer session to another frontend',
|
|
'/quit': 'Exit TUI',
|
|
'/exit': 'Exit TUI',
|
|
};
|
|
|
|
// Model aliases for /model command autocompletion
|
|
export const MODEL_ALIASES = ['local', 'default', 'fast', 'complex', 'opus', 'sonnet', 'haiku', 'ollama'];
|
|
|
|
// Provider names for /model <tier> <provider/model> syntax — derived from config schema
|
|
export const PROVIDER_NAMES: readonly string[] = MODEL_PROVIDERS;
|
|
|
|
// Model alias descriptions
|
|
export const MODEL_TOOLTIPS: Record<string, string> = {
|
|
local: 'Local model (Ollama/llama.cpp)',
|
|
default: 'Default model tier',
|
|
fast: 'Fast/lightweight model tier',
|
|
complex: 'Complex reasoning model tier',
|
|
opus: 'Alias for complex tier',
|
|
sonnet: 'Alias for default tier',
|
|
haiku: 'Alias for fast tier',
|
|
ollama: 'Alias for local tier',
|
|
};
|
|
|
|
export function getCommandCompletions(partial: string): string[] {
|
|
const trimmed = partial.trim();
|
|
|
|
// Complete /model <tier> <provider/model>
|
|
if (trimmed.startsWith('/model ')) {
|
|
const args = trimmed.slice('/model '.length).trim();
|
|
const parts = args.split(/\s+/);
|
|
|
|
if (parts.length === 1) {
|
|
// Single word - suggest model aliases
|
|
const modelPartial = parts[0].toLowerCase();
|
|
return MODEL_ALIASES
|
|
.filter(alias => alias.startsWith(modelPartial))
|
|
.map(alias => `/model ${alias}`);
|
|
} else if (parts.length === 2) {
|
|
// Two words - suggest provider prefixes
|
|
const providerPartial = parts[1].toLowerCase();
|
|
return PROVIDER_NAMES
|
|
.filter(provider => provider.startsWith(providerPartial))
|
|
.map(provider => `/model ${parts[0]} ${provider}`);
|
|
}
|
|
}
|
|
|
|
// Complete slash commands
|
|
if (trimmed.startsWith('/')) {
|
|
return SLASH_COMMANDS.filter(cmd => cmd.startsWith(trimmed.toLowerCase()));
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
export function getCommandTooltip(partial: string): string | null {
|
|
const trimmed = partial.trim().toLowerCase();
|
|
|
|
// Tooltip for /model arguments
|
|
if (trimmed.startsWith('/model ')) {
|
|
const args = trimmed.slice('/model '.length).trim();
|
|
const parts = args.split(/\s+/);
|
|
|
|
if (parts.length === 1) {
|
|
// Single word - model tier or provider
|
|
const modelArg = parts[0].toLowerCase();
|
|
if (modelArg && MODEL_TOOLTIPS[modelArg]) {
|
|
return MODEL_TOOLTIPS[modelArg];
|
|
}
|
|
// Show tooltip for partial match
|
|
const matches = MODEL_ALIASES.filter(a => a.startsWith(modelArg));
|
|
if (matches.length === 1 && MODEL_TOOLTIPS[matches[0]]) {
|
|
return MODEL_TOOLTIPS[matches[0]];
|
|
}
|
|
return 'Choose: local, default, fast, complex';
|
|
} else if (parts.length === 2) {
|
|
// Two words - tier + provider
|
|
const providerPartial = parts[1].toLowerCase();
|
|
const matches = PROVIDER_NAMES.filter(p => p.startsWith(providerPartial));
|
|
if (matches.length === 1) {
|
|
return `Enter provider/model (e.g. ${matches[0]}/...)`;
|
|
}
|
|
return 'Enter provider/model (e.g. anthropic/claude-sonnet-4)';
|
|
}
|
|
}
|
|
|
|
// Exact match tooltip
|
|
if (COMMAND_TOOLTIPS[trimmed]) {
|
|
return COMMAND_TOOLTIPS[trimmed];
|
|
}
|
|
|
|
// Partial match - show tooltip if only one command matches
|
|
if (trimmed.startsWith('/')) {
|
|
const matches = SLASH_COMMANDS.filter(cmd => cmd.startsWith(trimmed));
|
|
if (matches.length === 1 && COMMAND_TOOLTIPS[matches[0]]) {
|
|
return COMMAND_TOOLTIPS[matches[0]];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function resolveModelAlias(alias: string): 'local' | 'default' | 'fast' | 'complex' {
|
|
const map: Record<string, 'local' | 'default' | 'fast' | 'complex'> = {
|
|
local: 'local',
|
|
ollama: 'local',
|
|
default: 'default',
|
|
sonnet: 'default',
|
|
fast: 'fast',
|
|
haiku: 'fast',
|
|
complex: 'complex',
|
|
opus: 'complex',
|
|
};
|
|
return map[alias.toLowerCase()] ?? 'default';
|
|
}
|