fix(tui): handle dispatch payloads from slash exec (#49337)

This commit is contained in:
Gille 2026-06-19 19:05:58 -06:00 committed by GitHub
parent cf58f1a520
commit 857d0244af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 82 additions and 39 deletions

View file

@ -694,6 +694,42 @@ describe('createSlashHandler', () => {
expect(ctx.transcript.send).toHaveBeenCalledWith(skillMessage)
})
it('handles command.dispatch payloads returned directly by slash.exec', async () => {
patchUiState({ sid: 'sid-abc' })
const ctx = buildCtx({
gateway: {
gw: {
getLogTail: vi.fn(() => ''),
request: vi.fn((method: string) => {
if (method === 'slash.exec') {
return Promise.resolve({
message: 'complete all the steps and provide a final report',
notice: '⊙ Goal set (20-turn budget): complete all the steps and provide a final report',
type: 'send'
})
}
return Promise.resolve({})
})
},
rpc: vi.fn(() => Promise.resolve({}))
}
})
const h = createSlashHandler(ctx)
expect(h('/goal complete all the steps and provide a final report')).toBe(true)
await vi.waitFor(() => {
expect(ctx.transcript.sys).toHaveBeenCalledWith(
'⊙ Goal set (20-turn budget): complete all the steps and provide a final report'
)
})
expect(ctx.transcript.send).toHaveBeenCalledWith('complete all the steps and provide a final report')
expect(ctx.transcript.sys).not.toHaveBeenCalledWith('/goal: no output')
expect(ctx.gateway.gw.request).not.toHaveBeenCalledWith('command.dispatch', expect.anything())
})
it('/history pages the current TUI transcript (user + assistant)', () => {
const ctx = buildCtx({
local: {

View file

@ -74,12 +74,57 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
}
}
const handleDispatch = (raw: unknown): void => {
const d = asCommandDispatch(raw)
if (!d) {
return sys('error: invalid response: command.dispatch')
}
if (d.type === 'exec' || d.type === 'plugin') {
return sys(d.output || '(no output)')
}
if (d.type === 'alias') {
return void handler(`/${d.target}${argTail}`)
}
if (d.type === 'skill') {
sys(`⚡ loading skill: ${d.name}`)
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: skill payload missing message`)
}
if (d.type === 'send') {
if (d.notice?.trim()) {
sys(d.notice)
}
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: empty message`)
}
if (d.type === 'prefill') {
// /undo returns prefill: drop the backed-up message text into
// the composer so the user can edit and resubmit, instead of
// submitting it immediately like 'send'.
if (d.notice?.trim()) {
sys(d.notice)
}
if (d.message) {
ctx.composer.setInput(d.message)
}
}
}
gw.request<SlashExecResponse>('slash.exec', { command: cmd.slice(1), session_id: sid })
.then(r => {
if (stale()) {
return
}
if (asCommandDispatch(r)) {
return handleDispatch(r)
}
const body = r?.output || `/${parsed.name}: no output`
const text = r?.warning ? `warning: ${r.warning}\n${body}` : body
const long = text.length > 180 || text.split('\n').filter(Boolean).length > 2
@ -93,45 +138,7 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
return
}
const d = asCommandDispatch(raw)
if (!d) {
return sys('error: invalid response: command.dispatch')
}
if (d.type === 'exec' || d.type === 'plugin') {
return sys(d.output || '(no output)')
}
if (d.type === 'alias') {
return handler(`/${d.target}${argTail}`)
}
if (d.type === 'skill') {
sys(`⚡ loading skill: ${d.name}`)
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: skill payload missing message`)
}
if (d.type === 'send') {
if (d.notice?.trim()) {
sys(d.notice)
}
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: empty message`)
}
if (d.type === 'prefill') {
// /undo returns prefill: drop the backed-up message text into
// the composer so the user can edit and resubmit, instead of
// submitting it immediately like 'send'.
if (d.notice?.trim()) {
sys(d.notice)
}
if (d.message) {
ctx.composer.setInput(d.message)
}
return
}
handleDispatch(raw)
})
.catch(guardedErr)
})