// 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[])
}