@nanmicoder/dsh-agent-teams 0.1.12 → 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.
Files changed (39) hide show
  1. package/README.md +43 -7
  2. package/README_ZH.md +20 -7
  3. package/lib/client/ActivityPanel.js +219 -50
  4. package/lib/client/StagingPlanEditor.js +493 -0
  5. package/lib/client/activity-model.js +71 -0
  6. package/lib/client/activity-monitor.js +39 -13
  7. package/lib/client/index.js +2 -2
  8. package/lib/client/locales.js +224 -2
  9. package/lib/client.js +1808 -250
  10. package/lib/client.js.map +1 -1
  11. package/lib/command.js +116 -99
  12. package/lib/index.js +286 -14
  13. package/lib/members.js +137 -16
  14. package/lib/profiles.js +572 -0
  15. package/lib/quality-gates.js +777 -0
  16. package/lib/scheduler.js +215 -18
  17. package/lib/snapshot.js +25 -1
  18. package/lib/state.js +116 -10
  19. package/lib/tools.js +1230 -38
  20. package/lib/types/client/ActivityPanel.d.ts +3 -1
  21. package/lib/types/client/StagingPlanEditor.d.ts +17 -0
  22. package/lib/types/client/activity-model.d.ts +67 -0
  23. package/lib/types/client/activity-monitor.d.ts +32 -7
  24. package/lib/types/client/locales.d.ts +222 -0
  25. package/lib/types/command.d.ts +11 -56
  26. package/lib/types/event-types.d.ts +35 -1
  27. package/lib/types/index.d.ts +9 -0
  28. package/lib/types/members.d.ts +48 -3
  29. package/lib/types/profiles.d.ts +124 -0
  30. package/lib/types/quality-gates.d.ts +148 -0
  31. package/lib/types/scheduler.d.ts +48 -1
  32. package/lib/types/snapshot.d.ts +18 -1
  33. package/lib/types/state.d.ts +8 -3
  34. package/lib/types/tools.d.ts +73 -9
  35. package/lib/types/types.d.ts +118 -0
  36. package/lib/types.js +11 -0
  37. package/package.json +10 -4
  38. package/release-notes/v0.1.13.md +60 -0
  39. package/release-notes/v0.1.14.md +68 -0
@@ -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
- return t('member.status.executing', { taskId: current.id });
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 summaryTone = blocked > 0 ? 'warning' : completed === team.tasks.length && team.tasks.length > 0 ? 'completed' : 'running';
181
- 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": 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) })] })] }));
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 }) => (_jsxs("button", { type: "button", className: css.dagNode, style: parallel
233
- ? { height: COMPACT_DAG_NODE_HEIGHT }
234
- : { 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": t('task.runningAria'), 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, t) })] }), _jsxs("span", { className: css.taskDetailLine, children: [detailTask.assignee || t('task.assignee.unclaimed'), " \u00B7 ", detailTask.status === 'completed'
235
- ? t('task.detail.completed')
236
- : detailTask.dependencies.length === 0
237
- ? t('task.detail.noPrerequisite')
238
- : waitingOn.length === 0
239
- ? t('task.detail.ready')
240
- : t('task.detail.waitingOn', { tasks: formatTaskIds(waitingOn, t) })] }), _jsx("span", { className: css.taskDetailMeta, children: dependents.length === 0
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
- 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: t('team.ended') }), _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 }) })] })] }), _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: t('captain.summary', {
251
- tasks: assignedCount,
252
- members: team.members.length,
253
- }) })] }), _jsxs("span", { className: css.captainState, "data-busy": busyCount > 0, children: [_jsx(WorkGlyph, { active: busyCount > 0 }), busyCount > 0
254
- ? t('captain.state.working', { count: busyCount })
255
- : t(allCompleted ? 'captain.state.collected' : 'captain.state.waiting')] })] }), _jsx(ProgressOverview, { team: team, t: t }), _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) => {
256
- const owned = team.tasks.filter((task) => task.assignee === member.name);
257
- 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: () => {
258
- if (member.id !== '') {
259
- onNavigate(team.captainSessionId, member.id);
260
- }
261
- }, 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, t)] })] }), _jsx("span", { className: css.memberStatusLine, children: memberStatusText(member, team.tasks, t) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children: t('assignment.label') }), _jsx("span", { className: css.assignmentTasks, children: owned.length === 0
262
- ? _jsx("span", { className: css.taskEmpty, children: t('assignment.empty') })
263
- : 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));
264
- })] })] }), _jsx(DependencyMap, { tasks: team.tasks, t: t })] }));
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
- return () => { controller.stop(); };
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
- // Auto-expand only after the page-settle window: opening (and its
450
- // main-column yield) right after load reads as a whole-page flicker.
451
- const settled = performance.now() - mountedAtRef.current >= AUTO_OPEN_SETTLE_MS;
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, 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('archive.label') }), _jsx(TeamSection, { team: team, onNavigate: navigateToSession, t: t, historic: true })] }, `${team.captainSessionId}:${team.teamId}`))), visibleHistoric.map(({ data: team, owner }) => {
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 })] }))] }))] }));