From 47070fdc147fd3039c4e22440d7ed0b79a49dfbd Mon Sep 17 00:00:00 2001
From: BB-fat <1056871944@qq.com>
Date: Fri, 14 Aug 2026 07:57:07 +0000
Subject: [PATCH] feat(dsh-plugin-browserskill): default the overlay to the
top-right + dsh-native styling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Review follow-ups: the card/capsule now default to the top-right (clear
of the composer), and the overlay reads as part of the shell instead of
a foreign widget:
- shared primitives everywhere: Button (outline/ghost sm) for
Interrupt/Pop out with the shell's danger hover token, Tooltip for
the one-time interrupt semantics hint (native span anchor — Button
does not forward refs), StateDot for status, and outline icons
(Stop/RightUp/ChevronDown/Warning) replacing text glyphs;
- every color/surface/shadow/label now uses --dsw-alias-* semantic
tokens (bg-overlay, border-l1/l2, state-*-primary, label-*,
interactive-bg-hover[-danger], brand-primary focus rings,
dsw-shadow-lv1/lv2) — no invented tokens, no literal colors (dsh 0.1
ships no spacing/radius scale; those stay px like dsh's own code);
- strip item focus ring uses brand-primary; focus-visible and
reduced-motion preserved.
---
.../src/client/ObservationOverlay.module.css | 159 +++++++-----------
.../src/client/ObservationOverlay.tsx | 119 ++++++++-----
.../tests/client/observation-overlay.test.tsx | 6 +-
3 files changed, 139 insertions(+), 145 deletions(-)
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}
-
+