fix(dsh-plugin): show observation outages on the collapsed capsule

The expanded header already replaced a stale "s1 · clicking" line with
"reconnecting…", but the collapsed capsule and both status dots still
read the last session as live. Collapse is the form users leave on the
page, so an outage there looked like an in-progress action.

- treat reconnecting as its own chrome state so the header and capsule
  dots leave the active color
- keep the capsule's session count and swap the action timer for
  "reconnecting…" until the feed delivers a frame again
This commit is contained in:
drakezhang
2026-09-27 00:08:24 +08:00
parent d102adcbd9
commit 8f485ab845
2 changed files with 49 additions and 10 deletions
@@ -239,14 +239,16 @@ function StopSessionAction(props: {
);
}
type ChromeState = "active" | "idle" | "error" | "reconnecting";
/** Flat status dot, specced after the BSK popup's ConnectionStatusIndicator. */
function StatusDot({ state }: { state: "active" | "idle" | "error" | "dead" }) {
function StatusDot({ state }: { state: ChromeState | "dead" }) {
const color =
state === "active"
? "bg-emerald-500"
: state === "error"
? "bg-red-500"
: state === "dead"
: state === "dead" || state === "reconnecting"
? "bg-amber-500"
: "bg-muted-foreground/40";
return (
@@ -390,7 +392,13 @@ export function OverlayBody(props: {
: focus === undefined
? "no session"
: `${focus.sessionId} · ${focus.action === "idle" ? "idle" : focus.action} · ${formatElapsed(focus.since, now)}`;
const state = !available ? "error" : focus !== undefined ? statusOf(focus) : "idle";
const state: ChromeState = reconnecting
? "reconnecting"
: !available
? "error"
: focus !== undefined
? statusOf(focus)
: "idle";
return (
<div
@@ -406,7 +414,7 @@ export function OverlayBody(props: {
onPointerDown={onHeaderPointerDown}
role="presentation"
>
<StatusDot state={state === "error" ? "error" : state === "active" ? "active" : "idle"} />
<StatusDot state={state} />
<span className={css["status-text"]}>{statusText}</span>
{onUseFloating !== undefined ? (
<IconAction
@@ -660,21 +668,28 @@ export function ObservationOverlay({ store }: { store: ObservationClientStore })
if (snapshot.sessions.length === 0) return null;
if (collapsed) {
const state = focus !== undefined ? statusOf(focus) : "idle";
const state: ChromeState = snapshot.reconnecting
? "reconnecting"
: focus !== undefined
? statusOf(focus)
: "idle";
return (
<button
type="button"
className={cn(css.capsule, "bsk-obs")}
data-state={state}
data-testid="obs-capsule"
aria-label="Expand browser observation overlay"
onClick={() => setCollapsed(false)}
>
<StatusDot state={state} />
<span className={css["capsule-text"]}>
{snapshot.sessions.length} session{snapshot.sessions.length === 1 ? "" : "s"}
{focus !== undefined && focus.action !== "idle"
? ` · ${focus.action} · ${formatElapsed(focus.since, now)}`
: ""}
{snapshot.reconnecting
? " · reconnecting…"
: focus !== undefined && focus.action !== "idle"
? ` · ${focus.action} · ${formatElapsed(focus.since, now)}`
: ""}
</span>
</button>
);
@@ -228,13 +228,37 @@ describe("ObservationOverlay", () => {
h.es().readyState = 0;
h.es().onerror?.({});
});
expect(screen.getByTestId("obs-header").textContent).toContain("reconnecting…");
const header = screen.getByTestId("obs-header");
expect(header.textContent).toContain("reconnecting…");
expect(header.querySelector("[data-state]")?.getAttribute("data-state")).toBe("reconnecting");
expect(screen.queryByText(/s1 · clicking/)).toBeNull();
act(() => h.emitRaw({ type: "snapshot", sessions: [BUSY], available: true }));
expect(screen.getByTestId("obs-header").textContent).not.toContain("reconnecting");
expect(header.textContent).not.toContain("reconnecting");
expect(header.querySelector("[data-state]")?.getAttribute("data-state")).toBe("active");
expect(screen.getByText(/s1 · clicking/)).toBeTruthy();
});
it("collapses to a capsule that drops stale action timing while reconnecting", async () => {
const h = makeHarness([BUSY]);
render(<ObservationOverlay store={h.store} />);
await screen.findByText(/s1 · clicking/);
fireEvent.click(screen.getByRole("button", { name: "Collapse" }));
const capsule = await screen.findByTestId("obs-capsule");
expect(capsule.textContent).toContain("clicking");
expect(capsule.getAttribute("data-state")).toBe("active");
act(() => {
h.es().readyState = 0;
h.es().onerror?.({});
});
expect(capsule.textContent).toContain("reconnecting…");
expect(capsule.textContent).not.toContain("clicking");
expect(capsule.getAttribute("data-state")).toBe("reconnecting");
expect(capsule.querySelector("[data-state]")?.getAttribute("data-state")).toBe("reconnecting");
act(() => h.emitRaw({ type: "snapshot", sessions: [BUSY], available: true }));
expect(capsule.textContent).toContain("clicking");
expect(capsule.getAttribute("data-state")).toBe("active");
});
it("shows the status row and a placeholder without a thumbnail", async () => {
const h = makeHarness([BUSY]);
render(<ObservationOverlay store={h.store} />);