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
package/kit/studio/web/models.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Crewforth's command for it and lowers nothing. Then the normal work that ran above the table's model: said, with
|
|
7
7
|
// the record's evidence where there is some, and not acted on either.
|
|
8
8
|
import { classRow, suggestionText, aboveText, emptyNote, totalsLine, costLine, SOURCE, LOWER_RULE, ABOVE_HEAD, ABOVE_RULE } from './models-plan.js';
|
|
9
|
+
import { t } from './i18n.js';
|
|
9
10
|
|
|
10
11
|
function mk(tag, cls, text) {
|
|
11
12
|
const n = document.createElement(tag);
|
|
@@ -14,7 +15,7 @@ function mk(tag, cls, text) {
|
|
|
14
15
|
return n;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const COLUMNS = [['Agent · change', ''], ['Risk', ''], ['Model', ''], ['Tasks', 'mv-num'], ['First try', 'mv-num'], ['Escalated', 'mv-num'], ['Est. cost', 'mv-num']];
|
|
18
|
+
const COLUMNS = [[t('Agent · change'), ''], [t('Risk'), ''], [t('Model'), ''], [t('Tasks'), 'mv-num'], [t('First try'), 'mv-num'], [t('Escalated'), 'mv-num'], [t('Est. cost'), 'mv-num']];
|
|
18
19
|
|
|
19
20
|
export class Models {
|
|
20
21
|
/** @param hooks.onCopy(text) put the command on the clipboard */
|
|
@@ -40,8 +41,8 @@ export class Models {
|
|
|
40
41
|
const source = mk('span', 'sub', SOURCE);
|
|
41
42
|
source.title = [totalsLine(d), costLine(this.costs)].filter(Boolean).join(' \u00b7 ');
|
|
42
43
|
// Lines the hooks could not have written are not counted, and that is said where the count is.
|
|
43
|
-
if (d.dropped) source.textContent = `${SOURCE} \u00b7 ${d.dropped
|
|
44
|
-
head.append(mk('h2', 'mv-title', 'Which model each kind of task needs'), mk('span', 'row-fill'), source);
|
|
44
|
+
if (d.dropped) source.textContent = `${SOURCE} \u00b7 ${d.dropped === 1 ? t('1 line not read') : t('{n} lines not read', { n: d.dropped })}`;
|
|
45
|
+
head.append(mk('h2', 'mv-title', t('Which model each kind of task needs')), mk('span', 'row-fill'), source);
|
|
45
46
|
pane.append(head);
|
|
46
47
|
|
|
47
48
|
const scroll = mk('div', 'mv-scroll');
|
|
@@ -64,7 +65,7 @@ export class Models {
|
|
|
64
65
|
risk.append(tag);
|
|
65
66
|
const model = mk('td');
|
|
66
67
|
const ran = mk('span', null, r.models);
|
|
67
|
-
ran.title = r.ranOn ?
|
|
68
|
+
ran.title = r.ranOn ? t('Ran on: {model}', { model: r.ranOn }) : '';
|
|
68
69
|
model.append(ran);
|
|
69
70
|
if (r.floorTag) {
|
|
70
71
|
const raised = mk('span', 'mv-tag', r.floorTag);
|
|
@@ -74,7 +75,7 @@ export class Models {
|
|
|
74
75
|
raisedNotes.push({ tag: r.floorTag, text: `${r.who} · ${r.change}: ${r.raisedWhy}` });
|
|
75
76
|
}
|
|
76
77
|
const first = mk('td', 'mv-num');
|
|
77
|
-
first.title = [r.firstTryOf, r.after, r.notVerified ?
|
|
78
|
+
first.title = [r.firstTryOf, r.after, r.notVerified ? t('Not verified: {what}', { what: r.notVerified }) : null].filter(Boolean).join('\n');
|
|
78
79
|
first.append(mk('span', null, r.firstTry));
|
|
79
80
|
if (r.rate !== null) {
|
|
80
81
|
const meter = mk('span', 'mv-meter');
|
|
@@ -89,7 +90,7 @@ export class Models {
|
|
|
89
90
|
const cost = mk('td', 'mv-num', r.cost);
|
|
90
91
|
cost.title = r.costNote;
|
|
91
92
|
if (r.partial) cost.dataset.partial = 'true';
|
|
92
|
-
if (r.advisor) { const
|
|
93
|
+
if (r.advisor) { const tag = mk('span', 'mv-tag', t('advisor')); tag.dataset.kind = 'advisor'; tag.title = r.advisor; cost.append(tag); }
|
|
93
94
|
tr.append(who, risk, model, mk('td', 'mv-num', r.tasks), first, mk('td', 'mv-num', r.escalated), cost);
|
|
94
95
|
tbody.append(tr);
|
|
95
96
|
}
|
|
@@ -99,19 +100,19 @@ export class Models {
|
|
|
99
100
|
|
|
100
101
|
if (d.suggestions?.length) {
|
|
101
102
|
const box = mk('section', 'mv-suggest');
|
|
102
|
-
box.append(mk('h3', 'mv-suggest-h', 'Suggestion · needs your approval'));
|
|
103
|
+
box.append(mk('h3', 'mv-suggest-h', t('Suggestion · needs your approval')));
|
|
103
104
|
for (const s of d.suggestions) {
|
|
104
|
-
const
|
|
105
|
-
box.append(mk('p', 'mv-suggest-text', `${
|
|
105
|
+
const s2 = suggestionText(s);
|
|
106
|
+
box.append(mk('p', 'mv-suggest-text', `${s2.title} ${s2.says}`));
|
|
106
107
|
const row = mk('div', 'mv-suggest-row');
|
|
107
|
-
row.append(mk('span', 'sub', `${LOWER_RULE} ${
|
|
108
|
-
if (
|
|
108
|
+
row.append(mk('span', 'sub', `${LOWER_RULE} ${s2.how}`), mk('span', 'row-fill'));
|
|
109
|
+
if (s2.command) {
|
|
109
110
|
// It copies the words and does nothing else: the hold is lowered where the user sends them.
|
|
110
|
-
const b = mk('button', 'btn sm', 'Copy command');
|
|
111
|
+
const b = mk('button', 'btn sm', t('Copy command'));
|
|
111
112
|
b.type = 'button';
|
|
112
|
-
b.title = 'Copies the command. Nothing is lowered until you send it in your own session.';
|
|
113
|
-
b.addEventListener('click', () => this.hooks.onCopy?.(
|
|
114
|
-
row.append(mk('code', 'mv-cmd',
|
|
113
|
+
b.title = t('Copies the command. Nothing is lowered until you send it in your own session.');
|
|
114
|
+
b.addEventListener('click', () => this.hooks.onCopy?.(s2.command));
|
|
115
|
+
row.append(mk('code', 'mv-cmd', s2.command), b);
|
|
115
116
|
}
|
|
116
117
|
box.append(row);
|
|
117
118
|
}
|
|
@@ -121,12 +122,12 @@ export class Models {
|
|
|
121
122
|
const box = mk('section', 'mv-above');
|
|
122
123
|
box.append(mk('h3', 'mv-suggest-h', ABOVE_HEAD));
|
|
123
124
|
for (const a of d.above) {
|
|
124
|
-
const
|
|
125
|
-
box.append(mk('p', 'mv-suggest-text', `${
|
|
126
|
-
const v = mk('p', 'mv-above-verdict',
|
|
127
|
-
v.dataset.measured = String(
|
|
125
|
+
const at = aboveText(a, this.costs?.measured ? (this.costs.above?.[a.key] ?? null) : null);
|
|
126
|
+
box.append(mk('p', 'mv-suggest-text', `${at.title} ${at.says}`));
|
|
127
|
+
const v = mk('p', 'mv-above-verdict', at.verdict);
|
|
128
|
+
v.dataset.measured = String(at.measured);
|
|
128
129
|
box.append(v);
|
|
129
|
-
if (
|
|
130
|
+
if (at.costs) box.append(mk('p', 'sub', at.costs));
|
|
130
131
|
}
|
|
131
132
|
box.append(mk('p', 'sub', ABOVE_RULE));
|
|
132
133
|
pane.append(box);
|
package/kit/studio/web/names.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// anywhere but on screen. Keys, ids and comparisons use the type.
|
|
7
7
|
|
|
8
8
|
// A word that is not written the way capitalising its first letter would write it.
|
|
9
|
+
import { t } from './i18n.js';
|
|
10
|
+
|
|
9
11
|
const WORD = { devops: 'DevOps' };
|
|
10
12
|
|
|
11
13
|
/** The type as the transcript has it; an agent with none is said to have none. */
|
|
@@ -27,7 +29,7 @@ export function roleName(type) {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/** The name a node is drawn with. */
|
|
30
|
-
export const shownType = (n) => roleName(
|
|
32
|
+
export const shownType = (n) => (n?.agentType ? roleName(n.agentType) : t('unknown agent'));
|
|
31
33
|
|
|
32
34
|
/** What hovering an agent says: the type as it is declared, and its task in full, since both are cut on screen. */
|
|
33
35
|
/**
|
|
@@ -39,12 +41,13 @@ export const shownType = (n) => roleName(typeOf(n));
|
|
|
39
41
|
export function originOf(type, origins) {
|
|
40
42
|
if (!origins || typeof type !== 'string' || !type) return null;
|
|
41
43
|
const at = type.indexOf(':');
|
|
42
|
-
if (at > 0 && at < type.length - 1) return
|
|
43
|
-
if (origins.project && Object.hasOwn(origins.project, type)) return
|
|
44
|
-
if (origins.user && Object.hasOwn(origins.user, type)) return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
if (at > 0 && at < type.length - 1) return t('Defined in the plugin \u201c{plugin}\u201d', { plugin: type.slice(0, at) });
|
|
45
|
+
if (origins.project && Object.hasOwn(origins.project, type)) return t('Defined in this project: .claude/agents/{file}', { file: origins.project[type] });
|
|
46
|
+
if (origins.user && Object.hasOwn(origins.user, type)) return t('Defined in your own agents: ~/.claude/agents/{file}', { file: origins.user[type] });
|
|
47
|
+
if (!origins.project && !origins.user) return t('Where it is defined was not read: no agents directory in the project or your own');
|
|
48
|
+
if (!origins.project) return t('No definition file found (the project agents directory was not read): built in to Claude Code, or defined where Studio does not look');
|
|
49
|
+
if (!origins.user) return t('No definition file found (your own agents directory was not read): built in to Claude Code, or defined where Studio does not look');
|
|
50
|
+
return t('No definition file found: built in to Claude Code, or defined where Studio does not look');
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
export const hoverOf = (n) => [n?.agentType ?? null, n?.description ?? null].filter(Boolean).join('\n');
|
|
@@ -96,9 +99,9 @@ export function modelOf(n) {
|
|
|
96
99
|
const differs = Boolean(n?.model && n?.modelAsked)
|
|
97
100
|
&& (modelFamily(n.model) !== modelFamily(n.modelAsked) || (n.modelAsked.includes('-') && n.modelAsked !== n.model));
|
|
98
101
|
const title = [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
differs ? 'The two differ.' : null,
|
|
102
|
+
t('Ran on: {model}', { model: n?.model ?? t('not recorded') }),
|
|
103
|
+
t('Asked for: {model}', { model: n?.modelAsked ?? t('no model named (the agent\'s default)') }),
|
|
104
|
+
differs ? t('The two differ.') : null,
|
|
102
105
|
].filter(Boolean).join('\n');
|
|
103
106
|
return { ran, asked, differs, title };
|
|
104
107
|
}
|
package/kit/studio/web/nav.js
CHANGED
|
@@ -8,19 +8,21 @@
|
|
|
8
8
|
// status nobody recognises keeps its own word and gets no colour; a branch that could not be read leaves the line
|
|
9
9
|
// without one; a project that cannot be told to update by a command gets a hint instead of a command.
|
|
10
10
|
|
|
11
|
+
import { t } from './i18n.js';
|
|
12
|
+
|
|
11
13
|
/* ------------------------------------------------------------- status --- */
|
|
12
14
|
|
|
13
15
|
// The words the design uses, and the token each is drawn in. `tone` names a status-* token; null is the hollow
|
|
14
16
|
// ring of something that is over.
|
|
15
17
|
const AGENT_STATUS = {
|
|
16
|
-
running: { word: 'running', tone: 'busy' },
|
|
17
|
-
starting: { word: 'starting', tone: 'busy' },
|
|
18
|
-
done: { word: 'done', tone: 'good' },
|
|
19
|
-
failed: { word: 'failed', tone: 'fail' },
|
|
20
|
-
killed: { word: 'killed', tone: 'fail' },
|
|
21
|
-
stopped: { word: 'stopped', tone: 'fail' },
|
|
22
|
-
stale: { word: 'stale', tone: 'idle' },
|
|
23
|
-
ended: { word: 'ended', tone: null },
|
|
18
|
+
running: { word: t('running'), tone: 'busy' },
|
|
19
|
+
starting: { word: t('starting'), tone: 'busy' },
|
|
20
|
+
done: { word: t('done'), tone: 'good' },
|
|
21
|
+
failed: { word: t('failed'), tone: 'fail' },
|
|
22
|
+
killed: { word: t('killed'), tone: 'fail' },
|
|
23
|
+
stopped: { word: t('stopped'), tone: 'fail' },
|
|
24
|
+
stale: { word: t('stale'), tone: 'idle' },
|
|
25
|
+
ended: { word: t('ended'), tone: null },
|
|
24
26
|
};
|
|
25
27
|
const CHIP_ORDER = ['running', 'starting', 'stale', 'done', 'failed', 'killed', 'stopped', 'ended'];
|
|
26
28
|
|
|
@@ -29,7 +31,7 @@ export function agentStatus(status) {
|
|
|
29
31
|
const known = AGENT_STATUS[status];
|
|
30
32
|
return known
|
|
31
33
|
? { word: known.word, tone: known.tone, known: true }
|
|
32
|
-
: { word: String(status ?? 'unknown'), tone: null, known: false };
|
|
34
|
+
: { word: String(status ?? t('unknown')), tone: null, known: false };
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -55,9 +57,9 @@ export function summaryChips(byStatus) {
|
|
|
55
57
|
// A session, as the machine reports it. `claude agents` knows three states; a session it does not list is not
|
|
56
58
|
// running anywhere on this machine.
|
|
57
59
|
const SESSION_STATUS = {
|
|
58
|
-
busy: { word: 'Running', tone: 'busy' },
|
|
59
|
-
waiting: { word: 'Needs you', tone: 'waiting' },
|
|
60
|
-
idle: { word: 'Idle', tone: 'idle' },
|
|
60
|
+
busy: { word: t('Running'), tone: 'busy' },
|
|
61
|
+
waiting: { word: t('Needs you'), tone: 'waiting' },
|
|
62
|
+
idle: { word: t('Idle'), tone: 'idle' },
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
/**
|
|
@@ -67,7 +69,7 @@ const SESSION_STATUS = {
|
|
|
67
69
|
export function sessionStatus(sessionId, fleet) {
|
|
68
70
|
if (!fleet || fleet.measured === false) return { key: 'unmeasured', word: null, tone: null, known: false };
|
|
69
71
|
const row = (fleet.sessions ?? []).find((s) => s.sessionId === sessionId && s.local !== false);
|
|
70
|
-
if (!row) return { key: 'ended', word: 'Ended', tone: null, known: true };
|
|
72
|
+
if (!row) return { key: 'ended', word: t('Ended'), tone: null, known: true };
|
|
71
73
|
const known = SESSION_STATUS[row.status];
|
|
72
74
|
if (!known) return { key: String(row.status), word: String(row.status), tone: null, known: false };
|
|
73
75
|
return { key: row.status, word: known.word, tone: known.tone, known: true, waitingFor: row.waitingFor ?? null };
|
|
@@ -95,9 +97,9 @@ export function sessionName(sn, labels) {
|
|
|
95
97
|
export function sessionSub(sn, status) {
|
|
96
98
|
const parts = [];
|
|
97
99
|
if (sn.branch) parts.push(sn.branch);
|
|
98
|
-
if (status?.key === 'waiting' && status.waitingFor) parts.push(
|
|
100
|
+
if (status?.key === 'waiting' && status.waitingFor) parts.push(`${t('Needs you')} · ${status.waitingFor}`);
|
|
99
101
|
else if (status?.word) parts.push(status.word);
|
|
100
|
-
if (sn.agentCount > 0) parts.push(
|
|
102
|
+
if (sn.agentCount > 0) parts.push(sn.agentCount === 1 ? t('{n} agent', { n: 1 }) : t('{n} agents', { n: sn.agentCount }));
|
|
101
103
|
return parts.join(' · ');
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -105,20 +107,20 @@ export function sessionSub(sn, status) {
|
|
|
105
107
|
export function ago(thenMs, nowMs) {
|
|
106
108
|
if (typeof thenMs !== 'number' || !Number.isFinite(thenMs)) return '';
|
|
107
109
|
const s = Math.max(0, Math.round((nowMs - thenMs) / 1000));
|
|
108
|
-
if (s < 60) return 'now';
|
|
109
|
-
if (s < 3600) return
|
|
110
|
-
if (s < 86400) return
|
|
111
|
-
return
|
|
110
|
+
if (s < 60) return t('now');
|
|
111
|
+
if (s < 3600) return t('{n}m', { n: Math.floor(s / 60) });
|
|
112
|
+
if (s < 86400) return t('{n}h', { n: Math.floor(s / 3600) });
|
|
113
|
+
return t('{n}d', { n: Math.floor(s / 86400) });
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
/** "seen just now", "seen 3 min ago", "seen 2 h ago" — for a fact recorded earlier, not read now. */
|
|
115
117
|
export function seenAgo(thenMs, nowMs) {
|
|
116
|
-
if (typeof thenMs !== 'number' || !Number.isFinite(thenMs)) return 'seen at an unrecorded time';
|
|
118
|
+
if (typeof thenMs !== 'number' || !Number.isFinite(thenMs)) return t('seen at an unrecorded time');
|
|
117
119
|
const s = Math.max(0, Math.round((nowMs - thenMs) / 1000));
|
|
118
|
-
if (s < 60) return 'seen just now';
|
|
119
|
-
if (s < 3600) return
|
|
120
|
-
if (s < 86400) return
|
|
121
|
-
return
|
|
120
|
+
if (s < 60) return t('seen just now');
|
|
121
|
+
if (s < 3600) return t('seen {n} min ago', { n: Math.floor(s / 60) });
|
|
122
|
+
if (s < 86400) return t('seen {n} h ago', { n: Math.floor(s / 3600) });
|
|
123
|
+
return t('seen {n} d ago', { n: Math.floor(s / 86400) });
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
/* ------------------------------------------------------------- search --- */
|
|
@@ -161,29 +163,28 @@ export function highlight(text, query) {
|
|
|
161
163
|
export function badgeFor(kit) {
|
|
162
164
|
if (!kit || !kit.installed) {
|
|
163
165
|
return {
|
|
164
|
-
text: 'no Crewforth',
|
|
166
|
+
text: t('no Crewforth'),
|
|
165
167
|
tone: 'none',
|
|
166
168
|
copy: null,
|
|
167
|
-
title: 'No Crewforth files in this project. A plugin install leaves none, so this cannot tell the two '
|
|
168
|
-
+ 'apart; to update one, run /crew-update in the project.',
|
|
169
|
+
title: t('No Crewforth files in this project. A plugin install leaves none, so this cannot tell the two apart; to update one, run /crew-update in the project.'),
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
const command = typeof kit.updateCommand === 'string' && kit.updateCommand ? kit.updateCommand : null;
|
|
172
|
-
const how = command ?
|
|
173
|
+
const how = command ? t('click to copy: {command}', { command }) : t('run /crew-update in this project');
|
|
173
174
|
if (kit.compared === false) {
|
|
174
175
|
return {
|
|
175
176
|
text: `${kit.version} ?`,
|
|
176
177
|
tone: 'unknown',
|
|
177
178
|
copy: null,
|
|
178
|
-
title:
|
|
179
|
+
title: t('Crewforth {version} — not compared: {reason}', { version: kit.version, reason: kit.reason ?? t('no reason given') }),
|
|
179
180
|
};
|
|
180
181
|
}
|
|
181
182
|
if (kit.outdated) {
|
|
182
183
|
return {
|
|
183
|
-
text: `${kit.version} · update`,
|
|
184
|
+
text: `${kit.version} · ${t('update')}`,
|
|
184
185
|
tone: 'old',
|
|
185
186
|
copy: command,
|
|
186
|
-
title:
|
|
187
|
+
title: t('Crewforth {version} is behind {latest} — {how}', { version: kit.version, latest: kit.latest, how }),
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
if (kit.ahead) {
|
|
@@ -191,10 +192,10 @@ export function badgeFor(kit) {
|
|
|
191
192
|
text: kit.version,
|
|
192
193
|
tone: 'ahead',
|
|
193
194
|
copy: null,
|
|
194
|
-
title:
|
|
195
|
+
title: t('Crewforth {version} is ahead of the published {latest}', { version: kit.version, latest: kit.latest }),
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
|
-
return { text: kit.version, tone: 'current', copy: null, title:
|
|
198
|
+
return { text: kit.version, tone: 'current', copy: null, title: t('Crewforth {version} — current', { version: kit.version }) };
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
/* --------------------------------------------------------------- live --- */
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// The choices are the server's. The projects are the ones it listed; the modes are the ones it offers, and this
|
|
4
4
|
// file can describe a mode but cannot add one. Starting is guarded by a one-shot: two clicks start one session.
|
|
5
5
|
import { oneShot } from './convo.js';
|
|
6
|
+
import { t, tParts } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* What each permission mode means for a session started HERE.
|
|
@@ -13,9 +14,9 @@ import { oneShot } from './convo.js';
|
|
|
13
14
|
* of repeating a difference that only exists in a terminal.
|
|
14
15
|
*/
|
|
15
16
|
export const MODE_TEXT = {
|
|
16
|
-
plan: { word: 'Plan', says: 'Reads and plans. Cannot change files.' },
|
|
17
|
-
acceptEdits: { word: 'Accept edits', says: 'Edits files and runs commands, each one after you allow it in the approval dock.' },
|
|
18
|
-
default: { word: 'Default', says: 'The same in Studio: every call waits for you. It differs from Accept edits only in a terminal.' },
|
|
17
|
+
plan: { word: t('Plan'), says: t('Reads and plans. Cannot change files.') },
|
|
18
|
+
acceptEdits: { word: t('Accept edits'), says: t('Edits files and runs commands, each one after you allow it in the approval dock.') },
|
|
19
|
+
default: { word: t('Default'), says: t('The same in Studio: every call waits for you. It differs from Accept edits only in a terminal.') },
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
const PREFERRED = 'plan';
|
|
@@ -42,7 +43,7 @@ export function projectChoices(projects, preferKey = null) {
|
|
|
42
43
|
cwd: p.cwd,
|
|
43
44
|
label: p.label ?? p.cwd,
|
|
44
45
|
branch: p.branch ?? null,
|
|
45
|
-
kit: p.kit?.installed ? (p.kit.version ?? 'installed') : null,
|
|
46
|
+
kit: p.kit?.installed ? (p.kit.version ?? t('installed')) : null,
|
|
46
47
|
initial: p === initial,
|
|
47
48
|
}));
|
|
48
49
|
}
|
|
@@ -82,16 +83,16 @@ export class NewSession {
|
|
|
82
83
|
const head = mk('div', 'np-head');
|
|
83
84
|
const close = mk('button', 'btn icon sm ghost');
|
|
84
85
|
close.type = 'button';
|
|
85
|
-
close.setAttribute('aria-label', 'Close');
|
|
86
|
+
close.setAttribute('aria-label', t('Close'));
|
|
86
87
|
close.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
|
87
88
|
close.addEventListener('click', () => this.close());
|
|
88
|
-
head.append(mk('h2', 'np-title', 'New session'), mk('span', 'row-fill'), close);
|
|
89
|
+
head.append(mk('h2', 'np-title', t('New session')), mk('span', 'row-fill'), close);
|
|
89
90
|
|
|
90
91
|
const form = mk('form', 'np-body');
|
|
91
92
|
form.addEventListener('submit', (e) => { e.preventDefault(); this.submit(); });
|
|
92
93
|
|
|
93
94
|
const projWrap = mk('div', 'np-field');
|
|
94
|
-
const projLabel = mk('label', 'np-label', 'Project');
|
|
95
|
+
const projLabel = mk('label', 'np-label', t('Project'));
|
|
95
96
|
projLabel.htmlFor = 'np-project';
|
|
96
97
|
this.projectEl = mk('select', 'np-select');
|
|
97
98
|
this.projectEl.id = 'np-project';
|
|
@@ -102,20 +103,20 @@ export class NewSession {
|
|
|
102
103
|
this.modesEl = mk('fieldset', 'np-modes');
|
|
103
104
|
|
|
104
105
|
const firstWrap = mk('div', 'np-field');
|
|
105
|
-
const firstLabel = mk('label', 'np-label', 'First message');
|
|
106
|
+
const firstLabel = mk('label', 'np-label', t('First message'));
|
|
106
107
|
firstLabel.htmlFor = 'np-first';
|
|
107
108
|
this.firstEl = mk('textarea', 'np-first');
|
|
108
109
|
this.firstEl.id = 'np-first';
|
|
109
110
|
this.firstEl.rows = 5;
|
|
110
|
-
this.firstEl.placeholder = 'What should this session do?';
|
|
111
|
+
this.firstEl.placeholder = t('What should this session do?');
|
|
111
112
|
firstWrap.append(firstLabel, this.firstEl,
|
|
112
|
-
mk('span', 'sub', 'Optional. You can also start empty and write in the conversation panel.'));
|
|
113
|
+
mk('span', 'sub', t('Optional. You can also start empty and write in the conversation panel.')));
|
|
113
114
|
|
|
114
115
|
// What starting here means, said before the button: a closed panel is a refusal.
|
|
115
116
|
const note = mk('div', 'np-note');
|
|
116
117
|
note.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 7.5v3.5M8 5h.01"/></svg>';
|
|
117
118
|
note.append(mk('span', 'sub',
|
|
118
|
-
'Every tool call in this session waits for your approval here in Studio. If Studio is closed, the request is denied.'));
|
|
119
|
+
t('Every tool call in this session waits for your approval here in Studio. If Studio is closed, the request is denied.')));
|
|
119
120
|
|
|
120
121
|
this.errorEl = mk('div', 'np-error');
|
|
121
122
|
this.errorEl.setAttribute('role', 'alert');
|
|
@@ -124,10 +125,10 @@ export class NewSession {
|
|
|
124
125
|
form.append(projWrap, this.modesEl, firstWrap, note, this.errorEl);
|
|
125
126
|
|
|
126
127
|
const foot = mk('div', 'np-foot');
|
|
127
|
-
const cancel = mk('button', 'btn', 'Cancel');
|
|
128
|
+
const cancel = mk('button', 'btn', t('Cancel'));
|
|
128
129
|
cancel.type = 'button';
|
|
129
130
|
cancel.addEventListener('click', () => this.close());
|
|
130
|
-
this.startEl = mk('button', 'btn primary', 'Start session');
|
|
131
|
+
this.startEl = mk('button', 'btn primary', t('Start session'));
|
|
131
132
|
this.startEl.type = 'button';
|
|
132
133
|
this.startEl.addEventListener('click', () => this.submit());
|
|
133
134
|
foot.append(cancel, this.startEl);
|
|
@@ -164,7 +165,7 @@ export class NewSession {
|
|
|
164
165
|
|
|
165
166
|
paint() {
|
|
166
167
|
this.projectEl.replaceChildren(...this.projects.map((p) => {
|
|
167
|
-
const o = mk('option', null, p.kit ? `${p.label} · Crewforth ${p.kit}` : `${p.label} · no Crewforth`);
|
|
168
|
+
const o = mk('option', null, p.kit ? `${p.label} · Crewforth ${p.kit}` : `${p.label} · ${t('no Crewforth')}`);
|
|
168
169
|
o.value = p.key;
|
|
169
170
|
o.selected = p.initial;
|
|
170
171
|
return o;
|
|
@@ -179,17 +180,17 @@ export class NewSession {
|
|
|
179
180
|
paintRunsIn() {
|
|
180
181
|
const r = runsIn(this.project, this.shorten);
|
|
181
182
|
if (!r) {
|
|
182
|
-
this.runsEl.textContent = 'No project on this machine to start a session in.';
|
|
183
|
+
this.runsEl.textContent = t('No project on this machine to start a session in.');
|
|
183
184
|
return;
|
|
184
185
|
}
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
const path = mk('code', null, r.path);
|
|
187
|
+
this.runsEl.replaceChildren(...(r.branch
|
|
188
|
+
? tParts('Runs in {path} on branch {branch}.', { path, branch: mk('code', null, r.branch) })
|
|
189
|
+
: tParts('Runs in {path}.', { path })));
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
paintModes() {
|
|
192
|
-
const legend = mk('legend', 'np-label', 'Permission mode');
|
|
193
|
+
const legend = mk('legend', 'np-label', t('Permission mode'));
|
|
193
194
|
const rows = this.modes.map((m) => {
|
|
194
195
|
const row = mk('label', 'np-mode');
|
|
195
196
|
const radio = mk('input');
|
|
@@ -201,19 +202,19 @@ export class NewSession {
|
|
|
201
202
|
row.classList.toggle('on', m.mode === this.mode);
|
|
202
203
|
const text = mk('span', 'np-mode-text');
|
|
203
204
|
const word = mk('span', 'np-mode-word', m.word);
|
|
204
|
-
if (m.initial) word.append(mk('span', 'sub',
|
|
205
|
+
if (m.initial) word.append(mk('span', 'sub', ` · ${t('default')}`));
|
|
205
206
|
text.append(word);
|
|
206
207
|
if (m.says) text.append(mk('span', 'sub', m.says));
|
|
207
208
|
row.append(radio, text);
|
|
208
209
|
return row;
|
|
209
210
|
});
|
|
210
|
-
this.modesEl.replaceChildren(legend, ...rows, mk('span', 'sub', 'Modes that skip Crewforth\'s gates are not offered.'));
|
|
211
|
+
this.modesEl.replaceChildren(legend, ...rows, mk('span', 'sub', t('Modes that skip Crewforth\'s gates are not offered.')));
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
paintStart() {
|
|
214
215
|
const busy = this.start.spent();
|
|
215
216
|
this.startEl.disabled = busy || !this.project || !this.mode;
|
|
216
|
-
this.startEl.textContent = busy ? 'Starting…' : 'Start session';
|
|
217
|
+
this.startEl.textContent = busy ? t('Starting…') : t('Start session');
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
async submit() {
|
|
@@ -226,7 +227,7 @@ export class NewSession {
|
|
|
226
227
|
if (out?.ok) {
|
|
227
228
|
this.close();
|
|
228
229
|
} else {
|
|
229
|
-
this.errorEl.textContent =
|
|
230
|
+
this.errorEl.textContent = t('Could not start a session — {reason}', { reason: out?.reason ?? t('no reason given') });
|
|
230
231
|
this.errorEl.hidden = false;
|
|
231
232
|
this.paintStart();
|
|
232
233
|
}
|
package/kit/studio/web/style.css
CHANGED
|
@@ -176,6 +176,8 @@ body {
|
|
|
176
176
|
.btn.icon:hover { color: var(--ink); }
|
|
177
177
|
.btn.sm { height: 30px; padding: 0 10px; font-size: 12px; }
|
|
178
178
|
.btn.icon.sm { width: 30px; padding: 0; }
|
|
179
|
+
/* The language button names the language it switches to (EN, TR) in letters, where its neighbours draw an icon. */
|
|
180
|
+
.lang-btn { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em; }
|
|
179
181
|
/* The one filled control on the page. */
|
|
180
182
|
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
|
181
183
|
.btn.primary:hover { background: color-mix(in srgb, var(--accent) 88%, var(--ink)); }
|
package/kit/studio/web/theme.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
//
|
|
7
7
|
// The choice is remembered per browser. Storage that refuses (private mode, a blocked origin) costs the memory and
|
|
8
8
|
// not the click: the theme still switches for this page.
|
|
9
|
+
import { t } from './i18n.js';
|
|
10
|
+
|
|
9
11
|
export const THEME_KEY = 'crewforth-studio-theme';
|
|
10
12
|
|
|
11
13
|
const chosen = (v) => (v === 'light' || v === 'dark' ? v : null);
|
|
@@ -28,8 +30,9 @@ export function initTheme(root, button, ls, systemLight) {
|
|
|
28
30
|
|
|
29
31
|
const label = () => {
|
|
30
32
|
const to = effectiveTheme(root.dataset.theme, systemLight()) === 'light' ? 'dark' : 'light';
|
|
31
|
-
|
|
32
|
-
button.
|
|
33
|
+
const say = to === 'light' ? t('Switch to light theme') : t('Switch to dark theme');
|
|
34
|
+
button.setAttribute('aria-label', say);
|
|
35
|
+
button.title = say;
|
|
33
36
|
};
|
|
34
37
|
label();
|
|
35
38
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// its whole length — the transcript says how it ended, not when it started going wrong.
|
|
11
11
|
import { stateOf, tally } from './graph-plan.js';
|
|
12
12
|
import { typeOf, shownType } from './names.js';
|
|
13
|
+
import { t } from './i18n.js';
|
|
13
14
|
|
|
14
15
|
export const RANGES = [
|
|
15
16
|
{ key: '5m', ms: 5 * 60_000 },
|
|
@@ -67,14 +68,14 @@ export function segments(n, approvals, now) {
|
|
|
67
68
|
|
|
68
69
|
/** What the right of a row says about one agent. */
|
|
69
70
|
export function agentSub(n, waiting) {
|
|
70
|
-
if (waiting?.has(n.id)) return { text: 'needs you', tone: 'wait' };
|
|
71
|
+
if (waiting?.has(n.id)) return { text: t('needs you'), tone: 'wait' };
|
|
71
72
|
const s = stateOf(n.status);
|
|
72
73
|
if (s === 'failed') return { text: n.status, tone: 'fail' };
|
|
73
|
-
if (s === 'live') return { text: 'running', tone: null };
|
|
74
|
-
if (s === 'waking') return { text: 'starting', tone: null };
|
|
75
|
-
if (s === 'done') return { text: 'done', tone: null };
|
|
74
|
+
if (s === 'live') return { text: t('running'), tone: null };
|
|
75
|
+
if (s === 'waking') return { text: t('starting'), tone: null };
|
|
76
|
+
if (s === 'done') return { text: t('done'), tone: null };
|
|
76
77
|
// A status this panel has no word for keeps its own.
|
|
77
|
-
return { text: String(n.status ?? 'unknown'), tone: null };
|
|
78
|
+
return { text: String(n.status ?? t('unknown')), tone: null };
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
/** "12 done · 2 running" for several agents, in the order a reader looks for them. */
|
|
@@ -82,8 +83,8 @@ export function countsSub(members, waiting) {
|
|
|
82
83
|
const need = members.filter((m) => waiting?.has(m.id)).length;
|
|
83
84
|
const counts = tally(members.filter((m) => !waiting?.has(m.id)));
|
|
84
85
|
const parts = [];
|
|
85
|
-
if (need) parts.push(
|
|
86
|
-
const word = { running: 'running', starting: 'starting', done: 'done', failed: 'failed', killed: 'killed', stopped: 'stopped', ended: 'ended', stale: 'stale' };
|
|
86
|
+
if (need) parts.push(t('{n} need you', { n: need }));
|
|
87
|
+
const word = { running: t('running'), starting: t('starting'), done: t('done'), failed: t('failed'), killed: t('killed'), stopped: t('stopped'), ended: t('ended'), stale: t('stale') };
|
|
87
88
|
for (const k of ['failed', 'killed', 'stopped', 'running', 'starting', 'done', 'ended', 'stale']) {
|
|
88
89
|
if (counts[k]) parts.push(`${counts[k]} ${word[k]}`);
|
|
89
90
|
}
|
|
@@ -124,15 +125,15 @@ export function groupsOf(nodes, grouping) {
|
|
|
124
125
|
if (grouping === 'type') return { id: `type:${typeOf(n)}`, label: shownType(n), real: typeOf(n) };
|
|
125
126
|
if (grouping === 'parent') {
|
|
126
127
|
const p = byId.get(n.parentId);
|
|
127
|
-
if (!p || p.kind === 'session') return { id: 'direct', label: 'Session · direct' };
|
|
128
|
-
return { id: `parent:${p.id}`, label: p.kind === 'workflow' ? (p.workflowId ?? p.id) :
|
|
128
|
+
if (!p || p.kind === 'session') return { id: 'direct', label: t('Session · direct') };
|
|
129
|
+
return { id: `parent:${p.id}`, label: p.kind === 'workflow' ? (p.workflowId ?? p.id) : t('under {type}', { type: shownType(p) }) };
|
|
129
130
|
}
|
|
130
131
|
if (grouping === 'none') return { id: 'all', label: null };
|
|
131
132
|
// `order`: the agents called in one assistant message are a wave, and a wave is a group.
|
|
132
|
-
if (grouping === 'order') return n.wave != null ? { id: `wave:${n.wave}`, label:
|
|
133
|
+
if (grouping === 'order') return n.wave != null ? { id: `wave:${n.wave}`, label: t('Wave {n}', { n: n.wave }) } : { id: 'wave:none', label: t('Not called by the session') };
|
|
133
134
|
// `run`: a workflow run is a group; everything the session started itself is another.
|
|
134
135
|
if (n.workflow) return { id: `run:${n.workflow}`, label: String(n.workflow) };
|
|
135
|
-
return { id: 'direct', label: 'Session · direct' };
|
|
136
|
+
return { id: 'direct', label: t('Session · direct') };
|
|
136
137
|
};
|
|
137
138
|
const groups = new Map();
|
|
138
139
|
for (const n of agents) {
|
|
@@ -178,7 +179,7 @@ export function rows(nodes, opts = {}) {
|
|
|
178
179
|
id: g.id,
|
|
179
180
|
label: g.label,
|
|
180
181
|
folded: isFolded,
|
|
181
|
-
sub: `${g.members.length
|
|
182
|
+
sub: `${g.members.length === 1 ? t('{n} agent', { n: 1 }) : t('{n} agents', { n: g.members.length })}${countsSub(g.members, waiting) ? ` · ${countsSub(g.members, waiting)}` : ''}`,
|
|
182
183
|
// Folded, the header carries its members' bars, so a group that is out of sight is not out of time.
|
|
183
184
|
lanes: isFolded ? lanes(g.members.flatMap(bar)) : [],
|
|
184
185
|
});
|
|
@@ -290,18 +291,18 @@ export function stepRange(key, by) {
|
|
|
290
291
|
* @param owned the session's summary when Studio started it, else null
|
|
291
292
|
*/
|
|
292
293
|
export function waitNote(owned) {
|
|
293
|
-
if (!owned) return { measured: false, text: 'Not measured: this session\'s approvals are not seen by Studio.' };
|
|
294
|
-
if (owned.gated === false) return { measured: false, text: 'Not measured: this session runs without the approval gate.' };
|
|
295
|
-
return { measured: true, text:
|
|
294
|
+
if (!owned) return { measured: false, text: t('Not measured: this session\'s approvals are not seen by Studio.') };
|
|
295
|
+
if (owned.gated === false) return { measured: false, text: t('Not measured: this session runs without the approval gate.') };
|
|
296
|
+
return { measured: true, text: t('Waiting periods since Studio started this session at {time}.', { time: clock(owned.startedAt) }) };
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
/** "14:01 → 14:09 · 8m 12s · 31 tool calls · 57k tokens", with only the parts that were read. */
|
|
299
300
|
export function factsOf(n, now, fmt) {
|
|
300
301
|
const parts = [];
|
|
301
302
|
const end = n.kind === 'agent' ? endOf(n, now) : n.endedAt;
|
|
302
|
-
if (n.startedAt != null) parts.push(isLive(n) || end == null ? `${clock(n.startedAt)} → now` : `${clock(n.startedAt)} → ${clock(end)}`);
|
|
303
|
+
if (n.startedAt != null) parts.push(isLive(n) || end == null ? `${clock(n.startedAt)} → ${t('now')}` : `${clock(n.startedAt)} → ${clock(end)}`);
|
|
303
304
|
if (n.startedAt != null && end != null) parts.push(fmt.duration(end - n.startedAt));
|
|
304
|
-
if (n.toolCount != null) parts.push(
|
|
305
|
-
if (n.tokens != null) parts.push(
|
|
305
|
+
if (n.toolCount != null) parts.push(n.toolCount === 1 ? t('{n} tool call', { n: 1 }) : t('{n} tool calls', { n: n.toolCount }));
|
|
306
|
+
if (n.tokens != null) parts.push(t('{n} tokens', { n: fmt.tokens(n.tokens) }));
|
|
306
307
|
return parts.join(' · ');
|
|
307
308
|
}
|