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.
@@ -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.present ? 'Saved' : 'None', M.typesafe.present ? 'ok' : '') : tag('Reading') }),
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 key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text].join(':');
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": [
@@ -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
+ }