Skip to content

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

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 LiveStreamingEntriesmaterialize.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

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