import type { RenderableMessage } from '../types/message.js'import {INTERRUPT_MESSAGE,INTERRUPT_MESSAGE_FOR_TOOL_USE,} from './messages.js'const SYSTEM_REMINDER_CLOSE = '</system-reminder>'// UserTextMessage.tsx:~84 replaces these with <InterruptedByUser />// (renders 'Interrupted · /issue...'). Raw text never appears on screen;// searching it yields phantom matches — /terr → in[terr]upted.const RENDERED_AS_SENTINEL = new Set([INTERRUPT_MESSAGE,INTERRUPT_MESSAGE_FOR_TOOL_USE,])const searchTextCache = new WeakMap<RenderableMessage, string>()/** Flatten a RenderableMessage to lowercased searchable text. WeakMap-* cached — messages are append-only and immutable so a hit is always* valid. Lowercased at cache time: the only caller immediately* .toLowerCase()d the result, re-lowering ~1.5MB on every keystroke* (the backspace hang). Returns '' for non-searchable types. */export function renderableSearchText(msg: RenderableMessage): string {const cached = searchTextCache.get(msg)if (cached !== undefined) return cachedconst result = computeSearchText(msg).toLowerCase()searchTextCache.set(msg, result)return result}function computeSearchText(msg: RenderableMessage): string {let raw = ''switch (msg.type) {case 'user': {const c = msg.message.contentif (typeof c === 'string') {raw = RENDERED_AS_SENTINEL.has(c) ? '' : c} else {const parts: string[] = []for (const b of c) {if (b.type === 'text') {if (!RENDERED_AS_SENTINEL.has(b.text)) parts.push(b.text)} else if (b.type === 'tool_result') {// b.content is the MODEL-facing serialization (from each tool's// mapToolResultToToolResultBlockParam) — adds system-reminders,// <persisted-output> wrappers, backgroundInfo strings,// CYBER_RISK_MITIGATION_REMINDER. The UI// renders msg.toolUseResult (the tool's native Out) via// renderToolResultMessage — DIFFERENT text. Indexing b.content// yields phantoms: /malware → matches the reminder, /background// → matches the model-only ID string, none render.//// Duck-type the native Out instead. Covers the common shapes:// Bash {stdout,stderr}, Grep {content,filenames}, Read// {file.content}. Unknown shapes index empty — under-count is// honest, phantom is a lie. Proper fix is per-tool// extractSearchText(Out) on the Tool interface (TODO).parts.push(toolResultSearchText(msg.toolUseResult))}}raw = parts.join('\n')}break}case 'assistant': {const c = msg.message.contentif (Array.isArray(c)) {// text blocks + tool_use inputs. tool_use renders as "⏺ Bash(cmd)"// — the command/pattern/path is visible and searchable-expected.// Skip thinking (hidden by hidePastThinking in transcript mount).raw = c.flatMap(b => {if (b.type === 'text') return [b.text]if (b.type === 'tool_use') return [toolUseSearchText(b.input)]return []}).join('\n')}break}case 'attachment': {// relevant_memories renders full m.content in transcript mode// (AttachmentMessage.tsx <Ansi>{m.content}</Ansi>). Visible but// unsearchable without this — [ dump finds it, / doesn't.if (msg.attachment.type === 'relevant_memories') {raw = msg.attachment.memories.map(m => m.content).join('\n')} else if (// Mid-turn prompts — queued while an agent is running. Render via// UserTextMessage (AttachmentMessage.tsx:~348). stickyPromptText// (VirtualMessageList.tsx:~103) has the same guards — mirror here.msg.attachment.type === 'queued_command' &&msg.attachment.commandMode !== 'task-notification' &&!msg.attachment.isMeta) {const p = msg.attachment.promptraw =typeof p === 'string'? p: (p as any[]).flatMap(b => (b.type === 'text' ? [b.text] : [])).join('\n')}break}case 'collapsed_read_search': {// relevant_memories attachments are absorbed into collapse groups// (collapseReadSearch.ts); their content is visible in transcript mode// via CollapsedReadSearchContent, so mirror it here for / search.if (msg.relevantMemories) {raw = msg.relevantMemories.map(m => m.content).join('\n')}break}default:// grouped_tool_use, system — no text contentbreak}// Strip <system-reminder> anywhere — Claude context, not user-visible.// Mid-message on cc -c resumes (memory reminders between prompt lines).let t = rawlet open = t.indexOf('<system-reminder>')while (open >= 0) {const close = t.indexOf(SYSTEM_REMINDER_CLOSE, open)if (close < 0) breakt = t.slice(0, open) + t.slice(close + SYSTEM_REMINDER_CLOSE.length)open = t.indexOf('<system-reminder>')}return t}/** Tool invocation display: renderToolUseMessage shows input fields like* command (Bash), pattern (Grep), file_path (Read/Edit), prompt (Agent).* Same duck-type strategy as toolResultSearchText — known field names,* unknown → empty. Under-count > phantom. */export function toolUseSearchText(input: unknown): string {if (!input || typeof input !== 'object') return ''const o = input as Record<string, unknown>const parts: string[] = []// renderToolUseMessage typically shows one or two of these as the// primary argument. tool_name itself is in the "⏺ Bash(...)" chrome,// handled by under-count (the overlay matches it but we don't count it).for (const k of ['command','pattern','file_path','path','prompt','description','query','url','skill', // SkillTool]) {const v = o[k]if (typeof v === 'string') parts.push(v)}// args[] (Tmux/TungstenTool), files[] (SendUserFile) — tool-use// renders the joined array as the primary display. Under-count > skip.for (const k of ['args', 'files']) {const v = o[k]if (Array.isArray(v) && v.every(x => typeof x === 'string')) {parts.push((v as string[]).join(''))}}return parts.join('\n')}/** Duck-type the tool's native Out for searchable text. Known shapes:* {stdout,stderr} (Bash/Shell), {content} (Grep), {file:{content}} (Read),* {filenames:[]} (Grep/Glob), {output} (generic). Falls back to concating* all top-level string fields — crude but better than indexing model-chatter.* Empty for unknown shapes: under-count > phantom. */export function toolResultSearchText(r: unknown): string {if (!r || typeof r !== 'object') return typeof r === 'string' ? r : ''const o = r as Record<string, unknown>// Known shapes first (common tools).if (typeof o.stdout === 'string') {const err = typeof o.stderr === 'string' ? o.stderr : ''return o.stdout + (err ? '\n' + err : '')}if (o.file &&typeof o.file === 'object' &&typeof (o.file as { content?: unknown }).content === 'string') {return (o.file as { content: string }).content}// Known output-field names only. A blind walk would index metadata// the UI doesn't show (rawOutputPath, backgroundTaskId, filePath,// durationMs-as-string). Allowlist the fields tools actually render.// Tools not matching any shape index empty — add them here as found.const parts: string[] = []for (const k of ['content', 'output', 'result', 'text', 'message']) {const v = o[k]if (typeof v === 'string') parts.push(v)}for (const k of ['filenames', 'lines', 'results']) {const v = o[k]if (Array.isArray(v) && v.every(x => typeof x === 'string')) {parts.push((v as string[]).join('\n'))}}return parts.join('\n')}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。