crewforth 3.5.0-rc.1 → 3.5.0-rc.2
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 +32 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/kit/CLAUDE.md +1 -1
- package/kit/eval/lib/statusline.sh +22 -5
- package/kit/eval/lib/todo.awk +167 -0
- package/kit/eval/lib/todo.sh +120 -0
- package/kit/eval/smoke-test.sh +60 -2
- package/kit/hooks/prompt-approval.sh +5 -5
- package/kit/skills/crew-plan/SKILL.md +2 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/spec-planning/SKILL.md +9 -2
- package/kit/studio/web/agent-spend.js +32 -0
- package/kit/studio/web/app.js +245 -236
- package/kit/studio/web/approvals.js +14 -13
- package/kit/studio/web/ask.js +10 -9
- package/kit/studio/web/canvas.js +56 -54
- package/kit/studio/web/chat.js +41 -41
- package/kit/studio/web/convo.js +21 -18
- package/kit/studio/web/dock.js +20 -19
- package/kit/studio/web/graph-plan.js +62 -22
- package/kit/studio/web/i18n-tr.js +760 -0
- package/kit/studio/web/i18n.js +79 -0
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +27 -24
- package/kit/studio/web/list-plan.js +16 -15
- package/kit/studio/web/list.js +6 -5
- package/kit/studio/web/liveness.js +11 -9
- package/kit/studio/web/map-plan.js +33 -29
- package/kit/studio/web/map.js +74 -73
- package/kit/studio/web/models-plan.js +47 -42
- package/kit/studio/web/models.js +21 -20
- package/kit/studio/web/names.js +13 -10
- package/kit/studio/web/nav.js +34 -33
- package/kit/studio/web/newsession.js +25 -24
- package/kit/studio/web/style.css +2 -0
- package/kit/studio/web/theme.js +5 -2
- package/kit/studio/web/timeline-plan.js +19 -18
- package/kit/studio/web/timeline.js +23 -22
- package/kit/studio/web/usage.js +36 -32
- package/package.json +1 -1
package/kit/studio/web/map.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
emptyNote, layoutOf, routesOf, chipsOf, inferredOnly, groupsOf, foldView, placed, BANDS, cardOf, tourOf, searchOf, resourceWord,
|
|
14
14
|
} from './map-plan.js';
|
|
15
15
|
import { Viewport, draggable, swallowClick } from './viewport.js';
|
|
16
|
+
import { t } from './i18n.js';
|
|
16
17
|
|
|
17
18
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
18
19
|
export const LAYOUT_KEY = 'crewforth-studio-map-layout:';
|
|
@@ -31,9 +32,9 @@ function svg(tag, attrs = {}) {
|
|
|
31
32
|
const BAND_ORDER = Object.fromEntries(BANDS.map(([k], i) => [k, i]));
|
|
32
33
|
const BAND_WORD = Object.fromEntries(BANDS);
|
|
33
34
|
|
|
34
|
-
const LAYERS = [['modules', 'Modules'], ['external', 'External'], ['endpoints', 'Endpoints'], ['findings', 'Findings']];
|
|
35
|
+
const LAYERS = [['modules', t('Modules')], ['external', t('External')], ['endpoints', t('Endpoints')], ['findings', t('Findings')]];
|
|
35
36
|
// How a line between two modules is named on the map.
|
|
36
|
-
const EDGE_WORD = { references: 'uses', calls: 'calls', builds: 'builds' };
|
|
37
|
+
const EDGE_WORD = { references: t('uses'), calls: t('calls'), builds: t('builds') };
|
|
37
38
|
|
|
38
39
|
export class MapView {
|
|
39
40
|
/**
|
|
@@ -76,9 +77,9 @@ export class MapView {
|
|
|
76
77
|
});
|
|
77
78
|
this.below = mk('div', 'mp-below');
|
|
78
79
|
this.list = mk('section', 'mp-modlist');
|
|
79
|
-
this.list.setAttribute('aria-label', 'Modules');
|
|
80
|
+
this.list.setAttribute('aria-label', t('Modules'));
|
|
80
81
|
this.side = mk('aside', 'mp-inspect');
|
|
81
|
-
this.side.setAttribute('aria-label', 'Selected node');
|
|
82
|
+
this.side.setAttribute('aria-label', t('Selected node'));
|
|
82
83
|
const main = mk('div', 'mp-main');
|
|
83
84
|
main.append(this.pane, this.below, this.list);
|
|
84
85
|
this.body = mk('div', 'mp-body');
|
|
@@ -162,7 +163,7 @@ export class MapView {
|
|
|
162
163
|
const note = emptyNote(this.data);
|
|
163
164
|
if (note) {
|
|
164
165
|
this.empty.replaceChildren(mk('p', null, note));
|
|
165
|
-
if (this.data?.missing) this.empty.append(mk('p', 'sub', 'Studio only reads the map: nothing is scanned from the panel.'));
|
|
166
|
+
if (this.data?.missing) this.empty.append(mk('p', 'sub', t('Studio only reads the map: nothing is scanned from the panel.')));
|
|
166
167
|
this.root.replaceChildren(this.empty);
|
|
167
168
|
return;
|
|
168
169
|
}
|
|
@@ -222,9 +223,9 @@ export class MapView {
|
|
|
222
223
|
const frame = mk('div', 'mp-group');
|
|
223
224
|
frame.dataset.group = g.id;
|
|
224
225
|
frame.style.left = `${x}px`; frame.style.top = `${y}px`; frame.style.width = `${w}px`; frame.style.height = `${h}px`;
|
|
225
|
-
const fold = mk('button', 'mp-group-fold', `${g.folder}/ · fold`);
|
|
226
|
+
const fold = mk('button', 'mp-group-fold', `${g.folder}/ · ${t('fold')}`);
|
|
226
227
|
fold.type = 'button';
|
|
227
|
-
fold.title =
|
|
228
|
+
fold.title = t('Fold the {n} modules under {folder}/ into one card', { n: g.members.length, folder: g.folder });
|
|
228
229
|
fold.addEventListener('click', () => this.toggleGroup(g.id));
|
|
229
230
|
frame.append(fold);
|
|
230
231
|
parts.push(frame);
|
|
@@ -244,7 +245,7 @@ export class MapView {
|
|
|
244
245
|
if (EDGE_WORD[e.kind] && !e.calls) tags.push(mk('span', 'mp-badge', EDGE_WORD[e.kind]));
|
|
245
246
|
if (e.line === 'model') { const m = mk('span', 'mp-pill', 'model'); m.dataset.tone = 'model'; m.title = 'Proposed by the model, not shown by a file'; tags.push(m); }
|
|
246
247
|
if (e.calls) {
|
|
247
|
-
tags.push(mk('span', 'mp-pill',
|
|
248
|
+
tags.push(mk('span', 'mp-pill', e.calls === 1 ? t('{n} call', { n: 1 }) : t('{n} calls', { n: e.calls })));
|
|
248
249
|
if (e.missing) { const m = mk('span', 'mp-pill', `${e.missing} missing`); m.dataset.tone = 'missing'; tags.push(m); }
|
|
249
250
|
}
|
|
250
251
|
if (tags.length) {
|
|
@@ -272,14 +273,14 @@ export class MapView {
|
|
|
272
273
|
|
|
273
274
|
#head(doc, findings) {
|
|
274
275
|
const head = mk('header', 'mp-head');
|
|
275
|
-
head.append(mk('h2', 'mp-title', 'Map'), mk('span', 'row-fill'));
|
|
276
|
+
head.append(mk('h2', 'mp-title', t('Map')), mk('span', 'row-fill'));
|
|
276
277
|
for (const s of summaryOf(doc, findings)) head.append(mk('span', 'mp-pill', s));
|
|
277
278
|
return head;
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
#toolbar(d) {
|
|
281
282
|
const bar = mk('div', 'mp-toolbar');
|
|
282
|
-
bar.append(mk('span', 'lbl', 'Layers'));
|
|
283
|
+
bar.append(mk('span', 'lbl', t('Layers')));
|
|
283
284
|
for (const [k, label] of LAYERS) {
|
|
284
285
|
const b = mk('button', 'mp-tog', label);
|
|
285
286
|
b.type = 'button';
|
|
@@ -290,8 +291,8 @@ export class MapView {
|
|
|
290
291
|
bar.append(mk('span', 'row-fill'));
|
|
291
292
|
const search = mk('input', 'mp-search');
|
|
292
293
|
search.type = 'search';
|
|
293
|
-
search.placeholder = 'Search files and modules';
|
|
294
|
-
search.setAttribute('aria-label', 'Search files and modules');
|
|
294
|
+
search.placeholder = t('Search files and modules');
|
|
295
|
+
search.setAttribute('aria-label', t('Search files and modules'));
|
|
295
296
|
search.value = this.query;
|
|
296
297
|
search.addEventListener('input', () => { this.query = search.value; this.render(); this.top.querySelector('.mp-search')?.focus(); });
|
|
297
298
|
search.addEventListener('keydown', (e) => {
|
|
@@ -301,16 +302,16 @@ export class MapView {
|
|
|
301
302
|
});
|
|
302
303
|
bar.append(search);
|
|
303
304
|
if (tourOf(d.doc).length) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
bar.append(
|
|
305
|
+
const tb = mk('button', 'mp-tog', t('Tour'));
|
|
306
|
+
tb.type = 'button';
|
|
307
|
+
tb.setAttribute('aria-pressed', String(this.tourAt >= 0));
|
|
308
|
+
tb.title = t('The stops /crew-map wrote for someone new to the project, in order');
|
|
309
|
+
tb.addEventListener('click', () => this.tour(this.tourAt >= 0 ? -1 : 0));
|
|
310
|
+
bar.append(tb);
|
|
310
311
|
}
|
|
311
312
|
const pick = mk('select', 'mp-compare');
|
|
312
|
-
pick.setAttribute('aria-label', 'Compare with');
|
|
313
|
-
const none = mk('option', null, d.others?.length ? 'Compare with…' : 'Compare with: no other project has a map');
|
|
313
|
+
pick.setAttribute('aria-label', t('Compare with'));
|
|
314
|
+
const none = mk('option', null, d.others?.length ? t('Compare with…') : t('Compare with: no other project has a map'));
|
|
314
315
|
none.value = '';
|
|
315
316
|
pick.append(none);
|
|
316
317
|
for (const o of d.others ?? []) { const opt = mk('option', null, o.label); opt.value = o.cwd; pick.append(opt); }
|
|
@@ -318,20 +319,20 @@ export class MapView {
|
|
|
318
319
|
pick.disabled = !d.others?.length;
|
|
319
320
|
pick.addEventListener('change', () => this.hooks.onCompare?.(pick.value || null));
|
|
320
321
|
const legend = mk('div', 'mp-legend');
|
|
321
|
-
const lb = mk('button', 'btn sm ghost', 'Legend');
|
|
322
|
+
const lb = mk('button', 'btn sm ghost', t('Legend'));
|
|
322
323
|
lb.type = 'button';
|
|
323
324
|
lb.setAttribute('aria-expanded', String(this.legendOpen));
|
|
324
325
|
lb.addEventListener('click', () => { this.legendOpen = !this.legendOpen; this.render(); });
|
|
325
326
|
legend.append(lb);
|
|
326
327
|
if (this.legendOpen) {
|
|
327
328
|
const lg = mk('div', 'mp-legend-box');
|
|
328
|
-
for (const [kind, text] of [['evidence', 'A file proves this line'], ['inferred', 'Inferred, not proven'], ['model', 'Proposed by the model'], ['lit', 'A line of the selected node']]) {
|
|
329
|
+
for (const [kind, text] of [['evidence', t('A file proves this line')], ['inferred', t('Inferred, not proven')], ['model', t('Proposed by the model')], ['lit', t('A line of the selected node')]]) {
|
|
329
330
|
const row = mk('div', 'mp-legend-row');
|
|
330
331
|
const s = mk('span', 'mp-legend-line'); s.dataset.kind = kind;
|
|
331
332
|
row.append(s, mk('span', 'sub', text));
|
|
332
333
|
lg.append(row);
|
|
333
334
|
}
|
|
334
|
-
for (const [kind, text] of [['solid', 'A resource a file shows'], ['dashed', 'A resource only inferred']]) {
|
|
335
|
+
for (const [kind, text] of [['solid', t('A resource a file shows')], ['dashed', t('A resource only inferred')]]) {
|
|
335
336
|
const row = mk('div', 'mp-legend-row');
|
|
336
337
|
const s = mk('span', 'mp-legend-card'); s.dataset.kind = kind;
|
|
337
338
|
row.append(s, mk('span', 'sub', text));
|
|
@@ -349,7 +350,7 @@ export class MapView {
|
|
|
349
350
|
strip.append(mk('code', 'mp-file', s.file), mk('span', 'sub', `· ${s.at}`));
|
|
350
351
|
if (s.behind) { const b = mk('span', 'mp-pill', s.behind); b.dataset.tone = 'behind'; strip.append(b); }
|
|
351
352
|
if (s.behindNote) strip.append(mk('span', 'sub', s.behindNote));
|
|
352
|
-
if (d.compare?.reason) strip.append(mk('span', 'sub',
|
|
353
|
+
if (d.compare?.reason) strip.append(mk('span', 'sub', t('Compare: {reason}', { reason: d.compare.reason })));
|
|
353
354
|
strip.append(mk('span', 'row-fill'), mk('span', 'sub', s.rule));
|
|
354
355
|
return strip;
|
|
355
356
|
}
|
|
@@ -372,7 +373,7 @@ export class MapView {
|
|
|
372
373
|
const name = mk('span', 'mp-name', n.name ?? n.id);
|
|
373
374
|
name.title = n.name ?? n.id;
|
|
374
375
|
top.append(name);
|
|
375
|
-
if (count) { const f = mk('span', 'mp-fb', String(count)); f.title =
|
|
376
|
+
if (count) { const f = mk('span', 'mp-fb', String(count)); f.title = count === 1 ? t('1 finding') : t('{n} findings', { n: count }); top.append(f); }
|
|
376
377
|
top.append(mk('span', 'row-fill'));
|
|
377
378
|
if (n.kind === 'group') { card.append(top, mk('span', 'sub', `${n.members.length} modules · click to open`)); }
|
|
378
379
|
else if (n.kind !== 'module') { card.append(top, mk('span', 'sub', cardSub(n, doc))); }
|
|
@@ -387,7 +388,7 @@ export class MapView {
|
|
|
387
388
|
card.append(row);
|
|
388
389
|
}
|
|
389
390
|
if (this.layers.external) {
|
|
390
|
-
const fold = mk('span', `mp-fold${c.outside.length ? '' : ' none'}`, c.outside.length ? `▸ ${c.outside.join(' · ')}` : 'no outside calls');
|
|
391
|
+
const fold = mk('span', `mp-fold${c.outside.length ? '' : ' none'}`, c.outside.length ? `▸ ${c.outside.join(' · ')}` : t('no outside calls'));
|
|
391
392
|
if (c.outside.length) fold.title = c.outside.join('\n');
|
|
392
393
|
card.append(fold);
|
|
393
394
|
}
|
|
@@ -408,26 +409,26 @@ export class MapView {
|
|
|
408
409
|
|
|
409
410
|
#findings(f) {
|
|
410
411
|
const box = mk('section', 'mp-findings');
|
|
411
|
-
box.setAttribute('aria-label', 'Findings');
|
|
412
|
+
box.setAttribute('aria-label', t('Findings'));
|
|
412
413
|
const h = mk('div', 'mp-row');
|
|
413
|
-
h.append(mk('span', 'lbl',
|
|
414
|
+
h.append(mk('span', 'lbl', `${t('Findings')} · ${f.shown.length}`), mk('span', 'sub', t('{n} proven by a file · {m} inferred', { n: f.proven, m: f.shown.length - f.proven })), mk('span', 'row-fill'));
|
|
414
415
|
if (f.hidden) {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
h.append(
|
|
416
|
+
const tb = mk('button', 'btn sm ghost', this.showHidden ? t('Hide {n} hidden by default', { n: f.hidden }) : t('Show {n} hidden by default', { n: f.hidden }));
|
|
417
|
+
tb.type = 'button';
|
|
418
|
+
tb.title = t('Rules wrong too often to show unasked. Their findings are kept, not deleted.');
|
|
419
|
+
tb.addEventListener('click', () => { this.showHidden = !this.showHidden; this.render(); });
|
|
420
|
+
h.append(tb);
|
|
420
421
|
}
|
|
421
|
-
h.append(mk('span', 'sub mp-hint', 'Click a row to show it on the map'));
|
|
422
|
+
h.append(mk('span', 'sub mp-hint', t('Click a row to show it on the map')));
|
|
422
423
|
box.append(h);
|
|
423
|
-
if (!f.shown.length) box.append(mk('p', 'sub', f.total ? 'Every finding of this map is hidden by default.' : 'No finding in this map.'));
|
|
424
|
+
if (!f.shown.length) box.append(mk('p', 'sub', f.total ? t('Every finding of this map is hidden by default.') : t('No finding in this map.')));
|
|
424
425
|
const list = mk('div', 'mp-flist');
|
|
425
426
|
for (const x of f.shown) {
|
|
426
427
|
const row = mk('button', 'mp-frow');
|
|
427
428
|
row.type = 'button';
|
|
428
429
|
const dot = mk('span', 'mp-dot'); dot.dataset.proven = String(x.proven);
|
|
429
|
-
row.append(dot, mk('span', 'mp-ftitle', x.title), mk('span', 'mp-fwhere', x.where), mk('span', 'row-fill'), mk('span', 'mp-badge', x.proven ? 'proven' : 'inferred'));
|
|
430
|
-
if (x.hidden) row.title = x.hiddenReason ?? 'Hidden by default';
|
|
430
|
+
row.append(dot, mk('span', 'mp-ftitle', x.title), mk('span', 'mp-fwhere', x.where), mk('span', 'row-fill'), mk('span', 'mp-badge', x.proven ? t('proven') : t('inferred')));
|
|
431
|
+
if (x.hidden) row.title = x.hiddenReason ?? t('Hidden by default');
|
|
431
432
|
row.addEventListener('click', () => { if (x.modules[0]) this.select(x.modules[0], { reveal: true }); });
|
|
432
433
|
list.append(row);
|
|
433
434
|
}
|
|
@@ -447,7 +448,7 @@ export class MapView {
|
|
|
447
448
|
r.addEventListener('click', () => this.select(n.id));
|
|
448
449
|
return r;
|
|
449
450
|
});
|
|
450
|
-
this.list.replaceChildren(mk('span', 'lbl',
|
|
451
|
+
this.list.replaceChildren(mk('span', 'lbl', `${t('Modules')} · ${rows.length}`), ...items);
|
|
451
452
|
}
|
|
452
453
|
|
|
453
454
|
#inspector(node, doc, edges, findings) {
|
|
@@ -456,82 +457,82 @@ export class MapView {
|
|
|
456
457
|
// The project's overview, when /crew-map wrote one: what it says, and the lines it rests on.
|
|
457
458
|
const ov = doc?.overview;
|
|
458
459
|
if (typeof ov?.text === 'string' && ov.text.trim()) {
|
|
459
|
-
parts.push(mk('span', 'lbl', 'Overview'), mk('p', 'mp-overview', ov.text));
|
|
460
|
+
parts.push(mk('span', 'lbl', t('Overview')), mk('p', 'mp-overview', ov.text));
|
|
460
461
|
if (Array.isArray(ov.cites) && ov.cites.length) parts.push(mk('p', 'sub mp-cites', `From ${ov.cites.filter((c) => typeof c === 'string').join(', ')}`));
|
|
461
462
|
}
|
|
462
|
-
parts.push(mk('p', 'sub', 'Select a module to see what it connects to and what was found in it. Esc lets it go.'));
|
|
463
|
+
parts.push(mk('p', 'sub', t('Select a module to see what it connects to and what was found in it. Esc lets it go.')));
|
|
463
464
|
} else {
|
|
464
465
|
const v = inspectOf(node, doc, edges, findings);
|
|
465
466
|
const top = mk('div', 'mp-row');
|
|
466
467
|
const close = mk('button', 'btn icon sm ghost', '×');
|
|
467
|
-
close.type = 'button'; close.setAttribute('aria-label', 'Close inspector');
|
|
468
|
+
close.type = 'button'; close.setAttribute('aria-label', t('Close inspector'));
|
|
468
469
|
close.addEventListener('click', () => this.select(null));
|
|
469
470
|
top.append(mk('span', 'mp-name', v.title), mk('span', 'row-fill'), close);
|
|
470
471
|
parts.push(top);
|
|
471
472
|
if (v.layer || v.domain) {
|
|
472
473
|
const pills = mk('div', 'mp-row');
|
|
473
|
-
if (v.layer) pills.append(mk('span', 'mp-pill',
|
|
474
|
-
if (v.domain) pills.append(mk('span', 'mp-pill',
|
|
474
|
+
if (v.layer) pills.append(mk('span', 'mp-pill', `${t('Layer')} · ${v.layer}`));
|
|
475
|
+
if (v.domain) pills.append(mk('span', 'mp-pill', `${t('Domain')} · ${v.domain}`));
|
|
475
476
|
parts.push(pills);
|
|
476
477
|
}
|
|
477
478
|
if (v.path) parts.push(mk('code', 'sub', v.path));
|
|
478
479
|
if (v.chips.length) { const c = mk('div', 'mp-chips'); for (const t of v.chips) c.append(mk('span', 'mp-chip', t)); parts.push(c); }
|
|
479
480
|
const tabs = mk('nav', 'mp-tabs');
|
|
480
|
-
for (const [k, label] of [['overview', 'Overview'], ['connections',
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
tabs.append(
|
|
481
|
+
for (const [k, label] of [['overview', t('Overview')], ['connections', `${t('Connections')} ${v.connections.out.length + v.connections.in.length}`], ['findings', `${t('Findings')} ${v.findings.length}`]]) {
|
|
482
|
+
const tb = mk('button', 'mp-tab', label);
|
|
483
|
+
tb.type = 'button';
|
|
484
|
+
tb.setAttribute('aria-selected', String(this.tab === k));
|
|
485
|
+
tb.addEventListener('click', () => { this.tab = k; this.render(); });
|
|
486
|
+
tabs.append(tb);
|
|
486
487
|
}
|
|
487
488
|
parts.push(tabs);
|
|
488
489
|
const sec = mk('div', 'mp-isec');
|
|
489
490
|
if (this.tab === 'overview') {
|
|
490
|
-
sec.append(mk('span', 'lbl', 'What it is for'), mk('p', v.narrative ? null : 'sub', v.narrative ?? v.narrativeNote));
|
|
491
|
-
if (v.narrative && v.narrativeCites.length) sec.append(mk('p', 'sub mp-cites',
|
|
491
|
+
sec.append(mk('span', 'lbl', t('What it is for')), mk('p', v.narrative ? null : 'sub', v.narrative ?? v.narrativeNote));
|
|
492
|
+
if (v.narrative && v.narrativeCites.length) sec.append(mk('p', 'sub mp-cites', t('From {files}', { files: v.narrativeCites.join(', ') })));
|
|
492
493
|
if (v.inside && (v.inside.entry || v.inside.folders.length)) {
|
|
493
|
-
sec.append(mk('span', 'lbl', 'Inside'));
|
|
494
|
+
sec.append(mk('span', 'lbl', t('Inside')));
|
|
494
495
|
if (v.inside.entry) { const r = mk('div', 'mp-conn mp-inside'); r.append(this.#cite(v.inside.entry), mk('span', 'mp-ev', 'entry point')); sec.append(r); }
|
|
495
496
|
for (const f of v.inside.folders) { const r = mk('div', 'mp-conn mp-inside'); r.append(mk('span', 'mp-file', `${f.folder === '.' ? '(root)' : `${f.folder}/`}`), mk('span', 'mp-ev', `${f.files} ${f.files === 1 ? 'file' : 'files'}`)); sec.append(r); }
|
|
496
|
-
if (v.inside.other) sec.append(mk('p', 'sub',
|
|
497
|
-
if (v.inside.tests) sec.append(mk('p', 'sub',
|
|
497
|
+
if (v.inside.other) sec.append(mk('p', 'sub', t('{n} more files in other folders', { n: v.inside.other })));
|
|
498
|
+
if (v.inside.tests) sec.append(mk('p', 'sub', v.inside.tests === 1 ? t('1 test file') : t('{n} test files', { n: v.inside.tests })));
|
|
498
499
|
}
|
|
499
500
|
if (v.changed) sec.append(mk('p', 'sub', v.changed));
|
|
500
501
|
if (v.testFiles.length) {
|
|
501
|
-
sec.append(mk('span', 'lbl',
|
|
502
|
+
sec.append(mk('span', 'lbl', `${t('Tests')} · ${v.testTotal}`));
|
|
502
503
|
for (const f of v.testFiles) { const r = mk('div', 'mp-conn mp-inside'); r.append(mk('span', 'mp-file', f)); sec.append(r); }
|
|
503
|
-
if (v.testTotal > v.testFiles.length) sec.append(mk('p', 'sub',
|
|
504
|
+
if (v.testTotal > v.testFiles.length) sec.append(mk('p', 'sub', t('{n} more in the map file', { n: v.testTotal - v.testFiles.length })));
|
|
504
505
|
}
|
|
505
506
|
if (v.notes.length) {
|
|
506
|
-
sec.append(mk('span', 'lbl',
|
|
507
|
+
sec.append(mk('span', 'lbl', `${t('Notes in the code')} · ${v.notesTotal}`));
|
|
507
508
|
for (const x of v.notes) { const r = mk('div', 'mp-conn mp-inside'); r.append(this.#cite(x.at), mk('span', 'mp-ev', x.text)); sec.append(r); }
|
|
508
|
-
if (v.notesTotal > v.notes.length) sec.append(mk('p', 'sub',
|
|
509
|
+
if (v.notesTotal > v.notes.length) sec.append(mk('p', 'sub', t('{n} more in the map file', { n: v.notesTotal - v.notes.length })));
|
|
509
510
|
}
|
|
510
|
-
sec.append(mk('span', 'lbl', 'Connects to'));
|
|
511
|
-
if (!v.connectsTo.length) sec.append(mk('p', 'sub', 'Nothing on this map.'));
|
|
511
|
+
sec.append(mk('span', 'lbl', t('Connects to')));
|
|
512
|
+
if (!v.connectsTo.length) sec.append(mk('p', 'sub', t('Nothing on this map.')));
|
|
512
513
|
for (const c of v.connectsTo) sec.append(this.#conn(c));
|
|
513
514
|
if (v.evidence.length) {
|
|
514
|
-
sec.append(mk('span', 'lbl',
|
|
515
|
+
sec.append(mk('span', 'lbl', `${t('Evidence')} · ${v.evidenceTotal}`));
|
|
515
516
|
for (const e of v.evidence) sec.append(this.#cite(e));
|
|
516
|
-
if (v.evidenceTotal > v.evidence.length) sec.append(mk('p', 'sub',
|
|
517
|
+
if (v.evidenceTotal > v.evidence.length) sec.append(mk('p', 'sub', t('{n} more in the map file', { n: v.evidenceTotal - v.evidence.length })));
|
|
517
518
|
}
|
|
518
519
|
} else if (this.tab === 'connections') {
|
|
519
|
-
if (!v.byKind.length) sec.append(mk('p', 'sub', 'Nothing on this map.'));
|
|
520
|
+
if (!v.byKind.length) sec.append(mk('p', 'sub', t('Nothing on this map.')));
|
|
520
521
|
for (const [kind, { out, in: into }] of v.byKind) {
|
|
521
522
|
sec.append(mk('span', 'lbl', `${kind} · ${out.length + into.length}`));
|
|
522
523
|
for (const c of out) sec.append(this.#conn(c, 'out'));
|
|
523
524
|
for (const c of into) sec.append(this.#conn(c, 'in'));
|
|
524
525
|
}
|
|
525
526
|
} else {
|
|
526
|
-
if (!v.findings.length) sec.append(mk('p', 'sub', 'No finding shown for this node.'));
|
|
527
|
+
if (!v.findings.length) sec.append(mk('p', 'sub', t('No finding shown for this node.')));
|
|
527
528
|
for (const f of v.findings) {
|
|
528
529
|
const card = mk('div', 'mp-fcard');
|
|
529
530
|
card.dataset.proven = String(f.proven);
|
|
530
531
|
const r = mk('div', 'mp-row');
|
|
531
|
-
r.append(mk('span', 'mp-ftitle', f.title), mk('span', 'row-fill'), mk('span', 'mp-badge', f.proven ? 'proven' : 'inferred'));
|
|
532
|
-
const b = mk('button', 'btn sm', 'Copy as task');
|
|
532
|
+
r.append(mk('span', 'mp-ftitle', f.title), mk('span', 'row-fill'), mk('span', 'mp-badge', f.proven ? t('proven') : t('inferred')));
|
|
533
|
+
const b = mk('button', 'btn sm', t('Copy as task'));
|
|
533
534
|
b.type = 'button';
|
|
534
|
-
b.title = 'Copies the finding as a task to paste into a session. Nothing is run.';
|
|
535
|
+
b.title = t('Copies the finding as a task to paste into a session. Nothing is run.');
|
|
535
536
|
b.addEventListener('click', () => this.hooks.onCopy?.(taskOf(f), 'task'));
|
|
536
537
|
card.append(r);
|
|
537
538
|
if (f.where) card.append(this.#cite(f.where));
|
|
@@ -542,8 +543,8 @@ export class MapView {
|
|
|
542
543
|
parts.push(sec);
|
|
543
544
|
}
|
|
544
545
|
const nm = mk('div', 'mp-notmeasured');
|
|
545
|
-
nm.append(mk('span', 'lbl', 'Not measured'));
|
|
546
|
-
if (!doc.notMeasured.length) nm.append(mk('p', 'sub', 'Nothing: every file of the project was read.'));
|
|
546
|
+
nm.append(mk('span', 'lbl', t('Not measured')));
|
|
547
|
+
if (!doc.notMeasured.length) nm.append(mk('p', 'sub', t('Nothing: every file of the project was read.')));
|
|
547
548
|
for (const x of doc.notMeasured) nm.append(mk('p', 'sub', `${x.what} — ${x.reason}`));
|
|
548
549
|
parts.push(nm);
|
|
549
550
|
this.side.replaceChildren(...parts);
|
|
@@ -555,11 +556,11 @@ export class MapView {
|
|
|
555
556
|
if (this.tourAt < 0 || !stops.length) { this.tourBox.hidden = true; return; }
|
|
556
557
|
const s = stops[Math.min(this.tourAt, stops.length - 1)];
|
|
557
558
|
const head = mk('div', 'mp-row');
|
|
558
|
-
head.append(mk('span', 'lbl',
|
|
559
|
+
head.append(mk('span', 'lbl', `${t('Tour')} · ${t('{at} of {n}', { at: this.tourAt + 1, n: stops.length })}`), mk('span', 'mp-name', s.name), mk('span', 'row-fill'));
|
|
559
560
|
const nav = (label, at, off) => { const b = mk('button', 'btn sm ghost', label); b.type = 'button'; b.disabled = off; b.addEventListener('click', () => this.tour(at)); return b; };
|
|
560
|
-
head.append(nav('Back', this.tourAt - 1, this.tourAt === 0), nav(this.tourAt === stops.length - 1 ? 'Done' : 'Next', this.tourAt === stops.length - 1 ? -1 : this.tourAt + 1, false));
|
|
561
|
+
head.append(nav(t('Back'), this.tourAt - 1, this.tourAt === 0), nav(this.tourAt === stops.length - 1 ? t('Done') : t('Next'), this.tourAt === stops.length - 1 ? -1 : this.tourAt + 1, false));
|
|
561
562
|
const body = [head, mk('p', null, s.text)];
|
|
562
|
-
if (s.cites.length) body.push(mk('p', 'sub mp-cites',
|
|
563
|
+
if (s.cites.length) body.push(mk('p', 'sub mp-cites', t('From {files}', { files: s.cites.join(', ') })));
|
|
563
564
|
this.tourBox.replaceChildren(...body);
|
|
564
565
|
this.tourBox.hidden = false;
|
|
565
566
|
}
|
|
@@ -568,7 +569,7 @@ export class MapView {
|
|
|
568
569
|
#cite(text) {
|
|
569
570
|
const b = mk('button', 'mp-ev mp-cite', text);
|
|
570
571
|
b.type = 'button';
|
|
571
|
-
b.title = 'Copy the path';
|
|
572
|
+
b.title = t('Copy the path');
|
|
572
573
|
b.addEventListener('click', () => this.hooks.onCopy?.(text, 'path'));
|
|
573
574
|
return b;
|
|
574
575
|
}
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// apart from the ones that passed or failed.
|
|
6
6
|
import { roleName, modelName } from './names.js';
|
|
7
7
|
import { fmtCost, fmtCount } from './usage.js';
|
|
8
|
+
import { t } from './i18n.js';
|
|
8
9
|
|
|
9
10
|
const TIER = { haiku: 'Haiku', sonnet: 'Sonnet', opus: 'Opus', fable: 'Fable' };
|
|
10
|
-
export const tierWord = (
|
|
11
|
+
export const tierWord = (k) => TIER[k] ?? (k ? String(k) : t('no model'));
|
|
11
12
|
const pct = (r) => `${Math.round(r * 100)}%`;
|
|
12
|
-
const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* The task card a crew agent's work starts with: three lines at the top of its task text.
|
|
@@ -27,7 +27,7 @@ export function cardOf(prompt) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const VERIFY_TONE = { pass: 'pass', fail: 'fail' };
|
|
30
|
-
const VERIFY_SAYS = { none: 'no verify command', blocked: 'verify not run: the command was not allowed', timeout: 'verify not finished: the command ran out of time' };
|
|
30
|
+
const VERIFY_SAYS = { none: t('no verify command'), blocked: t('verify not run: the command was not allowed'), timeout: t('verify not finished: the command ran out of time') };
|
|
31
31
|
|
|
32
32
|
/** The card as the inspector lists it: the three lines, each kept as it was written. */
|
|
33
33
|
export function cardLines(card, row) {
|
|
@@ -40,11 +40,11 @@ export function cardLines(card, row) {
|
|
|
40
40
|
|
|
41
41
|
function stepOf(r, again) {
|
|
42
42
|
const on = r.ranOn ? modelName(r.ranOn) : tierWord(r.model);
|
|
43
|
-
const bits = [again ?
|
|
44
|
-
if (r.fixes > 0) bits.push(
|
|
43
|
+
const bits = [again ? t('Re-run on {model}', { model: on }) : t('Ran on {model}', { model: on })];
|
|
44
|
+
if (r.fixes > 0) bits.push(r.fixes === 1 ? t('fixed once inside the agent') : t('fixed {n} times inside the agent', { n: r.fixes }));
|
|
45
45
|
// A verdict is a tag (pass, fail); anything else is said in words, because it is no verdict.
|
|
46
46
|
const tone = VERIFY_TONE[r.verify] ?? null;
|
|
47
|
-
if (!tone) bits.push(VERIFY_SAYS[r.verify] ??
|
|
47
|
+
if (!tone) bits.push(VERIFY_SAYS[r.verify] ?? t('verify {verdict}', { verdict: r.verify }));
|
|
48
48
|
return { text: bits.join(' \u00b7 '), verify: tone ? r.verify : null, tone, escalated: again };
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -55,13 +55,13 @@ function stepOf(r, again) {
|
|
|
55
55
|
* @returns { risk, riskWhy, steps: [{ text, verify, tone, escalated }], note }
|
|
56
56
|
*/
|
|
57
57
|
export function workSteps(row) {
|
|
58
|
-
if (!row) return { risk: null, riskWhy: null, steps: [], note: 'Not recorded yet: the line is written when the agent stops.' };
|
|
58
|
+
if (!row) return { risk: null, riskWhy: null, steps: [], note: t('Not recorded yet: the line is written when the agent stops.') };
|
|
59
59
|
const steps = [...(row.before ?? []).map((r) => stepOf(r, false)), stepOf(row, Boolean(row.escalatedFrom))];
|
|
60
60
|
if (row.escalatedFrom && !(row.before ?? []).length) {
|
|
61
|
-
steps.unshift({ text:
|
|
61
|
+
steps.unshift({ text: t('An earlier call on {model} did not pass; its line is not in the record', { model: tierWord(row.escalatedFrom) }), verify: null, tone: null, escalated: false });
|
|
62
62
|
}
|
|
63
63
|
// The reason is the gate's own sentence, kept as it wrote it; a line written before the gate kept one has none.
|
|
64
|
-
return { risk: row.risk ?? null, riskWhy: row.risk === 'critical' ? row.riskWhy ?? null : null, steps, note: row.escalatedFrom ? 'A re-run with the same or a lower model is refused by the gate.' : null };
|
|
64
|
+
return { risk: row.risk ?? null, riskWhy: row.risk === 'critical' ? row.riskWhy ?? null : null, steps, note: row.escalatedFrom ? t('A re-run with the same or a lower model is refused by the gate.') : null };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -70,7 +70,7 @@ export function workSteps(row) {
|
|
|
70
70
|
*/
|
|
71
71
|
export function outsideRouting(agentType) {
|
|
72
72
|
if (typeof agentType !== 'string' || !agentType || agentType.startsWith('crew-')) return null;
|
|
73
|
-
return 'Outside model routing: not a crew agent, so it has no floor and Crewforth keeps no outcome line for it. The gate only holds the expensive end: a call that asks for opus or fable, or names no model and would take the session\u2019s, is held at sonnet unless its task carries an above: <why> line or opens with a card naming critical work. An agent whose own definition names a model keeps it.';
|
|
73
|
+
return t('Outside model routing: not a crew agent, so it has no floor and Crewforth keeps no outcome line for it. The gate only holds the expensive end: a call that asks for opus or fable, or names no model and would take the session\u2019s, is held at sonnet unless its task carries an above: <why> line or opens with a card naming critical work. An agent whose own definition names a model keeps it.');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -80,12 +80,12 @@ export function outsideRouting(agentType) {
|
|
|
80
80
|
*/
|
|
81
81
|
export function floorWords(floor) {
|
|
82
82
|
if (!floor) return { floorTag: null, raised: null, raisedWhy: '' };
|
|
83
|
-
const why =
|
|
83
|
+
const why = t('Raised to {model} automatically: {n} of {calls} calls one model down did not pass first time', { model: tierWord(floor.model), n: floor.notFirstTry, calls: floor.calls ?? '?' });
|
|
84
84
|
if (!floor.lowered) return { floorTag: 'raised', raised: tierWord(floor.model), raisedWhy: why };
|
|
85
85
|
return {
|
|
86
86
|
floorTag: 'lowered',
|
|
87
87
|
raised: null,
|
|
88
|
-
raisedWhy: `${why}. You lowered it to
|
|
88
|
+
raisedWhy: `${why}. ${t('You lowered it to {model} on {date}; its outcomes are counted afresh from then.', { model: tierWord(floor.lowered.to), date: String(floor.lowered.ts).slice(0, 10) })}`,
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -95,15 +95,15 @@ export function floorWords(floor) {
|
|
|
95
95
|
* ("\u2265$0.75 \u00b7 2/7"), because a sum of two calls must not be read as the cost of seven. None: a dash.
|
|
96
96
|
*/
|
|
97
97
|
export function costCell(cost, runs) {
|
|
98
|
-
if (!cost) return { cost: '\u2026', costNote: 'Reading what this class cost\u2026', partial: false, advisor: null };
|
|
99
|
-
if (!cost.costed) return { cost: '\u2014', costNote: 'No agent of this class has a transcript on this machine that could be priced', partial: false, advisor: null };
|
|
98
|
+
if (!cost) return { cost: '\u2026', costNote: t('Reading what this class cost\u2026'), partial: false, advisor: null };
|
|
99
|
+
if (!cost.costed) return { cost: '\u2014', costNote: t('No agent of this class has a transcript on this machine that could be priced'), partial: false, advisor: null };
|
|
100
100
|
const usd = fmtCost(cost.cost);
|
|
101
101
|
const advisor = advisorWords(cost);
|
|
102
102
|
const tail = advisor ? `\n${advisor}` : '';
|
|
103
|
-
if (cost.costed >= runs) return { cost: usd, costNote:
|
|
103
|
+
if (cost.costed >= runs) return { cost: usd, costNote: `${runs === 1 ? t('The 1 task priced, at API list price. An estimate: what a subscription is charged is not known here.') : t('All {n} tasks priced, at API list price. An estimate: what a subscription is charged is not known here.', { n: runs })}${tail}`, partial: false, advisor };
|
|
104
104
|
return {
|
|
105
105
|
cost: `\u2265${usd.replace(/^~/, '')} \u00b7 ${cost.costed}/${runs}`,
|
|
106
|
-
costNote:
|
|
106
|
+
costNote: `${t('At least this much: {n} of {runs} tasks could be priced; the others have no transcript on this machine. At API list price.', { n: cost.costed, runs })}${tail}`,
|
|
107
107
|
partial: true,
|
|
108
108
|
advisor,
|
|
109
109
|
};
|
|
@@ -116,17 +116,18 @@ export function costCell(cost, runs) {
|
|
|
116
116
|
export function advisorWords(cost) {
|
|
117
117
|
const n = cost?.advisorCalls ?? 0;
|
|
118
118
|
if (!(n > 0)) return null;
|
|
119
|
-
const share = typeof cost.advisorCost === 'number' && cost.advisorCost > 0 ?
|
|
120
|
-
return
|
|
119
|
+
const share = typeof cost.advisorCost === 'number' && cost.advisorCost > 0 ? t('{cost} of it', { cost: fmtCost(cost.advisorCost) }) : t('its cost not priced');
|
|
120
|
+
return n === 1 ? t('Advisor: 1 call, {share}. A second model the agents consulted, at that model\'s rates; included above.', { share })
|
|
121
|
+
: t('Advisor: {n} calls, {share}. A second model the agents consulted, at that model\'s rates; included above.', { n, share });
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/** One class as the view's row: who and what kind of change, how risky, what ran it, and how it went. */
|
|
124
125
|
export function classRow(c, cost = null) {
|
|
125
|
-
const models = Object.entries(c.models ?? {}).sort((a, b) => b[1].runs - a[1].runs).map(([
|
|
126
|
+
const models = Object.entries(c.models ?? {}).sort((a, b) => b[1].runs - a[1].runs).map(([k, m]) => `${tierWord(k)} ${m.runs}`).join(' \u00b7 ');
|
|
126
127
|
const apart = [
|
|
127
|
-
c.notVerified?.none ?
|
|
128
|
-
c.notVerified?.blocked ?
|
|
129
|
-
c.notVerified?.timeout ?
|
|
128
|
+
c.notVerified?.none ? t('{n} with no verify command', { n: c.notVerified.none }) : null,
|
|
129
|
+
c.notVerified?.blocked ? t('{n} not run', { n: c.notVerified.blocked }) : null,
|
|
130
|
+
c.notVerified?.timeout ? t('{n} timed out', { n: c.notVerified.timeout }) : null,
|
|
130
131
|
].filter(Boolean);
|
|
131
132
|
return {
|
|
132
133
|
key: c.key,
|
|
@@ -141,10 +142,10 @@ export function classRow(c, cost = null) {
|
|
|
141
142
|
tasks: String(c.runs),
|
|
142
143
|
// The rate and what it is a rate of: "83%" of five calls is not "83%" of five hundred.
|
|
143
144
|
firstTry: c.firstTryRate === null ? '\u2014' : pct(c.firstTryRate),
|
|
144
|
-
firstTryOf: c.firstTryRate === null ? 'No call of this class got a verdict from its verify command' :
|
|
145
|
+
firstTryOf: c.firstTryRate === null ? t('No call of this class got a verdict from its verify command') : t('{n} of {of} verified calls passed the first time', { n: c.firstTry, of: c.verified }),
|
|
145
146
|
rate: c.firstTryRate,
|
|
146
147
|
notVerified: apart.join(' \u00b7 '),
|
|
147
|
-
after: [c.fixed ?
|
|
148
|
+
after: [c.fixed ? t('{n} fixed by the agent', { n: c.fixed }) : null, c.failed ? t('{n} failed', { n: c.failed }) : null].filter(Boolean).join(' \u00b7 '),
|
|
148
149
|
escalated: String(c.escalated ?? 0),
|
|
149
150
|
...costCell(cost, c.runs),
|
|
150
151
|
};
|
|
@@ -164,8 +165,8 @@ export function lowerCommand(s) {
|
|
|
164
165
|
export function suggestionText(s) {
|
|
165
166
|
return {
|
|
166
167
|
title: `${roleName(s.agent)} \u00b7 ${s.change}`,
|
|
167
|
-
says:
|
|
168
|
-
how: 'Yours to decide: send the command in your own session. Studio does not write it.',
|
|
168
|
+
says: t('passed {n} of {n} on {from}: try {to} for this class?', { n: s.passed, from: tierWord(s.from), to: tierWord(s.to) }),
|
|
169
|
+
how: t('Yours to decide: send the command in your own session. Studio does not write it.'),
|
|
169
170
|
command: lowerCommand(s),
|
|
170
171
|
};
|
|
171
172
|
}
|
|
@@ -179,43 +180,47 @@ export function suggestionText(s) {
|
|
|
179
180
|
*/
|
|
180
181
|
export function aboveText(a, cost = null) {
|
|
181
182
|
const to = tierWord(a.to);
|
|
182
|
-
const ran = Object.entries(a.models ?? {}).sort((x, y) => y[1] - x[1]).map(([
|
|
183
|
+
const ran = Object.entries(a.models ?? {}).sort((x, y) => y[1] - x[1]).map(([k, n]) => t('{n} on {model}', { n, model: tierWord(k) })).join(', ');
|
|
183
184
|
let costs = null;
|
|
184
185
|
if (cost?.costed) {
|
|
185
186
|
const some = cost.costed < cost.runs;
|
|
186
187
|
const usd = (v) => (some ? `\u2265${fmtCost(v).replace(/^~/, '')}` : fmtCost(v));
|
|
187
|
-
costs =
|
|
188
|
+
costs = some
|
|
189
|
+
? t('{cost} as run; the same tokens at {model}\'s price {normal} ({n} of {runs} priced). API list price; another model would not spend the same tokens.', { cost: usd(cost.cost), model: to, normal: usd(cost.atNormal), n: cost.costed, runs: cost.runs })
|
|
190
|
+
: t('{cost} as run; the same tokens at {model}\'s price {normal}. API list price; another model would not spend the same tokens.', { cost: usd(cost.cost), model: to, normal: usd(cost.atNormal) });
|
|
188
191
|
}
|
|
189
192
|
return {
|
|
190
193
|
title: `${roleName(a.agent)} \u00b7 ${a.change}`,
|
|
191
|
-
says:
|
|
194
|
+
says: a.runs === 1 ? t('normal risk, 1 task above {model}: {ran}.', { model: to, ran }) : t('normal risk, {n} tasks above {model}: {ran}.', { n: a.runs, model: to, ran }),
|
|
192
195
|
verdict: a.passed
|
|
193
|
-
?
|
|
194
|
-
:
|
|
196
|
+
? t('{model} is enough for this class: its latest {n} verified calls on {model} all passed the first time.', { model: to, n: a.passed })
|
|
197
|
+
: a.need != null
|
|
198
|
+
? t('Whether {model} would have done is not measured: this class does not have {n} verified calls on {model} that all passed the first time.', { model: to, n: a.need })
|
|
199
|
+
: t('Whether {model} would have done is not measured: this class does not have enough verified calls on {model} that all passed the first time.', { model: to }),
|
|
195
200
|
measured: Boolean(a.passed),
|
|
196
201
|
costs,
|
|
197
202
|
};
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
export const ABOVE_HEAD = 'Above the table \u00b7 nothing is blocked';
|
|
201
|
-
export const ABOVE_RULE = 'The gate holds a floor, not a ceiling: which model runs normal work is yours to choose.';
|
|
205
|
+
export const ABOVE_HEAD = t('Above the table \u00b7 nothing is blocked');
|
|
206
|
+
export const ABOVE_RULE = t('The gate holds a floor, not a ceiling: which model runs normal work is yours to choose.');
|
|
202
207
|
|
|
203
208
|
/** What the view says when there is nothing to list. */
|
|
204
209
|
export function emptyNote(data) {
|
|
205
|
-
if (!data) return 'Reading the record…';
|
|
206
|
-
if (data.measured === false) return
|
|
207
|
-
if (!data.classes?.length) return 'The record is there and has no line yet.';
|
|
210
|
+
if (!data) return t('Reading the record…');
|
|
211
|
+
if (data.measured === false) return t('Not measured — {reason}', { reason: data.reason });
|
|
212
|
+
if (!data.classes?.length) return t('The record is there and has no line yet.');
|
|
208
213
|
return null;
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
export const SOURCE = 'Source: .claude/state/model-outcomes.tsv';
|
|
212
|
-
export const LOWER_RULE = 'Lowering a floor is never automatic. Raising it is.';
|
|
216
|
+
export const SOURCE = t('Source: .claude/state/model-outcomes.tsv');
|
|
217
|
+
export const LOWER_RULE = t('Lowering a floor is never automatic. Raising it is.');
|
|
213
218
|
|
|
214
219
|
/** What was read for cost, for the header's tooltip; null before it has been. */
|
|
215
|
-
export const costLine = (costs) => (costs?.measured ?
|
|
220
|
+
export const costLine = (costs) => (costs?.measured ? (costs.sessionsRead === 1 ? t('1 session read for cost') : t('{n} sessions read for cost', { n: fmtCount(costs.sessionsRead ?? 0) })) : null);
|
|
216
221
|
|
|
217
222
|
export const totalsLine = (data) => [
|
|
218
|
-
`${
|
|
219
|
-
data.dropped ? `${data.dropped
|
|
220
|
-
data.truncated ? 'only the end of a long record was read' : null,
|
|
223
|
+
`${(data.rows ?? 0) === 1 ? t('1 task') : t('{n} tasks', { n: data.rows ?? 0 })} ${(data.classes?.length ?? 0) === 1 ? t('in 1 class') : t('in {n} classes', { n: data.classes?.length ?? 0 })}`,
|
|
224
|
+
data.dropped ? `${data.dropped === 1 ? t('1 line not read') : t('{n} lines not read', { n: data.dropped })} (${Object.entries(data.droppedWhy ?? {}).map(([why, n]) => `${n}: ${why}`).join('; ') || t('no reason kept')})` : null,
|
|
225
|
+
data.truncated ? t('only the end of a long record was read') : null,
|
|
221
226
|
].filter(Boolean).join(' · ');
|