From c44bc387b7f61f3800d585f8485c4da0833f7e4e Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 24 Feb 2026 10:41:33 -0800 Subject: [PATCH] fix(tui): reserve /runtime and block local tool-loop fallback --- src/frontends/tui/commands.test.ts | 11 +++++++++++ src/frontends/tui/commands.ts | 13 +++++++++++++ src/frontends/tui/components/App.tsx | 12 ++++++++++++ src/frontends/tui/minimal.test.ts | 23 +++++++++++++++++++++++ src/frontends/tui/minimal.ts | 14 ++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/src/frontends/tui/commands.test.ts b/src/frontends/tui/commands.test.ts index ef845cb..9478979 100644 --- a/src/frontends/tui/commands.test.ts +++ b/src/frontends/tui/commands.test.ts @@ -113,6 +113,11 @@ describe('parseCommand', () => { expect(parseCommand('/backend ollama')).toEqual({ type: 'backend', provider: 'ollama' }); }); + it('parses /runtime command', () => { + expect(parseCommand('/runtime')).toEqual({ type: 'runtime' }); + expect(parseCommand('/runtime status')).toEqual({ type: 'runtime', input: 'status' }); + }); + it('parses /transfer command', () => { expect(parseCommand('/transfer telegram')).toEqual({ type: 'transfer', target: 'telegram' }); expect(parseCommand('/transfer')).toEqual({ type: 'transfer', target: '' }); @@ -146,6 +151,7 @@ describe('getHelpText', () => { expect(help).toContain('/help'); expect(help).toContain('/paste'); expect(help).toContain('/model'); + expect(help).toContain('/runtime'); expect(help).toContain('/tools'); expect(help).toContain('/research'); expect(help).toContain('/council'); @@ -215,6 +221,11 @@ describe('getCommandCompletions', () => { const completions = getCommandCompletions('/council pre'); expect(completions).toEqual(['/council preflight']); }); + + it('completes /runtime command', () => { + const completions = getCommandCompletions('/ru'); + expect(completions).toContain('/runtime'); + }); }); describe('isToolInventoryQuery', () => { diff --git a/src/frontends/tui/commands.ts b/src/frontends/tui/commands.ts index aa73983..599a026 100644 --- a/src/frontends/tui/commands.ts +++ b/src/frontends/tui/commands.ts @@ -14,6 +14,7 @@ export type Command = | { type: 'verbose' } | { type: 'model'; name?: string; providerModel?: string } | { type: 'backend'; provider?: string } + | { type: 'runtime'; input?: string } | { type: 'login'; provider?: string } | { type: 'transfer'; target: string } | { type: 'pair'; action?: 'generate' | 'list' | 'revoke'; args?: string } @@ -154,6 +155,15 @@ export function parseCommand(input: string): Command | null { return { type: 'backend', provider }; } + // Runtime backend mode control (daemon/channel command; reserved in TUI) + if (trimmed === '/runtime') { + return { type: 'runtime' }; + } + if (trimmed.startsWith('/runtime ')) { + const input = trimmed.slice('/runtime '.length).trim(); + return { type: 'runtime', input }; + } + // Transfer if (trimmed === '/transfer') { return { type: 'transfer', target: '' }; @@ -223,6 +233,7 @@ Commands: /model [name] Show or switch model tier (local, default, fast, complex) /model

Change tier's provider/model (e.g. /model default anthropic/claude-sonnet-4) /backend [provider] Show or switch local backend (ollama, llamacpp) + /runtime [args] Runtime backend mode control (daemon/channel sessions) /research Delegate a task to the configured research agent /council Run the councils pipeline for a task /council preflight Check council tier routing, endpoint/auth mode, and probe latency @@ -261,6 +272,7 @@ export const SLASH_COMMANDS = [ '/tools', '/model', '/backend', + '/runtime', '/research', '/council', '/reset', @@ -293,6 +305,7 @@ export const COMMAND_TOOLTIPS: Record = { '/tools': 'Show authoritative runtime tool list for this session', '/model': 'Show or switch model (local, default, fast, complex)', '/backend': 'Show or switch local backend (ollama, llamacpp)', + '/runtime': 'Runtime backend mode control (daemon/channel command; not local TUI backend switch)', '/research': 'Delegate a task to the configured research agent', '/council': 'Run the councils pipeline for a task; use "/council preflight" for route/auth checks', '/reset': 'Clear conversation history', diff --git a/src/frontends/tui/components/App.tsx b/src/frontends/tui/components/App.tsx index e71c79e..bbe6de4 100644 --- a/src/frontends/tui/components/App.tsx +++ b/src/frontends/tui/components/App.tsx @@ -563,6 +563,18 @@ export function App({ return; } + case 'runtime': { + pushAssistantMessage( + 'Runtime backend mode command is not available in fullscreen TUI mode.\n' + + 'Use it in daemon/channel sessions:\n' + + '/runtime status\n' + + '/runtime activate pi\n' + + '/runtime deactivate pi\n' + + '/runtime use config', + ); + return; + } + case 'login': { const provider = (command.provider ?? '').trim().toLowerCase(); if (!provider) { diff --git a/src/frontends/tui/minimal.test.ts b/src/frontends/tui/minimal.test.ts index 67d0e1a..5a7d3bc 100644 --- a/src/frontends/tui/minimal.test.ts +++ b/src/frontends/tui/minimal.test.ts @@ -403,6 +403,29 @@ describe('MinimalTui backend command', () => { } }); + it('prints guidance when /runtime is invoked in TUI mode', async () => { + const mockSession = { + id: 'test', + getHistory: () => [], + addMessage: vi.fn(), + clear: vi.fn(), + replaceHistory: vi.fn(), + }; + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + try { + const tui = new MinimalTui({ + session: asSession(mockSession), + modelClient: asModelClient({}), + systemPrompt: 'test', + }); + + await minimalTuiPrivates(tui).handleCommand({ type: 'runtime', input: 'status' }); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Runtime backend mode command is not available in this TUI mode.')); + } finally { + logSpy.mockRestore(); + } + }); + it('collects multiline input from /paste and sends as one message', async () => { const mockSession = { id: 'test', diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index 82adecb..c757aae 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -524,6 +524,10 @@ export class MinimalTui { await this.handleBackendCommand(command.provider); break; + case 'runtime': + this.handleRuntimeCommand(command.input); + break; + case 'login': await this.handleLoginCommand(command.provider); break; @@ -895,6 +899,16 @@ export class MinimalTui { console.log(`${colors.gray}Switched to backend: ${provider}${colors.reset}\n`); } + private handleRuntimeCommand(_input?: string): void { + console.log(`${colors.gray}Runtime backend mode command is not available in this TUI mode.${colors.reset}`); + console.log(`${colors.gray}Use it in daemon/channel sessions:${colors.reset}`); + console.log(' /runtime status'); + console.log(' /runtime activate pi'); + console.log(' /runtime deactivate pi'); + console.log(' /runtime use config'); + console.log(''); + } + private async stopBackend(provider: string): Promise { try { const { exec } = await import('child_process');