@nanmicoder/dsh-agent-teams 0.1.11 → 0.1.13
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 +5 -3
- package/README_ZH.md +5 -3
- package/lib/client/ActivityPanel.js +76 -61
- package/lib/client/AgentTeamsCard.js +2 -2
- package/lib/client/activity-monitor.js +38 -13
- package/lib/client/index.js +7 -3
- package/lib/client/locales.js +165 -0
- package/lib/client.js +337 -127
- package/lib/client.js.map +1 -1
- package/lib/index.js +3 -3
- package/lib/scheduler.js +49 -11
- package/lib/types/client/ActivityPanel.d.ts +4 -2
- package/lib/types/client/AgentTeamsCard.d.ts +1 -1
- package/lib/types/client/activity-monitor.d.ts +18 -6
- package/lib/types/client/index.d.ts +8 -1
- package/lib/types/client/locales.d.ts +169 -0
- package/lib/types/scheduler.d.ts +4 -1
- package/package.json +1 -1
- package/release-notes/v0.1.12.md +54 -0
- package/release-notes/v0.1.13.md +60 -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
|
|
@@ -76,7 +78,7 @@ Then ask for a team directly:
|
|
|
76
78
|
1. The current session creates a team and becomes its captain.
|
|
77
79
|
2. The captain adds role-specific members backed by continuable sub-agents.
|
|
78
80
|
3. The goal becomes tasks with owners and explicit dependencies.
|
|
79
|
-
4. The shared scheduler uses real `running / idle / ready` state to atomically claim one ready task per idle member and wake it.
|
|
81
|
+
4. The shared scheduler uses real `running / idle / ready` state to atomically claim one ready task per idle member and wake it. An interrupted resident attempt stays parked and can resume through a direct message without losing its capability; after a cold process restart, the scheduler retries stranded open work with a fresh attempt.
|
|
80
82
|
5. Members update with the current `attempt_id`; reassignment or captain takeover revokes the old attempt and waits for the old worker to quiesce before a new attempt starts.
|
|
81
83
|
6. The captain presents the combined result, then archives the complete team record.
|
|
82
84
|
|
|
@@ -127,7 +129,7 @@ Defaults work without extra setup. A trusted profile can override member behavio
|
|
|
127
129
|
## Boundaries
|
|
128
130
|
|
|
129
131
|
- One captain leads one active team at a time.
|
|
130
|
-
- Idle members are automatically reused for ready work; messages that cannot be delivered live remain durable and are retried at a later status boundary.
|
|
132
|
+
- Idle members with no open task are automatically reused for ready work. An idle member that still owns an open attempt is parked until messaged or explicitly reassigned; messages that cannot be delivered live remain durable and are retried at a later status boundary.
|
|
131
133
|
- State is file-backed and serialized within one DSH process; concurrent processes editing the same team are not coordinated.
|
|
132
134
|
- The activity panel reports persisted state as-is. Models may occasionally finish work without performing the expected task-state update.
|
|
133
135
|
|
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
|
### 从源码构建
|
|
@@ -76,7 +78,7 @@ dsh web
|
|
|
76
78
|
1. 当前会话创建团队并成为队长。
|
|
77
79
|
2. 队长按角色添加由可续聊子 Agent 驱动的成员。
|
|
78
80
|
3. 目标被拆成有负责人和显式依赖的任务。
|
|
79
|
-
4. 共享调度器依据真实 `running / idle / ready`
|
|
81
|
+
4. 共享调度器依据真实 `running / idle / ready` 状态,为每个空闲成员原子领取一项就绪任务并唤醒它;驻留成员被中断时会停驻当前 attempt,可通过直接消息继续而不丢 capability;只有冷进程重启后的遗留任务才会生成新 attempt 恢复。
|
|
80
82
|
5. 成员携带当前 `attempt_id` 更新任务;转派或队长接管会先撤销旧 attempt、等待原成员安静,再启动新 attempt。
|
|
81
83
|
6. 队长汇总结果,随后归档完整团队记录。
|
|
82
84
|
|
|
@@ -117,7 +119,7 @@ dsh web
|
|
|
117
119
|
## 使用边界
|
|
118
120
|
|
|
119
121
|
- 一个队长同一时间只能带一个活动团队。
|
|
120
|
-
-
|
|
122
|
+
- 没有开放任务的空闲成员会自动续领就绪任务;仍持有开放 attempt 的空闲成员会停驻,队长可发消息让其沿用原 attempt 继续,或显式转派;冷重启遗留的开放任务才会生成新 attempt。暂时无法实时投递的消息会持久保存在邮箱中并在后续状态边界重投。
|
|
121
123
|
- 状态使用文件持久化,并在单个 DSH 进程内串行操作;多个进程同时修改同一团队不保证一致。
|
|
122
124
|
- 活动面板如实展示持久化状态;模型偶尔可能完成工作却没有按协议更新任务状态。
|
|
123
125
|
|
|
@@ -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))) }))] }));
|
|
@@ -99,18 +99,30 @@ export function updateActivitySnapshots(update) {
|
|
|
99
99
|
}
|
|
100
100
|
/** Poll cadence for the live host snapshot route. */
|
|
101
101
|
export const ACTIVITY_POLL_MS = 1000;
|
|
102
|
+
/**
|
|
103
|
+
* Low-frequency probe cadence while a cardless discovery session still owns
|
|
104
|
+
* no team. The probe keeps the panel able to pick up a team created later in
|
|
105
|
+
* that session (e.g. a run_code-wrapped agent_teams_create) without turning
|
|
106
|
+
* every ordinary session into a one-second filesystem scan.
|
|
107
|
+
*/
|
|
108
|
+
export const ACTIVITY_PROBE_MS = 5000;
|
|
102
109
|
/** Host route serving live and archived team snapshots. */
|
|
103
110
|
export const ACTIVITY_STATE_URL = '/plugins/dsh-agent-teams/state';
|
|
104
111
|
/**
|
|
105
112
|
* Start the single polling loop for the current session's requested targets.
|
|
106
113
|
*
|
|
107
114
|
* With neither targets nor a discovery session this is deliberately inert.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
115
|
+
* Explicit card targets poll at the live cadence from the start. A discovery
|
|
116
|
+
* session performs an immediate live+archive restore pass, then — while it
|
|
117
|
+
* still owns no team — probes on a low-frequency cadence, so a team created
|
|
118
|
+
* later in that session (e.g. a run_code-wrapped agent_teams_create) is
|
|
119
|
+
* discovered without a manual reload, without turning every ordinary session
|
|
120
|
+
* into a one-second filesystem scan. The moment a team for the discovery
|
|
121
|
+
* session appears, the controller upgrades to the live one-second cadence for
|
|
122
|
+
* the rest of its lifetime. The caller — the session view, which stops the
|
|
123
|
+
* controller when the session is no longer current — bounds the lifetime, and
|
|
124
|
+
* archive state is refreshed when a target or a previously discovered live
|
|
125
|
+
* team disappears.
|
|
114
126
|
*/
|
|
115
127
|
export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
116
128
|
const discoverySessionId = runtime.discoverySessionId?.trim();
|
|
@@ -124,16 +136,21 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
124
136
|
const settleTargets = runtime.settleTargets ?? settleActivityMonitorTargets;
|
|
125
137
|
let cancelled = false;
|
|
126
138
|
let inFlight = false;
|
|
139
|
+
// Explicit card targets are demanded work: start at the live cadence. A
|
|
140
|
+
// discovery session starts probing low-frequency and upgrades on detection.
|
|
141
|
+
let hot = monitorTargets.length > 0;
|
|
127
142
|
let discoveryComplete = false;
|
|
128
143
|
let discoveredLiveKeys = new Set();
|
|
129
144
|
let controller;
|
|
145
|
+
let timer;
|
|
146
|
+
const intervalMs = () => (hot ? ACTIVITY_POLL_MS : ACTIVITY_PROBE_MS);
|
|
147
|
+
const reschedule = () => {
|
|
148
|
+
cancel(timer);
|
|
149
|
+
timer = schedule(() => { void tick(); }, intervalMs());
|
|
150
|
+
};
|
|
130
151
|
const tick = async () => {
|
|
131
152
|
if (inFlight || cancelled)
|
|
132
153
|
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;
|
|
137
154
|
inFlight = true;
|
|
138
155
|
controller = new AbortController();
|
|
139
156
|
try {
|
|
@@ -154,6 +171,12 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
154
171
|
: liveTeams
|
|
155
172
|
.filter((team) => team.captainSessionId === discoverySessionId)
|
|
156
173
|
.map((team) => team.teamId));
|
|
174
|
+
// A discovery session found its first team: upgrade from the low-frequency
|
|
175
|
+
// probe to the live cadence for the rest of the controller lifetime.
|
|
176
|
+
if (!hot && discoveredLiveKeys.size > 0) {
|
|
177
|
+
hot = true;
|
|
178
|
+
reschedule();
|
|
179
|
+
}
|
|
157
180
|
const discoveredTeamArchived = [...previousDiscoveredKeys]
|
|
158
181
|
.some((teamId) => !discoveredLiveKeys.has(teamId));
|
|
159
182
|
const missing = monitorTargets.filter((target) => !liveTeams.some((team) => team.captainSessionId === target.sessionId && team.teamId === target.teamId));
|
|
@@ -164,8 +187,9 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
164
187
|
return;
|
|
165
188
|
// Archives are immutable per team generation. A successful fallback
|
|
166
189
|
// retires every missing explicit target, including legacy cards whose
|
|
167
|
-
// host archive no longer exists; discovery
|
|
168
|
-
//
|
|
190
|
+
// host archive no longer exists; a discovery session that already
|
|
191
|
+
// upgraded keeps polling, and a still-probing one keeps probing, so a
|
|
192
|
+
// team created later in the same session stays discoverable.
|
|
169
193
|
const archivedResponse = await fetchState(`${ACTIVITY_STATE_URL}?archived=1`, {
|
|
170
194
|
cache: 'no-store',
|
|
171
195
|
signal: controller.signal,
|
|
@@ -189,7 +213,8 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
|
|
|
189
213
|
}
|
|
190
214
|
};
|
|
191
215
|
const firstTick = tick();
|
|
192
|
-
|
|
216
|
+
if (timer === undefined)
|
|
217
|
+
timer = schedule(() => { void tick(); }, intervalMs());
|
|
193
218
|
return {
|
|
194
219
|
firstTick,
|
|
195
220
|
stop: () => {
|
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
|
}),
|