feat(chat-history): end the transcript with a live agent status trail - #1188
Merged
Conversation
Replaces the PlanningFooter row with a single status line that persists for the whole session: "Agent working for 1h 20m 26s · Agent is typing..." while a round runs, and a resting agent mark between rounds. The row reports the session's own state through the same helpers the sidebar dot uses, so the two surfaces can never disagree, and it stops calling a session "running" once its transcript has been quiet for five minutes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A running round told the user almost nothing, and it told it twice.
PlanningFooterrendered a row saying "Planning next step..." / "Agent istyping...", but
usePlanningIndicatorhides that row for a second after everystore mutation — so during a tool batch, which is exactly when a long round is
least legible, the transcript simply ended. Nothing on screen said how long the
round had been going, and the finished turn's "Agent worked for X" bar only
appeared once the round was already over.
Nothing carried the session's identity either: the transcript stopped at the
last event rather than at the agent that produced it.
Solution
One row now closes the transcript, for the whole session:
AgentStatusTrailreplacesPlanningFooter. The activity phrase becamethis line's second segment rather than a second line, and it keeps its exact
behaviour — same
usePlanningIndicatorcount, same stable variant index, sameagentTyping/compactingselection (pickPlanningVariantwas extracted, notrewritten). The duration is the row's anchor so the row cannot flicker when
the phrase hides itself between beats.
"Agent worked for X" bar, anchored to the same
ChatGroupMeta.startMs, so thelive line and the line it becomes are the same sentence over the same span.
resolveTrailPhaseapplies the sameprecedence
buildSessionMenuItemuses — asking above running above resting —reading the session's own status via
isSessionInProgress/isSessionPendingAsking, with liveness through the existingresolveTailTurnAgentWorking. The trail, the tail-turn collapse phase and thesidebar dot now share one rule.
minutes and the trail drops to resting whatever the status says: nothing
writes a terminal status for an imported transcript when that process exits,
and a dropped
agent:completeleaves the native atom onrunning. Either waythe old behaviour would have been a counter climbing forever next to work that
had stopped.
imported Claude Code / Codex / Cursor agent and cannot know it is idle, so
those sessions report
DataSourceConfig.lastScannedAt— the same timestampthe Runtime scanning panel shows — instead of "Agent is idle".
SessionIdentityIcon(thesidebar row's projection), breathing on a slow
agent-pulsewhile running andparked at that animation's own low point when resting, so switching phases
changes whether it moves, never where it sits.
Live context tokens and the background-task count are deliberately not on the
running line: the composer's context pill and
ActiveProcessesalready carrythem. Running tasks do appear while resting, where "Agent is idle" beside two
live shells would be false.
Potential risks
planning row came and went. That is a persistent ~36px row at the end of every
transcript, including historical and imported ones. Visual judgement call; the
cheapest dial is dropping the resting label to icon-only.
here is confirmed against real pixels — in particular whether
text-2plus along planning phrase reads well at the 800px measure. Everything below is
compiler- and test-level evidence.
PlanningFooteris deleted. Its only consumer was this footer slot(verified by grep), but any out-of-tree import would break.
planning.statusTrailkeys wereauthored for all 13 locales without a native-speaker review.
startedAtMs, not a ticking value, soChatHistoryList'smemois notinvalidated once a second. A 1s clock runs only while running, a 30s clock only
while showing "last refreshed".
their own liveness and suppress the token/asking paths, since they have no
sidebar row to agree with. Not exercised beyond unit coverage.
Verification
Commands run, all on this branch's content:
npx tsc --noEmit --pretty false— 0 errors (in the working checkout).An isolated worktree typecheck at this commit timed out after 10 minutes,
a known stall in this repo; instead I diffed the one file that had drifted
against
origin/developand confirmed by grep that noPlanningFooterreference survives and that the rebased barrel still exports everything its
two importers (
ChatHistoryList,ChatHistoryListTypes) pull from it.npx eslint src/engines/ChatPanel/— clean.npm run check:circular— no circular dependencies across 6407 modules.npx vitest run src/engines/ChatPanel/ src/i18n src/util/session src/store/session src/scaffold/NavigationSidebar— 272 files / 1919 tests passed.Regressions reproduced before fixing, not assumed:
planningIndicatorCount > 0alone fails 2tests in
ChatHistoryListStatusTrail.test.ts; restoring it passes.staleterm fromresolveTrailPhasefails 3 tests acrossagentStatusTrailMath.test.tsanduseAgentStatusTrail.test.ts; restoringpasses.
New coverage: 44 tests over four files — duration formatting, activity and
phase precedence (including that
waiting_for_useris itself an in-progressstatus and must not read as running), the quiet-window timer arming and
re-arming under fake timers, the composed line under a real renderer, and the
footer slot surviving in both the virtualized and static render paths.
Also verified:
.animate-agent-pulse, its@keyframes, and.motion-reduce\:animate-noneare actually emitted by compiling the Tailwindconfig; every locale's
planning.statusTrailkey set matches the keys thecomponent renders, with no orphans.
Not run: the E2E suite, and the app itself.