@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,375 @@
|
|
|
1
|
+
function ArtifactsRack({ sessionId, liveTeam, live, t }) {
|
|
2
|
+
const [filterRole, setFilterRole] = React.useState('all');
|
|
3
|
+
const [diffState, setDiffState] = React.useState({});
|
|
4
|
+
const [openError, setOpenError] = React.useState(null);
|
|
5
|
+
|
|
6
|
+
const AC = typeof AbortController === 'function' ? AbortController : class { constructor() { this.signal = { aborted: false }; } abort() { this.signal.aborted = true; } };
|
|
7
|
+
const abortRef = React.useRef(null);
|
|
8
|
+
if (!abortRef.current) abortRef.current = new AC();
|
|
9
|
+
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
return () => {
|
|
12
|
+
if (abortRef.current) {
|
|
13
|
+
abortRef.current.abort();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const members = liveTeam?.members ?? [];
|
|
19
|
+
const roleMap = new Map();
|
|
20
|
+
for (const m of members) {
|
|
21
|
+
if (m.id) roleMap.set(m.id, m.role ?? m.name ?? m.id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Aggregate every member capabilityTeamArtifacts projection
|
|
25
|
+
let versionUnsupported = false;
|
|
26
|
+
const allArtifacts = [];
|
|
27
|
+
const changesMap = new Map();
|
|
28
|
+
|
|
29
|
+
for (const m of members) {
|
|
30
|
+
if (!m.id) continue;
|
|
31
|
+
const artView = live?.projections?.(m.id)?.capabilityTeamArtifacts;
|
|
32
|
+
if (!artView || typeof artView !== 'object') continue;
|
|
33
|
+
if (artView.schemaVersion !== 1) {
|
|
34
|
+
versionUnsupported = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const roleId = roleMap.get(m.id) ?? m.name ?? m.id;
|
|
38
|
+
if (Array.isArray(artView.items)) {
|
|
39
|
+
for (const item of artView.items) {
|
|
40
|
+
allArtifacts.push({ ...item, memberSessionId: m.id, memberName: m.name, roleId });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(artView.changes)) {
|
|
44
|
+
for (const chg of artView.changes) {
|
|
45
|
+
const cKey = `${m.id}:${chg.seq}`;
|
|
46
|
+
if (!changesMap.has(cKey)) {
|
|
47
|
+
changesMap.set(cKey, { ...chg, memberSessionId: m.id, memberName: m.name, roleId });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (versionUnsupported && !allArtifacts.length && !changesMap.size) {
|
|
54
|
+
return h('p', { className: 'ctm-sub ctm-warn' }, t('version.unsupported'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Sort by (at, sessionId, presentSeq, fileIndex) descending; retain at most 100 items
|
|
58
|
+
allArtifacts.sort((a, b) => {
|
|
59
|
+
const atDiff = (b.at ?? 0) - (a.at ?? 0);
|
|
60
|
+
if (atDiff !== 0) return atDiff;
|
|
61
|
+
const sDiff = String(b.sessionId ?? '').localeCompare(String(a.sessionId ?? ''));
|
|
62
|
+
if (sDiff !== 0) return sDiff;
|
|
63
|
+
const pDiff = (b.presentSeq ?? 0) - (a.presentSeq ?? 0);
|
|
64
|
+
if (pDiff !== 0) return pDiff;
|
|
65
|
+
return (b.fileIndex ?? 0) - (a.fileIndex ?? 0);
|
|
66
|
+
});
|
|
67
|
+
const shelfItems = allArtifacts.slice(0, 100);
|
|
68
|
+
|
|
69
|
+
const roles = Array.from(new Set(shelfItems.map(item => item.roleId))).filter(Boolean);
|
|
70
|
+
const filteredItems = filterRole === 'all' ? shelfItems : shelfItems.filter(item => item.roleId === filterRole);
|
|
71
|
+
|
|
72
|
+
// Changes in this session
|
|
73
|
+
const changesList = Array.from(changesMap.values()).sort((a, b) => (b.at ?? 0) - (a.at ?? 0));
|
|
74
|
+
|
|
75
|
+
const handleOpenPresent = async item => {
|
|
76
|
+
setOpenError(null);
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(apiUrl('api/present.open', { sessionId: item.sessionId, seq: item.presentSeq, index: item.fileIndex }), {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
signal: abortRef.current?.signal,
|
|
81
|
+
});
|
|
82
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
setOpenError(`${t('artifacts.openFail')} (${res.status})`);
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (abortRef.current?.signal?.aborted || e?.name === 'AbortError') return;
|
|
88
|
+
setOpenError(String(e.message ?? e));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleToggleChange = async chg => {
|
|
93
|
+
const cKey = `${chg.memberSessionId}:${chg.seq}`;
|
|
94
|
+
if (diffState[cKey]) {
|
|
95
|
+
setDiffState(prev => { const n = { ...prev }; delete n[cKey]; return n; });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setDiffState(prev => ({ ...prev, [cKey]: { loading: true } }));
|
|
99
|
+
try {
|
|
100
|
+
const sumRes = await fetch(apiUrl('api/changes.summary', { sessionId: chg.memberSessionId, seq: chg.seq }), {
|
|
101
|
+
signal: abortRef.current?.signal,
|
|
102
|
+
});
|
|
103
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
104
|
+
if (!sumRes.ok) {
|
|
105
|
+
const errText = sumRes.status === 404 ? t('artifacts.baselineInvalid') : `${sumRes.status}`;
|
|
106
|
+
setDiffState(prev => ({ ...prev, [cKey]: { error: errText } }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const sumData = await sumRes.json();
|
|
110
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
111
|
+
const files = sumData.files ?? [];
|
|
112
|
+
const diffResults = [];
|
|
113
|
+
for (let i = 0; i < files.length; i++) {
|
|
114
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
115
|
+
const diffRes = await fetch(apiUrl('api/changes.diff', { sessionId: chg.memberSessionId, seq: chg.seq, index: i }), {
|
|
116
|
+
signal: abortRef.current?.signal,
|
|
117
|
+
});
|
|
118
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
119
|
+
if (!diffRes.ok) {
|
|
120
|
+
diffResults.push({ path: files[i].path, error: diffRes.status === 404 ? t('artifacts.baselineInvalid') : `Error ${diffRes.status}` });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const diffJson = await diffRes.json();
|
|
124
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
125
|
+
const textContent = JSON.stringify(diffJson);
|
|
126
|
+
if (textContent.length > 1024 * 1024) {
|
|
127
|
+
diffResults.push({ path: files[i].path, error: t('artifacts.diffOversized') });
|
|
128
|
+
} else {
|
|
129
|
+
diffResults.push({ path: files[i].path, diff: diffJson });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (abortRef.current?.signal?.aborted) return;
|
|
133
|
+
setDiffState(prev => ({ ...prev, [cKey]: { files: diffResults } }));
|
|
134
|
+
} catch (e) {
|
|
135
|
+
if (abortRef.current?.signal?.aborted || e?.name === 'AbortError') return;
|
|
136
|
+
setDiffState(prev => ({ ...prev, [cKey]: { error: String(e.message ?? e) } }));
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return h('div', { className: 'ctm-art-shelf' },
|
|
141
|
+
h('div', { className: 'ctm-art-filter' },
|
|
142
|
+
h('span', { className: 'ctm-label' }, t('artifacts.title')),
|
|
143
|
+
roles.length > 1 ? h('select', {
|
|
144
|
+
className: 'ctm-btn',
|
|
145
|
+
value: filterRole,
|
|
146
|
+
onChange: e => setFilterRole(e.target.value),
|
|
147
|
+
},
|
|
148
|
+
h('option', { value: 'all' }, t('artifacts.allRoles')),
|
|
149
|
+
...roles.map(r => h('option', { key: r, value: r }, r))) : null),
|
|
150
|
+
openError ? h('p', { className: 'ctm-warn' }, openError) : null,
|
|
151
|
+
!filteredItems.length ? h('p', { className: 'ctm-sub' }, t('artifacts.empty'))
|
|
152
|
+
: h('div', { className: 'ctm-art-list' }, ...filteredItems.map(item =>
|
|
153
|
+
h('div', { key: `${item.sessionId}:${item.presentSeq}:${item.fileIndex}`, className: 'ctm-art-card' },
|
|
154
|
+
h('div', { className: 'ctm-art-head' },
|
|
155
|
+
h('span', { className: 'ctm-badge', 'data-tone': 'brand' }, item.roleId),
|
|
156
|
+
h('button', {
|
|
157
|
+
type: 'button', className: 'ctm-btn',
|
|
158
|
+
onClick: () => handleOpenPresent(item),
|
|
159
|
+
}, t('artifacts.openDesktop'))),
|
|
160
|
+
h('span', { className: 'ctm-art-path', title: item.path }, item.displayPath ?? item.path),
|
|
161
|
+
item.at ? h('span', { className: 'ctm-sub' }, when(item.at)) : null))),
|
|
162
|
+
h('div', { className: 'ctm-art-changes' },
|
|
163
|
+
h('span', { className: 'ctm-label' }, t('artifacts.changes')),
|
|
164
|
+
!changesList.length ? h('p', { className: 'ctm-sub' }, t('artifacts.noChanges'))
|
|
165
|
+
: (changesList.length <= 5
|
|
166
|
+
? changesList.map(chg => {
|
|
167
|
+
const cKey = `${chg.memberSessionId}:${chg.seq}`;
|
|
168
|
+
const diffEntry = diffState[cKey];
|
|
169
|
+
return h('details', { key: cKey, className: 'ctm-chg-item', onToggle: e => { if (e.target.open && !diffEntry) handleToggleChange(chg); } },
|
|
170
|
+
h('summary', null, `${chg.roleId} · Turn ${chg.turn ?? '?'} · Seq ${chg.seq}`),
|
|
171
|
+
diffEntry?.loading ? h('p', { className: 'ctm-sub' }, t('overview.loading'))
|
|
172
|
+
: diffEntry?.error ? h('p', { className: 'ctm-warn' }, diffEntry.error)
|
|
173
|
+
: diffEntry?.files ? h('div', { className: 'ctm-chg-diff' },
|
|
174
|
+
...diffEntry.files.map((f, fi) => h('div', { key: fi },
|
|
175
|
+
h('strong', null, f.path),
|
|
176
|
+
f.error ? h('p', { className: 'ctm-warn' }, f.error)
|
|
177
|
+
: h('pre', { className: 'ctm-pre' }, JSON.stringify(f.diff?.hunks ?? f.diff ?? t('artifacts.diffEmpty'), null, 2)))))
|
|
178
|
+
: null);
|
|
179
|
+
})
|
|
180
|
+
: h(CollapsibleList, {
|
|
181
|
+
items: changesList,
|
|
182
|
+
limit: 5,
|
|
183
|
+
t,
|
|
184
|
+
renderItem: chg => {
|
|
185
|
+
const cKey = `${chg.memberSessionId}:${chg.seq}`;
|
|
186
|
+
const diffEntry = diffState[cKey];
|
|
187
|
+
return h('details', { key: cKey, className: 'ctm-chg-item', onToggle: e => { if (e.target.open && !diffEntry) handleToggleChange(chg); } },
|
|
188
|
+
h('summary', null, `${chg.roleId} · Turn ${chg.turn ?? '?'} · Seq ${chg.seq}`),
|
|
189
|
+
diffEntry?.loading ? h('p', { className: 'ctm-sub' }, t('overview.loading'))
|
|
190
|
+
: diffEntry?.error ? h('p', { className: 'ctm-warn' }, diffEntry.error)
|
|
191
|
+
: diffEntry?.files ? h('div', { className: 'ctm-chg-diff' },
|
|
192
|
+
...diffEntry.files.map((f, fi) => h('div', { key: fi },
|
|
193
|
+
h('strong', null, f.path),
|
|
194
|
+
f.error ? h('p', { className: 'ctm-warn' }, f.error)
|
|
195
|
+
: h('pre', { className: 'ctm-pre' }, JSON.stringify(f.diff?.hunks ?? f.diff ?? t('artifacts.diffEmpty'), null, 2)))))
|
|
196
|
+
: null);
|
|
197
|
+
},
|
|
198
|
+
}))));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Battle report view (MVP-B). */
|
|
202
|
+
function BattleReportView({ sessionId, liveTeam, live, t, runtimeState, singleModel }) {
|
|
203
|
+
const members = liveTeam?.members ?? [];
|
|
204
|
+
let totalTurns = 0;
|
|
205
|
+
let totalSteps = 0;
|
|
206
|
+
let totalCompletedMs = 0;
|
|
207
|
+
let earliestFirstTurn = null;
|
|
208
|
+
let latestEndTurn = null;
|
|
209
|
+
|
|
210
|
+
let uncachedInput = 0;
|
|
211
|
+
let cacheRead = 0;
|
|
212
|
+
let cacheWrite = 0;
|
|
213
|
+
let outputTokens = 0;
|
|
214
|
+
|
|
215
|
+
let boardMutationsVal = null;
|
|
216
|
+
let hostRetries = 0;
|
|
217
|
+
let failedTurns = 0;
|
|
218
|
+
let msgAccepted = 0;
|
|
219
|
+
|
|
220
|
+
let versionUnsupported = false;
|
|
221
|
+
let hasReport = false;
|
|
222
|
+
// Team-level usage coverage (item 2): any member reporting 'partial' downgrades the whole team to
|
|
223
|
+
// 'partial'; members reporting 'unknown' never contribute tokens to the totals below and are listed
|
|
224
|
+
// separately so the aggregate never silently absorbs an unknown member's usage as zero.
|
|
225
|
+
let teamCoverage = 'complete';
|
|
226
|
+
const unknownMembers = [];
|
|
227
|
+
|
|
228
|
+
for (const m of members) {
|
|
229
|
+
if (!m.id) continue;
|
|
230
|
+
const rpt = live?.projections?.(m.id)?.capabilityTeamReport;
|
|
231
|
+
if (!rpt || typeof rpt !== 'object') continue;
|
|
232
|
+
if (rpt.schemaVersion !== 1) {
|
|
233
|
+
versionUnsupported = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
hasReport = true;
|
|
237
|
+
totalTurns += rpt.turns ?? 0;
|
|
238
|
+
totalSteps += rpt.steps ?? 0;
|
|
239
|
+
totalCompletedMs += rpt.completedTurnMs ?? 0;
|
|
240
|
+
|
|
241
|
+
if (typeof rpt.firstTurnAt === 'number') {
|
|
242
|
+
earliestFirstTurn = earliestFirstTurn === null ? rpt.firstTurnAt : Math.min(earliestFirstTurn, rpt.firstTurnAt);
|
|
243
|
+
}
|
|
244
|
+
if (typeof rpt.lastTurnEndAt === 'number') {
|
|
245
|
+
latestEndTurn = latestEndTurn === null ? rpt.lastTurnEndAt : Math.max(latestEndTurn, rpt.lastTurnEndAt);
|
|
246
|
+
}
|
|
247
|
+
if (typeof rpt.openTurnAt === 'number') {
|
|
248
|
+
latestEndTurn = latestEndTurn === null ? rpt.openTurnAt : Math.max(latestEndTurn, rpt.openTurnAt);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const memberCoverage = rpt.usageCoverage?.coverage;
|
|
252
|
+
if (memberCoverage === 'unknown') {
|
|
253
|
+
unknownMembers.push(m.name ?? m.id);
|
|
254
|
+
} else {
|
|
255
|
+
if (memberCoverage === 'partial') teamCoverage = teamCoverage === 'complete' ? 'partial' : teamCoverage;
|
|
256
|
+
const u = rpt.usage;
|
|
257
|
+
if (u) {
|
|
258
|
+
uncachedInput += u.uncachedInputTokens ?? 0;
|
|
259
|
+
cacheRead += u.cacheReadTokens ?? 0;
|
|
260
|
+
cacheWrite += u.cacheWriteTokens ?? 0;
|
|
261
|
+
outputTokens += u.outputTokens ?? 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const cnt = rpt.counters;
|
|
266
|
+
if (cnt) {
|
|
267
|
+
hostRetries += cnt.hostRetriesStarted ?? 0;
|
|
268
|
+
failedTurns += cnt.failedTurns ?? 0;
|
|
269
|
+
msgAccepted += cnt.sendMessageAccepted ?? 0;
|
|
270
|
+
if (m.role === 'lead' || m.name === 'lead') {
|
|
271
|
+
boardMutationsVal = cnt.boardMutations ?? null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (versionUnsupported && !hasReport) {
|
|
277
|
+
return h('p', { className: 'ctm-sub ctm-warn' }, t('version.unsupported'));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const billableTokens = uncachedInput + cacheWrite + outputTokens;
|
|
281
|
+
const windowEnd = latestEndTurn ?? Date.now();
|
|
282
|
+
const windowStart = earliestFirstTurn ?? windowEnd;
|
|
283
|
+
const windowSpan = elapsed(Math.max(0, windowEnd - windowStart));
|
|
284
|
+
|
|
285
|
+
// Runtime failover metrics summary
|
|
286
|
+
const runtimeAvailable = runtimeState && !runtimeState.unavailable && runtimeState.data;
|
|
287
|
+
const runtimeMembers = runtimeAvailable && Array.isArray(runtimeState.data.members) ? runtimeState.data.members : [];
|
|
288
|
+
let rRequestFailures = 0;
|
|
289
|
+
let rPluginRetries = 0;
|
|
290
|
+
let rFailoverSwitches = 0;
|
|
291
|
+
let rLongContextSwitches = 0;
|
|
292
|
+
if (runtimeAvailable) {
|
|
293
|
+
for (const rm of runtimeMembers) {
|
|
294
|
+
const rc = rm.counters;
|
|
295
|
+
if (rc) {
|
|
296
|
+
rRequestFailures += rc.requestFailures ?? 0;
|
|
297
|
+
rPluginRetries += rc.pluginRetries ?? 0;
|
|
298
|
+
rFailoverSwitches += rc.failoverSwitches ?? 0;
|
|
299
|
+
rLongContextSwitches += rc.longContextSwitches ?? 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Aggregate presentation
|
|
305
|
+
return h('div', { className: 'ctm-rpt-shelf' },
|
|
306
|
+
h('span', { className: 'ctm-label' }, t('report.title'),
|
|
307
|
+
singleModel ? h('span', { className: 'ctm-badge', 'data-tone': 'warn', 'data-single-model': '', title: `${singleModel.provider}/${singleModel.model}` }, t('roles.singleModel')) : null),
|
|
308
|
+
h('p', { className: 'ctm-sub' },
|
|
309
|
+
fmt(t('report.window'), {
|
|
310
|
+
start: earliestFirstTurn ? when(earliestFirstTurn) : t('common.unknown'),
|
|
311
|
+
end: latestEndTurn ? when(latestEndTurn) : when(windowEnd),
|
|
312
|
+
span: windowSpan,
|
|
313
|
+
})),
|
|
314
|
+
h('div', { className: 'ctm-rpt-grid' },
|
|
315
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
316
|
+
h('span', { className: 'ctm-sub' }, t('report.wallTime')),
|
|
317
|
+
h('strong', null, elapsed(totalCompletedMs))),
|
|
318
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
319
|
+
h('span', { className: 'ctm-sub' }, t('report.tokensTotal')),
|
|
320
|
+
h('strong', null, compact(billableTokens)),
|
|
321
|
+
teamCoverage === 'partial' ? badge(t('usage.partialLow'), 'warn', 'team-partial') : null,
|
|
322
|
+
h('span', { className: 'ctm-sub' }, fmt(t('report.tokensIo'), { in: compact(uncachedInput), out: compact(outputTokens), cw: compact(cacheWrite) })),
|
|
323
|
+
unknownMembers.length ? h('p', { className: 'ctm-sub ctm-warn' }, fmt(t('report.unknownMembers'), { list: unknownMembers.join(t('common.listSep')) })) : null),
|
|
324
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
325
|
+
h('span', { className: 'ctm-sub' }, t('report.cacheRead')),
|
|
326
|
+
h('strong', null, compact(cacheRead))),
|
|
327
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
328
|
+
h('span', { className: 'ctm-sub' }, t('report.turnsSteps')),
|
|
329
|
+
h('strong', null, `${totalTurns} / ${totalSteps}`)),
|
|
330
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
331
|
+
h('span', { className: 'ctm-sub' }, t('report.mutations')),
|
|
332
|
+
h('strong', null, boardMutationsVal !== null ? String(boardMutationsVal) : '—'))),
|
|
333
|
+
h('div', { className: 'ctm-rpt-section' },
|
|
334
|
+
h('span', { className: 'ctm-label' }, t('report.sectionOwn')),
|
|
335
|
+
h('div', { className: 'ctm-rpt-grid' },
|
|
336
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
337
|
+
h('span', { className: 'ctm-sub' }, t('report.hostRetries', { n: '' })),
|
|
338
|
+
h('strong', null, String(hostRetries))),
|
|
339
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
340
|
+
h('span', { className: 'ctm-sub' }, t('report.failedTurns', { n: '' })),
|
|
341
|
+
h('strong', null, String(failedTurns))),
|
|
342
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
343
|
+
h('span', { className: 'ctm-sub' }, t('report.msgAccepted', { n: '' })),
|
|
344
|
+
h('strong', null, String(msgAccepted))))),
|
|
345
|
+
h('div', { className: 'ctm-rpt-section' },
|
|
346
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
347
|
+
h('span', { className: 'ctm-label' }, t('report.sectionRuntime')),
|
|
348
|
+
runtimeState?.restarted ? h('span', { className: 'ctm-badge', 'data-tone': 'brand' }, t('runtime.restarted')) : null),
|
|
349
|
+
h('div', { className: 'ctm-rpt-grid' },
|
|
350
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
351
|
+
h('span', { className: 'ctm-sub' }, t('report.requestFailures', { n: '' })),
|
|
352
|
+
h('strong', null, runtimeAvailable ? String(rRequestFailures) : t('common.unknown'))),
|
|
353
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
354
|
+
h('span', { className: 'ctm-sub' }, t('report.pluginRetries', { n: '' })),
|
|
355
|
+
h('strong', null, runtimeAvailable ? String(rPluginRetries) : t('common.unknown'))),
|
|
356
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
357
|
+
h('span', { className: 'ctm-sub' }, t('report.failoverSwitches', { n: '' })),
|
|
358
|
+
h('strong', null, runtimeAvailable ? String(rFailoverSwitches) : t('common.unknown'))),
|
|
359
|
+
h('div', { className: 'ctm-rpt-box' },
|
|
360
|
+
h('span', { className: 'ctm-sub' }, t('report.longContextSwitches', { n: '' })),
|
|
361
|
+
h('strong', null, runtimeAvailable ? String(rLongContextSwitches) : t('common.unknown'))))),
|
|
362
|
+
h('details', { className: 'ctm-rpt-defs' },
|
|
363
|
+
h('summary', null, t('report.def.title')),
|
|
364
|
+
h('div', { className: 'ctm-rpt-defs-list' },
|
|
365
|
+
h('p', null, t('report.def.wallTime')),
|
|
366
|
+
h('p', null, t('report.def.cacheRead')),
|
|
367
|
+
h('p', null, t('report.def.mutations')),
|
|
368
|
+
h('p', null, t('report.def.hostRetries')),
|
|
369
|
+
h('p', null, t('report.def.failedTurns')),
|
|
370
|
+
h('p', null, t('report.def.msgAccepted')),
|
|
371
|
+
h('p', null, t('report.def.requestFailures')),
|
|
372
|
+
h('p', null, t('report.def.pluginRetries')),
|
|
373
|
+
h('p', null, t('report.def.failoverSwitches')),
|
|
374
|
+
h('p', null, t('report.def.longContextSwitches')))));
|
|
375
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ---------- task control (per-member step budget and temporary model) ----------
|
|
2
|
+
// Reads the lease from the runtime snapshot the board already subscribes to (no extra polling). Writes go
|
|
3
|
+
// through POST api/capability-team.task-control; a temporary model is only sent after the in-page confirm.
|
|
4
|
+
const CONTROL_ERRORS = { 400: 'badRequest', 403: 'forbidden', 404: 'notFound', 409: 'conflict', 422: 'invalid', 503: 'unavailable' };
|
|
5
|
+
const COMPLEXITIES = ['simple', 'medium', 'complex'];
|
|
6
|
+
const requestId = () => (typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `r-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
7
|
+
const routeOf = r => (r ? `${r.provider ? r.provider + '/' : ''}${r.model ?? ''}` : '');
|
|
8
|
+
/** The member's live lease, else its most recent one. */
|
|
9
|
+
function leaseFor(control, roleId) {
|
|
10
|
+
const all = Array.isArray(control?.leases) ? control.leases.filter(l => l.roleId === roleId) : [];
|
|
11
|
+
return all.find(l => l.status !== 'ended') ?? all[all.length - 1];
|
|
12
|
+
}
|
|
13
|
+
const stepTone = judgment => (judgment === 'unqualified' ? 'error' : judgment === 'warning' ? 'warn' : 'ok');
|
|
14
|
+
async function controlRequest(path, init) {
|
|
15
|
+
let res;
|
|
16
|
+
try { res = await fetch(path, init); } catch (error) { if (error?.name === 'AbortError') throw error; return { ok: false, kind: 'network' }; }
|
|
17
|
+
let body = null;
|
|
18
|
+
try { body = await res.json(); } catch { /* empty or non-JSON body */ }
|
|
19
|
+
if (res.ok) return { ok: true, body };
|
|
20
|
+
return { ok: false, kind: CONTROL_ERRORS[res.status] ?? 'unknown', status: res.status, message: body?.message };
|
|
21
|
+
}
|
|
22
|
+
function TaskControl({ leadSessionId, roleId, name, configured, control, t }) {
|
|
23
|
+
const lease = leaseFor(control, roleId);
|
|
24
|
+
const [open, setOpen] = React.useState(false);
|
|
25
|
+
const [candidates, setCandidates] = React.useState(null);
|
|
26
|
+
const [pick, setPick] = React.useState('');
|
|
27
|
+
const [reason, setReason] = React.useState('');
|
|
28
|
+
const [complexity, setComplexity] = React.useState('medium');
|
|
29
|
+
const [confirming, setConfirming] = React.useState(false);
|
|
30
|
+
const [pending, setPending] = React.useState(false);
|
|
31
|
+
const [error, setError] = React.useState(null);
|
|
32
|
+
const aborter = React.useRef(null);
|
|
33
|
+
React.useEffect(() => () => aborter.current?.abort(), []);
|
|
34
|
+
const freshSignal = () => { aborter.current?.abort(); aborter.current = typeof AbortController === 'function' ? new AbortController() : null; return aborter.current?.signal; };
|
|
35
|
+
const stop = event => event.stopPropagation();
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (!open || candidates || !lease || lease.status === 'ended') return;
|
|
38
|
+
let live = true;
|
|
39
|
+
controlRequest(apiUrl('api/capability-team.override-candidates', { sessionId: leadSessionId, role: roleId }), { signal: freshSignal() })
|
|
40
|
+
.then(r => { if (live) setCandidates(r.ok ? r.body : { error: r.kind, candidates: [] }); })
|
|
41
|
+
.catch(() => {});
|
|
42
|
+
return () => { live = false; };
|
|
43
|
+
}, [open, leadSessionId, roleId, Boolean(lease && lease.status !== 'ended')]);
|
|
44
|
+
const post = async body => {
|
|
45
|
+
setPending(true); setError(null);
|
|
46
|
+
try {
|
|
47
|
+
const r = await controlRequest(apiUrl('api/capability-team.task-control'), {
|
|
48
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, signal: freshSignal(),
|
|
49
|
+
body: JSON.stringify({ sessionId: leadSessionId, requestId: requestId(), role: roleId, ...body }),
|
|
50
|
+
});
|
|
51
|
+
if (!r.ok) setError(r.kind); else { setConfirming(false); setPick(''); setReason(''); }
|
|
52
|
+
} catch (e) { if (e?.name !== 'AbortError') setError('network'); }
|
|
53
|
+
finally { setPending(false); }
|
|
54
|
+
};
|
|
55
|
+
const cas = lease ? { lease_id: lease.leaseId, expected_revision: lease.revision } : {};
|
|
56
|
+
const chosen = candidates?.candidates?.find(c => routeOf(c) === pick);
|
|
57
|
+
const ended = !lease || lease.status === 'ended';
|
|
58
|
+
const used = lease?.startsUsed ?? 0;
|
|
59
|
+
const est = lease?.estimatedSteps ?? 0;
|
|
60
|
+
const ratio = est > 0 ? Math.min(1, used / est) : 0;
|
|
61
|
+
const tone = stepTone(lease?.judgment);
|
|
62
|
+
const audit = (Array.isArray(control?.auditTail) ? control.auditTail : []).filter(r => r.roleId === roleId);
|
|
63
|
+
const errorText = error ? `${label(t, `control.error.${error}`, t('control.error.unknown'))}${error === 'conflict' ? ` ${t('control.error.reconfirm')}` : ''}` : null;
|
|
64
|
+
const summary = lease
|
|
65
|
+
? (lease.status === 'ended' ? t('control.lease.ended') : fmt(t('control.steps.label'), { used, est }))
|
|
66
|
+
: t('control.budget.title');
|
|
67
|
+
return h('details', { className: 'ctm-tc', 'data-tone': tone, open, onClick: stop, onKeyDown: stop,
|
|
68
|
+
onToggle: event => setOpen(event.currentTarget.open) },
|
|
69
|
+
h('summary', null, h('span', { className: 'ctm-label' }, t('control.title')), h('span', { className: 'ctm-sub' }, summary)),
|
|
70
|
+
lease ? h('div', { className: 'ctm-tc-body' },
|
|
71
|
+
h('div', { className: 'ctm-tc-row' },
|
|
72
|
+
badge(label(t, `control.lease.status.${lease.status}`, lease.status), ended ? undefined : 'brand', 'st'),
|
|
73
|
+
!ended && lease.judgment !== 'within' ? badge(t(`control.steps.${lease.judgment}`), tone, 'j') : null),
|
|
74
|
+
est > 0 ? h('div', { className: 'ctm-us-meter', 'data-tone': tone, role: 'meter', 'aria-valuemin': 0, 'aria-valuemax': est, 'aria-valuenow': used, 'aria-label': fmt(t('control.steps.label'), { used, est }) },
|
|
75
|
+
h('i', { style: { width: `${(ratio * 100).toFixed(1)}%` } })) : null,
|
|
76
|
+
h('p', { className: 'ctm-sub' }, t('control.steps.disclaimer')),
|
|
77
|
+
lease.override
|
|
78
|
+
? h('div', { className: 'ctm-tc-override' },
|
|
79
|
+
h('span', { className: 'ctm-chip' }, fmt(t('control.override.badge'), { from: routeOf(lease.override.from), to: routeOf(lease.override) })),
|
|
80
|
+
lease.expiresAt ? h('span', { className: 'ctm-sub' }, fmt(t('control.override.expiresAt'), { time: timeOfDay(lease.expiresAt) })) : null,
|
|
81
|
+
h('span', { className: 'ctm-sub' }, t('control.override.autoRestore')),
|
|
82
|
+
h('span', { className: 'ctm-sub' }, t('control.override.pauseLongContext')))
|
|
83
|
+
: h('p', { className: 'ctm-sub' }, t('control.override.none')),
|
|
84
|
+
ended ? null : h('div', { className: 'ctm-tc-actions' },
|
|
85
|
+
h('label', { className: 'ctm-sub' }, t('control.override.pick'),
|
|
86
|
+
h('select', { value: pick, disabled: pending || !candidates?.candidates?.length, onChange: e => { setPick(e.target.value); setConfirming(false); } },
|
|
87
|
+
h('option', { value: '' }, candidates?.candidates?.length ? '—' : t('control.override.noCandidates')),
|
|
88
|
+
...(candidates?.candidates ?? []).map(c => h('option', { key: routeOf(c), value: routeOf(c) }, `${routeOf(c)} · ${label(t, `tier.${c.tier}`, c.tier)}`)))),
|
|
89
|
+
h('span', { className: 'ctm-sub' }, t('control.override.pickHint')),
|
|
90
|
+
h('input', { type: 'text', className: 'ctm-tc-reason', placeholder: t('control.override.reason'), value: reason, maxLength: 200, onChange: e => setReason(e.target.value) }),
|
|
91
|
+
h('div', { className: 'ctm-tc-row' },
|
|
92
|
+
h('button', { type: 'button', className: 'ctm-btn', disabled: pending || !chosen, onClick: () => setConfirming(true) }, t('control.override.setAction')),
|
|
93
|
+
lease.override ? h('button', { type: 'button', className: 'ctm-btn', disabled: pending, onClick: () => post({ action: 'clear-override', ...cas, reason }) }, t('control.override.restoreAction')) : null,
|
|
94
|
+
h('button', { type: 'button', className: 'ctm-btn', disabled: pending, onClick: () => post({ action: 'finish-lease', ...cas, reason }) }, t('control.action.end')))),
|
|
95
|
+
confirming && chosen ? h('div', { className: 'ctm-tc-confirm', role: 'alertdialog', 'aria-label': t('control.override.confirm.title') },
|
|
96
|
+
h('strong', null, t('control.override.confirm.title')),
|
|
97
|
+
h('span', null, fmt(t('control.override.confirm.member'), { name })),
|
|
98
|
+
h('span', null, fmt(t('control.override.confirm.change'), { from: lease.override ? routeOf(lease.override) : (configured || t('live.configModel')), to: routeOf(chosen), tier: label(t, `tier.${chosen.tier}`, chosen.tier) })),
|
|
99
|
+
h('span', { className: 'ctm-warn' }, t('control.override.confirm.cost')),
|
|
100
|
+
h('span', { className: 'ctm-sub' }, t('control.override.confirm.restore')),
|
|
101
|
+
h('div', { className: 'ctm-tc-row' },
|
|
102
|
+
h('button', { type: 'button', className: 'ctm-btn', 'data-primary': '', disabled: pending,
|
|
103
|
+
onClick: () => post({ action: 'set-override', ...cas, provider: chosen.provider, model: chosen.model, catalog_version: candidates.catalogVersion, reason }) }, t('control.override.confirm.ok')),
|
|
104
|
+
h('button', { type: 'button', className: 'ctm-btn', disabled: pending, onClick: () => setConfirming(false) }, t('control.override.confirm.cancel')))) : null)
|
|
105
|
+
: null,
|
|
106
|
+
ended ? h('div', { className: 'ctm-tc-row' },
|
|
107
|
+
h('label', { className: 'ctm-sub' }, t('control.budget.complexity'),
|
|
108
|
+
h('select', { value: complexity, disabled: pending, onChange: e => setComplexity(e.target.value) },
|
|
109
|
+
...COMPLEXITIES.map(c => h('option', { key: c, value: c }, t(`control.budget.complexity.${c}`))))),
|
|
110
|
+
h('button', { type: 'button', className: 'ctm-btn', disabled: pending, onClick: () => post({ action: 'start-budget', complexity, reason }) }, t('control.budget.startAction'))) : null,
|
|
111
|
+
pending ? h('p', { className: 'ctm-sub' }, t('control.pending')) : null,
|
|
112
|
+
errorText ? h('p', { className: 'ctm-sub ctm-warn', role: 'alert' }, errorText) : null,
|
|
113
|
+
h('div', { className: 'ctm-tc-audit' },
|
|
114
|
+
h('span', { className: 'ctm-sub' }, t('control.audit.title')),
|
|
115
|
+
audit.length
|
|
116
|
+
? (audit.length <= 5
|
|
117
|
+
? h('ul', null, ...audit.map(r => h('li', { key: r.eventId }, `${timeOfDay(r.at)} · ${String(r.action ?? '')}${r.to ? ` → ${routeOf(r.to)}` : ''}${r.reason ? ` · ${String(r.reason)}` : ''}`)))
|
|
118
|
+
: h(CollapsibleList, {
|
|
119
|
+
items: audit,
|
|
120
|
+
limit: 5,
|
|
121
|
+
as: 'ul',
|
|
122
|
+
t,
|
|
123
|
+
renderItem: r => h('li', { key: r.eventId }, `${timeOfDay(r.at)} · ${String(r.action ?? '')}${r.to ? ` → ${routeOf(r.to)}` : ''}${r.reason ? ` · ${String(r.reason)}` : ''}`),
|
|
124
|
+
}))
|
|
125
|
+
: h('span', { className: 'ctm-sub' }, t('control.audit.empty'))));
|
|
126
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// ---------- office stage: deterministic palettes and pixel sprites (canvas draws shapes only; every text is DOM) ----------
|
|
2
|
+
const STAGE_SKINS = ['#f3d3a2', '#f6e1c3', '#e8b98a', '#d9a06e', '#fbe3cc'];
|
|
3
|
+
/** Desk prop per capability; unknown capabilities get a bare desk. */
|
|
4
|
+
const STAGE_PROPS = { orchestrator: 'map', 'technical-director': 'scroll', backend: 'gear', frontend: 'palette', research: 'glass', writer: 'brush', qa: 'flask' };
|
|
5
|
+
function stageHash(str) { let n = 2166136261; for (const ch of String(str)) { n ^= ch.charCodeAt(0); n = Math.imul(n, 16777619) >>> 0; } return n; }
|
|
6
|
+
/** Theme-agnostic look: the role id picks the hues, the capability picks the prop, so any theme's names render consistently. */
|
|
7
|
+
function stagePalette(id, capability) {
|
|
8
|
+
const n = stageHash(id), hue = n % 360, hue2 = (hue + 150 + ((n >>> 8) % 60)) % 360;
|
|
9
|
+
const hsl = (h, s, l) => `hsl(${h} ${s}% ${l}%)`;
|
|
10
|
+
return { robe: hsl(hue, 55, 46), leg: hsl(hue, 45, 28), hat: hsl(hue2, 55, 42), accent: hsl((hue + 180) % 360, 75, 68),
|
|
11
|
+
skin: STAGE_SKINS[(n >>> 16) % STAGE_SKINS.length], prop: STAGE_PROPS[capability] ?? 'none' };
|
|
12
|
+
}
|
|
13
|
+
/** Desk with monitor and the capability prop. `px(x, y, w, h, color)` paints one device-independent rectangle. */
|
|
14
|
+
function stageDrawDesk(px, s, S, desk, deskTop) {
|
|
15
|
+
const x = s.home.x, y = s.home.y, present = s.data.status !== 'inactive';
|
|
16
|
+
px(x - 12 * S, y + S, 24 * S, 7 * S, desk); px(x - 12 * S, y, 24 * S, 5 * S, deskTop); px(x - 12 * S, y, 24 * S, S, 'rgba(255,255,255,.12)');
|
|
17
|
+
px(x - 4 * S, y - 5 * S, 8 * S, 5 * S, '#1a1b22');
|
|
18
|
+
stageDrawScreen(px, s, S, x, y, present);
|
|
19
|
+
px(x - S, y, 2 * S, S, '#111');
|
|
20
|
+
if (s.data.activity === 'reading') { // open book on the desk, the page flips every other frame
|
|
21
|
+
px(x - 6 * S, y + S, 5 * S, 3 * S, '#f1ead6'); px(x - 4 * S, y + S, S, 3 * S, '#cdbf9a');
|
|
22
|
+
if (s.frame % 2) px(x - 6 * S, y + S, 2 * S, 2 * S, '#ffffff');
|
|
23
|
+
}
|
|
24
|
+
const a = s.palette.accent;
|
|
25
|
+
switch (s.palette.prop) {
|
|
26
|
+
case 'map': px(x + 5 * S, y + S, 6 * S, 3 * S, '#d9c38a'); px(x + 6 * S, y + 2 * S, 2 * S, S, '#b3412f'); break;
|
|
27
|
+
case 'scroll': px(x + 6 * S, y + S, 5 * S, 2 * S, '#e8dcbf'); px(x + 6 * S, y + 3 * S, 5 * S, S, '#cdbf9a'); break;
|
|
28
|
+
case 'gear': px(x + 6 * S, y, 4 * S, 4 * S, '#9aa3ad'); px(x + 7 * S, y + S, 2 * S, 2 * S, '#3b4149'); px(x + 5 * S, y + S, S, 2 * S, '#9aa3ad'); px(x + 10 * S, y + S, S, 2 * S, '#9aa3ad'); break;
|
|
29
|
+
case 'palette': px(x + 5 * S, y + S, 6 * S, 3 * S, '#e8dcbf'); px(x + 6 * S, y + 2 * S, S, S, '#e0493f'); px(x + 8 * S, y + 2 * S, S, S, '#3a9ad9'); px(x + 10 * S, y + 2 * S, S, S, '#3ddc84'); break;
|
|
30
|
+
case 'glass': px(x + 6 * S, y, 3 * S, 3 * S, '#c9ced6'); px(x + 7 * S, y + S, S, S, '#a9d4ff'); px(x + 9 * S, y + 3 * S, 2 * S, S, '#8a5a2b'); break;
|
|
31
|
+
case 'brush': px(x + 6 * S, y + S, 2 * S, 3 * S, '#2b2b2b'); px(x + 9 * S, y + S, 2 * S, 3 * S, '#f1ead6'); break;
|
|
32
|
+
case 'flask': px(x + 7 * S, y - S, 2 * S, 2 * S, '#c9ced6'); px(x + 6 * S, y + S, 4 * S, 3 * S, '#d8dde3'); px(x + 7 * S, y + 2 * S, 2 * S, 2 * S, a); break;
|
|
33
|
+
default: break;
|
|
34
|
+
}
|
|
35
|
+
px(x - 10 * S, y + S, 3 * S, 2 * S, '#c84b3c'); px(x - 9 * S, y, S, S, '#f0e9d8');
|
|
36
|
+
}
|
|
37
|
+
/** Monitor content per activity: code lines scroll while writing, a green terminal flickers, failures turn red. */
|
|
38
|
+
function stageDrawScreen(px, s, S, x, y, present) {
|
|
39
|
+
const a = s.data.activity, f = s.frame;
|
|
40
|
+
const screen = !present ? '#2b3340' : a === 'failed' ? '#8f2f2f' : a === 'writing' ? '#1e3a5f' : a === 'terminal' ? '#0c2214'
|
|
41
|
+
: a === 'browsing' || a === 'searching' ? '#2d5f8a' : a === 'messaging' || a === 'board' ? '#5b4a8a' : a === 'thinking' || a === 'reading' ? '#3a4a5c' : '#4b5563';
|
|
42
|
+
px(x - 3 * S, y - 4 * S, 6 * S, 3 * S, screen);
|
|
43
|
+
if (a === 'writing') for (let i = 0; i < 3; i++) px(x - 2 * S, y - (4 - i) * S, ((f + i) % 3 + 2) * S, S * .6, i === (f % 3) ? '#bfe6ff' : '#6fa8dc');
|
|
44
|
+
else if (a === 'terminal') { px(x - 2 * S, y - 3 * S, 3 * S, S * .6, '#3ddc84'); if (f % 2) px(x + S, y - 3 * S, S, S * .6, '#b6ffcf'); }
|
|
45
|
+
else if (a === 'failed') { px(x - .5 * S, y - 4 * S, S, 2 * S * .8, '#ffd0d0'); if (f % 2) px(x - .5 * S, y - 1.6 * S, S, S * .5, '#ffd0d0'); }
|
|
46
|
+
else if (a === 'browsing' || a === 'searching') px(x - S, y - 4 * S + (f % 3) * S * .7, 2 * S, S * .6, '#a9d4ff');
|
|
47
|
+
else if (a === 'messaging' || a === 'board') px(x - 2 * S, y - 3 * S, 4 * S, S, f % 2 ? '#d9c38a' : '#f3e7c6');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 12×16 pixel person. The pose follows `s.data.activity` (typing / chin-on-hand / book / head-down / dozing) plus
|
|
51
|
+
* the one-shot overlays: `s.gesture` (idle stretch or coffee) and `s.celebrate` (jump with raised arms).
|
|
52
|
+
*/
|
|
53
|
+
function stageDrawPerson(px, s, S) {
|
|
54
|
+
const c = s.palette, f = s.facing, walking = s.phase === 'walking', a = s.data.activity, fr = s.frame;
|
|
55
|
+
const jumping = s.celebrate > 0, typing = !jumping && ['writing', 'terminal', 'searching', 'browsing', 'messaging', 'board', 'working'].includes(a);
|
|
56
|
+
const lift = jumping ? Math.round(Math.abs(Math.sin(s.celebrate * 7)) * 4) * S : 0;
|
|
57
|
+
const bob = !jumping && !walking && fr === 2 && a !== 'failed' ? S : 0;
|
|
58
|
+
const x = s.x, y0 = s.y - 16 * S + bob - lift;
|
|
59
|
+
const X = u => x + (f > 0 ? u : (12 - u - 1)) * S - 6 * S, Y = v => y0 + v * S;
|
|
60
|
+
const R = (u, v, w, h, col) => px(f > 0 ? X(u) : X(u + w - 1), Y(v), w * S, h * S, col);
|
|
61
|
+
const legA = walking ? (fr % 2 ? 1 : 0) : 0;
|
|
62
|
+
R(4 - legA, 12, 2, 4, c.leg); R(7 + legA, 12, 2, 4, c.leg); R(4 - legA, 15, 2, 1, '#222'); R(7 + legA, 15, 2, 1, '#222');
|
|
63
|
+
R(3, 6, 7, 6, c.robe); R(6, 6, 1, 6, c.accent);
|
|
64
|
+
const arm = (u, top, col = c.robe) => { R(u, top, 1, 4, col); R(u, top + 3, 1, 1, c.skin); };
|
|
65
|
+
const head = a === 'failed' ? 1 : 0; // failed members hang their head
|
|
66
|
+
if (jumping || s.gesture === 'stretch') { R(2, 2, 1, 4, c.robe); R(10, 2, 1, 4, c.robe); R(2, 1, 1, 1, c.skin); R(10, 1, 1, 1, c.skin); }
|
|
67
|
+
else if (a === 'thinking') { arm(2, 7); R(10, 5, 1, 3, c.robe); R(9, 5, 1, 1, c.skin); } // chin on hand
|
|
68
|
+
else if (a === 'reading') { R(2, 8, 1, 2, c.robe); R(10, 8, 1, 2, c.robe); R(3, 9, 7, 2, '#e8dcbf'); R(6, 9, 1, 2, '#b3a37e'); }
|
|
69
|
+
else if (s.gesture === 'coffee') { arm(2, 7); R(10, 5, 1, 3, c.robe); R(9, 5, 2, 2, '#f1ead6'); R(9, 5, 2, 1, '#6b4226'); }
|
|
70
|
+
else {
|
|
71
|
+
const fast = a === 'writing' ? fr % 2 : fr === 1 ? 1 : 0, other = a === 'writing' ? 1 - fr % 2 : fr === 3 ? 1 : 0;
|
|
72
|
+
arm(2, 7 - (typing ? fast : 0)); arm(10, 7 - (typing ? other : 0));
|
|
73
|
+
}
|
|
74
|
+
R(4, 3 + head, 5, 3, c.skin);
|
|
75
|
+
if (a === 'waiting' || a === 'failed') { R(5, 5 + head, 1, 1, '#222'); R(8, 5 + head, 1, 1, '#222'); } // closed / downcast eyes
|
|
76
|
+
else { R(5, 4, 1, 1, '#222'); R(8, 4, 1, 1, '#222'); }
|
|
77
|
+
R(3, head, 7, 3, c.hat); R(3, 2 + head, 7, 1, c.accent);
|
|
78
|
+
if (s.data.lead) { R(4, -1 + head, 1, 1, '#ffd766'); R(6, -2 + head, 1, 2, '#ffd766'); R(8, -1 + head, 1, 1, '#ffd766'); }
|
|
79
|
+
if (a === 'failed') { R(11, -3, 1, 3, '#ff5c5c'); R(11, 1, 1, 1, '#ff5c5c'); } // red "!"
|
|
80
|
+
if (a === 'thinking') for (let i = 0; i < Math.max(1, fr % 4); i++) R(4 + i * 2, -3, 1, 1, '#e6e6ee'); // animated ellipsis
|
|
81
|
+
if (a === 'waiting') { const z = fr % 3; R(10 + z, -2 - z, 2, 1, '#cfd8ff'); R(11 + z, -3 - z, 1, 1, '#cfd8ff'); R(10 + z, -4 - z, 2, 1, '#cfd8ff'); } // zzz
|
|
82
|
+
if (s.data.ctxRatio >= .9) { R(1, 2 + (fr % 2), 1, 2, '#7fc8ff'); R(1, 4 + (fr % 2), 1, 1, '#c4e6ff'); } // sweat drop
|
|
83
|
+
}
|
|
84
|
+
/** A letter in flight: wax-sealed scroll from the Lead, blue-sealed report back to the Lead. */
|
|
85
|
+
function stageDrawCourier(px, c, S) {
|
|
86
|
+
px(c.x - 3 * S, c.y - 2 * S, 6 * S, 4 * S, '#f3e7c6'); px(c.x - 3 * S, c.y - 2 * S, 6 * S, S, '#d9c38a'); px(c.x - S, c.y - S, 2 * S, 2 * S, c.reply ? '#3a9ad9' : '#c84b3c');
|
|
87
|
+
}
|
|
88
|
+
/** Floor, wall and windows; reads the stage's colour tokens so light and dark themes both get a readable office. */
|
|
89
|
+
function stageDrawRoom(ctx, px, w, h, S, colors) {
|
|
90
|
+
ctx.fillStyle = colors.floorA; ctx.fillRect(0, 0, w, h);
|
|
91
|
+
const tile = 8 * S;
|
|
92
|
+
for (let y = 0; y < h; y += tile) for (let x = ((y / tile) % 2) * tile; x < w; x += tile * 2) px(x, y, tile, tile, colors.floorB);
|
|
93
|
+
for (let y = 0; y < h; y += tile) px(0, y, w, 1, colors.grout);
|
|
94
|
+
for (let x = 0; x < w; x += tile) px(x, 0, 1, h, colors.grout);
|
|
95
|
+
px(0, 0, w, 10 * S, colors.wall); px(0, 10 * S, w, 1, 'rgba(0,0,0,.35)');
|
|
96
|
+
for (let x = 6 * S; x < w; x += 42 * S) { px(x, 2 * S, 10 * S, 6 * S, '#7fb2e5'); px(x + S, 3 * S, 8 * S, 4 * S, '#a9d4ff'); }
|
|
97
|
+
}
|