From 15663725bbbe0513649d1e08fd74efae829342a1 Mon Sep 17 00:00:00 2001 From: Jannes Stubbemann Date: Sat, 4 Jul 2026 20:01:15 +0200 Subject: [PATCH] fix(ui): exclude null part from OpenCode JSONL detection Greptile flagged that `record.part !== undefined` also accepts `part: null` (null !== undefined is true), so arbitrary JSON like {"type":"text","part":null} would lock the parser onto the OpenCode path. Real OpenCode events always carry a sessionID string or a non-null part payload, so tighten the guard and add a regression test. Co-Authored-By: Claude Fable 5 --- ui/src/adapters/process/parse-stdout.test.ts | 12 ++++++++++++ ui/src/adapters/process/parse-stdout.ts | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ui/src/adapters/process/parse-stdout.test.ts b/ui/src/adapters/process/parse-stdout.test.ts index eed3ab45b05..860ac3c4dd5 100644 --- a/ui/src/adapters/process/parse-stdout.test.ts +++ b/ui/src/adapters/process/parse-stdout.test.ts @@ -61,6 +61,18 @@ describe("process adapter transcript parsing (OpenCode JSONL detection)", () => expect(stdoutTexts.some((t) => t.includes('"type"'))).toBe(false); }); + it("does not lock on for JSON with a matching type but a null part", () => { + const chunks: RunLogChunk[] = [ + { + ts, + stream: "stdout", + chunk: JSON.stringify({ type: "text", part: null }) + "\n" + "still plain\n", + }, + ]; + const entries = buildTranscript(chunks, processUIAdapter); + expect(entries.every((e) => e.kind === "stdout")).toBe(true); + }); + it("leaves genuinely non-JSONL process output as raw stdout", () => { const chunks: RunLogChunk[] = [ { ts, stream: "stdout", chunk: "plain line one\nplain line two\n" }, diff --git a/ui/src/adapters/process/parse-stdout.ts b/ui/src/adapters/process/parse-stdout.ts index 8b5f3282bad..181c9092104 100644 --- a/ui/src/adapters/process/parse-stdout.ts +++ b/ui/src/adapters/process/parse-stdout.ts @@ -26,8 +26,9 @@ function isOpenCodeJsonlLine(line: string): boolean { const record = parsed as Record; if (typeof record.type !== "string" || !OPENCODE_EVENT_TYPES.has(record.type)) return false; // Guard against arbitrary JSON that merely happens to carry a matching `type`: - // real OpenCode events always carry a `sessionID` string or a `part` payload. - return typeof record.sessionID === "string" || record.part !== undefined; + // real OpenCode events always carry a `sessionID` string or a non-null `part` + // payload (`part: null` is not a real OpenCode shape, so don't lock on for it). + return typeof record.sessionID === "string" || (record.part !== undefined && record.part !== null); } /**