fix(ui): clear terminal session activity indicators (#120327)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Ayaan Gazali
2026-08-21 11:52:00 -07:00
committed by GitHub
parent 35175c314b
commit 2213d569ce
8 changed files with 48 additions and 6 deletions
+2 -1
View File
@@ -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({
@@ -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);
@@ -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: {
@@ -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");
+3 -2
View File
@@ -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";
}
+5
View File
@@ -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", () => {
@@ -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" }),
@@ -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") {