create-pmos 0.163.0 → 0.165.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/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/guide/console-calendar.md +17 -0
- package/workspace/guide/settings.md +1 -1
- package/workspace/guide/validate-with-typesafe.md +14 -1
- package/workspace/lib/headless.js +1 -1
- package/workspace/lib/meeting-focus.js +292 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-meeting-focus.js +291 -0
- package/workspace/lib/typesafe-settings.js +48 -15
- package/workspace/lib/typesafe.js +73 -12
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-calendar.js +7 -1
- package/workspace/public/pm/c3-demo-focus.js +133 -0
- package/workspace/public/pm/c3-demo.js +3 -0
- package/workspace/public/pm/c3-focus.css +169 -0
- package/workspace/public/pm/c3-focus.js +587 -0
- package/workspace/public/pm/c3-settings.js +29 -1
- package/workspace/public/pm/c3-shell.js +1 -0
- package/workspace/public/pm/components.css +1 -0
- package/workspace/public/pm/shell-recording.js +18 -2
- package/workspace/replaced-tests.json +11 -0
- package/workspace/server.js +17 -0
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/c3-meeting-focus.json +6 -0
|
@@ -587,19 +587,47 @@ export function mountSettings(host, ctx = {}) {
|
|
|
587
587
|
['Update check', 'Asks the npm registry for the latest version.', val('upd.check') === 'daily' ? 'Once a day' : 'Only when you press Check', val('upd.check') === 'daily' ? 'warn' : ''],
|
|
588
588
|
['Online tunnel', 'Cloudflare carries your pages to your other devices (D89).', o && o.on ? 'On' : 'Off', o && o.on ? 'warn' : ''],
|
|
589
589
|
['Jev (TypeSafe)', 'Note lines and matching passages. Never audio, names or private notes (D79).', M.ws ? (jevOn ? 'On in ' : 'Off in ') + wsName(M.ws) : 'Pick a workspace', jevOn ? 'warn' : ''],
|
|
590
|
+
['Local model', 'Winnow or Laya on 127.0.0.1 answers Jev questions when there is no TypeSafe key or TypeSafe fails (D95).', M.typesafe && M.typesafe.local ? M.typesafe.local.name + ', stays here' : 'Off', 'ok'],
|
|
590
591
|
['Your connected tools', 'Calendar, Jira, Wispr Flow and the sender are reached by your Claude session, not by PMOS.', 'Through profile ' + (profileNow().id || 'none'), ''],
|
|
591
592
|
];
|
|
592
593
|
const leave = L.map(([t, h, s, tone]) => el('li', { class: 'nrow' }, el('span', { class: cx('ki', tone === 'ok' ? 'hot' : tone === 'warn' ? 't-warn' : '') }, c3ic(tone === 'ok' ? 'lock' : 'out')),
|
|
593
594
|
el('div', { class: 'nb' }, el('span', { class: 'tt' }, t), help(h)), el('div', { class: 'acts' }, tag(s, tone))));
|
|
594
595
|
return [sect('What leaves this computer', [list(leave)], { ic: 'shield', attrs: { 'data-c3set-leaves': '' } }),
|
|
595
596
|
sect('Keys and files', [list([
|
|
596
|
-
frame({ key: 'typesafe', label: 'TypeSafe key', help: 'Kept on this computer only. It is never shown, here or anywhere.', scope: 'computer', control: M.typesafe ? tag(M.typesafe.
|
|
597
|
+
frame({ key: 'typesafe', label: 'TypeSafe key', help: 'Kept on this computer only. It is never shown, here or anywhere.', scope: 'computer', control: M.typesafe ? tag(M.typesafe.source === 'settings' || M.typesafe.source === 'environment' ? 'Saved' : 'None', M.typesafe.source === 'settings' || M.typesafe.source === 'environment' ? 'ok' : '') : tag('Reading') }),
|
|
598
|
+
localModelRow(),
|
|
597
599
|
frame({ key: 'state', label: 'Data folder', help: 'Notes, audio, settings and caches. Outside every repository, so no commit can carry them.', scope: 'computer', control: chip((M.data && M.data.state_dir) || '', 'mono st-wrap-text') }),
|
|
598
600
|
lockedRow('priv.cards', 'Send card text to Jev for icons', 'Card kinds and icons are chosen by code rules on this computer (D7).', 'Off', 'computer'),
|
|
599
601
|
notYet('priv.export', 'Export or reset every setting', 'Save every setting to a file, or put them all back to their defaults. Your workspaces, notes and audio stay.', 'Export and reset all come later. Reset one setting or one section with its Reset button.', 'computer')]),
|
|
600
602
|
el('div', { class: 'iacts' }, btn('Export settings', { ic: 'out', disabled: true }), btn('Reset all settings', { variant: 'ghost', disabled: true }), w && w.keep_audio !== false ? btn('Recording settings', { variant: 'ghost', ic: 'next', onclick: () => goSec('recording') }) : null)], { ic: 'lock' })];
|
|
601
603
|
}
|
|
602
604
|
|
|
605
|
+
/* D95 — a model on this computer answers Jev's questions when there is no TypeSafe key, or when TypeSafe fails.
|
|
606
|
+
* Winnow and Laya both speak Jev's interface; PMOS only calls 127.0.0.1 on the port shown. */
|
|
607
|
+
function localModelRow() {
|
|
608
|
+
const t = M.typesafe; const loc = t && t.local; const cur = loc ? loc.backend : 'off';
|
|
609
|
+
const pick = async (v) => {
|
|
610
|
+
const r = await pmPost('/api/typesafe/local', { backend: v });
|
|
611
|
+
if (r && r.ok !== false) { M.typesafe = r.key; delete M.err['typesafe.local']; M.localTest = null; toast(v === 'off' ? 'Saved: no local model' : 'Saved: local model'); }
|
|
612
|
+
else M.err['typesafe.local'] = (r && r.error) || 'Not saved.';
|
|
613
|
+
draw();
|
|
614
|
+
};
|
|
615
|
+
const test = async () => {
|
|
616
|
+
M.localTest = { busy: true }; draw();
|
|
617
|
+
const r = await pmPost('/api/typesafe/local/test', {});
|
|
618
|
+
M.localTest = r && r.ok ? { ok: true, text: 'Answered (' + (r.model || r.local) + ').' } : { ok: false, text: (r && r.error) || 'No answer.' };
|
|
619
|
+
draw();
|
|
620
|
+
};
|
|
621
|
+
const lt = M.localTest;
|
|
622
|
+
const note = !loc ? 'Off. With no TypeSafe key, Jev features stay off.'
|
|
623
|
+
: (t.source === 'local' ? 'No TypeSafe key, so ' + loc.name + ' answers every Jev question.' : 'TypeSafe answers first; ' + loc.name + ' answers when it fails.')
|
|
624
|
+
+ ' PMOS calls 127.0.0.1:' + loc.port + ' only. Nothing leaves this computer.' + (lt && !lt.busy ? ' ' + lt.text : '');
|
|
625
|
+
return frame({ key: 'typesafe.local', label: 'Local model if Jev is missing', help: 'Start Winnow (winnow-inference, port 8091) or Laya (laya-serve, port 8000) on this computer, then pick it here.', scope: 'computer',
|
|
626
|
+
err: M.err['typesafe.local'], note,
|
|
627
|
+
control: t ? el('div', { class: 'iacts' }, seg('Local model', [['off', 'Off'], ['winnow', 'Winnow'], ['laya', 'Laya']], cur, pick),
|
|
628
|
+
loc ? btn(lt && lt.busy ? 'Testing' : 'Test', { variant: 'ghost', disabled: !!(lt && lt.busy), onclick: test }) : null) : tag('Reading') });
|
|
629
|
+
}
|
|
630
|
+
|
|
603
631
|
function appearanceSec() {
|
|
604
632
|
const pref = ctx.themePref || ctx.theme;
|
|
605
633
|
return [group('Look', [extRow('look.theme', seg('Theme', [['system', 'Same as system'], ['light', 'Light'], ['dark', 'Dark']], pref, (v) => { const before = ctx.themePref || ctx.theme; if (ctx.setTheme) ctx.setTheme(v); ctx.themePref = v; ctx.theme = v; toast('Saved: theme', () => { if (ctx.setTheme) ctx.setTheme(before); ctx.themePref = before; ctx.theme = before; draw(); }); draw(); }), { scope: 'computer', note: 'Kept in this browser.' }),
|
|
@@ -616,6 +616,7 @@ async function boot() {
|
|
|
616
616
|
let start = q.get('tab');
|
|
617
617
|
const saved = DEMO ? null : await pmGet('/api/settings' + (S.ws ? '?ws=' + encodeURIComponent(S.ws) : '')).catch(() => null); // demo mode's workspace is not registered
|
|
618
618
|
if (saved && saved.ok) { applyLook(root, saved.settings); if (!start) { const s = (saved.settings || []).find((x) => x.key === 'look.start'); if (s && BUILD[s.value]) start = s.value; } }
|
|
619
|
+
if (q.get('focus') || q.get('fdate')) import('./c3-focus.js').then((m) => m.openFocus({ event: { id: q.get('focus') || '' }, date: q.get('fdate') || '' })).catch(() => { /* the focus page is not available */ }); // a reload keeps the Meeting focus page open
|
|
619
620
|
if (!S.userNav) showTab(start || 'brief', false); // a press during the read above (the gear, a tab) wins over the start page
|
|
620
621
|
new ResizeObserver(moveInd).observe(S.tabNav);
|
|
621
622
|
workCount(); briefCount(); askCount(); S.dock.start();
|
|
@@ -8356,4 +8356,5 @@
|
|
|
8356
8356
|
}
|
|
8357
8357
|
|
|
8358
8358
|
/* -- layouts · .cal3- the console3 Calendar page (calendar-time-page): its rules sit in console3.css beside the .vday rules they extend -- */
|
|
8359
|
+
/* -- layouts · .fc- the console3 meeting focus page (c3-meeting-focus): its rules sit in c3-focus.css -- */
|
|
8359
8360
|
/* -- end of the layout sections: add nothing below this line -- */
|
|
@@ -15,6 +15,7 @@ import { el } from './dom.js';
|
|
|
15
15
|
import { button } from './primitives.js';
|
|
16
16
|
import { ensureC3Css, c3ic } from './pm-work-view.js';
|
|
17
17
|
import * as dock from './note-dock.js';
|
|
18
|
+
import { noteById } from './notes-links.js';
|
|
18
19
|
import { recView, clockOf } from './shell-model.js';
|
|
19
20
|
import { reducedMotion } from './shell-ticker.js';
|
|
20
21
|
import { toast } from './c3-toast.js';
|
|
@@ -141,7 +142,7 @@ export function disposeRecording(host) {
|
|
|
141
142
|
|
|
142
143
|
const NS_SVG = 'http://www.w3.org/2000/svg';
|
|
143
144
|
const glyph = (d) => { const s = document.createElementNS(NS_SVG, 'svg'); s.setAttribute('class', 'ic sm'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true'); const p = document.createElementNS(NS_SVG, 'path'); p.setAttribute('d', d); s.appendChild(p); return s; };
|
|
144
|
-
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
145
|
+
const RGL = { highlighter: () => c3ic('flag', 'sm'), pause: () => glyph('M8 5v14M16 5v14'), play: () => c3ic('play', 'sm'), square: () => c3ic('stop', 'sm'), mic: () => c3ic('mic', 'sm'), focus: () => c3ic('target', 'sm'), down: () => glyph('M6 9l6 6 6-6') };
|
|
145
146
|
|
|
146
147
|
/** The chip opens the note: on console3, Calendar › Notes on the During stage (calendar-views.js listens for c3-open-note);
|
|
147
148
|
* elsewhere the main app, where the full note is. */
|
|
@@ -165,6 +166,19 @@ async function engineLabel() {
|
|
|
165
166
|
} catch { return ''; }
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/** The calendar meeting a note is recording, or null for a note that is not linked to one. */
|
|
170
|
+
function meetingOfNote(id) {
|
|
171
|
+
let n = null; try { n = noteById(id); } catch { n = null; }
|
|
172
|
+
const m = n && n.meeting;
|
|
173
|
+
return m && m.kind === 'event' && m.start ? m : null;
|
|
174
|
+
}
|
|
175
|
+
/** Meeting focus (initiative c3-meeting-focus): the page for the meeting being recorded. The event id is empty for a meeting without a stored id; the page then finds it by start and title. */
|
|
176
|
+
function openFocusFor(m) {
|
|
177
|
+
const day = String(m.start || '').slice(0, 10);
|
|
178
|
+
import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.event_id || '', at: m.start, end: m.end || null, title: m.title }, date: day }))
|
|
179
|
+
.catch(() => toast({ text: 'The focus page could not load.', tone: 'error' }));
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
function mountReal(host) {
|
|
169
183
|
ensureC3Css();
|
|
170
184
|
let ready = false;
|
|
@@ -201,7 +215,8 @@ function mountReal(host) {
|
|
|
201
215
|
let st; try { st = dock.recordState(); } catch { st = { mode: 'idle' }; }
|
|
202
216
|
if (st.mode !== 'idle' && eng.id !== st.id) { eng = { id: st.id, text: '' }; const want = st.id; engineLabel().then((t) => { if (eng.id === want && t) { eng.text = t; rec.key = ''; sync(); } }); }
|
|
203
217
|
const hl = st.mode === 'recording' && dock.highlightBusy ? dock.highlightBusy() : null;
|
|
204
|
-
const
|
|
218
|
+
const mt = st.mode !== 'idle' ? meetingOfNote(st.id) : null;
|
|
219
|
+
const key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text, mt ? 'f' + (mt.event_id || '') + mt.start : ''].join(':');
|
|
205
220
|
if (key !== rec.key) {
|
|
206
221
|
rec.key = key; rec.clock = null;
|
|
207
222
|
const recording = st.mode === 'recording';
|
|
@@ -218,6 +233,7 @@ function mountReal(host) {
|
|
|
218
233
|
group.push(btn(hl ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next', 'highlighter', () => dock.highlightNow(), { 'data-fr-rec-highlight': hl ? hl.state : '', 'data-shell-rec-mark': '' }),
|
|
219
234
|
btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
|
|
220
235
|
} else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
|
|
236
|
+
if (mt) group.push(btn('Open the focus page for this meeting', 'focus', () => openFocusFor(mt), { 'data-fr-rec-focus': '', 'data-shell-rec-focus': '' }));
|
|
221
237
|
group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
|
|
222
238
|
kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
|
|
223
239
|
}
|
|
@@ -162,6 +162,17 @@
|
|
|
162
162
|
"names": [
|
|
163
163
|
"(d) the Tree pane shows at most ${TREE_BUDGET} controls on first paint (specimen A's own Tree view)"
|
|
164
164
|
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"id": "STD-LOCAL",
|
|
168
|
+
"file": "workspace/typesafe-validation.test.js",
|
|
169
|
+
"run": "jev-local-fallback",
|
|
170
|
+
"ref": "DECISIONS.md D95 and planning/initiatives/jev-local-fallback.md (PM wawan, 2026-10-12)",
|
|
171
|
+
"reason": "D95 adds the local fallback server to the key store's status, so status() gains a `local` field (null when none is set). B1 and B5 compared the exact JSON; they keep their intent (absent, then present from settings, never the key) with `\"local\":null` in the expected text.",
|
|
172
|
+
"names": [
|
|
173
|
+
"B1 with nothing stored, status says absent",
|
|
174
|
+
"B5 status never carries the key or its length"
|
|
175
|
+
]
|
|
165
176
|
}
|
|
166
177
|
],
|
|
167
178
|
"entries": [
|
package/workspace/server.js
CHANGED
|
@@ -238,6 +238,7 @@ const STATE_RESOLVED = stateRoot.resolveStateRoot({
|
|
|
238
238
|
legacy: REQUIRED_AS_LIBRARY ? null : LEGACY_STATE,
|
|
239
239
|
});
|
|
240
240
|
const STATE = STATE_RESOLVED.root;
|
|
241
|
+
tsSettings.status(STATE); // D95 — load the saved local fallback (Winnow or Laya) into the TypeSafe module
|
|
241
242
|
// ── AN UNSETTLED STATE ROOT MUST NOT DEGRADE INTO "YOU HAVE NOTHING". ─────────────────────────
|
|
242
243
|
// When both directories hold state nothing is moved (only a human can decide which sixteen
|
|
243
244
|
// sessions are the real sixteen) — but the stores the per-user root does NOT hold used to resolve
|
|
@@ -7965,6 +7966,22 @@ async function handleRequest(req, res) {
|
|
|
7965
7966
|
applyOnline();
|
|
7966
7967
|
return json(res, onlineView(req));
|
|
7967
7968
|
}
|
|
7969
|
+
/* D95 — the local fallback: a System One server on this machine (Winnow or Laya) answers when there is no TypeSafe key,
|
|
7970
|
+
* or when TypeSafe fails. Loopback only; the port is the one setting. `test` asks it one question about a fixed sentence. */
|
|
7971
|
+
if (p === '/api/typesafe/local' && req.method === 'POST') {
|
|
7972
|
+
let body = {};
|
|
7973
|
+
try { body = JSON.parse((await readBody(req)) || '{}'); }
|
|
7974
|
+
catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
|
|
7975
|
+
const out = tsSettings.setLocal(STATE, { backend: body.backend, port: body.port }); // backend 'off' clears it
|
|
7976
|
+
if (!out.ok) return json(res, { ok: false, error: out.error }, /choose|port/.test(out.error) ? 400 : 500);
|
|
7977
|
+
return json(res, { ok: true, key: tsSettings.status(STATE) });
|
|
7978
|
+
}
|
|
7979
|
+
if (p === '/api/typesafe/local/test' && req.method === 'POST') {
|
|
7980
|
+
if (!tsSettings.status(STATE).local) return json(res, { ok: false, error: 'No local model is set.' }, 409);
|
|
7981
|
+
const r = await tsClient.ask({ key: tsClient.LOCAL_KEY, state: 'PMOS is checking that the local model answers.', retries: 0,
|
|
7982
|
+
questions: { check: { type: 'noul', instructions: 'Is this sentence a connection check?' } } });
|
|
7983
|
+
return json(res, r.ok ? { ok: true, model: r.model, local: r.local } : { ok: false, error: r.reason }, r.ok ? 200 : 502);
|
|
7984
|
+
}
|
|
7968
7985
|
if (p === '/api/typesafe/key' && (req.method === 'PUT' || req.method === 'DELETE')) {
|
|
7969
7986
|
let body = {};
|
|
7970
7987
|
if (req.method === 'PUT') {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"story-artifacts.test.js",
|
|
23
23
|
"no-network.test.js",
|
|
24
24
|
"typesafe-validation.test.js",
|
|
25
|
+
"typesafe-local.test.js",
|
|
25
26
|
"claims.test.js",
|
|
26
27
|
"pm-items.test.js",
|
|
27
28
|
"jobs-to-be-done.test.js",
|
|
@@ -156,6 +157,7 @@
|
|
|
156
157
|
"story-artifacts.test.js": 1,
|
|
157
158
|
"no-network.test.js": 1,
|
|
158
159
|
"typesafe-validation.test.js": 1,
|
|
160
|
+
"typesafe-local.test.js": 1,
|
|
159
161
|
"claims.test.js": 1,
|
|
160
162
|
"pm-items.test.js": 1,
|
|
161
163
|
"jobs-to-be-done.test.js": 1,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Initiative c3-meeting-focus: the Meeting focus page of console3. The unit suite covers the route (read, topic, ask), the companion prompt and its parsing, and when the companion runs; the browser suite runs the page in demo mode.",
|
|
3
|
+
"modules": ["meeting-focus.test.js"],
|
|
4
|
+
"panels": ["pm-c3-meeting-focus-test.js"],
|
|
5
|
+
"seconds": { "meeting-focus.test.js": 10, "pm-c3-meeting-focus-test.js": 60 }
|
|
6
|
+
}
|