* Scans a chunk of text for the first meaningful user prompt.
(chunk: string)
| 4816 | * Scans a chunk of text for the first meaningful user prompt. |
| 4817 | */ |
| 4818 | function extractFirstPromptFromChunk(chunk: string): string { |
| 4819 | let start = 0 |
| 4820 | let hasTickMessages = false |
| 4821 | let firstCommandFallback = '' |
| 4822 | while (start < chunk.length) { |
| 4823 | const newlineIdx = chunk.indexOf('\n', start) |
| 4824 | const line = |
| 4825 | newlineIdx >= 0 ? chunk.slice(start, newlineIdx) : chunk.slice(start) |
| 4826 | start = newlineIdx >= 0 ? newlineIdx + 1 : chunk.length |
| 4827 | |
| 4828 | if (!line.includes('"type":"user"') && !line.includes('"type": "user"')) { |
| 4829 | continue |
| 4830 | } |
| 4831 | if (line.includes('"tool_result"')) continue |
| 4832 | if (line.includes('"isMeta":true') || line.includes('"isMeta": true')) |
| 4833 | continue |
| 4834 | |
| 4835 | try { |
| 4836 | const entry = jsonParse(line) as Record<string, unknown> |
| 4837 | if (entry.type !== 'user') continue |
| 4838 | |
| 4839 | const message = entry.message as Record<string, unknown> | undefined |
| 4840 | if (!message) continue |
| 4841 | |
| 4842 | const content = message.content |
| 4843 | // Collect all text values from the message content. For array content |
| 4844 | // (common in VS Code where IDE metadata tags come before the user's |
| 4845 | // actual prompt), iterate all text blocks so we don't miss the real |
| 4846 | // prompt hidden behind <ide_selection>/<ide_opened_file> blocks. |
| 4847 | const texts: string[] = [] |
| 4848 | if (typeof content === 'string') { |
| 4849 | texts.push(content) |
| 4850 | } else if (Array.isArray(content)) { |
| 4851 | for (const block of content) { |
| 4852 | const b = block as Record<string, unknown> |
| 4853 | if (b.type === 'text' && typeof b.text === 'string') { |
| 4854 | texts.push(b.text as string) |
| 4855 | } |
| 4856 | } |
| 4857 | } |
| 4858 | |
| 4859 | for (const text of texts) { |
| 4860 | if (!text) continue |
| 4861 | |
| 4862 | let result = text.replace(/\n/g, ' ').trim() |
| 4863 | |
| 4864 | // Skip command messages (slash commands) but remember the first one |
| 4865 | // as a fallback title. Matches skip logic in |
| 4866 | // getFirstMeaningfulUserMessageTextContent, but instead of discarding |
| 4867 | // command messages entirely, we format them cleanly (e.g. "/clear") |
| 4868 | // so the session still appears in the resume picker. |
| 4869 | const commandNameTag = extractTag(result, COMMAND_NAME_TAG) |
| 4870 | if (commandNameTag) { |
| 4871 | const name = commandNameTag.replace(/^\//, '') |
| 4872 | const commandArgs = extractTag(result, 'command-args')?.trim() || '' |
| 4873 | if (builtInCommandNames().has(name) || !commandArgs) { |
| 4874 | if (!firstCommandFallback) { |
| 4875 | firstCommandFallback = commandNameTag |
no test coverage detected