fix(desktop): render send/prefill directive notices (/goal, /undo) (#49073)

The desktop slash dispatcher dropped the `notice` field on `send` and
never handled `prefill` directives at all. `/goal <text>` returns
{type: send, notice: "⊙ Goal set …", message} from command.dispatch —
the desktop submitted the goal text as a plain prompt with no feedback,
so the goal looked like it did nothing. `/undo` returns a prefill
directive that fell through to "invalid response".

- types: add `notice?` to SendCommandDispatchResponse; add
  PrefillCommandDispatchResponse to the union.
- parseCommandDispatch: keep `notice` on send, parse prefill.
- runExec dispatcher: render the notice as a system line before acting,
  and handle prefill by dropping the message into the composer for
  editing (mirrors the TUI's createSlashHandler).

Tests: parseCommandDispatch send-notice / prefill cases.
This commit is contained in:
Teknium 2026-06-19 07:28:50 -07:00 committed by GitHub
parent e00b965406
commit b936f92b25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 62 additions and 2 deletions

View file

@ -32,6 +32,7 @@ import {
clearComposerAttachments,
type ComposerAttachment,
setComposerAttachmentUploadState,
setComposerDraft,
terminalContextBlocksFromDraft,
updateComposerAttachment
} from '@/store/composer'
@ -951,8 +952,26 @@ export function usePromptActions({
return
}
// send / prefill carry an optional `notice` (e.g. "⊙ Goal set …")
// that the backend wants shown as a system line before the message
// is acted on. Mirrors the TUI's createSlashHandler — without it a
// `/goal <text>` looked like it did nothing.
if ((dispatch.type === 'send' || dispatch.type === 'prefill') && dispatch.notice?.trim()) {
renderSlashOutput(dispatch.notice.trim())
}
const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? ''
// /undo returns a prefill directive: drop the backed-up message into
// the composer for editing instead of submitting it immediately.
if (dispatch.type === 'prefill') {
if (message) {
setComposerDraft(message)
}
return
}
if (!message) {
renderSlashOutput(
`/${name}: ${dispatch.type === 'skill' ? 'skill payload missing message' : 'empty message'}`

View file

@ -106,6 +106,13 @@ export interface SkillCommandDispatchResponse {
export interface SendCommandDispatchResponse {
type: 'send'
message: string
notice?: string
}
export interface PrefillCommandDispatchResponse {
type: 'prefill'
message: string
notice?: string
}
export type CommandDispatchResponse =
@ -113,6 +120,7 @@ export type CommandDispatchResponse =
| AliasCommandDispatchResponse
| SkillCommandDispatchResponse
| SendCommandDispatchResponse
| PrefillCommandDispatchResponse
export type SidebarNavId = 'artifacts' | 'command-center' | 'messaging' | 'new-session' | 'settings' | 'skills'

View file

@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import type { ComposerAttachment } from '@/store/composer'
import { coerceThinkingText, optimisticAttachmentRef } from './chat-runtime'
import { coerceThinkingText, optimisticAttachmentRef, parseCommandDispatch } from './chat-runtime'
const DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANS'
@ -52,3 +52,31 @@ describe('coerceThinkingText', () => {
).toBe('')
})
})
describe('parseCommandDispatch', () => {
it('keeps the notice on a send directive (e.g. /goal set)', () => {
// The backend's /goal set returns {type:send, notice:"⊙ Goal set …", message}.
// Dropping the notice made /goal look like it did nothing in the desktop app.
const parsed = parseCommandDispatch({ type: 'send', notice: '⊙ Goal set', message: 'do the thing' })
expect(parsed).toEqual({ type: 'send', message: 'do the thing', notice: '⊙ Goal set' })
})
it('keeps message-only send directives working (no notice)', () => {
expect(parseCommandDispatch({ type: 'send', message: 'hi' })).toEqual({
type: 'send',
message: 'hi',
notice: undefined
})
})
it('parses a prefill directive with its notice (e.g. /undo)', () => {
const parsed = parseCommandDispatch({ type: 'prefill', notice: 'backed up 1 turn', message: 'edit me' })
expect(parsed).toEqual({ type: 'prefill', message: 'edit me', notice: 'backed up 1 turn' })
})
it('rejects a prefill directive missing its message', () => {
expect(parseCommandDispatch({ type: 'prefill', notice: 'x' })).toBeNull()
})
})

View file

@ -238,7 +238,12 @@ export function parseCommandDispatch(raw: unknown): CommandDispatchResponse | nu
return typeof row.name === 'string' ? { type: 'skill', name: row.name, message: str(row.message) } : null
case 'send':
return typeof row.message === 'string' ? { type: 'send', message: row.message } : null
return typeof row.message === 'string' ? { type: 'send', message: row.message, notice: str(row.notice) } : null
case 'prefill':
return typeof row.message === 'string'
? { type: 'prefill', message: row.message, notice: str(row.notice) }
: null
default:
return null