create-pmos 0.69.1 → 0.70.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.69.1",
3
+ "version": "0.70.0",
4
4
  "description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@bbb66c2
1
+ kernel-source: pmos@9bbf3fc
2
2
  assembled: 2026-10-07
3
3
  mode: vendor
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Eval calibration log"
4
4
  description: "Append-only record of every PM override of an evaluator verdict."
5
- generated: {by: process:create-pmos, at: "2026-10-07T03:27:10Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T03:58:26Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Health budgets"
4
4
  description: "Closed registry of health-budget anchors (D59), edited only by the PM."
5
- generated: {by: process:create-pmos, at: "2026-10-07T03:27:10Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T03:58:26Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -2,7 +2,7 @@
2
2
  type: Reference
3
3
  title: "Discoveries and friction log"
4
4
  description: "Append-only record of what runs discovered and where the process rubbed."
5
- generated: {by: process:create-pmos, at: "2026-10-07T03:27:10Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-07T03:58:26Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -55,7 +55,7 @@ The dock holds your terminals, documents, conversations, skills and web pages. I
55
55
  - Press **⋯** on a row to change the name, the initiative or the type of work.
56
56
  - Press **✕** on a row to end the session. The row asks "End “name”?". Press **End** or **Cancel**. When you end the last running session on its own working copy, the message offers **Remove working copy**. This removes the folder only. The branch stays. If the folder has unsaved work, the workspace keeps it and tells you why.
57
57
  - **+ Session** at the top of the dock, or **+** on an initiative, opens one sheet. Type a name, or leave it empty to name the session after the skill and the initiative. **For** is the initiative. **On** is where the session runs. **Automatic** uses the initiative's own working copy on `initiative/<id>` (made the first time), or the main tree when there is no initiative. You can also choose a branch that has a working copy, or **New branch…**. Then press **Blank session** (the CLI and model from Settings) or a skill. The session starts at once.
58
- - The icons at the bottom of the dock show the sessions as a strip on top, give the dock the whole window (Focus), and fold the dock. On top, a select chooses the group, and each session of the group is a button. **+** starts a session, and **✕** ends the session in front.
58
+ - The icons at the top of the session list, under **+ Session**, show the sessions as a strip on top, give the dock the whole window (Focus), and fold the dock. They stay in view when you scroll the list. On top, a select chooses the group, and each session of the group is a button. **+** starts a session, and **✕** ends the session in front.
59
59
  - To make the dock wider or narrower, drag its left edge. With the edge focused, the left and right arrow keys change the width in steps. Double-click the edge to go back to the default width. The width is kept in this browser.
60
60
  - **Focus** in the dock gives the dock the whole window. The place and the detail are hidden, and the dock stays full width on every place. Press Focus again, or ⌘⇧J (Ctrl-Shift-J), to show the place again. Folding the dock also ends Focus.
61
61
  - **Scratchpad** in the bar at the bottom of the dock opens and closes the scratchpad. The pad keeps one note for each terminal session, and one for the repository when no terminal is in front. **Send to terminal** types the note into the session in front and presses Enter. It is available only when a terminal is in front. **Copy** and **Clear** act on the note.
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: notes"
4
4
  description: "How to take notes in a meeting: listen to your microphone and the meeting on this machine, see decisions, actions and questions caught as they are said, check them against the repository, correct and tag lines, see what changed in your notes, replay the meeting, link a note to its meeting and its initiative, and get a summary."
5
- timestamp: 2026-10-06
5
+ timestamp: 2026-10-07
6
6
  ---
7
7
 
8
8
  # Guide: notes
@@ -95,7 +95,7 @@ Under the title, a note shows two lines: **Meeting** and **Initiative**.
95
95
 
96
96
  When a note has no initiative, the summary suggests one: the open initiative that shares 3 or more words with what was said, when no other initiative shares as many. A suggestion never replaces an initiative you chose.
97
97
 
98
- To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. The note is linked to the event, and to the event's initiative when it has one. An event that already has a note shows its summary and **Open notes** instead. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
98
+ To take notes for a calendar event, open the event's detail on the Agenda and press **Take notes**. The note is linked to the event, and to the event's initiative when it has one. The note opens inside the event's detail, in one column: type your notes, press **Listen**, and read the transcript there, without leaving the calendar. Press **Open in Notes** to see the same note on the full screen, and **Close** to fold it back to its summary. An event that already has a note shows its summary and **Open notes**, which opens the note in the detail. While one meeting is being listened to, another event's detail shows only its summary, so the meeting that is being heard is not stopped. See [Today](today.md) and [the initiative sheet](initiatives-screen.md).
99
99
 
100
100
  ## Dictate into your notes
101
101
 
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Today: what needs you"
4
4
  description: "The Agenda month and the calendar it remembers, the meeting notes of a meeting, the two-column day (your jobs and the agent's jobs), the eight kinds of card on Today, the one button on each card, the count, the empty state, and the pmos today command."
5
- timestamp: 2026-10-06
5
+ timestamp: 2026-10-07
6
6
  ---
7
7
 
8
8
  # Today: what needs you
@@ -50,7 +50,7 @@ Each day of the month answers three questions:
50
50
 
51
51
  The switch above the month shows one answer at a time: **Overview** (all three), **Full** (the time in meetings), **Key** (the meeting that matters), **Prep** (how ready you are) or **Free** (the free blocks of 90 minutes or more).
52
52
 
53
- A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one. When the meeting has a note, the detail shows its summary and **Open notes**, and the meeting's row shows **notes**. See [Notes](notes.md).
53
+ A meeting's detail ends with **Meeting notes**. Press **Take notes** to start a note linked to the meeting, and to its initiative when the brief names one. The note runs in the detail: you type and listen there. **Open in Notes** shows it on the full screen. When the meeting has a note, the detail shows its summary and **Open notes**, which opens it in the detail again, and the meeting's row shows **notes**. See [Notes](notes.md).
54
54
 
55
55
  The memory of a meeting says what kind of meeting it is (one-to-one, onboarding or group), how many times it was held, how many are to come, and why it matters.
56
56
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.69.1",
3
+ "version": "0.70.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.69.1",
3
+ "version": "0.70.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -2500,13 +2500,21 @@
2500
2500
  align-self: center;
2501
2501
  padding: 0 6px;
2502
2502
  }
2503
- .fr-ds-foot {
2503
+ .fr-ds-top {
2504
+ position: sticky;
2505
+ top: 0;
2506
+ z-index: 1;
2504
2507
  display: flex;
2508
+ flex-wrap: wrap;
2505
2509
  justify-content: flex-end;
2510
+ align-items: center;
2506
2511
  gap: 4px;
2507
- margin-top: auto;
2508
- padding-top: 8px;
2509
- border-top: 1px solid var(--border);
2512
+ padding-bottom: 6px;
2513
+ background: var(--background);
2514
+ }
2515
+ /* + Session keeps its whole label on its own row; the controls sit in a row under it, right-aligned, still above the list */
2516
+ .fr-ds-top .fr-session {
2517
+ flex: 1 0 100%;
2510
2518
  }
2511
2519
  .fr-ds-note {
2512
2520
  margin: 0;
@@ -6027,6 +6035,30 @@
6027
6035
  font-size: 12px;
6028
6036
  color: var(--muted-foreground);
6029
6037
  }
6038
+
6039
+ /* meeting-notes Run 5: the note runs inside a calendar event's detail, in one column */
6040
+ .nt-inhost__head {
6041
+ display: flex;
6042
+ align-items: center;
6043
+ justify-content: space-between;
6044
+ gap: 8px;
6045
+ }
6046
+ .nt-wrap--inline .nt-body2 {
6047
+ grid-template-columns: 1fr;
6048
+ }
6049
+ .nt-wrap--inline .nt-notecol {
6050
+ padding-right: 0;
6051
+ }
6052
+ .nt-wrap--inline .nt-trans {
6053
+ padding-left: 0;
6054
+ padding-top: 16px;
6055
+ border-left: 0;
6056
+ border-top: 1px solid var(--border);
6057
+ }
6058
+ .nt-wrap--inline .nt-lines {
6059
+ max-height: 320px;
6060
+ }
6061
+
6030
6062
  /* -- layouts · .lg- the legacy and fixture shells (Run 3, slice 3D) -- */
6031
6063
  /* The legacy and fixture cockpit's two columns: the list head (a stack) and the rail's tile column. */
6032
6064
  .lg-stack {
@@ -2,8 +2,9 @@
2
2
  *
3
3
  * notesFor(e) → the note rows linked to a calendar event (same id, or same start minute and title)
4
4
  * notesOfInitiative(id) → the note rows linked to an initiative, newest first
5
- * notesBlock(e, { ws, repo, initiative }) → the "Meeting notes" block of an event's detail: the summary and Open notes,
6
- * or Take notes; Take notes on an event that already has a note opens that note
5
+ * notesBlock(e, { ws, repo, initiative }) → the "Meeting notes" block of an event's detail. Take notes makes a note linked
6
+ * to the event and runs it right there (typing and listening, pm/view-notes.js renderInline);
7
+ * an event that has a note shows its summary and Open notes, which opens it there too
7
8
  * noteRows(rows, { onOpen }) → the rows of an initiative's Notes tab; onOpen runs before the note opens
8
9
  * openNote(id) → opens the Notes tab on that note
9
10
  * onNotes(fn) · refreshNotes() → redraw when the list is read
@@ -14,10 +15,14 @@
14
15
  import { el } from './dom.js';
15
16
  import { button } from './primitives.js';
16
17
  import { pmGet, pmPost } from './http.js';
18
+ import { state, setState, FIXTURE } from './state.js';
17
19
 
18
20
  const READ_MS = 15000; // the list is read again at most this often while a calendar or an initiative is drawn
19
21
  let rows = null; let readAt = 0; let reading = null; let failed = false;
20
22
  const subs = new Set();
23
+ let shown = null; // the note open in an event's detail (Take notes or Open notes); the listening note is shown too
24
+ let V = null; // pm/view-notes.js, read the first time a note is shown in a detail
25
+ const viewCtx = { state, setState, FIXTURE };
21
26
 
22
27
  export function onNotes(fn) { subs.add(fn); return () => subs.delete(fn); }
23
28
  export function refreshNotes() {
@@ -64,8 +69,14 @@ async function takeNotes(e, { ws, repo, initiative } = {}, btn) {
64
69
  const r = await pmPost('/api/notes/new', { meeting: meetingOf(e), ...(ini ? { initiative: ini } : {}) });
65
70
  if (btn) btn.disabled = false;
66
71
  if (!r || !r.ok) { const { toast } = await import('./primitives.js'); toast({ text: (r && r.error) || 'The note was not made.', tone: 'destructive' }); return; }
67
- refreshNotes();
68
- openNote(r.note.id);
72
+ shown = r.note.id;
73
+ await refreshNotes();
74
+ setState({});
75
+ }
76
+ function show(id) { shown = id; setState({}); }
77
+ function view() {
78
+ if (!V) import('./view-notes.js').then((m) => { V = m; setState({}); });
79
+ return V;
69
80
  }
70
81
 
71
82
  const dayOf = (iso) => { try { return new Date(iso).toLocaleDateString([], { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return ''; } };
@@ -77,13 +88,24 @@ export function notesBlock(e, opts = {}) {
77
88
  const ended = !!e.ended;
78
89
  if (mine.length) {
79
90
  const n = mine[0];
91
+ const v = view();
92
+ const live = v && v.listeningId() === n.id;
93
+ if (v && (shown === n.id || live)) {
94
+ const host = el('div', { class: 'td-day__notes nt-inhost', 'data-notes-block': 'inline', 'data-notes-id': n.id });
95
+ const head = el('div', { class: 'nt-inhost__head' }, el('h4', null, 'Meeting notes'),
96
+ live ? null : button({ label: 'Close', variant: 'ghost', size: 'sm', attrs: { 'data-notes-close': n.id }, onClick: () => show(null) }));
97
+ host.appendChild(head);
98
+ if (v.renderInline(host, viewCtx, n.id)) return host;
99
+ }
80
100
  const lines = n.summary_lines || [];
101
+ const busy = v && v.listeningId() && v.listeningId() !== n.id;
81
102
  return el('div', { class: 'td-day__notes', 'data-notes-block': 'linked', 'data-notes-id': n.id },
82
103
  el('h4', null, 'Meeting notes'),
83
104
  n.private ? el('p', { class: 'td-day__note' }, 'This note is private.')
84
105
  : lines.length ? el('ul', { class: 'nt-lsum' }, ...lines.map((l) => el('li', null, l)))
85
106
  : el('p', { class: 'td-day__note' }, n.segments ? 'No summary yet. Open the note and press Summarize.' : 'No summary yet.'),
86
- el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => openNote(n.id) })));
107
+ busy ? el('p', { class: 'td-day__note', 'data-notes-busy': '' }, 'Another meeting is being listened to. Stop it first to open this note here.') : null,
108
+ busy ? null : el('div', { class: 'td-day__acts' }, button({ label: 'Open notes', size: 'sm', attrs: { 'data-notes-open': n.id }, onClick: () => show(n.id) })));
87
109
  }
88
110
  if (!notesKnown()) return null;
89
111
  const take = button({ label: 'Take notes', variant: ended ? undefined : 'primary', size: 'sm', attrs: { 'data-notes-take': e.id || '' }, onClick: () => takeNotes(e, opts, take) });
@@ -1139,6 +1139,7 @@ let dsMod = null;
1139
1139
  if (!FIXTURE) import('./dock-sessions.js').then((m) => { dsMod = m; if (inFrame()) setState({}); }).catch(() => { dsMod = null; });
1140
1140
  let dockParts = null;
1141
1141
  let dockOrient = null;
1142
+ let dockHead = null; // the side list's head: + Session and the dock's controls, one node across paints
1142
1143
  function frameDock() {
1143
1144
  if (!dockMod || !inFrame()) return null;
1144
1145
  const repo = currentProduct();
@@ -1429,16 +1430,18 @@ export function renderTabs(host) {
1429
1430
  * the frame's dock (split stays only to undo one already made). */
1430
1431
  if (ck && ds) {
1431
1432
  const others = notListed(dsMod.listedSessionIds(ds));
1433
+ // The dock's own controls (tabs on top, Focus, Minimize) sit beside + Session at the head of the list, held in view while
1434
+ // the list scrolls: at the foot they were below every session, and a long list put them off screen.
1432
1435
  setChildren(tabBar, vertical ? [
1433
- fr.session,
1436
+ dockHead || (dockHead = el('div', { 'data-tabs': 'vcontrols', class: 'fr-ds-top' })),
1434
1437
  ...dsMod.railNodes(ds, others.map((t) => dockTabRow(t, (act && act.id === t.id) || (splitTab && splitTab.id === t.id)))),
1435
- el('div', { 'data-tabs': 'vcontrols', class: 'fr-ds-foot' }, state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold),
1436
1438
  ] : [
1437
1439
  ...dsMod.stripNodes(ds, others.map((t) => dockTabChip(t, (act && act.id === t.id) || (splitTab && splitTab.id === t.id)))),
1438
1440
  state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold,
1439
1441
  ]);
1440
1442
  }
1441
1443
 
1444
+ if (ck && ds && vertical) setChildren(dockHead, [fr.session, state.splitId ? splitBtn : null, fr.orient, fr.focus, fr.fold]);
1442
1445
  if (tabBar.scrollLeft !== keepX) tabBar.scrollLeft = keepX;
1443
1446
  if (tabBar.scrollTop !== keepY) tabBar.scrollTop = keepY;
1444
1447
  // When the tab in front changes, the bar scrolls just enough to show it, so a tab opened at the far end of a crowded strip or a
@@ -1,4 +1,4 @@
1
- /* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 4).
1
+ /* pm/view-notes.js — the Notes view tab: one meeting on one screen (meeting-notes, Runs 1 to 5).
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;
@@ -26,7 +26,10 @@
26
26
  *
27
27
  * MEETING AND INITIATIVE (Run 4). Under the title, the note says which meeting it is (a calendar event, or not on the
28
28
  * calendar) and which initiative it belongs to, and how that was set: from the calendar event, suggested from what was said,
29
- * or chosen by the PM. Change opens a sheet for each. A note taken from an event's detail (pm/notes-links.js) opens here.
29
+ * or chosen by the PM. Change opens a sheet for each.
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.
30
33
  *
31
34
  * Reads and writes (lib/routes-notes.js). Notes live in the per-user runtime directory, never in a repository:
32
35
  * GET /api/notes · /api/notes/one?id=… · /api/notes/engine?ws=… · /api/notes/recording?id=… · /api/people?ws=…
@@ -64,6 +67,7 @@ let busy = '';
64
67
  let player = null; // the replay: { id, bytes, el, playing }, kept across redraws so playback goes on
65
68
  let people = null; // { ws, names } — the repository's people, offered in the line editor
66
69
  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
67
71
  const LABELS = [['decision', 'Decision'], ['action', 'Action'], ['question', 'Question'], ['fact', 'Fact'], ['topic', 'Topic'], ['statement', 'Statement'], ['small talk', 'Small talk']];
68
72
 
69
73
  const nowClock = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch { return ''; } };
@@ -873,7 +877,7 @@ function listening(n) {
873
877
  if (speech && speech.id === n.id && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking')) return { kind: 'browser', state: speech.state, sources: ['mic'], started: speech.started };
874
878
  return null;
875
879
  }
876
- function header(ctx, n, ed) {
880
+ function header(ctx, n, ed, opts = {}) {
877
881
  const live = listening(n);
878
882
  const when = el('span', { class: 'nt-when' }, dayOf(n.created_at) + ', ' + nowClock(n.created_at));
879
883
  const nv = (n.versions || []).length;
@@ -890,11 +894,12 @@ function header(ctx, n, ed) {
890
894
  } else if ((n.transcript || []).length) {
891
895
  acts.push(button({ label: 'Copy as Markdown', variant: 'ghost', onClick: () => copyMarkdown(ctx, n), attrs: { 'data-nt-copy': '' } }));
892
896
  if (!n.private) acts.push(button({ label: 'Listen again', variant: 'ghost', onClick: () => chooseSources(ctx, n), attrs: { 'data-nt-listen': 'again' } }));
893
- acts.push(button({ label: busy === 'new' ? 'Creating …' : 'New meeting', disabled: busy === 'new', onClick: () => newNote(ctx), attrs: { 'data-nt-new': '' } }));
897
+ if (!opts.inline) acts.push(button({ label: busy === 'new' ? 'Creating …' : 'New meeting', disabled: busy === 'new', onClick: () => newNote(ctx), attrs: { 'data-nt-new': '' } }));
894
898
  } else if (!n.private) {
895
899
  acts.push(button({ label: 'Listen', variant: 'primary', onClick: () => chooseSources(ctx, n), attrs: { 'data-nt-listen': '' } }));
896
900
  }
897
- acts.push(button({ label: 'All notes', variant: 'ghost', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
901
+ if (opts.inline) acts.push(button({ label: 'Open in Notes', variant: 'ghost', onClick: () => openFull(ctx, n.id), attrs: { 'data-nt-full': '' } }));
902
+ else acts.push(button({ label: 'All notes', variant: 'ghost', onClick: () => { showList = !showList; ctx.setState({}); if (showList) loadList(ctx); }, attrs: { 'data-nt-all': '', 'aria-expanded': showList ? 'true' : 'false' } }));
898
903
  acts.push(iconButton({ icon: 'settings', ariaLabel: 'Settings', onClick: () => openSettings(ctx, n), attrs: { 'data-nt-settings': '' } }));
899
904
  return el('header', { class: 'nt-header', 'data-nt-part': 'header' }, el('div', { class: 'nt-headwrap' }, left, linksLine(ctx, n)), el('div', { class: 'nt-acts' }, acts));
900
905
  }
@@ -1009,8 +1014,7 @@ export function renderBoard(inner, ctx) {
1009
1014
  if (pendingOpen) { const id = pendingOpen; pendingOpen = null; if (id !== currentId) { selectNote(ctx, id); loadList(ctx); } }
1010
1015
  if (!engine || engine.ws !== wsOf(ctx)) { if (!engine || !engine.loading || engine.ws !== wsOf(ctx)) loadEngine(ctx); }
1011
1016
 
1012
- const focus = typeof document !== 'undefined' && editor && (document.activeElement === editor.body ? 'body' : document.activeElement === editor.title ? 'title' : null);
1013
- const caret = focus ? [editor[focus].selectionStart, editor[focus].selectionEnd] : null;
1017
+ const keep = keepFocus();
1014
1018
 
1015
1019
  const wrap = el('div', { class: 'nt-wrap', 'data-nt-part': 'view' });
1016
1020
  if (list && list.state === 'error') wrap.appendChild(el('div', { class: 'nt-banner nt-banner--bad', role: 'alert' }, list.error));
@@ -1037,7 +1041,53 @@ export function renderBoard(inner, ctx) {
1037
1041
  requestAnimationFrame(fitBody);
1038
1042
  }
1039
1043
  inner.appendChild(wrap);
1040
- // the redraw moves the fields, which blurs them; focus comes back in a microtask, after the frame is attached and before the
1041
- // next key event, so no keystroke lands while the field is detached
1042
- if (caret) queueMicrotask(() => { try { const t = editor[focus]; if (t.isConnected) { t.focus(); t.setSelectionRange(caret[0], caret[1]); } } catch { /* the field is gone */ } });
1044
+ keep();
1045
+ }
1046
+
1047
+ /** Which field has the focus, and a function that gives it back after the redraw. The redraw moves the fields, which blurs
1048
+ * them; focus comes back in a microtask, after the frame is attached and before the next key event, so no keystroke lands
1049
+ * while the field is detached. */
1050
+ function keepFocus() {
1051
+ const focus = typeof document !== 'undefined' && editor && (document.activeElement === editor.body ? 'body' : document.activeElement === editor.title ? 'title' : null);
1052
+ if (!focus) return () => {};
1053
+ const caret = [editor[focus].selectionStart, editor[focus].selectionEnd];
1054
+ return () => queueMicrotask(() => { try { const t = editor[focus]; if (t.isConnected) { t.focus(); t.setSelectionRange(caret[0], caret[1]); } } catch { /* the field is gone */ } });
1055
+ }
1056
+ async function openFull(ctx, id) {
1057
+ await flushSave(ctx);
1058
+ const c = await import('./panel-cockpit.js');
1059
+ pendingOpen = id;
1060
+ c.openView('notes');
1061
+ }
1062
+
1063
+ /** The note that is listening now, or null. */
1064
+ export const listeningId = () => (audio && audio.id) || (speech && speech.mode === 'record' && (speech.state === 'on' || speech.state === 'checking') ? speech.id : null);
1065
+
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. */
1069
+ export function renderInline(host, ctx, id) {
1070
+ if (ctx.FIXTURE) return false;
1071
+ const other = listeningId();
1072
+ if (other && other !== id) return false;
1073
+ ensureComponentsCss();
1074
+ if (!engine || engine.ws !== wsOf(ctx)) { if (!engine || !engine.loading || engine.ws !== wsOf(ctx)) loadEngine(ctx); }
1075
+ if (id !== currentId) { if (!inlineSwitch) { inlineSwitch = selectNote(ctx, id).finally(() => { inlineSwitch = null; }); } host.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); return true; }
1076
+ const n = cache.get(id);
1077
+ if (!n) { loadNote(ctx, id); host.appendChild(el('p', { class: 'nt-hint' }, 'reading the note …')); return true; }
1078
+ const keep = keepFocus();
1079
+ const ed = editorFor(ctx, n);
1080
+ const wrap = el('div', { class: 'nt-wrap nt-wrap--inline', 'data-nt-part': 'inline', 'data-nt-id': n.id });
1081
+ 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));
1082
+ wrap.appendChild(header(ctx, n, ed, { inline: true }));
1083
+ 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.'));
1084
+ const pl = !listening(n) ? playerFor(ctx, n) : null;
1085
+ if (pl) wrap.appendChild(el('div', { class: 'nt-replay', 'data-nt-part': 'replay' }, el('span', { class: 'nt-sec__h' }, 'Replay'), pl.el));
1086
+ wrap.appendChild(el('div', { class: 'nt-body2' }, noteColumn(ctx, n, ed), transcriptColumn(ctx, n)));
1087
+ if (pl) queueMicrotask(paintNow);
1088
+ if (n.agent && n.agent.started && !n.agent.markdown && !agentTimer) pollAgent(ctx, n.id);
1089
+ requestAnimationFrame(fitBody);
1090
+ host.appendChild(wrap);
1091
+ keep();
1092
+ return true;
1043
1093
  }