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/chat.js
CHANGED
|
@@ -16,6 +16,7 @@ import { renderMarkdown } from './md.js';
|
|
|
16
16
|
import {
|
|
17
17
|
toolBlock, resultsIn, isDelegation, summaryOf, outputOf, delegationCard, reminderOf, headerOf, refusals,
|
|
18
18
|
} from './convo.js';
|
|
19
|
+
import { t, tParts } from './i18n.js';
|
|
19
20
|
|
|
20
21
|
const el = (tag, cls, text) => {
|
|
21
22
|
const n = document.createElement(tag);
|
|
@@ -148,7 +149,7 @@ class Pane {
|
|
|
148
149
|
this.remindEl = el('div', 'chat-remind');
|
|
149
150
|
this.remindEl.hidden = true;
|
|
150
151
|
this.remindText = el('span', 'chat-remind-text');
|
|
151
|
-
const review = el('button', 'chat-review', 'Review');
|
|
152
|
+
const review = el('button', 'chat-review', t('Review'));
|
|
152
153
|
review.type = 'button';
|
|
153
154
|
review.addEventListener('click', () => this.hooks.onReview?.(this));
|
|
154
155
|
this.remindEl.append(el('span', 'dot'), this.remindText, review);
|
|
@@ -157,12 +158,12 @@ class Pane {
|
|
|
157
158
|
this.formEl = el('form', 'chat-form');
|
|
158
159
|
this.inputEl = el('textarea', 'chat-input');
|
|
159
160
|
this.inputEl.rows = 3;
|
|
160
|
-
this.inputEl.placeholder = 'Message this session';
|
|
161
|
-
this.inputEl.setAttribute('aria-label', 'Message this session');
|
|
161
|
+
this.inputEl.placeholder = t('Message this session');
|
|
162
|
+
this.inputEl.setAttribute('aria-label', t('Message this session'));
|
|
162
163
|
const formFoot = el('div', 'chat-form-foot');
|
|
163
|
-
const send = el('button', 'btn sm primary chat-send', 'Send');
|
|
164
|
+
const send = el('button', 'btn sm primary chat-send', t('Send'));
|
|
164
165
|
send.type = 'submit';
|
|
165
|
-
formFoot.append(el('span', 'sub', 'Enter to send · Shift+Enter for a new line'), el('span', 'row-fill'), send);
|
|
166
|
+
formFoot.append(el('span', 'sub', t('Enter to send · Shift+Enter for a new line')), el('span', 'row-fill'), send);
|
|
166
167
|
this.formEl.append(this.inputEl, formFoot);
|
|
167
168
|
|
|
168
169
|
this.roEl = el('div', 'chat-ro');
|
|
@@ -205,15 +206,15 @@ class Pane {
|
|
|
205
206
|
conv = await fetch(this.api(`/api/session/${encodeURIComponent(from)}/conversation`), { cache: 'no-store' })
|
|
206
207
|
.then((r) => r.json());
|
|
207
208
|
} catch (e) {
|
|
208
|
-
this.note(
|
|
209
|
+
this.note(t('History not read: {reason}', { reason: e.message }), 'bad');
|
|
209
210
|
return;
|
|
210
211
|
}
|
|
211
|
-
if (conv?.transcriptsHidden) { this.note('Transcripts are hidden on this link', 'muted'); return; }
|
|
212
|
-
if (conv?.measured === false) { this.note(
|
|
212
|
+
if (conv?.transcriptsHidden) { this.note(t('Transcripts are hidden on this link'), 'muted'); return; }
|
|
213
|
+
if (conv?.measured === false) { this.note(t('History not measured — {reason}', { reason: conv.reason }), 'bad'); return; }
|
|
213
214
|
|
|
214
215
|
const frag = document.createDocumentFragment();
|
|
215
216
|
if (conv.truncated) {
|
|
216
|
-
frag.append(el('div', 'chat-note',
|
|
217
|
+
frag.append(el('div', 'chat-note', t('{n} earlier message(s) not shown', { n: conv.total - conv.messages.length })));
|
|
217
218
|
}
|
|
218
219
|
for (const m of conv.messages ?? []) frag.append(this.renderMessage(m, { quick: false }));
|
|
219
220
|
if (!this.readOnly) {
|
|
@@ -223,8 +224,8 @@ class Pane {
|
|
|
223
224
|
// is the one thing it is not; a real continuation that calls itself a
|
|
224
225
|
// fork undersells it.
|
|
225
226
|
frag.append(el('div', 'chat-seam', this.session?.forked
|
|
226
|
-
?
|
|
227
|
-
:
|
|
227
|
+
? t('— forked from {id} · a separate session; the original does not see this —', { id: from.slice(0, 8) })
|
|
228
|
+
: t('— continuing {id} · same session, same transcript —', { id: from.slice(0, 8) })));
|
|
228
229
|
}
|
|
229
230
|
this.logEl.prepend(frag);
|
|
230
231
|
this.logEl.scrollTop = this.logEl.scrollHeight;
|
|
@@ -328,7 +329,7 @@ class Pane {
|
|
|
328
329
|
return wrap;
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
wrap.append(el('div', 'msg-role', 'Session'));
|
|
332
|
+
wrap.append(el('div', 'msg-role', t('Session')));
|
|
332
333
|
const body = el('div', 'msg-body');
|
|
333
334
|
for (const b of msg.blocks) {
|
|
334
335
|
if (b.kind === 'text') {
|
|
@@ -355,7 +356,7 @@ class Pane {
|
|
|
355
356
|
b.addEventListener('click', () => { row.remove(); this.sendText(o); });
|
|
356
357
|
row.append(b);
|
|
357
358
|
}
|
|
358
|
-
row.append(el('span', 'sub quick-hint', 'Clicking an option sends it as your reply.'));
|
|
359
|
+
row.append(el('span', 'sub quick-hint', t('Clicking an option sends it as your reply.')));
|
|
359
360
|
wrap.append(row);
|
|
360
361
|
}
|
|
361
362
|
}
|
|
@@ -377,7 +378,7 @@ class Pane {
|
|
|
377
378
|
row.addEventListener('click', () => {
|
|
378
379
|
const text = outputOf(block);
|
|
379
380
|
// A call that has not come back has nothing to open, and says which it is rather than opening an empty box.
|
|
380
|
-
out.textContent = text ?? (this.readOnly ? 'Output not read from the transcript.' : 'Not returned yet.');
|
|
381
|
+
out.textContent = text ?? (this.readOnly ? t('Output not read from the transcript.') : t('Not returned yet.'));
|
|
381
382
|
out.hidden = !out.hidden;
|
|
382
383
|
row.setAttribute('aria-expanded', String(!out.hidden));
|
|
383
384
|
});
|
|
@@ -463,7 +464,7 @@ class Pane {
|
|
|
463
464
|
}
|
|
464
465
|
node.replaceChildren(...parts);
|
|
465
466
|
node.disabled = !card.agentId;
|
|
466
|
-
node.title = card.agentId ? 'Show this agent on the graph' : 'This agent is not on the graph being shown';
|
|
467
|
+
node.title = card.agentId ? t('Show this agent on the graph') : t('This agent is not on the graph being shown');
|
|
467
468
|
}
|
|
468
469
|
|
|
469
470
|
/** The graph moved: the cards say what their agents are doing now. */
|
|
@@ -487,7 +488,7 @@ class Pane {
|
|
|
487
488
|
if (!this.streaming) { node?.remove(); return; }
|
|
488
489
|
if (!node) {
|
|
489
490
|
node = el('div', 'msg msg-assistant msg-streaming');
|
|
490
|
-
node.append(el('div', 'msg-role', 'Session'));
|
|
491
|
+
node.append(el('div', 'msg-role', t('Session')));
|
|
491
492
|
node.append(el('div', 'msg-body'));
|
|
492
493
|
this.logEl.append(node);
|
|
493
494
|
}
|
|
@@ -496,7 +497,7 @@ class Pane {
|
|
|
496
497
|
body.textContent = this.streaming.text;
|
|
497
498
|
body.classList.add('msg-text');
|
|
498
499
|
} else {
|
|
499
|
-
body.replaceChildren(el('span', 'thinking', 'thinking…'));
|
|
500
|
+
body.replaceChildren(el('span', 'thinking', t('thinking…')));
|
|
500
501
|
}
|
|
501
502
|
this.scroll();
|
|
502
503
|
}
|
|
@@ -512,24 +513,24 @@ class Pane {
|
|
|
512
513
|
const parts = [badge];
|
|
513
514
|
for (const p of h.parts) {
|
|
514
515
|
// The mode is a control: a click offers the modes a session can be in. Everything else in the strip is said.
|
|
515
|
-
if (h.mode && p ===
|
|
516
|
+
if (h.mode && p === t('mode: {mode}', { mode: h.mode }) && this.hooks.onMode && s?.state !== 'exited' && s?.state !== 'failed') {
|
|
516
517
|
const b = el('button', 'sub pane-mode', p);
|
|
517
518
|
b.type = 'button';
|
|
518
|
-
b.title = 'Change the mode this session is in';
|
|
519
|
+
b.title = t('Change the mode this session is in');
|
|
519
520
|
b.setAttribute('aria-haspopup', 'menu');
|
|
520
521
|
b.addEventListener('click', (e) => { e.stopPropagation(); this.hooks.onMode(this, b, h.mode); });
|
|
521
522
|
parts.push(b);
|
|
522
523
|
} else parts.push(el('span', 'sub', p));
|
|
523
524
|
}
|
|
524
525
|
if (h.ungated) {
|
|
525
|
-
const warn = el('span', 'pill pane-ungated', 'No approval gate');
|
|
526
|
-
warn.title = 'This session was started without the approval gate: its tool calls do not wait for you.';
|
|
526
|
+
const warn = el('span', 'pill pane-ungated', t('No approval gate'));
|
|
527
|
+
warn.title = t('This session was started without the approval gate: its tool calls do not wait for you.');
|
|
527
528
|
parts.push(warn);
|
|
528
529
|
}
|
|
529
530
|
parts.push(el('span', 'row-fill'));
|
|
530
531
|
if (!this.readOnly && s?.costUsd) {
|
|
531
532
|
const cost = el('span', 'sub pane-cost', `$${s.costUsd.toFixed(4)}`);
|
|
532
|
-
cost.title = 'What this session has cost, as the CLI reported it';
|
|
533
|
+
cost.title = t('What this session has cost, as the CLI reported it');
|
|
533
534
|
parts.push(cost);
|
|
534
535
|
}
|
|
535
536
|
// Stopping ends the process. It is offered for as long as there is one.
|
|
@@ -537,7 +538,7 @@ class Pane {
|
|
|
537
538
|
const stop = el('button', 'btn sm chat-stop');
|
|
538
539
|
stop.type = 'button';
|
|
539
540
|
stop.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><rect x="4" y="4" width="8" height="8" rx="1.5"/></svg>';
|
|
540
|
-
stop.append(el('span', null, 'Stop'));
|
|
541
|
+
stop.append(el('span', null, t('Stop')));
|
|
541
542
|
stop.addEventListener('click', () => this.stop());
|
|
542
543
|
parts.push(stop);
|
|
543
544
|
}
|
|
@@ -553,36 +554,35 @@ class Pane {
|
|
|
553
554
|
*/
|
|
554
555
|
paintReadOnly() {
|
|
555
556
|
const lead = el('p', 'chat-ro-lead');
|
|
556
|
-
lead.append(el('strong', null, 'This session was not started here.'),
|
|
557
|
-
'
|
|
557
|
+
lead.append(el('strong', null, t('This session was not started here.')),
|
|
558
|
+
` ${t('Studio can read it but cannot write to it or answer its approvals.')}`);
|
|
558
559
|
|
|
559
|
-
const cont = el('button', 'btn sm primary', 'Continue here');
|
|
560
|
+
const cont = el('button', 'btn sm primary', t('Continue here'));
|
|
560
561
|
cont.type = 'button';
|
|
561
562
|
cont.addEventListener('click', () => this.hooks.onContinue?.(this, cont));
|
|
562
563
|
const contRow = el('div', 'chat-ro-row');
|
|
563
564
|
// Said before the press, from the server's own check (the one Continue here acts on): the same session, or a copy.
|
|
564
565
|
// Until the server answers, and if it cannot: the sentence true of both outcomes.
|
|
565
|
-
const FALLBACK = 'Continues it in Studio. If it is still open somewhere else, Studio starts a copy instead: '
|
|
566
|
-
+ 'the original does not see this, and messages here never reach your terminal.';
|
|
566
|
+
const FALLBACK = t('Continues it in Studio. If it is still open somewhere else, Studio starts a copy instead: the original does not see this, and messages here never reach your terminal.');
|
|
567
567
|
const contText = el('span', 'sub', FALLBACK);
|
|
568
568
|
contRow.append(cont, contText);
|
|
569
569
|
|
|
570
|
-
const term = el('button', 'btn sm', 'Open in terminal');
|
|
570
|
+
const term = el('button', 'btn sm', t('Open in terminal'));
|
|
571
571
|
term.type = 'button';
|
|
572
572
|
term.addEventListener('click', (e) => { e.stopPropagation(); this.hooks.onTerminal?.(this, term); });
|
|
573
573
|
const line = el('code', 'chat-ro-cmd');
|
|
574
574
|
const termText = el('span', 'sub');
|
|
575
|
-
termText.append('Shows the command first, then opens a terminal: ', line);
|
|
575
|
+
termText.append(...tParts('Shows the command first, then opens a terminal: {command}', { command: line }));
|
|
576
576
|
const termRow = el('div', 'chat-ro-row');
|
|
577
577
|
termRow.append(term, termText);
|
|
578
578
|
|
|
579
579
|
// Back to the desktop app: the command, to copy and run where the app is.
|
|
580
580
|
const deskLine = el('code', 'chat-ro-cmd');
|
|
581
|
-
const deskCopy = el('button', 'btn sm', 'Copy');
|
|
581
|
+
const deskCopy = el('button', 'btn sm', t('Copy'));
|
|
582
582
|
deskCopy.type = 'button';
|
|
583
583
|
deskCopy.addEventListener('click', () => { if (deskLine.dataset.cmd) navigator.clipboard?.writeText(deskLine.dataset.cmd).then(() => { deskCopy.textContent = 'Copied'; }, () => {}); });
|
|
584
584
|
const deskText = el('span', 'sub');
|
|
585
|
-
deskText.append('Open it in the desktop app again: ', deskLine);
|
|
585
|
+
deskText.append(...tParts('Open it in the desktop app again: {command}', { command: deskLine }));
|
|
586
586
|
const deskRow = el('div', 'chat-ro-row');
|
|
587
587
|
deskRow.append(deskCopy, deskText);
|
|
588
588
|
|
|
@@ -592,18 +592,18 @@ class Pane {
|
|
|
592
592
|
fetch(this.api(`/api/session/${encodeURIComponent(this.id)}/terminal`), { cache: 'no-store' })
|
|
593
593
|
.then((r) => r.json())
|
|
594
594
|
.then((r) => {
|
|
595
|
-
line.textContent = r?.plan?.line ?? 'not measured — no working directory or session id recorded';
|
|
595
|
+
line.textContent = r?.plan?.line ?? t('not measured — no working directory or session id recorded');
|
|
596
596
|
// On Windows the command is a PowerShell line (no one line works in cmd and PowerShell 5.1 both): say so.
|
|
597
|
-
deskLine.textContent = r?.desktop?.line ?? 'not measured — no working directory or session id recorded';
|
|
597
|
+
deskLine.textContent = r?.desktop?.line ?? t('not measured — no working directory or session id recorded');
|
|
598
598
|
if (r?.desktop?.line) deskLine.dataset.cmd = r.desktop.line; else deskCopy.disabled = true;
|
|
599
|
-
if (r?.desktop?.shell === 'PowerShell') deskText.append(
|
|
599
|
+
if (r?.desktop?.shell === 'PowerShell') deskText.append(` ${t('(in PowerShell)')}`);
|
|
600
600
|
contText.textContent = r?.continueAs === 'same'
|
|
601
|
-
? 'Continues this same session in Studio: its id and history stay, and the desktop app or a terminal that opens it later sees what you add here. Nothing else has it open now.'
|
|
601
|
+
? t('Continues this same session in Studio: its id and history stay, and the desktop app or a terminal that opens it later sees what you add here. Nothing else has it open now.')
|
|
602
602
|
: r?.continueAs === 'copy'
|
|
603
|
-
? 'It is open somewhere else right now, so Studio starts a copy: the original does not see this, and messages here never reach your terminal or the desktop app.'
|
|
603
|
+
? t('It is open somewhere else right now, so Studio starts a copy: the original does not see this, and messages here never reach your terminal or the desktop app.')
|
|
604
604
|
: FALLBACK;
|
|
605
605
|
})
|
|
606
|
-
.catch((e) => { line.textContent =
|
|
606
|
+
.catch((e) => { line.textContent = t('not read — {reason}', { reason: e.message }); deskLine.textContent = t('not read — {reason}', { reason: e.message }); deskCopy.disabled = true; });
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
paintState() {
|
|
@@ -654,7 +654,7 @@ class Pane {
|
|
|
654
654
|
body: JSON.stringify({ text }),
|
|
655
655
|
}).then((r) => r.json()).catch((e) => ({ ok: false, reason: e.message }));
|
|
656
656
|
|
|
657
|
-
if (!res.ok) { this.clearPending(); this.note(
|
|
657
|
+
if (!res.ok) { this.clearPending(); this.note(t('Not sent: {reason}', { reason: res.reason }), 'bad'); }
|
|
658
658
|
else { this.session = res.session; this.paintState(); this.onChange(this); }
|
|
659
659
|
return res;
|
|
660
660
|
}
|
|
@@ -811,11 +811,11 @@ export class Chat {
|
|
|
811
811
|
tab.append(el('span', 'tab-name', String(name).slice(0, 28)));
|
|
812
812
|
if (p.permissions?.length) tab.append(el('span', 'tab-badge warn', String(p.permissions.length)));
|
|
813
813
|
else if (p.unread) tab.append(el('span', 'tab-badge', String(p.unread)));
|
|
814
|
-
tab.title = `${name}\n${id}\n${p.readOnly ? 'read only' : `${p.session?.state ?? ''} · ${p.session?.permissionMode ?? ''}`}`;
|
|
814
|
+
tab.title = `${name}\n${id}\n${p.readOnly ? t('read only') : `${p.session?.state ?? ''} · ${p.session?.permissionMode ?? ''}`}`;
|
|
815
815
|
tab.addEventListener('click', () => this.activate(id));
|
|
816
816
|
|
|
817
817
|
const x = el('span', 'tab-x', '×');
|
|
818
|
-
x.title = 'Close this tab — the session keeps running';
|
|
818
|
+
x.title = t('Close this tab — the session keeps running');
|
|
819
819
|
x.addEventListener('click', (e) => { e.stopPropagation(); this.close(id); });
|
|
820
820
|
tab.append(x);
|
|
821
821
|
frag.append(tab);
|
package/kit/studio/web/convo.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// watches) and the live stream of a session started here. Both go through these functions, so a tool row reads
|
|
5
5
|
// the same whichever way it arrived.
|
|
6
6
|
import { roleName, modelOf } from './names.js';
|
|
7
|
+
import { t, numLocale } from './i18n.js';
|
|
7
8
|
|
|
8
9
|
// How much of a tool's output the page keeps for a live result: the same tail the server keeps for history.
|
|
9
10
|
const RESULT_TAIL = 4096;
|
|
@@ -60,7 +61,7 @@ export function summaryOf(block) {
|
|
|
60
61
|
if (!r) return { state: 'pending', text: null };
|
|
61
62
|
const lines = r.text.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
62
63
|
const last = lines.length ? lines[lines.length - 1] : null;
|
|
63
|
-
const text = last == null ? 'no output' : (last.length > SUMMARY_MAX ? `${last.slice(0, SUMMARY_MAX - 1)}…` : last);
|
|
64
|
+
const text = last == null ? t('no output') : (last.length > SUMMARY_MAX ? `${last.slice(0, SUMMARY_MAX - 1)}…` : last);
|
|
64
65
|
return { state: r.error ? 'error' : 'ok', text };
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -68,8 +69,8 @@ export function summaryOf(block) {
|
|
|
68
69
|
export function outputOf(block) {
|
|
69
70
|
const r = block?.result;
|
|
70
71
|
if (!r) return null;
|
|
71
|
-
if (!r.text) return '(no output)';
|
|
72
|
-
return r.truncated ?
|
|
72
|
+
if (!r.text) return t('(no output)');
|
|
73
|
+
return r.truncated ? `${t('… {n} earlier characters not shown', { n: (r.length - r.text.length).toLocaleString(numLocale()) })}\n${r.text}` : r.text;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
@@ -112,11 +113,11 @@ export function refusals(denials, wasAllowed, resultOf = () => null) {
|
|
|
112
113
|
// Who refused is in the result's own words. Another PreToolUse hook that blocked the call is reported by
|
|
113
114
|
// the harness as a hook error; anything else is the harness's own check. With no result on screen the line
|
|
114
115
|
// says only what is known: it was allowed here and did not run.
|
|
115
|
-
const by = reason == null ? 'it did not run'
|
|
116
|
-
: /^PreToolUse:\S* hook error/.test(reason) ? 'another gate blocked it' : 'Claude Code refused it';
|
|
116
|
+
const by = reason == null ? t('it did not run')
|
|
117
|
+
: /^PreToolUse:\S* hook error/.test(reason) ? t('another gate blocked it') : t('Claude Code refused it');
|
|
117
118
|
out.push({
|
|
118
119
|
id: d.tool_use_id,
|
|
119
|
-
text:
|
|
120
|
+
text: `${t('Allowed here, but {by}: {tool}', { by, tool: d.tool_name ?? t('tool') })}${what ? ` · ${String(what).slice(0, 120)}` : ''}`
|
|
120
121
|
+ (reason ? ` — ${reason.slice(0, 200)}` : ''),
|
|
121
122
|
});
|
|
122
123
|
}
|
|
@@ -127,29 +128,33 @@ export function refusals(denials, wasAllowed, resultOf = () => null) {
|
|
|
127
128
|
export function reminderOf(waiting) {
|
|
128
129
|
const list = waiting ?? [];
|
|
129
130
|
if (!list.length) return null;
|
|
130
|
-
const who = list[0].agentType ? roleName(list[0].agentType) : 'This session';
|
|
131
|
+
const who = list[0].agentType ? roleName(list[0].agentType) : t('This session');
|
|
131
132
|
const more = list.length - 1;
|
|
132
|
-
return
|
|
133
|
+
if (more <= 0) return t('{who} is waiting for approval', { who });
|
|
134
|
+
return more === 1 ? t('{who} is waiting for approval, and 1 more request is', { who })
|
|
135
|
+
: t('{who} is waiting for approval, and {n} more requests are', { who, n: more });
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
// The modes a session started here can be put in: the three it can be started in, and no other.
|
|
136
139
|
export const SESSION_MODES = [
|
|
137
|
-
{ mode: 'plan', label: 'plan', title: 'Explore and plan; nothing is edited until a plan is approved.' },
|
|
138
|
-
{ mode: 'acceptEdits', label: 'acceptEdits', title: 'File edits in the project are accepted; every call is still shown in the dock first.' },
|
|
139
|
-
{ mode: 'default', label: 'default', title: 'Each call is asked about in the dock.' },
|
|
140
|
+
{ mode: 'plan', label: 'plan', title: t('Explore and plan; nothing is edited until a plan is approved.') },
|
|
141
|
+
{ mode: 'acceptEdits', label: 'acceptEdits', title: t('File edits in the project are accepted; every call is still shown in the dock first.') },
|
|
142
|
+
{ mode: 'default', label: 'default', title: t('Each call is asked about in the dock.') },
|
|
140
143
|
];
|
|
141
144
|
|
|
142
145
|
/** The strip above a conversation: what kind of session this is, in the order the design has it. */
|
|
143
146
|
export function headerOf(session, { readOnly = false, contextTokens = null } = {}) {
|
|
144
|
-
if (readOnly) return { badge: 'Read only', tone: 'none', parts: [] };
|
|
147
|
+
if (readOnly) return { badge: t('Read only'), tone: 'none', parts: [] };
|
|
145
148
|
const parts = [];
|
|
146
|
-
if (session?.permissionMode) parts.push(
|
|
149
|
+
if (session?.permissionMode) parts.push(t('mode: {mode}', { mode: session.permissionMode }));
|
|
147
150
|
const facts = [];
|
|
148
|
-
if (typeof session?.turns === 'number') facts.push(
|
|
149
|
-
if (contextTokens != null) facts.push(
|
|
151
|
+
if (typeof session?.turns === 'number') facts.push(session.turns === 1 ? t('{n} turn', { n: 1 }) : t('{n} turns', { n: session.turns }));
|
|
152
|
+
if (contextTokens != null) facts.push(t('{n} ctx', { n: fmtK(contextTokens) }));
|
|
150
153
|
if (facts.length) parts.push(facts.join(' · '));
|
|
154
|
+
const changed = changesLine(session?.changes);
|
|
155
|
+
if (changed) parts.push(changed);
|
|
151
156
|
return {
|
|
152
|
-
badge: 'Started here',
|
|
157
|
+
badge: t('Started here'),
|
|
153
158
|
tone: 'good',
|
|
154
159
|
parts,
|
|
155
160
|
// The mode on its own as well: in the strip it is a control, and the rest of `parts` is not.
|
|
@@ -159,6 +164,18 @@ export function headerOf(session, { readOnly = false, contextTokens = null } = {
|
|
|
159
164
|
};
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
/**
|
|
168
|
+
* What a session started here has changed since it started, as the server read it from git: null when it was not
|
|
169
|
+
* measured or nothing changed, so a strip never shows a 0 nobody counted.
|
|
170
|
+
*/
|
|
171
|
+
export function changesLine(c) {
|
|
172
|
+
if (!c?.measured || (!c.files && !c.untracked)) return null;
|
|
173
|
+
const bits = [];
|
|
174
|
+
if (c.files) bits.push(c.files === 1 ? t('{n} file changed (+{added} −{removed})', { n: 1, added: c.added, removed: c.removed }) : t('{n} files changed (+{added} −{removed})', { n: c.files, added: c.added, removed: c.removed }));
|
|
175
|
+
if (c.untracked) bits.push(t('{n} new untracked', { n: c.untracked }));
|
|
176
|
+
return bits.join(' · ');
|
|
177
|
+
}
|
|
178
|
+
|
|
162
179
|
function fmtK(t) {
|
|
163
180
|
if (t < 1000) return `${t}`;
|
|
164
181
|
if (t < 1_000_000) return `${(t / 1000).toFixed(t < 10_000 ? 1 : 0)}k`;
|
package/kit/studio/web/dock.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// approvals.js; this file draws it and hands the three answers back.
|
|
6
6
|
import { remaining, asker, allowSessionLabel, VERDICTS, OUTCOME_WORD } from './approvals.js';
|
|
7
7
|
import { AskForms } from './ask.js';
|
|
8
|
+
import { t } from './i18n.js';
|
|
8
9
|
|
|
9
10
|
const SVG = 'http://www.w3.org/2000/svg';
|
|
10
11
|
const RING = 2 * Math.PI * 15; // the countdown ring's circumference, r = 15
|
|
@@ -65,11 +66,11 @@ export class Dock {
|
|
|
65
66
|
|
|
66
67
|
const text = mk('div', 'dock-text');
|
|
67
68
|
const head = mk('div', 'dock-head');
|
|
68
|
-
const word = mk('span', 'dock-word', 'Needs you');
|
|
69
|
+
const word = mk('span', 'dock-word', t('Needs you'));
|
|
69
70
|
// Who asked is the way to them: it brings that agent's card into view.
|
|
70
71
|
const who = mk('button', 'dock-who');
|
|
71
72
|
who.type = 'button';
|
|
72
|
-
who.title = 'Show on the canvas';
|
|
73
|
+
who.title = t('Show on the canvas');
|
|
73
74
|
who.addEventListener('click', () => { if (this.current) this.onLocate(this.current); });
|
|
74
75
|
const tool = mk('span', 'dock-tool');
|
|
75
76
|
const where = mk('span', 'dock-where');
|
|
@@ -84,12 +85,12 @@ export class Dock {
|
|
|
84
85
|
const nav = mk('div', 'dock-nav');
|
|
85
86
|
const prev = mk('button', 'btn icon sm ghost dock-step');
|
|
86
87
|
prev.type = 'button';
|
|
87
|
-
prev.setAttribute('aria-label', 'Previous request');
|
|
88
|
+
prev.setAttribute('aria-label', t('Previous request'));
|
|
88
89
|
prev.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M10 4l-4 4 4 4"/></svg>';
|
|
89
90
|
const count = mk('span', 'dock-count');
|
|
90
91
|
const next = mk('button', 'btn icon sm ghost dock-step');
|
|
91
92
|
next.type = 'button';
|
|
92
|
-
next.setAttribute('aria-label', 'Next request');
|
|
93
|
+
next.setAttribute('aria-label', t('Next request'));
|
|
93
94
|
next.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg>';
|
|
94
95
|
prev.addEventListener('click', () => this.step(-1));
|
|
95
96
|
next.addEventListener('click', () => this.step(1));
|
|
@@ -192,14 +193,14 @@ export class Dock {
|
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
195
|
this.root.dataset.state = 'waiting';
|
|
195
|
-
p.word.textContent = 'Needs you';
|
|
196
|
+
p.word.textContent = t('Needs you');
|
|
196
197
|
|
|
197
198
|
p.who.textContent = asker(item);
|
|
198
|
-
p.who.title = item.agentType ?
|
|
199
|
+
p.who.title = item.agentType ? t('{type} — show on the canvas', { type: item.agentType }) : t('Show on the canvas');
|
|
199
200
|
p.tool.textContent = item.toolName;
|
|
200
201
|
const where = this.where(item);
|
|
201
202
|
p.where.hidden = !where;
|
|
202
|
-
p.where.textContent = where ?
|
|
203
|
+
p.where.textContent = where ? t('in {name}', { name: where }) : '';
|
|
203
204
|
// A question or a plan: its form, in place of the command and the three buttons.
|
|
204
205
|
const form = this.forms.get(item);
|
|
205
206
|
p.ask.hidden = !form;
|
|
@@ -207,37 +208,37 @@ export class Dock {
|
|
|
207
208
|
// Put in once and left there: a form taken out and put back on every tick would lose the caret in it.
|
|
208
209
|
if (this.shownForm !== form) { p.ask.replaceChildren(...(form ? [form] : [])); this.shownForm = form; }
|
|
209
210
|
this.root.dataset.ask = form ? item.ask.kind : '';
|
|
210
|
-
p.tool.textContent = form ? (item.ask.kind === 'plan' ? 'wants its plan approved' : 'asks') : item.toolName;
|
|
211
|
+
p.tool.textContent = form ? (item.ask.kind === 'plan' ? t('wants its plan approved') : t('asks')) : item.toolName;
|
|
211
212
|
p.cmd.hidden = !item.detail || Boolean(form);
|
|
212
213
|
p.cmd.textContent = item.detail ?? '';
|
|
213
214
|
p.cmd.classList.toggle('open', this.open);
|
|
214
215
|
p.cmd.setAttribute('aria-expanded', String(this.open));
|
|
215
|
-
p.cmd.title = this.open ? 'Show on one line' : 'Show the whole command';
|
|
216
|
+
p.cmd.title = this.open ? t('Show on one line') : t('Show the whole command');
|
|
216
217
|
|
|
217
218
|
const r = remaining(item, this.now());
|
|
218
219
|
if (r.known) {
|
|
219
220
|
p.ring.style.strokeDasharray = `${(r.fraction * RING).toFixed(1)} ${RING.toFixed(1)}`;
|
|
220
|
-
p.secs.textContent =
|
|
221
|
-
p.timer.setAttribute('aria-label',
|
|
222
|
-
p.note.textContent = form ?
|
|
221
|
+
p.secs.textContent = t('{n}s', { n: r.left });
|
|
222
|
+
p.timer.setAttribute('aria-label', t('Auto-deny in {n} seconds', { n: r.left }));
|
|
223
|
+
p.note.textContent = form ? t('Refused in {n}s if nobody answers', { n: r.left }) : t('Auto-deny in {n}s if nobody answers', { n: r.left });
|
|
223
224
|
} else {
|
|
224
225
|
// The server did not say how long the hook waits. No clock is better than an invented one.
|
|
225
226
|
p.ring.style.strokeDasharray = `0 ${RING.toFixed(1)}`;
|
|
226
227
|
p.secs.textContent = '?';
|
|
227
|
-
p.timer.setAttribute('aria-label', 'Time left not measured');
|
|
228
|
-
p.note.textContent = 'Time left not measured — it is denied if nobody answers';
|
|
228
|
+
p.timer.setAttribute('aria-label', t('Time left not measured'));
|
|
229
|
+
p.note.textContent = t('Time left not measured — it is denied if nobody answers');
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
p.nav.hidden = this.items.length < 2;
|
|
232
|
-
p.count.textContent =
|
|
233
|
-
p.nav.setAttribute('aria-label',
|
|
233
|
+
p.count.textContent = t('{at} of {n}', { at: this.at + 1, n: this.items.length });
|
|
234
|
+
p.nav.setAttribute('aria-label', t('Request {at} of {n}', { at: this.at + 1, n: this.items.length }));
|
|
234
235
|
|
|
235
236
|
const sent = this.busy.has(item.key);
|
|
236
237
|
p.always.textContent = allowSessionLabel(item.toolName);
|
|
237
|
-
p.always.title =
|
|
238
|
+
p.always.title = t('Allow this call, and stop asking here about {tool} for the rest of the session. Later calls are not approved for Claude Code: it still applies its own checks to them.', { tool: item.toolName });
|
|
238
239
|
for (const b of [p.deny, p.always, p.allow]) b.disabled = sent;
|
|
239
240
|
this.root.setAttribute('aria-label', form
|
|
240
|
-
?
|
|
241
|
-
:
|
|
241
|
+
? (item.ask.kind === 'plan' ? t('Waiting for you: {who} wants its plan approved', { who: asker(item) }) : t('Waiting for you: {who} asks a question', { who: asker(item) }))
|
|
242
|
+
: t('Waiting for approval: {who} wants to run {tool}', { who: asker(item), tool: item.toolName }));
|
|
242
243
|
}
|
|
243
244
|
}
|