(meta: ConversationMeta)
| 183 | } |
| 184 | |
| 185 | async function parseFileAsync(meta: ConversationMeta): Promise<ConversationItem | null> { |
| 186 | return new Promise((resolve) => { |
| 187 | const messages: ConversationMessage[] = []; |
| 188 | let firstTimestamp = ""; |
| 189 | let lastTimestamp = ""; |
| 190 | let lineCount = 0; |
| 191 | |
| 192 | const rl = createInterface({ |
| 193 | input: createReadStream(meta.filePath, { encoding: "utf-8" }), |
| 194 | crlfDelay: Number.POSITIVE_INFINITY, |
| 195 | }); |
| 196 | |
| 197 | rl.on("line", (line) => { |
| 198 | if (!line.trim()) return; |
| 199 | lineCount++; |
| 200 | if (lineCount > MAX_LINES_PER_FILE) { rl.close(); return; } |
| 201 | |
| 202 | try { |
| 203 | const entry = JSON.parse(line) as Record<string, unknown>; |
| 204 | const ts = typeof entry.timestamp === "string" ? entry.timestamp : undefined; |
| 205 | if (ts) { |
| 206 | if (!firstTimestamp) firstTimestamp = ts; |
| 207 | lastTimestamp = ts; |
| 208 | } |
| 209 | |
| 210 | if (entry.type === "user") { |
| 211 | const msg = entry.message as { content?: unknown } | undefined; |
| 212 | if (!msg) return; |
| 213 | const text = extractText(msg.content); |
| 214 | if (text) messages.push({ role: "human", text, timestamp: ts || "" }); |
| 215 | } |
| 216 | |
| 217 | if (entry.type === "assistant") { |
| 218 | const msg = entry.message as { content?: unknown } | undefined; |
| 219 | if (!msg) return; |
| 220 | const text = extractText(msg.content); |
| 221 | const toolCalls = extractToolCalls(msg.content); |
| 222 | if (text) { |
| 223 | messages.push({ |
| 224 | role: "assistant", text, timestamp: ts || "", |
| 225 | toolCalls: toolCalls.length > 0 ? toolCalls : undefined, |
| 226 | }); |
| 227 | } |
| 228 | } |
| 229 | } catch { /* skip */ } |
| 230 | }); |
| 231 | |
| 232 | rl.on("close", () => { |
| 233 | if (messages.length === 0) { resolve(null); return; } |
| 234 | const firstHuman = messages.find((m) => m.role === "human"); |
| 235 | const title = firstHuman |
| 236 | ? firstHuman.text.slice(0, 120).replace(/\n/g, " ") |
| 237 | : "(empty conversation)"; |
| 238 | |
| 239 | resolve({ |
| 240 | id: meta.uuid, |
| 241 | uuid: meta.uuid, |
| 242 | project: meta.project, |
no test coverage detected