fix(tui): make mutating slash paths native and lifecycle-safe

Route /browser, /reload-mcp, /rollback, /stop, /fast, and /busy through direct TUI RPC handlers so state changes hit the live gateway session instead of slash-worker fallback. Add TUI session finalize/reset parity hooks (memory commit + plugin boundaries) and parity matrix tests to keep mutating commands off fallback.
This commit is contained in:
Brooklyn Nicholson 2026-04-27 12:20:08 -05:00
parent d5a89283b7
commit a4cb3ef66c
7 changed files with 594 additions and 11 deletions

View file

@ -1,5 +1,11 @@
import type {
BrowserManageResponse,
DelegationPauseResponse,
ProcessStopResponse,
ReloadMcpResponse,
RollbackDiffResponse,
RollbackListResponse,
RollbackRestoreResponse,
SlashExecResponse,
SpawnTreeListResponse,
SpawnTreeLoadResponse,
@ -50,6 +56,155 @@ interface SkillsBrowseResponse {
}
export const opsCommands: SlashCommand[] = [
{
help: 'stop background processes',
name: 'stop',
run: (_arg, ctx) => {
ctx.gateway
.rpc<ProcessStopResponse>('process.stop', {})
.then(
ctx.guarded<ProcessStopResponse>(r => {
const killed = Number(r.killed ?? 0)
const noun = killed === 1 ? 'process' : 'processes'
ctx.transcript.sys(`stopped ${killed} background ${noun}`)
})
)
.catch(ctx.guardedErr)
}
},
{
aliases: ['reload_mcp'],
help: 'reload MCP servers in the live session',
name: 'reload-mcp',
run: (_arg, ctx) => {
ctx.gateway
.rpc<ReloadMcpResponse>('reload.mcp', { session_id: ctx.sid })
.then(
ctx.guarded<ReloadMcpResponse>(r => {
ctx.transcript.sys(r.status === 'reloaded' ? 'MCP servers reloaded' : 'reload complete')
})
)
.catch(ctx.guardedErr)
}
},
{
help: 'manage browser CDP connection [connect|disconnect|status]',
name: 'browser',
run: (arg, ctx) => {
const trimmed = arg.trim()
const [rawAction, ...rest] = trimmed ? trimmed.split(/\s+/) : ['status']
const action = (rawAction || 'status').toLowerCase()
if (!['connect', 'disconnect', 'status'].includes(action)) {
return ctx.transcript.sys('usage: /browser [connect|disconnect|status] [url]')
}
const payload: Record<string, unknown> = { action }
if (action === 'connect') {
payload.url = rest.join(' ').trim() || 'http://localhost:9222'
}
ctx.gateway
.rpc<BrowserManageResponse>('browser.manage', payload)
.then(
ctx.guarded<BrowserManageResponse>(r => {
if (action === 'status') {
return ctx.transcript.sys(
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')
})
)
.catch(ctx.guardedErr)
}
},
{
help: 'list, diff, or restore checkpoints',
name: 'rollback',
run: (arg, ctx) => {
const trimmed = arg.trim()
const [first = '', ...rest] = trimmed.split(/\s+/).filter(Boolean)
const lower = first.toLowerCase()
if (!trimmed || lower === 'list' || lower === 'ls') {
return ctx.gateway
.rpc<RollbackListResponse>('rollback.list', { session_id: ctx.sid })
.then(
ctx.guarded<RollbackListResponse>(r => {
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) => [
`${idx + 1}. ${c.hash.slice(0, 10)}`,
[c.timestamp, c.message].filter(Boolean).join(' · ') || '(no metadata)'
])
}
])
})
)
.catch(ctx.guardedErr)
}
if (lower === 'diff') {
const hash = rest[0]
if (!hash) {
return ctx.transcript.sys('usage: /rollback diff <checkpoint>')
}
return ctx.gateway
.rpc<RollbackDiffResponse>('rollback.diff', { hash, session_id: ctx.sid })
.then(
ctx.guarded<RollbackDiffResponse>(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')
})
)
.catch(ctx.guardedErr)
}
const hash = first
const filePath = rest.join(' ').trim()
return ctx.gateway
.rpc<RollbackRestoreResponse>('rollback.restore', {
...(filePath ? { file_path: filePath } : {}),
hash,
session_id: ctx.sid
})
.then(
ctx.guarded<RollbackRestoreResponse>(r => {
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))
}
})
)
.catch(ctx.guardedErr)
}
},
{
aliases: ['tasks'],
help: 'open the spawn-tree dashboard (live audit + kill/pause controls)',