@nanmicoder/dsh-agent-teams 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/assets/agent-teams/action-celebrating.png +0 -0
  4. package/assets/agent-teams/action-reporting.png +0 -0
  5. package/assets/agent-teams/action-sending.png +0 -0
  6. package/assets/agent-teams/action-sleeping.png +0 -0
  7. package/assets/agent-teams/action-thinking.png +0 -0
  8. package/assets/agent-teams/action-working.png +0 -0
  9. package/assets/agent-teams/data-analyst.png +0 -0
  10. package/assets/agent-teams/designer.png +0 -0
  11. package/assets/agent-teams/docs-coordinator.png +0 -0
  12. package/assets/agent-teams/engineer.png +0 -0
  13. package/assets/agent-teams/qa-engineer.png +0 -0
  14. package/assets/agent-teams/researcher.png +0 -0
  15. package/assets/agent-teams/security-reviewer.png +0 -0
  16. package/assets/agent-teams/team-lead.png +0 -0
  17. package/cordis.patch.yml +21 -0
  18. package/lib/client/ActivityPanel.js +340 -0
  19. package/lib/client/AgentTeamsCard.js +74 -0
  20. package/lib/client/activity-model.js +70 -0
  21. package/lib/client/agent-teams-card-definition.js +85 -0
  22. package/lib/client/artwork.js +40 -0
  23. package/lib/client/index.js +33 -0
  24. package/lib/client.js +1235 -0
  25. package/lib/client.js.map +1 -0
  26. package/lib/event-types.js +12 -0
  27. package/lib/events.js +60 -0
  28. package/lib/index.js +172 -0
  29. package/lib/members.js +168 -0
  30. package/lib/snapshot.js +155 -0
  31. package/lib/state.js +461 -0
  32. package/lib/tools.js +749 -0
  33. package/lib/types/client/ActivityPanel.d.ts +64 -0
  34. package/lib/types/client/AgentTeamsCard.d.ts +24 -0
  35. package/lib/types/client/activity-model.d.ts +31 -0
  36. package/lib/types/client/agent-teams-card-definition.d.ts +44 -0
  37. package/lib/types/client/artwork.d.ts +19 -0
  38. package/lib/types/client/index.d.ts +11 -0
  39. package/lib/types/event-types.d.ts +103 -0
  40. package/lib/types/events.d.ts +37 -0
  41. package/lib/types/index.d.ts +42 -0
  42. package/lib/types/members.d.ts +86 -0
  43. package/lib/types/snapshot.d.ts +83 -0
  44. package/lib/types/state.d.ts +144 -0
  45. package/lib/types/tools.d.ts +40 -0
  46. package/lib/types/types.d.ts +73 -0
  47. package/lib/types.js +11 -0
  48. package/package.json +108 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 程序员阿江(Relakkes)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @nanmicoder/dsh-agent-teams
2
+
3
+ [![npm](https://img.shields.io/npm/v/@nanmicoder/dsh-agent-teams.svg)](https://www.npmjs.com/package/@nanmicoder/dsh-agent-teams)
4
+ [![license](https://img.shields.io/npm/l/@nanmicoder/dsh-agent-teams.svg)](LICENSE)
5
+
6
+ DeepSeek Harness 的 AgentTeams 插件:安装后,任何会话只需一句自然语言(例如"用 AgentTeams 调研一下 XX"),即可驱动一个**多智能体团队**协作完成目标,并在 Web GUI 右上角实时看到团队活动面板。
7
+
8
+ 核心语义移植自 Claude Code 的 AgentTeams:**创建团队**(队长 = 当前会话 agent)→ **拉成员**(可续聊子代理)→ **拆任务并声明依赖** → **成员间直接收发消息**(邮箱直达 + 唤醒,无队长中转)。
9
+
10
+ ## 界面预览
11
+
12
+ ![AgentTeams 活动面板](https://raw.githubusercontent.com/NanmiCoder/dsh-agent-teams/main/assets/ui.png)
13
+
14
+ ## 安装
15
+
16
+ ### 前置要求
17
+
18
+ - Node.js `^22.19` 或 `>=24`;pnpm 11
19
+ - 已安装或可通过 `npx` 使用 DeepSeek Harness
20
+
21
+ ### 方式一:从 npm 安装(推荐)
22
+
23
+ 插件已发布到 npm(`@nanmicoder` scope),一条命令装好:
24
+
25
+ ```sh
26
+ npx -p @deepseek-ai/dsh dsh plugin --profile web add @nanmicoder/dsh-agent-teams@0.1.0
27
+ ```
28
+
29
+ 带版本号安装最稳妥:`latest` 的解析可能受 registry 缓存影响(国内镜像源同步新版本通常有几分钟到几小时延迟)。升级时把 `@0.1.0` 换成新版本号即可。
30
+
31
+ ### 方式二:从 GitHub 安装
32
+
33
+ 不想走 npm、或想直接装未发布的最新提交:
34
+
35
+ ```sh
36
+ npx -p @deepseek-ai/dsh dsh plugin --profile web add github:NanmiCoder/dsh-agent-teams
37
+ ```
38
+
39
+ 两种方式效果相同:`dsh plugin` 会把插件加入 `web` profile,并根据包内的 `dsh.bundle` 声明自动启用它;工具、系统提示和 Web 客户端入口随该 profile 一起加载。
40
+
41
+ > **重启生效**:安装完成后,重启正在运行的 DeepSeek Harness Web 服务并刷新页面。
42
+
43
+ ## 使用
44
+
45
+ 安装并重启后,直接对助手说:
46
+
47
+ > 用 AgentTeams 帮我调研一下开源 RAG 框架的选型,输出对比报告
48
+
49
+ 插件内置提示段会指导模型按协议执行:建团队 → 按角色拉成员 → 拆任务声明依赖 → 领取并唤醒成员 → 轮询收集产出 → 汇报后删除(归档保留)。
50
+
51
+ ## 开发 Skill
52
+
53
+ 仓库按开放 Agent Skills 规范提供 [`dsh-plugin-development`](skills/dsh-plugin-development/SKILL.md),可直接安装:
54
+
55
+ ```sh
56
+ npx skills add NanmiCoder/dsh-agent-teams --skill dsh-plugin-development
57
+ ```
58
+
59
+ `skills/dsh-plugin-development/` 是唯一权威源码;`.dsh/skills/` 保存供 DSH 在本仓库作为 cwd 时自动发现的跨平台镜像。修改 Skill 后运行 `pnpm sync:skill`,`pnpm verify` 会检查镜像没有漂移。
60
+
61
+ ## 文档
62
+
63
+ | 文档 | 内容 |
64
+ |---|---|
65
+ | [docs/usage.md](docs/usage.md) | 工作原理、Web UI 行为、工具一览、配置、已知限制、验证 |
66
+ | [docs/verification-guide.md](docs/verification-guide.md) | 四层验证方法(离线 / 组合 / 真实 e2e / ego-browser GUI) |
67
+ | [skills/dsh-plugin-development/SKILL.md](skills/dsh-plugin-development/SKILL.md) | 可通过 `npx skills` 安装的 DSH 插件开发 Skill |
68
+ | [docs/developing-dsh-plugins.md](docs/developing-dsh-plugins.md) | 面向人类阅读的开发指南(本插件为样例) |
69
+
70
+ ## License
71
+
72
+ MIT
Binary file
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ # dsh-agent-teams bundle patch: mounts the agent-teams plugin into the host
2
+ # composition of a dsh profile. The plugin registers its `agent_teams_*` tools
3
+ # into the shared `tools` registry and one usage section into the global
4
+ # system prompt, so every session of the profile can drive AgentTeams through
5
+ # natural language ("用 AgentTeams 做 X").
6
+ #
7
+ # Install: `dsh plugin --profile <name> add <this package>` (npm or a local
8
+ # path). The `dsh plugin` command pnpm-installs the package into the profile
9
+ # and reconciles it into the profile's `dsh.profile.bundles` layer list.
10
+ - insert:
11
+ - id: agent-teams
12
+ # Node-resolvable package name — must stay in sync with package.json
13
+ # `name`. Quoted because `@` is a reserved indicator in YAML and cannot
14
+ # open a plain scalar.
15
+ name: '@nanmicoder/dsh-agent-teams'
16
+ config:
17
+ # Team state lives under `<session workspace>/<stateDir>/<teamId>/`
18
+ # (team.json + inbox/*.jsonl mailboxes).
19
+ stateDir: .agent-teams
20
+ # Provider used to spawn member subagents ('spawn' or 'fork').
21
+ memberProvider: spawn
@@ -0,0 +1,340 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * AgentTeams activity panel: the top-right floater monitoring every team.
4
+ *
5
+ * Modeled on the Claude Code desktop SessionActivityPanel: a fixed glass
6
+ * panel at the top-right corner. On wide viewports it cooperatively makes the
7
+ * conversation column yield space; narrow viewports keep overlay mode. It
8
+ * polls the host `/plugins/dsh-agent-teams/state` route for
9
+ * server-side snapshots (durable files + live subagent activity), with a
10
+ * collapsed badge that auto-expands once when activity appears. Archived
11
+ * teams stay available for the owning conversation after live work ends.
12
+ *
13
+ * The floater mounts through a body portal (no top-right slot exists in the
14
+ * web shell); it is not a conversation node — the in-conversation panel was
15
+ * removed in favor of this always-available monitor.
16
+ * @module dsh-agent-teams/client/activity
17
+ */
18
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
19
+ import { IconBranchOutline16, IconChevronRightOutline14, IconCloseOutline16, StateDot, } from '@deepseek-ai/dsh-client-ui-primitives';
20
+ import { activityPanelExpandedForSession, relatedTaskIds, taskStages } from "./activity-model.js";
21
+ import { ACTION_ART, LEAD_ART, memberArtUrl } from "./artwork.js";
22
+ import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
23
+ import css from './ActivityPanel.module.css';
24
+ /** Poll cadence for the host snapshot route. */
25
+ const POLL_MS = 1000;
26
+ /** Grace before the panel collapses once no team remains. */
27
+ const AUTOCLOSE_GRACE_MS = 2000;
28
+ /**
29
+ * Page-settle window after mount: activity restored on page load only shows
30
+ * the collapsed badge, so the panel never yanks the conversation column
31
+ * right after load. New activity after this window auto-expands as usual.
32
+ */
33
+ const AUTO_OPEN_SETTLE_MS = 4000;
34
+ /** Host route serving team snapshots. */
35
+ const STATE_URL = '/plugins/dsh-agent-teams/state';
36
+ /** Root marker shared with the panel CSS while the portal is expanded. */
37
+ const PANEL_OPEN_ATTRIBUTE = 'data-agent-teams-panel-open';
38
+ /** Initial-letter fallback for unmatched roles. */
39
+ function memberInitial(name) {
40
+ return name.trim().slice(0, 1).toUpperCase() || '?';
41
+ }
42
+ function stableHash(value) {
43
+ let hash = 0;
44
+ for (let index = 0; index < value.length; index += 1) {
45
+ hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0;
46
+ }
47
+ return Math.abs(hash);
48
+ }
49
+ const ACCENTS = [
50
+ 'var(--dsw-alias-state-business-primary)',
51
+ 'var(--dsw-alias-state-success)',
52
+ 'var(--dsw-alias-state-danger)',
53
+ 'var(--dsw-alias-state-warning)',
54
+ 'var(--dsw-alias-label-tertiary)',
55
+ ];
56
+ function accentOf(id) {
57
+ return ACCENTS[stableHash(id) % ACCENTS.length] ?? ACCENTS[0];
58
+ }
59
+ /** Badge text follows the raw task status (finer than the 4 visual states):
60
+ * claimed/pending/failed/cancelled keep their own labels and colors. */
61
+ const TASK_STATUS_LABEL = {
62
+ pending: '待领取',
63
+ claimed: '已认领',
64
+ in_progress: '进行中',
65
+ completed: '已完成',
66
+ failed: '失败',
67
+ cancelled: '已取消',
68
+ };
69
+ function taskStatusLabel(status) {
70
+ return TASK_STATUS_LABEL[status] ?? status;
71
+ }
72
+ /** Badge/bar coloring key: visual state, widened for terminal statuses. */
73
+ function taskTone(state, status) {
74
+ if (status === 'failed')
75
+ return 'failed';
76
+ if (status === 'cancelled')
77
+ return 'cancelled';
78
+ return state;
79
+ }
80
+ /** Collapsed badge: an always-visible corner pill while any team exists. */
81
+ function CollapsedBadge({ count, busy, onClick }) {
82
+ return (_jsxs("button", { type: "button", className: css.badge, "data-busy": busy, onClick: onClick, "aria-label": `AgentTeams 活动,${count} 个团队`, children: [_jsx("span", { className: css.badgeDot, "data-busy": busy, "aria-hidden": true }), _jsx("span", { className: css.badgeCount, children: count })] }));
83
+ }
84
+ function memberDotState(member, tasks) {
85
+ const owned = tasks.filter((task) => task.assignee === member.name);
86
+ if (member.activity === 'working')
87
+ return 'ongoing';
88
+ if (owned.some((task) => task.status === 'failed'))
89
+ return 'error';
90
+ if (owned.length > 0 && owned.every((task) => task.status === 'completed'))
91
+ return 'done';
92
+ return 'warning';
93
+ }
94
+ function memberStateLabel(member, tasks) {
95
+ const owned = tasks.filter((task) => task.assignee === member.name);
96
+ if (member.activity === 'working')
97
+ return '工作中';
98
+ if (owned.some((task) => task.status === 'failed'))
99
+ return '有失败';
100
+ if (owned.some((task) => task.state === 'blocked'))
101
+ return '等待';
102
+ if (owned.length > 0 && owned.every((task) => task.status === 'completed'))
103
+ return '已交付';
104
+ if (owned.length > 0)
105
+ return '待执行';
106
+ return '待派工';
107
+ }
108
+ function memberStatusText(member, tasks) {
109
+ const owned = tasks.filter((task) => task.assignee === member.name);
110
+ const current = owned.find((task) => task.id === member.currentTask);
111
+ const blocked = owned.find((task) => task.state === 'blocked');
112
+ if (member.activity === 'working' && current !== undefined)
113
+ return `正在执行 ${current.id}`;
114
+ if (member.activity === 'working')
115
+ return '正在处理已派任务';
116
+ if (blocked !== undefined) {
117
+ const dependency = tasks.find((task) => blocked.dependencies.includes(task.id) && task.state !== 'completed');
118
+ if (dependency !== undefined)
119
+ return `等待 ${dependency.id} · ${dependency.assignee || '待认领'}`;
120
+ return '等待前置任务';
121
+ }
122
+ if (member.total === 0)
123
+ return '等待队长派工';
124
+ if (member.done === member.total)
125
+ return '任务已交付';
126
+ return member.activity === 'idle' ? '待继续执行' : '状态未知';
127
+ }
128
+ function dependencyLabel(task, tasks) {
129
+ return task.dependencies.map((id) => {
130
+ const dependency = tasks.find((candidate) => candidate.id === id);
131
+ return dependency?.assignee ? `${id}·${dependency.assignee}` : id;
132
+ }).join('、');
133
+ }
134
+ function TaskNode({ task, tasks, focused, dimmed, pinned, onPin, onPreview }) {
135
+ const tone = taskTone(task.state, task.status);
136
+ return (_jsxs("button", { type: "button", className: css.taskNode, "data-task-id": task.id, "data-state": tone, "data-focused": focused, "data-dimmed": dimmed, "aria-pressed": pinned, title: `${task.id} · ${task.subject}(点击固定依赖链)`, onClick: () => { onPin(task.id); }, onMouseEnter: () => { onPreview(task.id); }, onMouseLeave: () => { onPreview(null); }, onFocus: () => { onPreview(task.id); }, onBlur: () => { onPreview(null); }, children: [_jsxs("span", { className: css.taskNodeHead, children: [_jsx("span", { className: css.taskId, children: task.id }), _jsx("span", { className: css.taskBadge, "data-state": tone, children: taskStatusLabel(task.status) })] }), _jsx("span", { className: css.taskSubject, children: task.subject }), _jsxs("span", { className: css.taskRoute, children: [_jsx("span", { className: css.taskOwner, children: task.assignee || '待认领' }), task.dependencies.length === 0
137
+ ? _jsx("span", { className: css.taskStart, children: "\u8D77\u70B9" })
138
+ : _jsxs("span", { className: css.taskDeps, children: ["\u4F9D\u8D56 ", dependencyLabel(task, tasks)] })] })] }));
139
+ }
140
+ function DependencyMap({ tasks }) {
141
+ const [previewTaskId, setPreviewTaskId] = useState(null);
142
+ const [pinnedTaskId, setPinnedTaskId] = useState(null);
143
+ const focusedTaskId = pinnedTaskId ?? previewTaskId;
144
+ const stages = useMemo(() => taskStages(tasks), [tasks]);
145
+ const related = useMemo(() => focusedTaskId === null ? null : relatedTaskIds(focusedTaskId, tasks), [focusedTaskId, tasks]);
146
+ useEffect(() => {
147
+ const onKeyDown = (event) => {
148
+ if (event.key === 'Escape')
149
+ setPinnedTaskId(null);
150
+ };
151
+ window.addEventListener('keydown', onKeyDown);
152
+ return () => { window.removeEventListener('keydown', onKeyDown); };
153
+ }, []);
154
+ if (tasks.length === 0)
155
+ return null;
156
+ return (_jsxs("section", { className: css.dependencySection, "aria-label": "\u4EFB\u52A1\u4F9D\u8D56\u94FE", "data-dependency-map": true, children: [_jsxs("header", { className: css.sectionHead, children: [_jsxs("span", { className: css.sectionTitle, children: [_jsx(IconBranchOutline16, {}), " \u4EFB\u52A1\u4F9D\u8D56"] }), _jsx("span", { className: css.sectionHint, children: pinnedTaskId === null ? '悬停预览 · 点击固定' : `${pinnedTaskId} 已固定 · Esc 取消` })] }), _jsx("div", { className: css.stageFlow, children: stages.map((stage, index) => (_jsxs("div", { className: css.stageGroup, "data-depth": stage.depth, children: [index > 0 && (_jsxs("span", { className: css.stageConnector, "aria-hidden": true, children: [_jsx("span", { className: css.stageLine }), _jsx(IconChevronRightOutline14, {})] })), _jsxs("div", { className: css.stageColumn, children: [_jsxs("span", { className: css.stageLabel, children: [stage.depth === 0 ? '起点' : `依赖层 ${stage.depth}`, _jsx("span", { children: stage.tasks.length })] }), _jsx("div", { className: css.stageTasks, children: stage.tasks.map((task) => (_jsx(TaskNode, { task: task, tasks: tasks, focused: related?.has(task.id) ?? false, dimmed: related !== null && !related.has(task.id), pinned: pinnedTaskId === task.id, onPin: (id) => { setPinnedTaskId((current) => current === id ? null : id); }, onPreview: setPreviewTaskId }, task.id))) })] })] }, stage.depth))) })] }));
157
+ }
158
+ function TeamSection({ team, onNavigate, historic = false }) {
159
+ const busyCount = team.members.filter((member) => member.activity === 'working').length;
160
+ const assignedCount = team.tasks.filter((task) => task.assignee !== '').length;
161
+ const completedCount = team.tasks.filter((task) => task.status === 'completed').length;
162
+ const allCompleted = team.tasks.length > 0 && completedCount === team.tasks.length;
163
+ const unclaimed = team.tasks.filter((task) => {
164
+ if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled')
165
+ return false;
166
+ if (task.assignee === '')
167
+ return true;
168
+ return !team.members.some((member) => member.name === task.assignee);
169
+ });
170
+ return (_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: "\u5DF2\u7ED3\u675F" }), _jsxs("span", { className: css.teamStats, children: [_jsxs("span", { "data-stat": "members", children: [team.members.length, " \u6210\u5458"] }), _jsxs("span", { "data-stat": "tasks", children: [completedCount, "/", team.tasks.length, " \u5B8C\u6210"] }), _jsxs("span", { "data-stat": "messages", children: [team.messageCount, " \u6D88\u606F"] })] })] }), _jsxs("section", { className: css.delegationSection, "aria-label": "\u961F\u957F\u6D3E\u5DE5\u5173\u7CFB", "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: "\u961F\u957F" }), _jsx("span", { className: css.captainRole, children: "\u62C6\u89E3 \u00B7 \u6D3E\u53D1 \u00B7 \u6C47\u603B" })] }), _jsxs("span", { className: css.captainSummary, children: ["\u5DF2\u6D3E\u53D1 ", assignedCount, " \u9879\u4EFB\u52A1\u7ED9 ", team.members.length, " \u540D\u6210\u5458"] })] }), _jsxs("span", { className: css.captainState, "data-busy": busyCount > 0, children: [_jsx(StateDot, { state: busyCount > 0 ? 'ongoing' : allCompleted ? 'done' : 'warning' }), busyCount > 0 ? `${busyCount} 人执行中` : allCompleted ? '已收齐' : '等待回报'] })] }), _jsxs("div", { className: css.delegationTree, children: [team.members.length === 0 && _jsx("span", { className: css.emptyHint, children: "\u6682\u65E0\u6210\u5458\uFF0C\u7B49\u5F85\u961F\u957F\u7EC4\u5EFA\u56E2\u961F" }), team.members.map((member) => {
171
+ const owned = team.tasks.filter((task) => task.assignee === member.name);
172
+ 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: () => { if (member.id !== '')
173
+ onNavigate(member.id); }, 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 }), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(StateDot, { state: memberDotState(member, team.tasks) }), memberStateLabel(member, team.tasks)] })] }), _jsx("span", { className: css.memberStatusLine, children: memberStatusText(member, team.tasks) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children: "\u961F\u957F\u6D3E\u53D1" }), _jsx("span", { className: css.assignmentTasks, children: owned.length === 0
174
+ ? _jsx("span", { className: css.taskEmpty, children: "\u6682\u65E0\u4EFB\u52A1" })
175
+ : owned.map((task) => (_jsx("span", { className: css.assignmentChip, "data-state": taskTone(task.state, task.status), title: task.subject, children: task.id }, task.id))) }), member.unread > 0 && _jsxs("span", { className: css.unreadPill, children: [member.unread, " \u6761\u6D88\u606F"] })] })] }, member.id));
176
+ })] })] }), _jsx(DependencyMap, { tasks: team.tasks }), unclaimed.length > 0 && (_jsxs("section", { className: css.unclaimed, "aria-label": "\u5F85\u8BA4\u9886\u4EFB\u52A1", children: [_jsx("span", { className: css.unclaimedTitle, children: "\u5F85\u961F\u957F\u8BA4\u9886\u6216\u6539\u6D3E" }), _jsx("span", { className: css.assignmentTasks, children: unclaimed.map((task) => (_jsxs("span", { className: css.assignmentChip, "data-state": taskTone(task.state, task.status), title: task.subject, children: [task.id, " \u00B7 ", task.assignee || '未分配'] }, task.id))) })] })), team.captainInbox.length > 0 && (_jsxs("section", { className: css.inbox, "aria-label": "\u6210\u5458\u56DE\u62A5\u961F\u957F", children: [_jsxs("header", { className: css.sectionHead, children: [_jsx("span", { className: css.sectionTitle, children: "\u6210\u5458\u56DE\u62A5" }), _jsx("span", { className: css.sectionHint, children: "\u6D41\u5411\u961F\u957F" })] }), team.captainInbox.slice(-2).map((message, index) => (_jsxs("div", { className: css.inboxRow, children: [_jsxs("span", { className: css.inboxRoute, children: [message.from, _jsx(IconChevronRightOutline14, {}), "\u961F\u957F"] }), _jsx("span", { className: css.inboxContent, title: message.content, children: message.content })] }, index)))] }))] }));
177
+ }
178
+ /** The top-right activity floater. Teams follow the current session: live
179
+ * snapshots and historic card summaries are only shown while their captain
180
+ * session is the one currently open. */
181
+ export function ActivityPanel({ sessionsList, openSession }) {
182
+ // Navigating to a member's subagent transcript is an explicit departure:
183
+ // hide the floater immediately instead of waiting out the autocollapse
184
+ // grace, so the panel never lingers over the member session.
185
+ const navigateToSession = (id) => {
186
+ setOpen(false);
187
+ setWasActive(false);
188
+ openSession(id);
189
+ };
190
+ const [teams, setTeams] = useState([]);
191
+ const [archivedTeams, setArchivedTeams] = useState([]);
192
+ const [open, setOpen] = useState(false);
193
+ const [openOwner, setOpenOwner] = useState();
194
+ const [autoOpened, setAutoOpened] = useState(false);
195
+ const [wasActive, setWasActive] = useState(false);
196
+ const [historic, setHistoric] = useState(new Map());
197
+ const current = useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot).current;
198
+ const currentRef = useRef(current);
199
+ useEffect(() => { currentRef.current = current; }, [current]);
200
+ const mountedAtRef = useRef(performance.now());
201
+ const expanded = activityPanelExpandedForSession(open, openOwner, current);
202
+ // This portal survives conversation route changes. Gate expansion by its
203
+ // owning session during render, then clear stale state before paint. This
204
+ // removes the old panel immediately instead of waiting for the no-team
205
+ // autoclose grace period on the destination page.
206
+ useLayoutEffect(() => {
207
+ if (openOwner === undefined || openOwner === current)
208
+ return;
209
+ setOpen(false);
210
+ setOpenOwner(undefined);
211
+ setWasActive(false);
212
+ setAutoOpened(false);
213
+ }, [current, openOwner]);
214
+ // The activity panel is a body portal, so announce its open state on body.
215
+ // CSS can then make the conversation column yield space without knowing the
216
+ // host shell's hashed module class names. Narrow viewports keep overlay mode.
217
+ useLayoutEffect(() => {
218
+ const root = document.documentElement;
219
+ if (expanded)
220
+ root.setAttribute(PANEL_OPEN_ATTRIBUTE, '');
221
+ else
222
+ root.removeAttribute(PANEL_OPEN_ATTRIBUTE);
223
+ return () => { root.removeAttribute(PANEL_OPEN_ATTRIBUTE); };
224
+ }, [expanded]);
225
+ useEffect(() => {
226
+ let cancelled = false;
227
+ let inFlight = false;
228
+ const tick = async () => {
229
+ if (inFlight || cancelled)
230
+ return;
231
+ inFlight = true;
232
+ try {
233
+ const [liveResponse, archivedResponse] = await Promise.all([
234
+ fetch(STATE_URL, { cache: 'no-store' }),
235
+ fetch(`${STATE_URL}?archived=1`, { cache: 'no-store' }),
236
+ ]);
237
+ if (liveResponse.ok) {
238
+ const body = (await liveResponse.json());
239
+ if (!cancelled && Array.isArray(body.teams))
240
+ setTeams(body.teams);
241
+ }
242
+ if (archivedResponse.ok) {
243
+ const body = (await archivedResponse.json());
244
+ if (!cancelled && Array.isArray(body.teams))
245
+ setArchivedTeams(body.teams);
246
+ }
247
+ }
248
+ catch {
249
+ // Host restarting; keep the last snapshot.
250
+ }
251
+ finally {
252
+ inFlight = false;
253
+ }
254
+ };
255
+ void tick();
256
+ const timer = setInterval(() => { void tick(); }, POLL_MS);
257
+ return () => {
258
+ cancelled = true;
259
+ clearInterval(timer);
260
+ };
261
+ }, []);
262
+ useEffect(() => {
263
+ const onOpenPanel = (event) => {
264
+ const activeSession = currentRef.current;
265
+ if (activeSession === undefined)
266
+ return;
267
+ setOpenOwner(activeSession);
268
+ setOpen(true);
269
+ const detail = event.detail;
270
+ if (detail?.teamId !== undefined) {
271
+ // A card from a log that predates captainSessionId belongs to the
272
+ // session that activated it (the current one at injection time).
273
+ const owner = detail.captainSessionId !== '' ? detail.captainSessionId : currentRef.current ?? '';
274
+ const teamKey = `${owner}:${detail.teamId}`;
275
+ setHistoric((previous) => {
276
+ const next = new Map(previous);
277
+ next.set(teamKey, { data: detail, owner });
278
+ return next;
279
+ });
280
+ }
281
+ };
282
+ window.addEventListener(OPEN_PANEL_EVENT, onOpenPanel);
283
+ return () => {
284
+ window.removeEventListener(OPEN_PANEL_EVENT, onOpenPanel);
285
+ };
286
+ }, []);
287
+ // Teams follow the current session: live snapshots and historic card
288
+ // summaries are visible only while their captain session is current.
289
+ const visibleTeams = useMemo(
290
+ // No current session (initial load): show nothing until one is picked,
291
+ // so cross-session teams never leak into the floater.
292
+ () => (current === undefined ? [] : teams.filter((team) => team.captainSessionId === current)), [teams, current]);
293
+ const visibleHistoric = useMemo(() => (current === undefined ? [] : [...historic.values()].filter(({ data, owner }) => owner === current && !teams.some((live) => live.captainSessionId === current && live.teamId === data.teamId) && !archivedTeams.some((archived) => archived.captainSessionId === current && archived.teamId === data.teamId))), [historic, current, teams, archivedTeams]);
294
+ const visibleArchived = useMemo(() => (current === undefined ? [] : archivedTeams.filter((team) => team.captainSessionId === current && !teams.some((live) => live.captainSessionId === current && live.teamId === team.teamId))), [archivedTeams, current, teams]);
295
+ const visibleCount = visibleTeams.length + visibleArchived.length + visibleHistoric.length;
296
+ useEffect(() => {
297
+ if (visibleCount > 0) {
298
+ setWasActive(true);
299
+ // Auto-expand only after the page-settle window: opening (and its
300
+ // main-column yield) right after load reads as a whole-page flicker.
301
+ const settled = performance.now() - mountedAtRef.current >= AUTO_OPEN_SETTLE_MS;
302
+ if (!autoOpened && settled) {
303
+ setOpenOwner(current);
304
+ setOpen(true);
305
+ setAutoOpened(true);
306
+ }
307
+ return;
308
+ }
309
+ if (!wasActive)
310
+ return;
311
+ const timer = setTimeout(() => {
312
+ setOpen(false);
313
+ setOpenOwner(undefined);
314
+ setWasActive(false);
315
+ // Re-arm auto-expand: a later activity (new team, new session) may
316
+ // open the panel on its own again.
317
+ setAutoOpened(false);
318
+ }, AUTOCLOSE_GRACE_MS);
319
+ return () => { clearTimeout(timer); };
320
+ }, [visibleCount, autoOpened, wasActive]);
321
+ const busy = useMemo(() => visibleTeams.some((team) => team.members.some((member) => member.activity === 'working')), [visibleTeams]);
322
+ const hasTeams = visibleCount > 0;
323
+ if (!hasTeams && !expanded)
324
+ return null;
325
+ return (_jsxs(_Fragment, { children: [!expanded && (_jsx(CollapsedBadge, { count: visibleCount, busy: busy, onClick: () => {
326
+ if (current === undefined)
327
+ return;
328
+ setOpenOwner(current);
329
+ setOpen(true);
330
+ } })), expanded && (_jsxs("aside", { className: css.panel, "data-agent-teams-activity": true, children: [_jsxs("header", { className: css.panelHead, children: [_jsxs("span", { className: css.panelTitle, children: ["AgentTeams \u6D3B\u52A8", _jsx("span", { className: css.panelDot, "data-busy": busy, "aria-hidden": true })] }), _jsx("button", { type: "button", className: css.closeButton, onClick: () => {
331
+ setOpen(false);
332
+ setOpenOwner(undefined);
333
+ }, "aria-label": "\u5173\u95ED", children: _jsx(IconCloseOutline16, {}) })] }), _jsx("div", { className: css.teams, children: visibleCount === 0
334
+ ? _jsx("span", { className: css.emptyHint, children: "\u6682\u65E0\u56E2\u961F\u6D3B\u52A8" })
335
+ : (_jsxs(_Fragment, { children: [visibleTeams.map((team) => (_jsx(TeamSection, { team: team, onNavigate: navigateToSession }, team.teamId))), visibleArchived.map((team) => (_jsx("div", { "data-team-id": team.teamId, "data-historic": true, className: css.archivedWrap, children: _jsx(TeamSection, { team: team, onNavigate: navigateToSession, historic: true }) }, `${team.captainSessionId}:${team.teamId}`))), visibleHistoric.map(({ data: team, owner }) => {
336
+ const teamKey = `${owner}:${team.teamId}`;
337
+ return (_jsxs("section", { className: css.team, "data-team-id": team.teamId, "data-historic": true, children: [_jsxs("header", { className: css.teamHead, children: [_jsxs("span", { className: css.teamName, title: team.teamName, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), " ", team.teamName] }), _jsx("span", { className: css.historicPill, children: "\u5DF2\u7ED3\u675F" })] }), _jsx("div", { className: css.members, children: team.members.map((member) => (_jsxs("button", { type: "button", className: css.memberRow, "data-activity": "idle", onClick: () => { if (member.id !== '')
338
+ navigateToSession(member.id); }, children: [_jsx("span", { className: css.memberAvatar, 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("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 })] }) })] }, member.id))) })] }, teamKey));
339
+ })] })) })] }))] }));
340
+ }
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * AgentTeams conversation card: the lightweight in-conversation summary for
4
+ * one team — the captain's whale avatar and name, the member roster as
5
+ * clickable whale avatars (opening the member's subagent transcript), and
6
+ * an "activity panel" button that re-activates the top-right floater.
7
+ *
8
+ * The floater and this card share the `agent-teams:open-panel` window event
9
+ * so the card can summon the panel even after it was closed (or when an old
10
+ * session is re-opened for review).
11
+ * @module dsh-agent-teams/client/card
12
+ */
13
+ import { useEffect, useMemo, useState } from 'react';
14
+ import { LEAD_ART, memberArtUrl } from "./artwork.js";
15
+ import css from './AgentTeamsCard.module.css';
16
+ /** Window event name the floater listens for to open itself. */
17
+ export const OPEN_PANEL_EVENT = 'agent-teams:open-panel';
18
+ /** Re-activate the top-right activity panel, carrying this team's summary
19
+ * so the panel can show it even when the team no longer exists on disk
20
+ * (historical session review). */
21
+ function openActivityPanel(data) {
22
+ window.dispatchEvent(new CustomEvent(OPEN_PANEL_EVENT, {
23
+ detail: {
24
+ teamId: data.teamId,
25
+ captainSessionId: data.captainSessionId,
26
+ teamName: data.teamName,
27
+ members: data.members,
28
+ },
29
+ }));
30
+ }
31
+ /** Render one durable team as a compact conversation card. */
32
+ export function AgentTeamsCard({ node, openSession, currentSessionId }) {
33
+ const data = node.data;
34
+ const owner = data.captainSessionId || currentSessionId() || '';
35
+ const [snapshot, setSnapshot] = useState();
36
+ useEffect(() => {
37
+ let cancelled = false;
38
+ const tick = async () => {
39
+ for (const url of ['/plugins/dsh-agent-teams/state', '/plugins/dsh-agent-teams/state?archived=1']) {
40
+ try {
41
+ const response = await fetch(url, { cache: 'no-store' });
42
+ if (!response.ok)
43
+ continue;
44
+ const body = (await response.json());
45
+ const found = Array.isArray(body.teams)
46
+ ? body.teams.find((team) => team.teamId === data.teamId && (owner === '' || team.captainSessionId === owner))
47
+ : undefined;
48
+ if (found !== undefined) {
49
+ if (!cancelled)
50
+ setSnapshot(found);
51
+ return;
52
+ }
53
+ }
54
+ catch {
55
+ // Host restarting; retry on the next poll.
56
+ }
57
+ }
58
+ };
59
+ void tick();
60
+ const timer = setInterval(() => { void tick(); }, 1500);
61
+ return () => {
62
+ cancelled = true;
63
+ clearInterval(timer);
64
+ };
65
+ }, [data.teamId, owner]);
66
+ const resolved = useMemo(() => ({
67
+ ...data,
68
+ captainSessionId: snapshot?.captainSessionId ?? owner,
69
+ teamName: snapshot?.name ?? data.teamName,
70
+ members: snapshot?.members.map((member) => ({ id: member.id, name: member.name, role: member.role })) ?? data.members,
71
+ }), [data, owner, snapshot]);
72
+ 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 }), _jsxs("span", { className: css.memberCount, children: [resolved.members.length, " \u540D\u6210\u5458"] }), _jsx("button", { type: "button", className: css.panelButton, onClick: () => { openActivityPanel(resolved); }, "aria-label": "\u6253\u5F00\u6D3B\u52A8\u9762\u677F", title: "\u6253\u5F00\u6D3B\u52A8\u9762\u677F", children: "\u6D3B\u52A8\u9762\u677F" })] }), resolved.members.length > 0 && (_jsx("div", { className: css.members, children: resolved.members.map((member) => (_jsxs("button", { type: "button", className: css.member, onClick: () => { if (member.id !== '')
73
+ openSession(member.id); }, 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))) }))] }));
74
+ }