Desktop skill slash command parsing and rendering bugs
Cluster 55510 · apps/desktop. Data as of
Nothing open on GitHub. Fix merged in #57226 on 2 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #57226. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #57226. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and fixes concern bugs in the desktop client where skill slash commands render incorrectly, lose payload content across multiple lines, or produce degraded chip states. The work targets parsing and display reliability for skill and goal slash command inputs.
Open issues0
Open PRs0
Fixes on record
-
#57226 fix(desktop): parse multiline slash commands so long-context skill/goal payloads stop vanishing (fixes #41323, supersedes #55541)
files:
apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx,apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts,apps/desktop/src/lib/chat-runtime.test.ts,apps/desktop/src/lib/chat-runtime.tsShow the diff
-
apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx@@ -4,7 +4,7 @@ import { useEffect, useRef } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { textPart } from '@/lib/chat-messages' -import { $composerAttachments, type ComposerAttachment } from '@/store/composer' +import { $composerAttachments, $composerDraft, type ComposerAttachment, setComposerDraft } from '@/store/composer' import { $busy, $connection, $messages, $sessions, setSessions } from '@/store/session' import type { SessionInfo } from '@/types/hermes' @@ -272,6 +272,70 @@ describe('usePromptActions slash.exec dispatch payloads', () => { expect(renderedText).toContain('⊙ Goal set. Starting now.') expect(renderedText).not.toContain('/goal: no output') }) + + it('dispatches a slash command with a multiline arg instead of "empty slash command" (#41323, #55510)', async () => { + const calls: { method: string; params?: Record<string, unknown> }[] = [] + const states: Record<string, unknown>[] = [] + + const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => { + calls.push({ method, params }) + + if (method === 'slash.exec') { + return { type: 'send', message: 'Write a Python script\nthat prints Hello World' } as never + } + + return {} as never + }) + + let handle: HarnessHandle | null = null + render( + <Harness + onReady={h => (handle = h)} + onSeedState={s => states.push(s)} + refreshSessions={async () => undefined} + requestGateway={requestGateway} + /> + ) + + await handle!.submitText('/goal Write a Python script\nthat prints Hello World') + + // The newline lives in the arg — the command still reaches the gateway + // whole, exactly as the CLI and Telegram handle it. + expect(calls.map(c => c.method)).toEqual(['slash.exec', 'prompt.submit']) + expect(calls[0]?.params).toEqual({ + command: 'goal Write a Python script\nthat prints Hello World', + session_id: RUNTIME_SESSION_ID + }) + + const renderedText = states + .flatMap(state => { + const messages = Array.isArray(state.messages) + ? (state.messages as Array<{ parts?: Array<{ text?: string }> }>) + : [] + + return messages.flatMap(message => (message.parts ?? []).map(part => part.text ?? '')) + }) + .join('\n') + + expect(renderedText).not.toContain('empty slash command') + }) + + it('restores a degenerate slash payload to the composer instead of losing it', async () => { + setComposerDraft('') + + const requestGateway = vi.fn(async () => ({}) as never) + + let handle: HarnessHandle | null = null + render(<Harness onReady={h => (handle = h)} refreshSessions={async () => undefined} requestGateway={requestGateway} />) + + // `/ text` parses to an empty command name on every surface (CLI parity). + // The composer draft was already cleared on submit and slash input never + // enters the Up-arrow history ring, so the payload must be handed back. + await handle!.submitText('/ pasted context that must not vanish') + + expect($composerDraft.get()).toBe('/ pasted context that must not vanish') + expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything()) + }) }) describe('usePromptActions desktop slash pickers', () => { -
apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts@@ -561,6 +561,14 @@ export function useSlashCommand(deps: SlashCommandDeps) { const { name, arg } = parseSlashCommand(command) if (!name) { + // The composer draft was already cleared on submit, and slash input + // never lands in the Up-arrow history ring (it derives from sent user + // messages) — so without this restore, any payload after a degenerate + // slash (`/ text`, `/` + newline) is lost forever. Hand it back. + if (command.replace(/^\/+/, '').trim()) { + setComposerDraft(command) + } + const sessionId = await ensureSessionId(sessionHint) if (sessionId) { -
apps/desktop/src/lib/chat-runtime.test.ts@@ -6,7 +6,8 @@ import { attachmentDisplayText, coerceThinkingText, optimisticAttachmentRef, - parseCommandDispatch + parseCommandDispatch, + parseSlashCommand } from './chat-runtime' const DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANS' @@ -111,3 +112,41 @@ describe('parseCommandDispatch', () => { expect(parseCommandDispatch({ type: 'prefill', notice: 'x' })).toBeNull() }) }) + +describe('parseSlashCommand', () => { + it('parses a single-line command', () => { + expect(parseSlashCommand('/some-skill do something')).toEqual({ + arg: 'do something', + name: 'some-skill' + }) + }) + + it('keeps a multiline arg intact instead of failing the whole parse (#41323)', () => { + expect(parseSlashCommand('/goal Write a Python script\nthat prints Hello World')).toEqual({ + arg: 'Write a Python script\nthat prints Hello World', + name: 'goal' + }) + }) + + it('parses a skill command with a long pasted multi-paragraph context (#55510)', () => { + const context = 'summarize this:\n\nparagraph one\nparagraph two\n\nparagraph three' + + expect(parseSlashCommand(`/some-skill ${context}`)).toEqual({ + arg: context, + name: 'some-skill' + }) + }) + + it('takes the name across a newline boundary like the CLI and gateway (split on any whitespace)', () => { + expect(parseSlashCommand('/goal\npasted block')).toEqual({ arg: 'pasted block', name: 'goal' }) + }) + + it('keeps truly empty slash input empty', () => { + expect(parseSlashCommand('/')).toEqual({ arg: '', name: '' }) + expect(parseSlashCommand('/ ')).toEqual({ arg: '', name: '' }) + }) + + it('does not treat text after horizontal whitespace as a command name (CLI parity)', () => { + expect(parseSlashCommand('/ some words')).toEqual({ arg: '', name: '' }) + }) +}) -
apps/desktop/src/lib/chat-runtime.ts@@ -223,7 +223,12 @@ export function normalizePersonalityValue(value: string): string { } export function parseSlashCommand(command: string) { - const match = command.replace(/^\/+/, '').match(/^(\S+)\s*(.*)$/) + // `[\s\S]*` (not `.*`): the arg may span newlines — `/goal <multi-line text>` + // or a skill command with a long pasted context. The old `.*$` regex failed + // the whole match on any newline, so every multiline slash command parsed as + // an empty name and got swallowed (#41323, #55510). The backend and CLI both + // split on any whitespace (`split(maxsplit=1)`), so this is the parity fix. + const match = command.replace(/^\/+/, '').match(/^(\S+)([\s\S]*)$/) return match ? { name: match[1], arg: match[2].trim() } : { name: '', arg: '' } }
-
1 PR closed without merging
From the close comments: other 1
-
#55541 fix(desktop): recover degraded skill slash chips · other →
#55510
Closed issues · 1
- Closed#55510bug(desktop): skill slash command can render empty slash command despite non-empty input
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- apps/desktop
- Merged PRs
- #57226
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 2 Jul 2026