fix(ui): unify live process activity and settled disclosures - #5301
Conversation
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
Preserve transient tail selection and shared reply identity; align spinner assertions with the spinner-free process header. Generated-by: Codex
Validate browser-applied motion and final geometry without requiring an intermediate animation frame on busy CI runners. Generated-by: Codex
Generated-by: Codex
Generated-by: Codex
jackwener
left a comment
There was a problem hiding this comment.
Independent review — top status, run/fold lifecycle, reply identity, motion preference, Storybook evidence, and incremental necessity
Reviewed SHA: 3ec48f6f70d98f566d3aca62246c4e7fcbdc4d2d, verified as the live head immediately before publishing. Base ea990cab7, 13 files, +456/−111.
No P0–P2. One area I could only verify in part is stated explicitly at the end rather than folded into that claim.
I worked from the PR's real increment against its base rather than the headline comparison in the description, which spans a different range.
Reply identity is genuinely shared, and the mechanism holds up
foldTimeline now returns { entries, finalReply }, so the reply is identified once during folding. The part worth checking is that finalAssistantReplyText was not left as a second derivation — it is reimplemented to delegate, and every remaining production consumer resolves through the same fold: the turn body and assistantText in chat-turn.tsx, action availability in app-shell-turn-view-model.ts, preview availability in quote-companion-panel.tsx, and the prompt rail reply in chat-view.tsx.
Two deliberate behaviour changes come with it, and both look right to me: process commentary before tool activity or a new user instruction can no longer become the reply, and the assistant aggregate is no longer a fallback. A turn that ends on tool activity therefore has no reply — and because availability resolves through the same source, the action is unavailable rather than offering commentary as an answer.
Verified by ablation rather than by a green suite. The new if (buffer.length === 0) return; in flush() is load-bearing: without it the final empty-buffer flush erases a reply that an earlier flush had already settled. Removing it turns exactly one test red — display and copy share reply identity across process, steering and interrupted boundaries — with the rest of the file passing, and restoring it returns the file to green.
Run and fold lifecycle, checked in a real browser
I built Storybook at this SHA, served it locally and drove it with Playwright rather than reading the stories.
While running, the process disclosure is open, and clicking its summary with a forced click leaves it open — it is not collapsible. The story's own play assertions reach the same conclusion from the mechanism side (aria-disabled="true", tabindex="-1", no toggle glyph), which is a stronger guard than relying on a click handler.
On completion the disclosure is collapsed, and the settled answer renders outside it while process commentary stays inside — I confirmed this by walking the turn's leaf text nodes and partitioning them by details containment, not by trusting a class name.
Top status carries a single .maka-turn-processing in the whole document, with no spinner element present; the story additionally pins that the footer contains no second copy.
One process note: an intermediate probe of mine reported no answer outside the fold, which would have been a serious finding. It was my selector, not the product — the answer renders in .astryx-markdown-paragraph. I treated the empty result as a broken instrument and checked it before drawing a conclusion.
Storybook evidence goes through production code
The lifecycle story drives the production reconcileLiveTurnBuffer and settleLiveTurnBufferStep with real SessionEvent values through a reducer and an onStreamingSettled callback, rather than assembling view state by hand. Its play function asserts rather than merely rendering, and my independent DOM observations agree with it.
Incremental necessity
I suspected a rendering-cost regression: finalAssistantReplyText now runs a full fold per call, where it previously scanned backwards and returned early. The call sites are guarded — the prompt rail is inside a useMemo with an identity comparison, and the presentation entry sits behind a per-turn cache — so the concern does not hold. I am recording it as a checked-and-dropped hypothesis so the effort is not repeated.
What I verified only in part
For the motion preference, two of the three properties are established. The subscription is live rather than a one-time read — useMediaQuery uses useSyncExternalStore whose subscribe attaches a change listener to the media query list. Elapsed time is computed independently of the preference and I observed it advancing in a running story. The selection expression pins the phrase to the first entry whenever the preference is set.
What I could not observe is the rotation itself. In every story I tried the phrase stayed on the first entry, and the freeze conditions were all absent — no data-maka-e2e-fixture, no data-maka-reduced-motion, no frozen ancestor, and prefers-reduced-motion unset. The most likely explanation is that the elapsed value feeding the phrase index is undefined in those stories while the displayed duration comes from elsewhere, but I did not confirm that, so I am not claiming the rotation works or that it does not, and I could not run the before/after differential at equal elapsed time.
That has a consequence worth stating for the evidence base: the stories I exercised do not cover phrase rotation, so Storybook runs cannot protect it. A case that feeds a start time far enough back for the elapsed value to cross the interval would close that gap.
Verification basis and limits
Built @maka/core and @maka/ui at this SHA; 58 targeted tests pass across timeline folding, reply identity, the running spinner and materialisation, plus the ablation above. Built Storybook and ran real-browser checks with Playwright as described.
I did not run the Desktop suite, the theme rendering matrix, or a real Electron session. The author's 470 UI tests and 361 stories / 390 theme renders are his evidence, not mine. My code conclusions are independent of CI status and of mergeability.
Automated review, agent-operated. Posted from the shared jackwener GitHub account; the reviewing agent is @kabi-opus (human owner: 卡比卡比 / @WAWQAQ), acting at the PR author's request. This is an AI-assisted review and does not replace independent human review. No merge is performed and none is authorised by this review.
Summary
An active Bash tool could appear between a process header saying “Working…” and a footer saying “Waiting for model output…”. The current process summary now owns the working cue, with a footer fallback only before that segment has a process. Earlier segments separated by user instructions do not repeat the cue. The top process header shows only its phrase and elapsed time, without a spinner; tool-level progress indicators remain owned by their tools.
Restore the rotating working phrases removed in #4270, including “正在琢磨…” and “正在鼓捣…”. They rotate every 20 seconds using the existing elapsed clock, with a stable accessible status name. Reduced motion freezes only the rotating phrase and disables transitions; elapsed time continues updating. Astryx’s existing media-query subscription applies preference changes immediately. Provider retries, input requests, failures, and cancellation keep their explicit surfaces; retries and input requests suppress the playful cue.
Live work stays expanded: its summary has no disclosure arrow, is excluded from Tab navigation, and cannot be collapsed by pointer or keyboard. Settled work remains toggleable. Opening, closing and automatic completion use Astryx's grid disclosure pattern with duration-medium/ease-standard; the arrow uses duration-fast. Native ::details-content and a discrete content-visibility transition preserve the exit animation without a second open state or JavaScript timer. Unsupported browsers retain immediate native disclosure. Reduced motion disables transitions.
This extends the process disclosure introduced in #5259. The timeline fold now returns both disclosure entries and the original final-reply item. Display, clipboard content, main/side-chat copy availability, and prompt-rail answer previews share that reply identity. Text followed by tools is process commentary, and a new user instruction clears the current reply; neither hidden commentary nor the concatenated assistant aggregate can masquerade as the answer. Interrupted replies remain visible, and a replacement reply becomes the current answer. Timeline ordering, paging, and scroll authority are unchanged.
Before / after
Full feature: before whole-turn folding → current result
Before:
d2e6c1f27, the parent of #5259. After:3ec48f6f7, the current #5301 head. This comparison covers the combined user-visible change across #5259 and #5301. The historical Before is not current main: #5259 already introduced the whole-turn disclosure on main. The previous images showed only #5301's incremental change, so both sides already had a disclosure bar.Both sides use the same fictional login-repair messages, 1100 × 950 viewport, light theme and fixed clock. Each runs its revision's real
ComposedShell → ChatView → TurnView; no disclosure was hidden or added for the screenshots.Working, after tool results
Completed
Before completion and after completion, the old UI keeps intermediate commentary and tools inline. The current UI keeps running work expanded, then folds the completed process above the final answer. Individual tool/reasoning disclosures existed in the old UI; the new row folds the whole process.
Incremental comparison for PR #5301 only (its main baseline already includes #5259)
Real
ComposedShell → ChatView → TurnViewrenders using the same synthetic login-repair events, 1100 × 950 viewport, light theme and fixed clock. Before: baseea990cab7; after:84d03b220. The identical review fixture is rendered against each revision's UI source; these are browser screenshots, not mockups.Running after tool results, before the final answer
Before: a “正在处理…” header plus a separate “等待模型输出…” footer; the running process can be collapsed. After: one rotating phrase and elapsed time in the header, no header spinner, and running work stays expanded.
Completed, with the final answer outside the process
This settled layout already existed and is intentionally preserved. The new behavior is the animated transition into it and the shared reply identity used by display, copy and answer previews; a static screenshot does not demonstrate either animation or clipboard correctness.
Replay
Product / Shell Official AppShell / 完整过程:展开 → 最终回答 → 自动折叠(process-reply-lifecycle-complete) in Storybook to inspect expanded work → final answer → automatic collapse.CompletedProcessExpandedexercises reopening/closing, focus, and preservation of the final-answer selection. Screenshots are hosted on an isolated evidence branch in my fork, pinned to an immutable commit, and add no image assets to this PR's product diff.Verification
git diff --checkpassed.Separate performance probe: a warmed synthetic 45-tool process (~9,500px), 1200×900 viewport, 4x CPU slowdown, two samples per direction/mode, measured over 600ms windows. Without transitions: one layout, 0.5–1.3ms cumulative layout time. With transitions: opening 32–33 layouts / 17–19ms; closing 36 layouts / 25–26ms. Animation adds layout cost; this is a visual continuity improvement, not a performance optimization or a real Host/scrolling benchmark. Reduced motion resolves to 0s. The completed lifecycle and reduced-motion expanded scene also passed static Storybook play checks. Disabling the content transition fails the browser-applied motion assertion. Reduced motion and an injected 350ms main-thread stall pass; the test does not depend on sampling a particular animation frame.
AI use
Tool(s) and scope: Codex implemented the change and ran local tests and Storybook verification.
Checklist
Does this PR entail a change in behavior?