create-pmos 0.133.0 → 0.135.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 (212) 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/bin/notes-replay.js +235 -0
  7. package/workspace/c3-briefing-test.js +130 -0
  8. package/workspace/c3-foundation-test.js +159 -0
  9. package/workspace/c3-ib-items-test.js +105 -0
  10. package/workspace/c3-ib-more-test.js +120 -0
  11. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  12. package/workspace/c3-ini-harness.js +66 -0
  13. package/workspace/c3-initiatives-test.js +140 -0
  14. package/workspace/c3-one-home-test.js +14 -1
  15. package/workspace/c3-reports-test.js +57 -18
  16. package/workspace/c3-settings-test.js +139 -0
  17. package/workspace/c3-work-test.js +42 -10
  18. package/workspace/fixtures/one-home-baseline.json +1 -9
  19. package/workspace/guide/briefing-cards.md +15 -3
  20. package/workspace/guide/briefing.md +12 -8
  21. package/workspace/guide/calendar-popup.md +2 -2
  22. package/workspace/guide/console-calendar.md +39 -10
  23. package/workspace/guide/index.md +1 -0
  24. package/workspace/guide/initiative-boards.md +65 -0
  25. package/workspace/guide/jira.md +3 -1
  26. package/workspace/guide/layout.md +2 -0
  27. package/workspace/guide/my-work.md +5 -3
  28. package/workspace/guide/notes.md +35 -2
  29. package/workspace/guide/reports-dashboards.md +29 -5
  30. package/workspace/guide/settings.md +68 -0
  31. package/workspace/guide/welcome.md +10 -10
  32. package/workspace/lib/calendar-cli.js +3 -2
  33. package/workspace/lib/calendar-layers.js +21 -10
  34. package/workspace/lib/calendar-tidy.js +230 -0
  35. package/workspace/lib/engine/boards.js +542 -0
  36. package/workspace/lib/engine/breakdown-ops.js +180 -0
  37. package/workspace/lib/engine/calendar-import.js +84 -0
  38. package/workspace/lib/engine/calendar-model.js +185 -0
  39. package/workspace/lib/engine/card-state.js +92 -0
  40. package/workspace/lib/engine/derived/derive.js +92 -0
  41. package/workspace/lib/engine/derived/plan.js +3 -0
  42. package/workspace/lib/engine/derived/trust.js +3 -0
  43. package/workspace/lib/engine/doc-health.js +142 -0
  44. package/workspace/lib/engine/doc-sheet.js +75 -0
  45. package/workspace/lib/engine/intake.js +78 -0
  46. package/workspace/lib/engine/outbox.js +86 -0
  47. package/workspace/lib/engine/plan-forecast.js +342 -0
  48. package/workspace/lib/engine/promises.js +29 -0
  49. package/workspace/lib/engine/records/index.js +114 -0
  50. package/workspace/lib/engine/releases.js +96 -0
  51. package/workspace/lib/engine/replies.js +99 -0
  52. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  53. package/workspace/lib/engine/report-families.js +175 -0
  54. package/workspace/lib/engine/roadmap-ops.js +150 -0
  55. package/workspace/lib/engine/sessions-list.js +27 -0
  56. package/workspace/lib/engine/signal-rules.js +95 -0
  57. package/workspace/lib/engine/signals/cards.js +57 -0
  58. package/workspace/lib/engine/signals/detectors.js +370 -0
  59. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  60. package/workspace/lib/engine/signals/facts.js +197 -0
  61. package/workspace/lib/engine/signals/index.js +33 -0
  62. package/workspace/lib/engine/signals/kinds.js +110 -0
  63. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  64. package/workspace/lib/engine/signals/options.js +24 -0
  65. package/workspace/lib/engine/signals/score.js +72 -0
  66. package/workspace/lib/engine/signals/select.js +47 -0
  67. package/workspace/lib/engine/step-planner.js +65 -0
  68. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  69. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  70. package/workspace/lib/engine/trust/import-guard.js +36 -0
  71. package/workspace/lib/engine/trust/ledger.js +53 -0
  72. package/workspace/lib/engine/trust/outbox.js +45 -0
  73. package/workspace/lib/engine/trust/plan.js +5 -0
  74. package/workspace/lib/engine/trust/trust.js +21 -0
  75. package/workspace/lib/engine/truth-table.js +57 -0
  76. package/workspace/lib/engine/workspace-lib.js +24 -0
  77. package/workspace/lib/factory-join.js +1 -1
  78. package/workspace/lib/facts-async.js +4 -4
  79. package/workspace/lib/headless.js +95 -0
  80. package/workspace/lib/import-overlay.js +2 -1
  81. package/workspace/lib/jira-status.js +30 -3
  82. package/workspace/lib/notes-audio.js +28 -9
  83. package/workspace/lib/notes-capture.js +136 -0
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-live-guard.js +117 -0
  86. package/workspace/lib/notes-replay.js +125 -0
  87. package/workspace/lib/notes-screen.js +180 -0
  88. package/workspace/lib/notes-whisper.js +139 -16
  89. package/workspace/lib/notes.js +4 -2
  90. package/workspace/lib/outbox-store.js +10 -6
  91. package/workspace/lib/pmos-verbs.js +3 -1
  92. package/workspace/lib/proactive-cli.js +7 -0
  93. package/workspace/lib/proactive.js +15 -2
  94. package/workspace/lib/repo-script.js +36 -0
  95. package/workspace/lib/report-dashboards.js +5 -2
  96. package/workspace/lib/report-frame.js +122 -0
  97. package/workspace/lib/report-schedules.js +113 -0
  98. package/workspace/lib/route-modules.js +9 -2
  99. package/workspace/lib/routes-boards.js +734 -0
  100. package/workspace/lib/routes-brief-page.js +13 -4
  101. package/workspace/lib/routes-briefing.js +22 -11
  102. package/workspace/lib/routes-c3-init.js +4 -3
  103. package/workspace/lib/routes-c3-life.js +2 -1
  104. package/workspace/lib/routes-c3-reports.js +96 -13
  105. package/workspace/lib/routes-c3-strategy.js +4 -3
  106. package/workspace/lib/routes-c3-timeline.js +3 -2
  107. package/workspace/lib/routes-calendar-import.js +3 -2
  108. package/workspace/lib/routes-calendar-views.js +9 -4
  109. package/workspace/lib/routes-doc-health.js +2 -1
  110. package/workspace/lib/routes-doc-sheet.js +2 -1
  111. package/workspace/lib/routes-forecast.js +2 -1
  112. package/workspace/lib/routes-hooks.js +3 -2
  113. package/workspace/lib/routes-initiatives.js +2 -1
  114. package/workspace/lib/routes-intake.js +2 -1
  115. package/workspace/lib/routes-notes.js +113 -15
  116. package/workspace/lib/routes-outbox.js +23 -3
  117. package/workspace/lib/routes-pm-items.js +19 -6
  118. package/workspace/lib/routes-records.js +3 -2
  119. package/workspace/lib/routes-releases.js +2 -1
  120. package/workspace/lib/routes-report-facts.js +4 -3
  121. package/workspace/lib/routes-report-families.js +2 -1
  122. package/workspace/lib/routes-roadmap-ops.js +5 -4
  123. package/workspace/lib/routes-runs.js +6 -5
  124. package/workspace/lib/routes-settings.js +51 -0
  125. package/workspace/lib/routes-signals.js +3 -2
  126. package/workspace/lib/routes-stage-tools.js +220 -0
  127. package/workspace/lib/run-answers.js +2 -1
  128. package/workspace/lib/settings-store.js +226 -0
  129. package/workspace/lib/stage-tools.js +208 -0
  130. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  131. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  132. package/workspace/lib/work-evidence.js +73 -0
  133. package/workspace/package-lock.json +2 -2
  134. package/workspace/package.json +1 -1
  135. package/workspace/public/pm/briefing-deck.js +63 -17
  136. package/workspace/public/pm/briefing-model.js +36 -7
  137. package/workspace/public/pm/c3-ambient.js +100 -0
  138. package/workspace/public/pm/c3-brief-needs.js +26 -6
  139. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  140. package/workspace/public/pm/c3-brief-today.js +56 -21
  141. package/workspace/public/pm/c3-brief.js +2 -1
  142. package/workspace/public/pm/c3-calendar.js +161 -42
  143. package/workspace/public/pm/c3-calpop.js +5 -5
  144. package/workspace/public/pm/c3-demo-boards.js +649 -0
  145. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  146. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  147. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  148. package/workspace/public/pm/c3-demo-data.js +17 -27
  149. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  150. package/workspace/public/pm/c3-demo.js +2 -1
  151. package/workspace/public/pm/c3-dock.js +1 -1
  152. package/workspace/public/pm/c3-ib-docs.js +80 -0
  153. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  154. package/workspace/public/pm/c3-ib-items.js +201 -0
  155. package/workspace/public/pm/c3-ib-releases.js +51 -0
  156. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  157. package/workspace/public/pm/c3-ib-triage.js +178 -0
  158. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  159. package/workspace/public/pm/c3-ini-records.js +197 -0
  160. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  161. package/workspace/public/pm/c3-ini-ui.js +229 -0
  162. package/workspace/public/pm/c3-ini-work.js +139 -0
  163. package/workspace/public/pm/c3-initiatives.css +218 -0
  164. package/workspace/public/pm/c3-initiatives.js +741 -0
  165. package/workspace/public/pm/c3-plan.js +2 -2
  166. package/workspace/public/pm/c3-reports.css +47 -47
  167. package/workspace/public/pm/c3-reports.js +223 -185
  168. package/workspace/public/pm/c3-settings.css +74 -0
  169. package/workspace/public/pm/c3-settings.js +703 -0
  170. package/workspace/public/pm/c3-shell.js +102 -36
  171. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  172. package/workspace/public/pm/c3-terminal.js +2 -2
  173. package/workspace/public/pm/calendar-views.js +26 -13
  174. package/workspace/public/pm/components.css +319 -213
  175. package/workspace/public/pm/console3.css +118 -23
  176. package/workspace/public/pm/dest-notes.js +26 -4
  177. package/workspace/public/pm/notes-screen-capture.js +165 -0
  178. package/workspace/public/pm/notes-screen-view.js +98 -0
  179. package/workspace/public/pm/panel-trees.js +5 -2
  180. package/workspace/public/pm/pm-work-view.js +233 -146
  181. package/workspace/public/pm/shell-bell.js +3 -2
  182. package/workspace/public/pm/shell-recording.js +148 -17
  183. package/workspace/public/pm/tokens.css +2 -2
  184. package/workspace/public/pm/view-notes.js +53 -11
  185. package/workspace/public/pm/welcome.js +197 -89
  186. package/workspace/public/welcome.html +6 -4
  187. package/workspace/server.js +105 -6
  188. package/workspace/skills/day-check.skill +1 -1
  189. package/workspace/skills/jira-sync.skill +6 -4
  190. package/workspace/skills/morning-brief.skill +1 -1
  191. package/workspace/test-files.json +14 -3
  192. package/workspace/test-lists/c3-ib-items.json +9 -0
  193. package/workspace/test-lists/c3-ib-more.json +9 -0
  194. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  195. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  196. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  197. package/workspace/test-lists/console3-briefing.json +9 -0
  198. package/workspace/test-lists/console3-foundation.json +9 -0
  199. package/workspace/test-lists/console3-reports.json +3 -3
  200. package/workspace/test-lists/meeting-notes.json +12 -4
  201. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  202. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  203. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  204. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  205. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  206. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  207. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  208. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  209. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  210. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  211. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  212. package/workspace/lib/vendor/workspace/lib/state-root.js +0 -587
@@ -66,7 +66,8 @@ let unreadSeen = 0; // how many the person has already looked
66
66
  let lastCount = 0;
67
67
  let bellSheet = null;
68
68
  let liveTimer = null; let liveDone = false;
69
- const notifList = (b) => [...arrived, ...notifsOf(b)];
69
+ /* b.notices: what a dashboard schedule made (routes-briefing.js); each goes to Reports > Drafts. */
70
+ const notifList = (b) => [...arrived, ...notifsOf(b), ...(b && Array.isArray(b.notices) ? b.notices : [])];
70
71
 
71
72
  export function openBellList() {
72
73
  if (bellSheet && bellSheet.root.isConnected) { bellSheet.close(); return bellSheet; }
@@ -128,7 +129,7 @@ export function mountBell(host, opts) {
128
129
  };
129
130
  rec.off = onBriefing((b) => {
130
131
  rec.b = b;
131
- const n = bellCount(b) + arrived.length;
132
+ const n = bellCount(b) + arrived.length + (b && Array.isArray(b.notices) ? b.notices.length : 0);
132
133
  const bump = n > rec.count; rec.count = n; lastCount = n;
133
134
  rec.sync();
134
135
  if (bump) { badge.classList.remove('bump'); void badge.offsetWidth; badge.classList.add('bump'); }
@@ -1,14 +1,18 @@
1
- /* pm/shell-recording.js — the recording indicator beside the bell (console3 wave E2b).
1
+ /* pm/shell-recording.js — the Record control in console3's top bar (console3 wave E2b; the Record control port).
2
2
  *
3
3
  * mountRecording(host) / disposeRecording(host)
4
4
  *
5
- * It starts nothing. It reads the real recorder through note-dock.js (recordState) and offers only the controls the Record control in
6
- * the top bar already has: open the note, mark a moment (highlightNow), pause, resume and stop (pauseNow, resumeNow, stopNow). When
7
- * nothing is recording or paused the group is hidden. While one is, a red dot sits on the Notes tab (`[data-fr-dest="notes"]` in the
8
- * frame's bar; a phone tab `[data-dest="notes"]` if one is drawn). Lifted: console3.html `.recg` and `recCtl` (lines 916 to 924, 4385
9
- * to 4400). The group shows the recorder's state under reduced motion as a static dot. The clock is the recorder's own start time.
5
+ * The control is always there, idle included, with the same capabilities as the main app's Record control (pm/topbar.js, pm/note-dock.js):
6
+ * New (an untitled note that records at once), an arrow that lists today's meetings that have not started (a pick makes a note linked to that
7
+ * calendar event and records it), and, while a meeting records or is paused, a chip that names it with Highlight, Pause or Resume, and Stop
8
+ * (Stop asks first and offers "Pause instead"). All of it goes through pm/note-dock.js, which owns the real recorder, so the audio capture of
9
+ * pm/view-notes.js runs here without the main app's Notes place on screen. The chip opens the note in the main app, because console3 has no
10
+ * Notes place. While one meeting records, a red dot sits on the Calendar tab (`[data-c3-tab="cal"]`) and on a Notes tab if a frame draws one.
11
+ * On /c3.html?demo=1 the control drives a SIMULATED recorder (see below): it opens no microphone and saves nothing; it shows the idle Record button and
12
+ * arrow, and the live group while the simulation runs. Lifted: console3.html `.recg` and `recCtl` (lines 916 to 924, 4385 to 4400). The clock is the recorder's own start time.
10
13
  */
11
14
  import { el } from './dom.js';
15
+ import { button } from './primitives.js';
12
16
  import { ensureC3Css, c3ic } from './pm-work-view.js';
13
17
  import * as dock from './note-dock.js';
14
18
  import { recView, clockOf } from './shell-model.js';
@@ -20,10 +24,10 @@ import { toast } from './c3-toast.js';
20
24
  const TAB = '[data-fr-dest="notes"], #mobNav [data-dest="notes"], [data-c3-tab="cal"]';
21
25
 
22
26
  /* ── demo mode: a SIMULATED recorder ──────────────────────────────────────────────────────────────────────────────────────────────
23
- * A page cannot record (nothing on a page may record). On /c3.html?demo=1 the Record button of the Calendar's During and After stages drives
24
- * this stand-in, so the top bar shows the mockup's recording group: a pulsing dot, the clock, mark moment, pause or resume, and stop.
25
- * It keeps a clock (performance.now: the demo freezes Date) and nothing else. No microphone is opened and nothing is saved.
26
- * Real mode never reaches it: `isDemo()` reads the page's data-c3-demo mark, which only the demo sets. */
27
+ * On /c3.html?demo=1 the Record button of the Calendar's During and After stages drives this stand-in, so the top bar shows the mockup's
28
+ * recording group: a pulsing dot, the clock, mark moment, pause or resume, and stop. It keeps a clock (performance.now: the demo freezes
29
+ * Date) and nothing else. No microphone is opened and nothing is saved. Real mode never reaches it: `isDemo()` reads the page's
30
+ * data-c3-demo mark, which only the demo sets. */
27
31
  export const isDemo = () => typeof document !== 'undefined' && !!document.querySelector('[data-c3-demo]');
28
32
  const SIM = { mode: 'idle', id: 'sim', title: '', started: 0, pausedAt: 0 };
29
33
  const tick = () => performance.now();
@@ -46,15 +50,36 @@ export const simDock = {
46
50
  };
47
51
  const live = new WeakMap();
48
52
 
53
+ /* Demo mode, idle: the mockup's Record button and the arrow beside it (the same two buttons the real control draws). Record starts the simulated recorder with an
54
+ * untitled note; the arrow lists the same choice and today's meetings that have not started. Nothing is opened or saved. */
55
+ function simIdle(d) {
56
+ const glyph = (dd) => { const sv = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); sv.setAttribute('class', 'ic sm'); sv.setAttribute('viewBox', '0 0 24 24'); sv.setAttribute('aria-hidden', 'true'); const pth = document.createElementNS('http://www.w3.org/2000/svg', 'path'); pth.setAttribute('d', dd); sv.appendChild(pth); return sv; };
57
+ const start = (title) => { if (d === simDock) simStart(title); };
58
+ const mic = button({ variant: 'ghost', label: [c3ic('mic', 'sm'), el('span', { class: 'c3rec__lbl' }, 'Record')], onClick: () => start('Untitled note'),
59
+ attrs: { title: 'Record an untitled note', 'aria-label': 'Record an untitled note', 'data-fr-rec-new': '' } });
60
+ const arrow = button({ variant: 'ghost', label: glyph('M6 9l6 6 6-6'), attrs: { title: 'Record for an upcoming meeting', 'aria-label': 'Record for an upcoming meeting', 'aria-haspopup': 'menu', 'data-fr-rec-upcoming': '' },
61
+ onClick: async (e) => {
62
+ const anchor = e.currentTarget;
63
+ const { menu } = await import('./primitives.js');
64
+ let evs = [];
65
+ try { const links = await import('./notes-links.js'); evs = links.upcomingWithoutNote(await links.upcomingToday()); } catch { evs = []; }
66
+ menu({ anchor, ariaLabel: 'Record a new meeting', items: [{ label: 'Untitled note', icon: 'plus', detail: 'not linked to a meeting', attrs: { 'data-fr-rec-untitled': '' }, onSelect: () => start('Untitled note') }, { sep: true },
67
+ ...(evs.length ? evs.slice(0, 4).map((x) => ({ label: x.title, hint: x.time || '', attrs: { 'data-fr-rec-event': x.id || '' }, onSelect: () => start(x.title) })) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
68
+ } });
69
+ return [mic, arrow];
70
+ }
71
+
49
72
  export function mountRecording(host, opts) {
50
73
  if (!host) return;
51
74
  disposeRecording(host);
52
75
  ensureC3Css();
53
- const d = (opts && opts.dock) || (isDemo() ? simDock : dock);
76
+ if (!(opts && opts.dock) && !isDemo()) return mountReal(host);
77
+ const d = (opts && opts.dock) || simDock;
54
78
  const nowMs = () => (d.now ? d.now() : Date.now());
55
79
  const grp = el('span', { class: 'recg', role: 'group', 'aria-label': 'Recording', hidden: true, 'data-shell': 'recording' });
56
- host.insertBefore(grp, host.firstChild);
57
- const rec = { grp, key: '', clock: null, tabs: [] };
80
+ const wrap = el('span', { class: 'c3rec', 'data-fr': 'record' }, grp); // the same wrapper the real control has, so the idle group takes its look
81
+ host.insertBefore(wrap, host.firstChild);
82
+ const rec = { grp: wrap, key: '', clock: null, tabs: [] };
58
83
  const tabDot = (on) => {
59
84
  for (const t of document.querySelectorAll(TAB)) {
60
85
  let dot = t.querySelector('.shell-recdot');
@@ -73,14 +98,16 @@ export function mountRecording(host, opts) {
73
98
  const key = [v.mode, st.id, v.title, v.dot].join(':');
74
99
  if (key !== rec.key) {
75
100
  rec.key = key;
76
- grp.hidden = !v.shown;
101
+ grp.hidden = false; // the mockup shows the idle Record control too
77
102
  grp.classList.toggle('paused', v.mode === 'paused');
78
- grp.setAttribute('aria-label', v.shown ? (v.mode === 'recording' ? 'Recording ' : 'Paused: ') + v.title : 'Recording');
79
- if (!v.shown) { grp.replaceChildren(); rec.clock = null; }
103
+ grp.classList.toggle('idle', !v.shown);
104
+ grp.setAttribute('aria-label', v.shown ? (v.mode === 'recording' ? 'Recording ' : 'Paused: ') + v.title : 'New recording');
105
+ if (!v.shown) { rec.clock = null; grp.replaceChildren(...simIdle(d)); }
80
106
  else {
81
107
  rec.clock = el('span', { class: 'recclk', 'data-shell-rec-clock': '' });
82
108
  const open = el('button', { type: 'button', title: (v.mode === 'recording' ? 'Recording: ' : 'Paused: ') + v.title + '. Open the live note', 'aria-label': 'Open the note: ' + v.title, 'data-shell-rec-open': '', onclick: () => d.openDock(st.id) },
83
- el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title));
109
+ el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
110
+ el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings). Demo value.' }, 'EN forced \u00b7 whisper'));
84
111
  const kids = [open];
85
112
  if (v.canMark) kids.push(btn('Mark this moment', 'highlighter', () => d.highlightNow(), 'data-shell-rec-mark'));
86
113
  if (v.canPause) kids.push(btn('Pause', 'pause', () => d.pauseNow(), 'data-shell-rec-pause'));
@@ -103,8 +130,112 @@ export function disposeRecording(host) {
103
130
  const rec = host && live.get(host);
104
131
  if (!rec) return;
105
132
  clearInterval(rec.timer);
133
+ if (rec.off) rec.off();
106
134
  if (rec.onSim) window.removeEventListener('c3-sim-rec', rec.onSim);
107
135
  rec.grp.remove();
108
136
  for (const t of document.querySelectorAll(TAB)) { const dot = t.querySelector('.shell-recdot'); if (dot) dot.remove(); }
109
137
  live.delete(host);
110
138
  }
139
+
140
+ /* ── real mode: the always-visible Record control ─────────────────────────────────────────────────────────────────────────────────── */
141
+
142
+ const NS_SVG = 'http://www.w3.org/2000/svg';
143
+ 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
+
146
+ /** The chip opens the note. console3 has no Notes place, so it opens the main app, where the full note is. */
147
+ const openNote = () => { try { window.open('/', '_blank', 'noopener'); } catch { /* a blocked pop-up */ } };
148
+
149
+ /* The engine and language the live transcript uses, on the chip (notes-recording-reliability): a meeting transcribed in the wrong
150
+ * language because of a setting is visible while it records. Read once per recording from GET /api/notes/engine. */
151
+ const ENGINE = { Whisper: 'whisper', Qwen: 'qwen' };
152
+ async function engineLabel() {
153
+ try {
154
+ const { pmGet } = await import('./http.js');
155
+ const r = await pmGet('/api/notes/engine');
156
+ const w = r && r.ok ? r.whisper : null;
157
+ if (!w || !w.live) return '';
158
+ const name = Object.keys(ENGINE).find((k) => ENGINE[k] === w.live) || w.live;
159
+ const lang = ((w.languages || []).find((x) => x.value === w.language) || {}).label || w.language || '';
160
+ return name + (lang ? ' · ' + lang : '');
161
+ } catch { return ''; }
162
+ }
163
+
164
+ function mountReal(host) {
165
+ ensureC3Css();
166
+ let ready = false;
167
+ const wrap = el('span', { class: 'c3rec', 'data-fr': 'record', 'data-shell': 'recording' });
168
+ host.insertBefore(wrap, host.firstChild);
169
+ const rec = { grp: wrap, key: '', clock: null };
170
+ const tabDot = (on) => {
171
+ for (const t of document.querySelectorAll(TAB)) {
172
+ let dot = t.querySelector('.shell-recdot');
173
+ if (!dot) { dot = el('span', { class: 'shell-recdot', 'aria-hidden': 'true' }); t.appendChild(dot); }
174
+ dot.hidden = !on;
175
+ }
176
+ };
177
+ const btn = (label, name, fn, attrs) => button({ label: RGL[name](), variant: 'ghost', onClick: fn, attrs: { title: label, 'aria-label': label, ...attrs } });
178
+ const off = (b, why) => { b.setAttribute('aria-disabled', 'true'); b.classList.add('c3rec__off'); b.title = why; b.setAttribute('aria-label', why); return b; };
179
+ const chipText = (st) => (st.mode === 'recording' ? (st.started ? clockOf(Date.now() - st.started) : '') : st.until ? 'paused · ' + Math.max(1, Math.ceil((st.until - Date.now()) / 60000)) + ' min left' : 'paused');
180
+
181
+ /** The arrow beside New: Untitled note, then today's meetings that have not started and have no note, the next one marked. */
182
+ async function openUpcoming(anchor) {
183
+ const { menu } = await import('./primitives.js');
184
+ const links = await import('./notes-links.js');
185
+ const evs = links.upcomingWithoutNote(await links.upcomingToday());
186
+ const fmt = (e, i) => {
187
+ const mins = Math.max(1, Math.round((Date.parse(e.start) - Date.now()) / 60000));
188
+ const inText = mins < 60 ? 'in ' + mins + ' min' : 'in ' + Math.floor(mins / 60) + ' h' + (mins % 60 ? ' ' + (mins % 60) + ' min' : '');
189
+ return { label: e.title, hint: e.time || '', detail: [i === 0 ? 'Next' : null, inText, e.with || null].filter(Boolean).join(' · '), attrs: { 'data-fr-rec-event': e.id || '' }, onSelect: () => dock.recordEvent(e) };
190
+ };
191
+ menu({ anchor, ariaLabel: 'Record a new meeting', items: [{ label: 'Untitled note', icon: 'plus', detail: 'not linked to a meeting', attrs: { 'data-fr-rec-untitled': '' }, onSelect: () => dock.recordNew() }, { sep: true },
192
+ ...(evs.length ? evs.map(fmt) : [{ label: 'No more meetings today', attrs: { 'data-fr-rec-none': '' }, onSelect: () => {} }])] });
193
+ }
194
+
195
+ let eng = { id: null, text: '' };
196
+ const sync = () => {
197
+ let st; try { st = dock.recordState(); } catch { st = { mode: 'idle' }; }
198
+ 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(); } }); }
199
+ const hl = st.mode === 'recording' && dock.highlightBusy ? dock.highlightBusy() : null;
200
+ const key = [st.mode, st.id, st.title, hl ? hl.state + hl.open : '', eng.text].join(':');
201
+ if (key !== rec.key) {
202
+ rec.key = key; rec.clock = null;
203
+ const recording = st.mode === 'recording';
204
+ const kids = [];
205
+ if (st.mode !== 'idle') {
206
+ const v = recView(st, reducedMotion());
207
+ rec.clock = el('span', { class: 'recclk', 'data-fr-rec-clock': '' }, chipText(st));
208
+ const chip = button({ variant: 'ghost', onClick: () => openNote(st.id),
209
+ attrs: { class: 'c3rec__chip', title: (recording ? 'Recording: ' : 'Paused: ') + v.title + '. Open the note in the main app', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + v.title + '. Open the note.', 'data-fr-rec': st.mode, 'data-shell-rec-open': '' },
210
+ label: [el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
211
+ ...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : [])] });
212
+ const group = [chip];
213
+ if (recording) {
214
+ 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': '' }),
215
+ btn('Pause (you can resume within 1 hour)', 'pause', () => dock.pauseNow(), { 'data-fr-rec-pause': '', 'data-shell-rec-pause': '' }));
216
+ } else group.push(btn('Resume ' + st.title, 'play', () => dock.resumeNow(), { 'data-fr-rec-resume': '', 'data-shell-rec-resume': '' }));
217
+ group.push(btn('Stop for good (cannot be resumed)', 'square', () => dock.stopNow(), { 'data-fr-rec-stop': '', 'data-shell-rec-stop': '' }));
218
+ kids.push(el('span', { class: 'recg c3rec__live' + (recording ? '' : ' paused'), role: 'group', 'aria-label': (recording ? 'Recording ' : 'Paused: ') + st.title }, group));
219
+ }
220
+ const why = 'Pause or stop the meeting being recorded to start a new one';
221
+ const mic = button({ variant: 'ghost', label: [RGL.mic(), el('span', { class: 'c3rec__lbl' }, 'Record')], onClick: (e) => { if (!recording) dock.recordNew(e.currentTarget); },
222
+ attrs: { title: 'Record an untitled note', 'aria-label': 'Record an untitled note', 'data-fr-rec-new': '' } });
223
+ const arrow = btn('Record for an upcoming meeting', 'down', (e) => { if (!recording) openUpcoming(e.currentTarget); }, { 'data-fr-rec-upcoming': '', 'aria-haspopup': 'menu' });
224
+ if (recording) { off(mic, why); off(arrow, why); }
225
+ kids.push(el('span', { class: 'recg idle', role: 'group', 'aria-label': 'New recording' }, mic, arrow));
226
+ const panel = recording && dock.highlightPanel ? dock.highlightPanel() : null;
227
+ if (panel) kids.push(el('div', { class: 'c3rec__pop' }, panel));
228
+ wrap.replaceChildren(...kids);
229
+ tabDot(st.mode !== 'idle');
230
+ }
231
+ if (rec.clock) rec.clock.textContent = chipText(st);
232
+ };
233
+ /* note-dock reads the notes (a meeting the server still holds open is found again) and re-draws on every state change */
234
+ try { dock.mountDock(); } catch { /* the dock is not available on this page */ }
235
+ let unsub = null;
236
+ import('./state.js').then((m) => { unsub = m.subscribe(sync); }).catch(() => {});
237
+ rec.timer = setInterval(sync, 1000);
238
+ rec.off = () => { if (unsub) unsub(); };
239
+ sync();
240
+ live.set(host, rec);
241
+ }
@@ -63,7 +63,7 @@
63
63
  --brand-mark:oklch(0.623 0.214 259.815);
64
64
  --brand:var(--foreground);--brand-wash:color-mix(in oklab, var(--foreground) 8%, transparent);
65
65
  --success:oklch(0.76 0.17 152);--warning:oklch(0.82 0.15 82);--info:oklch(0.79 0.11 195);
66
- --purple:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
66
+ --purple:oklch(0.72 0.15 300);--focus-window:oklch(0.72 0.15 300);--destructive:oklch(0.704 0.191 22.216);
67
67
  --term-bg:oklch(0.148 0.004 228.8);
68
68
  --mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
69
69
  --sans:"Space Grotesk",ui-sans-serif,system-ui,sans-serif;
@@ -91,7 +91,7 @@
91
91
  --brand-mark:oklch(0.488 0.243 264.376);
92
92
  --brand:var(--foreground);--brand-wash:color-mix(in oklab, var(--foreground) 8%, transparent);
93
93
  --success:oklch(0.46 0.13 152);--warning:oklch(0.48 0.11 72);--info:oklch(0.47 0.09 195);
94
- --purple:oklch(0.5 0.2 300);--destructive:oklch(0.5 0.21 27.325);
94
+ --purple:oklch(0.5 0.2 300);--focus-window:oklch(0.5 0.2 300);--destructive:oklch(0.5 0.21 27.325);
95
95
  --term-bg:oklch(0.148 0.004 228.8);
96
96
  }
97
97
  @keyframes pmblink{0%,49%{opacity:1}50%,100%{opacity:0}}
@@ -47,6 +47,8 @@ import { mdRender } from './markdown.js';
47
47
  import { icon } from './icons.js';
48
48
  import { button, iconButton, field, row, sheet, empty, menu, toast, ensureComponentsCss } from './primitives.js';
49
49
  import { toInt16, makeResampler, openCapture } from './audio-capture.js';
50
+ import { startScreen } from './notes-screen-capture.js';
51
+ import { screenStrip } from './notes-screen-view.js';
50
52
  import { agendaHeard, peopleOf as contextPeople } from './notes-context.js';
51
53
 
52
54
  const SAVE_MS = 700; // a pause this long in typing saves the note
@@ -72,6 +74,8 @@ let banner = null; // { tone, text }
72
74
  let speech = null; // the browser recogniser: { mode: 'dictate'|'record', id, state, rec, interim, lang, msg, last }
73
75
  let audio = null; // local Whisper capture: { id, state, sources, ctx, streams, started, clock, msg }
74
76
  let engine = null; // { ws, whisper, jev, at }
77
+ /** Capture the shared screen (notes-screen-capture): on unless the PM switched it off on this machine. */
78
+ const screenOn = () => !(engine && engine.whisper && engine.whisper.screen === false);
75
79
  let showList = false;
76
80
  let inlineDraw = false; // the live block is drawing: the fields it shows are left in place (renderLive)
77
81
  /** A node this view keeps across redraws (an editor field, the player). The pop-up patches itself (dom.js patchChildren), so
@@ -257,6 +261,9 @@ async function loadNote(ctx, id) {
257
261
  if (editor && editor.id === id && dirty) { r.note.title = editor.title.value; r.note.body = editor.body.value; }
258
262
  r.note.__audio = r.audio || null;
259
263
  r.note.__recording = r.recording || null;
264
+ r.note.__shots = r.screen ? r.screen.frames || [] : [];
265
+ // a new line that points at the screen, or a decision or next step, keeps the picture on screen when it was said
266
+ if (audio && audio.id === id && audio.screen) audio.screen.hear(r.note.transcript || []);
260
267
  r.note.bullets_state = r.bullets_state || null;
261
268
  cache.set(id, r.note);
262
269
  if (!had || sig(had) !== sig(r.note)) ctx.setState({});
@@ -365,7 +372,7 @@ async function setPrivate(ctx, n, priv) {
365
372
  // a private press drops what is still buffered: nothing more is sent, transcribed or summarized
366
373
  if (priv) await stopAll(ctx, { discard: true });
367
374
  const r = await pmPost('/api/notes/private', { id: n.id, private: priv });
368
- if (r && r.ok) { n.private = r.private; if (priv) { n.__recording = null; dropPlayer(); } touchRow(n); banner = priv ? { tone: 'ok', text: 'Marked private. Listening stopped, its audio was deleted, and nothing from this note goes to Jev or to any agent.' } : null; }
375
+ if (r && r.ok) { n.private = r.private; if (priv) { n.__recording = null; n.__shots = []; dropPlayer(); } touchRow(n); banner = priv ? { tone: 'ok', text: 'Marked private. Listening stopped, its audio was deleted, and nothing from this note goes to Jev or to any agent.' } : null; }
369
376
  else banner = { tone: 'bad', text: (r && r.error) || 'not changed' };
370
377
  ctx.setState({});
371
378
  }
@@ -595,6 +602,14 @@ function paintNow() {
595
602
  const k = noteLineAt(editor && editor.id === n.id ? editor.body.value : n.body, n.line_times, ms);
596
603
  if (k >= 0) { const nl = document.querySelector('[data-nt-noteline="' + k + '"]'); if (nl) nl.classList.add('nt-now'); }
597
604
  }
605
+ async function deleteShots(ctx, n) {
606
+ const sure = await askConfirm({ title: 'Delete this note\'s screenshots?', body: 'The screenshots of "' + (n.title || 'Untitled note') + '" are deleted from this machine. The notes, the transcript and the audio stay. This cannot be undone.', confirmLabel: 'Delete screenshots', danger: true });
607
+ if (!sure) return;
608
+ const r = await pmPost('/api/notes/screen-delete', { id: n.id });
609
+ if (r && r.ok) { n.__shots = []; banner = { tone: 'ok', text: 'The screenshots were deleted. The notes, the transcript and the audio are kept.' }; }
610
+ else banner = { tone: 'bad', text: (r && r.error) || 'the screenshots were not deleted' };
611
+ ctx.setState({});
612
+ }
598
613
  async function deleteAudio(ctx, n) {
599
614
  const sure = await askConfirm({ title: 'Delete this note\'s audio?', body: 'The recording of "' + (n.title || 'Untitled note') + '" is deleted from this machine. The notes and the transcript stay. This cannot be undone.', confirmLabel: 'Delete audio', danger: true });
600
615
  if (!sure) return;
@@ -621,16 +636,18 @@ async function cleanNow(ctx, n) {
621
636
  }
622
637
 
623
638
  /* ── listening with local Whisper ─────────────────────────────────────────────────────────────── */
624
- async function openSource(kind) {
639
+ async function openSource(kind, { screen = false } = {}) {
625
640
  const md = navigator.mediaDevices;
626
641
  if (kind === 'mic') return md.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, channelCount: 1 } });
627
642
  // The meeting is often in Zoom or another app, not in this tab: offer the whole screen first (its system audio carries every app),
628
643
  // keep every window and tab selectable, and leave PMOS's own tab out, because it has no sound. Chrome ignores a hint it does not know.
629
644
  const s = await md.getDisplayMedia({ video: { displaySurface: 'monitor' }, audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false }, systemAudio: 'include', monitorTypeSurfaces: 'include', surfaceSwitching: 'include', selfBrowserSurface: 'exclude' });
630
645
  const tracks = s.getAudioTracks();
631
- for (const t of s.getVideoTracks()) t.stop(); // only the sound is used
632
- if (!tracks.length) throw Object.assign(new Error('The shared screen, window or tab has no audio. Share it again, and tick "Also share system audio" (entire screen) or "Share tab audio" (a tab).'), { noAudio: true });
633
- return new MediaStream(tracks);
646
+ // the picture of the same share is kept for screenshots when Capture the shared screen is on (notes-screen-capture); else only the sound is used
647
+ const video = screen ? s.getVideoTracks()[0] || null : null;
648
+ for (const t of s.getVideoTracks()) if (t !== video) t.stop();
649
+ if (!tracks.length) { if (video) video.stop(); throw Object.assign(new Error('The shared screen, window or tab has no audio. Share it again, and tick "Also share system audio" (entire screen) or "Share tab audio" (a tab).'), { noAudio: true }); }
650
+ return Object.assign(new MediaStream(tracks), { screenTrack: video });
634
651
  }
635
652
  /** The meeting source reaches PMOS as exact silence while the microphone is heard: the other people are not being recorded. Most often the
636
653
  * sharing window had the wrong tab (PMOS's own tab has no sound). Say so once, and take the message back if sound arrives. */
@@ -647,21 +664,23 @@ function checkSilence(ctx, a) {
647
664
  ctx.setState({});
648
665
  }
649
666
  async function startAudio(ctx, n, want) {
650
- const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0 };
667
+ const a = { id: n.id, state: 'starting', sources: [], streams: [], ctx: null, started: Date.now(), clock: null, msg: null, queue: {}, buf: {}, rs: {}, rs24: {}, buf24: {}, peak: {}, began: Date.now(), lost: 0, ended: 0, cap: null, seq: {}, off: {} };
651
668
  const opened = [];
652
669
  // the meeting first, straight from the press: the browser lets a page share a screen only right after a click
653
670
  for (const kind of want.slice().sort((x, y) => (x === 'meeting' ? -1 : y === 'meeting' ? 1 : 0))) {
654
- try { opened.push([kind, await openSource(kind)]); }
671
+ try { opened.push([kind, await openSource(kind, { screen: kind === 'meeting' && screenOn() })]); }
655
672
  catch (e) { a.msg = (a.msg ? a.msg + ' ' : '') + (kind === 'mic' ? 'The microphone was not opened' : 'The meeting audio was not opened') + ': ' + ((e && e.noAudio) ? e.message : ((e && e.message) || 'permission was refused') + '.'); }
656
673
  }
657
674
  await stopAll(ctx); await flushSave(ctx);
658
675
  audio = a; if (banner && banner.audioMsg) banner = null; ctx.setState({});
659
676
  if (!opened.length) { audio = null; banner = { tone: 'bad', text: a.msg || 'Nothing to listen to.' }; ctx.setState({}); return; }
660
677
  const r = await pmPost('/api/notes/record', wsOf(ctx) ? { id: n.id, ws: wsOf(ctx) } : { id: n.id });
661
- if (!r || !r.ok) { for (const [, s] of opened) for (const t of s.getTracks()) t.stop(); audio = null; banner = { tone: 'bad', text: (r && r.error) || 'listening did not start' }; ctx.setState({}); return; }
678
+ if (!r || !r.ok) { for (const [, s] of opened) { for (const t of s.getTracks()) t.stop(); if (s.screenTrack) s.screenTrack.stop(); } audio = null; banner = { tone: 'bad', text: (r && r.error) || 'listening did not start' }; ctx.setState({}); return; }
662
679
  n.recorded_from = r.recorded_from;
663
680
  // the clock counts the meeting time, as the line times do: Listen again carries on from where it stopped
664
681
  a.started = Date.parse(r.clock_from || r.recorded_from) || a.started;
682
+ // each chunk names its capture, its sequence number and its sample offset, so the server writes it once and in its place
683
+ a.cap = r.capture && r.capture.token ? r.capture.token : null;
665
684
  let ac;
666
685
  // the device's own rate: the 16 kHz audio for transcription and the 24 kHz audio for the replay are both made from the same blocks (D92 as amended)
667
686
  try { ac = new AudioContext(); } catch { ac = new AudioContext({ sampleRate: RATE }); }
@@ -685,6 +704,14 @@ async function startAudio(ctx, n, want) {
685
704
  });
686
705
  for (const t of stream.getTracks()) t.addEventListener('ended', () => { if (audio !== a) return; a.ended++; if (a.ended >= a.streams.length) stopAudio(ctx); });
687
706
  }
707
+ // screenshots of the shared screen, on the meeting clock, while this capture lasts (notes-screen-capture)
708
+ const shared = opened.find(([kind, s]) => kind === 'meeting' && s.screenTrack);
709
+ if (shared && a.cap) {
710
+ const track = shared[1].screenTrack;
711
+ a.screen = startScreen({ track, id: a.id, cap: a.cap, clock: () => Date.now() - a.started, post: (b) => pmPost('/api/notes/screen', b) });
712
+ // the PM stops sharing the picture only: the sound goes on, and the shots stop
713
+ track.addEventListener('ended', () => { if (a.screen) a.screen.stop(); });
714
+ } else if (shared) shared[1].screenTrack.stop();
688
715
  a.state = 'on';
689
716
  a.clock = setInterval(() => { tickClocks(a.started); checkSilence(ctx, a); }, 1000);
690
717
  ctx.setState({});
@@ -694,13 +721,16 @@ async function startAudio(ctx, n, want) {
694
721
  function sendChunk(ctx, a, kind, final) {
695
722
  const b = a.buf[kind] || []; a.buf[kind] = [];
696
723
  const pcm = b64(Int16Array.from(b));
724
+ const seq = a.seq[kind] = a.seq[kind] === undefined ? 0 : a.seq[kind] + 1;
725
+ const off = a.off[kind] || 0; a.off[kind] = off + b.length;
726
+ const where = a.cap ? { cap: a.cap, seq, off } : {};
697
727
  const b24 = a.buf24[kind] || []; a.buf24[kind] = [];
698
728
  const pcm24 = b24.length ? b64(Int16Array.from(b24)) : undefined;
699
729
  const seconds = b.length / RATE;
700
730
  a.queue[kind] = a.queue[kind].then(async () => {
701
731
  let r = null;
702
732
  for (let tries = 0; tries < SEND_TRIES; tries++) {
703
- r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final });
733
+ r = await pmPost('/api/notes/audio', { id: a.id, source: kind, pcm, pcm24, final, ...where });
704
734
  if (r && r.ok) return;
705
735
  if (r && (r.status === 409 || r.status === 404)) break; // the server refused this note's audio: sending again changes nothing
706
736
  if (tries + 1 < SEND_TRIES) await new Promise((res) => setTimeout(res, SEND_BACKOFF_MS * 3 ** tries));
@@ -722,6 +752,7 @@ async function stopAudio(ctx, { discard = false, resumable = true } = {}) {
722
752
  audio = null;
723
753
  if (banner && banner.silent) banner = null;
724
754
  clearInterval(a.clock);
755
+ if (a.screen) a.screen.stop();
725
756
  for (const s of a.streams) for (const t of s.getTracks()) { try { t.stop(); } catch { /* stopped */ } }
726
757
  try { if (a.ctx) await a.ctx.close(); } catch { /* closed */ }
727
758
  if (discard) { ctx.setState({}); return; }
@@ -850,6 +881,7 @@ export function highlightNow(ctx, id) {
850
881
  if (at === null) return false;
851
882
  if (highlight && highlight.state === 'listening') { hlOpen = true; ctx.setState({}); return true; }
852
883
  highlight = { id: n.id, title: n.title || '', at, from: Math.max(0, at - HL_BEFORE_MS), state: 'listening', pressed: Date.now(), result: null, error: null };
884
+ if (audio && audio.id === n.id && audio.screen) audio.screen.cue(at, 'highlight'); // the picture on screen when the PM pressed it
853
885
  hlOpen = true;
854
886
  clearInterval(highlightTimer);
855
887
  highlightTimer = setInterval(() => hlTick(ctx), 500);
@@ -1003,7 +1035,7 @@ async function chooseSources(ctx, n) {
1003
1035
  }
1004
1036
  async function setSwitch(ctx, patch) {
1005
1037
  const ws = wsOf(ctx);
1006
- if (!ws && patch.language === undefined && patch.model === undefined && patch.keep_audio === undefined && patch.clean_audio === undefined) return;
1038
+ if (!ws && patch.language === undefined && patch.model === undefined && patch.keep_audio === undefined && patch.clean_audio === undefined && patch.screen === undefined) return;
1007
1039
  const r = await pmPost('/api/notes/switch', ws ? { ws, ...patch } : patch);
1008
1040
  if (r && r.ok) engine = { ws, whisper: r.whisper, jev: r.jev, at: Date.now() };
1009
1041
  ctx.setState({});
@@ -1043,7 +1075,13 @@ function openSettings(ctx, n) {
1043
1075
  w && w.keep_audio !== false ? el('p', { class: 'nt-hint', 'data-nt-clean-hint': w.ffmpeg ? 'ready' : 'missing' }, w.ffmpeg
1044
1076
  ? (w.clean_audio === true ? 'After Stop, the replay gets a cleaned copy: less rumble and hiss, and an even loudness. The transcript is not changed.'
1045
1077
  : 'Off. When on, the replay gets a cleaned copy after Stop: less rumble and hiss, and an even loudness.')
1046
- : 'This needs ffmpeg, which was not found on this machine. Install it with brew install ffmpeg, then open Settings again.') : null));
1078
+ : 'This needs ffmpeg, which was not found on this machine. Install it with brew install ffmpeg, then open Settings again.') : null,
1079
+ w ? field({ label: 'Capture the shared screen', kind: 'check', value: w.screen !== false,
1080
+ onCommit: (v) => setSwitch(ctx, { screen: v }).then(() => { h.close(); openSettings(ctx, n); }), attrs: { 'data-nt-screen-switch': '' } }) : null,
1081
+ w ? el('p', { class: 'nt-hint', 'data-nt-screen-hint': w.ocr ? 'ocr' : 'no-ocr' }, w.screen !== false
1082
+ ? 'When you share the meeting\'s screen, a screenshot is kept each time the picture changes, on this machine, beside the transcript. Nothing is sent anywhere. '
1083
+ + (w.ocr ? 'The text on each screenshot is read on this machine with tesseract.' : 'To also read the text on each screenshot, install tesseract (brew install tesseract).')
1084
+ : 'Off. Only the sound of the shared screen is used.') : null));
1047
1085
  const fp = w && w.final;
1048
1086
  if (fp) {
1049
1087
  const have = [fp.transcriber === 'qwen' ? 'Qwen3-ASR' : fp.transcriber === 'parakeet' ? 'Parakeet' : fp.transcriber === 'whisper' ? 'Whisper with beam search' : null,
@@ -1076,6 +1114,7 @@ function openSettings(ctx, n) {
1076
1114
  button({ label: busy === 'agent' ? 'Starting …' : 'Summary by agent', disabled: n.private || busy === 'agent', onClick: () => { h.close(); summarizeAgent(ctx, n); }, attrs: { 'data-nt-summarize-agent': '' } }),
1077
1115
  n.__recording || (n.final_state && n.final_state.state === 'failed') ? button({ label: 'Transcribe again', disabled: n.private || finalPending(n), onClick: () => { h.close(); runFinal(ctx, n); }, attrs: { 'data-nt-final-again': '' } }) : null,
1078
1116
  n.__recording && w && w.clean_audio === true && w.ffmpeg && !n.__recording.cleaned ? button({ label: 'Clean up this note\'s sound', disabled: n.private || listening(n), onClick: () => { h.close(); cleanNow(ctx, n); }, attrs: { 'data-nt-clean-now': '' } }) : null,
1117
+ (n.__shots || []).length ? button({ label: 'Delete this note\'s screenshots', variant: 'ghost', onClick: () => { h.close(); deleteShots(ctx, n); }, attrs: { 'data-nt-delete-shots': '' } }) : null,
1079
1118
  n.__recording ? button({ label: 'Delete this note\'s audio', variant: 'ghost', onClick: () => { h.close(); deleteAudio(ctx, n); }, attrs: { 'data-nt-delete-audio': '' } }) : null,
1080
1119
  button({ label: 'Delete', variant: 'ghost', onClick: () => { h.close(); deleteNote(ctx, n); }, attrs: { 'data-nt-delete': '' } }))));
1081
1120
  return parts;
@@ -1592,6 +1631,7 @@ function noteColumn(ctx, n, ed) {
1592
1631
  const bs = n.bullets_state;
1593
1632
  const parts = [];
1594
1633
  if (n.__audio && n.__audio.error && live) parts.push(el('p', { class: 'nt-hint nt-hint--bad', 'data-nt-audio-error': '' }, n.__audio.error));
1634
+ if (live && audio && audio.id === n.id && audio.screen) parts.push(el('p', { class: 'nt-hint', 'data-nt-screen-live': '' }, 'The shared screen is captured each time it changes, on this machine.'));
1595
1635
  if (!live && n.final_notes) {
1596
1636
  // the notes written after Stop replace the live draft (Run 9)
1597
1637
  parts.push(...finalNotesSections(ctx, n), myNotes(ctx, n, ed), wisprSection(ctx, n));
@@ -1698,6 +1738,8 @@ export function renderBoard(inner, ctx) {
1698
1738
  const pl = !listening(n) ? playerFor(ctx, n) : null;
1699
1739
  if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el,
1700
1740
  el('span', { class: 'nt-hint' }, 'Kept on this machine. Press a time in the transcript to play from there.')));
1741
+ // the screenshots of the shared screen, on the replay's clock (notes-screen-capture)
1742
+ if (!listening(n) && !n.private && (n.__shots || []).length) wrap.appendChild(screenStrip({ id: n.id, frames: n.__shots, seek: pl ? (ms) => seek(ctx, n, ms) : null, replay: pl ? pl.el : null }));
1701
1743
  wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
1702
1744
  if (pl) queueMicrotask(paintNow);
1703
1745
  if (((n.agent && n.agent.started && !n.agent.markdown) || finalPending(n)) && !agentTimer) pollAgent(ctx, n.id);