Skip to content

Commit b88e287

Browse files
Merge pull request #320 from corbitsdev/cl-5346-session-clock-pr
Replace session wall clock with completed sub-agent durations
2 parents 9cd7a1a + 26e01a0 commit b88e287

10 files changed

Lines changed: 95 additions & 112 deletions

src/tui/app.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useState, useMemo, useEffect, useRef, type ReactNode } from "react";
77
import { useAgentStream } from "./use-stream.js";
88
import { Header } from "./components/header.js";
99
import { EventLog, TEXT_GUTTER, resolveViewportExpandIds } from "./components/event-log.js";
10-
import { StatusBar } from "./components/status-bar.js";
10+
import { StatusBar, formatCompletedAgentsLabel } from "./components/status-bar.js";
1111
import { useGitBranch } from "./git-branch.js";
1212
import { formatStatusBarSegments } from "../cost/cost-summary.js";
1313
import { OnboardingAnimation } from "./components/onboarding-animation.js";
@@ -42,7 +42,6 @@ import type { SubAgentProvider, SubAgentSessionStore } from "../subagent/index.j
4242
import { useSpinner } from "./hooks/use-spinner.js";
4343
import { chromeDividerLine } from "./chrome-zones.js";
4444
import { useQuotaRetry } from "./hooks/use-quota-retry.js";
45-
import { useSessionClock } from "./hooks/use-session-clock.js";
4645
import { useRevolvingVerb } from "./hooks/use-revolving-verb.js";
4746
import { color } from "./theme.js";
4847
import { useTerminalSize } from "./hooks/use-terminal-size.js";
@@ -179,9 +178,6 @@ export type AppProps = {
179178
// Emits "scrollUp"/"scrollDown" for mouse-wheel events, which are stripped
180179
// from stdin before they reach useInput (see createFilteredStdin).
181180
mouseEvents?: EventEmitter;
182-
// Wall-clock ms timestamp the session started. Drives the whole-session timer
183-
// in the status bar; reset on /new.
184-
sessionStartedAt?: number;
185181
// Inspectable child sessions for the Agents strip and enter-session UI.
186182
subAgentSessions?: SubAgentSessionStore;
187183
/** Goal mode operator surface. */
@@ -267,7 +263,6 @@ export function App({
267263
globallyOnboarded = false,
268264
globalOnboardingPath,
269265
mouseEvents,
270-
sessionStartedAt: sessionStartedAtProp,
271266
subAgentSessions,
272267
goalApi,
273268
telemetryNotice,
@@ -592,6 +587,8 @@ export function App({
592587
providerCatalog,
593588
extraChromeRows,
594589
});
590+
591+
const completedAgentsLabel = formatCompletedAgentsLabel(subAgentSessions?.list() ?? []);
595592
const { leftWidth, visibleRows, effectiveOverlayRows, permissionsOverlayRows } = layout;
596593
// Text wraps and renders inside the gutter so prose never touches the edges.
597594
const contentWidth = Math.max(8, leftWidth - TEXT_GUTTER * 2);
@@ -661,8 +658,6 @@ export function App({
661658
const copyTargetList = copyModeOpen ? copyTargetsRef.current : [];
662659

663660
const [, forceRender] = useState(0);
664-
// Whole-session timer for the status bar. Held in state so /new can zero it.
665-
const [sessionStartedAt, setSessionStartedAt] = useState(sessionStartedAtProp ?? Date.now());
666661

667662
const {
668663
sendMessage,
@@ -703,7 +698,6 @@ export function App({
703698
promptXaiRelogin,
704699
setExpandedTools,
705700
setInputValue,
706-
setSessionStartedAt,
707701
setEnteredSessionId,
708702
setAgentsNavOpen,
709703
setAgentsNavIndex,
@@ -767,7 +761,6 @@ export function App({
767761
streamingType: state.streamingType,
768762
});
769763

770-
const sessionElapsedMs = useSessionClock(sessionStartedAt);
771764
// Persistent status bar segment: refreshes on an interval, never
772765
// blocks render on the git process.
773766
const gitBranch = useGitBranch(cwd);
@@ -1347,7 +1340,7 @@ export function App({
13471340
)}
13481341
<Box marginTop={1}>
13491342
<StatusBar
1350-
sessionElapsedMs={sessionElapsedMs}
1343+
{...(completedAgentsLabel !== undefined ? { completedAgentsLabel } : {})}
13511344
mcpCount={mcpStatus.connected.length}
13521345
model={model}
13531346
cwd={cwd}

src/tui/components/agents-strip.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Task } from "../../agent/tasks.js";
44
import type { SubAgentSession, SubAgentSessionStatus } from "../../subagent/session-store.js";
55
import { color } from "../theme.js";
66
import { describeToolCall } from "../tool-formatter.js";
7+
import { formatElapsed } from "./in-flight-indicator.js";
78

89
export type AgentsStripProps = {
910
sessions: readonly SubAgentSession[];
@@ -314,6 +315,13 @@ function currentToolArguments(session: SubAgentSession): string {
314315
}
315316

316317
export function formatSessionLabel(session: SubAgentSession): string {
318+
// Only finished workers get a duration suffix — matches status-bar policy
319+
// (completed sub-agent times, not a live tick for in-flight sessions).
320+
const duration =
321+
session.finishedAt !== undefined && session.finishedAt >= session.startedAt
322+
? formatElapsed(session.finishedAt - session.startedAt)
323+
: undefined;
324+
const durationSuffix = duration !== undefined ? ` · ${duration}` : "";
317325
if (session.status === "running" && session.currentToolName !== null) {
318326
const args = currentToolArguments(session);
319327
const { summary, isShell } = describeToolCall(session.currentToolName, args);
@@ -325,13 +333,13 @@ export function formatSessionLabel(session: SubAgentSession): string {
325333
? `${session.currentToolName} ${summary}`
326334
: session.currentToolName;
327335
const tool = ` — ${preview}`;
328-
return `${session.agentId}: ${session.description}${tool}`;
336+
return `${session.agentId}: ${session.description}${tool}${durationSuffix}`;
329337
}
330338
const tool =
331339
session.toolNames.length > 0 && session.status !== "running"
332340
? ` · ${session.toolNames.length} tool${session.toolNames.length === 1 ? "" : "s"}`
333341
: "";
334-
return `${session.agentId}: ${session.description}${tool}`;
342+
return `${session.agentId}: ${session.description}${tool}${durationSuffix}`;
335343
}
336344

337345
function summaryCounts(sessions: readonly SubAgentSession[]): string {

src/tui/components/status-bar.tsx

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { color, type SemanticRole } from "../theme.js";
99
import { PRODUCT_NAME } from "../../branding.js";
1010

1111
export type StatusBarProps = {
12-
// Whole-session elapsed time, always counting (not per-turn).
13-
sessionElapsedMs: number;
12+
// Label for completed sub-agent timing (e.g. "agents 2m 14s"). Replaces the
13+
// whole-session wall clock which was noise for long sessions.
14+
completedAgentsLabel?: string;
1415
mcpCount: number;
1516
// Pre-formatted by src/cost/cost-summary.ts; omitted entirely when cost
1617
// should stay hidden (free model, coding plan) rather than shown as $0.
@@ -76,7 +77,7 @@ const MIN_TRUNCATED_CWD = 5;
7677

7778
export type StatusBarLayoutArgs = {
7879
columns: number;
79-
timerText: string;
80+
agentsText?: string;
8081
mcpText?: string;
8182
model?: string;
8283
// Already home-abbreviated.
@@ -108,7 +109,7 @@ function joinModelCwdBranch(model?: string, cwd?: string, gitBranch?: string): s
108109
}
109110

110111
// Decides which low-priority segments fit in the terminal width. Priority
111-
// (highest to lowest, dropped first when narrow): brand+timer > MCP >
112+
// (highest to lowest, dropped first when narrow): brand+agents > MCP >
112113
// model/cwd/branch > cost > context. Only the cwd part is truncated — model
113114
// and branch names stay intact; if the cwd cannot absorb the overflow the
114115
// whole model/cwd/branch segment is dropped.
@@ -121,7 +122,7 @@ export function planStatusBarLayout(args: StatusBarLayoutArgs): StatusBarLayout
121122
const overflow = () =>
122123
usedColumns([
123124
BRAND,
124-
args.timerText,
125+
args.agentsText,
125126
segment,
126127
showCost ? args.costLabel : undefined,
127128
showContext ? args.contextLabel : undefined,
@@ -147,29 +148,20 @@ export function planStatusBarLayout(args: StatusBarLayoutArgs): StatusBarLayout
147148
};
148149
}
149150

150-
export function formatElapsed(elapsedMs: number): string {
151-
const totalSeconds = Math.floor(elapsedMs / 1000);
152-
if (totalSeconds < 60) return `${totalSeconds}s`;
151+
export { formatElapsed } from "./in-flight-indicator.js";
152+
import { formatElapsed } from "./in-flight-indicator.js";
153153

154-
const seconds = totalSeconds % 60;
155-
const totalMinutes = Math.floor(totalSeconds / 60);
156-
if (totalMinutes < 60) return `${totalMinutes}m ${seconds}s`;
157-
158-
const minutes = totalMinutes % 60;
159-
const hours = Math.floor(totalMinutes / 60);
160-
return `${hours}h ${minutes}m ${seconds}s`;
161-
}
162-
163-
// Slim footer: brand anchors the bottom-left with the session timer beside it;
164-
// MCP health sits on the right. The per-turn timer lives on the in-flight
165-
// indicator above the prompt box, not here.
154+
// Slim footer: brand anchors the bottom-left; completed sub-agent timing sits
155+
// beside it when any worker has finished this session. MCP health sits on the
156+
// right. The per-turn timer lives on the in-flight indicator above the prompt
157+
// box, not here.
166158
//
167159
// Segment priority (highest to lowest, dropped first when the terminal is
168-
// narrow): brand+timer > model/cwd/branch > cost/context. cwd is truncated
160+
// narrow): brand+agents > model/cwd/branch > cost/context. cwd is truncated
169161
// with a middle ellipsis before the model/cwd/branch segment is dropped
170162
// entirely.
171163
export function StatusBar({
172-
sessionElapsedMs,
164+
completedAgentsLabel,
173165
mcpCount,
174166
costLabel,
175167
contextLabel,
@@ -179,11 +171,14 @@ export function StatusBar({
179171
gitBranch,
180172
columns,
181173
}: StatusBarProps): ReactNode {
182-
const timerText = formatElapsed(sessionElapsedMs);
174+
const agentsText =
175+
completedAgentsLabel !== undefined && completedAgentsLabel.length > 0
176+
? completedAgentsLabel
177+
: undefined;
183178
const mcpText = mcpCount > 0 ? `MCP ✓ ${mcpCount}` : undefined;
184179
const { modelCwdBranchText, showCost, showContext } = planStatusBarLayout({
185180
columns: columns ?? 120,
186-
timerText,
181+
...(agentsText !== undefined ? { agentsText } : {}),
187182
...(mcpText !== undefined ? { mcpText } : {}),
188183
...(model !== undefined ? { model } : {}),
189184
...(cwd !== undefined ? { cwd: abbreviateHome(cwd) } : {}),
@@ -199,7 +194,9 @@ export function StatusBar({
199194
return (
200195
<Box flexDirection="row" paddingX={1} gap={1} overflow="hidden">
201196
<Text bold color={color("muted")} dimColor wrap="truncate-end">{BRAND}</Text>
202-
<Text color={color("muted")} dimColor>{timerText}</Text>
197+
{agentsText !== undefined && (
198+
<Text color={color("muted")} dimColor>{agentsText}</Text>
199+
)}
203200
{modelCwdBranchText !== undefined && (
204201
<Text color={color("muted")} dimColor wrap="truncate-end">{modelCwdBranchText}</Text>
205202
)}
@@ -216,3 +213,19 @@ export function StatusBar({
216213
</Box>
217214
);
218215
}
216+
217+
/** Sum of finished agent wall times (not multi-agent phase wall clock) as a compact status-bar label. */
218+
export function formatCompletedAgentsLabel(
219+
sessions: ReadonlyArray<{ status: string; startedAt: number; finishedAt?: number }>,
220+
): string | undefined {
221+
let totalMs = 0;
222+
let count = 0;
223+
for (const s of sessions) {
224+
if (s.status !== "done" && s.status !== "failed" && s.status !== "cancelled") continue;
225+
if (s.finishedAt === undefined || s.finishedAt < s.startedAt) continue;
226+
totalMs += s.finishedAt - s.startedAt;
227+
count += 1;
228+
}
229+
if (count === 0) return undefined;
230+
return `agents ${formatElapsed(totalMs)}`;
231+
}

src/tui/hooks/use-message-pipeline.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export type UseMessagePipelineArgs = {
4444
promptXaiRelogin: (name: string) => void;
4545
setExpandedTools: Dispatch<SetStateAction<ReadonlySet<string>>>;
4646
setInputValue: Dispatch<SetStateAction<string>>;
47-
setSessionStartedAt: Dispatch<SetStateAction<number>>;
4847
setEnteredSessionId: Dispatch<SetStateAction<string | null>>;
4948
setAgentsNavOpen: Dispatch<SetStateAction<boolean>>;
5049
setAgentsNavIndex: Dispatch<SetStateAction<number>>;
@@ -98,7 +97,6 @@ export function useMessagePipeline({
9897
promptXaiRelogin,
9998
setExpandedTools,
10099
setInputValue,
101-
setSessionStartedAt,
102100
setEnteredSessionId,
103101
setAgentsNavOpen,
104102
setAgentsNavIndex,
@@ -226,7 +224,6 @@ export function useMessagePipeline({
226224
lastSentMessageRef.current = "";
227225
quotaAutoRetryFiredRef.current = true;
228226
setInputValue("");
229-
setSessionStartedAt(Date.now());
230227
setEnteredSessionId(null);
231228
setAgentsNavOpen(false);
232229
setAgentsNavIndex(0);

src/tui/hooks/use-session-clock.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/tui/runner.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1466,7 +1466,6 @@ export async function runTUI(initialConfig: Config): Promise<number> {
14661466
activePlugins={activePlugins}
14671467
initialWorkflowStatus={workflowController.status()}
14681468
mouseEvents={mouseEvents}
1469-
sessionStartedAt={startedAt}
14701469
subAgentSessions={subAgentSessions}
14711470
goalApi={{
14721471
get: () => goalGovernor.get(),

tests/unit/tui/agents-strip.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,9 @@ describe("formatSessionLabel", () => {
258258
status: "done",
259259
currentToolName: null,
260260
toolNames: ["grep", "read_file"],
261+
finishedAt: 5_000,
261262
});
262-
expect(formatSessionLabel(session)).toBe("researcher: researching things · 2 tools");
263+
expect(formatSessionLabel(session)).toBe("researcher: researching things · 2 tools · 5s");
263264
});
264265

265266
test("shell tool preview leads with the command, not a redundant tool name", () => {
@@ -269,6 +270,15 @@ describe("formatSessionLabel", () => {
269270
const session = baseSession({ currentToolName: "run_shell", entries });
270271
expect(formatSessionLabel(session)).toBe("researcher: researching things — bun test");
271272
});
273+
274+
test("appends finished duration when finishedAt is set", () => {
275+
const session = baseSession({
276+
status: "done",
277+
currentToolName: null,
278+
finishedAt: 65_000,
279+
});
280+
expect(formatSessionLabel(session)).toBe("researcher: researching things · 1m 5s");
281+
});
272282
});
273283

274284
describe("agentsStripRowColor", () => {

tests/unit/tui/chrome-zone-budgets.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test("status budget matches the rows StatusBar paints inside App's marginTop wra
3535
// App wraps StatusBar in <Box marginTop={1}>; mirror that wrapper here.
3636
const { lastFrame } = render(
3737
<Box marginTop={1}>
38-
<StatusBar sessionElapsedMs={0} mcpCount={0} />
38+
<StatusBar mcpCount={0} />
3939
</Box>,
4040
);
4141
expect(frameRows(lastFrame())).toBe(CHROME_ZONE_ROWS.status);

0 commit comments

Comments
 (0)