@nanmicoder/dsh-agent-teams 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -5
- package/README_ZH.md +18 -5
- package/lib/client/ActivityPanel.js +219 -50
- package/lib/client/StagingPlanEditor.js +493 -0
- package/lib/client/activity-model.js +71 -0
- package/lib/client/activity-monitor.js +1 -0
- package/lib/client/index.js +2 -2
- package/lib/client/locales.js +224 -2
- package/lib/client.js +1775 -241
- package/lib/client.js.map +1 -1
- package/lib/command.js +116 -99
- package/lib/index.js +285 -13
- package/lib/members.js +137 -16
- package/lib/profiles.js +572 -0
- package/lib/quality-gates.js +777 -0
- package/lib/scheduler.js +167 -8
- package/lib/snapshot.js +25 -1
- package/lib/state.js +116 -10
- package/lib/tools.js +1230 -38
- package/lib/types/client/ActivityPanel.d.ts +3 -1
- package/lib/types/client/StagingPlanEditor.d.ts +17 -0
- package/lib/types/client/activity-model.d.ts +67 -0
- package/lib/types/client/activity-monitor.d.ts +14 -1
- package/lib/types/client/locales.d.ts +222 -0
- package/lib/types/command.d.ts +11 -56
- package/lib/types/event-types.d.ts +35 -1
- package/lib/types/index.d.ts +9 -0
- package/lib/types/members.d.ts +48 -3
- package/lib/types/profiles.d.ts +124 -0
- package/lib/types/quality-gates.d.ts +148 -0
- package/lib/types/scheduler.d.ts +44 -0
- package/lib/types/snapshot.d.ts +18 -1
- package/lib/types/state.d.ts +8 -3
- package/lib/types/tools.d.ts +73 -9
- package/lib/types/types.d.ts +118 -0
- package/lib/types.js +11 -0
- package/package.json +10 -4
- package/release-notes/v0.1.14.md +68 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Editable pre-run roster and DAG review for staged AgentTeams plans.
|
|
4
|
+
*
|
|
5
|
+
* This leaf owns only transient form/disclosure state. Durable truth remains
|
|
6
|
+
* on the host and returns through the ordinary activity polling snapshot.
|
|
7
|
+
* @module dsh-agent-teams/client/staging-plan
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useEffect, useId, useState, useSyncExternalStore } from 'react';
|
|
10
|
+
import { Menu } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
11
|
+
import css from './ActivityPanel.module.css';
|
|
12
|
+
const PLAN_URL = '/plugins/dsh-agent-teams/plan';
|
|
13
|
+
function useDismissSuccess(feedback, setFeedback) {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (feedback?.tone !== 'success')
|
|
16
|
+
return;
|
|
17
|
+
const timeout = window.setTimeout(() => { setFeedback(undefined); }, 3_500);
|
|
18
|
+
return () => { window.clearTimeout(timeout); };
|
|
19
|
+
}, [feedback, setFeedback]);
|
|
20
|
+
}
|
|
21
|
+
async function mutatePlan(payload) {
|
|
22
|
+
const response = await fetch(PLAN_URL, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
cache: 'no-store',
|
|
25
|
+
headers: { 'content-type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(payload),
|
|
27
|
+
});
|
|
28
|
+
if (response.ok)
|
|
29
|
+
return;
|
|
30
|
+
let message = `HTTP ${response.status}`;
|
|
31
|
+
try {
|
|
32
|
+
const body = await response.json();
|
|
33
|
+
if (typeof body.error === 'string' && body.error.trim() !== '')
|
|
34
|
+
message = body.error;
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
throw new Error(message);
|
|
38
|
+
}
|
|
39
|
+
function errorMessage(error) {
|
|
40
|
+
return error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
function DisclosureChevron({ open }) {
|
|
43
|
+
return (_jsx("svg", { className: css.planChevron, "data-open": open, width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", "aria-hidden": true, children: _jsx("path", { d: "M4 2.5 7.5 6 4 9.5" }) }));
|
|
44
|
+
}
|
|
45
|
+
function Feedback({ value }) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return null;
|
|
48
|
+
return (_jsxs("span", { className: css.planFeedback, "data-tone": value.tone, role: value.tone === 'error' ? 'alert' : 'status', "aria-live": value.tone === 'error' ? 'assertive' : 'polite', children: [_jsx("span", { "aria-hidden": true, children: value.tone === 'success'
|
|
49
|
+
? _jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.8", children: _jsx("path", { d: "m2.5 6.2 2.2 2.2 4.8-5" }) })
|
|
50
|
+
: _jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.8", children: _jsx("path", { d: "M6 2.3v4.1M6 8.8v.1" }) }) }), value.message] }));
|
|
51
|
+
}
|
|
52
|
+
function routeKey(provider, model) {
|
|
53
|
+
return JSON.stringify([provider, model]);
|
|
54
|
+
}
|
|
55
|
+
const MODEL_MENU_OPEN_MODELS = 'open:models';
|
|
56
|
+
const MODEL_MENU_OPEN_EFFORT = 'open:effort';
|
|
57
|
+
const MODEL_MENU_BACK = 'navigate:back';
|
|
58
|
+
const MODEL_MENU_RETRY = 'action:retry';
|
|
59
|
+
const MODEL_MENU_DEFAULT_EFFORT = 'effort:default';
|
|
60
|
+
function modelMenuId(provider, model) {
|
|
61
|
+
return `model:${routeKey(provider, model)}`;
|
|
62
|
+
}
|
|
63
|
+
function effortMenuId(effort) {
|
|
64
|
+
return `effort:${effort}`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Thin staged-plan adapter over the official model directory. It deliberately
|
|
68
|
+
* reads only catalog metadata: choosing a member route must not change the
|
|
69
|
+
* captain session's composer model.
|
|
70
|
+
*/
|
|
71
|
+
function StagedModelPicker({ directory, provider, model, reasoningEffort, busy, onChange, t, }) {
|
|
72
|
+
const state = useSyncExternalStore(directory.store.subscribe, directory.store.getSnapshot);
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
const [pane, setPane] = useState('root');
|
|
75
|
+
const catalogRoutes = state.groups.flatMap((group) => group.models.map((candidate) => ({
|
|
76
|
+
key: routeKey(group.id, candidate.id),
|
|
77
|
+
provider: group.id,
|
|
78
|
+
providerName: group.name,
|
|
79
|
+
model: candidate,
|
|
80
|
+
})));
|
|
81
|
+
const selectedKey = routeKey(provider, model);
|
|
82
|
+
const selected = catalogRoutes.find((candidate) => candidate.key === selectedKey);
|
|
83
|
+
const efforts = selected?.model.reasoning?.efforts ?? [];
|
|
84
|
+
const currentMissing = provider !== '' && model !== '' && selected === undefined;
|
|
85
|
+
const defaultEffort = selected?.model.reasoning?.defaultEffort;
|
|
86
|
+
const effectiveEffort = reasoningEffort === '' || reasoningEffort === 'default'
|
|
87
|
+
? defaultEffort
|
|
88
|
+
: reasoningEffort;
|
|
89
|
+
const selectedEffort = efforts.find((effort) => effort.id === effectiveEffort);
|
|
90
|
+
const modelLabel = selected?.model.name
|
|
91
|
+
?? (model === '' ? t('plan.model.choose') : model);
|
|
92
|
+
const effortLabel = selectedEffort?.name
|
|
93
|
+
?? (effectiveEffort === undefined ? t('plan.model.providerDefault') : effectiveEffort);
|
|
94
|
+
const unavailable = state.status === 'error' || state.failures.length > 0;
|
|
95
|
+
const close = () => {
|
|
96
|
+
setOpen(false);
|
|
97
|
+
setPane('root');
|
|
98
|
+
};
|
|
99
|
+
const rootItems = [
|
|
100
|
+
{
|
|
101
|
+
id: MODEL_MENU_OPEN_MODELS,
|
|
102
|
+
label: (_jsxs("span", { className: css.planModelMenuRow, children: [_jsx("span", { children: t('plan.member.model') }), _jsx("strong", { children: modelLabel }), _jsx(DisclosureChevron, { open: false })] })),
|
|
103
|
+
disabled: state.status === 'loading' && catalogRoutes.length === 0,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: MODEL_MENU_OPEN_EFFORT,
|
|
107
|
+
label: (_jsxs("span", { className: css.planModelMenuRow, children: [_jsx("span", { children: t('plan.member.reasoning') }), _jsx("strong", { children: effortLabel }), _jsx(DisclosureChevron, { open: false })] })),
|
|
108
|
+
disabled: selected?.model.reasoning === undefined,
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
const modelItems = [
|
|
112
|
+
{
|
|
113
|
+
id: MODEL_MENU_BACK,
|
|
114
|
+
label: (_jsxs("span", { className: css.planModelMenuBack, children: [_jsx(DisclosureChevron, { open: false }), t('plan.model.back')] })),
|
|
115
|
+
},
|
|
116
|
+
{ type: 'separator', id: 'models:separator' },
|
|
117
|
+
];
|
|
118
|
+
if (catalogRoutes.length === 0) {
|
|
119
|
+
modelItems.push({
|
|
120
|
+
id: 'models:empty',
|
|
121
|
+
label: state.status === 'loading' ? t('plan.model.loading') : t('plan.model.empty'),
|
|
122
|
+
disabled: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
for (const group of state.groups) {
|
|
127
|
+
modelItems.push({ type: 'label', id: `provider:${group.id}`, text: group.name });
|
|
128
|
+
for (const candidate of group.models) {
|
|
129
|
+
modelItems.push({
|
|
130
|
+
id: modelMenuId(group.id, candidate.id),
|
|
131
|
+
label: candidate.name,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const effortItems = [
|
|
137
|
+
{
|
|
138
|
+
id: MODEL_MENU_BACK,
|
|
139
|
+
label: (_jsxs("span", { className: css.planModelMenuBack, children: [_jsx(DisclosureChevron, { open: false }), t('plan.model.back')] })),
|
|
140
|
+
},
|
|
141
|
+
{ type: 'separator', id: 'effort:separator' },
|
|
142
|
+
{
|
|
143
|
+
id: MODEL_MENU_DEFAULT_EFFORT,
|
|
144
|
+
label: defaultEffort === undefined
|
|
145
|
+
? t('plan.model.providerDefault')
|
|
146
|
+
: t('plan.model.modelDefault', {
|
|
147
|
+
effort: efforts.find((effort) => effort.id === defaultEffort)?.name ?? defaultEffort,
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
...efforts.map((effort) => ({
|
|
151
|
+
id: effortMenuId(effort.id),
|
|
152
|
+
label: (_jsxs("span", { className: css.planModelEffortRow, children: [_jsx("span", { children: effort.name }), effort.description !== undefined && _jsx("small", { children: effort.description })] })),
|
|
153
|
+
})),
|
|
154
|
+
];
|
|
155
|
+
const items = pane === 'models' ? modelItems : pane === 'effort' ? effortItems : rootItems;
|
|
156
|
+
const selectedId = pane === 'models'
|
|
157
|
+
? modelMenuId(provider, model)
|
|
158
|
+
: pane === 'effort'
|
|
159
|
+
? reasoningEffort === '' || reasoningEffort === 'default'
|
|
160
|
+
? MODEL_MENU_DEFAULT_EFFORT
|
|
161
|
+
: effortMenuId(reasoningEffort)
|
|
162
|
+
: undefined;
|
|
163
|
+
const choose = (id) => {
|
|
164
|
+
if (id === MODEL_MENU_OPEN_MODELS) {
|
|
165
|
+
setPane('models');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (id === MODEL_MENU_OPEN_EFFORT) {
|
|
169
|
+
setPane('effort');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (id === MODEL_MENU_BACK) {
|
|
173
|
+
setPane('root');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (id === MODEL_MENU_RETRY) {
|
|
177
|
+
void directory.load().catch(() => undefined);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const nextModel = catalogRoutes.find((candidate) => modelMenuId(candidate.provider, candidate.model.id) === id);
|
|
181
|
+
if (nextModel !== undefined) {
|
|
182
|
+
close();
|
|
183
|
+
if (nextModel.provider === provider && nextModel.model.id === model)
|
|
184
|
+
return;
|
|
185
|
+
onChange({
|
|
186
|
+
provider: nextModel.provider,
|
|
187
|
+
model: nextModel.model.id,
|
|
188
|
+
reasoningEffort: 'default',
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (id === MODEL_MENU_DEFAULT_EFFORT) {
|
|
193
|
+
close();
|
|
194
|
+
if (effectiveEffort === defaultEffort)
|
|
195
|
+
return;
|
|
196
|
+
onChange({ provider, model, reasoningEffort: 'default' });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const nextEffort = efforts.find((effort) => effortMenuId(effort.id) === id);
|
|
200
|
+
if (nextEffort === undefined)
|
|
201
|
+
return;
|
|
202
|
+
close();
|
|
203
|
+
if (nextEffort.id === reasoningEffort)
|
|
204
|
+
return;
|
|
205
|
+
onChange({ provider, model, reasoningEffort: nextEffort.id });
|
|
206
|
+
};
|
|
207
|
+
return (_jsxs("div", { className: css.planModelPicker, "data-model-directory-status": state.status, children: [_jsx(Menu, { open: open, portal: true, align: "end", compact: true, className: css.planModelMenu, items: items, footer: unavailable ? [{ id: MODEL_MENU_RETRY, label: t('plan.model.retry') }] : undefined, selectedId: selectedId, onSelect: choose, onClose: close, anchor: (_jsxs("button", { type: "button", className: css.planModelTrigger, "data-plan-model-trigger": true, "aria-label": t('plan.model.triggerAria', { model: modelLabel, effort: effortLabel }), "aria-haspopup": "menu", "aria-expanded": open, disabled: busy, onClick: () => {
|
|
208
|
+
if (open)
|
|
209
|
+
close();
|
|
210
|
+
else {
|
|
211
|
+
setPane('root');
|
|
212
|
+
setOpen(true);
|
|
213
|
+
void directory.load().catch(() => undefined);
|
|
214
|
+
}
|
|
215
|
+
}, children: [_jsxs("span", { className: css.planModelTriggerCopy, children: [_jsx("strong", { children: state.status === 'loading' && catalogRoutes.length === 0 ? t('plan.model.loading') : modelLabel }), _jsx("span", { children: effortLabel })] }), _jsx(DisclosureChevron, { open: open })] })) }), _jsx("small", { className: css.planModelHint, children: currentMissing
|
|
216
|
+
? t('plan.model.currentUnavailable', { provider, model })
|
|
217
|
+
: selected?.model.description ?? t('plan.model.route', { provider, model }) }), unavailable && (_jsxs("span", { className: css.planModelNotice, role: state.status === 'error' ? 'alert' : 'status', children: [_jsx("span", { children: state.error ?? t('plan.model.partialFailure', { count: state.failures.length }) }), _jsx("button", { type: "button", disabled: busy || state.status === 'loading', onClick: () => { void directory.load().catch(() => undefined); }, children: t('plan.model.retry') })] }))] }));
|
|
218
|
+
}
|
|
219
|
+
function StagedMemberEditor({ team, member, modelDirectory, onPendingChange, t }) {
|
|
220
|
+
const bodyId = useId();
|
|
221
|
+
const [open, setOpen] = useState(false);
|
|
222
|
+
const [role, setRole] = useState(member.role);
|
|
223
|
+
const [provider, setProvider] = useState(member.provider ?? '');
|
|
224
|
+
const [model, setModel] = useState(member.model ?? '');
|
|
225
|
+
const [reasoningEffort, setReasoningEffort] = useState(member.reasoningEffort ?? '');
|
|
226
|
+
const [executionPrompt, setExecutionPrompt] = useState(member.executionPrompt ?? '');
|
|
227
|
+
const remoteSignature = JSON.stringify([
|
|
228
|
+
member.role,
|
|
229
|
+
member.provider ?? '',
|
|
230
|
+
member.model ?? '',
|
|
231
|
+
member.reasoningEffort ?? '',
|
|
232
|
+
member.executionPrompt ?? '',
|
|
233
|
+
]);
|
|
234
|
+
const [savedSignature, setSavedSignature] = useState(remoteSignature);
|
|
235
|
+
const [busy, setBusy] = useState(false);
|
|
236
|
+
const [feedback, setFeedback] = useState();
|
|
237
|
+
useDismissSuccess(feedback, setFeedback);
|
|
238
|
+
const signature = JSON.stringify([role, provider, model, reasoningEffort, executionPrompt]);
|
|
239
|
+
const dirty = signature !== savedSignature;
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
onPendingChange(`member:${member.name}`, dirty || busy);
|
|
242
|
+
return () => { onPendingChange(`member:${member.name}`, false); };
|
|
243
|
+
}, [busy, dirty, member.name, onPendingChange]);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
setRole(member.role);
|
|
246
|
+
setProvider(member.provider ?? '');
|
|
247
|
+
setModel(member.model ?? '');
|
|
248
|
+
setReasoningEffort(member.reasoningEffort ?? '');
|
|
249
|
+
setExecutionPrompt(member.executionPrompt ?? '');
|
|
250
|
+
setSavedSignature(remoteSignature);
|
|
251
|
+
}, [member.role, member.provider, member.model, member.reasoningEffort, member.executionPrompt, remoteSignature]);
|
|
252
|
+
const markEdited = () => { setFeedback(undefined); };
|
|
253
|
+
const persist = async (selection = { provider, model, reasoningEffort }) => {
|
|
254
|
+
const nextSignature = JSON.stringify([
|
|
255
|
+
role,
|
|
256
|
+
selection.provider,
|
|
257
|
+
selection.model,
|
|
258
|
+
selection.reasoningEffort,
|
|
259
|
+
executionPrompt,
|
|
260
|
+
]);
|
|
261
|
+
setProvider(selection.provider);
|
|
262
|
+
setModel(selection.model);
|
|
263
|
+
setReasoningEffort(selection.reasoningEffort);
|
|
264
|
+
setBusy(true);
|
|
265
|
+
setFeedback(undefined);
|
|
266
|
+
try {
|
|
267
|
+
await mutatePlan({
|
|
268
|
+
sessionId: team.captainSessionId,
|
|
269
|
+
teamId: team.teamId,
|
|
270
|
+
action: 'update_member',
|
|
271
|
+
memberName: member.name,
|
|
272
|
+
role,
|
|
273
|
+
provider: selection.provider,
|
|
274
|
+
model: selection.model,
|
|
275
|
+
reasoningEffort: selection.reasoningEffort,
|
|
276
|
+
executionPrompt,
|
|
277
|
+
});
|
|
278
|
+
setSavedSignature(nextSignature);
|
|
279
|
+
setFeedback({ tone: 'success', message: t('plan.saved') });
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
setBusy(false);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
const save = async (event) => {
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
await persist();
|
|
291
|
+
};
|
|
292
|
+
const route = `${provider}/${model}`.replace(/^\//u, '');
|
|
293
|
+
return (_jsxs("article", { className: css.planCard, "data-plan-member": member.name, "data-open": open, children: [_jsxs("button", { type: "button", className: css.planCardHeader, "aria-expanded": open, "aria-controls": bodyId, onClick: () => { setOpen((current) => !current); }, children: [_jsxs("span", { className: css.planCardIdentity, children: [_jsx("strong", { children: member.name }), _jsx("span", { children: role || t('plan.member.roleFallback') })] }), _jsx("span", { className: css.planCardMeta, title: route, children: route }), dirty && _jsx("em", { className: css.planDirty, children: t('plan.unsaved') }), _jsx(DisclosureChevron, { open: open })] }), open && (_jsxs("form", { id: bodyId, className: css.planCardBody, onSubmit: (event) => { void save(event); }, children: [_jsxs("fieldset", { disabled: busy, children: [_jsxs("label", { children: [t('plan.member.role'), _jsx("input", { name: "role", value: role, onChange: (event) => { setRole(event.currentTarget.value); markEdited(); } })] }), _jsx(StagedModelPicker, { directory: modelDirectory, provider: provider, model: model, reasoningEffort: reasoningEffort, busy: busy, onChange: (selection) => { void persist(selection); }, t: t }), _jsxs("label", { children: [t('plan.member.prompt'), _jsx("textarea", { name: "executionPrompt", value: executionPrompt, onChange: (event) => { setExecutionPrompt(event.currentTarget.value); markEdited(); }, rows: 3 })] })] }), _jsxs("span", { className: css.planActions, children: [_jsx(Feedback, { value: feedback }), _jsx("button", { type: "submit", disabled: busy || !dirty || provider.trim() === '' || model.trim() === '', children: busy ? t('plan.saving') : t('plan.save') })] })] }))] }));
|
|
294
|
+
}
|
|
295
|
+
function StagedTaskEditor({ team, task, onPendingChange, t }) {
|
|
296
|
+
const bodyId = useId();
|
|
297
|
+
const taskDependencies = task.dependencies.join(', ');
|
|
298
|
+
const [open, setOpen] = useState(false);
|
|
299
|
+
const [subject, setSubject] = useState(task.subject);
|
|
300
|
+
const [description, setDescription] = useState(task.description ?? '');
|
|
301
|
+
const [assignee, setAssignee] = useState(task.assignee);
|
|
302
|
+
const [dependencies, setDependencies] = useState(taskDependencies);
|
|
303
|
+
const remoteSignature = JSON.stringify([task.subject, task.description ?? '', task.assignee, taskDependencies]);
|
|
304
|
+
const [savedSignature, setSavedSignature] = useState(remoteSignature);
|
|
305
|
+
const [busy, setBusy] = useState(false);
|
|
306
|
+
const [confirmingRemove, setConfirmingRemove] = useState(false);
|
|
307
|
+
const [feedback, setFeedback] = useState();
|
|
308
|
+
useDismissSuccess(feedback, setFeedback);
|
|
309
|
+
const signature = JSON.stringify([subject, description, assignee, dependencies]);
|
|
310
|
+
const dirty = signature !== savedSignature;
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
onPendingChange(`task:${task.id}`, dirty || busy);
|
|
313
|
+
return () => { onPendingChange(`task:${task.id}`, false); };
|
|
314
|
+
}, [busy, dirty, onPendingChange, task.id]);
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
setSubject(task.subject);
|
|
317
|
+
setDescription(task.description ?? '');
|
|
318
|
+
setAssignee(task.assignee);
|
|
319
|
+
setDependencies(taskDependencies);
|
|
320
|
+
setSavedSignature(remoteSignature);
|
|
321
|
+
}, [task.subject, task.description, task.assignee, taskDependencies, remoteSignature]);
|
|
322
|
+
const markEdited = () => {
|
|
323
|
+
setFeedback(undefined);
|
|
324
|
+
setConfirmingRemove(false);
|
|
325
|
+
};
|
|
326
|
+
const save = async (event) => {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
setBusy(true);
|
|
329
|
+
setFeedback(undefined);
|
|
330
|
+
try {
|
|
331
|
+
await mutatePlan({
|
|
332
|
+
sessionId: team.captainSessionId,
|
|
333
|
+
teamId: team.teamId,
|
|
334
|
+
action: 'update_task',
|
|
335
|
+
taskId: task.id,
|
|
336
|
+
subject,
|
|
337
|
+
description,
|
|
338
|
+
assignee,
|
|
339
|
+
dependencies: dependencies.split(',').map((item) => item.trim()).filter(Boolean),
|
|
340
|
+
});
|
|
341
|
+
setSavedSignature(signature);
|
|
342
|
+
setFeedback({ tone: 'success', message: t('plan.saved') });
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
setBusy(false);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const remove = async () => {
|
|
352
|
+
setBusy(true);
|
|
353
|
+
setFeedback(undefined);
|
|
354
|
+
try {
|
|
355
|
+
await mutatePlan({
|
|
356
|
+
sessionId: team.captainSessionId,
|
|
357
|
+
teamId: team.teamId,
|
|
358
|
+
action: 'remove_task',
|
|
359
|
+
taskId: task.id,
|
|
360
|
+
});
|
|
361
|
+
setFeedback({ tone: 'success', message: t('plan.removed') });
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
365
|
+
setBusy(false);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const dependencySummary = task.dependencies.length === 0
|
|
369
|
+
? t('plan.dependencies.none')
|
|
370
|
+
: t('plan.dependencies.count', { count: task.dependencies.length });
|
|
371
|
+
return (_jsxs("article", { className: css.planCard, "data-plan-task": task.id, "data-open": open, children: [_jsxs("button", { type: "button", className: css.planCardHeader, "aria-expanded": open, "aria-controls": bodyId, onClick: () => { setOpen((current) => !current); }, children: [_jsx("span", { className: css.planTaskId, children: task.id }), _jsx("span", { className: css.planTaskSummary, title: subject, children: subject }), _jsxs("span", { className: css.planCardMeta, children: [assignee || t('plan.task.unassigned'), " \u00B7 ", dependencySummary] }), dirty && _jsx("em", { className: css.planDirty, children: t('plan.unsaved') }), _jsx(DisclosureChevron, { open: open })] }), open && (_jsxs("form", { id: bodyId, className: css.planCardBody, onSubmit: (event) => { void save(event); }, children: [_jsxs("fieldset", { disabled: busy, children: [_jsxs("label", { children: [t('plan.task.subject'), _jsx("input", { name: "subject", required: true, value: subject, onChange: (event) => { setSubject(event.currentTarget.value); markEdited(); } })] }), _jsxs("label", { children: [t('plan.task.description'), _jsx("textarea", { name: "description", value: description, onChange: (event) => { setDescription(event.currentTarget.value); markEdited(); }, rows: 3 })] }), _jsxs("span", { className: css.planGrid, children: [_jsxs("label", { children: [t('plan.task.assignee'), _jsxs("select", { name: "assignee", value: assignee, onChange: (event) => { setAssignee(event.currentTarget.value); markEdited(); }, children: [_jsx("option", { value: "", children: t('plan.task.unassigned') }), team.members.map((member) => _jsx("option", { value: member.name, children: member.name }, member.name))] })] }), _jsxs("label", { children: [t('plan.task.dependencies'), _jsx("input", { name: "dependencies", value: dependencies, onChange: (event) => { setDependencies(event.currentTarget.value); markEdited(); } }), _jsx("small", { children: t('plan.task.dependenciesHint') })] })] })] }), confirmingRemove && (_jsxs("span", { className: css.planConfirm, role: "alert", children: [_jsx("span", { children: t('plan.removeWarning', { task: task.id }) }), _jsx("button", { type: "button", onClick: () => { setConfirmingRemove(false); }, children: t('plan.cancel') }), _jsx("button", { type: "button", "data-danger": true, "data-confirming": true, onClick: () => { void remove(); }, children: t('plan.removeConfirm') })] })), _jsxs("span", { className: css.planActions, children: [_jsx(Feedback, { value: feedback }), _jsx("button", { type: "button", "data-danger": true, onClick: () => { setConfirmingRemove(true); setFeedback(undefined); }, disabled: busy || confirmingRemove, children: t('plan.remove') }), _jsx("button", { type: "submit", disabled: busy || !dirty || subject.trim() === '', children: busy ? t('plan.saving') : t('plan.save') })] })] }))] }));
|
|
372
|
+
}
|
|
373
|
+
export function StagingPlanEditor({ team, modelDirectory, onContinuePlanning, onDiscarded, t }) {
|
|
374
|
+
const membersId = useId();
|
|
375
|
+
const tasksId = useId();
|
|
376
|
+
const [membersOpen, setMembersOpen] = useState(true);
|
|
377
|
+
const [tasksOpen, setTasksOpen] = useState(true);
|
|
378
|
+
const [newTask, setNewTask] = useState('');
|
|
379
|
+
const [busy, setBusy] = useState(false);
|
|
380
|
+
const [discardArmed, setDiscardArmed] = useState(false);
|
|
381
|
+
const [pendingEditors, setPendingEditors] = useState(new Set());
|
|
382
|
+
const [feedback, setFeedback] = useState();
|
|
383
|
+
useDismissSuccess(feedback, setFeedback);
|
|
384
|
+
const dependencyLinks = team.tasks.reduce((total, task) => total + task.dependencies.length, 0);
|
|
385
|
+
const runnable = team.members.length > 0 && team.tasks.length > 0;
|
|
386
|
+
const hasPendingEdits = pendingEditors.size > 0 || newTask.trim() !== '';
|
|
387
|
+
const waitingForFeedback = team.planReviewState === 'awaiting_feedback';
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
void modelDirectory.load().catch(() => undefined);
|
|
390
|
+
}, [modelDirectory]);
|
|
391
|
+
const onPendingChange = useCallback((key, pending) => {
|
|
392
|
+
setPendingEditors((current) => {
|
|
393
|
+
if (pending === current.has(key))
|
|
394
|
+
return current;
|
|
395
|
+
const next = new Set(current);
|
|
396
|
+
if (pending)
|
|
397
|
+
next.add(key);
|
|
398
|
+
else
|
|
399
|
+
next.delete(key);
|
|
400
|
+
return next;
|
|
401
|
+
});
|
|
402
|
+
}, []);
|
|
403
|
+
const addTask = async (event) => {
|
|
404
|
+
event.preventDefault();
|
|
405
|
+
setBusy(true);
|
|
406
|
+
setFeedback(undefined);
|
|
407
|
+
try {
|
|
408
|
+
await mutatePlan({
|
|
409
|
+
sessionId: team.captainSessionId,
|
|
410
|
+
teamId: team.teamId,
|
|
411
|
+
action: 'add_task',
|
|
412
|
+
subject: newTask,
|
|
413
|
+
dependencies: [],
|
|
414
|
+
});
|
|
415
|
+
setNewTask('');
|
|
416
|
+
setFeedback({ tone: 'success', message: t('plan.taskAdded') });
|
|
417
|
+
setTasksOpen(true);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
setBusy(false);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const approve = async () => {
|
|
427
|
+
setBusy(true);
|
|
428
|
+
setFeedback(undefined);
|
|
429
|
+
try {
|
|
430
|
+
await mutatePlan({
|
|
431
|
+
sessionId: team.captainSessionId,
|
|
432
|
+
teamId: team.teamId,
|
|
433
|
+
action: 'approve',
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
438
|
+
setBusy(false);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const continueInChat = async () => {
|
|
442
|
+
if (waitingForFeedback) {
|
|
443
|
+
onContinuePlanning();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
setBusy(true);
|
|
447
|
+
setFeedback(undefined);
|
|
448
|
+
try {
|
|
449
|
+
await mutatePlan({
|
|
450
|
+
sessionId: team.captainSessionId,
|
|
451
|
+
teamId: team.teamId,
|
|
452
|
+
action: 'continue',
|
|
453
|
+
});
|
|
454
|
+
onContinuePlanning();
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
458
|
+
setBusy(false);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const discard = async () => {
|
|
462
|
+
setBusy(true);
|
|
463
|
+
setFeedback(undefined);
|
|
464
|
+
try {
|
|
465
|
+
await mutatePlan({
|
|
466
|
+
sessionId: team.captainSessionId,
|
|
467
|
+
teamId: team.teamId,
|
|
468
|
+
action: 'discard',
|
|
469
|
+
});
|
|
470
|
+
onDiscarded();
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
setFeedback({ tone: 'error', message: t('plan.failed', { message: errorMessage(error) }) });
|
|
474
|
+
setBusy(false);
|
|
475
|
+
setDiscardArmed(false);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
return (_jsxs("section", { className: css.planEditor, "data-staging-editor": true, children: [_jsxs("header", { className: css.planHeader, children: [_jsxs("span", { children: [_jsxs("span", { children: [_jsx("strong", { children: t('plan.title') }), _jsx("small", { children: t('plan.readySummary', { members: team.members.length, tasks: team.tasks.length, links: dependencyLinks }) })] }), _jsx("em", { children: t('plan.badge') })] }), _jsx("p", { children: t('plan.description') })] }), _jsxs("ol", { className: css.planFlow, "aria-label": t('plan.flow.aria'), children: [_jsxs("li", { "data-active": true, children: [_jsx("span", { children: "1" }), t('plan.flow.review')] }), _jsxs("li", { children: [_jsx("span", { children: "2" }), t('plan.flow.spawn')] }), _jsxs("li", { children: [_jsx("span", { children: "3" }), t('plan.flow.run')] })] }), _jsxs("section", { className: css.planSection, children: [_jsxs("button", { type: "button", className: css.planSectionToggle, "aria-expanded": membersOpen, "aria-controls": membersId, onClick: () => { setMembersOpen((current) => !current); }, children: [_jsxs("span", { children: [_jsx("strong", { children: t('plan.members.title') }), _jsx("small", { children: t('plan.members.count', { count: team.members.length }) })] }), _jsx(DisclosureChevron, { open: membersOpen })] }), membersOpen && (_jsx("div", { id: membersId, className: css.planList, children: team.members.length === 0
|
|
479
|
+
? _jsx("p", { className: css.planEmpty, children: t('plan.members.empty') })
|
|
480
|
+
: team.members.map((member) => (_jsx(StagedMemberEditor, { team: team, member: member, modelDirectory: modelDirectory, onPendingChange: onPendingChange, t: t }, member.name))) }))] }), _jsxs("section", { className: css.planSection, children: [_jsxs("button", { type: "button", className: css.planSectionToggle, "aria-expanded": tasksOpen, "aria-controls": tasksId, onClick: () => { setTasksOpen((current) => !current); }, children: [_jsxs("span", { children: [_jsx("strong", { children: t('plan.tasks.title') }), _jsx("small", { children: t('plan.tasks.count', { count: team.tasks.length, links: dependencyLinks }) })] }), _jsx(DisclosureChevron, { open: tasksOpen })] }), tasksOpen && (_jsx("div", { id: tasksId, className: css.planList, children: team.tasks.length === 0
|
|
481
|
+
? _jsx("p", { className: css.planEmpty, children: t('plan.tasks.empty') })
|
|
482
|
+
: team.tasks.map((task) => _jsx(StagedTaskEditor, { team: team, task: task, onPendingChange: onPendingChange, t: t }, task.id)) }))] }), _jsxs("form", { className: css.planNewTask, onSubmit: (event) => { void addTask(event); }, children: [_jsxs("label", { children: [_jsx("span", { children: t('plan.newTaskLabel') }), _jsx("input", { name: "newTask", value: newTask, onChange: (event) => { setNewTask(event.currentTarget.value); setFeedback(undefined); }, placeholder: t('plan.newTask'), disabled: busy })] }), _jsx("button", { type: "submit", disabled: busy || newTask.trim() === '', children: busy ? t('plan.adding') : t('plan.addTask') })] }), _jsxs("div", { className: css.planApproveRow, "data-armed": discardArmed || undefined, "data-discard": discardArmed || undefined, "data-review-state": waitingForFeedback ? 'awaiting-feedback' : 'awaiting-review', children: [_jsxs("span", { className: css.planApproveCopy, children: [_jsx("strong", { children: discardArmed
|
|
483
|
+
? t('plan.discardConfirmTitle')
|
|
484
|
+
: waitingForFeedback
|
|
485
|
+
? t('plan.feedbackTitle')
|
|
486
|
+
: t('plan.approveTitle') }), _jsx("small", { children: discardArmed
|
|
487
|
+
? t('plan.discardWarning')
|
|
488
|
+
: waitingForFeedback
|
|
489
|
+
? t('plan.feedbackHint')
|
|
490
|
+
: hasPendingEdits
|
|
491
|
+
? t('plan.pendingEdits')
|
|
492
|
+
: t('plan.approveHint', { members: team.members.length, tasks: team.tasks.length }) })] }), _jsx(Feedback, { value: feedback }), discardArmed ? (_jsxs("span", { className: css.planApproveActions, children: [_jsx("button", { type: "button", disabled: busy, onClick: () => { setDiscardArmed(false); }, children: t('plan.cancel') }), _jsx("button", { type: "button", "data-plan-discard": true, "data-danger": true, "data-confirming": true, disabled: busy, onClick: () => { void discard(); }, children: busy ? t('plan.discarding') : t('plan.discardConfirm') })] })) : (_jsxs("span", { className: css.planReviewActions, children: [_jsx("button", { type: "button", "data-plan-approve": true, disabled: busy || !runnable || hasPendingEdits, onClick: () => { void approve(); }, children: t('plan.approve') }), _jsxs("span", { className: css.planSecondaryActions, children: [_jsx("button", { type: "button", "data-plan-continue": true, disabled: busy, onClick: () => { void continueInChat(); }, children: t(waitingForFeedback ? 'plan.returnToChat' : 'plan.continue') }), _jsx("button", { type: "button", "data-plan-discard": true, "data-danger": true, disabled: busy, onClick: () => { setDiscardArmed(true); setFeedback(undefined); }, children: t('plan.discard') })] })] }))] })] }));
|
|
493
|
+
}
|
|
@@ -4,6 +4,66 @@ export const COMPACT_DAG_NODE_WIDTH = 92;
|
|
|
4
4
|
export const COMPACT_DAG_NODE_HEIGHT = 30;
|
|
5
5
|
export const COMPACT_DAG_COLUMN_GAP = 26;
|
|
6
6
|
export const COMPACT_DAG_ROW_GAP = 8;
|
|
7
|
+
/** Compact `provider/model` route, or just the model when the provider is absent. */
|
|
8
|
+
export function memberRouteLabel(member) {
|
|
9
|
+
if (member === undefined)
|
|
10
|
+
return '';
|
|
11
|
+
const provider = member.provider?.trim() ?? '';
|
|
12
|
+
const model = member.model?.trim() ?? '';
|
|
13
|
+
if (provider !== '' && model !== '')
|
|
14
|
+
return `${provider}/${model}`;
|
|
15
|
+
return model;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compact route shown on a running task. Prefer the task's own snapshot
|
|
19
|
+
* field; fall back to the assignee member when older hosts omit it.
|
|
20
|
+
*/
|
|
21
|
+
export function taskModelLabel(task, members) {
|
|
22
|
+
const direct = task.model?.trim() ?? '';
|
|
23
|
+
if (direct !== '')
|
|
24
|
+
return direct;
|
|
25
|
+
return memberRouteLabel(members.find((candidate) => candidate.name === task.assignee));
|
|
26
|
+
}
|
|
27
|
+
/** Short model id for tight DAG/chip surfaces (`openai/gpt-5.6-sol` → `gpt-5.6-sol`). */
|
|
28
|
+
export function compactModelLabel(route) {
|
|
29
|
+
const trimmed = route.trim();
|
|
30
|
+
if (trimmed === '')
|
|
31
|
+
return '';
|
|
32
|
+
const slash = trimmed.lastIndexOf('/');
|
|
33
|
+
return slash === -1 ? trimmed : trimmed.slice(slash + 1);
|
|
34
|
+
}
|
|
35
|
+
/** A live team the current captain still owns and has not halted. */
|
|
36
|
+
export function liveCaptainTeam(teams, sessionId) {
|
|
37
|
+
const owner = sessionId?.trim() ?? '';
|
|
38
|
+
if (owner === '')
|
|
39
|
+
return undefined;
|
|
40
|
+
return teams.find((team) => team.captainSessionId === owner && team.halted !== true);
|
|
41
|
+
}
|
|
42
|
+
/** Whether the captain chat should keep showing the in-progress banner. */
|
|
43
|
+
export function teamIsActive(team) {
|
|
44
|
+
if (team.halted === true || team.phase === 'staged')
|
|
45
|
+
return false;
|
|
46
|
+
if (team.members.some((member) => member.activity === 'working' || member.status === 'working'))
|
|
47
|
+
return true;
|
|
48
|
+
if (team.tasks.some((task) => task.status === 'pending' || task.status === 'claimed' || task.status === 'in_progress'))
|
|
49
|
+
return true;
|
|
50
|
+
return team.members.length > 0 && team.tasks.length === 0;
|
|
51
|
+
}
|
|
52
|
+
/** Compact banner copy: running members, otherwise the current planning state. */
|
|
53
|
+
export function teamProgressSummary(team, separator) {
|
|
54
|
+
const workingMembers = team.members.filter((member) => member.activity === 'working' || member.status === 'working');
|
|
55
|
+
const runningTasks = team.tasks.filter((task) => task.status === 'claimed' || task.status === 'in_progress');
|
|
56
|
+
const labels = runningTasks.map((task) => task.subject.trim() || task.id).filter((label) => label !== '');
|
|
57
|
+
if (workingMembers.length > 0 || labels.length > 0) {
|
|
58
|
+
return {
|
|
59
|
+
working: Math.max(workingMembers.length, labels.length),
|
|
60
|
+
detail: labels.slice(0, 2).join(separator),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (team.tasks.length === 0)
|
|
64
|
+
return { working: 0, detail: '' };
|
|
65
|
+
return { working: 0, detail: '' };
|
|
66
|
+
}
|
|
7
67
|
/** Use a fill-width grid when the task graph has no real dependency edges. */
|
|
8
68
|
export function usesParallelTaskGrid(tasks) {
|
|
9
69
|
if (tasks.length === 0)
|
|
@@ -22,6 +82,17 @@ export function usesParallelTaskGrid(tasks) {
|
|
|
22
82
|
export function activityPanelExpandedForSession(open, owner, current) {
|
|
23
83
|
return open && owner !== undefined && owner === current;
|
|
24
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Auto-expand only for live teams that appear after the current session's
|
|
87
|
+
* initial restore pass. Replayed cards, archived teams, and live teams restored
|
|
88
|
+
* while reopening a conversation must remain behind the collapsed badge.
|
|
89
|
+
*/
|
|
90
|
+
export function activityPanelShouldAutoExpand({ alreadyAutoOpened, pageSettled, restoreComplete, previousLiveTeamIds, currentLiveTeamIds, }) {
|
|
91
|
+
return !alreadyAutoOpened
|
|
92
|
+
&& pageSettled
|
|
93
|
+
&& restoreComplete
|
|
94
|
+
&& currentLiveTeamIds.some((teamId) => !previousLiveTeamIds.has(teamId));
|
|
95
|
+
}
|
|
25
96
|
/**
|
|
26
97
|
* Resolve the task whose dependency chain should be highlighted.
|
|
27
98
|
*
|
|
@@ -108,6 +108,7 @@ export const ACTIVITY_POLL_MS = 1000;
|
|
|
108
108
|
export const ACTIVITY_PROBE_MS = 5000;
|
|
109
109
|
/** Host route serving live and archived team snapshots. */
|
|
110
110
|
export const ACTIVITY_STATE_URL = '/plugins/dsh-agent-teams/state';
|
|
111
|
+
export const ACTIVITY_HALT_URL = '/plugins/dsh-agent-teams/halt';
|
|
111
112
|
/**
|
|
112
113
|
* Start the single polling loop for the current session's requested targets.
|
|
113
114
|
*
|
package/lib/client/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { agentTeamsCardDefinition } from "./agent-teams-card-definition.js";
|
|
|
5
5
|
import { AGENT_TEAMS_LOCALE_NAMESPACE, en, zh, } from "./locales.js";
|
|
6
6
|
import { openAgentTeamMember } from "./session-navigation.js";
|
|
7
7
|
/** Required services: conversation nodes, slots, sessions navigation, and locale. */
|
|
8
|
-
export const inject = ['conversationEvents', 'slots', 'sessions', 'locale'];
|
|
8
|
+
export const inject = ['conversationEvents', 'slots', 'sessions', 'locale', 'modelDirectories'];
|
|
9
9
|
/** The replayed user message is the canonical transcript entry. */
|
|
10
10
|
function HiddenAgentTeamsCommand() {
|
|
11
11
|
return null;
|
|
@@ -22,7 +22,7 @@ export function apply(ctx) {
|
|
|
22
22
|
console.warn(`agent-teams: failed to open member transcript ${childId}: ${String(error)}`);
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
|
-
const Panel = ({ t }) => (_jsx(ActivityPanel, { sessionsList: ctx.sessions.list, openMember: openMember, t: t }));
|
|
25
|
+
const Panel = ({ t }) => (_jsx(ActivityPanel, { sessionsList: ctx.sessions.list, modelDirectories: ctx.modelDirectories, openMember: openMember, t: t }));
|
|
26
26
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
27
27
|
name: 'shell.overlay',
|
|
28
28
|
id: 'agent-teams-activity',
|