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 (
);