diff --git a/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.module.css b/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.module.css index 697749c..951e285 100644 --- a/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.module.css +++ b/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.module.css @@ -1,6 +1,8 @@ -/* Observation overlay: floating card + collapsed capsule. Semantic tokens - only (--dsw-alias-*); the shell overlay layer is click-through, so the card - and capsule opt back into pointer events themselves. */ +/* Observation overlay: floating card + collapsed capsule. Every color, + surface, shadow, and label style rides the dsh --dsw-alias-* semantic + tokens (dsh 0.1 ships no spacing/radius scales — those stay px, same as + dsh's own components). The shell overlay layer is click-through, so the + card and capsule opt back into pointer events themselves. */ .card { position: fixed; @@ -8,10 +10,10 @@ display: flex; flex-direction: column; pointer-events: auto; - background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base)); - border: 1px solid var(--dsw-alias-border-secondary); + background: var(--dsw-alias-bg-overlay); + border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; - box-shadow: 0 8px 28px rgb(0 0 0 / 18%); + box-shadow: var(--dsw-shadow-lv2); overflow: hidden; } @@ -20,6 +22,7 @@ flex-direction: column; height: 100%; min-height: 0; + font: var(--dsw-font-base-16, inherit); font-size: 12px; color: var(--dsw-alias-label-primary); } @@ -32,7 +35,7 @@ cursor: move; user-select: none; touch-action: none; - border-bottom: 1px solid var(--dsw-alias-border-secondary); + border-bottom: 1px solid var(--dsw-alias-border-l2); } .dot { @@ -44,12 +47,12 @@ } .dot[data-state="active"] { - background: var(--dsw-alias-label-success); + background: var(--dsw-alias-state-success-primary); animation: bsk-obs-pulse 1.6s ease-in-out infinite; } .dot[data-state="error"] { - background: var(--dsw-alias-label-error); + background: var(--dsw-alias-state-error-primary); } @keyframes bsk-obs-pulse { 0%, @@ -73,9 +76,12 @@ .icon-button { flex: none; + display: inline-flex; + align-items: center; border: none; background: none; - padding: 0 4px; + padding: 2px; + border-radius: 4px; font: inherit; color: var(--dsw-alias-label-tertiary); cursor: pointer; @@ -83,6 +89,7 @@ .icon-button:hover { color: var(--dsw-alias-label-secondary); + background: var(--dsw-alias-interactive-bg-hover); } .stage { @@ -127,11 +134,9 @@ display: inline-flex; align-items: center; justify-content: center; - font-size: 11px; - font-weight: 700; - color: var(--dsw-alias-label-warning); - border: 1px solid var(--dsw-alias-label-warning); - background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base)); + color: var(--dsw-alias-state-warn-primary); + border: 1px solid var(--dsw-alias-state-warn-primary); + background: var(--dsw-alias-bg-overlay); } .actions { @@ -140,65 +145,17 @@ justify-content: space-between; gap: 8px; padding: 6px 8px; - border-top: 1px solid var(--dsw-alias-border-secondary); -} - -.interrupt-wrap { - position: relative; - display: inline-flex; + border-top: 1px solid var(--dsw-alias-border-l2); } +/* Destructive action, styled like the shell's own danger affordances. */ .interrupt { - border: 1px solid var(--dsw-alias-label-error); - background: none; - color: var(--dsw-alias-label-error); - border-radius: 6px; - padding: 3px 12px; - font: inherit; - font-weight: 600; - cursor: pointer; -} - -.interrupt:disabled { - opacity: 0.45; - cursor: default; -} - -.interrupt:focus-visible { - outline: 2px solid var(--dsw-alias-border-focus); - outline-offset: 2px; + color: var(--dsw-alias-state-error-primary); + border-color: var(--dsw-alias-state-error-primary); } .interrupt:not(:disabled):hover { - background: var(--dsw-alias-bg-danger-secondary, rgb(220 60 60 / 12%)); -} - -.hint { - position: absolute; - bottom: calc(100% + 6px); - left: 0; - width: 220px; - padding: 6px 8px; - border-radius: 6px; - background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base)); - border: 1px solid var(--dsw-alias-border-secondary); - color: var(--dsw-alias-label-secondary); - box-shadow: 0 4px 14px rgb(0 0 0 / 16%); - z-index: 41; -} - -.popout { - border: 1px solid var(--dsw-alias-border-secondary); - background: none; - color: var(--dsw-alias-label-secondary); - border-radius: 6px; - padding: 3px 10px; - font: inherit; - cursor: pointer; -} - -.popout:hover { - color: var(--dsw-alias-label-primary); + background: var(--dsw-alias-interactive-bg-hover-danger); } .resize-handle { @@ -225,7 +182,7 @@ .capsule { position: fixed; right: 16px; - bottom: 16px; + top: 16px; z-index: 40; display: inline-flex; align-items: center; @@ -233,28 +190,16 @@ padding: 6px 12px; pointer-events: auto; border-radius: 999px; - border: 1px solid var(--dsw-alias-border-secondary); - background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base)); - box-shadow: 0 4px 14px rgb(0 0 0 / 16%); + border: 1px solid var(--dsw-alias-border-l2); + background: var(--dsw-alias-bg-overlay); + box-shadow: var(--dsw-shadow-lv1); font-size: 12px; color: var(--dsw-alias-label-secondary); cursor: pointer; } -.popout:focus-visible, -.icon-button:focus-visible, -.capsule:focus-visible { - outline: 2px solid var(--dsw-alias-border-focus); - outline-offset: 2px; -} -@media (prefers-reduced-motion: reduce) { - .dot[data-state="active"] { - animation: none; - } - - .thumb { - animation: none; - } +.capsule:hover { + background: var(--dsw-alias-interactive-bg-hover); } .strip { @@ -262,22 +207,22 @@ gap: 6px; padding: 4px 8px; overflow-x: auto; - border-top: 1px solid var(--dsw-alias-border-secondary); + border-top: 1px solid var(--dsw-alias-border-l2); } .strip-item { position: relative; flex: none; border-radius: 6px; - border: 1px solid var(--dsw-alias-border-secondary); + border: 1px solid var(--dsw-alias-border-l2); } .strip-item[data-focused] { - border-color: var(--dsw-alias-border-focus); + border-color: var(--dsw-alias-brand-primary); } .strip-item[data-state="error"] { - border-color: var(--dsw-alias-label-error); + border-color: var(--dsw-alias-state-error-primary); } .strip-item[data-state="dead"] { @@ -290,6 +235,7 @@ gap: 4px; padding: 3px 6px; border: none; + border-radius: 6px; background: none; font: inherit; font-size: 11px; @@ -297,6 +243,10 @@ cursor: pointer; } +.strip-main:hover { + background: var(--dsw-alias-interactive-bg-hover); +} + .strip-thumb { width: 28px; height: 20px; @@ -324,8 +274,7 @@ } .pin-badge { - font-size: 9px; - line-height: 1; + color: var(--dsw-alias-brand-primary); } .strip-interrupt { @@ -335,11 +284,12 @@ width: 16px; height: 16px; border-radius: 50%; - border: 1px solid var(--dsw-alias-label-error); - background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base)); - color: var(--dsw-alias-label-error); - font-size: 8px; - line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--dsw-alias-state-error-primary); + background: var(--dsw-alias-bg-overlay); + color: var(--dsw-alias-state-error-primary); padding: 0; cursor: pointer; } @@ -349,8 +299,19 @@ cursor: default; } +.icon-button:focus-visible, +.capsule:focus-visible, .strip-main:focus-visible, .strip-interrupt:focus-visible { - outline: 2px solid var(--dsw-alias-border-focus); - outline-offset: 1px; + outline: 2px solid var(--dsw-alias-brand-primary); + outline-offset: 2px; +} +@media (prefers-reduced-motion: reduce) { + .dot[data-state="active"] { + animation: none; + } + + .thumb { + animation: none; + } } diff --git a/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.tsx b/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.tsx index 243b177..972789c 100644 --- a/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.tsx +++ b/packages/dsh-plugin-browserskill/src/client/ObservationOverlay.tsx @@ -1,10 +1,20 @@ // Observation overlay: the default in-app carrier for live session watching. -// Expanded = draggable/resizable floating card (bottom-right); collapsed = a -// status capsule; "pop out" upgrades the same content to a native Document -// PiP window (user gesture required by the browser). Multi-session renders a -// meeting-style strip under the focus view. Pure client state — position, -// size, collapse, and the pin live for the page lifetime only. +// Expanded = draggable/resizable floating card (top-right, out of the +// composer's way); collapsed = a status capsule; "pop out" upgrades the same +// content to a native Document PiP window (user gesture required by the +// browser). Multi-session renders a meeting-style strip under the focus view. +// Visuals ride dsh's shared primitives and --dsw-alias-* tokens throughout, +// so the card reads as a native part of the shell, not a foreign widget. +import { + Button, + IconChevronDownOutline14, + IconRightUpOutline16, + IconStopFill16, + IconWarningOutline16, + StateDot, + Tooltip, +} from "@deepseek-ai/dsh-client-ui-primitives"; import { type PointerEvent as ReactPointerEvent, useCallback, @@ -82,6 +92,21 @@ export function statusOf(obs: SessionObservation): "active" | "idle" | "error" { return obs.action === "idle" ? "idle" : "active"; } +function dotStateOf( + state: "active" | "idle" | "error" | "dead", +): "done" | "ongoing" | "error" | "warning" { + switch (state) { + case "active": + return "ongoing"; + case "error": + return "error"; + case "dead": + return "warning"; + default: + return "done"; + } +} + /** One strip item: mini frame, id, status dot, hover interrupt, pin toggle. */ function StripItem(props: { store: ObservationClientStore; @@ -122,11 +147,20 @@ function StripItem(props: { )} {obs.sessionId} - + {pinned ? ( - - 📌 - + + + ) : null} {hover && !pinned && obs.dead !== true ? ( @@ -140,7 +174,7 @@ function StripItem(props: { void store.interrupt(obs.sessionId); }} > - ■ + ) : null} @@ -175,7 +209,7 @@ function OverlayBody(props: { onHeaderPointerDown, } = props; const [interrupting, setInterrupting] = useState(false); - const [hintOpen, setHintOpen] = useState(false); + // One-time semantics hint: a shared Tooltip that retires after first use. const [hintSeen, setHintSeen] = useState(false); const thumbId = focus?.thumbnailAttachmentId; @@ -193,7 +227,6 @@ function OverlayBody(props: { const onInterrupt = (): void => { if (!canInterrupt || focus === undefined) return; setHintSeen(true); - setHintOpen(false); setInterrupting(true); void store.interrupt(focus.sessionId).finally(() => setInterrupting(false)); }; @@ -213,7 +246,7 @@ function OverlayBody(props: { onPointerDown={onHeaderPointerDown} role="presentation" > - + {statusText} {onCollapse !== undefined ? ( ) : null} @@ -245,7 +278,7 @@ function OverlayBody(props: { )} {thumb?.status === "error" ? ( - ! + ) : null} @@ -264,36 +297,36 @@ function OverlayBody(props: { ) : null}
- - - {hintOpen && !hintSeen ? ( - - Interrupt stops only the current browser action — the agent run continues (use the - chat Stop button to halt the run). - - ) : null} - + + + + + {onPopOut !== undefined ? ( - + ) : null}
@@ -375,7 +408,7 @@ export function ObservationOverlay({ store }: { store: ObservationClientStore }) )?.getBoundingClientRect(); const base = { x: rect?.left ?? pos?.x ?? viewport().w - size.w - EDGE_MARGIN, - y: rect?.top ?? pos?.y ?? viewport().h - size.h - EDGE_MARGIN, + y: rect?.top ?? pos?.y ?? EDGE_MARGIN, w: rect?.width ?? size.w, h: rect?.height ?? size.h, }; @@ -427,7 +460,7 @@ export function ObservationOverlay({ store }: { store: ObservationClientStore }) aria-label="Expand browser observation overlay" onClick={() => setCollapsed(false)} > - + {snapshot.sessions.length} session{snapshot.sessions.length === 1 ? "" : "s"} {focus !== undefined && focus.action !== "idle" @@ -441,7 +474,7 @@ export function ObservationOverlay({ store }: { store: ObservationClientStore }) const style: React.CSSProperties = pos !== null ? { left: pos.x, top: pos.y, width: size.w, height: size.h } - : { right: EDGE_MARGIN, bottom: EDGE_MARGIN, width: size.w, height: size.h }; + : { right: EDGE_MARGIN, top: EDGE_MARGIN, width: size.w, height: size.h }; return (
{body} diff --git a/packages/dsh-plugin-browserskill/tests/client/observation-overlay.test.tsx b/packages/dsh-plugin-browserskill/tests/client/observation-overlay.test.tsx index f3a5c22..3b9eb2a 100644 --- a/packages/dsh-plugin-browserskill/tests/client/observation-overlay.test.tsx +++ b/packages/dsh-plugin-browserskill/tests/client/observation-overlay.test.tsx @@ -109,11 +109,11 @@ describe("ObservationOverlay", () => { const h = makeHarness([BUSY]); render(); const button = await screen.findByRole("button", { name: /Interrupt the current/ }); - fireEvent.pointerEnter(button); + fireEvent.mouseOver(button); await screen.findByRole("tooltip"); - fireEvent.pointerLeave(button); + fireEvent.mouseLeave(button); fireEvent.click(button); - fireEvent.pointerEnter(button); + fireEvent.mouseOver(button); expect(screen.queryByRole("tooltip")).toBeNull(); });