diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index de48c4bb486..0ba81cd905a 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -193,7 +193,6 @@ describe('createSlashHandler', () => { it.each([ ['/browser status', 'browser.manage', { action: 'status' }], ['/reload-mcp', 'reload.mcp', { session_id: null }], - ['/rollback', 'rollback.list', { session_id: null }], ['/stop', 'process.stop', {}], ['/fast status', 'config.get', { key: 'fast', session_id: null }], ['/busy status', 'config.get', { key: 'busy' }] @@ -206,6 +205,16 @@ describe('createSlashHandler', () => { expect(ctx.gateway.gw.request).not.toHaveBeenCalled() }) + it('routes /rollback through native RPC when a session is active', () => { + patchUiState({ sid: 'sid-abc' }) + const rpc = vi.fn(() => Promise.resolve({})) + const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } }) + + expect(createSlashHandler(ctx)('/rollback')).toBe(true) + expect(rpc).toHaveBeenCalledWith('rollback.list', { session_id: 'sid-abc' }) + expect(ctx.gateway.gw.request).not.toHaveBeenCalled() + }) + it('drops stale slash.exec output after a newer slash', async () => { let resolveLate: (v: { output?: string }) => void let slashExecCalls = 0 @@ -412,6 +421,16 @@ describe('createSlashHandler', () => { expect(ctx.transcript.sys).toHaveBeenCalledWith('no active session — nothing to save') }) + it('/rollback without an active session tells the user instead of hitting the RPC', () => { + const rpc = vi.fn(() => Promise.resolve({})) + const ctx = buildCtx({ gateway: { ...buildGateway(), rpc } }) + + createSlashHandler(ctx)('/rollback') + + expect(rpc).not.toHaveBeenCalled() + expect(ctx.transcript.sys).toHaveBeenCalledWith('no active session — nothing to rollback') + }) + it('/title uses session.title RPC and bypasses slash.exec', async () => { patchUiState({ sid: 'sid-abc' }) const rpc = vi.fn(() => Promise.resolve({ pending: false, title: 'my title' })) diff --git a/ui-tui/src/__tests__/slashParity.test.ts b/ui-tui/src/__tests__/slashParity.test.ts index 13c8aa02196..333793cebaf 100644 --- a/ui-tui/src/__tests__/slashParity.test.ts +++ b/ui-tui/src/__tests__/slashParity.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'node:fs' +import { execFileSync } from 'node:child_process' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -38,8 +38,17 @@ const MUTATING_COMMANDS = [ const loadCommandRegistryNames = (): string[] => { const here = dirname(fileURLToPath(import.meta.url)) - const source = readFileSync(resolve(here, '../../../hermes_cli/commands.py'), 'utf8') - const names = [...source.matchAll(/CommandDef\("([^"]+)"/g)].map(match => match[1]!) + + const names = JSON.parse( + execFileSync( + process.env.PYTHON ?? 'python3', + [ + '-c', + 'import json; from hermes_cli.commands import COMMAND_REGISTRY; print(json.dumps([c.name for c in COMMAND_REGISTRY]))' + ], + { cwd: resolve(here, '../../..'), encoding: 'utf8' } + ) + ) as string[] return [...new Set(names)] } diff --git a/ui-tui/src/app/slash/commands/ops.ts b/ui-tui/src/app/slash/commands/ops.ts index 772cc2fd5bc..ef1b2406048 100644 --- a/ui-tui/src/app/slash/commands/ops.ts +++ b/ui-tui/src/app/slash/commands/ops.ts @@ -134,6 +134,10 @@ export const opsCommands: SlashCommand[] = [ help: 'list, diff, or restore checkpoints', name: 'rollback', run: (arg, ctx) => { + if (!ctx.sid) { + return ctx.transcript.sys('no active session — nothing to rollback') + } + const trimmed = arg.trim() const [first = '', ...rest] = trimmed.split(/\s+/).filter(Boolean) const lower = first.toLowerCase()