Skip to content

fix(status-bar): keep spinner/timer alive across turn boundaries#122

Merged
yishuiliunian merged 1 commit intomainfrom
fix/status-bar-activity-grace
Apr 19, 2026
Merged

fix(status-bar): keep spinner/timer alive across turn boundaries#122
yishuiliunian merged 1 commit intomainfrom
fix/status-bar-activity-grace

Conversation

@yishuiliunian
Copy link
Copy Markdown
Contributor

Summary

  • TUI 状态栏在 agent 执行中偶发「卡死」:spinner 停、turn_elapsed 冻结,但内容区仍在流式更新,一段时间后自愈。
  • 根因是 AwaitingInputRunning 是两条独立事件,中间存在 ms~数百 ms 的时序窗口;该窗口内 status_icon_and_label 所有 active 分支全部 miss,状态栏渲染成 Idle。retry / context-overflow / stream-error 路径会放大这个抖动。
  • 修复方向:不改事件语义,让显示层对时序抖动更鲁棒——每个活动事件打戳 last_active_at,TUI 在 750ms grace 窗内仍视为活跃;spinner 动画改用全局单调时钟,独立于 turn_elapsed

Changes

session (crates/loopal-session)

  • agent_conversation.rs: 新增 last_active_at 字段 + mark_active() / is_recently_active(grace) API,reset_timer() 同步清戳
  • agent_handler.rs: Stream / ThinkingStream / ThinkingComplete / ToolCall / ToolResult / ToolBatchStart / ToolProgress / RetryError / Started / Running / ServerToolUse / ServerToolResult 在处理时都 conv.mark_active()
  • tests/suite/activity_grace_test.rs(新): 6 条单测覆盖各事件打戳、grace 窗口跨 AwaitingInputRunning 保留、reset_timer 清戳、零-grace 即过期

tui (crates/loopal-tui/src/views/unified_status.rs)

  • is_agent_active 新增 is_recently_active(ACTIVITY_GRACE=750ms) 分支
  • status_icon_and_label 新增 grace 分支 → 保持 "Working" 而非 "Idle"
  • spinner 帧索引改用进程级 animation_clock();显示的 turn 时长仍沿用 turn_elapsed,不改语义

Test plan

  • bazel build //... --config=clippy 通过
  • bazel build //... --config=rustfmt 通过
  • bazel test //... 52/52 全过(含 6 条新 session 测试)
  • CI passes

参考:上一个相关修复 #121 (emit explicit Running agent event on turn start)。本 PR 是该修复的补充——即使 Running 事件按预期发出,跨进程/跨 channel 的时序抖动仍会被用户察觉,此处从显示层兜底。

The status bar would flicker to Idle mid-execution while the agent was
still actively processing. Users saw the spinner animation stop, the
turn timer freeze, and yet the content area kept streaming new output.
It resolved itself after a short moment.

Root cause: AwaitingInput (end of turn N) and Running (start of turn
N+1) are two separate events that hop agent-proc -> stdio -> hub
broadcast -> mpsc bridge -> TUI. Between their arrivals the session
state has status = WaitingForInput, turn_start = None, and all six
"active" branches in status_icon_and_label miss, so the status bar
renders Idle. ToolResult / TokenUsage events continue to arrive in
the same window, which is why the content area keeps updating. The
same transition happens mid-turn on retry / context-overflow /
stream-error paths, amplifying the visibility.

Fix: decouple status-bar liveness from any specific event's observable
status. Every agent activity event now stamps last_active_at on the
conversation; the TUI treats anything within a 750ms grace window as
"working", keeps the spinner rotating via a global monotonic animation
clock, and only falls back to Idle once activity has genuinely stopped.

- session: AgentConversation.mark_active / is_recently_active, stamped
  from Stream, ThinkingStream, ThinkingComplete, ToolCall, ToolResult,
  ToolBatchStart, ToolProgress, RetryError, Started, Running,
  ServerToolUse, ServerToolResult.
- tui: is_agent_active consumes the grace window; spinner uses a
  process-wide clock so frame progression is independent of
  turn_elapsed freezing; displayed turn duration is unchanged.

Tests: 6 new session tests cover stamping on each event type, the
grace-window bridging of AwaitingInput -> Running, and reset_timer
clearing the stamp.
@yishuiliunian yishuiliunian merged commit 9f2a7e4 into main Apr 19, 2026
4 checks passed
@yishuiliunian yishuiliunian deleted the fix/status-bar-activity-grace branch April 19, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant