dsh-subagent-hub 0.10.2 → 0.11.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,15 +1,15 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.10.2/docs/assets/hero.webp" alt="Subagent Hub — Named roles. Clear routes. Visible models." width="100%">
2
+ <img src="https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.11.0/docs/assets/hero.webp" alt="Subagent Hub — Named roles. Clear routes. Visible models." width="100%">
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <a href="https://github.com/ccsert/dsh-subagent-hub/releases/tag/v0.10.2"><img src="https://img.shields.io/badge/version-0.10.2-66c8ab" alt="Version 0.10.2"></a>
7
- <a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
8
- <a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/install.md"><img src="https://img.shields.io/badge/DSH_Desktop-0.12.1-7087ad" alt="Tested with DSH Desktop 0.12.1"></a>
9
- <a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/CONTRIBUTING.md"><img src="https://img.shields.io/badge/verified-102_unit_%2B_31_runtime-66c8ab" alt="102 unit tests and 31 runtime assertions verified"></a>
6
+ <a href="https://github.com/ccsert/dsh-subagent-hub/releases/tag/v0.11.0"><img src="https://img.shields.io/badge/version-0.11.0-66c8ab" alt="Version 0.11.0"></a>
7
+ <a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
8
+ <a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/install.md"><img src="https://img.shields.io/badge/DSH_Desktop-0.12.1-7087ad" alt="Tested with DSH Desktop 0.12.1"></a>
9
+ <a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/CONTRIBUTING.md"><img src="https://img.shields.io/badge/verified-120_unit_%2B_32_runtime-66c8ab" alt="120 unit tests and 32 runtime assertions verified"></a>
10
10
  </p>
11
11
 
12
- <p align="center"><a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/README.md">简体中文</a> · <b>English</b></p>
12
+ <p align="center"><a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/README.md">简体中文</a> · <b>English</b></p>
13
13
 
14
14
  # Subagent Hub
15
15
 
@@ -17,7 +17,7 @@
17
17
 
18
18
  Uses official `subagent` / `subagent_fork` dispatch, Agent Presets, and conversation UI. Keep your existing main-agent preset.
19
19
 
20
- [Install](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/install.md) · [Download a release](https://github.com/ccsert/dsh-subagent-hub/releases/latest) · [Configuration example](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/examples/subagent-hub.yml) · [Changelog](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/CHANGELOG.md) · [Contribute](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/CONTRIBUTING.md)
20
+ [Install](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/install.md) · [Download a release](https://github.com/ccsert/dsh-subagent-hub/releases/latest) · [Configuration example](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/examples/subagent-hub.yml) · [Changelog](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/CHANGELOG.md) · [Contribute](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/CONTRIBUTING.md)
21
21
 
22
22
  ## Features
23
23
 
@@ -30,33 +30,43 @@ Uses official `subagent` / `subagent_fork` dispatch, Agent Presets, and conversa
30
30
  | **Native settings form** | Edit models, roles, tags, and management options in **Settings → Subagent Hub** |
31
31
  | **Request model visibility** | Read the latest provider/model from the child header; expand for full IDs and recorded reasoning effort |
32
32
 
33
+ ## Agent overview
34
+
35
+ Since 0.11.0, an optional **Agent overview** is available, off by default. Enable **Show agent overview** under **Settings → Subagent Hub → General** and save to show the header entry immediately. Turning it off leaves the official Agent Team UI available. It combines independent subagents and Team members with search, filters, sorting, native conversation navigation, shared tasks, dependencies, and write-scope warnings. Models, execution time, and token usage come from native projections. An inactive teammate is not a completed task; missing usage remains unknown.
36
+
37
+ Diagnostics replays committed tool results to show the latest 64 Hub guard rejections and the recorded total. Other tool failures are outside this history. Opening or refreshing the panel performs read-only projection refreshes with at most four concurrent reads; it starts no model requests.
38
+
33
39
  ## Screenshots
34
40
 
35
41
  **Models and named agents**
36
42
 
37
- ![Actual configuration component with demo models and named agents](https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.10.2/docs/assets/settings.webp)
43
+ ![Actual configuration component with demo models and named agents](https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.11.0/docs/assets/settings.webp)
38
44
 
39
45
  **The latest child request**
40
46
 
41
- ![Child header with full provider, model ID and reasoning effort](https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.10.2/docs/assets/model-badge.webp)
47
+ ![Child header with full provider, model ID and reasoning effort](https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.11.0/docs/assets/model-badge.webp)
48
+
49
+ **Agent overview**
50
+
51
+ ![Overview panel with members, run state, model and token evidence](https://raw.githubusercontent.com/ccsert/dsh-subagent-hub/v0.11.0/docs/assets/overview.webp)
42
52
 
43
- These are the actual plugin components rendered with demo data. They contain no user profile, private conversation, or credentials. See [screenshot provenance](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/screenshots.md) for full-size and narrow views. The Hero was generated with [dsh-image-gen](https://github.com/shanliuling/dsh-image-gen).
53
+ These are the actual plugin components rendered with demo data. They contain no user profile, private conversation, or credentials. See [screenshot provenance](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/screenshots.md) for full-size and narrow views. The Hero was generated with [dsh-image-gen](https://github.com/shanliuling/dsh-image-gen).
44
54
 
45
55
  ## Quick start
46
56
 
47
57
  Tested with **DSH Desktop 0.12.1 / `@deepseek-ai/dsh` 0.2.1-rc.2**. Configure a working model provider in DSH first.
48
58
 
49
- 1. Download `dsh-subagent-hub-0.10.2.tgz` from [Releases](https://github.com/ccsert/dsh-subagent-hub/releases/latest).
59
+ 1. Download `dsh-subagent-hub-0.11.0.tgz` from [Releases](https://github.com/ccsert/dsh-subagent-hub/releases/latest).
50
60
  2. Install it from the target profile directory:
51
61
 
52
62
  ```bash
53
- pnpm add /absolute/path/to/dsh-subagent-hub-0.10.2.tgz
63
+ pnpm add /absolute/path/to/dsh-subagent-hub-0.11.0.tgz
54
64
  ```
55
65
 
56
66
  3. **Append** `"dsh-subagent-hub"` to `dsh.profile.bundles` in that profile's package manifest. Preserve existing bundles and restart DSH to load the plugin code.
57
67
  4. Open **Settings → Subagent Hub**, select models already available in your DSH, and add a role. Enable **Manage the official delegation settings** if you want Hub to synchronize the official allowlists. Save, then start a new conversation.
58
68
 
59
- Git checkout installation, profile locations, upgrades, and removal are covered in the [installation guide](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/install.md). The package is published as [dsh-subagent-hub](https://www.npmjs.com/package/dsh-subagent-hub); fixed GitHub release archives are also available. The Team/inheritance guard requires 0.10.2 or newer.
69
+ Git checkout installation, profile locations, upgrades, and removal are covered in the [installation guide](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/install.md). The package is published as [dsh-subagent-hub](https://www.npmjs.com/package/dsh-subagent-hub); fixed GitHub release archives are also available. The Team/inheritance guard requires 0.10.2 or newer.
60
70
 
61
71
  ## Minimal configuration
62
72
 
@@ -85,13 +95,14 @@ The main agent selects a role based on the task and catalog. When the official t
85
95
 
86
96
  ## Behavior to keep in mind
87
97
 
88
- - **Team context selects history.** In the supported runtime, fresh and fork both inherit the Lead model and have no model-selection input. For a specific child model, use a `subagent` tool whose actual schema exposes model selection. See [Team model routing](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/team-model-routing.md).
98
+ - **Team context selects history.** In the supported runtime, fresh and fork both inherit the Lead model and have no model-selection input. For a specific child model, use a `subagent` tool whose actual schema exposes model selection. See [Team model routing](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/team-model-routing.md).
89
99
  - **Global routes also guard inheritance.** A nonempty `routes` list bounds all three creation tools; out-of-list or unconfirmed models are refused. An empty list retains native inheritance. Existing members and later request rewrites are outside this creation check.
90
100
  - **Tags guide decisions.** Tags such as `fast`, `reasoning`, or `free` are maintained by you. Use a role's `routes` for a hard route boundary. Tags do not verify pricing or implement automatic fallback.
91
101
  - **The guard runs before dispatch.** Model rewrites performed by other plugins during a later child request are outside this guard. Check those plugins when combining route policies.
92
102
  - **Fork follows official capabilities.** The target fork provider does not support an independent `agent_preset`; named roles use `subagent`.
93
103
  - **Managed settings are opt-in.** `manageOfficialSettings` defaults to `false`. Enabling it synchronizes official allowlists. Disabling it stops syncing and retains settings already written.
94
104
  - **Existing conversations keep their tool schema.** Saved configuration is live; use a new conversation for newly added roles or tool parameters. Plugin code upgrades require a DSH restart.
105
+ - **Desktop model badges need 0.10.3 or newer.** This fixes the required native list-slot registration field. A component demo is distinct from a reload of the current Desktop window; see the [mounting fix record](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/model-badge-acceptance.md).
95
106
  - **The model badge shows request history.** `lastUsed` identifies the latest request; a different `next` selection is shown separately. A ready empty history says “No request yet”; unavailable data says “Unconfirmed.” A request record does not establish success or billing, and forked history may include inherited requests.
96
107
 
97
108
  ## Development (repository checkout)
@@ -104,8 +115,8 @@ bash test.sh # real DSH integration in a temporary DSH_HOME
104
115
  npm run pack:check # inspect the distributable archive
105
116
  ```
106
117
 
107
- This release is verified by **87 unit tests and 23 real-runtime assertions**. Integration uses scripted model streams and makes no external model calls. CI runs unit, documentation, and package checks. See the [settings acceptance record](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/ui-acceptance.md) and [model badge acceptance record](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/model-badge-acceptance.md) for scope and limitations. [Team routing verification](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/team-model-routing.md) covers the new creation checks.
118
+ This release is verified by **120 unit tests and 32 real-runtime assertions**. Integration uses scripted model streams and makes no external model calls. CI runs unit, documentation, and package checks. See the [settings acceptance record](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/ui-acceptance.md) and [model badge acceptance record](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/model-badge-acceptance.md) for scope and limitations. [Team routing verification](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/team-model-routing.md) covers the new creation checks.
108
119
 
109
- Issues and PRs are welcome. Read [CONTRIBUTING](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/CONTRIBUTING.md), the [design notes](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/design.md), and the [release guide](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/docs/releasing.md). The previous v0.3.0 architecture remains available at its tag.
120
+ Issues and PRs are welcome. Read [CONTRIBUTING](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/CONTRIBUTING.md), the [design notes](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/design.md), and the [release guide](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/docs/releasing.md). The previous v0.3.0 architecture remains available at its tag.
110
121
 
111
- [MIT License](https://github.com/ccsert/dsh-subagent-hub/blob/v0.10.2/LICENSE) · Maintained by [ccsert](https://github.com/ccsert)
122
+ [MIT License](https://github.com/ccsert/dsh-subagent-hub/blob/v0.11.0/LICENSE) · Maintained by [ccsert](https://github.com/ccsert)
package/icon.svg ADDED
@@ -0,0 +1,13 @@
1
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M18 18L9 9M18 18L27 9M18 18V27.5" stroke="#7CB7FF" stroke-opacity="0.7" stroke-width="1.75" stroke-linecap="round" />
3
+ <circle cx="9" cy="9" r="3.25" fill="#45D9E7" />
4
+ <circle cx="27" cy="9" r="3.25" fill="#F2AF63" />
5
+ <circle cx="18" cy="27.5" r="3.25" fill="#7CB7FF" />
6
+ <rect x="13.25" y="13.25" width="9.5" height="9.5" rx="2.75" fill="url(#a)" />
7
+ <defs>
8
+ <linearGradient id="a" x1="21.5" y1="12.5" x2="13.5" y2="23" gradientUnits="userSpaceOnUse">
9
+ <stop stop-color="#7CB7FF" />
10
+ <stop offset="1" stop-color="#145AF3" />
11
+ </linearGradient>
12
+ </defs>
13
+ </svg>
package/lib/client.js CHANGED
@@ -127,7 +127,9 @@ window.__ModuleLoader__.load({
127
127
  'preview.copy': '复制',
128
128
  'preview.copied': '已复制',
129
129
  'preview.copyFailed': '复制失败,请展开目录后手动选择并复制。',
130
- 'save.refused': '保存未应用。配置可能已在其他页面更新,请保留修改并重试。',
130
+ 'save.refused': '保存未应用,修改仍保留。请重试;如仍失败,请按 ⌘⇧R 重启 Harness 后再保存。',
131
+ 'save.conflict': '配置已在其他页面更新,已同步最新版本,修改仍保留。再次保存会用当前内容覆盖。',
132
+ 'overview.restartHint': 'Harness 尚未加载此设置。请按 ⌘⇧R 重启 Harness 后再开启;其他设置可正常保存。',
131
133
  'common.removeTag': '删除标签 {tag}',
132
134
  'common.readOnly': '此配置当前只读。',
133
135
  'footer.dirty': '有未保存的修改',
@@ -257,7 +259,9 @@ window.__ModuleLoader__.load({
257
259
  'preview.copy': 'Copy',
258
260
  'preview.copied': 'Copied',
259
261
  'preview.copyFailed': 'Copy failed. Expand the preview and select the text to copy it.',
260
- 'save.refused': 'The save was not applied. Configuration may have changed in another page; keep your edits and try again.',
262
+ 'save.refused': 'The save was not applied; your edits are kept. Try again, and if it still fails, restart Harness with ⌘⇧R and save.',
263
+ 'save.conflict': 'Configuration changed in another page. The latest version is loaded and your edits are kept; saving again overwrites it with this page.',
264
+ 'overview.restartHint': 'Harness has not loaded this setting yet. Restart Harness with ⌘⇧R to turn it on; other settings save normally.',
261
265
  'common.removeTag': 'Remove tag {tag}',
262
266
  'common.readOnly': 'This configuration is read-only.',
263
267
  'footer.dirty': 'Unsaved changes',
@@ -417,6 +421,7 @@ window.__ModuleLoader__.load({
417
421
  const routes = Array.isArray(value?.routes) ? value.routes : []
418
422
  const agents = Array.isArray(value?.agents) ? value.agents : []
419
423
  return {
424
+ showAgentOverview: value?.showAgentOverview === true,
420
425
  manageOfficialSettings: value?.manageOfficialSettings === true,
421
426
  maxConcurrent: Number.isFinite(value?.maxConcurrent) ? Number(value.maxConcurrent) : 4,
422
427
  routes: routes.map(route => ({
@@ -462,14 +467,17 @@ window.__ModuleLoader__.load({
462
467
  ...(agent.needs.length > 0 ? { needs: agent.needs } : {}),
463
468
  ...(agent.basePreset && agent.basePreset !== 'standard' ? { basePreset: agent.basePreset } : {}),
464
469
  }))
465
- return { manageOfficialSettings: drafts.manageOfficialSettings === true, maxConcurrent: drafts.maxConcurrent, routes, agents }
470
+ return { showAgentOverview: drafts.showAgentOverview === true, manageOfficialSettings: drafts.manageOfficialSettings === true, maxConcurrent: drafts.maxConcurrent, routes, agents }
466
471
  }
467
472
 
468
473
  /** The atomic mutate operations one save performs, against a single revision. */
469
- function saveOps(values) {
474
+ // A Host still running an older schema refuses the whole write over one unknown path, so
475
+ // `showAgentOverview` is only written once the Host's resolved section carries that field.
476
+ function saveOps(values, overviewSupported = true) {
470
477
  return [
471
478
  { op: 'set', path: ['routes'], value: values.routes },
472
479
  { op: 'set', path: ['agents'], value: values.agents },
480
+ ...(overviewSupported ? [{ op: 'set', path: ['showAgentOverview'], value: values.showAgentOverview === true }] : []),
473
481
  { op: 'set', path: ['manageOfficialSettings'], value: values.manageOfficialSettings },
474
482
  { op: 'set', path: ['maxConcurrent'], value: values.maxConcurrent },
475
483
  ]
@@ -1210,6 +1218,10 @@ window.__ModuleLoader__.load({
1210
1218
  const rootRef = React.useRef(null)
1211
1219
  const writable = form !== undefined && form.state?.status !== 'loading' && form.state?.writable !== false
1212
1220
  const hydrated = React.useRef(form?.state?.status !== 'loading')
1221
+ // The schema-resolved section lists every field the running Host knows, defaults included.
1222
+ const overviewSupported = Object.prototype.hasOwnProperty.call(value, 'showAgentOverview')
1223
+ const latestRevision = React.useRef(form?.state?.revision)
1224
+ latestRevision.current = form?.state?.revision
1213
1225
  const disposed = React.useRef(false)
1214
1226
  React.useEffect(() => {
1215
1227
  disposed.current = false
@@ -1367,8 +1379,15 @@ window.__ModuleLoader__.load({
1367
1379
  setBusy(true)
1368
1380
  stopSync.current()
1369
1381
  try {
1370
- const applied = await form.mutate(saveOps(values), form.state?.revision)
1371
- if (applied !== true) throw new Error(t('save.refused'))
1382
+ const revision = form.state?.revision
1383
+ const applied = await form.mutate(saveOps(values, overviewSupported), revision)
1384
+ if (applied !== true) {
1385
+ // A refused write reloads Host state; a moved revision means another editor won.
1386
+ await new Promise(resolve => setTimeout(resolve, 0))
1387
+ const conflict = latestRevision.current !== undefined && latestRevision.current !== revision
1388
+ setStatus({ kind: 'error', text: t(conflict ? 'save.conflict' : 'save.refused') })
1389
+ return
1390
+ }
1372
1391
  setBaseline(draftsOf(values))
1373
1392
  setDraft(editorDraftsOf(values))
1374
1393
  setSubmitted(false)
@@ -1468,6 +1487,13 @@ window.__ModuleLoader__.load({
1468
1487
  disabled: locked,
1469
1488
  onClick: () => setDraft(current => ({ ...current, maxConcurrent: limit })),
1470
1489
  }, limit === 0 ? t('concurrency.unlimited') : String(limit)))))),
1490
+ h(SettingRow, { title: t('overview.setting'), hint: t(overviewSupported || form?.state?.status !== 'ready' ? 'overview.settingHint' : 'overview.restartHint') },
1491
+ h(Toggle, {
1492
+ checked: draft.showAgentOverview,
1493
+ disabled: locked || !overviewSupported,
1494
+ label: t('overview.setting'),
1495
+ onChange: next => setDraft(current => ({ ...current, showAgentOverview: next })),
1496
+ })),
1471
1497
  h(SettingRow, { title: t('official.switch'), hint: t('official.hint') },
1472
1498
  h(Toggle, {
1473
1499
  checked: draft.manageOfficialSettings,
@@ -1578,21 +1604,24 @@ window.__ModuleLoader__.load({
1578
1604
  const EMPTY_SESSION = Object.freeze({ subagent: null })
1579
1605
  const MODEL_BADGE_SLOT = 'conversation.session.header.utilities'
1580
1606
  const MODEL_BADGE_CSS = `
1581
- .sah-runtime-model{position:relative;min-width:0;max-width:260px;color:var(--dsw-alias-label-secondary,#b8b8b8);font-size:12px;line-height:1.4;-webkit-app-region:no-drag}
1582
- .sah-runtime-model summary{display:flex;align-items:center;gap:5px;min-height:28px;max-width:min(260px,42vw);padding:4px 7px;list-style:none;border:1px solid var(--dsw-alias-border-l1,#36373b);border-radius:6px;background:transparent;cursor:pointer;user-select:none}
1607
+ .sah-runtime-model{position:relative;min-width:0;max-width:260px;color:var(--dsw-alias-label-tertiary,#999);font-size:12px;line-height:16px;-webkit-app-region:no-drag}
1608
+ .sah-runtime-model summary{display:inline-flex;box-sizing:border-box;align-items:center;gap:5px;min-height:28px;max-width:min(260px,42vw);padding:3px 7px;list-style:none;border:0;border-radius:6px;background:transparent;color:inherit;cursor:pointer;user-select:none;transition:color .12s ease-out}
1609
+ .sah-runtime-model summary:hover,.sah-runtime-model summary:focus-visible,.sah-runtime-model[open] summary{color:var(--dsw-alias-label-primary,#e6e6e6)}
1610
+ .sah-runtime-model-icon{display:inline-flex;flex:none}
1611
+ @container (width<=480px){.sah-runtime-model-value{display:none}}
1583
1612
  .sah-runtime-model summary::-webkit-details-marker{display:none}
1584
1613
  .sah-runtime-model summary:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#5686fe);outline-offset:2px}
1585
- .sah-runtime-model[open] summary{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12))}
1586
- .sah-runtime-model-label,.sah-runtime-model-arrow{flex:none;color:var(--dsw-alias-label-tertiary,#8c8c8c)}
1587
- .sah-runtime-model-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,#e6e6e6)}
1588
- .sah-runtime-model-panel{position:fixed;top:64px;right:12px;z-index:1000;box-sizing:border-box;width:320px;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);overflow:auto;padding:12px;border:1px solid var(--dsw-alias-border-l1,#36373b);border-radius:10px;background:var(--dsw-alias-bg-layer-2,#232529);color:var(--dsw-alias-label-primary,#e6e6e6);box-shadow:0 4px 16px rgba(0,0,0,.16);user-select:text}
1614
+ .sah-runtime-model-arrow{flex:none;display:inline-flex;color:var(--dsw-alias-label-caption,#888)}
1615
+ .sah-runtime-model-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1616
+ .sah-runtime-model-panel{position:fixed;top:64px;right:12px;z-index:1000;box-sizing:border-box;width:320px;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);overflow:auto;padding:12px 14px;border:0;border-radius:12px;background:transparent;isolation:isolate;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1,#36373b);box-shadow:var(--dsw-elevation-prominent,0 0 0 1px #36373b,0 8px 24px rgba(0,0,0,.24));color:var(--dsw-alias-label-primary,#e6e6e6);font-size:13px;line-height:20px;user-select:text}
1617
+ .sah-runtime-model-panel::before{content:"";position:absolute;inset:0;z-index:-1;border-radius:12px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2,#232529));backdrop-filter:var(--dsw-menu-backdrop-filter,none)}
1618
+ .sah-runtime-model-panel strong{font-weight:500}
1589
1619
  .sah-runtime-model-panel p{margin:0 0 8px;overflow-wrap:anywhere}
1590
1620
  .sah-runtime-model-panel dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:6px 12px;margin:0}
1591
1621
  .sah-runtime-model-panel dt{color:var(--dsw-alias-label-secondary,#b8b8b8)}
1592
- .sah-runtime-model-panel dd{margin:0;overflow-wrap:anywhere;white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,monospace}
1593
- .sah-runtime-model-note{margin-top:10px!important;color:var(--dsw-alias-label-secondary,#b8b8b8)}
1622
+ .sah-runtime-model-panel dd{margin:0;overflow-wrap:anywhere;white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px}
1623
+ .sah-runtime-model-note{margin-top:10px!important;color:var(--dsw-alias-label-tertiary,#999);font-size:12px}
1594
1624
  .sah-runtime-model-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
1595
- @media(hover:hover){.sah-runtime-model summary:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12))}}
1596
1625
  @media(forced-colors:active){.sah-runtime-model summary:focus-visible{outline-color:Highlight}}
1597
1626
  `
1598
1627
 
@@ -1702,11 +1731,17 @@ window.__ModuleLoader__.load({
1702
1731
  },
1703
1732
  },
1704
1733
  h('summary', { 'aria-label': fullLabel, 'aria-controls': panelId, title: fullLabel },
1705
- h('span', { className: 'sah-runtime-model-label' }, t('modelBadge.label')),
1734
+ h('span', { className: 'sah-runtime-model-icon', 'aria-hidden': true },
1735
+ h('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' },
1736
+ h('rect', { x: 6, y: 6, width: 12, height: 12, rx: 2.5 }), h('rect', { x: 9.5, y: 9.5, width: 5, height: 5, rx: 1 }),
1737
+ h('path', { d: 'M9.5 2.5v3.5M14.5 2.5v3.5M9.5 18v3.5M14.5 18v3.5M2.5 9.5H6M2.5 14.5H6M18 9.5h3.5M18 14.5h3.5' }))),
1738
+ h('span', { className: 'sah-runtime-model-label sah-runtime-model-sr' }, t('modelBadge.label')),
1706
1739
  h('span', { className: 'sah-runtime-model-value' }, data.kind === 'recorded'
1707
1740
  ? `${data.last.provider} · ${data.last.model.split('/').filter(Boolean).at(-1) ?? data.last.model}`
1708
1741
  : t(data.kind === 'waiting' ? 'modelBadge.waitingShort' : 'modelBadge.unknownShort')),
1709
- h('span', { className: 'sah-runtime-model-arrow', 'aria-hidden': true }, '▾')),
1742
+ h('span', { className: 'sah-runtime-model-arrow', 'aria-hidden': true },
1743
+ h('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' },
1744
+ h('path', { d: 'm7 10 5 5 5-5' })))),
1710
1745
  h('div', { className: 'sah-runtime-model-panel', ref: panelRef, id: panelId, role: 'group', 'aria-label': t('modelBadge.details') },
1711
1746
  h('p', null, h('strong', null, t(data.kind === 'recorded' ? 'modelBadge.recorded' : data.kind === 'waiting' ? 'modelBadge.waiting' : 'modelBadge.unknown'))),
1712
1747
  data.kind === 'recorded' ? rows(data.last) : null,
@@ -1719,17 +1754,379 @@ window.__ModuleLoader__.load({
1719
1754
  function registerModelBadge(ctx, t) {
1720
1755
  ctx.inject(['sessions'], scope => scope.slots.inject(MODEL_BADGE_SLOT, () => scope.slots.register({
1721
1756
  name: MODEL_BADGE_SLOT,
1722
- key: 'subagent-hub-model',
1757
+ id: 'subagent-hub-model',
1723
1758
  order: -10,
1724
1759
  locale: NS,
1725
1760
  inject: sessionId => modelBadgeShares(scope.sessions, sessionId),
1726
1761
  }, props => h(ModelBadge, { ...props, t: props.t ?? t }))))
1727
1762
  }
1728
1763
 
1764
+ // ── native child / Team overview ──────────────────────────────────────────
1765
+ const OVERVIEW_SLOT = 'conversation.session.header.actions'
1766
+ Object.assign(en, {
1767
+ 'overview.setting': 'Show agent overview',
1768
+ 'overview.settingHint': 'Off by default. Adds the Hub overview to the session header after saving; the official Agent Team UI is always available.',
1769
+ 'overview.triggerHint': 'Agent overview · {running} running / {total} total',
1770
+ 'overview.title': 'Agent overview', 'overview.close': 'Close', 'overview.refresh': 'Refresh',
1771
+ 'overview.refreshing': 'Refreshing…', 'overview.search': 'Search name, model or session ID',
1772
+ 'overview.all': 'All', 'overview.team': 'Team', 'overview.independent': 'Independent',
1773
+ 'overview.running': 'Running', 'overview.inactive': 'Inactive', 'overview.completed': 'Completed',
1774
+ 'overview.interrupted': 'Interrupted', 'overview.provisioning': 'Provisioning', 'overview.failed': 'Failed',
1775
+ 'overview.unknown': 'Unconfirmed', 'overview.members': 'Members', 'overview.tasks': 'Task board',
1776
+ 'overview.diagnostics': 'Diagnostics', 'overview.empty': 'No child agents in this session.',
1777
+ 'overview.noMatches': 'No matching members. Clear the filters to see all members.',
1778
+ 'overview.loading': 'Loading the child catalog…', 'overview.unavailable': 'The child catalog is unavailable.',
1779
+ 'overview.migration': 'Open this conversation in DSH to migrate its history.',
1780
+ 'overview.stale': 'Some data could not be refreshed. Displayed records may be outdated.',
1781
+ 'overview.open': 'Open conversation', 'overview.noNavigation': 'This child mode cannot be opened here.',
1782
+ 'overview.details': 'Evidence and details', 'overview.model': 'Last request', 'overview.next': 'Planned next request',
1783
+ 'overview.noRequest': 'No request recorded', 'overview.noModel': 'Request model unconfirmed',
1784
+ 'overview.duration': 'Active time', 'overview.tokens': 'Recorded tokens', 'overview.created': 'Created',
1785
+ 'overview.kind': 'Type', 'overview.state': 'State', 'overview.sort': 'Sort',
1786
+ 'overview.priority': 'Activity first', 'overview.newest': 'Newest first', 'overview.name': 'Name',
1787
+ 'overview.timeSort': 'Active time', 'overview.tokenSort': 'Recorded tokens',
1788
+ 'overview.inactiveNote': 'Inactive Team members are available for another turn. Only the task board records task completion.',
1789
+ 'overview.tokenNote': 'Token totals cover {known} of {total} members; missing records are not zero. No price estimate.',
1790
+ 'overview.durationNote': 'Active time sums agent turns, excluding time between turns. Interrupted open turns stop at the last recorded event.',
1791
+ 'overview.owner': 'Owner', 'overview.unowned': 'Unassigned', 'overview.dependencies': 'Dependencies',
1792
+ 'overview.scopes': 'Write scopes', 'overview.pending': 'Pending', 'overview.in_progress': 'In progress',
1793
+ 'overview.ready': 'Ready', 'overview.blocked': 'Blocked', 'overview.noTasks': 'No shared tasks recorded.',
1794
+ 'overview.noTeam': 'Team projection unavailable; Team may be disabled.',
1795
+ 'overview.healthy': 'No projection errors reported.',
1796
+ 'overview.coverage': 'Hub guard rejections reconstructed from committed tool results. This is not a log of every tool failure.',
1797
+ 'overview.rejections': 'Dispatch rejections', 'overview.noRejections': 'No Hub guard rejections recorded.',
1798
+ 'overview.noHistory': 'Rejection projection unavailable. Inspect tool results in the parent conversation.',
1799
+ 'overview.historyCount': 'Showing the latest {shown} of {total} recorded rejections.',
1800
+ 'overview.reset': 'Clear filters', 'overview.current': 'Current conversation', 'overview.lead': 'Lead',
1801
+ })
1802
+ Object.assign(zh, {
1803
+ 'overview.setting': '显示子智能体总览',
1804
+ 'overview.settingHint': '默认关闭。保存后在会话标题栏显示 Hub 总览入口;官方的智能体团队界面始终可用。',
1805
+ 'overview.triggerHint': '子智能体总览 · {running} 个运行中 / 共 {total} 个',
1806
+ 'overview.title': '子智能体总览', 'overview.close': '关闭', 'overview.refresh': '刷新',
1807
+ 'overview.refreshing': '刷新中…', 'overview.search': '搜索名称、模型或会话 ID',
1808
+ 'overview.all': '全部', 'overview.team': 'Team 成员', 'overview.independent': '独立子任务',
1809
+ 'overview.running': '运行中', 'overview.inactive': '空闲', 'overview.completed': '已完成',
1810
+ 'overview.interrupted': '已中断', 'overview.provisioning': '创建中', 'overview.failed': '创建失败',
1811
+ 'overview.unknown': '未确认', 'overview.members': '成员', 'overview.tasks': '共享任务板',
1812
+ 'overview.diagnostics': '诊断', 'overview.empty': '这个会话还没有子智能体。',
1813
+ 'overview.noMatches': '没有匹配的成员。清除筛选可查看全部成员。',
1814
+ 'overview.loading': '正在加载子任务目录…', 'overview.unavailable': '暂时无法获取子任务目录。',
1815
+ 'overview.migration': '请在 DSH 中打开此会话以迁移历史记录。',
1816
+ 'overview.stale': '部分数据刷新失败,当前显示的记录可能不是最新状态。',
1817
+ 'overview.open': '打开会话', 'overview.noNavigation': '此类子任务暂不支持从这里打开。',
1818
+ 'overview.details': '证据与详情', 'overview.model': '最近一次请求', 'overview.next': '下次请求计划',
1819
+ 'overview.noRequest': '尚无请求记录', 'overview.noModel': '请求模型未确认',
1820
+ 'overview.duration': '累计执行', 'overview.tokens': '已记录 Token', 'overview.created': '创建时间',
1821
+ 'overview.kind': '类型', 'overview.state': '状态', 'overview.sort': '排序',
1822
+ 'overview.priority': '活动优先', 'overview.newest': '最新创建', 'overview.name': '名称',
1823
+ 'overview.timeSort': '执行耗时', 'overview.tokenSort': 'Token 用量',
1824
+ 'overview.inactiveNote': 'Team 成员空闲表示当前没有回合在执行,任务是否完成以共享任务板为准。',
1825
+ 'overview.tokenNote': 'Token 汇总覆盖 {known}/{total} 位成员;缺失记录不当作零,不估算费用。',
1826
+ 'overview.durationNote': '累计执行时间只计各执行回合,不含回合间空闲;中断回合固定到最后一条事件的时间。',
1827
+ 'overview.owner': '负责人', 'overview.unowned': '未分配', 'overview.dependencies': '依赖任务',
1828
+ 'overview.scopes': '写入范围', 'overview.pending': '待处理', 'overview.in_progress': '进行中',
1829
+ 'overview.ready': '可开始', 'overview.blocked': '依赖阻塞', 'overview.noTasks': '尚无共享任务记录。',
1830
+ 'overview.noTeam': 'Team 投影不可用,可能未启用 Team。',
1831
+ 'overview.healthy': '未报告投影错误。',
1832
+ 'overview.coverage': '从持久化工具结果重建的 Hub 守卫拒绝记录,不包含其他工具错误。',
1833
+ 'overview.rejections': '派发拒绝', 'overview.noRejections': '尚无 Hub 守卫拒绝记录。',
1834
+ 'overview.noHistory': '拒绝记录投影不可用,可在父会话工具结果中查看。',
1835
+ 'overview.historyCount': '显示最近 {shown} 条,共记录 {total} 次拒绝。',
1836
+ 'overview.reset': '清除筛选', 'overview.current': '当前会话', 'overview.lead': 'Lead',
1837
+ })
1838
+
1839
+ const OVERVIEW_CSS = `
1840
+ .sah-overview-trigger{display:inline-flex;align-items:center;gap:5px;font:inherit;font-size:12px;font-weight:400;min-height:28px;padding:3px 7px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#999);cursor:pointer;white-space:nowrap;-webkit-app-region:no-drag}
1841
+ .sah-overview-trigger:hover,.sah-overview-trigger:focus-visible,.sah-overview-trigger[aria-expanded=true]{color:var(--dsw-alias-label-primary,#eee)}
1842
+ .sah-overview-trigger-icon{display:inline-flex;align-items:center;flex:none}
1843
+ .sah-overview-trigger-count{color:var(--dsw-alias-label-caption,#888);font-variant-numeric:tabular-nums;font-size:12px;font-weight:400;line-height:16px}
1844
+ @container (width<=480px){.sah-overview-trigger-label{display:none}}
1845
+ .sah-overview{box-sizing:border-box;width:min(880px,calc(100vw - 32px));max-height:min(760px,calc(100dvh - 32px));padding:0;border:0;border-radius:12px;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1,#ffffff1f);box-shadow:var(--dsw-elevation-prominent,0 0 0 .5px #ffffff1f,0 3px 8px #0000000a,0 0 20px #0000000d);background:var(--dsw-specific-menu,#303136f0);-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));color:var(--dsw-alias-label-primary,#eee);font-size:13px;line-height:20px;overflow:hidden;-webkit-app-region:no-drag}
1846
+ .sah-overview[open]{display:flex;flex-direction:column}
1847
+ .sah-overview::backdrop{background:#0000004d}
1848
+ .sah-overview *{box-sizing:border-box}.sah-overview h2{font-size:14px;font-weight:500;margin:0}.sah-overview h3{font-size:13px;font-weight:500;margin:0}.sah-overview p{margin:4px 0;overflow-wrap:anywhere}
1849
+ .sah-overview-head{flex:none;display:flex;align-items:center;gap:4px;padding:12px 12px 8px 18px}.sah-overview-head h2{margin-inline-end:auto}
1850
+ .sah-overview-body{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;padding:4px 12px 18px 18px;display:grid;align-content:start;gap:14px}
1851
+ .sah-overview button,.sah-overview select,.sah-overview input{font:inherit;font-size:12px;color:inherit;border-radius:6px;min-height:28px}
1852
+ .sah-overview button{display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:3px 9px;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#bbb);cursor:pointer;white-space:nowrap;transition:background-color .12s ease-out,color .12s ease-out,transform .1s ease-out}
1853
+ .sah-overview button:not(:disabled):active{transform:scale(.96)}
1854
+ .sah-overview button:disabled{opacity:.5;cursor:default}
1855
+ .sah-overview select,.sah-overview input{padding:4px 8px;border:0;background:var(--dsw-alias-bg-layer-2,#ffffff0d);--dsw-elevation-stroke-color:var(--dsw-alias-border-l2,#ffffff1f);box-shadow:var(--dsw-elevation-stroke,0 0 0 .5px #ffffff1f)}
1856
+ .sah-overview input::placeholder{color:var(--dsw-alias-label-caption,#888)}
1857
+ @media(hover:hover){.sah-overview button:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff14);color:var(--dsw-alias-label-primary,#eee)}}
1858
+ .sah-overview :is(button,input,select,summary):focus-visible,.sah-overview-trigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#719cff);outline-offset:2px}
1859
+ .sah-overview-stats,.sah-overview-toolbar,.sah-overview-tabs{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
1860
+ .sah-overview-stat{flex:1;min-width:100px;padding:10px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#ffffff0a);--dsw-elevation-stroke-color:var(--dsw-alias-border-l2,#ffffff1f);box-shadow:var(--dsw-elevation-stroke,0 0 0 .5px #ffffff1f)}
1861
+ .sah-overview-stat strong{display:block;margin-top:2px;font-size:18px;line-height:24px;font-weight:500;font-variant-numeric:tabular-nums}
1862
+ .sah-overview-muted{color:var(--dsw-alias-label-tertiary,#999)}
1863
+ .sah-overview-tabs{gap:2px}.sah-overview-tabs button[aria-pressed=true]{background:var(--dsw-alias-interactive-bg-hover,#ffffff14);color:var(--dsw-alias-label-primary,#eee);font-weight:500}
1864
+ .sah-overview-toolbar{color:var(--dsw-alias-label-tertiary,#999);font-size:12px}.sah-overview-toolbar label{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.sah-overview-toolbar input{width:100%;min-width:0}.sah-overview-search{flex:1 1 100%}
1865
+ .sah-overview-list{display:grid;gap:8px;list-style:none;margin:0;padding:0}
1866
+ .sah-overview-card{min-width:0;padding:10px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#ffffff0a);--dsw-elevation-stroke-color:var(--dsw-alias-border-l2,#ffffff1f);box-shadow:var(--dsw-elevation-stroke,0 0 0 .5px #ffffff1f);overflow-wrap:anywhere}
1867
+ .sah-overview-card-head{display:flex;align-items:center;flex-wrap:wrap;gap:6px}.sah-overview-card-head h3{flex:1;min-width:0}
1868
+ .sah-overview-chip{font-size:11px;line-height:16px;padding:1px 7px;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover,#ffffff14);color:var(--dsw-alias-label-tertiary,#999)}
1869
+ .sah-overview-chip[data-state=running]{color:var(--dsw-alias-state-business-primary,#9bb8ff);background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#9bb8ff) 14%,transparent)}
1870
+ .sah-overview-chip[data-state=failed],.sah-overview-error{color:var(--dsw-alias-state-error-primary,#ffb0a8)}
1871
+ .sah-overview-chip[data-state=failed]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ffb0a8) 14%,transparent)}
1872
+ .sah-overview-meta{display:flex;flex-wrap:wrap;gap:4px 16px;margin-block:4px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999);font-variant-numeric:tabular-nums}
1873
+ .sah-overview details{margin-block-start:4px}.sah-overview summary{cursor:pointer;min-height:28px;display:flex;align-items:center;gap:4px;font-size:12px;color:var(--dsw-alias-label-tertiary,#999);border-radius:6px}.sah-overview summary:hover{color:var(--dsw-alias-label-primary,#eee)}
1874
+ .sah-overview dl{display:grid;grid-template-columns:minmax(90px,auto) minmax(0,1fr);gap:4px 16px;margin:6px 0;font-size:12px}.sah-overview dd{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}.sah-overview dt{color:var(--dsw-alias-label-tertiary,#999)}
1875
+ .sah-overview code{font-size:12px;overflow-wrap:anywhere}
1876
+ .sah-overview-notice{padding:10px 12px;border-radius:10px;background:var(--dsw-alias-bg-layer-2,#ffffff0a);--dsw-elevation-stroke-color:var(--dsw-alias-border-l2,#ffffff1f);box-shadow:var(--dsw-elevation-stroke,0 0 0 .5px #ffffff1f)}
1877
+ .sah-overview-task-description{white-space:pre-wrap}
1878
+ @media(prefers-reduced-motion:reduce){.sah-overview button{transition:none}.sah-overview button:not(:disabled):active{transform:none}}
1879
+ @media(max-width:520px){.sah-overview{width:calc(100vw - 16px);max-height:calc(100dvh - 16px)}.sah-overview-head{padding:10px 8px 6px 12px}.sah-overview-body{padding:4px 8px 12px 12px;gap:12px}.sah-overview dl{grid-template-columns:1fr;gap:2px}.sah-overview dd{margin-block-end:6px}.sah-overview-toolbar label{flex:1 1 100%}.sah-overview-toolbar select{flex:1;min-width:0}.sah-overview-card-head button{margin-inline-start:auto}}
1880
+ `
1881
+
1882
+ function overviewDuration(timing, running, now) {
1883
+ if (!Number.isFinite(timing?.settledMs) || timing.settledMs < 0) return undefined
1884
+ if (!timing.active) return timing.settledMs
1885
+ const { since, through } = timing.active
1886
+ if (!Number.isFinite(since) || !Number.isFinite(through)) return undefined
1887
+ return timing.settledMs + Math.max(0, (running ? Math.max(now, through) : through) - since)
1888
+ }
1889
+ function overviewTokens(usage) {
1890
+ const buckets = ['uncachedInputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']
1891
+ if (!usage || buckets.some(key => !Number.isFinite(usage[key]) || usage[key] < 0)) return undefined
1892
+ return buckets.reduce((sum, key) => sum + usage[key], 0)
1893
+ }
1894
+ function overviewTime(ms) {
1895
+ if (!Number.isFinite(ms)) return '—'
1896
+ const seconds = Math.floor(Math.max(0, ms) / 1000)
1897
+ return `${Math.floor(seconds / 3600)}:${String(Math.floor(seconds / 60) % 60).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`
1898
+ }
1899
+ /** Membership is authoritative only in the parent's catalog/roster, never inferred from global sessions. */
1900
+ function overviewData(rootId, state, statuses, now = Date.now()) {
1901
+ const snapshots = state?.projectionsBySession ?? {}
1902
+ const summaries = state?.byId ?? {}
1903
+ const snapshot = snapshots[rootId]
1904
+ const values = { ...summaries[rootId]?.projectionValues, ...snapshot?.values }
1905
+ const catalog = values.subagentCatalog
1906
+ const team = values.agentTeam
1907
+ const members = new Map()
1908
+ for (const entry of catalog ?? []) members.set(entry.id, { ...entry, kind: 'independent', name: entry.label ?? summaries[entry.id]?.displayTitle ?? entry.id })
1909
+ for (const member of team?.members ?? []) {
1910
+ if (member.role === 'lead') continue
1911
+ members.set(member.id, { ...members.get(member.id), ...member, kind: 'team', mode: 'continuable' })
1912
+ }
1913
+ const rows = [...members.values()].map(member => {
1914
+ const child = snapshots[member.id]
1915
+ const projected = { ...summaries[member.id]?.projectionValues, ...child?.values }
1916
+ const running = statuses?.get(member.id)?.running ?? summaries[member.id]?.running
1917
+ const timing = projected.subagentTiming
1918
+ const status = member.phase === 'failed' ? 'failed' : member.phase === 'provisioning' ? 'provisioning'
1919
+ : running === true ? 'running' : running === undefined ? 'unknown'
1920
+ : member.kind === 'team' ? 'inactive'
1921
+ : member.mode === 'one-shot' && timing?.lastTurnCompleted === true ? 'completed'
1922
+ : timing?.lastTurnCompleted === false ? 'interrupted' : 'inactive'
1923
+ return { ...member, status, model: modelBadgeData({ subagent: true }, projected.modelSelection),
1924
+ duration: overviewDuration(timing, running === true, now), tokens: overviewTokens(projected.tokenUsage),
1925
+ readState: child?.state, readError: child?.error?.message,
1926
+ address: ['one-shot', 'continuable'].includes(member.mode) ? { parentSessionId: rootId, childSessionId: member.id, mode: member.mode } : undefined }
1927
+ })
1928
+ const issues = []
1929
+ if (snapshot?.error) issues.push({ id: rootId, message: snapshot.error.message ?? String(snapshot.error) })
1930
+ if (team?.failure) issues.push({ id: rootId, message: team.failure })
1931
+ for (const row of rows) {
1932
+ if (row.error) issues.push({ id: row.id, message: row.error })
1933
+ if (row.readError) issues.push({ id: row.id, message: row.readError })
1934
+ }
1935
+ return { rows, dispatch: values.subagentHubDispatch, tasks: (team?.tasks ?? []).filter(task => task.status !== 'deleted'), issues,
1936
+ teamAvailable: team !== undefined, catalogAvailable: Array.isArray(catalog),
1937
+ readState: snapshot?.state ?? (state?.phase === 'pending' ? 'loading' : 'idle'),
1938
+ running: rows.filter(row => row.status === 'running').length,
1939
+ tokenKnown: rows.filter(row => row.tokens !== undefined).length,
1940
+ tokens: rows.reduce((sum, row) => sum + (row.tokens ?? 0), 0) }
1941
+ }
1942
+ function overviewFilter(rows, { search = '', kind = 'all', status = 'all', sort = 'priority' } = {}) {
1943
+ const query = search.trim().toLocaleLowerCase()
1944
+ const rank = { running: 0, provisioning: 1, failed: 2, interrupted: 3, unknown: 4, inactive: 5, completed: 6 }
1945
+ return rows.filter(row => (kind === 'all' || row.kind === kind) && (status === 'all' || row.status === status)
1946
+ && (!query || [row.name, row.id, row.model.last?.provider, row.model.last?.model, row.error].filter(Boolean).join(' ').toLocaleLowerCase().includes(query)))
1947
+ .sort((a, b) => {
1948
+ const primary = sort === 'name' ? a.name.localeCompare(b.name)
1949
+ : sort === 'newest' ? (b.createdAt ?? -1) - (a.createdAt ?? -1)
1950
+ : sort === 'timeSort' ? (b.duration ?? -1) - (a.duration ?? -1)
1951
+ : sort === 'tokenSort' ? (b.tokens ?? -1) - (a.tokens ?? -1) : rank[a.status] - rank[b.status]
1952
+ return primary || a.name.localeCompare(b.name) || a.id.localeCompare(b.id)
1953
+ })
1954
+ }
1955
+ /** Bound read pressure and let closing the panel stop scheduling further work. */
1956
+ async function refreshOverview(sessions, rootId, ids, cancelled = () => false) {
1957
+ const queue = [...new Set([rootId, ...ids].filter(Boolean))]
1958
+ const failures = []
1959
+ async function worker() {
1960
+ while (queue.length && !cancelled()) {
1961
+ const id = queue.shift()
1962
+ try { await sessions.refreshProjections(id) }
1963
+ catch (error) { failures.push({ id, message: String(error?.message ?? error) }) }
1964
+ }
1965
+ }
1966
+ await Promise.all([worker(), worker(), worker(), worker()])
1967
+ return failures
1968
+ }
1969
+ function OverviewMember({ row, currentId, openChild, t }) {
1970
+ const route = value => `${value.provider} / ${value.model}${value.reasoningEffort ? ` · ${value.reasoningEffort}` : ''}`
1971
+ return h('li', { className: 'sah-overview-card' },
1972
+ h('div', { className: 'sah-overview-card-head' }, h('h3', null, row.name),
1973
+ h('span', { className: 'sah-overview-chip' }, t(`overview.${row.kind}`)),
1974
+ h('span', { className: 'sah-overview-chip', 'data-state': row.status }, t(`overview.${row.status}`)),
1975
+ h('button', { type: 'button', disabled: !row.address || row.id === currentId,
1976
+ title: !row.address ? t('overview.noNavigation') : undefined,
1977
+ onClick: () => openChild(row.address) }, t(row.id === currentId ? 'overview.current' : 'overview.open'))),
1978
+ h('p', null, row.model.kind === 'recorded' ? `${t('overview.model')}: ${route(row.model.last)}`
1979
+ : t(row.model.kind === 'waiting' ? 'overview.noRequest' : 'overview.noModel')),
1980
+ h('div', { className: 'sah-overview-meta sah-overview-muted' },
1981
+ h('span', null, `${t('overview.duration')}: ${overviewTime(row.duration)}`),
1982
+ h('span', null, `${t('overview.tokens')}: ${row.tokens?.toLocaleString() ?? '—'}`)),
1983
+ row.error || row.readError ? h('p', { className: 'sah-overview-error' }, row.error || row.readError) : null,
1984
+ h('details', null, h('summary', null, t('overview.details')),
1985
+ h('dl', null, h('dt', null, 'Session ID'), h('dd', null, h('code', null, row.id)),
1986
+ h('dt', null, t('overview.kind')), h('dd', null, row.mode),
1987
+ Number.isFinite(row.createdAt) ? h(React.Fragment, null, h('dt', null, t('overview.created')), h('dd', null, new Date(row.createdAt).toLocaleString())) : null,
1988
+ row.model.next ? h(React.Fragment, null, h('dt', null, t('overview.next')), h('dd', null, route(row.model.next))) : null),
1989
+ row.readState === 'migration-required' ? h('p', null, t('overview.migration')) : null))
1990
+ }
1991
+ function OverviewTasks({ tasks, teamAvailable, t }) {
1992
+ if (!teamAvailable) return h('p', { role: 'status' }, t('overview.noTeam'))
1993
+ if (!tasks.length) return h('p', null, t('overview.noTasks'))
1994
+ return h('ul', { className: 'sah-overview-list' }, ...tasks.map(task => h('li', { key: task.id, className: 'sah-overview-card' },
1995
+ h('div', { className: 'sah-overview-card-head' }, h('h3', null, task.subject),
1996
+ h('span', { className: 'sah-overview-chip' }, t(`overview.${task.status}`)),
1997
+ task.status === 'pending' ? h('span', { className: 'sah-overview-chip' }, t(task.ready ? 'overview.ready' : 'overview.blocked')) : null),
1998
+ h('p', { className: 'sah-overview-muted' }, `${task.id} · ${t('overview.owner')}: ${task.ownerName ?? t('overview.unowned')}`),
1999
+ h('details', null, h('summary', null, t('overview.details')),
2000
+ h('p', { className: 'sah-overview-task-description' }, task.description),
2001
+ h('dl', null, h('dt', null, t('overview.dependencies')), h('dd', null, task.blockedBy?.join(', ') || '—'),
2002
+ h('dt', null, t('overview.scopes')), h('dd', null, task.writeScopes?.join('\n') || '—'))),
2003
+ ...(task.writeScopeWarnings ?? []).map((warning, index) => h('p', { key: index, className: 'sah-overview-error' }, warning)))))
2004
+ }
2005
+ function OverviewContent({ data, currentId, openChild, t, errors = [] }) {
2006
+ const [tab, setTab] = React.useState('members')
2007
+ const [filters, setFilters] = React.useState({ search: '', kind: 'all', status: 'all', sort: 'priority' })
2008
+ const rows = overviewFilter(data.rows, filters)
2009
+ const issues = [...data.issues, ...errors]
2010
+ const select = (key, values) => h('label', null, t(`overview.${key === 'status' ? 'state' : key}`),
2011
+ h('select', { value: filters[key], onChange: event => setFilters(old => ({ ...old, [key]: event.target.value })) },
2012
+ ...values.map(value => h('option', { key: value, value }, t(`overview.${value}`)))))
2013
+ return h('div', { className: 'sah-overview-body' },
2014
+ h('div', { className: 'sah-overview-stats' }, ...[
2015
+ ['members', data.rows.length], ['running', data.running], ['tasks', data.tasks.length],
2016
+ ['tokens', data.tokenKnown ? data.tokens.toLocaleString() : '—'],
2017
+ ].map(([label, count]) => h('div', { key: label, className: 'sah-overview-stat' }, h('span', { className: 'sah-overview-muted' }, t(`overview.${label}`)), h('strong', null, count)))),
2018
+ issues.length ? h('div', { className: 'sah-overview-notice sah-overview-error', role: 'status' }, t('overview.stale')) : null,
2019
+ h('div', { className: 'sah-overview-tabs', role: 'group', 'aria-label': t('overview.title') },
2020
+ ...['members', 'tasks', 'diagnostics'].map(value => h('button', { type: 'button', key: value, 'aria-pressed': tab === value, onClick: () => setTab(value) },
2021
+ t(`overview.${value}`), value === 'diagnostics' && issues.length ? ` (${issues.length})` : ''))),
2022
+ tab === 'members' ? h(React.Fragment, null,
2023
+ h('div', { className: 'sah-overview-toolbar' },
2024
+ h('label', { className: 'sah-overview-search' }, t('overview.search'), h('input', { type: 'search', value: filters.search, onChange: event => setFilters(old => ({ ...old, search: event.target.value })) })),
2025
+ select('kind', ['all', 'team', 'independent']),
2026
+ select('status', ['all', 'running', 'provisioning', 'inactive', 'completed', 'interrupted', 'failed', 'unknown']),
2027
+ select('sort', ['priority', 'newest', 'name', 'timeSort', 'tokenSort']),
2028
+ h('button', { type: 'button', onClick: () => setFilters({ search: '', kind: 'all', status: 'all', sort: 'priority' }) }, t('overview.reset'))),
2029
+ !data.catalogAvailable ? h('p', { role: 'status' }, t(data.readState === 'loading' ? 'overview.loading' : data.readState === 'migration-required' ? 'overview.migration' : 'overview.unavailable')) : null,
2030
+ rows.length ? h('ul', { className: 'sah-overview-list' }, ...rows.map(row => h(OverviewMember, { key: row.id, row, currentId, openChild, t })))
2031
+ : data.rows.length || data.catalogAvailable ? h('p', { role: 'status' }, t(data.rows.length ? 'overview.noMatches' : 'overview.empty')) : null,
2032
+ h('p', { className: 'sah-overview-muted' }, t('overview.inactiveNote')))
2033
+ : tab === 'tasks' ? h(OverviewTasks, { tasks: data.tasks, teamAvailable: data.teamAvailable, t })
2034
+ : h('section', null, h('h3', null, t('overview.diagnostics')),
2035
+ issues.length ? h('ul', null, ...issues.map((issue, index) => h('li', { key: index }, h('code', null, issue.id), ': ', issue.message))) : h('p', null, t('overview.healthy')),
2036
+ h('h3', null, t('overview.rejections')),
2037
+ !data.dispatch ? h('p', null, t('overview.noHistory')) : data.dispatch.records.length
2038
+ ? h(React.Fragment, null,
2039
+ h('p', null, t('overview.historyCount', { shown: data.dispatch.records.length, total: data.dispatch.total })),
2040
+ h('ul', { className: 'sah-overview-list' }, ...[...data.dispatch.records].reverse().map(record => h('li', { key: record.seq, className: 'sah-overview-card' },
2041
+ h('h3', null, record.tool), h('p', { className: 'sah-overview-muted' }, `${new Date(record.time).toLocaleString()} · #${record.seq}`),
2042
+ h('p', null, record.reason), h('code', null, record.callId)))))
2043
+ : h('p', null, t('overview.noRejections')),
2044
+ h('p', null, t('overview.coverage'))),
2045
+ h('div', { className: 'sah-overview-muted' },
2046
+ h('p', null, t('overview.tokenNote', { known: data.tokenKnown, total: data.rows.length })), h('p', null, t('overview.durationNote'))))
2047
+ }
2048
+ function AgentOverview({ sessionId, useSession, useSessions, useSessionStatus, sessions, openSession, t }) {
2049
+ const rootId = useSession(value => value.subagent?.address.parentSessionId) ?? sessionId
2050
+ const state = useSessions(value => value)
2051
+ const statuses = useSessionStatus(value => value)
2052
+ const [open, setOpen] = React.useState(false)
2053
+ const [now, setNow] = React.useState(Date.now)
2054
+ const [busy, setBusy] = React.useState(false)
2055
+ const [errors, setErrors] = React.useState([])
2056
+ const dialog = React.useRef(null)
2057
+ const trigger = React.useRef(null)
2058
+ const generation = React.useRef(0)
2059
+ const data = overviewData(rootId, state, statuses, now)
2060
+ const titleId = React.useId()
2061
+ const close = () => { dialog.current?.close(); setOpen(false); trigger.current?.focus() }
2062
+ const refresh = React.useCallback(async () => {
2063
+ const run = ++generation.current
2064
+ const cancelled = () => generation.current !== run
2065
+ setBusy(true)
2066
+ setErrors([])
2067
+ // Read the parent first so newly discovered members are included in this refresh.
2068
+ const failures = await refreshOverview(sessions, rootId, [], cancelled)
2069
+ const refreshed = overviewData(rootId, sessions.list.getSnapshot(), new Map())
2070
+ if (!cancelled()) failures.push(...await refreshOverview(sessions, undefined, refreshed.rows.map(row => row.id), cancelled))
2071
+ if (!cancelled()) { setErrors(failures); setBusy(false) }
2072
+ }, [sessions, rootId])
2073
+ React.useEffect(() => {
2074
+ if (!open) return
2075
+ dialog.current?.showModal()
2076
+ setNow(Date.now())
2077
+ dialog.current?.querySelector('[data-overview-close]')?.focus()
2078
+ refresh()
2079
+ return () => { generation.current++; dialog.current?.close() }
2080
+ }, [open, refresh])
2081
+ React.useEffect(() => { setOpen(false); setErrors([]); setBusy(false) }, [sessionId])
2082
+ React.useEffect(() => {
2083
+ if (!open || !data.running) return
2084
+ const timer = setInterval(() => setNow(Date.now()), 1000)
2085
+ return () => clearInterval(timer)
2086
+ }, [open, data.running])
2087
+ const openChild = async address => {
2088
+ try { await openSession(address); close() }
2089
+ catch (error) { setErrors(old => [...old, { id: address.childSessionId, message: String(error?.message ?? error) }]) }
2090
+ }
2091
+ return h(React.Fragment, null, h('style', null, OVERVIEW_CSS),
2092
+ h('button', { type: 'button', ref: trigger, className: 'sah-overview-trigger', 'aria-haspopup': 'dialog', 'aria-expanded': open,
2093
+ 'aria-label': t('overview.triggerHint', { running: data.running, total: data.rows.length }),
2094
+ title: t('overview.triggerHint', { running: data.running, total: data.rows.length }), onClick: () => setOpen(true) },
2095
+ h('span', { className: 'sah-overview-trigger-icon', 'aria-hidden': true }, glyph('IconBrowseOutlineRegular',
2096
+ h('svg', { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5 },
2097
+ h('rect', { x: 3, y: 3, width: 7, height: 7, rx: 1.5 }), h('rect', { x: 14, y: 3, width: 7, height: 7, rx: 1.5 }),
2098
+ h('rect', { x: 3, y: 14, width: 7, height: 7, rx: 1.5 }), h('rect', { x: 14, y: 14, width: 7, height: 7, rx: 1.5 })))),
2099
+ h('span', { className: 'sah-overview-trigger-label' }, t('overview.title')),
2100
+ data.rows.length ? h('span', { className: 'sah-overview-trigger-count', 'aria-hidden': true }, `${data.running}/${data.rows.length}`) : null),
2101
+ h('dialog', { ref: dialog, className: 'sah-overview', 'aria-labelledby': titleId, onCancel: event => { event.preventDefault(); close() }, onClose: () => { if (!dialog.current?.open) setOpen(false) } },
2102
+ h('div', { className: 'sah-overview-head' }, h('h2', { id: titleId }, t('overview.title')),
2103
+ h('button', { type: 'button', disabled: busy, onClick: refresh }, t(busy ? 'overview.refreshing' : 'overview.refresh')),
2104
+ h('button', { type: 'button', onClick: close, 'data-overview-close': '', autoFocus: true }, t('overview.close'))),
2105
+ open ? h(OverviewContent, { key: rootId, data, currentId: sessionId, openChild, t, errors }) : null))
2106
+ }
2107
+ function OverviewGate({ overviewForm, ...props }) {
2108
+ const [snapshot, setSnapshot] = React.useState(() => overviewForm?.getSnapshot())
2109
+ React.useEffect(() => {
2110
+ const update = () => setSnapshot(overviewForm?.getSnapshot())
2111
+ const dispose = overviewForm?.subscribe(update)
2112
+ update()
2113
+ return dispose
2114
+ }, [overviewForm])
2115
+ return snapshot?.status === 'ready' && snapshot.value?.showAgentOverview === true
2116
+ ? h(AgentOverview, props) : null
2117
+ }
2118
+ function registerOverview(ctx, t) {
2119
+ ctx.inject(['sessions', 'uiWorkspace'], scope => scope.slots.inject(OVERVIEW_SLOT, () => scope.slots.register({
2120
+ name: OVERVIEW_SLOT, id: 'subagent-hub-overview', order: -15, locale: NS,
2121
+ inject: () => ({ sessions: scope.sessions, openSession: address => scope.uiWorkspace.openSession(address), overviewForm: ctx.configForms.get(ENTRY_ID) }),
2122
+ }, props => h(OverviewGate, { ...props, t: props.t ?? t }))))
2123
+ }
2124
+
1729
2125
  function apply(ctx) {
1730
2126
  ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'subagent-hub: dictionaries')
1731
2127
  const t = ctx.locale.bind(NS)
1732
2128
  registerModelBadge(ctx, t)
2129
+ registerOverview(ctx, t)
1733
2130
  // The same form is reachable from the Plugins page row and from a top-level
1734
2131
  // Settings section, which is where people expect plugin configuration.
1735
2132
  ctx.effect(() => ctx.configForms.whileServed([ENTRY_ID], () => ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
@@ -1750,6 +2147,9 @@ window.__ModuleLoader__.load({
1750
2147
  inject: ['slots', 'locale', 'configForms', 'remote', 'remote.session', 'remote.agentPresets'],
1751
2148
  apply,
1752
2149
  // Pure helpers, exported for tests only.
2150
+ AgentOverview, OverviewGate, OverviewContent, OverviewMember, OverviewTasks, overviewData, overviewFilter,
2151
+ overviewDuration, overviewTokens, overviewTime, refreshOverview, registerOverview, OVERVIEW_SLOT,
2152
+ overviewLocales: { en, zh }, OVERVIEW_CSS,
1753
2153
  ModelBadge,
1754
2154
  modelBadgeData,
1755
2155
  modelBadgeShares,
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod'
2
+ import { GUARD_PREFIX } from './guard.js'
3
+
4
+ export const DISPATCH_PROJECTION = 'subagentHubDispatch'
5
+ export const DISPATCH_HISTORY_LIMIT = 64
6
+ const viewSchema = z.object({
7
+ total: z.number().int().nonnegative(),
8
+ records: z.array(z.object({ seq: z.number().int(), time: z.number(), callId: z.string(), tool: z.string(), reason: z.string() })).max(DISPATCH_HISTORY_LIMIT),
9
+ })
10
+ const stateSchema = z.object({
11
+ pending: z.array(z.object({ callId: z.string(), tool: z.string() })).max(128),
12
+ view: viewSchema,
13
+ })
14
+
15
+ // Fold committed history, not a client-side cache or a pre-execution side effect.
16
+ // Persist no prompts, tool arguments, credentials, or successful tool output.
17
+ export function dispatchFold(state, event) {
18
+ if (event.type === 'tool/call') {
19
+ return { ...state, pending: [...state.pending.filter(call => call.callId !== event.data.callId),
20
+ { callId: event.data.callId, tool: event.data.name }].slice(-128) }
21
+ }
22
+ if (event.type !== 'tool/result') return state
23
+ const message = event.data.message
24
+ const callId = message.toolCallId
25
+ const call = state.pending.find(item => item.callId === callId)
26
+ const pending = state.pending.filter(item => item.callId !== callId)
27
+ // Native pre-execution denials omit error.info unless the guard supplied it.
28
+ // Their durable representation is a single `Error: <reason>` text block.
29
+ const text = message.content?.length === 1 && message.content[0].type === 'text' ? message.content[0].text : undefined
30
+ const nativeReason = ['subagent', 'subagent_fork', 'spawn_teammate'].includes(call?.tool)
31
+ && text?.startsWith(`Error: ${GUARD_PREFIX}`) ? text.slice(7) : undefined
32
+ const reason = event.data.error?.reason ?? nativeReason
33
+ if (message.isError !== true || (event.surfaceOp !== undefined && event.surfaceOp !== 'append') || typeof reason !== 'string' || !reason.startsWith(GUARD_PREFIX)) {
34
+ return pending.length === state.pending.length ? state : { ...state, pending }
35
+ }
36
+ // Surface rewrites retain the call ID. A correction is not another dispatch.
37
+ if (state.view.records.some(record => record.callId === callId)) return { ...state, pending }
38
+ const record = { seq: event.seq, time: event.time, callId, tool: call?.tool ?? 'unknown', reason }
39
+ return { pending, view: { total: state.view.total + 1, records: [...state.view.records, record].slice(-DISPATCH_HISTORY_LIMIT) } }
40
+ }
41
+ export const dispatchProjection = {
42
+ key: DISPATCH_PROJECTION, stateVersion: 1, stateSchema,
43
+ init: () => ({ pending: [], view: { total: 0, records: [] } }),
44
+ apply: dispatchFold,
45
+ wire: { viewSchema, view: state => state.view },
46
+ }
package/lib/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  // `routes`, `agents`, and `manageOfficialSettings` are volatile: the Plugins page form edits them
12
12
  // live. After such an edit the service re-registers affected presets and re-syncs the official
13
13
  // allowlists (see `loader/volatile-update` below).
14
+ import { dispatchProjection } from './dispatch-history.js'
14
15
  import { load } from 'js-yaml'
15
16
  import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
16
17
  import { Service } from '@deepseek-ai/cordis'
@@ -60,6 +61,8 @@ export default class SubagentHub extends Service {
60
61
  + 'Enforced before a delegation starts, so the main agent is told to wait instead of piling up children.'),
61
62
  delegationTools: z.array(z.string()).default(['subagent', 'subagent_fork']).volatile()
62
63
  .description('Tool names that start a child agent. The official delegation tools are named `subagent` and `subagent_fork`.'),
64
+ showAgentOverview: z.boolean().default(false).volatile()
65
+ .description('Show the optional Hub agent overview in the session header. The official Agent Team UI remains available.'),
63
66
  manageOfficialSettings: z.boolean().default(false).volatile()
64
67
  .description('When true, write `allowedAgentPresets` onto the official delegation tool rows and '
65
68
  + 'mirror `routes` into the Host `subagent-model-selection` setting through configEditor. '
@@ -117,6 +120,9 @@ export default class SubagentHub extends Service {
117
120
  order: this.ctx.systemPrompt.getSectionOrder('TOOL_SUBAGENT') + 10,
118
121
  text: context => this.sectionText(context.scope),
119
122
  }))
123
+ this.disposers.push(this.ctx.inject(['sessionProjections'], scope => {
124
+ scope.sessionProjections.register(dispatchProjection)
125
+ }))
120
126
  this.disposers.push(this.ctx.on('tools/pre-execute', (exec, next) => this.preExecute(exec, next)))
121
127
  // The Plugins page form writes volatile fields; the Loader commits them in place and announces
122
128
  // the changed paths on this plugin's own fiber.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "dsh-subagent-hub",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "description": "Named roles, model route guards, native configuration, and request-model visibility for official DeepSeek Harness subagents.",
5
+ "icon": "./icon.svg",
5
6
  "license": "MIT",
6
7
  "type": "module",
7
8
  "repository": {
@@ -16,6 +17,7 @@
16
17
  "./locale/*.json": "./locale/*.json"
17
18
  },
18
19
  "files": [
20
+ "icon.svg",
19
21
  "lib",
20
22
  "locale/*.json",
21
23
  "cordis.patch.yml",
@@ -46,7 +48,8 @@
46
48
  "@deepseek-ai/dsh": ">=0.2.1-rc.2 <0.3"
47
49
  },
48
50
  "dependencies": {
49
- "js-yaml": "^4.1.0"
51
+ "js-yaml": "^4.1.0",
52
+ "zod": "^4.0.0"
50
53
  },
51
54
  "homepage": "https://github.com/ccsert/dsh-subagent-hub#readme",
52
55
  "bugs": {