// Claude-family harnesses record a slash input's user turn as a command // envelope (`/x…`), not as the typed text. The // noise filter rightly hides those for catalog commands — Orca shows a local // `Ran /x` line instead — but a skill invocation IS the user's chat turn, so // dropping its envelope makes the assistant appear to answer an empty // conversation. Surface non-catalog envelopes back as plain user text. import { isTextBlock, type NativeChatMessage } from './native-chat-types' const COMMAND_NAME = /([\s\S]*?)<\/command-name>/ const COMMAND_ARGS = /([\s\S]*?)<\/command-args>/ export type NativeChatCommandEnvelope = { name: string; args: string } /** Parse a user turn recorded as a command envelope. Returns null for any text * that does not lead with an envelope tag (ordinary prompts, XML pastes). */ export function parseNativeChatCommandEnvelope(text: string): NativeChatCommandEnvelope | null { const trimmed = text.trimStart() if (!trimmed.toLowerCase().startsWith(' ): NativeChatMessage[] { let changed = false const out = messages.map((message) => { if (message.role !== 'user' || !message.blocks.every(isTextBlock)) { return message } const envelope = parseNativeChatCommandEnvelope( message.blocks.map((block) => block.text).join('\n') ) if (!envelope || catalogCommandNames.has(envelope.name.replace(/^\//, ''))) { return message } // Why: the harness canonicalizes a plugin skill to `/plugin:name`, but the // user (and the picker) sent the short frontmatter name. Render the short // token so the bubble shows what was typed and the optimistic echo prunes // instead of duplicating the turn. const shortName = envelope.name.replace(/^\//, '').split(':').at(-1) ?? '' const token = `/${shortName}` changed = true return { ...message, blocks: [{ type: 'text' as const, text: envelope.args ? `${token} ${envelope.args}` : token }] } }) return changed ? out : (messages as NativeChatMessage[]) }