* Scans a chunk of text for the first meaningful user prompt.
(chunk: string)
| 4931 | * Scans a chunk of text for the first meaningful user prompt. |
| 4932 | */ |
| 4933 | function extractFirstPromptFromChunk(chunk: string): string { |
| 4934 | let start = 0 |
| 4935 | let hasTickMessages = false |
| 4936 | let firstCommandFallback = '' |
| 4937 | while (start < chunk.length) { |
| 4938 | const newlineIdx = chunk.indexOf('\n', start) |
| 4939 | const line = |
| 4940 | newlineIdx >= 0 ? chunk.slice(start, newlineIdx) : chunk.slice(start) |
| 4941 | start = newlineIdx >= 0 ? newlineIdx + 1 : chunk.length |
| 4942 | |
| 4943 | if (!line.includes('"type":"user"') && !line.includes('"type": "user"')) { |
| 4944 | continue |
| 4945 | } |
| 4946 | if (line.includes('"tool_result"')) continue |
| 4947 | if (line.includes('"isMeta":true') || line.includes('"isMeta": true')) |
| 4948 | continue |
| 4949 | |
| 4950 | try { |
| 4951 | const entry = jsonParse(line) as Record<string, unknown> |
| 4952 | if (entry.type !== 'user') continue |
| 4953 | |
| 4954 | const message = entry.message as Record<string, unknown> | undefined |
| 4955 | if (!message) continue |
| 4956 | |
| 4957 | const content = message.content |
| 4958 | // Collect all text values from the message content. For array content |
| 4959 | // (common in VS Code where IDE metadata tags come before the user's |
| 4960 | // actual prompt), iterate all text blocks so we don't miss the real |
| 4961 | // prompt hidden behind <ide_selection>/<ide_opened_file> blocks. |
| 4962 | const texts: string[] = [] |
| 4963 | if (typeof content === 'string') { |
| 4964 | texts.push(content) |
| 4965 | } else if (Array.isArray(content)) { |
| 4966 | for (const block of content) { |
| 4967 | const b = block as Record<string, unknown> |
| 4968 | if (b.type === 'text' && typeof b.text === 'string') { |
| 4969 | texts.push(b.text as string) |
| 4970 | } |
| 4971 | } |
| 4972 | } |
| 4973 | |
| 4974 | for (const text of texts) { |
| 4975 | if (!text) continue |
| 4976 | |
| 4977 | let result = text.replace(/\n/g, ' ').trim() |
| 4978 | |
| 4979 | // Skip command messages (slash commands) but remember the first one |
| 4980 | // as a fallback title. Matches skip logic in |
| 4981 | // getFirstMeaningfulUserMessageTextContent, but instead of discarding |
| 4982 | // command messages entirely, we format them cleanly (e.g. "/clear") |
| 4983 | // so the session still appears in the resume picker. |
| 4984 | const commandNameTag = extractTag(result, COMMAND_NAME_TAG) |
| 4985 | if (commandNameTag) { |
| 4986 | const name = commandNameTag.replace(/^\//, '') |
| 4987 | const commandArgs = extractTag(result, 'command-args')?.trim() || '' |
| 4988 | if (builtInCommandNames().has(name) || !commandArgs) { |
| 4989 | if (!firstCommandFallback) { |
| 4990 | firstCommandFallback = commandNameTag |
no test coverage detected