Skip to content

feat(packages): add dsh-plugin-browserskill — DeepSeek Harness tools for bsk - #91

Merged
iuyo5678 merged 34 commits into
mainfrom
feat/dsh-plugin
Aug 18, 2026
Merged

feat(packages): add dsh-plugin-browserskill — DeepSeek Harness tools for bsk#91
iuyo5678 merged 34 commits into
mainfrom
feat/dsh-plugin

Conversation

@BB-fat

@BB-fat BB-fat commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Add dsh-plugin-browserskill: DeepSeek Harness tools for BrowserSkill

Motivation

DeepSeek Harness (dsh) is DeepSeek's open-source agent harness where everything is a Cordis plugin and tools are registered via ctx.tools.register(defineTool({...})). This PR adds a new monorepo package, packages/dsh-plugin-browserskill, that exposes bsk's browser automation as model-visible dsh tools — so any dsh agent can drive a browser through BrowserSkill.

The package is a standard dsh bundle (dsh.bundle manifest + cordis.patch.yml), installable with dsh plugin --profile <name> add <package>. It does not change any existing BrowserSkill code.

Design

Each tool maps to one bsk <cmd> --json invocation: spawn the CLI → parse the structured JSON → return a canonical typed value (output.schema for programmatic use vs. output.render for model-facing text, per the dsh tool contract).

11 tools: browser_session_start / browser_session_stop / browser_session_list / browser_navigate / browser_snapshot / browser_observe / browser_click / browser_fill / browser_press / browser_screenshot / browser_emulate.

  • Multi-session: one agent conversation can drive several bsk sessions. browser_session_start returns the session id and makes it the current session; every operation tool takes an optional session arg (explicit wins and becomes current; omitted falls back to current), and every result echoes the session it acted on. Concurrent starts go through a synchronous reservation protocol so the cap (maxSessions, default 5) holds even under parallel calls. Window size (--width/--height) and mobile device emulation (bsk emulate --device …) are exposed as tool parameters.
  • Strict ownership boundary (the daemon may be shared with other agents/terminals/dsh instances): the plugin only ever sees and operates on sessions it created itself — a session arg naming a foreign or unknown id is rejected before any command reaches the daemon, browser_session_list returns only plugin-created sessions (no daemon-wide view), and stop/unload cleanup can never touch a session owned by another program.
  • Cancellation: aborting a tool call (exec.signal) kills the underlying bsk child process, aligned with the cooperative cancellation model landed in fix(extension): make tool cancellation cooperative #89.
  • UI cards: pending calls render as terminal cards (the bsk command line as title), completed calls as terminal output. browser_screenshot additionally commits the PNG through the host attachment store and attaches the image itself only when an attachment service is mounted and the active model route declares image input (mirroring dsh-tool-fs read_image's gate); otherwise it returns the file path.
  • Config (Schemastery Config): bskPath (default bsk from PATH), defaultTimeoutMs (120s), maxSessions (5).
  • Errors: non-zero exits surface bsk's JSON error envelope (code/message/hint) to the model; a missing bsk binary produces install guidance (also probed once at plugin activation).
  • Dispose: unloading the plugin kills in-flight children and stops every session it started (sessions started elsewhere are left alone).

Verification

  • Unit tests (41, vitest) mock the bsk runner — no real browser required: arg→CLI mapping for every tool, multi-session resolution/cap/cleanup, cancellation → AbortError, JSON error-envelope mapping, screenshot attachment gating (path-only vs. inlined image), terminal card presenters.
  • Integration spike against the published dsh CLI (@deepseek-ai/dsh@0.1.0-rc.6): the bundle installs via dsh plugin add, its layer composes in --dump-config, apply runs at boot (verified through config override + install probe), and all 11 tools register. A defineTool round-trip through the real @deepseek-ai/dsh-tools registry path (schema validation → execute → render) passes using a mock bsk executable. A real-browser end-to-end run was not possible in the CI-like test environment (no Chrome/extension); the bridge layer is fully covered by the mocked tests above.

CI note (maintainer action requested)

The pushing token lacks the workflow scope, so this PR deliberately does not touch .github/workflows/. As a stopgap, the root lint script now also runs the new package's typecheck + test, so the existing frontend CI job covers it. A maintainer with workflow scope may prefer to revert that one-line change and apply this instead:

--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -69,6 +69,15 @@ jobs:
       - name: Build extension
         run: pnpm ext:build
 
+      - name: Typecheck dsh plugin
+        run: pnpm --filter dsh-plugin-browserskill typecheck
+
+      - name: Run dsh plugin tests
+        run: pnpm --filter dsh-plugin-browserskill test
+
+      - name: Build dsh plugin
+        run: pnpm --filter dsh-plugin-browserskill build
+
   node-scripts:

Follow-ups (not in this PR)

  • npm publishing of dsh-plugin-browserskill (currently private: true) and adding the dsh-plugin topic for discoverability.
  • Background long-running bsk work (e.g. bsk record) through ctx.jobs.
  • More tools as needed (browser_console, browser_network, browser_evaluate, browser_get_html, tab management).

Update: Web client half — browser_screenshot toolview (6b96fcc)

The package is now dual-face: a dsh.client declaration (platform web) plus exports["./client"]lib/client.js, built in dsh's closure-factory contract (window.__ModuleLoader__.load handoff; react, dsh-client-ui-* platform modules external and answered by the loader's frozen module table; everything else inlined; CSS Modules compiled by lightningcss; a build-time purity gate rejects cross-plugin value imports, mirroring packages/client/tsdown.client.ts).

The client half registers a keyed tool.call.toolview view for browser_screenshot (the same extension point dsh-client-ui-skill uses for the skill tool):

  • The custom view keeps the terminal block (command line + output, via the shared TerminalBlock primitive) — no information the stock card showed is lost.
  • When the settled result content carries an image block, the view resolves the durable attachment through the client session's authorized readAttachment RPC and renders it with the shared MessageImage thumbnail + ImageLightbox atoms. Bytes never enter the session log or the page upfront — the reference is resolved on demand into a blob URL.
  • Path-only (text-only route) results render the same terminal block with the PNG path, unchanged.
  • Every other browser_* tool stays on the stock terminal card; only the browser_screenshot key is registered.
  • Styling follows docs/web-styling.md: --dsw-alias-* semantic tokens, CSS Modules, no component library, keyboard-focus and prefers-reduced-motion preserved.

Tests (+8, 50 total): view-model derivation (running / ok / error, image vs path-only), component rendering through @testing-library/react + happy-dom (image loads through the loader; path-only never touches it), and the keyed registration itself.

Verified live in the real Web UI (scripted OpenAI-compatible mock provider deciding tool calls; every tool executes against a real bsk + headless Chromium): the screenshot card shows the captured image inline in both collapsed and expanded states, the lightbox opens the original, the text-only route degrades to the path form, and the other tools' terminal cards are unaffected.

Update: live observation overlay (PiP mini-window) — host + client

The plugin now ships a live observation overlay for the dsh Web UI (Phases 1-A/1-B/1-C):

  • Host: an ObservationService tracks one record per owned session (action, since, url, thumbnailAttachmentId, lastError, dead), instrumented at the shared runBsk wrapper (tool entry/exit → action events; action end → immediate frame refresh). A throttled loop (1.5s active / 8s idle / 3-strike backoff) captures frames into the attachment store; observation traffic is fully isolated (no action events, never moves the current pointer) and — like every other command — flows through a per-session FIFO (KeyedExecutor), because the daemon accepts only one unfinished command per session. Edge states: a global failure streak flips an available flag (client shows "browser unavailable", keeps the last frame, greys interrupt); a session_not_found envelope marks the session dead.
  • Wire: dsh 0.1's Typert Remote pipeline and forwarded-event allowlist are closed to out-of-tree packages, so state/events/interrupt/thumbnail-bytes are served over the documented webServer route seam: GET /bsk-observation/state, GET /bsk-observation/events (SSE), POST /bsk-observation/interrupt, GET /bsk-observation/thumbnail/<id> (routes mount via ctx.inject(['webServer'], …), so headless compositions are unaffected). Frames are plugin-owned (no session-log reference), so the session-authorized readAttachment RPC rightly refuses them — hence the plugin's own thumbnail route.
  • Client: registered into shell.overlay (the sanctioned frame-wide seat): auto-appearing floating card (drag-move, corner resize min 240×180 / max 80% viewport, remembered for the page), collapsible capsule, focus view (status dot + session + action + ticking elapsed, breathing thumbnail with fade-in and failure badge), one-click Interrupt (no confirm, greys while settling, one-time semantics tooltip), multi-session meeting-style strip (per-session tile with hover interrupt, click-to-pin focus, red-edge errors never steal focus, dead sessions greyed), and Document-PiP pop-out (gesture-gated, inherits the card size, styles cloned, pagehide falls back; unsupported browsers hide the button).
  • Config: observationEnabled (true), thumbnailIntervalMs (1500), idleIntervalMs (8000).
  • Tests: 99 unit tests (host state machine/cadence/backoff/isolation/interrupt routing/HTTP routes + client store and overlay rendering incl. pin/strip/PiP-mock/drag clamps), all mocked at the bsk boundary. Real-machine e2e (scripted model + real bsk + headless Chromium) verified: overlay auto-appears, frames refresh with actions, PiP pops, interrupt kills the in-flight child, strip pins/follows, resize adapts.

BB-fat added 5 commits August 13, 2026 15:04
…for bsk

Add a new monorepo package that registers BrowserSkill (bsk) browser
automation as model-visible tools in DeepSeek Harness (dsh):

- 11 tools mapping to bsk CLI --json commands: session start/stop/list,
  navigate, snapshot, observe, click, fill, press, screenshot, emulate
- multi-session support: optional session arg with a current-session
  pointer, session id echoed in every result, configurable concurrency
  cap (default 5), and dispose-time cleanup of plugin-started sessions
- cancellation: exec.signal aborts kill the underlying bsk child process
- terminal-style UI cards (command line as the call card, output as the
  result card); screenshots inline the PNG through the host attachment
  store when the model route accepts image input, else return the path
- config: bskPath / defaultTimeoutMs / maxSessions via Schemastery schema
- install guidance when the bsk binary is missing; bsk JSON error
  envelopes (code/message/hint) surfaced to the model
- unit tests mock the bsk runner (no real browser required); the root
  lint script now also runs the package's typecheck + tests so the
  existing CI frontend job covers the new package (a dedicated CI job
  diff is provided in the PR; the pushing token lacks workflow scope)
The concurrency cap was checked in registry.add() AFTER
`bsk session start` had already created the session, so a rejected
start leaked a live session outside the plugin's tracking (found by the
real-browser e2e: the rejected third session stayed in
`bsk session list` after dispose). Check capacity before spawning;
registry.add() keeps the same check as a backstop.
…n message

The daemon's JSON error envelope carries an actionable hint (e.g.
'choose an input, textarea, or contenteditable element from the latest
snapshot'), but the thrown BskError only embedded the message field, so
the model-facing error text never showed the hint. Append it.
…-block results

browser_screenshot on an image-capable route returns text + image
blocks; the shared result presenter required exactly one text block and
bailed to the generic card (tool name + raw args). Project the first
text block instead so the terminal card shows in both modes.
…_screenshot toolview

Make the package dual-face: a dsh.client declaration (platform 'web')
plus an exports["./client"] bundle built in dsh's closure-factory shape
(window.__ModuleLoader__.load handoff, platform modules external,
everything else inlined, CSS Modules via lightningcss).

The client registers a keyed 'tool.call.toolview' view for
browser_screenshot that keeps the terminal block (command + output) and,
when the settled result carries an image block, resolves the durable
attachment through the client session's authorized readAttachment RPC
and renders it with the shared MessageImage thumbnail/lightbox atoms.
Path-only results render unchanged; every other browser_* tool keeps
the stock terminal card.

Tests: view-model derivation (running/ok/error, image vs path-only),
component rendering through @testing-library/react + happy-dom, and the
registration key. biome.json now excludes the package's lib/ build
output (same treatment as dist/).
@iuyo5678

Copy link
Copy Markdown
Collaborator

@BB-fat 你再检查一下,就是如果有别的agent工具在使用当前bsk在做一些事情,agent window激活,然后插件完成工作,session关闭,会不会影响之前的 agent window或者session

…emon

Review hardening for daemon-sharing deployments (other agents,
terminals, or dsh instances on the same bsk daemon):

- Ownership split: only sessions created by browser_session_start are
  'owned'. Explicitly referenced foreign sessions are still tracked for
  the current-session pointer, but browser_session_stop refuses them
  and unload cleanup stops exactly the owned set — no path can stop a
  session this plugin did not create.
- Start race: the capacity check is now a synchronous
  reserveStart/completeStart/abandonStart protocol, so two concurrent
  starts can never both pass the cap (check-and-reserve is atomic on
  the event loop); a rejected or failed start never leaks a session.
- Stale handles: dispose already swallowed per-stop errors; the owned
  set is computed at dispose time so an externally stopped session is a
  no-op, not a failure.

Tests (+7): reservation race (two concurrent starts, cap 1 -> exactly
one spawn), foreign-stop refusal (no stop command reaches the daemon),
reference adoption never becoming owned, resolveForStop pointer
stability, and an apply-level dispose test proving only owned sessions
are stopped while stale stops are tolerated. e2e: a manually started
foreign session survived the plugin's full lifecycle while both plugin
sessions were cleaned up.
@BB-fat

BB-fat commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Effect screenshots — scripted model driving real tool execution (real bsk + Chromium behind):

Tool chain (terminal cards) Screenshot rendered inline (custom toolview)
1 2
Multi-session routing Error packet with hint
3 4

@BB-fat
BB-fat marked this pull request as draft August 14, 2026 04:06
…ssions are invisible

Tighten the shared-daemon boundary from 'cleanup only touches owned
sessions' to 'the plugin only ever sees and operates on sessions it
created':

- browser_session_list no longer queries the daemon at all; it returns
  the plugin's own session table (with the current marker), so foreign
  sessions cannot even be enumerated through the plugin.
- Every tool's optional session argument must name a plugin-created
  session; foreign/unknown ids are rejected with a clear error before
  any command reaches the daemon (the reference-adoption channel is
  gone).
- Unchanged: dispose stops exactly the owned set, stop requires
  ownership, the start reservation protocol, and handle-precise child
  kills.

Tests updated (59): explicit-foreign rejection without daemon contact,
registry-only listing, reservation race, stale-handle-tolerant dispose.
e2e: a manually started foreign session was rejected on reference,
absent from session_list, refused on stop, and survived dispose while
both plugin sessions were cleaned up.
@BB-fat

BB-fat commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Feature idea: live BSK observation overlay with one-click interrupt

When the agent is driving the browser, the user has no ambient visibility into BSK's control state, and interrupting means hunting for the Stop button in the chat flow. Proposal: a video-conference-style mini-window.

Interaction — a real Picture-in-Picture window (Document PiP API), always on top:

  • Live state: a "breathing thumbnail" of the controlled page (periodic bsk screenshot refresh), plus current session / action / elapsed time.
  • One-click interrupt: exactly the same semantics as the chat's abort — interrupts the in-flight bsk tool call. One button, one meaning.
  • Multi-session: laid out like a multi-participant call — one breathing thumbnail per session (strip/gallery view), the most recently active session takes the focus frame, each with its own state line and interrupt affordance.

Phasing

  1. L2 breathing thumbnail + interrupt — built entirely on existing extension points (plugin session registry + pre/post-execute events + this package's client half + Document PiP).
  2. Video recording of the controlled session.
  3. L3 live screencast — needs an upstream bsk capability (e.g. CDP screencast); also the right moment to add cooperative daemon-side cancel, so interrupting also stops the daemon's in-flight command (today killing the CLI child doesn't).

Open point: in-app floating overlay as fallback when PiP is unavailable.

Feedback welcome — happy to prototype this behind the plugin's client half.

BB-fat added 18 commits August 14, 2026 04:59
…iP overlay (Phase 1-A)

Per-session live observation for owned sessions only, feeding the
upcoming client overlay:

- State model: SessionObservation (sessionId/url/action/since/
  thumbnailAttachmentId/lastError), one entry per owned session;
  added on start, removed on stop, cleared on dispose.
- Instrumentation: the shared runBsk wrapper begins/ends an action
  (tool name mapped to a verb) around every model-facing call and tags
  the child with its session id; action end triggers an immediate
  thumbnail refresh.
- Thumbnail loop: 1.5s cadence while active, 8s idle downclock, x3
  failure backoff, silent frame retention on errors; captures run
  OUTSIDE the instrumentation (no action events, no current-pointer
  movement) and land in the attachment store by reference.
- Interrupt: runner.killFor(tag) kills exactly the tagged in-flight bsk
  children — chat-Stop-equivalent semantics for the current or a named
  owned session; foreign ids and the no-in-flight case return false.
- Remote seam: dsh 0.1's Typert Remote pipeline and forwarded-event
  allowlist are closed to out-of-tree packages, so state/events/
  interrupt are served over the documented webServer route seam:
  GET /bsk-observation/state, GET /bsk-observation/events (SSE),
  POST /bsk-observation/interrupt; routes mount only when a webServer
  service exists (headless compositions skip them).
- Config: observationEnabled (true), thumbnailIntervalMs (1500),
  idleIntervalMs (8000).

Tests (+13, 72 total): state machine + event stream, cadence fast/idle/
backoff, headless no-store silence, observation-traffic isolation,
interrupt routing (default/specified/none/foreign), HTTP route
handlers, disabled-webServer no-op.
…nterrupt, and PiP (Phase 1-B)

Client half of the PiP observation window, wired to the host
ObservationService over the /bsk-observation HTTP+SSE seam:

- ObservationClientStore: initial state fetch + SSE increments +
  on-demand thumbnail blob loading (session-authorized readAttachment,
  same path as the toolview) + interrupt POST. All I/O injectable.
- ObservationOverlay registered into the shell.overlay list seat (the
  sanctioned frame-wide surface; root is off-limits). Hidden with no
  owned sessions; appears on first start; vanishes when all stop.
- Focus view: status row (green/grey/red dot + session + action + mm:ss
  ticking), breathing thumbnail (new frames fade in, failures keep the
  last frame with a warning badge, pre-navigate placeholder), and the
  action area.
- Interrupt: single click, no confirm; greys into 'Interrupting…' and
  back; disabled when nothing is in flight; one-time tooltip explains
  the semantics (stops the current action only; the run continues).
- Card: drag-move via the header, corner-handle resize (min 240x180,
  max 80% viewport), both clamped and kept for the page lifetime;
  collapsible status capsule.
- PiP: Pop out (user gesture) opens Document PiP with the card's
  current size, portals the same content in, and clones the document's
  style nodes; pagehide falls back to the card with state intact;
  unsupported browsers hide the button; nothing auto-pops.

Tests (+17, 89 total): store (fetch/SSE apply/malformed frames/
thumbnail lifecycle/interrupt wire/stop cleanup) and overlay (hidden->
visible lifecycle, status row, thumbnail via loader, interrupt
disabled/active/hint-once, capsule, resize clamps, move clamps, PiP
unsupported, PiP pop/fallback). Styling follows web-styling (dsw-alias
tokens, CSS Modules, focus-visible, reduced-motion).
…ge states (Phase 1-C)

- Strip (meeting-style multi-session layout): one item per session
  (mini frame + id + status dot), horizontal row with scroll overflow;
  hover reveals a per-item interrupt button that acts without
  refocusing; click pins the focus view (pin badge, click again to
  release); auto-follow picks the most recently active session but
  never steals focus for errored or dead sessions (red edge / greyed
  item instead).
- Edge states: the host flips an availability flag after repeated
  global capture failures (client shows 'browser unavailable', keeps
  the last frame, greys interrupt, and recovers on the next success);
  a session_not_found envelope marks the session dead (no more frame
  requests, grey strip item, removable as usual); lastError now clears
  when the next action starts or succeeds.
- State/SSE wire gains { available } and an 'availability' event;
  SessionObservation gains dead?.

Tests (+8, 97 total): host availability flip, dead marking +
instrumentation drop + clean removal, lastError lifecycle; client
strip render/pin/unpin/auto-follow, error no-steal, unavailable strip,
dead grey, strip-item interrupt without refocus.
…ver appears

ctx.get('webServer') at apply time raced the web composition's service
registration (the fallback SPA then answered /bsk-observation/* with
index.html). Ride ctx.inject(['webServer'], …) instead: the callback
runs when the service is provided and never runs in headless
compositions, keeping route registration timing-safe and optional.
…tion vs tool calls)

The daemon accepts only one unfinished command per session; the
thumbnail loop racing a model tool call produced 'session already has
an unfinished command' failures on both sides (found by the Phase 1-C
e2e: navigate/snapshot/click all errored while captures kept running).
Add a KeyedExecutor: every command for a session — tool calls and
observation captures alike — runs FIFO; queued tasks reject early on
abort, running tasks keep the runner's signal-driven kill.
…lugin's own route

The session-authorized client RPC (readAttachment) refuses images that
no session log references — observation frames are plugin-owned runtime
data, so every overlay thumbnail failed with ATTACHMENT_NOT_REFERENCED.
The host now keeps the full attachment ref per frame and serves bytes
through GET /bsk-observation/thumbnail/<id> (verified readImage path);
the client loader is a plain fetch of that route.
…ng slash

The webserver prefix matcher joins prefix + '/', so a registered
'/bsk-observation/thumbnail/' never matched real paths.
…errupted

A child killed via killFor (overlay interrupt) exits with a null code
and empty output, which used to render as a duplicated 'bsk x failed:
bsk x failed'. Report it as interrupted instead.
… dsh-native styling

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.
The plain top:16 default overlapped the shell's Session log action;
dock the card and capsule at the content area's top-right (64px) so
neither collides with the header controls.
… own UI system

The previous pass aligned the observation overlay with the dsh shell; the
product call is the opposite direction — the floating card should read as a
BrowserSkill surface, not a shell-native widget.

- Reuse @browser-skill/ui directly (Button, cn); status dots spec'd after the
  extension popup's ConnectionStatusIndicator; Remix icons for
  interrupt/pin/warn/pop-out.
- Compile the BSK tailwind utility sheet scoped under the .bsk-obs root class
  (scripts/build-client-css.mjs + postcss-prefix-selector) and ship the oklch
  design tokens on the same scope, so nothing leaks into the host shell and
  the shell theme cannot bleed back. tsdown injects .nomodule.css verbatim
  (minified, unhashed) alongside the hashed CSS modules.
- BSK ships no shared tooltip, so the one-time interrupt semantics hint is a
  card-spec bubble (bg-card/border/12px radius), retired after first use.
- Tests: pin react/react-dom to this package's 18.x copies in vitest — the ui
  package sources peer on react ^19 and would otherwise emit react-19
  (transitional) elements that the react-18 renderer rejects. Production is
  unaffected (react stays external, provided by the shell).
- stylelint: ignore the generated bsk-ui.nomodule.css.
The client entry imports the generated bsk-ui.nomodule.css; the file is
gitignored, so a fresh CI checkout could not resolve it. Generate it as part
of the test script.
…ence, queue race)

Blocking:
- B1: delete screenshot scratch PNGs — observation frames reuse one fixed
  per-session path and unlink after every read (finally); browser_screenshot
  unlinks once the bytes are in the attachment store (kept only when the file
  itself is the model-facing artifact).
- B2: replicate dsh's browser-trust fence on /bsk-observation/*: loopback
  Host only, Origin must match Host, sec-fetch-site: cross-site refused, POST
  requires application/json; README documents the loopback trust premise and
  the 0.0.0.0 warning.

Major:
- M1: KeyedExecutor tail chains previous+task (allSettled) so a task aborted
  while queued cannot release the next one into the running session.
- M2: client tracks the live frame per session — replacing/removing/reset
  revokes the old blob URL at once; loads settling after replacement never
  resurrect it.

Minor: drop replaced thumbRefs (+on remove); beginAction fires inside the
queue (no label overwrite while queued, no idle flash); half-initialized
session cleanup stops through the queue with one retry; SSE (re)open refetches
/state (+ snapshot flag renamed subscribed); install probe uses --version (no
daemon spawn); emulate mobile documents the width+height requirement (the
daemon refuses it alone — verified).

Nit: single BskRunOptions declaration; tails map entry dropped on drain; SSE
cleanup on res close; popOut catches requestWindow rejections; resize handle
gains keyboard control (arrows, 16px steps, aria value attrs).

Tests: +14 (fence rules, scratch lifecycle, queue race, blob revocation,
instrumentation timing, SSE resync). 113/113 green.
The plugin exposed only tools; the BrowserSkill agent skill never reached
the harness's <available_skills> catalog. Register it as an embedded runtime
skill (progressive disclosure: catalog entry resident, body on skill-tool
invocation) via the official ctx.skills seam — silent no-op when the
composition lacks it.

Content is build-time assembled so there is one source of truth:
skill/prelude.md (committed, dsh-specific: tool<->CLI map, owned-session
semantics, plugin-only overrides) + the canonical repo-root skill/SKILL.md
verbatim (the same file crates/bsk-cli/build.rs mirrors; frontmatter
stripped, name/description supplied by the registration). Generated into
src/skill-content.generated.ts (gitignored) — registration and every
pre-step snapshot are pure in-memory reads: no disk, no process, no daemon.

Scripts: build:skill chained into build/test/typecheck (same pattern as
build:css); biome excludes the generated artifacts. Tests: +3 (catalog
content, in-memory weight, silent degradation). 116/116 green.
…nvocation

Progressive disclosure, final stage: with lazyTools on (default), the eleven
browser_* tool schemas stay out of the system prompt — the browser-skill
catalog entry is the sole advertisement. One successful skill invocation
(model tool call via tools/result, or a /browser-skill user gesture via the
session append feed) registers the suite for the process lifetime, idempotent
on repeats; session resume is covered by scanning durable events for a past
tool/call+tool/result pair (or skill-invocation message) on session entry and
at apply time. lazyTools: false keeps the legacy always-on registration.

Investigation (recorded in the ticket): tools/result carries normalized
name/arguments with an isError discriminant; dsh 0.1 has no official global
visibility switch (ctx.tools.restrict is agent-scoped and throws from a plain
context), making conditional registration the intended pattern; per-step
assembly picks the new suite up on the next step, and visibility changes are
first-class inputs of tool-skill's catalog digest. registerTools now returns
the combined disposer via a prototype-preserving tracking overlay; the skill
prelude tells the model the browser_* tools unlock on skill load.

Tests: +8 (hidden-before/reveal/idempotent/gesture/history hit+miss/dispose/
scanner pairing/two-state apply wiring). 124/124 green.
Cordis contexts are fiber-owned: assigning onto an Object.create() overlay of
the apply-time context fails with 'cannot set property tools in multiple
fibers' when the reveal fires from the tools/result listener fiber (caught by
the e2e: the skill invocation matched but the suite never registered).
registerTools now tracks disposers through a plain closure over the captured
context — no context mutation anywhere. Also documents lazyTools in the
README and hardens the reveal against a failed registration (log and stay
hidden instead of latching).

Verified end to end against the real stack: first request carries 0 browser_*
schemas (skill catalog entry only); after the scripted skill invocation the
next request carries exactly the 11 browser_* tools and browser_session_start
dispatches for real.
- @browser-skill/ui and @remixicon/react move to devDependencies (the client
  bundle inlines both; the host face never imports them — verified).
- Drop private, add license: MIT, ship the repo-root MIT LICENSE in files.
- peerDependencies: add the runtime-required @deepseek-ai/schemastery, and
  the previously dangling meta entries @deepseek-ai/dsh-attachment and
  @deepseek-ai/dsh-llm (optional, matching actual type-level use).
- prepack runs the full build; npm pack now yields exactly 7 files
  (lib/index.mjs, lib/index.d.mts, lib/client.cjs, cordis.patch.yml,
  README.md, LICENSE, package.json).
- publint clean: the client face ships as lib/client.cjs (CJS extension in
  a type:module package); add lint:publint script. Built declarations pass a
  NodeNext consumer typecheck (strict, skipLibCheck false).
@BB-fat

BB-fat commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

【讨论】插件 npm 发布的身份与流程

这个插件后续计划发布到 npm 公开安装,发布前有以下事项需要讨论:

1. 包名 / scope(两个名字在 npm 上均未被占用)

  • A. 无 scope:dsh-plugin-browserskill —— 沿用现名,零额外动作;
  • B. 注册 @browser-skill npm org 发 scoped 版(如 @browser-skill/dsh-plugin)—— 更正式,BSK 家族后续其他包(如 @browser-skill/ui 若单独发布)也有归属。

2. 发布主体与凭证

  • Tencent 侧是否已有 npm 组织 / 发布流程可走(2FA、token 保管、权限审批)?还是先以个人账号发布、后续再移交组织?
  • 若组织渠道方便,建议优先组织;另可考虑 npm trusted publishing(GitHub Actions OIDC 免 token 发布 + provenance 溯源),把发布接进 CI。

BB-fat added 3 commits August 17, 2026 07:04
…between frames

Hold the last ready thumbnail until the next one decodes, and swap the
img src in place instead of remounting through a placeholder.
… resize

Use icon-only actions with a short hover tip, swap the interrupt glyph to
RiStopCircleLine, and let every corner resize the card without a grip icon.
@iuyo5678

Copy link
Copy Markdown
Collaborator

【讨论】插件 npm 发布的身份与流程

这个插件后续计划发布到 npm 公开安装,发布前有以下事项需要讨论:

1. 包名 / scope(两个名字在 npm 上均未被占用)

  • A. 无 scope:dsh-plugin-browserskill —— 沿用现名,零额外动作;
  • B. 注册 @browser-skill npm org 发 scoped 版(如 @browser-skill/dsh-plugin)—— 更正式,BSK 家族后续其他包(如 @browser-skill/ui 若单独发布)也有归属。

2. 发布主体与凭证

  • Tencent 侧是否已有 npm 组织 / 发布流程可走(2FA、token 保管、权限审批)?还是先以个人账号发布、后续再移交组织?
  • 若组织渠道方便,建议优先组织;另可考虑 npm trusted publishing(GitHub Actions OIDC 免 token 发布 + provenance 溯源),把发布接进 CI。

这块包名我申请了一个 npm包的发布账号,不过scope是:wxg-prc-cpg 包名可以是 @wxg-prc-cpg /browser-skill-dsh-plugin 后面还想其他的开源仓库就用这个scope就成,不过这块可能有问题没有思考到。

其中NPM包发布流程的 accesstoken 我放到了 Environment Secrets 中 NPM_TOKEN 变量,给你增加了action 权限,你可以在CI流程中使用,应该没有问题,这块你看是否还有其他问题?可以随时反馈,我这边可以尽快调整。

BB-fat added 3 commits August 17, 2026 09:48
CI's frontend job failed on formatter-only diffs in the overlay and the
skill-content assertion.
Add the missing blank line before the actions comment, and hoist
.tool-button:focus-visible above the wrap-hover rules so specificity
no longer descends.
…lugin

Add a Release dsh plugin workflow (dsh-plugin-v* tags or workflow_dispatch)
that reads NPM_TOKEN from the GitHub Environment of the same name. The
Cordis plugin id stays dsh-plugin-browserskill; only the npm package name
changes.
@iuyo5678

Copy link
Copy Markdown
Collaborator

按这个结论把 npm 发布接进 CI 了(edb1496)。

已落地

  • 包名改为 @wxg-prc-cpg/browser-skill-dsh-pluginpublishConfig.access: public)。Cordis 插件 id 仍是 dsh-plugin-browserskill,避免配置和 client bundle id 跟着 scope 变。

  • 新 workflow:.github/workflows/release-dsh-plugin.yml

    • 触发:打 dsh-plugin-v* tag,或 Actions 里手动 workflow_dispatch
    • 会校验 tag / package.json 版本一致,再跑 typecheck + test,然后 pnpm publish
    • token 从 GitHub Environment NPM_TOKEN 的 secret NPM_TOKEN 读取(当前仓库里就是这个名字)

发布方式

git tag dsh-plugin-v0.1.0
git push origin dsh-plugin-v0.1.0

还想确认的几点(不挡这次接入,方便你这边调)

  1. Environment 现在就叫 NPM_TOKEN,workflow 已按这个名字接好。如果以后想改成 npm 这类更常规的名字,改 Environment 后告诉我一声即可。
  2. Token 需要能往 @wxg-prc-cpgpublic scoped 包。建议用 Granular Access Token(该 scope 的 Read and write),或 Classic 的 Automation token;带 2FA 的 Publish token 在 CI 里会卡 OTP。
  3. chrome-web-store 那个 Environment 有 required reviewers;NPM_TOKEN 目前没有保护规则。公开发包的话,建议至少加 reviewer 或限制到 main / 对应 tag。
  4. 这次按你准备好的 token 走,没有上 npm trusted publishing(OIDC + provenance)。以后如果 org 配了 trusted publisher,可以把长效 token 拿掉。

第一次发布等 PR 合进 main 再打 dsh-plugin-v0.1.0 即可,我这边不会在 PR 分支上直接发。

好,我处理一下。当前是这么设计的。

  1. 现在这个token是忽略 2FA 的,这个我专门设置,先走流程。
  2. 这个也有 required reviewers ,不过我把你加到了reviewer,因为是禁止自己review的,所以是需要peer review的,核心开发者,我们很信任,先这样运行一阵,我后面再调。
  3. 同一,先按这个流程走一次,我调整一下CI流程。

@BB-fat

BB-fat commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

这个“还想确认几点”,是我的 Agent 乱评论的 🤣,气死我了,打扰了

@BB-fat

BB-fat commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

npm 发布的 workflow 已经调好了,测试了一次也发布成功了。但 npm 上看不到,应该是需要去后台设置一下可见性。 @iuyo5678
https://github.com/Tencent/BrowserSkill/actions/runs/32026250084/job/95376239839

@iuyo5678

Copy link
Copy Markdown
Collaborator

npm 发布的 workflow 已经调好了,测试了一次也发布成功了。但 npm 上看不到,应该是需要去后台设置一下可见性。 @iuyo5678 https://github.com/Tencent/BrowserSkill/actions/runs/32026250084/job/95376239839

不用操作,默认就发布了,我自己用 npm i @wxg-prc-cpg/browser-skill-dsh-plugin 安装了一下,没有问题的。是不是你是发布了马上测试,可能需要等一会。

@BB-fat

BB-fat commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

是不是因为你那边已经配置了 token?

@wxg-prc-cpg/browser-skill-dsh-plugin is not in the npm registry, or you have no permission to fetch it.

@iuyo5678

Copy link
Copy Markdown
Collaborator

奇怪,我找了几个电脑测试都ok,windows mac, 其中window机器我都没有怎么使用过,另外找别人的机器也测试下,找了一台没有内网的机器也测试了下,安装都没有任何问题,你做了什么npm相关配置么?
image

@BB-fat

BB-fat commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

可以了,是我这边源的问题

@BB-fat
BB-fat marked this pull request as ready for review August 18, 2026 02:44
Document the dsh plugin highlights and the one-line install for the web profile.
}

/** Interrupt the current or named session's in-flight call. */
async interrupt(sessionId?: string): Promise<boolean> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

如果用户在使用过程中要interrupt 请求,比如发现提交一些不是自己想提交的求,这里发出去的请求是:没有的content-type 字段信息的


/** Run the fence; returns true when the request was rejected (handled). */
function fenceRejected(req: IncomingMessage, res: ServerResponse): boolean {
const violation = fenceViolation(req);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里会直接 403 导致中断没有效果吧。

@iuyo5678

Copy link
Copy Markdown
Collaborator

代码有如下的问题:

  1. 悬浮窗 Interrupt 按钮一定会403 无法中止,代码位置我指出了, 你看一下,改动也比较容易 bservation-store.ts 283–286 行补上 "content-type": "application/json"
  2. 观察帧会无限写入 attachment store 这个还好,我觉得可以接受,
    第一个问题我觉得要修一下。 @BB-fat

…rupt

The observation HTTP fence rejects POSTs without application/json, so the
Interrupt button always got 403.
@iuyo5678
iuyo5678 merged commit 8e4de6c into main Aug 18, 2026
4 checks passed
@BB-fat
BB-fat deleted the feat/dsh-plugin branch August 18, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants