create-pmos 0.72.1 → 0.74.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.
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { el } from './dom.js';
28
- import { button, iconButton, badge, toast, row, tabs, field } from './primitives.js';
28
+ import { button, iconButton, badge, toast, row, tabs, field, menu, meter } from './primitives.js';
29
29
  import { pmGet, pmPost } from './http.js';
30
30
  import { FIXTURE } from './state.js';
31
31
  import { COPY } from './today-copy.js';
@@ -35,6 +35,7 @@ import { agendaPanel, meetingDetail, memoryBlock, refreshAgenda, agendaSig, onAg
35
35
  import { notesBlock, onNotes } from './notes-links.js';
36
36
 
37
37
  const D = COPY.day;
38
+ const A = COPY.act;
38
39
  const SLOW_MS = 30000;
39
40
  const FAST_MS = 4000;
40
41
  const MAX_REVISIONS = 3;
@@ -58,6 +59,8 @@ const ui = {
58
59
  open: (() => { const v = store.get(OPEN_KEY); return v === 'true' ? true : v === 'false' ? false : null; })(),
59
60
  sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
60
61
  revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
62
+ // today-act-prepare: the folded groups the PM opened, the item whose hold form is open, and its draft
63
+ groupsOpen: new Set(), holding: null, holdDraft: { who: '', when: 'h2' }, compose: new Map(), verdict: new Map(),
61
64
  };
62
65
  let rerender = () => {};
63
66
  let notesSub = null; // the redraw when the meeting notes list is read (pm/notes-links.js)
@@ -119,7 +122,8 @@ const isUrl = (s) => typeof s === 'string' && s.length <= 500 && /^https?:\/\/\S
119
122
  const srcName = (k) => D.srcName[k] || (k === 'derived' ? 'PMOS' : String(k || ''));
120
123
 
121
124
  function jobOf(item, task, worth) {
122
- const kind = (item && item.kind) || (task && task.kind) || (item && item.owner === 'pm' ? 'you' : 'approve');
125
+ // a job the PM gave to the agent (Have the agent draft it) has the task's kind: approve
126
+ const kind = (task && task.kind) || (item && item.kind) || (item && item.owner === 'pm' ? 'you' : 'approve');
123
127
  const status = task ? task.status : kind === 'you' ? 'you' : 'notStarted';
124
128
  const group = worth ? 'worth' : ['you', 'review', 'failed'].includes(status) ? 'needs' : ['done', 'sent'].includes(status) ? 'done' : 'working';
125
129
  return {
@@ -160,10 +164,20 @@ function model(d, view) {
160
164
  for (const x of (brief && brief.worth_asking) || []) add(x, true);
161
165
  for (const t of tasks) if (!used.has(t.id)) jobs.push(jobOf(null, t, false)); // an item a check took off: its task stays
162
166
  const now = d && d.now ? (typeof d.now === 'number' ? d.now : Date.parse(d.now)) : Date.now();
167
+ const marks = (d && d.marks) || {};
163
168
  const events = ((brief && brief.calendar) || []).map((e, i) => {
164
169
  const s = Date.parse(e.start);
165
170
  const f = e.end ? Date.parse(e.end) : s + 30 * 60000;
166
- return { ...e, id: e.id || 'ev-' + i, s, f, time: hhmm(e.start), ended: Number.isFinite(f) && f <= now };
171
+ const mark = marks[e.id] || null;
172
+ const checked = new Set((mark && mark.steps) || []);
173
+ const before = ((e.steps && e.steps.before) || []).map((st) => ({ ...st, done: !!st.done || checked.has(String(st.t || '')) }));
174
+ const ended = Number.isFinite(f) && f <= now;
175
+ // a meeting needs prep when the brief gave a prep line or Before steps (the explainer's contradiction: Before steps
176
+ // with no prep line read "Nothing to prepare" while the Agenda counted them as prep left)
177
+ const needsPrep = !ended && (!!e.prep || before.length > 0);
178
+ const st = mark && mark.state;
179
+ const group = !needsPrep ? null : st === 'done' || st === 'dismissed' ? 'done' : st === 'snoozed' ? 'later' : 'needs';
180
+ return { ...e, id: e.id || 'ev-' + i, s, f, time: hhmm(e.start), ended, mark, before, needsPrep, group };
167
181
  }).sort((a, b) => a.s - b.s);
168
182
  const settings = (d && d.settings) || null;
169
183
  const scopes = (settings && settings.scopes) || {};
@@ -176,6 +190,13 @@ function model(d, view) {
176
190
  const repos = (d && Array.isArray(d.repos)) ? d.repos : [];
177
191
  const repoName = (id) => { const r = repos.find((x) => x.id === id); return r ? r.name : id; };
178
192
  for (const j of jobs) j.repo = itemRepos[j.id] || null;
193
+ // the PM's marks (today-act-prepare, PRD M1 to M6): a job the PM owns leaves Needs you when it is done, snoozed, held
194
+ // or not the PM's; a snooze or hold whose time has passed comes back as open (the server reads it so)
195
+ for (const j of jobs) {
196
+ j.mark = marks[j.id] || null;
197
+ const st = j.mark && j.mark.state;
198
+ if (j.status === 'you' && st) j.group = { done: 'done', dismissed: 'done', snoozed: 'later', waiting: 'waiting' }[st] || j.group;
199
+ }
179
200
  for (const g of digests) g.repo = placeRepo(g.source, g.name);
180
201
  const v = view || {};
181
202
  const narrow = !v.all && !!v.here;
@@ -193,8 +214,9 @@ function model(d, view) {
193
214
  readFailed: (d && d.read_failed) || null,
194
215
  free: plan && plan.time ? plan.time.free_minutes : null,
195
216
  maxGoals: (d && d.max_goals) || 7,
196
- needs: shownJobs.filter((j) => j.group === 'needs').length,
197
- done: shownJobs.filter((j) => j.group === 'done').length,
217
+ preps: events.filter((e) => e.needsPrep),
218
+ needs: shownJobs.filter((j) => j.group === 'needs').length + events.filter((e) => e.group === 'needs').length,
219
+ done: shownJobs.filter((j) => j.group === 'done').length + events.filter((e) => e.group === 'done').length,
198
220
  };
199
221
  }
200
222
 
@@ -226,7 +248,7 @@ function statusPill(j) {
226
248
  const map = {
227
249
  notStarted: [s.notStarted, ''], waiting: [s.waiting, ''], working: [s.working, 'info'], sending: [s.sending, 'info'],
228
250
  review: [j.kind === 'auto' ? s.reviewAuto : s.review, 'purple'], sent: [s.sent, 'ok'], done: [s.done, 'ok'],
229
- failed: [s.failed, 'bad'], you: [s.you, 'warn'],
251
+ failed: [s.failed, 'bad'],
230
252
  };
231
253
  const [t, tone] = map[j.status] || [j.status, ''];
232
254
  return pill(t, tone, { 'data-day-status': j.status });
@@ -306,6 +328,48 @@ async function decide(ws, task, action, note) {
306
328
  load(ws);
307
329
  }
308
330
 
331
+ /* The PM's marks (today-act-prepare, PRD M1 to M6). Every press but a step check says what it did, with Undo: the
332
+ * answer carries the mark before the press, and Undo puts it back. */
333
+ async function markPress(ws, id, op, said) {
334
+ const key = 'mark:' + id;
335
+ if (ui.busy.has(key)) return;
336
+ ui.busy.add(key); rerender();
337
+ const r = await pmPost('/api/proactive/day/mark', { id, ...op });
338
+ ui.busy.delete(key);
339
+ if (r && r.ok) {
340
+ if (op.action !== 'step') ui.holding = null;
341
+ if (said) toast({ text: said, actionLabel: A.undo, onAction: () => undoMark(ws, id, r.prev) });
342
+ } else toast({ text: (op.action === 'step' ? A.stepFailed : D.refused)((r && r.error) || D.unreached), tone: 'destructive' });
343
+ rerender();
344
+ load(ws);
345
+ }
346
+ async function undoMark(ws, id, prev) {
347
+ const r = await pmPost('/api/proactive/day/mark', { id, action: 'restore', mark: prev === undefined ? null : prev });
348
+ toast(r && r.ok ? { text: A.undone } : { text: D.refused((r && r.error) || D.unreached), tone: 'destructive' });
349
+ load(ws);
350
+ }
351
+ /** "Have the agent draft it" (PRD C2): one briefed task for this job; the job then shows the draft to approve. */
352
+ async function sendPress(ws, j, body, to) {
353
+ const key = 'send:' + j.id;
354
+ if (ui.busy.has(key)) return;
355
+ ui.busy.add(key); rerender();
356
+ const r = await pmPost('/api/proactive/day/send', { item: j.id, ws, ...body });
357
+ ui.busy.delete(key);
358
+ if (r && r.ok) { ui.compose.delete(j.id); ui.verdict.delete(j.id); toast({ text: A.sendStarted(to) }); } else toast({ text: D.refused((r && r.error) || D.unreached), tone: 'destructive' });
359
+ rerender();
360
+ load(ws);
361
+ }
362
+ async function draftPress(ws, j) {
363
+ const key = 'draft:' + j.id;
364
+ if (ui.busy.has(key)) return;
365
+ ui.busy.add(key); rerender();
366
+ const r = await pmPost('/api/proactive/day/draft', { item: j.id, ws });
367
+ ui.busy.delete(key);
368
+ toast(r && r.ok ? { text: A.draftStarted } : { text: D.refused((r && r.error) || D.unreached), tone: 'destructive' });
369
+ rerender();
370
+ load(ws);
371
+ }
372
+
309
373
  async function setSettings(ws, patch) {
310
374
  const r = await pmPost('/api/proactive/settings', patch);
311
375
  if (!r || r.ok === false) { toast({ text: D.refused((r && r.error) || D.unreached), tone: 'destructive' }); return false; }
@@ -421,7 +485,11 @@ function jobRow(ctx, j) {
421
485
  const n = j.goal ? ctx.m.goalNo.get(j.goal) : null;
422
486
  const sub = j.status === 'failed' && j.task && j.task.error ? j.task.error
423
487
  : [j.item && j.item.where, srcName(j.source)].filter(Boolean).join(' · ');
424
- return rowBtn(ctx, 'job', j.id, { lead: n ? D.pill.goal(n) : '·', title: j.title, meta: sub, trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), statusPill(j)),
488
+ const mine = j.status === 'you';
489
+ const due = mine && j.item && j.item.due ? Date.parse(j.item.due) : NaN;
490
+ const meta = mine && j.mark && j.mark.state === 'waiting' ? `${A.hold.replace('…', '')} ${j.mark.on}` : sub;
491
+ return rowBtn(ctx, 'job', j.id, { lead: mine ? verbChip(verbOf(j.item)) : n ? D.pill.goal(n) : '·', title: j.title, meta,
492
+ trail: el('span', { class: 'td-day__chips' }, repoTag(ctx.m, j.repo), mine ? dueChip(ctx.m, j.mark, due, false) : statusPill(j)),
425
493
  dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null });
426
494
  }
427
495
 
@@ -438,18 +506,38 @@ function owedItems(owed) {
438
506
  });
439
507
  }
440
508
 
509
+ /** A meeting to prepare for, as a row of the jobs (PRD D4): it selects the meeting. */
510
+ function prepRow(ctx, e) {
511
+ const total = e.before.length; const d = e.before.filter((x) => x.done).length;
512
+ return rowBtn(ctx, 'event', e.id, { lead: verbChip('prepare'), title: e.title, meta: [total ? A.readiness(d, total) : null, e.with || null].filter(Boolean).join(' · '),
513
+ trail: dueChip(ctx.m, e.mark, e.s, true) }, { 'data-day-prep': e.id });
514
+ }
515
+
441
516
  function owedRow(ctx, o) {
442
- return rowBtn(ctx, 'owed', o.id, { lead: '·', title: o.title, meta: o.meta, trail: pill(D.status2.you, 'warn') });
517
+ return rowBtn(ctx, 'owed', o.id, { lead: '·', title: o.title, meta: o.meta, trail: pill(A.chip.today, 'warn', { 'data-day-status': 'you' }) });
443
518
  }
444
519
 
520
+ // Needs you holds only what the PM can do now (PRD D1): snoozed, held and done items fold into groups of their own.
521
+ const JOB_GROUPS = ['needs', 'later', 'waiting', 'working', 'done', 'worth'];
522
+ const FOLDED = new Set(['later', 'waiting', 'done']);
445
523
  function jobsPanel(ctx) {
446
524
  const { m } = ctx;
447
525
  const owed = ctx.owed;
448
- if (!m.jobs.length && !owed.length) return panel('jobs', null, note(m.run && m.stage === 'reading' ? D.readingJobs : D.noJobs, { 'data-day': 'no-jobs' }));
449
- const groups = ['needs', 'working', 'done', 'worth'].map((g) => [g, m.jobs.filter((j) => j.group === g)]).filter(([g, l]) => l.length || (g === 'needs' && owed.length));
450
- return panel('jobs', D.jobsCount(m.needs + owed.length, m.done), ...groups.map(([g, l]) => el('div', { class: 'td-day__group', 'data-day-group': g },
451
- el('div', { class: 'td-day__gh' }, D.groups[g], pill(String(l.length + (g === 'needs' ? owed.length : 0)), '')),
452
- el('ul', { class: 'td-day__rows', 'data-day-list': 'jobs-' + g }, ...l.map((j) => jobRow(ctx, j)), ...(g === 'needs' ? owed.map((o) => owedRow(ctx, o)) : [])))));
526
+ if (!m.jobs.length && !owed.length && !m.preps.length) return panel('jobs', null, note(m.run && m.stage === 'reading' ? D.readingJobs : D.noJobs, { 'data-day': 'no-jobs' }));
527
+ const groups = JOB_GROUPS.map((g) => [g, m.jobs.filter((j) => j.group === g), m.preps.filter((e) => e.group === g)])
528
+ .filter(([g, l, p]) => l.length || p.length || (g === 'needs' && owed.length));
529
+ return panel('jobs', D.jobsCount(m.needs + owed.length, m.done), ...groups.map(([g, l, p]) => {
530
+ const count = l.length + p.length + (g === 'needs' ? owed.length : 0);
531
+ const label = A.groups[g] && g !== 'done' ? A.groups[g] : g === 'done' ? A.groups.done : D.groups[g];
532
+ const foldable = FOLDED.has(g);
533
+ const shut = foldable && !ui.groupsOpen.has(g);
534
+ const head = foldable
535
+ ? button({ label: `${label} · ${count}`, variant: 'ghost', size: 'sm', iconEnd: shut ? 'chevron-right' : 'chevron-down', attrs: { class: 'td-day__fold-head', 'aria-expanded': shut ? 'false' : 'true', 'data-day-fold': g },
536
+ onClick: () => { if (shut) ui.groupsOpen.add(g); else ui.groupsOpen.delete(g); rerender(); } })
537
+ : el('div', { class: 'td-day__gh' }, label, pill(String(count), ''));
538
+ return el('div', { class: 'td-day__group', 'data-day-group': g }, head,
539
+ shut ? null : el('ul', { class: 'td-day__rows', 'data-day-list': 'jobs-' + g }, ...p.map((e) => prepRow(ctx, e)), ...l.map((j) => jobRow(ctx, j)), ...(g === 'needs' ? owed.map((o) => owedRow(ctx, o)) : [])));
540
+ }));
453
541
  }
454
542
 
455
543
  /* The coordinator's view of the run: what every agent job came back with, in one place. The server runs the jobs as one
@@ -489,7 +577,8 @@ function goalsPanel(ctx) {
489
577
 
490
578
  function mentionStatus(j) {
491
579
  if (j.group === 'done') return pill(D.status2.handled, 'ok');
492
- if (j.status === 'you') return j.item && j.item.reply_needed ? pill(D.status2.replyNeeded, 'warn') : pill(D.status2.you, 'warn');
580
+ if (j.status === 'you' && j.mark && j.mark.state) return dueChip({ now: Date.now() }, j.mark, NaN, false);
581
+ if (j.status === 'you') return j.item && j.item.reply_needed ? pill(D.status2.replyNeeded, 'warn', { 'data-day-status': 'you' }) : verbChip(verbOf(j.item));
493
582
  return statusPill(j);
494
583
  }
495
584
 
@@ -523,6 +612,243 @@ function mentionsPanel(ctx) {
523
612
  return panel('mentions', m.mentions.length ? D.mentionsCount(m.mentions.length, need) : null, mtabs, rows, dig);
524
613
  }
525
614
 
615
+ /* ── act and prepare (initiative today-act-prepare) ─────────────────────────────────────────────────────────────── */
616
+
617
+ const MIN_MS = 60000;
618
+ const clockAt = (ms, hh, mm) => { const d = new Date(ms); return new Date(d.getFullYear(), d.getMonth(), d.getDate(), hh, mm).getTime(); };
619
+ const sameDay = (a, b) => new Date(a).toDateString() === new Date(b).toDateString();
620
+ const verbOf = (it) => (it && A.verb[it.verb] ? it.verb : it && it.reply_needed ? 'reply' : 'do');
621
+ function verbChip(verb) {
622
+ return el('span', { class: 'td-day__verb', 'data-verb': verb, 'data-day': 'verb' }, A.verb[verb] || A.verb.do);
623
+ }
624
+ /** The chip at the end of a row the PM acts on: where its mark left it, else its due time, coloured by time left. */
625
+ function dueChip(m, mark, due, prep) {
626
+ const st = mark && mark.state;
627
+ const at = (v) => hhmm(v);
628
+ if (st === 'snoozed') return pill(A.back(at(mark.until)), '', { 'data-day-status': 'snoozed' });
629
+ if (st === 'waiting') return pill(A.check(at(mark.until)), '', { 'data-day-status': 'waiting' });
630
+ if (st === 'done') return pill(prep ? A.chip.ready : A.chip.done, 'ok', { 'data-day-status': 'marked-done' });
631
+ if (st === 'dismissed') return pill(prep ? A.chip.skipped : A.chip.dismissed, '', { 'data-day-status': 'dismissed' });
632
+ const status = prep ? 'prepare' : 'you';
633
+ if (!Number.isFinite(due)) return pill(A.chip.today, 'warn', { 'data-day-status': status });
634
+ const left = Math.round((due - m.now) / MIN_MS);
635
+ return pill(prep ? A.at(at(due)) : A.by(at(due)), left < 60 ? 'bad' : left < 180 ? 'warn' : '', { 'data-day-status': status, title: A.left(left) });
636
+ }
637
+ /** The times a snooze can take, later today: [label, ms]. */
638
+ function snoozeTimes(m, { due, start }) {
639
+ const out = [];
640
+ const add = (label, t) => { if (Number.isFinite(t) && t > m.now + 5 * MIN_MS && sameDay(t, m.now) && !out.some((x) => x[1] === t)) out.push([label, t]); };
641
+ if (Number.isFinite(start)) { add(A.snoozeTo.m30, start - 30 * MIN_MS); add(A.snoozeTo.m15, start - 15 * MIN_MS); return out; }
642
+ add(A.snoozeTo.hour, m.now + 60 * MIN_MS);
643
+ if (Number.isFinite(due)) add(A.snoozeTo.due, due - 30 * MIN_MS);
644
+ add(A.snoozeTo.later, clockAt(m.now, 16, 0));
645
+ add(A.snoozeTo.end, clockAt(m.now, 17, 30));
646
+ return out;
647
+ }
648
+ /** The banner of a marked item: what the mark is, and the press that undoes it. */
649
+ function markBanner(ctx, id, mark, prep) {
650
+ if (!mark) return null;
651
+ if (mark.back) return note(A.state.back, { 'data-day': 'mark-back' });
652
+ const st = mark.state;
653
+ if (!st) return null;
654
+ const text = st === 'snoozed' ? A.state.snoozed(hhmm(mark.until)) : st === 'waiting' ? A.state.waiting(mark.on, hhmm(mark.until))
655
+ : st === 'done' ? (prep ? A.state.ready : A.state.done)(hhmm(mark.at)) : A.state.dismissed(A.why[mark.why] || mark.why);
656
+ return el('div', { class: 'td-day__mark', 'data-day': 'mark', 'data-day-mark-state': st }, el('span', null, text),
657
+ button({ label: st === 'done' ? A.reopen : A.bringBack, size: 'sm', attrs: { 'data-day-mark': 'reopen' }, onClick: () => markPress(ctx.ws, id, { action: 'reopen' }, A.did.reopen) }));
658
+ }
659
+ /** The steps the PM checks off. A step the brief already marked done stays done. A step that reads a source shows what
660
+ * the source says under it (PRD X2), and a note step holds the PM's own note (PRD F3), so no other file needs opening. */
661
+ function stepChecks(ctx, id, steps, { refs = [], mark = null } = {}) {
662
+ if (!steps.length) return null;
663
+ return el('ul', { class: 'td-day__steps2', 'data-day': 'steps' }, ...steps.map((st) => {
664
+ const f = field({ kind: 'check', label: st.t, value: !!st.done, attrs: { 'data-day-step': st.t, disabled: st.fixed ? true : null },
665
+ onCommit: (v) => markPress(ctx.ws, id, { action: 'step', step: st.t, done: !!v }) });
666
+ const r = Number.isInteger(st.ref) ? refs[st.ref] : null;
667
+ return el('li', { class: 'td-day__step2', 'data-done': st.done ? 'true' : 'false' },
668
+ el('div', { class: 'td-day__steprow' }, f, st.who ? el('span', { class: 'td-day__who' }, D.who[st.who] || st.who) : null),
669
+ r ? refBlock(r, { open: !st.done, step: true }) : null,
670
+ st.note ? noteBox(ctx, id, mark) : null);
671
+ }));
672
+ }
673
+ /** One source in a few lines, in the card: what it says, and a small link to the source for when the PM wants more. */
674
+ function refBlock(r, { open, step } = {}) {
675
+ return el('details', { class: 'td-day__ref', 'data-day': 'ref', open: open ? true : null },
676
+ el('summary', null, el('span', null, (step ? A.refRead : '') + r.label), r.source ? el('span', { class: 'td-day__src' }, srcName(r.source)) : null),
677
+ el('ul', { class: 'td-day__reflines' }, ...(r.lines || []).map((l) => el('li', null, l))),
678
+ isUrl(r.link) ? extLink(A.refOpen, r.link, { 'data-day-link': 'ref' }) : null);
679
+ }
680
+ /** The sources no step names, under one heading. */
681
+ function refsBlock(refs, steps) {
682
+ const named = new Set(steps.map((st) => st.ref).filter(Number.isInteger));
683
+ const rest = refs.filter((_, i) => !named.has(i));
684
+ return rest.length ? el('div', { class: 'td-day__phase', 'data-day': 'refs' }, el('h4', null, A.refsTitle), ...rest.map((r) => refBlock(r, { open: false }))) : null;
685
+ }
686
+ /** The conversation, as the brief read it: at most 4 messages, the last one first in weight. */
687
+ function threadBlock(thread) {
688
+ if (!Array.isArray(thread) || !thread.length) return null;
689
+ return el('div', { class: 'td-day__phase td-day__thread', 'data-day': 'thread' }, el('h4', null, A.threadTitle),
690
+ ...thread.map((x, i) => el('div', { class: 'td-day__msg' + (i === thread.length - 1 ? ' td-day__msg--last' : '') },
691
+ el('div', { class: 'td-day__src' }, [x.who, x.at].filter(Boolean).join(' · ')), el('p', null, x.text))));
692
+ }
693
+ /** The PM's own note on a meeting, saved with its marks when the box loses focus. Nothing is sent. */
694
+ function noteBox(ctx, id, mark) {
695
+ const f = field({ label: A.noteTitle, kind: 'textarea', value: (mark && mark.note) || '', placeholder: A.noteHint, attrs: { maxlength: '1000', 'data-day-input': 'note' },
696
+ onCommit: (v) => markPress(ctx.ws, id, { action: 'note', text: v }, A.noteSaved) });
697
+ return el('div', { class: 'td-day__notebox', 'data-day': 'note' }, f, el('span', { class: 'td-day__dim' }, A.noteStays));
698
+ }
699
+ /** Where the PM's own answer goes, and the box to write it in (PRD F1, F2). The press is the approval: the agent posts
700
+ * exactly this text, as the PM, to the place the item names. */
701
+ function composer(ctx, j, verb) {
702
+ const it = j.item || {};
703
+ const sd = it.send || {};
704
+ const to = sd.to || it.where || srcName(j.source);
705
+ const review = !!sd.review;
706
+ const busy = ui.busy.has('send:' + j.id);
707
+ const f = field({ label: A.compose[verb] || A.compose.reply, kind: 'textarea', value: ui.compose.get(j.id) || '', placeholder: A.composeHint,
708
+ attrs: { maxlength: '4000', rows: '4', 'data-day-input': 'compose' } });
709
+ const ta = f.control;
710
+ ta.addEventListener('input', () => { ui.compose.set(j.id, ta.value); });
711
+ const chips = (sd.suggest || []).length ? el('div', { class: 'td-day__chipsrow', 'data-day': 'suggest' }, el('span', { class: 'td-day__dim' }, A.startFrom),
712
+ ...sd.suggest.map((t, i) => button({ label: t.length > 60 ? t.slice(0, 57) + '…' : t, size: 'sm', variant: 'ghost', attrs: { 'data-day-suggest': String(i), title: t },
713
+ onClick: () => { ta.value = t; ui.compose.set(j.id, t); ta.focus(); } }))) : null;
714
+ const verdict = ui.verdict.get(j.id) || null;
715
+ const toggle = review ? tabs({ variant: 'segmented', role: 'radiogroup', ariaLabel: A.verdictLabel, value: verdict, attrs: { 'data-day': 'verdict' },
716
+ items: [{ id: 'approve', label: A.verdict.approve }, { id: 'changes', label: A.verdict.changes }], onChange: (k) => { ui.verdict.set(j.id, k); rerender(); } }) : null;
717
+ const go = () => {
718
+ const text = (ui.compose.get(j.id) || ta.value || '').trim();
719
+ if (!text) { toast({ text: A.sendNeed, tone: 'destructive' }); return; }
720
+ if (review && !verdict) { toast({ text: A.verdictNeed, tone: 'destructive' }); return; }
721
+ sendPress(ctx.ws, j, { text, verdict: review ? verdict : undefined }, to);
722
+ };
723
+ return el('div', { class: 'td-day__compose', 'data-day': 'compose' }, f, chips, toggle,
724
+ el('div', { class: 'td-day__acts' }, button({ label: review ? A.postReview(to) : A.sendTo(to), variant: 'primary', size: 'sm', iconEnd: 'arrow-right', disabled: busy, attrs: { 'data-day-act': 'send' }, onClick: go })),
725
+ el('span', { class: 'td-day__dim' }, A.sendNote));
726
+ }
727
+ /** The four presses of a job (Mark done, Snooze, Waiting on…, Not mine) or the three of a meeting (Mark ready, Remind
728
+ * me, Skip prep). `strong` makes the first one the primary button: every step is checked. */
729
+ function marksBar(ctx, { id, title, kind, due, start, strong }) {
730
+ const { m, ws } = ctx;
731
+ const prep = kind === 'event';
732
+ const busy = ui.busy.has('mark:' + id);
733
+ const st = (m.d && m.d.marks && m.d.marks[id] && m.d.marks[id].state) || null;
734
+ const times = snoozeTimes(m, { due, start });
735
+ const kids = [];
736
+ if (st !== 'done') kids.push(button({ label: prep ? A.ready : A.done, variant: strong ? 'primary' : 'outline', size: 'sm', icon: 'check', disabled: busy, attrs: { 'data-day-mark': 'done' },
737
+ onClick: () => markPress(ws, id, { action: 'done' }, (prep ? A.did.ready : A.did.done)(title)) }));
738
+ if (times.length) {
739
+ const b = button({ label: prep ? A.remind : A.snooze, size: 'sm', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'snooze', 'aria-haspopup': 'menu' },
740
+ onClick: () => menu({ anchor: b, ariaLabel: prep ? A.remind : A.snooze, items: times.map(([l, t]) => ({ label: `${l} · ${hhmm(t)}`, attrs: { 'data-day-snooze': hhmm(t) },
741
+ onSelect: () => markPress(ws, id, { action: 'snooze', until: new Date(t).toISOString() }, A.did.snooze(hhmm(t))) })) }) });
742
+ kids.push(b);
743
+ }
744
+ if (!prep) kids.push(button({ label: A.hold, size: 'sm', disabled: busy, attrs: { 'data-day-mark': 'hold', 'aria-expanded': ui.holding === id ? 'true' : 'false' },
745
+ onClick: () => { ui.holding = ui.holding === id ? null : id; ui.holdDraft = { who: '', when: 'h2' }; rerender(); } }));
746
+ const reasons = prep ? ['nothing', 'elsewhere'] : ['no-action', 'someone-else', 'done-elsewhere'];
747
+ if (st !== 'dismissed') {
748
+ const nb = button({ label: prep ? A.skip : A.notMine, size: 'sm', variant: 'ghost', iconEnd: 'chevron-down', disabled: busy, attrs: { 'data-day-mark': 'dismiss', 'aria-haspopup': 'menu' },
749
+ onClick: () => menu({ anchor: nb, ariaLabel: prep ? A.skip : A.notMine, items: reasons.map((w) => ({ label: A.why[w], attrs: { 'data-day-why': w },
750
+ onSelect: () => markPress(ws, id, { action: 'dismiss', why: w }, A.did.dismiss(A.why[w])) })) }) });
751
+ kids.push(el('span', { class: 'td-day__grow', 'aria-hidden': 'true' }), nb);
752
+ }
753
+ return [el('div', { class: 'td-day__marks', 'data-day': 'marks', role: 'group', 'aria-label': title }, ...kids), ui.holding === id && !prep ? holdForm(ctx, id) : null];
754
+ }
755
+ function holdForm(ctx, id) {
756
+ const { m } = ctx;
757
+ const h2 = m.now + 120 * MIN_MS; const end = clockAt(m.now, 17, 30);
758
+ const opts = [['h2', A.holdWhenOpts.h2, h2], ['end', A.holdWhenOpts.end, end]].filter(([, , t]) => t > m.now && sameDay(t, m.now));
759
+ if (!opts.some((o) => o[0] === ui.holdDraft.when) && opts.length) ui.holdDraft.when = opts[0][0];
760
+ const who = field({ label: A.holdWho, value: ui.holdDraft.who, placeholder: A.holdHint, attrs: { maxlength: '80', 'data-day-input': 'hold-who' } });
761
+ who.control.addEventListener('input', () => { ui.holdDraft.who = who.control.value; });
762
+ const go = () => {
763
+ const v = (ui.holdDraft.who || '').trim();
764
+ const o = opts.find((x) => x[0] === ui.holdDraft.when);
765
+ if (!v || v.length > 80) { toast({ text: A.holdNeed, tone: 'destructive' }); return; }
766
+ if (!o) return;
767
+ markPress(ctx.ws, id, { action: 'hold', on: v, until: new Date(o[2]).toISOString() }, A.did.hold(v, hhmm(o[2])));
768
+ };
769
+ return el('div', { class: 'td-day__revise', 'data-day': 'hold' }, who,
770
+ opts.length ? el('div', { class: 'td-day__ch' }, el('span', null, A.holdWhen), tabs({ variant: 'segmented', role: 'radiogroup', ariaLabel: A.holdWhen, value: ui.holdDraft.when,
771
+ attrs: { 'data-day': 'hold-when' }, items: opts.map(([k, l, t]) => ({ id: k, label: `${l} (${hhmm(t)})` })), onChange: (k) => { ui.holdDraft.when = k; rerender(); } })) : null,
772
+ el('div', { class: 'td-day__acts' }, button({ label: A.holdGo, variant: 'primary', size: 'sm', disabled: !opts.length, attrs: { 'data-day-act': 'hold-go' }, onClick: go }),
773
+ button({ label: D.cancel, size: 'sm', variant: 'ghost', onClick: () => { ui.holding = null; rerender(); } })));
774
+ }
775
+ const open0 = (mark) => !mark || !mark.state;
776
+ /** A link drawn as the primary button of a card. */
777
+ function primaryLink(label, href, attrs) {
778
+ return extLink(label, href, { class: 'td-day__a td-day__a--primary', ...(attrs || {}) });
779
+ }
780
+
781
+ /** The card of a job only the PM can do (PRD A1, C1, C2, M1, D3): the verb and one sentence, From and Why now, the
782
+ * steps, the button that fits, the marks, and the rest of the context folded. */
783
+ function actCard(ctx, j) {
784
+ const { m } = ctx;
785
+ const it = j.item || {};
786
+ const mark = j.mark || null;
787
+ const verb = verbOf(it);
788
+ const todo = it.todo || it.how || it.ask || null;
789
+ const checked = new Set((mark && mark.steps) || []);
790
+ const steps = (Array.isArray(it.steps) ? it.steps : []).map((x) => (typeof x === 'string' ? { t: x } : x)).map((x) => ({ ...x, done: checked.has(x.t) }));
791
+ const refs = Array.isArray(it.refs) ? it.refs : [];
792
+ const canSend = open0(mark) && it.owner === 'pm' && isUrl(j.link) && (!!it.send || ['reply', 'review', 'decide', 'sign-off'].includes(verb));
793
+ const due = it.due ? Date.parse(it.due) : NaN;
794
+ const n = j.goal ? m.goalNo.get(j.goal) : null;
795
+ const open = !mark || !mark.state;
796
+ const allDone = steps.length > 0 && steps.every((x) => x.done);
797
+ const reply = typeof it.reply_needed === 'boolean' ? (it.reply_needed ? D.yes : D.no) : null;
798
+ const more = dl([[D.about, it.about || null], [D.ask, it.ask || null], [D.reply, reply], [D.how, todo === it.how ? null : it.how || null]]);
799
+ return el('article', { class: 'td-day__job td-day__job--act', 'data-day-job': j.id, 'data-day-act-card': verb },
800
+ el('div', { class: 'td-day__jobtop' }, el('span', { class: 'td-day__chips' }, verbChip(verb), el('span', { class: 'td-day__src' }, [srcName(j.source), n ? D.pill.goal(n) : null].filter(Boolean).join(' · '))),
801
+ dueChip(m, mark, due, false)),
802
+ el('h3', { class: 'td-day__acth' }, j.title),
803
+ todo ? el('p', { class: 'td-day__todo', 'data-day': 'todo' }, todo) : null,
804
+ markBanner(ctx, j.id, mark, false),
805
+ dl([[D.where, A.from(it.who, it.where)], [D.why, it.why_now || null]]),
806
+ threadBlock(it.thread),
807
+ steps.length ? el('div', { class: 'td-day__phase' }, el('h4', null, A.howTitle(steps.filter((x) => x.done).length, steps.length)), stepChecks(ctx, j.id, steps, { refs })) : null,
808
+ refsBlock(refs, steps),
809
+ canSend ? composer(ctx, j, verb) : null,
810
+ open ? el('div', { class: 'td-day__acts', 'data-day': 'cta' },
811
+ (canSend ? extLink : primaryLink)(A.open[j.source] || A.openLink, j.link, { 'data-day-link': 'conversation' }),
812
+ j.item && j.item.owner === 'pm' ? button({ label: A.draft, size: 'sm', disabled: ui.busy.has('draft:' + j.id), attrs: { 'data-day-act': 'draft' }, onClick: () => draftPress(ctx.ws, j) }) : null) : null,
813
+ marksBar(ctx, { id: j.id, title: j.title, kind: 'item', due, strong: allDone }),
814
+ more ? el('details', { class: 'td-day__fold', 'data-day': 'more' }, el('summary', null, A.more), more, note(A.readOnly)) : null);
815
+ }
816
+
817
+ /** Free minutes from now to the meeting's start, less the other meetings in between. */
818
+ function freeBefore(m, e) {
819
+ if (!Number.isFinite(e.s) || e.s <= m.now) return 0;
820
+ let busy = 0;
821
+ for (const o of m.events) {
822
+ if (o.id === e.id) continue;
823
+ const a = Math.max(o.s, m.now); const b = Math.min(o.f, e.s);
824
+ if (b > a) busy += b - a;
825
+ }
826
+ return Math.max(0, Math.round((e.s - m.now - busy) / MIN_MS));
827
+ }
828
+
829
+ /** A meeting's prep (PRD A1, C1, M1, M6, D2): the verb and the prep line, how ready the PM is, the Before steps to
830
+ * check off, the button that fits, and the marks. */
831
+ function prepBlock(ctx, e) {
832
+ const { m } = ctx;
833
+ const steps = e.before.map((st) => ({ t: String(st.t || ''), who: st.who, done: st.done, ref: st.ref, note: !!st.note, fixed: !!(e.steps && e.steps.before && e.steps.before.find((x) => x.t === st.t && x.done)) }));
834
+ const refs = Array.isArray(e.refs) ? e.refs : [];
835
+ const d = steps.filter((x) => x.done).length; const total = steps.length;
836
+ const left = total - d;
837
+ const free = freeBefore(m, e);
838
+ const fit = !total ? null : !left ? A.fit.ok : free >= left * 15 ? A.fit.time(left, free) : A.fit.short(left, free);
839
+ const open = !e.mark || !e.mark.state;
840
+ const link = Array.isArray(e.links) ? e.links.find((l) => isUrl(l.url)) : null;
841
+ return el('div', { class: 'td-day__prep', 'data-day': 'prep' },
842
+ el('div', { class: 'td-day__jobtop' }, verbChip('prepare'), dueChip(m, e.mark, e.s, true)),
843
+ e.prep ? el('p', { class: 'td-day__todo', 'data-day': 'todo' }, e.prep) : null,
844
+ markBanner(ctx, e.id, e.mark, true),
845
+ total ? el('div', { class: 'td-day__ready', 'data-day': 'readiness' }, meter({ done: d, total, label: A.readiness(d, total) }), fit ? el('span', { class: 'td-day__dim' }, fit) : null) : null,
846
+ total ? el('div', { class: 'td-day__phase' }, el('h4', null, A.beforeTitle(e.time)), stepChecks(ctx, e.id, steps, { refs, mark: e.mark })) : null,
847
+ refsBlock(refs, steps),
848
+ open && link && left ? el('div', { class: 'td-day__acts', 'data-day': 'cta' }, primaryLink((link.label || link.url) + ' ↗', link.url, { 'data-day-link': 'prep' })) : null,
849
+ marksBar(ctx, { id: e.id, title: e.title, kind: 'event', start: e.s, strong: total > 0 && !left }));
850
+ }
851
+
526
852
  /* ── the right column ─────────────────────────────────────────────────────────────────────────────────────────── */
527
853
 
528
854
  function dhead(kind, title) {
@@ -563,27 +889,31 @@ function eventDetail(ctx, e) {
563
889
  el('p', { class: 'td-day__dim td-day__mono' }, `${e.time}${e.end ? '–' + hhmm(e.end) : ''}${e.with ? ' · ' + e.with : ''}${e.ended ? ' · ' + D.ended : ''}`),
564
890
  e.with ? peopleLinks(ctx.ws, e.with, { 'data-day': 'people' }) : null,
565
891
  e.purpose ? el('p', null, el('b', null, D.purpose + ': '), e.purpose) : null,
566
- e.prep ? el('p', null, el('b', null, D.prep + ': '), e.prep) : null,
892
+ e.needsPrep ? prepBlock(ctx, e) : null,
567
893
  ini ? el('div', { class: 'td-day__ini', 'data-day': 'initiative' }, el('span', null, D.initiative + ': ', el('b', { class: 'td-day__mono' }, ini.id), ini.why ? el('span', { class: 'td-day__dim' }, ' (' + D.suggested(ini.why) + ')') : null),
568
894
  ctx.openIni ? button({ label: D.openInitiative, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'open-initiative' }, onClick: () => ctx.openIni(ini.id) }) : null) : null,
569
895
  tie,
570
896
  memoryBlock(e.id),
571
- !e.ended ? el('div', { class: 'td-day__acts' }, e.prep ? pill(D.needPrep, 'warn') : steps.before && steps.before.length ? pill(D.nothingToPrep, 'ok') : null) : null,
572
897
  after,
573
898
  notesBlock(e, { ws: ctx.ws, initiative: ini }),
574
- !e.ended ? [list(D.phases.before, (steps.before || []).map(stepItem)), list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), mailBlock] : null,
899
+ // with prep, the Before steps are the checklist above; During, After and the mail fold away (PRD D2)
900
+ !e.ended && e.needsPrep && ((steps.during || []).length || (steps.after || []).length || mailBlock)
901
+ ? el('details', { class: 'td-day__fold', 'data-day': 'more' }, el('summary', null, A.moreMeeting((steps.during || []).length, (steps.after || []).length)),
902
+ list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), mailBlock) : null,
903
+ !e.ended && !e.needsPrep ? [list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), mailBlock] : null,
575
904
  Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null,
576
905
  ];
577
906
  }
578
907
 
579
908
  function jobCard(ctx, j) {
909
+ if (j.status === 'you') return actCard(ctx, j);
580
910
  const { m, ws } = ctx;
581
911
  const t = j.task;
582
912
  const it = j.item || {};
583
913
  const n = j.goal ? m.goalNo.get(j.goal) : null;
584
914
  const busy = t && ui.busy.has(t.id);
585
915
  const revs = t && Array.isArray(t.revisions) ? t.revisions.length : 0;
586
- const statusNote = { notStarted: D.notTask, waiting: D.waitingNote, working: D.workingNote, sending: D.sendingNote, you: D.youNote }[j.status]
916
+ const statusNote = { notStarted: D.notTask, waiting: D.waitingNote, working: D.workingNote, sending: D.sendingNote }[j.status]
587
917
  || (j.status === 'done' && j.kind === 'auto' ? D.autoNote : null);
588
918
  const draft = t && t.draft ? el('div', { class: 'td-day__draftbox', 'data-day': 'draft' }, el('h4', null, D.draft),
589
919
  t.draft.summary ? el('p', null, t.draft.summary) : null,
@@ -902,6 +1232,6 @@ export function daySection({ ws, setState, openIni, owed, here, all } = {}) {
902
1232
 
903
1233
  export function daySig(ws) {
904
1234
  const c = cache.get(ws) || {};
905
- return JSON.stringify([forced, ui.open, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], c.error, runs.get(ws) || null, [...seen].length])
1235
+ return JSON.stringify([forced, ui.open, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], [...ui.groupsOpen], ui.holding, ui.holdDraft.when, [...ui.verdict], c.error, runs.get(ws) || null, [...seen].length])
906
1236
  + dataSig(c.data) + goalsSig() + agendaSig();
907
1237
  }
@@ -1,4 +1,4 @@
1
- /* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 5).
1
+ /* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 6).
2
2
  *
3
3
  * The screen follows the simplified mockup the PM reviewed (prototype/meeting-notes):
4
4
  * header the title and the date, and one action: Listen; while listening a red dot, the time, the sources and Stop;
@@ -28,8 +28,10 @@
28
28
  * calendar) and which initiative it belongs to, and how that was set: from the calendar event, suggested from what was said,
29
29
  * or chosen by the PM. Change opens a sheet for each.
30
30
  *
31
- * IN THE EVENT'S DETAIL (Run 5). renderInline draws the same note inside a calendar event's detail (pm/notes-links.js), in
32
- * one column: the PM types and listens there without leaving the calendar. Open in Notes shows it on the full screen.
31
+ * MINIMIZED AT THE BOTTOM RIGHT (Run 6). Take notes on a calendar event starts listening at once (listenNow: no chooser) and
32
+ * keeps the note as a pill at the bottom right (pm/note-dock.js). The pill opens the note as a pop-up, which renderInline
33
+ * draws in one column. While the pop-up shows a note, the Notes tab says so and does not draw it a second time.
34
+ * Open in Notes shows it on the full screen.
33
35
  *
34
36
  * Reads and writes (lib/routes-notes.js). Notes live in the per-user runtime directory, never in a repository:
35
37
  * GET /api/notes · /api/notes/one?id=… · /api/notes/engine?ws=… · /api/notes/recording?id=… · /api/people?ws=…
@@ -67,7 +69,8 @@ let busy = '';
67
69
  let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
68
70
  let people = null; // { ws, names } — the repository's people, offered in the line editor
69
71
  let pendingOpen = null; // a note another page asked to open (openNote), selected at the next draw
70
- let inlineSwitch = null; // the note an event's detail is switching to (renderInline), so a redraw does not start it twice
72
+ let inlineSwitch = null; // the note the pop-up is switching to (renderInline), so a redraw does not start it twice
73
+ let popped = null; // the note the pop-up shows (pm/note-dock.js), which the Notes tab then does not draw
71
74
  const LABELS = [['decision', 'Decision'], ['action', 'Action'], ['question', 'Question'], ['fact', 'Fact'], ['topic', 'Topic'], ['statement', 'Statement'], ['small talk', 'Small talk']];
72
75
 
73
76
  const nowClock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
@@ -1021,7 +1024,12 @@ export function renderBoard(inner, ctx) {
1021
1024
  if (banner) wrap.appendChild(el('div', { class: 'nt-banner' + (banner.tone === 'bad' ? ' nt-banner--bad' : ''), 'data-nt-banner': banner.tone, role: banner.tone === 'bad' ? 'alert' : 'status' }, banner.text));
1022
1025
  const n = currentId ? cache.get(currentId) : null;
1023
1026
  if (currentId && !n) { loadNote(ctx, currentId); wrap.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); }
1024
- else if (!n) {
1027
+ else if (n && popped === n.id) {
1028
+ wrap.appendChild(el('div', { class: 'nt-start', 'data-nt-popped': '' },
1029
+ el('h2', { class: 'nt-h' }, n.title || 'Untitled note'),
1030
+ el('p', { class: 'nt-hint' }, 'This note is open in the pop-up at the bottom right.'),
1031
+ button({ label: 'Show it here', variant: 'primary', onClick: () => openFull(ctx, n.id), attrs: { 'data-nt-unpop': '' } })));
1032
+ } else if (!n) {
1025
1033
  wrap.appendChild(el('div', { class: 'nt-start' },
1026
1034
  el('h2', { class: 'nt-h' }, 'Notes'),
1027
1035
  el('p', { class: 'nt-hint', 'data-nt-about': '' }, 'Take notes in a meeting. PMOS listens on this machine, catches the decisions, actions and questions, and checks them against your repository. No audio leaves this machine, and notes are kept outside every repository.'),
@@ -1055,17 +1063,39 @@ function keepFocus() {
1055
1063
  }
1056
1064
  async function openFull(ctx, id) {
1057
1065
  await flushSave(ctx);
1058
- const c = await import('./panel-cockpit.js');
1066
+ popped = null;
1067
+ const [c, d] = await Promise.all([import('./panel-cockpit.js'), import('./note-dock.js')]);
1068
+ d.minimize();
1059
1069
  pendingOpen = id;
1060
1070
  c.openView('notes');
1061
1071
  }
1062
1072
 
1073
+ /** The note the pop-up shows (pm/note-dock.js), or null. */
1074
+ export function setPopped(id) { popped = id || null; }
1075
+ /** The note's title as last read or typed, or ''. */
1076
+ export const titleOf = (id) => (editor && editor.id === id && editor.title.value.trim()) || ((cache.get(id) || {}).title) || '';
1077
+ /** When the note that is listening started, or null. */
1078
+ export const liveStarted = () => (audio && audio.state === 'on' ? audio.started : speech && speech.mode === 'record' && speech.state === 'on' ? speech.started : null);
1079
+ /** Stop the note that is listening, as its Stop does. */
1080
+ export const stopListening = (ctx) => stopRecording(ctx);
1081
+ /** Start listening on this note at once, with no chooser (Run 6, Take notes): the microphone and the meeting when Whisper is set
1082
+ * up, else the microphone through the browser's on-device recognition. False when the note cannot listen (private, gone). */
1083
+ export async function listenNow(ctx, id) {
1084
+ if (id !== currentId) await selectNote(ctx, id);
1085
+ let n = cache.get(id);
1086
+ if (!n) { await loadNote(ctx, id); n = cache.get(id); }
1087
+ if (!n || n.private) return false;
1088
+ const eng = engine && engine.ws === wsOf(ctx) && engine.whisper ? engine : await loadEngine(ctx);
1089
+ if (eng.whisper && eng.whisper.ready) await startAudio(ctx, n, ['mic', 'meeting']); else await startSpeech(ctx, n, 'record');
1090
+ return listeningId() === id;
1091
+ }
1092
+
1063
1093
  /** The note that is listening now, or null. */
1064
1094
  export const listeningId = () => (audio && audio.id) || (speech && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking') ? speech.id : null);
1065
1095
 
1066
- /** One note drawn inside a calendar event's detail (meeting-notes Run 5): the same note, editor and listening as the Notes
1067
- * tab, in one column, with Open in Notes for the full screen. It never stops a meeting that is being heard: when another note
1068
- * is listening it draws nothing and returns false. */
1096
+ /** One note drawn in the pop-up (meeting-notes Run 6, pm/note-dock.js): the same note, editor and listening as the Notes tab,
1097
+ * in one column, with Open in Notes for the full screen. It never stops a meeting that is being heard: when another note is
1098
+ * listening it draws nothing and returns false. */
1069
1099
  export function renderInline(host, ctx, id) {
1070
1100
  if (ctx.FIXTURE) return false;
1071
1101
  const other = listeningId();