crewforth 3.1.1 → 3.2.0-rc.10
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 +323 -0
- package/README.md +6 -3
- package/VERSION +1 -1
- package/adopt.sh +66 -6
- package/bin/cli.js +3 -2
- package/kit/AGENT_TEMPLATE.md +2 -1
- package/kit/CLAUDE.md +15 -14
- package/kit/agents/crew-commit-agent.md +11 -4
- package/kit/agents/crew-docs-agent.md +71 -0
- package/kit/agents/crew-planner.md +2 -0
- package/kit/agents/crew-refuter.md +77 -0
- package/kit/agents/crew-review-agent.md +2 -0
- package/kit/agents/crew-security-expert.md +3 -1
- package/kit/agents/crew-session-manager.md +11 -9
- package/kit/eval/doctor.sh +128 -6
- package/kit/eval/gate-report.sh +2 -0
- package/kit/eval/golden-routing.txt +9 -0
- package/kit/eval/lib/crew-map/crew-map.sh +26 -0
- package/kit/eval/lib/crew-map/detect.mjs +238 -0
- package/kit/eval/lib/crew-map/narrative.mjs +132 -0
- package/kit/eval/lib/crew-map/parse.mjs +305 -0
- package/kit/eval/lib/crew-map/project.mjs +164 -0
- package/kit/eval/lib/crew-map/rules/names.json +38 -0
- package/kit/eval/lib/crew-map/rules/packages.json +142 -0
- package/kit/eval/lib/crew-map/rules/python-stdlib.json +197 -0
- package/kit/eval/lib/crew-map/rules/tech.json +35 -0
- package/kit/eval/lib/crew-map/scan.mjs +266 -0
- package/kit/eval/lib/crew-map/stacks.mjs +547 -0
- package/kit/eval/lib/crew-map/surface.mjs +535 -0
- package/kit/eval/lib/findings/findings.mjs +410 -0
- package/kit/eval/lib/findings/findings.schema.json +122 -0
- package/kit/eval/lib/findings/findings.sh +29 -0
- package/kit/eval/lib/gate-log.sh +52 -0
- package/kit/eval/lib/json-string.awk +27 -0
- package/kit/eval/lib/preset.sh +109 -0
- package/kit/eval/lib/presets/default.json +7 -0
- package/kit/eval/lib/presets/solo.json +7 -0
- package/kit/eval/lib/presets/strict.json +11 -0
- package/kit/eval/lib/preview.sh +150 -0
- package/kit/eval/lib/review-bench/bench.mjs +273 -0
- package/kit/eval/lib/review-prepass/prepass.mjs +203 -0
- package/kit/eval/lib/review-prepass/prepass.sh +23 -0
- package/kit/eval/lib/settings-default.sh +36 -0
- package/kit/eval/lib/settings-json.awk +19 -4
- package/kit/eval/lib/star.sh +23 -7
- package/kit/eval/lib/statusline.settings.json +6 -0
- package/kit/eval/lib/statusline.sh +100 -0
- package/kit/eval/lib/verify-cmd.sh +17 -0
- package/kit/eval/lib/version.sh +41 -0
- package/kit/eval/review-bench.sh +26 -0
- package/kit/eval/smoke-test.sh +878 -53
- package/kit/hooks/agent-outcome.sh +44 -12
- package/kit/hooks/board-sync.sh +2 -23
- package/kit/hooks/guard-agent-model.sh +177 -24
- package/kit/hooks/guard-bash.sh +295 -204
- package/kit/hooks/guard-commit-scan.sh +10 -2
- package/kit/hooks/guard-question.sh +73 -0
- package/kit/hooks/guard-write.sh +24 -7
- package/kit/hooks/precompact-snapshot.sh +75 -0
- package/kit/hooks/prompt-approval.sh +389 -103
- package/kit/hooks/refuter-ledger.sh +80 -0
- package/kit/hooks/session-rehydrate.sh +25 -2
- package/kit/hooks/session-update-check.sh +9 -8
- package/kit/hooks/trace-blocklist.txt +9 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +51 -0
- package/kit/skills/crew-approve/SKILL.md +7 -0
- package/kit/skills/crew-code-review/SKILL.md +16 -0
- package/kit/skills/crew-doctor/SKILL.md +5 -1
- package/kit/skills/crew-handoff/SKILL.md +3 -2
- package/kit/skills/crew-map/SKILL.md +55 -0
- package/kit/skills/crew-review/SKILL.md +4 -1
- package/kit/skills/crew-ship/SKILL.md +3 -1
- package/kit/skills/crew-update/SKILL.md +1 -1
- package/kit/skills/incident-runbook/SKILL.md +2 -2
- package/kit/skills/security-scan/references/verify.md +6 -3
- package/kit/studio/README.md +25 -2
- package/kit/studio/server/index.js +187 -21
- package/kit/studio/server/lib/code-stamp.js +35 -0
- package/kit/studio/server/lib/crew-map.js +113 -0
- package/kit/studio/server/lib/graph.js +9 -1
- package/kit/studio/server/lib/kit-telemetry.js +30 -5
- package/kit/studio/server/lib/lan.js +87 -0
- package/kit/studio/server/lib/model-outcomes.js +130 -15
- package/kit/studio/server/lib/owned-stream.js +56 -0
- package/kit/studio/server/lib/pricing.js +3 -0
- package/kit/studio/server/lib/projects.js +11 -4
- package/kit/studio/server/lib/redact.js +60 -0
- package/kit/studio/server/lib/remote.js +185 -0
- package/kit/studio/server/lib/session.js +1 -1
- package/kit/studio/server/lib/terminal.js +51 -13
- package/kit/studio/server/lib/transcript.js +32 -0
- package/kit/studio/server/lib/usage.js +74 -14
- package/kit/studio/web/app.js +287 -17
- package/kit/studio/web/canvas.js +58 -122
- package/kit/studio/web/chat.js +114 -24
- package/kit/studio/web/graph-plan.js +13 -6
- package/kit/studio/web/index.html +10 -5
- package/kit/studio/web/liveness.js +14 -0
- package/kit/studio/web/map-plan.js +410 -0
- package/kit/studio/web/map.js +596 -0
- package/kit/studio/web/models-plan.js +61 -7
- package/kit/studio/web/models.js +19 -3
- package/kit/studio/web/qr.js +200 -0
- package/kit/studio/web/style.css +165 -0
- package/kit/studio/web/tokens.css +6 -0
- package/kit/studio/web/usage.js +12 -0
- package/kit/studio/web/viewport.js +160 -0
- package/package.json +6 -5
- package/start.sh +24 -1
|
@@ -52,15 +52,25 @@ function stepOf(r, again) {
|
|
|
52
52
|
* How the work went, as steps: the earlier calls of the same card, then this one. `row` is Crewforth's line for
|
|
53
53
|
* the agent with `before` (see server/lib/model-outcomes.js), or null when it has none: it has not stopped, or it
|
|
54
54
|
* is not a crew agent.
|
|
55
|
-
* @returns { risk, steps: [{ text, verify, tone, escalated }], note }
|
|
55
|
+
* @returns { risk, riskWhy, steps: [{ text, verify, tone, escalated }], note }
|
|
56
56
|
*/
|
|
57
57
|
export function workSteps(row) {
|
|
58
|
-
if (!row) return { risk: null, steps: [], note: 'Not recorded yet: the line is written when the agent stops.' };
|
|
58
|
+
if (!row) return { risk: null, riskWhy: null, steps: [], note: 'Not recorded yet: the line is written when the agent stops.' };
|
|
59
59
|
const steps = [...(row.before ?? []).map((r) => stepOf(r, false)), stepOf(row, Boolean(row.escalatedFrom))];
|
|
60
60
|
if (row.escalatedFrom && !(row.before ?? []).length) {
|
|
61
61
|
steps.unshift({ text: `An earlier call on ${tierWord(row.escalatedFrom)} did not pass; its line is not in the record`, verify: null, tone: null, escalated: false });
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// The reason is the gate's own sentence, kept as it wrote it; a line written before the gate kept one has none.
|
|
64
|
+
return { risk: row.risk ?? null, riskWhy: row.risk === 'critical' ? row.riskWhy ?? null : null, steps, note: row.escalatedFrom ? 'A re-run with the same or a lower model is refused by the gate.' : null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What is said about an agent the model gate does not look at: one whose type is not crew-* (hooks/guard-agent-model.sh
|
|
69
|
+
* leaves it alone), so there is no card to read and no line in the record. Null for a crew agent or an unknown type.
|
|
70
|
+
*/
|
|
71
|
+
export function outsideRouting(agentType) {
|
|
72
|
+
if (typeof agentType !== 'string' || !agentType || agentType.startsWith('crew-')) return null;
|
|
73
|
+
return 'Outside model routing: not a crew agent, so the gate chose nothing for it and Crewforth kept no record of it. It ran on the model its call asked for, or else the session\u2019s.';
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
/**
|
|
@@ -85,17 +95,31 @@ export function floorWords(floor) {
|
|
|
85
95
|
* ("\u2265$0.75 \u00b7 2/7"), because a sum of two calls must not be read as the cost of seven. None: a dash.
|
|
86
96
|
*/
|
|
87
97
|
export function costCell(cost, runs) {
|
|
88
|
-
if (!cost) return { cost: '\u2026', costNote: 'Reading what this class cost\u2026', partial: false };
|
|
89
|
-
if (!cost.costed) return { cost: '\u2014', costNote: 'No agent of this class has a transcript on this machine that could be priced', partial: false };
|
|
98
|
+
if (!cost) return { cost: '\u2026', costNote: 'Reading what this class cost\u2026', partial: false, advisor: null };
|
|
99
|
+
if (!cost.costed) return { cost: '\u2014', costNote: 'No agent of this class has a transcript on this machine that could be priced', partial: false, advisor: null };
|
|
90
100
|
const usd = fmtCost(cost.cost);
|
|
91
|
-
|
|
101
|
+
const advisor = advisorWords(cost);
|
|
102
|
+
const tail = advisor ? `\n${advisor}` : '';
|
|
103
|
+
if (cost.costed >= runs) return { cost: usd, costNote: `All ${runs} ${runs === 1 ? 'task' : 'tasks'} priced, at API list price. An estimate: what a subscription is charged is not known here.${tail}`, partial: false, advisor };
|
|
92
104
|
return {
|
|
93
105
|
cost: `\u2265${usd.replace(/^~/, '')} \u00b7 ${cost.costed}/${runs}`,
|
|
94
|
-
costNote: `At least this much: ${cost.costed} of ${runs} tasks could be priced; the others have no transcript on this machine. At API list price
|
|
106
|
+
costNote: `At least this much: ${cost.costed} of ${runs} tasks could be priced; the others have no transcript on this machine. At API list price.${tail}`,
|
|
95
107
|
partial: true,
|
|
108
|
+
advisor,
|
|
96
109
|
};
|
|
97
110
|
}
|
|
98
111
|
|
|
112
|
+
/**
|
|
113
|
+
* What of a class's cost was the advisor (a second model its agents consulted mid-task), or null when they consulted
|
|
114
|
+
* none: no call is no tag, not a zero. Its share is already in the class's cost; this only says how much it was.
|
|
115
|
+
*/
|
|
116
|
+
export function advisorWords(cost) {
|
|
117
|
+
const n = cost?.advisorCalls ?? 0;
|
|
118
|
+
if (!(n > 0)) return null;
|
|
119
|
+
const share = typeof cost.advisorCost === 'number' && cost.advisorCost > 0 ? `${fmtCost(cost.advisorCost)} of it` : 'its cost not priced';
|
|
120
|
+
return `Advisor: ${n} ${n === 1 ? 'call' : 'calls'}, ${share}. A second model the agents consulted, at that model's rates; included above.`;
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
/** One class as the view's row: who and what kind of change, how risky, what ran it, and how it went. */
|
|
100
124
|
export function classRow(c, cost = null) {
|
|
101
125
|
const models = Object.entries(c.models ?? {}).sort((a, b) => b[1].runs - a[1].runs).map(([t, m]) => `${tierWord(t)} ${m.runs}`).join(' \u00b7 ');
|
|
@@ -146,6 +170,36 @@ export function suggestionText(s) {
|
|
|
146
170
|
};
|
|
147
171
|
}
|
|
148
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Normal work that ran above the table's model, in words. The gate holds a floor, not a ceiling, so this is said
|
|
175
|
+
* and nothing is blocked. That the table's model would have done is said only from the record (`a.passed`); the
|
|
176
|
+
* cost beside it is the same tokens at that model's price, which is not what that model would have spent.
|
|
177
|
+
* @param a one class of server/lib/model-outcomes.js `aboveTableOf`
|
|
178
|
+
* @param cost { cost, atNormal, costed, runs } for it, or null while costs have not been read
|
|
179
|
+
*/
|
|
180
|
+
export function aboveText(a, cost = null) {
|
|
181
|
+
const to = tierWord(a.to);
|
|
182
|
+
const ran = Object.entries(a.models ?? {}).sort((x, y) => y[1] - x[1]).map(([t, n]) => `${n} on ${tierWord(t)}`).join(', ');
|
|
183
|
+
let costs = null;
|
|
184
|
+
if (cost?.costed) {
|
|
185
|
+
const some = cost.costed < cost.runs;
|
|
186
|
+
const usd = (v) => (some ? `\u2265${fmtCost(v).replace(/^~/, '')}` : fmtCost(v));
|
|
187
|
+
costs = `${usd(cost.cost)} as run; the same tokens at ${to}'s price ${usd(cost.atNormal)}${some ? ` (${cost.costed} of ${cost.runs} priced)` : ''}. API list price; another model would not spend the same tokens.`;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
title: `${roleName(a.agent)} \u00b7 ${a.change}`,
|
|
191
|
+
says: `normal risk, ${plural(a.runs, 'task')} above ${to}: ${ran}.`,
|
|
192
|
+
verdict: a.passed
|
|
193
|
+
? `${to} is enough for this class: its latest ${a.passed} verified calls on ${to} all passed the first time.`
|
|
194
|
+
: `Whether ${to} would have done is not measured: this class does not have ${a.need ?? 'enough'} verified calls on ${to} that all passed the first time.`,
|
|
195
|
+
measured: Boolean(a.passed),
|
|
196
|
+
costs,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const ABOVE_HEAD = 'Above the table \u00b7 nothing is blocked';
|
|
201
|
+
export const ABOVE_RULE = 'The gate holds a floor, not a ceiling: which model runs normal work is yours to choose.';
|
|
202
|
+
|
|
149
203
|
/** What the view says when there is nothing to list. */
|
|
150
204
|
export function emptyNote(data) {
|
|
151
205
|
if (!data) return 'Reading the record…';
|
package/kit/studio/web/models.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// A class is an agent, a kind of change and a risk. Each line says what ran it, how many tasks, how many of the
|
|
4
4
|
// verified ones passed the first time, how many were repeated one model up, and what the agents' tokens come to.
|
|
5
5
|
// Under the table: the holds the record could let go of. They are shown, not acted on: the one button copies
|
|
6
|
-
// Crewforth's command for it and lowers nothing.
|
|
7
|
-
|
|
6
|
+
// Crewforth's command for it and lowers nothing. Then the normal work that ran above the table's model: said, with
|
|
7
|
+
// the record's evidence where there is some, and not acted on either.
|
|
8
|
+
import { classRow, suggestionText, aboveText, emptyNote, totalsLine, costLine, SOURCE, LOWER_RULE, ABOVE_HEAD, ABOVE_RULE } from './models-plan.js';
|
|
8
9
|
|
|
9
10
|
function mk(tag, cls, text) {
|
|
10
11
|
const n = document.createElement(tag);
|
|
@@ -26,7 +27,7 @@ export class Models {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
setData(data) { this.data = data; this.render(); }
|
|
29
|
-
/** What the classes cost, read apart and less often: { measured, costs: { classKey: { cost, costed, runs } } }. */
|
|
30
|
+
/** What the classes cost, read apart and less often: { measured, costs: { classKey: { cost, costed, runs } }, above }. */
|
|
30
31
|
setCosts(costs) { this.costs = costs; this.render(); }
|
|
31
32
|
|
|
32
33
|
render() {
|
|
@@ -88,6 +89,7 @@ export class Models {
|
|
|
88
89
|
const cost = mk('td', 'mv-num', r.cost);
|
|
89
90
|
cost.title = r.costNote;
|
|
90
91
|
if (r.partial) cost.dataset.partial = 'true';
|
|
92
|
+
if (r.advisor) { const t = mk('span', 'mv-tag', 'advisor'); t.dataset.kind = 'advisor'; t.title = r.advisor; cost.append(t); }
|
|
91
93
|
tr.append(who, risk, model, mk('td', 'mv-num', r.tasks), first, mk('td', 'mv-num', r.escalated), cost);
|
|
92
94
|
tbody.append(tr);
|
|
93
95
|
}
|
|
@@ -115,6 +117,20 @@ export class Models {
|
|
|
115
117
|
}
|
|
116
118
|
pane.append(box);
|
|
117
119
|
}
|
|
120
|
+
if (d.above?.length) {
|
|
121
|
+
const box = mk('section', 'mv-above');
|
|
122
|
+
box.append(mk('h3', 'mv-suggest-h', ABOVE_HEAD));
|
|
123
|
+
for (const a of d.above) {
|
|
124
|
+
const t = aboveText(a, this.costs?.measured ? (this.costs.above?.[a.key] ?? null) : null);
|
|
125
|
+
box.append(mk('p', 'mv-suggest-text', `${t.title} ${t.says}`));
|
|
126
|
+
const v = mk('p', 'mv-above-verdict', t.verdict);
|
|
127
|
+
v.dataset.measured = String(t.measured);
|
|
128
|
+
box.append(v);
|
|
129
|
+
if (t.costs) box.append(mk('p', 'sub', t.costs));
|
|
130
|
+
}
|
|
131
|
+
box.append(mk('p', 'sub', ABOVE_RULE));
|
|
132
|
+
pane.append(box);
|
|
133
|
+
}
|
|
118
134
|
for (const note of raisedNotes) {
|
|
119
135
|
const row = mk('div', 'mv-raised');
|
|
120
136
|
const tag = mk('span', 'mv-tag', note.tag);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// qr.js — a QR code for a viewer's link, made in the page: no dependency, no request.
|
|
2
|
+
//
|
|
3
|
+
// Byte mode, error correction level M (a phone reads it off a laptop screen at an angle), versions 1 to 10: up to
|
|
4
|
+
// 213 bytes, which a Wi-Fi link with a token (about 70) fits in with room to spare. The construction is ISO/IEC
|
|
5
|
+
// 18004's, in the order Project Nayuki's reference implementation lays it out: data and padding, Reed–Solomon error
|
|
6
|
+
// correction per block, interleaving, the function patterns, the codewords in their zigzag, the eight masks scored by
|
|
7
|
+
// the standard's four penalty rules, and the format (and, from version 7, version) bits. Checked against an
|
|
8
|
+
// independent decoder (OpenCV's QRCodeDetector) on every version and mask; the self-check pins the result.
|
|
9
|
+
|
|
10
|
+
const ECC_M = { perBlock: [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26], blocks: [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5], formatBits: 0 };
|
|
11
|
+
const MAX_VERSION = 10;
|
|
12
|
+
|
|
13
|
+
function rawModules(ver) {
|
|
14
|
+
let n = (16 * ver + 128) * ver + 64;
|
|
15
|
+
if (ver >= 2) {
|
|
16
|
+
const align = Math.floor(ver / 7) + 2;
|
|
17
|
+
n -= (25 * align - 10) * align - 55;
|
|
18
|
+
if (ver >= 7) n -= 36;
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
const dataCodewords = (ver) => Math.floor(rawModules(ver) / 8) - ECC_M.perBlock[ver] * ECC_M.blocks[ver];
|
|
23
|
+
|
|
24
|
+
// GF(256) with the QR polynomial x^8 + x^4 + x^3 + x^2 + 1.
|
|
25
|
+
function mul(x, y) {
|
|
26
|
+
let z = 0;
|
|
27
|
+
for (let i = 7; i >= 0; i--) { z = (z << 1) ^ ((z >>> 7) * 0x11d); z ^= ((y >>> i) & 1) * x; }
|
|
28
|
+
return z & 0xff;
|
|
29
|
+
}
|
|
30
|
+
function divisor(degree) {
|
|
31
|
+
const r = new Array(degree).fill(0); r[degree - 1] = 1;
|
|
32
|
+
let root = 1;
|
|
33
|
+
for (let i = 0; i < degree; i++) {
|
|
34
|
+
for (let j = 0; j < r.length; j++) { r[j] = mul(r[j], root); if (j + 1 < r.length) r[j] ^= r[j + 1]; }
|
|
35
|
+
root = mul(root, 0x02);
|
|
36
|
+
}
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
function remainder(data, div) {
|
|
40
|
+
const r = div.map(() => 0);
|
|
41
|
+
for (const b of data) {
|
|
42
|
+
const f = b ^ r.shift(); r.push(0);
|
|
43
|
+
div.forEach((c, i) => { r[i] ^= mul(c, f); });
|
|
44
|
+
}
|
|
45
|
+
return r;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function alignmentPositions(ver) {
|
|
49
|
+
if (ver === 1) return [];
|
|
50
|
+
const n = Math.floor(ver / 7) + 2;
|
|
51
|
+
const size = ver * 4 + 17;
|
|
52
|
+
const step = Math.ceil((ver * 4 + 4) / (n * 2 - 2)) * 2;
|
|
53
|
+
const out = [6];
|
|
54
|
+
for (let pos = size - 7; out.length < n; pos -= step) out.splice(1, 0, pos);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The bytes of `text` (UTF-8) as the codewords of the smallest version that holds them, error correction included. */
|
|
59
|
+
function codewords(bytes) {
|
|
60
|
+
let ver = 1;
|
|
61
|
+
for (; ver <= MAX_VERSION; ver++) {
|
|
62
|
+
const cap = dataCodewords(ver) * 8;
|
|
63
|
+
if (4 + (ver < 10 ? 8 : 16) + bytes.length * 8 <= cap) break;
|
|
64
|
+
}
|
|
65
|
+
if (ver > MAX_VERSION) throw new Error(`qr: ${bytes.length} bytes do not fit version ${MAX_VERSION}`);
|
|
66
|
+
const bits = [];
|
|
67
|
+
const put = (v, n) => { for (let i = n - 1; i >= 0; i--) bits.push((v >>> i) & 1); };
|
|
68
|
+
put(0b0100, 4); put(bytes.length, ver < 10 ? 8 : 16);
|
|
69
|
+
for (const b of bytes) put(b, 8);
|
|
70
|
+
const cap = dataCodewords(ver) * 8;
|
|
71
|
+
put(0, Math.min(4, cap - bits.length));
|
|
72
|
+
put(0, (8 - (bits.length % 8)) % 8);
|
|
73
|
+
for (let pad = 0xec; bits.length < cap; pad ^= 0xec ^ 0x11) put(pad, 8);
|
|
74
|
+
const data = [];
|
|
75
|
+
for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0));
|
|
76
|
+
|
|
77
|
+
const nBlocks = ECC_M.blocks[ver]; const eccLen = ECC_M.perBlock[ver];
|
|
78
|
+
const raw = Math.floor(rawModules(ver) / 8);
|
|
79
|
+
const nShort = nBlocks - (raw % nBlocks); const shortLen = Math.floor(raw / nBlocks);
|
|
80
|
+
const div = divisor(eccLen);
|
|
81
|
+
const blocks = [];
|
|
82
|
+
for (let i = 0, k = 0; i < nBlocks; i++) {
|
|
83
|
+
const d = data.slice(k, k + shortLen - eccLen + (i < nShort ? 0 : 1)); k += d.length;
|
|
84
|
+
const e = remainder(d, div);
|
|
85
|
+
if (i < nShort) d.push(0); // a placeholder, skipped when interleaving
|
|
86
|
+
blocks.push(d.concat(e));
|
|
87
|
+
}
|
|
88
|
+
const out = [];
|
|
89
|
+
for (let i = 0; i < blocks[0].length; i++) {
|
|
90
|
+
blocks.forEach((b, j) => { if (i !== shortLen - eccLen || j >= nShort) out.push(b[i]); });
|
|
91
|
+
}
|
|
92
|
+
return { ver, out };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MASKS = [
|
|
96
|
+
(x, y) => (x + y) % 2 === 0, (x, y) => y % 2 === 0, (x) => x % 3 === 0, (x, y) => (x + y) % 3 === 0,
|
|
97
|
+
(x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
|
|
98
|
+
(x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The QR code for `text`: { size, modules } where modules[y][x] is true for a dark module, quiet zone not included.
|
|
103
|
+
* `mask` forces one of the eight masks (the self-check reads every one); otherwise the one the penalty rules pick.
|
|
104
|
+
*/
|
|
105
|
+
export function qr(text, { mask = null } = {}) {
|
|
106
|
+
const bytes = [...new TextEncoder().encode(String(text))];
|
|
107
|
+
const { ver, out } = codewords(bytes);
|
|
108
|
+
const size = ver * 4 + 17;
|
|
109
|
+
const m = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
110
|
+
const fn = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
111
|
+
const set = (x, y, dark) => { m[y][x] = dark; fn[y][x] = true; };
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < size; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
|
114
|
+
for (const [cx, cy] of [[3, 3], [size - 4, 3], [3, size - 4]]) {
|
|
115
|
+
for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) {
|
|
116
|
+
const x = cx + dx; const y = cy + dy;
|
|
117
|
+
if (x >= 0 && x < size && y >= 0 && y < size) { const d = Math.max(Math.abs(dx), Math.abs(dy)); set(x, y, d !== 2 && d !== 4); }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const al = alignmentPositions(ver);
|
|
121
|
+
al.forEach((ax, i) => al.forEach((ay, j) => {
|
|
122
|
+
if ((i === 0 && j === 0) || (i === 0 && j === al.length - 1) || (i === al.length - 1 && j === 0)) return;
|
|
123
|
+
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(ax + dx, ay + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
124
|
+
}));
|
|
125
|
+
const format = (mk) => {
|
|
126
|
+
const d = (ECC_M.formatBits << 3) | mk;
|
|
127
|
+
let r = d; for (let i = 0; i < 10; i++) r = (r << 1) ^ ((r >>> 9) * 0x537);
|
|
128
|
+
const b = ((d << 10) | r) ^ 0x5412; const bit = (i) => ((b >>> i) & 1) === 1;
|
|
129
|
+
for (let i = 0; i <= 5; i++) set(8, i, bit(i));
|
|
130
|
+
set(8, 7, bit(6)); set(8, 8, bit(7)); set(7, 8, bit(8));
|
|
131
|
+
for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i));
|
|
132
|
+
for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(i));
|
|
133
|
+
for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(i));
|
|
134
|
+
set(8, size - 8, true);
|
|
135
|
+
};
|
|
136
|
+
format(0); // reserves the format area before the data is drawn
|
|
137
|
+
if (ver >= 7) {
|
|
138
|
+
let r = ver; for (let i = 0; i < 12; i++) r = (r << 1) ^ ((r >>> 11) * 0x1f25);
|
|
139
|
+
const b = (ver << 12) | r;
|
|
140
|
+
for (let i = 0; i < 18; i++) { const dark = ((b >>> i) & 1) === 1; const a = size - 11 + (i % 3); const c = Math.floor(i / 3); set(a, c, dark); set(c, a, dark); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let i = 0;
|
|
144
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
145
|
+
if (right === 6) right = 5;
|
|
146
|
+
for (let v = 0; v < size; v++) for (let j = 0; j < 2; j++) {
|
|
147
|
+
const x = right - j; const up = ((right + 1) & 2) === 0; const y = up ? size - 1 - v : v;
|
|
148
|
+
if (!fn[y][x] && i < out.length * 8) { m[y][x] = ((out[i >>> 3] >>> (7 - (i & 7))) & 1) === 1; i++; }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const apply = (mk) => { for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (!fn[y][x] && MASKS[mk](x, y)) m[y][x] = !m[y][x]; };
|
|
153
|
+
let chosen = mask;
|
|
154
|
+
if (chosen === null) {
|
|
155
|
+
let best = Infinity;
|
|
156
|
+
for (let mk = 0; mk < 8; mk++) {
|
|
157
|
+
apply(mk); format(mk);
|
|
158
|
+
const p = penalty(m, size);
|
|
159
|
+
if (p < best) { best = p; chosen = mk; }
|
|
160
|
+
apply(mk);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
apply(chosen); format(chosen);
|
|
164
|
+
return { size, version: ver, mask: chosen, modules: m };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// The standard's four penalty rules: runs of five or more, 2x2 blocks, finder-like patterns, dark/light balance.
|
|
168
|
+
function penalty(m, size) {
|
|
169
|
+
let p = 0;
|
|
170
|
+
const line = (get) => {
|
|
171
|
+
let run = 0; let prev = null; const hist = [];
|
|
172
|
+
for (let i = 0; i < size; i++) {
|
|
173
|
+
const c = get(i);
|
|
174
|
+
if (c === prev) { run++; if (run === 5) p += 3; else if (run > 5) p += 1; } else { run = 1; prev = c; }
|
|
175
|
+
hist.push(c ? 1 : 0);
|
|
176
|
+
}
|
|
177
|
+
const s = hist.join('');
|
|
178
|
+
for (const pat of ['10111010000', '00001011101']) { let k = s.indexOf(pat); while (k !== -1) { p += 40; k = s.indexOf(pat, k + 1); } }
|
|
179
|
+
};
|
|
180
|
+
for (let y = 0; y < size; y++) line((x) => m[y][x]);
|
|
181
|
+
for (let x = 0; x < size; x++) line((y) => m[y][x]);
|
|
182
|
+
let dark = 0;
|
|
183
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
184
|
+
if (m[y][x]) dark++;
|
|
185
|
+
if (x < size - 1 && y < size - 1 && m[y][x] === m[y][x + 1] && m[y][x] === m[y + 1][x] && m[y][x] === m[y + 1][x + 1]) p += 3;
|
|
186
|
+
}
|
|
187
|
+
const total = size * size;
|
|
188
|
+
p += Math.floor(Math.abs(dark * 20 - total * 10) / total) * 10;
|
|
189
|
+
return p;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The code as an SVG string: one path and a quiet zone of four modules. Its two colours are tokens.css's
|
|
193
|
+
* --qr-dark and --qr-light, the same in both themes: a camera reads contrast, not the palette. */
|
|
194
|
+
export function qrSvg(text, { px = 4 } = {}) {
|
|
195
|
+
const { size, modules } = qr(text);
|
|
196
|
+
const q = 4; const n = size + q * 2;
|
|
197
|
+
let d = '';
|
|
198
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (modules[y][x]) d += `M${x + q} ${y + q}h1v1h-1z`;
|
|
199
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n} ${n}" width="${n * px}" height="${n * px}" shape-rendering="crispEdges" role="img" aria-label="QR code of the viewer link"><rect class="qr-light" width="${n}" height="${n}"/><path class="qr-dark" d="${d}"/></svg>`;
|
|
200
|
+
}
|
package/kit/studio/web/style.css
CHANGED
|
@@ -402,6 +402,37 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
402
402
|
}
|
|
403
403
|
.nav-foot-label:hover { color: var(--ink); }
|
|
404
404
|
|
|
405
|
+
/* Remote viewers, under the machines: who holds a read-only link, and the way to take it back. */
|
|
406
|
+
.rv { margin-top: var(--space-3); }
|
|
407
|
+
.rv-head { display: flex; align-items: center; gap: var(--space-2); padding: 0 var(--space-1); }
|
|
408
|
+
.rv-title {
|
|
409
|
+
font-size: 11px; line-height: var(--type-label-line); font-weight: var(--type-label-weight);
|
|
410
|
+
letter-spacing: var(--type-label-tracking); text-transform: uppercase; color: var(--ink-dim);
|
|
411
|
+
}
|
|
412
|
+
.rv-say { white-space: normal; padding: 0 var(--space-1); }
|
|
413
|
+
.rv-give {
|
|
414
|
+
display: flex; flex-direction: column; gap: var(--space-2);
|
|
415
|
+
padding: var(--space-2); border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-raised);
|
|
416
|
+
}
|
|
417
|
+
.rv-give .rv-say { padding: 0; }
|
|
418
|
+
.rv-url { font-size: 12px; line-height: 16px; overflow-wrap: anywhere; user-select: all; color: var(--ink); }
|
|
419
|
+
.rv-acts { display: flex; gap: var(--space-2); }
|
|
420
|
+
/* The QR code is dark on white whatever the theme: a phone's camera reads contrast, not the palette. */
|
|
421
|
+
.rv-qr { align-self: center; line-height: 0; border-radius: var(--radius-md); overflow: hidden; }
|
|
422
|
+
.rv-qr svg { display: block; width: 168px; height: auto; max-width: 100%; }
|
|
423
|
+
.rv-qr .qr-light { fill: var(--qr-light); }
|
|
424
|
+
.rv-qr .qr-dark { fill: var(--qr-dark); }
|
|
425
|
+
.rv-opt { display: flex; align-items: center; gap: var(--space-1); padding: var(--space-1); font-size: 12px; color: var(--ink-dim); }
|
|
426
|
+
/* A viewer link past its eight hours: the page is covered, with what happened and what to do. */
|
|
427
|
+
.expired {
|
|
428
|
+
position: fixed; inset: 0; z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
429
|
+
gap: var(--space-2); padding: var(--space-4); text-align: center; background: var(--surface); color: var(--ink);
|
|
430
|
+
}
|
|
431
|
+
.expired h1 { margin: 0; font-size: 20px; }
|
|
432
|
+
.expired p { margin: 0; max-width: 34ch; color: var(--ink-dim); }
|
|
433
|
+
/* A viewer's page offers no way to start a session: the server refuses it, and a button that only fails is noise. */
|
|
434
|
+
body[data-role="viewer"] #new-session { display: none; }
|
|
435
|
+
|
|
405
436
|
/* ---- menu and toast ---- */
|
|
406
437
|
.menu {
|
|
407
438
|
position: fixed; z-index: 60; min-width: 190px; max-width: 272px;
|
|
@@ -525,6 +556,12 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
525
556
|
border-bottom-color: color-mix(in srgb, var(--status-fail) 45%, var(--line));
|
|
526
557
|
}
|
|
527
558
|
.stage-note-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
559
|
+
/* A server running older code than the page: the whole sentence is the instruction, so it wraps, not ellipsizes. */
|
|
560
|
+
.stage-note[data-kind="restart"] {
|
|
561
|
+
color: var(--ink); background: color-mix(in srgb, var(--status-waiting) 12%, var(--surface));
|
|
562
|
+
border-bottom-color: color-mix(in srgb, var(--status-waiting) 45%, var(--line));
|
|
563
|
+
}
|
|
564
|
+
.stage-note[data-kind="restart"] .stage-note-text { white-space: normal; }
|
|
528
565
|
/* The last picture stays while the server is away, and is drawn as what it is: old. */
|
|
529
566
|
.stage[data-offline="true"] .canvas,
|
|
530
567
|
.stage[data-offline="true"] .tl,
|
|
@@ -553,6 +590,126 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
553
590
|
.mv { flex: 1; min-height: 0; min-width: 0; overflow: auto; background: var(--ground); padding: var(--space-4); }
|
|
554
591
|
.mv[hidden] { display: none; }
|
|
555
592
|
.mv-empty { padding: var(--space-6); color: var(--ink-dim); font-size: 13px; }
|
|
593
|
+
|
|
594
|
+
/* The Map view: the project as its crew map shows it. Bands, cards, lines, the findings, the inspector. */
|
|
595
|
+
.mp { flex: 1; min-height: 0; min-width: 0; overflow: auto; background: var(--ground); padding: var(--space-4); }
|
|
596
|
+
.mp[hidden] { display: none; }
|
|
597
|
+
.mp .sub { white-space: normal; flex: initial; min-width: 0; }
|
|
598
|
+
.mp .lbl { font-size: 11px; line-height: 16px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-dim); }
|
|
599
|
+
.mp-empty { padding: var(--space-6); color: var(--ink-dim); font-size: 13px; display: flex; flex-direction: column; gap: 6px; }
|
|
600
|
+
.mp-empty p { margin: 0; }
|
|
601
|
+
.mp-pane { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; display: flex; flex-direction: column; min-height: 100%; }
|
|
602
|
+
.mp-head, .mp-toolbar, .mp-source { display: flex; align-items: center; gap: var(--space-2); padding: 10px 18px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
603
|
+
.mp-title { margin: 0; font-size: 15px; font-weight: 700; }
|
|
604
|
+
.mp-pill { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: var(--radius-sm); background: var(--surface-raised); font-size: 12px; font-weight: 600; color: var(--ink); white-space: nowrap; }
|
|
605
|
+
.mp-pill[data-tone="behind"], .mp-pill[data-tone="missing"] { color: var(--status-bad); background: color-mix(in srgb, var(--status-bad) 14%, transparent); }
|
|
606
|
+
.mp-tog { height: 28px; padding: 0 10px; border-radius: var(--radius-pill); border: 1px solid var(--line); background: var(--surface); color: var(--ink-dim); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
|
|
607
|
+
.mp-tog[aria-pressed="true"] { background: var(--surface-raised); color: var(--ink); }
|
|
608
|
+
.mp-compare { height: 30px; border-radius: var(--radius-md); border: 1px solid var(--line); background: var(--surface); color: var(--ink); font: inherit; font-size: 12px; padding: 0 8px; max-width: 260px; }
|
|
609
|
+
.mp-legend { position: relative; }
|
|
610
|
+
.mp-legend-box { position: absolute; right: 0; top: 34px; z-index: 5; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-raised); min-width: 220px; }
|
|
611
|
+
.mp-legend-row { display: flex; align-items: center; gap: 8px; }
|
|
612
|
+
.mp-legend-line { width: 28px; height: 0; border-top: 2px solid var(--ink-faint); flex: none; }
|
|
613
|
+
.mp-legend-line[data-kind="inferred"] { border-top-style: dotted; }
|
|
614
|
+
.mp-legend-line[data-kind="model"] { border-top-style: dashed; border-top-color: var(--studio-accent); opacity: 0.7; }
|
|
615
|
+
.mp-legend-line[data-kind="lit"] { border-top-color: var(--studio-accent); }
|
|
616
|
+
.mp-legend-card { width: 22px; height: 14px; border: 1px solid var(--ink-faint); border-radius: 4px; flex: none; }
|
|
617
|
+
.mp-legend-card[data-kind="dashed"] { border-style: dashed; }
|
|
618
|
+
.mp-file { font-family: var(--font-mono); font-size: 12px; color: var(--ink); }
|
|
619
|
+
.mp-body { display: flex; min-height: 0; flex: 1; }
|
|
620
|
+
.mp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
621
|
+
/* The pane pans and zooms (viewport.js): the layer is moved by one transform, so nothing inside restyles. */
|
|
622
|
+
.mp-canvas-pane { position: relative; flex: 1; min-height: 380px; overflow: hidden; cursor: grab; background-color: var(--ground); background-image: radial-gradient(var(--line) 1px, transparent 1.2px); background-size: 20px 20px; touch-action: none; }
|
|
623
|
+
.mp-canvas-pane.cv-panning { cursor: grabbing; }
|
|
624
|
+
.mp-canvas { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
|
|
625
|
+
.mp-band { position: absolute; left: 0; border-bottom: 1px solid var(--line); padding: 12px 16px; box-sizing: border-box; }
|
|
626
|
+
.mp-group { position: absolute; border: 1px dashed var(--line); border-radius: var(--radius-md); box-sizing: border-box; }
|
|
627
|
+
.mp-group-fold { position: absolute; left: 8px; top: 3px; border: 0; background: none; padding: 0; font: inherit; font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim); cursor: pointer; }
|
|
628
|
+
.mp-group-fold:hover { color: var(--ink); }
|
|
629
|
+
.mp-lines { position: absolute; left: 0; top: 0; pointer-events: none; }
|
|
630
|
+
.mp-line { fill: none; stroke: var(--ink-faint); stroke-width: 1.5; }
|
|
631
|
+
.mp-line[data-line="inferred"] { stroke-dasharray: 2 4; }
|
|
632
|
+
.mp-line[data-line="model"] { stroke-dasharray: 7 4; stroke: var(--studio-accent); opacity: 0.7; }
|
|
633
|
+
.mp-line[data-lit="false"] { opacity: 0.35; }
|
|
634
|
+
.mp-line[data-lit="true"] { stroke: var(--studio-accent); stroke-width: 2.2; }
|
|
635
|
+
.mp-elab { position: absolute; transform: translate(-50%, -50%); display: flex; gap: 4px; z-index: 2; }
|
|
636
|
+
.mp-mod, .mp-ext { position: absolute; box-sizing: border-box; display: flex; flex-direction: column; gap: 4px; text-align: left; font: inherit; color: var(--ink); cursor: pointer; border: 1px solid var(--line); border-radius: var(--radius-md); z-index: 1; overflow: hidden; }
|
|
637
|
+
.mp-mod { background: var(--surface); padding: 10px 14px; }
|
|
638
|
+
.mp-mod[data-kind="client"] { border-style: dashed; }
|
|
639
|
+
.mp-mod[data-kind="group"] { background: var(--surface-raised); }
|
|
640
|
+
.mp-ext[data-inferred="true"] { border-style: dashed; }
|
|
641
|
+
.mp-card.cv-dragging { cursor: grabbing; z-index: 3; }
|
|
642
|
+
.mp-ext { background: var(--ground); padding: 6px 10px; justify-content: center; }
|
|
643
|
+
.mp-card:focus-visible { outline: 2px solid var(--studio-accent); outline-offset: 2px; }
|
|
644
|
+
.mp-mod[data-selected="true"], .mp-ext[data-selected="true"] { border-color: var(--studio-accent); box-shadow: 0 0 0 1px var(--studio-accent); }
|
|
645
|
+
.mp-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
646
|
+
.mp-name { font-family: var(--font-mono); font-size: 14px; line-height: 20px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
647
|
+
.mp-ename { font-family: var(--font-mono); font-size: 13px; line-height: 18px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
648
|
+
.mp-kind { font-size: 10px; line-height: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-dim); flex: none; }
|
|
649
|
+
.mp-fb { min-width: 20px; height: 20px; padding: 0 6px; box-sizing: border-box; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--status-bad) 18%, transparent); color: var(--status-bad); font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
|
650
|
+
.mp-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
651
|
+
.mp-chip { font-family: var(--font-mono); font-size: 11px; height: 22px; padding: 0 8px; border-radius: var(--radius-sm); background: var(--surface-raised); display: inline-flex; align-items: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
652
|
+
.mp-findings { border-top: 1px solid var(--line); padding: 10px 14px; display: flex; flex-direction: column; gap: 4px; }
|
|
653
|
+
.mp-flist { max-height: 132px; overflow: auto; display: flex; flex-direction: column; }
|
|
654
|
+
.mp-frow { display: flex; align-items: center; gap: 10px; min-height: 28px; padding: 0 8px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
|
|
655
|
+
.mp-frow:hover { background: var(--surface-raised); }
|
|
656
|
+
.mp-dot { width: 8px; height: 8px; border-radius: 999px; flex: none; box-sizing: border-box; border: 1.5px solid var(--status-bad); }
|
|
657
|
+
.mp-dot[data-proven="true"] { background: var(--status-bad); }
|
|
658
|
+
.mp-ftitle { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 1 auto; }
|
|
659
|
+
.mp-fcard .mp-ftitle { white-space: normal; }
|
|
660
|
+
.mp-fwhere { font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex: 0 2 auto; }
|
|
661
|
+
.mp-badge { font-family: var(--font-mono); font-size: 11px; height: 20px; padding: 0 6px; border-radius: var(--radius-pill); border: 1px solid var(--line); color: var(--ink-dim); display: inline-flex; align-items: center; flex: none; }
|
|
662
|
+
.mp-inspect { width: 340px; flex: none; border-left: 1px solid var(--line); padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; overflow: auto; }
|
|
663
|
+
.mp-inspect p { margin: 0; font-size: 13px; line-height: 19px; }
|
|
664
|
+
.mp-tabs { display: flex; gap: 16px; border-bottom: 1px solid var(--line); }
|
|
665
|
+
.mp-tab { font: inherit; font-size: 13px; font-weight: 600; color: var(--ink-dim); background: none; border: 0; border-bottom: 2px solid transparent; padding: 8px 0; cursor: pointer; }
|
|
666
|
+
.mp-tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); }
|
|
667
|
+
.mp-isec { display: flex; flex-direction: column; gap: 8px; }
|
|
668
|
+
.mp-overview { white-space: pre-wrap; margin: 4px 0 10px; line-height: 1.5; }
|
|
669
|
+
.mp-cites { font-family: var(--font-mono); font-size: 11px; overflow-wrap: anywhere; }
|
|
670
|
+
.mp-conn { display: flex; flex-direction: column; gap: 2px; padding: 6px 0; border-bottom: 1px solid var(--line); }
|
|
671
|
+
.mp-ev { font-family: var(--font-mono); font-size: 11px; color: var(--ink-dim); }
|
|
672
|
+
.mp-fcard { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; padding: 10px; border: 1px solid var(--line); border-radius: var(--radius-md); }
|
|
673
|
+
.mp-fcard[data-proven="false"] { border-style: dashed; }
|
|
674
|
+
.mp-notmeasured { margin-top: auto; padding-top: 10px; border-top: 1px solid var(--line); display: flex; flex-direction: column; gap: 4px; }
|
|
675
|
+
/* The navigator's Map entry under a project, and its count of findings (spec §11). */
|
|
676
|
+
.map-row .map-word { font-size: 13px; font-weight: 600; }
|
|
677
|
+
.map-count { min-width: 20px; height: 20px; padding: 0 6px; box-sizing: border-box; border-radius: var(--radius-pill); background: color-mix(in srgb, var(--status-bad) 18%, transparent); color: var(--status-bad); font-size: 11px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; flex: none; }
|
|
678
|
+
.mp-cite { border: 0; background: none; padding: 0; text-align: left; cursor: pointer; }
|
|
679
|
+
.mp-cite:hover { color: var(--ink); text-decoration: underline; }
|
|
680
|
+
.mp-pill[data-tone="model"] { color: var(--studio-accent); background: color-mix(in srgb, var(--studio-accent) 14%, transparent); }
|
|
681
|
+
/* A phone shows the findings and a list of the modules in place of the pane (spec §11). */
|
|
682
|
+
/* CM3: a module card names itself in full (the kind line gives way), carries what it is built with, and folds the
|
|
683
|
+
outside services it calls into one line under it. A search dims what it does not name; the tour sits over the
|
|
684
|
+
bottom of the pane. */
|
|
685
|
+
.mp-mod { gap: 6px; }
|
|
686
|
+
.mp-mod .mp-row .mp-name { flex: none; overflow: visible; }
|
|
687
|
+
.mp-mod .mp-row .mp-kind { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
688
|
+
.mp-fold { font-family: var(--font-mono); font-size: 12px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
689
|
+
.mp-fold.none { color: var(--ink-faint); }
|
|
690
|
+
.mp-card[data-match="false"] { opacity: 0.3; }
|
|
691
|
+
.mp-card[data-match="true"] { border-color: var(--studio-accent); }
|
|
692
|
+
.mp-search { height: 30px; min-width: 200px; padding: 0 10px; border-radius: var(--radius-md); border: 1px solid var(--line); background: var(--surface); color: var(--ink); font: inherit; font-size: 12px; }
|
|
693
|
+
.mp-search::placeholder { color: var(--ink-dim); }
|
|
694
|
+
.mp-tour { position: absolute; left: 16px; right: 16px; bottom: 14px; z-index: 6; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--studio-accent); border-radius: var(--radius-md); background: var(--surface-raised); cursor: default; }
|
|
695
|
+
.mp-tour p { margin: 0; font-size: 14px; line-height: 21px; }
|
|
696
|
+
.mp-tour[hidden] { display: none; }
|
|
697
|
+
.mp-inside { flex-direction: row; align-items: center; gap: 10px; }
|
|
698
|
+
.mp-inside .mp-ev { margin-left: auto; }
|
|
699
|
+
.mp-inside .mp-cite, .mp-inside .mp-file { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
700
|
+
.mp-inside .mp-ev:last-child { flex: none; }
|
|
701
|
+
.mp-modlist { display: none; flex-direction: column; gap: 2px; padding: 10px 14px; border-top: 1px solid var(--line); }
|
|
702
|
+
.mp-mrow { display: flex; align-items: center; gap: 8px; min-height: 36px; padding: 0 8px; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--ink); font: inherit; text-align: left; cursor: pointer; }
|
|
703
|
+
.mp-mrow:hover { background: var(--surface-raised); }
|
|
704
|
+
@media (max-width: 1023px) {
|
|
705
|
+
.mp-body { flex-direction: column; }
|
|
706
|
+
.mp-inspect { width: auto; border-left: 0; border-top: 1px solid var(--line); }
|
|
707
|
+
}
|
|
708
|
+
@media (max-width: 639px) {
|
|
709
|
+
.mp-canvas-pane, .mp-hint { display: none; }
|
|
710
|
+
.mp-modlist { display: flex; }
|
|
711
|
+
.mp-flist { max-height: none; }
|
|
712
|
+
}
|
|
556
713
|
.mv-pane { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
|
|
557
714
|
.mv-head { display: flex; align-items: center; gap: var(--space-2); padding: 14px 18px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
558
715
|
.mv-title { margin: 0; font-size: 15px; font-weight: 700; }
|
|
@@ -571,6 +728,7 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
571
728
|
.mv-raised .mv-tag[data-kind="lowered"] { margin-left: 0; }
|
|
572
729
|
.mv-tag[data-tone="pass"] { color: var(--status-good); background: color-mix(in srgb, var(--status-good) 14%, transparent); }
|
|
573
730
|
.mv-tag[data-tone="fail"] { color: var(--status-fail); background: color-mix(in srgb, var(--status-fail) 14%, transparent); }
|
|
731
|
+
.mv-tag[data-kind="advisor"] { margin-left: 6px; color: var(--studio-accent); background: color-mix(in srgb, var(--studio-accent) 16%, transparent); }
|
|
574
732
|
.mv-tag[data-kind="escalated"] { color: var(--studio-accent); background: color-mix(in srgb, var(--studio-accent) 16%, transparent); }
|
|
575
733
|
.mv-table td[data-partial="true"] { color: var(--ink-dim); }
|
|
576
734
|
.mv-meter { display: inline-block; width: 72px; height: 6px; margin-left: 8px; border-radius: 999px; background: var(--surface-raised); overflow: hidden; vertical-align: middle; }
|
|
@@ -581,6 +739,11 @@ mark { background: color-mix(in srgb, var(--studio-accent) 30%, transparent); co
|
|
|
581
739
|
.mv-suggest-h { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--studio-accent); }
|
|
582
740
|
.mv-suggest-text { margin: 0; font-size: 14px; line-height: 21px; }
|
|
583
741
|
.mv-suggest-row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
|
|
742
|
+
.mv-above { margin: 16px 18px; padding: 14px 16px; display: flex; flex-direction: column; gap: 6px; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface-raised); }
|
|
743
|
+
.mv-above .mv-suggest-h { color: var(--ink-dim); }
|
|
744
|
+
.mv-above p { margin: 0; }
|
|
745
|
+
.mv-above-verdict { font-size: 13px; line-height: 19px; }
|
|
746
|
+
.mv-above-verdict[data-measured="false"] { color: var(--ink-dim); }
|
|
584
747
|
.mv-cmd { padding: 2px 6px; border-radius: var(--radius-sm); background: var(--surface-raised); font-size: 12px; user-select: all; }
|
|
585
748
|
.mv-raised { display: flex; align-items: baseline; gap: var(--space-2); padding: 0 18px 16px; }
|
|
586
749
|
.mv-raised .mv-tag { margin-left: 0; flex: none; }
|
|
@@ -1079,6 +1242,8 @@ button.tl-lab { cursor: pointer; }
|
|
|
1079
1242
|
and do not shrink. */
|
|
1080
1243
|
.cv-sub { font-size: 12px; line-height: 16px; color: var(--ink-dim); white-space: nowrap; flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
1081
1244
|
.cv-sub[hidden] { display: none; }
|
|
1245
|
+
/* Agent calls the model gate refused: no agent started, so this line is the only trace on the canvas. */
|
|
1246
|
+
.cv-refused { color: var(--status-waiting); }
|
|
1082
1247
|
.cv-node > .cv-row, .cv-node > .cv-under, .cv-node > .cv-name, .cv-node > .cv-sub { max-width: 100%; }
|
|
1083
1248
|
.cv-word { font-size: 11px; color: var(--ink-dim); white-space: nowrap; flex: none; }
|
|
1084
1249
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
--ink: #e6e9f0;
|
|
11
11
|
--ink-dim: #9aa7cc;
|
|
12
12
|
--ink-faint: #5c6472;
|
|
13
|
+
--qr-dark: #000000;
|
|
14
|
+
--qr-light: #ffffff;
|
|
13
15
|
--accent: #A78BFA;
|
|
14
16
|
--on-accent: #0B1020;
|
|
15
17
|
--wordmark-ink: #FFFFFF;
|
|
@@ -86,6 +88,8 @@
|
|
|
86
88
|
--ink: #1a1d24;
|
|
87
89
|
--ink-dim: #5c6472;
|
|
88
90
|
--ink-faint: #8d95a3;
|
|
91
|
+
--qr-dark: #000000;
|
|
92
|
+
--qr-light: #ffffff;
|
|
89
93
|
--accent: #6D28D9;
|
|
90
94
|
--on-accent: #ffffff;
|
|
91
95
|
--wordmark-ink: #0F172A;
|
|
@@ -113,6 +117,8 @@
|
|
|
113
117
|
--ink: #1a1d24;
|
|
114
118
|
--ink-dim: #5c6472;
|
|
115
119
|
--ink-faint: #8d95a3;
|
|
120
|
+
--qr-dark: #000000;
|
|
121
|
+
--qr-light: #ffffff;
|
|
116
122
|
--accent: #6D28D9;
|
|
117
123
|
--on-accent: #ffffff;
|
|
118
124
|
--wordmark-ink: #0F172A;
|
package/kit/studio/web/usage.js
CHANGED
|
@@ -111,6 +111,18 @@ export function summaryRows(u, { live = false, now = null } = {}) {
|
|
|
111
111
|
};
|
|
112
112
|
part('Session', u.parts?.session, 'The session\'s own transcript');
|
|
113
113
|
part('Agents', u.parts?.agents, 'Everything its agents did');
|
|
114
|
+
// The advisor: a second model the main one consulted mid-response. Its tokens are in the rows above already; this
|
|
115
|
+
// says how much of them it was. No call recorded, no row: a session that never consulted it shows no zero.
|
|
116
|
+
if (u.advisor?.calls) {
|
|
117
|
+
const a = u.advisor;
|
|
118
|
+
const who = Object.keys(u.advisorModels ?? {}).map(modelName).join(', ');
|
|
119
|
+
rows.push({
|
|
120
|
+
label: 'Advisor',
|
|
121
|
+
value: `${a.calls} ${a.calls === 1 ? 'call' : 'calls'} \u00b7 ${fmtCount(a.tokens.fresh)} \u00b7 ${fmtCost(a.cost)}`,
|
|
122
|
+
note: `Advice the main model asked a second model for${who ? ` (${who})` : ''}, priced at that model's rates. Included in the totals above; the API's own token count leaves it out\n${costNote(a)}`,
|
|
123
|
+
detail: kinds(a.tokens),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
114
126
|
// Behind the cost: each model's new tokens and what they come to. A model with no price says so with a dash.
|
|
115
127
|
const perModel = Object.entries(u.byModel ?? {}).map(([model, t]) => [modelName(model), `${fmtCount(t.tokens.fresh)} \u00b7 ${fmtCost(t.cost)}`]);
|
|
116
128
|
rows.push({ label: 'Cost', value: k.total ? fmtCost(u.cost) : NO_COST, note: costNote(u), detail: perModel.length ? perModel : undefined });
|