Skip to content

fix: parse OpenCode nested part.text in JSON output#92

Open
Aliancn wants to merge 2 commits into
nexu-io:mainfrom
Aliancn:fix/opencode-output-parser
Open

fix: parse OpenCode nested part.text in JSON output#92
Aliancn wants to merge 2 commits into
nexu-io:mainfrom
Aliancn:fix/opencode-output-parser

Conversation

@Aliancn

@Aliancn Aliancn commented May 27, 2026

Copy link
Copy Markdown

Problem

OpenCode's --format json output wraps content in nested fields, but the parser only checked top-level text/content/message fields (which are empty strings). This causes both draft and convert features to produce no output when using OpenCode as the agent.

Closes #67

OpenCode actual output format

OpenCode emits JSON lines like:

{"type":"text","part":{"type":"text","text":"actual content here"}}

The real content lives in:

  • obj.part.text (single object, when obj.type === "text")
  • obj.parts[].text (array form, for forward-compatibility)

Top-level text/content/message are empty strings.

Fix

Extended the OpenCode/Qwen parser in argv.ts to also extract content from:

  1. obj.parts[].text — array form
  2. obj.part.text when obj.type === "text" — the actual current output format

Testing

# Before fix: no delta events
curl -N http://localhost:10033/api/draft \
  -H "Content-Type: application/json" \
  -d '{"agent":"opencode","instruction":"写一句关于春天的短句"}' 2>/dev/null
# → event: start → event: done (no delta)

# After fix: delta events with content
# → event: start → event: delta {"type":"delta","text":"春风拂过枝头,万物在泥土里翻了个身。"} → event: done

Relationship to PR #78

PR #78 addressed this issue by adding parts[].text parsing, but it doesn't cover the actual part.text (singular) field that OpenCode currently emits. This PR covers both cases.

OpenCode's --format json output wraps content in obj.part.text (type: "text") or obj.parts[].text (array), but the parser only checked top-level text/content/message fields which are empty strings. This caused both draft and convert features to produce no output when using OpenCode as the agent.

Closes nexu-io#67

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Copilot AI review requested due to automatic review settings May 27, 2026 12:20
@lefarcen lefarcen requested a review from nettee May 27, 2026 12:24
@lefarcen lefarcen added size/XS Extra-small change (<20 lines) risk/medium Medium risk change type/bugfix Bug fix labels May 27, 2026
@lefarcen

Copy link
Copy Markdown

Heads-up: #78 is also open against this area — both PRs touch next/src/lib/agents/argv.ts and address the OpenCode output parsing path tied to #67. This PR covers the current singular part.text shape, while #78 also includes broader progress/usage handling, so @Aliancn and @ZhongXiandou may want to compare approaches. The maintainer team will decide which path lands; sharing so neither effort gets wasted.

@nettee nettee left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one blocking issue in the new OpenCode/Qwen parser branch; the details are inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

}
}
if (obj.type === "text" && typeof obj.part?.text === "string") {
out.push({ kind: "delta", text: obj.part.text });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseLineWithState now handles the live part.text shape, 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-level text/content/message plus singular part.text and array parts[].text, a later refactor can silently reintroduce the exact start -> done no-output failure this PR is fixing. Please add a small regression matrix around parseLine('opencode', ...)/parseLine('qwen', ...) (for example in a new next/src/lib/agents/argv.test.ts) that asserts these envelopes emit the expected delta chunks.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@lefarcen lefarcen mentioned this pull request May 27, 2026
Covers part.text (singular), parts[].text (array), top-level fields, empty-field scenarios, and multi-line streaming via makeParser.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

@nettee nettee left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one blocking issue in the new OpenCode/Qwen parser branch; the details are inline.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Comment on lines +374 to +375
if (obj.type === "text" && typeof obj.part?.text === "string") {
out.push({ kind: "delta", text: obj.part.text });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new obj.part?.text / obj.part.text reads do not type-check against the file's parsed Record<string, unknown> shape. On this head, pnpm -F @html-anything/next typecheck fails with TS2339: Property 'text' does not exist on type '{}' on lines 374-375, so the PR still breaks the package's required compile gate even though the runtime regression tests pass. Please narrow obj.part to an object type before reading text here (for example by assigning it to a local, checking typeof part === "object", and then casting to { text?: unknown }) so the parser fix compiles cleanly as well as running correctly.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@lefarcen lefarcen added size/M Medium change: 100-299 lines and removed size/XS Extra-small change (<20 lines) labels May 28, 2026
@lefarcen lefarcen requested a review from nettee May 28, 2026 02:32
@lefarcen

Copy link
Copy Markdown

Hey @Aliancn — wanted to close the loop on this. PR #78 just merged into main, and it turns out it landed the same obj.part.text fix you diagnosed here. Looking at the current argv.ts:

const part =
  obj.part && typeof obj.part === "object"
    ? (obj.part as Record<string, unknown>)
    : null;
const text = [part?.text, part?.content, part?.message, obj.text, ...].find(...)

That's the singular part.text path you identified, and with the typeof obj.part === "object" narrowing that nettee's review flagged on this PR too — both problems solved together. Your diagnosis was spot on; the fix got into main through the parallel effort on #78.

I'll flag this to the maintainer team to formally close as superseded. Appreciate the contribution — you found the right field at the right time. 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/medium Medium risk change size/M Medium change: 100-299 lines type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

http://localhost:3000/页面的log打印了几次如下的信息,最后展示的状态是: agent 进程退出 (exit=0)

3 participants