From 2213d569ceef780299b06fa2460b02a365a8ea8f Mon Sep 17 00:00:00 2001 From: Ayaan Gazali Date: Fri, 21 Aug 2026 11:52:00 -0700 Subject: [PATCH] fix(ui): clear terminal session activity indicators (#120327) Co-authored-by: Peter Steinberger --- ui/src/components/app-sidebar-render.ts | 3 ++- .../app-sidebar-session-catalog-render.ts | 3 ++- ...pp-sidebar-session-navigation-logic.test.ts | 18 ++++++++++++++++++ .../app-sidebar-session-navigation-logic.ts | 2 +- ui/src/components/lobster-pet-contract.ts | 5 +++-- ui/src/components/lobster-pet.test.ts | 5 +++++ .../components/chat-swarm-progress.test.ts | 15 +++++++++++++++ .../chat/components/chat-swarm-progress.ts | 3 ++- 8 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ui/src/components/app-sidebar-render.ts b/ui/src/components/app-sidebar-render.ts index 40d4d03e7542..ff95c1c03b0c 100644 --- a/ui/src/components/app-sidebar-render.ts +++ b/ui/src/components/app-sidebar-render.ts @@ -21,6 +21,7 @@ import { presenceViewerLabel, projectOnlinePresenceViewers, } from "../lib/presence-users.ts"; +import { isSessionRunActive } from "../lib/session-run-state.ts"; import { resolveSessionPreferredFace, sessionNavigationTarget, @@ -178,7 +179,7 @@ export function renderAppSidebarHomeRow(host: AppSidebarRenderHost) { isSessionRouteId(host.activeRouteId) && areUiSessionKeysEquivalent(host.getRouteSessionKey(), mainKey); const hasComposerDraft = host.hasSessionDraft(mainKey); - const running = mainRow?.hasActiveRun === true; + const running = mainRow ? isSessionRunActive(mainRow) : false; const unread = mainRow?.unread === true && !active; // Home keeps its page/attention glyph leading and shares trailing activity with session rows. const homeGlyph = renderSessionGlyph({ diff --git a/ui/src/components/app-sidebar-session-catalog-render.ts b/ui/src/components/app-sidebar-session-catalog-render.ts index 6fe724fa2d61..fefca67400d3 100644 --- a/ui/src/components/app-sidebar-session-catalog-render.ts +++ b/ui/src/components/app-sidebar-session-catalog-render.ts @@ -11,6 +11,7 @@ import { t } from "../i18n/index.ts"; import { formatUiError } from "../lib/format-error.ts"; import { handleContextMenuEvent } from "../lib/keyboard-shortcuts.ts"; import { shouldHandleNavigationClick } from "../lib/navigation-click.ts"; +import { isSessionRunActive } from "../lib/session-run-state.ts"; import type { CatalogSessionKey } from "../lib/sessions/catalog-key.ts"; import { buildCatalogSessionKey } from "../lib/sessions/catalog-key.ts"; import { @@ -140,7 +141,7 @@ export function renderSessionCatalogGroups(params: SessionCatalogGroupsParams) { const row = session.sessionKey ? liveRowsByKey.get(session.sessionKey) : undefined; return row ? [row] : []; }); - const hasActiveRun = liveRows.some((row) => row.hasActiveRun === true); + const hasActiveRun = liveRows.some(isSessionRunActive); const hasUnread = liveRows.some((row) => row.unread === true); const hasBrandIcon = hasProviderBrandIcon(catalog.id); const loadingMore = params.loadingMoreCatalogIds.has(catalog.id); diff --git a/ui/src/components/app-sidebar-session-navigation-logic.test.ts b/ui/src/components/app-sidebar-session-navigation-logic.test.ts index 67ae94acacbe..5520f6270295 100644 --- a/ui/src/components/app-sidebar-session-navigation-logic.test.ts +++ b/ui/src/components/app-sidebar-session-navigation-logic.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it, vi } from "vitest"; import type { GatewaySessionRow, SessionsListResult } from "../api/types.ts"; +import { t } from "../i18n/index.ts"; import { createTestGatewayClient } from "../test-helpers/gateway-client.ts"; import { collectKnownSessionRows, fetchSessionLineage } from "./app-sidebar-child-session-data.ts"; import { buildSidebarSessionNavigationState, compareSidebarSessionRowsByMode, + resolveSidebarAgentChipSubtitle, } from "./app-sidebar-session-navigation-logic.ts"; import { projectSessionTree } from "./app-sidebar-session-tree.ts"; import type { SidebarRecentSession } from "./app-sidebar-session-types.ts"; @@ -177,6 +179,22 @@ describe("sidebar session live-run projection", () => { }, ); + it("stops calling a completed registry-active agent Working", () => { + const session = { + key: "agent:main:main", + kind: "direct", + updatedAt: 1, + hasActiveRun: true, + } satisfies GatewaySessionRow; + + expect(resolveSidebarAgentChipSubtitle({ ...session, status: "done" })).not.toBe( + t("agentChip.working"), + ); + expect(resolveSidebarAgentChipSubtitle({ ...session, status: "running" })).toBe( + t("agentChip.working"), + ); + }); + it("carries active cloud disk pressure into the existing sidebar badge model", () => { const projected = projectSidebarSession({ placement: { diff --git a/ui/src/components/app-sidebar-session-navigation-logic.ts b/ui/src/components/app-sidebar-session-navigation-logic.ts index 40e997668ad3..b22d8beda1ea 100644 --- a/ui/src/components/app-sidebar-session-navigation-logic.ts +++ b/ui/src/components/app-sidebar-session-navigation-logic.ts @@ -557,7 +557,7 @@ export function resolveSidebarAgentResumeKey( } export function resolveSidebarAgentChipSubtitle(latest: SessionRow | null): string { - if (latest?.hasActiveRun) { + if (latest && isSessionRunActive(latest)) { return t("agentChip.working"); } return latest ? resolveSessionDisplayName(latest.key, latest) : t("agentChip.ready"); diff --git a/ui/src/components/lobster-pet-contract.ts b/ui/src/components/lobster-pet-contract.ts index 1dadb734b5a7..cb72e6f75806 100644 --- a/ui/src/components/lobster-pet-contract.ts +++ b/ui/src/components/lobster-pet-contract.ts @@ -1,5 +1,6 @@ import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js"; import { fnv1aUtf16 } from "../lib/fnv1a.ts"; +import { isSessionRunActive } from "../lib/session-run-state.ts"; export type LobsterPetMode = "idle" | "busy" | "offline"; @@ -159,10 +160,10 @@ export function resolveLobsterRunOutcome( export function resolveLobsterPetMode( connected: boolean, - sessions: ReadonlyArray<{ hasActiveRun?: boolean | null }> | null | undefined, + sessions: ReadonlyArray<{ hasActiveRun?: boolean; status?: SessionRunStatus }> | null | undefined, ): LobsterPetMode { if (!connected) { return "offline"; } - return sessions?.some((row) => row.hasActiveRun === true) ? "busy" : "idle"; + return sessions?.some(isSessionRunActive) ? "busy" : "idle"; } diff --git a/ui/src/components/lobster-pet.test.ts b/ui/src/components/lobster-pet.test.ts index 8049bc19b95e..ed1758780b6f 100644 --- a/ui/src/components/lobster-pet.test.ts +++ b/ui/src/components/lobster-pet.test.ts @@ -169,6 +169,11 @@ describe("resolveLobsterPetMode", () => { "busy", ); }); + + it("stops looking busy after a registry-active run reaches a terminal status", () => { + expect(resolveLobsterPetMode(true, [{ status: "done", hasActiveRun: true }])).toBe("idle"); + expect(resolveLobsterPetMode(true, [{ status: "running", hasActiveRun: true }])).toBe("busy"); + }); }); describe("resolveLobsterRunOutcome", () => { diff --git a/ui/src/pages/chat/components/chat-swarm-progress.test.ts b/ui/src/pages/chat/components/chat-swarm-progress.test.ts index 55cfddc7d517..2edbbbb1a138 100644 --- a/ui/src/pages/chat/components/chat-swarm-progress.test.ts +++ b/ui/src/pages/chat/components/chat-swarm-progress.test.ts @@ -105,6 +105,21 @@ describe("chat Swarm progress", () => { expect(container.querySelector("[data-test-id=chat-swarm]")).toBeNull(); }); + it("keeps registry-active terminal workers completed and hides finished groups", () => { + const running = session({ key: "running", status: "running" }); + const completed = session({ key: "completed", status: "done", hasActiveRun: true }); + const failed = session({ key: "failed", status: "failed", hasActiveRun: true }); + const container = renderProgress([running, completed, failed]); + + expect(container.textContent?.replace(/\s+/g, " ")).toContain("1 Running · 1 Done · 1 Failed"); + + render( + renderChatSwarmProgress({ sessionKey: parentSessionKey, sessions: [completed, failed] }), + container, + ); + expect(container.querySelector("[data-test-id=chat-swarm]")).toBeNull(); + }); + it("buckets children by phase, labels the default bucket, and shows the latest log", () => { const container = renderProgress([ session({ key: "unphased", label: "Older child", status: "running" }), diff --git a/ui/src/pages/chat/components/chat-swarm-progress.ts b/ui/src/pages/chat/components/chat-swarm-progress.ts index 722f3ab63160..2d07f2219240 100644 --- a/ui/src/pages/chat/components/chat-swarm-progress.ts +++ b/ui/src/pages/chat/components/chat-swarm-progress.ts @@ -1,6 +1,7 @@ import { html, nothing, type TemplateResult } from "lit"; import type { GatewaySessionRow } from "../../../api/types.ts"; import { t } from "../../../i18n/index.ts"; +import { isSessionRunActive } from "../../../lib/session-run-state.ts"; import { areUiSessionKeysEquivalent } from "../../../lib/sessions/session-key.ts"; type SwarmDotStatus = "queued" | "running" | "done" | "failed"; @@ -53,7 +54,7 @@ function swarmDotStatus(row: GatewaySessionRow): SwarmDotStatus | null { if (row.status === "queued") { return "queued"; } - if (row.status === "running" || row.hasActiveRun === true) { + if (isSessionRunActive(row)) { return "running"; } if (row.status === "done") {