* Scans a chunk of text for the first meaningful user prompt.
(chunk: string)
| 5086 | * Scans a chunk of text for the first meaningful user prompt. |
| 5087 | */ |
| 5088 | function extractFirstPromptFromChunk(chunk: string): string { |
| 5089 | let start = 0 |
| 5090 | let hasTickMessages = false |
| 5091 | let firstCommandFallback = '' |
| 5092 | while (start < chunk.length) { |
| 5093 | const newlineIdx = chunk.indexOf('\n', start) |
| 5094 | const line = |
| 5095 | newlineIdx >= 0 ? chunk.slice(start, newlineIdx) : chunk.slice(start) |
| 5096 | start = newlineIdx >= 0 ? newlineIdx + 1 : chunk.length |
| 5097 | |
| 5098 | if (!line.includes('"type":"user"') && !line.includes('"type": "user"')) { |
| 5099 | continue |
| 5100 | } |
| 5101 | if (line.includes('"tool_result"')) continue |
| 5102 | if (line.includes('"isMeta":true') || line.includes('"isMeta": true')) |
| 5103 | continue |
| 5104 | |
| 5105 | try { |
| 5106 | const entry = jsonParse(line) as Record<string, unknown> |
| 5107 | if (entry.type !== 'user') continue |
| 5108 | |
| 5109 | const message = entry.message as Record<string, unknown> | undefined |
| 5110 | if (!message) continue |
| 5111 | |
| 5112 | const content = message.content |
| 5113 | // Collect all text values from the message content. For array content |
| 5114 | // (common in VS Code where IDE metadata tags come before the user's |
| 5115 | // actual prompt), iterate all text blocks so we don't miss the real |
| 5116 | // prompt hidden behind <ide_selection>/<ide_opened_file> blocks. |
| 5117 | const texts: string[] = [] |
| 5118 | if (typeof content === 'string') { |
| 5119 | texts.push(content) |
| 5120 | } else if (Array.isArray(content)) { |
| 5121 | for (const block of content) { |
| 5122 | const b = block as Record<string, unknown> |
| 5123 | if (b.type === 'text' && typeof b.text === 'string') { |
| 5124 | texts.push(b.text as string) |
| 5125 | } |
| 5126 | } |
| 5127 | } |
| 5128 | |
| 5129 | for (const text of texts) { |
| 5130 | if (!text) continue |
| 5131 | |
| 5132 | let result = text.replace(/\n/g, ' ').trim() |
| 5133 | |
| 5134 | // Skip command messages (slash commands) but remember the first one |
| 5135 | // as a fallback title. Matches skip logic in |
| 5136 | // getFirstMeaningfulUserMessageTextContent, but instead of discarding |
| 5137 | // command messages entirely, we format them cleanly (e.g. "/clear") |
| 5138 | // so the session still appears in the resume picker. |
| 5139 | const commandNameTag = extractTag(result, COMMAND_NAME_TAG) |
| 5140 | if (commandNameTag) { |
| 5141 | const name = commandNameTag.replace(/^\//, '') |
| 5142 | const commandArgs = extractTag(result, 'command-args')?.trim() || '' |
| 5143 | if (builtInCommandNames().has(name) || !commandArgs) { |
| 5144 | if (!firstCommandFallback) { |
| 5145 | firstCommandFallback = commandNameTag |
no test coverage detected