Skip to content

fix(ui): unify live process activity and settled disclosures - #5301

Merged
Astro-Han merged 10 commits into
apache:mainfrom
Astro-Han:fix/process-activity-single-status
Sep 15, 2026
Merged

Astro-Han merged 10 commits into
apache:mainfrom
Astro-Han:fix/process-activity-single-status

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

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

Before the feature Current result
Before: process messages remain inline with a waiting footer and no whole-turn disclosure After: one spinner-free working phrase with elapsed time above the expanded process

Completed

Before the feature Current result
Before: intermediate messages and tool results remain inline above the final answer After: the whole process is collapsed to an elapsed-time row while the final answer remains visible

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 → TurnView renders using the same synthetic login-repair events, 1100 × 950 viewport, light theme and fixed clock. Before: base ea990cab7; 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 After
Before: process header and separate waiting status After: one spinner-free header with rotating phrase and elapsed time

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

Before After
Before: completed process collapsed above the final answer After: completed process collapsed above the final answer

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. CompletedProcessExpanded exercises 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

  • UI suite: 470 tests passed. Additional checks cover phrase rotation, reduced-motion presentation, input/retry priority, and one activity owner across inserted instructions. Existing tests for settlement, manual expansion, final-answer identity, and failure disclosure remain in place.
  • Full static Storybook render smoke passed: 361 stories / 390 theme renders. Desktop Storybook typecheck and static build passed. Desktop main build and the imported OpenCode reply/reasoning regression test passed.
  • Focused static Storybook smoke: 8 scenes plus the running scene in dark mode passed. The running scene checks that clicking cannot collapse it; completed scenes exercise disclosure, browser-applied motion settings and expanded/collapsed geometry, focus, and final-answer selection. A live browser check also verified Enter/Space cannot collapse running work and Enter opens settled work.
  • Reply regression tests cover tool-followed commentary, trailing reasoning, steering, interrupted/replacement replies, and empty timelines. A real TurnView clipboard test verifies that completion and disclosure toggles copy only the visible final reply. Temporarily restoring the old copy selector fails the new consistency test; restoring the shared projection passes.
  • A committed, replayable lifecycle Storybook scene feeds real text/tool/completion events through the production live-buffer reducer, settlement and reconciliation, then hands off to durable messages in ledger order. Its play assertions verify an actually streaming final reply, preservation of its answer container through the live-to-durable handoff, automatic collapse of the same process node, and final-answer visibility outside it. Temporary Markdown text spans may change while streaming; the answer container is the identity invariant.
  • Changed-file lint and git diff --check passed.
  • Ablation: removing the footer ownership condition reproduced duplicate indicators and failed 3 regression tests; restoring it passed all 5 focused status tests. The rotation shares the elapsed timer rather than restoring separate rotation/fade timers.

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

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex implemented the change and ran local tests and Storybook verification.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 14, 2026
@Astro-Han Astro-Han changed the title fix(ui): unify process activity and restore rotating working phrases fix(ui): unify live process activity and settled disclosures Sep 14, 2026
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

@jackwener jackwener left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@Astro-Han
Astro-Han merged commit 2bc8a54 into apache:main Sep 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants