dsh-plugin-t-expert 0.2.14 → 0.2.16

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.
Files changed (3) hide show
  1. package/README.md +49 -2
  2. package/lib/client.js +578 -6
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -15,6 +15,18 @@ DeepSeek Harness 插件:**22 个分类 / 316 位专家**的名册(中文名
15
15
  设置页一共**三个标签**:**专家**(启停 / 看提示词 / 新建自建专家)、**分类**(自建分类的增删改)、
16
16
  **队伍**(小队模板 + 运行中的团队)。
17
17
 
18
+ 真实的后台任务列表不在设置页。右上角有一个**常驻的小徽章**(与团队徽章同一层、同一视觉体系),
19
+ 它是随时点开查看的入口;只有真的还有东西在跑时才亮起来并给出运行中数量(安静时只是一个灰点,不显示数字),
20
+ 点开就是任务面板:当前会话及其子代理的后台任务,只读(状态、退出码与耗时,
21
+ 不含输出与终止)。面板可拖动,位置记在本机。
22
+
23
+ 徽章只在"从无到有"时冒出,**面板绝不自动展开**——展开与否完全由你决定,关掉之后同一批任务不会再打扰你。
24
+ 子代理区显示的是**子代理的真名**(取自语宿主的子代理目录 `label`,而不是从提示词生成的会话标题),
25
+ 点一行即可跳到那个子代理的会话(与官方任务页同一个 `openSubagent` 接口)。
26
+ 每行副标题显示该子代理的**请求文本前一段**(会话标题就是从它生成的),并标注运行中/已结束;
27
+ 列表顺序沿用宿主子代理目录的原生次序——与官方任务页一致(快照里没有创建时间字段,不自造一种序)。
28
+ 输入区 **T专家** 按钮弹开的浮层里也有一个**任务**页(与团队 tab 同层),随时手动查看。
29
+
18
30
  ---
19
31
 
20
32
  ## 一、安装
@@ -183,15 +195,50 @@ Host/Origin 校验与浏览器认证,静态资源另有白名单,非白名
183
195
  ```bash
184
196
  npm install # 装开发期依赖(.npmrc 里开了 legacy-peer-deps,原因见该文件)
185
197
  npm run typecheck # 类型检查(tsconfig.json 只覆盖自研 Host 文件,不碰并入的引擎)
186
- npm run verify # 自检套件(现 511 项断言):插件契约、工具、remote、客户端产物、播种与快照,
198
+ npm run verify # 自检套件(现 521 项断言):插件契约、工具、remote、客户端产物、播种与快照,
187
199
  # 外加「发布包自洽」(真打一份 tgz、解开、再用插件解析器读一遍)
200
+ npm run invariants # 引擎不变量门禁:真 cordis + 真 lib/teams + 桩 Agent,只报「违反集 D」
188
201
  npm run build # 构建客户端产物(lib/client.js)
189
202
  npm run sync-data # 把运行时数据同步进包内 data/
190
203
  ```
191
204
 
192
205
  `tools/` 里的构建 / 自检 / 同步脚本**随仓库走**,所以干净 clone 就能跑 `typecheck` 与 `verify`;
193
206
  需要运维台(`tz.sh`、`add-expert.py`)的那几节在本机没有运维目录时会**显式打印 skip**,不会
194
- 静默少跑。推送与 PR 由 `.github/workflows/ci.yml` 跑同一套门禁。
207
+ 静默少跑。推送与 PR 由 `.github/workflows/ci.yml` 跑同一套门禁(含 `npm run invariants`)。
208
+
209
+ ### 引擎不变量(`npm run invariants`)
210
+
211
+ `lib/teams/**` 是内置的团队引擎(约 9,600 行)。它没有「读一遍代码」的判据,所以这里用
212
+ **可执行探针**把引擎声称永远成立的契约钉住,只看**违反集 D**:`D = ∅` 即本轮干净。
213
+
214
+ | 不变量 | 钉住什么 |
215
+ | --- | --- |
216
+ | `INV-1` | 任务被转交/重试后**旧 `attempt_id` 必须失效**(旧主人与伪造令牌都被拒) |
217
+ | `INV-2` | 两个未完成的**写**任务不得共用同一 `inScope` 路径;`implementation` 必须带 `inScope` |
218
+ | `INV-3` | 一成员同时只能有一个未完成任务(顺序与**并发**都成立,并发恰好 1 胜) |
219
+ | `INV-4` | `halted` 语义:停→取消未完成任务并拒新单;`resume` 必须带非空 reason,之后才恢复派发 |
220
+ | `INV-5` | 并发写不丢更新:N 次并发建单全部落盘、id 唯一、状态文件始终可解析 |
221
+ | `INV-6` | `review` 必须带 `reviewedTaskId`;`needs_revision` 走 `failed` 且自动生成 `repair`;`verdict=pass` 才算完成 |
222
+ | `INV-7` | 质量类建单契约:`implementation` 必须带 `objective`/`acceptance`/`inScope`/`verify` |
223
+ | `INV-8` | 调度器契约:`approve` 后 pending 任务会被自动派给空闲成员(不是停在池里等人 claim) |
224
+ | `INV-9` | 宿主路径契约:`agents` 未就绪时快照不得抛错(用**真 cordis 受门控 ctx** 钉住;旧写法会抛 `without inject`,那一瞬间所有团队会从面板消失) |
225
+
226
+ **聚合入口**:`npm run invariants -- --all` 会把引擎不变量与既有的三道门禁一起跑,统一输出违反集 `D`:
227
+
228
+ | 聚合项 | 说明 |
229
+ | --- | --- |
230
+ | 引擎不变量 | 本文件的 9 条(真 cordis + 真 `lib/teams` + 桩 Agent) |
231
+ | 插件契约 | 以**黑盒**调用 `tools/verify.mjs`(521 项),只取退出码与摘要行 |
232
+ | 宿主 skill 漂移 | `tools/skill-drift.mjs`(它现在也能独立跑:有漂移即退出码 1) |
233
+ | 数据快照一致 | `tools/sync-data.mjs --check`(**源目录不存在时显式记 SKIP**,不算违反) |
234
+
235
+ 约定:门禁报告「已跳过」不算违反(与 `verify.mjs` 的 skip 规矩一致)。CI 里仍分别跑 `verify` 与
236
+ `invariants`(不重复跑一遍 `--all`)。
237
+
238
+ 探针基座的三条忠告写在 `tools/invariants.mjs` 的文件头(成员 id 要 approve 后才出现、团队 id
239
+ 会被 slug 化、工具执行上下文必须带 `signal` 且成员 agent 要有 `status`/`whenIdle`)——它们都是
240
+ 写这个门禁时踩出来的,改探针前先读那段。
241
+
195
242
 
196
243
  四条容易踩的开发约定:
197
244
 
package/lib/client.js CHANGED
@@ -19888,6 +19888,31 @@ var zh = {
19888
19888
  "tab.squads": "\u5C0F\u961F",
19889
19889
  "tab.teams": "\u56E2\u961F",
19890
19890
  "tab.roster": "\u961F\u4F0D",
19891
+ "tab.tasks": "\u4EFB\u52A1",
19892
+ "tasks.hint": "\u5F53\u524D\u4F1A\u8BDD\u53CA\u5176\u5B50\u4EE3\u7406\u7684\u540E\u53F0\u4EFB\u52A1\uFF1A\u7531\u5BBF\u4E3B\u7684 session/jobs \u63A8\u9001\u5B9E\u65F6\u5237\u65B0\uFF0C\u53EA\u8BFB\uFF0C\u4E0D\u542B\u8F93\u51FA\u4E0E\u7EC8\u6B62\u3002",
19893
+ "tasks.empty": "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u540E\u53F0\u4EFB\u52A1\u3002",
19894
+ "tasks.count": "{total} \u9879",
19895
+ "tasks.countRunning": "{running} \u9879\u8FD0\u884C\u4E2D \xB7 {total} \u9879",
19896
+ "tasks.owner": "\u5F52\u5C5E\uFF1A{name}",
19897
+ "tasks.status.running": "\u8FD0\u884C\u4E2D",
19898
+ "tasks.status.stopping": "\u505C\u6B62\u4E2D",
19899
+ "tasks.status.completed": "\u5DF2\u5B8C\u6210",
19900
+ "tasks.status.killed": "\u5DF2\u7EC8\u6B62",
19901
+ "tasks.status.failed": "\u5931\u8D25",
19902
+ "tasks.duration.seconds": "{seconds} \u79D2",
19903
+ "tasks.duration.minutes": "{minutes} \u5206 {seconds} \u79D2",
19904
+ "tasks.badgeTitle": "\u540E\u53F0\u4EFB\u52A1\uFF1A\u70B9\u5F00\u770B\u5217\u8868\uFF08\u53EF\u62D6\u52A8\u9762\u677F\uFF09",
19905
+ "tasks.subagents": "\u5B50\u4EE3\u7406",
19906
+ "tasks.subagentRunningCount": "{running} \u4E2A\u5B50\u4EE3\u7406\u8FD0\u884C\u4E2D",
19907
+ "tasks.subagentCount": "{total} \u4E2A\u5B50\u4EE3\u7406",
19908
+ "tasks.jobRunningCount": "{running} \u9879\u4EFB\u52A1\u8FD0\u884C\u4E2D",
19909
+ "tasks.jobCount": "{total} \u9879\u4EFB\u52A1",
19910
+ "tasks.noSubagents": "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5B50\u4EE3\u7406\u3002",
19911
+ "tasks.stateRunning": "\u8FD0\u884C\u4E2D",
19912
+ "tasks.modeOneShot": "\u4E00\u6B21\u6027",
19913
+ "tasks.modeContinuable": "\u53EF\u7EE7\u7EED",
19914
+ "tasks.stateInactive": "\u5DF2\u7ED3\u675F",
19915
+ "tasks.subagentOpen": "\u70B9\u51FB\u6253\u5F00\u8BE5\u5B50\u4EE3\u7406\u4F1A\u8BDD",
19891
19916
  "roster.squads": "\u5C0F\u961F\u6A21\u677F\uFF08\u53EF\u590D\u7528\uFF09",
19892
19917
  "roster.squadsHint": "\u9884\u5148\u6311\u597D\u7684\u73ED\u5E95\uFF1A\u6210\u5458\u3001\u522B\u540D\u3001\u542F\u505C\u3002\u7528\u5B83\u5C31\u8BF4 /t <\u5C0F\u961F> <\u76EE\u6807>\uFF0C\u6216\u5728\u8F93\u5165\u6846\u5F39\u7A97\u91CC\u70B9\u300C\u586B\u5165\u300D\uFF1B\u6539\u5B8C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\u3001\u65E0\u9700\u91CD\u542F\uFF08/t \u5217\u8868\u4F1A\u7ACB\u5373\u5237\u65B0\uFF0C\u5F15\u64CE\u4F1A\u5C31\u5730\u70ED\u91CD\u8F7D\uFF09\uFF1B\u4FDD\u5B58\u5931\u8D25\u4F1A\u56DE\u6EDA\uFF0C\u76D8\u4E0A\u4ECD\u662F\u65E7\u914D\u7F6E\u3002",
19893
19918
  "roster.teams": "\u8FD0\u884C\u4E2D\u7684\u56E2\u961F",
@@ -20033,6 +20058,31 @@ var en = {
20033
20058
  "tab.squads": "Squads",
20034
20059
  "tab.teams": "Teams",
20035
20060
  "tab.roster": "Roster",
20061
+ "tab.tasks": "Tasks",
20062
+ "tasks.hint": "Background jobs of the current conversation and its subagents, refreshed live from the host session/jobs push \u2014 read-only, no output view, no kill.",
20063
+ "tasks.empty": "No background jobs in this conversation yet.",
20064
+ "tasks.count": "{total} total",
20065
+ "tasks.countRunning": "{running} running \xB7 {total} total",
20066
+ "tasks.owner": "owner: {name}",
20067
+ "tasks.status.running": "running",
20068
+ "tasks.status.stopping": "stopping",
20069
+ "tasks.status.completed": "completed",
20070
+ "tasks.status.killed": "killed",
20071
+ "tasks.status.failed": "failed",
20072
+ "tasks.duration.seconds": "{seconds}s",
20073
+ "tasks.duration.minutes": "{minutes}m {seconds}s",
20074
+ "tasks.badgeTitle": "Background jobs \u2014 click to open the list (draggable)",
20075
+ "tasks.subagents": "Subagents",
20076
+ "tasks.subagentRunningCount": "{running} subagents running",
20077
+ "tasks.subagentCount": "{total} subagents",
20078
+ "tasks.jobRunningCount": "{running} jobs running",
20079
+ "tasks.jobCount": "{total} jobs",
20080
+ "tasks.noSubagents": "No subagents in this conversation.",
20081
+ "tasks.stateRunning": "running",
20082
+ "tasks.modeOneShot": "one-shot",
20083
+ "tasks.modeContinuable": "continuable",
20084
+ "tasks.stateInactive": "finished",
20085
+ "tasks.subagentOpen": "Open this subagent session",
20036
20086
  "roster.squads": "Squad templates (reusable)",
20037
20087
  "roster.squadsHint": "A pre-picked roster: members, aliases, enable/disable. Run it with /t <squad> <goal>, or tap Fill in the composer popup. Saving takes effect immediately, with no restart (the /t list refreshes at once and the engine hot-reloads in place); a failed save rolls back, so the old config stays on disk.",
20038
20088
  "roster.teams": "Running teams",
@@ -20179,6 +20229,17 @@ var CSS = `
20179
20229
  .t-team-picker-row:hover{background:var(--dsw-alias-bg-layer-3)}
20180
20230
  .t-team-picker-slug{color:var(--dsw-alias-label-caption);font-family:var(--dsw-font-markdown-code-block-small);font-size:11px}
20181
20231
  .t-team-selected-row{display:flex;align-items:center;gap:6px;font-size:12px;padding:3px 4px;border-bottom:1px solid var(--dsw-alias-border-l2)}
20232
+ .t-team-jobs{display:flex;flex-direction:column;gap:4px}
20233
+ .t-team-job-row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-2)}
20234
+ .t-team-job-dot{width:8px;height:8px;border-radius:50%;flex:none;background:var(--dsw-alias-label-caption)}
20235
+ .t-team-job-dot[data-state="running"]{background:var(--dsw-alias-state-success-primary)}
20236
+ .t-team-job-dot[data-state="stopping"]{background:var(--dsw-alias-state-warn-primary)}
20237
+ .t-team-job-dot[data-state="completed"]{background:var(--dsw-alias-label-secondary)}
20238
+ .t-team-job-dot[data-state="killed"]{background:var(--dsw-alias-state-warn-primary)}
20239
+ .t-team-job-dot[data-state="failed"]{background:var(--dsw-alias-state-error-primary)}
20240
+ .t-team-job-main{flex:1 1 auto;min-width:0;width:100%;display:flex;flex-direction:column;gap:2px}
20241
+ .t-team-job-label{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
20242
+ .t-team-job-meta{font-size:12px;color:var(--dsw-alias-label-caption);display:flex;flex-direction:column;gap:1px;min-width:0}
20182
20243
  .t-team-teams{display:flex;flex-direction:column;gap:8px}
20183
20244
  .t-team-team{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:10px 12px;display:flex;flex-direction:column;gap:6px}
20184
20245
  .t-team-team-head{display:flex;align-items:center;gap:8px}
@@ -20258,6 +20319,40 @@ var CSS = `
20258
20319
  .t-team-pop .t-team-squad{padding:8px;gap:6px}
20259
20320
  .t-team-pop .t-team-squad-desc{font-size:12px}
20260
20321
  .t-team-pop .t-team-team{padding:8px}
20322
+ .t-team-pop .t-team-job-row{padding:5px 7px}
20323
+ .t-team-job-row[data-kind="subagent"] .t-team-job-label{font-weight:500}
20324
+ .t-team-job-desc{min-width:0;width:100%;display:block;font-size:11.5px;line-height:1.35;color:var(--dsw-alias-label-caption);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
20325
+ .t-team-job-row[data-clickable="true"]{cursor:pointer}
20326
+ .t-team-job-row[data-clickable="true"]:hover{background:var(--dsw-alias-interactive-bg-hover)}
20327
+ /* shell.overlay \u91CC\u7684\u4EFB\u52A1\u6D6E\u5C42\uFF1A\u5FBD\u7AE0\u4E0E\u56E2\u961F\u5FBD\u7AE0\u540C\u4E00\u89C6\u89C9\u4F53\u7CFB\uFF08\u6BDB\u73BB\u7483+\u8109\u51B2\uFF09 */
20328
+ .t-team-job-badge{position:fixed;z-index:55;cursor:grab;touch-action:none;box-sizing:border-box;display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));backdrop-filter:blur(16px);box-shadow:0 8px 28px color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent);color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;font-weight:600;line-height:20px;cursor:pointer;transition:border-color .15s,transform .12s}
20329
+ .t-team-job-badge:hover{border-color:var(--dsw-alias-border-l3);transform:translateY(-1px)}
20330
+ .t-team-job-badge:active{cursor:grabbing}
20331
+ .t-team-job-badge:active{transform:translateY(0) scale(.98)}
20332
+ .t-team-job-badge-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-label-tertiary)}
20333
+ .t-team-job-badge-dot[data-live="true"]{background:var(--dsw-alias-state-success-primary);animation:t-team-job-pulse 1.25s ease-in-out infinite}
20334
+ .t-team-job-badge-count{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
20335
+ @keyframes t-team-job-pulse{0%,to{opacity:.42}50%{opacity:1}}
20336
+ .t-team-job-panel{position:fixed;z-index:60;display:flex;flex-direction:column;width:380px;max-height:min(460px,72vh);box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-3));backdrop-filter:blur(20px) saturate(1.08);box-shadow:0 12px 32px color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent), 0 32px 72px color-mix(in srgb, var(--dsw-alias-label-primary) 16%, transparent);color:var(--dsw-alias-label-primary);animation:t-team-job-panel-in .16s ease-out}
20337
+ @keyframes t-team-job-panel-in{0%{opacity:0}to{opacity:1}}
20338
+ .t-team-job-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:44px;padding:0 10px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2);cursor:grab;touch-action:none;user-select:none}
20339
+ .t-team-job-panel-head:active{cursor:grabbing}
20340
+ .t-team-job-panel-title{font-size:14px;font-weight:600}
20341
+ .t-team-job-panel-close{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;cursor:pointer}
20342
+ .t-team-job-panel-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
20343
+ .t-team-job-panel-tabs{display:flex;gap:4px;padding:6px 6px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
20344
+ .t-team-job-panel-tabs .t-team-tab{font-size:12px;padding:3px 9px}
20345
+ .t-team-job-tab-count{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;margin-left:2px}
20346
+ .t-team-job-panel-body{display:flex;flex-direction:column;gap:6px;padding:6px;overflow:auto;min-height:0;scrollbar-width:thin;scrollbar-color:color-mix(in srgb, var(--dsw-alias-label-tertiary) 28%, transparent) transparent}
20347
+ .t-team-job-panel-body::-webkit-scrollbar{width:6px}
20348
+ .t-team-job-panel-body::-webkit-scrollbar-track{background:transparent}
20349
+ .t-team-job-panel-body::-webkit-scrollbar-thumb{background:color-mix(in srgb, var(--dsw-alias-label-tertiary) 28%, transparent);background-clip:padding-box;border:2px solid transparent;border-radius:999px}
20350
+ .t-team-job-panel-body:hover::-webkit-scrollbar-thumb{background:color-mix(in srgb, var(--dsw-alias-label-tertiary) 44%, transparent);background-clip:padding-box}
20351
+ .t-team-job-panel-body .t-team-jobs{gap:2px}
20352
+ .t-team-job-panel-body .t-team-job-row{padding:3px 6px;gap:6px;border-radius:6px;background:color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent)}
20353
+ .t-team-job-panel-body .t-team-job-meta{gap:2px}
20354
+ .t-team-job-panel-body .t-team-section-head{margin-top:2px}
20355
+ .t-team-pop[data-tab="tasks"]{width:420px;max-height:520px}
20261
20356
  .t-team-pop .t-team-picker-list{max-height:150px}
20262
20357
  .t-team-pop-list{display:flex;flex-direction:column;gap:6px;overflow:auto;min-height:0}
20263
20358
  .t-team-pop-item{display:flex;gap:8px;align-items:flex-start;text-align:left;padding:6px 8px;border-radius:8px;border:1px solid transparent;background:transparent;color:inherit;font:inherit;cursor:pointer}
@@ -20410,7 +20505,7 @@ var SectionBoundary = class extends import_react.default.Component {
20410
20505
  return this.props.children;
20411
20506
  }
20412
20507
  };
20413
- function SettingsPanel({ t, remote, getActive, refreshCatalog, initialTab, squadsPreview, teamsPreview, draftPreview }) {
20508
+ function SettingsPanel({ t, ctx, remote, getActive, refreshCatalog, initialTab, squadsPreview, teamsPreview, draftPreview }) {
20414
20509
  const [snapshot, setSnapshot] = import_react.default.useState(() => snapshotOf(remote) ?? null);
20415
20510
  const [query, setQuery] = import_react.default.useState("");
20416
20511
  const [division, setDivision] = import_react.default.useState("");
@@ -20824,6 +20919,472 @@ function SettingsPanel({ t, remote, getActive, refreshCatalog, initialTab, squad
20824
20919
  )
20825
20920
  ));
20826
20921
  }
20922
+ function isJobRowLive(job) {
20923
+ return job.status === "running" || job.status === "stopping";
20924
+ }
20925
+ function inJobSessionTree(byId, sessionId, rootId) {
20926
+ const seen = /* @__PURE__ */ new Set();
20927
+ let cursor = sessionId;
20928
+ while (typeof cursor === "string" && cursor !== "" && !seen.has(cursor)) {
20929
+ if (cursor === rootId) return true;
20930
+ seen.add(cursor);
20931
+ const parent = byId?.[cursor]?.parentId;
20932
+ cursor = typeof parent === "string" ? parent : void 0;
20933
+ }
20934
+ return false;
20935
+ }
20936
+ function collectJobRows(byId, jobsBySession, rootId) {
20937
+ const rows = [];
20938
+ if (jobsBySession === void 0 || rootId === void 0) return rows;
20939
+ for (const [sessionId, jobs] of Object.entries(jobsBySession)) {
20940
+ if (!Array.isArray(jobs) || jobs.length === 0) continue;
20941
+ if (!inJobSessionTree(byId, sessionId, rootId)) continue;
20942
+ const ownerTitle = byId?.[sessionId]?.displayTitle ?? sessionId;
20943
+ for (const job of jobs) rows.push({ ownerSessionId: sessionId, ownerTitle, job });
20944
+ }
20945
+ return rows;
20946
+ }
20947
+ function orderJobRows(rows) {
20948
+ return [...rows].sort((left, right) => {
20949
+ const liveLeft = isJobRowLive(left.job);
20950
+ if (liveLeft !== isJobRowLive(right.job)) return liveLeft ? -1 : 1;
20951
+ if (liveLeft) return left.job.startedAt - right.job.startedAt;
20952
+ const settled = (right.job.finishedAt ?? right.job.startedAt) - (left.job.finishedAt ?? left.job.startedAt);
20953
+ return settled !== 0 ? settled : left.job.startedAt - right.job.startedAt;
20954
+ });
20955
+ }
20956
+ function jobRowDotState(status) {
20957
+ switch (status) {
20958
+ case "running":
20959
+ return "running";
20960
+ case "stopping":
20961
+ return "stopping";
20962
+ case "completed":
20963
+ return "completed";
20964
+ case "killed":
20965
+ return "killed";
20966
+ case "failed":
20967
+ return "failed";
20968
+ default:
20969
+ return "unknown";
20970
+ }
20971
+ }
20972
+ function jobRowStatusKey(status) {
20973
+ switch (status) {
20974
+ case "running":
20975
+ return "tasks.status.running";
20976
+ case "stopping":
20977
+ return "tasks.status.stopping";
20978
+ case "completed":
20979
+ return "tasks.status.completed";
20980
+ case "killed":
20981
+ return "tasks.status.killed";
20982
+ case "failed":
20983
+ return "tasks.status.failed";
20984
+ default:
20985
+ return void 0;
20986
+ }
20987
+ }
20988
+ function jobRowDetail(job) {
20989
+ const detail = typeof job.detail === "string" ? job.detail.trim() : "";
20990
+ if (detail === "") return void 0;
20991
+ if (job.status === "completed" && /^exit code:\s*0$/iu.test(detail)) return void 0;
20992
+ return detail;
20993
+ }
20994
+ function formatJobRowDuration(ms, t) {
20995
+ if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) return "";
20996
+ const total = Math.floor(ms / 1e3);
20997
+ const minutes = Math.floor(total / 60);
20998
+ const seconds = total % 60;
20999
+ return minutes > 0 ? t("tasks.duration.minutes", { minutes, seconds }) : t("tasks.duration.seconds", { seconds });
21000
+ }
21001
+ function collectTreeSubagents(byId, subagentsByParent, rootId) {
21002
+ const out = [];
21003
+ if (rootId === void 0) return out;
21004
+ const now = Date.now();
21005
+ for (const summary of Object.values(byId ?? {})) {
21006
+ if (summary?.id === void 0 || summary.id === rootId) continue;
21007
+ if (!inJobSessionTree(byId, summary.id, rootId)) continue;
21008
+ const catalog = subagentsByParent?.[summary.parentId ?? ""];
21009
+ const entry = (catalog?.entries ?? []).find((row) => row?.kind === "child" && row.id === summary.id);
21010
+ const label = typeof entry?.label === "string" && entry.label !== "" ? entry.label : void 0;
21011
+ out.push({
21012
+ id: summary.id,
21013
+ parentId: summary.parentId,
21014
+ title: label ?? summary.displayTitle ?? summary.id,
21015
+ named: label !== void 0,
21016
+ // 请求文本前一段:会话标题就是从它生成的,官方也用同一份来当副标题
21017
+ request: summary.displayTitle ?? "",
21018
+ running: entry === void 0 ? summary.running === true : entry.activity === "running",
21019
+ mode: entry?.mode,
21020
+ direct: summary.parentId === rootId,
21021
+ // 目录数组里靠后的视为更新(排序时按倒序用),未收录的以 -1 沉底
21022
+ order: entry === void 0 ? -1 : (catalog?.entries ?? []).indexOf(entry),
21023
+ now
21024
+ });
21025
+ }
21026
+ return out.sort((left, right) => {
21027
+ if (left.order !== right.order) return right.order - left.order;
21028
+ return String(left.title).localeCompare(String(right.title));
21029
+ });
21030
+ }
21031
+ function renderSubagentRows({ subagents, t, onOpen }) {
21032
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-jobs" }, subagents.map((item) => /* @__PURE__ */ import_react.default.createElement(
21033
+ "div",
21034
+ {
21035
+ className: "t-team-job-row",
21036
+ "data-kind": "subagent",
21037
+ "data-clickable": onOpen === void 0 ? void 0 : "true",
21038
+ key: `subagent:${item.id}`,
21039
+ role: onOpen === void 0 ? void 0 : "button",
21040
+ tabIndex: onOpen === void 0 ? void 0 : 0,
21041
+ title: onOpen === void 0 ? item.id : `${item.id} \xB7 ${t("tasks.subagentOpen")}`,
21042
+ onClick: onOpen === void 0 ? void 0 : () => onOpen(item),
21043
+ onKeyDown: onOpen === void 0 ? void 0 : (event) => {
21044
+ if (event.key === "Enter" || event.key === " ") {
21045
+ event.preventDefault();
21046
+ onOpen(item);
21047
+ }
21048
+ }
21049
+ },
21050
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-dot", "data-state": item.running ? "running" : "completed", "aria-hidden": "true" }),
21051
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-main" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-label", title: `${item.id} \xB7 ${item.running ? t("tasks.stateRunning") : t("tasks.stateInactive")}` }, item.title), /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-desc" }, [
21052
+ item.request !== "" ? item.request : void 0,
21053
+ // 目录没给 mode 就整段跳过,不要冒出一个孤零零的"可继续"
21054
+ item.mode === "one-shot" ? t("tasks.modeOneShot") : item.mode === void 0 ? void 0 : t("tasks.modeContinuable"),
21055
+ item.running ? t("tasks.stateRunning") : t("tasks.stateInactive")
21056
+ ].filter(Boolean).join(" \xB7 ")))
21057
+ )));
21058
+ }
21059
+ function useJobRows(ctx) {
21060
+ const list = ctx?.sessions?.list;
21061
+ const snapshot = import_react.default.useSyncExternalStore(
21062
+ import_react.default.useCallback((onChange) => typeof list?.subscribe === "function" ? list.subscribe(onChange) : () => {
21063
+ }, [list]),
21064
+ import_react.default.useCallback(() => typeof list?.getSnapshot === "function" ? list.getSnapshot() : void 0, [list]),
21065
+ // 首次渲染(含 SSR)也要立刻拿到数据,否则要先空一帧、等订阅推一次才出现。
21066
+ import_react.default.useCallback(() => typeof list?.getSnapshot === "function" ? list.getSnapshot() : void 0, [list])
21067
+ );
21068
+ const rootId = snapshot?.current;
21069
+ const byId = snapshot?.byId ?? {};
21070
+ const rows = import_react.default.useMemo(
21071
+ () => orderJobRows(collectJobRows(byId, snapshot?.jobsBySession, rootId)),
21072
+ [byId, snapshot?.jobsBySession, rootId]
21073
+ );
21074
+ const liveCount = rows.reduce((count, row) => count + (isJobRowLive(row.job) ? 1 : 0), 0);
21075
+ const [now, setNow] = import_react.default.useState(() => Date.now());
21076
+ import_react.default.useEffect(() => {
21077
+ if (liveCount === 0) return void 0;
21078
+ const timer = window.setInterval(() => setNow(Date.now()), 1e3);
21079
+ return () => window.clearInterval(timer);
21080
+ }, [liveCount]);
21081
+ const subagents = import_react.default.useMemo(
21082
+ () => collectTreeSubagents(byId, snapshot?.subagentsByParent, rootId),
21083
+ [byId, snapshot?.subagentsByParent, rootId]
21084
+ );
21085
+ const subagentsRunning = subagents.reduce((count, item) => count + (item.running ? 1 : 0), 0);
21086
+ return { rows, liveCount, subagents, subagentsRunning, now };
21087
+ }
21088
+ function renderJobRows({ rows, now, multiOwner, t }) {
21089
+ if (rows.length === 0) return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-meta" }, t("tasks.empty"));
21090
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-jobs" }, rows.map((row) => {
21091
+ const job = row.job ?? {};
21092
+ const live = isJobRowLive(job);
21093
+ const elapsed = live ? now - job.startedAt : (job.finishedAt ?? job.startedAt) - job.startedAt;
21094
+ const statusKey = jobRowStatusKey(job.status);
21095
+ const statusText = statusKey === void 0 ? String(job.status ?? "") : t(statusKey);
21096
+ const meta3 = [
21097
+ live ? void 0 : statusText,
21098
+ jobRowDetail(job),
21099
+ formatJobRowDuration(elapsed, t),
21100
+ multiOwner ? t("tasks.owner", { name: row.ownerTitle }) : void 0
21101
+ ].filter(Boolean);
21102
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-job-row", key: `${row.ownerSessionId}:${job.id}` }, /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-dot", "data-state": jobRowDotState(job.status), "aria-hidden": "true" }), /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-main" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-label", title: `${job.label ?? ""} \xB7 ${job.id ?? ""}` }, live ? `${statusText} \xB7 ${job.label ?? ""}` : job.label ?? ""), /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-meta" }, meta3.map((part, index) => /* @__PURE__ */ import_react.default.createElement("span", { key: index }, part)))));
21103
+ }));
21104
+ }
21105
+ function JobsTab({ t, ctx, compact }) {
21106
+ const { rows, liveCount, subagents, now } = useJobRows(ctx);
21107
+ const multiOwner = new Set(rows.map((row) => row.ownerSessionId)).size > 1;
21108
+ return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody" }, compact !== true && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("tasks.hint")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team-meta" }, liveCount > 0 ? t("tasks.countRunning", { running: liveCount, total: rows.length }) : t("tasks.count", { total: rows.length })), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-section-head" }, t("tasks.subagents")), subagents.length === 0 ? /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-meta" }, t("tasks.noSubagents")) : renderSubagentRows({ subagents, t, onOpen: (item) => openSubagentSession(ctx, item) }), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-section-head" }, t("tab.tasks")), renderJobRows({ rows, now, multiOwner, t }));
21109
+ }
21110
+ function openSubagentSession(ctx, item) {
21111
+ const sessions = ctx?.sessions;
21112
+ if (sessions === void 0 || item?.id === void 0) return;
21113
+ const resolved = sessions.subagentAddress?.(item.id);
21114
+ const address = resolved ?? (item.parentId === void 0 ? void 0 : { parentSessionId: item.parentId, childSessionId: item.id, mode: item.mode ?? "continuable" });
21115
+ if (address === void 0) return;
21116
+ try {
21117
+ sessions.openSubagent?.(address);
21118
+ } catch (error62) {
21119
+ console.error("[t-team] \u6253\u5F00\u5B50\u4EE3\u7406\u4F1A\u8BDD\u5931\u8D25\uFF1A", error62);
21120
+ }
21121
+ }
21122
+ var JOB_DOCK_POS_KEY = "t-team:jobs-dock:v1";
21123
+ var JOB_BADGE_POS_KEY = "t-team:jobs-badge:v1";
21124
+ var JOB_BADGE_DEFAULT = { left: 0, top: 108 };
21125
+ var JOB_BADGE_ESTIMATE_WIDTH = 180;
21126
+ var JOB_DOCK_DEFAULT = { left: 18, top: 112 };
21127
+ var JOB_DOCK_MARGIN = 8;
21128
+ var JOB_DOCK_PANEL_WIDTH = 380;
21129
+ var JOB_DOCK_FALLBACK_WIDTH = 320;
21130
+ var JOB_DOCK_FALLBACK_HEIGHT = 420;
21131
+ var JOB_DOCK_DRAG_THRESHOLD = 4;
21132
+ function nonNegative(value, fallback) {
21133
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback();
21134
+ }
21135
+ function readBadgePos() {
21136
+ const fallback = { left: JOB_DOCK_MARGIN, top: JOB_BADGE_DEFAULT.top };
21137
+ if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
21138
+ try {
21139
+ const raw = window.localStorage.getItem(JOB_BADGE_POS_KEY);
21140
+ if (raw !== null) {
21141
+ const parsed = JSON.parse(raw);
21142
+ if (typeof parsed?.left === "number" && typeof parsed?.top === "number") {
21143
+ return clampJobDockPos(parsed, { offsetWidth: JOB_BADGE_ESTIMATE_WIDTH });
21144
+ }
21145
+ }
21146
+ } catch {
21147
+ }
21148
+ }
21149
+ const computed = clampJobDockPos({
21150
+ left: (typeof window === "undefined" ? JOB_DOCK_MARGIN : window.innerWidth) - JOB_BADGE_ESTIMATE_WIDTH,
21151
+ top: JOB_BADGE_DEFAULT.top
21152
+ }, { offsetWidth: JOB_BADGE_ESTIMATE_WIDTH });
21153
+ return Number.isFinite(computed.left) && Number.isFinite(computed.top) ? computed : fallback;
21154
+ }
21155
+ function clampJobDockPos(pos, panel) {
21156
+ const viewport = typeof document === "undefined" ? void 0 : document.documentElement;
21157
+ const width = nonNegative(viewport?.clientWidth, () => nonNegative(
21158
+ typeof window === "undefined" ? void 0 : window.innerWidth,
21159
+ () => JOB_DOCK_PANEL_WIDTH
21160
+ ));
21161
+ const height = nonNegative(viewport?.clientHeight, () => nonNegative(
21162
+ typeof window === "undefined" ? void 0 : window.innerHeight,
21163
+ () => JOB_DOCK_PANEL_WIDTH
21164
+ ));
21165
+ const measured = panel?.offsetWidth ?? 0;
21166
+ const panelWidth = measured > 0 ? measured : JOB_DOCK_FALLBACK_WIDTH;
21167
+ const measuredHeight = panel?.offsetHeight ?? 0;
21168
+ const panelHeight = measuredHeight > 0 ? measuredHeight : JOB_DOCK_FALLBACK_HEIGHT;
21169
+ const maxLeft = Math.max(JOB_DOCK_MARGIN, width - panelWidth - JOB_DOCK_MARGIN);
21170
+ const maxTop = Math.max(JOB_DOCK_MARGIN, height - panelHeight - JOB_DOCK_MARGIN);
21171
+ return {
21172
+ left: Math.max(JOB_DOCK_MARGIN, Math.min(pos.left, maxLeft)),
21173
+ top: Math.max(JOB_DOCK_MARGIN, Math.min(pos.top, maxTop))
21174
+ };
21175
+ }
21176
+ function readJobDockPos() {
21177
+ if (typeof window === "undefined") return JOB_DOCK_DEFAULT;
21178
+ try {
21179
+ const raw = window.localStorage.getItem(JOB_DOCK_POS_KEY);
21180
+ if (raw === null) return JOB_DOCK_DEFAULT;
21181
+ const parsed = JSON.parse(raw);
21182
+ if (typeof parsed?.left !== "number" || typeof parsed?.top !== "number") return JOB_DOCK_DEFAULT;
21183
+ return { left: parsed.left, top: parsed.top };
21184
+ } catch {
21185
+ return JOB_DOCK_DEFAULT;
21186
+ }
21187
+ }
21188
+ function JobsDock({ t, ctx, preview }) {
21189
+ const { rows, liveCount, subagents, subagentsRunning, now } = useJobRows(ctx);
21190
+ const [open2, setOpen] = import_react.default.useState(preview?.open === true);
21191
+ const [panelTab, setPanelTab] = import_react.default.useState(preview?.tab ?? "subagents");
21192
+ const [pos, setPos] = import_react.default.useState(() => clampJobDockPos(readJobDockPos(), { offsetWidth: JOB_DOCK_PANEL_WIDTH }));
21193
+ const dragRef = import_react.default.useRef(void 0);
21194
+ const panelRef = import_react.default.useRef(null);
21195
+ const badgeRef = import_react.default.useRef(null);
21196
+ const badgeDragRef = import_react.default.useRef(void 0);
21197
+ const badgeMovedRef = import_react.default.useRef(false);
21198
+ const [badgePos, setBadgePos] = import_react.default.useState(readBadgePos);
21199
+ const closedByUserRef = import_react.default.useRef(false);
21200
+ const wasEmptyRef = import_react.default.useRef(true);
21201
+ import_react.default.useEffect(() => {
21202
+ const empty = rows.length === 0;
21203
+ const wasEmpty = wasEmptyRef.current;
21204
+ wasEmptyRef.current = empty;
21205
+ if (empty) {
21206
+ closedByUserRef.current = false;
21207
+ return;
21208
+ }
21209
+ if (wasEmpty && !closedByUserRef.current) setOpen(true);
21210
+ }, [rows.length]);
21211
+ const badgePlacedRef = import_react.default.useRef(false);
21212
+ import_react.default.useEffect(() => {
21213
+ if (badgePlacedRef.current) return;
21214
+ badgePlacedRef.current = true;
21215
+ const el = badgeRef.current;
21216
+ let saved;
21217
+ try {
21218
+ saved = window.localStorage.getItem(JOB_BADGE_POS_KEY);
21219
+ } catch {
21220
+ saved = null;
21221
+ }
21222
+ if (saved !== null) {
21223
+ setBadgePos((current) => clampJobDockPos(current, el));
21224
+ return;
21225
+ }
21226
+ const width = el?.offsetWidth ?? JOB_BADGE_ESTIMATE_WIDTH;
21227
+ setBadgePos(clampJobDockPos({
21228
+ left: window.innerWidth - width - JOB_DOCK_MARGIN,
21229
+ top: JOB_BADGE_DEFAULT.top
21230
+ }, el));
21231
+ }, []);
21232
+ import_react.default.useEffect(() => {
21233
+ if (!open2) return void 0;
21234
+ const sessions = ctx?.sessions;
21235
+ const list = sessions?.list?.getSnapshot?.();
21236
+ const rootId = list?.current;
21237
+ if (rootId === void 0) return void 0;
21238
+ try {
21239
+ sessions.setSubagentCatalogOpen?.(rootId, true);
21240
+ void sessions.refreshSubagents?.(rootId);
21241
+ } catch {
21242
+ }
21243
+ return () => {
21244
+ try {
21245
+ sessions.setSubagentCatalogOpen?.(rootId, false);
21246
+ } catch {
21247
+ }
21248
+ };
21249
+ }, [open2, ctx]);
21250
+ import_react.default.useEffect(() => {
21251
+ if (!open2) return void 0;
21252
+ const onResize = () => setPos((current) => clampJobDockPos(current, panelRef.current));
21253
+ window.addEventListener("resize", onResize);
21254
+ return () => window.removeEventListener("resize", onResize);
21255
+ }, [open2]);
21256
+ const multiOwner = new Set(rows.map((row) => row.ownerSessionId)).size > 1;
21257
+ if (rows.length === 0 && subagents.length === 0) return null;
21258
+ const anyLive = liveCount > 0 || subagentsRunning > 0;
21259
+ const badgeText = [
21260
+ subagents.length > 0 ? subagentsRunning > 0 ? t("tasks.subagentRunningCount", { running: subagentsRunning }) : t("tasks.subagentCount", { total: subagents.length }) : void 0,
21261
+ rows.length > 0 ? liveCount > 0 ? t("tasks.jobRunningCount", { running: liveCount }) : t("tasks.jobCount", { total: rows.length }) : void 0
21262
+ ].filter(Boolean).join(" \xB7 ");
21263
+ function startBadgeDrag(event) {
21264
+ if (event.button !== void 0 && event.button !== 0) return;
21265
+ event.currentTarget.setPointerCapture?.(event.pointerId);
21266
+ badgeMovedRef.current = false;
21267
+ badgeDragRef.current = {
21268
+ pointerId: event.pointerId,
21269
+ startX: event.clientX,
21270
+ startY: event.clientY,
21271
+ originLeft: badgePos.left,
21272
+ originTop: badgePos.top
21273
+ };
21274
+ }
21275
+ function moveBadgeDrag(event) {
21276
+ const drag = badgeDragRef.current;
21277
+ if (drag === void 0 || drag.pointerId !== event.pointerId) return;
21278
+ const dx = event.clientX - drag.startX;
21279
+ const dy = event.clientY - drag.startY;
21280
+ if (Math.abs(dx) + Math.abs(dy) >= JOB_DOCK_DRAG_THRESHOLD) badgeMovedRef.current = true;
21281
+ if (!badgeMovedRef.current) return;
21282
+ setBadgePos(clampJobDockPos({ left: drag.originLeft + dx, top: drag.originTop + dy }, badgeRef.current));
21283
+ }
21284
+ function endBadgeDrag(event) {
21285
+ const drag = badgeDragRef.current;
21286
+ badgeDragRef.current = void 0;
21287
+ if (drag === void 0 || drag.pointerId !== event.pointerId) return;
21288
+ if (!badgeMovedRef.current) return;
21289
+ try {
21290
+ window.localStorage.setItem(JOB_BADGE_POS_KEY, JSON.stringify(badgePos));
21291
+ } catch {
21292
+ }
21293
+ }
21294
+ function startDrag(event) {
21295
+ if (event.button !== void 0 && event.button !== 0) return;
21296
+ event.preventDefault();
21297
+ event.currentTarget.setPointerCapture?.(event.pointerId);
21298
+ dragRef.current = {
21299
+ pointerId: event.pointerId,
21300
+ startX: event.clientX,
21301
+ startY: event.clientY,
21302
+ originLeft: pos.left,
21303
+ originTop: pos.top,
21304
+ moved: false
21305
+ };
21306
+ }
21307
+ function moveDrag(event) {
21308
+ const drag = dragRef.current;
21309
+ if (drag === void 0 || drag.pointerId !== event.pointerId) return;
21310
+ const dx = event.clientX - drag.startX;
21311
+ const dy = event.clientY - drag.startY;
21312
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) < JOB_DOCK_DRAG_THRESHOLD) return;
21313
+ drag.moved = true;
21314
+ setPos(clampJobDockPos(
21315
+ { left: drag.originLeft + dx, top: drag.originTop + dy },
21316
+ panelRef.current
21317
+ ));
21318
+ }
21319
+ function endDrag(event) {
21320
+ const drag = dragRef.current;
21321
+ dragRef.current = void 0;
21322
+ if (drag === void 0 || drag.pointerId !== event.pointerId) return;
21323
+ if (!drag.moved) return;
21324
+ try {
21325
+ window.localStorage.setItem(JOB_DOCK_POS_KEY, JSON.stringify(pos));
21326
+ } catch {
21327
+ }
21328
+ }
21329
+ return /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(
21330
+ "button",
21331
+ {
21332
+ className: "t-team-job-badge",
21333
+ type: "button",
21334
+ ref: badgeRef,
21335
+ "data-testid": "t-team-job-badge",
21336
+ "data-live": anyLive,
21337
+ "data-dragging": badgeDragRef.current === void 0 ? void 0 : "true",
21338
+ style: { left: badgePos.left, top: badgePos.top },
21339
+ "aria-expanded": open2,
21340
+ title: t("tasks.badgeTitle"),
21341
+ onPointerDown: startBadgeDrag,
21342
+ onPointerMove: moveBadgeDrag,
21343
+ onPointerUp: endBadgeDrag,
21344
+ onPointerCancel: endBadgeDrag,
21345
+ onClick: () => {
21346
+ if (badgeMovedRef.current) {
21347
+ badgeMovedRef.current = false;
21348
+ return;
21349
+ }
21350
+ setOpen((value) => !value);
21351
+ }
21352
+ },
21353
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-badge-dot", "data-live": anyLive, "aria-hidden": "true" }),
21354
+ /* @__PURE__ */ import_react.default.createElement("span", null, t("tab.tasks")),
21355
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-badge-count" }, badgeText)
21356
+ ), open2 && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-job-panel", ref: panelRef, style: { left: pos.left, top: pos.top } }, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-job-panel-head", onPointerDown: startDrag, onPointerMove: moveDrag, onPointerUp: endDrag, onPointerCancel: endDrag }, /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-panel-title" }, t("tab.tasks")), /* @__PURE__ */ import_react.default.createElement(
21357
+ "button",
21358
+ {
21359
+ className: "t-team-job-panel-close",
21360
+ type: "button",
21361
+ title: t("button.close"),
21362
+ onPointerDown: (event) => event.stopPropagation(),
21363
+ onClick: () => {
21364
+ closedByUserRef.current = true;
21365
+ setOpen(false);
21366
+ }
21367
+ },
21368
+ "\u2715"
21369
+ )), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-job-panel-tabs", role: "tablist" }, [
21370
+ ["subagents", "tasks.subagents", subagentsRunning > 0 ? `${subagents.length} \xB7 ${subagentsRunning}` : String(subagents.length)],
21371
+ ["jobs", "tab.tasks", liveCount > 0 ? `${rows.length} \xB7 ${liveCount}` : String(rows.length)]
21372
+ ].map(([key, labelKey, count]) => /* @__PURE__ */ import_react.default.createElement(
21373
+ "button",
21374
+ {
21375
+ className: "t-team-tab",
21376
+ type: "button",
21377
+ role: "tab",
21378
+ key,
21379
+ "data-active": panelTab === key,
21380
+ "aria-selected": panelTab === key,
21381
+ onClick: () => setPanelTab(key)
21382
+ },
21383
+ t(labelKey),
21384
+ " ",
21385
+ /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-job-tab-count" }, count)
21386
+ ))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-job-panel-body" }, panelTab === "subagents" ? subagents.length === 0 ? /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-meta" }, t("tasks.noSubagents")) : renderSubagentRows({ subagents, t, onOpen: (item) => openSubagentSession(ctx, item) }) : renderJobRows({ rows, now, multiOwner, t }))));
21387
+ }
20827
21388
  function squadToInput(squad) {
20828
21389
  return {
20829
21390
  key: String(squad.key ?? "").trim(),
@@ -21209,9 +21770,9 @@ function insertAtTarget(target, reference) {
21209
21770
  const offset = referencePrefixEnd(snapshot);
21210
21771
  return target.insertReference(reference, { start: offset, end: offset, draftRev: snapshot.draftRev }) === true;
21211
21772
  }
21212
- function SummonButton({ t, remote, getActive, insertReference, fillSquadCommand, sessionId, preview }) {
21773
+ function SummonButton({ t, ctx, remote, getActive, insertReference, fillSquadCommand, sessionId, preview }) {
21213
21774
  const [open2, setOpen] = import_react.default.useState(preview?.open === true);
21214
- const [popTab, setPopTab] = import_react.default.useState(preview?.tab ?? "experts");
21775
+ const [popTab, setPopTab] = import_react.default.useState(preview?.tab ?? "tasks");
21215
21776
  const [squads, setSquads] = import_react.default.useState(preview?.squads ?? null);
21216
21777
  const [teams, setTeams] = import_react.default.useState(preview?.teams ?? null);
21217
21778
  const [squadQuery, setSquadQuery] = import_react.default.useState("");
@@ -21267,9 +21828,9 @@ function SummonButton({ t, remote, getActive, insertReference, fillSquadCommand,
21267
21828
  }
21268
21829
  const active = getActive();
21269
21830
  const teamCount = teams?.length ?? 0;
21270
- const popTabs = [["experts", "tab.experts"], ["squads", "tab.squads"], ...teamCount > 0 ? [["teams", "tab.teams"]] : []];
21831
+ const popTabs = [["tasks", "tab.tasks"], ["experts", "tab.experts"], ["squads", "tab.squads"], ...teamCount > 0 ? [["teams", "tab.teams"]] : []];
21271
21832
  import_react.default.useEffect(() => {
21272
- if (popTab === "teams" && teamCount === 0) setPopTab("experts");
21833
+ if (popTab === "teams" && teamCount === 0) setPopTab("tasks");
21273
21834
  }, [popTab, teamCount]);
21274
21835
  const squadNeedle = squadQuery.trim().toLowerCase();
21275
21836
  const visibleSquads = (squads?.squads ?? []).filter((squad) => {
@@ -21349,7 +21910,7 @@ function SummonButton({ t, remote, getActive, insertReference, fillSquadCommand,
21349
21910
  onClick: () => void fillSquad(squad)
21350
21911
  },
21351
21912
  starting === squad.key ? t("pop.picking") : t("pop.fill")
21352
- )), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, squad.aliases.slice(0, 6).map((alias) => /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-member-chip", key: alias }, alias)), squad.aliases.length > 6 && /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-note" }, "+", squad.aliases.length - 6)), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, squad.members.map((member) => /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-member-chip", "data-unknown": member.known === false, key: member.slug }, member.nameZh))))), squads !== null && visibleSquads.length === 0 && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-pop-empty" }, squads.squads.length === 0 ? t("pop.emptySquads") : t("pop.squadNoMatch")))), popTab === "teams" && /* @__PURE__ */ import_react.default.createElement(TeamsTab, { t, remote, compact: true, initialTeams: preview?.teams }), popTab === "experts" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(
21913
+ )), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, squad.aliases.slice(0, 6).map((alias) => /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-member-chip", key: alias }, alias)), squad.aliases.length > 6 && /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-note" }, "+", squad.aliases.length - 6)), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, squad.members.map((member) => /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-member-chip", "data-unknown": member.known === false, key: member.slug }, member.nameZh))))), squads !== null && visibleSquads.length === 0 && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-pop-empty" }, squads.squads.length === 0 ? t("pop.emptySquads") : t("pop.squadNoMatch")))), popTab === "teams" && /* @__PURE__ */ import_react.default.createElement(TeamsTab, { t, remote, compact: true, initialTeams: preview?.teams }), popTab === "tasks" && /* @__PURE__ */ import_react.default.createElement(JobsTab, { t, ctx, compact: true }), popTab === "experts" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(
21353
21914
  "input",
21354
21915
  {
21355
21916
  className: "t-team-input",
@@ -21432,6 +21993,17 @@ async function apply(ctx) {
21432
21993
  locale: NS,
21433
21994
  icon: "expert"
21434
21995
  }, (props) => import_react.default.createElement(SettingsPanel, { ...props, t: ctx.locale.bind(NS), remote, getActive, refreshCatalog })));
21996
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
21997
+ name: "shell.overlay",
21998
+ id: `${NS}-jobs`,
21999
+ order: 81,
22000
+ label: () => ctx.locale.bind(NS)("tab.tasks"),
22001
+ locale: NS
22002
+ }, (props) => import_react.default.createElement(JobsDock, {
22003
+ ...props,
22004
+ t: ctx.locale.bind(NS),
22005
+ ctx: props?.ctx ?? ctx
22006
+ })));
21435
22007
  ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
21436
22008
  name: "conversation.input.left",
21437
22009
  id: NS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-t-expert",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "type": "module",
5
5
  "description": "T Expert — a 316-expert, 22-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin, with a built-in multi-agent team engine (T Team).",
6
6
  "license": "MIT",
@@ -71,7 +71,8 @@
71
71
  "build": "node tools/build-client.mjs",
72
72
  "verify": "node tools/verify.mjs",
73
73
  "sync-data": "node tools/sync-data.mjs",
74
- "prepublishOnly": "npm run typecheck && npm run build && npm run verify && node tools/sync-data.mjs --check"
74
+ "prepublishOnly": "npm run typecheck && npm run build && npm run verify && npm run invariants && node tools/sync-data.mjs --check",
75
+ "invariants": "node tools/invariants.mjs"
75
76
  },
76
77
  "peerDependencies": {
77
78
  "@deepseek-ai/cordis": "^4.0.1",