feat(ai): surface live AI generation status with smooth timing, per-tool duration, and prompt terminal handling - #6938
Open
Abeautifulsnow wants to merge 10 commits into
Conversation
Add a pure state machine (lib/ai/aiGenerationStatus.ts) driven by existing ai-agent-event events, plus a status line in the AI assistant panel that covers the whole generation period (Issue t8y2#6743 feature 1): - elapsed timer (tabular-nums), 1s recompute while generating - phase: preparing / waiting_model / generating / running_tool / cancelling - running tool: "第 N 轮 · 正在执行 {tool} · 已运行 {s}s" - no events yet: "等待模型响应 · 已运行 {s}s" (CLI slow first token) - idle >20s: "等待此步骤完成 · 最后活动 {n}s 前 [ · 正在执行 {tool}]" - >60s: gentle hint near Stop "响应时间较长,可继续等待或停止" - never uses "卡死"/stuck wording - timer is elapsed proof, not liveness proof Phase is event-driven, not mode-based (Ask mode emits turn_start and can call read-only tools); tool_call_end clears activeTool; dual-path reset (finally + abandon/onUnmounted/ensureLoaded) prevents cross-conversation leakage. Adds 24 unit tests + component reset assertions; i18n keys for all 8 locales.
The status line updated every second and on stream events but had no role="status" / aria-live semantics, so screen readers never received the newly added execution state. Add an sr-only live region (role="status", aria-live="polite", aria-atomic="true") fed by a new pure helper `liveAnnouncementText` that announces discrete state changes (phase / tool / turn / idle crossing) but deliberately EXCLUDES the per-second elapsed/idle numerals - a live region re-announced once per second would spam screen-reader users with the running timer. Mark decorative spinner/clock aria-hidden. Adds 6 unit tests (announced string never embeds a ticking \d+s countdown) + a rendering assertion pinning the live-region markup in AiAssistant.vue. i18n: waitingModelLive/generatingLive/idleLive keys across all 8 locales.
…ply completes Fix two defects in the Issue t8y2#6743 generation-status line (data-ai-generation-status): 1. Timer jumps/stutters under streaming load. The 1s setInterval recomputed the elapsed from wall clock at irregular fire times, so a delayed tick landed past a second boundary and Math.ceil skipped intermediate values (jump) while the display froze between ticks. Replace it with a self-rescheduling requestAnimationFrame ticker that writes statusNow only when the displayed whole second changes (mirroring the existing runAssistantDeltaFrame pattern), so the counter rolls +1s within ~a frame of each real boundary. Clamp elapsed/idle to >=0 so a stale ticker can never render "-1s". 2. Stale "thinking…0s" residue after the reply completes. agent_end/error reset the status via createGenerationStatus(now) (startedAt=now, phase=preparing), and the line was gated only on isGenerating — which is cleared in send()'s finally after runAgentStream()'s promise resolves, while the terminal event arrives earlier through the event callback (CLI child.wait/stderr drain and SSE stream close can take seconds). Agent_end/ error now enter a terminal "finished" phase that preserves startedAt/turn and clears activeTool; the line hides the instant it arrives (v-if excludes finished), the ticker stops, markCancelling is a no-op on finished, and statusText/liveAnnouncementText return "" for finished so no new i18n key is needed. Also clear msg.isThinking at agent_end so the reply card's spinner stops with the terminal event instead of waiting for the finally. Adds finished-phase state-machine tests (termination, idle-copy non-leak, cancelling no-op, long-running-hint suppression) and component source-text assertions pinning the v-if finished guard and the reset paths.
…r-tool step duration Mirror the Issue t8y2#6743 high-fidelity mockup (tmp/ai-generation-status-mockup.html): - Per-tool step duration: AiAgentStepItem gains startedAtMs/endedAtMs/durationMs. agentEventToStep stamps tool_call_start/tool_call_end with Date.now(); upsertAgentStep computes durationMs when both stamps exist (preserving startedAtMs across the merge, clamping negatives to 0). Completed tool steps render a right-aligned tabular-nums duration (0.8s/1.2s/12s via the new pure formatToolDurationMs); running tool steps swap the Play icon for a spinning Loader2 and render a spinner + "执行中…" tail. New i18n key ai.agentSteps.executing across all 8 locales. - Status-line polish to match the mockup: idle state uses a non-spinning Hourglass (the >60s hint keeps Clock); container gap 6px -> 7px; status icons 14px -> 12px; turn badge padding px-1 -> px-[5px]; tool chip bg-chart-2/10 -> /12; >60s hint px-2 py-1 -> px-[9px] py-[5px]. Tool names keep the spec-sanctioned i18n toolLabel map and the elapsed numerals keep tabular-nums (no mono added). Adds unit tests for the duration merge/formatter boundaries and source-text assertions for the step tail branches and the Hourglass idle branch.
…umed
run_cli_jsonl_agent emitted the fallback AgentEnd only after child.wait() +
the stderr drain, so a CLI that finished its work but was slow to exit
(lingering shutdown, held stderr, session teardown) kept the desktop
generation-status line ("正在生成回复 · 已运行 Xs") visible for the whole
reap duration even though the reply was already complete.
Move the synthesized AgentEnd dispatch to right after the stdout read loop
breaks, gated on no terminal error and no CLI-supplied AgentEnd. The process
is still reaped and a non-zero exit still returns an error; only the
terminal-event ordering changes so the frontend hides the status line the
instant the stream ends. Cancellation/read-error paths skip it so the
cancelling phase persists until the frontend finally. Covers all providers
that share the runner (codex, claude-code, cursor, grok, codebuddy, qoder,
opencode); Pi already emits AgentEnd at terminal-event parse time.
Adds a regression test where the fake CLI closes stdout then lingers 2s: the
AgentEnd must arrive within 1s (before the process reap) and be the terminal
event with correct aggregated usage.
…letes first agent_loop emits every ToolCallStart before running read-only tools in parallel, and their results can arrive in any order. The reducer previously cleared activeTool on the FIRST tool_call_end, so the status line flipped to "正在生成回复" while a later parallel tool was still executing. Track all outstanding tools per turn (activeTools by tool_call_id) and remove only the matching call on tool_call_end, keeping the newest remaining tool as the displayed activeTool and the phase running_tool until the last one completes. An end without a seen start keeps the old safe fallback, and the terminal agent_end/error clears activeTools. Adds a unit test: c1 completes before c2 -> phase stays running_tool with c2 active, then c2 completes -> generating.
…ion-status-display-6743
…ion-status-display-6743
…ion-status-display-6743 # Conflicts: # packages/app-tests/queryResultToolbar.test.ts
t8y2
requested changes
Aug 22, 2026
t8y2
left a comment
Owner
There was a problem hiding this comment.
request changes: AgentEnd 目前在 CLI 进程退出结果确定前就发送了。
crates/dbx-core/src/ai_cli_agent.rs:1101 在 stdout EOF 后立即发送 AgentEnd,但直到后面的 child.wait() 和退出状态检查才知道进程是否真正成功。CLI 可能先关闭 stdout,再卡住或以非零状态退出;与此同时,Tauri 前端收到 agent_end 后会取消事件监听并隐藏生成状态,因此失败流程会短暂或永久地被展示成已经完成。
请保持 AgentEnd 只表示已确认成功的终态。若需要在 stdout 完成后提前结束回复动画,建议新增独立的非终态事件(例如 response_complete / finalizing),不要提前复用 AgentEnd。
请补充两个时序测试:
- stdout 关闭后进程以状态 1 退出,失败确定前不得发送
AgentEnd; - stdout 关闭后进程延迟成功退出,验证新增事件和最终
AgentEnd的顺序。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
变更说明
Summary
The AI assistant panel previously showed only a static "Thinking…" placeholder while the model was working, making it impossible to tell a slow request from a hung one. This PR adds a live generation-status line (Issue #6743, feature 1) driven entirely by the existing
ai-agent-eventstream, plus a series of fixes that make it behave correctly under real-world streaming load.It replaces the placeholder with a full status display — phase, turn, active tool, overall elapsed time, and a gentle >60s hint — while fixing three defects found once the feature shipped:
It also aligns the display with the high-fidelity mockup and adds per-tool step durations.
Highlights
Status state machine (
apps/desktop/src/lib/ai/aiGenerationStatus.ts)phaseis derived solely fromai-agent-event(turn_start→running_tool→generating, never mode-based), so Ask mode's read-only tool calls are surfaced correctly.finishedphase:agent_end/errorpreservestartedAt/turn, clear the active tool, and let the component hide the line instantly.finishedis terminal (later events can't resurrect it),markCancellingis a no-op on it, andstatusText/liveAnnouncementTextreturn""so no wrong copy or new i18n key is needed.agent_loopemits everyToolCallStartbefore running read-only tools in parallel; the reducer now tracks all outstanding calls bytool_call_idso an earlier completion cannot clear the "正在执行" phase while a later tool is still running.Status line (
apps/desktop/src/components/editor/AiAssistant.vue)setIntervalwith a self-reschedulingrequestAnimationFrameloop that writesstatusNowonly when the displayed whole second changes — the counter rolls +1s within a frame of each real boundary instead of skipping values (jump) or freezing between delayed ticks (stutter). Elapsed/idle clamp to>= 0so a stale ticker can't render "-1s".finishedarrives (v-if="isGenerating && generationStatus.phase !== 'finished'"), independent ofisGeneratingclearing later.agent_endrather than waiting forfinally.Hourglass(the >60s hint keepsClock), container gap 7px, 12px status icons, turn-badge padding, tool-chip tint, and >60s hint padding.Per-tool step duration (
apps/desktop/src/lib/ai/aiAgentStepPresentation.ts)AiAgentStepItemgainsstartedAtMs/endedAtMs/durationMs;upsertAgentStepcomputes the duration across the start/end merge.0.8s/1.2s/12sviaformatToolDurationMs); running tool steps swap thePlayicon for a spinner and show "执行中…" (ai.agentSteps.executing, all 8 locales).Backend terminal-event timing (
crates/dbx-core/src/ai_cli_agent.rs)AgentEndis now emitted as soon as CLI stdout is fully consumed, beforechild.wait()/ the stderr drain — a CLI that finished its work but was slow to exit no longer keeps the status line visible for the whole process reap. Cancellation/read-error paths skip it so thecancellingphase persists. Applies to codex, claude-code, cursor, grok, codebuddy, qoder, and opencode (Pi already emits at terminal-event parse time).Accessibility
role="status"/aria-live="polite"/aria-atomic="true"sr-only live region fed byliveAnnouncementText, which announces discrete state changes (phase / tool / turn / idle crossing) but deliberately excludes the per-second numerals so screen readers aren't spammed by the running timer.Tests
apps/desktop/src/lib/ai/__tests__/aiGenerationStatus.spec.ts— phase transitions, idle/60s thresholds, cancelling-first priority, terminalfinished(startedAt preserved, idle copy never leaks), parallel-tool completion, elapsed clamping.apps/desktop/src/lib/ai/__tests__/aiAgentStepPresentation.spec.ts(new) — duration merge across start/end, end-without-start, negative clamp,formatToolDurationMsboundaries.apps/desktop/src/components/editor/__tests__/AiAssistant.clearAndCancel.spec.ts— dual-path status resets, live-region markup,finishedv-if guard, >60s hint gating, step-tail branches.crates/dbx-core/src/ai_cli_agent.rs— regression: a CLI that closes stdout then lingers 2s must still get itsAgentEndwithin 1s (before the reap) with correct aggregated usage.vue-tsctype-check andoxlintclean on the changed frontend files; the desktop frontend suite runs green (the single failing spec,windowsInstallerTemplate, is a pre-existing Windows-only env failure, reproducible on the base commit).Commits
f55e78e06d032cdfd71d693e81e64fb00e3db1bf626f5agent_endas soon as stdout is consumedac3ba616eFiles changed
15 files changed, +1283 / −23apps/desktop/src/lib/ai/aiGenerationStatus.ts— state machine,finishedphase, parallel tool trackingapps/desktop/src/components/editor/AiAssistant.vue— status line, rAF ticker, step tail, mockup polishapps/desktop/src/lib/ai/aiAgentStepPresentation.ts— step duration model + formattercrates/dbx-core/src/ai_cli_agent.rs— early synthesizedAgentEndapps/desktop/src/i18n/locales/{en,es,it,ja,ko,pt-BR,zh-CN,zh-TW}.ts—ai.status.*,ai.agentSteps.executingaiGenerationStatus.spec.ts,aiAgentStepPresentation.spec.ts(new),AiAssistant.clearAndCancel.spec.ts变更类型
涉及前端
验证
make check通过make cargo-check-fast通过关联 Issue