@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.
- package/CHANGELOG.md +156 -0
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/README.zh-CN.md +150 -0
- package/access.js +93 -0
- package/activity-summary.js +36 -0
- package/allocation.js +34 -0
- package/board-dispatch.js +141 -0
- package/brand.js +41 -0
- package/client/00-core-zh.js +284 -0
- package/client/01-i18n-en.js +265 -0
- package/client/02-styles.js +336 -0
- package/client/02b-stage-styles.js +65 -0
- package/client/03-shared.js +82 -0
- package/client/03b-collapsible-list.js +34 -0
- package/client/03c-config-panel.js +605 -0
- package/client/04-tool-cards.js +110 -0
- package/client/04a-dispatch-confirm.js +162 -0
- package/client/04b-more-cards.js +78 -0
- package/client/04c-model-picker.js +68 -0
- package/client/04d-confirm-dock.js +40 -0
- package/client/05-policy.js +217 -0
- package/client/06-usage.js +195 -0
- package/client/06b-model-source.js +67 -0
- package/client/07-board-parts.js +312 -0
- package/client/07a-role-theme.js +25 -0
- package/client/08-artifacts-report.js +375 -0
- package/client/08a-task-control.js +126 -0
- package/client/08b-stage-sprites.js +97 -0
- package/client/08c-stage-colors.js +55 -0
- package/client/08c-stage-scene.js +238 -0
- package/client/08d-stage-adapter.js +65 -0
- package/client/08e-stage-activity.js +83 -0
- package/client/09-team-board.js +181 -0
- package/client/09b-role-view.js +95 -0
- package/client/09c-model-tabs.js +20 -0
- package/client/10-overview.js +364 -0
- package/client/10b-onboarding.js +121 -0
- package/client/11-register.js +249 -0
- package/client.js +5053 -0
- package/config-projection.js +40 -0
- package/config.js +411 -0
- package/cordis.patch.yml +110 -0
- package/defaults.js +222 -0
- package/dispatch-confirm.js +293 -0
- package/dispatch-routes-store.js +31 -0
- package/failover.js +85 -0
- package/file-limits.js +119 -0
- package/free-worker.mjs +125 -0
- package/http-guard.js +14 -0
- package/i18n/roles.en.json +23 -0
- package/i18n/roles.zh.json +23 -0
- package/i18n/server.en.json +659 -0
- package/i18n/server.zh.json +659 -0
- package/i18n.js +34 -0
- package/icon.svg +7 -0
- package/index.js +111 -0
- package/integrations-config.js +301 -0
- package/integrations-messages.js +62 -0
- package/integrations.js +17 -0
- package/lead-route.js +30 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/long-context.js +30 -0
- package/managed-config-route.js +49 -0
- package/managed-ref.js +38 -0
- package/managed-resolve.js +47 -0
- package/member-task-control.js +242 -0
- package/memory-backends.js +89 -0
- package/memory-embed.js +70 -0
- package/memory-hub.js +150 -0
- package/memory-openviking.js +101 -0
- package/memory-redact.js +34 -0
- package/memory-store.js +219 -0
- package/memory-sync-queue.js +28 -0
- package/model-policy.js +85 -0
- package/model-prefs.js +104 -0
- package/observability-api.js +76 -0
- package/observability-projection.js +141 -0
- package/onboarding.js +47 -0
- package/opencode.js +74 -0
- package/package.json +153 -4
- package/plan-gate.js +92 -0
- package/preset-guard.js +47 -0
- package/request-evidence.js +41 -0
- package/roles.js +69 -0
- package/roster-defaults.js +70 -0
- package/route-health.js +39 -0
- package/runtime-observability.js +78 -0
- package/runtime-view.js +21 -0
- package/scripts/memory-cli.mjs +138 -0
- package/search-engines.js +119 -0
- package/search.js +164 -0
- package/session-policy.js +59 -0
- package/session-rotation.js +206 -0
- package/settings-schema.js +40 -0
- package/shared/managed-config.js +84 -0
- package/single-model.js +44 -0
- package/skills/project-role-theme/SKILL.md +46 -0
- package/task-bridge.js +32 -0
- package/task-control-api.js +36 -0
- package/task-control-audit.js +60 -0
- package/team.js +101 -0
- package/teammate-context.js +120 -0
- package/teammate-wait.js +18 -0
- package/template.js +46 -0
- package/theme-plugin.js +172 -0
- package/theme.js +115 -0
- package/token-discipline.js +116 -0
- package/token-metrics.js +37 -0
- package/tool-names.js +10 -0
- package/tool-schema.js +23 -0
- package/transcribe.js +104 -0
- package/ui.js +113 -0
- package/usage.js +92 -0
- package/workflow.js +246 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
function boardCards(roles, activity, allocations) {
|
|
2
|
+
const members = activity?.members ?? [];
|
|
3
|
+
const tasks = activity?.tasks ?? [];
|
|
4
|
+
const primary = new Map((allocations ?? []).map(a => [a.id, a.primary]));
|
|
5
|
+
const allocById = new Map((allocations ?? []).map(a => [a.id, a]));
|
|
6
|
+
const allocNames = new Map((allocations ?? []).map(a => [a.id, a.displayName]).filter(([_, name]) => Boolean(name)));
|
|
7
|
+
const used = new Set();
|
|
8
|
+
const cards = roles.map(role => {
|
|
9
|
+
const lead = role.id === 'lead' || role.capability === 'orchestrator';
|
|
10
|
+
const member = members.find(m => !used.has(m) && (lead ? (m.role === 'lead' || baseName(m.name) === 'lead') : (m.name === role.id || baseName(m.name) === role.id)));
|
|
11
|
+
if (member) used.add(member);
|
|
12
|
+
const resolvedName = (role.displayName && role.displayName !== role.id)
|
|
13
|
+
? role.displayName
|
|
14
|
+
: (allocNames.get(role.id) ?? role.displayName ?? role.id);
|
|
15
|
+
return { id: role.id, name: resolvedName, lead, member, role };
|
|
16
|
+
});
|
|
17
|
+
for (const member of members) {
|
|
18
|
+
if (!used.has(member)) {
|
|
19
|
+
const lead = member.role === 'lead';
|
|
20
|
+
const bName = lead ? 'lead' : baseName(member.name);
|
|
21
|
+
const matchedRole = roles.find(r => r.id === bName || (lead && (r.id === 'lead' || r.capability === 'orchestrator')));
|
|
22
|
+
const resolvedName = (matchedRole?.displayName && matchedRole.displayName !== matchedRole.id)
|
|
23
|
+
? matchedRole.displayName
|
|
24
|
+
: (allocNames.get(bName) ?? matchedRole?.displayName ?? member.name);
|
|
25
|
+
cards.push({ id: matchedRole?.id ?? member.name, name: resolvedName, lead, member, role: matchedRole ?? null });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return cards.map(card => {
|
|
29
|
+
const owner = card.member?.name ?? card.id;
|
|
30
|
+
const bId = card.id ? baseName(card.id) : '';
|
|
31
|
+
const mine = tasks.filter(task => (task.ownerName === owner || task.ownerName === card.id || (task.ownerName && baseName(task.ownerName) === bId)) && task.status !== 'completed');
|
|
32
|
+
const status = card.member?.status ?? 'inactive';
|
|
33
|
+
const task = mine.find(task => task.status === 'in_progress') ?? mine[0];
|
|
34
|
+
// Item 9: "configured" (role.provider/role.model, or this role's primary allocation) is distinct from
|
|
35
|
+
// "last request" (this plugin's own capabilityTeamUsage route, put on card.member.model by withTrustedRoutes;
|
|
36
|
+
// the host roster's model is never used). They are the
|
|
37
|
+
// same route most of the time, but failover/long-context switches or a stale config can make them differ —
|
|
38
|
+
// that difference must stay visible rather than being collapsed into one badge.
|
|
39
|
+
const configuredModel = primary.get(card.id) ?? (card.role?.provider ? `${card.role.provider}/${card.role.model}` : null);
|
|
40
|
+
const lastRequestModel = card.member?.model ? [card.member.provider, card.member.model].filter(Boolean).join('/') : null;
|
|
41
|
+
const fallbackRoutes = (Array.isArray(card.role?.fallbacks) ? card.role.fallbacks : []).map(routeText).filter(Boolean);
|
|
42
|
+
return { ...card, status,
|
|
43
|
+
model: lastRequestModel ?? configuredModel,
|
|
44
|
+
configuredModel, lastRequestModel,
|
|
45
|
+
// The Lead runs on the user's pick, so its configured route is only a default and is not contrasted.
|
|
46
|
+
modelDiffers: !card.lead && !!(lastRequestModel && configuredModel && lastRequestModel !== configuredModel),
|
|
47
|
+
lastRequestIsFallback: !!(lastRequestModel && fallbackRoutes.includes(lastRequestModel)),
|
|
48
|
+
// Shown model's source: 'request' (trusted route) or 'config' (configured primary, labelled as such).
|
|
49
|
+
modelSource: lastRequestModel ? 'request' : configuredModel ? 'config' : null,
|
|
50
|
+
routeWarn: routeOutsideConfig(lastRequestModel, card.role, allocById.get(card.id)),
|
|
51
|
+
liveModel: !!card.member?.model, task, queued: mine.length,
|
|
52
|
+
// A teammate is "delegated to" while it works or holds an in-progress task from the Lead.
|
|
53
|
+
delegated: !card.lead && (status === 'running' || status === 'provisioning' || task?.status === 'in_progress') };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Static model badge mapping: provider/model -> alias & rarity rank (legendary/epic/rare/cloth/default). */
|
|
57
|
+
function modelBadgeInfo(route, t) {
|
|
58
|
+
if (!route) return null;
|
|
59
|
+
const slash = route.indexOf('/');
|
|
60
|
+
const provider = slash > 0 ? route.slice(0, slash) : '';
|
|
61
|
+
const rawModel = slash > 0 ? route.slice(slash + 1) : route;
|
|
62
|
+
const p = provider.toLowerCase();
|
|
63
|
+
const m = rawModel.toLowerCase();
|
|
64
|
+
|
|
65
|
+
// Free / OpenCode channels use the cloth grade
|
|
66
|
+
if (p === 'opencode' || m.includes('free') || p.includes('free') || m.startsWith('free-')) {
|
|
67
|
+
return { alias: rawModel, grade: 'cloth', gradeLabel: t ? t('model.grade.cloth') : en['model.grade.cloth'] };
|
|
68
|
+
}
|
|
69
|
+
// claude-opus-* uses the legendary grade
|
|
70
|
+
if (m.includes('claude-opus') || m.includes('claude-3-opus') || m.includes('opus')) {
|
|
71
|
+
return { alias: rawModel, grade: 'legendary', gradeLabel: t ? t('model.grade.legendary') : en['model.grade.legendary'] };
|
|
72
|
+
}
|
|
73
|
+
// gpt-6.1-sol uses the epic grade
|
|
74
|
+
if (m.includes('gpt-6.1-sol') || m.includes('-sol')) {
|
|
75
|
+
return { alias: rawModel, grade: 'epic', gradeLabel: t ? t('model.grade.epic') : en['model.grade.epic'] };
|
|
76
|
+
}
|
|
77
|
+
// gemini-*-flash uses the rare grade
|
|
78
|
+
if (m.includes('gemini') && m.includes('flash')) {
|
|
79
|
+
return { alias: rawModel, grade: 'rare', gradeLabel: t ? t('model.grade.rare') : en['model.grade.rare'] };
|
|
80
|
+
}
|
|
81
|
+
// Unknown models retain their original name and default grade
|
|
82
|
+
return { alias: route, grade: 'default', gradeLabel: null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Status bubble for cards: task subject + status text, or idle static lines. */
|
|
86
|
+
function statusBubble(card, t, live) {
|
|
87
|
+
const isRunning = card.status === 'running';
|
|
88
|
+
const task = card.task;
|
|
89
|
+
const memberId = card.member?.id;
|
|
90
|
+
const act = memberId ? live?.projections?.(memberId)?.capabilityTeamActivity : undefined;
|
|
91
|
+
|
|
92
|
+
// Inspect the capabilityTeamActivity projection
|
|
93
|
+
if (act && typeof act === 'object') {
|
|
94
|
+
if (act.schemaVersion !== 1) {
|
|
95
|
+
return h('div', { className: 'ctm-bd-bubble' },
|
|
96
|
+
h('span', { className: 'ctm-sub ctm-warn' }, t('version.unsupported')));
|
|
97
|
+
}
|
|
98
|
+
const phase = act.phase;
|
|
99
|
+
const cur = act.current;
|
|
100
|
+
const activeCount = typeof act.activeCount === 'number' ? act.activeCount : 0;
|
|
101
|
+
const isHistoryUnsettled = !isRunning && cur && cur.status === 'running';
|
|
102
|
+
const recentList = Array.isArray(act.recent) ? act.recent : [];
|
|
103
|
+
|
|
104
|
+
let headBadge = null;
|
|
105
|
+
let bodyContent = null;
|
|
106
|
+
|
|
107
|
+
if (isHistoryUnsettled) {
|
|
108
|
+
headBadge = h('span', { className: 'ctm-badge', 'data-tone': 'warn' }, t('activity.unsettled'));
|
|
109
|
+
bodyContent = cur ? [cur.summary, cur.target ? ` (${cur.target})` : null].filter(Boolean).join('') : t('activity.unsettled');
|
|
110
|
+
} else if (phase === 'tool') {
|
|
111
|
+
headBadge = h('span', { className: 'ctm-badge', 'data-tone': 'brand' },
|
|
112
|
+
activeCount > 1 ? fmt(t('activity.parallel'), { n: activeCount }) : (cur?.tool ? fmt(t('activity.tool'), { tool: cur.tool }) : t('live.bubbleRunning')));
|
|
113
|
+
const summaryText = cur?.summary ?? '';
|
|
114
|
+
const targetText = cur?.target ? ` ${cur.target}` : '';
|
|
115
|
+
bodyContent = summaryText || targetText ? `${summaryText}${targetText}` : (task?.subject ?? t('live.busyNoTask'));
|
|
116
|
+
} else if (phase === 'model' && activeCount === 0) {
|
|
117
|
+
headBadge = h('span', { className: 'ctm-badge', 'data-tone': 'brand' }, t('activity.processing'));
|
|
118
|
+
bodyContent = task?.subject ?? t('live.busyNoTask');
|
|
119
|
+
} else if (phase === 'idle') {
|
|
120
|
+
const idleText = label(t, `live.${card.status}`, card.status);
|
|
121
|
+
headBadge = h('span', { className: 'ctm-sub' }, idleText);
|
|
122
|
+
const qKey = card.role?.quoteKey ?? characterQuoteKey(card.role?.id ?? card.id);
|
|
123
|
+
const quote = t(qKey);
|
|
124
|
+
bodyContent = cur?.summary ? `${cur.summary} · “${quote}”` : `“${quote}”`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (headBadge && bodyContent) {
|
|
128
|
+
return h('div', { className: 'ctm-bd-bubble' },
|
|
129
|
+
h('div', { className: 'ctm-bd-bubble-head' },
|
|
130
|
+
headBadge,
|
|
131
|
+
task ? h('span', { className: 'ctm-sub' }, `#${task.id}`) : null),
|
|
132
|
+
h('div', { className: 'ctm-bd-bubble-body ctm-bd-subject' }, bodyContent),
|
|
133
|
+
recentList.length ? h('details', { className: 'ctm-act-recent' },
|
|
134
|
+
h('summary', null, fmt(t('activity.recent'), { n: recentList.length })),
|
|
135
|
+
h('div', { className: 'ctm-act-list' },
|
|
136
|
+
h(CollapsibleList, {
|
|
137
|
+
items: recentList,
|
|
138
|
+
limit: 5,
|
|
139
|
+
t,
|
|
140
|
+
renderItem: (item, idx) => h('div', { key: item.id ?? item.callId ?? idx, className: 'ctm-act-row' },
|
|
141
|
+
h('span', { title: item.target ?? '' }, `${item.tool ? item.tool + ': ' : ''}${item.summary ?? ''}${item.target ? ' ' + item.target : ''}`),
|
|
142
|
+
h('span', { className: 'ctm-badge', 'data-tone': item.status === 'succeeded' ? 'success' : item.status === 'failed' ? 'error' : undefined }, item.status)),
|
|
143
|
+
}))) : null);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (task) {
|
|
148
|
+
const statusText = isRunning ? t('live.bubbleRunning') : t('live.bubbleWaiting');
|
|
149
|
+
return h('div', { className: 'ctm-bd-bubble', title: task.subject },
|
|
150
|
+
h('div', { className: 'ctm-bd-bubble-head' },
|
|
151
|
+
h('span', { className: 'ctm-badge', 'data-tone': isRunning ? 'brand' : 'warn' }, statusText),
|
|
152
|
+
h('span', { className: 'ctm-sub' }, `#${task.id}`),
|
|
153
|
+
card.queued > 1 ? h('span', { className: 'ctm-sub' }, `+${card.queued - 1}`) : null),
|
|
154
|
+
h('div', { className: 'ctm-bd-bubble-body ctm-bd-subject' }, task.subject));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isRunning) {
|
|
158
|
+
return h('div', { className: 'ctm-bd-bubble' },
|
|
159
|
+
h('div', { className: 'ctm-bd-bubble-head' },
|
|
160
|
+
h('span', { className: 'ctm-badge', 'data-tone': 'brand' }, t('live.bubbleRunning'))),
|
|
161
|
+
h('div', { className: 'ctm-bd-bubble-body ctm-sub' }, t('live.busyNoTask')));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const qKey = card.role?.quoteKey ?? characterQuoteKey(card.role?.id ?? card.id);
|
|
165
|
+
const quote = t(qKey);
|
|
166
|
+
const idleText = label(t, `live.${card.status}`, card.status);
|
|
167
|
+
return h('div', { className: 'ctm-bd-bubble' },
|
|
168
|
+
h('div', { className: 'ctm-bd-bubble-head' },
|
|
169
|
+
h('span', { className: 'ctm-sub' }, idleText)),
|
|
170
|
+
h('div', { className: 'ctm-bd-bubble-body ctm-bd-quote' }, `“${quote}”`));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Context pressure & token budget meters for one board card. */
|
|
174
|
+
function cardMeters(card, live, usage, t) {
|
|
175
|
+
const memberId = card.member?.id;
|
|
176
|
+
const proj = memberId ? live?.projections?.(memberId) : undefined;
|
|
177
|
+
|
|
178
|
+
// 1) Context pressure: projectedTokens / contextWindow (or pressureTokens)
|
|
179
|
+
const cp = proj?.contextPressure;
|
|
180
|
+
const projected = cp?.projectedTokens ?? cp?.pressureTokens;
|
|
181
|
+
const contextWindow = cp?.contextWindow;
|
|
182
|
+
const hasPressure = typeof projected === 'number' && typeof contextWindow === 'number' && contextWindow > 0;
|
|
183
|
+
|
|
184
|
+
// 2) Budget meter: reused from usageRows / maxTokens, rendered only when maxTokens exists
|
|
185
|
+
const max = card.role?.maxTokens;
|
|
186
|
+
let budgetNode = null;
|
|
187
|
+
if (max && max > 0) {
|
|
188
|
+
const uRow = usage?.find(u => (memberId && u.id === memberId) || u.name === card.id || (card.lead && u.name === 'lead'));
|
|
189
|
+
const ownUsage = proj?.capabilityTeamUsage;
|
|
190
|
+
// Strict Unknown contract: a budget bar may only render from a row that is itself not Unknown
|
|
191
|
+
// (coverage !== 'unknown' and reportedSamples > 0); a truthy `totals` object alone is not enough.
|
|
192
|
+
const totals = !isUnknownUsage(uRow) ? uRow.totals : (ownUsage && !isUnknownUsage(ownUsage) ? ownUsage.totals : null);
|
|
193
|
+
if (totals) {
|
|
194
|
+
const used = billable(totals);
|
|
195
|
+
const bRatio = Math.min(1, used / max);
|
|
196
|
+
const bTone = used >= max ? 'error' : bRatio >= 0.8 ? 'warn' : undefined;
|
|
197
|
+
budgetNode = h('div', { className: 'ctm-bd-meter-row', key: 'budget' },
|
|
198
|
+
h('div', { className: 'ctm-bd-meter-labels' },
|
|
199
|
+
h('span', null, t('live.budgetTitle')),
|
|
200
|
+
h('span', { className: 'ctm-us-num' }, fmt(t('usage.budget'), { used: compact(used), max: compact(max) }))),
|
|
201
|
+
h('div', { className: 'ctm-us-meter', 'data-tone': bTone, role: 'presentation' },
|
|
202
|
+
h('i', { style: { width: `${(bRatio * 100).toFixed(1)}%` } })));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let cpNode = null;
|
|
207
|
+
if (hasPressure) {
|
|
208
|
+
const cpRatio = Math.min(1, Math.max(0, projected / contextWindow));
|
|
209
|
+
const cpTone = cpRatio >= 0.9 ? 'error' : cpRatio >= 0.8 ? 'warn' : undefined;
|
|
210
|
+
cpNode = h('div', { className: 'ctm-bd-meter-row', key: 'pressure' },
|
|
211
|
+
h('div', { className: 'ctm-bd-meter-labels' },
|
|
212
|
+
h('span', null, t('live.contextPressure')),
|
|
213
|
+
h('span', { className: 'ctm-us-num' }, `${(cpRatio * 100).toFixed(0)}%`)),
|
|
214
|
+
h('div', { className: 'ctm-us-meter', 'data-tone': cpTone, role: 'presentation' },
|
|
215
|
+
h('i', { style: { width: `${(cpRatio * 100).toFixed(1)}%` } })));
|
|
216
|
+
} else {
|
|
217
|
+
// Missing data shows unknown without false meters
|
|
218
|
+
cpNode = h('div', { className: 'ctm-bd-meter-row', key: 'pressure-unknown' },
|
|
219
|
+
h('div', { className: 'ctm-bd-meter-labels' },
|
|
220
|
+
h('span', null, t('live.contextPressure')),
|
|
221
|
+
h('span', { className: 'ctm-sub' }, t('live.contextUnknown'))));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return h('div', { className: 'ctm-bd-meters' }, cpNode, budgetNode);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Compute longest dependency chain in DAG with cycle detection; robust on empty graphs. */
|
|
228
|
+
function computeLongestChain(tasks) {
|
|
229
|
+
if (!Array.isArray(tasks) || !tasks.length) return [];
|
|
230
|
+
const taskMap = new Map();
|
|
231
|
+
for (const tk of tasks) if (tk?.id != null) taskMap.set(String(tk.id), tk);
|
|
232
|
+
if (!taskMap.size) return [];
|
|
233
|
+
|
|
234
|
+
const adj = new Map();
|
|
235
|
+
for (const id of taskMap.keys()) adj.set(id, []);
|
|
236
|
+
for (const tk of taskMap.values()) {
|
|
237
|
+
const v = String(tk.id);
|
|
238
|
+
const blockers = Array.isArray(tk.blockedBy)
|
|
239
|
+
? tk.blockedBy
|
|
240
|
+
: (typeof tk.blockedBy === 'string' || typeof tk.blockedBy === 'number') && tk.blockedBy
|
|
241
|
+
? [tk.blockedBy]
|
|
242
|
+
: [];
|
|
243
|
+
for (const b of blockers) {
|
|
244
|
+
const u = String(b);
|
|
245
|
+
if (taskMap.has(u)) {
|
|
246
|
+
adj.get(u).push(v);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function dfs(u, visiting) {
|
|
252
|
+
visiting.add(u);
|
|
253
|
+
let bestChild = [];
|
|
254
|
+
const neighbors = adj.get(u) || [];
|
|
255
|
+
for (const v of neighbors) {
|
|
256
|
+
if (visiting.has(v)) continue; // Skip back-edge to avoid duplicate nodes and cycles
|
|
257
|
+
const path = dfs(v, visiting);
|
|
258
|
+
if (path.length > bestChild.length) {
|
|
259
|
+
bestChild = path;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
visiting.delete(u);
|
|
263
|
+
return [u, ...bestChild];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let longest = [];
|
|
267
|
+
for (const id of taskMap.keys()) {
|
|
268
|
+
const path = dfs(id, new Set());
|
|
269
|
+
if (path.length > longest.length) {
|
|
270
|
+
longest = path;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return longest;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Task dependency graph/list with highlighted longest dependency chain. */
|
|
277
|
+
function taskDependencyView(tasks, t) {
|
|
278
|
+
if (!tasks || !tasks.length) return null;
|
|
279
|
+
|
|
280
|
+
const longestChain = computeLongestChain(tasks);
|
|
281
|
+
const longestSet = new Set(longestChain);
|
|
282
|
+
const hasDeps = tasks.some(tk => {
|
|
283
|
+
if (Array.isArray(tk?.blockedBy)) return tk.blockedBy.length > 0;
|
|
284
|
+
return (typeof tk?.blockedBy === 'string' || typeof tk?.blockedBy === 'number') && String(tk.blockedBy).trim().length > 0;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return h('div', { className: 'ctm-bd-deps' },
|
|
288
|
+
h('div', { className: 'ctm-bd-deps-head' },
|
|
289
|
+
h('span', { className: 'ctm-label' }, t('live.taskDeps')),
|
|
290
|
+
longestChain.length >= 2 ? h('div', { className: 'ctm-bd-deps-chain', title: t('live.longestChain') },
|
|
291
|
+
h('strong', null, `${t('live.longestChain')}: `),
|
|
292
|
+
longestChain.map((id, idx) => h(React.Fragment, { key: id },
|
|
293
|
+
idx > 0 ? h('span', { className: 'ctm-sub' }, ' → ') : null,
|
|
294
|
+
h('span', { className: 'ctm-us-num' }, `#${id}`)))) : null),
|
|
295
|
+
!hasDeps ? h('p', { className: 'ctm-sub' }, t('live.noDeps'))
|
|
296
|
+
: h('div', { className: 'ctm-bd-deps-list' }, ...tasks.map(tk => {
|
|
297
|
+
const isLongest = longestSet.has(String(tk.id));
|
|
298
|
+
const blockers = Array.isArray(tk.blockedBy)
|
|
299
|
+
? tk.blockedBy
|
|
300
|
+
: (typeof tk.blockedBy === 'string' || typeof tk.blockedBy === 'number') && tk.blockedBy
|
|
301
|
+
? [tk.blockedBy]
|
|
302
|
+
: [];
|
|
303
|
+
return h('div', { key: tk.id, className: 'ctm-bd-dep-node', ...(isLongest ? { 'data-longest': '' } : {}) },
|
|
304
|
+
h('div', { className: 'ctm-bd-dep-top' },
|
|
305
|
+
h('span', { className: 'ctm-us-num' }, `#${tk.id}`),
|
|
306
|
+
h('span', { className: 'ctm-badge', 'data-tone': tk.status === 'completed' ? 'success' : tk.status === 'in_progress' ? 'brand' : undefined }, tk.status)),
|
|
307
|
+
h('span', { className: 'ctm-bd-dep-subj', title: tk.subject }, tk.subject),
|
|
308
|
+
blockers.length ? h('span', { className: 'ctm-bd-dep-blocked' }, `blockedBy: ${blockers.map(b => '#' + b).join(', ')}`) : null);
|
|
309
|
+
})));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Artifacts rack & Changes view (MVP-B). */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ---------- role skins: neutral default names are localized; Three Kingdoms quotes apply only to that roster ----------
|
|
2
|
+
const NEUTRAL_ROLE_NAMES = { lead: 'Lead', architect: 'Architect', backend: 'Backend', frontend: 'Frontend', researcher: 'Researcher', writer: 'Writer', qa: 'QA' };
|
|
3
|
+
const THREE_KINGDOMS_IDS = ['zhuge-liang', 'lv-bu', 'diao-chan', 'zhao-yun', 'chen-lin', 'zhou-yu'];
|
|
4
|
+
/** Idle quote key of one role id; `heroes` marks a roster using the optional three-kingdoms theme. */
|
|
5
|
+
function characterQuoteKey(roleId, heroes = false) {
|
|
6
|
+
if (roleId === 'lead' || roleId === 'sun-quan') return heroes || roleId === 'sun-quan' ? 'live.quote.sun-quan' : 'live.quote.lead';
|
|
7
|
+
if (THREE_KINGDOMS_IDS.includes(roleId) || Object.hasOwn(NEUTRAL_ROLE_NAMES, roleId)) return `live.quote.${roleId}`;
|
|
8
|
+
return 'live.quote.default';
|
|
9
|
+
}
|
|
10
|
+
/** Roles with untouched neutral default names translated (`role.name.<id>`) and their theme-aware idle quote key. */
|
|
11
|
+
function localizeRoles(roles, t) {
|
|
12
|
+
const list = Array.isArray(roles) ? roles : [];
|
|
13
|
+
const heroes = list.some(r => THREE_KINGDOMS_IDS.includes(r?.id));
|
|
14
|
+
return list.map(r => {
|
|
15
|
+
if (!r || typeof r !== 'object') return r;
|
|
16
|
+
const key = `role.name.${r.id}`, text = typeof t === 'function' && NEUTRAL_ROLE_NAMES[r.id] === r.displayName ? t(key) : key;
|
|
17
|
+
return { ...r, ...(text !== key ? { displayName: text } : {}), quoteKey: characterQuoteKey(r.id, heroes) };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
Object.assign(en, {
|
|
21
|
+
'role.name.lead': 'Lead', 'role.name.architect': 'Architect', 'role.name.backend': 'Backend', 'role.name.frontend': 'Frontend',
|
|
22
|
+
'role.name.researcher': 'Researcher', 'role.name.writer': 'Writer', 'role.name.qa': 'QA',
|
|
23
|
+
'live.quote.lead': 'Goals set, work assigned.', 'live.quote.architect': 'Design and contracts first, then code.', 'live.quote.backend': 'APIs and data, ready when you are.',
|
|
24
|
+
'live.quote.frontend': 'Pages and interactions, on it.', 'live.quote.researcher': 'Verify first, conclude second.', 'live.quote.writer': 'Docs ready to be written.', 'live.quote.qa': 'No evidence, not done.',
|
|
25
|
+
});
|