diff --git a/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts b/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts index 62c982d15..a56a6e326 100644 --- a/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +++ b/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts @@ -16,6 +16,7 @@ interface SlashItemMetadata extends Record { command: string display: string meta: string + rawText?: string } function textValue(value: unknown, fallback = ''): string { @@ -91,7 +92,13 @@ export function useSlashCompletions(options: { gateway: HermesGateway | null }): const metadata: SlashItemMetadata = { command, display, - meta + meta, + // Provide rawText so hermesDirectiveFormatter.serialize uses the + // direct-insertion path instead of the legacy @type:id fallback. + // Without this, the item.id (which includes a "|index" suffix for + // trigger-adapter uniqueness) leaks into the serialized chip text + // and the submitted command. + rawText: command } return { diff --git a/apps/desktop/src/app/chat/composer/index.tsx b/apps/desktop/src/app/chat/composer/index.tsx index 97e7d78a2..7f14286e8 100644 --- a/apps/desktop/src/app/chat/composer/index.tsx +++ b/apps/desktop/src/app/chat/composer/index.tsx @@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button' import { useMediaQuery } from '@/hooks/use-media-query' import { useResizeObserver } from '@/hooks/use-resize-observer' import { chatMessageText } from '@/lib/chat-messages' +import { SLASH_COMMAND_RE } from '@/lib/chat-runtime' import { DATA_IMAGE_URL_RE } from '@/lib/embedded-images' import { triggerHaptic } from '@/lib/haptics' import { cn } from '@/lib/utils' @@ -1037,7 +1038,19 @@ export function ChatBar({ if (queueEdit) { exitQueuedEdit('save') } else if (busy) { - if (hasComposerPayload) { + // Slash commands should execute immediately even while the agent is + // busy — they're client-side operations (/yolo, /skin, /new, /help, + // etc.) or self-contained gateway RPCs (/status, /compress). onSubmit + // routes them to executeSlashCommand, which has its own per-command + // busy guard for commands that genuinely need an idle session (skill + // /send directives). Queuing them would make every slash command wait + // for the current turn to finish, which is how the TUI never behaves. + if (!attachments.length && SLASH_COMMAND_RE.test(draft.trim())) { + const submitted = draft + triggerHaptic('submit') + clearDraft() + void onSubmit(submitted) + } else if (hasComposerPayload) { queueCurrentDraft() } else { // Stop button: an explicit interrupt must actually halt the running