From 1eeb7da2e6e5463018cb4be9aff1ec97bb09488a Mon Sep 17 00:00:00 2001 From: ethernet Date: Thu, 4 Jun 2026 17:06:45 -0400 Subject: [PATCH] fix(desktop): slash commands bypass queue when busy and chip id suffix leak (#39289) Two fixes for desktop app slash command handling: 1. Slash commands submitted while the agent is busy now execute immediately instead of being queued. Previously submitDraft() unconditionally queued any draft when busy, but slash commands are client-side operations or self-contained gateway RPCs that should run regardless of busy state (matching TUI behavior). executeSlashCommand already has its own per-command busy guard for commands that genuinely need an idle session. 2. Slash command trigger items no longer leak the "|index" suffix from their item.id into the serialized chip text. The toItem callback now sets rawText in metadata so hermesDirectiveFormatter.serialize takes the direct-insertion path instead of the legacy @type:id fallback. This also means slash commands enter the composer as plain text (not chips), matching selectSkinSlashCommand and TUI behavior. --- .../chat/composer/hooks/use-slash-completions.ts | 9 ++++++++- apps/desktop/src/app/chat/composer/index.tsx | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) 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