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 +28 -17
- package/icon.svg +13 -0
- package/lib/client.js +418 -18
- package/lib/dispatch-history.js +46 -0
- package/lib/index.js +6 -0
- package/package.json +5 -2
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.
|
|
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.
|
|
7
|
-
<a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.
|
|
8
|
-
<a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.
|
|
9
|
-
<a href="https://github.com/ccsert/dsh-subagent-hub/blob/v0.
|
|
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.
|
|
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.
|
|
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
|
-

|
|
38
44
|
|
|
39
45
|
**The latest child request**
|
|
40
46
|
|
|
41
|
-

|
|
48
|
+
|
|
49
|
+
**Agent overview**
|
|
50
|
+
|
|
51
|
+

|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 **
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1371
|
-
|
|
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-
|
|
1582
|
-
.sah-runtime-model summary{display:flex;align-items:center;gap:5px;min-height:28px;max-width:min(260px,42vw);padding:
|
|
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
|
|
1586
|
-
.sah-runtime-model-
|
|
1587
|
-
.sah-runtime-model-
|
|
1588
|
-
.sah-runtime-model-panel{
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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": {
|