Skip to content
dsh.fish
Bundle

dsh-tab-watchdog

Tab Watchdog for DeepSeek Harness Web: when a workspace finishes a work round (green) or needs attention (error, question, approval) the tab title blinks with a badge. Persistent two-half plugin (host + browser).

Source
andreagosto
License
MIT
Updated
Updated 2 days ago

Readme

# dsh-tab-watchdog

A persistent plugin for **DeepSeek Harness Web**: when one of your workspaces/sessions finishes a work round, errors out, or needs your input, the browser tab signals it — a little **whale** (the DeepSeek Harness logo) blinks **green** or **yellow** in the favicon, and the tab title shows the overall pending counter.

This is the "bundle" version (persistent, installable, shareable) of the **Tab Watchdog** dynamic plugin originally developed in Creator mode. Unlike a dynamic plugin it **survives `dsh` restarts and browser reloads (F5)**.

## How it works (architecture)

Two halves in a single npm package, like the official plugins:

- **Host half** (`index.js`) — profile composition row (`cordis.patch.yml` + `dsh.bundle.patch`). It makes the package a Loader entry: that row is what pulls the browser half into the boot manifest. No business logic here.
- **Browser half** (`lib/client.js`, served via `dsh.client` + the `./client` export) — a bundle in the `window.__ModuleLoader__.load({ id, factory })` format. It subscribes to the **Host events forwarded to the browser** (`ctx.remote.$on`) and handles everything (state, watermark, blink). No UI is added to the harness.

### Triggers (what makes it blink)

The browser only receives the events on the `dsh-api-remotes` allowlist. The watchdog uses:

| Event | Level | Label |
|---|---|---|
| `api-session/status(sessionId, running=false)` | 🟢 green whale | "Finished a work round" |
| `api-session/error(sessionId, message)` | 🟡 yellow whale | "Error…" |
| pending `approval` interaction | 🟡 yellow whale | "Requires approval" |
| pending `question` / `plan-review` interaction | 🟡 yellow whale | "Asking for a response" |

Green/errors use the forwarded `emit` events (`ctx.remote.$on`). Yellow for questions and approvals does **not** rely on the `user-questions/request`/`approval/request` waterfall events: those are single-consumer and claimed in registration order by the official UI answerers, and the forwarded request does not carry a stable session id. Instead the watchdog polls the state the official UI publishes while it waits on you: `ctx.uiSession.pendingInteractions.getSnapshot()` — a `Map<sessionId, interaction>` with `kind` ∈ `approval` | `question` | `plan-review`. The poll runs in the existing 1s tick; entries are re-bumped while pending and removed when the interaction clears, so an answer stops the blink by itself. This covers **every** session in the app, not just the one in view.

Note: `workflow/end`, `agent/status`, and `goal/changed` are **not** forwarded to the browser in this version's allowlist; observing them would require host-side aggregation over a custom channel (out of scope by design). The yellow path depends on the app's own pending-interaction UI (`ui-approval`/`ui-user-questions`) publishing its wait state; if a third-party answerer claims a request without going through that UI, that interaction cannot be seen from the browser.

### Blink model (watermark)

- Events only arrive **while the page is open** (even on another browser tab — that is exactly the intended use case).
- New events stay "pending" until you **come back** to the page.
- While the page is not focused and at least one item is pending, the **favicon whale** blinks (alternating with the original favicon) and the title alternates between `N · <title>` and `<title>`, where `N` is the **overall** pending count across sessions and levels.
- Whale color: **green** when only finished rounds are pending, **yellow** when there is any error/question/approval, and it **alternates green↔yellow** when both kinds are pending at once.
- The original favicon is captured on load and always restored when you come back (or when pending clears) — nothing is permanently altered.
- An event that arrives **while you are focused** is marked read immediately (no leftover blinking).
- When a session **restarts** (`running=true`), its pending entries are removed.
- An error on a session "freezes" its entry in yellow: a later `running=false` does not downgrade it to green.

## Installation (local `web` profile)

From the plugin checkout:

```sh
# from the directory that contains dsh-tab-watchdog/
npx @deepseek-ai/dsh@latest plugin --profile web add ./dsh-tab-watchdog
```

Then restart `dsh web`. The browser half activates on the next reload/startup. The added row is the `tab-watchdog` entry in the profile's `cordis.yml`.

Removal:

```sh
npx @deepseek-ai/dsh@latest plugin --profile web remove dsh-tab-watchdog
```

> `dsh plugin` forwards to pnpm inside the profile. If pnpm is not installed: `corepack enable` or `npm i -g pnpm`.

## Development

The browser half source is `src/client.js` (a CJS fragment living inside the loader factory). The `lib/client.js` artifact is **committed** (no build step at git-install time).

```sh
npm run build   # regenerates lib/client.js from src/client.js
npm test        # verify (bundle structure) + smoke (blink/watermark logic)
```

No `prepare`/monorepo toolchain needed: out-of-tree plugins cannot use the internal `clientBundle` preset, so the loader format is reproduced by hand (`scripts/build.mjs`).

## Sharing with the community

The package declares `dsh.bundle.patch`, so it installs as a profile bundle. Because `lib/client.js` and `index.js` are committed artifacts, a **git install works without `prepare`**:

```sh
npx @deepseek-ai/dsh@latest plugin --profile web add github:andreagosto/dsh-tab-watchdog
```

For discoverability, the GitHub repository carries the **`dsh-plugin`** topic (see the official deepseek-harness README, "Community and support"). Alternatively: `npm publish`, then `dsh plugin --profile web add dsh-tab-watchdog`.

## Notes and limitations

- Tested against runtime `@deepseek-ai/dsh` **0.1.2-rc.1** (mind the "breaking changes" notice on rc prereleases).
- The browser half is hand-written against the APIs observed in the installed bundles; the structure is validated by `npm test`, but verify it live in your own profile.
- No settings namespace: the plugin does not appear in Settings → Plugins. To disable it use `dsh plugin remove` (or set `disabled: true` on the row in the profile patch).

## License

MIT

Install

dsh plugin --profile web add github:andreagosto/dsh-tab-watchdog

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source