dsh-subagent-workspace-ui 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,98 @@
1
1
  # DSH Subagent Workspace UI
2
2
 
3
+ **English** | [中文](README.zh.md)
4
+
5
+ <p align="center"> <img src="https://raw.githubusercontent.com/miuzel/dsh-subagent-ui/main/docs/banner.png" alt="DSH Subagent Workspace UI —— a subagent manager panel with a live floating panel" width="100%"> </p>
6
+ <p align="center"> <a href="https://www.npmjs.com/package/dsh-subagent-workspace-ui"><img src="https://img.shields.io/npm/v/dsh-subagent-workspace-ui?style=flat-square&label=npm&color=cb3837" alt="npm version"></a> <a href="https://www.npmjs.com/package/dsh-subagent-workspace-ui"><img src="https://img.shields.io/npm/dm/dsh-subagent-workspace-ui?style=flat-square&label=downloads&color=cb3837" alt="npm downloads"></a> <a href="LICENSE"><img src="https://img.shields.io/npm/l/dsh-subagent-workspace-ui?style=flat-square&label=license&color=green" alt="license"></a> <a href="https://awesome-dsh-plugin.com"><img src="https://img.shields.io/badge/awesome--dsh--plugin-listed-2f6feb?style=flat-square" alt="listed in awesome-dsh-plugin (an aggregated index, not an official curated list)"></a> <a href="#compatibility"><img src="https://img.shields.io/badge/DSH-%3E%3D0.1.5--rc.3-2f6feb?style=flat-square" alt="dsh host range: 0.1.5-rc.3 and later"></a> </p>
7
+
3
8
  A Web client plugin that adds a **子代理管理** button to the conversation-header action row. It opens a searchable panel for the subagents currently discovered by the DSH client runtime.
4
9
 
10
+ Current release: **v1.9.1** — full history in [`CHANGELOG.md`](https://github.com/miuzel/dsh-subagent-ui/blob/HEAD/CHANGELOG.md) (English) and [`CHANGELOG.zh.md`](https://github.com/miuzel/dsh-subagent-ui/blob/HEAD/CHANGELOG.zh.md) (中文).
11
+
12
+ ## Install in the Web profile
13
+
14
+ Install the published package from npm into your DSH Web profile:
15
+
16
+ ```bash
17
+ dsh plugin --profile web add dsh-subagent-workspace-ui
18
+ ```
19
+
20
+ Upgrade to the latest published version:
21
+
22
+ ```bash
23
+ dsh plugin --profile web add dsh-subagent-workspace-ui@latest
24
+ # or, equivalently:
25
+ dsh plugin --profile web update dsh-subagent-workspace-ui
26
+ ```
27
+
28
+ Uninstall:
29
+
30
+ ```bash
31
+ dsh plugin --profile web remove dsh-subagent-workspace-ui
32
+ ```
33
+
34
+ `dsh plugin --profile <name> <args…>` forwards its arguments to pnpm inside that profile, so the usual `add` / `update` / `remove` subcommands apply. No `--allow-build` flag is needed: the published tarball already ships a prebuilt `lib/`, and the package declares no install-time lifecycle scripts.
35
+
36
+ The bundle includes [`cordis.patch.yml`](cordis.patch.yml), which inserts the manager and shadows exactly one stock slot: `conversation.session.header.lineage` is claimed by a `priority: -1` title-only shadow so DSH's stock `ui-subagent` lineage dropdown stays invisible while this package is installed. The stock `ui-subagent` plugin itself is **left enabled** — this package no longer disables it wholesale, because the sidebar tab it provides is the navigation target of the new "open in the sidebar" button. The shadow renders the session title instead of an empty entry, so the subagent session header keeps its title. Removing the package removes this bundle layer, and the host's `ui-subagent` setting is untouched, so your own configuration is restored as it was. Restart the existing `dsh web` process, then refresh `http://127.0.0.1:3080` after the plugin is available.
37
+
38
+ If you previously disabled `ui-subagent` manually in `$DSH_HOME/profiles/web/cordis.patch.yml`, you can keep that stanza: it is user-owned and intentionally preserved. The plugin no longer needs (or writes) such a stanza.
39
+
40
+ Working on a checkout instead of the npm release? See [Local checkout (contributors)](#local-checkout-contributors).
41
+
5
42
  ## Features
6
43
 
44
+ ### Scope: entry, workspace and session
45
+
7
46
  - Compact title-bar trigger shows the active-child count and animated activity dot without opening the panel.
8
47
  - Defaults to the main session's current workspace and current session, even when the user is viewing a child session.
9
48
  - Workspace and session selectors support current workspace, all workspaces, named workspaces, current session, and named sessions.
49
+ - Show session IDs beside names, compact metadata, token totals, and creation time in the relative-time tooltip.
50
+ - Ordinary search matches names, titles and workspace names; the `id:` prefix searches Session IDs only (`id: graph:g-a92e1406`).
51
+
52
+ ### Search, sorting and grouping
53
+
10
54
  - Sort by recent activity, name, or type; recently running children remain near the top after they finish.
11
55
  - Group by session, workspace, category, type, or no grouping. Session headers show the workspace and parent session name.
56
+
57
+ ### Categories
58
+
12
59
  - Browser-local classification tabs support custom regular expressions. Built-ins include all, other, review, test, implementation, and planning.
13
60
  - One-shot children carry a compact `⚡ 一次性` badge; continuable children remain visually uncluttered.
14
- - Show each child's current **type and model provider/id** (`provider/model`, plus the reasoning effort when the host publishes one) from the host's public projections only — no new RPC, no model-switch UI. See [Type and model](#type-and-model-read-only-projections).
15
- - Show each child's usage inline, computed with the host's own definitions: `↑ 131.3k (未缓存 39.1k) / ↓ 12.7k · 命中 70% · 104 tps · 3 轮 · 9 步` (English UI: `miss` / `Hit` / `rnds` / `stps`). The `↑` figure is the **billed input** (`uncachedInputTokens + cacheReadTokens + cacheWriteTokens`) and that billed input alone is the cache-hit denominator — exactly what DSH's own composer footer does, so the plugin and the host agree instead of disagreeing by six points. `tps` is `decodeTokens / (decodeMs / 1000)` from the same public `sessionStats` projection. Hovering the row or the floating panel reveals the complete breakdown — total, every bucket, the share, the speed, and the session's LLM / tool / TTFT timings — in the native `title` tooltip.
61
+
62
+ ### Filtering and hiding
63
+
64
+ - The filter block (category tabs, scope/session selectors, sorting, grouping, options) opens **collapsed by default**, showing one summary line (`Current workspace · Current session … · All`, then `Sort · recent activity` and `Group · by session`). That summary line is itself the disclosure control — click it, or press Enter/Space (`role=button` / `aria-expanded`) — and the preference is still persisted.
65
+ - Filtering and hiding are plugin-local: a hidden child simply leaves the list, no DSH session or workspace file is deleted, and the header counts subtract it. The options row offers hide one-shot, hide long-inactive, show hidden, the show-active-float toggle, a separately configurable subagent background colour for the dark and light themes, and reset filters.
66
+
67
+ ### Pause, continue and one-click pause
68
+
69
+ - A running continuable child can be paused from its own row, from the active-subagent group header (`⏸ Pause all`, with a confirmation), or from the floating panel.
70
+ - An ended continuable row offers **`▶ Continue`** next to pause/hide/delete and sends one **localized** continue instruction into the child (Chinese UI `继续`, English UI `continue`) through the session face the host really exposes: `sessions.retain(…)` → `binding.session.prompt([…], 'queue')` where `retain` exists (0.1.6-alpha.2 on, including 0.1.7-rc.1), and the borrowed `sessions.binding` face on 0.1.5-rc.3, which has no `retain`. Pure capability probing, never a version check: a host without such a face renders no button at all, and running and one-shot rows never show it.
71
+
72
+ ### Batch operations and permanent deletion
73
+
16
74
  - Archive state is local and never deletes a DSH session. Single-row archive actions and a batch mode support shift-selection, select-all, time-based selection (up to 1,000 rows), batch archive, restore, and archive-all.
17
- - Load catalogs in pages of 40 with an independent wheel-scroll container; batch time selection expands loading up to 1,000 children.
18
75
  - Batch mode changes cards into selection targets and hides individual archive/restore actions. The highlighted 完成 button exits batch mode.
19
- - Open a loaded child at its exact `{ parentSessionId, childSessionId, mode }` address. In normal mode the whole card opens the child; archive controls do not.
20
- - Every row also carries a dedicated **open in the sidebar** button (`◫`) that opens the child as a right-sidebar tab, so the main conversation stays where it is. The button stops propagation (it never triggers the row's default navigation and never toggles batch selection), and it is rendered only when the runtime exposes the sidebar capability: without `ctx.sidebarRight` plus a type that claims the address, the button is hidden entirely; for a single row whose subagent address cannot be resolved it stays visible but disabled with a readable reason. The same button is on the active-subagent floating panel.
21
- - Show session IDs beside names, compact metadata, token totals, and creation time in the relative-time tooltip.
76
+ - Load catalogs in pages of 40 with an independent wheel-scroll container; batch time selection expands loading up to 1,000 children.
77
+
78
+ ### Live activity and streaming output
79
+
22
80
  - Active children are grouped at the top in a collapsible section. The panel shows the latest two lines of live output, recent tool calls, context injection, command status, and a gray final snapshot after completion — for **every** running row, not only the selected child, because the plugin retains its own session binding instead of borrowing whatever the main view happens to hold.
23
81
  - Ended rows mark how the last turn finished, from the child's own public read-only `subagentTiming.lastTurnCompleted` projection: a normal end is a **hollow green ring**, an abnormal one (stopped/aborted, error, blocked, a token ceiling …) a **solid red dot**, each with a zh/en hover label. A host that does not publish the projection keeps exactly the previous dot — an unknown end is never shown as a normal one — and a running row keeps its original green dot.
24
- - An ended continuable row offers **`▶ Continue`** next to pause/hide/delete and sends one **localized** continue instruction into the child (Chinese UI `继续`, English UI `continue`) through the session face the host really exposes: `sessions.retain(…)` → `binding.session.prompt([…], 'queue')` where `retain` exists (0.1.6-alpha.2 on, including 0.1.7-rc.1), and the borrowed `sessions.binding` face on 0.1.5-rc.3, which has no `retain`. Pure capability probing, never a version check: a host without such a face renders no button at all, and running and one-shot rows never show it.
25
- - The filter block (category tabs, scope/session selectors, sorting, grouping, options) opens **collapsed by default**, showing one summary line (`Current workspace · Current session … · All`, then `Sort · recent activity` and `Group · by session`). That summary line is itself the disclosure control — click it, or press Enter/Space (`role=button` / `aria-expanded`) — and the preference is still persisted.
82
+
83
+ ### Opening and inspecting a child
84
+
85
+ - Open a loaded child at its exact `{ parentSessionId, childSessionId, mode }` address. In normal mode the whole card opens the child; archive controls do not. The navigation tiers are documented under [Compatibility](#compatibility).
86
+ - Every row also carries a dedicated **open in the sidebar** button (`◫`) that opens the child as a right-sidebar tab, so the main conversation stays where it is. The button stops propagation (it never triggers the row's default navigation and never toggles batch selection), and it is rendered only when the runtime exposes the sidebar capability: without `ctx.sidebarRight` plus a type that claims the address, the button is hidden entirely; for a single row whose subagent address cannot be resolved it stays visible but disabled with a readable reason. The same button is on the active-subagent floating panel.
87
+ - Show each child's current **type and model provider/id** (`provider/model`, plus the reasoning effort when the host publishes one) from the host's public projections only — no new RPC, no model-switch UI. See [Type and model](#type-and-model-read-only-projections).
88
+ - Show each child's usage inline, computed with the host's own definitions: `↑ 131.3k (未缓存 39.1k) / ↓ 12.7k · 命中 70% · 104 tps · 3 轮 · 9 步` (English UI: `miss` / `Hit` / `rnds` / `stps`). The `↑` figure is the **billed input** (`uncachedInputTokens + cacheReadTokens + cacheWriteTokens`) and that billed input alone is the cache-hit denominator — exactly what DSH's own composer footer does, so the plugin and the host agree instead of disagreeing by six points. `tps` is `decodeTokens / (decodeMs / 1000)` from the same public `sessionStats` projection. Hovering the row or the floating panel reveals the complete breakdown — total, every bucket, the share, the speed, and the session's LLM / tool / TTFT timings — in the native `title` tooltip.
89
+
90
+ ### Prompt preview inside a subagent session
91
+
92
+ - A subagent's prompt is usually one long structured markdown brief (a task book, a review checklist). Each user message inside a child session therefore carries a `预览` / `原文` toggle in the official action row: `预览` renders that message's raw text as markdown in place of the official body, `原文` — pinned to the top-right of the rendered block, next to `展开` while the block is folded — puts the official rendering back. Switching either way never moves the layout, and opening a preview scrolls the block's top into view only when it was not already visible.
93
+ - The renderer is **self-built and text-only**: headings, nested ordered/unordered lists, bold/italic, inline code, fenced code with its language, nested blockquotes, and links whose target is `http(s)` or relative. Every element is created as a text node — no third-party markdown library, no `dangerouslySetInnerHTML`, no `innerHTML` — so an injected `<script>` and a `javascript:` / `data:` link stay inert text.
94
+ - Long bodies start folded and `展开` / `收起` toggles them; a 20 000-character body stays responsive. Activation binds to `pointerdown` (a host that rebuilds the action row between press and release cannot lose it) with a per-message guard against a double toggle, while `click` keeps Enter/Space working.
95
+ - The official `conversation.chat.node` renderer is never taken over: the plugin adds a second, **headless** cell to the same slot, so a host change costs that one cell rather than the conversation. A non-subagent session gets no preview at all, and the text source is documented under [Prompt text](#prompt-text-read-from-the-childs-user-messages).
26
96
 
27
97
  ## Screenshot guide
28
98
 
@@ -32,6 +102,8 @@ Both screenshots were taken on the `1.7.0-dev` line (commit `79d9d0f`) against a
32
102
 
33
103
  ![Active subagent floating panel (English UI)](docs/images/screenshot-2.en.png)
34
104
 
105
+ ### Legend
106
+
35
107
  1. **Header** — panel title, `Current session n · Current workspace n · Active n`, the show-active-float toggle, and the close action.
36
108
  2. **Search and scope row** — ordinary name/title/workspace search, with `id: xxx` reserved for Session ID search; workspace, session, sorting, and grouping selectors stay on one compact row.
37
109
  3. **Classification row** (once the filter block is expanded) — built-in and custom categories with live counts, custom-category creation and deletion inside the same tab frame.
@@ -41,23 +113,21 @@ Both screenshots were taken on the `1.7.0-dev` line (commit `79d9d0f`) against a
41
113
  7. **Detail block** — `Type: … · Model: …` straight from the host's read-only projections, then the official usage line `↑ billed (miss …) / ↓ output · Hit n% · n tps · n rnds · n stps`; a running child also shows its latest live-output lines, while a finished child keeps its final snapshot.
42
114
  8. **Floating panel** — the running children of the current session in a compact always-on-top card, each with its live output, usage line and stop button; it appears whenever at least one child is running and the manager panel is closed. While it is shown it keeps every running child it lists live, independently of which child the main view has selected.
43
115
 
44
- ## Install in the Web profile
45
-
46
- From this directory:
116
+ ## Runtime data boundary
47
117
 
48
- ```bash
49
- dsh plugin --profile web add file:.
50
- ```
118
+ The public DSH Web session store exposes subagent summaries that have been discovered in the current browser runtime. It deliberately does not expose a global historical subagent index or a mode for every unvisited child. Therefore this first plugin version manages the discovered catalog; rows whose type is not yet loaded remain visible and searchable and fall back to DSH's retained session navigation. Exact catalog navigation is used automatically as soon as DSH supplies the address and mode.
51
119
 
52
- The bundle includes [`cordis.patch.yml`](cordis.patch.yml), which inserts the manager and shadows exactly one stock slot: `conversation.session.header.lineage` is claimed by a `priority: -1` title-only shadow so DSH's stock `ui-subagent` lineage dropdown stays invisible while this package is installed. The stock `ui-subagent` plugin itself is **left enabled** — this package no longer disables it wholesale, because the sidebar tab it provides is the navigation target of the new "open in the sidebar" button. The shadow renders the session title instead of an empty entry, so the subagent session header keeps its title. Removing the package removes this bundle layer, and the host's `ui-subagent` setting is untouched, so your own configuration is restored as it was. Restart the existing `dsh web` process, then refresh `http://127.0.0.1:3080` after the plugin is available.
120
+ A full persistent workspace-wide archive view requires a host-side catalog RPC (or an upstream DSH API) that enumerates every child address and its mode. The public `SessionSummary` exposes no prompt, so the prompt is never read from it: the preview's text comes from the child session's own `user/message` bodies through the same retained binding and is documented in [Prompt text](#prompt-text-read-from-the-childs-user-messages); **type and model** come from the public projections documented in [Type and model](#type-and-model-read-only-projections). Live output and tool/context activity are read from a bound session automatically: on dsh **0.1.2-alpha.2** they are derived from the raw `binding.eventSource` event stream (showing the tool description or target filename), while older hosts (e.g. **0.1.1-rc.2**) fall back to `session.getSnapshot().chat.legacy`. On hosts with the retain contract (**0.1.6-alpha.2** and on) the plugin first **retains** each running child itself — `sessions.binding(id)` merely borrows a binding somebody else retained, so without retaining, only the child the main view had selected ever streamed. Retention is bounded to 8 children (the float first, then the panel's rendered running rows, most recently active first) and is released as soon as a row stops rendering or stops running, when a surface closes, or when the plugin is disposed; a child beyond the cap, and any host without `retain`, falls back to the borrowed binding and then to the durable summary. Capability detection selects the path (never a version check), so the plugin stays forward compatible. The UI is isolated in [`lib/client.js`](lib/client.js), so it can switch to a richer source without changing the panel interaction model.
53
121
 
54
- If you previously disabled `ui-subagent` manually in `$DSH_HOME/profiles/web/cordis.patch.yml`, you can keep that stanza: it is user-owned and intentionally preserved. The plugin no longer needs (or writes) such a stanza.
122
+ Archive, category and recent-use order are kept in browser `localStorage`; nothing is ever written into the DSH session log.
55
123
 
56
- ## Runtime data boundary
124
+ ### Prompt text (read from the child's user messages)
57
125
 
58
- The public DSH Web session store exposes subagent summaries that have been discovered in the current browser runtime. It deliberately does not expose a global historical subagent index or a mode for every unvisited child. Therefore this first plugin version manages the discovered catalog; rows whose type is not yet loaded remain visible and searchable and fall back to DSH's retained session navigation. Exact catalog navigation is used automatically as soon as DSH supplies the address and mode.
126
+ The prompt shown by the preview is read from the child session's own **`user/message` events**, through the same retained binding the live-output features use, and only for a session the plugin is currently rendering. `SessionSummary.prompt` is still not a source — that projection carries no prompt at all:
59
127
 
60
- A full persistent workspace-wide archive view requires a host-side catalog RPC (or an upstream DSH API) that enumerates every child address and its mode. The public `SessionSummary` does not expose the original prompt, so prompts are neither queried nor displayed; **type and model** come from the public projections documented in [Type and model](#type-and-model-read-only-projections). Live output and tool/context activity are read from a bound session automatically: on dsh **0.1.2-alpha.2** they are derived from the raw `binding.eventSource` event stream (showing the tool description or target filename), while older hosts (e.g. **0.1.1-rc.2**) fall back to `session.getSnapshot().chat.legacy`. On hosts with the retain contract (**0.1.6-alpha.2** and on) the plugin first **retains** each running child itself — `sessions.binding(id)` merely borrows a binding somebody else retained, so without retaining, only the child the main view had selected ever streamed. Retention is bounded to 8 children (the float first, then the panel's rendered running rows, most recently active first) and is released as soon as a row stops rendering or stops running, when a surface closes, or when the plugin is disposed; a child beyond the cap, and any host without `retain`, falls back to the borrowed binding and then to the durable summary. Capability detection selects the path (never a version check), so the plugin stays forward compatible. The UI is isolated in [`lib/client.js`](lib/client.js), so it can switch to a richer source without changing the panel interaction model.
128
+ - A message node is matched to its record **by message id**; the node key's `<anchor>` is a feed counter, not `event.seq`, and is used only when a key carries no id at all. Positional filling never crosses into an addressable node.
129
+ - A node whose own record is outside the retained event window is not guessed from a neighbour: that node's **own** body is read from the DOM, and wherever the record is available the result is asserted against it (length plus a rolling hash).
130
+ - Reading the DOM merges every body fragment of that node in document order — marked and unmarked — through a walk that drops chrome (clock, action buttons, card title, footer). Nothing is written back: the session log is never modified, and archive/category state stays in `localStorage`.
61
131
 
62
132
  ### Type and model (read-only projections)
63
133
 
@@ -95,10 +165,18 @@ The display rules are identical on all three surfaces and differ only in density
95
165
 
96
166
  ## Compatibility
97
167
 
98
- v1.8.0 and later support dsh **0.1.5-rc.3**, **0.1.6-alpha.2** and **0.1.7-rc.1** and stay backward compatible with all DeepSeek Harness versions. Those hosts span two API tiers — 0.1.5-rc.3 and 0.1.6-alpha.2 still publish `subagentsByParent`, `refreshSubagents` and `setSubagentCatalogOpen`, while 0.1.7-rc.1 **removed** `sessions.setSubagentCatalogOpen`, **renamed** `refreshSubagents(parentSessionId)` to `refreshProjections(sessionId)`, and **replaced** `SessionListState.subagentsByParent` with the parent session's own `subagentCatalog` projection. Every call site probes for the capability it needs, so both generations work and no version number is ever compared. Opening a subagent session is capability-detected at runtime and degrades in three tiers:
168
+ v1.8.0 and later support dsh **0.1.5-rc.3**, **0.1.6-alpha.2**, **0.1.7-rc.1**, **0.1.7-rc.2** and **0.2.0-rc.1** and stay backward compatible with all DeepSeek Harness versions. Those hosts span two API tiers — 0.1.5-rc.3 and 0.1.6-alpha.2 still publish `subagentsByParent`, `refreshSubagents` and `setSubagentCatalogOpen`, while 0.1.7-rc.1 **removed** `sessions.setSubagentCatalogOpen`, **renamed** `refreshSubagents(parentSessionId)` to `refreshProjections(sessionId)`, and **replaced** `SessionListState.subagentsByParent` with the parent session's own `subagentCatalog` projection. Every call site probes for the capability it needs, so both generations work and no version number is ever compared.
169
+
170
+ **0.2.0-rc.1** was checked the same way — package by package against **0.1.7-rc.1** (23 host packages plus the CLI, app boot and the web frontend), not against its release notes — and it needs **no adaptation** either: no host face this plugin reads changed structurally. The two slot declarations it registers, the module-loader shell and its seeded `require` keys, the `subagentCatalog` / `subagent` / `subagentTiming` projections (including the generational `lastTurnCompleted` field), `subagentAddress` / `openResource` / `candidates`, the `dsh plugin --profile web add` flow and all 21 `--dsw-*` tokens this plugin uses are unchanged or purely additive; the only contract addition is an optional `fork(onCreated)` the plugin never calls, and the stock web graph dropping its `ui-schedule` row does not touch this plugin or its slot shadow. Two differences are visible but not breaking: one dark-theme token the panel and the floating card use for their glass surface changed value, and the sidebar services appear **even later** than on 0.1.7-rc.1 (see “Open in the sidebar” below).
171
+
172
+ A smoke run on **0.2.0-rc.1** renders the same panel: on a parent session with six catalog children the manager lists all six rows with their catalog labels, `Type: continuable` on every row and an enabled `◫` on every row, and each child session that gets mounted shows its ended row as the hollow green ring with its own model and usage figures — line for line the same as the **0.1.7-rc.1** run over the same session data, and a click on `◫` opens that child as a right-sidebar tab on both.
99
173
 
100
174
  A smoke run on **0.1.5-rc.3** shows no degradation on that line: on a fresh instance with one running child the manager opens on the live session with the child's catalog label, `Type: continuable`, the concrete model id, `◫` on the row, the pause/hide/delete actions, the live-output section rendering that child's running `bash` tool, and the usage figures — with no error from this plugin in the console.
101
175
 
176
+ **0.1.7-rc.2** needs no adaptation either, and it exposed one thing the earlier matrix had missed: the stock `ui-subagent` plugin registers **two** header entries, not one. Besides the `conversation.session.header.lineage` dropdown, **0.1.7-rc.1** added a second registration in `conversation.session.header.actions` (`id: "subagent-catalog"`, `order: -30`) that draws the official “N subagents” count dropdown as soon as the current session's `subagentCatalog` projection is loaded. Because that slot is a `list` slot whose cells are keyed by `id`, the manager's own `subagent-workspace-manager` cell never competed with it, so both entries could show side by side — the plugin now shadows that cell too (`priority: -1`, rendering `null`), exactly as it shadows the lineage slot, and either shadow degrades to “official entry visible” if a host ever occupies the same cell at that priority. Verified on **0.1.7-rc.1**, **0.1.7-rc.2** and **0.2.0-rc.1** against the same six-child fixture: the header shows only `🧩 Subagents 0/6`, the six-row panel opens, and each row's `◫` still opens that child as the stock `subagentchat` right-sidebar tab. On **0.1.5-rc.3** and **0.1.6-alpha.2** — which have no such stock registration — the extra shadow cell adds no element to the header and raises no error.
177
+
178
+ ### Session navigation (three capability tiers)
179
+
102
180
  ```text
103
181
  # dsh 0.1.2-alpha.5 .. 0.1.6-alpha.1: the session controller entry points
104
182
  ctx.sessions.openSubagent(address) # exact child address
@@ -112,6 +190,8 @@ ctx.get('uiWorkspace').openSession({ parentSessionId, childSessionId, mode } | s
112
190
 
113
191
  `uiWorkspace` is read through `ctx.get('uiWorkspace')`, never through a required injection, so hosts that do not register the service (everything before **0.1.2-alpha.5**) still load and keep using the session-controller path, while hosts that removed `openSubagent`/`open` (**0.1.6-alpha.2**) use the workspace service. **The probe order follows the argument shape, not the version**: `sessions.openSubagent` accepts an address object on every host that has it, whereas `uiWorkspace.openSession` only accepts a `SessionTarget` from **0.1.6-alpha.2** on — on **0.1.5-alpha.2 … 0.1.6-alpha.1** it is `openSession(sessionId)` and routes through `sessions.open(id)`, which throws on an address. The session-controller tier is therefore tried first and the workspace service is the fallback. Within the controller tier the exact `{ parentSessionId, childSessionId, mode }` address is used first and only a missing mode/child falls back to plain session navigation, because `openSubagent` rejects addresses that are not healthy catalog children. The 0.1.2-series capability paths (live output via `binding.eventSource`, chat-tab switch via slot `actions`) remain the primary branches, and the **0.1.1-rc.2** legacy fallbacks (`chat.legacy` snapshot) are unchanged.
114
192
 
193
+ ### Open in the sidebar (◫ button)
194
+
115
195
  The **open in the sidebar** button follows the same rule — capability detection, never a version check:
116
196
 
117
197
  ```text
@@ -127,75 +207,13 @@ ctx.sessions.subagentAddress → typeof function (per-row address lookup)
127
207
  # with a readable reason instead of throwing.
128
208
  ```
129
209
 
130
- Both faces are resolved **per row and per click, never once at activation**: dsh **0.1.7-rc.1** registers `sidebarRight`/`sidebarRightTabs` only *after* a plugin's `apply` has run — on **0.1.6-alpha.2** they are already present when `apply` runs — so a probe performed once at activation hides the whole `◫` column on the newer host while leaving the older one untouched.
210
+ Both faces are resolved **per row and per click, never once at activation**: dsh **0.1.7-rc.1** registers `sidebarRight`/`sidebarRightTabs` only *after* a plugin's `apply` has run — on **0.1.6-alpha.2** they are already present when `apply` runs — so a probe performed once at activation hides the whole `◫` column on the newer host while leaving the older one untouched. dsh **0.2.0-rc.1** pushes that window further out still: its `ui-sidebar-right` now also injects `shortcuts`, so it waits for one more service before it publishes those two faces. The per-row, per-click resolution is what keeps the `◫` column rendered on all of these hosts.
131
211
 
132
212
  Adding the sidebar capability does not change the navigation probe above: the row's default click still walks the same three tiers, and the new button never touches them.
133
213
 
134
214
  Opening a subagent in the right sidebar uses DSH's own `subagentchat` right-sidebar tab, so the pane is a *session view*: the subagent session's composer there is DSH's official read-only composer (`一次性子代理记录` / "one-shot subagent record") rather than this plugin's UI. That is expected, and it is the only official way to read a child session without leaving the main conversation.
135
- ## v1.9.0
136
-
137
- - **Feature**: an ended subagent can be **continued from its own row**. The row action group (`⏸ Pause` / `⊘ Hide` / `🗑 Delete`) gains `▶ Continue`, which sends one **localized** continue instruction into that child (Chinese UI `继续`, English UI `continue`) so it runs again. The implementation is pure capability probing and **never compares a version**: a host with `retain` (0.1.6-alpha.2 on, including 0.1.7-rc.1) goes `sessions.retain(…)` → `binding.session.prompt([{ type: 'text', text: '继续' }], 'queue')` (the English UI passes `'continue'`), while **0.1.5-rc.3** has no `retain` and uses the same face obtained by borrowing `sessions.binding`, which works just as well. A host without that capability **renders no button**; running rows and one-shot rows never show it either; the manager panel itself lives in the parent session's header, so "the parent agent must be alive" is satisfied by construction in normal use (an unavailable parent only logs a console warning instead of becoming an uncaught rejection).
138
- - **Feature**: the **filter block opens collapsed**. On first open the manager shows a single summary line (`Current workspace · Current session … · All`, plus `Sort · recent activity` and `Group · by session`); clicking that summary line expands or collapses the block, and Enter/Space do the same (`role=button` / `aria-expanded`). The preference is still persisted. A bug falls out with it: with the preference unset the toggle could only collapse and never expand, because `!undefined === true`.
139
- - **Feature**: an **ended row separates a normal end from an abnormal one**. A normal end is a hollow green ring, an abnormal one (⏸ stopped/aborted, error, blocked, max-tokens …) a solid red dot, each with a zh/en hover label (`正常结束` / `异常结束`). The verdict comes only from the child's own public read-only `subagentTiming.lastTurnCompleted` projection (the host writes it on `turn/end` and removes it on `turn/start`); a host that does not publish the projection **keeps the previous dot**, so an unknown end is never rendered as a normal one. The verdict field itself is projected by the host only from **dsh 0.1.7-alpha.1**: **0.1.5-rc.3** and **0.1.6-alpha.2** publish `{ settledMs, active? }` only, so on those two hosts an ended row keeps the neutral dot and does not distinguish how it ended. Running rows keep their original green dot.
140
- - **Compatibility**: all three changes are capability-probed and **never version-checked**; v1.9.0 supports dsh **0.1.5-rc.3** / **0.1.6-alpha.2** / **0.1.7-rc.1** exactly as v1.8.0 does (verified on all three hosts; the terminal indicator stays a neutral dot on 0.1.5-rc.3 and 0.1.6-alpha.2 per the boundary above). The only code changes are the row-action alignment (`src/client/styles.ts`) and one corrected comment (`src/client/format.ts`); the rest is the version bump and docs, and `pnpm run check` is green with the bundle freshness in sync.
141
-
142
- ## v1.8.0
143
-
144
- - **Fix**: live output only updated for the **currently selected** subagent. `sessions.binding(id)` only *borrows* a binding somebody else retained, and the stock view retains just the session the main view has selected — so every other child had no live region at all. The plugin now **retains** each running child it is showing itself: the float keeps every running child it displays, the panel keeps the running rows it renders, both share a cap of 8 (float first, most recently active first), and each row that stops rendering or stops running, every closed surface and the plugin's disposal releases in pairs. A host without `retain`, and any child beyond the cap, still falls back to the borrowed binding and then to the durable summary. Landing with it: **Pause now works on a row that is not selected** — it previously called into a binding that did not exist and silently did nothing.
145
- - **Compatibility with dsh 0.1.7-rc.1**: that host **removed** `sessions.setSubagentCatalogOpen`, **renamed** `refreshSubagents(parentSessionId)` to `refreshProjections(sessionId)`, and **replaced** `SessionListState.subagentsByParent[parent]` with the parent session's own `projectionValues.subagentCatalog` projection (the same direct children in catalog event order; entries lost their `kind` discriminator and gained a third `mode: 'unknown'` arm). Un-guarded, the removed method threw out of the manager panel's open effect and the error boundary took the whole header slot down — **opening the manager crashed the title bar**. Every host call is now capability-probed and never version-checked: one reader resolves the catalog from the parent's `subagentCatalog` on 0.1.7-rc.1+ and from `subagentsByParent` up to 0.1.6-alpha.2 (the legacy source wins when a host publishes both, so older hosts read exactly what they read before), `refresh` prefers `refreshProjections` and falls back to `refreshSubagents`, and a missing `setSubagentCatalogOpen` is skipped. An entry whose `mode` is `unknown` keeps its label and yields the mode to the child's own identity projection instead of trusting that arm.
146
- - **Fix**: on 0.1.7-rc.1 the per-row **open in the sidebar** action (`◫`) disappeared from every row. That host registers `sidebarRight`/`sidebarRightTabs` *after* this plugin's `apply` runs, so the one-time probe at activation judged both absent for good. Both faces are now resolved per row and per click: a host that never publishes them still renders no button (byte-for-byte the previous surface), while an available face whose row address does not validate keeps the existing disabled button and its readable reason.
147
- - **Docs**: the type/model ladder documents both catalog generations, and the compatibility section records the 0.1.7-rc.1 API changes above.
148
- - **Tweak**: the active float pins its background at **exactly 95% opacity**. dsh **0.1.7-rc.1** redefined `--dsw-specific-menu` as a translucent glass colour — `#30313680` in the dark theme (alpha `0x80` = 50%) and `#f8f9fa94` in the light one (58%) — where 0.1.6 pointed that same token at the opaque `--dsw-alias-bg-layer-3`, so the float turned see-through on the newer host. It now composites the host's **opaque** surface token at 95% (`color-mix`, with the previous declaration kept ahead of it as the fallback for a host without `color-mix`), which also makes the float's density identical on 0.1.6 and 0.1.7 — no version check, and the tone still comes from the host's own theme tokens.
149
-
150
- Verified on dsh **0.1.6-alpha.2** and **0.1.7-rc.1**: `pnpm run check` green (bundle freshness in sync, 98,439 bytes). On 0.1.7-rc.1 the panel opens on a live session with all 7 of its subagents, rows carrying their catalog labels, `Type: continuable`, model and usage figures, `◫` on every row, and **no console error after the click**. On 0.1.6-alpha.2 the panel object is byte-for-byte identical to the pre-change build, and the same batch of data shows the same unknowns it showed before (a child whose projections the host never published stays `model unknown`, exactly as it does on 0.1.6). The float's background was then measured on a live 0.1.7-rc.1 instance with a running child: it renders `rgb(51,52,54)`, the exact value `#353638` at 95% predicts over the `rgb(21,21,23)` header backdrop behind it, where the previous 50% glass would have rendered `rgb(34,35,38)`; sampling the same float over a backdrop 22 levels lighter (`rgb(43,43,45)`) moved it by at most one level, which is what a 95% surface does and a 50% one cannot (it would have moved 11 levels). The host's own `dsh-client-ui-open-in-app` logs a load-time `inactive context` error on 0.1.7-rc.1; it is unrelated to this plugin and deliberately left alone.
151
-
152
- ## v1.7.0
153
-
154
- - **Fix**: the **open in the sidebar** button (`◫`) — on a panel row and on the active float — was a far smaller target than the 24×24 CSS px minimum: 18.03×16 and 17.14×17, i.e. only the glyph itself was clickable. Both now measure **24.03×24** and **24.14×25**, while the layout box, the glyph position, the row heights and every neighbouring button's coordinates stay **pixel-identical**: the padding growth is cancelled by negative margins, and the hover pill moved into an `::after` pseudo-element whose insets reproduce the previous border box exactly (18.03×16 / 17.14×17). The one rendering difference is compositing order — the hover overlay now paints over the glyph instead of under it (≲1.3/255 per channel in the dark theme, ≲0.7/255 in the light one), which is imperceptible. Disabled rows keep their opacity and `not-allowed` cursor, and the larger area cannot turn a row press into a float drag: `closest('button,input,label')` is DOM ancestry, not geometry.
155
- - **Docs**: the dshmarket screenshots were re-shot on a real dsh **0.1.6-alpha.2** instance of the `1.7.0-dev` line (commit `79d9d0f`) — the manager panel and the active-subagents float, each in Chinese and in English (`*.en.png`, the host locale actually switched), dark theme, 1280×900 CSS at DPR 1.75. The captured session ran four background subagents at once (three running, one finished), so every row carries its real type, model and the official usage line instead of fallback text, with the `◫`, pause, hide and delete actions visible. `screenshots.json` now lists all four paths (dshmarket's carousel cap is 6), each README points at its own language pair instead of both sharing one, and the screenshot guide was rewritten — it still described controls (`show archived`, `→ archive`) that no longer exist.
156
215
 
157
- Verified on dsh **0.1.6-alpha.2**: `pnpm run check` green — the bundle's only change in this release is the hit-area CSS, and the screenshot work touched no source at all (four PNGs, `screenshots.json` and the two READMEs). The hit area was checked geometrically (padding / margin / `::after` insets against the previous border box) and by a pixel profile of the button column before and after; `./test.sh` smoke runs on an isolated `DSH_HOME` clicked the newly added edge on both the panel row and the float, exercised a disabled orphan row, and A/B-tested that pressing the enlarged area still opens the sidebar without starting a float drag or misfiring the neighbouring pause button.
158
-
159
- ## v1.6.0
160
-
161
- - **Feature**: every row and the active-subagent floating panel now carry an **open in the sidebar** button (`◫`) that opens the child as DSH's own `subagentchat` right-sidebar tab, so the main conversation stays where it is. It is capability-detected and never version-checked: the button is hidden entirely when the runtime exposes no `sidebarRight` / `sidebarRightTabs` capability or no tab type claims the row's address, and a single row whose address cannot be resolved stays visible but disabled with a readable reason. Clicking it never triggers the row's default navigation and never toggles batch selection. Because that sidebar tab *is* stock `ui-subagent`'s, the bundle patch no longer disables that plugin wholesale — it now only shadows `conversation.session.header.lineage` at `priority: -1` (a **title-only** shadow, so a subagent session's header title survives) and `ui-subagent` itself stays enabled. Your own `cordis.patch.yml` stanza is still preserved on removal.
162
- - **Feature**: the manager shows each child's **type and model** (`provider/model`, plus the reasoning effort when the host publishes one) purely from public read-only projections, with the type and the model degrading independently (`类型待加载` / `模型未知`). No new RPC, no model-switch UI.
163
- - **Fix**: a child's type could read `类型待加载` (*type loading*) until the manager panel had been opened once, even though the host had already published the mode — and opening the panel then flipped that same row to `一次性`. The mode was read only from the lazily pulled subagent catalog, and only the panel pulls it. It now resolves through one reader with one fallback order: ① the discovered catalog entry → ② the child's own `subagent` identity projection (pushed on the live-control stream and on every session-added summary, so a freshly loaded page already has it) → ③ the existing fallback text. A freshly spawned child's sidebar button can stay briefly disabled until that child is discovered; that is the documented capability probe, not a defect.
164
- - **Fix**: the inline usage figures disagreed with DSH's own composer footer for the same child (`in 39.1k / out 12.7k · cache hit 64%` versus `143K tok · Cache hit 70%`). The line showed the *uncached input* bucket labelled as "input", and it divided the cache-hit share by the four-bucket **total**, which counts output tokens in a prompt-side ratio. Both now follow the host's definitions: `↑ billed input (uncached) / ↓ output · 命中 n% · tps · turns · steps`, with the billed input alone as the denominator, and `tps = decodeTokens / (decodeMs / 1000)` from the same public `sessionStats` projection. The complete breakdown — total, every bucket, the share, the speed and the session's LLM / tool / TTFT timings — moved into the native `title` tooltip on both the row and the float. The host's cache-hit formatter is ported expression-for-expression, so a partial hit can never round up to 100%.
165
- - **Docs**: the lineage slot shadow is now described as what it is — a *title-only* shadow. The host's conversation module replaces the caller fallback once an entry exists in that single slot, so a literally empty entry would delete a subagent session's header title.
166
-
167
- Verified on dsh **0.1.6-alpha.2**: `pnpm run check` green, and the usage figures cross-checked against the official composer footer for the same child in a single screenshot (`132 tok/s` / `缓存命中 98%` / `29.8M tok` against `132 tps` / `命中 98%` / `↑ 29.7m + ↓ 119.6k` = `29.8M`). The cache-hit formatter is covered by a differential test that slices the three official helper functions out of the installed host client at run time and asserts identical output over 80,980 `(read, billed)` pairs — 0 mismatches, with 425 cases where the previous implementation differed.
168
-
169
- ## v1.5.0
170
-
171
- - **Fix**: on DeepSeek Harness **0.1.6-alpha.2** clicking a subagent row failed with `TypeError: ctx.sessions.openSubagent is not a function` (the click was silently swallowed by the row's `try`/`catch`, so the panel just looked dead). 0.1.6-alpha.2 removed `sessions.openSubagent(address)` and `sessions.open(id)`; the plugin now detects the navigation capability at runtime in three tiers (`sessions.openSubagent` → `sessions.open` → `uiWorkspace.openSession`) instead of hard-switching, so the same bundle keeps working on both sides of that upstream change, and the failure is surfaced in the UI when no navigation API exists at all.
172
- - **Fix**: the tier order above is argument-shape driven, because **0.1.5-alpha.2 … 0.1.6-alpha.1** ships a `uiWorkspace.openSession(sessionId)` that only accepts a string id and throws `sessions.select: unknown session [object Object]` for an address, while `sessions.openSubagent(address)` is still present and does accept one on those hosts. Trying the workspace service first there swallowed that throw and showed the "unsupported version" notice, so the address now goes to the object-capable controller entry point first and `uiWorkspace` remains the fallback for 0.1.6-alpha.2+. Verified live on both sides of the band: on **0.1.5-alpha.1** (client packages 0.1.5-rc.2) the reordered bundle switches session in ~142 ms with no alert and no navigation error, and on **0.1.6-alpha.2** the served bundle still resolves to `uiWorkspace.openSession` and opens the child session.
173
- - **Fix**: 0.1.6-alpha.2 also dropped the `current` field from the session list snapshot, which the current-session resolution and the Chat-tab fallback relied on. Both now degrade gracefully: the fallback recognizes a missing `current`, tries the host's own Chat tab immediately, and gives up quietly after 2 s instead of polling for 8 s.
174
-
175
- ## v1.4.0
176
-
177
- - **Compatibility**: supports DeepSeek Harness **0.1.5-rc.2**, backward compatible with all dsh versions. 0.1.5-rc.2 changed the conversation view structure (the slot renderer no longer injects store `actions` into entries that do not declare a store, and the selected view is now persisted per session, with new tabs such as Trajectory) — clicking a subagent lands back on the Chat tab again (when `actions` is unavailable the plugin clicks the host's own Chat tab, preserving the host's activation and persistence semantics). The 0.1.2 capability-detection path remains the untouched primary branch and the 0.1.1 legacy fallback is unchanged.
178
- - **TypeScript migration**: `src/client/*.ts` is now the single source of truth and `lib/client.js` is generated by `pnpm run build` (sucrase type erasure + deterministic linker) instead of being hand-written. New tooling: `verify-build` (token/line-level diff against the v1.3.4 hand-written golden — migration equivalence proof and delta viewer for intentional changes) and `verify-fresh` (stale-bundle gate); `pnpm run check` runs build + typecheck + syntax + freshness in one shot.
179
- - **Fix**: a latent `scopeKey` ReferenceError in the collapsed filter summary (found during the TypeScript migration) is corrected to `workspaceKey`.
180
-
181
- ## v1.3.4
182
-
183
- - **Feature**: the whole UI is localized through DSH client-locale (zh/en, AI-assisted English strings). Labels, buttons, stats, live output (context injection / thinking / tool details) and confirm dialogs now use translation keys and follow the host language. Contributed by [@Marcuss2](https://github.com/Marcuss2) in [PR #1](https://github.com/miuzel/dsh-subagent-ui/pull/1) — thank you!
184
-
185
- ## v1.3.3
186
-
187
- - **Fix**: batch delete no longer errors on large selections (host request-body limit raised to 8 MiB).
188
- - **Feature**: with details hidden, hovering a row/name shows the stats (`输入/输出 · 缓存命中 · 轮数 · 步数`) via the title tooltip.
189
- - **Fix**: live output now shows context injection (`上下文注入 · <form>`) and the thinking state.
190
- - **Feature**: while thinking, a rotating "思考中…" spinner indicates the state instead of the low-priority reasoning text.
191
-
192
- ## v1.3.2
193
-
194
- - **Performance**: the manager now does one base scan (`subagentRows`) and derives `allRows`/`activeRows`/`tabCounts` from it (no repeated full scans or `modeMap` merges), `tabCounts` is computed from a deferred value and is skipped while the panel is closed, and `useSessions` subscribes only the fields the manager reads. Live output is capped to a few simultaneous subagents (`liveCap`, default 3, `0` = unlimited) and fully releases its subscriptions when live display is off or the float/panel is closed.
195
- - **UX**: opening a subagent now auto-switches the session to the Chat tab.
196
- - **Fix**: the batch "select N hours ago" now selects the truly-old subagents in the current view (accurate count) and no longer overwrites or re-selects your manual changes.
197
-
198
- ## Validation
216
+ ## Development and validation
199
217
 
200
218
  The client bundle `lib/client.js` is **generated from the TypeScript sources** in [`src/client/`](src/client/) — edit those, never the bundle, then rebuild:
201
219
 
@@ -207,7 +225,17 @@ pnpm run check # build + tsc --noEmit + node --check lib/index.js + bundle fr
207
225
 
208
226
  `pnpm run verify:build` compares the generated bundle with the hand-written pre-refactor bundle (git ref `v1.3.4`) up to insignificant whitespace, using token-level and line-level comparison. After the migration it doubles as a delta viewer that prints the exact differences of any intentional change.
209
227
 
210
- Smoke-test a specific dsh version:
228
+ ### Local checkout (contributors)
229
+
230
+ To iterate on a clone instead of the npm release, install the working tree into the Web profile from the repository root:
231
+
232
+ ```bash
233
+ dsh plugin --profile web add file:.
234
+ ```
235
+
236
+ Profile installs are store copies, so after editing `src/client/*.ts` you must run `pnpm run build` and re-add the plugin (or run `./test.sh`) before the Web UI can pick the change up. The host-entry (`lib/index.js`) path always needs a `dsh web` restart. The same contributor loop is documented in [`AGENTS.md`](AGENTS.md).
237
+
238
+ ### Smoke test (any dsh version)
211
239
 
212
240
  ```bash
213
241
  ./test.sh # local dsh, port 8084
@@ -219,8 +247,14 @@ DSH_PLUGIN_DIR=.worktrees/x ./test.sh # smoke another checkout's bundle
219
247
 
220
248
  The script always uses an isolated `DSH_HOME` (`$HOME/tmp/dsh-test`, override with `DSH_SMOKE_HOME=…`) and refuses to touch the real `~/.dsh` profile. With `DSH_VERSION` set it runs `pnpx @deepseek-ai/dsh@<version>`; pnpm 12 ignores dependency lifecycle scripts by default, so the script passes `--allow-build=<pkg>` for dsh's native dependencies (`DSH_ALLOW_BUILDS=…` overrides the list).
221
249
 
250
+ ## Release notes
251
+
252
+ Full release notes — every version, newest first — live in the changelog:
253
+
254
+ - [`CHANGELOG.md`](https://github.com/miuzel/dsh-subagent-ui/blob/HEAD/CHANGELOG.md) — English, back to v1.3.2.
255
+ - [`CHANGELOG.zh.md`](https://github.com/miuzel/dsh-subagent-ui/blob/HEAD/CHANGELOG.zh.md) — 中文完整版, back to v1.0.1.
256
+
222
257
  ## Acknowledgements
223
258
 
224
259
  - UI localization (zh/en) contributed by [@Marcuss2](https://github.com/Marcuss2) via [PR #1](https://github.com/miuzel/dsh-subagent-ui/pull/1) — many thanks!
225
260
  - Subagent permanent deletion and session cleanup design inspired by and referencing [@heiheiha798/dsh-plugin-subagent-delete](https://github.com/heiheiha798/dsh-plugin-subagent-delete).
226
-