diff --git a/infra/runtime-accumulator.mjs b/infra/runtime-accumulator.mjs index c0dc2197..b6d33063 100644 --- a/infra/runtime-accumulator.mjs +++ b/infra/runtime-accumulator.mjs @@ -123,6 +123,10 @@ function normalizeCompletedSession(session = {}) { const sessionKey = String( session.sessionKey || `${taskId || "task"}:${stableId}:${startedAt}:${endedAt}`, ).trim(); + const turnCount = Math.max(0, toFiniteNumber(session.turnCount, 0)); + const turns = Array.isArray(session.turns) + ? session.turns.map((turn) => ({ ...turn })) + : []; return { type: "completed_session", @@ -139,6 +143,8 @@ function normalizeCompletedSession(session = {}) { tokenCount, inputTokens, outputTokens, + turnCount, + turns, costUsd, recordedAt: String(session.recordedAt || new Date().toISOString()), }; @@ -357,6 +363,7 @@ export function getRuntimeStats() { runtimeMs: _state.runtimeMs, totalCostUsd: _state.totalCostUsd, sessionCount: _state.completedSessions.length, + completedSessions: _state.completedSessions.map((entry) => ({ ...entry })), startedAt: _state.startedAt, lastUpdated: _state.lastUpdated, }; diff --git a/infra/session-tracker.mjs b/infra/session-tracker.mjs index 5f61c212..82baf4a1 100644 --- a/infra/session-tracker.mjs +++ b/infra/session-tracker.mjs @@ -395,6 +395,17 @@ function updateTurnTimeline(session, msg) { }; } turn.durationMs = Math.max(0, Number(turn.endedAt || timestampMs) - Number(turn.startedAt || timestampMs)); + const derivedInsights = buildSessionInsights({ + ...session, + insights: null, + messages: Array.isArray(session.messages) ? session.messages : [], + }); + session.insights = { + ...(session.insights && typeof session.insights === "object" ? session.insights : {}), + ...derivedInsights, + turnTimeline: derivedInsights.turnTimeline, + turns: derivedInsights.turns, + }; } /** Debounce interval for disk writes (ms). */ @@ -1391,6 +1402,8 @@ export class SessionTracker { startedAt, endedAt, durationMs: Math.max(0, endedAt - startedAt), + turnCount: session.turnCount || 0, + turns: turns.map((turn) => ({ ...turn })), tokenCount: tokenUsage?.totalTokens || 0, inputTokens: tokenUsage?.inputTokens || 0, outputTokens: tokenUsage?.outputTokens || 0, @@ -1415,6 +1428,7 @@ export class SessionTracker { #extractTrajectoryStep(event, session) { const ts = new Date().toISOString(); + const eventTimestamp = String(event?.timestamp || event?.item?.timestamp || "").trim() || ts; const id = `step-${Date.now()}-${randomToken(6)}`; // String event @@ -1435,13 +1449,13 @@ export class SessionTracker { if (event?.type === "item.started" && event?.item) { const item = event.item; if (item.type === "command_execution") { - return { id, kind: "tool_call", summary: `Ran ${item.command || "unknown"}`, timestamp: ts }; + return { id, kind: "tool_call", summary: `Ran ${item.command || "unknown"}`, timestamp: eventTimestamp }; } if (item.type === "reasoning") { - return { id, kind: "reasoning", summary: item.text || "", timestamp: ts }; + return { id, kind: "reasoning", summary: item.text || "", timestamp: eventTimestamp }; } if (item.type === "function_call" || item.type === "mcp_tool_call") { - return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts }; + return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: eventTimestamp }; } return null; } @@ -1450,10 +1464,10 @@ export class SessionTracker { if (event?.type === "item.completed" && event?.item) { const item = event.item; if (item.type === "reasoning") { - return { id, kind: "reasoning", summary: item.text || "", timestamp: ts }; + return { id, kind: "reasoning", summary: item.text || "", timestamp: eventTimestamp }; } if (item.type === "function_call" || item.type === "mcp_tool_call") { - return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts }; + return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: eventTimestamp }; } if (item.type === "command_execution") { const cmd = item.command || ""; @@ -1461,12 +1475,12 @@ export class SessionTracker { (s) => s.kind === "tool_call" && s.summary === `Ran ${cmd}`, ); if (hasPriorStart) { - return { id, kind: "tool_result", summary: `${cmd} (exit ${item.exit_code ?? "?"})`, timestamp: ts }; + return { id, kind: "tool_result", summary: `${cmd} (exit ${item.exit_code ?? "?"})`, timestamp: eventTimestamp }; } - return { id, kind: "command", summary: cmd, timestamp: ts }; + return { id, kind: "command", summary: cmd, timestamp: eventTimestamp }; } if (item.type === "agent_message") { - return { id, kind: "assistant", summary: item.text || "", timestamp: ts }; + return { id, kind: "assistant", summary: item.text || "", timestamp: eventTimestamp }; } return null; } @@ -1474,7 +1488,7 @@ export class SessionTracker { // Assistant message events if (event?.type === "assistant.message") { const content = event?.data?.content || event?.content || ""; - return { id, kind: "agent_message", summary: content.slice(0, 200), timestamp: ts }; + return { id, kind: "agent_message", summary: content.slice(0, 200), timestamp: eventTimestamp }; } return null; @@ -1790,6 +1804,7 @@ export class SessionTracker { if (!event || !event.type) return null; const ts = new Date().toISOString(); + const eventTimestamp = String(event.timestamp || "").trim() || ts; const toText = (value) => { if (value == null) return ""; if (typeof value === "string") return value; @@ -2049,7 +2064,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "system", content: "Turn completed", - timestamp: ts, + timestamp: eventTimestamp, meta: { lifecycle: "turn_completed" }, }; } @@ -2058,7 +2073,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "system", content: "Session completed", - timestamp: ts, + timestamp: eventTimestamp, meta: { lifecycle: "session_completed" }, }; } @@ -2068,7 +2083,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "error", content: `Turn failed: ${detail}`.slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2076,7 +2091,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "agent_message", content: toText(event.data.content).slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2084,7 +2099,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "agent_message", content: toText(event.data.deltaContent).slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2094,7 +2109,7 @@ ${items.join("\n")}` : "todo updated"; type: "agent_message", content: (typeof event.content === "string" ? event.content : JSON.stringify(event.content)) .slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2102,7 +2117,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "tool_call", content: `${event.name || event.tool || "tool"}(${(event.arguments || event.input || "").slice(0, 500)})`, - timestamp: ts, + timestamp: eventTimestamp, meta: { toolName: event.name || event.tool }, }; } @@ -2111,7 +2126,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "tool_result", content: (event.output || event.result || "").slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2120,7 +2135,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "agent_message", content: event.delta.text.slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2129,7 +2144,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "system", content: `${event.type}${event.delta?.stop_reason ? ` (${event.delta.stop_reason})` : ""}`, - timestamp: ts, + timestamp: eventTimestamp, ...(lifecycle ? { meta: { lifecycle } } : {}), }; } @@ -2139,7 +2154,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "error", content: (event.error?.message || event.message || JSON.stringify(event)).slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, }; } @@ -2148,7 +2163,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "system", content: `Voice session started (provider: ${event.provider || "unknown"}, tier: ${event.tier || "?"})`, - timestamp: ts, + timestamp: eventTimestamp, meta: { voiceEvent: "start", provider: event.provider, tier: event.tier }, }; } @@ -2156,7 +2171,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "system", content: `Voice session ended (duration: ${event.duration || 0}s)`, - timestamp: ts, + timestamp: eventTimestamp, meta: { voiceEvent: "end", duration: event.duration }, }; } @@ -2164,7 +2179,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "user", content: (event.text || event.transcript || "").slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, meta: { voiceEvent: "transcript" }, }; } @@ -2172,7 +2187,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "agent_message", content: (event.text || event.response || "").slice(0, MAX_MESSAGE_CHARS), - timestamp: ts, + timestamp: eventTimestamp, meta: { voiceEvent: "response" }, }; } @@ -2180,7 +2195,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "tool_call", content: `voice:${event.name || "tool"}(${(event.arguments || "").slice(0, 500)})`, - timestamp: ts, + timestamp: eventTimestamp, meta: { voiceEvent: "tool_call", toolName: event.name }, }; } @@ -2188,7 +2203,7 @@ ${items.join("\n")}` : "todo updated"; return { type: "system", content: `Voice delegated to ${event.executor || "agent"}: ${(event.message || "").slice(0, 500)}`, - timestamp: ts, + timestamp: eventTimestamp, meta: { voiceEvent: "delegate", executor: event.executor }, }; } diff --git a/lib/session-insights.mjs b/lib/session-insights.mjs index dac11cb5..835f8773 100644 --- a/lib/session-insights.mjs +++ b/lib/session-insights.mjs @@ -258,15 +258,21 @@ function buildTurnTimeline(messages = []) { const tsMs = toTimestampMs(timestamp); const entry = turns.get(turnIndex) || { turn: turnIndex + 1, + index: turnIndex + 1, turnIndex, startedAt: null, endedAt: null, - durationMs: null, + durationMs: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, + tokenCount: 0, toolCalls: 0, + toolResults: 0, + assistantMessages: 0, + errors: 0, assistantPreview: "", + preview: "", }; if (tsMs !== null) { const startedMs = toTimestampMs(entry.startedAt); @@ -279,14 +285,29 @@ function buildTurnTimeline(messages = []) { if (type === "tool_call" && String(msg?.meta?.lifecycle || "").toLowerCase() !== "started") { entry.toolCalls += 1; } - const usage = normalizeTokenUsageMeta(msg?.meta) || normalizeUsage(msg?.usage) || null; + if (type === "tool_result" || type === "tool_output") { + entry.toolResults += 1; + } + if (type === "error" || type === "stream_error") { + entry.errors += 1; + } + const usage = normalizeUsage(msg?.meta?.usage) || normalizeUsage(msg?.usage) || null; if (usage) { entry.inputTokens += usage.input; entry.outputTokens += usage.output; entry.totalTokens += usage.total; } - if ((role === "assistant" || type === "agent_message" || type === "assistant_message") && !entry.assistantPreview) { - entry.assistantPreview = toText(msg.content).replace(/\s+/g, " ").trim().slice(0, 180); + const content = toText(msg.content).replace(/\s+/g, " ").trim(); + if (role === "assistant" || type === "agent_message" || type === "assistant_message") { + entry.assistantMessages += 1; + if (!entry.assistantPreview) { + entry.assistantPreview = content.slice(0, 180); + } + if (!entry.preview) { + entry.preview = content.slice(0, 140); + } + } else if ((type === "error" || type === "stream_error") && !entry.preview) { + entry.preview = content.slice(0, 140); } turns.set(turnIndex, entry); } @@ -297,7 +318,14 @@ function buildTurnTimeline(messages = []) { const endedMs = toTimestampMs(entry.endedAt); return { ...entry, - durationMs: startedMs !== null && endedMs !== null ? Math.max(0, endedMs - startedMs) : null, + durationMs: startedMs !== null && endedMs !== null ? Math.max(0, endedMs - startedMs) : 0, + tokenCount: entry.totalTokens, + tokenUsage: { + inputTokens: entry.inputTokens, + outputTokens: entry.outputTokens, + totalTokens: entry.totalTokens, + }, + preview: entry.preview || entry.assistantPreview || `Turn ${entry.turn}`, }; }); } @@ -426,6 +454,7 @@ export function buildSessionInsights(fullSession = null) { }; } + const turnTimeline = buildTurnTimeline(messages); const derived = { totals: { messages: messages.length, @@ -457,7 +486,8 @@ export function buildSessionInsights(fullSession = null) { contextWindow, contextBreakdown, tokenUsage, - turnTimeline: buildTurnTimeline(messages), + turnTimeline, + turns: turnTimeline, activityDiff: { files: edited.map((entry) => ({ path: entry.path, @@ -486,10 +516,15 @@ export function buildSessionInsights(fullSession = null) { ? persisted.contextBreakdown : derived.contextBreakdown, tokenUsage: persisted.tokenUsage || derived.tokenUsage, + turnTimeline: Array.isArray(persisted.turnTimeline) + ? persisted.turnTimeline + : (Array.isArray(persisted.turns) ? persisted.turns : derived.turnTimeline), + turns: Array.isArray(persisted.turns) + ? persisted.turns + : (Array.isArray(persisted.turnTimeline) ? persisted.turnTimeline : derived.turns), activityDiff: persisted.activityDiff || derived.activityDiff, generatedAt: persisted.generatedAt || derived.generatedAt, }; } - diff --git a/server/ui-server.mjs b/server/ui-server.mjs index c5aa0065..76ca47e5 100644 --- a/server/ui-server.mjs +++ b/server/ui-server.mjs @@ -13436,6 +13436,25 @@ async function readReplayableAgentRun(attemptId) { const first = events[0] || null; const last = events[events.length - 1] || null; const overview = buildReplayOverview(events); + const turns = []; + let turnIndex = 0; + for (const event of events) { + if (event?.type !== "usage") continue; + turnIndex += 1; + const usage = event?.data?.usage && typeof event.data.usage === "object" ? event.data.usage : {}; + const tokenCount = Number(usage.total_tokens ?? usage.totalTokens ?? event?.data?.tokenCount ?? 0) || 0; + const inputTokens = Number(usage.input_tokens ?? usage.inputTokens ?? 0) || 0; + const outputTokens = Number(usage.output_tokens ?? usage.outputTokens ?? 0) || 0; + const durationMs = Number(event?.data?.duration_ms ?? event?.data?.durationMs ?? 0) || 0; + turns.push({ + index: turnIndex, + timestamp: event.timestamp || null, + tokenCount, + inputTokens, + outputTokens, + durationMs, + }); + } return { attemptId: normalizedAttemptId, taskId: first?.taskId || last?.taskId || null, @@ -13448,6 +13467,7 @@ async function readReplayableAgentRun(attemptId) { : "in_progress", shortSteps: overview.shortSteps, totals: overview.totals, + turns, events, }; } diff --git a/site/ui/tabs/agents.js b/site/ui/tabs/agents.js index 64756f9f..2b095a05 100644 --- a/site/ui/tabs/agents.js +++ b/site/ui/tabs/agents.js @@ -1948,6 +1948,7 @@ export function AgentsTab() { }} >
+
Turns ${s.turnCount || 0}
<${StatusDot} status=${s.status || "idle"} /> @@ -1957,6 +1958,7 @@ export function AgentsTab() { ${s.id || "?"} ${s.taskId ? ` · ${s.taskId}` : ""} ${s.branch ? ` · ${s.branch}` : ""} + · Turns ${s.turnCount || 0}
${`Turns ${Number(s.turnCount || 0)}`} diff --git a/tests/runtime-accumulator.test.mjs b/tests/runtime-accumulator.test.mjs index e27fc3f6..0e461433 100644 --- a/tests/runtime-accumulator.test.mjs +++ b/tests/runtime-accumulator.test.mjs @@ -156,4 +156,55 @@ describe("runtime-accumulator", () => { rmSync(cacheDir, { recursive: true, force: true }); } }); + + it("preserves turn count and timeline details on completed session records", () => { + const cacheDir = mkdtempSync(join(tmpdir(), "bosun-runtime-accumulator-turns-")); + const taskId = "task-session-turns"; + + try { + _resetRuntimeAccumulatorForTests({ cacheDir }); + + const record = addCompletedSession({ + id: `${taskId}-session-1`, + sessionId: `${taskId}-session-1`, + sessionKey: `${taskId}:session-1`, + taskId, + taskTitle: "Turn persistence test", + startedAt: 1_000, + endedAt: 6_000, + durationMs: 5_000, + tokenCount: 180, + inputTokens: 120, + outputTokens: 60, + turnCount: 2, + turns: [ + { turnIndex: 0, durationMs: 2_000, totalTokens: 75, status: "completed" }, + { turnIndex: 1, durationMs: 3_000, totalTokens: 105, status: "completed" }, + ], + status: "completed", + }); + + expect(record).toEqual(expect.objectContaining({ + turnCount: 2, + turns: [ + expect.objectContaining({ turnIndex: 0, totalTokens: 75 }), + expect.objectContaining({ turnIndex: 1, totalTokens: 105 }), + ], + })); + + _resetRuntimeAccumulatorForTests({ cacheDir }); + const restoredStats = getRuntimeStats(); + expect(restoredStats.completedSessions[0]).toEqual(expect.objectContaining({ + taskId, + turnCount: 2, + turns: [ + expect.objectContaining({ turnIndex: 0, totalTokens: 75 }), + expect.objectContaining({ turnIndex: 1, totalTokens: 105 }), + ], + })); + } finally { + _resetRuntimeAccumulatorForTests(); + rmSync(cacheDir, { recursive: true, force: true }); + } + }); }); diff --git a/tests/session-tracker.test.mjs b/tests/session-tracker.test.mjs index 35c08651..23e9e1ad 100644 --- a/tests/session-tracker.test.mjs +++ b/tests/session-tracker.test.mjs @@ -269,6 +269,47 @@ describe("session-tracker", () => { expect(messages[0].type).toBe("agent_message"); expect(messages[0].content).toContain("Done. Changes are applied."); }); + it("builds per-turn token and duration timeline insights", () => { + tracker.startSession("task-1", "Timeline Test"); + tracker.recordEvent("task-1", { + role: "user", + content: "Check the failing tests", + timestamp: "2026-03-26T10:00:00.000Z", + }); + tracker.recordEvent("task-1", { + type: "tool_call", + content: "read_file(tests/app.test.mjs)", + timestamp: "2026-03-26T10:00:01.000Z", + meta: { toolName: "read_file" }, + }); + tracker.recordEvent("task-1", { + role: "assistant", + content: "I found the flaky assertion.", + timestamp: "2026-03-26T10:00:04.000Z", + meta: { + usage: { input_tokens: 120, output_tokens: 30, total_tokens: 150 }, + }, + }); + tracker.recordEvent("task-1", { + type: "system", + content: "Turn completed", + timestamp: "2026-03-26T10:00:05.000Z", + meta: { lifecycle: "turn_completed" }, + }); + + const session = tracker.getSession("task-1"); + expect(session.turnCount).toBeGreaterThanOrEqual(1); + expect(session.insights?.turns).toEqual([ + expect.objectContaining({ + index: 1, + durationMs: 4000, + inputTokens: 120, + outputTokens: 30, + tokenCount: 150, + toolCalls: 1, + }), + ]); + }); it("ignores low-signal stream noise for activity tracking", () => { tracker.startSession("task-1", "Test"); @@ -980,3 +1021,6 @@ describe("session-tracker", () => { }); }); }); + + + diff --git a/tests/ui-server.test.mjs b/tests/ui-server.test.mjs index b847e092..2b11e3a7 100644 --- a/tests/ui-server.test.mjs +++ b/tests/ui-server.test.mjs @@ -4900,6 +4900,18 @@ describe("ui-server mini app", () => { { timestamp: new Date(now.getTime() - 40_000).toISOString(), attempt_id: attemptId, + event_type: "usage", + taskId: "task-123", + task_title: "Replayable task", + executor: "codex", + data: { + usage: { input_tokens: 120, output_tokens: 45, total_tokens: 165 }, + duration_ms: 3200, + }, + }, + { + timestamp: new Date(now.getTime() - 35_000).toISOString(), + attempt_id: attemptId, event_type: "tool_result", taskId: "task-123", task_title: "Replayable task", @@ -4939,8 +4951,9 @@ describe("ui-server mini app", () => { taskTitle: "Replayable task", executor: "codex", status: "failed", - eventCount: 4, + eventCount: 5, })); + expect(payload.data[0].totals.usageEvents).toBe(1); expect(payload.data[0].shortSteps).toEqual(expect.arrayContaining([ expect.stringContaining("Started codex run"), expect.stringContaining("Called web.search"), @@ -5034,6 +5047,18 @@ describe("ui-server mini app", () => { { timestamp: new Date(now.getTime() - 30_000).toISOString(), attempt_id: attemptId, + event_type: "usage", + taskId: "task-456", + task_title: "Replay detail task", + executor: "claude", + data: { + usage: { input_tokens: 80, output_tokens: 20, total_tokens: 100 }, + duration_ms: 1800, + }, + }, + { + timestamp: new Date(now.getTime() - 20_000).toISOString(), + attempt_id: attemptId, event_type: "agent_output", taskId: "task-456", task_title: "Replay detail task", @@ -5079,7 +5104,19 @@ describe("ui-server mini app", () => { expect.stringContaining("Error: Context window exhausted"), ])); expect(payload.data.totals.errors).toBe(1); + expect(payload.data.turns).toEqual([ + expect.objectContaining({ + index: 1, + tokenCount: 100, + inputTokens: 80, + outputTokens: 20, + durationMs: 1800, + }), + ]); expect(payload.data.events[1]).toEqual(expect.objectContaining({ + type: "usage", + })); + expect(payload.data.events[2]).toEqual(expect.objectContaining({ type: "agent_output", summary: "Investigated failure and prepared patch.", })); @@ -5089,6 +5126,38 @@ describe("ui-server mini app", () => { rmSync(isolatedRepoRoot, { recursive: true, force: true }); } }); + it("includes turn counts in live sessions snapshots", async () => { + const mod = await import("../infra/tui-bridge.mjs"); + const payload = mod.buildSessionsUpdatePayload([ + { + id: "session-live-1", + taskId: "task-live-1", + title: "Live task", + type: "task", + status: "active", + workspaceId: null, + workspaceDir: null, + branch: "feature/live-turns", + turnCount: 3, + createdAt: "2026-03-21T00:00:00.000Z", + lastActiveAt: "2026-03-21T00:01:00.000Z", + idleMs: 0, + elapsedMs: 60000, + recommendation: "continue", + preview: "Working", + lastMessage: "Latest output", + insights: {}, + }, + ]); + + expect(payload).toEqual([ + expect.objectContaining({ + id: "session-live-1", + taskId: "task-live-1", + turnCount: 3, + }), + ]); + }); it("serves benchmark snapshots and persists benchmark mode for the active workspace", async () => { process.env.TELEGRAM_UI_TUNNEL = "disabled"; const tmpDir = mkdtempSync(join(tmpdir(), "bosun-ui-benchmark-mode-")); diff --git a/ui/tabs/agents.js b/ui/tabs/agents.js index 911ee7de..00db3914 100644 --- a/ui/tabs/agents.js +++ b/ui/tabs/agents.js @@ -474,6 +474,7 @@ function WorkspaceViewer({ agent, onClose }) { .catch(() => { if (active) setLogText("(failed to load logs)"); }); }; + const fetchContext = () => { apiFetch(`/api/agent-context?query=${encodeURIComponent(query)}`, { _silent: true }) .then((res) => { if (active) setContextData(res.data ?? res ?? null); }) @@ -1949,6 +1950,7 @@ export function AgentsTab() { }} >
+
Turns ${s.turnCount || 0}
<${StatusDot} status=${s.status || "idle"} /> @@ -1958,6 +1960,7 @@ export function AgentsTab() { ${s.id || "?"} ${s.taskId ? ` · ${s.taskId}` : ""} ${s.branch ? ` · ${s.branch}` : ""} + · Turns ${s.turnCount || 0}
${`Turns ${Number(s.turnCount || 0)}`} @@ -2041,6 +2044,7 @@ function ContextViewer({ query, sessionId = "", taskId = "", branch = "" }) { setError(null); setCtx(null); fetchContext(); + intervalRef.current = setInterval(fetchContext, 10000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [fetchContext]); @@ -2082,7 +2086,7 @@ function ContextViewer({ query, sessionId = "", taskId = "", branch = "" }) { }; const copyContext = () => { - if (!ctx?.context) return; + const c = ctx.context; const ab = parseAheadBehind(c.gitAheadBehind); const commits = parseCommits(c.gitLogDetailed);