diff --git a/.github/workflows/release-dsh-plugin.yml b/.github/workflows/release-dsh-plugin.yml index c7dacdb..3e5ca41 100644 --- a/.github/workflows/release-dsh-plugin.yml +++ b/.github/workflows/release-dsh-plugin.yml @@ -103,3 +103,39 @@ jobs: fi pnpm publish --access public --no-git-checks echo "Published ${PACKAGE_NAME}@${{ needs.resolve.outputs.version }}" + + - name: Verify published version and README + working-directory: ${{ env.PACKAGE_DIR }} + run: | + node --input-type=module <<'NODE' + import assert from "node:assert/strict"; + import { readFile } from "node:fs/promises"; + import { setTimeout } from "node:timers/promises"; + + const { name, version } = JSON.parse(await readFile("package.json", "utf8")); + const readme = await readFile("README.md", "utf8"); + const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`; + + for (let attempt = 1; attempt <= 6; attempt++) { + try { + const response = await fetch(url, { + headers: { "Cache-Control": "no-cache" }, + signal: AbortSignal.timeout(10_000), + }); + assert.ok(response.ok, `npm registry returned HTTP ${response.status}`); + const metadata = await response.json(); + assert.equal(metadata.versions?.[version]?.version, version, "Published version is missing"); + assert.equal(metadata["dist-tags"]?.latest, version, "npm latest does not match the release"); + assert.ok(metadata.readme === readme, "npm README does not match the release"); + console.log(`Verified ${name}@${version}: latest tag and README match`); + break; + } catch (error) { + if (attempt === 6) { + console.error("Publication succeeded, but registry verification failed. Check npm before retrying the workflow."); + throw error; + } + console.log(`Registry verification attempt ${attempt} failed; retrying in 10 seconds`); + await setTimeout(10_000); + } + } + NODE diff --git a/README.md b/README.md index 136dc6b..83108d0 100644 --- a/README.md +++ b/README.md @@ -147,20 +147,26 @@ Start a new Agent session and write a prompt that needs the browser, for example Using [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`)? BrowserSkill ships a first-class dsh plugin on npm as [`@wxg-prc-cpg/browser-skill-dsh-plugin`](https://www.npmjs.com/package/@wxg-prc-cpg/browser-skill-dsh-plugin). -It injects native `browser_*` tools (no shelling out to `bsk`) and a live Web UI -overlay of each Agent Window. +It gives the agent native `browser_*` tools and a live view of its browser sessions +in the Web UI. The plugin runs `bsk` on the agent's behalf. -Add it to a dsh profile, then start that profile: +Install the `bsk` CLI and connect the browser extension first. Then add the latest +published plugin to a dsh profile and start it (replace `web` with your profile name): ```sh -dsh plugin --profile web add @wxg-prc-cpg/browser-skill-dsh-plugin +dsh plugin --profile web add @wxg-prc-cpg/browser-skill-dsh-plugin@latest dsh --profile web ``` -The plugin carries its own copy of the skill, so `bsk install-skill` is not needed -for dsh — but the `bsk` CLI and the browser extension are still prerequisites. See -the [plugin README](packages/dsh-plugin-browserskill/README.md) for the tool list, -configuration, and the observation overlay. +The plugin includes the `browser-skill` skill, so `bsk install-skill` is not needed +for dsh. Installed plugins do not update automatically. To upgrade this plugin: + +```sh +dsh plugin --profile web update @wxg-prc-cpg/browser-skill-dsh-plugin --latest +``` + +Restart the profile after upgrading. See the +[plugin README](packages/dsh-plugin-browserskill/README.md) for usage and configuration. ## How It Works diff --git a/README.zh-CN.md b/README.zh-CN.md index d05705a..f0810da 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -125,16 +125,22 @@ bsk install-skill ## DeepSeek Harness 插件 -在用 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)?BrowserSkill 提供了官方 dsh 插件,已发布到 npm:[`@wxg-prc-cpg/browser-skill-dsh-plugin`](https://www.npmjs.com/package/@wxg-prc-cpg/browser-skill-dsh-plugin)。它会注入原生 `browser_*` 工具(无需再通过 Shell 调用 `bsk`),并在 Web UI 中实时观察每个 Agent Window。 +在用 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)?BrowserSkill 提供了官方 dsh 插件,已发布到 npm:[`@wxg-prc-cpg/browser-skill-dsh-plugin`](https://www.npmjs.com/package/@wxg-prc-cpg/browser-skill-dsh-plugin)。它为 Agent 提供原生 `browser_*` 工具,由插件代为调用 `bsk`,并在 Web UI 中实时展示浏览器会话。 -把它装进某个 dsh profile,然后启动该 profile: +先安装 `bsk` CLI 并连接浏览器扩展,再将最新发布的插件装进 dsh profile 并启动(将 `web` 替换为你的 profile 名称): ```sh -dsh plugin --profile web add @wxg-prc-cpg/browser-skill-dsh-plugin +dsh plugin --profile web add @wxg-prc-cpg/browser-skill-dsh-plugin@latest dsh --profile web ``` -插件自带 skill,所以在 dsh 下无需执行 `bsk install-skill`;但 `bsk` CLI 和浏览器扩展仍是前置条件。工具清单、配置项与观察浮层见[插件 README](packages/dsh-plugin-browserskill/README.md)。 +插件自带 `browser-skill` skill,所以在 dsh 下无需执行 `bsk install-skill`。已安装的插件不会自动更新;升级此插件请运行: + +```sh +dsh plugin --profile web update @wxg-prc-cpg/browser-skill-dsh-plugin --latest +``` + +升级后重启该 profile。用法与配置见[插件 README](packages/dsh-plugin-browserskill/README.md)。 ## 工作原理 diff --git a/packages/dsh-plugin-browserskill/README.md b/packages/dsh-plugin-browserskill/README.md index aee68f7..9406fb6 100644 --- a/packages/dsh-plugin-browserskill/README.md +++ b/packages/dsh-plugin-browserskill/README.md @@ -1,13 +1,51 @@ -# dsh-plugin-browserskill +# BrowserSkill for DeepSeek Harness -npm: [`@wxg-prc-cpg/browser-skill-dsh-plugin`](https://www.npmjs.com/package/@wxg-prc-cpg/browser-skill-dsh-plugin) +[![npm version](https://img.shields.io/npm/v/@wxg-prc-cpg/browser-skill-dsh-plugin)](https://www.npmjs.com/package/@wxg-prc-cpg/browser-skill-dsh-plugin) -A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) tool plugin that exposes -[BrowserSkill](https://github.com/Tencent/BrowserSkill) (`bsk`) browser automation to the model. +Use [BrowserSkill](https://github.com/Tencent/BrowserSkill) in +[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) to browse +websites, fill forms, and capture screenshots through native `browser_*` tools. +Browser tasks run in Agent Windows, with a live view in the dsh Web UI. -Each action maps to one `bsk --json` invocation: the plugin spawns the bsk CLI, parses its -structured JSON output, and returns a canonical typed value. The bsk daemon, browser, and browser -extension keep owning the actual browser control — this package is a thin, well-typed bridge. +## Installation + +Before installing the plugin: + +- Install [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) and + [pnpm](https://pnpm.io/installation), which dsh uses to manage plugins. +- Install the `bsk` CLI and connect the BrowserSkill extension in Chrome or Edge. + Follow the [BrowserSkill setup guide](https://github.com/Tencent/BrowserSkill#quick-start). +- Make sure `bsk` is on the `PATH` used to start dsh, or set `bskPath` in the plugin configuration. + +Install the latest published version into the `web` profile, then start it: + +```sh +dsh plugin --profile web add @wxg-prc-cpg/browser-skill-dsh-plugin@latest +dsh --profile web +``` + +Replace `web` with your profile name if you use a different profile. The plugin +includes the `browser-skill` skill; no separate `bsk install-skill` step is needed. + +In a conversation, try: + +```text +/browser-skill open example.com and summarize the page. +``` + +By default, the browser tools become available when the skill is invoked. + +## Updating + +`@latest` selects npm's latest published version when you install. Installed plugins +do not update automatically. To upgrade this plugin in your profile: + +```sh +dsh plugin --profile web update @wxg-prc-cpg/browser-skill-dsh-plugin --latest +``` + +Restart that dsh profile after upgrading. This command updates the plugin; update +the `bsk` CLI and browser extension separately when a release requires it. ## Tools @@ -20,23 +58,7 @@ extension keep owning the actual browser control — this package is a thin, wel | `browser_tabs` | `list`, `create`, `select`, `close`, `borrow`, `return` | Manage Agent Window tabs and temporarily borrow user tabs. | | `browser_assist` | `resize`, `emulate`, `request-help` | Resize or emulate the browser and pause for human-only steps. | -`bsk evaluate` and `bsk record` are intentionally not exposed by this plugin: arbitrary page -evaluation is a higher-risk capability, while recording is long-running and needs a dedicated -DeepSeek Harness lifecycle before it can be added safely. - -## Agent skill (progressive disclosure) - -Beyond the tools, the plugin publishes the **`browser-skill` agent skill** through the harness's -official skill seam (`ctx.skills.register`): the catalog entry (name + routing description) is -resident in ``, and the body is loaded only when the model invokes the `skill` -tool. Its single source is the DSH-specific `skill/SKILL.md`, which documents only structured -`browser_*` calls and their plugin semantics. The repository-root CLI skill is intentionally not -concatenated: its command examples belong to a different execution interface and would bypass -the plugin's ownership, live observation UI, cancellation, and cleanup path if followed directly. -The build rejects internal CLI-name leakage, command-line code blocks, unknown browser tools, and -missing supported browser tools before embedding the Markdown. Registration and every pre-step -catalog snapshot are pure in-memory reads (no disk/process/daemon); compositions without the skill -seam degrade silently. +Arbitrary page-script evaluation and interaction recording are not supported. ## Multi-session model @@ -55,136 +77,37 @@ an explicit `session` argument naming a foreign or unknown id is rejected, the ` `browser_session` shows plugin-created sessions only (no daemon-wide view), and stop/unload cleanup can never touch a session owned by another program. -## Installation - -The plugin follows the standard dsh bundle layout (`dsh.bundle` manifest + `cordis.patch.yml`): - -```sh -dsh plugin --profile add @wxg-prc-cpg/browser-skill-dsh-plugin -dsh --profile -``` - -Prerequisite: the `bsk` CLI must be installed and on `PATH`, and the BrowserSkill browser extension -(Chrome or Edge) must be connected — see the -[BrowserSkill README](https://github.com/Tencent/BrowserSkill). When bsk is missing, tool calls fail -with install guidance instead of a bare spawn error. - ## Configuration -All fields are optional and validated through the plugin's Schemastery `Config`: +All plugin configuration fields are optional: -```yaml -# cordis.patch.yml override example -- insert: - - id: browserskill - name: "@wxg-prc-cpg/browser-skill-dsh-plugin" - config: - bskPath: bsk # path to the bsk binary (default: resolve from PATH) - defaultTimeoutMs: 120000 - maxSessions: 5 - # observationEnabled: true # live PiP/overlay observation (below) - # thumbnailIntervalMs: 1500 # frame cadence while a session is active - # idleIntervalMs: 8000 # idle cadence / recent-activity window - # lazyTools: true # reveal browser_* tools only after the skill is invoked -``` +| Option | Default | Purpose | +| --- | --- | --- | +| `bskPath` | `bsk` | Path to the CLI binary. | +| `defaultTimeoutMs` | `120000` | Default command timeout in milliseconds. | +| `maxSessions` | `5` | Maximum concurrent sessions started by this plugin. | +| `observationEnabled` | `true` | Enable live browser observation. | +| `thumbnailIntervalMs` | `1500` | Screenshot interval for active sessions, in milliseconds. | +| `idleIntervalMs` | `8000` | Screenshot interval for idle sessions and the recent-activity window, in milliseconds. | +| `lazyTools` | `true` | Reveal the browser tools when the skill is invoked. Set `false` to register them at startup. | -- **`lazyTools` (default `true`)** — the final progressive-disclosure stage: the six - `browser_*` tool schemas stay OUT of the system prompt (zero schema tokens) until the - `browser-skill` skill is actually invoked — the skill catalog entry is the only - advertisement. One successful invocation (model tool call, or a `/browser-skill` user - gesture) registers the whole suite for the rest of the process; repeated invocations are - no-ops, and sessions resumed with a past invocation in their durable log reveal the suite - on entry. Set `false` for the legacy always-on registration. +## Live browser view -## Observation overlay (PiP mini-window) +The dsh Web UI shows the plugin's browser sessions in a floating panel. If your +profile provides the `dsh-better-sidebar` integration, the view appears in a +**Browser Skill** sidebar tab instead. -When the plugin runs inside the dsh Web UI, an **observation overlay** floats over the app -(registered into the `shell.overlay` seat): a breathing thumbnail per owned session plus its -current action and elapsed time. The card docks at the top-right of the content area (clear of -the composer and the shell's header controls) and wears the **BrowserSkill product family's own -look**: the overlay reuses `@browser-skill/ui` components (`Button`, `cn`) and its oklch design -tokens (`--card`, `--primary`, `--destructive`, `--ring`, …), status dots spec'd after the -extension popup's connection indicator, and Remix icons. The BSK utility sheet is compiled -scoped under the `.bsk-obs` root class (`scripts/build-client-css.mjs`), so the overlay looks -like the BrowserSkill extension without leaking a single selector into the host shell — and the -shell's theme cannot bleed back in. +- See the current action, elapsed time, and recent screenshot for each session. +- Select a session to focus on it. The sidebar view follows the current conversation. +- Use **Interrupt** to cancel the current browser command. The agent may continue + with another action afterward. +- Drag or resize the floating panel, or use **Pop out** to open a Picture-in-Picture + window in browsers that support it. +- Periodic screenshots are requested while a browser observation view is visible. + Configure the active and idle intervals with the options above. -- **Lifecycle**: hidden while the plugin owns no sessions; appears on the first - `browser_session` start action; disappears when all sessions stop (or the plugin unloads). -- **Focus view**: status row (green/idle/red dot + session + action + mm:ss), the latest page - frame (while viewed, refreshes every ~1.5s while active, ~8s when idle; the last good frame stays on - stage while the next one loads, and is kept on errors so the card does not flash), - and a compact icon toolbar (Interrupt + Pop out, hover for the label). -- **Interrupt**: one click kills the in-flight bsk command of the focus session (same semantics - as the chat Stop button — the current action fails, the agent run may continue). Strip items - carry their own hover interrupt button. -- **Multi-session strip**: every session gets a tile (mini frame + id + status dot); focus - auto-follows the most recently active session; clicking a tile pins focus (pin badge, click - again to release); errored sessions get a red edge without stealing focus; sessions the daemon - lost are greyed out; prolonged daemon/browser outage shows "browser unavailable" and greys - the interrupt button until captures recover. -- **Drag & resize**: drag the header to move the card; drag any of the four - corners to resize (min 240×180, max 80% of the viewport; no visible grip). - Both are remembered for the page lifetime. -- **Pop out (PiP)**: upgrades the card into a native Document PiP window (requires a user - gesture, per browser rules), sized from the current card; closing the PiP falls back to the - in-page card with state intact. Browsers without Document PiP simply hide the button. -- **Screenshot demand**: the client requests periodic screenshots while an observation view is - visible, using the PiP document's visibility when popped out. Hidden or collapsed views can - keep subscribing to state without requesting screenshots. The first screenshot subscriber - starts capture scheduling; the last one's departure cancels timers and skips queued captures. - An already running capture may finish. Demand applies to the service's owned sessions, not just - the selected session. -- **Wire**: `GET /bsk-observation/events` is the live synchronization channel (SSE). Every connection, - including reconnects, starts with `{ type: "snapshot", sessions, available }`, followed by - `upsert`, `remove`, `reset`, and `availability` events on the same stream. Replace local state - with each snapshot before applying subsequent events. Use `?thumbnails=0` for state-only - subscriptions or `?thumbnails=1` to request screenshots; omitting the parameter defaults to - requesting screenshots for compatibility. Closing the stream releases its screenshot demand. - `GET /bsk-observation/state` remains a one-time `{ sessions, available }` read and does not - request screenshots. It has no ordering guarantee relative to a separate SSE connection; - live clients should initialize and resynchronize from the stream's snapshot instead. - The host also serves `POST /bsk-observation/interrupt`, `POST /bsk-observation/stop`, and - `GET /bsk-observation/thumbnail/` through the dsh `webServer` route seam - (dsh 0.1's Typert Remote pipeline is closed to out-of-tree packages). All commands for one - session — tool calls and frame captures alike — run through a per-session FIFO, because the - daemon accepts only one unfinished command per session. -- **Programmatic subscription**: `ObservationService.subscribe(listener, { thumbnails: false })` - subscribes to state without requesting screenshots. On an active service, the listener receives - the initial `snapshot` synchronously, before `subscribe` returns, then receives subsequent - changes. `thumbnails` defaults to `true`, so callers should choose it explicitly. The returned - unsubscribe function is safe to call repeatedly and releases this subscription's screenshot - demand. Subscribing after service disposal is a no-op. -- **Trust model**: these routes expose live screenshots (and an interrupt write), so they - replicate the browser-trust fence dsh applies to its own `/api` routes: the request Host - must be a loopback authority (`localhost`, `127.0.0.0/8`, `[::1]`), a present Origin must - match the Host, `sec-fetch-site: cross-site` is refused, and POST requires an - `application/json` body (cross-site simple requests can never satisfy that). The channel is - therefore built for **loopback-only serving** — binding the dsh web server to `0.0.0.0` and - reaching it through a LAN address will (deliberately) fail the fence; do not put these - routes behind a non-loopback reverse proxy without adding your own authentication. -- Configure with `observationEnabled` / `thumbnailIntervalMs` / `idleIntervalMs`. - -## Behavior notes - -- **Cancellation**: aborting a tool call (`exec.signal`) kills the underlying bsk child process, - matching BrowserSkill's cooperative tool-cancellation model. -- **UI cards**: calls render as terminal cards (command line as title, output as the completed - card). Screenshots additionally attach the image itself when the host mounts an attachment store - and the active model route declares image input; otherwise the PNG path is returned. -- **Web UI toolview (browser half)**: the package is dual-face. `dsh.client` (platform `web`) ships - `lib/client.cjs`, which registers a keyed `tool.call.toolview` view for `browser_inspect`. The - custom view keeps a terminal block for every inspect action and, when a screenshot 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. - The other five tools keep the stock terminal card. The bundle follows the dsh client - contract: a CJS closure factory handed to `window.__ModuleLoader__.load`, platform modules - (`react`, `dsh-client-ui-*`) external, everything else inlined, CSS Modules compiled by - lightningcss. -- **Errors**: non-zero bsk exits surface the CLI's JSON error envelope (`code`, `message`, `hint`) - so the model gets the daemon's actionable guidance. -- **Long-running work** (e.g. `bsk record`) is not backgrounded via `ctx.jobs` yet — tracked as a - follow-up. +The observation endpoints require a loopback address such as `localhost` or +`127.0.0.1`. Access through a LAN hostname or non-loopback reverse proxy is not supported. ## Development @@ -195,22 +118,39 @@ pnpm --filter @wxg-prc-cpg/browser-skill-dsh-plugin test # unit tests mock b pnpm --filter @wxg-prc-cpg/browser-skill-dsh-plugin build # tsdown -> lib/ ``` +See the [development notes](https://github.com/Tencent/BrowserSkill/blob/main/packages/dsh-plugin-browserskill/docs/development.md) +for skill registration, tool results, and observation APIs in the current source. + ## Publishing -The GitHub Actions workflow **Release dsh plugin** publishes this package to npm -as `@wxg-prc-cpg/browser-skill-dsh-plugin`. The Cordis plugin id, client -bundle registration, and `cordis.patch.yml` name are the same specifier so -dsh can import and materialize the plugin without a ModuleLoader id mismatch. +The [Release dsh plugin workflow](https://github.com/Tencent/BrowserSkill/blob/main/.github/workflows/release-dsh-plugin.yml) +publishes the package and this README to npm. Pushing a `dsh-plugin-vX.Y.Z` tag +triggers it; ordinary commits to `main` do not. -Trigger it by pushing a tag that matches `package.json`'s `version`: +1. Update this README and prepare a new stable version using the repository's + [release script](https://github.com/Tencent/BrowserSkill/blob/main/scripts/release.mjs). + CLI, extension, and DSH plugin versions are coordinated by that script. +2. Commit the release changes before tagging. From the repository root, derive the + tag from the plugin's `package.json` so the versions match: -```sh -git tag dsh-plugin-v0.1.2 -git push origin dsh-plugin-v0.1.2 -``` + ```sh + version=$(node -p "require('./packages/dsh-plugin-browserskill/package.json').version") + git tag "dsh-plugin-v${version}" + git push origin "dsh-plugin-v${version}" + ``` -Or run the workflow from the Actions tab (`workflow_dispatch`). The job reads the -`NPM_TOKEN` secret from the `npm-publish` GitHub Environment. + If the release script already created the tag, run the version assignment and + push command, skipping `git tag`. +3. The workflow checks the version, runs typechecks and tests, builds the package, + publishes it, and verifies npm's `latest` version and README. + +You can also run the workflow manually from GitHub Actions on the intended release +ref. Both triggers require an unpublished version and the `NPM_TOKEN` secret in the +`npm-publish` GitHub Environment. + +npm updates the package README only when a new version is published, including +for documentation-only changes. Published versions cannot be overwritten. See +[npm's README update rules](https://docs.npmjs.com/about-package-readme-files/). ## License diff --git a/packages/dsh-plugin-browserskill/docs/development.md b/packages/dsh-plugin-browserskill/docs/development.md new file mode 100644 index 0000000..75d637f --- /dev/null +++ b/packages/dsh-plugin-browserskill/docs/development.md @@ -0,0 +1,84 @@ +# DSH plugin development notes + +For installation, usage, and configuration, see the [plugin README](../README.md). +These notes describe the current source on this branch; a published npm version may +not include later changes. + +## Tool execution + +The plugin runs the `bsk` CLI and parses its JSON output. The daemon and browser +extension perform the browser operations. The published package name is also the +Cordis plugin id, client bundle registration id, and `cordis.patch.yml` plugin name. +Keep these identifiers aligned so dsh can load both halves of the plugin. + +## Agent skill + +Beyond the tools, the plugin publishes the **`browser-skill` agent skill** through the harness's +official skill seam (`ctx.skills.register`): the catalog entry (name + routing description) is +resident in ``, and the body is loaded only when the model invokes the `skill` +tool. Its single source is the DSH-specific `skill/SKILL.md`, which documents only structured +`browser_*` calls and their plugin semantics. The repository-root CLI skill is intentionally not +concatenated: its command examples belong to a different execution interface and would bypass +the plugin's ownership, live observation UI, cancellation, and cleanup path if followed directly. +The build rejects internal CLI-name leakage, command-line code blocks, unknown browser tools, and +missing supported browser tools before embedding the Markdown. Registration and every pre-step +catalog snapshot are pure in-memory reads (no disk/process/daemon); compositions without the skill +seam degrade silently. + +## Observation subscriptions and routes + +- **Screenshot demand**: the client requests periodic screenshots while an observation view is + visible, using the PiP document's visibility when popped out. Hidden or collapsed views can + keep subscribing to state without requesting screenshots. The first screenshot subscriber + starts capture scheduling; the last one's departure cancels timers and skips queued captures. + An already running capture may finish. Demand applies to the service's owned sessions, not just + the selected session. +- **Wire**: `GET /bsk-observation/events` is the live synchronization channel (SSE). Every connection, + including reconnects, starts with `{ type: "snapshot", sessions, available }`, followed by + `upsert`, `remove`, `reset`, and `availability` events on the same stream. Replace local state + with each snapshot before applying subsequent events. Use `?thumbnails=0` for state-only + subscriptions or `?thumbnails=1` to request screenshots; omitting the parameter defaults to + requesting screenshots for compatibility. Closing the stream releases its screenshot demand. + `GET /bsk-observation/state` remains a one-time `{ sessions, available }` read and does not + request screenshots. It has no ordering guarantee relative to a separate SSE connection; + live clients should initialize and resynchronize from the stream's snapshot instead. + The host also serves `POST /bsk-observation/interrupt`, `POST /bsk-observation/stop`, and + `GET /bsk-observation/thumbnail/` through the dsh `webServer` route seam + (dsh 0.1's Typert Remote pipeline is closed to out-of-tree packages). All commands for one + session — tool calls and frame captures alike — run through a per-session FIFO, because the + daemon accepts only one unfinished command per session. +- **Programmatic subscription**: `ObservationService.subscribe(listener, { thumbnails: false })` + subscribes to state without requesting screenshots. On an active service, the listener receives + the initial `snapshot` synchronously, before `subscribe` returns, then receives subsequent + changes. `thumbnails` defaults to `true`, so callers should choose it explicitly. The returned + unsubscribe function is safe to call repeatedly and releases this subscription's screenshot + demand. Subscribing after service disposal is a no-op. +- **Trust model**: these routes expose live screenshots (and an interrupt write), so they + replicate the browser-trust fence dsh applies to its own `/api` routes: the request Host + must be a loopback authority (`localhost`, `127.0.0.0/8`, `[::1]`), a present Origin must + match the Host, `sec-fetch-site: cross-site` is refused, and POST requires an + `application/json` body (cross-site simple requests can never satisfy that). The channel is + therefore built for **loopback-only serving** — binding the dsh web server to `0.0.0.0` and + reaching it through a LAN address will (deliberately) fail the fence; do not put these + routes behind a non-loopback reverse proxy without adding your own authentication. + +## Tool results and cancellation + +- **Cancellation**: aborting a tool call (`exec.signal`) kills the underlying bsk child process, + matching BrowserSkill's cooperative tool-cancellation model. +- **UI cards**: calls render as terminal cards (command line as title, output as the completed + card). Screenshots additionally attach the image itself when the host mounts an attachment store + and the active model route declares image input; otherwise the PNG path is returned. +- **Web UI toolview (browser half)**: the package is dual-face. `dsh.client` (platform `web`) ships + `lib/client.cjs`, which registers a keyed `tool.call.toolview` view for `browser_inspect`. The + custom view keeps a terminal block for every inspect action and, when a screenshot 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. + The other five tools keep the stock terminal card. The bundle follows the dsh client + contract: a CJS closure factory handed to `window.__ModuleLoader__.load`, platform modules + (`react`, `dsh-client-ui-*`) external, everything else inlined, CSS Modules compiled by + lightningcss. +- **Errors**: non-zero bsk exits surface the CLI's JSON error envelope (`code`, `message`, `hint`) + so the model gets the daemon's actionable guidance. +- **Long-running work** (e.g. `bsk record`) is not backgrounded via `ctx.jobs` yet — tracked as a + follow-up.