@nanmicoder/dsh-agent-teams 0.1.20 → 0.1.22-rc.1

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 (54) hide show
  1. package/README.md +36 -15
  2. package/README_ZH.md +36 -15
  3. package/assets/readme/workspace.png +0 -0
  4. package/compatibility.json +22 -2
  5. package/lib/capabilities.js +3 -1
  6. package/lib/client/ActivityPanel.js +80 -51
  7. package/lib/client/AgentTeamsCard.js +14 -5
  8. package/lib/client/TeamChatEntry.js +22 -0
  9. package/lib/client/WorkspaceActivity.js +100 -0
  10. package/lib/client/activity-model.js +73 -8
  11. package/lib/client/activity-monitor.js +11 -4
  12. package/lib/client/agent-teams-card-definition.js +11 -1
  13. package/lib/client/icons.js +18 -0
  14. package/lib/client/index.js +35 -4
  15. package/lib/client/locales.js +44 -0
  16. package/lib/client/session-navigation.js +13 -1
  17. package/lib/client/workspace-state.js +43 -0
  18. package/lib/client.js +832 -210
  19. package/lib/client.js.map +1 -1
  20. package/lib/git-artifact-stamp.json +5 -0
  21. package/lib/harness-compat.js +20 -3
  22. package/lib/index.js +1 -1
  23. package/lib/mailbox.js +23 -3
  24. package/lib/members.js +3 -3
  25. package/lib/quality-gates.js +54 -0
  26. package/lib/scheduler.js +8 -2
  27. package/lib/snapshot.js +1 -0
  28. package/lib/state.js +15 -3
  29. package/lib/tools.js +71 -22
  30. package/lib/types/capabilities.d.ts +0 -1
  31. package/lib/types/client/ActivityPanel.d.ts +18 -1
  32. package/lib/types/client/AgentTeamsCard.d.ts +14 -2
  33. package/lib/types/client/TeamChatEntry.d.ts +4 -0
  34. package/lib/types/client/WorkspaceActivity.d.ts +22 -0
  35. package/lib/types/client/activity-model.d.ts +37 -1
  36. package/lib/types/client/activity-monitor.d.ts +4 -0
  37. package/lib/types/client/agent-teams-card-definition.d.ts +8 -0
  38. package/lib/types/client/icons.d.ts +5 -0
  39. package/lib/types/client/locales.d.ts +44 -0
  40. package/lib/types/client/session-navigation.d.ts +13 -2
  41. package/lib/types/client/workspace-state.d.ts +19 -0
  42. package/lib/types/harness-compat.d.ts +14 -1
  43. package/lib/types/mailbox.d.ts +4 -0
  44. package/lib/types/quality-gates.d.ts +7 -1
  45. package/lib/types/scheduler.d.ts +1 -0
  46. package/lib/types/snapshot.d.ts +2 -0
  47. package/lib/types/types.d.ts +16 -0
  48. package/package.json +61 -54
  49. package/pnpm-workspace.yaml +288 -0
  50. package/release-notes/v0.1.21-rc.1.md +14 -0
  51. package/release-notes/v0.1.21.md +23 -0
  52. package/release-notes/v0.1.22-rc.1.md +24 -0
  53. package/scripts/compatibility.mjs +26 -4
  54. package/scripts/doctor.mjs +7 -2
package/README.md CHANGED
@@ -25,12 +25,12 @@
25
25
  Ask in natural language. The plugin provides the team protocol, 14 coordination tools, persistent state, an automatic shared-task scheduler, and a live Web UI—without requiring a separate workflow engine.
26
26
 
27
27
  <p align="center">
28
- <img src="./assets/ui.png" width="100%" alt="DeepSeek Harness conversation with the AgentTeams live activity panel, members, tasks, dependencies, and reports">
28
+ <img src="./assets/readme/workspace.png" width="100%" alt="AgentTeams native workspace with members, task dependencies, and progress">
29
29
  </p>
30
30
 
31
31
  ## Releases
32
32
 
33
- [v0.1.20](./release-notes/v0.1.20.md) syncs the version references in this document with the npm `latest` channel. It changes no code and no behavior; the packaged artifact is identical to `0.1.19`. The substantive changes since the last documented release are in [v0.1.19](./release-notes/v0.1.19.md): member startup survives a host that disables or renames its delegation tools, automatic repair scope is derived from `requiredFix`, and captains gain `agent_teams_amend_task`. Recommended host: DeepSeek Harness `0.1.5-rc.1`; the three older supported host targets are retained.
33
+ [v0.1.22-rc.1](./release-notes/v0.1.22-rc.1.md) prepares for Harness desktop 0.2.0 and fixes Git installation by shipping prebuilt entries. This preview uses npm `next`; the recommended host is Harness `0.1.7-rc.2` (its `next` channel). See the exact [support matrix](./compatibility.json).
34
34
 
35
35
  ## Why AgentTeams?
36
36
 
@@ -49,37 +49,52 @@ The conversation card and activity panel use Harness's official locale service.
49
49
 
50
50
  ## Install and choose versions
51
51
 
52
- **Recommended pair: DeepSeek Harness `0.1.5-rc.1` + AgentTeams `0.1.20`. Harness remains a prerelease.**
52
+ **Recommended pair: DeepSeek Harness `0.1.7-rc.2` + AgentTeams `0.1.22-rc.1`. Both packages are prereleases.**
53
53
 
54
54
  | Use case | DeepSeek Harness | AgentTeams plugin |
55
55
  | --- | --- | --- |
56
- | **Recommended installation** | **`0.1.5-rc.1`** | **`0.1.20`** |
57
- | Retaining an older RC | `0.1.2-rc.1` | `0.1.20` |
58
- | Developer Alpha testing | `0.1.2-alpha.5` | `0.1.20` |
59
- | Retaining an older Alpha | `0.1.2-alpha.2` | `0.1.20` |
56
+ | **Recommended** | **`0.1.7-rc.2`** | **`0.1.22-rc.1`** |
57
+ | Legacy RC | `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3` | `0.1.22-rc.1` |
58
+ | Retaining an older RC | `0.1.2-rc.1` | `0.1.22-rc.1` |
59
+ | Developer Alpha testing | `0.1.2-alpha.5` | `0.1.22-rc.1` |
60
+ | Retaining an older Alpha | `0.1.2-alpha.2` | `0.1.22-rc.1` |
60
61
 
61
- ### 1. Install DeepSeek Harness
62
+ ### Desktop app
63
+
64
+ In the app sidebar, open **Plugins → Add plugin** and enter the npm package spec:
65
+
66
+ ```text
67
+ @nanmicoder/dsh-agent-teams@0.1.22-rc.1
68
+ ```
69
+
70
+ Install it, then choose **Enable now**. If the host asks for a restart, restart the desktop app. Use the package spec above for the published build; an npm or GitHub webpage URL is not the same install source. For Git installation, use a commit containing the verified build outputs described below.
71
+
72
+ The desktop app supplies its own Harness core and package manager. Upgrading the global CLI does not upgrade that core, and replacing `--profile web` with `--profile desktop` in a CLI command does not install into the app. Check the embedded core against the compatibility list. The upcoming `0.2.0` desktop release is being pre-adapted against source commit `21638c56315ae6a2b552d6091945d3144c9af32e`; it is recorded separately as a source candidate, not in the published-host support matrix above. See the [0.2.0 pre-adaptation scope and evidence](./docs/harness-0.2.0-pre-adaptation/README.md).
73
+
74
+ ### CLI / Web: 1. Install DeepSeek Harness
62
75
 
63
76
  ```sh
64
- npm install --global @deepseek-ai/dsh@0.1.5-rc.1
77
+ npm install --global @deepseek-ai/dsh@0.1.7-rc.2
65
78
  dsh --version
66
79
  ```
67
80
 
68
81
  Skip this if you already run this version. Alpha is opt-in: select an exact Alpha version from the table and lock all host dependencies as described in the [maintenance guide](./docs/maintenance-workflow.md).
69
82
 
70
- ### 2. Install the AgentTeams plugin
83
+ ### CLI / Web: 2. Install the AgentTeams plugin
71
84
 
72
- Install into the `web` profile. Replace the profile name if needed:
85
+ Install or upgrade from npm (pinned to this `next` preview):
73
86
 
74
87
  ```sh
75
- dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.20
88
+ dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.22-rc.1
76
89
  ```
77
90
 
78
- **After installation, stop and restart Harness for that profile, then refresh the browser.**
91
+ For a CLI-managed installation, replace `web` with your active CLI profile. **Stop and restart that profile's Harness process, then refresh the browser.** Installing the plugin does not upgrade Harness; the host and plugin have independent `latest` channels.
92
+
93
+ For Git installation, use a Git package spec pinned to a commit containing the verified server, client, and type build outputs in `lib/`, rather than an npm webpage URL. The repository distributes these outputs so installation does not need a `prepare` hook or build-script approval. Source maintainers must run `pnpm build` and update the committed outputs whenever the source changes. Existing npm releases remain immutable.
79
94
 
80
- The default npm `latest` tag points to `0.1.20`, so `dsh plugin --profile web add @nanmicoder/dsh-agent-teams` installs this version on a fresh profile. Use the exact-version command above to pin it. The recommended Harness version is `0.1.5-rc.1`; installing the plugin does not upgrade the host. See the [source installation guide](./docs/maintenance-workflow.md) and [release verification](./docs/releases/v0.1.19/README.md).
95
+ For a manually cloned checkout, run `pnpm install --frozen-lockfile` and `pnpm build` after pulling, then restart the corresponding Harness process. Updating Git alone does not update local build output. See the [maintenance guide](./docs/maintenance-workflow.md).
81
96
 
82
- > Desktop users must check the app's embedded Harness core; upgrading the global CLI does not upgrade it. For older `0.1.0-*` / `0.1.1-*` or unlisted hosts, keep a working pair and follow the [older-version and diagnostic guide](./docs/maintenance-workflow.md).
97
+ > For older `0.1.0-*` / `0.1.1-*` or unlisted hosts, keep a working pair and follow the [older-version and diagnostic guide](./docs/maintenance-workflow.md).
83
98
 
84
99
  See the full [compatibility matrix](./compatibility.json), [source installation and Alpha testing guide](./docs/maintenance-workflow.md), and [verification coverage and platform limits](./docs/maintenance-2026-09-06/release/README.md).
85
100
 
@@ -87,6 +102,12 @@ Then ask for a team directly:
87
102
 
88
103
  > Use AgentTeams to review the commits after v0.5.3 from performance, security, and product perspectives. Return one consolidated report.
89
104
 
105
+ ## View teams in the workspace
106
+
107
+ Click **View team** in the current chat header or the team card below a reply to open the native **Team collaboration** tab. The entry belongs to its chat, rather than the general workspace start page.
108
+
109
+ Team and task content starts immediately: wide panes use columns and narrow panes stack them. Select a task to locate its owner; member icons open their conversations. Closing the tab does not stop the team, and its chat can reopen it. Completed teams retain their archives. Older hosts keep the activity panel.
110
+
90
111
  ## How it works
91
112
 
92
113
  1. For a request to use AgentTeams, the captain follows the core protocol already in its system instructions. It continues an existing team and uses `agent_teams_status` when current state needs checking. When no team exists, the goal becomes a staged plan for review.
package/README_ZH.md CHANGED
@@ -25,12 +25,12 @@
25
25
  你只需用自然语言提出目标。插件会提供精简的固定团队协议、14 个业务工具、持久化状态、自动共享任务调度和实时 Web UI,不需要额外的 Workflow 引擎。
26
26
 
27
27
  <p align="center">
28
- <img src="./assets/ui.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
28
+ <img src="./assets/readme/workspace.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
29
29
  </p>
30
30
 
31
31
  ## 版本更新
32
32
 
33
- [v0.1.20](./release-notes/v0.1.20.md) 仅把本文档中的版本引用同步到 npm `latest`,不含任何代码或行为变更,打包产物与 `0.1.19` 完全一致。上一份有实质变更的版本是 [v0.1.19](./release-notes/v0.1.19.md):宿主禁用或改名委派工具时成员仍能正常启动;自动修复范围改为从 `requiredFix` 推导;新增队长专用的 `agent_teams_amend_task`。推荐 DeepSeek Harness `0.1.5-rc.1`,保留三个旧宿主目标。
33
+ [v0.1.22-rc.1](./release-notes/v0.1.22-rc.1.md) 预适配 Harness 桌面端 0.2.0,并通过分发预编译入口修复 Git 安装。本预览版使用 npm `next` 渠道;推荐搭配 Harness `0.1.7-rc.2`(宿主的 `next` 渠道)。精确支持范围见 [compatibility.json](./compatibility.json)。
34
34
 
35
35
  ## 为什么需要 AgentTeams?
36
36
 
@@ -48,37 +48,52 @@
48
48
 
49
49
  ## 安装与版本选择
50
50
 
51
- **推荐组合:DeepSeek Harness `0.1.5-rc.1` + AgentTeams `0.1.20`。Harness 仍为预发布版本。**
51
+ **推荐组合:DeepSeek Harness `0.1.7-rc.2` + AgentTeams `0.1.22-rc.1`。两者均为预发布版本。**
52
52
 
53
53
  | 使用场景 | DeepSeek Harness | AgentTeams 插件 |
54
54
  | --- | --- | --- |
55
- | **推荐安装** | **`0.1.5-rc.1`** | **`0.1.20`** |
56
- | 保留旧 RC | `0.1.2-rc.1` | `0.1.20` |
57
- | 开发者测试 Alpha | `0.1.2-alpha.5` | `0.1.20` |
58
- | 保留旧 Alpha | `0.1.2-alpha.2` | `0.1.20` |
55
+ | **推荐版本** | **`0.1.7-rc.2`** | **`0.1.22-rc.1`** |
56
+ | 保留旧 RC | `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3` | `0.1.22-rc.1` |
57
+ | 保留旧 RC | `0.1.2-rc.1` | `0.1.22-rc.1` |
58
+ | 开发者测试 Alpha | `0.1.2-alpha.5` | `0.1.22-rc.1` |
59
+ | 保留旧 Alpha | `0.1.2-alpha.2` | `0.1.22-rc.1` |
59
60
 
60
- ### 1. 安装 DeepSeek Harness
61
+ ### 桌面端
62
+
63
+ 在应用侧栏打开「插件 → 添加插件」,输入 npm 包名和版本:
64
+
65
+ ```text
66
+ @nanmicoder/dsh-agent-teams@0.1.22-rc.1
67
+ ```
68
+
69
+ 安装完成后点击「立即启用」;宿主提示需要重启时,重启桌面应用。发布版请使用上面的包名,npm 或 GitHub 网页链接与 npm 包名不是同一种安装来源。从 Git 安装时,请选用包含下方所述已验证构建产物的提交。
70
+
71
+ 桌面应用自带 Harness 核心和包管理器。升级全局 CLI 不会升级桌面内核,也不能把命令中的 `--profile web` 换成 `--profile desktop` 来安装到桌面应用。请核对应用内置核心与兼容清单。即将发布的 `0.2.0` 桌面端正在基于源码提交 `21638c56315ae6a2b552d6091945d3144c9af32e` 做预适配,单独记录为源码候选,不进入上方已发布宿主支持矩阵。详见 [0.2.0 预适配范围与证据](./docs/harness-0.2.0-pre-adaptation/README.md)。
72
+
73
+ ### CLI / Web:1. 安装 DeepSeek Harness
61
74
 
62
75
  ```sh
63
- npm install --global @deepseek-ai/dsh@0.1.5-rc.1
76
+ npm install --global @deepseek-ai/dsh@0.1.7-rc.2
64
77
  dsh --version
65
78
  ```
66
79
 
67
80
  已有该版本可跳过。Alpha 仅供主动测试:手动指定表中的 Alpha 版本,并按[维护指南](./docs/maintenance-workflow.md)锁定整组宿主依赖。
68
81
 
69
- ### 2. 安装 AgentTeams 插件
82
+ ### CLI / Web:2. 安装 AgentTeams 插件
70
83
 
71
- 安装到 `web` profile;使用其他 profile 时替换名称:
84
+ 从 npm 安装或升级插件(命令锁定本次 `next` 预览版):
72
85
 
73
86
  ```sh
74
- dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.20
87
+ dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.22-rc.1
75
88
  ```
76
89
 
77
- **安装后,停止并重新启动该 profile 的 Harness 进程,再刷新浏览器。**
90
+ CLI 管理的安装可将 `web` 换成实际使用的 CLI profile。**安装后,停止并重新启动该 profile 的 Harness 进程,再刷新浏览器。** 安装插件不会自动升级宿主;Harness 的 `latest` 与插件的 `latest` 是两个独立渠道。
91
+
92
+ 从 Git 安装时,使用固定到包含已验证 `lib/` 构建产物的提交的 Git 包规格,不要填 npm 网页链接。仓库随代码分发服务端、客户端和类型产物,安装时不需要执行 `prepare` 或批准构建脚本。源码维护者每次修改源码后,都需运行 `pnpm build` 并更新已提交的产物。已经发布的 npm 产物保持不变。
78
93
 
79
- npm 默认标签 `latest` 现指向 `0.1.20`,因此新 profile 使用 `dsh plugin --profile web add @nanmicoder/dsh-agent-teams` 即可安装本版;需要锁定版本时使用上面的精确版本命令。推荐宿主为 Harness `0.1.5-rc.1`,安装插件不会自动升级宿主。源码安装见[维护指南](./docs/maintenance-workflow.md),验证范围见[本版验收记录](./docs/releases/v0.1.19/README.md)。
94
+ 手动克隆源码的用户,拉取代码后需重新运行 `pnpm install --frozen-lockfile` 和 `pnpm build`,再重启对应的 Harness;只更新 Git 不会更新本地构建产物。详见[维护指南](./docs/maintenance-workflow.md)。
80
95
 
81
- > Desktop 用户需核对应用内置的 Harness 核心;全局 CLI 升级不会升级桌面内核。旧 `0.1.0-*` / `0.1.1-*` 或其他未列出的宿主,请先保留已工作的组合,参考[旧版本与诊断指引](./docs/maintenance-workflow.md)。
96
+ > 旧 `0.1.0-*` / `0.1.1-*` 或其他未列出的宿主,请先保留已工作的组合,参考[旧版本与诊断指引](./docs/maintenance-workflow.md)。
82
97
 
83
98
  完整[兼容清单](./compatibility.json)、[源码安装与 Alpha 验证](./docs/maintenance-workflow.md)、[已验证范围与平台限制](./docs/maintenance-2026-09-06/release/README.md)。
84
99
 
@@ -88,6 +103,12 @@ npm 默认标签 `latest` 现指向 `0.1.20`,因此新 profile 使用 `dsh plu
88
103
 
89
104
  队长会话从第一轮起保留精简的固定核心协议和原有 14 个业务工具,直接调用,无需额外的加载工具或激活调用。已配置模板的精简目录固定保留。创建、批准、继续或结束团队都不会改写系统提示词和工具 schema,会话压缩或代码模式丢弃工具结果也不会丢失核心规则。成员保留四个团队工具、固定成员说明及普通编程/研究工具。Web 批准后会通知并唤醒队长,后续成员报告会再次唤醒它,无需忙轮询。详见[固定协议与 benchmark 标准](./docs/progressive-loading.md)。
90
105
 
106
+ ## 在工作区查看团队
107
+
108
+ 从当前聊天标题栏或回复下方的团队卡片点击 **查看团队**,在原生「团队协作」标签页中查看成员分工、任务依赖和执行进度。入口跟随所属聊天,不放在通用工作区开始页。
109
+
110
+ 页面直接呈现团队和任务内容;宽屏并排展示,窄分栏自动堆叠。点击任务可定位负责成员,成员图标可打开对应会话。关闭标签页不会停止团队;回到所属聊天可以再次打开。已完成团队保留归档,旧宿主继续使用活动面板。
111
+
91
112
  ## 工作方式
92
113
 
93
114
  1. 当前会话创建团队并成为队长。
Binary file
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "recommendedHost": "0.1.5-rc.1",
3
+ "recommendedHost": "0.1.7-rc.2",
4
4
  "previewTag": "next",
5
5
  "supportedHosts": [
6
6
  {
7
- "version": "0.1.5-rc.1",
7
+ "version": "0.1.7-rc.2",
8
8
  "track": "recommended"
9
9
  },
10
+ {
11
+ "version": "0.1.5-rc.3",
12
+ "track": "legacy"
13
+ },
14
+ {
15
+ "version": "0.1.5-rc.2",
16
+ "track": "legacy"
17
+ },
18
+ {
19
+ "version": "0.1.5-rc.1",
20
+ "track": "legacy"
21
+ },
10
22
  {
11
23
  "version": "0.1.2-rc.1",
12
24
  "track": "legacy"
@@ -19,5 +31,13 @@
19
31
  "version": "0.1.2-alpha.2",
20
32
  "track": "legacy"
21
33
  }
34
+ ],
35
+ "sourceCandidates": [
36
+ {
37
+ "version": "0.2.0",
38
+ "repository": "https://github.com/deepseek-ai/deepseek-harness",
39
+ "commit": "21638c56315ae6a2b552d6091945d3144c9af32e",
40
+ "evidence": "docs/harness-0.2.0-pre-adaptation/README.md"
41
+ }
22
42
  ]
23
43
  }
@@ -1,3 +1,5 @@
1
+ /** Stable, agent-scoped presentation. Business authority stays in the tools. */
2
+ import { onAgentReady } from "./harness-compat.js";
1
3
  import { readdirSync } from 'node:fs';
2
4
  import { join } from 'node:path';
3
5
  import { readTeamSync, readRetiredMemberIdsSync } from "./state.js";
@@ -95,7 +97,7 @@ export function installTeamCapabilities(ctx, config) {
95
97
  return agent !== undefined && states.get(agent)?.member ? TEAM_MEMBER_PROMPT : captainPrompt;
96
98
  },
97
99
  });
98
- ctx.on('agent/session-start', ({ agent }) => { attach(agent); });
100
+ onAgentReady(ctx, agent => { attach(agent); });
99
101
  ctx.effect(() => () => {
100
102
  mounted = false;
101
103
  for (const state of [...active])
@@ -18,8 +18,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
18
18
  * @module dsh-agent-teams/client/activity
19
19
  */
20
20
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
21
- import { IconBranchOutline16, IconChevronDownOutline14, IconPanelLeftOutline16, IconStopFill16, IconWarningOutline16, Modal, } from '@deepseek-ai/dsh-client-ui-primitives';
22
- import { activityPanelExpandedForSession, activityPanelShouldAutoExpand, compactDagLayout, compactModelLabel, COMPACT_DAG_NODE_HEIGHT, COMPACT_DAG_NODE_WIDTH, dependencyFocusTaskId, memberRouteLabel, relatedTaskIds, taskModelLabel, teamIsActive, usesParallelTaskGrid, } from "./activity-model.js";
21
+ import { IconBranchOutline16, IconChevronDownOutline14, IconPanelLeftOutline16, IconStopFill16, IconWarningOutline16, } from "./icons.js";
22
+ import { Modal } from '@deepseek-ai/dsh-client-ui-primitives';
23
+ import { currentSessionId } from "./session-navigation.js";
24
+ import { activityPanelExpandedForSession, activityPanelShouldAutoExpand, compactDagLayout, compactModelLabel, COMPACT_DAG_NODE_HEIGHT, COMPACT_DAG_NODE_WIDTH, dependencyFocusTaskId, memberRouteLabel, orderDelegationMembers, relatedTaskIds, taskModelLabel, teamIsActive, usesParallelTaskGrid, } from "./activity-model.js";
23
25
  import { ACTIVITY_HALT_URL, getActivityMonitorTargetsSnapshot, getActivitySnapshotsSnapshot, startActivityPolling, subscribeActivityMonitorTargets, subscribeActivitySnapshots, } from "./activity-monitor.js";
24
26
  import { ACTION_ART, LEAD_ART, memberArtUrl } from "./artwork.js";
25
27
  import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
@@ -158,6 +160,10 @@ function memberStatusText(member, tasks, t) {
158
160
  return t('member.status.waitingAssignment');
159
161
  if (member.done === member.total)
160
162
  return t('member.status.delivered');
163
+ if (owned.some((task) => task.status === 'failed'))
164
+ return t('task.detail.failed');
165
+ if (owned.length > 0 && owned.every((task) => task.status === 'completed' || task.status === 'cancelled'))
166
+ return t('member.status.settled');
161
167
  return t(member.activity === 'idle' ? 'member.status.idle' : 'member.status.unknown');
162
168
  }
163
169
  function compactTaskLabel(subject) {
@@ -209,17 +215,20 @@ function ProgressOverview({ team, t, discarded = false }) {
209
215
  const blocked = discarded ? 0 : team.tasks.filter((task) => task.state === 'blocked').length;
210
216
  const completed = discarded ? 0 : team.tasks.filter((task) => task.status === 'completed').length;
211
217
  const settled = !discarded && team.tasks.length > 0 && team.tasks.every((task) => (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'));
212
- const summaryTone = discarded ? 'discarded' : blocked > 0 ? 'warning' : settled ? 'completed' : 'running';
218
+ const summaryTone = discarded ? 'discarded' : blocked > 0 || team.tasks.some(task => task.status === 'failed' || task.status === 'cancelled') ? 'warning' : settled ? 'completed' : 'running';
213
219
  return (_jsxs("section", { className: css.progressOverview, "aria-label": t('progress.aria'), "data-progress-summary": true, children: [_jsx("span", { className: css.progressTitle, children: t('progress.title') }), team.tasks.length > 0 ? (_jsx("span", { className: css.progressSegments, "aria-hidden": true, children: team.tasks.map((task) => _jsx("span", { "data-state": discarded ? 'cancelled' : taskTone(task.state, task.status) }, task.id)) })) : _jsx("span", { className: css.progressEmpty }), _jsxs("span", { className: css.progressLegend, children: [_jsx("span", { "data-state": "running", children: t('progress.running', { count: running }) }), _jsx("span", { "data-state": "blocked", children: t('progress.blocked', { count: blocked }) }), _jsx("span", { "data-state": "completed", children: t('progress.delivered', { count: completed }) })] }), _jsxs("span", { className: css.progressSummary, "data-state": summaryTone, children: [_jsx("span", { className: css.progressSummaryDot }), _jsx("span", { children: taskSummary(team, t, discarded) })] })] }));
214
220
  }
215
- function DependencyMap({ tasks, members, t, discarded = false }) {
221
+ function DependencyMap({ tasks, members, t, discarded = false, workspace = false, onSelectTask }) {
216
222
  const [open, setOpen] = useState(true);
217
223
  const [hoverTaskId, setHoverTaskId] = useState(null);
218
224
  const [keyboardTaskId, setKeyboardTaskId] = useState(null);
219
225
  const [pinnedTaskId, setPinnedTaskId] = useState(null);
220
226
  const hoverTimer = useRef(null);
221
227
  const focusedTaskId = dependencyFocusTaskId(pinnedTaskId, keyboardTaskId, hoverTaskId);
222
- const layout = useMemo(() => compactDagLayout(tasks), [tasks]);
228
+ const nodeWidth = workspace ? 164 : COMPACT_DAG_NODE_WIDTH;
229
+ const nodeHeight = workspace ? 76 : COMPACT_DAG_NODE_HEIGHT;
230
+ const layout = useMemo(() => compactDagLayout(tasks, workspace
231
+ ? { nodeWidth: 164, nodeHeight: 76, columnGap: 36, rowGap: 16 } : undefined), [tasks, workspace]);
223
232
  const parallel = useMemo(() => usesParallelTaskGrid(tasks), [tasks]);
224
233
  const related = useMemo(() => focusedTaskId === null ? null : relatedTaskIds(focusedTaskId, tasks), [focusedTaskId, tasks]);
225
234
  const scheduleHover = (id) => {
@@ -242,12 +251,14 @@ function DependencyMap({ tasks, members, t, discarded = false }) {
242
251
  }, []);
243
252
  useEffect(() => {
244
253
  const onKeyDown = (event) => {
245
- if (event.key === 'Escape')
254
+ if (event.key === 'Escape') {
246
255
  setPinnedTaskId(null);
256
+ onSelectTask?.(null);
257
+ }
247
258
  };
248
259
  window.addEventListener('keydown', onKeyDown);
249
260
  return () => { window.removeEventListener('keydown', onKeyDown); };
250
- }, []);
261
+ }, [onSelectTask]);
251
262
  if (tasks.length === 0)
252
263
  return null;
253
264
  const fallbackTask = tasks.find((task) => task.state === 'blocked')
@@ -258,30 +269,35 @@ function DependencyMap({ tasks, members, t, discarded = false }) {
258
269
  const waitingOn = detailTask.dependencies.filter((dependency) => (tasks.find((task) => task.id === dependency)?.status !== 'completed'));
259
270
  const dependents = tasks.filter((task) => task.dependencies.includes(detailTask.id));
260
271
  return (_jsxs("section", { className: css.dependencySection, "aria-label": t('dependency.aria'), "data-dependency-map": true, children: [_jsxs("header", { className: css.sectionHead, children: [_jsxs("button", { type: "button", className: css.sectionToggleTitle, onClick: () => { setOpen((current) => !current); }, "aria-expanded": open, children: [_jsx(Chevron, { open: open }), _jsx(IconBranchOutline16, {}), " ", t(parallel ? 'dependency.parallel' : 'dependency.title')] }), _jsx("span", { className: css.sectionHint, children: pinnedTaskId === null
261
- ? t(parallel ? 'dependency.hint.parallel' : 'dependency.hint.chain')
272
+ ? t(parallel ? 'dependency.hint.parallel' : workspace ? 'workspace.dependencies' : 'dependency.hint.chain')
262
273
  : t('dependency.hint.pinned', { taskId: pinnedTaskId }) })] }), open && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.dagViewport, children: _jsxs("div", { className: css.dagCanvas, "data-layout": parallel ? 'parallel' : 'dependency', style: parallel ? undefined : { width: layout.width, height: layout.height }, children: [!parallel && _jsx("svg", { className: css.dagEdges, width: layout.width, height: layout.height, "aria-hidden": true, children: layout.edges.map((edge) => {
263
274
  const active = related !== null && related.has(edge.from) && related.has(edge.to);
264
275
  return _jsx("path", { d: edge.path, "data-active": active, "data-dimmed": related !== null && !active }, `${edge.from}:${edge.to}`);
265
276
  }) }), layout.nodes.map(({ task, x, y }) => {
266
277
  const model = taskModelLabel(task, members);
267
- const shortModel = compactModelLabel(model);
268
278
  return (_jsxs("button", { type: "button", className: css.dagNode, style: parallel
269
- ? { height: COMPACT_DAG_NODE_HEIGHT }
270
- : { left: x, top: y, width: COMPACT_DAG_NODE_WIDTH, height: COMPACT_DAG_NODE_HEIGHT }, "data-task-id": task.id, "data-state": discarded ? 'cancelled' : taskTone(task.state, task.status), "data-task-model": model || undefined, "data-focused": related?.has(task.id) ?? false, "data-dimmed": related !== null && !related.has(task.id), "aria-pressed": pinnedTaskId === task.id, title: taskTitle(task, model), onClick: () => { setPinnedTaskId((current) => current === task.id ? null : task.id); }, onMouseEnter: () => { scheduleHover(task.id); }, onMouseLeave: () => { scheduleHover(null); }, onFocus: () => { setKeyboardTaskId(task.id); }, onBlur: () => { setKeyboardTaskId(null); }, children: [_jsxs("span", { className: css.dagNodeHead, children: [_jsx("span", { className: css.dagNodeDot }), task.id] }), _jsx("span", { className: css.dagNodeLabel, children: task.state === 'running' && shortModel !== '' ? shortModel : compactTaskLabel(task.subject) }), task.state === 'running' && (_jsx("span", { className: css.dagRunningState, "aria-label": t('task.runningAria'), children: _jsx(WorkGlyph, { active: true }) }))] }, task.id));
279
+ ? { height: nodeHeight }
280
+ : { left: x, top: y, width: nodeWidth, height: nodeHeight }, "data-task-id": task.id, "data-state": discarded ? 'cancelled' : taskTone(task.state, task.status), "data-task-model": model || undefined, "data-focused": related?.has(task.id) ?? false, "data-dimmed": related !== null && !related.has(task.id), "aria-pressed": pinnedTaskId === task.id, title: taskTitle(task, model), onClick: () => { const next = pinnedTaskId === task.id ? null : task.id; setPinnedTaskId(next); onSelectTask?.(next); }, onMouseEnter: () => { scheduleHover(task.id); }, onMouseLeave: () => { scheduleHover(null); }, onFocus: () => { setKeyboardTaskId(task.id); }, onBlur: () => { setKeyboardTaskId(null); }, children: [_jsxs("span", { className: css.dagNodeHead, children: [_jsx("span", { className: css.dagNodeDot }), task.id, workspace && _jsx("span", { className: css.dagOwner, children: task.assignee || t('task.assignee.unclaimed') })] }), _jsx("span", { className: css.dagNodeLabel, children: workspace ? task.subject : compactTaskLabel(task.subject) }), task.state === 'running' && (_jsx("span", { className: css.dagRunningState, "aria-label": t('task.runningAria'), children: _jsx(WorkGlyph, { active: true }) }))] }, task.id));
271
281
  })] }) }), _jsxs("section", { className: css.taskDetail, "data-task-detail": detailTask.id, children: [_jsxs("span", { className: css.taskDetailHead, children: [_jsx("span", { className: css.taskDetailId, children: detailTask.id }), _jsx("span", { className: css.taskDetailSubject, title: detailTask.subject, children: detailTask.subject.replace(/^开发\s*/u, '') }), _jsx("span", { className: css.taskDetailBadge, "data-state": discarded ? 'cancelled' : taskTone(detailTask.state, detailTask.status), children: discarded ? t('task.status.notRun') : taskStatusLabel(detailTask.status, t) })] }), _jsxs("span", { className: css.taskDetailLine, children: [detailTask.assignee || t('task.assignee.unclaimed'), " \u00B7 ", discarded
272
282
  ? t('task.detail.notRun')
273
283
  : detailTask.status === 'completed'
274
284
  ? t('task.detail.completed')
275
- : detailTask.dependencies.length === 0
276
- ? t('task.detail.noPrerequisite')
277
- : waitingOn.length === 0
278
- ? t('task.detail.ready')
279
- : t('task.detail.waitingOn', { tasks: formatTaskIds(waitingOn, t) })] }), detailModel !== '' && (_jsx("span", { className: css.taskDetailModel, "data-task-model": detailModel, children: t('task.model', { model: detailModel }) })), _jsx("span", { className: css.taskDetailMeta, children: dependents.length === 0
285
+ : detailTask.status === 'cancelled'
286
+ ? t('task.detail.cancelled')
287
+ : detailTask.status === 'failed'
288
+ ? t('task.detail.failed')
289
+ : detailTask.dependencies.length === 0
290
+ ? t('task.detail.noPrerequisite')
291
+ : waitingOn.length === 0
292
+ ? t('task.detail.ready')
293
+ : t('task.detail.waitingOn', { tasks: formatTaskIds(waitingOn, t) })] }), detailModel !== '' && (_jsx("span", { className: css.taskDetailModel, "data-task-model": detailModel, children: t('task.model', { model: detailModel }) })), _jsx("span", { className: css.taskDetailMeta, children: dependents.length === 0
280
294
  ? t('task.detail.noDownstream')
281
295
  : t('task.detail.unlocks', { tasks: formatTaskIds(dependents.map((task) => task.id), t) }) })] })] }))] }));
282
296
  }
283
- function TeamSection({ team, modelDirectory, onContinuePlanning, onDiscarded, onNavigate, t, historic = false }) {
284
- const [membersOpen, setMembersOpen] = useState(true);
297
+ export function TeamSection({ team, modelDirectory, onContinuePlanning, onDiscarded, onNavigate, t, historic = false, workspace = false }) {
298
+ const [selectedTaskId, setSelectedTaskId] = useState(null);
299
+ const selectedAssignee = team.tasks.find(task => task.id === selectedTaskId)?.assignee;
300
+ const [membersOpen, setMembersOpen] = useState(workspace);
285
301
  const [stopOpen, setStopOpen] = useState(false);
286
302
  const [stopping, setStopping] = useState(false);
287
303
  const [stopError, setStopError] = useState('');
@@ -329,7 +345,7 @@ function TeamSection({ team, modelDirectory, onContinuePlanning, onDiscarded, on
329
345
  setStopping(false);
330
346
  }
331
347
  };
332
- return (_jsxs(_Fragment, { children: [_jsxs("section", { className: css.team, "data-team-id": team.teamId, children: [_jsxs("header", { className: css.teamHead, children: [_jsx("span", { className: css.teamName, title: team.name, children: team.name }), historic && _jsx("span", { className: css.historicPill, children: t(discarded ? 'team.discarded' : 'team.ended') }), stopped && _jsx("span", { className: css.historicPill, children: t('team.stopped') }), _jsxs("span", { className: css.teamStats, children: [_jsx("span", { "data-stat": "members", children: t('team.stats.members', { count: team.members.length }) }), _jsx("span", { "data-stat": "tasks", children: t('team.stats.completed', { completed: completedCount, total: team.tasks.length }) }), _jsx("span", { "data-stat": "messages", children: t('team.stats.messages', { count: team.messageCount }) })] }), canStop && (_jsx("button", { type: "button", className: css.teamStopButton, "aria-label": t('team.stop'), title: t('team.stop'), onClick: () => { setStopError(''); setStopOpen(true); }, children: _jsx(IconStopFill16, {}) }))] }), team.phase === 'staged' && !historic && modelDirectory !== undefined && onContinuePlanning !== undefined && onDiscarded !== undefined && (_jsx(StagingPlanEditor, { team: team, modelDirectory: modelDirectory, onContinuePlanning: onContinuePlanning, onDiscarded: onDiscarded, t: t })), _jsxs("section", { className: css.delegationSection, "aria-label": t('delegation.aria'), "data-delegation-map": true, children: [_jsxs("div", { className: css.captainNode, children: [_jsx("span", { className: css.captainAvatar, children: _jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }) }), _jsxs("span", { className: css.captainInfo, children: [_jsxs("span", { className: css.captainLine, children: [_jsx("span", { className: css.captainName, children: t('captain.name') }), _jsx("span", { className: css.captainRole, children: t('captain.role') })] }), _jsx("span", { className: css.captainSummary, children: discarded
348
+ return (_jsxs(_Fragment, { children: [_jsxs("section", { className: css.team, "data-team-id": team.teamId, "data-workspace-team": workspace || undefined, children: [_jsxs("header", { className: css.teamHead, children: [_jsx("span", { className: css.teamName, title: team.name, children: team.name }), historic && _jsx("span", { className: css.historicPill, children: t(discarded ? 'team.discarded' : 'team.ended') }), stopped && _jsx("span", { className: css.historicPill, children: t('team.stopped') }), _jsxs("span", { className: css.teamStats, children: [_jsx("span", { "data-stat": "members", children: t('team.stats.members', { count: team.members.length }) }), _jsx("span", { "data-stat": "tasks", children: t('team.stats.completed', { completed: completedCount, total: team.tasks.length }) }), _jsx("span", { "data-stat": "messages", children: t('team.stats.messages', { count: team.messageCount }) })] }), canStop && (_jsx("button", { type: "button", className: css.teamStopButton, "aria-label": t('team.stop'), title: t('team.stop'), onClick: () => { setStopError(''); setStopOpen(true); }, children: _jsx(IconStopFill16, {}) }))] }), team.phase === 'staged' && !historic && modelDirectory !== undefined && onContinuePlanning !== undefined && onDiscarded !== undefined && (_jsx(StagingPlanEditor, { team: team, modelDirectory: modelDirectory, onContinuePlanning: onContinuePlanning, onDiscarded: onDiscarded, t: t })), _jsxs("section", { className: css.delegationSection, "aria-label": t('delegation.aria'), "data-delegation-map": true, children: [_jsxs("div", { className: css.captainNode, children: [_jsx("span", { className: css.captainAvatar, children: _jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }) }), _jsxs("span", { className: css.captainInfo, children: [_jsxs("span", { className: css.captainLine, children: [_jsx("span", { className: css.captainName, children: t('captain.name') }), _jsx("span", { className: css.captainRole, children: t('captain.role') })] }), _jsx("span", { className: css.captainSummary, children: discarded
333
349
  ? t('captain.summary.discarded', { tasks: team.tasks.length, members: team.members.length })
334
350
  : captainBusy
335
351
  ? t('captain.summary.withTakeover', { tasks: assignedCount, captainTasks: captainTaskIds })
@@ -351,44 +367,57 @@ function TeamSection({ team, modelDirectory, onContinuePlanning, onDiscarded, on
351
367
  ? 'captain.state.collected'
352
368
  : allSettled
353
369
  ? 'captain.state.settled'
354
- : 'captain.state.waiting')] })] }), _jsx(ProgressOverview, { team: team, t: t, discarded: discarded }), _jsxs("button", { type: "button", className: css.membersToggle, onClick: () => { setMembersOpen((current) => !current); }, "aria-expanded": membersOpen, "data-members-toggle": true, children: [_jsxs("span", { children: [_jsx(Chevron, { open: membersOpen }), t('members.toggle', { count: team.members.length })] }), _jsx("span", { children: t(membersOpen ? 'members.collapse' : 'members.expand') })] }), membersOpen && _jsxs("div", { className: css.delegationTree, children: [team.members.length === 0 && _jsx("span", { className: css.emptyHint, children: t('members.empty') }), team.members.map((member) => {
355
- const owned = team.tasks.filter((task) => task.assignee === member.name);
356
- const memberModel = memberRouteLabel(member);
357
- return (_jsxs("div", { className: css.memberBlock, "data-activity": member.activity, children: [_jsx("span", { className: css.memberBranch, "aria-hidden": true, children: _jsx("span", {}) }), _jsxs("button", { type: "button", className: css.memberRow, "data-activity": member.activity, onClick: () => {
358
- if (member.id !== '') {
359
- onNavigate(team.captainSessionId, member.id);
360
- }
361
- }, children: [_jsxs("span", { className: css.memberAvatar, "data-unread": member.unread > 0, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, style: { background: accentOf(member.id) }, children: memberInitial(member.name) })), _jsx("img", { className: css.stateArt, "data-activity": member.activity, src: ACTION_ART[member.activity], alt: "", "aria-hidden": true })] }), _jsxs("span", { className: css.memberInfo, children: [_jsxs("span", { className: css.memberLine, children: [_jsx("span", { className: css.memberName, children: member.name }), member.role !== '' && _jsx("span", { className: css.memberRole, children: member.role }), memberModel !== '' && (_jsx("span", { className: css.memberModel, role: "img", "data-member-model": memberModel, title: memberModel, "aria-label": memberModel, children: compactModelLabel(memberModel) })), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(WorkGlyph, { active: member.activity === 'working' }), discarded
362
- ? t('member.state.notCreated')
370
+ : 'captain.state.waiting')] })] }), _jsx(ProgressOverview, { team: team, t: t, discarded: discarded }), (() => {
371
+ const orderedMembers = orderDelegationMembers(team.members, team.tasks);
372
+ const runningMembers = orderedMembers.filter((member) => (member.activity === 'working' || member.status === 'working'
373
+ || team.tasks.some((task) => task.assignee === member.name
374
+ && (task.status === 'pending' || task.status === 'claimed' || task.status === 'in_progress'))));
375
+ const visibleMembers = membersOpen ? orderedMembers : runningMembers;
376
+ const hiddenFinishedCount = orderedMembers.length - visibleMembers.length;
377
+ const expandLabel = membersOpen
378
+ ? t('members.collapse')
379
+ : hiddenFinishedCount > 0
380
+ ? t('members.expandFinished', { count: hiddenFinishedCount })
381
+ : t('members.expand');
382
+ return (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", className: css.membersToggle, onClick: () => { setMembersOpen((current) => !current); }, "aria-expanded": membersOpen, "data-members-toggle": true, children: [_jsxs("span", { children: [_jsx(Chevron, { open: membersOpen }), t('members.toggle', { count: team.members.length })] }), _jsx("span", { children: expandLabel })] }), (membersOpen || visibleMembers.length > 0) && _jsxs("div", { className: css.delegationTree, children: [team.members.length === 0 && _jsx("span", { className: css.emptyHint, children: t('members.empty') }), visibleMembers.map((member) => {
383
+ const owned = team.tasks.filter((task) => task.assignee === member.name);
384
+ const memberModel = memberRouteLabel(member);
385
+ return (_jsxs("div", { className: css.memberBlock, "data-activity": member.activity, "data-selected-member": workspace && selectedAssignee === member.name || undefined, children: [_jsx("span", { className: css.memberBranch, "aria-hidden": true, children: _jsx("span", {}) }), _jsxs("button", { type: "button", className: css.memberRow, "data-activity": member.activity, onClick: () => {
386
+ if (member.id !== '') {
387
+ onNavigate(team.captainSessionId, member.id);
388
+ }
389
+ }, children: [_jsxs("span", { className: css.memberAvatar, "data-unread": member.unread > 0, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, style: { background: accentOf(member.id) }, children: memberInitial(member.name) })), _jsx("img", { className: css.stateArt, "data-activity": member.activity, src: ACTION_ART[member.activity], alt: "", "aria-hidden": true })] }), _jsxs("span", { className: css.memberInfo, children: [_jsxs("span", { className: css.memberLine, children: [_jsx("span", { className: css.memberName, children: member.name }), member.role !== '' && _jsx("span", { className: css.memberRole, children: member.role }), memberModel !== '' && (_jsx("span", { className: css.memberModel, role: "img", "data-member-model": memberModel, title: memberModel, "aria-label": memberModel, children: compactModelLabel(memberModel) })), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(WorkGlyph, { active: member.activity === 'working' }), discarded
390
+ ? t('member.state.notCreated')
391
+ : stopped
392
+ ? t('member.state.stopped')
393
+ : team.phase === 'staged'
394
+ ? t('member.state.staged')
395
+ : memberStateLabel(member, team.tasks, historic, t)] })] }), _jsx("span", { className: css.memberStatusLine, children: discarded
396
+ ? t('member.status.discarded')
363
397
  : stopped
364
- ? t('member.state.stopped')
398
+ ? t('member.status.stopped')
365
399
  : team.phase === 'staged'
366
- ? t('member.state.staged')
367
- : memberStateLabel(member, team.tasks, historic, t)] })] }), _jsx("span", { className: css.memberStatusLine, children: discarded
368
- ? t('member.status.discarded')
369
- : stopped
370
- ? t('member.status.stopped')
400
+ ? t('member.status.staged')
401
+ : historic && owned.length > 0 && owned.every((task) => (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'))
402
+ ? t('member.status.settled')
403
+ : memberStatusText(member, team.tasks, t) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children: t(discarded
404
+ ? 'assignment.discarded'
371
405
  : team.phase === 'staged'
372
- ? t('member.status.staged')
373
- : historic && owned.length > 0 && owned.every((task) => (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'))
374
- ? t('member.status.settled')
375
- : memberStatusText(member, team.tasks, t) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children: t(discarded
376
- ? 'assignment.discarded'
377
- : team.phase === 'staged'
378
- ? 'assignment.staged'
379
- : 'assignment.label') }), _jsx("span", { className: css.assignmentTasks, children: owned.length === 0
380
- ? _jsx("span", { className: css.taskEmpty, children: t('assignment.empty') })
381
- : owned.map((task) => {
382
- const model = taskModelLabel(task, team.members);
383
- const shortModel = compactModelLabel(model);
384
- return (_jsx("span", { className: css.assignmentChip, "data-state": discarded ? 'cancelled' : taskTone(task.state, task.status), "data-task-model": model || undefined, title: taskTitle(task, model), children: task.state === 'running' && shortModel !== '' ? `${task.id} · ${shortModel}` : task.id }, task.id));
385
- }) })] })] }, member.id || member.name));
386
- })] })] }), _jsx(DependencyMap, { tasks: team.tasks, members: team.members, t: t, discarded: discarded })] }), _jsx(Modal, { open: stopOpen, onClose: () => { if (!stopping)
406
+ ? 'assignment.staged'
407
+ : 'assignment.label') }), _jsx("span", { className: css.assignmentTasks, children: owned.length === 0
408
+ ? _jsx("span", { className: css.taskEmpty, children: t('assignment.empty') })
409
+ : owned.map((task) => {
410
+ const model = taskModelLabel(task, team.members);
411
+ const shortModel = compactModelLabel(model);
412
+ return (_jsx("span", { className: css.assignmentChip, "data-state": discarded ? 'cancelled' : taskTone(task.state, task.status), "data-task-model": model || undefined, title: taskTitle(task, model), children: task.state === 'running' && shortModel !== '' ? `${task.id} · ${shortModel}` : task.id }, task.id));
413
+ }) })] })] }, member.id || member.name));
414
+ })] })] }));
415
+ })()] }), _jsx(DependencyMap, { tasks: team.tasks, members: team.members, t: t, discarded: discarded, workspace: workspace, onSelectTask: setSelectedTaskId })] }), _jsx(Modal, { open: stopOpen, onClose: () => { if (!stopping)
387
416
  setStopOpen(false); }, title: t('team.stopTitle', { team: team.name }), closeLabel: t('plan.cancel'), description: t('team.stopDescription', { tasks: unfinishedCount, members: busyCount }), footer: (_jsxs("span", { className: css.stopModalActions, children: [_jsx("button", { type: "button", disabled: stopping, onClick: () => { setStopOpen(false); }, children: t('team.stopCancel') }), _jsxs("button", { type: "button", "data-danger": true, disabled: stopping, onClick: () => { void stopTeam(); }, children: [_jsx(IconStopFill16, {}), stopping ? t('team.stopping') : t('team.stopConfirm')] })] })), children: stopError !== '' && _jsxs("p", { className: css.stopModalError, role: "alert", children: [_jsx(IconWarningOutline16, {}), stopError] }) })] }));
388
417
  }
389
418
  /** Legacy conversation cards may outlive their host archive. Project their
390
419
  * durable roster through the same rebuilt panel instead of a second UI. */
391
- function historicCardTeam(data, owner) {
420
+ export function historicCardTeam(data, owner) {
392
421
  return {
393
422
  workspace: '',
394
423
  teamId: data.teamId,
@@ -432,7 +461,7 @@ export function ActivityPanel({ sessionsList, modelDirectories, openMember, t, c
432
461
  const gestureRef = useRef(null);
433
462
  const frameRef = useRef(null);
434
463
  const pendingLayoutRef = useRef(null);
435
- const current = useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot).current;
464
+ const current = currentSessionId(useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot));
436
465
  const autoOpenTrackerRef = useRef({ sessionId: current, restoreComplete: false, liveTeamIds: new Set() });
437
466
  const monitorTargets = useSyncExternalStore(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
438
467
  const returnToComposer = () => {
@@ -16,10 +16,10 @@ import { LEAD_ART, memberArtUrl } from "./artwork.js";
16
16
  import css from './AgentTeamsCard.module.css';
17
17
  /** Window event name the floater listens for to open itself. */
18
18
  export const OPEN_PANEL_EVENT = 'agent-teams:open-panel';
19
- /** Re-activate the top-right activity panel, carrying this team's summary
19
+ /** Open the matching team view, carrying this team's summary
20
20
  * so the panel can show it even when the team no longer exists on disk
21
21
  * (historical session review). */
22
- function openActivityPanel(data) {
22
+ export function openActivityPanel(data) {
23
23
  window.dispatchEvent(new CustomEvent(OPEN_PANEL_EVENT, {
24
24
  detail: {
25
25
  teamId: data.teamId,
@@ -30,8 +30,17 @@ function openActivityPanel(data) {
30
30
  }));
31
31
  }
32
32
  /** Render one durable team as a compact conversation card. */
33
- export function AgentTeamsCard({ node, openMember, sessionId, t }) {
34
- const data = node.data;
33
+ const noWorkspace = () => undefined;
34
+ const noSubscription = () => () => { };
35
+ export function AgentTeamsCard({ node, openMember, sessionId, t, workspaceBridge }) {
36
+ const native = useSyncExternalStore(workspaceBridge?.subscribe ?? noSubscription, workspaceBridge?.getSnapshot ?? noWorkspace);
37
+ const location = node.location;
38
+ // Closed turns render their card through the official, non-folding turn tail.
39
+ if (native && (location.kind === 'turn' || location.kind === 'step') && location.turn.status === 'closed')
40
+ return null;
41
+ return _jsx(AgentTeamsSummary, { data: node.data, openMember: openMember, sessionId: sessionId, t: t });
42
+ }
43
+ export function AgentTeamsSummary({ data, openMember, sessionId, t }) {
35
44
  // `conversation.chat.node` is session-scoped, so its framework-owned id is
36
45
  // a stable owner even while another conversation becomes current.
37
46
  const owner = data.captainSessionId || sessionId;
@@ -47,7 +56,7 @@ export function AgentTeamsCard({ node, openMember, sessionId, t }) {
47
56
  teamName: snapshot?.name ?? data.teamName,
48
57
  members: snapshot?.members.map((member) => ({ id: member.id, name: member.name, role: member.role })) ?? data.members,
49
58
  }), [data, owner, snapshot]);
50
- return (_jsxs("section", { className: css.root, "data-agent-teams-card": true, "data-team-id": resolved.teamId, children: [_jsxs("header", { className: css.head, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), _jsx("span", { className: css.teamName, title: resolved.teamName, children: resolved.teamName }), _jsx("span", { className: css.memberCount, children: t('card.memberCount', { count: resolved.members.length }) }), _jsx("button", { type: "button", className: css.panelButton, onClick: () => { openActivityPanel(resolved); }, "aria-label": t('action.openActivityPanel'), title: t('action.openActivityPanel'), children: t('activity.panelButton') })] }), resolved.members.length > 0 && (_jsx("div", { className: css.members, children: resolved.members.map((member) => (_jsxs("button", { type: "button", className: css.member, onClick: () => {
59
+ return (_jsxs("section", { className: css.root, "data-agent-teams-card": true, "data-team-id": resolved.teamId, children: [_jsxs("header", { className: css.head, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), _jsx("span", { className: css.teamName, title: resolved.teamName, children: resolved.teamName }), _jsx("span", { className: css.memberCount, children: t('card.memberCount', { count: resolved.members.length }) }), _jsx("button", { type: "button", className: css.panelButton, onClick: () => { openActivityPanel(resolved); }, "aria-label": t('workspace.focus'), title: t('workspace.focus'), children: t('workspace.focus') })] }), resolved.members.length > 0 && (_jsx("div", { className: css.members, children: resolved.members.map((member) => (_jsxs("button", { type: "button", className: css.member, onClick: () => {
51
60
  if (member.id !== '')
52
61
  openMember(owner, member.id);
53
62
  }, title: member.role === '' ? member.name : `${member.name} · ${member.role}`, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, children: member.name.trim().slice(0, 1).toUpperCase() || '?' })), _jsx("span", { className: css.memberName, children: member.name })] }, member.id || member.name))) }))] }));
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Session-owned entry points: a persistent title action and a durable turn card. */
3
+ import { useSyncExternalStore } from 'react';
4
+ import { AgentTeamsSummary, openActivityPanel } from "./AgentTeamsCard.js";
5
+ import { getActivitySnapshotsSnapshot, subscribeActivitySnapshots } from "./activity-monitor.js";
6
+ import { teamCardsForTurn } from "./agent-teams-card-definition.js";
7
+ import { LEAD_ART } from "./artwork.js";
8
+ import css from './AgentTeamsCard.module.css';
9
+ export function TeamChatEntry({ sessionId, t }) {
10
+ const { teams, archivedTeams } = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
11
+ const team = [...teams, ...archivedTeams].find(team => team.captainSessionId === sessionId);
12
+ if (!team)
13
+ return null;
14
+ return _jsxs("button", { className: css.chatEntry, "data-team-chat-entry": true, type: "button", title: t('workspace.focus'), onClick: () => openActivityPanel({ teamId: team.teamId, captainSessionId: sessionId, teamName: team.name, members: team.members }), children: [_jsx("img", { src: LEAD_ART, alt: "", "aria-hidden": true }), t('workspace.focus')] });
15
+ }
16
+ export function TeamTurnCard({ sessionId, turn, useChat, t, openMember }) {
17
+ const nodes = useChat(snapshot => snapshot.nodes);
18
+ const cards = teamCardsForTurn(nodes.values(), turn.turn);
19
+ if (turn.status !== 'closed' || cards.length === 0)
20
+ return null;
21
+ return _jsx("div", { className: css.turnCards, "data-team-turn-cards": true, children: cards.map(node => _jsx(AgentTeamsSummary, { data: node.data, sessionId: sessionId, t: t, openMember: openMember }, node.key)) });
22
+ }