From f0414101c94f2938256b36fb53b2993d90705d97 Mon Sep 17 00:00:00 2001 From: badcuban <108198679+badcuban@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:07:21 -0400 Subject: [PATCH] feat(web): the + menu marks a live Agents surface with the pulsing dot While subagents run, the Agents tab and the sidebar panel button draw a pulsing dot, but the + menu that opens surfaces still showed the plain icon next to Agents. The menu now reads the same live-tab list the strip already receives and swaps the icon for the pulsing dot in a live row, sat in the icon's slot so labels stay aligned. Covered by a browser test that opens the menu with agents live. --- .../chat/RightPanelTabStrip.browser.tsx | 39 +++++++++++++++++++ .../components/chat/RightPanelTabStrip.tsx | 26 ++++++++++--- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/chat/RightPanelTabStrip.browser.tsx b/apps/web/src/components/chat/RightPanelTabStrip.browser.tsx index 27c435c17..80bf335d9 100644 --- a/apps/web/src/components/chat/RightPanelTabStrip.browser.tsx +++ b/apps/web/src/components/chat/RightPanelTabStrip.browser.tsx @@ -1,6 +1,7 @@ import "../../index.css"; import { afterEach, describe, expect, it, vi } from "vite-plus/test"; +import { page } from "vite-plus/test/browser"; import { render } from "vitest-browser-react"; import { RightPanelTabStrip } from "./RightPanelTabStrip"; @@ -163,3 +164,41 @@ describe("RightPanelTabStrip drag reorder", () => { } }); }); + +describe("RightPanelTabStrip + menu", () => { + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("draws a live tab's row with the live node instead of its icon", async () => { + const mounted = await render( +
+ +
, + ); + + try { + await page.getByRole("button", { name: "Open panel" }).click(); + + const agents = page.getByRole("menuitem", { name: "Agents" }); + await expect.element(agents).toHaveAttribute("data-right-panel-menu-tab-live", "true"); + expect(agents.element().querySelector(".thread-halo")).not.toBeNull(); + expect(agents.element().querySelector("svg")).toBeNull(); + + const diff = page.getByRole("menuitem", { name: "Diff" }); + await expect.element(diff).not.toHaveAttribute("data-right-panel-menu-tab-live"); + expect(diff.element().querySelector(".thread-halo")).toBeNull(); + expect(diff.element().querySelector("svg")).not.toBeNull(); + } finally { + await mounted.unmount(); + } + }); +}); diff --git a/apps/web/src/components/chat/RightPanelTabStrip.tsx b/apps/web/src/components/chat/RightPanelTabStrip.tsx index 5cb4143b7..0fa520ed3 100644 --- a/apps/web/src/components/chat/RightPanelTabStrip.tsx +++ b/apps/web/src/components/chat/RightPanelTabStrip.tsx @@ -9,7 +9,8 @@ * because there is a fixed set of them and each can only be open once. A * surface already in the strip stays listed and dimmed: choosing it focuses the * tab it already has. Empty surfaces stay clickable too, but use the same quiet - * treatment as their rows in the panel launcher. + * treatment as their rows in the panel launcher. A surface with live work (agents + * still running) swaps its icon for the same pulsing node its tab would show. * * The row shares its width with the Windows window-controls overlay, which * reserves ~150px of it, so the strip measures itself and drops every label @@ -853,6 +854,7 @@ export const RightPanelTabStrip = memo(function RightPanelTabStrip({ const surface = RIGHT_PANEL_SURFACES[tab]; const alreadyOpen = openTabs.includes(tab); const empty = surfaceStates?.[tab]?.empty ?? false; + const live = liveTabs?.includes(tab) ?? false; const MenuIcon = RIGHT_PANEL_TAB_ICONS[tab]; return ( onSelectTab(tab)} > - );