(path: string)
| 542 | } |
| 543 | |
| 544 | function parseTranscriptJsonl(path: string): ParsedSession | null { |
| 545 | // Best-effort tolerant parser. Handles truncated last lines (D10 partial-flag). |
| 546 | let raw: string; |
| 547 | try { |
| 548 | raw = readFileSync(path, "utf-8"); |
| 549 | } catch { |
| 550 | return null; |
| 551 | } |
| 552 | const lines = raw.split("\n").filter((l) => l.trim().length > 0); |
| 553 | if (lines.length === 0) return null; |
| 554 | |
| 555 | // Detect partial: if the last line doesn't end with `}` or doesn't parse, mark partial. |
| 556 | let partial = false; |
| 557 | let parsedLines: any[] = []; |
| 558 | for (let i = 0; i < lines.length; i++) { |
| 559 | try { |
| 560 | parsedLines.push(JSON.parse(lines[i])); |
| 561 | } catch { |
| 562 | // Last-line truncation is the common case (D10). |
| 563 | if (i === lines.length - 1) partial = true; |
| 564 | else continue; |
| 565 | } |
| 566 | } |
| 567 | if (parsedLines.length === 0) return null; |
| 568 | |
| 569 | // Detect format: Codex `session_meta` or Claude Code `type: user|assistant|tool` |
| 570 | const first = parsedLines[0]; |
| 571 | const isCodex = first?.type === "session_meta" || first?.payload?.id != null; |
| 572 | const agent: "claude-code" | "codex" = isCodex ? "codex" : "claude-code"; |
| 573 | |
| 574 | let session_id = ""; |
| 575 | let cwd = ""; |
| 576 | let start_time: string | undefined; |
| 577 | let end_time: string | undefined; |
| 578 | |
| 579 | if (isCodex) { |
| 580 | session_id = first.payload?.id || first.id || basename(path, ".jsonl"); |
| 581 | cwd = first.payload?.cwd || first.cwd || ""; |
| 582 | start_time = first.timestamp || first.payload?.timestamp; |
| 583 | } else { |
| 584 | // Claude Code: look for cwd in first non-queue record |
| 585 | for (const r of parsedLines) { |
| 586 | if (r?.cwd) { |
| 587 | cwd = r.cwd; |
| 588 | break; |
| 589 | } |
| 590 | } |
| 591 | session_id = basename(path, ".jsonl"); |
| 592 | start_time = parsedLines.find((r) => r?.timestamp)?.timestamp; |
| 593 | const last = parsedLines[parsedLines.length - 1]; |
| 594 | end_time = last?.timestamp; |
| 595 | } |
| 596 | |
| 597 | // Render body — collapsed conversation |
| 598 | let messageCount = 0; |
| 599 | let toolCalls = 0; |
| 600 | const bodyParts: string[] = []; |
| 601 | for (const rec of parsedLines) { |
no test coverage detected