create-pmos 0.134.0 → 0.136.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 (211) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  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/payload/state/log.md +1 -1
  7. package/workspace/bin/notes-replay.js +235 -0
  8. package/workspace/c3-briefing-test.js +130 -0
  9. package/workspace/c3-foundation-test.js +159 -0
  10. package/workspace/c3-ib-items-test.js +105 -0
  11. package/workspace/c3-ib-more-test.js +120 -0
  12. package/workspace/c3-ib-triage-backlog-test.js +119 -0
  13. package/workspace/c3-ini-harness.js +66 -0
  14. package/workspace/c3-initiatives-test.js +140 -0
  15. package/workspace/c3-one-home-test.js +14 -1
  16. package/workspace/c3-reports-test.js +57 -18
  17. package/workspace/c3-settings-test.js +139 -0
  18. package/workspace/c3-work-test.js +42 -10
  19. package/workspace/fixtures/one-home-baseline.json +1 -9
  20. package/workspace/guide/briefing-cards.md +15 -3
  21. package/workspace/guide/briefing.md +12 -8
  22. package/workspace/guide/calendar-popup.md +2 -2
  23. package/workspace/guide/console-calendar.md +39 -10
  24. package/workspace/guide/index.md +1 -0
  25. package/workspace/guide/initiative-boards.md +65 -0
  26. package/workspace/guide/jira.md +3 -1
  27. package/workspace/guide/layout.md +2 -0
  28. package/workspace/guide/my-work.md +5 -3
  29. package/workspace/guide/notes.md +16 -1
  30. package/workspace/guide/reports-dashboards.md +29 -5
  31. package/workspace/guide/settings.md +68 -0
  32. package/workspace/guide/welcome.md +10 -10
  33. package/workspace/lib/calendar-cli.js +3 -2
  34. package/workspace/lib/calendar-layers.js +21 -10
  35. package/workspace/lib/calendar-tidy.js +230 -0
  36. package/workspace/lib/engine/boards.js +542 -0
  37. package/workspace/lib/engine/breakdown-ops.js +180 -0
  38. package/workspace/lib/engine/calendar-import.js +84 -0
  39. package/workspace/lib/engine/calendar-model.js +185 -0
  40. package/workspace/lib/engine/card-state.js +92 -0
  41. package/workspace/lib/engine/derived/derive.js +92 -0
  42. package/workspace/lib/engine/derived/plan.js +3 -0
  43. package/workspace/lib/engine/derived/trust.js +3 -0
  44. package/workspace/lib/engine/doc-health.js +142 -0
  45. package/workspace/lib/engine/doc-sheet.js +75 -0
  46. package/workspace/lib/engine/intake.js +78 -0
  47. package/workspace/lib/engine/outbox.js +86 -0
  48. package/workspace/lib/engine/plan-forecast.js +342 -0
  49. package/workspace/lib/engine/promises.js +29 -0
  50. package/workspace/lib/engine/records/index.js +114 -0
  51. package/workspace/lib/engine/releases.js +96 -0
  52. package/workspace/lib/engine/replies.js +99 -0
  53. package/workspace/lib/engine/reply-contract/reply-contract.js +372 -0
  54. package/workspace/lib/engine/report-families.js +175 -0
  55. package/workspace/lib/engine/roadmap-ops.js +150 -0
  56. package/workspace/lib/engine/sessions-list.js +27 -0
  57. package/workspace/lib/engine/signal-rules.js +95 -0
  58. package/workspace/lib/engine/signals/cards.js +57 -0
  59. package/workspace/lib/engine/signals/detectors.js +370 -0
  60. package/workspace/lib/engine/signals/facts-cli.js +8 -0
  61. package/workspace/lib/engine/signals/facts.js +197 -0
  62. package/workspace/lib/engine/signals/index.js +33 -0
  63. package/workspace/lib/engine/signals/kinds.js +110 -0
  64. package/workspace/lib/engine/signals/layout-paths.js +44 -0
  65. package/workspace/lib/engine/signals/options.js +24 -0
  66. package/workspace/lib/engine/signals/score.js +72 -0
  67. package/workspace/lib/engine/signals/select.js +47 -0
  68. package/workspace/lib/engine/step-planner.js +65 -0
  69. package/workspace/lib/engine/trust/claim-rules.json +93 -0
  70. package/workspace/lib/engine/trust/import-calendar.js +21 -0
  71. package/workspace/lib/engine/trust/import-guard.js +36 -0
  72. package/workspace/lib/engine/trust/ledger.js +53 -0
  73. package/workspace/lib/engine/trust/outbox.js +45 -0
  74. package/workspace/lib/engine/trust/plan.js +5 -0
  75. package/workspace/lib/engine/trust/trust.js +21 -0
  76. package/workspace/lib/engine/truth-table.js +57 -0
  77. package/workspace/lib/engine/workspace-lib.js +24 -0
  78. package/workspace/lib/factory-join.js +1 -1
  79. package/workspace/lib/facts-async.js +4 -4
  80. package/workspace/lib/headless.js +95 -0
  81. package/workspace/lib/import-overlay.js +2 -1
  82. package/workspace/lib/jira-status.js +30 -3
  83. package/workspace/lib/notes-capture.js +9 -1
  84. package/workspace/lib/notes-final.js +38 -13
  85. package/workspace/lib/notes-replay.js +125 -0
  86. package/workspace/lib/notes-screen.js +180 -0
  87. package/workspace/lib/notes-whisper.js +10 -3
  88. package/workspace/lib/outbox-store.js +10 -6
  89. package/workspace/lib/pmos-verbs.js +3 -1
  90. package/workspace/lib/proactive-cli.js +7 -0
  91. package/workspace/lib/proactive.js +15 -2
  92. package/workspace/lib/records-dir.js +29 -0
  93. package/workspace/lib/repo-script.js +36 -0
  94. package/workspace/lib/report-dashboards.js +5 -2
  95. package/workspace/lib/report-frame.js +122 -0
  96. package/workspace/lib/report-schedules.js +113 -0
  97. package/workspace/lib/route-modules.js +9 -2
  98. package/workspace/lib/routes-boards.js +734 -0
  99. package/workspace/lib/routes-brief-page.js +13 -4
  100. package/workspace/lib/routes-briefing.js +22 -11
  101. package/workspace/lib/routes-c3-init.js +9 -8
  102. package/workspace/lib/routes-c3-life.js +2 -1
  103. package/workspace/lib/routes-c3-reports.js +99 -15
  104. package/workspace/lib/routes-c3-strategy.js +7 -5
  105. package/workspace/lib/routes-c3-timeline.js +3 -2
  106. package/workspace/lib/routes-calendar-import.js +3 -2
  107. package/workspace/lib/routes-calendar-views.js +9 -4
  108. package/workspace/lib/routes-doc-health.js +2 -1
  109. package/workspace/lib/routes-doc-sheet.js +2 -1
  110. package/workspace/lib/routes-forecast.js +2 -1
  111. package/workspace/lib/routes-hooks.js +3 -2
  112. package/workspace/lib/routes-initiatives.js +2 -1
  113. package/workspace/lib/routes-intake.js +2 -1
  114. package/workspace/lib/routes-notes.js +55 -9
  115. package/workspace/lib/routes-outbox.js +23 -3
  116. package/workspace/lib/routes-pm-items.js +19 -6
  117. package/workspace/lib/routes-records.js +10 -7
  118. package/workspace/lib/routes-releases.js +2 -1
  119. package/workspace/lib/routes-report-facts.js +4 -3
  120. package/workspace/lib/routes-report-families.js +2 -1
  121. package/workspace/lib/routes-roadmap-ops.js +17 -14
  122. package/workspace/lib/routes-runs.js +6 -5
  123. package/workspace/lib/routes-settings.js +51 -0
  124. package/workspace/lib/routes-signals.js +3 -2
  125. package/workspace/lib/routes-stage-tools.js +220 -0
  126. package/workspace/lib/run-answers.js +2 -1
  127. package/workspace/lib/settings-store.js +226 -0
  128. package/workspace/lib/stage-tools.js +208 -0
  129. package/workspace/lib/vendor/scripts/signals/layout-paths.js +2 -1
  130. package/workspace/lib/vendor/scripts/workspace-lib.js +24 -0
  131. package/workspace/lib/work-evidence.js +73 -0
  132. package/workspace/package-lock.json +2 -2
  133. package/workspace/package.json +1 -1
  134. package/workspace/public/pm/briefing-deck.js +63 -17
  135. package/workspace/public/pm/briefing-model.js +36 -7
  136. package/workspace/public/pm/c3-ambient.js +100 -0
  137. package/workspace/public/pm/c3-brief-needs.js +26 -6
  138. package/workspace/public/pm/c3-brief-pulse.js +12 -4
  139. package/workspace/public/pm/c3-brief-today.js +56 -21
  140. package/workspace/public/pm/c3-brief.js +2 -1
  141. package/workspace/public/pm/c3-calendar.js +161 -42
  142. package/workspace/public/pm/c3-calpop.js +5 -5
  143. package/workspace/public/pm/c3-demo-boards.js +649 -0
  144. package/workspace/public/pm/c3-demo-brief-page.js +1 -1
  145. package/workspace/public/pm/c3-demo-cal4.js +3 -2
  146. package/workspace/public/pm/c3-demo-cal5.js +52 -0
  147. package/workspace/public/pm/c3-demo-data.js +17 -27
  148. package/workspace/public/pm/c3-demo-rep3.js +46 -5
  149. package/workspace/public/pm/c3-demo.js +2 -1
  150. package/workspace/public/pm/c3-dock.js +1 -1
  151. package/workspace/public/pm/c3-ib-docs.js +80 -0
  152. package/workspace/public/pm/c3-ib-initiatives.js +134 -0
  153. package/workspace/public/pm/c3-ib-items.js +201 -0
  154. package/workspace/public/pm/c3-ib-releases.js +51 -0
  155. package/workspace/public/pm/c3-ib-sprints.js +55 -0
  156. package/workspace/public/pm/c3-ib-triage.js +178 -0
  157. package/workspace/public/pm/c3-ii-tabs.js +240 -0
  158. package/workspace/public/pm/c3-ini-records.js +197 -0
  159. package/workspace/public/pm/c3-ini-timeline.js +278 -0
  160. package/workspace/public/pm/c3-ini-ui.js +229 -0
  161. package/workspace/public/pm/c3-ini-work.js +139 -0
  162. package/workspace/public/pm/c3-initiatives.css +218 -0
  163. package/workspace/public/pm/c3-initiatives.js +741 -0
  164. package/workspace/public/pm/c3-plan.js +2 -2
  165. package/workspace/public/pm/c3-reports.css +47 -47
  166. package/workspace/public/pm/c3-reports.js +223 -185
  167. package/workspace/public/pm/c3-settings.css +74 -0
  168. package/workspace/public/pm/c3-settings.js +703 -0
  169. package/workspace/public/pm/c3-shell.js +102 -36
  170. package/workspace/public/pm/c3-terminal-icons.js +2 -2
  171. package/workspace/public/pm/c3-terminal.js +2 -2
  172. package/workspace/public/pm/calendar-views.js +1 -1
  173. package/workspace/public/pm/components.css +319 -213
  174. package/workspace/public/pm/console3.css +99 -23
  175. package/workspace/public/pm/dest-notes.js +26 -4
  176. package/workspace/public/pm/notes-screen-capture.js +165 -0
  177. package/workspace/public/pm/notes-screen-view.js +98 -0
  178. package/workspace/public/pm/panel-trees.js +5 -2
  179. package/workspace/public/pm/pm-work-view.js +233 -146
  180. package/workspace/public/pm/shell-bell.js +3 -2
  181. package/workspace/public/pm/shell-recording.js +29 -8
  182. package/workspace/public/pm/tokens.css +2 -2
  183. package/workspace/public/pm/view-notes.js +46 -9
  184. package/workspace/public/pm/welcome.js +197 -89
  185. package/workspace/public/welcome.html +6 -4
  186. package/workspace/server.js +105 -6
  187. package/workspace/skills/day-check.skill +1 -1
  188. package/workspace/skills/jira-sync.skill +6 -4
  189. package/workspace/skills/morning-brief.skill +1 -1
  190. package/workspace/test-files.json +12 -3
  191. package/workspace/test-lists/c3-ib-items.json +9 -0
  192. package/workspace/test-lists/c3-ib-more.json +9 -0
  193. package/workspace/test-lists/c3-ib-triage-backlog.json +9 -0
  194. package/workspace/test-lists/c3-initiative-boards.json +6 -0
  195. package/workspace/test-lists/calendar-tidy-up.json +6 -0
  196. package/workspace/test-lists/console3-briefing.json +9 -0
  197. package/workspace/test-lists/console3-foundation.json +9 -0
  198. package/workspace/test-lists/console3-reports.json +3 -3
  199. package/workspace/test-lists/meeting-notes.json +10 -4
  200. package/workspace/test-lists/workspace-headless-jobs.json +5 -0
  201. package/workspace/test-lists/workspace-package-script-closure.json +6 -0
  202. package/workspace/lib/vendor/workspace/lib/actor.js +0 -91
  203. package/workspace/lib/vendor/workspace/lib/board-cache.js +0 -156
  204. package/workspace/lib/vendor/workspace/lib/console-state.js +0 -295
  205. package/workspace/lib/vendor/workspace/lib/factory-join.js +0 -158
  206. package/workspace/lib/vendor/workspace/lib/git-info.js +0 -306
  207. package/workspace/lib/vendor/workspace/lib/plan-items.js +0 -922
  208. package/workspace/lib/vendor/workspace/lib/plan-tree.js +0 -419
  209. package/workspace/lib/vendor/workspace/lib/review-gate.js +0 -39
  210. package/workspace/lib/vendor/workspace/lib/sink.js +0 -321
  211. 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'); }
@@ -8,8 +8,8 @@
8
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
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
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, and it shows only while
12
- * 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.
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.
13
13
  */
14
14
  import { el } from './dom.js';
15
15
  import { button } from './primitives.js';
@@ -50,6 +50,25 @@ export const simDock = {
50
50
  };
51
51
  const live = new WeakMap();
52
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
+
53
72
  export function mountRecording(host, opts) {
54
73
  if (!host) return;
55
74
  disposeRecording(host);
@@ -58,8 +77,9 @@ export function mountRecording(host, opts) {
58
77
  const d = (opts && opts.dock) || simDock;
59
78
  const nowMs = () => (d.now ? d.now() : Date.now());
60
79
  const grp = el('span', { class: 'recg', role: 'group', 'aria-label': 'Recording', hidden: true, 'data-shell': 'recording' });
61
- host.insertBefore(grp, host.firstChild);
62
- 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: [] };
63
83
  const tabDot = (on) => {
64
84
  for (const t of document.querySelectorAll(TAB)) {
65
85
  let dot = t.querySelector('.shell-recdot');
@@ -78,15 +98,16 @@ export function mountRecording(host, opts) {
78
98
  const key = [v.mode, st.id, v.title, v.dot].join(':');
79
99
  if (key !== rec.key) {
80
100
  rec.key = key;
81
- grp.hidden = !v.shown;
101
+ grp.hidden = false; // the mockup shows the idle Record control too
82
102
  grp.classList.toggle('paused', v.mode === 'paused');
83
- grp.setAttribute('aria-label', v.shown ? (v.mode === 'recording' ? 'Recording ' : 'Paused: ') + v.title : 'Recording');
84
- 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)); }
85
106
  else {
86
107
  rec.clock = el('span', { class: 'recclk', 'data-shell-rec-clock': '' });
87
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) },
88
109
  el('span', { class: 'recdot' + (v.dot === 'static' ? ' static' : ''), 'data-dot': v.dot }), rec.clock, el('span', { class: 'rt' }, v.title),
89
- ...(eng.text ? [el('span', { class: 'c3rec__eng', 'data-fr-rec-engine': '', title: 'The live transcript: engine and language (Notes settings)' }, eng.text)] : []));
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'));
90
111
  const kids = [open];
91
112
  if (v.canMark) kids.push(btn('Mark this moment', 'highlighter', () => d.highlightNow(), 'data-shell-rec-mark'));
92
113
  if (v.canPause) kids.push(btn('Pause', 'pause', () => d.pauseNow(), 'data-shell-rec-pause'));
@@ -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. */
@@ -651,14 +668,14 @@ async function startAudio(ctx, n, want) {
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;
@@ -687,6 +704,14 @@ async function startAudio(ctx, n, want) {
687
704
  });
688
705
  for (const t of stream.getTracks()) t.addEventListener('ended', () => { if (audio !== a) return; a.ended++; if (a.ended >= a.streams.length) stopAudio(ctx); });
689
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();
690
715
  a.state = 'on';
691
716
  a.clock = setInterval(() => { tickClocks(a.started); checkSilence(ctx, a); }, 1000);
692
717
  ctx.setState({});
@@ -727,6 +752,7 @@ async function stopAudio(ctx, { discard = false, resumable = true } = {}) {
727
752
  audio = null;
728
753
  if (banner && banner.silent) banner = null;
729
754
  clearInterval(a.clock);
755
+ if (a.screen) a.screen.stop();
730
756
  for (const s of a.streams) for (const t of s.getTracks()) { try { t.stop(); } catch { /* stopped */ } }
731
757
  try { if (a.ctx) await a.ctx.close(); } catch { /* closed */ }
732
758
  if (discard) { ctx.setState({}); return; }
@@ -855,6 +881,7 @@ export function highlightNow(ctx, id) {
855
881
  if (at === null) return false;
856
882
  if (highlight && highlight.state === 'listening') { hlOpen = true; ctx.setState({}); return true; }
857
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
858
885
  hlOpen = true;
859
886
  clearInterval(highlightTimer);
860
887
  highlightTimer = setInterval(() => hlTick(ctx), 500);
@@ -1008,7 +1035,7 @@ async function chooseSources(ctx, n) {
1008
1035
  }
1009
1036
  async function setSwitch(ctx, patch) {
1010
1037
  const ws = wsOf(ctx);
1011
- 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;
1012
1039
  const r = await pmPost('/api/notes/switch', ws ? { ws, ...patch } : patch);
1013
1040
  if (r && r.ok) engine = { ws, whisper: r.whisper, jev: r.jev, at: Date.now() };
1014
1041
  ctx.setState({});
@@ -1048,7 +1075,13 @@ function openSettings(ctx, n) {
1048
1075
  w && w.keep_audio !== false ? el('p', { class: 'nt-hint', 'data-nt-clean-hint': w.ffmpeg ? 'ready' : 'missing' }, w.ffmpeg
1049
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.'
1050
1077
  : 'Off. When on, the replay gets a cleaned copy after Stop: less rumble and hiss, and an even loudness.')
1051
- : '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));
1052
1085
  const fp = w && w.final;
1053
1086
  if (fp) {
1054
1087
  const have = [fp.transcriber === 'qwen' ? 'Qwen3-ASR' : fp.transcriber === 'parakeet' ? 'Parakeet' : fp.transcriber === 'whisper' ? 'Whisper with beam search' : null,
@@ -1081,6 +1114,7 @@ function openSettings(ctx, n) {
1081
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': '' } }),
1082
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,
1083
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,
1084
1118
  n.__recording ? button({ label: 'Delete this note\'s audio', variant: 'ghost', onClick: () => { h.close(); deleteAudio(ctx, n); }, attrs: { 'data-nt-delete-audio': '' } }) : null,
1085
1119
  button({ label: 'Delete', variant: 'ghost', onClick: () => { h.close(); deleteNote(ctx, n); }, attrs: { 'data-nt-delete': '' } }))));
1086
1120
  return parts;
@@ -1597,6 +1631,7 @@ function noteColumn(ctx, n, ed) {
1597
1631
  const bs = n.bullets_state;
1598
1632
  const parts = [];
1599
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.'));
1600
1635
  if (!live && n.final_notes) {
1601
1636
  // the notes written after Stop replace the live draft (Run 9)
1602
1637
  parts.push(...finalNotesSections(ctx, n), myNotes(ctx, n, ed), wisprSection(ctx, n));
@@ -1703,6 +1738,8 @@ export function renderBoard(inner, ctx) {
1703
1738
  const pl = !listening(n) ? playerFor(ctx, n) : null;
1704
1739
  if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el,
1705
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 }));
1706
1743
  wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
1707
1744
  if (pl) queueMicrotask(paintNow);
1708
1745
  if (((n.agent && n.agent.started && !n.agent.markdown) || finalPending(n)) && !agentTimer) pollAgent(ctx, n.id);