create-pmos 0.162.0 → 0.164.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.
@@ -6,7 +6,21 @@
6
6
  import { el } from './dom.js';
7
7
  import { openSheet } from './c3-sheet.js';
8
8
  import { toast } from './c3-toast.js';
9
- import { pmGet } from './http.js';
9
+ import { pmGet, pmPost } from './http.js';
10
+ import { button } from './primitives.js';
11
+ import { c3ic } from './pm-work-view.js';
12
+
13
+ /* a primitives button (keyboard, role) carrying the console3 look and children (the pattern c3-dock.js uses) */
14
+ const c3btn = (attrs, onClick, ...kids) => { const b = button({ variant: 'ghost', attrs, onClick }); b.replaceChildren(...kids); return b; };
15
+ const store = {
16
+ get(k) { try { return localStorage.getItem(k); } catch { return null; } },
17
+ set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } },
18
+ };
19
+ export const SIZE_KEY = 'pmos.workspace.c3.term-size';
20
+ export const PAD_KEY = 'pmos.workspace.c3.term-pad';
21
+ /* the sheet widths: Normal is the 600px wide sheet; the others are classes in console3.css (desktop only) */
22
+ export const SIZES = [['normal', 'Normal', 'Normal'], ['half', 'Half', 'Half of the window'], ['two-thirds', '2/3', 'Two-thirds of the window'], ['whole', 'Whole', 'Whole window']];
23
+ export const sizeOf = (v) => (SIZES.some((x) => x[0] === v) ? v : 'normal');
10
24
 
11
25
  const VENDOR = ['/vendor/xterm.js', '/vendor/addon-fit.js', '/vendor/addon-web-links.js'];
12
26
  let vendor = null;
@@ -29,6 +43,43 @@ function loadVendor() {
29
43
  return vendor;
30
44
  }
31
45
 
46
+ /* the scratchpad under the terminal: the classic app's per-session pad (panel-tabs.js), kept through /api/scratchpad */
47
+ function makePad(id, term, tab) {
48
+ const url = '/api/scratchpad?session=' + encodeURIComponent(id);
49
+ let typed = false; let timer = null; let pending = null; let closed = false;
50
+ const flush = () => {
51
+ if (timer) { clearTimeout(timer); timer = null; }
52
+ if (pending === null) return;
53
+ const text = pending; pending = null;
54
+ pmPost('/api/scratchpad', { session: id, text }).catch(() => { /* the text stays in the box */ });
55
+ };
56
+ const ta = el('textarea', { class: 'c3t-pad-ta', rows: '4', placeholder: 'Scratchpad. One pad for each session. Saved on this machine.', 'aria-label': 'Scratchpad', 'data-c3-pad': 'text',
57
+ oninput: () => { typed = true; pending = ta.value; if (timer) clearTimeout(timer); timer = setTimeout(flush, 500); sync(); } });
58
+ const send = c3btn({ class: 'pill', 'data-c3-pad': 'send', title: 'Type the pad into this session. Enter is not pressed.' }, () => {
59
+ if (!ta.value.trim()) return;
60
+ if (!term.sendToTerm(tab, ta.value)) { toast({ text: 'The text was not sent. The terminal is not connected.', tone: 'error' }); return; }
61
+ if (typeof term.focusTerm === 'function') term.focusTerm(tab);
62
+ }, c3ic('out', 'sm'), 'Send to terminal');
63
+ const open0 = store.get(PAD_KEY) !== 'closed';
64
+ const box = el('div', { class: 'c3t-pad', 'data-c3-pad': 'box' });
65
+ const toggle = c3btn({ class: 'chip2 lk', 'data-c3-pad': 'toggle' }, () => { const on = box.dataset.open !== 'true'; store.set(PAD_KEY, on ? 'open' : 'closed'); set(on); },
66
+ c3ic('edit', 'sm'), 'Scratchpad');
67
+ function set(on) {
68
+ box.dataset.open = String(on);
69
+ toggle.setAttribute('aria-expanded', String(on));
70
+ toggle.title = on ? 'Hide the scratchpad. The text is kept.' : 'Show the scratchpad';
71
+ ta.hidden = !on;
72
+ requestAnimationFrame(() => term.refitVisible());
73
+ }
74
+ function sync() { send.disabled = !ta.value.trim(); }
75
+ box.append(el('div', { class: 'c3t-pad-h' }, toggle, send), ta);
76
+ set(open0); sync();
77
+ pmGet(url).then((r) => { if (!typed && r && typeof r.text === 'string') { ta.value = r.text; sync(); } }).catch(() => { /* an empty pad */ });
78
+ const onHide = () => flush();
79
+ window.addEventListener('pagehide', onHide);
80
+ return { box, ta, dispose() { if (closed) return; closed = true; window.removeEventListener('pagehide', onHide); flush(); } };
81
+ }
82
+
32
83
  export async function openSessionTerm({ id, name } = {}) {
33
84
  if (!id) return null;
34
85
  if (!name) { const r = await pmGet('/api/sessions').catch(() => null); const x = r && Array.isArray(r.sessions) ? r.sessions.find((y) => y.id === id) : null; name = x ? x.name : id; }
@@ -36,8 +87,23 @@ export async function openSessionTerm({ id, name } = {}) {
36
87
  if (!ok || !term) { toast({ text: 'The terminal could not be loaded on this page.', tone: 'error' }); return null; }
37
88
  const tab = { id: 'c3-session:' + id, kind: 'term', sessionId: id, spawn: false, sess: id };
38
89
  const host = el('div', { class: 'c3t-term', 'data-c3-session-term': id });
39
- // Esc and Tab belong to the shell while the terminal has focus (Esc interrupts an agent); the close button and a press outside still close
40
- const sheet = openSheet({ title: name || id, icon: 'term', wide: true, body: host, onClose: () => term.disposeTerm(tab), passKeys: (e) => host.contains(e.target) });
90
+ const pad = makePad(id, term, tab);
91
+ const wrap = el('div', { class: 'c3t-tw' }, host, pad.box);
92
+ // the size control: Normal is the 600px sheet, the others are the classic dock's presets. Hidden on a phone, where the sheet is full width.
93
+ let size = sizeOf(store.get(SIZE_KEY));
94
+ const sizeBtns = SIZES.map(([k, label, title]) => c3btn({ 'aria-pressed': String(k === size), 'data-c3-size': k, title, 'aria-label': title }, () => setSize(k), label));
95
+ const seg = el('div', { class: 'seg c3t-size', role: 'group', 'aria-label': 'Terminal size', 'data-c3-size-seg': '' }, ...sizeBtns);
96
+ const cls = (k) => 'tsz-' + k;
97
+ // Esc and Tab belong to the shell while the terminal has focus (Esc interrupts an agent); the close button and a press outside still close.
98
+ // The scratchpad is outside the terminal host, so its keys are the page's own.
99
+ const sheet = openSheet({ title: name || id, icon: 'term', wide: true, body: wrap, actions: seg, cls: cls(size),
100
+ onClose: () => { pad.dispose(); term.disposeTerm(tab); }, passKeys: (e) => host.contains(e.target) });
101
+ function setSize(k) {
102
+ sheet.sheet.classList.remove(cls(size)); size = sizeOf(k); sheet.sheet.classList.add(cls(size));
103
+ store.set(SIZE_KEY, size);
104
+ sizeBtns.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.c3Size === size)));
105
+ requestAnimationFrame(() => { term.refitVisible(); requestAnimationFrame(() => term.refitVisible()); });
106
+ }
41
107
  term.mountTerm(host, tab);
42
108
  requestAnimationFrame(() => { term.refitVisible(); term.focusTerm(tab); });
43
109
  return sheet;
@@ -1,7 +1,8 @@
1
1
  /* pm/c3-sheet.js — the console3 sheet, as a primitive (console3.html: bsHtml line 2502, the .sheet rules 143 to 145, 180, 496 to 502,
2
2
  * the Esc handler 2536 and the phone drag 2538 to 2540).
3
3
  *
4
- * openSheet({ title, icon, body, wide, dw, scrim, onClose, passKeys }) -> { close, root, sheet, body } passKeys(e): true leaves that key to the page
4
+ * openSheet({ title, icon, body, wide, dw, scrim, onClose, passKeys, actions, cls }) -> { close, root, sheet, body } passKeys(e): true leaves that key to the page
5
+ * actions: a Node or an array of Nodes placed in the header before the close button; cls: extra class names on the sheet
5
6
  * closeSheet() closes the open sheet, if any
6
7
  *
7
8
  * On a desktop window the sheet is the right-hand panel (340px; `wide` 600px; `dw` the document sheet, up to 780px) over a
@@ -50,7 +51,7 @@ export function closeSheet() {
50
51
  if (typeof c.onClose === 'function') { try { c.onClose(); } catch { /* the caller reports its own errors */ } }
51
52
  }
52
53
 
53
- export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = false, scrim = true, onClose, passKeys } = {}) {
54
+ export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = false, scrim = true, onClose, passKeys, actions, cls = '' } = {}) {
54
55
  closeSheet();
55
56
  ensureC3Css();
56
57
  const opener = document.activeElement && document.activeElement !== document.body ? document.activeElement : null;
@@ -60,8 +61,8 @@ export function openSheet({ title = '', icon = 'doc', body, wide = false, dw = f
60
61
  const sbb = el('div', { class: 'brief sbb' }, ...bodyNodes(body));
61
62
  const closeBtn = el('button', { type: 'button', class: 'ib sm', 'aria-label': 'Close', title: 'Close (Esc)', onclick: close }, c3ic('x', 'sm'));
62
63
  const grab = el('div', { class: 'grab' });
63
- const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic(icon), el('b', null, title)), el('span', { class: 'chips2' }, closeBtn));
64
- const sheet = el('div', { class: 'sheet bs' + (wide ? ' wide' : '') + (dw ? ' dw' : ''), role: 'dialog', 'aria-modal': String(!!scrim), 'aria-label': title, tabindex: '-1' },
64
+ const head = el('div', { class: 'sh2' }, el('span', { class: 'chips2' }, c3ic(icon), el('b', null, title)), el('span', { class: 'chips2' }, ...bodyNodes(actions), closeBtn));
65
+ const sheet = el('div', { class: 'sheet bs' + (wide ? ' wide' : '') + (dw ? ' dw' : '') + (cls ? ' ' + cls : ''), role: 'dialog', 'aria-modal': String(!!scrim), 'aria-label': title, tabindex: '-1' },
65
66
  grab, head, el('div', { class: 'sb' }, sbb));
66
67
  /* the mockup's person and playbooks sheets have no scrim (they are panels beside the page, closed by their own button or Esc): `scrim: false` */
67
68
  root.append(...(scrim ? [el('div', { class: 'scrim', onclick: close })] : []), sheet);
@@ -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 -- */
@@ -1660,6 +1660,22 @@
1660
1660
  .c3 .c3t-panel{display:grid;gap:var(--sp-2);border:1px solid var(--border);border-radius:10px;padding:var(--sp-3)}
1661
1661
  /* a session's terminal in a sheet (c3-session-term.js) */
1662
1662
  .c3 .c3t-term{height:calc(100vh - 150px);min-height:240px;display:flex;flex-direction:column}
1663
+ /* the terminal plus its scratchpad (c3-session-term.js): the terminal shrinks to make room for the pad */
1664
+ .c3 .c3t-tw{height:calc(100vh - 150px);min-height:240px;display:flex;flex-direction:column;gap:var(--sp-2);min-width:0}
1665
+ .c3 .c3t-tw .c3t-term{height:auto;flex:1 1 0;min-height:120px}
1666
+ .c3 .c3t-pad{display:grid;gap:var(--sp-2);flex:none;border-top:1px solid var(--border);padding-top:var(--sp-2)}
1667
+ .c3 .c3t-pad-h{display:flex;gap:var(--sp-2);align-items:center;justify-content:space-between}
1668
+ .c3 .c3t-pad-h .pill:disabled{opacity:.35;cursor:not-allowed}
1669
+ .c3 .c3t-pad-ta{width:100%;min-height:72px;max-height:40vh;resize:vertical;font:var(--fs-2) var(--mono);box-sizing:border-box}
1670
+ /* terminal sheet sizes, as the classic dock has them; the phone sheet is already full width */
1671
+ .c3 .c3t-size{flex:none}
1672
+ .c3 .c3t-size button{padding:4px 8px}
1673
+ .c3.ph .c3t-size{display:none}
1674
+ @media (min-width:761px){
1675
+ .c3 .sheet.wide.tsz-half{width:50vw}
1676
+ .c3 .sheet.wide.tsz-two-thirds{width:66.667vw}
1677
+ .c3 .sheet.wide.tsz-whole{width:calc(100vw - 24px)}
1678
+ }
1663
1679
  .c3 .c3t-panel .feed{display:flex;flex-direction:column;gap:var(--sp-2);max-height:220px;overflow:auto}
1664
1680
  .c3 .c3t-panel .m{padding:6px 12px;border-radius:12px;max-width:88%;overflow-wrap:anywhere}
1665
1681
  .c3 .c3t-panel .m.me{align-self:flex-end;background:var(--acc);color:var(--on-accent)}.c3 .c3t-panel .m.bot{background:var(--muted)}
@@ -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
  }
@@ -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
+ }