crewforth 3.4.0 → 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 +273 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/adopt.sh +40 -5
- package/bin/cli.js +138 -2
- package/kit/AGENT_TEMPLATE.md +15 -0
- package/kit/CLAUDE.md +50 -18
- package/kit/agents/crew-backend-expert.md +4 -3
- package/kit/agents/crew-commit-agent.md +2 -0
- package/kit/agents/crew-database-expert.md +2 -1
- package/kit/agents/crew-devops-expert.md +1 -0
- package/kit/agents/crew-docs-agent.md +3 -1
- package/kit/agents/crew-frontend-expert.md +2 -0
- package/kit/agents/crew-performance-expert.md +7 -2
- package/kit/agents/crew-planner.md +15 -3
- package/kit/agents/crew-privacy-agent.md +2 -0
- package/kit/agents/crew-review-agent.md +8 -1
- package/kit/agents/crew-security-expert.md +4 -3
- package/kit/agents/crew-test-expert.md +6 -2
- package/kit/eval/doctor.sh +201 -1
- package/kit/eval/gate-report.sh +28 -0
- package/kit/eval/golden-holdout.txt +2 -0
- package/kit/eval/golden-routing.txt +17 -0
- package/kit/eval/lib/crew-map/contracts.mjs +77 -0
- package/kit/eval/lib/crew-map/crew-map.sh +2 -1
- package/kit/eval/lib/crew-map/detect.mjs +2 -1
- package/kit/eval/lib/crew-map/narrative.mjs +14 -3
- package/kit/eval/lib/crew-map/project.mjs +45 -6
- package/kit/eval/lib/crew-map/scan.mjs +153 -14
- package/kit/eval/lib/findings/findings.schema.json +11 -1
- package/kit/eval/lib/findings/findings.sh +24 -1
- package/kit/eval/lib/hook-inject.sh +159 -0
- package/kit/eval/lib/lockfiles.sh +71 -0
- package/kit/eval/lib/payload-check.sh +29 -0
- package/kit/eval/lib/preview.sh +3 -3
- package/kit/eval/lib/review-bench/bench.mjs +14 -2
- package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
- package/kit/eval/lib/settings-json.awk +18 -0
- 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/lib/untrusted-text.awk +38 -0
- package/kit/eval/scan-skill.sh +67 -20
- package/kit/eval/smoke-test.sh +1759 -41
- package/kit/eval/utilization.sh +77 -3
- package/kit/hooks/agent-outcome.sh +9 -1
- package/kit/hooks/board-sync.sh +49 -3
- package/kit/hooks/board.sh +8 -8
- package/kit/hooks/branch-fresh.sh +101 -0
- package/kit/hooks/floor-blocklist.txt +19 -1
- package/kit/hooks/guard-bash.sh +92 -8
- package/kit/hooks/guard-question.sh +2 -1
- package/kit/hooks/loop-guard.sh +445 -0
- package/kit/hooks/map-stale.sh +437 -0
- package/kit/hooks/pre-commit +48 -7
- package/kit/hooks/precompact-snapshot.sh +102 -6
- package/kit/hooks/prompt-approval.sh +50 -14
- package/kit/hooks/route-hint.sh +36 -7
- package/kit/hooks/secret-blocklist.txt +55 -1
- package/kit/hooks/session-guard.sh +45 -15
- package/kit/hooks/session-rehydrate.sh +34 -11
- package/kit/hooks/session-stats.sh +169 -8
- package/kit/hooks/session-update-check.sh +5 -1
- package/kit/hooks/skill-trust.sh +23 -14
- package/kit/hooks/turn-check.sh +190 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +64 -0
- package/kit/skills/a11y/SKILL.md +50 -3
- package/kit/skills/a11y/references/recipes.md +41 -0
- package/kit/skills/adr/SKILL.md +14 -2
- package/kit/skills/api-design/SKILL.md +6 -1
- package/kit/skills/api-design/references/idempotency.md +39 -0
- package/kit/skills/brainstorm/SKILL.md +4 -2
- package/kit/skills/brainstorm/references/asking.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +11 -1
- package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
- package/kit/skills/commit-message/SKILL.md +9 -3
- package/kit/skills/confidence-check/SKILL.md +9 -7
- package/kit/skills/confidence-check/references/source-check.md +37 -0
- package/kit/skills/crew-approve/SKILL.md +3 -0
- package/kit/skills/crew-audit/SKILL.md +63 -0
- package/kit/skills/crew-board/SKILL.md +2 -2
- package/kit/skills/crew-code-review/SKILL.md +27 -4
- package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
- package/kit/skills/crew-code-review/references/simplify.md +28 -0
- package/kit/skills/crew-doctor/SKILL.md +3 -2
- package/kit/skills/crew-gates/SKILL.md +3 -0
- package/kit/skills/crew-gates/references/enforcers.md +30 -0
- package/kit/skills/crew-interview/SKILL.md +42 -0
- package/kit/skills/crew-map/SKILL.md +26 -6
- package/kit/skills/crew-plan/SKILL.md +10 -2
- package/kit/skills/crew-pr/SKILL.md +57 -0
- package/kit/skills/crew-pr/references/checks.md +35 -0
- package/kit/skills/crew-pr/references/threads.md +23 -0
- package/kit/skills/crew-review/SKILL.md +34 -1
- package/kit/skills/crew-ship/SKILL.md +5 -2
- package/kit/skills/crew-skill/SKILL.md +26 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/crew-update/SKILL.md +4 -1
- package/kit/skills/db-migration/SKILL.md +15 -0
- package/kit/skills/db-migration/references/expand-contract.md +52 -0
- package/kit/skills/dependency-audit/SKILL.md +34 -1
- package/kit/skills/dependency-audit/scripts/record.sh +37 -0
- package/kit/skills/dependency-upgrade/SKILL.md +29 -1
- package/kit/skills/deploy/SKILL.md +18 -0
- package/kit/skills/deploy/references/topology-a.md +7 -0
- package/kit/skills/deploy/references/topology-b.md +2 -2
- package/kit/skills/docs-writer/SKILL.md +37 -2
- package/kit/skills/eval-grader/SKILL.md +9 -0
- package/kit/skills/eval-grader/references/method.md +26 -0
- package/kit/skills/frontend/SKILL.md +5 -2
- package/kit/skills/frontend/references/from-design.md +29 -0
- package/kit/skills/frontend/references/page-metadata.md +17 -0
- package/kit/skills/frontend/references/verify-contract.md +18 -1
- package/kit/skills/frontend/references/worst-data.md +35 -0
- package/kit/skills/frontend-design/SKILL.md +20 -4
- package/kit/skills/frontend-design/references/design-review.md +21 -0
- package/kit/skills/frontend-design/references/detection.md +44 -0
- package/kit/skills/frontend-design/references/motion.md +54 -0
- package/kit/skills/frontend-design/references/ui-copy.md +35 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
- package/kit/skills/handoff/SKILL.md +28 -3
- package/kit/skills/handoff/references/cold-reader.md +13 -6
- package/kit/skills/i18n-integrity/SKILL.md +3 -0
- package/kit/skills/iterate/SKILL.md +7 -2
- package/kit/skills/mcp-builder/SKILL.md +21 -4
- package/kit/skills/mcp-builder/references/building.md +74 -13
- package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
- package/kit/skills/observability/SKILL.md +13 -1
- package/kit/skills/performance/SKILL.md +25 -1
- package/kit/skills/performance/references/bottlenecks.md +11 -0
- package/kit/skills/performance/references/web-vitals.md +31 -0
- package/kit/skills/red-team/SKILL.md +25 -3
- package/kit/skills/reflect/SKILL.md +7 -2
- package/kit/skills/release/SKILL.md +13 -0
- package/kit/skills/security-scan/SKILL.md +6 -2
- package/kit/skills/security-scan/references/ai-agents.md +16 -0
- package/kit/skills/security-scan/references/fronts.md +59 -2
- package/kit/skills/security-scan/references/reporting.md +13 -0
- package/kit/skills/security-scan/references/verify.md +9 -0
- package/kit/skills/sonarqube-check/SKILL.md +32 -2
- package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
- package/kit/skills/spec-planning/SKILL.md +32 -8
- package/kit/skills/systematic-debugging/SKILL.md +15 -7
- package/kit/skills/systematic-debugging/references/techniques.md +5 -0
- package/kit/skills/teamboard/SKILL.md +3 -3
- package/kit/skills/testing/SKILL.md +18 -3
- package/kit/skills/testing/references/browser-headless.md +21 -0
- package/kit/skills/token-budget/SKILL.md +26 -5
- package/kit/skills/worktree/SKILL.md +13 -5
- package/kit/studio/server/lib/session.js +23 -2
- package/kit/studio/server/lib/worktree-changes.js +64 -0
- 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 +33 -16
- 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 +86 -36
- package/kit/studio/web/map.js +85 -72
- 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 +5 -4
- package/start.sh +11 -2
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// (`askedAt`) and what time it is now (`now`) all come from the machine the hook runs on, so a viewer whose clock
|
|
9
9
|
// differs — a browser on the far end of a tunnel — still counts down to the moment the hook will really give up.
|
|
10
10
|
import { roleName } from './names.js';
|
|
11
|
+
import { t } from './i18n.js';
|
|
11
12
|
|
|
12
13
|
/** The server's clock, as seen from this page: the difference is measured each time the server says what time it is. */
|
|
13
14
|
export class ServerClock {
|
|
@@ -25,8 +26,8 @@ export class ServerClock {
|
|
|
25
26
|
|
|
26
27
|
// A plan is approved into one of these; the words are the buttons'.
|
|
27
28
|
export const PLAN_CHOICES = [
|
|
28
|
-
{ mode: 'acceptEdits', label: 'Approve \u00b7 accept edits', title: 'Approve the plan and go on in acceptEdits: file edits in the project are accepted without asking; every call is still shown here first.' },
|
|
29
|
-
{ mode: 'default', label: 'Approve \u00b7 ask each time', title: 'Approve the plan and go on in default: each call is asked about here.' },
|
|
29
|
+
{ mode: 'acceptEdits', label: t('Approve \u00b7 accept edits'), title: t('Approve the plan and go on in acceptEdits: file edits in the project are accepted without asking; every call is still shown here first.') },
|
|
30
|
+
{ mode: 'default', label: t('Approve \u00b7 ask each time'), title: t('Approve the plan and go on in default: each call is asked about here.') },
|
|
30
31
|
];
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -137,22 +138,22 @@ export function terminalWait(status, session, ownedHere) {
|
|
|
137
138
|
const call = session?.openCall ?? null;
|
|
138
139
|
const who = call?.agentType ? `${roleName(call.agentType)} · ` : '';
|
|
139
140
|
const what = call ? `${who}${call.toolName}${call.detail ? ` · ${call.detail}` : ''}` : (status.waitingFor ?? null);
|
|
140
|
-
return { text: 'Waiting for an answer in the terminal', what };
|
|
141
|
+
return { text: t('Waiting for an answer in the terminal'), what };
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
export function asker(item) {
|
|
144
|
-
return item.agentType ? roleName(item.agentType) : 'Session';
|
|
145
|
+
return item.agentType ? roleName(item.agentType) : t('Session');
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
export const VERDICTS = {
|
|
148
|
-
allow: { label: 'Allow once', said: 'Allowed' },
|
|
149
|
-
always: { label: null, said: 'Allowed for this session' },
|
|
150
|
-
deny: { label: 'Deny', said: 'Denied' },
|
|
149
|
+
allow: { label: t('Allow once'), said: t('Allowed') },
|
|
150
|
+
always: { label: null, said: t('Allowed for this session') },
|
|
151
|
+
deny: { label: t('Deny'), said: t('Denied') },
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
/** "Allow Bash this session": the button names the tool it widens the gate for. */
|
|
154
155
|
export function allowSessionLabel(toolName) {
|
|
155
|
-
return
|
|
156
|
+
return t('Allow {tool} this session', { tool: toolName });
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
/**
|
|
@@ -186,9 +187,9 @@ export function settled(before, after, decided, serverNowMs, slackMs = 2500) {
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
export const OUTCOME_WORD = {
|
|
189
|
-
allowed: 'Allowed',
|
|
190
|
-
'allowed-session': 'Allowed for this session',
|
|
191
|
-
denied: 'Denied',
|
|
192
|
-
'timed-out': 'Timed out — denied',
|
|
193
|
-
'answered-elsewhere': 'Answered elsewhere',
|
|
190
|
+
allowed: t('Allowed'),
|
|
191
|
+
'allowed-session': t('Allowed for this session'),
|
|
192
|
+
denied: t('Denied'),
|
|
193
|
+
'timed-out': t('Timed out — denied'),
|
|
194
|
+
'answered-elsewhere': t('Answered elsewhere'),
|
|
194
195
|
};
|
package/kit/studio/web/ask.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// A form is built once for a request and kept while the request waits. The dock and the List redraw around it every
|
|
5
5
|
// second; a form rebuilt on each of those would lose what was picked, and what was being typed.
|
|
6
6
|
import { answersOf, PLAN_CHOICES } from './approvals.js';
|
|
7
|
+
import { t } from './i18n.js';
|
|
7
8
|
|
|
8
9
|
function mk(tag, cls, text) {
|
|
9
10
|
const n = document.createElement(tag);
|
|
@@ -44,7 +45,7 @@ export class AskForms {
|
|
|
44
45
|
const picked = new Map(); // question -> Set of labels
|
|
45
46
|
const typed = new Map(); // question -> the viewer's own text
|
|
46
47
|
const controls = [];
|
|
47
|
-
const send = mk('button', 'btn sm primary', 'Send answers');
|
|
48
|
+
const send = mk('button', 'btn sm primary', t('Send answers'));
|
|
48
49
|
const refresh = () => { send.disabled = send.dataset.sent === '1' || answersOf(item.ask, picked, typed) === null; };
|
|
49
50
|
|
|
50
51
|
for (const q of item.ask.questions) {
|
|
@@ -53,7 +54,7 @@ export class AskForms {
|
|
|
53
54
|
const head = mk('div', 'ask-head');
|
|
54
55
|
if (q.header) head.append(mk('span', 'ask-tag', q.header));
|
|
55
56
|
head.append(mk('span', 'ask-text', q.question));
|
|
56
|
-
if (q.multiSelect) head.append(mk('span', 'sub', 'choose any'));
|
|
57
|
+
if (q.multiSelect) head.append(mk('span', 'sub', t('choose any')));
|
|
57
58
|
const opts = mk('div', 'ask-opts');
|
|
58
59
|
opts.setAttribute('role', q.multiSelect ? 'group' : 'radiogroup');
|
|
59
60
|
const buttons = [];
|
|
@@ -80,8 +81,8 @@ export class AskForms {
|
|
|
80
81
|
// The model's options do not always hold the answer: the viewer's own words are an answer too.
|
|
81
82
|
const own = mk('input', 'ask-own');
|
|
82
83
|
own.type = 'text';
|
|
83
|
-
own.placeholder = q.multiSelect ? 'Something else, as well…' : 'Something else…';
|
|
84
|
-
own.setAttribute('aria-label',
|
|
84
|
+
own.placeholder = q.multiSelect ? t('Something else, as well…') : t('Something else…');
|
|
85
|
+
own.setAttribute('aria-label', t('Your own answer to: {question}', { question: q.question }));
|
|
85
86
|
own.addEventListener('input', () => {
|
|
86
87
|
typed.set(q.question, own.value);
|
|
87
88
|
if (!q.multiSelect && own.value.trim()) {
|
|
@@ -96,9 +97,9 @@ export class AskForms {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
const acts = mk('div', 'ask-acts');
|
|
99
|
-
const dismiss = mk('button', 'btn sm', 'Dismiss');
|
|
100
|
+
const dismiss = mk('button', 'btn sm', t('Dismiss'));
|
|
100
101
|
dismiss.type = 'button';
|
|
101
|
-
dismiss.title = 'Refuse the question. The model is told nobody answered and may ask in words instead.';
|
|
102
|
+
dismiss.title = t('Refuse the question. The model is told nobody answered and may ask in words instead.');
|
|
102
103
|
send.type = 'button';
|
|
103
104
|
send.addEventListener('click', () => {
|
|
104
105
|
const answers = answersOf(item.ask, picked, typed);
|
|
@@ -114,12 +115,12 @@ export class AskForms {
|
|
|
114
115
|
|
|
115
116
|
#plan(item) {
|
|
116
117
|
const el = mk('div', 'ask');
|
|
117
|
-
const plan = mk('pre', 'ask-plan', item.ask.plan || 'The plan was not included in the request.');
|
|
118
|
+
const plan = mk('pre', 'ask-plan', item.ask.plan || t('The plan was not included in the request.'));
|
|
118
119
|
plan.tabIndex = 0;
|
|
119
120
|
const acts = mk('div', 'ask-acts');
|
|
120
|
-
const keep = mk('button', 'btn sm', 'Keep planning');
|
|
121
|
+
const keep = mk('button', 'btn sm', t('Keep planning'));
|
|
121
122
|
keep.type = 'button';
|
|
122
|
-
keep.title = 'Refuse to leave plan mode. The model stays in it and goes on planning.';
|
|
123
|
+
keep.title = t('Refuse to leave plan mode. The model stays in it and goes on planning.');
|
|
123
124
|
keep.addEventListener('click', () => this.onDecide(item, 'deny', {}));
|
|
124
125
|
const controls = [keep];
|
|
125
126
|
acts.append(keep);
|
package/kit/studio/web/canvas.js
CHANGED
|
@@ -18,6 +18,7 @@ import { Viewport, draggable, swallowClick } from './viewport.js';
|
|
|
18
18
|
import { agentStatus } from './nav.js';
|
|
19
19
|
import { roleName, shownType, hoverOf, originOf, orderTag, modelOf, modelMix, modelFamily, modelBadge } from './names.js';
|
|
20
20
|
import { fmtCount, fmtCost, COST_NOTE } from './usage.js';
|
|
21
|
+
import { t } from './i18n.js';
|
|
21
22
|
|
|
22
23
|
// How long an arriving wire takes to draw itself toward its new card. Short on purpose: this is a status panel,
|
|
23
24
|
// and anything a viewer has to wait through is a cost they pay on every spawn.
|
|
@@ -139,7 +140,7 @@ export class Canvas {
|
|
|
139
140
|
|
|
140
141
|
// The whole graph, small. The frame is what the pane shows.
|
|
141
142
|
this.mapEl = mk('div', 'cv-minimap');
|
|
142
|
-
this.mapEl.setAttribute('aria-label', 'Minimap');
|
|
143
|
+
this.mapEl.setAttribute('aria-label', t('Minimap'));
|
|
143
144
|
this.mapEl.hidden = true;
|
|
144
145
|
this.mapSvg = document.createElementNS(SVG, 'svg');
|
|
145
146
|
this.mapSvg.setAttribute('aria-hidden', 'true');
|
|
@@ -161,12 +162,12 @@ export class Canvas {
|
|
|
161
162
|
/** What the session spent: time, tokens and an estimate of the cost. Closed, it stays closed until asked for. */
|
|
162
163
|
#spend() {
|
|
163
164
|
const box = mk('div', 'cv-legend cv-spend');
|
|
164
|
-
box.setAttribute('aria-label', 'Session summary');
|
|
165
|
+
box.setAttribute('aria-label', t('Session summary'));
|
|
165
166
|
const head = mk('div', 'cv-legend-head');
|
|
166
|
-
head.append(mk('span', 'cv-label', 'Session'));
|
|
167
|
+
head.append(mk('span', 'cv-label', t('Session')));
|
|
167
168
|
const close = mk('button', 'cv-x');
|
|
168
169
|
close.type = 'button';
|
|
169
|
-
close.setAttribute('aria-label', 'Close the session summary');
|
|
170
|
+
close.setAttribute('aria-label', t('Close the session summary'));
|
|
170
171
|
close.innerHTML = MARK.close;
|
|
171
172
|
close.addEventListener('click', () => this.showSpend(false));
|
|
172
173
|
head.append(close);
|
|
@@ -217,7 +218,7 @@ export class Canvas {
|
|
|
217
218
|
const more = Boolean(r.detail?.length);
|
|
218
219
|
p.label.textContent = r.label;
|
|
219
220
|
p.value.textContent = r.value ?? '';
|
|
220
|
-
p.row.title = more ? `${r.note ?? ''}\
|
|
221
|
+
p.row.title = more ? `${r.note ?? ''}\n${t('Click for the breakdown')}` : (r.note ?? '');
|
|
221
222
|
p.row.classList.toggle('cv-spend-more', more);
|
|
222
223
|
if (more) p.row.setAttribute('aria-expanded', String(this.spendShown.has(r.label))); else p.row.removeAttribute?.('aria-expanded');
|
|
223
224
|
p.detail.hidden = !more || !this.spendShown.has(r.label);
|
|
@@ -244,12 +245,12 @@ export class Canvas {
|
|
|
244
245
|
/** What the lines and the two marks mean. Shown until it is closed, and then not again unless asked for. */
|
|
245
246
|
#legend() {
|
|
246
247
|
const box = mk('div', 'cv-legend');
|
|
247
|
-
box.setAttribute('aria-label', 'Legend');
|
|
248
|
+
box.setAttribute('aria-label', t('Legend'));
|
|
248
249
|
const head = mk('div', 'cv-legend-head');
|
|
249
|
-
head.append(mk('span', 'cv-label', 'Legend'));
|
|
250
|
+
head.append(mk('span', 'cv-label', t('Legend')));
|
|
250
251
|
const close = mk('button', 'cv-x');
|
|
251
252
|
close.type = 'button';
|
|
252
|
-
close.setAttribute('aria-label', 'Close the legend');
|
|
253
|
+
close.setAttribute('aria-label', t('Close the legend'));
|
|
253
254
|
close.innerHTML = MARK.close;
|
|
254
255
|
close.addEventListener('click', () => this.showLegend(false));
|
|
255
256
|
head.append(close);
|
|
@@ -262,10 +263,10 @@ export class Canvas {
|
|
|
262
263
|
return item;
|
|
263
264
|
};
|
|
264
265
|
const rows = mk('div', 'cv-legend-grid');
|
|
265
|
-
rows.append(line('live', 'working now'), line('waiting', 'needs you'), line('done', 'finished'), line('failed', 'failed'));
|
|
266
|
+
rows.append(line('live', t('working now')), line('waiting', t('needs you')), line('done', t('finished')), line('failed', t('failed')));
|
|
266
267
|
box.append(rows);
|
|
267
268
|
// The one wire that is not a call: in the Order view, from an agent to the wave called after it reported.
|
|
268
|
-
const inferred = line('inferred', 'after it reported (inferred, not a call)');
|
|
269
|
+
const inferred = line('inferred', t('after it reported (inferred, not a call)'));
|
|
269
270
|
inferred.title = INFERRED_NOTE;
|
|
270
271
|
box.append(inferred);
|
|
271
272
|
const marks = mk('div', 'cv-legend-grid');
|
|
@@ -273,7 +274,7 @@ export class Canvas {
|
|
|
273
274
|
const crew = mk('span', 'cv-tile cv-tile-sm');
|
|
274
275
|
crew.innerHTML = MARK.kit;
|
|
275
276
|
const a = mk('span', 'cv-legend-item');
|
|
276
|
-
a.append(this.legendBuiltin, mk('span', null, 'Claude Code agent'));
|
|
277
|
+
a.append(this.legendBuiltin, mk('span', null, t('Claude Code agent')));
|
|
277
278
|
const b = mk('span', 'cv-legend-item');
|
|
278
279
|
b.append(crew, mk('span', null, 'Crewforth'));
|
|
279
280
|
marks.append(a, b);
|
|
@@ -524,7 +525,7 @@ export class Canvas {
|
|
|
524
525
|
#plan() {
|
|
525
526
|
return plan(this.#visibleNodes(), {
|
|
526
527
|
group: this.group, density: this.density, seq: this.seq, open: this.open, pinned: this.pinned, sessionY: this.sessionY,
|
|
527
|
-
aside: this.aside, all: this.all, tallest: this.#tallest(),
|
|
528
|
+
aside: this.aside, sides: this.aside, all: this.all, tallest: this.#tallest(),
|
|
528
529
|
});
|
|
529
530
|
}
|
|
530
531
|
|
|
@@ -588,8 +589,8 @@ export class Canvas {
|
|
|
588
589
|
this.lastGraph = graph;
|
|
589
590
|
if (!graph?.nodes?.length) {
|
|
590
591
|
this.emptyEl.hidden = false;
|
|
591
|
-
this.emptyEl.replaceChildren(mk('strong', null, 'No agents yet'),
|
|
592
|
-
mk('div', null, 'This session has not delegated to a subagent. The moment it does, a card appears here.'));
|
|
592
|
+
this.emptyEl.replaceChildren(mk('strong', null, t('No agents yet')),
|
|
593
|
+
mk('div', null, t('This session has not delegated to a subagent. The moment it does, a card appears here.')));
|
|
593
594
|
// els must go with the DOM. Leaving stale elements behind made the next render believe they were still
|
|
594
595
|
// mounted, so cards never returned.
|
|
595
596
|
this.nodeLayer.replaceChildren(); this.edgeG.replaceChildren();
|
|
@@ -710,7 +711,7 @@ export class Canvas {
|
|
|
710
711
|
p.tile.innerHTML = MARK.session;
|
|
711
712
|
p.pill = this.#pill();
|
|
712
713
|
const row = mk('div', 'cv-row');
|
|
713
|
-
row.append(p.tile, mk('span', 'cv-title', 'Session'), mk('span', 'cv-fill'), p.pill.el);
|
|
714
|
+
row.append(p.tile, mk('span', 'cv-title', t('Session')), mk('span', 'cv-fill'), p.pill.el);
|
|
714
715
|
p.name = mk('div', 'cv-name cv-indent');
|
|
715
716
|
p.sub = mk('div', 'cv-sub cv-indent');
|
|
716
717
|
// What it has running in the background takes a line of its own, when there is any: on the line above it
|
|
@@ -786,7 +787,7 @@ export class Canvas {
|
|
|
786
787
|
/** The word and the colour an agent's status is drawn in. Waiting on the viewer outranks what the transcript
|
|
787
788
|
* says, because that is what the agent is doing now. */
|
|
788
789
|
#statusOf(n) {
|
|
789
|
-
if (this.waiting.has(n.id)) return { word: 'Needs you', tone: 'waiting', known: true, state: 'waiting' };
|
|
790
|
+
if (this.waiting.has(n.id)) return { word: t('Needs you'), tone: 'waiting', known: true, state: 'waiting' };
|
|
790
791
|
const s = agentStatus(n.status);
|
|
791
792
|
// A status this panel has no word for keeps its own, exactly as the transcript wrote it.
|
|
792
793
|
return { word: s.known ? cap(s.word) : s.word, tone: s.tone, known: s.known, state: stateOf(n.status) };
|
|
@@ -806,9 +807,9 @@ export class Canvas {
|
|
|
806
807
|
const type = n?.agentType ?? '';
|
|
807
808
|
if (this.palette.map?.[type]?.source === 'kit') return { source: 'kit', title: 'Crewforth agent' };
|
|
808
809
|
if (this.palette.measured === false && type.startsWith('crew-')) {
|
|
809
|
-
return { source: 'agent', title:
|
|
810
|
+
return { source: 'agent', title: t('Agent identity not measured — {reason}', { reason: this.palette.reason ?? t('no reason given') }) };
|
|
810
811
|
}
|
|
811
|
-
return { source: 'agent', title: 'Claude Code agent', where: originOf(type, this.origins) };
|
|
812
|
+
return { source: 'agent', title: t('Claude Code agent'), where: originOf(type, this.origins) };
|
|
812
813
|
}
|
|
813
814
|
|
|
814
815
|
#fillTile(tile, n) {
|
|
@@ -838,25 +839,25 @@ export class Canvas {
|
|
|
838
839
|
p.name.textContent = n.gitBranch || shortPath(n.cwd) || n.sessionId || '';
|
|
839
840
|
// Cut to the card's width; hovering says it whole.
|
|
840
841
|
p.name.title = p.name.textContent;
|
|
841
|
-
const bits = [
|
|
842
|
-
if (n.tokens != null) bits.push(
|
|
843
|
-
if (n.context?.toolShare != null) bits.push(
|
|
842
|
+
const bits = [n.turns === 1 ? t('{n} turn', { n: 1 }) : t('{n} turns', { n: n.turns ?? 0 })];
|
|
843
|
+
if (n.tokens != null) bits.push(t('{n} ctx', { n: fmtTokens(n.tokens) }));
|
|
844
|
+
if (n.context?.toolShare != null) bits.push(t('{n}% tool output', { n: n.context.toolShare }));
|
|
844
845
|
p.sub.textContent = bits.join(' · ');
|
|
845
846
|
// The share is of the text since the last compaction, in characters; the compactions are the transcript's own.
|
|
846
847
|
const lc = n.context?.lastCompact;
|
|
847
848
|
p.sub.title = [p.sub.textContent,
|
|
848
|
-
n.context?.toolShare != null ?
|
|
849
|
-
n.context?.compactions ?
|
|
849
|
+
n.context?.toolShare != null ? t('{n}% of the text in context since the last compaction is tool output (characters, not tokens)', { n: n.context.toolShare }) : null,
|
|
850
|
+
n.context?.compactions ? `${t('compacted {n}×', { n: n.context.compactions })}${lc?.before != null ? `, ${t('last {trigger} {before} → {after}', { trigger: lc.trigger ?? '', before: fmtTokens(lc.before), after: fmtTokens(lc.after) })}` : ''}` : null].filter(Boolean).join('\n');
|
|
850
851
|
// What the session has running that is not an agent: commands sent to the background.
|
|
851
852
|
p.bg.hidden = !bg;
|
|
852
|
-
p.bg.textContent = bg ?
|
|
853
|
-
p.bg.title = bg ? n.backgroundNow.map((c) =>
|
|
853
|
+
p.bg.textContent = bg ? t('{n} in background', { n: bg }) : '';
|
|
854
|
+
p.bg.title = bg ? n.backgroundNow.map((c) => `${t('In the background: {tool}', { tool: c.toolName })}${c.detail ? ` · ${c.detail}` : ''}`).join('\n') : '';
|
|
854
855
|
const refused = n.refusedCalls ?? [];
|
|
855
856
|
p.refused.hidden = !refused.length;
|
|
856
|
-
p.refused.textContent = refused.length ?
|
|
857
|
-
p.refused.title = refused.map((r) => `${r.agent ?? 'an agent'}${r.model ? ` (${r.model})` : ''}: ${r.text}`).join('\n');
|
|
858
|
-
el.title = 'Click to inspect this session';
|
|
859
|
-
el.setAttribute('aria-label',
|
|
857
|
+
p.refused.textContent = refused.length ? t('{n} refused by the model gate', { n: refused.length }) : '';
|
|
858
|
+
p.refused.title = refused.map((r) => `${r.agent ?? t('an agent')}${r.model ? ` (${r.model})` : ''}: ${r.text}`).join('\n');
|
|
859
|
+
el.title = t('Click to inspect this session');
|
|
860
|
+
el.setAttribute('aria-label', `${t('Session')}, ${st?.word ?? t('state not measured')}, ${p.name.textContent}, ${p.sub.textContent}`);
|
|
860
861
|
}
|
|
861
862
|
|
|
862
863
|
#fillAgent(el, n, compact, after = null) {
|
|
@@ -892,9 +893,9 @@ export class Canvas {
|
|
|
892
893
|
p.model.title = mdl.title;
|
|
893
894
|
p.model.dataset.differs = String(mdl.differs);
|
|
894
895
|
if (!compact) { p.facts.textContent = facts; p.facts.title = factsTitle(n); }
|
|
895
|
-
p.order.title = tag ?
|
|
896
|
+
p.order.title = tag ? t('Called {order} by the session', { order: orderTag(n, clock) }) : '';
|
|
896
897
|
// In the Order view: what had reported before this wave was called. A reading of the order of events.
|
|
897
|
-
el.title = after?.length ?
|
|
898
|
+
el.title = after?.length ? `${t('Called after {who} reported.', { who: after.join(', ') })}\n${INFERRED_NOTE}` : '';
|
|
898
899
|
if (compact) {
|
|
899
900
|
p.dot.dataset.tone = st.tone ?? 'none';
|
|
900
901
|
p.word.textContent = st.word;
|
|
@@ -923,17 +924,17 @@ export class Canvas {
|
|
|
923
924
|
el.dataset.aside = String(Boolean(it.open && !it.boxed));
|
|
924
925
|
p.name.textContent = it.label;
|
|
925
926
|
p.name.title = it.real ?? '';
|
|
926
|
-
if (it.type === 'run') { p.tile.innerHTML = MARK.run; p.tile.title = 'Workflow run'; }
|
|
927
|
+
if (it.type === 'run') { p.tile.innerHTML = MARK.run; p.tile.title = t('Workflow run'); }
|
|
927
928
|
else this.#fillTile(p.tile, it.type === 'type' ? { agentType: it.agentType } : (it.of ?? {}));
|
|
928
929
|
// Several agents of one type: the number is a badge beside the role. Any other group says how many it holds.
|
|
929
|
-
const size = it.type === 'type' ? `× ${it.count}` :
|
|
930
|
+
const size = it.type === 'type' ? `× ${it.count}` : (it.members.length === 1 ? t('{n} agent', { n: 1 }) : t('{n} agents', { n: it.members.length }));
|
|
930
931
|
// The group stands where its first member does in the order of the calls.
|
|
931
932
|
p.sub.textContent = [orderTag(it.first), size].filter(Boolean).join(' · ');
|
|
932
933
|
p.sub.classList.toggle('cv-times', it.type === 'type');
|
|
933
934
|
p.fold.innerHTML = it.boxed ? MARK.down : MARK.chevron;
|
|
934
935
|
// Read aloud, the badge is part of the name.
|
|
935
936
|
const said = it.type === 'type' ? `${it.label} × ${it.count}` : it.label;
|
|
936
|
-
p.fold.setAttribute('aria-label',
|
|
937
|
+
p.fold.setAttribute('aria-label', it.open ? t('Fold {name}', { name: said }) : t('Open {name}', { name: said }));
|
|
937
938
|
p.fold.setAttribute('aria-expanded', String(Boolean(it.open)));
|
|
938
939
|
// The bar: how the group went, in the order the eye wants it.
|
|
939
940
|
const order = ['done', 'running', 'starting', 'failed', 'killed', 'stopped', 'stale', 'ended'];
|
|
@@ -948,10 +949,10 @@ export class Canvas {
|
|
|
948
949
|
// Beside the bar: how the members went — or, at compact density where the card has no line for it, the
|
|
949
950
|
// models they ran on, with how they went left to the bar and its tooltip.
|
|
950
951
|
const mix = modelMix(it.members).replace(/, /g, ' \u00b7 ');
|
|
951
|
-
if (!it.boxed) { p.count.textContent = it.compact && mix ? `${says} \u00b7 ${mix}` : says; p.count.title = it.compact && mix ? `${says}\
|
|
952
|
+
if (!it.boxed) { p.count.textContent = it.compact && mix ? `${says} \u00b7 ${mix}` : says; p.count.title = it.compact && mix ? `${says}\n${t('The models its agents ran on: {mix}', { mix })}` : ''; }
|
|
952
953
|
if (!it.boxed) this.#fillTotals(p, it);
|
|
953
|
-
el.title = it.boxed ? '' : (it.open ? 'Click to fold' :
|
|
954
|
-
el.setAttribute('aria-label', `${said}, ${says}, ${it.open ? 'open' : 'folded'}`);
|
|
954
|
+
el.title = it.boxed ? '' : (it.open ? t('Click to fold') : t('Click to show {n} agents', { n: it.members.length }));
|
|
955
|
+
el.setAttribute('aria-label', `${said}, ${says}, ${it.open ? t('open') : t('folded')}`);
|
|
955
956
|
}
|
|
956
957
|
if (!it.boxed) return;
|
|
957
958
|
|
|
@@ -1018,13 +1019,13 @@ export class Canvas {
|
|
|
1018
1019
|
if (it.compact) return;
|
|
1019
1020
|
p.totals.textContent = [
|
|
1020
1021
|
s.timed ? fmtDuration(s.durationMs) : null,
|
|
1021
|
-
s.counted ?
|
|
1022
|
-
s.counted ?
|
|
1022
|
+
s.counted ? t('{n} new', { n: fmtCount(s.fresh) }) : null,
|
|
1023
|
+
s.counted ? t('{n} cache', { n: fmtCount(s.cacheRead) }) : null,
|
|
1023
1024
|
s.counted ? fmtCost(s.cost) : null,
|
|
1024
1025
|
].filter(Boolean).join(' \u00b7 ');
|
|
1025
|
-
p.totals.title =
|
|
1026
|
+
p.totals.title = t('{n} calls. Time, new tokens, tokens read from the cache, and cost: {note}', { n: it.members.length, note: COST_NOTE });
|
|
1026
1027
|
p.mix.textContent = modelMix(it.members);
|
|
1027
|
-
p.mix.title = 'The models its agents ran on';
|
|
1028
|
+
p.mix.title = t('The models its agents ran on');
|
|
1028
1029
|
}
|
|
1029
1030
|
|
|
1030
1031
|
/** What an inferred wire says: on hover that it is an inference, and once per reporter, in words beside it. */
|
|
@@ -1073,11 +1074,12 @@ export class Canvas {
|
|
|
1073
1074
|
this.edgeG.append(path);
|
|
1074
1075
|
}
|
|
1075
1076
|
|
|
1076
|
-
// Out of the right edge of what spawned it, into the left edge of the card
|
|
1077
|
-
// along the flow so siblings fan out instead of stacking on one line.
|
|
1077
|
+
// Out of the right edge of what spawned it, into the left edge of the card (mirrored on the left side, where
|
|
1078
|
+
// `dir` is -1). The control points are pushed along the flow so siblings fan out instead of stacking on one line.
|
|
1078
1079
|
const { from, to } = e;
|
|
1079
1080
|
const c = Math.max(24, Math.abs(to.x - from.x) * 0.55);
|
|
1080
|
-
const
|
|
1081
|
+
const k = c * (e.dir ?? 1);
|
|
1082
|
+
const d = `M ${from.x} ${from.y} C ${from.x + k} ${from.y}, ${to.x - k} ${to.y}, ${to.x} ${to.y}`;
|
|
1081
1083
|
if (path.d !== d) {
|
|
1082
1084
|
path.d = d;
|
|
1083
1085
|
path.setAttribute('d', d);
|
|
@@ -1345,14 +1347,14 @@ export class Canvas {
|
|
|
1345
1347
|
// What it is doing this moment, for an agent that is doing something.
|
|
1346
1348
|
if (n.status === 'running' && n.lastTool) bits.push(`\u25b8 ${n.lastTool}`);
|
|
1347
1349
|
if (n.durationMs != null) bits.push(fmtDuration(n.durationMs));
|
|
1348
|
-
if (n.toolCount) bits.push(
|
|
1349
|
-
if (n.tokens != null) bits.push(
|
|
1350
|
-
if (n.errors) bits.push(
|
|
1350
|
+
if (n.toolCount) bits.push(n.toolCount === 1 ? t('{n} call', { n: 1 }) : t('{n} calls', { n: n.toolCount }));
|
|
1351
|
+
if (n.tokens != null) bits.push(t('{n} tokens', { n: fmtTokens(n.tokens) }));
|
|
1352
|
+
if (n.errors) bits.push(n.errors === 1 ? t('{n} error', { n: 1 }) : t('{n} errors', { n: n.errors }));
|
|
1351
1353
|
this.tipEl.replaceChildren(
|
|
1352
1354
|
mk('strong', null, shownType(n)),
|
|
1353
1355
|
mk('div', 'cv-sub cv-real', n.agentType ?? ''),
|
|
1354
|
-
mk('div', null, n.description || 'No task recorded'),
|
|
1355
|
-
mk('div', 'cv-sub', bits.length ? bits.join(' · ') : 'No numbers yet'),
|
|
1356
|
+
mk('div', null, n.description || t('No task recorded')),
|
|
1357
|
+
mk('div', 'cv-sub', bits.length ? bits.join(' · ') : t('No numbers yet')),
|
|
1356
1358
|
);
|
|
1357
1359
|
const host = this.root.getBoundingClientRect();
|
|
1358
1360
|
const r = el.getBoundingClientRect();
|
|
@@ -1417,14 +1419,14 @@ function readPref(key, allowed, fallback) {
|
|
|
1417
1419
|
function factsOf(n) {
|
|
1418
1420
|
return [
|
|
1419
1421
|
n.durationMs != null ? fmtDuration(n.durationMs) : null,
|
|
1420
|
-
n.usage ?
|
|
1422
|
+
n.usage ? t('{n} new', { n: fmtCount(n.usage.tokens.fresh) }) : null,
|
|
1421
1423
|
n.usage ? fmtCost(n.usage.cost) : null,
|
|
1422
1424
|
].filter(Boolean).join(' \u00b7 ');
|
|
1423
1425
|
}
|
|
1424
1426
|
function factsTitle(n) {
|
|
1425
1427
|
if (!n.usage) return '';
|
|
1426
|
-
const
|
|
1427
|
-
return
|
|
1428
|
+
const k = n.usage.tokens;
|
|
1429
|
+
return `${t('New tokens {fresh} (input {input}, output {output}, cache write {write}); read from cache {read}', { fresh: fmtCount(k.fresh), input: fmtCount(k.input), output: fmtCount(k.output), write: fmtCount(k.cacheWrite), read: fmtCount(k.cacheRead) })}\n${COST_NOTE}`;
|
|
1428
1430
|
}
|
|
1429
1431
|
|
|
1430
1432
|
function fmtTokens(t) {
|
|
@@ -1436,9 +1438,9 @@ function fmtTokens(t) {
|
|
|
1436
1438
|
|
|
1437
1439
|
function fmtDuration(ms) {
|
|
1438
1440
|
const s = Math.round(ms / 1000);
|
|
1439
|
-
if (s < 60) return
|
|
1441
|
+
if (s < 60) return t('{n}s', { n: s });
|
|
1440
1442
|
const m = Math.floor(s / 60);
|
|
1441
|
-
return m < 60 ?
|
|
1443
|
+
return m < 60 ? t('{m}m {s}s', { m, s: s % 60 }) : t('{h}h {m}m', { h: Math.floor(m / 60), m: m % 60 });
|
|
1442
1444
|
}
|
|
1443
1445
|
|
|
1444
1446
|
function shortPath(p, max = 28) {
|