create-pmos 0.528.0 → 0.529.0
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/package.json +26 -14
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/briefing-premium-calendar-3-test.js +11 -9
- package/workspace/briefing-premium-calendar-test.js +1 -1
- package/workspace/changelog.json +25 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +4 -2
- package/workspace/guide/idle-lab.md +1 -1
- package/workspace/guide/notes.md +19 -7
- package/workspace/guide/reports-set-up.md +3 -1
- package/workspace/lib/bg-queue.js +5 -1
- package/workspace/lib/meeting-replay.js +11 -2
- package/workspace/lib/meeting-suggest.js +105 -0
- package/workspace/lib/meeting-terms.js +57 -17
- package/workspace/lib/report-chat.js +3 -3
- package/workspace/lib/routes-lab.js +44 -2
- package/workspace/lib/routes-meeting-points.js +27 -3
- package/workspace/lib/routes-report-flow.js +35 -5
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-vw-meet-test.js +17 -11
- package/workspace/public/pm/cozy-lab-ins.js +13 -4
- package/workspace/public/pm/cozy-vw-mtg.css +122 -24
- package/workspace/public/pm/premium-meeting.js +2 -1
- package/workspace/public/pm/premium-mtg-replay.js +149 -73
- package/workspace/public/pm/premium-report-tab.js +33 -4
- package/workspace/public/pm/premium-vw-mtg.js +176 -53
- package/workspace/public/pm/report-chat.js +1 -0
- package/workspace/public/pm/report-flow.js +7 -3
|
@@ -35,19 +35,61 @@ function titles(w) {
|
|
|
35
35
|
try { const b = require('./routes-boards.js').baseRows(w); if (b && b.ok) for (const r of b.rows || []) if (r && r.id) m.set(r.id, { title: r.title || r.id, board: r.board || '' }); } catch { /* none */ }
|
|
36
36
|
return m;
|
|
37
37
|
}
|
|
38
|
+
// Why a grill run started, in plain words (lib/grill-runs.js why), for "Checked <initiative> for risks · <why> · <what it found>"
|
|
39
|
+
// (Wawan, 2026-11-04: "I don't understand" "Grill · … why: monthly").
|
|
40
|
+
const GRILL_WHY = { monthly: 'monthly check', ask: 'you asked', exit: 'it passed a stage', forecast: 'its forecast moved', move: 'its tasks or end date moved', plan: 'its PRD changed' };
|
|
41
|
+
const plural = (n, one, many) => n + ' ' + (n === 1 ? one : many);
|
|
42
|
+
/** A grill run's findings: filed by the run (proposals.js propose lines carry the run), and the ones still waiting for the PM. */
|
|
43
|
+
function grillFound(P, run) {
|
|
44
|
+
let filed = 0, waiting = 0;
|
|
45
|
+
if (!P) return { filed, waiting };
|
|
46
|
+
const kept = PR.keptIds(P);
|
|
47
|
+
for (const p of P.props.values()) if (p.run === run) { filed++; if (!kept.has(p.id) && !P.drops.has(PR.sigOf(p))) waiting++; }
|
|
48
|
+
return { filed, waiting };
|
|
49
|
+
}
|
|
50
|
+
/** A report job of the shared queue (origin report-design:<id>), named with the report: "Redrew TSC Monthly in its saved look". */
|
|
51
|
+
function reportRow(x, root, runtimeDir) {
|
|
52
|
+
let name = '';
|
|
53
|
+
// a line written before the queue kept `ref`: the session's own record still names its origin
|
|
54
|
+
if (!x.ref && x.session && runtimeDir && /^[A-Za-z0-9-]{1,40}$/.test(x.session)) {
|
|
55
|
+
try { const o = JSON.parse(fs.readFileSync(require('path').join(runtimeDir, 'sessions', x.session + '.json'), 'utf8')).origin; if (typeof o === 'string' && o.startsWith('report-design:')) x = { ...x, ref: o.slice(14) }; } catch { /* the session record is gone */ }
|
|
56
|
+
}
|
|
57
|
+
// or the cycle render it wrote: a <slot>.design.json written within two minutes of the job's end
|
|
58
|
+
if (!x.ref && root) {
|
|
59
|
+
try {
|
|
60
|
+
const path = require('path'), dir = path.join(root, require('./report-draft.js').stateRel(root), 'reports'), end = Date.parse(x.at);
|
|
61
|
+
let best = null;
|
|
62
|
+
for (const id of fs.readdirSync(dir)) {
|
|
63
|
+
let files; try { files = fs.readdirSync(path.join(dir, id)); } catch { continue; }
|
|
64
|
+
for (const f of files) if (f.endsWith('.design.json')) { const d = Math.abs(fs.statSync(path.join(dir, id, f)).mtimeMs - end); if (d <= 120000 && (!best || d < best.d)) best = { id, d }; }
|
|
65
|
+
}
|
|
66
|
+
if (best) x = { ...x, ref: best.id };
|
|
67
|
+
} catch { /* no reports folder */ }
|
|
68
|
+
}
|
|
69
|
+
try { if (x.ref && root) { const s = require('./report-setups.js').stateOf(root).setups.get(x.ref); name = s && s.name ? s.name : ''; } } catch { name = ''; }
|
|
70
|
+
const what = /cycle/.test(x.title || '') ? 'Redrew ' + (name || 'a report') + ' in its saved look' : 'Made the next look of ' + (name || 'a report');
|
|
71
|
+
return { job: x.job, title: what, meta: '', at: x.at };
|
|
72
|
+
}
|
|
38
73
|
/** What the other background jobs did this week, as record rows. Each source that cannot be read is left out. */
|
|
39
74
|
function others(ctx, w, dir, T, now) {
|
|
40
75
|
const out = [], since = now - WEEK, name = (ini) => (T.get(ini) || {}).title || ini;
|
|
41
76
|
try {
|
|
42
77
|
const G = require('./grill.js'), F = ctx.runtimeDir ? G.read(ctx.runtimeDir) : null;
|
|
43
|
-
|
|
78
|
+
const P = (() => { try { return dir ? PR.read(dir) : null; } catch { return null; } })();
|
|
79
|
+
for (const r of F ? F.runs.values() : []) if (r.ended && r.ok !== false && r.ws === w.id && (Date.parse(r.endAt || r.at) || 0) >= since) {
|
|
80
|
+
const f = grillFound(P, r.run);
|
|
81
|
+
const found = !f.filed ? 'nothing found' : f.waiting === f.filed ? plural(f.filed, 'finding', 'findings') + ' waiting'
|
|
82
|
+
: plural(f.filed, 'finding', 'findings') + (f.waiting ? ', ' + f.waiting + ' waiting' : ', all seen');
|
|
83
|
+
// the initiative chip only when something waits there for the PM
|
|
84
|
+
out.push({ job: 'grill', title: 'Checked ' + name(r.ini) + ' for risks', meta: [GRILL_WHY[r.why] || r.why, found].filter(Boolean).join(' · '), at: r.endAt || r.at, ...(f.waiting ? { ini: r.ini, board: r.board || '' } : {}) });
|
|
85
|
+
}
|
|
44
86
|
} catch { /* no grill log */ }
|
|
45
87
|
try {
|
|
46
88
|
// the AI jobs' audit (lib/ai-guard.js AUDIT), read as a file: this page files nothing through the guard
|
|
47
89
|
const audit = dir ? fs.readFileSync(require('path').join(dir, 'ai-audit.jsonl'), 'utf8').split('\n').slice(-400).map((s) => { try { return JSON.parse(s); } catch { return null; } }).filter(isObj) : [];
|
|
48
90
|
for (const l of audit) if (l.event === 'end' && (Date.parse(l.at) || 0) >= since) out.push({ job: l.job, title: (JOB_NAMES[l.job] || l.job), meta: l.filed != null ? l.filed + ' filed' : '', at: l.at });
|
|
49
91
|
} catch { /* no audit */ }
|
|
50
|
-
try { if (ctx.bgQueue && typeof ctx.bgQueue.record === 'function') for (const x of ctx.bgQueue.record({ ws: w.id, since }) || []) if (isObj(x) && x.job !== 'grill') out.push(x); /* grill runs are read above, from their own log */ } catch { /* the queue says nothing */ }
|
|
92
|
+
try { if (ctx.bgQueue && typeof ctx.bgQueue.record === 'function') for (const x of ctx.bgQueue.record({ ws: w.id, since }) || []) if (isObj(x) && x.job !== 'grill') out.push(x.job === 'report-design' ? reportRow(x, w.root, ctx.runtimeDir) : x); /* grill runs are read above, from their own log */ } catch { /* the queue says nothing */ }
|
|
51
93
|
return out;
|
|
52
94
|
}
|
|
53
95
|
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
* POST /api/meeting-points/forget { lesson } → { ok } Undo of what PMOS learned, and of a word fix (its term id)
|
|
11
11
|
* POST /api/meeting-points/term { note, heard, means } → { ok, term, terms } a word fixed in place, in every place the note shows it
|
|
12
12
|
* The GET answers { terms } too: the word fixes that hold now (lib/meeting-terms.js).
|
|
13
|
+
* POST /api/meeting-points/keep { lesson, keep } → { ok, term, terms } the ✦ ask: remember a word fix for the next meetings (true) or not
|
|
14
|
+
* GET /api/meeting-points/suggest?note=<id>&heard=<words>&invite=<text>…&people=<name>… → { ok, suggest: { means, from, file?, alts } | null }
|
|
15
|
+
* the spelling a word fix offers (lib/meeting-suggest.js): a fix remembered before, the invite the page sends, the workspace's repository
|
|
13
16
|
* The POSTs are page presses (lib/pm-guard.js requirePagePress). Handlers are pure over (q | body, ctx) so a test needs no server. */
|
|
14
17
|
const MP = require('./meeting-points.js');
|
|
15
18
|
const ML = require('./meeting-learn.js');
|
|
16
19
|
const MT = require('./meeting-terms.js');
|
|
20
|
+
const MS = require('./meeting-suggest.js');
|
|
17
21
|
const guard = require('./pm-guard.js');
|
|
18
22
|
|
|
19
23
|
const fail = (status, error) => ({ status, body: { ok: false, error } });
|
|
@@ -26,7 +30,7 @@ function read(q, ctx) {
|
|
|
26
30
|
function learn(req, body, ctx, op) {
|
|
27
31
|
const g = guard.requirePagePress(req, body);
|
|
28
32
|
if (!g.ok) return fail(g.status, g.error);
|
|
29
|
-
const r = op === 'forget' ? ML.forget(ctx.runtimeDir, body) : op === 'term' ? MT.term(ctx.runtimeDir, body) : ML.edit(ctx.runtimeDir, body);
|
|
33
|
+
const r = op === 'forget' ? ML.forget(ctx.runtimeDir, body) : op === 'term' ? MT.term(ctx.runtimeDir, body) : op === 'keep' ? MT.keep(ctx.runtimeDir, body) : ML.edit(ctx.runtimeDir, body);
|
|
30
34
|
if (!r.ok) return fail(r.status || 400, r.error);
|
|
31
35
|
return { status: 200, body: r };
|
|
32
36
|
}
|
|
@@ -37,13 +41,33 @@ function write(req, body, ctx) {
|
|
|
37
41
|
return r.ok ? { status: 200, body: { ok: true, points: r.points } } : fail(r.status || 400, r.error);
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/** The ✦ spelling for heard words: the remembered fixes, the invite's words the page sends, and the note's workspace repository. */
|
|
45
|
+
function suggest(q, ctx, all = {}) {
|
|
46
|
+
const note = String(q.note || ''), heard = String(q.heard || '').replace(/\s+/g, ' ').trim();
|
|
47
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(note)) return fail(400, 'note is the id of a meeting note');
|
|
48
|
+
if (!heard || heard.length > MT.MAX_WORD) return fail(400, 'heard is the words to fix, at most ' + MT.MAX_WORD + ' characters');
|
|
49
|
+
const list = (k) => (Array.isArray(all[k]) ? all[k] : []).map((x) => String(x).slice(0, 300)).slice(0, 60);
|
|
50
|
+
let repo = [];
|
|
51
|
+
try {
|
|
52
|
+
const n = require('./notes.js').load(ctx.runtimeDir, note);
|
|
53
|
+
const w = n && n.ws && typeof ctx.getWs === 'function' ? ctx.getWs(n.ws) : null;
|
|
54
|
+
if (w && w.root) repo = MS.repoWords(w.root, { exclude: require('./typesafe-settings.js').getRepo(ctx.runtimeDir, w.root).exclude || [] });
|
|
55
|
+
} catch { repo = []; } // no repository to read: the other places still answer
|
|
56
|
+
const fixes = MT.terms(ctx.runtimeDir).filter((x) => x.keep === 'yes' || x.note === note);
|
|
57
|
+
return { status: 200, body: { ok: true, suggest: MS.suggest({ heard, fixes, invite: list('invite'), people: list('people'), repo }) } };
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
const respond = (ctx, res, r) => { ctx.json(res, r.body, r.status); return true; };
|
|
41
61
|
|
|
42
62
|
module.exports = {
|
|
43
63
|
prefixes: ['/api/meeting-points'],
|
|
44
|
-
read, write, learn,
|
|
64
|
+
read, write, learn, suggest,
|
|
45
65
|
handle: async (req, res, url, ctx) => {
|
|
46
|
-
|
|
66
|
+
if (url.pathname === '/api/meeting-points/suggest') {
|
|
67
|
+
if (req.method !== 'GET') return respond(ctx, res, fail(405, 'GET only'));
|
|
68
|
+
return respond(ctx, res, suggest(Object.fromEntries(url.searchParams), ctx, { invite: url.searchParams.getAll('invite'), people: url.searchParams.getAll('people') }));
|
|
69
|
+
}
|
|
70
|
+
const op = { '/api/meeting-points/edit': 'edit', '/api/meeting-points/forget': 'forget', '/api/meeting-points/term': 'term', '/api/meeting-points/keep': 'keep' }[url.pathname] || '';
|
|
47
71
|
if (op) {
|
|
48
72
|
if (req.method !== 'POST') return respond(ctx, res, fail(405, 'POST only'));
|
|
49
73
|
const got = await ctx.readJson(req);
|
|
@@ -55,6 +55,19 @@ function cyclesOf(root, id) {
|
|
|
55
55
|
}
|
|
56
56
|
function readText(root, rel) { try { return fs.readFileSync(path.join(path.resolve(String(root)), rel), 'utf8'); } catch { return null; } }
|
|
57
57
|
function sessionFile(root, id) { return path.join(path.dirname(RT.ledgerOf(root, id)), 'session.json'); }
|
|
58
|
+
// The drafting session's answer to a change (Wawan, 2026-11-04: it declined a look change in its own terminal and the panel said "drafting it"
|
|
59
|
+
// for good). ask() writes ask.json { at, change } and asks the session to write answer.json { text } when it is done or does nothing;
|
|
60
|
+
// stateOf() shows the answer once it is newer than the ask, and says the change is with the session until then.
|
|
61
|
+
const askFile = (root, id) => path.join(path.dirname(RT.ledgerOf(root, id)), 'ask.json');
|
|
62
|
+
const answerFile = (root, id) => path.join(path.dirname(RT.ledgerOf(root, id)), 'answer.json');
|
|
63
|
+
function readJson(f) { try { const o = JSON.parse(fs.readFileSync(f, 'utf8')); return isObj(o) ? o : null; } catch { return null; } }
|
|
64
|
+
function answerOf(root, id) {
|
|
65
|
+
const a = readJson(askFile(root, id)); if (!a || !a.at) return { asking: null, answer: null };
|
|
66
|
+
let st = null; try { st = fs.statSync(answerFile(root, id)); } catch { st = null; }
|
|
67
|
+
const ans = st && st.mtimeMs >= Date.parse(a.at) ? readJson(answerFile(root, id)) : null;
|
|
68
|
+
const txt = ans && typeof ans.text === 'string' ? ans.text.replace(/\s+/g, ' ').trim().slice(0, 400) : '';
|
|
69
|
+
return txt ? { asking: null, answer: { text: txt, at: new Date(st.mtimeMs).toISOString(), asked: a.at } } : { asking: { at: a.at }, answer: null };
|
|
70
|
+
}
|
|
58
71
|
function readSession(root, id) { try { const o = JSON.parse(fs.readFileSync(sessionFile(root, id), 'utf8')); return isObj(o) && typeof o.session === 'string' ? o : null; } catch { return null; } }
|
|
59
72
|
const alive = (ctx, sid) => !!sid && !!(ctx.chatSessions && typeof ctx.chatSessions.alive === 'function' && ctx.chatSessions.alive(sid));
|
|
60
73
|
function draftingOf(ctx, w, id) { try { return typeof ctx.structureDraftState === 'function' ? ctx.structureDraftState(w, id) || null : null; } catch { return null; } }
|
|
@@ -81,6 +94,7 @@ function stateOf(ctx, w, id, now = Date.now()) {
|
|
|
81
94
|
template: { current: f.current, versions: f.versions.map((v) => ({ ...v, words: RT.settingsWords(v.settings, names), cycles: counts.get(v.n) || 0 })) },
|
|
82
95
|
cycles, latest, drafting,
|
|
83
96
|
session: sess ? { id: sess.session, alive: alive(ctx, sess.session), since: sess.since || null } : null,
|
|
97
|
+
...answerOf(root, id),
|
|
84
98
|
review,
|
|
85
99
|
sections: { saved: live.structure, titles: names.sections(live.structure) || (s && Array.isArray(s.sections) ? s.sections.slice() : []) },
|
|
86
100
|
look: lookOf(root, id, live.design, names),
|
|
@@ -152,18 +166,23 @@ function draftWords(id, rel) {
|
|
|
152
166
|
return rel && slot ? `The draft: ${rel} (slot ${slot}). Change that file in place: edit only the note or notes the change is about, and keep every caveat. `
|
|
153
167
|
+ `Do not redraft for a change to the words. Only when the PM asks for fresh numbers, run \`pmos report draft ${id} ${slot} --redraft\`; never \`pmos report draft ${id}\` without the slot, and do not draft a new slot.` : '';
|
|
154
168
|
}
|
|
169
|
+
/** The last line of every change: where the session says what it did, so the PM reads it beside the chat (never only in this terminal). */
|
|
170
|
+
function answerWords(root, id) {
|
|
171
|
+
return `When you are done, or if you change nothing, write ONE JSON object {"text":"<one or two plain sentences for the PM: what you changed, or why you did not>"} to ${answerFile(root, id)}. A look change (palette, tables, a Gantt style) is not yours: say it goes in the report's look, which the PM keeps with ✓ beside the chat.`;
|
|
172
|
+
}
|
|
155
173
|
/** The words the report's session gets for one change. */
|
|
156
|
-
function askPrompt(skill, id, rel, change) {
|
|
174
|
+
function askPrompt(skill, id, rel, change, root) {
|
|
157
175
|
return `${skill}\n\nThe PM's request for this run, given when it was scheduled (do not ask again): ${id}\n\n`
|
|
158
176
|
+ `This is the report's own drafting session (the PMOS report flow), not the PM's chat. ${rel ? draftWords(id, rel) + ' ' : ''}`
|
|
159
|
-
+ `The change the PM asked for: ${change}\nChange only the draft. Never issue, send or share it: the PM issues it on the page
|
|
177
|
+
+ `The change the PM asked for: ${change}\nChange only the draft. Never issue, send or share it: the PM issues it on the page. ${answerWords(root, id)}`;
|
|
160
178
|
}
|
|
161
179
|
function ask(ctx, w, id, change, rel) {
|
|
162
180
|
const root = w.root;
|
|
163
181
|
const d = draftingOf(ctx, w, id);
|
|
164
182
|
const sess = readSession(root, id);
|
|
165
183
|
const target = d && d.state === 'drafting' && alive(ctx, d.session) ? d.session : sess && alive(ctx, sess.session) ? sess.session : null;
|
|
166
|
-
|
|
184
|
+
try { fs.mkdirSync(path.dirname(askFile(root, id)), { recursive: true }); fs.writeFileSync(askFile(root, id), JSON.stringify({ at: new Date().toISOString(), change: String(change).slice(0, 600) }) + '\n'); } catch { /* the panel then shows no answer */ }
|
|
185
|
+
const msg = `[PMOS report change] ${rel ? draftWords(id, rel) + ' ' : ''}The change: ${change}. Change only the draft; never issue, send or share it. ${answerWords(root, id)}`;
|
|
167
186
|
if (target) {
|
|
168
187
|
const sent = typeof ctx.sendToSession === 'function' && ctx.sendToSession(target, msg);
|
|
169
188
|
if (sent) return { ok: true, session: target, path: rel, passed: true };
|
|
@@ -174,7 +193,7 @@ function ask(ctx, w, id, change, rel) {
|
|
|
174
193
|
const origin = 'report-session:' + id;
|
|
175
194
|
if (typeof ctx.endSupersededRuns === 'function') { try { ctx.endSupersededRuns(origin); } catch { /* the old one ends by itself */ } }
|
|
176
195
|
const s = setupOf(root, id);
|
|
177
|
-
const sp = ctx.spawnSession({ name: 'Report · ' + ((s && s.name) || id), agent: 'claude', ws: w.id, origin, initialInput: askPrompt(skill, id, rel, change) + '\r' });
|
|
196
|
+
const sp = ctx.spawnSession({ name: 'Report · ' + ((s && s.name) || id), agent: 'claude', ws: w.id, origin, initialInput: askPrompt(skill, id, rel, change, root) + '\r' });
|
|
178
197
|
if (!sp || sp.state === 'exited' || !sp.id) return { ok: false, status: 409, error: 'The report\'s session did not start: ' + ((sp && sp.note) || 'no session') };
|
|
179
198
|
try { fs.mkdirSync(path.dirname(sessionFile(root, id)), { recursive: true }); fs.writeFileSync(sessionFile(root, id), JSON.stringify({ session: sp.id, since: new Date().toISOString() }) + '\n'); } catch { /* shown as no session */ }
|
|
180
199
|
return { ok: true, session: sp.id, path: rel, started: true };
|
|
@@ -250,7 +269,18 @@ async function handle(req, res, url, ctx) {
|
|
|
250
269
|
if (!ar.ok) return fail(ar);
|
|
251
270
|
const names = RT.namesOf(w.root, id);
|
|
252
271
|
const v = RT.addVersion(w.root, id, next, { names, why: RT.diffWords(was, next, names).join('; ').slice(0, 200) });
|
|
253
|
-
|
|
272
|
+
if (!v.ok) return fail(v);
|
|
273
|
+
// a new look kept with ✓ redraws the open draft in it at once, as the design studio's "This is it" does (Wawan, 2026-11-04: the look asked
|
|
274
|
+
// for in the chat never reached the draft). Only the look is redrawn: the text and its numbers stay; an issued cycle keeps its look.
|
|
275
|
+
let redraw = null;
|
|
276
|
+
if (ar.done && ar.done.design && next.design != null) {
|
|
277
|
+
const cur = cyclesOf(w.root, id)[0];
|
|
278
|
+
if (cur && cur.state === 'draft') {
|
|
279
|
+
let r; try { r = require('./routes-report-design.js').startCycle(ctx, w, id, cur.path); } catch (e) { r = { ok: false, error: String((e && e.message) || e) }; }
|
|
280
|
+
redraw = { path: cur.path, slot: cur.slot, started: !!(r && r.ok), ...(r && r.ok ? {} : { reason: String((r && r.error) || '') }) };
|
|
281
|
+
} else if (cur) redraw = { path: cur.path, slot: cur.slot, started: false, reason: 'sent' };
|
|
282
|
+
}
|
|
283
|
+
return send({ status: 200, body: { ok: true, n: v.n, redraw, state: stateOf(ctx, w, id) } });
|
|
254
284
|
}
|
|
255
285
|
if (sub === 'restore') {
|
|
256
286
|
const f = RT.backfill(w.root, id, RT.fold(RT.readLines(w.root, id))), v = f.versions.find((x) => x.n === +body.n);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.529.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.529.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -60,7 +60,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
60
60
|
const s1 = await sum(p);
|
|
61
61
|
/* batch 25 item 11 (MvC): the summary first in the left column, the kept points with Confirm N in their header, the evidence, one strip, the docked timeline */
|
|
62
62
|
ok('S1 the summary: the overview and the meta line, no primary (Confirm sits in the kept header)', /opens next to the chat/.test(s1.text) && !s1.pri, JSON.stringify(s1));
|
|
63
|
-
ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N;
|
|
63
|
+
ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N; one player (MpD1), no dock', await p.evaluate(() => { const l = document.querySelector('#czViewer .vzr-l'); return !!l && l.firstElementChild.classList.contains('czs-sum') && /^\d+ kept$/.test(l.querySelector('.vzr-ph h3').textContent) && /^Confirm \d+$/.test(l.querySelector('[data-a="confirmall"]').textContent) && !!document.querySelector('#czViewer .vzp-card .vzr-np') && !document.querySelector('#czViewer .vzr-dock'); }));
|
|
64
64
|
const chips = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzr-fold [data-a="strip"]')].map((x) => x.dataset.k).join());
|
|
65
65
|
ok('S1 one strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material, Transcript; no tabs, no checkbox, no filter', chips === 'left,steps,since,facts,names,prep,tr' && !(await p.$('#czViewer [role="tablist"]')) && !(await p.$('#czViewer .vzr [role="checkbox"]')) && !(await p.$('#czViewer [data-a="pf"]')), chips);
|
|
66
66
|
ok('S1 the Viewer opens wide for the two columns', await p.evaluate(() => document.getElementById('czViewer').offsetWidth >= 900 && getComputedStyle(document.querySelector('#czViewer .vzr-cols')).gridTemplateColumns.split(' ').length === 2));
|
|
@@ -80,7 +80,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
80
80
|
const strip = (k) => press(p, `.vzr-fold [data-a="strip"][data-k="${k}"]`);
|
|
81
81
|
await strip('since'); const since = await body(p); await strip('facts'); const facts = await body(p);
|
|
82
82
|
ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(since) && /Matches/.test(facts) && /Not checked/.test(facts), facts.slice(0, 600));
|
|
83
|
-
ok('S3 the
|
|
83
|
+
ok('S3 MpD4: the captures row is hidden until its icon is pressed', !(await p.$('#czViewer .vzp-card .vzp-th')));
|
|
84
|
+
await press(p, '.vzr-np:not(.vzr-ph1) [data-a="caps"]');
|
|
85
|
+
ok('S3 the captures icon shows the screen captures, with no Waiting for design marker', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzp-card .vzp-th').length > 0 && !document.querySelector('#czViewer [data-cz-placeholder]'))));
|
|
84
86
|
await press(p, '.vzp-card .vzp-th[data-a="seekto"]');
|
|
85
87
|
ok('S3 a thumbnail moves the replay to its time', await until(() => p.evaluate(() => /^03:10/.test((document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent || ''))), await p.evaluate(() => (document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent));
|
|
86
88
|
await strip('tr');
|
|
@@ -98,9 +100,9 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
98
100
|
|
|
99
101
|
/* S5 a correction, and what PMOS learns (XvAReplayEdit) */
|
|
100
102
|
const pid = await p.evaluate(() => { const r = document.querySelectorAll('#czViewer .vzr-row [data-a="psel"]')[1]; r.click(); return r.dataset.id; });
|
|
101
|
-
ok('S5 pressing a row selects it
|
|
102
|
-
await press(p, '.vzr-
|
|
103
|
-
ok('S5 Fix
|
|
103
|
+
ok('S5 pressing a row selects it; the selected row has Fix and Not right', await until(() => p.evaluate((id) => !!document.querySelector(`#czViewer .vzr-row.sel [data-a="psel"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-row.sel [data-a="pedit"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-row.sel [data-a="pwrong"][data-id="${id}"]`), pid)), pid);
|
|
104
|
+
await press(p, '.vzr-row.sel [data-a="pedit"]');
|
|
105
|
+
ok('S5 Fix on the row opens the editor in place: the kinds, the words, what PMOS will remember', !!pid && await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vzp-ed [data-a="etext"]') && document.querySelectorAll('#czViewer .vzp-dt .vzp-ed [data-a="ekind"]').length === 3 && document.querySelector('#czViewer .vzp-ed [data-a="esave"]').disabled)), pid);
|
|
104
106
|
const kNow = await p.evaluate(() => document.querySelector('#czViewer .vzp-ed [data-a="ekind"][aria-checked="true"]').dataset.k);
|
|
105
107
|
const kNew = kNow === 'question' ? 'action' : 'question';
|
|
106
108
|
await press(p, `.vzp-ed [data-a="ekind"][data-k="${kNew}"]`);
|
|
@@ -111,12 +113,16 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
111
113
|
ok('S5 the page says Fixed and what it will remember, with Undo', await until(async () => /Fixed\./.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))), (await body(p)).slice(0, 300));
|
|
112
114
|
await press(p, '.vz-msg [data-a="msgundo"]');
|
|
113
115
|
ok('S5 Undo forgets the lesson (POST /api/meeting-points/forget)', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 'l0123456789')), JSON.stringify(A.S.posts.map((x) => x.path)));
|
|
114
|
-
/*
|
|
115
|
-
await
|
|
116
|
-
|
|
117
|
-
await p.
|
|
118
|
-
ok('S5
|
|
119
|
-
await
|
|
116
|
+
/* MpW1, MpW2 (batch 27 item 12): words heard wrong, selected in Next steps, fixed in every place; the ✦ ask waits; Undo forgets it */
|
|
117
|
+
await strip('steps');
|
|
118
|
+
await until(() => p.evaluate(() => [...document.querySelectorAll('#czViewer .vzm-step .vzm-grow')].some((x) => /print/.test(x.textContent))));
|
|
119
|
+
await p.evaluate(() => { const g = [...document.querySelectorAll('#czViewer .vzm-step .vzm-grow')].find((x) => /print/.test(x.textContent)); const w = document.createTreeWalker(g, NodeFilter.SHOW_TEXT); let tn; while ((tn = w.nextNode()) && !/print/.test(tn.textContent)); const i = tn.textContent.indexOf('print'); const r = document.createRange(); r.setStart(tn, i); r.setEnd(tn, i + 5); const s = getSelection(); s.removeAllRanges(); s.addRange(r); g.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); });
|
|
120
|
+
ok('S5 selected words open the popup: Heard as, the fix field with ✓, the places; no ✦ line without a real match', await until(() => p.evaluate(() => { const f = document.querySelector('.vzr-wpop'); return !!f && f.dataset.w === 'print' && /Heard as “print”/.test(f.textContent) && !!f.querySelector('[data-a="wfok"]') && /in this meeting/.test(f.textContent) && !f.querySelector('.vzr-sug'); })));
|
|
121
|
+
await p.fill('.vzr-wpop [data-a="wfin"]', 'Print'); await p.press('.vzr-wpop [data-a="wfin"]', 'Enter');
|
|
122
|
+
ok('S5 Enter posts the word fix: it shows fixed, a toast with Undo, and Names and terms asks to remember it', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/term' && x.body.heard === 'print' && x.body.means === 'Print'))
|
|
123
|
+
&& await until(async () => !!(await p.$('#czViewer .vzr-fx')) && !!(await p.$('#czViewer .vzr-toast [data-a="msgundo"]')) && !!(await p.$('#czViewer [data-a="wkeep"][data-v="1"]'))));
|
|
124
|
+
ok('S5 remembering waits for the ✓: nothing was posted to keep yet', !A.S.posts.some((x) => x.path === '/api/meeting-points/keep'));
|
|
125
|
+
await press(p, '.vzr-toast [data-a="msgundo"]');
|
|
120
126
|
ok('S5 Undo forgets the word fix', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 't0123456789')) && await until(async () => !(await p.$('#czViewer .vzr-fx'))));
|
|
121
127
|
await press(p, '[data-a="confirmall"]');
|
|
122
128
|
ok('S5 Confirm N in the kept header confirms what is left to confirm', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points' && x.body.verdict === 'confirmed' && x.body.ids.length > 0)), JSON.stringify(A.S.posts.filter((x) => x.path === '/api/meeting-points')));
|
|
@@ -33,16 +33,25 @@ function pillOf(r) {
|
|
|
33
33
|
if (r.ini) return `<a class="lab-pill y" href="${esc(S.H.iniHref(r.ini))}">✦ on initiative</a>`;
|
|
34
34
|
return '';
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** When a job ran, short: "19:37" today, "Tue 14:10" this week (Wawan, 2026-11-04: the rows said what ran but not when). */
|
|
37
|
+
export function whenOf(at, now = Date.now()) {
|
|
38
|
+
const d = new Date(at); if (Number.isNaN(d.getTime())) return '';
|
|
39
|
+
const hm = String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
|
|
40
|
+
return new Date(now).toDateString() === d.toDateString() ? hm : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()] + ' ' + hm;
|
|
41
|
+
}
|
|
42
|
+
/** One row of the record. A background job says what Claude did and what came out (routes-lab.js others); its pill shows only when a result
|
|
43
|
+
* waits on the initiative, and says so. */
|
|
44
|
+
export function recRow(r, now = Date.now()) {
|
|
37
45
|
const open = r.compare ? ib(`data-lab-cmp="${esc(r.id)}" data-lab-board="${esc(r.board || '')}"`, 'Open in the compare view', LC.I.open) : '';
|
|
38
|
-
const pill = r.kind === 'job' ? (r.ini ? `<a class="lab-pill" href="${esc(S.H.iniHref(r.ini))}">
|
|
39
|
-
|
|
46
|
+
const pill = r.kind === 'job' ? (r.ini && S.H ? `<a class="lab-pill" href="${esc(S.H.iniHref(r.ini))}">See what it found</a>` : '') : pillOf(r);
|
|
47
|
+
const meta = r.kind === 'job' ? [r.meta, whenOf(r.at, now)].filter(Boolean).join(' · ') : r.meta || '';
|
|
48
|
+
return `<div class="lab-row"><span class="lab-ico">${SVG(ICON[r.kind] || ICON.job, 18)}</span><span class="w"><b>${esc(r.title)}</b><small>${esc(meta)}</small></span>${pill}${open}</div>`;
|
|
40
49
|
}
|
|
41
50
|
function html(L) {
|
|
42
51
|
const k = L.kept;
|
|
43
52
|
const keptCard = k ? `<div class="lab-card"><small>You kept</small><b class="lab-big">${k.kept} of ${k.filed}</b><small>${[k.proto[1] ? 'prototypes ' + k.proto[0] + ' of ' + k.proto[1] : '', k.scan[1] ? 'scans ' + k.scan[0] + ' of ' + k.scan[1] : ''].filter(Boolean).join(' · ')}. Claude learns from each reason you give.</small></div>` : '';
|
|
44
53
|
const next = L.next.length ? `<div class="lab-card"><b>Next time you step away</b>${L.next.map((x) => `<div class="lab-row"><span class="lab-dot open"></span><span class="w">${esc(x.title)}${x.why ? `<small>${esc(x.why)}</small>` : ''}</span></div>`).join('')}<small>Claude picks these by itself. To skip one, say so in chat.</small></div>` : '';
|
|
45
|
-
const rec = L.record.length ? `<div class="lab-card lab-rec"><div class="lab-bar"><b>What Claude did in the background</b><small>this week, every job</small></div>${L.record.map(recRow).join('')}${L.more ? `<small>and ${L.more} more</small>` : ''}</div>` : '';
|
|
54
|
+
const rec = L.record.length ? `<div class="lab-card lab-rec"><div class="lab-bar"><b>What Claude did in the background</b><small>this week, every job</small></div>${L.record.map((r) => recRow(r)).join('')}${L.more ? `<small>and ${L.more} more</small>` : ''}</div>` : '';
|
|
46
55
|
return `<h2 class="ins-h3" id="hLab">Lab</h2>`
|
|
47
56
|
+ (L.say ? `<p class="lab-say"><span aria-hidden="true">✦</span> ${esc(L.say)}</p>` : '')
|
|
48
57
|
+ L.loose.map(looseHtml).join('')
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
.vzr { container-type: inline-size; padding: 0; background: none; gap: 14px; }
|
|
236
236
|
.vzr-mbonly { display: none; }
|
|
237
237
|
.vzr-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
238
|
-
@container (min-width: 860px) { .vzr-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.
|
|
238
|
+
@container (min-width: 860px) { .vzr-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 22px; } .vzr-np:not(.vzr-ph1) { position: sticky; top: 0; } }
|
|
239
239
|
.vzr-l { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
240
240
|
.vzr-sum .czs-p { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: var(--fs-body); line-height: 1.55; margin: 0; }
|
|
241
241
|
.vzr-sum.all .czs-p { display: block; }
|
|
@@ -275,40 +275,138 @@
|
|
|
275
275
|
.vzr-ch[aria-expanded="true"] { background: var(--cz-ink) !important; color: var(--cz-card); }
|
|
276
276
|
.vzr-ch[aria-expanded="true"] b, .vzr-ch[aria-expanded="true"] span { color: var(--cz-card); }
|
|
277
277
|
.vzr-panel { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 18px; border: 1px solid var(--cz-hair); background: var(--cz-card); }
|
|
278
|
-
.vzr-ev { display: flex; flex-direction: column; gap: 12px; padding: 14px; border-radius: 20px; border: 1px solid var(--cz-hair); background: var(--cz-card); min-width: 0; }
|
|
279
|
-
.vzr-shot { aspect-ratio: 16 / 9; background: var(--cz-well) !important; }
|
|
280
278
|
.vzr-noshot { display: grid; place-items: center; min-height: 120px; border-radius: 12px; background: var(--cz-well); padding: 12px; text-align: center; }
|
|
281
|
-
.vzr-acts { display: flex; align-items: center; gap: 10px; }
|
|
282
|
-
.vzr-pri { width: 40px !important; height: 40px !important; background: var(--cz-blueink) !important; color: var(--cz-card) !important; }
|
|
283
|
-
.vzr-at { font-size: var(--fs-row); font-weight: var(--fw-strong); color: var(--cz-blueink); font-variant-numeric: tabular-nums; line-height: var(--lh-row); }
|
|
284
|
-
.vzr-q { margin: 0; font-size: var(--fs-body); line-height: 1.5; color: var(--cz-ink2); }
|
|
285
|
-
.vzr-film { display: flex; gap: 6px; align-items: center; overflow: hidden; }
|
|
286
|
-
.vzr-film.all { flex-wrap: wrap; }
|
|
287
|
-
.vzr-film .vzp-th { width: 72px; height: 44px; }
|
|
288
|
-
.vzr-more { height: 44px; min-width: 44px; padding: 0 8px !important; border-radius: 6px; background: var(--cz-well) !important; color: var(--cz-ink2); font-size: var(--fs-label); font-weight: var(--fw-strong); display: inline-grid !important; place-items: center; line-height: var(--lh-label); }
|
|
289
|
-
.vzr-dock { position: sticky; bottom: 0; z-index: 2; flex-direction: row !important; align-items: center; gap: 14px !important; padding: 10px 16px !important; border-radius: 22px; background: var(--cz-card) !important; box-shadow: 0 18px 44px -18px color-mix(in oklab, var(--cz-ink) 40%, transparent), 0 0 0 1px var(--cz-hair); }
|
|
290
|
-
.vzr-dock .vzp-ctl { flex: none; }
|
|
291
|
-
.vzr-dock .vzp-ctl .vzp-mut { flex: none; }
|
|
292
|
-
.vzr-tl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
293
|
-
.vzr-tops { align-items: flex-end; }
|
|
294
|
-
.vzr-tops .vzp-top { white-space: normal; text-overflow: clip; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-height: 1.2; font-weight: var(--fw-strong); color: var(--cz-ink2); padding: 0 6px 0 0 !important; text-align: left !important; }
|
|
295
|
-
.vzr-tops .vzp-top[aria-current="true"] { color: var(--cz-ink); font-weight: var(--fw-strong); }
|
|
296
279
|
.vzp-kd[data-k="decision"] { fill: var(--cz-ok); }
|
|
297
280
|
.vzp-kd[data-k="action"] { fill: var(--cz-blue); }
|
|
298
281
|
.vzp-kd[data-k="question"] { fill: var(--cz-late); }
|
|
299
282
|
/* a word fixed in place (MvF): fixed in green, a guess underlined dotted, the field in place */
|
|
300
|
-
.vzr-
|
|
283
|
+
.vzr-pts svg, .vzr-cfall svg, .vzr-panel .vzm-ib svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
301
284
|
.vzr-fl { display: inline !important; padding: 0 !important; font: inherit; color: var(--cz-ink); font-weight: var(--fw-strong); border-bottom: 2px dotted var(--cz-late) !important; border-radius: 0 !important; cursor: pointer; }
|
|
302
|
-
.vzr-fe { display: inline-flex; align-items: center; gap: 4px; padding: 0 2px 0 8px; border-radius: 10px; background: var(--cz-card); box-shadow: 0 0 0 2px var(--cz-blue); vertical-align: middle; }
|
|
303
|
-
.vzr-fe input { width: 12ch; height: 28px; border: 0; outline: 0; background: transparent; font: inherit; font-weight: var(--fw-strong); color: var(--cz-ink); }
|
|
304
|
-
.vzr-ok { width: 26px; height: 26px; border-radius: 50%; display: inline-grid !important; place-items: center; background: var(--cz-blueink) !important; color: var(--cz-card); }
|
|
305
|
-
.vzr-ok svg, .vzr-ev .vzp-ib svg, .vzr-pts svg, .vzr-cfall svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
306
|
-
.vzr-ev .vzp-fill, .vzr-pri svg { fill: currentColor; stroke: none; }
|
|
307
285
|
@media (max-width: 640px) {
|
|
308
286
|
.vzr-mbonly { display: block; }
|
|
309
287
|
.vzr-mbonly > .czs-secs { margin-top: 0; }
|
|
310
288
|
}
|
|
311
289
|
|
|
290
|
+
/* ── the player (batch 27 item 12: MpD1 now playing, MpD2 chapters, MpD4 captures shown, MpD3 phone) ── */
|
|
291
|
+
.vzr-np { position: relative; display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
292
|
+
.vzr-np svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
|
293
|
+
.vzr-np svg.vzr-fill, .vzr-np .vzr-big svg { fill: currentColor; stroke: none; }
|
|
294
|
+
.vzr-art { position: relative; border-radius: 20px; overflow: hidden; background: var(--cz-well); box-shadow: 0 0 0 1px var(--cz-hair), 0 18px 40px -24px color-mix(in oklab, var(--cz-ink) 40%, transparent); }
|
|
295
|
+
.vzr-crop { position: relative; display: block; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; }
|
|
296
|
+
.vzr-crop img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
|
|
297
|
+
.vzr-crop.boxed img { inset: auto; object-fit: fill; max-width: none; }
|
|
298
|
+
.vzr-art .vzr-crop { --mh: 340px; max-height: var(--mh); }
|
|
299
|
+
.vzr-art .vzr-crop.boxed { max-height: none; width: min(100%, calc(var(--mh) * var(--ar, 1.78))); margin: 0 auto; }
|
|
300
|
+
.vzr-tag { position: absolute; left: 12px; bottom: 12px; padding: 3px 10px; border-radius: 999px; background: color-mix(in oklab, var(--cz-card) 92%, transparent); color: var(--cz-ink2); font-size: var(--fs-label); font-weight: var(--fw-strong); line-height: var(--lh-label); box-shadow: 0 0 0 1px var(--cz-hair); }
|
|
301
|
+
.vzr-full { position: absolute !important; right: 10px; top: 10px; width: 36px; height: 36px; border-radius: 50%; display: inline-grid !important; place-items: center; background: color-mix(in oklab, var(--cz-card) 92%, transparent) !important; color: var(--cz-ink2); box-shadow: 0 0 0 1px var(--cz-hair); }
|
|
302
|
+
.vzr-noshot { display: grid; place-items: center; min-height: 120px; border-radius: 20px; background: var(--cz-well); padding: 12px; text-align: center; }
|
|
303
|
+
.vzr-nt { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
|
304
|
+
.vzr-nt b { font-size: var(--fs-panel); font-weight: var(--fw-strong); letter-spacing: -.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--cz-ink); line-height: 1.3; }
|
|
305
|
+
.vzr-nt span { font-size: var(--fs-meta); color: var(--cz-ink3); white-space: nowrap; line-height: var(--lh-meta); }
|
|
306
|
+
.vzr-barw { position: relative; padding-top: 2px; }
|
|
307
|
+
.vzr-bar { display: block; height: 26px; }
|
|
308
|
+
.vzr-bar svg { overflow: visible; }
|
|
309
|
+
.vzr-sg { stroke: none !important; fill: color-mix(in oklab, var(--cz-ink) 13%, var(--cz-card)); }
|
|
310
|
+
.vzr-sg[data-k="past"] { fill: color-mix(in oklab, var(--cz-blue) 50%, var(--cz-card)); }
|
|
311
|
+
.vzr-sg[data-k="cur"] { fill: color-mix(in oklab, var(--cz-blue) 20%, var(--cz-card)); }
|
|
312
|
+
.vzr-prog { stroke: none !important; fill: var(--cz-blue); }
|
|
313
|
+
.vzr-head { stroke: none !important; fill: var(--cz-blue); }
|
|
314
|
+
.vzr-mk { color: var(--cz-ink2); stroke-width: 3; }
|
|
315
|
+
.vzr-mk[data-k="question"] { color: var(--cz-late); }
|
|
316
|
+
.vzr-mk.on { color: var(--cz-ink); }
|
|
317
|
+
.vzr-md { fill: var(--cz-ok); stroke: none !important; }
|
|
318
|
+
.vzr-hov { position: absolute; bottom: 34px; z-index: 6; width: 160px; padding: 6px; border-radius: 14px; background: var(--cz-card); box-shadow: 0 16px 36px -14px color-mix(in oklab, var(--cz-ink) 50%, transparent), 0 0 0 1px var(--cz-hair); pointer-events: none; }
|
|
319
|
+
.vzr-hov[hidden] { display: none; }
|
|
320
|
+
.vzr-hth { display: block; border-radius: 9px; overflow: hidden; box-shadow: 0 0 0 1px var(--cz-hair); background: var(--cz-well); }
|
|
321
|
+
.vzr-hth .vzr-crop { aspect-ratio: 16 / 9; }
|
|
322
|
+
.vzr-hov b { display: block; margin: 6px 4px 0; font-size: var(--fs-meta); font-weight: var(--fw-strong); line-height: 1.25; color: var(--cz-ink); }
|
|
323
|
+
.vzr-hov small { display: block; margin: 0 4px 2px; font-size: var(--fs-badge); font-weight: var(--fw-strong); color: var(--cz-ink3); }
|
|
324
|
+
.vzr-times { display: flex; justify-content: space-between; margin-top: -6px; font-size: var(--fs-meta); font-weight: var(--fw-strong); color: var(--cz-ink3); font-variant-numeric: tabular-nums; line-height: var(--lh-meta); }
|
|
325
|
+
.vzr-tr { position: relative; display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 60px; }
|
|
326
|
+
.vzr-L { position: absolute; left: 0; }
|
|
327
|
+
.vzr-R { position: absolute; right: 0; display: flex; gap: 6px; }
|
|
328
|
+
.vzr-speed { height: 28px; padding: 0 8px !important; border-radius: 999px; color: var(--cz-ink2); font-size: var(--fs-label); font-weight: var(--fw-strong); display: inline-flex !important; align-items: center; line-height: var(--lh-label); }
|
|
329
|
+
.vzr-ib { width: 36px; height: 36px; border-radius: 50%; display: inline-grid !important; place-items: center; color: var(--cz-ink2); flex: none; }
|
|
330
|
+
.vzr-ib.vzr-o { box-shadow: inset 0 0 0 1px var(--cz-hair2, var(--cz-hair)); }
|
|
331
|
+
.vzr-ib.on, .vzr-ib[aria-pressed="true"], .vzr-ib[aria-expanded="true"] { background: var(--cz-bluebg) !important; color: var(--cz-blueink); box-shadow: none; }
|
|
332
|
+
.vzr-skip { width: 46px; height: 46px; border-radius: 50%; display: inline-grid !important; place-items: center; color: var(--cz-ink); }
|
|
333
|
+
.vzr-skip text { font-family: var(--cz-sans); font-size: var(--fs-label); font-weight: var(--fw-strong); }
|
|
334
|
+
.vzr-big { width: 52px; height: 52px; border-radius: 50%; display: inline-grid !important; place-items: center; color: var(--cz-ink); }
|
|
335
|
+
.vzr-big svg { width: 22px; height: 22px; }
|
|
336
|
+
.vzr-big:hover { background: var(--cz-well) !important; }
|
|
337
|
+
.vzr-ib:hover, .vzr-skip:hover, .vzr-speed:hover { background: var(--cz-well) !important; }
|
|
338
|
+
.vzr-cap { display: flex; flex-direction: column; gap: 3px; padding: 2px 2px 0; }
|
|
339
|
+
.vzr-cp, .vzr-cx { font-size: var(--fs-body); color: var(--cz-ink3); line-height: var(--lh-body); }
|
|
340
|
+
.vzr-cn { font-size: var(--fs-panel); font-weight: var(--fw-strong); color: var(--cz-ink); line-height: 1.35; }
|
|
341
|
+
.vzr-cw { display: inline-block; margin-right: 8px; padding: 1px 9px; border-radius: 999px; background: var(--cz-bluebg); color: var(--cz-blueink); font-size: var(--fs-badge); font-weight: var(--fw-strong); vertical-align: 1px; line-height: var(--lh-badge); }
|
|
342
|
+
.vzr-film { display: flex; gap: 8px; align-items: center; overflow: hidden; }
|
|
343
|
+
.vzr-film.all { flex-wrap: wrap; }
|
|
344
|
+
.vzr-th { width: 84px !important; height: 52px !important; border-radius: 9px !important; border: 0 !important; box-shadow: 0 0 0 1px var(--cz-hair); background: var(--cz-well) !important; overflow: hidden; }
|
|
345
|
+
.vzr-th .vzr-crop { height: 100%; aspect-ratio: auto !important; }
|
|
346
|
+
.vzr-th.on { box-shadow: 0 0 0 2.5px var(--cz-blue); }
|
|
347
|
+
.vzr-more { min-height: 44px; padding: 0 6px !important; font-size: var(--fs-meta); font-weight: var(--fw-strong); color: var(--cz-ink3); line-height: var(--lh-meta); }
|
|
348
|
+
.vzr-chw { position: relative; height: 0; z-index: 7; }
|
|
349
|
+
.vzr-chs { position: absolute; right: 0; top: -46px; width: min(520px, 100%); max-height: 352px; overflow: auto; display: flex; flex-direction: column; gap: 1px; padding: 8px; border-radius: 22px; background: var(--cz-card); box-shadow: 0 24px 60px -24px color-mix(in oklab, var(--cz-ink) 50%, transparent), 0 0 0 1px var(--cz-hair); }
|
|
350
|
+
.vzr-chh { display: flex; align-items: center; gap: 8px; padding: 6px 12px; }
|
|
351
|
+
.vzr-lbl { font-size: var(--fs-badge); font-weight: var(--fw-strong); color: var(--cz-ink3); line-height: var(--lh-badge); }
|
|
352
|
+
.vzr-hint0 { font-size: var(--fs-meta); color: var(--cz-ink3); line-height: var(--lh-meta); }
|
|
353
|
+
.vzr-chr { display: grid !important; grid-template-columns: minmax(0, 1fr) auto auto; gap: 10px; align-items: center; min-height: 34px; padding: 7px 10px !important; border-radius: 12px; font-size: var(--fs-body); text-align: left !important; line-height: var(--lh-body); }
|
|
354
|
+
.vzr-chr b { font-weight: var(--fw-strong); color: var(--cz-ink); }
|
|
355
|
+
.vzr-chr.cur { background: var(--cz-bluebg) !important; }
|
|
356
|
+
.vzr-chr.cur b { font-weight: var(--fw-strong); }
|
|
357
|
+
.vzr-chr:hover { background: var(--cz-well) !important; }
|
|
358
|
+
.vzr-chm { display: inline-flex; gap: 5px; align-items: center; min-width: 30px; justify-content: flex-end; }
|
|
359
|
+
.vzr-chm .vzr-k { width: 14px; height: 14px; }
|
|
360
|
+
.vzr-chm .vzr-k svg { width: 14px; height: 14px; }
|
|
361
|
+
.vzr-cht { font-size: var(--fs-meta); color: var(--cz-ink3); font-variant-numeric: tabular-nums; line-height: var(--lh-meta); }
|
|
362
|
+
.vzr-ra { grid-column: 2 / 5; display: flex; justify-content: flex-end; gap: 6px; margin-top: 6px; }
|
|
363
|
+
.vzr-edit { grid-column: 1 / -1; margin-top: 8px; }
|
|
364
|
+
.vzr-row .vzp-kt { cursor: pointer; user-select: text; -webkit-user-select: text; }
|
|
365
|
+
.vzr-row .vzp-kt:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 6px; }
|
|
366
|
+
.vzr-hint { margin: -4px 6px 0; font-size: var(--fs-meta); color: var(--cz-ink3); line-height: var(--lh-meta); }
|
|
367
|
+
.vzr-phk { display: flex; flex-direction: column; gap: 4px; }
|
|
368
|
+
.vzr-phk .vzr-cf { display: none !important; }
|
|
369
|
+
.vzr-phk .vzp-kr.vzr-row { grid-template-columns: 20px minmax(0, 1fr) auto; }
|
|
370
|
+
|
|
371
|
+
/* ── word fix A (MpW1 popup, MpW2 after, MpWPhone sheet): a selected word, the popup, the toast, Names and terms ── */
|
|
372
|
+
.vzr-fx { color: var(--cz-ok); font-weight: var(--fw-strong); border-bottom: 2px dotted color-mix(in oklab, var(--cz-ok) 50%, transparent); }
|
|
373
|
+
.vzr-selw { background: color-mix(in oklab, var(--cz-blue) 28%, transparent); border-radius: 4px; padding: 0 1px; box-shadow: 0 0 0 1px color-mix(in oklab, var(--cz-blue) 60%, transparent); }
|
|
374
|
+
.vzr-wpop { position: fixed; z-index: 40; display: flex; flex-direction: column; gap: 9px; width: 380px; padding: 12px 12px 12px 14px; border-radius: 20px; background: var(--cz-card); color: var(--cz-ink); box-shadow: 0 24px 60px -24px color-mix(in oklab, var(--cz-ink) 50%, transparent), 0 0 0 1px var(--cz-hair); font-size: var(--fs-body); line-height: var(--lh-body); }
|
|
375
|
+
.vzr-wpop svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
376
|
+
.vzr-wpop p { margin: 0; }
|
|
377
|
+
.vzr-grab { display: none; }
|
|
378
|
+
.vzr-wl { font-size: var(--fs-badge); font-weight: var(--fw-strong); color: var(--cz-ink3); line-height: var(--lh-badge); }
|
|
379
|
+
.vzr-wf { display: flex; align-items: center; gap: 8px; height: 46px; padding: 4px 4px 4px 14px; border-radius: 14px; background: var(--cz-well); }
|
|
380
|
+
.vzr-wf input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font: inherit; font-size: var(--fs-field); font-weight: var(--fw-strong); color: var(--cz-ink); }
|
|
381
|
+
.vzr-go { width: 38px; height: 38px; border-radius: 50%; display: inline-grid !important; place-items: center; flex: none; color: var(--cz-ink); border: 0 !important; box-shadow: none !important; }
|
|
382
|
+
.vzr-go:hover { background: var(--cz-card) !important; }
|
|
383
|
+
.vzr-sug { display: flex; gap: 8px; align-items: flex-start; font-size: var(--fs-meta); color: var(--cz-ink2); line-height: var(--lh-meta); }
|
|
384
|
+
.vzr-star { color: var(--cz-blueink); font-weight: var(--fw-strong); font-size: var(--fs-section); line-height: 1.2; }
|
|
385
|
+
.vzr-alt { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
386
|
+
.vzr-alt button { min-height: 28px; padding: 0 11px !important; border-radius: 999px; background: var(--cz-well) !important; color: var(--cz-ink2); font-size: var(--fs-meta); font-weight: var(--fw-strong); display: inline-flex !important; align-items: center; line-height: var(--lh-meta); border: 0 !important; box-shadow: none !important; }
|
|
387
|
+
.vzr-alt button[aria-pressed="true"] { background: var(--cz-bluebg) !important; color: var(--cz-blueink); }
|
|
388
|
+
.vzr-scope { font-size: var(--fs-meta); color: var(--cz-ink2); line-height: var(--lh-meta); }
|
|
389
|
+
.vzr-scope b { font-weight: var(--fw-strong); color: var(--cz-ink); }
|
|
390
|
+
.vzr-brk { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
391
|
+
.vzr-brk span { padding: 2px 9px; border-radius: 999px; background: var(--cz-well); font-size: var(--fs-badge); font-weight: var(--fw-strong); color: var(--cz-ink3); line-height: var(--lh-badge); }
|
|
392
|
+
.vzr-arr { position: absolute; left: var(--arr, 34px); top: -6px; width: 12px; height: 12px; background: var(--cz-card); transform: rotate(45deg); box-shadow: -1px -1px 0 0 var(--cz-hair); }
|
|
393
|
+
.vzr-wpop.up .vzr-arr { top: auto; bottom: -6px; box-shadow: 1px 1px 0 0 var(--cz-hair); }
|
|
394
|
+
.vzr-wpop.sheet { left: 0 !important; right: 0; bottom: 0; top: auto !important; width: auto !important; padding: 10px 16px 26px; border-radius: 28px 28px 0 0; box-shadow: 0 -20px 50px -20px color-mix(in oklab, var(--cz-ink) 35%, transparent), 0 0 0 100vmax color-mix(in oklab, var(--cz-ink) 12%, transparent); }
|
|
395
|
+
.vzr-wpop.sheet .vzr-grab { display: block; align-self: center; width: 40px; height: 5px; border-radius: 3px; background: var(--cz-hair); }
|
|
396
|
+
.vzr-wpop.sheet .vzr-arr { display: none; }
|
|
397
|
+
.vzr-toastw { position: sticky; bottom: 16px; z-index: 30; display: flex; justify-content: center; pointer-events: none; margin-top: 8px; }
|
|
398
|
+
.vzr-toast { pointer-events: auto; display: inline-flex; align-items: center; gap: 12px; margin: 0; padding: 6px 6px 6px 18px; border-radius: 999px; background: var(--cz-ink); color: var(--cz-card); font-size: var(--fs-body); white-space: nowrap; box-shadow: 0 10px 30px -10px color-mix(in oklab, var(--cz-ink) 60%, transparent); line-height: var(--lh-body); }
|
|
399
|
+
.vzr-toast:not(:has(button)) { padding-right: 18px; }
|
|
400
|
+
.vzr-toast button { min-height: 34px; padding: 0 14px !important; border-radius: 999px; background: color-mix(in oklab, var(--cz-card) 16%, transparent) !important; color: var(--cz-card); font-weight: var(--fw-strong); }
|
|
401
|
+
.vzr-trow { align-items: center; }
|
|
402
|
+
.vzr-fxn { color: var(--cz-ok); }
|
|
403
|
+
.vzr-ask { display: flex; align-items: center; gap: 10px; padding: 9px 10px 9px 14px; border-radius: 16px; background: var(--cz-bluebg); font-size: var(--fs-body); line-height: var(--lh-body); }
|
|
404
|
+
.vzr-ask .vzr-star { font-size: var(--fs-panel); }
|
|
405
|
+
.vzr-ask svg, .vzr-trow svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
406
|
+
.vzr-a { width: 34px; height: 34px; border-radius: 50%; display: inline-grid !important; place-items: center; flex: none; background: var(--cz-card) !important; color: var(--cz-ink2); box-shadow: 0 0 0 1px var(--cz-hair); }
|
|
407
|
+
.vzr-a.y { background: var(--cz-blueink) !important; color: var(--cz-card); box-shadow: none; }
|
|
408
|
+
@media (max-width: 640px) { .vzr-art .vzr-crop { --mh: 240px; } .vzr-big { width: 48px; height: 48px; } }
|
|
409
|
+
|
|
312
410
|
/* one Replay at a time: the phone's below 640px, else the desk's */
|
|
313
411
|
.vzp.vzp-mb { display: none; }
|
|
314
412
|
@media (max-width: 640px) {
|
|
@@ -317,7 +317,8 @@ export function replayModel(rep) {
|
|
|
317
317
|
const stretches = (Array.isArray(rep.runs) ? rep.runs : []).filter((x) => Number.isFinite(x.from_ms) && Number.isFinite(x.to_ms) && x.to_ms > x.from_ms).map((x) => { const t = topics.find((y) => y.id === x.topic); return { n: x.n || (t && t.n) || 0, text: t ? t.text : '', from: Math.max(0, x.from_ms), to: Math.min(len, x.to_ms) }; }).filter((x) => x.to > x.from);
|
|
318
318
|
const keys = (Array.isArray(rep.keys) ? rep.keys : []).filter((k) => k && ['decision', 'action', 'question'].includes(k.k)).map((k, i) => ({ id: k.id || 'k' + i, k: k.k, text: clip(k.text, 240), t: Number.isFinite(k.t) ? k.t : null, owner: clip(k.owner, 60), due: k.due || '', by: clip(k.by, 60), state: k.state || '',
|
|
319
319
|
need: !!k.warn || k.state === 'owner' || k.state === 'open' }));
|
|
320
|
-
const shots = (Array.isArray(rep.shots) ? rep.shots : []).filter((x) => Number.isFinite(x.t)).map((x) => ({ t: x.t, why: clip(x.why, 120)
|
|
320
|
+
const shots = (Array.isArray(rep.shots) ? rep.shots : []).filter((x) => Number.isFinite(x.t)).map((x) => ({ t: x.t, why: clip(x.why, 120), w: Number(x.w) || 0, h: Number(x.h) || 0,
|
|
321
|
+
...(x.jev && (x.jev.kind === 'content' || x.jev.kind === 'faces') ? { jev: { kind: x.jev.kind, ...(Array.isArray(x.jev.box) && x.jev.box.length === 4 ? { box: x.jev.box.map(Number) } : {}) } } : {}) })).sort((a, b) => a.t - b.t);
|
|
321
322
|
/* PMOS's notes (the meeting companion's, from the Focus file), the gaps where the recording was off, and the line each key point was caught from */
|
|
322
323
|
const pmos = (Array.isArray(rep.notes) ? rep.notes : []).filter((x) => x && Number.isFinite(x.t) && String(x.body || '').trim()).map((x) => ({ t: Math.max(0, Math.min(len, x.t)), cat: clip(x.cat, 30), body: clip(x.body, 4000) })).sort((a, b) => a.t - b.t);
|
|
323
324
|
const gaps = (Array.isArray(rep.gaps) ? rep.gaps : []).filter((g) => Number.isFinite(g.from_ms) && Number.isFinite(g.to_ms) && g.to_ms > g.from_ms).map((g) => ({ from: g.from_ms, to: Math.min(len, g.to_ms) }));
|