(msg: RenderableMessage)
| 30 | } |
| 31 | |
| 32 | function computeSearchText(msg: RenderableMessage): string { |
| 33 | let raw = '' |
| 34 | switch (msg.type) { |
| 35 | case 'user': { |
| 36 | const c = msg.message.content |
| 37 | if (typeof c === 'string') { |
| 38 | raw = RENDERED_AS_SENTINEL.has(c) ? '' : c |
| 39 | } else { |
| 40 | const parts: string[] = [] |
| 41 | for (const b of c) { |
| 42 | if (b.type === 'text') { |
| 43 | if (!RENDERED_AS_SENTINEL.has(b.text)) parts.push(b.text) |
| 44 | } else if (b.type === 'tool_result') { |
| 45 | // b.content is the MODEL-facing serialization (from each tool's |
| 46 | // mapToolResultToToolResultBlockParam) — adds system-reminders, |
| 47 | // <persisted-output> wrappers, backgroundInfo strings, |
| 48 | // CYBER_RISK_MITIGATION_REMINDER. The UI |
| 49 | // renders msg.toolUseResult (the tool's native Out) via |
| 50 | // renderToolResultMessage — DIFFERENT text. Indexing b.content |
| 51 | // yields phantoms: /malware → matches the reminder, /background |
| 52 | // → matches the model-only ID string, none render. |
| 53 | // |
| 54 | // Duck-type the native Out instead. Covers the common shapes: |
| 55 | // Bash {stdout,stderr}, Grep {content,filenames}, Read |
| 56 | // {file.content}. Unknown shapes index empty — under-count is |
| 57 | // honest, phantom is a lie. Proper fix is per-tool |
| 58 | // extractSearchText(Out) on the Tool interface (TODO). |
| 59 | parts.push(toolResultSearchText(msg.toolUseResult)) |
| 60 | } |
| 61 | } |
| 62 | raw = parts.join('\n') |
| 63 | } |
| 64 | break |
| 65 | } |
| 66 | case 'assistant': { |
| 67 | const c = msg.message.content |
| 68 | if (Array.isArray(c)) { |
| 69 | // text blocks + tool_use inputs. tool_use renders as "⏺ Bash(cmd)" |
| 70 | // — the command/pattern/path is visible and searchable-expected. |
| 71 | // Skip thinking (hidden by hidePastThinking in transcript mount). |
| 72 | raw = c |
| 73 | .flatMap(b => { |
| 74 | if (b.type === 'text') return [b.text] |
| 75 | if (b.type === 'tool_use') return [toolUseSearchText(b.input)] |
| 76 | return [] |
| 77 | }) |
| 78 | .join('\n') |
| 79 | } |
| 80 | break |
| 81 | } |
| 82 | case 'attachment': { |
| 83 | // relevant_memories renders full m.content in transcript mode |
| 84 | // (AttachmentMessage.tsx <Ansi>{m.content}</Ansi>). Visible but |
| 85 | // unsearchable without this — [ dump finds it, / doesn't. |
| 86 | if (msg.attachment.type === 'relevant_memories') { |
| 87 | raw = msg.attachment.memories.map(m => m.content).join('\n') |
| 88 | } else if ( |
| 89 | // Mid-turn prompts — queued while an agent is running. Render via |
no test coverage detected