@aiwuk/team 0.0.0-stage → 5.0.3

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 (116) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/LICENSE +21 -0
  3. package/README.md +149 -2
  4. package/README.zh-CN.md +150 -0
  5. package/access.js +93 -0
  6. package/activity-summary.js +36 -0
  7. package/allocation.js +34 -0
  8. package/board-dispatch.js +141 -0
  9. package/brand.js +41 -0
  10. package/client/00-core-zh.js +284 -0
  11. package/client/01-i18n-en.js +265 -0
  12. package/client/02-styles.js +336 -0
  13. package/client/02b-stage-styles.js +65 -0
  14. package/client/03-shared.js +82 -0
  15. package/client/03b-collapsible-list.js +34 -0
  16. package/client/03c-config-panel.js +605 -0
  17. package/client/04-tool-cards.js +110 -0
  18. package/client/04a-dispatch-confirm.js +162 -0
  19. package/client/04b-more-cards.js +78 -0
  20. package/client/04c-model-picker.js +68 -0
  21. package/client/04d-confirm-dock.js +40 -0
  22. package/client/05-policy.js +217 -0
  23. package/client/06-usage.js +195 -0
  24. package/client/06b-model-source.js +67 -0
  25. package/client/07-board-parts.js +312 -0
  26. package/client/07a-role-theme.js +25 -0
  27. package/client/08-artifacts-report.js +375 -0
  28. package/client/08a-task-control.js +126 -0
  29. package/client/08b-stage-sprites.js +97 -0
  30. package/client/08c-stage-colors.js +55 -0
  31. package/client/08c-stage-scene.js +238 -0
  32. package/client/08d-stage-adapter.js +65 -0
  33. package/client/08e-stage-activity.js +83 -0
  34. package/client/09-team-board.js +181 -0
  35. package/client/09b-role-view.js +95 -0
  36. package/client/09c-model-tabs.js +20 -0
  37. package/client/10-overview.js +364 -0
  38. package/client/10b-onboarding.js +121 -0
  39. package/client/11-register.js +249 -0
  40. package/client.js +5053 -0
  41. package/config-projection.js +40 -0
  42. package/config.js +411 -0
  43. package/cordis.patch.yml +110 -0
  44. package/defaults.js +222 -0
  45. package/dispatch-confirm.js +293 -0
  46. package/dispatch-routes-store.js +31 -0
  47. package/failover.js +85 -0
  48. package/file-limits.js +119 -0
  49. package/free-worker.mjs +125 -0
  50. package/http-guard.js +14 -0
  51. package/i18n/roles.en.json +23 -0
  52. package/i18n/roles.zh.json +23 -0
  53. package/i18n/server.en.json +659 -0
  54. package/i18n/server.zh.json +659 -0
  55. package/i18n.js +34 -0
  56. package/icon.svg +7 -0
  57. package/index.js +111 -0
  58. package/integrations-config.js +301 -0
  59. package/integrations-messages.js +62 -0
  60. package/integrations.js +17 -0
  61. package/lead-route.js +30 -0
  62. package/locale/en.json +6 -0
  63. package/locale/zh.json +6 -0
  64. package/long-context.js +30 -0
  65. package/managed-config-route.js +49 -0
  66. package/managed-ref.js +38 -0
  67. package/managed-resolve.js +47 -0
  68. package/member-task-control.js +242 -0
  69. package/memory-backends.js +89 -0
  70. package/memory-embed.js +70 -0
  71. package/memory-hub.js +150 -0
  72. package/memory-openviking.js +101 -0
  73. package/memory-redact.js +34 -0
  74. package/memory-store.js +219 -0
  75. package/memory-sync-queue.js +28 -0
  76. package/model-policy.js +85 -0
  77. package/model-prefs.js +104 -0
  78. package/observability-api.js +76 -0
  79. package/observability-projection.js +141 -0
  80. package/onboarding.js +47 -0
  81. package/opencode.js +74 -0
  82. package/package.json +153 -4
  83. package/plan-gate.js +92 -0
  84. package/preset-guard.js +47 -0
  85. package/request-evidence.js +41 -0
  86. package/roles.js +69 -0
  87. package/roster-defaults.js +70 -0
  88. package/route-health.js +39 -0
  89. package/runtime-observability.js +78 -0
  90. package/runtime-view.js +21 -0
  91. package/scripts/memory-cli.mjs +138 -0
  92. package/search-engines.js +119 -0
  93. package/search.js +164 -0
  94. package/session-policy.js +59 -0
  95. package/session-rotation.js +206 -0
  96. package/settings-schema.js +40 -0
  97. package/shared/managed-config.js +84 -0
  98. package/single-model.js +44 -0
  99. package/skills/project-role-theme/SKILL.md +46 -0
  100. package/task-bridge.js +32 -0
  101. package/task-control-api.js +36 -0
  102. package/task-control-audit.js +60 -0
  103. package/team.js +101 -0
  104. package/teammate-context.js +120 -0
  105. package/teammate-wait.js +18 -0
  106. package/template.js +46 -0
  107. package/theme-plugin.js +172 -0
  108. package/theme.js +115 -0
  109. package/token-discipline.js +116 -0
  110. package/token-metrics.js +37 -0
  111. package/tool-names.js +10 -0
  112. package/tool-schema.js +23 -0
  113. package/transcribe.js +104 -0
  114. package/ui.js +113 -0
  115. package/usage.js +92 -0
  116. package/workflow.js +246 -0
@@ -0,0 +1,95 @@
1
+ // ---------- role view (teammate session): own role card + own usage/logs/artifacts, no team canvas ----------
2
+ /**
3
+ * Teammate sessions see only themselves:
4
+ * 1. My Tasks (tasks where baseName(task.ownerName) === myRoleId, sorted: in_progress -> pending -> completed)
5
+ * 2. Current Activity & Recent Activities
6
+ * 3. Model & Permissions (RoleCards)
7
+ * 4. Task Control (TaskControl)
8
+ * 5. Usage (UsageGroups)
9
+ * 6. Artifacts (ArtifactsRack)
10
+ * 7. Logs (EvidenceGroups)
11
+ */
12
+ function RoleView({ sessionId, role, liveByRole, usage, d, liveTeam, live, runtimeState, t }) {
13
+ const myRoleId = role?.id;
14
+ const ownUsage = usage.filter(row => row.id === sessionId || row.name === role?.displayName || row.name === role?.id || baseName(row.name) === role?.id);
15
+ const ownEvidence = { ...d, memberEvidence: (d?.memberEvidence ?? []).filter(m => m.sessionId === sessionId) };
16
+ const ownLiveTeam = liveTeam ? { ...liveTeam, members: (liveTeam.members ?? []).filter(m => m.id === sessionId) } : liveTeam;
17
+ const control = runtimeState?.data?.taskControl;
18
+
19
+ // 1. My Tasks
20
+ const allTasks = Array.isArray(liveTeam?.tasks) ? liveTeam.tasks : [];
21
+ const myTasks = allTasks.filter(task => {
22
+ const owner = task.ownerName ?? task.owner;
23
+ return owner && (owner === sessionId || owner === myRoleId || baseName(owner) === myRoleId);
24
+ });
25
+ const taskWeight = { in_progress: 1, pending: 2, completed: 3 };
26
+ const sortedTasks = [...myTasks].sort((a, b) => (taskWeight[a.status] ?? 9) - (taskWeight[b.status] ?? 9));
27
+
28
+ // 2. Current Activity & Recent
29
+ const act = live?.projections?.(sessionId)?.activity;
30
+ const cur = act?.current;
31
+ const recent = Array.isArray(act?.recent) ? act.recent : [];
32
+
33
+ return h('div', { className: 'ctm-ov-block ctm-rv' },
34
+ // Section 1: My Tasks
35
+ h('div', { className: 'ctm-ov-block', 'data-section': 'my-tasks' },
36
+ h('div', { className: 'ctm-label' }, t('roleview.myTasks')),
37
+ sortedTasks.length
38
+ ? h('div', { className: 'ctm-rv-tasks' },
39
+ ...sortedTasks.map(task => h('div', { key: task.id, className: 'ctm-rv-task' },
40
+ h('div', { className: 'ctm-rv-task-title' },
41
+ h('span', { className: 'ctm-sub' }, `#${task.id}`),
42
+ h('span', { style: { fontWeight: 500 } }, task.subject)),
43
+ badge(label(t, `board.taskStatus.${task.status}`, task.status), task.status === 'in_progress' ? 'brand' : task.status === 'completed' ? 'success' : undefined, 'status'))))
44
+ : h('p', { className: 'ctm-rv-empty' }, t('roleview.noTasks'))),
45
+
46
+ // Section 2: Current Activity & Recent Activities
47
+ h('div', { className: 'ctm-ov-block', 'data-section': 'activity' },
48
+ h('div', { className: 'ctm-label' }, t('roleview.activity')),
49
+ cur || recent.length
50
+ ? h('div', { className: 'ctm-act-wrap' },
51
+ cur ? h('div', { className: 'ctm-bd-bubble', style: { margin: '4px 0' } },
52
+ h('div', { className: 'ctm-bd-bubble-head' },
53
+ badge(act.phase === 'tool' ? (cur.tool ? fmt(t('activity.tool'), { tool: cur.tool }) : t('live.bubbleRunning')) : t('activity.processing'), 'brand', 'cur')),
54
+ h('div', { className: 'ctm-bd-bubble-body' }, [cur.summary, cur.target ? ` (${cur.target})` : null].filter(Boolean).join(''))) : null,
55
+ recent.length
56
+ ? h(CollapsibleList, {
57
+ items: recent,
58
+ limit: 5,
59
+ t,
60
+ renderItem: (item, idx) => h('div', { key: item.id ?? item.callId ?? idx, className: 'ctm-act-row' },
61
+ h('span', { title: item.target ?? '' }, `${item.tool ? item.tool + ': ' : ''}${item.summary ?? ''}${item.target ? ' ' + item.target : ''}`),
62
+ h('span', { className: 'ctm-badge', 'data-tone': item.status === 'succeeded' ? 'success' : item.status === 'failed' ? 'error' : undefined }, item.status)),
63
+ })
64
+ : null)
65
+ : h('p', { className: 'ctm-rv-empty' }, t('roleview.noActivity'))),
66
+
67
+ // Section 3: Model & Permissions
68
+ h('div', { className: 'ctm-ov-block', 'data-section': 'model-perms' },
69
+ h('div', { className: 'ctm-label' }, t('roleview.modelAndPerms')),
70
+ role ? h(RoleCards, { roles: [role], live: liveByRole, t }) : null),
71
+
72
+ // Section 4: Task Control
73
+ control && role
74
+ ? h('div', { className: 'ctm-ov-block', 'data-section': 'task-control' },
75
+ h('div', { className: 'ctm-label' }, t('roleview.taskControl')),
76
+ h(TaskControl, { leadSessionId: live?.parentSessionId?.(sessionId) ?? sessionId, roleId: role.id, name: role.displayName ?? role.id, configured: role.provider ? `${role.provider}/${role.model}` : null, control, t }))
77
+ : null,
78
+
79
+ // Section 5: Usage
80
+ h('div', { className: 'ctm-ov-block', 'data-section': 'usage' },
81
+ h('div', { className: 'ctm-label' }, t('roleview.usage')),
82
+ ownUsage.length
83
+ ? h(UsageGroups, { rows: ownUsage, roles: role ? [role] : [], t })
84
+ : h('p', { className: 'ctm-rv-empty' }, t('roleview.noUsage'))),
85
+
86
+ // Section 6: Artifacts
87
+ h('div', { className: 'ctm-ov-block', 'data-section': 'artifacts' },
88
+ h('div', { className: 'ctm-label' }, t('roleview.artifacts')),
89
+ h(ArtifactsRack, { sessionId, liveTeam: ownLiveTeam, live, t })),
90
+
91
+ // Section 7: Logs
92
+ h('div', { className: 'ctm-ov-block', 'data-section': 'logs' },
93
+ h('div', { className: 'ctm-label' }, t('roleview.logs')),
94
+ d ? h(EvidenceGroups, { d: ownEvidence, sessionId, t }) : h('p', { className: 'ctm-rv-empty' }, t('roleview.noLogs'))));
95
+ }
@@ -0,0 +1,20 @@
1
+ // ---------- models panel sub-tabs: team members / model allocation / runtime status ----------
2
+ const MODEL_SUBTABS = ['team', 'models', 'runtime'];
3
+ /**
4
+ * Splits the former single "models" panel into three sub-tabs so each stays scannable on its own. The
5
+ * caller builds the three subtrees with h(...) itself and passes them as ordinary React children (team,
6
+ * then models, then runtime) — exactly like the old flat panel did — so a test walking the raw element
7
+ * tree without invoking this component (as the existing board/editor tests do) still finds the TeamBoard
8
+ * and PolicyEditor nodes exactly where they always lived. This component only toggles which one is visible.
9
+ */
10
+ function ModelSubTabs({ t, children }) {
11
+ const [sub, setSub] = React.useState(MODEL_SUBTABS[0]);
12
+ const [teamNode, modelsNode, runtimeNode] = Array.isArray(children) ? children : [children];
13
+ return h('div', { className: 'ctm-ov-block' },
14
+ h('nav', { className: 'ctm-ov-subtabs', 'aria-label': t('subtabs.team') },
15
+ ...MODEL_SUBTABS.map(id => h('button', { key: id, type: 'button', className: 'ctm-ov-subtab', 'aria-pressed': sub === id, onClick: () => setSub(id) },
16
+ t('subtabs.' + id)))),
17
+ h('div', { hidden: sub !== 'team' }, teamNode),
18
+ h('div', { hidden: sub !== 'models' }, modelsNode),
19
+ h('div', { hidden: sub !== 'runtime' }, runtimeNode));
20
+ }
@@ -0,0 +1,364 @@
1
+ function Overview({ sessionId, t: injectedT, hostT, locale: localeSource, load, save, live, cache, openMember, onboarding }) {
2
+ const [state, setState] = React.useState({ pending: false });
3
+ const [tab, setTab] = React.useState('models');
4
+ const [showOverview, setShowOverview] = React.useState(false);
5
+ const ticket = React.useRef(0);
6
+ const [, redraw] = React.useState(0);
7
+ const [children, setChildren] = React.useState(null);
8
+ // Language follows the host: ctx.locale (passed as `locale`) is the source of truth and its subscribe() re-renders on a switch.
9
+ React.useEffect(() => localeSource?.subscribe?.(() => redraw(n => n + 1)), [localeSource]);
10
+ const activeLocale = localeSource?.getLocale?.()?.active;
11
+ // Without the locale service (a bare harness) the language is inferred from the host translator's own text.
12
+ const hostProbe = (typeof injectedT === 'function' ? injectedT('overview.hint') : '') || (typeof hostT === 'function' ? hostT('overview.hint') : '');
13
+ const isZh = activeLocale ? /^zh/i.test(String(activeLocale)) : /[\u4e00-\u9fa5]/.test(hostProbe);
14
+ const dict = isZh ? zh : en;
15
+ const ownTranslator = (key, vals) => {
16
+ const template = dict[key] ?? en[key];
17
+ if (template !== undefined) return vals ? fmt(template, vals) : template;
18
+ if (typeof hostT === 'function') return hostT(key, vals);
19
+ return key;
20
+ };
21
+ const t = injectedT ?? ownTranslator;
22
+ // Root cause of the subagent canvas/roster mismatch (see item 5): a teammate session's own id is not the
23
+ // team's root. Every roster/projection/runtime/open-session call below must use the resolved Lead session
24
+ // id (parentSessionId when this is a subagent, else its own id), never the raw `sessionId` alone.
25
+ const parentSessionId = live?.parentSessionId?.(sessionId);
26
+ const isTeammateSession = !!(parentSessionId && parentSessionId !== sessionId);
27
+ const isOrphanSubagent = !isTeammateSession && !!live?.isOrphanSubagent?.(sessionId);
28
+ const effectiveSessionId = parentSessionId || sessionId;
29
+ const stored = state.sessionId === effectiveSessionId ? state : null;
30
+ const cached = stored?.data ? null : cache?.get(effectiveSessionId);
31
+ // Before the first manual read in this panel, show the last cached snapshot for this session.
32
+ const current = cached ? { ...stored, data: cached.data, at: cached.at, configVersion: cached.configVersion, cached: true } : stored ?? {};
33
+ const d = current.data;
34
+ const isPresetMounted = (() => {
35
+ if (d?.preset !== undefined) return d.preset === 'capability-team';
36
+ if (typeof live?.isCapabilityTeam === 'function') {
37
+ const res = live.isCapabilityTeam(effectiveSessionId);
38
+ if (res !== undefined) return res;
39
+ }
40
+ const proj = live?.projections?.(effectiveSessionId);
41
+ if (proj?.agentPreset !== undefined) return proj.agentPreset === 'capability-team';
42
+ if (proj?.capabilityTeamConfig !== undefined || proj?.capabilityTeamActivity !== undefined || proj?.capabilityTeamUsage !== undefined) return true;
43
+ if (parentSessionId && parentSessionId !== sessionId) {
44
+ const parentProj = live?.projections?.(parentSessionId);
45
+ if (parentProj?.agentPreset !== undefined) return parentProj.agentPreset === 'capability-team';
46
+ if (parentProj?.capabilityTeamConfig !== undefined || parentProj?.capabilityTeamActivity !== undefined || parentProj?.capabilityTeamUsage !== undefined) return true;
47
+ }
48
+ const p = live?.preset?.(effectiveSessionId);
49
+ if (p !== undefined) return p === 'capability-team';
50
+ return undefined;
51
+ })();
52
+ const configVersionNow = () => live?.projections?.(effectiveSessionId)?.capabilityTeamConfig?.version;
53
+ // The status command leaves a card in the conversation (never model context), so it runs on a click, once when
54
+ // a team session has no snapshot yet, and again only when a configuration save moves the config counter.
55
+ const refresh = async () => {
56
+ const id = ++ticket.current;
57
+ const configVersion = configVersionNow();
58
+ setState(previous => ({ ...(previous.sessionId === effectiveSessionId ? previous : {}), pending: true, error: undefined, sessionId: effectiveSessionId }));
59
+ try {
60
+ const data = await load(effectiveSessionId);
61
+ const at = Date.now();
62
+ cache?.put(effectiveSessionId, { data, at, configVersion });
63
+ if (ticket.current === id) setState({ data, sessionId: effectiveSessionId, at, configVersion });
64
+ } catch (error) {
65
+ if (ticket.current === id) setState(previous => ({ ...(previous.sessionId === effectiveSessionId ? previous : {}), pending: false, error: String(error.message ?? error), sessionId: effectiveSessionId }));
66
+ }
67
+ };
68
+ React.useEffect(() => () => { ticket.current++; }, [effectiveSessionId]);
69
+ // Host-pushed status changes and the read-only subagent catalog never touch the session log.
70
+ React.useEffect(() => live?.subscribe(() => redraw(n => n + 1)), [live]);
71
+ const rosterVersion = live?.rosterVersion?.() ?? 0;
72
+ React.useEffect(() => {
73
+ if (!live || isPresetMounted === false) return undefined;
74
+ let stopped = false;
75
+ live.children(effectiveSessionId).then(entries => { if (!stopped) setChildren({ sessionId: effectiveSessionId, entries }); }, () => {});
76
+ return () => { stopped = true; };
77
+ }, [effectiveSessionId, live, rosterVersion, isPresetMounted]);
78
+ // The Lead's agentTeam projection is pushed live; without it the board falls back to the last refresh.
79
+ const liveTeam = live?.projections?.(effectiveSessionId)?.agentTeam;
80
+ const liveActivity = liveTeam ? teamActivityFrom(liveTeam, live) : null;
81
+ // Models come only from this plugin's own request records (withTrustedRoutes), never from the host roster.
82
+ const roster = withTrustedRoutes(liveRoster(effectiveSessionId, liveActivity ?? d?.activity, children?.sessionId === effectiveSessionId ? children.entries : null, live, liveActivity ? Date.now() : current.at), live, d);
83
+ const usage = usageRows(d, liveTeam, live);
84
+ const shown = isPresetMounted !== false && !isOrphanSubagent && !!(d || liveTeam);
85
+ const boardRoles = d?.team?.roles ?? (liveTeam?.members ?? []).map(m => ({ id: m.name, displayName: m.name, ...(m.role === 'lead' ? { capability: 'orchestrator' } : {}) }));
86
+ const configVersion = configVersionNow();
87
+ // At most one automatic read per session and config counter; a failed one waits for a click or the next save.
88
+ const autoKey = React.useRef('');
89
+ React.useEffect(() => {
90
+ if (isPresetMounted === false || current.pending || !liveTeam) return;
91
+ const formed = (liveTeam.members?.length ?? 0) > 1;
92
+ const stale = d ? configVersion !== undefined && current.configVersion !== configVersion : formed;
93
+ const key = `${effectiveSessionId}:${d ? configVersion : 'initial'}`;
94
+ if (!stale || autoKey.current === key) return;
95
+ autoKey.current = key;
96
+ refresh();
97
+ }, [effectiveSessionId, configVersion, !!d, !!liveTeam, liveTeam?.members?.length, current.pending, isPresetMounted]);
98
+ // MVP-B: Failover runtime HTTP / SSE client
99
+ const [runtimeState, setRuntimeState] = React.useState({ data: null, lag: false, unavailable: false });
100
+ const lastEpoch = React.useRef(null);
101
+
102
+ React.useEffect(() => {
103
+ const AC = typeof AbortController === 'function' ? AbortController : class { constructor() { this.signal = { aborted: false }; } abort() { this.signal.aborted = true; } };
104
+ let abortCtrl = new AC();
105
+ let es = null;
106
+
107
+ const updateData = (newData, isLag = false) => {
108
+ if (!newData || typeof newData !== 'object' || newData.schemaVersion !== 1) {
109
+ setRuntimeState({ data: null, lag: false, unavailable: true });
110
+ return;
111
+ }
112
+ // 'unbound' is the placeholder of a lead that has not started yet; its first real snapshot is not a restart.
113
+ if (lastEpoch.current && lastEpoch.current !== 'unbound' && lastEpoch.current !== newData.epoch) {
114
+ // The epoch changed: the runtime restarted
115
+ setRuntimeState({ data: newData, lag: isLag, unavailable: false, restarted: true });
116
+ } else {
117
+ setRuntimeState({ data: newData, lag: isLag, unavailable: false });
118
+ }
119
+ lastEpoch.current = newData.epoch;
120
+ };
121
+
122
+ const fetchFallback = async () => {
123
+ if (typeof fetch !== 'function') return;
124
+ try {
125
+ const res = await fetch(apiUrl('api/capability-team.runtime', { sessionId: effectiveSessionId }), { signal: abortCtrl.signal });
126
+ if (res.status === 404) {
127
+ setRuntimeState({ data: null, lag: false, unavailable: false });
128
+ return;
129
+ }
130
+ if (!res.ok) {
131
+ setRuntimeState({ data: null, lag: false, unavailable: true });
132
+ return;
133
+ }
134
+ const json = await res.json();
135
+ updateData(json, true);
136
+ } catch (e) {
137
+ if (!abortCtrl.signal.aborted) {
138
+ setRuntimeState({ data: null, lag: false, unavailable: true });
139
+ }
140
+ }
141
+ };
142
+
143
+ // Prefer SSE
144
+ if (typeof EventSource === 'function') {
145
+ try {
146
+ es = new EventSource(apiUrl('api/capability-team.runtime/stream', { sessionId: effectiveSessionId }));
147
+ es.onmessage = evt => {
148
+ try {
149
+ const json = JSON.parse(evt.data);
150
+ updateData(json, false);
151
+ } catch {
152
+ // parse error
153
+ }
154
+ };
155
+ es.onerror = () => {
156
+ if (es) es.close();
157
+ es = null;
158
+ fetchFallback();
159
+ };
160
+ } catch {
161
+ fetchFallback();
162
+ }
163
+ } else {
164
+ fetchFallback();
165
+ }
166
+
167
+ return () => {
168
+ abortCtrl.abort();
169
+ if (es) es.close();
170
+ };
171
+ }, [effectiveSessionId]);
172
+
173
+ // In degraded mode: GET once when the real running state or the actual modelSelection route changes
174
+ const memberSig = (liveTeam?.members ?? []).map(m => {
175
+ const isRunning = live?.running ? live.running(m.id) : false;
176
+ const next = live?.projections?.(m.id)?.modelSelection?.next;
177
+ const route = next?.provider && next.model ? `${next.provider}/${next.model}` : '';
178
+ return `${m.id}:${isRunning ? 'running' : 'idle'}:${route}`;
179
+ }).join('|');
180
+ React.useEffect(() => {
181
+ if (!runtimeState.lag) return;
182
+ const AC = typeof AbortController === 'function' ? AbortController : class { constructor() { this.signal = { aborted: false }; } abort() { this.signal.aborted = true; } };
183
+ const ctrl = new AC();
184
+ if (typeof fetch === 'function') {
185
+ fetch(apiUrl('api/capability-team.runtime', { sessionId: effectiveSessionId }), { signal: ctrl.signal })
186
+ .then(res => res.ok ? res.json() : null)
187
+ .then(data => {
188
+ if (data && data.schemaVersion === 1) {
189
+ setRuntimeState(prev => ({ ...prev, data, lag: true }));
190
+ }
191
+ })
192
+ .catch(() => {});
193
+ }
194
+ return () => ctrl.abort();
195
+ }, [effectiveSessionId, memberSig, runtimeState.lag]);
196
+ const liveByRole = new Map((roster?.members ?? []).map(m => [m.role === 'lead' ? 'lead' : m.name, m]));
197
+ const since = new Map((roster?.members ?? []).filter(m => m.since).map(m => [m.id, m.since]));
198
+ const working = (roster?.members ?? []).filter(m => m.status === 'running').length;
199
+ const team = d?.team;
200
+ const policy = team?.modelPolicy;
201
+ const blocked = (policy?.allocations ?? []).filter(a => a.status !== 'eligible').length;
202
+ const savedOff = ruleMatcher(policy?.disabledRoutes ?? []);
203
+ const disabledCount = (policy?.rows ?? []).filter(row => savedOff(row.key)).length;
204
+ const health = !d ? null
205
+ : d.ready ? { tone: 'success', text: t('overview.ready') }
206
+ : d.preset !== 'capability-team' ? { tone: 'warn', text: t('overview.notMounted') }
207
+ : { tone: 'error', text: t('overview.abnormal') };
208
+ const diagnostics = d ? [
209
+ h('p', { key: 'e', className: 'ctm-text' }, d.explanation),
210
+ h('div', { key: 'f', className: 'ctm-chips' },
211
+ chip(t('overview.preset'), d.preset ?? '—', 'preset'),
212
+ team?.project ? chip(t('team.project'), team.project, 'project') : null,
213
+ d.missingTools?.length ? chip(t('overview.missingTools'), d.missingTools.join(t('common.listSep')), 'missing') : null),
214
+ d.activationError ? h('pre', { key: 'a', className: 'ctm-pre ctm-error' }, d.activationError) : null,
215
+ d.teamError ? h('p', { key: 't', role: 'alert', className: 'ctm-error' }, d.teamError) : null,
216
+ ] : [];
217
+ const tabs = [
218
+ ['models', policy?.rows?.length],
219
+ ['roles', team?.roles?.length],
220
+ ['usage', usageTotal(usage) ? compact(usageTotal(usage)) : null],
221
+ ['artifacts', null],
222
+ ['report', null],
223
+ ['logs', (d?.memberEvidence?.length ? d.memberEvidence : [d?.requestEvidence ?? {}]).reduce((n, m) => n + (m.records?.length ?? 0), 0)],
224
+ ];
225
+ const currentMember = isTeammateSession && liveTeam?.members?.find(m => m.id === sessionId);
226
+ const myRole = currentMember ? boardRoles.find(r => r.id === currentMember.name || r.id === baseName(currentMember.name) || (currentMember.role === 'lead' && (r.id === 'lead' || r.capability === 'orchestrator'))) : null;
227
+ const currentMemberDisplayName = myRole?.displayName ?? currentMember?.name;
228
+ const currentMemberLive = liveByRole.get(myRole?.id ?? sessionId);
229
+ const myAllocation = (policy?.allocations ?? []).find(a => a.id === myRole?.id);
230
+ const currentMemberRouted = currentMemberLive?.model ? [currentMemberLive.provider, currentMemberLive.model].filter(Boolean).join('/') : null;
231
+ const currentMemberConfigured = myAllocation?.primary ?? (myRole?.provider ? `${myRole.provider}/${myRole.model}` : null);
232
+ const currentMemberModel = currentMemberRouted ?? currentMemberConfigured;
233
+ const currentMemberWarn = !!currentMemberRouted && routeOutsideConfig(currentMemberRouted, myRole, myAllocation);
234
+ const currentMemberStatus = currentMemberLive?.status ?? (currentMember ? (live?.running?.(sessionId) ? 'running' : 'idle') : null);
235
+
236
+ const subagentNoticeNode = isTeammateSession ? h('div', { className: 'ctm-teammate-bar' },
237
+ h('div', { className: 'ctm-tm-info' },
238
+ h('span', { className: 'ctm-tm-avatar', 'aria-hidden': true }, Array.from(currentMemberDisplayName ?? '?')[0] ?? '?'),
239
+ h('div', { className: 'ctm-tm-meta' },
240
+ h('div', { className: 'ctm-tm-names' },
241
+ h('strong', { style: { fontSize: '13px' } }, currentMemberDisplayName),
242
+ h('span', { className: 'ctm-sub' }, [myRole?.id, myRole?.capability].filter(Boolean).join(' · ')),
243
+ currentMemberStatus ? badge(label(t, `live.${currentMemberStatus}`, currentMemberStatus), LIVE[currentMemberStatus], 'st') : null),
244
+ h('div', { className: 'ctm-tm-sub' },
245
+ currentMemberModel ? h('span', { className: 'ctm-ov-route', title: `${currentMemberModel} (${t(currentMemberRouted ? 'live.liveModel' : 'live.configModel')})` }, currentMemberModel) : null,
246
+ currentMemberModel && !currentMemberRouted ? h('span', { className: 'ctm-sub', 'data-config-model': '' }, t('live.configTag')) : null,
247
+ currentMemberWarn ? routeWarnBadge(t) : null,
248
+ h('span', { className: 'ctm-sub', style: { display: 'none' } }, t('subagent.notice')),
249
+ currentMember ? h('span', { className: 'ctm-sub', style: { display: 'none' } }, fmt(t('subagent.currentMember'), { name: currentMemberDisplayName, role: currentMember.role ?? currentMember.id })) : null))),
250
+ h('div', { className: 'ctm-tm-actions' },
251
+ openMember ? h('button', { type: 'button', className: 'ctm-btn', onClick: () => openMember(effectiveSessionId) }, t('subagent.openLead')) : null,
252
+ h('button', { type: 'button', className: 'ctm-btn ctm-ov-roleview-toggle', onClick: () => setShowOverview(v => !v) },
253
+ t(showOverview ? 'roleview.backToRole' : 'roleview.viewOverview'))))
254
+ : isOrphanSubagent ? h('div', { className: 'ctm-ov-callout', 'data-tone': 'warn', style: { marginBottom: '12px' } },
255
+ h('p', { style: { fontWeight: 600 } }, t('subagent.unresolved'))) : null;
256
+ const subRowText = [
257
+ team ? [team.name, fmt(t('overview.revision'), { rev: team.revision })].filter(Boolean).join(' · ') : t('overview.hint'),
258
+ current.at ? fmt(t('overview.updated'), { time: when(current.at) }) : '',
259
+ ].filter(Boolean).join(' · ');
260
+ return h('section', { className: 'ctm-ov' },
261
+ h('header', { className: 'ctm-ov-head' },
262
+ h('div', { className: 'ctm-ov-titlerow' },
263
+ h('h2', { className: 'ctm-ov-title' }, t('overview.title')),
264
+ health && (!isTeammateSession || showOverview) ? badge(health.text, health.tone, 'health') : null,
265
+ liveTeam && (!isTeammateSession || showOverview) ? badge(t('overview.live'), 'success', 'live') : null,
266
+ h('button', {
267
+ type: 'button',
268
+ className: 'ctm-btn ctm-ov-refresh',
269
+ disabled: current.pending,
270
+ onClick: refresh,
271
+ 'aria-busy': !!current.pending,
272
+ title: t('overview.logNote'),
273
+ }, t(current.pending ? 'overview.loading' : 'overview.refresh')),
274
+ isTeammateSession ? h('button', { type: 'button', className: 'ctm-btn ctm-ov-roleview-toggle', onClick: () => setShowOverview(v => !v) },
275
+ t(showOverview ? 'roleview.backToRole' : 'roleview.viewOverview')) : null),
276
+ h('p', { className: 'ctm-sub ctm-ov-subrow', title: subRowText }, subRowText)),
277
+ h('div', { className: 'ctm-ov-scroll' },
278
+ subagentNoticeNode,
279
+ onboarding ? onboarding({ t }) : null,
280
+ current.error ? h('p', { role: 'alert', className: 'ctm-ov-callout', 'data-tone': 'error' }, current.error) : null,
281
+ !shown ? h('div', { className: 'ctm-ov-empty' }, h('p', { className: 'ctm-sub' }, t(isPresetMounted === false ? 'overview.notMounted' : current.pending ? 'overview.loading' : 'overview.empty')),
282
+ (isPresetMounted === false || current.pending) ? null : h('button', { type: 'button', className: 'ctm-btn', 'data-primary': '', onClick: refresh }, t('overview.firstLoad'))) : null,
283
+ (!isTeammateSession || showOverview) && d && team ? h('div', { className: 'ctm-ov-stats' },
284
+ stat(team.roles?.length ?? 0, t('overview.statRoles'), undefined, 'r'),
285
+ stat(working, t('overview.statWorking'), working ? 'brand' : undefined, 'w'),
286
+ stat(policy?.rows?.length ?? 0, t('overview.statModels'), undefined, 'm'),
287
+ stat(disabledCount, t('overview.statDisabled'), disabledCount ? 'warn' : undefined, 'd'),
288
+ stat(blocked, t('overview.statBlocked'), blocked ? 'error' : undefined, 'b')) : null,
289
+ (!isTeammateSession || showOverview) && d ? (d.ready
290
+ ? h('details', { className: 'ctm-ov-group ctm-ov-diag' }, h('summary', null, h('span', { className: 'ctm-name' }, t('overview.details'))), h('div', { className: 'ctm-ov-block' }, ...diagnostics))
291
+ : h('div', { className: 'ctm-ov-callout', 'data-tone': health.tone }, ...diagnostics)) : null,
292
+ ...((!isTeammateSession || showOverview) ? (team?.catalogErrors ?? []).map(error => h('p', { key: error.provider, role: 'alert', className: 'ctm-ov-callout', 'data-tone': 'error' }, `${error.provider}: ${error.error}`)) : []),
293
+ (isTeammateSession && !showOverview) ? h(ErrorBoundary, { key: `${effectiveSessionId}:roleview`, t },
294
+ h(RoleView, { sessionId, role: myRole, liveByRole, usage, d, liveTeam, live, runtimeState, t })) : h(React.Fragment, null,
295
+ shown ? h('nav', { className: 'ctm-ov-tabs', 'aria-label': t('overview.title') },
296
+ ...tabs.map(([id, count]) => h('button', { key: id, type: 'button', className: 'ctm-ov-tab', 'aria-pressed': tab === id, title: t('navfull.' + id) + (count ? ` · ${count}` : ''), onClick: () => setTab(id) },
297
+ t('nav.' + id), count ? h('span', { className: 'ctm-ov-count' }, typeof count === 'number' && count > 99 ? '99+' : String(count)) : null))) : null,
298
+ shown ? h('div', { className: 'ctm-ov-panel', 'data-panel': 'usage', hidden: tab !== 'usage' },
299
+ h(ErrorBoundary, { key: `${effectiveSessionId}:usage`, t }, h(UsageGroups, { rows: usage, roles: team?.roles ?? [], t }))) : null,
300
+ shown ? h('div', { className: 'ctm-ov-panel', 'data-panel': 'logs', hidden: tab !== 'logs' },
301
+ h(ErrorBoundary, { key: `${effectiveSessionId}:logs`, t },
302
+ liveTeam ? h(LiveRequests, { rows: usage, t }) : null,
303
+ d ? h(EvidenceGroups, { d, sessionId: effectiveSessionId, t }) : h('p', { className: 'ctm-sub' }, t('overview.noData')))) : null,
304
+ shown ? h('div', { className: 'ctm-ov-panel', 'data-panel': 'models', hidden: tab !== 'models' },
305
+ h(ErrorBoundary, { key: `${effectiveSessionId}:models`, t },
306
+ h(ModelSubTabs, { t },
307
+ // Root cause fix: this used to pass the raw (possibly teammate) sessionId as the "parent" argument to
308
+ // openMember, so opening a member from inside a subagent session targeted the wrong parent.
309
+ team || liveTeam ? h(TeamBoard, { roles: boardRoles, activity: roster, allocations: policy?.allocations, since, t, open: openMember && (id => openMember(effectiveSessionId, id)), live, usage, runtimeState, currentMemberId: isTeammateSession ? sessionId : null, leadSessionId: effectiveSessionId }) : null,
310
+ policy ? h(PolicyEditor, { key: `${effectiveSessionId}:${policy.revision}`, sessionId: effectiveSessionId, policy, save, t }) : h('p', { className: 'ctm-sub' }, t('overview.noData')),
311
+ h('div', { className: 'ctm-ov-block' },
312
+ runtimeState.restarted ? h('p', { className: 'ctm-ov-callout', 'data-tone': 'warn' }, t('runtime.restarted')) : null,
313
+ runtimeState.lag ? h('p', { className: 'ctm-sub ctm-warn' }, t('runtime.snapshotLag')) : null,
314
+ runtimeState.unavailable ? h('p', { className: 'ctm-sub ctm-warn' }, t('runtime.unavailable')) : null,
315
+ !runtimeState.restarted && !runtimeState.lag && !runtimeState.unavailable ? h('p', { className: 'ctm-sub' }, t('runtime.noNotices')) : null)))) : null,
316
+ shown ? h('div', { className: 'ctm-ov-panel', 'data-panel': 'artifacts', hidden: tab !== 'artifacts' },
317
+ h(ErrorBoundary, { key: `${effectiveSessionId}:artifacts`, t },
318
+ h(ArtifactsRack, { sessionId: effectiveSessionId, liveTeam, live, t }))) : null,
319
+ shown ? h('div', { className: 'ctm-ov-panel', 'data-panel': 'report', hidden: tab !== 'report' },
320
+ h(ErrorBoundary, { key: `${effectiveSessionId}:report`, t },
321
+ h(BattleReportView, { sessionId: effectiveSessionId, liveTeam, live, t, runtimeState, singleModel: team?.singleModel }))) : null,
322
+ shown ? h('div', { className: 'ctm-ov-panel', 'data-panel': 'roles', hidden: tab !== 'roles' },
323
+ h(ErrorBoundary, { key: `${effectiveSessionId}:roles`, t },
324
+ team ? h(React.Fragment, null,
325
+ h(RoleCards, { roles: team.roles ?? [], live: liveByRole, t }),
326
+ team.modelNotice ? h('p', { className: 'ctm-sub ctm-ov-help' }, team.modelNotice) : null,
327
+ h('div', { className: 'ctm-ov-block ctm-theme-section' },
328
+ h('div', { className: 'ctm-label' }, t('theme.title')),
329
+ h('p', { className: 'ctm-name', style: { marginBottom: '4px' } }, fmt(t('theme.current'), { name: team.name || 'Default' })),
330
+ h('p', { className: 'ctm-sub' }, t('theme.scopeDesc')),
331
+ h('div', { className: 'ctm-label', style: { marginTop: '12px' } }, t('theme.howToChange')),
332
+ h('p', { className: 'ctm-sub' }, t('theme.chatInstruction')),
333
+ h('div', { className: 'ctm-theme-prompts', style: { display: 'flex', flexDirection: 'column', gap: '6px', margin: '8px 0' } },
334
+ ...[
335
+ t('theme.promptWestern'),
336
+ t('theme.promptOutlaws'),
337
+ t('theme.promptSciFi'),
338
+ ].map(promptText => h('div', { key: promptText, style: { display: 'flex', alignItems: 'center', gap: '8px' } },
339
+ h('code', { className: 'ctm-chip', style: { flex: '1 1 auto', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, promptText),
340
+ h('button', {
341
+ type: 'button', className: 'ctm-btn', style: { flex: 'none' },
342
+ onClick: async e => {
343
+ const btn = e.currentTarget;
344
+ const prev = btn.textContent;
345
+ // Only claim success after the browser actually confirms the write; any rejection
346
+ // (unsupported API, permission denial, insecure context, etc.) falls through to the
347
+ // manual-copy notice instead of silently lying about the clipboard state.
348
+ try {
349
+ if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
350
+ throw new Error('clipboard API unavailable');
351
+ }
352
+ await navigator.clipboard.writeText(promptText);
353
+ btn.textContent = t('theme.promptCopied');
354
+ } catch {
355
+ btn.textContent = t('theme.promptManual');
356
+ }
357
+ setTimeout(() => { btn.textContent = prev; }, 1500);
358
+ }
359
+ }, t('theme.promptCopy'))))),
360
+ h('p', { className: 'ctm-sub', style: { marginTop: '8px' } }, t('theme.templateInstruction')),
361
+ h('p', { className: 'ctm-sub ctm-ov-note' }, t('theme.templateDesc'))),
362
+ h('div', { className: 'ctm-ov-block' }, h('div', { className: 'ctm-label' }, t('overview.policy')), h(WorkflowList, { workflow: team.workflow, t })))
363
+ : h('p', { className: 'ctm-sub' }, t('overview.noData')))) : null)));
364
+ }
@@ -0,0 +1,121 @@
1
+ // ---------- onboarding: official Agent Teams bundle, model coverage, first team session; ----------
2
+ const AGENT_TEAM_BUNDLE = '@deepseek-ai/dsh-experimental-agent-team-profile';
3
+ const WELCOME_VERSION = '4.0.0';
4
+ const UI_NAMESPACE = 'capability-team-ui'; // this package's row id in cordis.patch.yml; its volatile Config keeps the welcome acknowledgement
5
+ // Texts of the integrations summary (shared panel dictionaries) join this namespace; nothing else is needed.
6
+ for (const [lang, target] of [['zh', zh], ['en', en]]) {
7
+ for (const [key, text] of Object.entries(aiwukConfig.panelDictionaries(aiwukConfig.PANELS.team)[lang])) if (/^(team\.(integrations|hint|unmanaged|managedCount)|lock\.|value\.)/.test(key)) target[key] = text;
8
+ }
9
+ Object.assign(en, {
10
+ 'onb.title': 'Capability Team: three steps', 'onb.step1': '1. Enable official Agent Teams', 'onb.step2': '2. Model tier coverage', 'onb.step3': '3. Run the first team task',
11
+ 'onb.teamReady': 'The official Agent Teams service is running.',
12
+ 'onb.teamMissing': 'Capability Team builds on the official experimental bundle {name}; it is not enabled, so the team cannot start.',
13
+ 'onb.teamWarning': 'Note: enabling it affects every session in this profile and disables the native generic subagent tools (subagent / fork); delegation then uses the team tools. Nothing changes until you click the button.',
14
+ 'onb.enable': 'Enable Agent Teams', 'onb.enabling': 'Enabling…', 'onb.applied': 'Enabled and applied. Reload the page, then start a new session.',
15
+ 'onb.restart': 'Enabled; restart DSH for it to take effect (reason: {reason}).', 'onb.failed': 'Enabling failed, error code: {code}. You can also enable it on the Plugins page.',
16
+ 'onb.noManager': 'This connection cannot manage plugins; enable {name} on the Plugins page.',
17
+ 'onb.coverage': '{covered}/{total} capabilities matched a qualified tier in your model catalog ({models} models).', 'onb.coverageUnknown': 'The model catalog is not readable yet; check the team overview later.',
18
+ 'onb.unassigned': 'Unmatched: {list}. Requests of these roles are blocked; add a channel, or ask the Lead to enable single-model mode (you confirm it).',
19
+ 'onb.newSession': 'New session → choose "Capability Team" in the mode picker, then describe your task.', 'onb.dismiss': 'Got it', 'onb.later': 'Later',
20
+ 'roles.singleModel': 'Single-model mode', 'roles.singleModelHint': 'Roles without an assigned model use the one model you confirmed; no backups, failures surface.',
21
+ });
22
+ async function fetchOnboarding(signal) {
23
+ const res = await fetch(apiUrl('api/capability-team.onboarding'), { signal });
24
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
25
+ return res.json();
26
+ }
27
+ /** Map a pluginManager ChangeResult (Remote reply `{ok, value}` or a bare value) to what the card shows. */
28
+ function enableOutcome(reply) {
29
+ if (reply && reply.ok === false) return { kind: 'failed', code: String(reply.error?.code ?? reply.error?.message ?? 'remote-error') };
30
+ const value = reply && reply.ok === true ? reply.value : reply;
31
+ if (value?.application === 'applied') return { kind: 'applied' };
32
+ if (value?.application === 'restart-required') return { kind: 'restart', reason: String(value.restartReason ?? '') };
33
+ return { kind: 'failed', code: String(value?.error?.code ?? (value?.application === 'cancelled' ? 'cancelled' : 'unknown-result')) };
34
+ }
35
+ /** Enable the official bundle only after a user click: confirm it is listed, then select it. */
36
+ async function enableAgentTeams(pluginManager) {
37
+ if (!pluginManager) return { kind: 'failed', code: 'no-plugin-manager' };
38
+ try {
39
+ const listed = await pluginManager.listBundles();
40
+ const rows = listed?.ok === true ? listed.value : Array.isArray(listed) ? listed : null;
41
+ if (!rows) return enableOutcome(listed);
42
+ const row = rows.find(b => b?.name === AGENT_TEAM_BUNDLE);
43
+ if (!row || row.availability === 'missing') return { kind: 'failed', code: 'not-installed' };
44
+ return enableOutcome(await pluginManager.setBundleEnabled(AGENT_TEAM_BUNDLE, true));
45
+ } catch (error) { return { kind: 'failed', code: String(error?.code ?? error?.message ?? error) }; }
46
+ }
47
+ function OnboardingCard({ status, enable, t, onDismiss, dismissLabel }) {
48
+ const [outcome, setOutcome] = React.useState(null);
49
+ const pending = outcome?.kind === 'pending';
50
+ const click = async () => { setOutcome({ kind: 'pending' }); setOutcome(await enable()); };
51
+ const cov = status?.coverage;
52
+ const outcomeText = !outcome || pending ? null : outcome.kind === 'applied' ? t('onb.applied')
53
+ : outcome.kind === 'restart' ? fmt(t('onb.restart'), { reason: outcome.reason || '-' }) : fmt(t('onb.failed'), { code: outcome.code });
54
+ return h('div', { className: 'ctm-ov-callout', 'data-tone': status?.teamServicePresent ? undefined : 'warn', 'data-onboarding': '' },
55
+ h('span', { className: 'ctm-name' }, t('onb.title')),
56
+ h('span', { className: 'ctm-label' }, t('onb.step1')),
57
+ status?.teamServicePresent ? h('span', { className: 'ctm-sub', 'data-step': 'team-ready' }, `✓ ${t('onb.teamReady')}`) : h('div', { 'data-step': 'team-missing' },
58
+ h('p', { className: 'ctm-sub' }, fmt(t('onb.teamMissing'), { name: AGENT_TEAM_BUNDLE })),
59
+ h('p', { className: 'ctm-sub ctm-warn', 'data-warning': '' }, t('onb.teamWarning')),
60
+ enable ? h('button', { type: 'button', className: 'ctm-btn', 'data-primary': '', 'data-action': 'enable-agent-teams', disabled: pending || outcome?.kind === 'applied' || outcome?.kind === 'restart', onClick: click },
61
+ t(pending ? 'onb.enabling' : 'onb.enable')) : h('p', { className: 'ctm-sub' }, fmt(t('onb.noManager'), { name: AGENT_TEAM_BUNDLE })),
62
+ outcomeText ? h('p', { className: outcome.kind === 'failed' ? 'ctm-sub ctm-error' : 'ctm-sub', role: 'status', 'data-outcome': outcome.kind }, outcomeText) : null),
63
+ h('span', { className: 'ctm-label' }, t('onb.step2')),
64
+ cov?.known ? h('span', { className: 'ctm-sub', 'data-step': 'coverage' }, fmt(t('onb.coverage'), { covered: cov.covered, total: cov.capabilities, models: cov.models })) : h('span', { className: 'ctm-sub' }, t('onb.coverageUnknown')),
65
+ cov?.unassigned?.length ? h('span', { className: 'ctm-sub ctm-warn' }, fmt(t('onb.unassigned'), { list: cov.unassigned.join(t('common.listSep')) })) : null,
66
+ h('span', { className: 'ctm-label' }, t('onb.step3')),
67
+ h('span', { className: 'ctm-sub' }, t('onb.newSession')),
68
+ onDismiss ? h('button', { type: 'button', className: 'ctm-btn', 'data-action': 'dismiss', onClick: onDismiss }, dismissLabel ?? t('onb.dismiss')) : null);
69
+ }
70
+ /** Re-render on a store's changes (ConfigForm and similar `{getSnapshot, subscribe}` stores). */
71
+ function useStoreSnapshot(store) {
72
+ const [, redraw] = React.useState(0);
73
+ React.useEffect(() => (typeof store?.subscribe === 'function' ? store.subscribe(() => redraw(n => n + 1)) : undefined), [store]);
74
+ return store?.getSnapshot?.() ?? { status: 'unavailable' };
75
+ }
76
+ /** Read the onboarding status once per mount. */
77
+ function useOnboardingStatus(fetchStatus) {
78
+ const [status, setStatus] = React.useState(null);
79
+ const started = React.useRef(false);
80
+ React.useEffect(() => {
81
+ if (started.current || !fetchStatus) return;
82
+ started.current = true;
83
+ fetchStatus().then(setStatus, () => setStatus({ unavailable: true }));
84
+ }, [fetchStatus]);
85
+ return status;
86
+ }
87
+ /** Overview banner: shown only while the official team service is missing. */
88
+ function OnboardingGate({ t, enable, fetchStatus }) {
89
+ const status = useOnboardingStatus(fetchStatus);
90
+ if (!status || status.unavailable || status.teamServicePresent !== false) return null;
91
+ return h('div', { style: { margin: '12px 16px 0' } }, h(OnboardingCard, { status, enable, t }));
92
+ }
93
+ /** settings.onboarding step: once per WELCOME_VERSION, acknowledged through this package's volatile Config. */
94
+ function WelcomeStep({ complete, explicit, form, t, enable, fetchStatus }) {
95
+ const snapshot = useStoreSnapshot(form);
96
+ const status = useOnboardingStatus(fetchStatus);
97
+ const acknowledged = snapshot.status === 'ready' && snapshot.value?.welcomeVersion === WELCOME_VERSION;
98
+ React.useEffect(() => { if (acknowledged && !explicit) complete?.(); }, [acknowledged, explicit]);
99
+ if (snapshot.status === 'loading' || !status || (acknowledged && !explicit)) return null;
100
+ const dismiss = async () => { if (snapshot.status === 'ready' && snapshot.writable !== false) { try { await form.set('welcomeVersion', WELCOME_VERSION); } catch { /* in-memory completion still applies */ } } complete?.(); };
101
+ return h('div', { role: 'dialog', 'aria-modal': 'true', className: 'ctm-onb-modal', style: { position: 'fixed', inset: 0, zIndex: 'var(--aw-z-modal)', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--dsw-alias-bg-mask-2)' } },
102
+ h('div', { style: { maxWidth: '520px', width: 'calc(100% - 48px)', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)', borderRadius: 'var(--dsw-radius-lg)', boxShadow: 'var(--dsw-shadow-lv3)', padding: '16px' } },
103
+ h(OnboardingCard, { status, enable, t, onDismiss: dismiss })));
104
+ }
105
+ /** Optional host services are reached through ctx.inject, so a host without them still mounts the plugin. */
106
+ function registerOnboarding(ctx, t) {
107
+ let pluginManager = null;
108
+ ctx.inject?.(['remote.pluginManager'], scoped => { pluginManager = scoped.remote.pluginManager; scoped.effect?.(() => () => { pluginManager = null; }); });
109
+ const enable = () => enableAgentTeams(pluginManager);
110
+ const gate = props => h(OnboardingGate, { t: props?.t ?? t, enable, fetchStatus: fetchOnboarding });
111
+ ctx.slots.inject('plugins.bundle.activation', () => ctx.slots.register({ name: 'plugins.bundle.activation', key: '@aiwuk/team', locale: NS },
112
+ props => h(OnboardingCard, { status: null, enable, t, onDismiss: props.onDismiss, dismissLabel: t('onb.later') })));
113
+ ctx.inject?.(['configForms', 'slots'], scoped => {
114
+ const forms = scoped.configForms;
115
+ scoped.slots.inject('settings.onboarding', () => scoped.slots.register({ name: 'settings.onboarding', id: 'capability-team-welcome', order: 50, label: () => t('onb.title'), locale: NS },
116
+ props => h(WelcomeStep, { ...props, t, enable, fetchStatus: fetchOnboarding, form: forms.get(UI_NAMESPACE) })));
117
+ });
118
+ // Integrations summary for this bundle's Plugins page; the Memory and Search panels ship in their own bundles.
119
+ aiwukConfig.createPanelKit(React).registerConfigPanel(ctx, { panel: aiwukConfig.PANELS.team, ns: NS, summaryOnly: true });
120
+ return gate;
121
+ }