diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index f9c38756686..ebacf2286c7 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createSlashHandler } from '../app/createSlashHandler.js' -import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js' import { getOverlayState, resetOverlayState } from '../app/overlayStore.js' import { getUiState, patchUiState, resetUiState } from '../app/uiStore.js' +import { TUI_SESSION_MODEL_FLAG } from '../domain/slash.js' describe('createSlashHandler', () => { beforeEach(() => { @@ -55,9 +55,7 @@ describe('createSlashHandler', () => { }) expect( - createSlashHandler(ctx)( - `/model anthropic/claude-sonnet-4.6 --provider openrouter ${TUI_SESSION_MODEL_FLAG}` - ) + createSlashHandler(ctx)(`/model anthropic/claude-sonnet-4.6 --provider openrouter ${TUI_SESSION_MODEL_FLAG}`) ).toBe(true) expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'model', diff --git a/ui-tui/src/__tests__/slashParity.test.ts b/ui-tui/src/__tests__/slashParity.test.ts index 0479d0049de..13c8aa02196 100644 --- a/ui-tui/src/__tests__/slashParity.test.ts +++ b/ui-tui/src/__tests__/slashParity.test.ts @@ -8,14 +8,7 @@ import { SLASH_COMMANDS } from '../app/slash/registry.js' type CommandRoute = 'fallback' | 'local' | 'native' -const NATIVE_MUTATING_COMMANDS = new Set([ - 'browser', - 'busy', - 'fast', - 'reload-mcp', - 'rollback', - 'stop' -]) +const NATIVE_MUTATING_COMMANDS = new Set(['browser', 'busy', 'fast', 'reload-mcp', 'rollback', 'stop']) const MUTATING_COMMANDS = [ 'background', @@ -57,12 +50,15 @@ const LOCAL_COMMAND_NAMES = new Set( const classifyRoute = (name: string): CommandRoute => { const normalized = name.toLowerCase() + if (NATIVE_MUTATING_COMMANDS.has(normalized)) { return 'native' } + if (LOCAL_COMMAND_NAMES.has(normalized)) { return 'local' } + return 'fallback' } diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 91f06bb570e..2cad70b9a5d 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -6,8 +6,8 @@ import type { ConfigGetValueResponse, ConfigSetResponse, SessionSaveResponse, - SessionTitleResponse, SessionSteerResponse, + SessionTitleResponse, SessionUndoResponse } from '../../../gatewayTypes.js' import { writeOsc52Clipboard } from '../../../lib/osc52.js' diff --git a/ui-tui/src/app/slash/commands/ops.ts b/ui-tui/src/app/slash/commands/ops.ts index 540935e9a43..772cc2fd5bc 100644 --- a/ui-tui/src/app/slash/commands/ops.ts +++ b/ui-tui/src/app/slash/commands/ops.ts @@ -102,6 +102,7 @@ export const opsCommands: SlashCommand[] = [ } const payload: Record = { action } + if (action === 'connect') { payload.url = rest.join(' ').trim() || 'http://localhost:9222' } @@ -115,11 +116,13 @@ export const opsCommands: SlashCommand[] = [ r.connected ? `browser connected: ${r.url || '(url unavailable)'}` : 'browser not connected' ) } + if (action === 'connect') { return ctx.transcript.sys( r.connected ? `browser connected: ${r.url || '(url unavailable)'}` : 'browser connect failed' ) } + ctx.transcript.sys('browser disconnected') }) ) @@ -143,10 +146,13 @@ export const opsCommands: SlashCommand[] = [ if (!r.enabled) { return ctx.transcript.sys('checkpoints are not enabled') } + const checkpoints = r.checkpoints ?? [] + if (!checkpoints.length) { return ctx.transcript.sys('no checkpoints found') } + ctx.transcript.panel('Rollback checkpoints', [ { rows: checkpoints.map((c, idx) => [ @@ -162,17 +168,21 @@ export const opsCommands: SlashCommand[] = [ if (lower === 'diff') { const hash = rest[0] + if (!hash) { return ctx.transcript.sys('usage: /rollback diff ') } + return ctx.gateway .rpc('rollback.diff', { hash, session_id: ctx.sid }) .then( ctx.guarded(r => { const body = (r.rendered || r.diff || '').trim() + if (!body && !r.stat) { return ctx.transcript.sys('no changes since this checkpoint') } + const text = [r.stat || '', body].filter(Boolean).join('\n\n') ctx.transcript.page(text, 'Rollback diff') }) @@ -182,6 +192,7 @@ export const opsCommands: SlashCommand[] = [ const hash = first const filePath = rest.join(' ').trim() + return ctx.gateway .rpc('rollback.restore', { ...(filePath ? { file_path: filePath } : {}), @@ -193,9 +204,11 @@ export const opsCommands: SlashCommand[] = [ if (!r.success) { return ctx.transcript.sys(`rollback failed: ${r.error || r.message || 'unknown error'}`) } + const target = filePath || 'workspace' const detail = r.reason || r.message || r.restored_to || 'restored' ctx.transcript.sys(`rollback restored ${target}: ${detail}`) + if ((r.history_removed ?? 0) > 0) { ctx.transcript.setHistoryItems(prev => ctx.transcript.trimLastExchange(prev)) } diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 3670f4d4228..ef0bf2daada 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -1,4 +1,5 @@ import { attachedImageNotice, introMsg, toTranscriptMessages } from '../../../domain/messages.js' +import { TUI_SESSION_MODEL_FLAG } from '../../../domain/slash.js' import type { BackgroundStartResponse, ConfigGetValueResponse, @@ -10,7 +11,6 @@ import type { VoiceToggleResponse } from '../../../gatewayTypes.js' import { fmtK } from '../../../lib/text.js' -import { TUI_SESSION_MODEL_FLAG } from '../../../domain/slash.js' import type { PanelSection } from '../../../types.js' import { patchOverlayState } from '../../overlayStore.js' import { patchUiState } from '../../uiStore.js' @@ -27,8 +27,7 @@ const persistedModelArg = (arg: string) => { return !trimmed || GLOBAL_MODEL_FLAG_RE.test(trimmed) ? trimmed : `${trimmed} --global` } -const stripTuiSessionFlag = (trimmed: string) => - trimmed.replace(TUI_SESSION_STRIP_RE, ' ').replace(/\s+/g, ' ').trim() +const stripTuiSessionFlag = (trimmed: string) => trimmed.replace(TUI_SESSION_STRIP_RE, ' ').replace(/\s+/g, ' ').trim() const modelValueForConfigSet = (arg: string) => { const trimmed = arg.trim() @@ -313,6 +312,7 @@ export const sessionCommands: SlashCommand[] = [ run: (arg, ctx) => { const mode = arg.trim().toLowerCase() const valid = new Set(['', 'status', 'normal', 'fast', 'on', 'off', 'toggle']) + if (!valid.has(mode)) { return ctx.transcript.sys('usage: /fast [normal|fast|status]') } @@ -356,6 +356,7 @@ export const sessionCommands: SlashCommand[] = [ run: (arg, ctx) => { const mode = arg.trim().toLowerCase() const valid = new Set(['', 'status', 'queue', 'steer', 'interrupt']) + if (!valid.has(mode)) { return ctx.transcript.sys('usage: /busy [queue|steer|interrupt|status]') } diff --git a/ui-tui/src/components/modelPicker.tsx b/ui-tui/src/components/modelPicker.tsx index b5882a1352d..8164147fa81 100644 --- a/ui-tui/src/components/modelPicker.tsx +++ b/ui-tui/src/components/modelPicker.tsx @@ -112,9 +112,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke const model = models[modelIdx] if (provider && model) { - onSelect( - `${model} --provider ${provider.slug}${persistGlobal ? ' --global' : ` ${TUI_SESSION_MODEL_FLAG}`}` - ) + onSelect(`${model} --provider ${provider.slug}${persistGlobal ? ' --global' : ` ${TUI_SESSION_MODEL_FLAG}`}`) } else { setStage('provider') }