@nanmicoder/dsh-agent-teams 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +19 -12
  2. package/README_ZH.md +19 -12
  3. package/assets/readme/workspace.png +0 -0
  4. package/compatibility.json +14 -2
  5. package/lib/capabilities.js +3 -1
  6. package/lib/client/ActivityPanel.js +80 -51
  7. package/lib/client/AgentTeamsCard.js +14 -5
  8. package/lib/client/TeamChatEntry.js +22 -0
  9. package/lib/client/WorkspaceActivity.js +100 -0
  10. package/lib/client/activity-model.js +73 -8
  11. package/lib/client/activity-monitor.js +11 -4
  12. package/lib/client/agent-teams-card-definition.js +11 -1
  13. package/lib/client/icons.js +18 -0
  14. package/lib/client/index.js +35 -4
  15. package/lib/client/locales.js +44 -0
  16. package/lib/client/session-navigation.js +13 -1
  17. package/lib/client/workspace-state.js +43 -0
  18. package/lib/client.js +832 -210
  19. package/lib/client.js.map +1 -1
  20. package/lib/harness-compat.js +20 -3
  21. package/lib/index.js +1 -1
  22. package/lib/mailbox.js +23 -3
  23. package/lib/members.js +3 -3
  24. package/lib/quality-gates.js +54 -0
  25. package/lib/scheduler.js +8 -2
  26. package/lib/snapshot.js +1 -0
  27. package/lib/state.js +15 -3
  28. package/lib/tools.js +71 -22
  29. package/lib/types/capabilities.d.ts +0 -1
  30. package/lib/types/client/ActivityPanel.d.ts +18 -1
  31. package/lib/types/client/AgentTeamsCard.d.ts +14 -2
  32. package/lib/types/client/TeamChatEntry.d.ts +4 -0
  33. package/lib/types/client/WorkspaceActivity.d.ts +22 -0
  34. package/lib/types/client/activity-model.d.ts +37 -1
  35. package/lib/types/client/activity-monitor.d.ts +4 -0
  36. package/lib/types/client/agent-teams-card-definition.d.ts +8 -0
  37. package/lib/types/client/icons.d.ts +5 -0
  38. package/lib/types/client/locales.d.ts +44 -0
  39. package/lib/types/client/session-navigation.d.ts +13 -2
  40. package/lib/types/client/workspace-state.d.ts +19 -0
  41. package/lib/types/harness-compat.d.ts +14 -1
  42. package/lib/types/mailbox.d.ts +4 -0
  43. package/lib/types/quality-gates.d.ts +7 -1
  44. package/lib/types/scheduler.d.ts +1 -0
  45. package/lib/types/snapshot.d.ts +2 -0
  46. package/lib/types/types.d.ts +16 -0
  47. package/package.json +53 -50
  48. package/release-notes/v0.1.21-rc.1.md +14 -0
  49. package/release-notes/v0.1.21.md +23 -0
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /** Native workspace content; discovery remains mounted independently of tab lifetime. */
3
+ import { useEffect, useMemo, useState, useSyncExternalStore } from 'react';
4
+ import { ActivityPanel, TeamSection, historicCardTeam } from "./ActivityPanel.js";
5
+ import { OPEN_PANEL_EVENT } from "./AgentTeamsCard.js";
6
+ import { currentSessionId } from "./session-navigation.js";
7
+ import { getActivityMonitorTargetsSnapshot, getActivitySnapshotsSnapshot, startActivityPolling, subscribeActivityMonitorTargets, subscribeActivitySnapshots, updateActivitySnapshots } from "./activity-monitor.js";
8
+ import { createTeamDiscovery } from "./workspace-state.js";
9
+ import css from './WorkspaceActivity.module.css';
10
+ export const TEAM_TAB_KIND = 'agent-teams';
11
+ export const TEAM_TAB_ID = '@nanmicoder/dsh-agent-teams/activity';
12
+ /** Optional host integration is observable so installing/removing it also switches the fallback. */
13
+ export function createWorkspaceBridge() {
14
+ let sidebar;
15
+ const listeners = new Set();
16
+ return {
17
+ getSnapshot: () => sidebar,
18
+ subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
19
+ set(value) { sidebar = value; for (const listener of listeners)
20
+ listener(); },
21
+ };
22
+ }
23
+ export function ActivitySurface({ bridge, state, ...props }) {
24
+ const sidebar = useSyncExternalStore(bridge.subscribe, bridge.getSnapshot);
25
+ return sidebar === undefined ? _jsx(ActivityPanel, { ...props }) : _jsx(WorkspaceMonitor, { ...props, sidebar: sidebar, state: state });
26
+ }
27
+ function WorkspaceMonitor({ sessionsList, sidebar, state }) {
28
+ const current = currentSessionId(useSyncExternalStore(sessionsList.subscribe, sessionsList.getSnapshot));
29
+ const targets = useSyncExternalStore(subscribeActivityMonitorTargets, getActivityMonitorTargetsSnapshot);
30
+ const currentTargets = useMemo(() => targets.filter(target => target.sessionId === current), [targets, current]);
31
+ // One discovery tracker per visit, outside polling effects restarted by card registration.
32
+ const discover = useMemo(() => createTeamDiscovery(), [current]);
33
+ useEffect(() => {
34
+ if (current === undefined)
35
+ return;
36
+ state.status(current, 'loading');
37
+ const controller = startActivityPolling(currentTargets, {
38
+ discoverySessionId: current,
39
+ onStatus(status) {
40
+ state.status(current, status);
41
+ },
42
+ publishSnapshots(update) {
43
+ updateActivitySnapshots(update);
44
+ if (update.teams === undefined)
45
+ return;
46
+ const added = discover(update.teams.filter(team => team.captainSessionId === current));
47
+ // Do not replace a file/terminal, or navigate a session whose surface is not bound.
48
+ if (added !== undefined && sidebar.mounted.getSnapshot() === current && !sidebar.isExpanded()) {
49
+ state.select(current, added);
50
+ sidebar.openTab(TEAM_TAB_KIND);
51
+ }
52
+ },
53
+ });
54
+ return () => { controller.stop(); };
55
+ }, [current, currentTargets, discover, sidebar, state]);
56
+ useEffect(() => {
57
+ const open = (event) => {
58
+ if (current === undefined || sidebar.mounted.getSnapshot() !== current)
59
+ return;
60
+ const data = event.detail;
61
+ if (data?.captainSessionId && data.captainSessionId !== current)
62
+ return;
63
+ if (data?.teamId)
64
+ state.remember(current, data);
65
+ sidebar.openTab(TEAM_TAB_KIND);
66
+ };
67
+ window.addEventListener(OPEN_PANEL_EVENT, open);
68
+ return () => { window.removeEventListener(OPEN_PANEL_EVENT, open); };
69
+ }, [current, sidebar, state]);
70
+ return null;
71
+ }
72
+ export function WorkspaceActivity({ sessionId, useTabInfo, t, state, modelDirectories, openMember }) {
73
+ const { tab } = useTabInfo();
74
+ const snapshots = useSyncExternalStore(subscribeActivitySnapshots, getActivitySnapshotsSnapshot);
75
+ const local = useSyncExternalStore(state.subscribe, state.getSnapshot);
76
+ const [retry, setRetry] = useState(0);
77
+ const live = snapshots.teams.filter(team => team.captainSessionId === sessionId);
78
+ const archived = snapshots.archivedTeams.filter(team => team.captainSessionId === sessionId && !live.some(item => item.teamId === team.teamId));
79
+ const historic = [...local.history.values()].filter(team => team.captainSessionId === sessionId
80
+ && !live.some(item => item.teamId === team.teamId) && !archived.some(item => item.teamId === team.teamId))
81
+ .map(team => historicCardTeam(team, sessionId));
82
+ const records = [...live, ...archived, ...historic];
83
+ const selected = records.find(team => team.teamId === local.selected.get(sessionId)) ?? records[0];
84
+ const history = selected !== undefined && !live.includes(selected);
85
+ const status = local.statuses.get(sessionId) ?? 'loading';
86
+ // Explicit reconnect is bounded and cleaned up; ordinary polling is owned by the monitor.
87
+ useEffect(() => {
88
+ if (retry === 0 || !tab.visible)
89
+ return;
90
+ const controller = startActivityPolling([], { discoverySessionId: sessionId, onStatus: value => state.status(sessionId, value) });
91
+ void controller.firstTick.finally(() => { controller.stop(); });
92
+ return () => { controller.stop(); };
93
+ }, [retry, sessionId, state, tab.visible]);
94
+ const backToChat = () => {
95
+ // Closing this occurrence does not cancel a team or destroy other workspace tabs.
96
+ tab.actions.close();
97
+ requestAnimationFrame(() => document.querySelector('[data-composer-card] [contenteditable="true"][role="textbox"], [data-composer-card] textarea')?.focus());
98
+ };
99
+ return (_jsx("div", { className: css.root, "data-agent-teams-workspace": true, "data-session-id": sessionId, children: _jsxs("div", { className: css.content, children: [status === 'error' && _jsxs("div", { className: css.error, role: "alert", children: [_jsx("span", { children: t('workspace.error') }), _jsx("button", { onClick: () => setRetry(value => value + 1), children: t('workspace.retry') })] }), records.length > 1 && _jsx("nav", { className: css.selector, "aria-label": t('workspace.title'), children: records.map(team => _jsxs("button", { "aria-pressed": selected?.teamId === team.teamId, onClick: () => state.select(sessionId, team.teamId), children: [team.name, _jsx("span", { children: t(live.includes(team) ? 'workspace.current' : 'workspace.history') })] }, team.teamId)) }), selected === undefined ? status === 'loading' ? _jsxs("div", { className: css.skeleton, role: "status", "aria-label": t('workspace.loading'), children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {})] }) : _jsxs("section", { className: css.empty, children: [_jsx("span", { className: css.emptyMark, "aria-hidden": true, children: "\u21B3" }), _jsx("h3", { children: t('workspace.emptyTitle') }), _jsx("p", { children: t('workspace.emptyBody') }), _jsx("button", { onClick: backToChat, children: t('workspace.back') })] }) : _jsx(_Fragment, { children: _jsx(TeamSection, { team: selected, workspace: true, historic: history, modelDirectory: selected.phase === 'staged' ? modelDirectories.directoryFor(sessionId) : undefined, onContinuePlanning: backToChat, onDiscarded: backToChat, onNavigate: openMember, t: t }, `${sessionId}:${selected.teamId}`) })] }) }));
100
+ }
@@ -39,6 +39,67 @@ export function liveCaptainTeam(teams, sessionId) {
39
39
  return undefined;
40
40
  return teams.find((team) => team.captainSessionId === owner && team.halted !== true);
41
41
  }
42
+ /**
43
+ * Latest terminal-task `updatedAt` owned by one member.
44
+ * @param memberName - member whose owned tasks are scanned.
45
+ * @param tasks - team tasks carrying durable `updatedAt` stamps.
46
+ * @returns the newest terminal stamp, or undefined when the member owns none.
47
+ */
48
+ export function memberFinishedAt(memberName, tasks) {
49
+ let latest;
50
+ for (const task of tasks) {
51
+ if (task.assignee !== memberName)
52
+ continue;
53
+ if (task.status !== 'completed' && task.status !== 'failed' && task.status !== 'cancelled')
54
+ continue;
55
+ const stamp = task.updatedAt;
56
+ if (typeof stamp !== 'number' || !Number.isFinite(stamp))
57
+ continue;
58
+ if (latest === undefined || stamp > latest)
59
+ latest = stamp;
60
+ }
61
+ return latest;
62
+ }
63
+ /** Whether a member still has live work (running activity or an open task). */
64
+ function memberIsRunning(member, tasks) {
65
+ if (member.activity === 'working' || member.status === 'working')
66
+ return true;
67
+ return tasks.some((task) => task.assignee === member.name
68
+ && (task.status === 'pending' || task.status === 'claimed' || task.status === 'in_progress'));
69
+ }
70
+ /**
71
+ * Order the delegation member list for issue #192.
72
+ *
73
+ * Running members keep their incoming relative order and come first; finished
74
+ * members follow sorted by finish time, newest first. Members without a
75
+ * derivable finish time keep their incoming relative order after the stamped
76
+ * ones, so an older host that omits stamps never reorders them randomly.
77
+ * @param members - delegation roster in snapshot order.
78
+ * @param tasks - team tasks carrying durable `updatedAt` stamps.
79
+ * @returns the members in display order.
80
+ */
81
+ export function orderDelegationMembers(members, tasks) {
82
+ const running = [];
83
+ const finishedStamped = [];
84
+ const finishedUnstamped = [];
85
+ members.forEach((member, index) => {
86
+ if (memberIsRunning(member, tasks)) {
87
+ running.push({ member, index });
88
+ return;
89
+ }
90
+ const finishedAt = memberFinishedAt(member.name, tasks);
91
+ if (finishedAt === undefined)
92
+ finishedUnstamped.push({ member, index });
93
+ else
94
+ finishedStamped.push({ member, index, finishedAt });
95
+ });
96
+ finishedStamped.sort((left, right) => right.finishedAt - left.finishedAt || left.index - right.index);
97
+ return [
98
+ ...running,
99
+ ...finishedStamped,
100
+ ...finishedUnstamped,
101
+ ].map((entry) => entry.member);
102
+ }
42
103
  /** Whether the captain chat should keep showing the in-progress banner. */
43
104
  export function teamIsActive(team) {
44
105
  if (team.halted === true || team.phase === 'staged')
@@ -126,14 +187,18 @@ export function taskStages(tasks) {
126
187
  * each stage. Edges use cubic curves so fan-in remains readable without
127
188
  * turning every task into a large card.
128
189
  */
129
- export function compactDagLayout(tasks) {
190
+ export function compactDagLayout(tasks, dimensions) {
191
+ const nodeWidth = dimensions?.nodeWidth ?? COMPACT_DAG_NODE_WIDTH;
192
+ const nodeHeight = dimensions?.nodeHeight ?? COMPACT_DAG_NODE_HEIGHT;
193
+ const columnGap = dimensions?.columnGap ?? COMPACT_DAG_COLUMN_GAP;
194
+ const rowGap = dimensions?.rowGap ?? COMPACT_DAG_ROW_GAP;
130
195
  const stages = taskStages(tasks);
131
196
  const positions = new Map();
132
197
  const nodes = [];
133
198
  for (const [column, stage] of stages.entries()) {
134
199
  for (const [row, task] of stage.tasks.entries()) {
135
- const x = column * (COMPACT_DAG_NODE_WIDTH + COMPACT_DAG_COLUMN_GAP);
136
- const y = row * (COMPACT_DAG_NODE_HEIGHT + COMPACT_DAG_ROW_GAP);
200
+ const x = column * (nodeWidth + columnGap);
201
+ const y = row * (nodeHeight + rowGap);
137
202
  positions.set(task.id, { x, y });
138
203
  nodes.push({ task, x, y });
139
204
  }
@@ -147,10 +212,10 @@ export function compactDagLayout(tasks) {
147
212
  const source = positions.get(dependency);
148
213
  if (source === undefined)
149
214
  continue;
150
- const x1 = source.x + COMPACT_DAG_NODE_WIDTH;
151
- const y1 = source.y + COMPACT_DAG_NODE_HEIGHT / 2;
215
+ const x1 = source.x + nodeWidth;
216
+ const y1 = source.y + nodeHeight / 2;
152
217
  const x2 = target.x;
153
- const y2 = target.y + COMPACT_DAG_NODE_HEIGHT / 2;
218
+ const y2 = target.y + nodeHeight / 2;
154
219
  edges.push({
155
220
  from: dependency,
156
221
  to: task.id,
@@ -162,10 +227,10 @@ export function compactDagLayout(tasks) {
162
227
  return {
163
228
  width: stages.length === 0
164
229
  ? 0
165
- : stages.length * COMPACT_DAG_NODE_WIDTH + (stages.length - 1) * COMPACT_DAG_COLUMN_GAP,
230
+ : stages.length * nodeWidth + (stages.length - 1) * columnGap,
166
231
  height: stages.length === 0
167
232
  ? 0
168
- : rows * COMPACT_DAG_NODE_HEIGHT + (rows - 1) * COMPACT_DAG_ROW_GAP,
233
+ : rows * nodeHeight + (rows - 1) * rowGap,
169
234
  nodes,
170
235
  edges,
171
236
  };
@@ -160,10 +160,13 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
160
160
  signal: controller.signal,
161
161
  });
162
162
  if (!liveResponse.ok)
163
- return;
163
+ throw new Error('Activity unavailable');
164
164
  const body = (await liveResponse.json());
165
- if (cancelled || !Array.isArray(body.teams))
165
+ if (cancelled)
166
166
  return;
167
+ if (!Array.isArray(body.teams))
168
+ throw new Error('Invalid activity response');
169
+ runtime.onStatus?.('ready');
167
170
  const liveTeams = body.teams;
168
171
  publishSnapshots({ teams: liveTeams });
169
172
  const previousDiscoveredKeys = discoveredLiveKeys;
@@ -196,10 +199,12 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
196
199
  signal: controller.signal,
197
200
  });
198
201
  if (!archivedResponse.ok)
199
- return;
202
+ throw new Error('Archive unavailable');
200
203
  const archivedBody = (await archivedResponse.json());
201
- if (cancelled || !Array.isArray(archivedBody.teams))
204
+ if (cancelled)
202
205
  return;
206
+ if (!Array.isArray(archivedBody.teams))
207
+ throw new Error('Invalid archive response');
203
208
  publishSnapshots({ archivedTeams: archivedBody.teams });
204
209
  discoveryComplete = true;
205
210
  settleTargets(new Set(missing.map((target) => target.key)));
@@ -207,6 +212,8 @@ export function startActivityPolling(monitorTargets, runtime = {}) {
207
212
  catch (error) {
208
213
  if (error?.name === 'AbortError')
209
214
  return;
215
+ if (!cancelled)
216
+ runtime.onStatus?.('error');
210
217
  // Host restarting; keep the last snapshot and retry on the next tick.
211
218
  }
212
219
  finally {
@@ -55,7 +55,7 @@ export const agentTeamsCardDefinition = {
55
55
  if (match.event.type !== 'tool/result')
56
56
  return context.state;
57
57
  const failed = match.event.data.error !== undefined
58
- || match.event.data.message.content.some((block) => block.type === 'tool-result' && block.isError === true);
58
+ || toolResultFailed(match.event.data.message);
59
59
  if (failed)
60
60
  return context.state;
61
61
  return { ...context.state, accepted: true };
@@ -83,3 +83,13 @@ export const agentTeamsCardDefinition = {
83
83
  };
84
84
  },
85
85
  };
86
+ /** V4 tool-role results carry isError directly; older logs nest tool-result blocks. */
87
+ export function toolResultFailed(message) {
88
+ return message.isError === true || message.content.some(block => typeof block === 'object' && block !== null && 'type' in block && block.type === 'tool-result'
89
+ && 'isError' in block && block.isError === true);
90
+ }
91
+ /** Keep summaries tied to the create turn, including multiple teams in one turn. */
92
+ export function teamCardsForTurn(nodes, turn) {
93
+ return [...nodes].filter(node => node.kind === 'agent-teams'
94
+ && (node.location.kind === 'turn' || node.location.kind === 'step') && node.location.turn.turn === turn);
95
+ }
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Plugin-owned icons avoid depending on the host's generated design-export names. */
3
+ const stroke = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' };
4
+ export function IconBranchOutline16() {
5
+ return _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", ...stroke, children: [_jsx("circle", { cx: "4", cy: "3", r: "1.5" }), _jsx("circle", { cx: "12", cy: "4", r: "1.5" }), _jsx("circle", { cx: "4", cy: "13", r: "1.5" }), _jsx("path", { d: "M4 4.5v7M4 9h3a5 5 0 0 0 5-3.5" })] });
6
+ }
7
+ export function IconChevronDownOutline14() {
8
+ return _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", ...stroke, children: _jsx("path", { d: "m3 5 4 4 4-4" }) });
9
+ }
10
+ export function IconPanelLeftOutline16() {
11
+ return _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", ...stroke, children: [_jsx("rect", { x: "2", y: "2.5", width: "12", height: "11", rx: "2" }), _jsx("path", { d: "M6 2.5v11" })] });
12
+ }
13
+ export function IconStopFill16() {
14
+ return _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: _jsx("rect", { x: "4", y: "4", width: "8", height: "8", rx: "1", fill: "currentColor" }) });
15
+ }
16
+ export function IconWarningOutline16() {
17
+ return _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", ...stroke, children: [_jsx("path", { d: "m7 2-5.5 10a1 1 0 0 0 1 1.5h11a1 1 0 0 0 1-1.5L9 2a1.1 1.1 0 0 0-2 0Z" }), _jsx("path", { d: "M8 6v3m0 2v.1" })] });
18
+ }
@@ -1,5 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { ActivityPanel } from "./ActivityPanel.js";
2
+ import { ActivitySurface, WorkspaceActivity, createWorkspaceBridge, TEAM_TAB_ID, TEAM_TAB_KIND } from "./WorkspaceActivity.js";
3
+ import { TeamChatEntry, TeamTurnCard } from "./TeamChatEntry.js";
4
+ import { createWorkspaceState } from "./workspace-state.js";
3
5
  import { AgentTeamsCard } from "./AgentTeamsCard.js";
4
6
  import { agentTeamsCardDefinition } from "./agent-teams-card-definition.js";
5
7
  import { AGENT_TEAMS_LOCALE_NAMESPACE, en, zh, } from "./locales.js";
@@ -17,9 +19,11 @@ function HiddenAgentTeamsCommand() {
17
19
  * monitor via a window event — the recovery path for an old session.
18
20
  */
19
21
  export function apply(ctx) {
22
+ const bridge = createWorkspaceBridge();
23
+ const state = createWorkspaceState();
20
24
  ctx.effect(() => ctx.locale.register(AGENT_TEAMS_LOCALE_NAMESPACE, { zh, en }), 'agent-teams: dictionaries');
21
25
  const openMember = (parentId, childId) => {
22
- void openAgentTeamMember(ctx.sessions, parentId, childId, ctx.layout).catch((error) => {
26
+ void openAgentTeamMember(ctx.sessions, parentId, childId, ctx.layout, ctx.get('uiWorkspace')).catch((error) => {
23
27
  console.warn(`agent-teams: failed to open member transcript ${childId}: ${String(error)}`);
24
28
  });
25
29
  };
@@ -27,8 +31,35 @@ export function apply(ctx) {
27
31
  // A host's standard hook set is fixed for this mounted plugin instance.
28
32
  const usePanel = usePanelInfo ?? useLegacyPanelInfo;
29
33
  const conversationVisible = usePanel(panel => panel.activePanelId === null);
30
- return (_jsx(ActivityPanel, { conversationVisible: conversationVisible, sessionsList: ctx.sessions.list, modelDirectories: ctx.modelDirectories, openMember: openMember, t: t }));
34
+ return (_jsx(ActivitySurface, { bridge: bridge, state: state, conversationVisible: conversationVisible, sessionsList: ctx.sessions.list, modelDirectories: ctx.modelDirectories, openMember: openMember, t: t }));
31
35
  };
36
+ // Optional service scope keeps legacy hosts working and removes every native
37
+ // contribution when the host provider disappears (including HMR).
38
+ ctx.inject(['sidebarRight', 'sidebarRightTabs'], (native) => {
39
+ const t = native.locale.bind(AGENT_TEAMS_LOCALE_NAMESPACE);
40
+ native.effect(() => native.sidebarRightTabs.register({
41
+ id: TEAM_TAB_ID, kind: TEAM_TAB_KIND,
42
+ title: () => t('workspace.title'),
43
+ }));
44
+ native.slots.inject('conversation.session.header.actions', () => native.slots.register({
45
+ name: 'conversation.session.header.actions', id: 'agent-teams-entry', order: 50,
46
+ locale: AGENT_TEAMS_LOCALE_NAMESPACE,
47
+ }, TeamChatEntry));
48
+ native.slots.inject('conversation.chat.turnTail', () => native.slots.register({
49
+ name: 'conversation.chat.turnTail', id: 'agent-teams-summary', order: 50,
50
+ locale: AGENT_TEAMS_LOCALE_NAMESPACE,
51
+ inject: () => ({ openMember }),
52
+ }, TeamTurnCard));
53
+ native.slots.inject('sidebar.right.pane.tab', () => {
54
+ const dispose = native.slots.register({
55
+ name: 'sidebar.right.pane.tab', key: TEAM_TAB_ID,
56
+ locale: AGENT_TEAMS_LOCALE_NAMESPACE,
57
+ inject: () => ({ state, modelDirectories: native.modelDirectories, openMember }),
58
+ }, WorkspaceActivity);
59
+ bridge.set(native.sidebarRight);
60
+ return () => { bridge.set(undefined); dispose(); };
61
+ });
62
+ });
32
63
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
33
64
  name: 'shell.overlay',
34
65
  id: 'agent-teams-activity',
@@ -49,7 +80,7 @@ export function apply(ctx) {
49
80
  key: 'agent-teams',
50
81
  locale: AGENT_TEAMS_LOCALE_NAMESPACE,
51
82
  inject: () => ({
52
- openMember,
83
+ openMember, workspaceBridge: bridge,
53
84
  }),
54
85
  }, AgentTeamsCard));
55
86
  }
@@ -3,6 +3,25 @@
3
3
  export const AGENT_TEAMS_LOCALE_NAMESPACE = 'agentTeams';
4
4
  /** Simplified Chinese dictionary (the key-set source of truth). */
5
5
  export const zh = {
6
+ 'workspace.dependencies': '从左到右:完成前置后解锁 · 点击查看详情',
7
+ 'workspace.title': '团队协作',
8
+ 'workspace.guide': '查看成员分工、任务依赖与执行进度',
9
+ 'workspace.eyebrow': 'AGENT TEAMS',
10
+ 'workspace.description': '队长统筹分工,成员按任务依赖并行协作。',
11
+ 'workspace.relationships': '队长协调成员分工;任务连线表示前置依赖。点击任务可定位负责成员。',
12
+ 'workspace.emptyTitle': '当前会话还没有团队',
13
+ 'workspace.emptyBody': '在聊天中使用 /agent-teams 描述目标,确认计划后即可在这里查看协作进度。',
14
+ 'workspace.loading': '正在读取团队状态',
15
+ 'workspace.error': '暂时无法连接团队服务,正在自动重试。已加载的状态可能不是最新。',
16
+ 'workspace.retry': '重新连接',
17
+ 'workspace.current': '当前团队',
18
+ 'workspace.history': '历史记录',
19
+ 'workspace.back': '返回对话',
20
+ 'workspace.tasks': '项任务',
21
+ 'workspace.members': '名成员',
22
+ 'workspace.completed': '已完成',
23
+ 'workspace.focus': '查看团队',
24
+ 'workspace.noTasks': '等待队长拆解任务',
6
25
  'card.memberCount': '{count} 名成员',
7
26
  'action.openActivityPanel': '打开活动面板',
8
27
  'activity.panelButton': '活动面板',
@@ -81,6 +100,8 @@ export const zh = {
81
100
  'dependency.hint.pinned': '{taskId} 已固定 · Esc 取消',
82
101
  'task.runningAria': '运行中',
83
102
  'task.model': '{model}',
103
+ 'task.detail.cancelled': '任务已取消,不会继续执行',
104
+ 'task.detail.failed': '任务失败,等待明确重试',
84
105
  'task.detail.completed': '已完成并交付',
85
106
  'task.detail.noPrerequisite': '无前置,可立即开工',
86
107
  'task.detail.ready': '前置已就绪,可开工',
@@ -184,6 +205,7 @@ export const zh = {
184
205
  'members.toggle': '{count} 名成员',
185
206
  'members.collapse': '收起',
186
207
  'members.expand': '展开',
208
+ 'members.expandFinished': '展开已结束({count})',
187
209
  'members.empty': '暂无成员,等待队长组建团队',
188
210
  'assignment.label': '队长派发',
189
211
  'assignment.staged': '计划任务',
@@ -194,6 +216,25 @@ export const zh = {
194
216
  };
195
217
  /** English dictionary, checked complete against the Chinese source key set. */
196
218
  export const en = {
219
+ 'workspace.dependencies': 'Left to right: prerequisites unlock tasks · Select for details',
220
+ 'workspace.title': 'Team workspace',
221
+ 'workspace.guide': 'Members, task dependencies, and execution progress',
222
+ 'workspace.eyebrow': 'AGENT TEAMS',
223
+ 'workspace.description': 'The captain coordinates. Members work in parallel as dependencies clear.',
224
+ 'workspace.relationships': 'The captain coordinates members. Connections show task prerequisites. Select a task to locate its owner.',
225
+ 'workspace.emptyTitle': 'No team in this conversation yet',
226
+ 'workspace.emptyBody': 'Use /agent-teams in chat to describe your goal. Review the plan, then follow the team here.',
227
+ 'workspace.loading': 'Loading team activity',
228
+ 'workspace.error': 'The team service is unavailable. Retrying automatically; loaded activity may be out of date.',
229
+ 'workspace.retry': 'Reconnect',
230
+ 'workspace.current': 'Current team',
231
+ 'workspace.history': 'History',
232
+ 'workspace.back': 'Back to conversation',
233
+ 'workspace.tasks': 'tasks',
234
+ 'workspace.members': 'members',
235
+ 'workspace.completed': 'completed',
236
+ 'workspace.focus': 'View team',
237
+ 'workspace.noTasks': 'Waiting for the captain to define tasks',
197
238
  'card.memberCount': '{count} members',
198
239
  'action.openActivityPanel': 'Open activity panel',
199
240
  'activity.panelButton': 'Activity panel',
@@ -272,6 +313,8 @@ export const en = {
272
313
  'dependency.hint.pinned': '{taskId} pinned · Esc to clear',
273
314
  'task.runningAria': 'Running',
274
315
  'task.model': '{model}',
316
+ 'task.detail.cancelled': 'Cancelled; execution will not continue',
317
+ 'task.detail.failed': 'Failed; awaiting an explicit retry',
275
318
  'task.detail.completed': 'Completed and delivered',
276
319
  'task.detail.noPrerequisite': 'No prerequisites; ready to start',
277
320
  'task.detail.ready': 'Prerequisites ready; can start',
@@ -375,6 +418,7 @@ export const en = {
375
418
  'members.toggle': 'Members {count}',
376
419
  'members.collapse': 'Collapse',
377
420
  'members.expand': 'Expand',
421
+ 'members.expandFinished': 'Show finished ({count})',
378
422
  'members.empty': 'No members yet; waiting for the captain to assemble the team',
379
423
  'assignment.label': 'Captain assigned',
380
424
  'assignment.staged': 'Planned task',
@@ -1,4 +1,10 @@
1
1
  /** Version-tolerant navigation into durable AgentTeams member transcripts. */
2
+ /** Newer sessions retain view ownership instead of publishing list.current. */
3
+ export function currentSessionId(state) {
4
+ if ('current' in state)
5
+ return state.current;
6
+ return Object.values(state.byId).find(row => (row.retainedBy?.mainView ?? 0) > 0)?.id;
7
+ }
2
8
  /**
3
9
  * Open one member's persisted transcript.
4
10
  *
@@ -8,9 +14,15 @@
8
14
  * the fallback preserves ordinary-session navigation. New layouts also select
9
15
  * the Conversation panel and cancel catalog refreshes superseded by navigation.
10
16
  */
11
- export async function openAgentTeamMember(sessions, parentSessionId, childSessionId, layout) {
17
+ export async function openAgentTeamMember(sessions, parentSessionId, childSessionId, layout, workspace) {
18
+ if (sessions.open === undefined && workspace !== undefined) {
19
+ workspace.openSession({ parentSessionId, childSessionId, mode: 'continuable' });
20
+ return 'subagent';
21
+ }
12
22
  const navigation = layout?.beginNavigation?.();
13
23
  if (sessions.openSubagent === undefined || sessions.refreshSubagents === undefined) {
24
+ if (sessions.open === undefined)
25
+ throw new Error('Harness does not expose session navigation');
14
26
  sessions.open(childSessionId);
15
27
  layout?.selectPanel?.(null);
16
28
  return 'session';
@@ -0,0 +1,43 @@
1
+ export function createWorkspaceState() {
2
+ let snapshot = { statuses: new Map(), history: new Map(), selected: new Map() };
3
+ const listeners = new Set();
4
+ const publish = (next) => {
5
+ snapshot = next;
6
+ for (const listener of listeners)
7
+ listener();
8
+ };
9
+ return {
10
+ getSnapshot: () => snapshot,
11
+ subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
12
+ status(session, status) {
13
+ if (snapshot.statuses.get(session) === status)
14
+ return;
15
+ publish({ ...snapshot, statuses: new Map(snapshot.statuses).set(session, status) });
16
+ },
17
+ select(session, team) {
18
+ if (snapshot.selected.get(session) === team)
19
+ return;
20
+ publish({ ...snapshot, selected: new Map(snapshot.selected).set(session, team) });
21
+ },
22
+ remember(session, data) {
23
+ const owner = data.captainSessionId || session;
24
+ if (owner !== session)
25
+ return;
26
+ publish({ ...snapshot,
27
+ history: new Map(snapshot.history).set(`${owner}:${data.teamId}`, { ...data, captainSessionId: owner }),
28
+ selected: new Map(snapshot.selected).set(owner, data.teamId),
29
+ });
30
+ },
31
+ };
32
+ }
33
+ /** Records restored at mount never reopen a closed tab; new teams are announced once. */
34
+ export function createTeamDiscovery() {
35
+ let restored = false;
36
+ let known = new Set();
37
+ return (teams) => {
38
+ const added = restored ? teams.find(team => !known.has(team.teamId))?.teamId : undefined;
39
+ known = new Set(teams.map(team => team.teamId));
40
+ restored = true;
41
+ return added;
42
+ };
43
+ }