# dsh-localnotify > **🌐 中文**: [README.md](README.md) · **🤖 Agent Guide**: [README.agent.md](README.agent.md) ![CI](https://github.com/yakoylp/dsh-localnotify/actions/workflows/test.yml/badge.svg) ![Version](https://img.shields.io/github/v/release/yakoylp/dsh-localnotify?label=version) ![License](https://img.shields.io/badge/license-MIT-blue) A local notification center plugin for DSH (DeepSeek Harness): adds a **Notifications** entry to the Web UI sidebar, opening a full-screen **notification center**. Notifications are shown as cards (title + one-line body preview) with **level color coding**, time/source filtering, sorting, title/body search, read/unread marking, and deletion. Clicking a card opens a **detail popup** with the full information and **one-click copy** of the title/body. Notifications are written by the **agent tool** (`notify_add`) or the **CLI** (`dsh-localnotify`) into a local JSON file, and the page **refreshes in real time** — fully local, no external services. > Typical use: the agent writes a notification to remind you when a task finishes; scripts and cron jobs can deliver notifications through the CLI. ## Features - **Sidebar Notifications entry**: bell icon + unread-count badge (dot badge in collapsed rail), beside Settings at the sidebar foot — additive, does not cover existing UI - **Full-screen notification center** (`shell.overlay`, independent of any session): - Card list: title + **one-line body preview** (ellipsis when truncated), unread cards highlighted with a level-colored left border and dot; compact card height - **Level color coding**: `info` (brand) / `success` (green) / `warn` (yellow) / `error` (red); level tags shown on cards and in the detail popup - **Pagination**: bottom bar with "per page 10/20/50/100" (default 20) and prev/next paging (shows `N total · page x/y`); only the current page is rendered - **Time filter**: All / Today / Last 7 days / Last 30 days - **Source filter**: filter by notification source (agent / cron / cli …, collected automatically) - **Sorting**: newest first (default) / oldest first / unread first - **Title + content search**: real-time filtering (case-insensitive) - **Detail popup**: clicking a card shows full info (full timestamp / level / source / read state + full body) with **one-click copy** of title, body, or both (the button shows "已复制 ✓" after copying); opening a detail auto-marks it read - "Mark all read" button; card "Delete" with two-step confirmation (prevents accidental deletion) - Close via the close button or clicking the backdrop - **Real-time refresh**: polls every 3s while the page is open, every 30s otherwise (agent/CLI writes appear within seconds) - **Theme aware**: all colors use DSH official theme tokens (`--dsw-alias-*`), following light/dark themes automatically - **i18n**: UI text follows the dsh web language automatically (zh / en; switches take effect live) - **Auto cleanup**: optional retention (in days); older notifications are removed automatically on each write (disabled by default; the 500-cap always applies) - **Write paths** (no create-form in the UI, keeping the interface clean): - **Agent tool**: the agent calls `notify_add` during a conversation (supports `level`) - **CLI**: `dsh-localnotify add "title" -b "body" [--level success]`, for scripts / cron ## Data storage Notifications live in **`~/.dsh/notify/notifications.json`** (`DSH_HOME` takes precedence), shaped as: ```json { "version": 2, "notifications": [ { "id": "n_mtisij3d_q32zcn", "title": "Task done", "body": "23 pages converted to Markdown.", "createdAt": 1788274582825, "read": false, "source": "agent", "level": "success" } ], "settings": { "retainDays": 0 } } ``` - `level`: `info | success | warn | error` (missing on old data renders as `info`) - `settings.retainDays`: auto-cleanup retention in days; `0` = keep forever (default); `>0` removes notifications older than N days on each write (set via CLI `config --retain-days`). version-1 files are upgraded to version 2 on the first write - Writes are **atomic** (temp file + rename), so a partial file can never appear - A missing or corrupt file falls back to an empty store and is **never overwritten** - In-process writes are serialized; at most 500 notifications are kept (oldest trimmed) - The file is transparent and editable, easy to back up or migrate with `~/.dsh` ## Install ### From GitHub (recommended) ```sh dsh plugin --profile web add github:yakoylp/dsh-localnotify ``` ### From a local directory (development) Clone the repository (or unpack the source archive) anywhere, then install from that directory: ```sh git clone https://github.com/yakoylp/dsh-localnotify.git dsh plugin --profile web add ./dsh-localnotify # macOS / Linux dsh plugin --profile web add "C:\path\to\dsh-localnotify" # Windows (use your real path) ``` **Restart dsh web** after installing (the client bundle is loaded at web startup). The 🔔 Notifications entry appears at the sidebar foot; verify with `dsh-localnotify add "test"`. ## CLI ```sh dsh-localnotify add "Task done" -b "converted" # add a notification dsh-localnotify add "Reminder" --source cron --level warn # custom source & level dsh-localnotify add "Deploy OK" --level success # level: info/success/warn/error dsh-localnotify list # list (* = unread; [E]/[W]/[OK] level marks) dsh-localnotify list --unread # unread only dsh-localnotify list --level error # error-level only dsh-localnotify list --json # JSON output (script-friendly) dsh-localnotify read n_xxxx # mark one as read dsh-localnotify read --all # mark all as read dsh-localnotify delete n_xxxx # delete one dsh-localnotify config [--retain-days 7] # show/set auto-cleanup days (0=keep forever) dsh-localnotify prune # clean up expired notifications now dsh-localnotify --help # help dsh-localnotify --file /path/to/notifications.json add ... # override storage path ``` The CLI reads/writes the same JSON file directly and **does not require cordis** — usable in any terminal, script, or cron job. ## Agent tool The plugin registers `notify_add`; the agent calls it in-conversation to write notifications (validated: title required ≤200 chars, body ≤5000 chars). Example when a task completes: ``` notify_add(title: "Conversion done", body: "23 scanned pages converted to Markdown ...", level: "success") ``` `level` is an optional enum `info | success | warn | error` for visual coding: `success` for completed tasks, `error` for failures, `warn` for things needing attention, omit for ordinary messages (default `info`). ## Notification submission paths (how agents / scripts deliver) | Path | Who uses it | Prerequisite | | --- | --- | --- | | `notify_add` tool | DSH agents (same profile) | Registered automatically on install; agents see the tool in their tool list and call it per its description (task done, important progress, needs attention) — no extra config | | `dsh-localnotify` CLI | scripts / cron / terminal | Installed (bin linked into the profile's `node_modules/.bin`); or `node /lib/cli.js` directly | | Direct JSON file write | any program | Write `~/.dsh/notify/notifications.json` per the format above; visible within ~3s | > Note: the `notify_add` tool is visible to agents **in the same DSH profile where the plugin is installed**; other environments/agents need their own install for the tool path, while the CLI and file write are environment-independent. ## HTTP API The host registers same-origin `POST /dsh-localnotify/api/` (reachable only by the DSH web server); JSON in/out with an `{ ok, value }` or `{ ok: false, error: { code, message } }` envelope: | Method | Args | Description | | --- | --- | --- | | `list` | `{}` | Returns `{ notifications, storagePath }` | | `markRead` | `{ id }` | Mark one as read | | `markAllRead` | `{}` | Mark all as read | | `delete` | `{ id }` | Delete one | ## Architecture - **Node half** (mounted via `cordis.patch.yml`, `lib/index.js`): registers the `notify_add` tool (`ctx.tools.register`) and the `/dsh-localnotify/api/*` route (`ctx.webServer.register`, prefix match); storage logic lives in `lib/store.js` (shared by host and CLI) - **Browser half** (`dsh.client` + `exports["./client"]` → `lib/client.js`): a hand-written ModuleLoader bundle (`window.__ModuleLoader__.load({ id, factory })`, same format as official tsdown output, depending only on the baseline external `react`); registers `sidebar.footer.action` (entry) and `shell.overlay` (center) via `ctx.slots.inject/register`, fetching data from the host API - **Theme**: all colors reference DSH theme tokens (`--dsw-alias-bg-*` / `--dsw-alias-label-*` / `--dsw-alias-border-*` / `--dsw-alias-brand-primary`, etc.) ## Development ```sh node --test # run all tests (store layer + API handler + CLI end-to-end) node lib/cli.js --file ./tmp-notify.json add "test" -b "body" --level success node lib/cli.js --file ./tmp-notify.json list node lib/cli.js --file ./tmp-notify.json config --retain-days 7 ``` ## Troubleshooting - **Sidebar entry visible but the notification center list stays empty**: first check whether the host route is registered — `curl -X POST http://127.0.0.1:/dsh-localnotify/api/list -H "Content-Type: application/json" -d "{}"`. If it returns **405** (empty body), the host route is not registered; upgrade to **≥1.1.0** (1.0.0 silently skipped route registration because `webServer` was missing from `inject`). If it returns `{ ok:true, value: { notifications, ... } }`, the route works — check the storage file for data - **Cards compressed to thin lines with many notifications / no scrollbar**: flex-layout bug in 1.0.0; upgrade to **≥1.1.0** (cards are `flex: none`, the list scrolls properly) ## Known limitations - The notification center is a full-screen overlay (not a routed page); closing returns to the previous session — a deliberate trade-off to stay lightweight - Real-time updates rely on polling (3s/30s); no Host→Client push yet. Polling cost is negligible (small same-origin requests); will be revisited if DSH provides a push channel - Concurrent writers (CLI + host) rely on atomic rename; the last writer wins under extreme concurrency. Write frequency is low for notifications, so this is safe enough (in-process writes are already serialized) - External file edits are picked up on read (visible within ~3s) ## Tests & CI - Test coverage spans three layers: the store (add/validation/level/auto-cleanup/legacy-file compat/corrupt fallback/500 cap), the HTTP API (method dispatch/error envelope/read-write flows), and the CLI (spawn end-to-end: add/list/read/delete/config/prune/error paths) - GitHub Actions runs the full suite on Node 18 / 20 / 22 (`.github/workflows/test.yml`) ## License MIT