-
Notifications
You must be signed in to change notification settings - Fork 833
fix: parse OpenCode nested part.text in JSON output #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { parseLine, makeParser } from "../argv"; | ||
|
|
||
| function deltas(result: ReturnType<typeof parseLine>) { | ||
| return result.filter((r) => r.kind === "delta").map((r) => r.text); | ||
| } | ||
|
|
||
| describe("parseLine (opencode)", () => { | ||
| it("extracts text from singular part.text envelope", () => { | ||
| const line = JSON.stringify({ | ||
| type: "text", | ||
| part: { type: "text", text: "hello world" }, | ||
| }); | ||
| expect(deltas(parseLine("opencode", line))).toEqual(["hello world"]); | ||
| }); | ||
|
|
||
| it("extracts text from parts[].text array envelope", () => { | ||
| const line = JSON.stringify({ | ||
| type: "text", | ||
| parts: [ | ||
| { type: "text", text: "hello " }, | ||
| { type: "text", text: "world" }, | ||
| ], | ||
| }); | ||
| expect(deltas(parseLine("opencode", line))).toEqual(["hello ", "world"]); | ||
| }); | ||
|
|
||
| it("extracts text from top-level text field", () => { | ||
| const line = JSON.stringify({ text: "top-level content" }); | ||
| expect(deltas(parseLine("opencode", line))).toContain("top-level content"); | ||
| }); | ||
|
|
||
| it("extracts text from top-level content field", () => { | ||
| const line = JSON.stringify({ content: "content field" }); | ||
| expect(deltas(parseLine("opencode", line))).toContain("content field"); | ||
| }); | ||
|
|
||
| it("extracts text from top-level message field", () => { | ||
| const line = JSON.stringify({ message: "message field" }); | ||
| expect(deltas(parseLine("opencode", line))).toContain("message field"); | ||
| }); | ||
|
|
||
| it("produces no non-empty delta when all text fields are empty", () => { | ||
| const line = JSON.stringify({ | ||
| type: "text", | ||
| text: "", | ||
| content: "", | ||
| message: "", | ||
| part: { type: "text", text: "" }, | ||
| }); | ||
| expect(deltas(parseLine("opencode", line)).filter(Boolean)).toEqual([]); | ||
| }); | ||
|
|
||
| it("handles empty top-level fields with content in part.text (regression #67)", () => { | ||
| const line = JSON.stringify({ | ||
| type: "text", | ||
| text: "", | ||
| content: "", | ||
| message: "", | ||
| part: { | ||
| type: "text", | ||
| text: "# 春之声\n\n三月的风是软的。", | ||
| }, | ||
| }); | ||
| expect(deltas(parseLine("opencode", line))).toContain( | ||
| "# 春之声\n\n三月的风是软的。", | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe("parseLine (qwen)", () => { | ||
| it("extracts text from singular part.text envelope", () => { | ||
| const line = JSON.stringify({ | ||
| type: "text", | ||
| part: { type: "text", text: "qwen output" }, | ||
| }); | ||
| expect(deltas(parseLine("qwen", line))).toEqual(["qwen output"]); | ||
| }); | ||
|
|
||
| it("extracts text from parts[].text array envelope", () => { | ||
| const line = JSON.stringify({ | ||
| parts: [{ text: "a" }, { text: "b" }], | ||
| }); | ||
| expect(deltas(parseLine("qwen", line))).toEqual(["a", "b"]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("makeParser (opencode) multi-line streaming", () => { | ||
| it("accumulates deltas across multiple lines", () => { | ||
| const parse = makeParser("opencode"); | ||
| parse( | ||
| JSON.stringify({ type: "step_start", part: { type: "step-start" } }), | ||
| ); | ||
| const r2 = parse( | ||
| JSON.stringify({ type: "text", part: { type: "text", text: "hello" } }), | ||
| ); | ||
| const r3 = parse( | ||
| JSON.stringify({ | ||
| type: "text", | ||
| part: { type: "text", text: " world" }, | ||
| }), | ||
| ); | ||
| expect(deltas(r2)).toEqual(["hello"]); | ||
| expect(deltas(r3)).toEqual([" world"]); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -366,6 +366,14 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age | |
| if (typeof obj.text === "string") out.push({ kind: "delta", text: obj.text }); | ||
| if (typeof obj.content === "string") out.push({ kind: "delta", text: obj.content }); | ||
| if (typeof obj.message === "string") out.push({ kind: "delta", text: obj.message }); | ||
| if (Array.isArray(obj.parts)) { | ||
| for (const part of obj.parts) { | ||
| if (typeof part?.text === "string") out.push({ kind: "delta", text: part.text }); | ||
| } | ||
| } | ||
| if (obj.type === "text" && typeof obj.part?.text === "string") { | ||
| out.push({ kind: "delta", text: obj.part.text }); | ||
|
Comment on lines
+374
to
+375
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These new |
||
| } | ||
| } | ||
|
|
||
| if (agent === "qoder") { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.parseLineWithStatenow handles the livepart.textshape, but this regression path still has no parser-level test coverage. That matters here because this file is the only contract between the agent CLIs and/api/draft//api/convert; without a fixture that exercises empty top-leveltext/content/messageplus singularpart.textand arrayparts[].text, a later refactor can silently reintroduce the exactstart -> doneno-output failure this PR is fixing. Please add a small regression matrix aroundparseLine('opencode', ...)/parseLine('qwen', ...)(for example in a newnext/src/lib/agents/argv.test.ts) that asserts these envelopes emit the expecteddeltachunks.