Skip to content

feat(ui): real-time status language across stages — connecting indicator + stable thinking/outputting signal #646

Closed

Description

@Astro-Han

Background

After #642 unified streaming and settled answers onto a single render path, manual testing surfaced a class of real-time feedback that still needs work: across the stages where the app communicates with the user (connecting → thinking → outputting → tool running→done), the "in progress" signal is either missing, unstable, or too fleeting, so the user can't reliably tell which stage the system is in.

This is the tracking issue for a consistent cross-stage real-time-status language, folded under the #546 visual-polish umbrella. Its first PR delivers one unified indicator plus the tool run→done seam (see Scope below).

Current state (verified in code)

  1. Connecting — missing. The streaming slot only has streaming / draining phases (app-shell-session-events.ts), and the composer's streaming prop is activeStreamingLive, which requires the first token to have arrived (app-shell.tsx). So the window between "sent" and "first token" — while the LLM connection is being established — has no dedicated signal, only the composer's brief "正在发送…". Note: turn_started / model_stream_started are RunTrace events, not SessionEvents, and model_stream_started fires before any network request — so "connecting" and "thinking" are not observably distinguishable; the real wait is one window (model_stream_started → first delta), armed locally from send().

  2. Thinking / outputting — unstable. Two separate signals that don't cover the window reliably:

    • The live "深度思考" disclosure shimmers its title (TextShimmer) only when a live thinking item exists, i.e. a thinking model that has already emitted reasoning tokens. Non-thinking models, and the gap before the first reasoning token, show nothing — so "正在思考" does not display stably.
    • The composer's "Maka 正在回答…" hint only appears once answer text streams. Before the first answer token there is no "outputting" signal.

    Net: for a stretch of every turn there is no clear thinking/outputting indicator.

  3. Tool run→done seam — too fleeting, no transition. The running-tool shimmer is correctly wired (refactor(ui): unify streaming (live) and committed answer onto one render path #642 verified running/pending tools render data-slot="text-shimmer" with a live animation), but it flashes by on sub-second tools and there is no explicit running→done transition, so in practice it feels like "I can't tell if a tool ran."

Goal

One consistent real-time status language across stages — connecting / thinking / outputting / tool running→done — so the user always knows which stage the system is in. Two properties matter: coverage (no dead window with zero signal) and consistency (the same visual vocabulary reused across stages, not four ad-hoc treatments).

Scope (first PR — one status-language change)

A single unified indicator plus the tool seam, sharing one debounced "legible working→settled" principle. The connecting-vs-thinking two-state split and IPC forwarding are not built (unobservable). Design:

  • 正在处理… indicator at the tail of the in-flight turn, covering every window where the model is being awaited with nothing streaming (turn head and the resume gap after a tool completes). Reuses the existing timeline live vocabulary (16px neutral icon + TextShimmer). Rendered as a transient view state in LiveStreamingEntries — materialize.ts is not touched.
  • State model: a turnActive boolean (armed in local send(), cleared on complete/error/abort, guarded by turnId against a late complete); visibility is a pure derivation turnActive && !streaming && !thinking && !in-flight tools, with a ~200ms delay on the composite's rising edge so fast responses never flash it. Timer lives in a React hook, not the event-handler closure. Visibility also gates on activeSession.status === 'running' so a backgrounded session self-heals.
  • Tool run→done seam: the running row is visible immediately but only shimmers after ~200ms (sub-second tools never shimmer), and settles running→done via a data- attribute + existing CSS transition (no new keyframe). Same debounce principle as the indicator.
  • Composer Stop is made available during the wait window (its streaming prop becomes the composite condition), since that window is exactly when a user wants to stop.

Out of scope: IPC / lifecycle-event forwarding; a global "only one shimmer at a time" invariant (multiple simultaneous shimmers are acceptable); artifact preview / browser panel / prose — those stay in #546 PR6.

Verification

Pure-derivation + injectable-scheduler unit tests (fake timers: 200ms rising edge, first-content-within-200ms never shows, mid-turn auto-recover, abort/error/complete clear, turnId guard); event→state wiring via the existing streaming-handoff harness; visual-smoke staged screenshots (indicator shown; tool running-shimmer and completed end-points). No E2E (timing-flaky and can't judge feel); the 200ms tuning and the settle transition are a one-time manual feel pass.

Notes

Activity

  1. changed the title [-]打磨:各沟通环节的实时状态反馈(连接中 / 输出中 / 工具运行 seam)[/-] [+]Polish: real-time status feedback across communication stages (connecting / streaming / tool run→done seam)[/+] on Jul 8, 2026
  2. Astro-Han commented on Jul 8, 2026

    @Astro-Han
    ContributorAuthor

    Rewritten in English for the record; parking this rather than tracking it as an open issue for now — reopen when the real-time-feedback polish is picked up.

  3. Astro-Han commented on Jul 8, 2026

    @Astro-Han
    ContributorAuthor

    Reopening to track under the long-term visual polish umbrella #546. The tool run→done seam and running-state visibility land under #546 PR6 (tool rendering polish); the connecting/outputting status language is a cross-stage real-time-status concern in the same family.

  4. changed the title [-]Polish: real-time status feedback across communication stages (connecting / streaming / tool run→done seam)[/-] [+]Real-time status language across stages: connecting indicator + stable thinking/outputting signal[/+] on Jul 9, 2026
  5. changed the title [-]Real-time status language across stages: connecting indicator + stable thinking/outputting signal[/-] [+]feat(ui): real-time status language across stages — connecting indicator + stable thinking/outputting signal[/+] on Jul 9, 2026
  6. Astro-Han commented on Jul 9, 2026

    @Astro-Han
    ContributorAuthor

    Field findings from live CDP measurement (GLM / glm-5.2)

    Two symptoms reported on a real machine; measured on the running app over CDP
    (instrumented the live event stream with timestamps + delta sizes, sampled the
    rendered bubble's innerText length per frame, and polled session status + the
    indicator DOM). Neither is a regression from the current branch — one lives in
    the streaming/markdown render path, the other in this issue's own indicator
    gating.

    Symptom 1 — "output jumps a chunk at a time" is markdown block reflow, not a stream-smoothing snap

    Measured a long, tool-free GLM answer (1945 text_deltas over ~34s):

    • Wire deltas are fine-grained: per-delta size p50=3, p90=8, max=31 graphemes; inter-arrival max gap 154ms. Simulated backlog at the smoother's maxCps=400 peaks at ~56 graphemes — nowhere near the maxBacklogGraphemes=800 snap threshold. Zero snaps.
    • Rendered growth is smooth for prose: per-frame displayed-length step p50=5, p90=9 graphemes.
    • The jump is a DOM reflow at markdown structural boundaries. On a table-producing answer, the rendered innerText jumped +797 in a single 46ms frame (2109 → 2906) at the moment the table separator row arrived — while the wire was still delivering 1–6 grapheme deltas and the smoother's displayed text was still trailing the raw text by ~377 graphemes (so no snap fired). The table's lines render as inline text until |---| completes, then re-parse into a grid all at once.

    Conclusion: the maxBacklogGraphemes=800 snap hypothesis does not explain the observed jumpiness for GLM — GLM streams tiny deltas and the snap never triggers. The "chunk then jump" is (a) incremental-markdown re-parse reflow when a block (table/heading/code fence) completes, plus (b) in multi-step turns, the natural per-step text + tool-gap cadence.

    Symptom 2 — the composer "正在处理…" indicator is intermittent (a status-gate race)

    Across 4 measured turns, "正在处理…" showed 0/4 (the composer went straight to "正在回答…"), yet the user sees it sometimes — i.e. it's racy, not dead.

    Mechanism:

    • armTurnActive sets turnPhase='waiting' synchronously at send (app-shell-chat-actions.ts), but the existing-session send path does not optimistically set session status to running (only the new-session path calls refreshSessions).
    • The renderer's activeSession.status only flips to running asynchronously via subscribeChanges (app-shell-effects.ts) after the runtime persists the run (agent-run.ts).
    • The head indicator is gated on sessionAwaitingModel = activeSession?.status === 'running' (app-shell.tsx:372) and a 200ms reveal delay.
    • So "正在处理…" only appears if renderer-status→running and the 200ms delay both resolve before GLM's first token flips turnPhase waiting→streamed. With GLM first-token ~2s and the status update lagging behind the store (store status was running at send+50–100ms in my captures, yet the indicator still never showed), the head window is usually missed.

    The status === 'running' gate is redundant with the phase model: turnInFlight (phase present) already means "armed, awaiting first token". Gating additionally on an async-lagging status is what makes the head indicator flaky.

    Fix directions (not applied yet):

    • Symptom 2 (this issue): key the head "正在处理…" off turnPhase === 'waiting' alone (drop the status === 'running' gate for the head), or set status optimistically at send. Minimal + removes the race.
    • Symptom 1 (separate concern): track under a new issue — options are smoothing markdown block transitions (render completed blocks progressively) or accepting table reflow as inherent. Not in this issue's scope.

    Method note

    Captured via CDP on the running dev build: parallel subscribeEvents subscription for wire timing/sizes, a .maka-bubble-streaming innerText sampler for render smoothness, session-status polling, and an indicator-DOM poller. Raw per-run numbers available on request.

  7. Astro-Han commented on Jul 11, 2026

    @Astro-Han
    ContributorAuthor

    Verified against current main. The intermittent processing-indicator race was fixed by #680: send now marks the session as running optimistically before the IPC status round-trip, with rollback on failed sends. The desktop suite passes with 2315 tests and no failures. Closing as completed. The separately observed markdown block-reflow jumpiness remains out of scope and should be tracked independently.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions