@nanmicoder/dsh-agent-teams 0.1.11 → 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 +76 -61
- package/lib/client/AgentTeamsCard.js +2 -2
- package/lib/client/index.js +7 -3
- package/lib/client/locales.js +165 -0
- package/lib/client.js +304 -118
- package/lib/client.js.map +1 -1
- package/lib/types/client/ActivityPanel.d.ts +4 -2
- package/lib/types/client/AgentTeamsCard.d.ts +1 -1
- package/lib/types/client/index.d.ts +8 -1
- package/lib/types/client/locales.d.ts +169 -0
- package/package.json +1 -1
- 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,37 +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
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: () => {
|
|
240
258
|
if (member.id !== '') {
|
|
241
259
|
onNavigate(team.captainSessionId, member.id);
|
|
242
260
|
}
|
|
243
|
-
}, children: [_jsxs("span", { className: css.memberAvatar, "data-unread": member.unread > 0, children: [memberArtUrl(member.name, member.role) !== null ? (_jsx("img", { className: css.memberArt, src: memberArtUrl(member.name, member.role) ?? '', alt: "", "aria-hidden": true })) : (_jsx("span", { className: css.memberInitial, style: { background: accentOf(member.id) }, children: memberInitial(member.name) })), _jsx("img", { className: css.stateArt, "data-activity": member.activity, src: ACTION_ART[member.activity], alt: "", "aria-hidden": true })] }), _jsxs("span", { className: css.memberInfo, children: [_jsxs("span", { className: css.memberLine, children: [_jsx("span", { className: css.memberName, children: member.name }), member.role !== '' && _jsx("span", { className: css.memberRole, children: member.role }), _jsxs("span", { className: css.memberState, "data-activity": member.activity, children: [_jsx(WorkGlyph, { active: member.activity === 'working' }), memberStateLabel(member, team.tasks, historic)] })] }), _jsx("span", { className: css.memberStatusLine, children: memberStatusText(member, team.tasks) })] }), _jsxs("span", { className: css.memberCount, children: [member.done, "/", member.total] })] }), _jsxs("div", { className: css.assignmentLine, children: [_jsx("span", { className: css.assignmentLabel, children:
|
|
244
|
-
? _jsx("span", { className: css.taskEmpty, children:
|
|
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') })
|
|
245
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));
|
|
246
|
-
})] })] }), _jsx(DependencyMap, { tasks: team.tasks })] }));
|
|
264
|
+
})] })] }), _jsx(DependencyMap, { tasks: team.tasks, t: t })] }));
|
|
247
265
|
}
|
|
248
266
|
/** Legacy conversation cards may outlive their host archive. Project their
|
|
249
267
|
* durable roster through the same rebuilt panel instead of a second UI. */
|
|
@@ -268,10 +286,7 @@ function historicCardTeam(data, owner) {
|
|
|
268
286
|
captainInbox: [],
|
|
269
287
|
};
|
|
270
288
|
}
|
|
271
|
-
|
|
272
|
-
* snapshots and historic card summaries are only shown while their captain
|
|
273
|
-
* session is the one currently open. */
|
|
274
|
-
export function ActivityPanel({ sessionsList, openMember }) {
|
|
289
|
+
export function ActivityPanel({ sessionsList, openMember, t }) {
|
|
275
290
|
// Navigating to a member's subagent transcript is an explicit departure:
|
|
276
291
|
// hide the floater immediately instead of waiting out the autocollapse
|
|
277
292
|
// grace, so the panel never lingers over the member session.
|
|
@@ -574,18 +589,18 @@ export function ActivityPanel({ sessionsList, openMember }) {
|
|
|
574
589
|
};
|
|
575
590
|
if (!hasTeams && !expanded)
|
|
576
591
|
return null;
|
|
577
|
-
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: () => {
|
|
578
593
|
if (current === undefined)
|
|
579
594
|
return;
|
|
580
595
|
setOpenOwner(current);
|
|
581
596
|
setOpen(true);
|
|
582
|
-
} })), 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: () => {
|
|
583
598
|
setOpen(false);
|
|
584
599
|
setOpenOwner(undefined);
|
|
585
|
-
}, "aria-label":
|
|
586
|
-
? _jsx("span", { className: css.emptyHint, children:
|
|
587
|
-
: (_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 }) => {
|
|
588
603
|
const teamKey = `${owner}:${team.teamId}`;
|
|
589
|
-
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));
|
|
590
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 })] }))] }))] }));
|
|
591
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, openMember, sessionId }) {
|
|
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,7 +47,7 @@ export function AgentTeamsCard({ node, openMember, 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 }),
|
|
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
51
|
if (member.id !== '')
|
|
52
52
|
openMember(owner, member.id);
|
|
53
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))) }))] }));
|
package/lib/client/index.js
CHANGED
|
@@ -2,9 +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
|
+
import { AGENT_TEAMS_LOCALE_NAMESPACE, en, zh, } from "./locales.js";
|
|
5
6
|
import { openAgentTeamMember } from "./session-navigation.js";
|
|
6
|
-
/** Required services: conversation nodes, slots,
|
|
7
|
-
export const inject = ['conversationEvents', 'slots', 'sessions'];
|
|
7
|
+
/** Required services: conversation nodes, slots, sessions navigation, and locale. */
|
|
8
|
+
export const inject = ['conversationEvents', 'slots', 'sessions', 'locale'];
|
|
8
9
|
/** The replayed user message is the canonical transcript entry. */
|
|
9
10
|
function HiddenAgentTeamsCommand() {
|
|
10
11
|
return null;
|
|
@@ -15,17 +16,19 @@ function HiddenAgentTeamsCommand() {
|
|
|
15
16
|
* monitor via a window event — the recovery path for an old session.
|
|
16
17
|
*/
|
|
17
18
|
export function apply(ctx) {
|
|
19
|
+
ctx.effect(() => ctx.locale.register(AGENT_TEAMS_LOCALE_NAMESPACE, { zh, en }), 'agent-teams: dictionaries');
|
|
18
20
|
const openMember = (parentId, childId) => {
|
|
19
21
|
void openAgentTeamMember(ctx.sessions, parentId, childId).catch((error) => {
|
|
20
22
|
console.warn(`agent-teams: failed to open member transcript ${childId}: ${String(error)}`);
|
|
21
23
|
});
|
|
22
24
|
};
|
|
23
|
-
const Panel = () => _jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openMember: openMember });
|
|
25
|
+
const Panel = ({ t }) => (_jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openMember: openMember, t: t }));
|
|
24
26
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
25
27
|
name: 'shell.overlay',
|
|
26
28
|
id: 'agent-teams-activity',
|
|
27
29
|
order: 80,
|
|
28
30
|
label: 'AgentTeams activity',
|
|
31
|
+
locale: AGENT_TEAMS_LOCALE_NAMESPACE,
|
|
29
32
|
}, Panel));
|
|
30
33
|
// The host command is only the slash-menu/admission surface. Its input is
|
|
31
34
|
// replayed as the visible user message, so the generic result row would be
|
|
@@ -38,6 +41,7 @@ export function apply(ctx) {
|
|
|
38
41
|
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
|
|
39
42
|
name: 'conversation.chat.node',
|
|
40
43
|
key: 'agent-teams',
|
|
44
|
+
locale: AGENT_TEAMS_LOCALE_NAMESPACE,
|
|
41
45
|
inject: () => ({
|
|
42
46
|
openMember,
|
|
43
47
|
}),
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** `agentTeams` namespace dictionaries for every plugin-owned Web surface. */
|
|
2
|
+
/** Dictionary namespace owned by the AgentTeams client plugin. */
|
|
3
|
+
export const AGENT_TEAMS_LOCALE_NAMESPACE = 'agentTeams';
|
|
4
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
5
|
+
export const zh = {
|
|
6
|
+
'card.memberCount': '{count} 名成员',
|
|
7
|
+
'action.openActivityPanel': '打开活动面板',
|
|
8
|
+
'activity.panelButton': '活动面板',
|
|
9
|
+
'activity.badgeAria': 'AgentTeams 活动,{count} 个团队',
|
|
10
|
+
'activity.panelAria': 'AgentTeams 活动面板',
|
|
11
|
+
'activity.title': 'AgentTeams 活动',
|
|
12
|
+
'activity.float': '切换为浮动面板',
|
|
13
|
+
'activity.dockRight': '停靠到右侧',
|
|
14
|
+
'activity.collapse': '收起活动面板',
|
|
15
|
+
'activity.empty': '暂无团队活动',
|
|
16
|
+
'format.listSeparator': '、',
|
|
17
|
+
'task.status.pending': '待领取',
|
|
18
|
+
'task.status.claimed': '已认领',
|
|
19
|
+
'task.status.inProgress': '进行中',
|
|
20
|
+
'task.status.completed': '已完成',
|
|
21
|
+
'task.status.failed': '失败',
|
|
22
|
+
'task.status.cancelled': '已取消',
|
|
23
|
+
'member.state.working': '工作中',
|
|
24
|
+
'member.state.failed': '有失败',
|
|
25
|
+
'member.state.waiting': '等待',
|
|
26
|
+
'member.state.delivered': '已交付',
|
|
27
|
+
'member.state.left': '已离队',
|
|
28
|
+
'member.state.removed': '已移除',
|
|
29
|
+
'member.state.pending': '待执行',
|
|
30
|
+
'member.state.unassigned': '待派工',
|
|
31
|
+
'member.status.executing': '正在执行 {taskId}',
|
|
32
|
+
'member.status.working': '正在处理已派任务',
|
|
33
|
+
'member.status.waitingOn': '等待 {taskId} · {assignee}',
|
|
34
|
+
'member.status.waitingPrerequisite': '等待前置任务',
|
|
35
|
+
'member.status.waitingAssignment': '等待队长派工',
|
|
36
|
+
'member.status.delivered': '任务已交付',
|
|
37
|
+
'member.status.idle': '待继续执行',
|
|
38
|
+
'member.status.unknown': '状态未知',
|
|
39
|
+
'task.assignee.unclaimed': '待认领',
|
|
40
|
+
'task.summary.waitingBreakdown': '等待队长拆解任务',
|
|
41
|
+
'task.summary.allDelivered': '全部 {count} 项任务已交付',
|
|
42
|
+
'task.summary.blockedAndRunning': '{tasks}{more} 等待前置,其余已开工',
|
|
43
|
+
'task.summary.more': ' 等 {count} 项',
|
|
44
|
+
'task.summary.running': '{tasks} 正在执行',
|
|
45
|
+
'task.summary.ready': '{tasks} 已就绪待开工',
|
|
46
|
+
'task.summary.blocked': '{tasks} 等待前置',
|
|
47
|
+
'task.summary.waitingSchedule': '等待下一轮调度',
|
|
48
|
+
'progress.aria': '团队总进度',
|
|
49
|
+
'progress.title': '总进度',
|
|
50
|
+
'progress.running': '■ 进行中 {count}',
|
|
51
|
+
'progress.blocked': '■ 等待依赖 {count}',
|
|
52
|
+
'progress.delivered': '■ 已交付 {count}',
|
|
53
|
+
'dependency.aria': '任务依赖链',
|
|
54
|
+
'dependency.parallel': '并行任务',
|
|
55
|
+
'dependency.title': '任务依赖',
|
|
56
|
+
'dependency.hint.parallel': '无前后依赖 · 点击查看详情',
|
|
57
|
+
'dependency.hint.chain': '悬停高亮依赖链 · 点击固定',
|
|
58
|
+
'dependency.hint.pinned': '{taskId} 已固定 · Esc 取消',
|
|
59
|
+
'task.runningAria': '运行中',
|
|
60
|
+
'task.detail.completed': '已完成并交付',
|
|
61
|
+
'task.detail.noPrerequisite': '无前置,可立即开工',
|
|
62
|
+
'task.detail.ready': '前置已就绪,可开工',
|
|
63
|
+
'task.detail.waitingOn': '等待 {tasks}',
|
|
64
|
+
'task.detail.noDownstream': '无下游任务',
|
|
65
|
+
'task.detail.unlocks': '完成后解锁 {tasks}',
|
|
66
|
+
'team.ended': '已结束',
|
|
67
|
+
'team.stats.members': '{count} 名成员',
|
|
68
|
+
'team.stats.completed': '{completed}/{total} 完成',
|
|
69
|
+
'team.stats.messages': '{count} 条消息',
|
|
70
|
+
'delegation.aria': '队长派工关系',
|
|
71
|
+
'captain.name': '队长',
|
|
72
|
+
'captain.role': '拆解 · 派发 · 汇总',
|
|
73
|
+
'captain.summary': '已派发 {tasks} 项任务给 {members} 名成员',
|
|
74
|
+
'captain.state.working': '{count} 人执行中',
|
|
75
|
+
'captain.state.collected': '已收齐',
|
|
76
|
+
'captain.state.waiting': '等待回报',
|
|
77
|
+
'members.toggle': '{count} 名成员',
|
|
78
|
+
'members.collapse': '收起',
|
|
79
|
+
'members.expand': '展开',
|
|
80
|
+
'members.empty': '暂无成员,等待队长组建团队',
|
|
81
|
+
'assignment.label': '队长派发',
|
|
82
|
+
'assignment.empty': '暂无任务',
|
|
83
|
+
'archive.label': '已结束 · 历史归档',
|
|
84
|
+
};
|
|
85
|
+
/** English dictionary, checked complete against the Chinese source key set. */
|
|
86
|
+
export const en = {
|
|
87
|
+
'card.memberCount': '{count} members',
|
|
88
|
+
'action.openActivityPanel': 'Open activity panel',
|
|
89
|
+
'activity.panelButton': 'Activity panel',
|
|
90
|
+
'activity.badgeAria': 'AgentTeams activity, {count} teams',
|
|
91
|
+
'activity.panelAria': 'AgentTeams activity panel',
|
|
92
|
+
'activity.title': 'AgentTeams activity',
|
|
93
|
+
'activity.float': 'Switch to floating panel',
|
|
94
|
+
'activity.dockRight': 'Dock to the right',
|
|
95
|
+
'activity.collapse': 'Collapse activity panel',
|
|
96
|
+
'activity.empty': 'No team activity',
|
|
97
|
+
'format.listSeparator': ', ',
|
|
98
|
+
'task.status.pending': 'Unclaimed',
|
|
99
|
+
'task.status.claimed': 'Claimed',
|
|
100
|
+
'task.status.inProgress': 'In progress',
|
|
101
|
+
'task.status.completed': 'Completed',
|
|
102
|
+
'task.status.failed': 'Failed',
|
|
103
|
+
'task.status.cancelled': 'Cancelled',
|
|
104
|
+
'member.state.working': 'Working',
|
|
105
|
+
'member.state.failed': 'Has failures',
|
|
106
|
+
'member.state.waiting': 'Waiting',
|
|
107
|
+
'member.state.delivered': 'Delivered',
|
|
108
|
+
'member.state.left': 'Left team',
|
|
109
|
+
'member.state.removed': 'Removed',
|
|
110
|
+
'member.state.pending': 'Pending',
|
|
111
|
+
'member.state.unassigned': 'Awaiting assignment',
|
|
112
|
+
'member.status.executing': 'Working on {taskId}',
|
|
113
|
+
'member.status.working': 'Working on assigned tasks',
|
|
114
|
+
'member.status.waitingOn': 'Waiting for {taskId} · {assignee}',
|
|
115
|
+
'member.status.waitingPrerequisite': 'Waiting for prerequisites',
|
|
116
|
+
'member.status.waitingAssignment': 'Waiting for the captain to assign work',
|
|
117
|
+
'member.status.delivered': 'Tasks delivered',
|
|
118
|
+
'member.status.idle': 'Ready to continue',
|
|
119
|
+
'member.status.unknown': 'Status unknown',
|
|
120
|
+
'task.assignee.unclaimed': 'Unclaimed',
|
|
121
|
+
'task.summary.waitingBreakdown': 'Waiting for the captain to break down the work',
|
|
122
|
+
'task.summary.allDelivered': 'All {count} tasks delivered',
|
|
123
|
+
'task.summary.blockedAndRunning': '{tasks}{more} waiting on prerequisites; other work has started',
|
|
124
|
+
'task.summary.more': ' and {count} more',
|
|
125
|
+
'task.summary.running': '{tasks} in progress',
|
|
126
|
+
'task.summary.ready': '{tasks} ready to start',
|
|
127
|
+
'task.summary.blocked': '{tasks} waiting on prerequisites',
|
|
128
|
+
'task.summary.waitingSchedule': 'Waiting for the next scheduling round',
|
|
129
|
+
'progress.aria': 'Overall team progress',
|
|
130
|
+
'progress.title': 'Overall progress',
|
|
131
|
+
'progress.running': '■ In progress {count}',
|
|
132
|
+
'progress.blocked': '■ Waiting {count}',
|
|
133
|
+
'progress.delivered': '■ Delivered {count}',
|
|
134
|
+
'dependency.aria': 'Task dependency chain',
|
|
135
|
+
'dependency.parallel': 'Parallel tasks',
|
|
136
|
+
'dependency.title': 'Task dependencies',
|
|
137
|
+
'dependency.hint.parallel': 'No dependencies · Click for details',
|
|
138
|
+
'dependency.hint.chain': 'Hover to highlight dependencies · Click to pin',
|
|
139
|
+
'dependency.hint.pinned': '{taskId} pinned · Esc to clear',
|
|
140
|
+
'task.runningAria': 'Running',
|
|
141
|
+
'task.detail.completed': 'Completed and delivered',
|
|
142
|
+
'task.detail.noPrerequisite': 'No prerequisites; ready to start',
|
|
143
|
+
'task.detail.ready': 'Prerequisites ready; can start',
|
|
144
|
+
'task.detail.waitingOn': 'Waiting for {tasks}',
|
|
145
|
+
'task.detail.noDownstream': 'No downstream tasks',
|
|
146
|
+
'task.detail.unlocks': 'Unlocks {tasks} when complete',
|
|
147
|
+
'team.ended': 'Ended',
|
|
148
|
+
'team.stats.members': '{count} members',
|
|
149
|
+
'team.stats.completed': '{completed}/{total} completed',
|
|
150
|
+
'team.stats.messages': '{count} messages',
|
|
151
|
+
'delegation.aria': 'Captain delegation map',
|
|
152
|
+
'captain.name': 'Captain',
|
|
153
|
+
'captain.role': 'Break down · Delegate · Synthesize',
|
|
154
|
+
'captain.summary': 'Assigned {tasks} tasks to {members} members',
|
|
155
|
+
'captain.state.working': '{count} active',
|
|
156
|
+
'captain.state.collected': 'All reports received',
|
|
157
|
+
'captain.state.waiting': 'Waiting for reports',
|
|
158
|
+
'members.toggle': 'Members {count}',
|
|
159
|
+
'members.collapse': 'Collapse',
|
|
160
|
+
'members.expand': 'Expand',
|
|
161
|
+
'members.empty': 'No members yet; waiting for the captain to assemble the team',
|
|
162
|
+
'assignment.label': 'Captain assigned',
|
|
163
|
+
'assignment.empty': 'No tasks',
|
|
164
|
+
'archive.label': 'Ended · Archived history',
|
|
165
|
+
};
|