@nanmicoder/dsh-agent-teams 0.1.13 → 0.1.14
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 +41 -5
- package/README_ZH.md +18 -5
- package/lib/client/ActivityPanel.js +219 -50
- package/lib/client/StagingPlanEditor.js +493 -0
- package/lib/client/activity-model.js +71 -0
- package/lib/client/activity-monitor.js +1 -0
- package/lib/client/index.js +2 -2
- package/lib/client/locales.js +224 -2
- package/lib/client.js +1775 -241
- package/lib/client.js.map +1 -1
- package/lib/command.js +116 -99
- package/lib/index.js +285 -13
- package/lib/members.js +137 -16
- package/lib/profiles.js +572 -0
- package/lib/quality-gates.js +777 -0
- package/lib/scheduler.js +167 -8
- package/lib/snapshot.js +25 -1
- package/lib/state.js +116 -10
- package/lib/tools.js +1230 -38
- package/lib/types/client/ActivityPanel.d.ts +3 -1
- package/lib/types/client/StagingPlanEditor.d.ts +17 -0
- package/lib/types/client/activity-model.d.ts +67 -0
- package/lib/types/client/activity-monitor.d.ts +14 -1
- package/lib/types/client/locales.d.ts +222 -0
- package/lib/types/command.d.ts +11 -56
- package/lib/types/event-types.d.ts +35 -1
- package/lib/types/index.d.ts +9 -0
- package/lib/types/members.d.ts +48 -3
- package/lib/types/profiles.d.ts +124 -0
- package/lib/types/quality-gates.d.ts +148 -0
- package/lib/types/scheduler.d.ts +44 -0
- package/lib/types/snapshot.d.ts +18 -1
- package/lib/types/state.d.ts +8 -3
- package/lib/types/tools.d.ts +73 -9
- package/lib/types/types.d.ts +118 -0
- package/lib/types.js +11 -0
- package/package.json +10 -4
- package/release-notes/v0.1.14.md +68 -0
package/README.md
CHANGED
|
@@ -7,16 +7,22 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://
|
|
11
|
-
<a href="
|
|
12
|
-
<img src="https://
|
|
10
|
+
<a href="https://dshfind.com/en/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://img.shields.io/badge/recommended%20by-dshfind-FFD700?style=flat-square" alt="Recommended by dshfind"></a>
|
|
11
|
+
<a href="https://dshfind.com/en/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://dshfind.com/api/badge/NanmiCoder/dsh-agent-teams?lang=en" alt="dshfind score"></a>
|
|
12
|
+
<a href="https://dshfind.com/en/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://dshfind.com/api/badge/NanmiCoder/dsh-agent-teams?metric=downloads&lang=en" alt="dshfind downloads"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/@nanmicoder/dsh-agent-teams"><img src="https://img.shields.io/npm/v/@nanmicoder/dsh-agent-teams?style=flat-square&color=5B4CF0" alt="npm version"></a>
|
|
17
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="MIT license"></a>
|
|
18
|
+
<a href="./cordis.patch.yml"><img src="https://img.shields.io/badge/DSH-Web%20%2B%20Headless-5B4CF0?style=flat-square" alt="DSH Web and Headless"></a>
|
|
13
19
|
</p>
|
|
14
20
|
|
|
15
21
|
## One prompt. A working team.
|
|
16
22
|
|
|
17
23
|
`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
24
|
|
|
19
|
-
Ask in natural language. The plugin provides the team protocol,
|
|
25
|
+
Ask in natural language. The plugin provides the team protocol, eleven coordination tools, persistent state, an automatic shared-task scheduler, and a live Web UI—without requiring a separate workflow engine.
|
|
20
26
|
|
|
21
27
|
<p align="center">
|
|
22
28
|
<img src="./assets/ui.png" width="100%" alt="DeepSeek Harness conversation with the AgentTeams live activity panel, members, tasks, dependencies, and reports">
|
|
@@ -35,7 +41,9 @@ Read the [latest release notes](https://github.com/NanmiCoder/dsh-agent-teams/re
|
|
|
35
41
|
| **Dependency-aware tasks** | Tasks move through explicit states and cannot be claimed before their dependencies finish. |
|
|
36
42
|
| **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. |
|
|
37
43
|
| **Direct messaging** | Members send durable mailbox messages directly to teammates or the captain—no relay required. |
|
|
38
|
-
| **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. |
|
|
44
|
+
| **Live activity panel** | The Web UI combines segmented progress, a collapsible roster, and an interactive task DAG; running tasks show the member's model, and completed archives retain their full member and task history. |
|
|
45
|
+
| **Plan before execution** | Normal `/agent-teams` runs stage an unspawned roster and DAG first. The Web panel uses the host model catalog for member routes. Returning to chat stops the planning turn, asks what should change, and revises the same draft; discarding archives the draft, aborts the turn, and explicitly prevents automatic recreation. Only **Approve & Run** creates members and starts scheduling. |
|
|
46
|
+
| **Quality gates** | Opt-in quality tasks support requirements → implementation → verification → review → integration contracts, automatic repair/re-review, and explicit resume. Scope control is a completion-time audit, not host write interception. See [docs/quality-gates.md](./docs/quality-gates.md). |
|
|
39
47
|
|
|
40
48
|
The conversation card and activity panel use Harness's official locale service. They follow live language changes between English and Simplified Chinese—including status labels, dynamic summaries, controls, archive markers, and accessibility text—without a page reload or a separate plugin setting.
|
|
41
49
|
|
|
@@ -160,6 +168,34 @@ pnpm build
|
|
|
160
168
|
pnpm verify
|
|
161
169
|
```
|
|
162
170
|
|
|
171
|
+
## Named multi-role profiles
|
|
172
|
+
|
|
173
|
+
Configure one or more complete team profiles in `cordis.patch.yml`. A profile always supplies the roster (independent provider/model/role/reasoning effort). Set `taskPlanning: captain` when the Captain should derive the DAG from the user's goal; omit it or set `taskPlanning: seed` to keep a fixed template workflow:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
profiles:
|
|
177
|
+
demo-delivery:
|
|
178
|
+
description: Ship a small feature
|
|
179
|
+
protocol: Discuss requirements, review, test, then prepare release; do not deploy automatically.
|
|
180
|
+
members:
|
|
181
|
+
- name: analyst
|
|
182
|
+
model: gpt-5.6-sol
|
|
183
|
+
role: Analyze requirements
|
|
184
|
+
- name: implementer
|
|
185
|
+
model: gpt-5.6-terra
|
|
186
|
+
role: Implement the approved solution
|
|
187
|
+
tasks:
|
|
188
|
+
- id: requirements
|
|
189
|
+
subject: Requirements discussion
|
|
190
|
+
assignee: analyst
|
|
191
|
+
- id: implementation
|
|
192
|
+
subject: Implement solution
|
|
193
|
+
assignee: implementer
|
|
194
|
+
dependencies: [requirements]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use an explicit profile flag: `/agent-teams --profile demo-delivery implement the feature`. The first ordinary token is never treated as an implicit profile. Normal command runs call `agent_teams_create({ profile, approval: "required" })`: the roster and seed/Captain-designed DAG remain staged, no child session is created, and no task is claimed. Edit the plan in the activity panel using the host model catalog, return to chat so the Captain asks what to revise and then atomically updates the same draft, discard it, or click **Approve & Run**. Return/discard actions cancel any planning turn still running; discard also parks model-facing context that forbids silently creating a replacement team. Approval resolves the final provider/model/reasoning choices, atomically spawns the roster, and starts only ready tasks. A running team is stopped from its own panel header through a confirmation dialog rather than from the composer. Direct tool clients may pass `approval: "automatic"` for the legacy immediate path. Failed review/test tasks do not unlock downstream work; automatic repair/review tasks do not depend on the failed review.
|
|
198
|
+
|
|
163
199
|
## License
|
|
164
200
|
|
|
165
201
|
[MIT](./LICENSE)
|
package/README_ZH.md
CHANGED
|
@@ -7,16 +7,22 @@
|
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
|
-
<a href="https://
|
|
11
|
-
<a href="
|
|
12
|
-
<img src="https://
|
|
10
|
+
<a href="https://dshfind.com/zh/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://img.shields.io/badge/%E7%94%B1%20dshfind-%E6%8E%A8%E8%8D%90-FFD700?style=flat-square" alt="由 dshfind 推荐"></a>
|
|
11
|
+
<a href="https://dshfind.com/zh/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://dshfind.com/api/badge/NanmiCoder/dsh-agent-teams?lang=zh" alt="dshfind 评分"></a>
|
|
12
|
+
<a href="https://dshfind.com/zh/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://dshfind.com/api/badge/NanmiCoder/dsh-agent-teams?metric=downloads&lang=zh" alt="dshfind 下载量"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/@nanmicoder/dsh-agent-teams"><img src="https://img.shields.io/npm/v/@nanmicoder/dsh-agent-teams?style=flat-square&color=5B4CF0" alt="npm 版本"></a>
|
|
17
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-0B7285?style=flat-square" alt="MIT 许可证"></a>
|
|
18
|
+
<a href="./cordis.patch.yml"><img src="https://img.shields.io/badge/DSH-Web%20%2B%20Headless-5B4CF0?style=flat-square" alt="DSH Web 与 Headless"></a>
|
|
13
19
|
</p>
|
|
14
20
|
|
|
15
21
|
## 一句话,拉起一支真正协作的团队
|
|
16
22
|
|
|
17
23
|
`dsh-agent-teams` 让当前 DeepSeek Harness 会话成为队长:创建可续聊的子 Agent、把目标拆成有依赖的任务,并通过直达消息协调成员工作。
|
|
18
24
|
|
|
19
|
-
你只需用自然语言提出目标。插件会提供团队协议、
|
|
25
|
+
你只需用自然语言提出目标。插件会提供团队协议、11 个协作工具、持久化状态、自动共享任务调度和实时 Web UI,不需要额外的 Workflow 引擎。
|
|
20
26
|
|
|
21
27
|
<p align="center">
|
|
22
28
|
<img src="./assets/ui.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
|
|
@@ -35,7 +41,8 @@
|
|
|
35
41
|
| **带依赖的任务** | 任务有明确状态;依赖未完成时不能领取。 |
|
|
36
42
|
| **自动续领与安全接管** | 成员空闲后自动领取下一项就绪任务;转派会撤销旧 attempt,冷恢复会重试遗留任务,迟到结果无法覆盖。 |
|
|
37
43
|
| **成员直达消息** | 成员通过持久化邮箱直接联系队友或队长,不需要队长中转。 |
|
|
38
|
-
| **实时活动面板** | Web UI 用分段进度、可折叠成员树和可交互 DAG
|
|
44
|
+
| **实时活动面板** | Web UI 用分段进度、可折叠成员树和可交互 DAG 展示实时工作;运行中的子任务会标出使用的模型,团队结束后仍保留完整成员与任务历史。 |
|
|
45
|
+
| **质量门禁** | 人只提供目标和约束。默认任务顺序是需求 → 实现 → 验证 → 审查 → 集成,失败后自动修复/复审,恢复团队必须显式 resume。第一版范围控制是完成时审计,不是 host 写入拦截。详见 [docs/quality-gates.md](./docs/quality-gates.md)。 |
|
|
39
46
|
|
|
40
47
|
对话卡片与活动面板接入 Harness 官方多语言服务,会随宿主在简体中文和英文之间实时切换;任务/成员状态、动态摘要、操作按钮、历史归档标识和无障碍文案都会同步更新,无需刷新页面,也不增加插件自己的语言设置。
|
|
41
48
|
|
|
@@ -150,6 +157,12 @@ pnpm build
|
|
|
150
157
|
pnpm verify
|
|
151
158
|
```
|
|
152
159
|
|
|
160
|
+
## 命名多角色团队配置
|
|
161
|
+
|
|
162
|
+
在 `cordis.patch.yml` 的 `profiles` 中配置完整团队模板。每个 profile 都提供成员阵容,可独立指定 provider、model、role、reasoning_effort。`taskPlanning: captain` 表示只提供阵容和约束,由 Captain 根据用户目标设计 DAG;省略该字段或设为 `seed` 时,展开模板中的固定任务图。使用 `/agent-teams --profile <名称> <目标>` 显式激活;不会把首个普通 token 隐式识别为 profile。
|
|
163
|
+
|
|
164
|
+
普通 `/agent-teams` 流程会调用 `agent_teams_create({ profile, approval: "required" })`:只落盘可编辑的成员占位和 DAG,不创建子会话、不领取任务。成员模型和推理等级直接读取 Harness 的模型目录。「返回对话修改」会终止仍在运行的规划轮次,让队长先追问修改方向,再用一次原子操作更新同一份草案;「放弃本次计划」经二次确认后会归档草案、中止轮次,并向模型注入不得自动重建团队的控制上下文。只有点击「确认并启动团队」才会按最终配置原子创建成员并启动就绪任务。运行中团队的停止入口位于该团队的面板标题,点击后需要二次确认,不再占用输入区域。直接工具调用方可显式传 `approval: "automatic"` 保留旧的立即执行路径。审查或测试失败不会解锁下游;自动 repair/review 不依赖 failed review。
|
|
165
|
+
|
|
153
166
|
## 许可证
|
|
154
167
|
|
|
155
168
|
[MIT](./LICENSE)
|
|
@@ -18,11 +18,12 @@ 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, } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
22
|
-
import { activityPanelExpandedForSession, compactDagLayout, COMPACT_DAG_NODE_HEIGHT, COMPACT_DAG_NODE_WIDTH, dependencyFocusTaskId, relatedTaskIds, usesParallelTaskGrid, } from "./activity-model.js";
|
|
23
|
-
import { getActivityMonitorTargetsSnapshot, getActivitySnapshotsSnapshot, startActivityPolling, subscribeActivityMonitorTargets, subscribeActivitySnapshots, } from "./activity-monitor.js";
|
|
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";
|
|
23
|
+
import { ACTIVITY_HALT_URL, getActivityMonitorTargetsSnapshot, getActivitySnapshotsSnapshot, startActivityPolling, subscribeActivityMonitorTargets, subscribeActivitySnapshots, } from "./activity-monitor.js";
|
|
24
24
|
import { ACTION_ART, LEAD_ART, memberArtUrl } from "./artwork.js";
|
|
25
25
|
import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
|
|
26
|
+
import { StagingPlanEditor } from "./StagingPlanEditor.js";
|
|
26
27
|
import { DEFAULT_PANEL_LAYOUT, PANEL_LAYOUT_STORAGE_KEY, compactPanelForBounds, dockPanelLayout, floatPanelLayout, movePanelLayout, panelMaximumHeight, panelUsesAutoHeight, parsePanelLayout, resizePanelLayout, resolvePanelGeometry, } from "./panel-geometry.js";
|
|
27
28
|
import css from './ActivityPanel.module.css';
|
|
28
29
|
/** Grace before the panel collapses once no team remains. */
|
|
@@ -39,6 +40,7 @@ const PANEL_OPEN_ATTRIBUTE = 'data-agent-teams-panel-open';
|
|
|
39
40
|
const PANEL_SHIFT_PROPERTY = '--agent-teams-panel-shift';
|
|
40
41
|
const PANEL_CONVERSATION_GAP = 14;
|
|
41
42
|
const MOVE_THRESHOLD = 4;
|
|
43
|
+
const CAPTAIN_ASSIGNEE = 'captain';
|
|
42
44
|
function initialPanelLayout() {
|
|
43
45
|
if (typeof window === 'undefined')
|
|
44
46
|
return DEFAULT_PANEL_LAYOUT;
|
|
@@ -87,6 +89,15 @@ function taskStatusLabel(status, t) {
|
|
|
87
89
|
function formatTaskIds(ids, t) {
|
|
88
90
|
return ids.join(t('format.listSeparator'));
|
|
89
91
|
}
|
|
92
|
+
function taskTitle(task, model) {
|
|
93
|
+
const extras = [
|
|
94
|
+
task.kind,
|
|
95
|
+
task.round === undefined ? undefined : `r${task.round}`,
|
|
96
|
+
task.verdict,
|
|
97
|
+
model === '' ? undefined : model,
|
|
98
|
+
].filter((item) => item !== undefined);
|
|
99
|
+
return extras.length === 0 ? `${task.id} · ${task.subject}` : `${task.id} · ${task.subject} · ${extras.join(' · ')}`;
|
|
100
|
+
}
|
|
90
101
|
/** Badge/bar coloring key: visual state, widened for terminal statuses. */
|
|
91
102
|
function taskTone(state, status) {
|
|
92
103
|
if (status === 'failed')
|
|
@@ -125,8 +136,12 @@ function memberStatusText(member, tasks, t) {
|
|
|
125
136
|
const owned = tasks.filter((task) => task.assignee === member.name);
|
|
126
137
|
const current = owned.find((task) => task.id === member.currentTask);
|
|
127
138
|
const blocked = owned.find((task) => task.state === 'blocked');
|
|
128
|
-
if (member.activity === 'working' && current !== undefined)
|
|
129
|
-
|
|
139
|
+
if (member.activity === 'working' && current !== undefined) {
|
|
140
|
+
const model = taskModelLabel(current, [member]);
|
|
141
|
+
return model === ''
|
|
142
|
+
? t('member.status.executing', { taskId: current.id })
|
|
143
|
+
: t('member.status.executingModel', { taskId: current.id, model });
|
|
144
|
+
}
|
|
130
145
|
if (member.activity === 'working')
|
|
131
146
|
return t('member.status.working');
|
|
132
147
|
if (blocked !== undefined) {
|
|
@@ -150,15 +165,31 @@ function compactTaskLabel(subject) {
|
|
|
150
165
|
const head = withoutVerb.split(/[((·::]/u)[0]?.trim() ?? withoutVerb;
|
|
151
166
|
return head.length > 18 ? `${head.slice(0, 17)}…` : head;
|
|
152
167
|
}
|
|
153
|
-
function taskSummary(team, t) {
|
|
168
|
+
function taskSummary(team, t, discarded = false) {
|
|
154
169
|
const completed = team.tasks.filter((task) => task.status === 'completed');
|
|
170
|
+
const cancelled = team.tasks.filter((task) => task.status === 'cancelled');
|
|
155
171
|
const running = team.tasks.filter((task) => task.state === 'running');
|
|
156
172
|
const blocked = team.tasks.filter((task) => task.state === 'blocked');
|
|
157
|
-
const ready = team.tasks.filter((task) => task.state === 'open' && task.status !== 'completed');
|
|
173
|
+
const ready = team.tasks.filter((task) => task.state === 'open' && task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled');
|
|
174
|
+
const failed = team.tasks.filter((task) => task.status === 'failed');
|
|
175
|
+
if (discarded)
|
|
176
|
+
return t('task.summary.discarded', { count: team.tasks.length });
|
|
158
177
|
if (team.tasks.length === 0)
|
|
159
178
|
return t('task.summary.waitingBreakdown');
|
|
179
|
+
if (team.phase === 'staged')
|
|
180
|
+
return t('task.summary.staged', { count: team.tasks.length });
|
|
160
181
|
if (completed.length === team.tasks.length)
|
|
161
182
|
return t('task.summary.allDelivered', { count: completed.length });
|
|
183
|
+
if (completed.length + cancelled.length + failed.length === team.tasks.length) {
|
|
184
|
+
return t('task.summary.ended', {
|
|
185
|
+
completed: completed.length,
|
|
186
|
+
cancelled: cancelled.length,
|
|
187
|
+
failed: failed.length,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (failed.length > 0 && running.length === 0 && ready.length === 0 && blocked.length === 0) {
|
|
191
|
+
return t('task.summary.failedSettled', { count: failed.length });
|
|
192
|
+
}
|
|
162
193
|
if (blocked.length > 0 && running.length > 0) {
|
|
163
194
|
return t('task.summary.blockedAndRunning', {
|
|
164
195
|
tasks: formatTaskIds(blocked.slice(0, 3).map((task) => task.id), t),
|
|
@@ -173,14 +204,15 @@ function taskSummary(team, t) {
|
|
|
173
204
|
return t('task.summary.blocked', { tasks: formatTaskIds(blocked.map((task) => task.id), t) });
|
|
174
205
|
return t('task.summary.waitingSchedule');
|
|
175
206
|
}
|
|
176
|
-
function ProgressOverview({ team, t }) {
|
|
177
|
-
const running = team.tasks.filter((task) => task.state === 'running').length;
|
|
178
|
-
const blocked = team.tasks.filter((task) => task.state === 'blocked').length;
|
|
179
|
-
const completed = team.tasks.filter((task) => task.status === 'completed').length;
|
|
180
|
-
const
|
|
181
|
-
|
|
207
|
+
function ProgressOverview({ team, t, discarded = false }) {
|
|
208
|
+
const running = discarded ? 0 : team.tasks.filter((task) => task.state === 'running').length;
|
|
209
|
+
const blocked = discarded ? 0 : team.tasks.filter((task) => task.state === 'blocked').length;
|
|
210
|
+
const completed = discarded ? 0 : team.tasks.filter((task) => task.status === 'completed').length;
|
|
211
|
+
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';
|
|
213
|
+
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) })] })] }));
|
|
182
214
|
}
|
|
183
|
-
function DependencyMap({ tasks, t }) {
|
|
215
|
+
function DependencyMap({ tasks, members, t, discarded = false }) {
|
|
184
216
|
const [open, setOpen] = useState(true);
|
|
185
217
|
const [hoverTaskId, setHoverTaskId] = useState(null);
|
|
186
218
|
const [keyboardTaskId, setKeyboardTaskId] = useState(null);
|
|
@@ -222,6 +254,7 @@ function DependencyMap({ tasks, t }) {
|
|
|
222
254
|
?? tasks.find((task) => task.state === 'running')
|
|
223
255
|
?? tasks[0];
|
|
224
256
|
const detailTask = tasks.find((task) => task.id === focusedTaskId) ?? fallbackTask;
|
|
257
|
+
const detailModel = taskModelLabel(detailTask, members);
|
|
225
258
|
const waitingOn = detailTask.dependencies.filter((dependency) => (tasks.find((task) => task.id === dependency)?.status !== 'completed'));
|
|
226
259
|
const dependents = tasks.filter((task) => task.dependencies.includes(detailTask.id));
|
|
227
260
|
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
|
|
@@ -229,39 +262,129 @@ function DependencyMap({ tasks, t }) {
|
|
|
229
262
|
: 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) => {
|
|
230
263
|
const active = related !== null && related.has(edge.from) && related.has(edge.to);
|
|
231
264
|
return _jsx("path", { d: edge.path, "data-active": active, "data-dimmed": related !== null && !active }, `${edge.from}:${edge.to}`);
|
|
232
|
-
}) }), layout.nodes.map(({ task, x, y }) =>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
?
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
265
|
+
}) }), layout.nodes.map(({ task, x, y }) => {
|
|
266
|
+
const model = taskModelLabel(task, members);
|
|
267
|
+
const shortModel = compactModelLabel(model);
|
|
268
|
+
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));
|
|
271
|
+
})] }) }), _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
|
+
? t('task.detail.notRun')
|
|
273
|
+
: detailTask.status === 'completed'
|
|
274
|
+
? 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
|
|
241
280
|
? t('task.detail.noDownstream')
|
|
242
281
|
: t('task.detail.unlocks', { tasks: formatTaskIds(dependents.map((task) => task.id), t) }) })] })] }))] }));
|
|
243
282
|
}
|
|
244
|
-
function TeamSection({ team, onNavigate, t, historic = false }) {
|
|
283
|
+
function TeamSection({ team, modelDirectory, onContinuePlanning, onDiscarded, onNavigate, t, historic = false }) {
|
|
245
284
|
const [membersOpen, setMembersOpen] = useState(true);
|
|
285
|
+
const [stopOpen, setStopOpen] = useState(false);
|
|
286
|
+
const [stopping, setStopping] = useState(false);
|
|
287
|
+
const [stopError, setStopError] = useState('');
|
|
288
|
+
const discarded = historic && team.phase === 'staged';
|
|
289
|
+
const stopped = !historic && team.halted === true;
|
|
246
290
|
const busyCount = team.members.filter((member) => member.activity === 'working').length;
|
|
247
|
-
const assignedCount = team.tasks.filter((task) => task.assignee !== '').length;
|
|
291
|
+
const assignedCount = team.tasks.filter((task) => task.assignee !== '' && task.assignee !== CAPTAIN_ASSIGNEE).length;
|
|
292
|
+
const captainOwned = team.tasks.filter((task) => task.assignee === CAPTAIN_ASSIGNEE
|
|
293
|
+
&& task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled');
|
|
294
|
+
const captainBusy = captainOwned.length > 0;
|
|
295
|
+
const captainTaskIds = formatTaskIds(captainOwned.map((task) => task.id), t);
|
|
248
296
|
const completedCount = team.tasks.filter((task) => task.status === 'completed').length;
|
|
249
297
|
const allCompleted = team.tasks.length > 0 && completedCount === team.tasks.length;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
298
|
+
const allSettled = team.tasks.length > 0 && team.tasks.every((task) => (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'));
|
|
299
|
+
const unfinishedCount = team.tasks.filter((task) => (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled')).length;
|
|
300
|
+
const canStop = !historic && team.phase === 'running' && team.halted !== true && teamIsActive(team);
|
|
301
|
+
const stopTeam = async () => {
|
|
302
|
+
if (stopping)
|
|
303
|
+
return;
|
|
304
|
+
setStopping(true);
|
|
305
|
+
setStopError('');
|
|
306
|
+
try {
|
|
307
|
+
const response = await fetch(ACTIVITY_HALT_URL, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
cache: 'no-store',
|
|
310
|
+
headers: { 'content-type': 'application/json' },
|
|
311
|
+
body: JSON.stringify({ sessionId: team.captainSessionId, teamId: team.teamId }),
|
|
312
|
+
});
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
let message = t('team.stopRequestFailed');
|
|
315
|
+
try {
|
|
316
|
+
const body = await response.json();
|
|
317
|
+
if (typeof body.error === 'string' && body.error.trim() !== '')
|
|
318
|
+
message = body.error;
|
|
319
|
+
}
|
|
320
|
+
catch { }
|
|
321
|
+
throw new Error(message);
|
|
322
|
+
}
|
|
323
|
+
setStopOpen(false);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
setStopError(t('team.stopFailed', { message: error instanceof Error ? error.message : String(error) }));
|
|
327
|
+
}
|
|
328
|
+
finally {
|
|
329
|
+
setStopping(false);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
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
|
|
333
|
+
? t('captain.summary.discarded', { tasks: team.tasks.length, members: team.members.length })
|
|
334
|
+
: captainBusy
|
|
335
|
+
? t('captain.summary.withTakeover', { tasks: assignedCount, captainTasks: captainTaskIds })
|
|
336
|
+
: team.phase === 'staged'
|
|
337
|
+
? t(team.planReviewState === 'awaiting_feedback'
|
|
338
|
+
? 'captain.summary.awaitingFeedback'
|
|
339
|
+
: 'captain.summary.staged', { tasks: team.tasks.length, members: team.members.length })
|
|
340
|
+
: t('captain.summary', { tasks: assignedCount, members: team.members.length }) })] }), _jsxs("span", { className: css.captainState, "data-busy": captainBusy || busyCount > 0, children: [_jsx(WorkGlyph, { active: captainBusy || busyCount > 0 }), discarded
|
|
341
|
+
? t('captain.state.discarded')
|
|
342
|
+
: captainBusy
|
|
343
|
+
? t('captain.state.takeover', { tasks: captainTaskIds })
|
|
344
|
+
: team.phase === 'staged'
|
|
345
|
+
? t(team.planReviewState === 'awaiting_feedback'
|
|
346
|
+
? 'captain.state.awaitingFeedback'
|
|
347
|
+
: 'captain.state.staged')
|
|
348
|
+
: busyCount > 0
|
|
349
|
+
? t('captain.state.working', { count: busyCount })
|
|
350
|
+
: t(allCompleted
|
|
351
|
+
? 'captain.state.collected'
|
|
352
|
+
: allSettled
|
|
353
|
+
? '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 }), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(WorkGlyph, { active: member.activity === 'working' }), discarded
|
|
362
|
+
? t('member.state.notCreated')
|
|
363
|
+
: stopped
|
|
364
|
+
? t('member.state.stopped')
|
|
365
|
+
: 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')
|
|
371
|
+
: 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) }), memberModel !== '' && (_jsx("span", { className: css.memberModel, "data-member-model": memberModel, children: t('member.model', { model: memberModel }) }))] }), _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)
|
|
387
|
+
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] }) })] }));
|
|
265
388
|
}
|
|
266
389
|
/** Legacy conversation cards may outlive their host archive. Project their
|
|
267
390
|
* durable roster through the same rebuilt panel instead of a second UI. */
|
|
@@ -271,6 +394,7 @@ function historicCardTeam(data, owner) {
|
|
|
271
394
|
teamId: data.teamId,
|
|
272
395
|
name: data.teamName,
|
|
273
396
|
captainSessionId: data.captainSessionId || owner,
|
|
397
|
+
phase: 'running',
|
|
274
398
|
members: data.members.map((member) => ({
|
|
275
399
|
...member,
|
|
276
400
|
status: 'removed',
|
|
@@ -286,7 +410,7 @@ function historicCardTeam(data, owner) {
|
|
|
286
410
|
captainInbox: [],
|
|
287
411
|
};
|
|
288
412
|
}
|
|
289
|
-
export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
413
|
+
export function ActivityPanel({ sessionsList, modelDirectories, openMember, t }) {
|
|
290
414
|
// Navigating to a member's subagent transcript is an explicit departure:
|
|
291
415
|
// hide the floater immediately instead of waiting out the autocollapse
|
|
292
416
|
// grace, so the panel never lingers over the member session.
|
|
@@ -309,7 +433,15 @@ export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
|
309
433
|
const frameRef = useRef(null);
|
|
310
434
|
const pendingLayoutRef = useRef(null);
|
|
311
435
|
const current = useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot).current;
|
|
436
|
+
const autoOpenTrackerRef = useRef({ sessionId: current, restoreComplete: false, liveTeamIds: new Set() });
|
|
312
437
|
const monitorTargets = useSyncExternalStore(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
|
|
438
|
+
const returnToComposer = () => {
|
|
439
|
+
setOpen(false);
|
|
440
|
+
setOpenOwner(undefined);
|
|
441
|
+
window.requestAnimationFrame(() => {
|
|
442
|
+
document.querySelector('[data-composer-card] textarea')?.focus();
|
|
443
|
+
});
|
|
444
|
+
};
|
|
313
445
|
const { teams, archivedTeams } = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
|
|
314
446
|
const currentTargets = useMemo(() => current === undefined ? [] : monitorTargets.filter((target) => target.sessionId === current), [current, monitorTargets]);
|
|
315
447
|
const currentRef = useRef(current);
|
|
@@ -374,12 +506,18 @@ export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
|
374
506
|
// removes the old panel immediately instead of waiting for the no-team
|
|
375
507
|
// autoclose grace period on the destination page.
|
|
376
508
|
useLayoutEffect(() => {
|
|
509
|
+
const tracker = autoOpenTrackerRef.current;
|
|
510
|
+
if (tracker.sessionId !== current) {
|
|
511
|
+
tracker.sessionId = current;
|
|
512
|
+
tracker.restoreComplete = false;
|
|
513
|
+
tracker.liveTeamIds = new Set();
|
|
514
|
+
setWasActive(false);
|
|
515
|
+
setAutoOpened(false);
|
|
516
|
+
}
|
|
377
517
|
if (openOwner === undefined || openOwner === current)
|
|
378
518
|
return;
|
|
379
519
|
setOpen(false);
|
|
380
520
|
setOpenOwner(undefined);
|
|
381
|
-
setWasActive(false);
|
|
382
|
-
setAutoOpened(false);
|
|
383
521
|
}, [current, openOwner]);
|
|
384
522
|
// Only the wide docked mode asks the conversation column to yield. Floating
|
|
385
523
|
// and compact modes are intentionally true overlays. The width is written as
|
|
@@ -407,7 +545,23 @@ export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
|
407
545
|
// also performs one cold-start discovery pass so archived/cardless teams
|
|
408
546
|
// survive a browser or `dsh web` restart.
|
|
409
547
|
const controller = startActivityPolling(currentTargets, { discoverySessionId: current });
|
|
410
|
-
|
|
548
|
+
let active = true;
|
|
549
|
+
const tracker = autoOpenTrackerRef.current;
|
|
550
|
+
if (tracker.sessionId === current && !tracker.restoreComplete) {
|
|
551
|
+
void controller.firstTick.then(() => {
|
|
552
|
+
const latest = autoOpenTrackerRef.current;
|
|
553
|
+
if (!active || latest.sessionId !== current || latest.restoreComplete)
|
|
554
|
+
return;
|
|
555
|
+
latest.liveTeamIds = new Set(getActivitySnapshotsSnapshot().teams
|
|
556
|
+
.filter((team) => team.captainSessionId === current)
|
|
557
|
+
.map((team) => team.teamId));
|
|
558
|
+
latest.restoreComplete = true;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return () => {
|
|
562
|
+
active = false;
|
|
563
|
+
controller.stop();
|
|
564
|
+
};
|
|
411
565
|
}, [current, currentTargets]);
|
|
412
566
|
useEffect(() => {
|
|
413
567
|
const onOpenPanel = (event) => {
|
|
@@ -443,13 +597,26 @@ export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
|
443
597
|
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]);
|
|
444
598
|
const visibleArchived = useMemo(() => (current === undefined ? [] : archivedTeams.filter((team) => team.captainSessionId === current && !teams.some((live) => live.captainSessionId === current && live.teamId === team.teamId))), [archivedTeams, current, teams]);
|
|
445
599
|
const visibleCount = visibleTeams.length + visibleArchived.length + visibleHistoric.length;
|
|
600
|
+
const visibleLiveTeamIds = useMemo(() => visibleTeams.map((team) => team.teamId).sort(), [visibleTeams]);
|
|
601
|
+
const visibleLiveTeamKey = visibleLiveTeamIds.join('\u0000');
|
|
446
602
|
useEffect(() => {
|
|
603
|
+
const tracker = autoOpenTrackerRef.current;
|
|
604
|
+
const settled = performance.now() - mountedAtRef.current >= AUTO_OPEN_SETTLE_MS;
|
|
605
|
+
const shouldAutoExpand = tracker.sessionId === current && activityPanelShouldAutoExpand({
|
|
606
|
+
alreadyAutoOpened: autoOpened,
|
|
607
|
+
pageSettled: settled,
|
|
608
|
+
restoreComplete: tracker.restoreComplete,
|
|
609
|
+
previousLiveTeamIds: tracker.liveTeamIds,
|
|
610
|
+
currentLiveTeamIds: visibleLiveTeamIds,
|
|
611
|
+
});
|
|
612
|
+
if (tracker.sessionId === current && tracker.restoreComplete) {
|
|
613
|
+
tracker.liveTeamIds = new Set(visibleLiveTeamIds);
|
|
614
|
+
}
|
|
447
615
|
if (visibleCount > 0) {
|
|
448
616
|
setWasActive(true);
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
if (!autoOpened && settled) {
|
|
617
|
+
// Existing state restored for a reopened conversation stays collapsed.
|
|
618
|
+
// Only a live team that appears after the restore pass may auto-expand.
|
|
619
|
+
if (shouldAutoExpand) {
|
|
453
620
|
setOpenOwner(current);
|
|
454
621
|
setOpen(true);
|
|
455
622
|
setAutoOpened(true);
|
|
@@ -467,7 +634,7 @@ export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
|
467
634
|
setAutoOpened(false);
|
|
468
635
|
}, AUTOCLOSE_GRACE_MS);
|
|
469
636
|
return () => { clearTimeout(timer); };
|
|
470
|
-
}, [visibleCount, autoOpened, wasActive]);
|
|
637
|
+
}, [visibleCount, visibleLiveTeamKey, autoOpened, wasActive, current]);
|
|
471
638
|
const busy = useMemo(() => visibleTeams.some((team) => team.members.some((member) => member.activity === 'working')), [visibleTeams]);
|
|
472
639
|
const hasTeams = visibleCount > 0;
|
|
473
640
|
// Auto-height panels do not store their live content height. Capture the
|
|
@@ -599,7 +766,9 @@ export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
|
599
766
|
setOpenOwner(undefined);
|
|
600
767
|
}, "aria-label": t('activity.collapse'), title: t('activity.collapse'), children: _jsx(IconChevronDownOutline14, {}) })] })] }), _jsx("div", { className: css.teams, children: visibleCount === 0
|
|
601
768
|
? _jsx("span", { className: css.emptyHint, children: t('activity.empty') })
|
|
602
|
-
: (_jsxs(_Fragment, { children: [visibleTeams.map((team) => (_jsx(TeamSection, { team: team,
|
|
769
|
+
: (_jsxs(_Fragment, { children: [visibleTeams.map((team) => (_jsx(TeamSection, { team: team, modelDirectory: team.phase === 'staged'
|
|
770
|
+
? modelDirectories.directoryFor(team.captainSessionId)
|
|
771
|
+
: undefined, onContinuePlanning: returnToComposer, onDiscarded: returnToComposer, onNavigate: navigateToSession, t: t }, team.teamId))), visibleArchived.map((team) => (_jsxs("div", { "data-team-id": team.teamId, "data-historic": true, className: css.archivedWrap, children: [_jsx("span", { className: css.archiveLabel, children: t(team.phase === 'staged' ? 'archive.discardedLabel' : 'archive.label') }), _jsx(TeamSection, { team: team, onNavigate: navigateToSession, t: t, historic: true })] }, `${team.captainSessionId}:${team.teamId}`))), visibleHistoric.map(({ data: team, owner }) => {
|
|
603
772
|
const teamKey = `${owner}:${team.teamId}`;
|
|
604
773
|
return (_jsx(TeamSection, { team: historicCardTeam(team, owner), onNavigate: navigateToSession, t: t, historic: true }, teamKey));
|
|
605
774
|
})] })) }), !compact && (_jsx("div", { className: css.resizeHandle, "data-resize-edge": "left", onPointerDown: (event) => { beginResize('left', event); }, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "aria-hidden": true })), !compact && geometry.mode === 'floating' && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.resizeHandle, "data-resize-edge": "bottom", onPointerDown: (event) => { beginResize('bottom', event); }, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "aria-hidden": true }), _jsx("div", { className: css.resizeHandle, "data-resize-edge": "corner", onPointerDown: (event) => { beginResize('corner', event); }, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "aria-hidden": true })] }))] }))] }));
|