@nanmicoder/dsh-agent-teams 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/README_ZH.md +8 -7
- package/lib/client/ActivityPanel.js +93 -38
- package/lib/client/activity-model.js +63 -0
- package/lib/client.js +478 -367
- package/lib/client.js.map +1 -1
- package/lib/index.js +7 -5
- package/lib/members.js +101 -18
- package/lib/scheduler.js +212 -0
- package/lib/snapshot.js +32 -19
- package/lib/state.js +275 -5
- package/lib/tools.js +303 -31
- package/lib/types/client/ActivityPanel.d.ts +1 -0
- package/lib/types/client/activity-model.d.ts +34 -0
- package/lib/types/event-types.d.ts +2 -0
- package/lib/types/members.d.ts +25 -8
- package/lib/types/scheduler.d.ts +23 -0
- package/lib/types/snapshot.d.ts +10 -2
- package/lib/types/state.d.ts +53 -0
- package/lib/types/types.d.ts +16 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
`dsh-agent-teams` turns the current DeepSeek Harness session into a captain that can assemble durable sub-agents, split a goal into dependency-aware tasks, and coordinate work through direct messages.
|
|
18
18
|
|
|
19
|
-
Ask in natural language. The plugin provides the team protocol,
|
|
19
|
+
Ask in natural language. The plugin provides the team protocol, ten coordination tools, persistent state, an automatic shared-task scheduler, and a live Web UI—without requiring a separate workflow engine.
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
22
|
<img src="./assets/ui.png" width="100%" alt="DeepSeek Harness conversation with the AgentTeams live activity panel, members, tasks, dependencies, and reports">
|
|
@@ -29,8 +29,9 @@ Ask in natural language. The plugin provides the team protocol, nine coordinatio
|
|
|
29
29
|
| **Captain-led delegation** | The current session creates the team, assigns roles, and consolidates the final result. |
|
|
30
30
|
| **Durable members** | Members are continuable DSH sub-agents that can be woken for focused follow-up turns. |
|
|
31
31
|
| **Dependency-aware tasks** | Tasks move through explicit states and cannot be claimed before their dependencies finish. |
|
|
32
|
+
| **Automatic reuse and safe takeover** | Idle members claim the next ready task; reassignment revokes stale attempts before new work starts, and cold recovery retries stranded open attempts. |
|
|
32
33
|
| **Direct messaging** | Members send durable mailbox messages directly to teammates or the captain—no relay required. |
|
|
33
|
-
| **Live activity panel** | The Web UI
|
|
34
|
+
| **Live activity panel** | The Web UI combines segmented progress, a collapsible roster, and an interactive task DAG; completed archives retain their full member and task history. |
|
|
34
35
|
|
|
35
36
|
## Install
|
|
36
37
|
|
|
@@ -71,13 +72,13 @@ Then ask for a team directly:
|
|
|
71
72
|
1. The current session creates a team and becomes its captain.
|
|
72
73
|
2. The captain adds role-specific members backed by continuable sub-agents.
|
|
73
74
|
3. The goal becomes tasks with owners and explicit dependencies.
|
|
74
|
-
4.
|
|
75
|
-
5. Members
|
|
75
|
+
4. The shared scheduler uses real `running / idle / ready` state to atomically claim one ready task per idle member and wake it. If an idle/ready member still owns an open task after an interrupted turn or process restart, the scheduler retries it with a fresh attempt.
|
|
76
|
+
5. Members update with the current `attempt_id`; reassignment or captain takeover revokes the old attempt and waits for the old worker to quiesce before a new attempt starts.
|
|
76
77
|
6. The captain presents the combined result, then archives the complete team record.
|
|
77
78
|
|
|
78
79
|
Team state is stored under `<workspace>/.agent-teams/`; the Web panel reads that disk truth and combines it with live sub-agent activity.
|
|
79
80
|
|
|
80
|
-
Member creation is zero-interaction by default:
|
|
81
|
+
Member creation is zero-interaction by default: a member on the captain's current LLM route snapshots that provider, model, and reasoning effort, while a member on a requested alternative route snapshots the target model's default effort; later continuations restore the resolved snapshot. Only an explicit heterogeneous-team request (for example, “backend on provider A/model X, frontend on provider B/model Y”) supplies a member-specific `provider` + `model`; there is no per-member model or reasoning prompt.
|
|
81
82
|
|
|
82
83
|
## Configuration
|
|
83
84
|
|
|
@@ -93,12 +94,12 @@ Defaults work without extra setup. A trusted profile can override member behavio
|
|
|
93
94
|
maxMembers: 8
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
`memberProvider` is the sub-agent runtime backend (`spawn` / `fork`), not an LLM provider. Cross-LLM-provider routing uses the optional `provider` + `model` fields of `agent_teams_add_member`; `memberModel` is only a model default for all members.
|
|
97
|
+
`memberProvider` is the sub-agent runtime backend (`spawn` / `fork`), not an LLM provider. Cross-LLM-provider routing uses the optional `provider` + `model` fields of `agent_teams_add_member`; `memberModel` is only a model default for all members. A member on the captain's current provider/model inherits the captain's reasoning effort, while a changed provider or model automatically uses the target model's default. To request a particular effort, pass the optional `reasoning_effort` field — one of the target model's supported effort ids, or `"default"` to force the model's own default.
|
|
97
98
|
|
|
98
99
|
## Boundaries
|
|
99
100
|
|
|
100
101
|
- One captain leads one active team at a time.
|
|
101
|
-
-
|
|
102
|
+
- Idle members are automatically reused for ready work; messages that cannot be delivered live remain durable and are retried at a later status boundary.
|
|
102
103
|
- State is file-backed and serialized within one DSH process; concurrent processes editing the same team are not coordinated.
|
|
103
104
|
- The activity panel reports persisted state as-is. Models may occasionally finish work without performing the expected task-state update.
|
|
104
105
|
|
package/README_ZH.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
`dsh-agent-teams` 让当前 DeepSeek Harness 会话成为队长:创建可续聊的子 Agent、把目标拆成有依赖的任务,并通过直达消息协调成员工作。
|
|
18
18
|
|
|
19
|
-
你只需用自然语言提出目标。插件会提供团队协议、
|
|
19
|
+
你只需用自然语言提出目标。插件会提供团队协议、10 个协作工具、持久化状态、自动共享任务调度和实时 Web UI,不需要额外的 Workflow 引擎。
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
22
|
<img src="./assets/ui.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
| **队长式委派** | 当前会话负责建队、分配角色并汇总最终结果。 |
|
|
30
30
|
| **可续聊成员** | 成员是可持续唤醒的 DSH 子 Agent,可以继续执行聚焦的后续轮次。 |
|
|
31
31
|
| **带依赖的任务** | 任务有明确状态;依赖未完成时不能领取。 |
|
|
32
|
+
| **自动续领与安全接管** | 成员空闲后自动领取下一项就绪任务;转派会撤销旧 attempt,冷恢复会重试遗留任务,迟到结果无法覆盖。 |
|
|
32
33
|
| **成员直达消息** | 成员通过持久化邮箱直接联系队友或队长,不需要队长中转。 |
|
|
33
|
-
| **实时活动面板** | Web UI
|
|
34
|
+
| **实时活动面板** | Web UI 用分段进度、可折叠成员树和可交互 DAG 展示实时工作;团队结束后仍保留完整成员与任务历史。 |
|
|
34
35
|
|
|
35
36
|
## 安装
|
|
36
37
|
|
|
@@ -71,13 +72,13 @@ dsh web
|
|
|
71
72
|
1. 当前会话创建团队并成为队长。
|
|
72
73
|
2. 队长按角色添加由可续聊子 Agent 驱动的成员。
|
|
73
74
|
3. 目标被拆成有负责人和显式依赖的任务。
|
|
74
|
-
4.
|
|
75
|
-
5.
|
|
75
|
+
4. 共享调度器依据真实 `running / idle / ready` 状态,为每个空闲成员原子领取一项就绪任务并唤醒它;成员在中断或进程重启后仍持有开放任务时,会以新 attempt 自动恢复执行。
|
|
76
|
+
5. 成员携带当前 `attempt_id` 更新任务;转派或队长接管会先撤销旧 attempt、等待原成员安静,再启动新 attempt。
|
|
76
77
|
6. 队长汇总结果,随后归档完整团队记录。
|
|
77
78
|
|
|
78
79
|
团队状态保存在 `<workspace>/.agent-teams/`;Web 面板读取这份磁盘真相,并与实时子 Agent 活动合并展示。
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
成员创建默认零交互:成员沿用队长当前 LLM 路由时会快照该 provider、model 与思考强度;用户要求改用其他路由时,则快照目标模型的默认强度,成员后续续跑仍使用最终解析出的快照。只有当用户明确提出异构分工(例如“后端用 provider A/model X,前端用 provider B/model Y”)时,队长才会把对应的 `provider` + `model` 传给该成员;不会逐个弹出模型或思考强度选择。
|
|
81
82
|
|
|
82
83
|
## 配置
|
|
83
84
|
|
|
@@ -93,12 +94,12 @@ dsh web
|
|
|
93
94
|
maxMembers: 8
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
这里的 `memberProvider` 指子 Agent 的运行后端(`spawn` / `fork`),不是 LLM provider。跨 LLM provider 由 `agent_teams_add_member` 的可选 `provider` + `model` 参数表达;`memberModel`
|
|
97
|
+
这里的 `memberProvider` 指子 Agent 的运行后端(`spawn` / `fork`),不是 LLM provider。跨 LLM provider 由 `agent_teams_add_member` 的可选 `provider` + `model` 参数表达;`memberModel` 只是所有成员的模型默认覆盖。成员沿用队长当前 provider/model 时会继承队长的思考强度;provider 或 model 任一改变时会自动使用目标模型的默认档。需要指定特定强度时,可传入可选的 `reasoning_effort` 参数(目标模型支持的档位 id,或 `"default"` 表示强制使用模型自身默认档)。
|
|
97
98
|
|
|
98
99
|
## 使用边界
|
|
99
100
|
|
|
100
101
|
- 一个队长同一时间只能带一个活动团队。
|
|
101
|
-
-
|
|
102
|
+
- 成员空闲后由共享调度器自动续领就绪任务;中断/冷重启遗留的开放任务会生成新 attempt 并重新唤醒原成员;暂时无法实时投递的消息会持久保存在邮箱中并在后续状态边界重投。
|
|
102
103
|
- 状态使用文件持久化,并在单个 DSH 进程内串行操作;多个进程同时修改同一团队不保证一致。
|
|
103
104
|
- 活动面板如实展示持久化状态;模型偶尔可能完成工作却没有按协议更新任务状态。
|
|
104
105
|
|
|
@@ -16,8 +16,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
16
16
|
* @module dsh-agent-teams/client/activity
|
|
17
17
|
*/
|
|
18
18
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
19
|
-
import { IconBranchOutline16,
|
|
20
|
-
import { activityPanelExpandedForSession, dependencyFocusTaskId, relatedTaskIds,
|
|
19
|
+
import { IconBranchOutline16, IconCloseOutline16, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
20
|
+
import { activityPanelExpandedForSession, compactDagLayout, COMPACT_DAG_NODE_HEIGHT, COMPACT_DAG_NODE_WIDTH, dependencyFocusTaskId, relatedTaskIds, usesParallelTaskGrid, } from "./activity-model.js";
|
|
21
21
|
import { ACTION_ART, LEAD_ART, memberArtUrl } from "./artwork.js";
|
|
22
22
|
import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
|
|
23
23
|
import css from './ActivityPanel.module.css';
|
|
@@ -77,21 +77,17 @@ function taskTone(state, status) {
|
|
|
77
77
|
return 'cancelled';
|
|
78
78
|
return state;
|
|
79
79
|
}
|
|
80
|
+
function Chevron({ open }) {
|
|
81
|
+
return (_jsx("svg", { className: css.chevron, "data-open": open, width: "9", height: "9", viewBox: "0 0 10 10", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", "aria-hidden": true, children: _jsx("path", { d: "M3.5 2l3 3-3 3" }) }));
|
|
82
|
+
}
|
|
83
|
+
function WorkGlyph({ active }) {
|
|
84
|
+
return (_jsx("svg", { className: css.workGlyph, "data-active": active, width: "11", height: "11", viewBox: "0 0 11 11", fill: "currentColor", "aria-hidden": true, children: [[0, 0], [4.2, 0], [8.4, 0], [0, 4.2], [4.2, 4.2], [8.4, 4.2]].map(([x, y], index) => (_jsx("rect", { x: x, y: y, width: "2.6", height: "2.6", rx: ".6", style: { animationDelay: `${index * 0.15}s` } }, `${x}:${y}`))) }));
|
|
85
|
+
}
|
|
80
86
|
/** Collapsed badge: an always-visible corner pill while any team exists. */
|
|
81
87
|
function CollapsedBadge({ count, busy, onClick }) {
|
|
82
88
|
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
89
|
}
|
|
84
|
-
function
|
|
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) {
|
|
90
|
+
function memberStateLabel(member, tasks, historic) {
|
|
95
91
|
const owned = tasks.filter((task) => task.assignee === member.name);
|
|
96
92
|
if (member.activity === 'working')
|
|
97
93
|
return '工作中';
|
|
@@ -101,6 +97,8 @@ function memberStateLabel(member, tasks) {
|
|
|
101
97
|
return '等待';
|
|
102
98
|
if (owned.length > 0 && owned.every((task) => task.status === 'completed'))
|
|
103
99
|
return '已交付';
|
|
100
|
+
if (member.status === 'removed')
|
|
101
|
+
return historic ? '已离队' : '已移除';
|
|
104
102
|
if (owned.length > 0)
|
|
105
103
|
return '待执行';
|
|
106
104
|
return '待派工';
|
|
@@ -125,25 +123,47 @@ function memberStatusText(member, tasks) {
|
|
|
125
123
|
return '任务已交付';
|
|
126
124
|
return member.activity === 'idle' ? '待继续执行' : '状态未知';
|
|
127
125
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
function compactTaskLabel(subject) {
|
|
127
|
+
const withoutVerb = subject.replace(/^开发\s*/u, '').replace(/^\d+[-_.、\s]*/u, '');
|
|
128
|
+
const head = withoutVerb.split(/[((·::]/u)[0]?.trim() ?? withoutVerb;
|
|
129
|
+
return head.length > 18 ? `${head.slice(0, 17)}…` : head;
|
|
130
|
+
}
|
|
131
|
+
function taskSummary(team) {
|
|
132
|
+
const completed = team.tasks.filter((task) => task.status === 'completed');
|
|
133
|
+
const running = team.tasks.filter((task) => task.state === 'running');
|
|
134
|
+
const blocked = team.tasks.filter((task) => task.state === 'blocked');
|
|
135
|
+
const ready = team.tasks.filter((task) => task.state === 'open' && task.status !== 'completed');
|
|
136
|
+
if (team.tasks.length === 0)
|
|
137
|
+
return '等待队长拆解任务';
|
|
138
|
+
if (completed.length === team.tasks.length)
|
|
139
|
+
return `全部 ${completed.length} 项任务已交付`;
|
|
140
|
+
if (blocked.length > 0 && running.length > 0) {
|
|
141
|
+
return `${blocked.slice(0, 3).map((task) => task.id).join('、')}${blocked.length > 3 ? ` 等 ${blocked.length} 项` : ''} 等待前置,其余已开工`;
|
|
142
|
+
}
|
|
143
|
+
if (running.length > 0)
|
|
144
|
+
return `${running.map((task) => task.id).join('、')} 正在执行`;
|
|
145
|
+
if (ready.length > 0)
|
|
146
|
+
return `${ready.map((task) => task.id).join('、')} 已就绪待开工`;
|
|
147
|
+
if (blocked.length > 0)
|
|
148
|
+
return `${blocked.map((task) => task.id).join('、')} 等待前置`;
|
|
149
|
+
return '等待下一轮调度';
|
|
133
150
|
}
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
function ProgressOverview({ team }) {
|
|
152
|
+
const running = team.tasks.filter((task) => task.state === 'running').length;
|
|
153
|
+
const blocked = team.tasks.filter((task) => task.state === 'blocked').length;
|
|
154
|
+
const completed = team.tasks.filter((task) => task.status === 'completed').length;
|
|
155
|
+
const summaryTone = blocked > 0 ? 'warning' : completed === team.tasks.length && team.tasks.length > 0 ? 'completed' : 'running';
|
|
156
|
+
return (_jsxs("section", { className: css.progressOverview, "aria-label": "\u56E2\u961F\u603B\u8FDB\u5EA6", "data-progress-summary": true, children: [_jsx("span", { className: css.progressTitle, children: "\u603B\u8FDB\u5EA6" }), team.tasks.length > 0 ? (_jsx("span", { className: css.progressSegments, "aria-hidden": true, children: team.tasks.map((task) => _jsx("span", { "data-state": taskTone(task.state, task.status) }, task.id)) })) : _jsx("span", { className: css.progressEmpty }), _jsxs("span", { className: css.progressLegend, children: [_jsxs("span", { "data-state": "running", children: ["\u25A0 \u8FDB\u884C\u4E2D ", running] }), _jsxs("span", { "data-state": "blocked", children: ["\u25A0 \u7B49\u5F85\u4F9D\u8D56 ", blocked] }), _jsxs("span", { "data-state": "completed", children: ["\u25A0 \u5DF2\u4EA4\u4ED8 ", completed] })] }), _jsxs("span", { className: css.progressSummary, "data-state": summaryTone, children: [_jsx("span", { className: css.progressSummaryDot }), _jsx("span", { children: taskSummary(team) })] })] }));
|
|
139
157
|
}
|
|
140
158
|
function DependencyMap({ tasks }) {
|
|
159
|
+
const [open, setOpen] = useState(true);
|
|
141
160
|
const [hoverTaskId, setHoverTaskId] = useState(null);
|
|
142
161
|
const [keyboardTaskId, setKeyboardTaskId] = useState(null);
|
|
143
162
|
const [pinnedTaskId, setPinnedTaskId] = useState(null);
|
|
144
163
|
const hoverTimer = useRef(null);
|
|
145
164
|
const focusedTaskId = dependencyFocusTaskId(pinnedTaskId, keyboardTaskId, hoverTaskId);
|
|
146
|
-
const
|
|
165
|
+
const layout = useMemo(() => compactDagLayout(tasks), [tasks]);
|
|
166
|
+
const parallel = useMemo(() => usesParallelTaskGrid(tasks), [tasks]);
|
|
147
167
|
const related = useMemo(() => focusedTaskId === null ? null : relatedTaskIds(focusedTaskId, tasks), [focusedTaskId, tasks]);
|
|
148
168
|
const scheduleHover = (id) => {
|
|
149
169
|
if (hoverTimer.current !== null) {
|
|
@@ -173,27 +193,63 @@ function DependencyMap({ tasks }) {
|
|
|
173
193
|
}, []);
|
|
174
194
|
if (tasks.length === 0)
|
|
175
195
|
return null;
|
|
176
|
-
|
|
196
|
+
const fallbackTask = tasks.find((task) => task.state === 'blocked')
|
|
197
|
+
?? tasks.find((task) => task.state === 'running')
|
|
198
|
+
?? tasks[0];
|
|
199
|
+
const detailTask = tasks.find((task) => task.id === focusedTaskId) ?? fallbackTask;
|
|
200
|
+
const waitingOn = detailTask.dependencies.filter((dependency) => (tasks.find((task) => task.id === dependency)?.status !== 'completed'));
|
|
201
|
+
const dependents = tasks.filter((task) => task.dependencies.includes(detailTask.id));
|
|
202
|
+
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("button", { type: "button", className: css.sectionToggleTitle, onClick: () => { setOpen((current) => !current); }, "aria-expanded": open, children: [_jsx(Chevron, { open: open }), _jsx(IconBranchOutline16, {}), " ", parallel ? '并行任务' : '任务依赖'] }), _jsx("span", { className: css.sectionHint, children: pinnedTaskId === null
|
|
203
|
+
? parallel ? '无前后依赖 · 点击查看详情' : '悬停高亮依赖链 · 点击固定'
|
|
204
|
+
: `${pinnedTaskId} 已固定 · Esc 取消` })] }), 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) => {
|
|
205
|
+
const active = related !== null && related.has(edge.from) && related.has(edge.to);
|
|
206
|
+
return _jsx("path", { d: edge.path, "data-active": active, "data-dimmed": related !== null && !active }, `${edge.from}:${edge.to}`);
|
|
207
|
+
}) }), layout.nodes.map(({ task, x, y }) => (_jsxs("button", { type: "button", className: css.dagNode, style: parallel
|
|
208
|
+
? { height: COMPACT_DAG_NODE_HEIGHT }
|
|
209
|
+
: { left: x, top: y, width: COMPACT_DAG_NODE_WIDTH, height: COMPACT_DAG_NODE_HEIGHT }, "data-task-id": task.id, "data-state": taskTone(task.state, task.status), "data-focused": related?.has(task.id) ?? false, "data-dimmed": related !== null && !related.has(task.id), "aria-pressed": pinnedTaskId === task.id, title: `${task.id} · ${task.subject}`, 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: compactTaskLabel(task.subject) }), task.state === 'running' && (_jsx("span", { className: css.dagRunningState, "aria-label": "\u8FD0\u884C\u4E2D", children: _jsx(WorkGlyph, { active: true }) }))] }, task.id)))] }) }), _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": taskTone(detailTask.state, detailTask.status), children: taskStatusLabel(detailTask.status) })] }), _jsxs("span", { className: css.taskDetailLine, children: [detailTask.assignee || '待认领', " \u00B7 ", detailTask.status === 'completed'
|
|
210
|
+
? '已完成并交付'
|
|
211
|
+
: detailTask.dependencies.length === 0
|
|
212
|
+
? '无前置,可立即开工'
|
|
213
|
+
: waitingOn.length === 0
|
|
214
|
+
? '前置已就绪,可开工'
|
|
215
|
+
: `等待 ${waitingOn.join('、')}`] }), _jsx("span", { className: css.taskDetailMeta, children: dependents.length === 0 ? '无下游任务' : `完成后解锁 ${dependents.map((task) => task.id).join('、')}` })] })] }))] }));
|
|
177
216
|
}
|
|
178
217
|
function TeamSection({ team, onNavigate, historic = false }) {
|
|
218
|
+
const [membersOpen, setMembersOpen] = useState(true);
|
|
179
219
|
const busyCount = team.members.filter((member) => member.activity === 'working').length;
|
|
180
220
|
const assignedCount = team.tasks.filter((task) => task.assignee !== '').length;
|
|
181
221
|
const completedCount = team.tasks.filter((task) => task.status === 'completed').length;
|
|
182
222
|
const allCompleted = team.tasks.length > 0 && completedCount === team.tasks.length;
|
|
183
|
-
|
|
184
|
-
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled')
|
|
185
|
-
return false;
|
|
186
|
-
if (task.assignee === '')
|
|
187
|
-
return true;
|
|
188
|
-
return !team.members.some((member) => member.name === task.assignee);
|
|
189
|
-
});
|
|
190
|
-
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) => {
|
|
223
|
+
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(WorkGlyph, { active: busyCount > 0 }), busyCount > 0 ? `${busyCount} 人执行中` : allCompleted ? '已收齐' : '等待回报'] })] }), _jsx(ProgressOverview, { team: team }), _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 }), "\u6210\u5458 ", team.members.length] }), _jsx("span", { children: membersOpen ? '收起' : '展开' })] }), membersOpen && _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) => {
|
|
191
224
|
const owned = team.tasks.filter((task) => task.assignee === member.name);
|
|
192
225
|
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 !== '')
|
|
193
|
-
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(
|
|
226
|
+
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(WorkGlyph, { active: member.activity === 'working' }), memberStateLabel(member, team.tasks, historic)] })] }), _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
|
|
194
227
|
? _jsx("span", { className: css.taskEmpty, children: "\u6682\u65E0\u4EFB\u52A1" })
|
|
195
|
-
: owned.map((task) => (_jsx("span", { className: css.assignmentChip, "data-state": taskTone(task.state, task.status), title: task.subject, children: task.id }, task.id))) })
|
|
196
|
-
})] })] }), _jsx(DependencyMap, { tasks: team.tasks })
|
|
228
|
+
: owned.map((task) => (_jsx("span", { className: css.assignmentChip, "data-state": taskTone(task.state, task.status), title: task.subject, children: task.id }, task.id))) })] })] }, member.id));
|
|
229
|
+
})] })] }), _jsx(DependencyMap, { tasks: team.tasks })] }));
|
|
230
|
+
}
|
|
231
|
+
/** Legacy conversation cards may outlive their host archive. Project their
|
|
232
|
+
* durable roster through the same rebuilt panel instead of a second UI. */
|
|
233
|
+
function historicCardTeam(data, owner) {
|
|
234
|
+
return {
|
|
235
|
+
workspace: '',
|
|
236
|
+
teamId: data.teamId,
|
|
237
|
+
name: data.teamName,
|
|
238
|
+
captainSessionId: data.captainSessionId || owner,
|
|
239
|
+
members: data.members.map((member) => ({
|
|
240
|
+
...member,
|
|
241
|
+
status: 'removed',
|
|
242
|
+
activity: 'idle',
|
|
243
|
+
progress: 0,
|
|
244
|
+
done: 0,
|
|
245
|
+
total: 0,
|
|
246
|
+
currentTask: '',
|
|
247
|
+
unread: 0,
|
|
248
|
+
})),
|
|
249
|
+
tasks: [],
|
|
250
|
+
messageCount: 0,
|
|
251
|
+
captainInbox: [],
|
|
252
|
+
};
|
|
197
253
|
}
|
|
198
254
|
/** The top-right activity floater. Teams follow the current session: live
|
|
199
255
|
* snapshots and historic card summaries are only shown while their captain
|
|
@@ -354,7 +410,6 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
354
410
|
? _jsx("span", { className: css.emptyHint, children: "\u6682\u65E0\u56E2\u961F\u6D3B\u52A8" })
|
|
355
411
|
: (_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 }) => {
|
|
356
412
|
const teamKey = `${owner}:${team.teamId}`;
|
|
357
|
-
return (
|
|
358
|
-
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));
|
|
413
|
+
return (_jsx(TeamSection, { team: historicCardTeam(team, owner), onNavigate: navigateToSession, historic: true }, teamKey));
|
|
359
414
|
})] })) })] }))] }));
|
|
360
415
|
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
/** Pure relationship projections used by the AgentTeams activity panel. */
|
|
2
|
+
/** Reference-panel geometry: narrow nodes with enough room for curved edges. */
|
|
3
|
+
export const COMPACT_DAG_NODE_WIDTH = 92;
|
|
4
|
+
export const COMPACT_DAG_NODE_HEIGHT = 30;
|
|
5
|
+
export const COMPACT_DAG_COLUMN_GAP = 26;
|
|
6
|
+
export const COMPACT_DAG_ROW_GAP = 8;
|
|
7
|
+
/** Use a fill-width grid when the task graph has no real dependency edges. */
|
|
8
|
+
export function usesParallelTaskGrid(tasks) {
|
|
9
|
+
if (tasks.length === 0)
|
|
10
|
+
return false;
|
|
11
|
+
const taskIds = new Set(tasks.map((task) => task.id));
|
|
12
|
+
return tasks.every((task) => task.dependencies.every((dependency) => !taskIds.has(dependency)));
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* Whether an expanded activity panel still belongs to the current session.
|
|
4
16
|
*
|
|
@@ -36,6 +48,57 @@ export function taskStages(tasks) {
|
|
|
36
48
|
tasks: stageTasks.slice().sort((left, right) => left.id.localeCompare(right.id, 'en', { numeric: true })),
|
|
37
49
|
}));
|
|
38
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Lay tasks out as the reference panel's compact left-to-right DAG.
|
|
53
|
+
*
|
|
54
|
+
* Columns are dependency-depth stages. Rows are stable task-id order within
|
|
55
|
+
* each stage. Edges use cubic curves so fan-in remains readable without
|
|
56
|
+
* turning every task into a large card.
|
|
57
|
+
*/
|
|
58
|
+
export function compactDagLayout(tasks) {
|
|
59
|
+
const stages = taskStages(tasks);
|
|
60
|
+
const positions = new Map();
|
|
61
|
+
const nodes = [];
|
|
62
|
+
for (const [column, stage] of stages.entries()) {
|
|
63
|
+
for (const [row, task] of stage.tasks.entries()) {
|
|
64
|
+
const x = column * (COMPACT_DAG_NODE_WIDTH + COMPACT_DAG_COLUMN_GAP);
|
|
65
|
+
const y = row * (COMPACT_DAG_NODE_HEIGHT + COMPACT_DAG_ROW_GAP);
|
|
66
|
+
positions.set(task.id, { x, y });
|
|
67
|
+
nodes.push({ task, x, y });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const edges = [];
|
|
71
|
+
for (const task of tasks) {
|
|
72
|
+
const target = positions.get(task.id);
|
|
73
|
+
if (target === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
for (const dependency of task.dependencies) {
|
|
76
|
+
const source = positions.get(dependency);
|
|
77
|
+
if (source === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
const x1 = source.x + COMPACT_DAG_NODE_WIDTH;
|
|
80
|
+
const y1 = source.y + COMPACT_DAG_NODE_HEIGHT / 2;
|
|
81
|
+
const x2 = target.x;
|
|
82
|
+
const y2 = target.y + COMPACT_DAG_NODE_HEIGHT / 2;
|
|
83
|
+
edges.push({
|
|
84
|
+
from: dependency,
|
|
85
|
+
to: task.id,
|
|
86
|
+
path: `M${x1} ${y1}C${x1 + 14} ${y1},${x2 - 14} ${y2},${x2} ${y2}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const rows = Math.max(1, ...stages.map((stage) => stage.tasks.length));
|
|
91
|
+
return {
|
|
92
|
+
width: stages.length === 0
|
|
93
|
+
? 0
|
|
94
|
+
: stages.length * COMPACT_DAG_NODE_WIDTH + (stages.length - 1) * COMPACT_DAG_COLUMN_GAP,
|
|
95
|
+
height: stages.length === 0
|
|
96
|
+
? 0
|
|
97
|
+
: rows * COMPACT_DAG_NODE_HEIGHT + (rows - 1) * COMPACT_DAG_ROW_GAP,
|
|
98
|
+
nodes,
|
|
99
|
+
edges,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
39
102
|
/**
|
|
40
103
|
* Return the complete upstream/downstream chain around one task.
|
|
41
104
|
*
|