create-pmos 0.106.0 → 0.108.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.
@@ -0,0 +1,36 @@
1
+ /* pm/notes-task.js — Make task: a meeting note's next step as a plan item of the note's initiative (meeting-notes Run 12,
2
+ * mockup board 3 "Next step to task in one click").
3
+ *
4
+ * makeTask(note, { text, owner?, due? }, { repo?, btn? }) → Promise<item id | null>
5
+ *
6
+ * One press, no form: the item goes into the initiative's first stage through the Breakdown's own writer (POST
7
+ * /api/plan-items, as the PM), with the owner when the line names one and the due day when it is a date. A toast says
8
+ * where it went and offers Undo (the item is withdrawn). A note with no initiative gets a toast that says to link one.
9
+ */
10
+ import { toast, SEL } from './primitives.js';
11
+ import { pmGet, pmPost } from './http.js';
12
+ import { wsOf } from './data.js';
13
+
14
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
15
+ const iniName = (i) => (i ? i.title || i.id : null);
16
+ const label = (btn, text, done) => { if (!btn) return; const l = btn.querySelector(SEL.btnLabel); if (l) l.textContent = text; btn.disabled = !!done; };
17
+
18
+ export async function makeTask(n, x, { repo, btn } = {}) {
19
+ const ini = n.initiative;
20
+ const ws = ini && (ini.ws || n.ws || (repo ? wsOf(repo) : null));
21
+ if (!ini || !ini.id || !ws) { toast({ text: 'Link the note to an initiative first. Make task adds the step to its breakdown.' }); return null; }
22
+ if (btn) btn.disabled = true;
23
+ const lay = await pmGet('/api/plan-items?ladder=0&ws=' + encodeURIComponent(ws) + '&id=' + encodeURIComponent(ini.id));
24
+ const stage = lay && lay.ok && (lay.stages || [])[0];
25
+ if (!stage) { if (btn) btn.disabled = false; toast({ text: 'The task was not made: ' + ((lay && lay.error) || 'the initiative has no stage'), tone: 'destructive' }); return null; }
26
+ const ev = { ev: 'planned', initiative: ini.id, title: String(x.text || '').slice(0, 200), stage: stage.id, note: 'From the meeting note "' + (n.title || 'Untitled note') + '"' };
27
+ if (x.owner) ev.owner = /^(you|me)$/i.test(x.owner) ? 'you' : 'person:' + String(x.owner).slice(0, 60);
28
+ if (x.due && DATE_RE.test(x.due)) ev.due = x.due;
29
+ const r = await pmPost('/api/plan-items', { ws, events: [ev] });
30
+ if (!r || !r.ok) { if (btn) btn.disabled = false; toast({ text: 'The task was not made' + ((r && r.error) ? ': ' + r.error : ''), tone: 'destructive' }); return null; }
31
+ const item = (r.written && r.written[0] && r.written[0].item) || null;
32
+ label(btn, 'Task made', true);
33
+ toast({ text: 'Added to ' + iniName(ini) + ', under ' + (stage.label || stage.id), actionLabel: item ? 'Undo' : undefined,
34
+ onAction: item ? async () => { const u = await pmPost('/api/plan-items', { ws, events: [{ ev: 'withdrawn', item, initiative: ini.id }] }); if (u && u.ok) label(btn, 'Make task', false); } : undefined });
35
+ return item;
36
+ }
@@ -47,7 +47,6 @@ export const VIEWS = {
47
47
  // meeting-notes Run 1: typed notes, dictation, meeting transcripts and summaries; the PM's own, the same in every repository
48
48
  notes: { label: 'notes', mode: null, scoped: false, palette: true, hint: 'type, dictate or record a meeting' },
49
49
  // meeting-notes Run 11: notes menu with two-column view grouped by initiative (R47)
50
- notesmenu: { label: 'notes menu', mode: null, scoped: false, hint: 'view and organize all notes by initiative' },
51
50
  };
52
51
 
53
52
  /** The view a list mode opens as, or null when the mode is not a view. `product` is the list itself. */
@@ -97,7 +96,7 @@ function modes() {
97
96
  * every `data-*` hook the suites use, and only adds the board's frame around it. A view with no board module draws as before. */
98
97
  export const BOARD_MODULES = {
99
98
  today: './view-today.js', validate: './view-validate.js', addrepo: './view-addrepo.js', livenotes: './view-livenotes.js',
100
- notes: './view-notes.js', notesmenu: './view-notes-menu.js',
99
+ notes: './view-notes.js',
101
100
  };
102
101
  const boardMods = {}; // view -> undefined (not tried) · null (loading or absent) · module
103
102
  function boardOf(view) {
@@ -53,6 +53,8 @@ export const DESTINATIONS = Object.freeze([
53
53
  // Not a pm-os-absorb run (run: null): docs-three-way-sync added it — is each linked document the same as its copies.
54
54
  Object.freeze({ id: 'docs', label: 'Docs', key: 'g d', module: 'dest-docs.js', run: null }),
55
55
  // Not a pm-os-absorb run (run: null): people-directory added it — the people the PM works with in a repository.
56
+ // Not a pm-os-absorb run (run: null): meeting-notes Run 12 added it — every meeting note by initiative, and the note.
57
+ Object.freeze({ id: 'notes', label: 'Notes', key: 'g n', module: 'dest-notes.js', run: null }),
56
58
  Object.freeze({ id: 'people', label: 'People', key: 'g p', module: 'dest-people.js', run: null }),
57
59
  Object.freeze({ id: 'settings', label: 'Settings', key: null, module: 'dest-settings.js', run: 3 }),
58
60
  ]);
@@ -21,13 +21,16 @@
21
21
  import { el, setChildren } from './dom.js';
22
22
  import { button, needsDot, needsCount } from './primitives.js';
23
23
  import { COPY } from './frame-copy.js';
24
- import { isRecording } from './note-dock.js';
24
+ import * as dock from './note-dock.js';
25
+ import { iconButton } from './primitives.js';
26
+ import { el as h } from './dom.js';
25
27
 
26
28
  /** The destinations the bar names, in order. Settings sits at the end of the bar, after Sync. Docs joined the bar at the
27
29
  * PM's word (2026-10-06, docs-three-way-sync: "I thought Docs is one of the main menu"). People joined after Docs when the
28
30
  * PM approved the People mockup and said build it (2026-10-06, people-directory). My work joined beside Agenda when the PM
29
- * approved the mockup that puts it there and said build it (2026-10-07, my-work), so the bar holds 10 controls. */
30
- export const BAR_DESTS = Object.freeze(['today', 'mywork', 'initiatives', 'board', 'reports', 'docs', 'people']);
31
+ * approved the mockup that puts it there and said build it (2026-10-07, my-work). Notes joined between Docs and People with the
32
+ * Record control before Settings, as the "Notes: next features" mockup the PM approved (2026-10-07, meeting-notes Run 12). */
33
+ export const BAR_DESTS = Object.freeze(['today', 'mywork', 'initiatives', 'board', 'reports', 'docs', 'notes', 'people']);
31
34
 
32
35
  /** Is this sync reading "not in sync"? Behind, ahead, records not yet recorded, or `remote_stale`: no fetch in the last
33
36
  * few minutes, so another machine's push would be invisible here and "in sync" cannot be claimed. An unreadable status
@@ -38,7 +41,10 @@ export function unsyncedOf(st) {
38
41
  }
39
42
 
40
43
  /** The lucide symbol each place wears in the bar (UI comfort, 2026-10-07: a word alone is slow to find; the phone's tab bar already wears these). */
41
- const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', people: 'users', settings: 'settings' });
44
+ const NAV_ICON = Object.freeze({ today: 'house', mywork: 'list-checks', initiatives: 'target', board: 'square-kanban', reports: 'chart-column', docs: 'file-text', notes: 'notebook-pen', people: 'users', settings: 'settings' });
45
+
46
+ const fmtClock = (ms) => { const t = Math.max(0, Math.floor(ms / 1000)); const m = Math.floor(t / 60); return (m >= 60 ? Math.floor(m / 60) + ':' + String(m % 60).padStart(2, '0') : String(m)) + ':' + String(t % 60).padStart(2, '0'); };
47
+ const dockHighlight = () => (dock.highlightBusy ? dock.highlightBusy() : null);
42
48
 
43
49
  export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {}) {
44
50
  const node = el('header', { class: 'fr-bar', 'data-shell': 'top', 'data-fr': 'bar' });
@@ -47,12 +53,6 @@ export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {})
47
53
  let repoLabel = null;
48
54
  const repoSlot = el('div', { class: 'fr-bar__repo' });
49
55
 
50
- const recordingIndicator = el('span', {
51
- class: 'fr-recording-dot', 'data-fr-recording': '',
52
- title: 'Notes are being recorded',
53
- 'aria-hidden': 'true',
54
- });
55
-
56
56
  const count = needsCount('0', { 'data-today-badge': '' });
57
57
  const dot = needsDot();
58
58
  const links = {};
@@ -87,11 +87,36 @@ export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {})
87
87
  });
88
88
  search.appendChild(el('kbd', { class: 'fr-kbd' }, COPY.searchKey));
89
89
 
90
- const notes = button({
91
- label: 'Notes', variant: 'outline', icon: 'notebook-pen',
92
- attrs: { class: 'fr-notes', 'data-fr': 'notes', 'data-fr-view': 'notes', title: 'Open notes menu (all notes by initiative)' },
93
- onClick: () => openView && openView('notesmenu'),
94
- });
90
+ /* The Record control (meeting-notes Run 12, mockup board 2), right of the bar before Settings. Idle: Record. Recording: the
91
+ * red dot and the clock (a press opens the note), the highlight and Pause. Paused: the clock and Resume. Stop is in the note. */
92
+ const recSlot = el('div', { class: 'fr-rec', 'data-fr': 'record' });
93
+ let recKey = '';
94
+ let recTick = null;
95
+ const recClock = () => { const st = dock.recordState(); return st.started ? fmtClock(Date.now() - st.started) : ''; };
96
+ function drawRecord() {
97
+ const st = dock.recordState();
98
+ const busy = st.mode === 'recording' && dockHighlight();
99
+ const key = st.mode + ':' + st.id + ':' + (busy ? busy.state + busy.open : '');
100
+ if (key === recKey) return;
101
+ recKey = key;
102
+ clearInterval(recTick); recTick = null;
103
+ if (st.mode === 'idle') {
104
+ setChildren(recSlot, [button({ label: 'Record', variant: 'outline', icon: 'mic', attrs: { class: 'fr-rec__start', 'data-fr-rec': 'idle', title: 'Start a note and record the meeting' }, onClick: (e) => dock.recordNew(e && e.currentTarget) })]);
105
+ return;
106
+ }
107
+ if (st.mode === 'paused') {
108
+ setChildren(recSlot, [button({ label: 'Resume', variant: 'outline', icon: 'mic', attrs: { class: 'fr-rec__start', 'data-fr-rec': 'paused', title: 'Resume recording ' + (st.title || 'the note') }, onClick: () => dock.resumeNow() })]);
109
+ return;
110
+ }
111
+ const clock = h('span', { class: 'fr-rec__clock', 'data-fr-rec-clock': '' }, recClock());
112
+ recTick = setInterval(() => { clock.textContent = recClock(); }, 1000);
113
+ const open = button({ label: h('span', { class: 'fr-rec__txt' }, h('span', { class: 'fr-rec-dot', 'aria-hidden': 'true' }), clock), variant: 'ghost',
114
+ attrs: { class: 'fr-rec__seg fr-rec__open', 'data-fr-rec': 'recording', 'aria-label': 'Recording ' + (st.title || 'a note') + '. Open the note.' }, onClick: () => dock.openDock(st.id) });
115
+ const hl = iconButton({ icon: 'highlighter', ariaLabel: busy ? 'Highlight in progress: show it' : 'Highlight the last 30 seconds and what is said next',
116
+ attrs: { class: 'fr-rec__seg' + (busy ? ' fr-rec__seg--busy' : ''), 'data-fr-rec-highlight': busy ? busy.state : '' }, onClick: () => dock.highlightNow() });
117
+ const pause = iconButton({ icon: 'pause', ariaLabel: 'Pause recording', attrs: { class: 'fr-rec__seg', 'data-fr-rec-pause': '' }, onClick: () => dock.pauseNow() });
118
+ setChildren(recSlot, [h('div', { class: 'fr-rec__group', role: 'group', 'aria-label': 'Recording' }, open, hl, pause)]);
119
+ }
95
120
 
96
121
  let syncBtn = null;
97
122
  let syncLabel = null;
@@ -129,8 +154,8 @@ export function createTopbar({ go, openPalette, openView, repoMenu, sync } = {})
129
154
  syncLabel = want;
130
155
  syncBtn = want ? syncButton(want, !!model.syncing) : null;
131
156
  }
132
- recordingIndicator.classList.toggle('fr-recording-dot--active', isRecording());
133
- setChildren(node, [repoSlot, recordingIndicator, nav, search, notes, syncBtn, links.settings]);
157
+ drawRecord();
158
+ setChildren(node, [repoSlot, nav, search, syncBtn, recSlot, links.settings]);
134
159
  }
135
160
 
136
161
  update({});
@@ -39,14 +39,12 @@
39
39
  * POST /api/notes/new · save · dictate · record · transcript · audio · label · speaker · switch · whisper-setup · private
40
40
  * · summarize · summarize-agent · delete · line · recording-delete · link
41
41
  */
42
- import { el } from './dom.js';
42
+ import { el, kept } from './dom.js';
43
43
  import { askConfirm, askText } from './dialogs.js';
44
44
  import { pmGet, pmPost } from './http.js';
45
45
  import { mdRender } from './markdown.js';
46
46
  import { icon } from './icons.js';
47
47
  import { button, iconButton, field, row, sheet, empty, menu, toast, ensureComponentsCss } from './primitives.js';
48
- import { startHighlight, updateListenProgress, formatListenRemaining, addHighlightToNote } from '/lib/notes-highlight.js';
49
- import { checkConflict, collectRepoPassages, formatConflict } from '/lib/notes-conflicts.js';
50
48
 
51
49
  const SAVE_MS = 700; // a pause this long in typing saves the note
52
50
  const POLL_MS = 1500; // while listening, and until the last line is labelled, the note is read this often
@@ -66,6 +64,10 @@ let speech = null; // the browser recogniser: { mode: 'dictate'|'reco
66
64
  let audio = null; // local Whisper capture: { id, state, sources, ctx, streams, started, clock, msg }
67
65
  let engine = null; // { ws, whisper, jev, at }
68
66
  let showList = false;
67
+ let inlineDraw = false; // the pop-up is drawing: the fields and the player it shows are left in place (pm/note-dock.js)
68
+ /** A node this view keeps across redraws (an editor field, the player). The pop-up patches itself (dom.js patchChildren), so
69
+ * there the node is a placeholder and stays where it is: a field that is typed in is never moved, and keeps its focus. */
70
+ const own = (node) => (inlineDraw ? kept(node) : node);
69
71
  let pollTimer = null; let agentTimer = null;
70
72
  let busy = '';
71
73
  let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
@@ -735,161 +737,119 @@ async function startSpeech(ctx, n, mode, install = false) {
735
737
 
736
738
  /* ── highlighting during recording (Run 11) ─────────────────────────────────────── */
737
739
  /** Start a highlight: capture last 30s, listen forward for the chosen duration. */
738
- async function startHighlighting(ctx, n) {
739
- try {
740
- highlight = await startHighlight(n.id, { buffer: true }); // placeholder for actual audio buffer
741
- highlight.state = 'capturing';
742
- ctx.setState({});
743
-
744
- // Simulate capture completion after 100ms
745
- setTimeout(() => {
746
- if (highlight && highlight.id) {
747
- highlight.state = 'listening';
748
- let listenedMs = 0;
749
- const duration = { '30s': 30 * 1000, '1min': 60 * 1000, '2min': 120 * 1000 }[highlightListenDuration] || 30 * 1000;
750
-
751
- highlightTimer = setInterval(() => {
752
- listenedMs += 100;
753
- if (highlight) highlight.listenedMs = listenedMs;
754
- ctx.setState({});
755
- if (listenedMs >= duration) {
756
- clearInterval(highlightTimer);
757
- highlightTimer = null;
758
- if (highlight) {
759
- highlight.state = 'transcribing';
760
- ctx.setState({});
761
- // Simulate transcription
762
- setTimeout(() => {
763
- if (highlight) {
764
- highlight.text = 'Transcribed highlight text here.';
765
- highlight.state = 'labeling';
766
- ctx.setState({});
767
- // Simulate labeling
768
- setTimeout(() => {
769
- if (highlight) {
770
- highlight.label = 'Statement';
771
- highlight.state = 'checking';
772
- ctx.setState({});
773
- // Simulate checking
774
- setTimeout(() => {
775
- if (highlight) {
776
- highlight.state = 'done';
777
- ctx.setState({});
778
- }
779
- }, 500);
780
- }
781
- }, 300);
782
- }
783
- }, 500);
784
- }
785
- }
786
- }, 100);
787
- }
788
- }, 100);
789
- } catch (e) {
790
- banner = { tone: 'bad', text: 'Highlight failed: ' + e.message };
791
- ctx.setState({});
792
- }
793
- }
794
-
795
- /** Close the highlight pop-up (work continues in background). */
796
- function closeHighlight(ctx) {
797
- highlight = null;
798
- if (highlightTimer) clearInterval(highlightTimer);
799
- highlightTimer = null;
740
+ /* ── highlights (meeting-notes Run 12, mockup board 0) ──────────────────────────────────────────────────────────────
741
+ * A press keeps the last 30 seconds and listens for 30 s, 1 min or 2 min more (the PM can change it while it listens).
742
+ * When the window closes and its audio is transcribed, POST /api/notes/highlight makes the highlight from the transcript
743
+ * lines said in the window: their words, the strongest label among them (Jev's when it is on), and the closest file in
744
+ * the repository with Jev's comparison when the check is on. The pop-up shows each step as it happens; closing it does not
745
+ * stop the work, and the highlight is kept in the note. Nothing here is made up: a window with no transcribed line says so. */
746
+ const HL_BEFORE_MS = 30 * 1000;
747
+ const HL_AFTER = [['30s', 30 * 1000, '30 s'], ['1min', 60 * 1000, '1 min'], ['2min', 120 * 1000, '2 min']];
748
+ const hlAfterMs = () => (HL_AFTER.find((x) => x[0] === highlightListenDuration) || HL_AFTER[0])[1];
749
+ let hlOpen = false;
750
+ /** Highlight on the note that is listening: from the Record control, the note's own button, or H. */
751
+ export function highlightNow(ctx, id) {
752
+ const n = cache.get(id);
753
+ const at = liveMs(n);
754
+ if (at === null) return false;
755
+ if (highlight && highlight.state === 'listening') { hlOpen = true; ctx.setState({}); return true; }
756
+ 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 };
757
+ hlOpen = true;
758
+ clearInterval(highlightTimer);
759
+ highlightTimer = setInterval(() => hlTick(ctx), 500);
800
760
  ctx.setState({});
761
+ return true;
801
762
  }
802
-
803
- /** Accept the highlight and add it to the note. */
804
- async function acceptHighlight(ctx, n) {
805
- if (highlight && highlight.text) {
806
- addHighlightToNote(n, highlight);
807
- closeHighlight(ctx);
808
- await pmPost('/api/notes/save', { id: n.id, notes: n });
809
- ctx.setState({});
763
+ function hlTick(ctx) {
764
+ const h = highlight;
765
+ if (!h) { clearInterval(highlightTimer); highlightTimer = null; return; }
766
+ const live = cache.get(h.id) && listening(cache.get(h.id));
767
+ // the window closes when its time is up, or when listening stops first
768
+ if (h.state === 'listening' && (Date.now() - h.pressed >= hlAfterMs() || !live)) {
769
+ clearInterval(highlightTimer); highlightTimer = null;
770
+ h.to = h.at + Math.min(hlAfterMs(), Date.now() - h.pressed);
771
+ finishHighlight(ctx, h);
810
772
  }
773
+ const t = typeof document !== 'undefined' && document.querySelector('[data-nt-hl-left]');
774
+ if (t) t.textContent = hlLeft(h); else ctx.setState({});
811
775
  }
812
-
813
- /** Create a task from a note next step (Run 11). */
814
- async function makeTask(ctx, n, text) {
815
- try {
816
- const title = text.split('\n')[0].slice(0, 100); // First line, max 100 chars
817
- const result = await pmPost('/api/notes/make-task', {
818
- noteId: n.id,
819
- title: title,
820
- description: text,
821
- relatedNoteId: n.id
822
- });
823
- banner = { tone: 'good', text: 'Task created: ' + title };
824
- ctx.setState({});
825
- setTimeout(() => { banner = null; ctx.setState({}); }, 2000);
826
- } catch (e) {
827
- banner = { tone: 'bad', text: 'Failed to create task: ' + e.message };
828
- ctx.setState({});
776
+ const hlLeft = (h) => fmtTime(Math.min(Date.now() - h.pressed, hlAfterMs())) + ' of ' + (HL_AFTER.find((x) => x[0] === highlightListenDuration) || HL_AFTER[0])[2];
777
+ async function finishHighlight(ctx, h) {
778
+ h.state = 'transcribing'; ctx.setState({});
779
+ // the window's last audio is still being transcribed: wait for it (at most 30 s)
780
+ const t0 = Date.now();
781
+ for (;;) {
782
+ const r = await loadNote(ctx, h.id);
783
+ const done = r && r.ok && !(r.audio && r.audio.pending) && (r.note.transcript || []).every((x) => !(typeof x.t === 'number' && x.t <= h.to && x.open));
784
+ if (done || Date.now() - t0 > 30000 || highlight !== h) break;
785
+ await new Promise((res) => setTimeout(res, 1000));
829
786
  }
787
+ if (highlight !== h) return;
788
+ h.state = 'checking'; ctx.setState({});
789
+ const r = await pmPost('/api/notes/highlight', { id: h.id, from: h.from, to: h.to });
790
+ if (highlight !== h) return;
791
+ if (r && r.ok) { h.result = r.highlight; h.checked = r.checked; h.jev = r.jev; h.state = 'done'; await loadNote(ctx, h.id); }
792
+ else { h.state = 'failed'; h.error = (r && r.error) || 'the highlight was not made'; }
793
+ hlOpen = true;
794
+ ctx.setState({});
830
795
  }
796
+ function closeHighlight(ctx) {
797
+ // closing while it still works only hides the pop-up; a finished one is let go
798
+ if (highlight && (highlight.state === 'done' || highlight.state === 'failed')) highlight = null;
799
+ hlOpen = false;
800
+ ctx.setState({});
801
+ }
802
+ async function removeHighlight(ctx, h) {
803
+ if (h.result) await pmPost('/api/notes/highlight-remove', { id: h.id, hid: h.result.id });
804
+ highlight = null; hlOpen = false;
805
+ await loadNote(ctx, h.id);
806
+ ctx.setState({});
807
+ }
808
+ /** Is a highlight being made, and is its result waiting to be seen? (the Record control's dot) */
809
+ export const highlightBusy = () => (highlight ? { state: highlight.state, open: hlOpen } : null);
810
+ export function openHighlight(ctx) { if (highlight) { hlOpen = true; ctx.setState({}); } }
831
811
 
832
- /** Render the highlight pop-up when a highlight is active. */
833
- function highlightPopup(ctx, n) {
834
- if (!highlight) return null;
835
-
836
- const steps = [
837
- { id: 'captured', label: 'Captured', done: highlight.state !== null && highlight.state !== 'capturing' },
838
- { id: 'listening', label: 'Listening', done: highlight.state !== 'listening' && highlight.state !== 'capturing' },
839
- { id: 'transcribed', label: 'Transcribed', done: highlight.state === 'labeling' || highlight.state === 'checking' || highlight.state === 'done' },
840
- { id: 'labeled', label: 'Labeled', done: highlight.state === 'checking' || highlight.state === 'done', dim: !highlight.label },
841
- { id: 'checked', label: 'Checked', done: highlight.state === 'done', dim: true },
842
- { id: 'done', label: 'Done', done: highlight.state === 'done' },
812
+ /** The highlight's pop-up (pm/note-dock.js draws it on every place), or null when there is none or it is closed. */
813
+ export function highlightPanel(ctx) {
814
+ const h = highlight;
815
+ if (!h || !hlOpen) return null;
816
+ const order = ['listening', 'transcribing', 'checking', 'done'];
817
+ const at = order.indexOf(h.state);
818
+ const step = (k, label, extra) => {
819
+ const i = order.indexOf(k);
820
+ const done = h.state === 'done' || (at > i && h.state !== 'failed');
821
+ const now = h.state === k;
822
+ return el('li', { class: 'nt-hl-step' + (done ? ' nt-hl-step--done' : now ? ' nt-hl-step--now' : ''), 'data-nt-hl-step': k },
823
+ el('span', { class: 'nt-hl-step__mk' }, icon(done ? 'check' : 'circle-dot')), el('span', null, label), extra || null);
824
+ };
825
+ const durs = el('div', { class: 'nt-hl-durs', role: 'group', 'aria-label': 'Listen after' }, HL_AFTER.map(([k, , label]) => button({ label, variant: highlightListenDuration === k ? 'secondary' : 'ghost', size: 'sm',
826
+ onClick: () => { highlightListenDuration = k; ctx.setState({}); }, attrs: { 'data-nt-duration': k, 'aria-pressed': highlightListenDuration === k ? 'true' : 'false' } })));
827
+ const body = [
828
+ el('div', { class: 'nt-hl-head' }, el('h3', { class: 'nt-hl-title' }, 'Highlight'), el('span', { class: 'nt-hl-at' }, fmtTime(h.from) + (h.to ? '–' + fmtTime(h.to) : ' →')),
829
+ h.result ? el('span', { class: 'nt-hl-tag' }, h.result.label) : null),
830
+ el('ol', { class: 'nt-hl-steps' },
831
+ el('li', { class: 'nt-hl-step nt-hl-step--done' }, el('span', { class: 'nt-hl-step__mk' }, icon('check')), el('span', null, 'Kept the last 30 seconds')),
832
+ step('listening', 'Listening', h.state === 'listening' ? el('span', { class: 'nt-hl-left', 'data-nt-hl-left': '' }, hlLeft(h)) : null),
833
+ step('transcribing', 'Transcribe the window'),
834
+ step('checking', 'Label it and check the repository'),
835
+ ),
836
+ h.state === 'listening' ? durs : null,
843
837
  ];
844
-
845
- const progress = el('div', { class: 'nt-highlight__progress' });
846
- steps.forEach((step) => {
847
- const stepEl = el('div', { class: 'nt-highlight__step' + (step.done ? ' nt-highlight__step--done' : '') + (step.dim ? ' nt-highlight__step--dim' : '') });
848
- const checkSpan = el('span', { class: 'nt-highlight__check' });
849
- const checkIcon = step.done ? icon('check', { 'aria-label': 'Done' }) : icon('circle-dot', { 'aria-label': 'Not done' });
850
- checkSpan.appendChild(checkIcon);
851
- stepEl.appendChild(checkSpan);
852
- stepEl.appendChild(el('span', { class: 'nt-highlight__label' }, step.label));
853
- if (step.id === 'listening' && highlight.state === 'listening') {
854
- const remaining = formatListenRemaining(highlight);
855
- stepEl.appendChild(el('span', { class: 'nt-highlight__time' }, remaining));
856
- }
857
- progress.appendChild(stepEl);
858
- });
859
-
860
- const durationOptions = el('div', { class: 'nt-highlight__durations' });
861
- ['30s', '1min', '2min'].forEach((dur) => {
862
- const opt = button({
863
- label: dur,
864
- variant: highlightListenDuration === dur ? 'primary' : 'secondary',
865
- onClick: () => {
866
- highlightListenDuration = dur;
867
- ctx.setState({});
868
- },
869
- attrs: { class: 'nt-highlight__duration-btn', 'data-nt-duration': dur }
870
- });
871
- durationOptions.appendChild(opt);
872
- });
873
-
874
- const actions = el('div', { class: 'nt-highlight__actions' });
875
- actions.appendChild(button({
876
- label: highlight.state === 'done' ? 'Accept' : 'Cancel',
877
- variant: 'primary',
878
- onClick: () => {
879
- if (highlight.state === 'done') acceptHighlight(ctx, n);
880
- else closeHighlight(ctx);
881
- },
882
- attrs: { 'data-nt-highlight-accept': '' }
883
- }));
884
-
885
- const popup = el('div', { class: 'nt-highlight__popup', 'data-nt-part': 'highlight-popup' },
886
- el('div', { class: 'nt-highlight__header' }, el('h3', {}, 'Recording Highlight')),
887
- progress,
888
- highlight.state === 'listening' ? durationOptions : null,
889
- actions
890
- );
891
-
892
- return popup;
838
+ if (h.state === 'done' && h.result) {
839
+ const c = h.result.check;
840
+ body.push(el('p', { class: 'nt-hl-text', 'data-nt-hl-text': '' }, h.result.text));
841
+ body.push(el('p', { class: 'nt-hint', 'data-nt-hl-check': c ? c.verdict : 'none' }, c && c.doc
842
+ ? (c.verdict === 'differs' ? 'Differs from ' : c.verdict === 'agrees' ? 'Agrees with ' : 'Closest file: ') + c.doc + (c.line ? ':' + c.line : '') + (c.by === 'jev' ? '' : ' (found on this machine; turn on the Jev check in Notes settings to compare)')
843
+ : h.checked ? 'No file in the repository talks about this.' : 'Not checked: the note is not linked to a repository.'));
844
+ }
845
+ if (h.state === 'failed') body.push(el('p', { class: 'nt-hint nt-hint--bad', 'data-nt-hl-failed': '' }, 'No highlight: ' + h.error + '.'));
846
+ const acts = [];
847
+ if (h.state === 'listening') acts.push(button({ label: 'Stop listening now', variant: 'outline', size: 'sm', onClick: () => { h.pressed = Date.now() - hlAfterMs(); hlTick(ctx); }, attrs: { 'data-nt-hl-stop': '' } }));
848
+ if (h.state === 'done') acts.push(button({ label: 'Done', variant: 'primary', size: 'sm', onClick: () => closeHighlight(ctx), attrs: { 'data-nt-hl-done': '' } }),
849
+ button({ label: 'Remove', variant: 'ghost', size: 'sm', onClick: () => removeHighlight(ctx, h), attrs: { 'data-nt-hl-remove': '' } }));
850
+ else acts.push(button({ label: 'Close', variant: 'ghost', size: 'sm', onClick: () => closeHighlight(ctx), attrs: { 'data-nt-hl-close': '' } }));
851
+ body.push(el('div', { class: 'nt-hl-acts' }, acts));
852
+ return el('section', { class: 'nt-hl', role: 'dialog', 'aria-label': 'Highlight', 'data-nt-part': 'highlight-popup', 'data-nt-hl': h.state }, body);
893
853
  }
894
854
 
895
855
  async function stopRecording(ctx) {
@@ -905,6 +865,17 @@ async function stopRecording(ctx) {
905
865
  ctx.setState({});
906
866
  }
907
867
 
868
+ /** Stop in the note: listening stops and the note leaves the Record control at the top right (pm/note-dock.js). */
869
+ async function stopAndFinish(ctx) {
870
+ const d = await import('./note-dock.js');
871
+ if (d.recordState().mode !== 'idle') await d.finish(); else await stopRecording(ctx);
872
+ }
873
+ /** Resume in the note: the Record control holds it again and it listens. */
874
+ async function resumeOn(n) {
875
+ const d = await import('./note-dock.js');
876
+ await d.recordNote(n.id, n.title);
877
+ }
878
+
908
879
  /* ── the two sheets: what to listen to, and settings ─────────────────────────────────────────── */
909
880
  async function chooseSources(ctx, n) {
910
881
  const eng = engine && engine.ws === wsOf(ctx) && engine.whisper ? engine : await loadEngine(ctx);
@@ -1193,22 +1164,22 @@ function header(ctx, n, ed, opts = {}) {
1193
1164
  el('span', { 'data-nt-sources': '' }, ' · ' + (live.state === 'starting' || live.state === 'checking' ? 'starting' : ['mic', 'meeting'].filter((s) => live.sources.includes(s)).join(' + '))),
1194
1165
  el('span', { class: 'nt-sr' }, ' Notes are being taken.'));
1195
1166
  // Highlight button (Run 11)
1196
- acts.push(iconButton({ icon: 'zap', ariaLabel: 'Highlight (H)', title: 'Highlight the last 30 seconds and listen forward', onClick: () => startHighlighting(ctx, n), attrs: { 'data-nt-highlight': '' } }));
1197
- acts.push(button({ label: 'Stop', variant: 'outline', size: 'sm', onClick: () => stopRecording(ctx), attrs: { 'data-nt-stop': '' } }));
1167
+ acts.push(iconButton({ icon: 'highlighter', ariaLabel: 'Highlight the last 30 seconds and what is said next', onClick: () => highlightNow(ctx, n.id), attrs: { 'data-nt-highlight': '' } }));
1168
+ acts.push(button({ label: 'Stop', variant: 'outline', size: 'sm', onClick: () => stopAndFinish(ctx), attrs: { 'data-nt-stop': '' } }));
1198
1169
  } else {
1199
1170
  const len = (n.transcript || []).length ? fmtTime(Math.max(...(n.transcript || []).map((x) => x.t || 0))) : '';
1200
1171
  status = el('span', { class: 'nt-live' }, el('b', null, 'Meeting note'), len ? el('span', null, len) : null);
1201
1172
  if (!n.private) acts.push((n.transcript || []).length
1202
- ? button({ label: 'Listen again', variant: 'ghost', size: 'sm', onClick: () => chooseSources(ctx, n), attrs: { 'data-nt-listen': 'again' } })
1173
+ ? button({ label: 'Resume', variant: 'primary', size: 'sm', onClick: () => resumeOn(n), attrs: { 'data-nt-listen': 'again' } })
1203
1174
  : button({ label: 'Listen', variant: 'primary', size: 'sm', onClick: () => chooseSources(ctx, n), attrs: { 'data-nt-listen': '' } }));
1204
1175
  }
1205
1176
  if (opts.onMinimize) acts.push(button({ label: 'Minimize', variant: 'ghost', size: 'sm', onClick: opts.onMinimize, attrs: { 'data-nt-minimize': '' } }));
1206
1177
  if (!opts.inline) acts.push(button({ label: 'All notes', variant: 'ghost', size: 'sm', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
1207
- const more = iconButton({ icon: 'ellipsis', ariaLabel: 'More', onClick: () => moreMenu(ctx, n, opts, more), attrs: { 'data-nt-more': '' } });
1178
+ const more = iconButton({ icon: 'ellipsis', ariaLabel: 'More', onClick: (e) => moreMenu(ctx, n, opts, (e && e.currentTarget) || more), attrs: { 'data-nt-more': '' } });
1208
1179
  acts.push(more);
1209
1180
  return el('header', { class: 'nt-header', 'data-nt-part': 'header' },
1210
1181
  el('div', { class: 'nt-bar' }, status, el('div', { class: 'nt-acts' }, acts)),
1211
- el('div', { class: 'nt-headl' }, ed.titleWrap, whenLine(ctx, n), peopleRow(n)));
1182
+ el('div', { class: 'nt-headl' }, own(ed.titleWrap), whenLine(ctx, n), peopleRow(n)));
1212
1183
  }
1213
1184
  function listPane(ctx) {
1214
1185
  const rows = (list && list.notes) || [];
@@ -1240,7 +1211,7 @@ function itemRow(ctx, n, key, title, x) {
1240
1211
  const actions = [];
1241
1212
  // Add "Make task" button for action items (Run 11)
1242
1213
  if (key === 'action') {
1243
- actions.push(iconButton({ icon: 'plus', ariaLabel: 'Make a task from this', onClick: () => makeTask(ctx, n, x.text), size: 'sm', attrs: { 'data-nt-make-task': '' } }));
1214
+ actions.push(iconButton({ icon: 'plus', ariaLabel: 'Make a task from this', onClick: (e) => import('./notes-task.js').then((m) => m.makeTask(n, x, { repo: ctx.state && ctx.state.product, btn: e && e.currentTarget })), size: 'sm', attrs: { 'data-nt-make-task': '' } }));
1244
1215
  }
1245
1216
  // Add remove buttons for unpinned items
1246
1217
  if (bullet) {
@@ -1421,7 +1392,7 @@ function myNotes(ctx, n, ed) {
1421
1392
  const t = liveMs(n);
1422
1393
  const parts = [el('h3', { class: 'nt-sec__h' }, 'Your notes')];
1423
1394
  if (items.length) parts.push(el('ul', { class: 'nt-items', 'data-nt-notelines': '' }, items));
1424
- if (!replaying) parts.push(el('div', { class: 'nt-addrow' }, el('span', { class: 'nt-at', 'data-nt-clock-now': '' }, t !== null ? fmtTime(t) : ''), ed.addWrap));
1395
+ if (!replaying) parts.push(el('div', { class: 'nt-addrow' }, el('span', { class: 'nt-at', 'data-nt-clock-now': '' }, t !== null ? fmtTime(t) : ''), own(ed.addWrap)));
1425
1396
  else parts.push(el('p', { class: 'nt-hint' }, 'Your notes, as the meeting plays. Pause to add more.'));
1426
1397
  const sp = speech && speech.id === n.id ? speech : null;
1427
1398
  const bar = [];
@@ -1553,11 +1524,6 @@ export function renderBoard(inner, ctx) {
1553
1524
  wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
1554
1525
  if (pl) queueMicrotask(paintNow);
1555
1526
  if (((n.agent && n.agent.started && !n.agent.markdown) || finalPending(n)) && !agentTimer) pollAgent(ctx, n.id);
1556
- // Add highlight pop-up if active
1557
- if (highlight && n.id === currentId) {
1558
- const hlPopup = highlightPopup(ctx, n);
1559
- if (hlPopup) wrap.appendChild(hlPopup);
1560
- }
1561
1527
  }
1562
1528
  inner.appendChild(wrap);
1563
1529
  keep();
@@ -1618,16 +1584,20 @@ export function renderInline(host, ctx, id, opts = {}) {
1618
1584
  if (!n) { loadNote(ctx, id); host.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); return true; }
1619
1585
  const keep = keepFocus();
1620
1586
  const ed = editorFor(ctx, n);
1587
+ inlineDraw = true;
1588
+ try { drawInline(ctx, n, ed, host, opts); } finally { inlineDraw = false; }
1589
+ keep();
1590
+ return true;
1591
+ }
1592
+ function drawInline(ctx, n, ed, host, opts) {
1621
1593
  const wrap = el('div', { class: 'nt-wrap nt-wrap--inline', 'data-nt-part': 'inline', 'data-nt-id': n.id });
1622
1594
  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));
1623
1595
  wrap.appendChild(header(ctx, n, ed, { inline: true, onMinimize: opts.onMinimize }));
1624
1596
  if (n.private) wrap.appendChild(el('p', { class: 'nt-hint', 'data-nt-private': '' }, 'This note is private: it is not recorded, and nothing from it goes to Jev or to any agent.'));
1625
1597
  const pl = !listening(n) ? playerFor(ctx, n) : null;
1626
- if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el));
1598
+ if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), own(pl.el)));
1627
1599
  wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n, { inline: true })));
1628
1600
  if (pl) queueMicrotask(paintNow);
1629
1601
  if (n.agent && n.agent.started && !n.agent.markdown && !agentTimer) pollAgent(ctx, n.id);
1630
1602
  host.appendChild(wrap);
1631
- keep();
1632
- return true;
1633
1603
  }
@@ -1,5 +1,5 @@
1
1
  /* lucide-sprite.js — GENERATED by workspace/third-party/build-lucide-sprite.js from lucide-static 1.52.0.
2
- * Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2, house, target, square-kanban, file-text, square-terminal, calendar, chart-column, users, git-merge, mail, message-square, ticket, ban.
2
+ * Do not edit by hand. The icons: grip-vertical, plus, check, square, square-check-big, x, undo-2, flag, bot, chevron-right, chevron-down, ellipsis, circle-alert, history, arrow-up, arrow-down, diamond, settings, search, refresh-cw, chevron-left, sun, moon, arrow-left, arrow-right, globe, columns-2, arrow-up-down, arrow-left-right, panel-top, panel-left, maximize-2, minimize-2, git-branch, clock, bold, italic, code, link, type, heading, list, list-checks, text-quote, file-code, git-compare, cloud, share-2, house, target, square-kanban, file-text, square-terminal, calendar, chart-column, users, git-merge, mail, message-square, ticket, ban, notebook-pen, mic, pause, play, highlighter, circle-dot.
3
3
  *
4
4
  * ISC License
5
5
  *
@@ -107,5 +107,11 @@ export const SPRITE = '<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true
107
107
  + '<symbol id="lucide-message-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" /></symbol>'
108
108
  + '<symbol id="lucide-ticket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z" /><path d="M13 5v2" /><path d="M13 17v2" /><path d="M13 11v2" /></symbol>'
109
109
  + '<symbol id="lucide-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M4.929 4.929 19.07 19.071" /></symbol>'
110
+ + '<symbol id="lucide-notebook-pen" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4" /><path d="M2 6h4" /><path d="M2 10h4" /><path d="M2 14h4" /><path d="M2 18h4" /><path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z" /></symbol>'
111
+ + '<symbol id="lucide-mic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19v3" /><path d="M19 10v2a7 7 0 0 1-14 0v-2" /><rect x="9" y="2" width="6" height="13" rx="3" /></symbol>'
112
+ + '<symbol id="lucide-pause" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="14" y="3" width="5" height="18" rx="1" /><rect x="5" y="3" width="5" height="18" rx="1" /></symbol>'
113
+ + '<symbol id="lucide-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" /></symbol>'
114
+ + '<symbol id="lucide-highlighter" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 11-6 6v3h9l3-3" /><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" /></symbol>'
115
+ + '<symbol id="lucide-circle-dot" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1" /><circle cx="12" cy="12" r="10" /></symbol>'
110
116
  + '</svg>';
111
- export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2","house","target","square-kanban","file-text","square-terminal","calendar","chart-column","users","git-merge","mail","message-square","ticket","ban"];
117
+ export const ICONS = ["grip-vertical","plus","check","square","square-check-big","x","undo-2","flag","bot","chevron-right","chevron-down","ellipsis","circle-alert","history","arrow-up","arrow-down","diamond","settings","search","refresh-cw","chevron-left","sun","moon","arrow-left","arrow-right","globe","columns-2","arrow-up-down","arrow-left-right","panel-top","panel-left","maximize-2","minimize-2","git-branch","clock","bold","italic","code","link","type","heading","list","list-checks","text-quote","file-code","git-compare","cloud","share-2","house","target","square-kanban","file-text","square-terminal","calendar","chart-column","users","git-merge","mail","message-square","ticket","ban","notebook-pen","mic","pause","play","highlighter","circle-dot"];