@nanmicoder/dsh-agent-teams 0.1.10 → 0.1.12
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 +3 -1
- package/README_ZH.md +3 -1
- package/lib/client/ActivityPanel.js +88 -69
- package/lib/client/AgentTeamsCard.js +5 -3
- package/lib/client/activity-monitor.js +32 -7
- package/lib/client/index.js +14 -4
- package/lib/client/locales.js +165 -0
- package/lib/client/session-navigation.js +21 -0
- package/lib/client.js +360 -135
- package/lib/client.js.map +1 -1
- package/lib/members.js +21 -47
- package/lib/snapshot.js +4 -15
- package/lib/tools.js +1 -1
- package/lib/types/client/ActivityPanel.d.ts +5 -3
- package/lib/types/client/AgentTeamsCard.d.ts +2 -2
- package/lib/types/client/activity-monitor.d.ts +13 -3
- package/lib/types/client/index.d.ts +8 -1
- package/lib/types/client/locales.d.ts +169 -0
- package/lib/types/client/session-navigation.d.ts +22 -0
- package/lib/types/members.d.ts +17 -13
- package/package.json +1 -1
- package/release-notes/v0.1.11.md +52 -0
- package/release-notes/v0.1.12.md +54 -0
package/README.md
CHANGED
|
@@ -37,6 +37,8 @@ Read the [latest release notes](https://github.com/NanmiCoder/dsh-agent-teams/re
|
|
|
37
37
|
| **Direct messaging** | Members send durable mailbox messages directly to teammates or the captain—no relay required. |
|
|
38
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. |
|
|
39
39
|
|
|
40
|
+
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
|
+
|
|
40
42
|
## Install
|
|
41
43
|
|
|
42
44
|
> [!NOTE]
|
|
@@ -45,7 +47,7 @@ Read the [latest release notes](https://github.com/NanmiCoder/dsh-agent-teams/re
|
|
|
45
47
|
### npm
|
|
46
48
|
|
|
47
49
|
```sh
|
|
48
|
-
dsh plugin --profile web add @nanmicoder/dsh-agent-teams
|
|
50
|
+
dsh plugin --profile web add @nanmicoder/dsh-agent-teams@latest
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
### Build from source
|
package/README_ZH.md
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
| **成员直达消息** | 成员通过持久化邮箱直接联系队友或队长,不需要队长中转。 |
|
|
38
38
|
| **实时活动面板** | Web UI 用分段进度、可折叠成员树和可交互 DAG 展示实时工作;团队结束后仍保留完整成员与任务历史。 |
|
|
39
39
|
|
|
40
|
+
对话卡片与活动面板接入 Harness 官方多语言服务,会随宿主在简体中文和英文之间实时切换;任务/成员状态、动态摘要、操作按钮、历史归档标识和无障碍文案都会同步更新,无需刷新页面,也不增加插件自己的语言设置。
|
|
41
|
+
|
|
40
42
|
## 安装
|
|
41
43
|
|
|
42
44
|
> [!NOTE]
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
### npm
|
|
46
48
|
|
|
47
49
|
```sh
|
|
48
|
-
dsh plugin --profile web add @nanmicoder/dsh-agent-teams
|
|
50
|
+
dsh plugin --profile web add @nanmicoder/dsh-agent-teams@latest
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
### 从源码构建
|
|
@@ -73,15 +73,19 @@ function accentOf(id) {
|
|
|
73
73
|
/** Badge text follows the raw task status (finer than the 4 visual states):
|
|
74
74
|
* claimed/pending/failed/cancelled keep their own labels and colors. */
|
|
75
75
|
const TASK_STATUS_LABEL = {
|
|
76
|
-
pending: '
|
|
77
|
-
claimed: '
|
|
78
|
-
in_progress: '
|
|
79
|
-
completed: '
|
|
80
|
-
failed: '
|
|
81
|
-
cancelled: '
|
|
76
|
+
pending: 'task.status.pending',
|
|
77
|
+
claimed: 'task.status.claimed',
|
|
78
|
+
in_progress: 'task.status.inProgress',
|
|
79
|
+
completed: 'task.status.completed',
|
|
80
|
+
failed: 'task.status.failed',
|
|
81
|
+
cancelled: 'task.status.cancelled',
|
|
82
82
|
};
|
|
83
|
-
function taskStatusLabel(status) {
|
|
84
|
-
|
|
83
|
+
function taskStatusLabel(status, t) {
|
|
84
|
+
const key = TASK_STATUS_LABEL[status];
|
|
85
|
+
return key === undefined ? status : t(key);
|
|
86
|
+
}
|
|
87
|
+
function formatTaskIds(ids, t) {
|
|
88
|
+
return ids.join(t('format.listSeparator'));
|
|
85
89
|
}
|
|
86
90
|
/** Badge/bar coloring key: visual state, widened for terminal statuses. */
|
|
87
91
|
function taskTone(state, status) {
|
|
@@ -98,78 +102,85 @@ function WorkGlyph({ active }) {
|
|
|
98
102
|
return (_jsx("svg", { className: css.workGlyph, "data-active": active, width: "11", height: "11", viewBox: "0 0 11 11", fill: "currentColor", "aria-hidden": true, children: [[0, 0], [4.2, 0], [8.4, 0], [0, 4.2], [4.2, 4.2], [8.4, 4.2]].map(([x, y], index) => (_jsx("rect", { x: x, y: y, width: "2.6", height: "2.6", rx: ".6", style: { animationDelay: `${index * 0.15}s` } }, `${x}:${y}`))) }));
|
|
99
103
|
}
|
|
100
104
|
/** Collapsed badge: an always-visible corner pill while any team exists. */
|
|
101
|
-
function CollapsedBadge({ count, busy, onClick }) {
|
|
102
|
-
return (_jsxs("button", { type: "button", className: css.badge, "data-agent-teams-collapsed": true, "data-busy": busy, onClick: onClick, "aria-label":
|
|
105
|
+
function CollapsedBadge({ count, busy, onClick, t }) {
|
|
106
|
+
return (_jsxs("button", { type: "button", className: css.badge, "data-agent-teams-collapsed": true, "data-busy": busy, onClick: onClick, "aria-label": t('activity.badgeAria', { count }), children: [_jsx("span", { className: css.badgeDot, "data-busy": busy, "aria-hidden": true }), _jsx("span", { className: css.badgeCount, children: count })] }));
|
|
103
107
|
}
|
|
104
|
-
function memberStateLabel(member, tasks, historic) {
|
|
108
|
+
function memberStateLabel(member, tasks, historic, t) {
|
|
105
109
|
const owned = tasks.filter((task) => task.assignee === member.name);
|
|
106
110
|
if (member.activity === 'working')
|
|
107
|
-
return '
|
|
111
|
+
return t('member.state.working');
|
|
108
112
|
if (owned.some((task) => task.status === 'failed'))
|
|
109
|
-
return '
|
|
113
|
+
return t('member.state.failed');
|
|
110
114
|
if (owned.some((task) => task.state === 'blocked'))
|
|
111
|
-
return '
|
|
115
|
+
return t('member.state.waiting');
|
|
112
116
|
if (owned.length > 0 && owned.every((task) => task.status === 'completed'))
|
|
113
|
-
return '
|
|
117
|
+
return t('member.state.delivered');
|
|
114
118
|
if (member.status === 'removed')
|
|
115
|
-
return historic ? '
|
|
119
|
+
return t(historic ? 'member.state.left' : 'member.state.removed');
|
|
116
120
|
if (owned.length > 0)
|
|
117
|
-
return '
|
|
118
|
-
return '
|
|
121
|
+
return t('member.state.pending');
|
|
122
|
+
return t('member.state.unassigned');
|
|
119
123
|
}
|
|
120
|
-
function memberStatusText(member, tasks) {
|
|
124
|
+
function memberStatusText(member, tasks, t) {
|
|
121
125
|
const owned = tasks.filter((task) => task.assignee === member.name);
|
|
122
126
|
const current = owned.find((task) => task.id === member.currentTask);
|
|
123
127
|
const blocked = owned.find((task) => task.state === 'blocked');
|
|
124
128
|
if (member.activity === 'working' && current !== undefined)
|
|
125
|
-
return
|
|
129
|
+
return t('member.status.executing', { taskId: current.id });
|
|
126
130
|
if (member.activity === 'working')
|
|
127
|
-
return '
|
|
131
|
+
return t('member.status.working');
|
|
128
132
|
if (blocked !== undefined) {
|
|
129
133
|
const dependency = tasks.find((task) => blocked.dependencies.includes(task.id) && task.state !== 'completed');
|
|
130
|
-
if (dependency !== undefined)
|
|
131
|
-
return
|
|
132
|
-
|
|
134
|
+
if (dependency !== undefined) {
|
|
135
|
+
return t('member.status.waitingOn', {
|
|
136
|
+
taskId: dependency.id,
|
|
137
|
+
assignee: dependency.assignee || t('task.assignee.unclaimed'),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return t('member.status.waitingPrerequisite');
|
|
133
141
|
}
|
|
134
142
|
if (member.total === 0)
|
|
135
|
-
return '
|
|
143
|
+
return t('member.status.waitingAssignment');
|
|
136
144
|
if (member.done === member.total)
|
|
137
|
-
return '
|
|
138
|
-
return member.activity === 'idle' ? '
|
|
145
|
+
return t('member.status.delivered');
|
|
146
|
+
return t(member.activity === 'idle' ? 'member.status.idle' : 'member.status.unknown');
|
|
139
147
|
}
|
|
140
148
|
function compactTaskLabel(subject) {
|
|
141
149
|
const withoutVerb = subject.replace(/^开发\s*/u, '').replace(/^\d+[-_.、\s]*/u, '');
|
|
142
150
|
const head = withoutVerb.split(/[((·::]/u)[0]?.trim() ?? withoutVerb;
|
|
143
151
|
return head.length > 18 ? `${head.slice(0, 17)}…` : head;
|
|
144
152
|
}
|
|
145
|
-
function taskSummary(team) {
|
|
153
|
+
function taskSummary(team, t) {
|
|
146
154
|
const completed = team.tasks.filter((task) => task.status === 'completed');
|
|
147
155
|
const running = team.tasks.filter((task) => task.state === 'running');
|
|
148
156
|
const blocked = team.tasks.filter((task) => task.state === 'blocked');
|
|
149
157
|
const ready = team.tasks.filter((task) => task.state === 'open' && task.status !== 'completed');
|
|
150
158
|
if (team.tasks.length === 0)
|
|
151
|
-
return '
|
|
159
|
+
return t('task.summary.waitingBreakdown');
|
|
152
160
|
if (completed.length === team.tasks.length)
|
|
153
|
-
return
|
|
161
|
+
return t('task.summary.allDelivered', { count: completed.length });
|
|
154
162
|
if (blocked.length > 0 && running.length > 0) {
|
|
155
|
-
return
|
|
163
|
+
return t('task.summary.blockedAndRunning', {
|
|
164
|
+
tasks: formatTaskIds(blocked.slice(0, 3).map((task) => task.id), t),
|
|
165
|
+
more: blocked.length > 3 ? t('task.summary.more', { count: blocked.length - 3 }) : '',
|
|
166
|
+
});
|
|
156
167
|
}
|
|
157
168
|
if (running.length > 0)
|
|
158
|
-
return
|
|
169
|
+
return t('task.summary.running', { tasks: formatTaskIds(running.map((task) => task.id), t) });
|
|
159
170
|
if (ready.length > 0)
|
|
160
|
-
return
|
|
171
|
+
return t('task.summary.ready', { tasks: formatTaskIds(ready.map((task) => task.id), t) });
|
|
161
172
|
if (blocked.length > 0)
|
|
162
|
-
return
|
|
163
|
-
return '
|
|
173
|
+
return t('task.summary.blocked', { tasks: formatTaskIds(blocked.map((task) => task.id), t) });
|
|
174
|
+
return t('task.summary.waitingSchedule');
|
|
164
175
|
}
|
|
165
|
-
function ProgressOverview({ team }) {
|
|
176
|
+
function ProgressOverview({ team, t }) {
|
|
166
177
|
const running = team.tasks.filter((task) => task.state === 'running').length;
|
|
167
178
|
const blocked = team.tasks.filter((task) => task.state === 'blocked').length;
|
|
168
179
|
const completed = team.tasks.filter((task) => task.status === 'completed').length;
|
|
169
180
|
const summaryTone = blocked > 0 ? 'warning' : completed === team.tasks.length && team.tasks.length > 0 ? 'completed' : 'running';
|
|
170
|
-
return (_jsxs("section", { className: css.progressOverview, "aria-label":
|
|
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) })] })] }));
|
|
171
182
|
}
|
|
172
|
-
function DependencyMap({ tasks }) {
|
|
183
|
+
function DependencyMap({ tasks, t }) {
|
|
173
184
|
const [open, setOpen] = useState(true);
|
|
174
185
|
const [hoverTaskId, setHoverTaskId] = useState(null);
|
|
175
186
|
const [keyboardTaskId, setKeyboardTaskId] = useState(null);
|
|
@@ -213,34 +224,44 @@ function DependencyMap({ tasks }) {
|
|
|
213
224
|
const detailTask = tasks.find((task) => task.id === focusedTaskId) ?? fallbackTask;
|
|
214
225
|
const waitingOn = detailTask.dependencies.filter((dependency) => (tasks.find((task) => task.id === dependency)?.status !== 'completed'));
|
|
215
226
|
const dependents = tasks.filter((task) => task.dependencies.includes(detailTask.id));
|
|
216
|
-
return (_jsxs("section", { className: css.dependencySection, "aria-label":
|
|
217
|
-
? parallel ? '
|
|
218
|
-
:
|
|
227
|
+
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
|
|
228
|
+
? t(parallel ? 'dependency.hint.parallel' : 'dependency.hint.chain')
|
|
229
|
+
: 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) => {
|
|
219
230
|
const active = related !== null && related.has(edge.from) && related.has(edge.to);
|
|
220
231
|
return _jsx("path", { d: edge.path, "data-active": active, "data-dimmed": related !== null && !active }, `${edge.from}:${edge.to}`);
|
|
221
232
|
}) }), layout.nodes.map(({ task, x, y }) => (_jsxs("button", { type: "button", className: css.dagNode, style: parallel
|
|
222
233
|
? { height: COMPACT_DAG_NODE_HEIGHT }
|
|
223
|
-
: { 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":
|
|
224
|
-
? '
|
|
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')
|
|
225
236
|
: detailTask.dependencies.length === 0
|
|
226
|
-
? '
|
|
237
|
+
? t('task.detail.noPrerequisite')
|
|
227
238
|
: waitingOn.length === 0
|
|
228
|
-
? '
|
|
229
|
-
:
|
|
239
|
+
? t('task.detail.ready')
|
|
240
|
+
: t('task.detail.waitingOn', { tasks: formatTaskIds(waitingOn, t) })] }), _jsx("span", { className: css.taskDetailMeta, children: dependents.length === 0
|
|
241
|
+
? t('task.detail.noDownstream')
|
|
242
|
+
: t('task.detail.unlocks', { tasks: formatTaskIds(dependents.map((task) => task.id), t) }) })] })] }))] }));
|
|
230
243
|
}
|
|
231
|
-
function TeamSection({ team, onNavigate, historic = false }) {
|
|
244
|
+
function TeamSection({ team, onNavigate, t, historic = false }) {
|
|
232
245
|
const [membersOpen, setMembersOpen] = useState(true);
|
|
233
246
|
const busyCount = team.members.filter((member) => member.activity === 'working').length;
|
|
234
247
|
const assignedCount = team.tasks.filter((task) => task.assignee !== '').length;
|
|
235
248
|
const completedCount = team.tasks.filter((task) => task.status === 'completed').length;
|
|
236
249
|
const allCompleted = team.tasks.length > 0 && completedCount === team.tasks.length;
|
|
237
|
-
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:
|
|
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) => {
|
|
238
256
|
const owned = team.tasks.filter((task) => task.assignee === member.name);
|
|
239
|
-
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: () => {
|
|
240
|
-
|
|
241
|
-
|
|
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') })
|
|
242
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));
|
|
243
|
-
})] })] }), _jsx(DependencyMap, { tasks: team.tasks })] }));
|
|
264
|
+
})] })] }), _jsx(DependencyMap, { tasks: team.tasks, t: t })] }));
|
|
244
265
|
}
|
|
245
266
|
/** Legacy conversation cards may outlive their host archive. Project their
|
|
246
267
|
* durable roster through the same rebuilt panel instead of a second UI. */
|
|
@@ -265,17 +286,14 @@ function historicCardTeam(data, owner) {
|
|
|
265
286
|
captainInbox: [],
|
|
266
287
|
};
|
|
267
288
|
}
|
|
268
|
-
|
|
269
|
-
* snapshots and historic card summaries are only shown while their captain
|
|
270
|
-
* session is the one currently open. */
|
|
271
|
-
export function ActivityPanel({ sessionsList, openSession }) {
|
|
289
|
+
export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
272
290
|
// Navigating to a member's subagent transcript is an explicit departure:
|
|
273
291
|
// hide the floater immediately instead of waiting out the autocollapse
|
|
274
292
|
// grace, so the panel never lingers over the member session.
|
|
275
|
-
const navigateToSession = (
|
|
293
|
+
const navigateToSession = (parentId, childId) => {
|
|
276
294
|
setOpen(false);
|
|
277
295
|
setWasActive(false);
|
|
278
|
-
|
|
296
|
+
openMember(parentId, childId);
|
|
279
297
|
};
|
|
280
298
|
const [open, setOpen] = useState(false);
|
|
281
299
|
const [openOwner, setOpenOwner] = useState();
|
|
@@ -383,13 +401,14 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
383
401
|
};
|
|
384
402
|
}, [compact, expanded, geometry.mode, geometry.width]);
|
|
385
403
|
useEffect(() => {
|
|
386
|
-
|
|
387
|
-
// AgentTeams conversation card registers the demand that reaches here.
|
|
388
|
-
if (currentTargets.length === 0)
|
|
404
|
+
if (current === undefined)
|
|
389
405
|
return;
|
|
390
|
-
|
|
406
|
+
// Cards keep live teams on the normal cadence. The current-session scope
|
|
407
|
+
// also performs one cold-start discovery pass so archived/cardless teams
|
|
408
|
+
// survive a browser or `dsh web` restart.
|
|
409
|
+
const controller = startActivityPolling(currentTargets, { discoverySessionId: current });
|
|
391
410
|
return () => { controller.stop(); };
|
|
392
|
-
}, [currentTargets]);
|
|
411
|
+
}, [current, currentTargets]);
|
|
393
412
|
useEffect(() => {
|
|
394
413
|
const onOpenPanel = (event) => {
|
|
395
414
|
const activeSession = currentRef.current;
|
|
@@ -570,18 +589,18 @@ export function ActivityPanel({ sessionsList, openSession }) {
|
|
|
570
589
|
};
|
|
571
590
|
if (!hasTeams && !expanded)
|
|
572
591
|
return null;
|
|
573
|
-
return (_jsxs(_Fragment, { children: [!expanded && (_jsx(CollapsedBadge, { count: visibleCount, busy: busy, onClick: () => {
|
|
592
|
+
return (_jsxs(_Fragment, { children: [!expanded && (_jsx(CollapsedBadge, { count: visibleCount, busy: busy, t: t, onClick: () => {
|
|
574
593
|
if (current === undefined)
|
|
575
594
|
return;
|
|
576
595
|
setOpenOwner(current);
|
|
577
596
|
setOpen(true);
|
|
578
|
-
} })), expanded && (_jsxs("aside", { ref: panelRef, className: css.panel, style: panelStyle, "data-agent-teams-activity": true, "data-panel-mode": geometry.mode, "data-height-mode": autoHeight ? 'auto' : 'manual', "data-compact": compact || undefined, "data-dragging": interaction === 'dragging' || undefined, "data-resizing": interaction === 'resizing' || undefined, "aria-label":
|
|
597
|
+
} })), expanded && (_jsxs("aside", { ref: panelRef, className: css.panel, style: panelStyle, "data-agent-teams-activity": true, "data-panel-mode": geometry.mode, "data-height-mode": autoHeight ? 'auto' : 'manual', "data-compact": compact || undefined, "data-dragging": interaction === 'dragging' || undefined, "data-resizing": interaction === 'resizing' || undefined, "aria-label": t('activity.panelAria'), children: [_jsxs("header", { className: css.panelHead, onPointerDown: beginMove, onPointerMove: updateGesture, onPointerUp: endGesture, onPointerCancel: cancelGesture, "data-drag-handle": !compact || undefined, children: [_jsxs("span", { className: css.panelTitle, children: [t('activity.title'), _jsx("span", { className: css.panelDot, "data-busy": busy, "aria-hidden": true })] }), _jsxs("span", { className: css.panelControls, children: [!compact && (_jsx("button", { type: "button", className: css.iconButton, "data-control": "dock", "data-mode": geometry.mode, onClick: toggleDock, "aria-label": t(geometry.mode === 'docked' ? 'activity.float' : 'activity.dockRight'), title: t(geometry.mode === 'docked' ? 'activity.float' : 'activity.dockRight'), children: _jsx(IconPanelLeftOutline16, {}) })), _jsx("button", { type: "button", className: css.iconButton, "data-control": "collapse", onClick: () => {
|
|
579
598
|
setOpen(false);
|
|
580
599
|
setOpenOwner(undefined);
|
|
581
|
-
}, "aria-label":
|
|
582
|
-
? _jsx("span", { className: css.emptyHint, children:
|
|
583
|
-
: (_jsxs(_Fragment, { children: [visibleTeams.map((team) => (_jsx(TeamSection, { team: team, onNavigate: navigateToSession }, team.teamId))), visibleArchived.map((team) => (
|
|
600
|
+
}, "aria-label": t('activity.collapse'), title: t('activity.collapse'), children: _jsx(IconChevronDownOutline14, {}) })] })] }), _jsx("div", { className: css.teams, children: visibleCount === 0
|
|
601
|
+
? _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 }) => {
|
|
584
603
|
const teamKey = `${owner}:${team.teamId}`;
|
|
585
|
-
return (_jsx(TeamSection, { team: historicCardTeam(team, owner), onNavigate: navigateToSession, historic: true }, teamKey));
|
|
604
|
+
return (_jsx(TeamSection, { team: historicCardTeam(team, owner), onNavigate: navigateToSession, t: t, historic: true }, teamKey));
|
|
586
605
|
})] })) }), !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 })] }))] }))] }));
|
|
587
606
|
}
|
|
@@ -30,7 +30,7 @@ function openActivityPanel(data) {
|
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
32
32
|
/** Render one durable team as a compact conversation card. */
|
|
33
|
-
export function AgentTeamsCard({ node,
|
|
33
|
+
export function AgentTeamsCard({ node, openMember, sessionId, t }) {
|
|
34
34
|
const data = node.data;
|
|
35
35
|
// `conversation.chat.node` is session-scoped, so its framework-owned id is
|
|
36
36
|
// a stable owner even while another conversation becomes current.
|
|
@@ -47,6 +47,8 @@ export function AgentTeamsCard({ node, openSession, sessionId }) {
|
|
|
47
47
|
teamName: snapshot?.name ?? data.teamName,
|
|
48
48
|
members: snapshot?.members.map((member) => ({ id: member.id, name: member.name, role: member.role })) ?? data.members,
|
|
49
49
|
}), [data, owner, snapshot]);
|
|
50
|
-
return (_jsxs("section", { className: css.root, "data-agent-teams-card": true, "data-team-id": resolved.teamId, children: [_jsxs("header", { className: css.head, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), _jsx("span", { className: css.teamName, title: resolved.teamName, children: resolved.teamName }),
|
|
51
|
-
|
|
50
|
+
return (_jsxs("section", { className: css.root, "data-agent-teams-card": true, "data-team-id": resolved.teamId, children: [_jsxs("header", { className: css.head, children: [_jsx("img", { className: css.leadAvatar, src: LEAD_ART, alt: "", "aria-hidden": true }), _jsx("span", { className: css.teamName, title: resolved.teamName, children: resolved.teamName }), _jsx("span", { className: css.memberCount, children: t('card.memberCount', { count: resolved.members.length }) }), _jsx("button", { type: "button", className: css.panelButton, onClick: () => { openActivityPanel(resolved); }, "aria-label": t('action.openActivityPanel'), title: t('action.openActivityPanel'), children: t('activity.panelButton') })] }), resolved.members.length > 0 && (_jsx("div", { className: css.members, children: resolved.members.map((member) => (_jsxs("button", { type: "button", className: css.member, onClick: () => {
|
|
51
|
+
if (member.id !== '')
|
|
52
|
+
openMember(owner, member.id);
|
|
53
|
+
}, title: member.role === '' ? member.name : `${member.name} · ${member.role}`, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, children: member.name.trim().slice(0, 1).toUpperCase() || '?' })), _jsx("span", { className: css.memberName, children: member.name })] }, member.id))) }))] }));
|
|
52
54
|
}
|
|
@@ -104,12 +104,17 @@ export const ACTIVITY_STATE_URL = '/plugins/dsh-agent-teams/state';
|
|
|
104
104
|
/**
|
|
105
105
|
* Start the single polling loop for the current session's requested targets.
|
|
106
106
|
*
|
|
107
|
-
* With
|
|
108
|
-
*
|
|
109
|
-
*
|
|
107
|
+
* With neither targets nor a discovery session this is deliberately inert.
|
|
108
|
+
* A discovery session performs one live+archive pass after selection/restart;
|
|
109
|
+
* it keeps polling only while that captain still owns a live team. This
|
|
110
|
+
* restores legacy/cardless history without turning every ordinary session
|
|
111
|
+
* into a permanent one-second filesystem scan. Explicit card targets retain
|
|
112
|
+
* the normal cadence, and archive state is refreshed when a target or a
|
|
113
|
+
* previously discovered live team disappears.
|
|
110
114
|
*/
|
|
111
115
|
export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
112
|
-
|
|
116
|
+
const discoverySessionId = runtime.discoverySessionId?.trim();
|
|
117
|
+
if (monitorTargets.length === 0 && (discoverySessionId === undefined || discoverySessionId === '')) {
|
|
113
118
|
return { firstTick: Promise.resolve(), stop: () => { } };
|
|
114
119
|
}
|
|
115
120
|
const fetchState = runtime.fetchState ?? ((url, init) => fetch(url, init));
|
|
@@ -119,10 +124,16 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
119
124
|
const settleTargets = runtime.settleTargets ?? settleActivityMonitorTargets;
|
|
120
125
|
let cancelled = false;
|
|
121
126
|
let inFlight = false;
|
|
127
|
+
let discoveryComplete = false;
|
|
128
|
+
let discoveredLiveKeys = new Set();
|
|
122
129
|
let controller;
|
|
123
130
|
const tick = async () => {
|
|
124
131
|
if (inFlight || cancelled)
|
|
125
132
|
return;
|
|
133
|
+
// A cardless ordinary or archive-only session needs one recovery pass,
|
|
134
|
+
// then stays dormant until the component is recreated for another session.
|
|
135
|
+
if (discoveryComplete && monitorTargets.length === 0 && discoveredLiveKeys.size === 0)
|
|
136
|
+
return;
|
|
126
137
|
inFlight = true;
|
|
127
138
|
controller = new AbortController();
|
|
128
139
|
try {
|
|
@@ -137,11 +148,24 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
137
148
|
return;
|
|
138
149
|
const liveTeams = body.teams;
|
|
139
150
|
publishSnapshots({ teams: liveTeams });
|
|
151
|
+
const previousDiscoveredKeys = discoveredLiveKeys;
|
|
152
|
+
discoveredLiveKeys = new Set(discoverySessionId === undefined || discoverySessionId === ''
|
|
153
|
+
? []
|
|
154
|
+
: liveTeams
|
|
155
|
+
.filter((team) => team.captainSessionId === discoverySessionId)
|
|
156
|
+
.map((team) => team.teamId));
|
|
157
|
+
const discoveredTeamArchived = [...previousDiscoveredKeys]
|
|
158
|
+
.some((teamId) => !discoveredLiveKeys.has(teamId));
|
|
140
159
|
const missing = monitorTargets.filter((target) => !liveTeams.some((team) => team.captainSessionId === target.sessionId && team.teamId === target.teamId));
|
|
141
|
-
|
|
160
|
+
const needsDiscoveryArchive = discoverySessionId !== undefined
|
|
161
|
+
&& discoverySessionId !== ''
|
|
162
|
+
&& !discoveryComplete;
|
|
163
|
+
if (missing.length === 0 && !needsDiscoveryArchive && !discoveredTeamArchived)
|
|
142
164
|
return;
|
|
143
|
-
// Archives are immutable. A successful fallback
|
|
144
|
-
// target, including legacy cards whose
|
|
165
|
+
// Archives are immutable per team generation. A successful fallback
|
|
166
|
+
// retires every missing explicit target, including legacy cards whose
|
|
167
|
+
// host archive no longer exists; discovery remains available from the
|
|
168
|
+
// shared snapshot after this controller becomes dormant.
|
|
145
169
|
const archivedResponse = await fetchState(`${ACTIVITY_STATE_URL}?archived=1`, {
|
|
146
170
|
cache: 'no-store',
|
|
147
171
|
signal: controller.signal,
|
|
@@ -152,6 +176,7 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
152
176
|
if (cancelled || !Array.isArray(archivedBody.teams))
|
|
153
177
|
return;
|
|
154
178
|
publishSnapshots({ archivedTeams: archivedBody.teams });
|
|
179
|
+
discoveryComplete = true;
|
|
155
180
|
settleTargets(new Set(missing.map((target) => target.key)));
|
|
156
181
|
}
|
|
157
182
|
catch (error) {
|
package/lib/client/index.js
CHANGED
|
@@ -2,8 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { ActivityPanel } from "./ActivityPanel.js";
|
|
3
3
|
import { AgentTeamsCard } from "./AgentTeamsCard.js";
|
|
4
4
|
import { agentTeamsCardDefinition } from "./agent-teams-card-definition.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { AGENT_TEAMS_LOCALE_NAMESPACE, en, zh, } from "./locales.js";
|
|
6
|
+
import { openAgentTeamMember } from "./session-navigation.js";
|
|
7
|
+
/** Required services: conversation nodes, slots, sessions navigation, and locale. */
|
|
8
|
+
export const inject = ['conversationEvents', 'slots', 'sessions', 'locale'];
|
|
7
9
|
/** The replayed user message is the canonical transcript entry. */
|
|
8
10
|
function HiddenAgentTeamsCommand() {
|
|
9
11
|
return null;
|
|
@@ -14,12 +16,19 @@ function HiddenAgentTeamsCommand() {
|
|
|
14
16
|
* monitor via a window event — the recovery path for an old session.
|
|
15
17
|
*/
|
|
16
18
|
export function apply(ctx) {
|
|
17
|
-
|
|
19
|
+
ctx.effect(() => ctx.locale.register(AGENT_TEAMS_LOCALE_NAMESPACE, { zh, en }), 'agent-teams: dictionaries');
|
|
20
|
+
const openMember = (parentId, childId) => {
|
|
21
|
+
void openAgentTeamMember(ctx.sessions, parentId, childId).catch((error) => {
|
|
22
|
+
console.warn(`agent-teams: failed to open member transcript ${childId}: ${String(error)}`);
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const Panel = ({ t }) => (_jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openMember: openMember, t: t }));
|
|
18
26
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
19
27
|
name: 'shell.overlay',
|
|
20
28
|
id: 'agent-teams-activity',
|
|
21
29
|
order: 80,
|
|
22
30
|
label: 'AgentTeams activity',
|
|
31
|
+
locale: AGENT_TEAMS_LOCALE_NAMESPACE,
|
|
23
32
|
}, Panel));
|
|
24
33
|
// The host command is only the slash-menu/admission surface. Its input is
|
|
25
34
|
// replayed as the visible user message, so the generic result row would be
|
|
@@ -32,8 +41,9 @@ export function apply(ctx) {
|
|
|
32
41
|
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
33
42
|
name: 'conversation.chat.node',
|
|
34
43
|
key: 'agent-teams',
|
|
44
|
+
locale: AGENT_TEAMS_LOCALE_NAMESPACE,
|
|
35
45
|
inject: () => ({
|
|
36
|
-
|
|
46
|
+
openMember,
|
|
37
47
|
}),
|
|
38
48
|
}, AgentTeamsCard));
|
|
39
49
|
}
|