create-pmos 0.146.0 → 0.148.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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-sessions-panel-test.js +126 -0
  7. package/workspace/guide/console-calendar.md +31 -7
  8. package/workspace/guide/console3-sessions.md +35 -0
  9. package/workspace/guide/index.md +1 -0
  10. package/workspace/guide/notes.md +1 -1
  11. package/workspace/lib/calendar-events.js +11 -1
  12. package/workspace/lib/calendar-layers.js +95 -5
  13. package/workspace/lib/routes-calendar-views.js +9 -4
  14. package/workspace/lib/routes-runs.js +12 -4
  15. package/workspace/package-lock.json +2 -2
  16. package/workspace/package.json +1 -1
  17. package/workspace/public/pm/c3-calendar.js +225 -32
  18. package/workspace/public/pm/c3-demo-cal4.js +18 -4
  19. package/workspace/public/pm/c3-demo-data.js +2 -2
  20. package/workspace/public/pm/c3-dock.js +2 -1
  21. package/workspace/public/pm/c3-notes.js +47 -9
  22. package/workspace/public/pm/c3-session-term.js +44 -0
  23. package/workspace/public/pm/c3-sheet.js +3 -2
  24. package/workspace/public/pm/c3-shell.js +2 -1
  25. package/workspace/public/pm/c3-terminal-data.js +1 -1
  26. package/workspace/public/pm/c3-terminal-icons.js +1 -1
  27. package/workspace/public/pm/console3.css +31 -0
  28. package/workspace/public/pm/note-dock.js +5 -8
  29. package/workspace/public/pm/run-card.js +4 -1
  30. package/workspace/public/pm/sessions-panel.js +40 -4
  31. package/workspace/test-files.json +2 -0
  32. package/workspace/test-lists/c3-meeting-card.json +5 -0
@@ -63,7 +63,7 @@ const setVar = (node, name, value) => { node.style.setProperty('--cal3-' + name,
63
63
  /* ── the page's own state on the instance: which layers are on, whether free time shows, what is selected ─────────── */
64
64
 
65
65
  function state(r) {
66
- const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, sel: null });
66
+ const st = r.page || (r.page = { on: new Set(LAYERS.map((l) => l[0]).filter((id) => !(r.ctx && r.ctx.overlay && mem.hide.includes(id)))), free: true, tidy: false, routine: false, sel: null });
67
67
  if (r.sel) { st.sel = { type: 'event', id: r.sel }; r.sel = null; } // a meeting chosen on the Briefing (openDay)
68
68
  if (r.pendingMode) { st.mode = r.pendingMode; r.pendingMode = null; } // a note opened before the page drew (the Record chip)
69
69
  if (!st.mode) st.mode = 'cal';
@@ -146,7 +146,7 @@ export function drawCalendarPage(host, r, h) {
146
146
 
147
147
  const body = el('div', { class: 'calbody' + (r.anim && !r.loading ? ' ' + r.anim : '') },
148
148
  el('div', { class: 'cal3-wrap' },
149
- el('div', { class: 'cal3-main' }, empty, grid),
149
+ el('div', { class: 'cal3-main' }, empty, empty ? null : highlights(ui, range), grid),
150
150
  el('div', { class: 'cal3-side' }, detail(ui), st.tidy ? tidyList(ui) : null, st.free ? freeList(ui, range) : null, lanes(ui, range))));
151
151
  return pageBar(chips(ui, range), body);
152
152
  }
@@ -186,7 +186,8 @@ function chips(ui, range) {
186
186
  }, el('i', { class: 'cal3-sw cal3-sw-' + id }))),
187
187
  el('span', { class: 'cal3-sep', 'aria-hidden': 'true' }),
188
188
  chip('free', 'Free time', freeN, st.free, () => { st.free = !st.free; }, c3ic('clock', 'sm')),
189
- chip('tidy', 'Tidy-up', tidyCount(M, ui.d), st.tidy, () => { st.tidy = !st.tidy; }, c3ic('spark', 'sm')));
189
+ chip('tidy', 'Tidy-up', tidyCount(M, ui.d), st.tidy, () => { st.tidy = !st.tidy; }, c3ic('spark', 'sm')),
190
+ WIDE.includes(M.view) ? chip('routine', 'Routine', routineCount(ui, range), st.routine, () => { st.routine = !st.routine; }, c3ic('report', 'sm')) : null);
190
191
  }
191
192
  /* the number of suggestions in the range: the server's total (the list may be capped), or null when it sent none */
192
193
  const tidyCount = (M, d) => (M.tidy ? Math.max(0, (typeof d.tidy_total === 'number' ? d.tidy_total : M.tidy.length) - (d.tidy.length - M.tidy.length)) : null); // less the ones the PM dismissed
@@ -332,7 +333,123 @@ function freeBlock(ui, date, b, Y) {
332
333
  return n;
333
334
  }
334
335
 
335
- /* ── Month: a cell per day with the meeting count, up to three markers and +N, and the free count ───────────────── */
336
+ /* ── significance (initiative calendar-highlights) ───────────────────────────────────────────────────────────────
337
+ * Month, Quarter and Year show what matters and fold the rest into a count. Routine: a report date that recurs more often
338
+ * than monthly (the server's `routine` flag, or a daily, weekly or bi-weekly cadence), a meeting that repeats in the range,
339
+ * a stand-up or a 1:1, and a meeting tied to nothing. A due item that is on track is counted, not named. Key at Day, Week and
340
+ * Month: milestones, releases, reports that go out, late or at-risk due items, and meetings tied to a promise or a decision.
341
+ * Quarter and Year are one step stricter: milestones, releases, reports that go out, and late items; meetings are counted.
342
+ * The Routine chip (Month, Quarter, Year) brings back every item as markers. */
343
+ const WIDE = ['month', 'quarter', 'year'];
344
+ const FREQUENT = /\b(daily|weekly|bi-?weekly|fortnightly|stand-?up)\b/i;
345
+ const normT = (t) => String(t || '').toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').trim();
346
+ const isDue = (m) => m.kind === 'due' || m.kind === 'own';
347
+ const routineMark = (m) => m.kind === 'report' && (m.routine === true || (m.routine !== false && (FREQUENT.test(String(m.cadence || '')) || FREQUENT.test(String(m.title || '')) || /\bdraft\b/i.test(String(m.title || '')))));
348
+ const quietDue = (m) => isDue(m) && m.tone !== 'bad' && m.tone !== 'warn';
349
+ const keyMark = (m) => !routineMark(m) && !quietDue(m);
350
+ const landMark = (m) => m.kind === 'milestone' || m.kind === 'release' || (m.kind === 'report' && !routineMark(m)) || (isDue(m) && m.tone === 'bad');
351
+ const shortDay = (h, date) => h.dayLabel(date).replace(/^\S+ /, '');
352
+
353
+ /* the meetings of the range, each marked key or routine; a title seen more than once in the range is a recurring meeting */
354
+ function meetingsKeyed(ui) {
355
+ const { M } = ui;
356
+ if (!M._keyed) {
357
+ const seen = new Map(); for (const m of M.meetings) { const k = normT(m.e.title); seen.set(k, (seen.get(k) || 0) + 1); }
358
+ const now = nowIso();
359
+ M._keyed = M.meetings.map((m) => {
360
+ const t = m.e.title || '';
361
+ const routine = m.e.routine === true || seen.get(normT(t)) > 1 || /stand-?up|\b1\s*[:/-]\s*1\b|one[- ]on[- ]one/i.test(t) || !evState(ui, m, now).tied;
362
+ return { ...m, key: m.e.key === true || !routine };
363
+ });
364
+ }
365
+ return M._keyed;
366
+ }
367
+ const meetingsIn = (ui, a, b) => (ui.st.on.has('meetings') ? meetingsKeyed(ui).filter((m) => m.date >= a && m.date <= b) : []);
368
+
369
+ /* the number of folded items in the range, for the Routine chip */
370
+ function routineCount(ui, range) {
371
+ const wide = ui.M.view !== 'month';
372
+ const marks = marksIn(ui.M, ui.st, range.start, range.end).filter((m) => (wide ? !landMark(m) : !keyMark(m))).length;
373
+ return marks + meetingsIn(ui, range.start, range.end).filter((m) => wide || !m.key).length;
374
+ }
375
+
376
+ /* a name inside a highlight: a link-styled button that selects the item, so its detail opens beside the grid */
377
+ function nameBtn(ui, sel, label, attrs) {
378
+ return button({ label, variant: 'ghost', attrs: { class: 'cal3-sl', ...attrs }, onClick: () => ui.select(sel, null) });
379
+ }
380
+ const markName = (ui, m, label) => nameBtn(ui, { type: 'marker', id: m.id }, label || m.title, { 'data-cal3-mk': m.id, 'data-cal3-layer': m.layer, 'data-cal3-kind': m.kind, title: kindOf(m)[0] + ': ' + m.title + ', ' + ui.h.dayLabel(m.date) });
381
+ const meetName = (ui, m) => nameBtn(ui, { type: 'event', id: m.e.id }, m.e.title || m.e.id, { 'data-cal3-hl-ev': m.e.id, title: (m.e.title || m.e.id) + ', ' + ui.h.dayLabel(m.date) });
382
+
383
+ /* join nodes as "a, b and c", with "N more" for what is past max */
384
+ function joinList(nodes, max) {
385
+ const a = nodes.slice(0, max); if (nodes.length > max) a.push(String(nodes.length - max) + ' more');
386
+ const out = [];
387
+ a.forEach((n, i) => { if (i) out.push(i === a.length - 1 ? ' and ' : ', '); out.push(n); });
388
+ return out;
389
+ }
390
+
391
+ /* the highlight of a span as a list of text and name nodes; [] when nothing in it is significant.
392
+ * mode 'cell' is the short form of a Month day; 'top' is the range summary and a Quarter or Year column. */
393
+ function highlightOf(ui, a, b, mode) {
394
+ const { h, M } = ui; const cell = mode === 'cell'; const wide = M.view === 'quarter' || M.view === 'year';
395
+ const marks = marksIn(M, ui.st, a, b); const meets = wide ? [] : meetingsIn(ui, a, b).filter((m) => m.key);
396
+ const on = (date) => (cell ? '' : ' on ' + shortDay(h, date));
397
+ const rel = marks.filter((m) => m.kind === 'release'); const ms = marks.filter((m) => m.kind === 'milestone' && (!m.tone || m.tone === 'ok'));
398
+ const rep = marks.filter((m) => m.kind === 'report' && !routineMark(m));
399
+ const lateMs = marks.filter((m) => m.kind === 'milestone' && (m.tone === 'bad' || m.tone === 'warn'));
400
+ const riskRel = rel.filter((m) => m.tone === 'bad' || m.tone === 'warn');
401
+ const lateDue = marks.filter((m) => isDue(m) && m.tone === 'bad'); const riskDue = wide ? [] : marks.filter((m) => isDue(m) && m.tone === 'warn');
402
+ const out = []; const sent = (...k) => { if (out.length) out.push(' '); out.push(...k); };
403
+ const dated = (list, max) => joinList(list.map((m) => el('span', null, markName(ui, m), on(m.date))), max);
404
+ if (rel.length) sent(...(cell ? [...joinList(rel.map((m) => markName(ui, m)), 2), rel.length === 1 ? ' ships.' : ' ship.'] : [plural(rel.length, 'release') + ': ', ...dated(rel, 3), '.']));
405
+ if (ms.length) sent(ms.length === 1 ? 'Milestone: ' : plural(ms.length, 'milestone') + ': ', ...dated(ms, 3), '.');
406
+ if (meets.length) {
407
+ if (cell || M.view === 'day') sent(...joinList(meets.map((m) => el('span', null, meetName(ui, m), h.timeOf(m.e.at) !== null ? ' at ' + h.hhmm(h.timeOf(m.e.at)) : '')), cell ? 2 : 4), '.');
408
+ else {
409
+ const big = meets.slice().sort((x, y) => hoursOf(y, h) - hoursOf(x, h) || String(x.e.at).localeCompare(String(y.e.at))).slice(0, 2).sort((x, y) => String(x.e.at).localeCompare(String(y.e.at)));
410
+ sent(plural(meets.length, 'key meeting') + (meets.length > big.length ? ', the biggest ' : ': '), ...joinList(big.map((m) => el('span', null, meetName(ui, m), ' on ' + shortDay(h, m.date))), 2), '.');
411
+ }
412
+ }
413
+ if (rep.length) sent(...(cell || rep.length <= 2 ? [...joinList(rep.map((m) => markName(ui, m, m.title.replace(/ goes out$/, ''))), 2), rep.length === 1 ? ' goes out.' : ' go out.'] : [rep.length + ' reports go out.']));
414
+ const watch = [];
415
+ for (const m of lateMs) watch.push(el('span', null, markName(ui, m, m.title.replace(/\s*\(late\)$/, '')), m.tone === 'bad' ? ' is late' : ' is at risk', cell ? '' : ' (' + shortDay(h, m.date) + ')'));
416
+ for (const m of riskRel) watch.push(el('span', null, markName(ui, m), m.tone === 'bad' ? ' is late' : ' is at risk'));
417
+ if (lateDue.length) watch.push(cell || lateDue.length <= 2 ? el('span', null, ...joinList(lateDue.map((m) => markName(ui, m)), 2), lateDue.length === 1 ? ' is late' : ' are late') : plural(lateDue.length, 'due item') + ' are late');
418
+ if (riskDue.length) watch.push(cell || riskDue.length <= 2 ? el('span', null, ...joinList(riskDue.map((m) => markName(ui, m)), 2), ' due soon') : plural(riskDue.length, 'due item') + ' due soon');
419
+ if (watch.length) sent(el('b', { class: 'cal3-watch' }, 'Watch:'), ' ', ...joinList(watch, 4), '.');
420
+ return out;
421
+ }
422
+
423
+ /* the range summary above the grid, at every zoom */
424
+ function highlights(ui, range) {
425
+ const { h, M } = ui;
426
+ const MF = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
427
+ const mo = Number(range.start.slice(5, 7));
428
+ const label = M.view === 'day' ? h.dayLabel(range.start) : M.view === 'week' ? 'Week of ' + h.dayLabel(range.start) : M.view === 'month' ? MF[mo - 1] : M.view === 'quarter' ? 'Q' + (Math.floor((mo - 1) / 3) + 1) : range.start.slice(0, 4);
429
+ const body = highlightOf(ui, range.start, range.end, 'top');
430
+ return el('div', { class: 'cal3-sum', 'data-cal3': 'highlights' },
431
+ el('span', { class: 'cal3-sum-h' }, label + ' highlights'),
432
+ el('p', null, ...(body.length ? body : ['Nothing significant. Only routine items fall in this range.'])));
433
+ }
434
+
435
+ /* the quiet line of a span: what is counted and not named; pressing it opens the span */
436
+ function quietLine(ui, a, b, view, opts) {
437
+ const wide = opts && opts.wide;
438
+ const marks = marksIn(ui.M, ui.st, a, b); const meets = meetingsIn(ui, a, b);
439
+ const due = marks.filter((m) => isDue(m) && (wide ? m.tone !== 'bad' : quietDue(m)));
440
+ const rt = marks.filter(routineMark).length + (wide ? 0 : meets.filter((m) => !m.key).length);
441
+ const nMeet = wide ? meets.length : 0;
442
+ const parts = [nMeet ? plural(nMeet, 'meeting') : null, due.length ? due.length + ' due' : null, rt ? rt + ' routine' : null].filter(Boolean);
443
+ if (!parts.length) return null;
444
+ const mo = Number(a.slice(5, 7));
445
+ const where = view === 'day' ? ui.h.dayLabel(a) : view === 'week' ? 'the week of ' + ui.h.dayLabel(a) : view === 'quarter' ? 'Q' + (Math.floor((mo - 1) / 3) + 1) + ' ' + a.slice(0, 4) : ui.h.MN[mo - 1] + ' ' + a.slice(0, 4);
446
+ const names = [...new Set([...due.map((m) => m.title), ...marks.filter(routineMark).map((m) => m.title)])];
447
+ const tip = parts.join(', ') + (names.length ? ': ' + names.slice(0, 8).join(', ') + (names.length > 8 ? ' and more' : '') : '') + '. Open ' + where;
448
+ return button({ label: parts.join(' · '), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-more cal3-rt', 'data-cal3-quiet': a, title: tip, 'aria-label': tip },
449
+ onClick: () => ui.h.go(ui.host, ui.r, { view, date: a, anim: 'zi' }) });
450
+ }
451
+
452
+ /* ── Month: a cell per day. Its highlight in a sentence and a quiet line; with Routine on, the meeting count and every marker ── */
336
453
 
337
454
  function monthGrid(ui) {
338
455
  const { M, st, h, d } = ui;
@@ -342,38 +459,58 @@ function monthGrid(ui) {
342
459
  for (let i = 0; i < off; i++) g.appendChild(el('div', { class: 'cal3-mc out', 'aria-hidden': 'true' }));
343
460
  for (const c of days) {
344
461
  const wd = h.dowOf(c.start); const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
345
- g.appendChild(el('div', { class: 'cal3-mc' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === d.today ? ' today' : ''), 'data-cal3-date': c.start },
462
+ const hl = st.routine ? [] : highlightOf(ui, c.start, c.start, 'cell');
463
+ g.appendChild(el('div', { class: 'cal3-mc' + (wd === 0 || wd === 6 ? ' we' : '') + (c.start === d.today ? ' today' : '') + (hl.length ? ' key' : ''), 'data-cal3-date': c.start },
346
464
  button({ label: String(Number(c.start.slice(8))), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dn', 'data-cal3-day': c.start, 'aria-label': 'Open ' + h.dayLabel(c.start) + (n ? ', ' + plural(n, 'meeting') : ''), title: 'Open ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'day', date: c.start, anim: 'zi' }) }),
347
- n ? el('span', { class: 'cal3-cnt' }, plural(n, 'meeting')) : null,
348
- ...markRow(ui, c.start, c.start, 3, true, 'day')));
465
+ ...(st.routine
466
+ ? [n ? el('span', { class: 'cal3-cnt' }, plural(n, 'meeting')) : null, ...markRow(ui, c.start, c.start, 3, true, 'day')]
467
+ : [hl.length ? el('p', { class: 'cal3-hl', 'data-cal3-hl': c.start }, ...hl) : null, quietLine(ui, c.start, c.start, 'day')])));
349
468
  }
350
- return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g);
469
+ return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, g, foldNote(ui));
470
+ }
471
+
472
+ /* the line under a Month, Quarter or Year grid that says what is counted and not named */
473
+ function foldNote(ui) {
474
+ if (ui.st.routine) return null;
475
+ return el('p', { class: 'dim cal3-fold', 'data-cal3': 'fold-note' }, (ui.M.view === 'month'
476
+ ? 'Report drafts, recurring meetings, stand-ups, 1:1s and due items on track are counted under each day, not listed.'
477
+ : 'Meetings, due items that are not late, and routine items are counted under each highlight, not listed. Open a month for its key meetings.')
478
+ + ' Turn on Routine to show everything.');
351
479
  }
352
480
 
353
- /* ── Quarter: a row per week; Year: a card per month. Counts, and the markers of the most important kinds ────────── */
481
+ /* ── Quarter: a column per month; Year: a column per quarter. Each has its highlight and its quiet line ──────────── */
482
+
483
+ const monthEnd = (start) => { const y = Number(start.slice(0, 4)); const m = Number(start.slice(5, 7)); const last = new Date(y, m, 0).getDate(); return start.slice(0, 8) + String(last).padStart(2, '0'); };
484
+ const monthStart = (y, m) => y + '-' + String(m).padStart(2, '0') + '-01';
354
485
 
355
- function spanCounts(ui, c) {
356
- const { M, st } = ui;
357
- const n = st.on.has('meetings') ? M.meetingsOf(c).length : 0;
358
- return el('div', { class: 'cal3-counts' },
359
- st.on.has('meetings') ? el('span', { class: 'tag' }, plural(n, 'meeting')) : null,
360
- el('span', { class: 'tag' }, plural(marksIn(M, st, c.start, c.end).length, 'dated item')));
486
+ function spanColumn(ui, a, b, head, now, extra, view) {
487
+ const hl = ui.st.routine ? null : highlightOf(ui, a, b, 'top');
488
+ return el('div', { class: 'cal3-ym cal3-col' + (now ? ' now' : ''), 'data-cal3-span': a }, head,
489
+ ui.st.routine ? el('div', { class: 'cal3-qmk' }, ...markRow(ui, a, b, 12, false, view))
490
+ : el('p', { class: 'cal3-hl' + (hl.length ? '' : ' dim'), 'data-cal3-hl': a }, ...(hl.length ? hl : ['Nothing significant.'])),
491
+ extra || null, ui.st.routine ? null : quietLine(ui, a, b, view, { wide: true }));
361
492
  }
362
493
 
363
494
  function quarterRows(ui) {
364
- const { M, h, d } = ui;
365
- const rows = M.cells.map((c) => el('div', { class: 'cal3-qrow' + (c.start <= d.today && d.today <= c.end ? ' now' : ''), 'data-cal3-week': c.start },
366
- button({ label: 'Week of ' + h.dayLabel(c.start), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', title: 'Open the week of ' + h.dayLabel(c.start) }, onClick: () => h.go(ui.host, ui.r, { view: 'week', date: c.start, anim: 'zi' }) }),
367
- spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 4, false, 'week'))));
368
- return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-quarter', 'data-cal3': 'quarter' }, ...rows));
495
+ const { h, d } = ui; const range = d.range || { start: d.today, end: d.today };
496
+ const y = Number(range.start.slice(0, 4)); const m0 = Number(range.start.slice(5, 7));
497
+ const cols = [0, 1, 2].map((i) => {
498
+ const a = monthStart(y, m0 + i); const b = monthEnd(a);
499
+ const head = button({ label: h.MN[m0 + i - 1] + ' ' + y, variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-month': a.slice(0, 7), title: 'Open ' + h.MN[m0 + i - 1] + ' ' + y }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: a, anim: 'zi' }) });
500
+ return spanColumn(ui, a, b, head, a <= d.today && d.today <= b, null, 'month');
501
+ });
502
+ return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-cols cal3-cols-3', 'data-cal3': 'quarter' }, ...cols), foldNote(ui));
369
503
  }
370
504
 
371
505
  function yearCards(ui) {
372
- const { M, h, d } = ui;
373
- const cards = M.cells.map((c) => el('div', { class: 'cal3-ym' + (c.start <= d.today && d.today <= c.end ? ' now' : ''), 'data-cal3-month': c.start.slice(0, 7) },
374
- button({ label: h.MN[Number(c.start.slice(5, 7)) - 1] + ' ' + c.start.slice(0, 4), variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', title: 'Open the month' }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: c.start, anim: 'zi' }) }),
375
- spanCounts(ui, c), el('div', { class: 'cal3-qmk' }, ...markRow(ui, c.start, c.end, 3, false, 'month'))));
376
- return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-year', 'data-cal3': 'year' }, ...cards));
506
+ const { h, d } = ui; const range = d.range || { start: d.today, end: d.today }; const y = Number(range.start.slice(0, 4));
507
+ const cols = [0, 1, 2, 3].map((q) => {
508
+ const a = monthStart(y, q * 3 + 1); const b = monthEnd(monthStart(y, q * 3 + 3));
509
+ const head = button({ label: 'Q' + (q + 1) + ' ' + y + ' · ' + h.MN[q * 3] + ' to ' + h.MN[q * 3 + 2], variant: 'ghost', size: 'sm', attrs: { class: 'cal3-dbtn', 'data-cal3-quarter': 'Q' + (q + 1), title: 'Open Q' + (q + 1) + ' ' + y }, onClick: () => h.go(ui.host, ui.r, { view: 'quarter', date: a, anim: 'zi' }) });
510
+ const months = el('div', { class: 'cal3-mlinks' }, ...[1, 2, 3].map((k) => { const s = monthStart(y, q * 3 + k); return button({ label: h.MN[q * 3 + k - 1], variant: 'ghost', size: 'sm', attrs: { class: 'cal3-more', 'data-cal3-month': s.slice(0, 7), title: 'Open ' + h.MN[q * 3 + k - 1] + ' ' + y }, onClick: () => h.go(ui.host, ui.r, { view: 'month', date: s, anim: 'zi' }) }); }));
511
+ return spanColumn(ui, a, b, head, a <= d.today && d.today <= b, months, 'quarter');
512
+ });
513
+ return el('section', { class: 'card cal3-grid', 'data-cv': 'grid' }, el('div', { class: 'cal3-cols cal3-cols-4', 'data-cal3': 'year' }, ...cols), foldNote(ui));
377
514
  }
378
515
 
379
516
  /* ── the detail column ────────────────────────────────────────────────────────────────────────────────────────────── */
@@ -396,7 +533,7 @@ function openRef(ui, ref, extra) {
396
533
 
397
534
  /* the home chip: where the record lives (the mockup's UI.homeChip) */
398
535
  const homeChip = (name) => el('span', { class: 'chip2', title: 'Where this lives', 'data-cal3': 'home' }, c3ic('repo', 'sm'), name || 'its home');
399
- const peopleOf = (e) => [].concat(e.people || e.with || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
536
+ const peopleOf = (e) => [].concat(e.people || e.with || e.attendees || []).map((p) => (p && typeof p === 'object' ? p.name || p.id : p)).filter(Boolean).map(String);
400
537
 
401
538
  function closeBtn(ui) {
402
539
  return button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { class: 'cal3-close', 'data-cal3': 'close' }, onClick: () => { ui.st.sel = null; ui.redraw('[data-cal3-chip="meetings"]'); } });
@@ -444,22 +581,78 @@ function meetingDetail(ui, hit) {
444
581
  };
445
582
  let body;
446
583
  if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
447
- else if (!rec || rec.held === false) body = [fold('moon', past ? 'Nothing held for this meeting: no note, decisions or linked items yet.' : 'Nothing to prepare for this meeting.', { 'data-cal3': 'nothing-held' })];
584
+ else if (past && (!rec || rec.held === false)) body = [fold('moon', 'Nothing held for this meeting: no note, decisions or linked items yet.', { 'data-cal3': 'nothing-held' })];
448
585
  else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
449
- else if (!items.some((x) => x.kind === 'prepared' || x.kind === 'todo')) body = [el('p', { class: 'dim cal3-note', 'data-cal3': 'nothing-to-prepare' }, 'Nothing to prepare for this meeting.')];
450
- else body = [list(['prepared'], 'Prepared', 'prepared', 'Nothing is prepared yet.'), list(['todo'], 'To prepare', 'todo', 'Nothing is left to prepare.')];
586
+ else body = upcomingBody(ui, rec, items);
587
+ const b = !past && rec && rec.brief ? rec.brief : null;
588
+ const iniId = typeof e.initiative === 'string' ? e.initiative : b && b.initiative ? b.initiative.id : null;
589
+ const iniTitle = iniId ? (b && b.initiative && b.initiative.id === iniId && b.initiative.title) || (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).title) || iniId : null;
590
+ const iniStatus = b && b.initiative && b.initiative.id === iniId ? b.initiative.status : (((ui.d.gantt || []).find((g) => g.id === iniId) || {}).status || null);
591
+ const sources = b && b.sources && b.sources.length ? 'Put together from ' + listWords(b.sources) + '. Each line opens its source. ' : '';
451
592
  return [
452
593
  el('div', { class: 'cal3-dt' }, el('h3', null, c3ic('cal'), 'Meeting'), el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), closeBtn(ui)),
453
594
  el('b', { class: 'cal3-title' }, e.title || e.id),
454
595
  el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
455
- el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag' }, n)),
456
- e.initiative ? el('span', { class: 'tag' }, 'Initiative: ' + (((ui.d.gantt || []).find((g) => g.id === e.initiative) || {}).title || e.initiative)) : null),
596
+ el('div', { class: 'chips2', 'data-cal3': 'meeting-chips' }, homeChip('Calendar'), ...peopleOf(e).map((n) => el('span', { class: 'tag', 'data-cal3': 'person' }, n)),
597
+ iniId ? el('span', { class: 'tag', 'data-cal3': 'meeting-initiative' }, 'Initiative: ' + iniTitle) : null,
598
+ iniStatus && STATUS_TONE[iniStatus] ? el('span', { class: 'tag cal3-tone-' + STATUS_TONE[iniStatus] }, iniStatus) : null),
457
599
  noteLink(ui, e),
458
600
  ...body,
459
- el('p', { class: 'dim cal3-note' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : 'PMOS proposes changes. It never edits your calendar.'),
601
+ el('p', { class: 'dim cal3-note', 'data-cal3': past ? 'past-note' : 'sources' }, past ? 'Only what PMOS holds a record for: items created or closed within 48 hours of the meeting. Each line opens its source.' : sources + 'PMOS proposes changes. It never edits your calendar.'),
460
602
  ];
461
603
  }
462
604
 
605
+ /* the card before a meeting (initiative c3-meeting-card, the mockup the PM approved on 2026-10-12): what it is for, what
606
+ * matters today, what happened last time and what to bring. Every part comes from the server's `brief` (lib/calendar-layers.js
607
+ * briefOf) or the prep items; a part with no record is left out. When PMOS knows little, it says what is missing. */
608
+ const STATUS_TONE = { 'at risk': 'warn', delayed: 'bad', overdue: 'bad' };
609
+ const listWords = (a) => (a.length < 2 ? a.join('') : a.slice(0, -1).join(', ') + ' and ' + a[a.length - 1]);
610
+ const ctxBox = (key, title, ...kids) => el('div', { class: 'cal3-ctx', 'data-cal3-ctx': key }, el('h4', null, ...[].concat(title)), ...kids);
611
+ const ul = (rows) => el('ul', { class: 'cal3-cl', role: 'list' }, ...rows);
612
+ const ci = (attrs, ...kids) => el('li', { class: 'cal3-ci', ...attrs }, ...kids);
613
+
614
+ function upcomingBody(ui, rec, items) {
615
+ const { h } = ui;
616
+ const b = rec && rec.brief ? rec.brief : null;
617
+ const prep = items.filter((x) => x.kind === 'prepared' || x.kind === 'todo');
618
+ const out = [];
619
+ if (b && b.gist && b.gist.length) out.push(el('div', { class: 'cal3-ctx cal3-gist', 'data-cal3-ctx': 'gist' }, el('h4', null, c3ic('spark', 'sm'), 'In short'), el('p', { class: 'cal3-p' }, b.gist.join(' '))));
620
+ if (b && b.matters && b.matters.length) out.push(ctxBox('matters', 'What matters today', ul(b.matters.map((x) => ci({ 'data-cal3-matter': x.tag },
621
+ el('span', { class: 'tag cal3-tone-' + x.tone }, x.tag), el('span', { class: 'cal3-ct' }, x.text), x.meta ? el('span', { class: 'dim mono cal3-at' }, x.meta) : null,
622
+ refLink(ui, x.ref, { initiative: x.ref.initiative, title: x.text }))))));
623
+ if (b && b.last) {
624
+ const l = b.last;
625
+ const open = ui.r.ctx.overlay ? null : el('a', { href: '#', class: 'cal3-ref', 'data-cal3': 'open-last-note', title: 'Open the note: ' + (l.title || 'Untitled note'),
626
+ onClick: (ev) => { ev.preventDefault(); openNote(ui.r, l.id); ui.redraw(); } }, c3ic('notes', 'sm'), 'Open the last note');
627
+ out.push(ctxBox('last', 'Since the last one · ' + h.dayLabel(String(l.at).slice(0, 10)),
628
+ l.private ? el('p', { class: 'dim cal3-note' }, 'That note is private, so only its title shows: ' + (l.title || 'Untitled note') + '.')
629
+ : l.overview ? el('p', { class: 'cal3-p' }, l.overview) : el('p', { class: 'dim cal3-note' }, 'That note has no summary yet.'),
630
+ !l.private && ((l.decisions || []).length || (l.actions || []).length) ? ul([
631
+ ...(l.decisions || []).map((t) => ci({ 'data-cal3-item': 'last-decision' }, el('span', { class: 'tag' }, 'Decision'), el('span', { class: 'cal3-ct' }, t))),
632
+ ...(l.actions || []).map((a) => ci({ 'data-cal3-item': 'last-action' }, el('span', { class: 'tag cal3-tone-warn' }, 'From last time'), el('span', { class: 'cal3-ct' }, a.text),
633
+ a.owner || a.due ? el('span', { class: 'dim mono cal3-at' }, [a.owner, a.due && 'due ' + a.due].filter(Boolean).join(' · ')) : null))]) : null,
634
+ el('div', { class: 'chips2' }, open, b.earlier > 1 ? el('span', { class: 'dim', 'data-cal3': 'earlier' }, b.earlier + ' earlier notes of this meeting') : null)));
635
+ }
636
+ if (prep.length || (b && b.prep)) out.push(ctxBox('prepare', 'To prepare', ul([
637
+ ...(b && b.prep ? [ci({ 'data-cal3-item': 'ready' }, el('span', { class: 'tag' }, 'To be ready'), el('span', { class: 'cal3-ct' }, b.prep))] : []),
638
+ ...prep.filter((x) => !(b && b.prep && /^Prep notes from your agent/.test(x.text))).map((x) => ci({ 'data-cal3-item': x.kind },
639
+ el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
640
+ x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
641
+ x.ref && REF[x.ref.type] ? refLink(ui, x.ref, { title: x.text }) : null))])));
642
+ if (b && ((b.links || []).length || (b.refs || []).length)) out.push(el('div', { class: 'chips2', 'data-cal3': 'meeting-links' },
643
+ ...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
644
+ ...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
645
+ : el('span', { class: 'chip2' }, r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null))));
646
+ const noIni = !(b && b.initiative); const noLast = !(b && b.last);
647
+ if (noIni || noLast) {
648
+ const missing = noIni && noLast ? 'No initiative is linked and there is no earlier note from this meeting.' : noIni ? 'No initiative is linked to this meeting.' : 'There is no earlier note from this meeting.';
649
+ out.push(ctxBox('knows', 'What PMOS knows', fold('info', missing + (out.length ? '' : ' Nothing is recorded to prepare.'), { 'data-cal3': 'knows' }),
650
+ ul([noIni ? ci({}, el('span', { class: 'tag' }, 'Link'), el('span', { class: 'cal3-ct' }, 'Link an initiative, and its blocked, late and due items show here.')) : null,
651
+ ci({}, el('span', { class: 'tag' }, 'Note'), el('span', { class: 'cal3-ct' }, 'Take a note in this meeting, and the next one opens with its summary and next steps.'))].filter(Boolean))));
652
+ }
653
+ return out;
654
+ }
655
+
463
656
  /* the free blocks of the range in words, with the suggested use or none; nothing is written */
464
657
  function freeList(ui, range) {
465
658
  const { M, h } = ui;
@@ -39,12 +39,12 @@ const DATED = [
39
39
  ['2026-10-13', 'own', 'mine', 'Share returns insights with sales', null, item('item-demo017'), 'returns'],
40
40
  ['2026-11-16', 'own', 'mine', 'Write the Q1 discovery plan', null, item('item-demo020'), null],
41
41
  ];
42
- /* the weekly status to leadership is a report date every Friday */
43
- for (let d = '2026-09-04'; d <= '2026-12-25'; d = addDays(d, 7)) DATED.push([d, 'report', 'reports', 'Weekly status to leadership', null, { type: 'report', id: 'leadership-weekly' }, null]);
42
+ /* the weekly status to leadership is a report date every Friday: routine, so Month, Quarter and Year count it (calendar-highlights) */
43
+ for (let d = '2026-09-04'; d <= '2026-12-25'; d = addDays(d, 7)) DATED.push([d, 'report', 'reports', 'Weekly status to leadership', null, { type: 'report', id: 'leadership-weekly' }, null, { cadence: 'weekly', routine: true }]);
44
44
 
45
45
  function layersIn(start, end) {
46
46
  return DATED.filter((x) => x[0] >= start && x[0] <= end).sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
47
- .map(([date, kind, layer, title, tone, ref, initiative], i) => ({ id: 'dated-' + date + '-' + i, date, kind, layer, title, tone, ref, ...(initiative ? { initiative } : {}) }));
47
+ .map(([date, kind, layer, title, tone, ref, initiative, extra], i) => ({ id: 'dated-' + date + '-' + i, date, kind, layer, title, tone, ref, ...(initiative ? { initiative } : {}), ...(extra || {}) }));
48
48
  }
49
49
 
50
50
  /* the suggested uses of a free block, from the demo's work items; a block past the first of a day gets none */
@@ -103,7 +103,21 @@ const CONTEXT = {
103
103
  'm4@2026-10-08': { phase: 'upcoming', held: true, items: [
104
104
  { kind: 'prepared', text: 'Three scope options with their dates', ref: init('checkout') },
105
105
  { kind: 'todo', text: 'Cut Checkout v2 to 3 core flows to hold 24 Oct?', ref: item('item-demo010') },
106
- ] },
106
+ ],
107
+ // the card before a meeting (initiative c3-meeting-card): what the server's briefOf puts together
108
+ brief: {
109
+ initiative: { id: 'checkout', title: 'Checkout v2', status: 'at risk' },
110
+ gist: ['For Checkout v2, which is at risk.', '1 item blocked, 1 item due today.'],
111
+ matters: [
112
+ { tone: 'bad', tag: 'Blocked', text: 'Sprint scope: waiting on the scope decision', meta: 'Dara · due 2026-10-08', ref: { type: 'plan-item', id: 'checkout-scope', initiative: 'checkout' } },
113
+ { tone: 'warn', tag: 'Due today', text: 'Confirm the 1 Oct numbers', meta: 'yours · due 2026-10-08', ref: item('item-demo001') },
114
+ ],
115
+ last: { id: 'note-demo-sp1', title: 'Sprint planning', at: '2026-10-01T14:00:00', private: false,
116
+ overview: 'The team split the checkout flows into 3 core and 2 later. Dara asked for a scope decision before this sprint.',
117
+ decisions: ['Payments stays in the first release.'], actions: [{ text: 'Send the 3 scope options', owner: 'You', due: 'Wed 7 Oct' }] },
118
+ earlier: 3, purpose: null, prep: null, links: [], refs: [],
119
+ sources: ['the calendar', '3 earlier notes of this meeting', 'the checkout plan', 'your items'],
120
+ } },
107
121
  'm5@2026-10-08': { phase: 'upcoming', held: false, items: [] },
108
122
  };
109
123
 
@@ -251,11 +251,11 @@ const MEET = [
251
251
  { id: 'm1', t: 'Standup', s: 9.5, e: 10, kind: 'ready', routine: true },
252
252
  { id: 'm2', t: 'Checkout sync', s: 10.5, e: 11.25, kind: 'prep ready' },
253
253
  { id: 'm3', t: 'Customer interview', s: 13, e: 14, kind: 'review prep' },
254
- { id: 'm4', t: 'Sprint planning', s: 14, e: 15, kind: 'blocked on your decision', hot: true, decide_by: '14:00' },
254
+ { id: 'm4', t: 'Sprint planning', s: 14, e: 15, kind: 'blocked on your decision', hot: true, decide_by: '14:00', att: ['Dara', 'Priya', 'Sam'] },
255
255
  { id: 'm5', t: 'Leadership update', s: 16.5, e: 17, kind: 'ready' },
256
256
  ];
257
257
  const hh = (x) => pad(Math.floor(x)) + ':' + pad(Math.round((x % 1) * 60));
258
- const ev = (date, m) => ({ id: m.id + '@' + date, title: m.t, at: date + 'T' + hh(m.s) + ':00', end: date + 'T' + hh(m.e) + ':00', kind: m.kind, ...(m.routine ? { mock_routine: true } : {}), ...(m.hot ? { mock_hot: true, mock_decide_by: m.decide_by } : {}) });
258
+ const ev = (date, m) => ({ id: m.id + '@' + date, title: m.t, at: date + 'T' + hh(m.s) + ':00', end: date + 'T' + hh(m.e) + ':00', kind: m.kind, ...(m.att ? { attendees: m.att } : {}), ...(m.routine ? { mock_routine: true } : {}), ...(m.hot ? { mock_hot: true, mock_decide_by: m.decide_by } : {}) });
259
259
  function eventsOn(date) {
260
260
  const dow = new Date(utcOf(date)).getUTCDay();
261
261
  if (date === TODAY) return MEET.map((m) => ev(date, m));
@@ -104,7 +104,8 @@ export function createDock(opts) {
104
104
  el('div', { class: 'chips2' }, el('b', { style: 'font-size:var(--fs-3)' }, s.name), el('span', { class: 'chips2 dim mono', style: 'font-size:var(--fs-1)' }, c3ic(PI[s.agent] || 'term', 'sm'), (PN[s.agent] || s.agent) + ' · ' + ((t && t.branch) || 'no branch'))),
105
105
  el('div', { class: 'dim mono', style: 'font-size:var(--fs-1)' }, facts),
106
106
  s.shown === 'needs' ? el('div', { class: 'fold a' }, c3ic('alert', 'sm'), 'needs you') : null,
107
- el('a', { class: 'chip2 lk', href: '/', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name + ' in the main app' }, ic('term', 'sm'), 'Open the session'),
107
+ el('a', { class: 'chip2 lk', href: '/', 'data-c3pg': 'dock-open-session', title: TIPS.terminalOpen, 'aria-label': 'Open the session ' + s.name,
108
+ onclick: (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.button > 0) return; e.preventDefault(); import('./c3-session-term.js').then((m) => m.openSessionTerm({ id: s.id, name: s.name })); } }, ic('term', 'sm'), 'Open the session'),
108
109
  term,
109
110
  el('div', { class: 'form' },
110
111
  el('input', { type: 'text', readonly: true, 'data-off': '', placeholder: s.shown === 'needs' ? 'Your answer' : 'Message', 'aria-label': 'Message', title: MORE, 'data-c3pg': 'dock-msg' }),
@@ -11,7 +11,7 @@
11
11
  * links and references, people. A note with no meeting says so and offers the main app's link pickers.
12
12
  * During the main app's live block while the note is recorded or paused (view-notes.js renderLive), else the transcript.
13
13
  * After the Notes card (decisions with why, next steps with owner, due and Make task, questions, conflicts, highlights,
14
- * the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
14
+ * the screenshots of the shared screen, the folded transcript) from final_notes, else from what was caught live (dest-notes.js partsOf).
15
15
  *
16
16
  * Nothing is invented: a part the note does not hold is left out or says so. The mockup's Rejected column, Settle, told and
17
17
  * not told, Route actions and the drafts have no data or route yet, so they are not drawn.
@@ -30,6 +30,7 @@ import { groupNotes, partsOf, conflictsOf, whenShort } from './dest-notes.js';
30
30
  import * as dock from './note-dock.js';
31
31
  import * as V from './view-notes.js';
32
32
  import { makeTask } from './notes-task.js';
33
+ import { screenStrip } from './notes-screen-view.js';
33
34
 
34
35
  const PHASES = [['before', 'clip', 'Before'], ['during', 'mic', 'During'], ['after', 'notes', 'After']];
35
36
  const startOf = (n) => (n.meeting && n.meeting.start) || n.recorded_from || n.created_at || n.updated_at;
@@ -91,13 +92,13 @@ async function read(s) {
91
92
  if (seq !== s.seq) return;
92
93
  if (l && l.ok) { s.rows = l.notes || []; s.err = null; } else { s.rows = s.rows || []; s.err = (l && l.error) || 'The notes could not be read.'; }
93
94
  if (!s.open && s.rows.length) { s.open = s.rows[0].id; return read(s); }
94
- if (one && one.ok && one.note && one.note.id === s.open) s.note = one.note;
95
+ if (one && one.ok && one.note && one.note.id === s.open) { s.note = one.note; s.shots = (one.screen && one.screen.frames) || []; s.rec = one.recording || null; }
95
96
  draw(s);
96
97
  }
97
98
 
98
99
  function choose(s, id) {
99
100
  if (s.open === id) return;
100
- s.open = id; s.note = null; s.phase = null;
101
+ s.open = id; s.note = null; s.phase = null; s.shots = []; s.rec = null;
101
102
  draw(s); read(s);
102
103
  }
103
104
 
@@ -164,14 +165,45 @@ function noteCol(s) {
164
165
  el('div', { class: 'stg', role: 'group', 'aria-label': 'Note stage', 'data-cv': 'phase' },
165
166
  PHASES.map(([k, ic, l]) => btn('sgb', { 'data-cal3-phase': k, 'aria-pressed': String(ph === k), title: l }, [c3ic(ic), el('span', null, l)], () => { s.phase = k; draw(s); })))),
166
167
  el('div', { class: 'chips2 dim cal3-sm' },
167
- [dayOf(startOf(n)), len, iniName(n.initiative) || 'No initiative'].filter(Boolean).join(' · '),
168
- ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('cal', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev))
169
- : linked(n) ? null : el('span', { class: 'tag' }, 'no meeting'),
170
- btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))));
168
+ [dayOf(startOf(n)), len].filter(Boolean).join(' · '),
169
+ btn('ib sm', { title: 'Copy as Markdown (with the meeting context)', 'aria-label': 'Copy as Markdown', 'data-cal3-notes': 'copy' }, c3ic('clip'), () => copy(n))),
170
+ links(s, n, ev));
171
171
  const body = ph === 'before' ? before(s, n) : ph === 'during' ? during(s, n, mode) : after(s, n, mode);
172
172
  return el('div', { class: 'cal3-ncol', 'data-cal3-notes': 'note', 'data-cal3-note': n.id, 'data-cal3-stage': ph }, head, ...[].concat(body));
173
173
  }
174
174
 
175
+ /* the note's meeting and initiative, each with its link: what it is linked to, then Link or Change (view-notes.js pickers) */
176
+ function links(s, n, ev) {
177
+ const adhoc = n.meeting && n.meeting.kind === 'adhoc';
178
+ const when = ev ? [dayOf(ev.start), hm(ev.start) + (ev.end ? ' to ' + hm(ev.end) : '')].filter(Boolean).join(' ') : '';
179
+ return el('div', { class: 'cal3-nlinks cal3-sm', 'data-cal3-notes': 'links' },
180
+ el('span', { class: 'chips2' }, c3ic('cal', 'sm'),
181
+ ev ? el('span', { 'data-cal3-notes': 'meeting' }, el('b', null, ev.title), ' · ' + when)
182
+ : el('span', { class: 'dim', 'data-cal3-notes': 'meeting' }, adhoc ? 'A meeting that is not on the calendar' : 'No meeting'),
183
+ !ev && !adhoc ? el('span', { class: 'tag' }, 'no meeting') : null,
184
+ btn('chip2 lk', { 'data-cal3-notes': 'meeting-link', title: ev || adhoc ? 'Link the note to another meeting' : 'Link the note to a meeting on the calendar' }, ev || adhoc ? 'Change' : 'Link to a meeting', () => V.linkMeeting(vctx(s), n.id)),
185
+ ev ? btn('chip2 lk', { 'data-cal3-notes': 'show-meeting', title: 'Show the meeting in the calendar' }, [c3ic('next', 'sm'), 'Show in the calendar'], () => showMeeting(s, ev)) : null),
186
+ el('span', { class: 'chips2' }, c3ic('init', 'sm'),
187
+ n.initiative ? el('span', { 'data-cal3-notes': 'initiative' }, el('b', null, iniName(n.initiative))) : el('span', { class: 'dim', 'data-cal3-notes': 'initiative' }, 'No initiative'),
188
+ btn('chip2 lk', { 'data-cal3-notes': 'initiative-link', title: n.initiative ? 'Link the note to another initiative' : 'Link the note to an initiative' }, n.initiative ? 'Change' : 'Link to an initiative', () => V.linkInitiative(vctx(s), n.id))));
189
+ }
190
+
191
+ /* the replay of the note's kept audio: one <audio> per note, kept across redraws so playback goes on (as dest-notes.js does) */
192
+ function replayOf(s, n) {
193
+ const rec = s.rec;
194
+ if (!rec || n.private) { s.audio = null; return null; }
195
+ const key = n.id + ':' + rec.bytes + ':' + (rec.cleaned ? 1 : 0);
196
+ if (s.audio && (s.audio.key === key || !s.audio.el.paused)) return s.audio.el;
197
+ const a = el('audio', { controls: true, preload: 'metadata', class: 'nt-audio cal3-audio', 'data-cal3-notes': 'audio', src: '/api/notes/recording?id=' + encodeURIComponent(n.id) + '&b=' + rec.bytes + (rec.cleaned ? '&c=1' : '') });
198
+ s.audio = { key, el: a };
199
+ return a;
200
+ }
201
+ function seek(s, ms) {
202
+ const a = s.audio && s.audio.el;
203
+ if (!a) return;
204
+ try { a.currentTime = Math.max(0, ms / 1000); a.play(); } catch { /* the audio is not ready */ }
205
+ }
206
+
175
207
  const vctx = (s) => ({ state: {}, setState: () => { if (s.box.isConnected) { read(s); } } });
176
208
  const linkButtons = (s, n) => el('div', { class: 'chips2' },
177
209
  btn('chip2 lk', { 'data-cal3-notes': 'link-meeting' }, [c3ic('cal', 'sm'), 'Link to a meeting'], () => V.linkMeeting(vctx(s), n.id)),
@@ -206,7 +238,9 @@ function before(s, n) {
206
238
  if (names.length) rows.push(prepRow('user', 'People', el('span', { class: 'chips2 cal3-wrapl' }, names.map(avatar))));
207
239
  else if (c && c.attendees) rows.push(prepRow('user', 'People', c.attendees + ' attendees. Names are not kept while calendar names are off.'));
208
240
  const when = (ev.start ? dayOf(ev.start) + ' ' + hm(ev.start) : '') + (ev.end ? ' to ' + hm(ev.end) : '');
209
- rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null].filter(Boolean).join(' · ')));
241
+ rows.push(prepRow('cal', 'Meeting', [ev.title, when, c && c.where ? 'where: ' + c.where : null, c && c.organizer ? 'organizer: ' + c.organizer : null,
242
+ c && c.series ? 'series: ' + c.series : null, c && c.role ? 'your role: ' + c.role : null, c && c.external ? 'with people outside the company' : null,
243
+ c && c.attendees && names.length ? c.attendees + ' attendees' : null].filter(Boolean).join(' · ')));
210
244
  return el('section', { class: 'card', 'data-cv': 'stage-before' }, h3, ...rows,
211
245
  c ? null : el('p', { class: 'dim cal3-sm' }, 'The calendar gave no more about this meeting.'));
212
246
  }
@@ -252,6 +286,7 @@ function after(s, n, mode) {
252
286
  const kids = [
253
287
  el('div', { class: 'c3cv-head' }, el('h3', null, c3ic('notes'), 'Notes'), recordButton(s, n, mode)),
254
288
  el('div', { class: 'chips2' }, el('b', null, n.title || 'Untitled note'), el('span', { class: 'dim mono cal3-sm' }, [dayOf(startOf(n)), mins(lenOf(n))].filter(Boolean).join(' · ')), ...names.map(avatar)),
289
+ ...(() => { const rp = mode ? null : replayOf(s, n); return rp ? [el('div', { class: 'cal3-replay', 'data-cal3-notes': 'replay' }, el('h4', null, 'Replay'), rp, el('p', { class: 'dim cal3-sm' }, 'Kept on this machine.'))] : []; })(),
255
290
  ...cf.map((c) => el('div', { class: 'fold w', 'data-cal3-notes': 'conflict' }, c3ic('alert', 'sm'), '“' + c.text + '” differs from ', el('span', { class: 'mono' }, c.doc + (c.line ? ':' + c.line : '')))),
256
291
  p.overview ? editable(p.from === 'final', 'p', p.overview, 'Overview', (t) => saveFinal(s, n, 'overview', 0, t), { class: 'cal3-ov', 'data-cal3-notes': 'overview' })
257
292
  : el('p', { class: 'dim cal3-ov', 'data-cal3-notes': 'no-overview' }, mode ? 'The overview is written when you stop recording.' : 'No overview yet.'),
@@ -268,7 +303,10 @@ function after(s, n, mode) {
268
303
  const hl = n.highlights || [];
269
304
  if (hl.length) kids.push(el('div', { class: 'cal3-ctx', 'data-cal3-notes': 'highlights' }, el('h4', null, 'Highlights'),
270
305
  el('ul', { class: 'cal3-cl', role: 'list' }, hl.map((x) => el('li', { class: 'cal3-ci' }, el('span', { class: 'dim mono cal3-at' }, typeof x.t === 'number' ? V.fmtTime(x.t) : ''), el('span', { class: 'tag' }, x.label || 'Highlight'), el('span', { class: 'cal3-ct' }, x.text))))));
271
- if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
306
+ if (!p.overview && !p.decisions.length && !p.actions.length && !p.questions.length && !hl.length && !cf.length && !(s.shots || []).length) kids.push(fold('moon', mode ? 'Decisions and next steps show here as they are caught, and in full after you stop.' : 'No decisions, next steps or questions are held for this note.', { 'data-cal3-notes': 'nothing' }));
307
+ // the screenshots of the shared screen, after Stop, as the main app's Notes shows them (notes-screen-view.js)
308
+ const shots = mode || n.private ? [] : s.shots || [];
309
+ if (shots.length) kids.push(el('div', { 'data-cal3-notes': 'screen' }, screenStrip({ id: n.id, frames: shots, seek: s.audio ? (ms) => seek(s, ms) : null, replay: s.audio ? s.audio.el : null })));
272
310
  const tr = transcriptOf(n);
273
311
  if (!mode && tr.length) kids.push(el('details', { 'data-cal3-notes': 'transcript-fold' }, el('summary', { class: 'dim cal3-sm' }, 'Transcript · ' + tr.length + (tr.length === 1 ? ' line' : ' lines')),
274
312
  el('div', { class: 'term cal3-term' }, tr.map((x) => V.fmtTime(x.t || 0) + ' ' + x.text).join('\n'))));
@@ -0,0 +1,44 @@
1
+ /* pm/c3-session-term.js — a session's real terminal inside console3, in a sheet, so "Open the session" no longer leaves /c3.html.
2
+ * openSessionTerm({ id, name })
3
+ * The terminal is tab-term.js's own (the one the main app draws): the same socket, attach, resize and input. Closing the sheet detaches the
4
+ * pane and leaves the session running, as closing a tab in the main app does. xterm and its addons are the main app's vendored globals,
5
+ * loaded here on the first open because /c3.html does not load them up front. */
6
+ import { el } from './dom.js';
7
+ import { openSheet } from './c3-sheet.js';
8
+ import { toast } from './c3-toast.js';
9
+ import { pmGet } from './http.js';
10
+
11
+ const VENDOR = ['/vendor/xterm.js', '/vendor/addon-fit.js', '/vendor/addon-web-links.js'];
12
+ let vendor = null;
13
+
14
+ function script(src) {
15
+ return new Promise((resolve) => {
16
+ if (document.querySelector('script[src="' + src + '"]')) { resolve(true); return; }
17
+ const s = document.createElement('script');
18
+ s.src = src; s.onload = () => resolve(true); s.onerror = () => resolve(false);
19
+ document.head.append(s);
20
+ });
21
+ }
22
+ function loadVendor() {
23
+ if (typeof window.Terminal === 'function') return Promise.resolve(true);
24
+ if (!vendor) {
25
+ if (!document.querySelector('link[href="/vendor/xterm.css"]')) document.head.append(el('link', { rel: 'stylesheet', href: '/vendor/xterm.css' }));
26
+ // the addons read window.Terminal when they run, so xterm goes first
27
+ vendor = script(VENDOR[0]).then((ok) => (ok ? Promise.all(VENDOR.slice(1).map(script)).then(() => true) : false));
28
+ }
29
+ return vendor;
30
+ }
31
+
32
+ export async function openSessionTerm({ id, name } = {}) {
33
+ if (!id) return null;
34
+ 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; }
35
+ const [ok, term] = await Promise.all([loadVendor(), import('./tab-term.js').catch(() => null)]);
36
+ if (!ok || !term) { toast({ text: 'The terminal could not be loaded on this page.', tone: 'error' }); return null; }
37
+ const tab = { id: 'c3-session:' + id, kind: 'term', sessionId: id, spawn: false, sess: id };
38
+ 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) });
41
+ term.mountTerm(host, tab);
42
+ requestAnimationFrame(() => { term.refitVisible(); term.focusTerm(tab); });
43
+ return sheet;
44
+ }