Skip to content

feat(desktop): Qoder-style thought/tool activity trail in the conversation - #527

Merged
PeterGuy326 merged 7 commits into
mainfrom
feat/qoder-thought-trail
Sep 29, 2026
Merged

PeterGuy326 merged 7 commits into
mainfrom
feat/qoder-thought-trail

Conversation

@PeterGuy326

@PeterGuy326 PeterGuy326 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Preflight

Summary

Mirrors the Qoder client's thinking/execution interaction (reference screenshot shared in the DingTalk thread): the turn thread now shows one collapsible chronological trail where the model's narration between tool calls appears as bounded "thought" rows interleaved with tool/agent executions, instead of the previous split between a coarse milestone list and a separate tool-only group.

A run reads the same while it streams and after a history reload: the live SSE projection and the persisted adapter fold the same events through the same primitives.

Change

Renderer (apps/desktop/renderer)

  • turns/trail.ts (new): the primitives both producers share — thoughtSnippet (single-line, 200-char, prefix-only so re-snippetting a growing segment is idempotent), thoughtItem, putTrailItem (an id keeps its first-seen position and takes the latest state; the head is dropped past TRAIL_MAX_ITEMS), foldNarration, closeNarration, dropOpenNarration. TRAIL_MAX_ITEMS = 128 bounds the whole trail — thoughts, tools and agents alike — and matches the bound the live activity buffer already used.
  • turns/adapter.ts: buildTrail(record) is now the same fold over the persisted events: a model.delta extends the open narration row, a trace.activity closes it and is inserted after it. A running turn keeps its open narration as the live thinking row; a settled turn drops its trailing narration because that text is the answer and already renders in the output section. Presentation-only: no contract, server or engine change.
  • turns/turnStream.ts (was the gap): the live projection previously folded turn.model.delta into the provisional body text only and built its trail from turn.trace.activity alone, so a watched run showed tool rows while the same run after a reload also showed the narration between them. It now folds narrations through the same primitives and carries thoughtSeq so row ids stay unique as segments close.
  • turns/types.ts: TurnTraceActivity gains kind: "thought" and optional text; title becomes tool/agent-only.
  • turns/TurnThread.tsx: ProgressTrail and the separate ActivityTrace block merge into one collapsible trail — running header「正在执行中」+ total elapsed timer, settled header summarized as tool counts with a failure-count variant, one line per item with a status word (运行中/已执行/运行失败) and status icon, live continuation marker while running. Rows without any trail activity keep the existing milestone steps unchanged.
  • roleweave-conversation.css: --thread list variant + thought/state/status styles, same theme-variable conventions.

Shared UI (packages/ui)

  • locales/zh.ts / en.ts: turn.activityRunningExpanded, turn.thoughtDone, turn.thoughtRunning, turn.toolRunning, turn.toolDone, turn.toolFailed, turn.toolsExecutedWithFailures.

Evidence

Check Result
npx tsc -b (full repo) pass
turn-adapter + turn-stream 38/38
turn-progress-interaction + groups-panel + TurnPanel 89/89
verify on 8a23530 6/7 jobs — see below

Node 24 / ubuntu-latest fails, but in the infrastructure, not in this diff. All three attempts at 8a23530 failed on a different non-verify-content cause, and each is reproducible independently of this branch:

Attempt Failing case Tracked as
1 service-connections PAT assertion #512, fix pending in #513
2 App.test.tsx:268:3 5000ms timeout — the unrelated codex/521-desktop-control-facade branch failed on the same line ten minutes earlier, and main has gone red and green the same way not tracked
3 security:check (npm audit --audit-level=high) — newly published fast-uri high advisory; 71/71 test files, the production build and 381 electron tests all pass in this same run #528

Node 24 / macos-14, both Unsigned installers, both Unpacked staging smoke and Layout parity are green on 8a23530.

Two tests state the guarantees directly: the live projection and the persisted adapter must produce an equal trail for the same run (closed and open narration), and the cap must bound interleaved thoughts and steps alike on both paths.

Assumptions

  • Engine trace.activity remains tool/agent only; thoughts are renderer-derived from model.delta, so engines need no change and turns recorded before this PR render the same trail retroactively.
  • A settled run's trailing narration is the answer, so it is not duplicated into the trail; while running, it is the honest live "thinking" row. A settled live buffer is discarded at the terminal event and the reloaded record takes over, so the two views agree in every state.

Acceptance

  • A completed turn with tool calls renders「已思考 · …」rows interleaved chronologically with tool rows, collapsible under one header.
  • A running turn shows the same rows live, with its open narration as「思考中」.
  • Settled header reads 执行工具 N 次(含失败变体); running header reads 正在执行中 with a live elapsed timer.
  • Turns without trail activity render exactly as before (milestone steps).
  • No raw chain-of-thought or engine NDJSON leaks: thought text is bounded, single-line, derived from the same public stream already persisted in turn-record.v1.

Refs #523, #512, #528

@Bindy-lbb Bindy-lbb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed commit 02727fb. The live SSE path does not yet produce the thought rows shown by the historical adapter, and the advertised 50-entry cap does not apply to tool/agent events. Node 24/macOS also has two stale assertions for the renamed running label; its third failure is the known project-board date fixture described in the PR. Node 24/Ubuntu has an unrelated qoder-engine child-process assertion failure.

Comment thread apps/desktop/renderer/src/turns/TurnThread.tsx
Comment thread apps/desktop/renderer/src/turns/adapter.ts Outdated
Comment thread packages/ui/src/locales/zh.ts Outdated
…ation

Mirror the Qoder client's thinking/execution interaction in the turn
thread:

- adapter: derive a chronological trail from the persisted event stream.
  model.delta narration between two trace.activity calls becomes a
  bounded "thought" item (whitespace-collapsed, 200-char snippet),
  interleaved with tool/agent activities in event order; tool entries
  keep their latest status at their first-seen position. A running turn
  keeps its open narration as a live "thinking" item; a settled turn's
  trailing narration IS the answer and stays in the output section.
  Bounded to 50 entries.
- TurnThread: merge ProgressTrail and the separate ActivityTrace block
  into one collapsible trail — running header "Executing" with the
  total elapsed timer, settled header summarized as tool counts (with a
  failure-count variant), one line per item with a status word and
  icon, and the live continuation marker while running. Rows without
  any trail activity keep the existing milestone steps unchanged.
- locales zh/en: thought and tool status copy.
- tests: adapter trail cases (interleave, dedupe, live open segment);
  interaction coverage for interleaved thoughts, failure-count summary
  and the live thinking item; TurnPanel running-summary copy updated.
@PeterGuy326
PeterGuy326 force-pushed the feat/qoder-thought-trail branch from 02727fb to afa9639 Compare September 28, 2026 15:03
@PeterGuy326

Copy link
Copy Markdown
Contributor Author

Rebased onto main @ f2fca7b (#523 merged) and re-landed as afa9639.

What changed after the rebase: #523 already carries its own Qoder-style activity line (collapsible header with tool-count summary, live tool naming, elapsed clock), so this PR now only adds the missing half — the thought half:

  • TurnThread.tsx: the tools/agents groups are replaced by one chronological turn.trace list. Thought items render a status icon plus a bounded narration snippet (已思考 / 思考中); tool items keep the terminal/wrench icon, the Markdown detail (with onOpenResource) and gain a status word (运行中 / 已执行 / 运行失败, also coloured by the existing status icon). Agents render as before.
  • Collapsed-while-running label now falls back to 思考中 when the open element is a running thought (previously only a running tool was named), and the generic 继续推理… row is suppressed when that narration is already the live element.
  • adapter.ts (unchanged from the first push): model.delta narration between two trace.activity events becomes a thought item, 200-char snippet, ≤50 entries; a running turn keeps its open segment as a live thinking item, a settled turn's trailing narration stays in the output section.
  • CSS: thought/state share the quiet subtle foreground; failed state word turns danger. The list itself reuses feat(desktop): integrate Sept 27 client acceptance #523's owb-activity-trace__group ol container.
  • Tests: adapter trail cases + two interaction cases (interleaved order with a failure-count summary; a running thought as the live element with no duplicate continuation row). Local: 58/59 across turn-adapter / turn-progress-interaction / TurnPanel — the single failure is the CSS-contract test's process.cwd() assumption when vitest is invoked from apps/desktop, which resolves from the repo root as CI does.

Note on the previous run's red Node 24 jobs: the failing case was qoder-engine turn run: terminating the Gemini adapter reaps its detached Antigravity client (expected ESRCH, nothing thrown) — a server-side process-reap assertion unrelated to this renderer change. A rerun has been triggered.

… items carry no title

TurnTraceActivity.title became optional (thought items carry text
instead), so the collapsed-while-running label must fall back to the
activity id rather than pass an undefined tool name into the
interpolated copy. Caught by the verify job's typecheck, which a
local incremental build had skipped.
@PeterGuy326

Copy link
Copy Markdown
Contributor Author

CI note on 05ec64b: the two Node 24 jobs failed, but the only failing test is renderer/test/App.test.tsx — App context navigation > consolidates the collaboration toolbar and places employee actions beside the tabs (App.test.tsx:268:3, Test timed out in 5000ms). That file alone runs ~190s on ubuntu / ~302s on macos, and it is not specific to this branch:/n/n- run 36441237473 on the unrelated branch codex/521-desktop-control-facade (b071b54) failed on the same test at the same line ~10 minutes earlier

  • main (f2fca7b) went red with the same App.test.tsx 5000ms timeouts and came back all green on rerun

I re-ran the two failed jobs rather than changing behaviour. Everything else (turn-adapter, turn-progress-interaction, project-board, build, installers, layout parity) is green.

… adapter shows

Review on #527 found the trail was only half-true. Two fixes:

The live SSE projection never opened a narration row: turnStream only folded
turn.model.delta into the provisional body text and built the trail from
turn.trace.activity alone, so watching a run live showed tool rows while the
same run after a history reload also showed the model's narration between them.

One 50-entry cap was advertised but only ever applied to thoughts; tool and
agent rows were appended unconditionally, so the trail could grow without
bound beside it.

Both paths now fold through one set of primitives in turns/trail.ts: a delta
extends the open narration row, an arriving activity closes it and keeps its
first-seen position, and the head is dropped past TRAIL_MAX_ITEMS — the bound
the live activity buffer already used, now covering every row type. A settled
turn drops its trailing narration because that text is the answer and renders
below the trail; a running turn keeps it as the live thinking row. turnStream
carries thoughtSeq so row ids stay unique as segments close.

Tests: live and persisted folds of the same run must be equal, and the cap must
bound interleaved thoughts and steps alike on both paths.
@PeterGuy326

Copy link
Copy Markdown
Contributor Author

Both real findings fixed in 8a23530, on top of a rebase onto main @ f2fca7b (#523 merged). Taking them in order.

1. The live SSE path now produces the same trail. You were right that I had deferred it — the old PR body even listed it as "a possible follow-up". turnStream.ts only folded turn.model.delta into the provisional body text and built its trail from turn.trace.activity alone, so a watched run showed tool rows while the same run after a reload also showed the narration between them. The two producers now fold through one set of primitives in the new turns/trail.ts: a delta extends the open narration row (foldNarration), an arriving activity closes it and is inserted after it (closeNarration), and inserts go through putTrailItem. LiveRunState carries thoughtSeq so ids stay unique as segments close. The agreement is not asserted by eye: turn-stream.test.ts folds one run live and as a persisted record and requires the two trails to be toEqual, including the open trailing narration of a running turn.

2. The cap was not a cap. TRAIL_MAX_ITEMS only ever gated thoughts; tool/agent rows were appended unconditionally, so a long run could grow the trail without bound beside it. putTrailItem now drops the head past the bound for every row type, and the bound is one shared constant (128, the number the live activity buffer already used, so no live run is trimmed differently than before — the old thought-only 50 was the misleading part). Both paths have a test with interleaved thoughts and 140 steps asserting the trail is 128 long and that the narration is not favoured over the steps that overflow it.

3. Stale running-label assertions — fixed before this push (TurnPanel.test.tsx now expects「正在执行中」); Node 24/macOS is green.

4. The project-board date fixture — that was #523's own failure; #523 is now merged into the base, so it is gone from this diff rather than worked around in it.

5. qoder-engine child-process assertion — agreed, unrelated to this change; it did not reproduce on rerun. One note for the record while you review: App.test.tsx is currently flaky repo-wide on the hosted Node 24 runners — run 36441237473 on the unrelated codex/521-desktop-control-facade failed on the same App.test.tsx:268:3 5000ms timeout ~10 minutes before mine did, and main went red and green again the same way. The whole verify matrix is green on 8a23530.

Still technical-only on my side: I have not merged, and the engine/contract surface is untouched (trace.activity stays tool/agent; thoughts are renderer-derived from model.delta, so old turns render the trail retroactively).

@PeterGuy326

Copy link
Copy Markdown
Contributor Author

Follow-up on the Node 24 / ubuntu-latest job, since it is the only red left and it is worth being precise about why.

8a23530 now has 71/71 test files passing, the production build green and all 381 electron tests passing in that job — and the job still fails, in security:check:

npm audit --audit-level=high  -> exit 1
fast-uri 3.0.0 - 3.1.6  Severity: high  (GHSA-qw65-cvwx-89v3, GHSA-58mr-gqgx-xq4g)

That advisory was published overnight: the same job on 05ec64b was green at 2026-09-28T15:26Z, and nothing in between changed a dependency. I reproduced it locally on 8a23530 (npm audit --audit-level=high exits 1 with the same four advisories), so it is the advisory database, not this branch — the default branch will fail the same way on its next push, and so will every open PR.

Filed as #528 with the full evidence table. I did not touch dependencies here: fixing it belongs in its own change, and it would bury the trail work in a lockfile diff.

For completeness, the two earlier failures on this same commit were also not this diff: attempt 1 was the service-connections PAT assertion already tracked as #512 (fix pending in #513), and attempt 2 was the App.test.tsx:268:3 5000ms timeout that also takes down unrelated branches and main. Attempt 3 is the only one that is deterministic, and it is deterministic for everyone.

Node 24 / macos-14, both installers, both staging smokes and layout parity are green on 8a23530. The PR description now says 6/7 rather than claiming a clean matrix.

Comment thread apps/desktop/renderer/src/turns/TurnThread.tsx Outdated
…lded title

A running thought is always the trail tail (an arriving step closes the
open narration before it lands), so when narration streams on while a
tool is still awaiting its result, the thought is the newest live
element and the folded title must read as thinking instead of naming
the tool. Check runningThought before runningActivity and cover the
coexistence case.

@Bindy-lbb Bindy-lbb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

复审 40a786a:running thought 现在优先于仍运行的工具作为折叠标题,并存交互用例验证了思考标题和工具行状态。先前提出的 live trail、轨迹上限和旧摘要断言问题也已在当前分支修复。代码审查通过。CI 当前仍有独立失败:Ubuntu 依赖审计(fast-uri/undici 等)和 macOS App.test.tsx 超时。

PeterGuy326 added a commit that referenced this pull request Sep 29, 2026
…#531)

The full-App toolbar test sits above the 5s vitest default on slower runners and failed fresh runs of main on both platforms (#529), blocking #520/#522/#524/#527 through no change of their own. 15s matches the other full-App tests in this file. Closes #529.
@PeterGuy326
PeterGuy326 merged commit 6409f73 into main Sep 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants