From d6510fa961d4e5f948ff541757fd46a4c25d7cf5 Mon Sep 17 00:00:00 2001 From: RoboClaw Date: Tue, 25 Aug 2026 18:07:10 -0700 Subject: [PATCH] fix(ui): show delegated work on selected threads (#129328) Fixes #129318 Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- .../server.chat.gateway-server-chat.test.ts | 2 +- .../app-sidebar-session-navigation.ts | 3 ++- .../session-attention-presentation.ts | 26 +++++++++++-------- .../components/session-leading-indicator.ts | 11 +++++--- .../session-management.sidebar.e2e.test.ts | 3 +++ 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index 4e569ddf30a8..1a4433724742 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -2674,10 +2674,10 @@ describe("gateway server chat", () => { 8_000, ); blockedReply.resolve(); - await waitForAgentRunOk(runId); const settledEvent = await settledSessionChange.catch(() => { throw new Error("Gateway did not publish settled run ownership after chat.send cleanup"); }); + await waitForAgentRunOk(runId); expectRecordFields(settledEvent.payload, { activeRunIds: [], hasActiveRun: false, diff --git a/ui/src/components/app-sidebar-session-navigation.ts b/ui/src/components/app-sidebar-session-navigation.ts index 55a106a3f4b7..f0e70b298136 100644 --- a/ui/src/components/app-sidebar-session-navigation.ts +++ b/ui/src/components/app-sidebar-session-navigation.ts @@ -231,11 +231,12 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase { pending.push(...session.children); if ( session.childSessionKeys.length > 0 && - this.isSessionChildrenExpanded(session) && + (session.visuallyActive || this.isSessionChildrenExpanded(session)) && !this.sessionData.loadedChildSessionKeys.has(session.key) && !this.sessionData.childSessionErrorsByParent.has(session.key) && !this.sessionData.loadingChildSessionKeys.has(session.key) ) { + // Selected collapsed rows need child liveness so delegated work does not look finished. void this.sessionData.loadChildSessions(session.key); } } diff --git a/ui/src/components/session-attention-presentation.ts b/ui/src/components/session-attention-presentation.ts index 5c0ecd2daa0b..1aa561699464 100644 --- a/ui/src/components/session-attention-presentation.ts +++ b/ui/src/components/session-attention-presentation.ts @@ -50,18 +50,22 @@ export function renderSessionRunSpinner(showTitle = true) { >`; } +export function sessionHasRunningWork(session: SidebarRecentSession): boolean { + return session.hasActiveRun || session.runningChildCount > 0; +} + export function renderSessionState(session: SidebarRecentSession, showTitle = true) { - if (session.hasActiveRun) { - if (session.status === "queued") { - const label = t("sessionsView.statusQueued"); - return html`${icons.hourglass}`; - } + if (session.hasActiveRun && session.status === "queued") { + const label = t("sessionsView.statusQueued"); + return html`${icons.hourglass}`; + } + if (sessionHasRunningWork(session)) { return renderSessionRunSpinner(showTitle); } if (!session.isChild) { diff --git a/ui/src/components/session-leading-indicator.ts b/ui/src/components/session-leading-indicator.ts index cb67965dd157..bbae39e9ce89 100644 --- a/ui/src/components/session-leading-indicator.ts +++ b/ui/src/components/session-leading-indicator.ts @@ -5,6 +5,7 @@ import { icons } from "./icons.ts"; import { renderSessionAttentionIcon, renderSessionState, + sessionHasRunningWork, } from "./session-attention-presentation.ts"; import { renderSessionGlyph, @@ -97,12 +98,14 @@ export function describeSessionTrailingState( session: SidebarRecentSession, pullRequestState: SessionPullRequestIndicatorState, ) { + const runningLabel = + session.hasActiveRun && session.status === "queued" + ? t("sessionsView.statusQueued") + : t("sessionsView.activeRun"); return [ session.forkSource ? t("sessionsView.forkedSession") : "", pullRequestState === "none" ? "" : pullRequestStateLabel(pullRequestState), - session.hasActiveRun - ? t(session.status === "queued" ? "sessionsView.statusQueued" : "sessionsView.activeRun") - : "", + sessionHasRunningWork(session) ? runningLabel : "", session.unread ? t("sessionsView.unread") : "", ] .filter(Boolean) @@ -124,7 +127,7 @@ export function renderSessionLeadingState( trailingIndicator: TemplateResult | typeof nothing; renderedOwnerId?: string; } { - const running = session.hasActiveRun; + const running = sessionHasRunningWork(session); const trailingIndicator = session.isChild ? nothing : renderSessionTrailingState(session, pullRequestState); diff --git a/ui/src/e2e/session-management.sidebar.e2e.test.ts b/ui/src/e2e/session-management.sidebar.e2e.test.ts index 0b196b37bddf..77059f187535 100644 --- a/ui/src/e2e/session-management.sidebar.e2e.test.ts +++ b/ui/src/e2e/session-management.sidebar.e2e.test.ts @@ -166,6 +166,9 @@ suite.define(() => { const parent = page.locator(`[data-session-key="${parentKey}"]`); await parent.waitFor({ state: "visible", timeout: 10_000 }); await expect.poll(() => page.locator(".sidebar-recent-session--child").count()).toBe(0); + await expect + .poll(() => parent.locator(".session-run-spinner").getAttribute("aria-label")) + .toBe("Active run"); await captureUiProof(page, "child-sessions-collapsed.png"); await parent.getByRole("button", { name: "Show 4 child sessions for Plan release" }).click();