create-pmos 0.47.0 → 0.49.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 +1 -1
- package/payload/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +14 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/lib/route.mjs +95 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +104 -19
- package/payload/mods/pmos-cockpit/types/index.d.ts +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +214 -0
- package/payload/scripts/workflow-state.js +59 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -2
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/index.md +2 -1
- package/workspace/guide/initiatives-and-next-steps.md +17 -2
- package/workspace/guide/knowledge-graph.md +94 -1
- package/workspace/guide/layout.md +31 -2
- package/workspace/guide/live-notes.md +78 -0
- package/workspace/guide/proactive-engine.md +6 -2
- package/workspace/guide/validate-with-typesafe.md +31 -2
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +214 -0
- package/workspace/lib/engine/workflow-state.js +59 -2
- package/workspace/lib/jev-categorize.js +433 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-live.js +37 -0
- package/workspace/lib/jev-route.js +139 -0
- package/workspace/lib/kg-cli.js +3 -1
- package/workspace/lib/kg.js +6 -1
- package/workspace/lib/learn.js +5 -3
- package/workspace/lib/live-notes.js +1035 -0
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +78 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +279 -18
- package/workspace/public/pm/panel-cockpit.js +638 -0
- package/workspace/public/pm/panel-detail.js +212 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +249 -29
- package/workspace/public/pm/state.js +48 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-livenotes.js +397 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +349 -7
- package/workspace/skills/live-notes.skill +58 -0
|
@@ -23,23 +23,36 @@ const ROW = 'display:block;width:100%;padding:7px 14px;min-height:40px;text-alig
|
|
|
23
23
|
const BTN = 'font-family:var(--mono);font-size:10.5px;padding:3px 9px;border:1px solid var(--border);color:var(--muted-foreground);min-height:26px';
|
|
24
24
|
const NOTE = 'padding:8px 14px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5';
|
|
25
25
|
|
|
26
|
-
/* one cached read per endpoint; `reload(key)` drops it so the next render refetches
|
|
26
|
+
/* one cached read per endpoint; `reload(key)` drops it so the next render refetches.
|
|
27
|
+
*
|
|
28
|
+
* AN ANSWER OLDER THAN A RELOAD IS DROPPED. `reload` follows a write (a saved setting, a dismissed
|
|
29
|
+
* item). A read that was already in flight when the write went out carries the state from BEFORE it,
|
|
30
|
+
* and it used to land after the fresh read and overwrite it — then count as fresh for 15 s. Measured
|
|
31
|
+
* on Today: quiet hours saved, the note shown, and 1.5 s later the note gone while the server held
|
|
32
|
+
* the setting (pm-r22-test.js, about 1 run in 8). `gen` counts reloads per key; a response whose
|
|
33
|
+
* generation is out of date is not written. */
|
|
27
34
|
const cache = new Map();
|
|
35
|
+
const gen = new Map();
|
|
28
36
|
function read(key, url) {
|
|
29
37
|
const hit = cache.get(key);
|
|
30
38
|
if (hit && (hit.state !== 'ok' || Date.now() - hit.at < 15000)) return hit;
|
|
31
39
|
if (!hit || hit.state === 'ok') {
|
|
32
40
|
const next = { state: hit ? hit.state : 'loading', data: hit ? hit.data : null, at: hit ? hit.at : 0 };
|
|
33
41
|
cache.set(key, next);
|
|
42
|
+
const g = gen.get(key) || 0;
|
|
43
|
+
const current = () => (gen.get(key) || 0) === g;
|
|
34
44
|
fetch(url).then(readJson).then(
|
|
35
|
-
(data) => { cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
|
|
36
|
-
(e) => { cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
|
|
45
|
+
(data) => { if (!current()) return; cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
|
|
46
|
+
(e) => { if (!current()) return; cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
|
|
37
47
|
);
|
|
38
48
|
return next;
|
|
39
49
|
}
|
|
40
50
|
return hit;
|
|
41
51
|
}
|
|
42
|
-
const reload = (key) => cache.delete(key);
|
|
52
|
+
const reload = (key) => { gen.set(key, (gen.get(key) || 0) + 1); cache.delete(key); };
|
|
53
|
+
/* refetch on the next render but keep showing the last answer meanwhile, so a list does not blank while it updates.
|
|
54
|
+
* It follows a write too, so it bumps the generation like `reload`: a read already in flight is older than the write. */
|
|
55
|
+
const refresh = (key) => { gen.set(key, (gen.get(key) || 0) + 1); const h = cache.get(key); if (h && h.state === 'ok') h.at = 0; else cache.delete(key); };
|
|
43
56
|
/* A route this server does not have answers 404 with the plain text `not found`. Parsing that as JSON
|
|
44
57
|
* showed the PM "Unexpected token 'o', \"not found\" is not valid JSON" — true, and no help. The usual
|
|
45
58
|
* cause is a server still running code older than the page, so say that and the one command. */
|
|
@@ -275,7 +288,8 @@ async function memDo(body) {
|
|
|
275
288
|
const out = await post('/api/memory', { ws, ...body });
|
|
276
289
|
memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
|
|
277
290
|
if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
|
|
278
|
-
reload
|
|
291
|
+
// the list caches its read per workspace, so the reload names that key; a bare 'memory' left the old list on screen for 15s
|
|
292
|
+
reload('memory:' + ws); reload('memory'); setState({});
|
|
279
293
|
}
|
|
280
294
|
const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
|
|
281
295
|
let dataMod = null;
|
|
@@ -494,10 +508,198 @@ export function skillDetailLive(det) {
|
|
|
494
508
|
|
|
495
509
|
/* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
|
|
496
510
|
function flatten(node, out, depth) {
|
|
497
|
-
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth });
|
|
511
|
+
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth, category: d.category || null, basis: d.category_basis || null, why: d.category_why || null, kind: d.category_kind || null, jev: d.category_jev || null, disagrees: d.category_disagrees || [] });
|
|
498
512
|
if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
|
|
499
513
|
return out;
|
|
500
514
|
}
|
|
515
|
+
/* jev-doc-categorizer Run 1 — each document's category, derived locally beside its own `type`, with the basis that decided it.
|
|
516
|
+
* `undetermined` is its own value: the file has no evidence, and it is shown as that, never as `other`. A press on a category
|
|
517
|
+
* narrows the list; a second press on it shows everything again. Nothing is written. */
|
|
518
|
+
let knCat = '';
|
|
519
|
+
const catTitle = (it) => it.category ? 'category ' + it.category + (it.kind ? ' (' + it.kind + ')' : '') + (it.basis ? ' · decided by ' + it.basis : ' · no evidence') + (it.why ? ' (' + it.why + ')' : '')
|
|
520
|
+
+ (it.disagrees && it.disagrees.length ? ' · ' + it.disagrees.map((d) => d.basis + ' says ' + d.category).join(', ') : '') + ' · advisory' : '';
|
|
521
|
+
const catChip = (it) => it.category ? el('span', { 'data-cat': it.category, 'data-cat-basis': it.basis || '', title: catTitle(it), style: 'margin-left:8px;font-size:9.5px;letter-spacing:.06em;color:var(--muted-foreground)' }, iv('· ' + it.category + (it.kind ? ' (' + it.kind + ')' : '') + (it.basis === 'jev' ? ' (jev)' : it.jev === 'unsure' ? ' (jev unsure)' : ''))) : null;
|
|
522
|
+
function categoryBar(trees) {
|
|
523
|
+
const counts = {};
|
|
524
|
+
for (const t of trees) for (const it of flatten(t, [], 0)) if (it.path && it.category) counts[it.category] = (counts[it.category] || 0) + 1;
|
|
525
|
+
const names = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
|
|
526
|
+
if (!names.length) return null;
|
|
527
|
+
return el('div', { 'data-cat-bar': '1', style: 'display:flex;flex-wrap:wrap;gap:4px;padding:2px 14px 6px' },
|
|
528
|
+
names.map((n) => el('button', { 'data-cat-filter': n, 'aria-pressed': knCat === n ? 'true' : 'false', title: 'show only ' + n + ' documents', onClick: () => { knCat = knCat === n ? '' : n; setState({}); },
|
|
529
|
+
style: 'font-family:var(--mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);' + (knCat === n ? 'background:var(--accent);' : '') }, iv(n + ' ' + counts[n]))));
|
|
530
|
+
}
|
|
531
|
+
/* D82 — Jev on documents the local rules leave undetermined. The switch is off until the PM turns it on; the first run shows
|
|
532
|
+
* its request count and sends only after the PM confirms that number; afterwards opening this list sends only new or changed
|
|
533
|
+
* files. Jev's categories are marked "(jev)" and are advisory. */
|
|
534
|
+
let dPlan = null; // the plan the PM is looking at: { ws, plan }
|
|
535
|
+
let dNote = null;
|
|
536
|
+
const dSay = (text) => { dNote = text; setState({}); };
|
|
537
|
+
async function dSwitch(ws, on) {
|
|
538
|
+
const out = await post('/api/doccat/jev', { ws, categorize: on });
|
|
539
|
+
reload('library:' + ws); dPlan = null;
|
|
540
|
+
dSay(out && out.ok ? (on ? 'categorizing with Jev is on — review the first run’s count, then confirm it; nothing is sent before that' : 'categorizing with Jev is off') : ((out && out.error) || 'not saved'));
|
|
541
|
+
}
|
|
542
|
+
async function dResume(ws) {
|
|
543
|
+
const out = await post('/api/doccat/jev/resume', { ws });
|
|
544
|
+
reload('library:' + ws);
|
|
545
|
+
dSay(out && out.ok ? 'categorizing with Jev resumed' : ((out && out.error) || 'not resumed'));
|
|
546
|
+
}
|
|
547
|
+
async function dPlanFor(ws) {
|
|
548
|
+
const out = await post('/api/doccat/jev/run', { ws });
|
|
549
|
+
if (!out || out.ok === false) { dPlan = null; return dSay((out && out.error) || 'no plan'); }
|
|
550
|
+
dPlan = { ws, plan: out.plan }; dSay('review the count, then confirm to send');
|
|
551
|
+
}
|
|
552
|
+
async function dRun() {
|
|
553
|
+
if (!dPlan) return;
|
|
554
|
+
const { ws, plan } = dPlan;
|
|
555
|
+
dSay('sending ' + plan.requests + ' request(s) to ' + plan.host + ' …');
|
|
556
|
+
const out = await post('/api/doccat/jev/run', { ws, confirm: true, expect_requests: plan.requests, expect_fingerprint: plan.fingerprint });
|
|
557
|
+
dPlan = null; reload('library:' + ws);
|
|
558
|
+
dSay(!out || out.ok === false ? ((out && out.error) || 'the run failed') : 'asked ' + out.sent + ' · ' + out.decided + ' categorized · ' + out.unsure + ' unsure · ' + (out.unreadable || 0) + ' unreadable · ' + out.cached + ' answered from the cache · ' + out.failed + ' failed');
|
|
559
|
+
}
|
|
560
|
+
/* Run 4 — what the last pass did to each undetermined file, so the PM can see which ones Jev categorized, which stayed
|
|
561
|
+
* undetermined and why, and which failed. Problems first. The record is the server's (per user, paths and outcomes only). */
|
|
562
|
+
let dFilesAll = false;
|
|
563
|
+
const D_ORDER = ['failed', 'not-asked', 'unreadable', 'unsure', 'withheld', 'excluded', 'categorized'];
|
|
564
|
+
const D_LABEL = { failed: 'failed', 'not-asked': 'not asked', unreadable: 'unreadable', unsure: 'unsure', withheld: 'withheld', excluded: 'excluded', categorized: 'categorized' };
|
|
565
|
+
const D_TONE = { failed: 'var(--destructive)', 'not-asked': 'var(--destructive)', unreadable: 'var(--destructive)', categorized: 'var(--foreground)' };
|
|
566
|
+
const KEEPS = ' The file keeps the local result: undetermined.';
|
|
567
|
+
function dOutcomeWords(f) {
|
|
568
|
+
const cached = f.cached ? ' Answer from the cache.' : '';
|
|
569
|
+
if (f.outcome === 'categorized') return 'Jev: ' + (f.category === 'ticket' ? 'ticket (' + (f.ticket_kind || 'other') + ')' : f.category) + '. ' + (f.sentence || '') + cached;
|
|
570
|
+
if (f.outcome === 'unsure') return 'Jev was unsure (' + (f.leading || []).join(' or ') + ').' + KEEPS + cached;
|
|
571
|
+
if (f.outcome === 'unreadable') return 'Jev’s answer could not be read.' + KEEPS + ' It is asked again only when the file changes.' + cached;
|
|
572
|
+
if (f.outcome === 'failed') return 'The request failed: ' + (f.reason || 'no reason given') + '.' + KEEPS + ' It is asked again on the next pass.';
|
|
573
|
+
if (f.outcome === 'withheld') return 'Not sent: the credential scan matched (' + (f.rules || []).join(', ') + ').';
|
|
574
|
+
if (f.outcome === 'excluded') return 'Not sent: the path is excluded.';
|
|
575
|
+
if (f.outcome === 'not-asked') return 'Not asked: ' + (f.reason || 'the pass stopped first');
|
|
576
|
+
return f.outcome;
|
|
577
|
+
}
|
|
578
|
+
function dLibraryLine(trees) {
|
|
579
|
+
let local = 0, jev = 0, undet = 0;
|
|
580
|
+
for (const t of trees || []) for (const it of flatten(t, [], 0)) {
|
|
581
|
+
if (!it.path || !it.category) continue;
|
|
582
|
+
if (it.category === 'undetermined') undet++; else if (it.basis === 'jev') jev++; else local++;
|
|
583
|
+
}
|
|
584
|
+
if (!local && !jev && !undet) return null;
|
|
585
|
+
return el('div', { 'data-doccat': 'library-line', style: NOTE }, iv('In this list: ' + local + ' decided by the local rules or your corrections · ' + jev + ' categorized by Jev · ' + undet + ' undetermined.'));
|
|
586
|
+
}
|
|
587
|
+
function dResultsBlock(ws, res) {
|
|
588
|
+
if (!res || !Array.isArray(res.files)) return [];
|
|
589
|
+
const counts = {};
|
|
590
|
+
for (const f of res.files) counts[f.outcome] = (counts[f.outcome] || 0) + 1;
|
|
591
|
+
const parts = D_ORDER.filter((k) => counts[k]).map((k) => counts[k] + ' ' + D_LABEL[k]);
|
|
592
|
+
const when = String(res.at || '').replace('T', ' ').slice(0, 16);
|
|
593
|
+
const out = [el('div', { 'data-doccat': 'results-summary', style: NOTE + ';color:var(--foreground)' },
|
|
594
|
+
iv('Last Jev pass (' + (res.kind === 'confirmed' ? 'confirmed by you' : 'automatic') + ', ' + when + '), ' + res.total + ' undetermined file(s): ' + (parts.join(' · ') || 'none') + '.'
|
|
595
|
+
+ (res.total > res.files.length ? ' Only the first ' + res.files.length + ' are listed.' : '')))];
|
|
596
|
+
const rows = res.files.slice().sort((a, b) => D_ORDER.indexOf(a.outcome) - D_ORDER.indexOf(b.outcome) || a.doc.localeCompare(b.doc));
|
|
597
|
+
const shown = dFilesAll ? rows : rows.slice(0, 50);
|
|
598
|
+
for (const f of shown) {
|
|
599
|
+
out.push(el('div', { 'data-doccat-file': f.doc, 'data-outcome': f.outcome, style: 'display:flex;gap:8px;align-items:baseline;padding:2px 14px 2px 20px;font-size:11px' },
|
|
600
|
+
el('span', { 'data-doccat-outcome': f.outcome, style: 'flex:none;width:76px;font-family:var(--mono);font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:' + (D_TONE[f.outcome] || 'var(--muted-foreground)') }, iv(D_LABEL[f.outcome] || f.outcome)),
|
|
601
|
+
el('span', { style: 'flex:1 1 auto;min-width:0' },
|
|
602
|
+
el('button', { style: 'font-family:var(--mono);font-size:11px;text-decoration:underline;color:var(--foreground);text-align:left', onClick: () => openDocIn(null, f.doc) }, iv(f.doc)),
|
|
603
|
+
el('span', { 'data-doccat-why': '1', style: 'display:block;color:var(--muted-foreground)' }, iv(dOutcomeWords(f))))));
|
|
604
|
+
}
|
|
605
|
+
if (rows.length > 50) out.push(el('div', { style: 'padding:2px 14px 6px 20px' },
|
|
606
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-doccat': 'files-all', onClick: () => { dFilesAll = !dFilesAll; setState({}); } }, dFilesAll ? 'show the first 50' : 'show all ' + rows.length)));
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function doccatBlock(ws, j, trees) {
|
|
610
|
+
if (!ws || !j) return [];
|
|
611
|
+
const kids = [];
|
|
612
|
+
kids.push(el('div', { 'data-doccat': 'explain', style: NOTE }, iv('Files with no local evidence are “undetermined”. Jev can be asked to categorize them from a short snippet: the title, the headings and the opening, never the whole file. Its answers are marked (jev), are advisory, and an unsure answer stays undetermined.')));
|
|
613
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
614
|
+
el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-doccat': 'switch', onClick: () => dSwitch(ws, !j.switch) },
|
|
615
|
+
iv(j.switch ? 'categorize with Jev: ON — turn off' : 'categorize with Jev: OFF — turn on')),
|
|
616
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', disabled: j.open ? null : 'true', 'data-doccat': 'plan',
|
|
617
|
+
title: 'Shows how many requests the undetermined files would take, and sends only after you confirm that number.', onClick: () => dPlanFor(ws) }, 'ask Jev about undetermined files'),
|
|
618
|
+
dPlan && dPlan.ws === ws ? el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', 'data-doccat': 'run', onClick: dRun }, 'confirm and send ' + dPlan.plan.requests + ' request(s)') : null));
|
|
619
|
+
kids.push(el('div', { 'data-doccat': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
|
|
620
|
+
iv(j.open ? (j.confirmed ? 'Active: new or changed undetermined files are sent when the workspace reads this repository’s documents (when it starts, and when this list opens), at most once a minute.' : 'Ready: nothing is sent until you confirm the first run’s count.') : 'Not active: ' + j.sentence + ' All three must hold before anything is sent.')));
|
|
621
|
+
if (j.paused) kids.push(el('div', { 'data-doccat': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
|
|
622
|
+
el('span', null, iv(j.paused.reason)),
|
|
623
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-doccat': 'resume', onClick: () => dResume(ws) }, 'resume')));
|
|
624
|
+
const l = j.last;
|
|
625
|
+
if (l && l.waiting) kids.push(el('div', { 'data-doccat': 'waiting', style: NOTE + ';color:var(--foreground)' }, iv(l.waiting + ' new or changed file(s) are waiting: more than an automatic pass sends. Ask Jev and confirm the count to send them.')));
|
|
626
|
+
else if (l && !j.results) kids.push(el('div', { 'data-doccat': 'last', style: NOTE }, iv('Last Jev pass ' + String(l.at).replace('T', ' ').slice(0, 16) + ': sent ' + l.sent + ' · categorized ' + l.decided + ' · unsure ' + l.unsure + ' · unreadable ' + (l.unreadable || 0) + ' · withheld ' + (l.withheld_count || 0) + ' · excluded ' + l.excluded + ' · failed ' + l.failed + '.')));
|
|
627
|
+
if (dPlan && dPlan.ws === ws) {
|
|
628
|
+
const p = dPlan.plan;
|
|
629
|
+
kids.push(el('div', { 'data-doccat': 'plan-text', style: NOTE }, iv(p.requests + ' request(s), about ' + p.characters.toLocaleString() + ' characters, to ' + p.host + ' · ' + p.candidates + ' undetermined file(s) · ' + p.cached + ' already answered · ' + p.excluded + ' excluded · ' + p.withheld_count + ' withheld by the credential scan.')));
|
|
630
|
+
}
|
|
631
|
+
if (dNote) kids.push(el('div', { 'data-doccat': 'note', style: NOTE }, iv(dNote)));
|
|
632
|
+
const line = dLibraryLine(trees); if (line) kids.push(line);
|
|
633
|
+
kids.push(...dResultsBlock(ws, j.results));
|
|
634
|
+
return kids;
|
|
635
|
+
}
|
|
636
|
+
/* jev-doc-categorizer Run 3 (C7) — the PM corrects a category. `fix` on a row opens a small control: a category, a ticket kind for
|
|
637
|
+
* a ticket, `save correction` and `clear correction`. A correction is kept per user, outside the repository, and never edits the
|
|
638
|
+
* file. `write it into the file` is a separate press: it shows the one-line diff first, and `apply` writes it only if the file is
|
|
639
|
+
* still the text the diff was made from (the server compares the sha1 and answers 409 when it is not). The server checks every
|
|
640
|
+
* value again; these two lists only fill the menus. */
|
|
641
|
+
const CATS = ['prd', 'meeting-notes', 'report', 'ticket', 'knowledge', 'spec-artifact', 'other'];
|
|
642
|
+
const KINDS = ['design', 'engineering', 'other'];
|
|
643
|
+
let knFix = null; // { path, category, ticket_kind } — the row whose control is open
|
|
644
|
+
let knPlan = null; // { path, category, ticket_kind, diff, sha, unchanged } — the write-back diff on screen
|
|
645
|
+
let knNote = null; // { path, tone, text }
|
|
646
|
+
let knBusy = false;
|
|
647
|
+
const FIX = 'font-family:var(--mono);font-size:9.5px;padding:1px 6px;margin-right:10px;border:1px solid var(--border);color:var(--muted-foreground);flex:none';
|
|
648
|
+
const SEL = 'font-family:var(--mono);font-size:10.5px;padding:2px 4px;border:1px solid var(--border);background:var(--background);color:var(--foreground)';
|
|
649
|
+
const knSay = (path, tone, text) => { knNote = { path, tone, text }; knBusy = false; setState({}); };
|
|
650
|
+
function knOpen(it) {
|
|
651
|
+
if (knFix && knFix.path === it.path) { knFix = null; knPlan = null; knNote = null; setState({}); return; }
|
|
652
|
+
const cat = CATS.includes(it.category) ? it.category : 'other';
|
|
653
|
+
knFix = { path: it.path, category: cat, ticket_kind: cat === 'ticket' ? (it.kind || 'other') : null };
|
|
654
|
+
knPlan = null; knNote = null; setState({});
|
|
655
|
+
}
|
|
656
|
+
async function knSave(ws, clear) {
|
|
657
|
+
if (!knFix || knBusy) return;
|
|
658
|
+
knBusy = true; setState({});
|
|
659
|
+
const body = clear ? { ws, path: knFix.path, clear: true } : { ws, path: knFix.path, category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
|
|
660
|
+
const res = await post('/api/doccat/correct', body);
|
|
661
|
+
refresh('library:' + ws);
|
|
662
|
+
if (res && res.ok) knSay(knFix.path, 'ok', clear ? (res.cleared ? 'correction cleared — the rules decide this file again' : 'this file had no correction') : 'saved — your correction wins over every rule. The file is not changed.');
|
|
663
|
+
else knSay(knFix.path, 'bad', (res && res.error) || 'not saved');
|
|
664
|
+
}
|
|
665
|
+
async function knWriteback(ws, confirm) {
|
|
666
|
+
if (!knFix || knBusy) return;
|
|
667
|
+
knBusy = true; setState({});
|
|
668
|
+
const want = { category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
|
|
669
|
+
if (!confirm) {
|
|
670
|
+
const res = await post('/api/doccat/writeback', { ws, path: knFix.path, ...want });
|
|
671
|
+
if (res && res.ok) { knPlan = { path: knFix.path, ...want, diff: res.diff, sha: res.sha, next_sha: res.next_sha, unchanged: !!res.unchanged }; knSay(knFix.path, 'mut', res.unchanged ? 'the file already says this — nothing to write' : 'this is the change. Press apply to write it into the file.'); }
|
|
672
|
+
else { knPlan = null; knSay(knFix.path, 'bad', (res && res.error) || 'no diff'); }
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!knPlan) { knBusy = false; return; }
|
|
676
|
+
const res = await post('/api/doccat/writeback', { ws, path: knPlan.path, category: knPlan.category, ticket_kind: knPlan.ticket_kind, confirm: true, expect_sha: knPlan.sha, expect_next_sha: knPlan.next_sha });
|
|
677
|
+
knPlan = null;
|
|
678
|
+
refresh('library:' + ws);
|
|
679
|
+
if (res && res.ok) knSay(knFix.path, 'ok', res.written ? 'written into the file\'s frontmatter' : 'the file already says this — nothing was written');
|
|
680
|
+
else knSay(knFix.path, 'bad', (res && res.error) || 'not written');
|
|
681
|
+
}
|
|
682
|
+
function knPanel(ws, it) {
|
|
683
|
+
const f = knFix;
|
|
684
|
+
const pad = 'padding:2px 14px 8px ' + (20 + it.depth * 12) + 'px';
|
|
685
|
+
const btn = (attr, label, fn, disabled) => el('button', { [attr]: '1', disabled: knBusy || disabled, onClick: fn, style: BTN, hover: 'color:var(--foreground)' }, label);
|
|
686
|
+
const kids = [
|
|
687
|
+
el('select', { 'data-cat-select': '1', 'aria-label': 'category', style: SEL, onChange: (e) => { f.category = e.target.value; f.ticket_kind = f.category === 'ticket' ? (f.ticket_kind || 'other') : null; knPlan = null; setState({}); } },
|
|
688
|
+
CATS.map((c) => el('option', { value: c, selected: c === f.category }, c))),
|
|
689
|
+
f.category === 'ticket' ? el('select', { 'data-cat-kind': '1', 'aria-label': 'ticket kind', style: SEL, onChange: (e) => { f.ticket_kind = e.target.value; knPlan = null; setState({}); } },
|
|
690
|
+
KINDS.map((k) => el('option', { value: k, selected: k === f.ticket_kind }, k))) : null,
|
|
691
|
+
btn('data-cat-save', 'save correction', () => knSave(ws, false)),
|
|
692
|
+
it.basis === 'pm correction' ? btn('data-cat-clear', 'clear correction', () => knSave(ws, true)) : null,
|
|
693
|
+
btn('data-cat-writeback', 'write it into the file', () => knWriteback(ws, false)),
|
|
694
|
+
];
|
|
695
|
+
const out = [el('div', { 'data-cat-panel': it.path, style: pad + ';display:flex;flex-wrap:wrap;gap:6px;align-items:center' }, kids)];
|
|
696
|
+
if (knPlan && knPlan.path === it.path && !knPlan.unchanged) {
|
|
697
|
+
out.push(el('pre', { 'data-cat-diff': '1', style: pad.replace('padding:2px', 'margin:0;padding:6px') + ';font-family:var(--mono);font-size:10.5px;white-space:pre-wrap;color:var(--foreground);background:var(--accent)' }, iv(knPlan.diff)));
|
|
698
|
+
out.push(el('div', { style: pad + ';display:flex;gap:6px' }, btn('data-cat-apply', 'apply', () => knWriteback(ws, true)), btn('data-cat-cancel', 'cancel', () => { knPlan = null; knNote = null; setState({}); })));
|
|
699
|
+
}
|
|
700
|
+
if (knNote && knNote.path === it.path) out.push(el('div', { 'data-cat-note': knNote.tone, style: pad + ';font-size:11px;color:' + TONE[knNote.tone] }, iv(knNote.text)));
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
501
703
|
export function knowledgeLive() {
|
|
502
704
|
const ws = state.wsId || '';
|
|
503
705
|
const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
|
|
@@ -505,11 +707,18 @@ export function knowledgeLive() {
|
|
|
505
707
|
if (st) return [st];
|
|
506
708
|
const trees = (r.data && r.data.knowledge) || [];
|
|
507
709
|
const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
|
|
710
|
+
const bar = categoryBar(trees); if (bar) kids.push(bar);
|
|
711
|
+
kids.push(...doccatBlock(ws, r.data && r.data.doccat, trees));
|
|
508
712
|
for (const t of trees) {
|
|
509
713
|
kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
|
|
510
714
|
for (const it of flatten(t, [], 0)) {
|
|
715
|
+
if (knCat && (it.dir || it.category !== knCat)) continue;
|
|
511
716
|
if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
|
|
512
|
-
|
|
717
|
+
const open = !!(knFix && knFix.path === it.path);
|
|
718
|
+
kids.push(el('div', { 'data-cat-row': it.path, style: 'display:flex;align-items:center' },
|
|
719
|
+
el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;flex:1 1 auto;min-width:0;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11.5px', hover: 'background:var(--accent)', 'data-r4': 'know' }, iv(it.name), catChip(it)),
|
|
720
|
+
el('button', { 'data-cat-fix': it.path, 'aria-expanded': open ? 'true' : 'false', title: 'correct the category of this document', onClick: () => knOpen(it), style: FIX + (open ? ';background:var(--accent);color:var(--foreground)' : ''), hover: 'color:var(--foreground)' }, 'fix')));
|
|
721
|
+
if (open) kids.push(...knPanel(ws, it));
|
|
513
722
|
}
|
|
514
723
|
}
|
|
515
724
|
// R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
|
|
@@ -557,6 +766,14 @@ async function vConsent(on) {
|
|
|
557
766
|
reload('vrepo'); vPlan = null;
|
|
558
767
|
vSay(out && out.ok ? 'ok' : 'bad', out && out.ok ? (on ? 'validation is on for this repository' : 'validation is off; nothing from this repository will be sent') : ((out && out.error) || 'not saved'));
|
|
559
768
|
}
|
|
769
|
+
/* prompt-model-routing Run 3 — the third D84 gate: route this repository's prompts with Jev. With it on (and the key and
|
|
770
|
+
* consent above), the cockpit mod in each terminal switches the model for a prompt when Jev decides its tier. */
|
|
771
|
+
async function vRoute(on) {
|
|
772
|
+
const ws = vWs(); if (!ws) return vSay('bad', 'select a repository first');
|
|
773
|
+
const out = await post('/api/model/route/switch', { ws, route: on });
|
|
774
|
+
reload('vrepo');
|
|
775
|
+
vSay(out && out.ok ? 'ok' : 'bad', out && out.ok ? (on ? (out.open ? 'routing prompts with Jev is on: each prompt is checked before it runs' : 'routing prompts with Jev is on, but not active yet: ' + out.sentence) : 'routing prompts with Jev is off; no prompt is sent') : ((out && out.error) || 'not saved'));
|
|
776
|
+
}
|
|
560
777
|
async function vExclude(text) {
|
|
561
778
|
const ws = vWs(); if (!ws) return;
|
|
562
779
|
const exclude = String(text || '').split(/[\n,]/).map((x) => x.trim()).filter(Boolean);
|
|
@@ -614,6 +831,12 @@ async function jSwitch(on) {
|
|
|
614
831
|
reload('claims'); jPlan = null;
|
|
615
832
|
jSay(out && out.ok ? (on ? 'checking with Jev automatically is on — the first check records what exists and sends nothing; after that only changed pairs are sent' : 'checking with Jev automatically is off') : ((out && out.error) || 'not saved'));
|
|
616
833
|
}
|
|
834
|
+
async function jStatus(on) {
|
|
835
|
+
const ws = vWs(); if (!ws) return;
|
|
836
|
+
const out = await post('/api/claims/jev/status', { ws, status_read: on });
|
|
837
|
+
reload('claims'); jPlan = null;
|
|
838
|
+
jSay(out && out.ok ? (on ? 'reading documents for status is on — the next check records the documents that exist and sends nothing for them; after that only changed documents are sent' : 'reading documents for status is off') : ((out && out.error) || 'not saved'));
|
|
839
|
+
}
|
|
617
840
|
async function jResume() {
|
|
618
841
|
const ws = vWs(); if (!ws) return;
|
|
619
842
|
const out = await post('/api/claims/jev/resume', { ws });
|
|
@@ -648,7 +871,11 @@ function jevBlock(ws, d) {
|
|
|
648
871
|
kids.push(el('div', { 'data-r24': 'explain', style: NOTE }, iv('Where the local checks cannot decide, a pair of short texts (a requirement and the rubric’s dimensions, a story section and the requirement it cites, a sentence and the decision it cites, two sections of one PRD) can be put to Jev. Only pairs that changed are sent. Jev’s answers are suspicions: advisory, shown in words, and never counted as problems.')));
|
|
649
872
|
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
650
873
|
el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r24': 'switch', onClick: () => jSwitch(!j.switch) },
|
|
651
|
-
iv(j.switch ? 'check with Jev automatically: ON — turn off' : 'check with Jev automatically: OFF — turn on'))
|
|
874
|
+
iv(j.switch ? 'check with Jev automatically: ON — turn off' : 'check with Jev automatically: OFF — turn on')),
|
|
875
|
+
el('button', { style: BTN + (j.status_read ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-r24': 'status-switch',
|
|
876
|
+
title: 'Also send the documents tied to each initiative (its PRD, rubric and brief, and notes that name it), to ask whether a document is a stub and whether a note contradicts a requirement. The answers are suggestions in the initiative detail. Needs the switch on the left.',
|
|
877
|
+
onClick: () => jStatus(!j.status_read) },
|
|
878
|
+
iv(j.status_read ? 'read documents for status: ON — turn off' : 'read documents for status: OFF — turn on'))));
|
|
652
879
|
kids.push(el('div', { 'data-r24': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
|
|
653
880
|
iv(j.open ? 'Active: a key is saved, validation is on for this repository, automatic checking is on and this switch is on.' : 'Not active: ' + j.sentence + ' All four must hold before anything is sent.')));
|
|
654
881
|
if (j.paused) kids.push(el('div', { 'data-r24': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
|
|
@@ -763,6 +990,10 @@ export function validateList() {
|
|
|
763
990
|
kids.push(el('div', { style: 'display:flex;gap:8px;align-items:center;padding:4px 14px' },
|
|
764
991
|
el('span', { style: 'font-size:12px', 'data-r19': 'consent-state' }, iv(on ? 'validation is ON for this repository' : 'validation is OFF — nothing from this repository is sent')),
|
|
765
992
|
el('button', { style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', 'data-r19': 'consent-toggle', onClick: () => vConsent(!on) }, on ? 'turn off' : 'turn on')));
|
|
993
|
+
const ro = !!(rp.data && rp.data.route);
|
|
994
|
+
kids.push(el('div', { style: 'display:flex;gap:8px;align-items:center;padding:4px 14px' },
|
|
995
|
+
el('span', { style: 'font-size:12px', 'data-route': 'state' }, iv(ro ? 'route prompts with Jev: ON — each prompt typed in a workspace terminal is checked, and the model switches when Jev decides' : 'route prompts with Jev: OFF — no prompt is sent')),
|
|
996
|
+
el('button', { style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', 'data-route': 'toggle', onClick: () => vRoute(!ro) }, ro ? 'turn off' : 'turn on')));
|
|
766
997
|
const ta = el('textarea', { rows: 2, placeholder: 'paths never sent, one per line (e.g. private/ or notes/*.md)', 'data-r19': 'exclude-input',
|
|
767
998
|
style: 'width:calc(100% - 28px);margin:4px 14px 0;font-family:var(--mono);font-size:10.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, (rp.data.exclude || []).join('\n'));
|
|
768
999
|
kids.push(ta, el('div', { style: 'padding:4px 14px 6px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r19': 'exclude-save', onClick: () => vExclude(ta.value) }, 'save excluded paths')));
|
|
@@ -810,6 +1041,35 @@ export function validateList() {
|
|
|
810
1041
|
/* ── Today (proactive-engine D1): what is waiting, computed from the ledgers, with feedback ─────────────────────── */
|
|
811
1042
|
let tNote = null;
|
|
812
1043
|
let tMissed = false;
|
|
1044
|
+
/* The text inputs of the Today list are kept across redraws. The list is drawn again on every state change (the 4-second retry of
|
|
1045
|
+
* an unread repository is one), and a new input on each draw threw away what the person was typing and the caret with it. Each
|
|
1046
|
+
* input is made once; a redraw writes the stored value into it only while the person has not edited it since the last save. */
|
|
1047
|
+
const tInputs = new Map();
|
|
1048
|
+
function tInput(key, attrs, stored) {
|
|
1049
|
+
let n = tInputs.get(key);
|
|
1050
|
+
if (!n) {
|
|
1051
|
+
n = el('input', { ...attrs, onInput: (e) => { e.target.dataset.dirty = '1'; } });
|
|
1052
|
+
tInputs.set(key, n);
|
|
1053
|
+
}
|
|
1054
|
+
if (!n.dataset.dirty && n.value !== stored) n.value = stored;
|
|
1055
|
+
// The redraw moves the input into a new row, and moving a node blurs it: give the focus and the caret back once the draw is done.
|
|
1056
|
+
if (typeof document !== 'undefined' && document.activeElement === n) {
|
|
1057
|
+
let a = null, b = null;
|
|
1058
|
+
try { a = n.selectionStart; b = n.selectionEnd; } catch { /* a number input has no selection */ }
|
|
1059
|
+
queueMicrotask(() => {
|
|
1060
|
+
if (!n.isConnected || document.activeElement === n) return;
|
|
1061
|
+
n.focus();
|
|
1062
|
+
try { if (a !== null) n.setSelectionRange(a, b); } catch { /* as above */ }
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
return n;
|
|
1066
|
+
}
|
|
1067
|
+
/** Save a settings patch from kept inputs; on success their text is the stored value again. */
|
|
1068
|
+
async function tSaveFrom(inputs, patch) {
|
|
1069
|
+
const out = await tSettings(patch);
|
|
1070
|
+
if (out && out.ok !== false) for (const n of inputs) delete n.dataset.dirty;
|
|
1071
|
+
setState({});
|
|
1072
|
+
}
|
|
813
1073
|
let tRetry = null;
|
|
814
1074
|
const tSay = (tone, text) => { tNote = { tone, text }; setState({}); };
|
|
815
1075
|
const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
|
|
@@ -834,6 +1094,7 @@ async function tSettings(patch) {
|
|
|
834
1094
|
const out = await post('/api/proactive/settings', patch);
|
|
835
1095
|
reload('today'); reload('brief'); reload('prepare'); reload('learn');
|
|
836
1096
|
tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
|
|
1097
|
+
return out;
|
|
837
1098
|
}
|
|
838
1099
|
function tCard(c) {
|
|
839
1100
|
const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
|
|
@@ -1011,7 +1272,7 @@ function learningSection() {
|
|
|
1011
1272
|
kids.push(el('div', { style: NOTE, 'data-r26': 'last-run' }, iv(last ? 'Last weekly review: ' + String(last.at).replace('T', ' ').slice(0, 16) + ' UTC. It runs about every 7 days, only while the workspace server runs, and does not catch up.' : 'No weekly review has run yet. It runs about every 7 days once there is feedback, only while the workspace server runs, and does not catch up.')));
|
|
1012
1273
|
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
1013
1274
|
b('run the weekly review now', 'run', () => lPost('/api/proactive/learn/run', {}, (o) => 'review taken — ' + o.proposed.length + ' lesson(s) proposed'), 'refused while learning is paused'),
|
|
1014
|
-
b('forget what it learned', 'reset', () => lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'), 'earlier feedback stops counting; nothing is deleted')));
|
|
1275
|
+
b('forget what it learned', 'reset', () => { if (typeof window !== 'undefined' && window.confirm && !window.confirm('Forget what it learned? Earlier feedback stops counting. Nothing is deleted.')) return; lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'); }, 'earlier feedback stops counting; nothing is deleted')));
|
|
1015
1276
|
return kids;
|
|
1016
1277
|
}
|
|
1017
1278
|
export function todayList() {
|
|
@@ -1024,9 +1285,9 @@ export function todayList() {
|
|
|
1024
1285
|
el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); reload('learn'); setState({}); } }, '↻ refresh'),
|
|
1025
1286
|
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
|
|
1026
1287
|
if (tMissed) {
|
|
1027
|
-
const ta =
|
|
1288
|
+
const ta = tInput('missed', { type: 'text', placeholder: 'what should it have shown you?', 'data-r22': 'missed-input', style: 'flex:1;min-width:0;font-size:11.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' }, '');
|
|
1028
1289
|
kids.push(el('div', { style: 'display:flex;gap:4px;padding:2px 14px 6px' }, ta,
|
|
1029
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') tMissed = false; setState({}); } }, 'save')));
|
|
1290
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') { tMissed = false; delete ta.dataset.dirty; } setState({}); } }, 'save')));
|
|
1030
1291
|
}
|
|
1031
1292
|
if (tNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE + ';color:' + (tNote.tone === 'bad' ? 'var(--destructive)' : tNote.tone === 'ok' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(tNote.text)));
|
|
1032
1293
|
const st = status(r, 'Today');
|
|
@@ -1053,16 +1314,16 @@ export function todayList() {
|
|
|
1053
1314
|
kids.push(el('div', { style: LABEL }, iv('morning brief')));
|
|
1054
1315
|
for (const k of Object.keys(s.briefSources || {})) kids.push(chk('read ' + k + (k === 'mail' ? ' (titles and senders only)' : ''), s.briefSources[k], 'set-src-' + k, (v) => ({ briefSources: { [k]: v } })));
|
|
1055
1316
|
kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
|
|
1056
|
-
const nm =
|
|
1057
|
-
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () =>
|
|
1317
|
+
const nm = tInput('name', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', 'data-r22': 'set-name', style: 'width:180px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.name || '');
|
|
1318
|
+
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () => tSaveFrom([nm], { name: nm.value }) }, 'save')));
|
|
1058
1319
|
kids.push(...learningSection());
|
|
1059
1320
|
kids.push(chk('pause learning (no weights, no lessons, nothing narrowed)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
|
|
1060
|
-
const days =
|
|
1061
|
-
const qs =
|
|
1062
|
-
const qe =
|
|
1321
|
+
const days = tInput('stalled', { type: 'number', min: '1', max: '90', 'data-r22': 'set-stalled-days', style: 'width:60px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, String(s.stalledDays));
|
|
1322
|
+
const qs = tInput('quiet-start', { type: 'text', placeholder: '22:00', 'data-r22': 'set-quiet-start', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.quiet ? s.quiet.start : '');
|
|
1323
|
+
const qe = tInput('quiet-end', { type: 'text', placeholder: '07:00', 'data-r22': 'set-quiet-end', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.quiet ? s.quiet.end : '');
|
|
1063
1324
|
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px;font-size:12px' }, iv('stalled after'), days, iv('days'),
|
|
1064
|
-
el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () =>
|
|
1325
|
+
el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSaveFrom([days], { stalledDays: Number(days.value) }) }, 'save')));
|
|
1065
1326
|
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px 8px;font-size:12px' }, iv('quiet hours'), qs, iv('to'), qe,
|
|
1066
|
-
el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () =>
|
|
1327
|
+
el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () => tSaveFrom([qs, qe], { quiet: qs.value.trim() || qe.value.trim() ? { start: qs.value.trim(), end: qe.value.trim() } : null }) }, 'save')));
|
|
1067
1328
|
return [el('div', { style: 'padding:2px 0 8px', 'data-r22': 'today' }, kids)];
|
|
1068
1329
|
}
|