# dsh-session-colorful-unread-pin-jobs [中文](./README.md) | **English** **See the state of every session in the DSH sidebar at a glance, by color.** Session titles are painted by state: **unread** (blue while the model is running, green once it has finished), **pinned** (yellow), and **holding live background shell jobs** (purple). When several states overlap they are composed into **one gradient** in a fixed semantic order — pinned → background → unread — reading left to right. Colors are derived from the current theme's brand color and adapt to light, dark, and monochrome themes automatically. ## What it looks like ![Every session title in the sidebar painted by state: pinned yellow, unread blue/green, background jobs purple, composed into one gradient](assets/screenshot-1-overview.png) ![The session row "…" menu: pin, mark unread, rename, archive](assets/screenshot-2-menu.png) ![Settings → "Session unread": every unread session, mark-read per row, clear-on-send toggle](assets/screenshot-3-settings.png) > These are **illustrative mockups** generated by `assets/mockups/generate.mjs` using the real DSH theme tokens — not screenshots, and they contain no user session data. ## Highlights | State | Color | Trigger | |---|---|---| | Unread · running | brand-blue gradient | The model produced content in **any** session (**including the one you have open**) and that session is still running | | Unread · finished | green gradient | The condition above stops holding (the session went idle) — the job is done | | Pinned | yellow gradient | Session row "…" menu → Pin; pinned sessions sort first under every sort mode | | Background jobs | purple gradient | The session currently holds **live** shell-kind background jobs (a `run_in_background` bash, etc.) | | Several at once | yellow → purple → blue/green | Composed in semantic order: pin leftmost, background in the middle, unread rightmost | - **Nothing to watch**: a session that finishes turns blue → green on its own, so one glance at the sidebar tells you which sessions came back with results. - **Nothing lost**: the unread and pinned sets are persisted server-side and survive page reloads and process restarts. - **Purely additive**: official behavior is never replaced — the plugin only tags rows and paints titles, and uninstalls cleanly. ## Features 1. **Automatic unread** — the moment the model produces content (`assistant/chunk` content blocks, `assistant/message`, `tool/result`) in **any session (including the currently open one)**, that session is marked unread and painted. The color tells you the task state: **still running = brand blue, finished and back to idle = green** (the running flag is read from the session-list store, so a store change re-decides immediately). The open session is marked too — that is where the in-place blue → green completion signal comes from. 2. **Cleared on switch-away / on re-click** — opening, staying, and sending do not clear the mark. Actively switching from session A to session B clears **A** only (removed locally at once and persisted server-side). While you are in a session, **clicking its row again** also clears the mark (matching the native "select = acknowledge" semantics). 3. **Title coloring (single color composer)** — one code path: row state → ordered anchor colors → paint. Semantic order, left to right: **pinned = yellow (leftmost) → live background shell = purple (middle) → unread = blue (running) / green (finished, rightmost)**. A single state paints the whole title in that family's gradient (unread derives a two-tone from the theme brand color; monochrome themes degrade gracefully; every endpoint is guarded for contrast against the background). Overlapping states expand a segmented template (solid spans plus smooth transitions between them). The gradient is drawn as an inline style on the title element, so React re-renders never overwrite it and nothing needs repainting. Gradient stops adapt to the real text width: short titles compress the stops to pixels, long titles use container-width percentages. 4. **Clear on send (optional, off by default)** — when enabled, sending a message inside a marked session also clears its mark. 5. **Manual marking** — the session row's "…" menu offers "Mark as unread / Mark as read". 6. **Pin** — the "…" menu offers "Pin / Unpin". Pinned sessions sort first under **every** sort mode (manual drag, recently updated, model updated) and their titles get a yellow gradient, composed with the other states as described above. The pinned set is persisted server-side, is not cleared by switching away, and is unaffected by "clear on send". 7. **Background shell indicator** — when a session holds **live** shell-kind background jobs (`bash`/`pwsh`/`pty`/`terminal` …; subagents and workflows do not count), a purple span joins its title; it disappears automatically when the jobs finish or are cancelled (transient, never persisted). The data comes from the host's background-job registry mirrored into the browser over `session/jobs` — the same source as the official "N background tasks running" counter. Without that mirror the feature degrades to hidden. 8. **Settings section** — Settings → "Session unread": every unread session (title, running flag, session id), a manual Refresh button, per-row "Mark as read", and the "clear on send" toggle. 9. **"Model updated" sort (third sort mode)** — orders by the last whitelisted model event per session; manual drag order is preserved and model activity floats a session up. **One-minute tolerance**: a session may overtake another only when it leads by **more than 60 seconds**; gaps under a minute are ties and keep their current relative order (stable sort), so parallel streams no longer shuffle endlessly. The official "recently updated" mode is untouched. 10. **Second "Deep diving" column** — the chat view's running timer gains a second column, "since agent output" (same source as the sort mode, polled once a second). 11. **Purely additive** — the native green completion dot (armed on running→idle, cleared on open, hidden while running) keeps its exact original behavior; this plugin only adds row state classes and title styling. ## Install ```bash dsh plugin --profile web add dsh-session-colorful-unread-pin-jobs@latest ``` From GitHub (equivalent): ```bash dsh plugin --profile web add github:enterhalf/dsh-session-colorful-unread-pin-jobs ``` Uninstall: ```bash dsh plugin --profile web remove dsh-session-colorful-unread-pin-jobs ``` Uninstalling restores the bundle transforms, routes, timers and event subscriptions. The unread list, pinned set and settings stay in `~/.dsh/storages/dsh-session-colorful-unread-pin-jobs/` and are picked up again on reinstall. ## How it works - **Unread data plane (server-side persistence)** — the host subscribes to `session/event`; when a whitelisted event lands, a non-subagent session (including the open one) is marked unread (idempotent — no change, no disk write). State lives in `~/.dsh/storages/dsh-session-colorful-unread-pin-jobs/unread.json` (atomic writes, FIFO capped at 2000 entries, holding the `clearOnSend` setting and the `topIds` pinned set). The client reconciles every 2 seconds via `GET /session-colorful-unread-pin-jobs/list` (the host is authoritative) and re-reports the open session each round via `POST /session-colorful-unread-pin-jobs/active`. - **API surface** (registered on the webServer route table): - `GET /session-colorful-unread-pin-jobs/list` → `{ ok, ids, topIds }` - `POST /session-colorful-unread-pin-jobs/toggle` `{ id, unread }` → `{ ok, ids, topIds }` - `POST /session-colorful-unread-pin-jobs/top` `{ id, top }` → `{ ok, ids, topIds }` - `GET/POST /session-colorful-unread-pin-jobs/settings` → `{ ok, clearOnSend }` - `POST /session-colorful-unread-pin-jobs/active` `{ id: string|null }` → `{ ok, ids }` - `GET /session-colorful-unread-pin-jobs/timestamps` → `{ ok, ts: { sessionId: epochMs } }` (in-memory sort timestamps) - **UI injected through bundle transforms** — anchor-string surgery on the sidebar bundle (`@deepseek-ai/dsh-client-ui-workspace`) and the chat-view bundle (`@deepseek-ai/dsh-client-ui-chat`, with `-conversation` auto-detected on older builds) injects row attributes, menu entries, the sort mode and the timestamp poller. If any anchor is missing (upstream change) or the rewritten source fails a syntax check, **the original bundle is kept** and the page is unaffected; partially matching segments degrade individually. The transform chain is shared with sibling plugins (e.g. dsh-notify) instead of excluding them. - **Both delivery architectures supported** — on older builds (per-file URLs) an HTTP route serves the transform cached by mtime plus chain fingerprint; on the current `dsh-client-modules` content-addressed combo URLs the only valid channel is writing the transform back to the on-disk `client.js` atomically and calling `clientModules.rebuilt(id)`. Original bytes are kept in a sidecar and restored on uninstall. - **Title coloring** — the client keeps three state sources: the unread set (host-authoritative, polled), the pinned set (same source) and the live shell set (the jobs mirror in the list store). The palette is derived from the body theme attribute plus brand-color aliases and guarded for contrast; a theme switch repaints automatically. - **Pinned sort partition** — after computing the order and before deciding whether anything changed, the sort function partitions the pinned set to the front (order preserved inside the group, effective in every sort mode). ## Limitations - Menu entries, sort UI and row attributes depend on anchor strings in the current bundles; after a major DSH upgrade re-validate them with `npm run check-anchors`. When an anchor is gone the corresponding feature disappears and the page stays intact. - Under the combo architecture the transform is written back to the on-disk bundle: install/uninstall restores it automatically, but if the host process crashes before uninstall the transformed bytes remain (harmless — the next load treats them as already transformed; the original bytes are in `~/.dsh/storages/dsh-session-colorful-unread-pin-jobs/bundle-*.client.js.orig` if you want to restore by hand). - Coloring relies on the hashed class name of the row's title element and the injected row attribute; if an upgrade changes the class name, coloring stops working while the page and menus stay fine. - The background-shell indicator needs the host's `session/jobs` mirror; on runtimes without it the feature is simply hidden. - Theme adaptation relies on the theme system exposing the theme on the body attribute and the brand color through alias variables; a third-party theme that bypasses that mechanism will not be followed. - "Currently open" is whatever client reported last: with several tabs open on different sessions, only the last reporter is exempt from automatic marking. - Sort timestamps are in memory only (reset on restart); unread and pinned sets are persisted. - Automatic unread is decided from whitelisted model events only — content-less anomalies such as `llm/retry` do not count. ## Development ```bash npm run build # validate and emit lib/index.js npm run build:client # validate and emit lib/client.js npm run check-anchors # offline check of every anchor in both bundles (order-dependent simulation + syntax check) dsh plugin --profile web add /path/to/dsh-session-colorful-unread-pin-jobs ``` ## License MIT