# Development

## What this plugin does

Host-side, OS-level notifications for DeepSeek Harness on Windows. It listens
to three existing DSH host events and spawns a tiny self-built .NET 4 helper
EXE (`DshToast.exe`) that raises a WinRT `ToastNotification`; toasts persist in
Notification Center under "DeepSeek Harness" with the black whale icon.

Only Windows is supported. `package.json` declares `"os": ["win32"]`
(install-time gate) and `apply()` contains a runtime gate
(`isSupportedPlatform(process.platform)`); on other platforms it logs once and
registers nothing, so installing the package on Linux/macOS is harmless.

## Events

| Event | Condition | Toast |
|---|---|---|
| `agent/status` | a user-facing **root** agent goes `running → idle` | `任务完成` + session title |
| `tools/execute` | tool name is `ask_user_question` | `需要您的输入` + question digest |
| `approval/request` | a root agent requests approval | `需要您的审批` + tool/reason |

### Why `tools/execute`, not `tools/result`

The DSH tool pipeline is `pre-execute → execute → post-execute → result`. For
`ask_user_question` the question modal is presented **while the `execute` body
runs** and that call blocks until the human answers; `tools/result` only fires
after the tool completed, i.e. after the user already answered. Hook
`tools/execute` and always `return next()` — the observer is a waterfall
listener and must never block or short-circuit the tool.

### Noise control

- `agent/status` fires for **every** agent (root agent, subagents, AgentTeams
  members, workflow workers). Only user-facing root agents notify: subagent
  sessions carry `origin: "subagent"` (+ `delegationDepth`) in their session
  header (`isRootAgent`) and are skipped; forked sessions (`parentSession`
  without `origin`) stay user-facing.
- Agent transitions are tracked per-agent in a `WeakMap` so interleaved events
  from concurrent agents cannot corrupt each other's state machine.
- A per-agent `FINISH_COOLDOWN_MS` (10 s) suppresses completion chatter from
  rapid consecutive turns / goal continuation rounds.
- Subagent questions and approvals (auto-rejected anyway) never ping the user.

## The helper EXE

`lib/.dsh-notify/` (source: repo-root `.dsh-notify/`, copied at build):

| File | Purpose |
|---|---|
| `DshToast.exe` | Pre-built WinRT toast raiser (31 KB), csc-compiled, icon embedded via `/win32icon:notify.ico` |
| `DshToast.cs` | .NET Framework 4 source; uses `ToastNotificationManager` + minimal COM interop for the shortcut |
| `notify.ico` | 9 real frames 16…256 px, transparent corners + black-background white whale |
| `whale-black-bg.png` | PNG twin of the icon (documentation / fallback asset) |

Why a self-compiled EXE rather than PowerShell:

- pwsh cannot load the WinRT Toast projection; its balloon tips neither persist
  nor can be branded.
- Windows PowerShell 5.1 can, but the agent sandbox blocks launching
  powershell.exe, and its toasts cannot be re-branded or re-iconed.
- A self-owned EXE owns its AUMID (`DeepSeekHarness.Notify`), its display name
  and its embedded icon, so the toast shows exactly "DeepSeek Harness" + the
  whale icon.

If `DshToast.exe` is missing at runtime, `ensureHelper` recompiles it on the
fly with the system `csc.exe` (ships with .NET Framework 4.x on every Windows
10/11 — zero extra dependencies); the compile is fire-and-forget, the current
round skips its toast, the next one uses the fresh EXE.

### Icon identity chain (why the icon shows)

Windows resolves a toast's small icon through the app identity: AUMID →
Start-Menu shortcut → embedded icon. Three historical fixes matter:

1. `notify.ico` must contain real small frames (16/20/24/32/… px), otherwise
   the 256 px frame downscales to a black blob.
2. The Start-Menu shortcut must point its `SetIconLocation` at `notify.ico`
   and carry the `AppUserModelID` property on disk — `IPersistFile.Save` must
   run **again after** `IPropertyStore.Commit`, otherwise the property is only
   in memory (verified empirically: lnk read back `vt=0`).
3. Registry `Software\Classes\AppUserModelId\DeepSeekHarness.Notify` sets
   `DisplayName` and `IconUri`.
4. Toast XML deliberately carries no `<image>`: `appLogoOverride` renders as a
   giant circle in Notification Center; the identity channel shows the proper
   small icon instead.

If a **new** toast still lacks the small icon after a fix: Windows caches the
toast app identity — re-login or reboot once. Old toasts in Notification
Center never change their icon.

## Build

TypeScript sources live under `src/`; `lib/` is generated by esbuild and
committed so the packaged tree matches the source (`build.mjs`):

- bundles `src/index.ts` into `lib/index.js` (ESM, node platform, node built-in
  modules kept external; the `@deepseek-ai/cordis` import is type-only and
  erased at build);
- copies `.dsh-notify/` → `lib/.dsh-notify/` (the plugin resolves helper paths
  as `join(__dirname, '.dsh-notify')` — relative to the built entry);
- copies `src/types/**` → `lib/types/**`.

Zero runtime dependencies; `@deepseek-ai/cordis` is dev-only for typing.

## Test

```sh
npm run check      # typecheck → build → vitest → smoke (CI runs this)
npm run typecheck  # tsc --noEmit on src/ + test/
npm run build      # regenerate lib/
npm test           # vitest run — behavior tests (node env)
npm run test:smoke # node test/smoke.mjs — built-artifact loading contract
```

- `test/core.test.ts` — pure functions from `src/core.ts`: `truncate`,
  `isSupportedPlatform` ('win32' only), `isRootAgent` (subagent origin /
  delegationDepth / permissive fallbacks), `resolveSessionTitle`,
  `resolveQuestionText` (multi-question count, aliases, truncation),
  `resolveApprovalText` (tool/reason variants).
- `test/plugin.test.ts` — `apply()` against a fully mocked ctx (no real
  subprocess, no Windows APIs), platform-adaptive:
  - Windows: registers the three listeners; drives `agent/status`
    running→idle → spawns `DshToast.exe` with `任务完成`; subagent completion
    churn and cooldown produce no toast; `ask_user_question` → `需要您的输入`
    and the waterfall is always released; subagent questions don't ping;
    approval → `需要您的审批`; session titles flow into the body.
  - Non-Windows: `apply()` registers nothing and warns.
- `test/smoke.mjs` — imports the **built** `lib/index.js`, asserts the export
  shape, listener registration, `HELPER_DIR` asset completeness
  (`DshToast.exe`/`.cs`/`notify.ico`/`whale-black-bg.png`) and, on Windows,
  one end-to-end mock event → toast argv; on other platforms it asserts the
  platform guard.

## Release

```sh
npm version patch
npm run check
npm publish --access public
git push --follow-tags
```

The `prepack` script rebuilds `lib/` before publish, so the tarball always
carries fresh output. The npm `repository` field points back at
`github.com/kongdexu/dsh-win-notify` — the awesome-dsh-plugin storefront links
the registry package to the listed repo automatically from that field (a
hand-written `npm:` key in the listing YAML is rejected).

## Store listing (awesome-dsh-plugin)

The plugin market (dsh-market, and the storefront at awesome-dsh-plugin.com)
serves `data/plugins/<owner>__<repo>.yml` entries from the
`awesome-dsh-plugin/awesome-dsh-plugin` repo. Submission:

```yaml
url: https://github.com/kongdexu/dsh-win-notify
name: kongdexu/dsh-win-notify
category: notify            # not 'ui' — what the plugin does is notify
description:
  en: 'Real Windows toasts ...'
  zh: '真正的 Windows 系统通知：...'
```

Gate checks: `dsh.bundle` manifest + `cordis.patch.yml` (this repo has them),
repo ≥ 1 day old and ≥ 10 commits, `dsh-plugin` GitHub topic, regenerated
READMEs via `node scripts/generate-readme.mjs` after `npm ci` in a checkout of
the catalog repo.