create-pmos 0.499.0 → 0.501.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.499.0",
3
+ "version": "0.501.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": {
@@ -32,6 +32,32 @@
32
32
  "audit"
33
33
  ],
34
34
  "pmosChanges": [
35
+ {
36
+ "v": "0.501.0",
37
+ "at": "2026-11-03T04:44:45.129Z",
38
+ "f": 3,
39
+ "prs": [
40
+ {
41
+ "n": 1184,
42
+ "t": "Home's list toggle always opens and closes the list",
43
+ "s": "Home's list toggle always opens and closes the list",
44
+ "k": "fix"
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "v": "0.500.0",
50
+ "at": "2026-11-03T04:17:03.191Z",
51
+ "f": 3,
52
+ "prs": [
53
+ {
54
+ "n": 1183,
55
+ "t": "Delete a captured meeting-note point from its edit card",
56
+ "s": "Delete a captured meeting-note point from its edit card",
57
+ "k": "new"
58
+ }
59
+ ]
60
+ },
35
61
  {
36
62
  "v": "0.499.0",
37
63
  "at": "2026-11-03T03:25:28.533Z",
@@ -397,31 +423,6 @@
397
423
  "k": "fix"
398
424
  }
399
425
  ]
400
- },
401
- {
402
- "v": "0.481.0",
403
- "at": "2026-11-01T15:21:25.176Z",
404
- "f": 3,
405
- "prs": [
406
- {
407
- "n": 1141,
408
- "t": "Batch 22: feedback fixes (held for one merge)",
409
- "s": "Batch 22: feedback fixes (held for one merge)"
410
- }
411
- ]
412
- },
413
- {
414
- "v": "0.480.0",
415
- "at": "2026-11-01T11:08:09.778Z",
416
- "f": 3,
417
- "prs": [
418
- {
419
- "n": 1139,
420
- "t": "Set up a custom report from chat in one press (held)",
421
- "s": "Set up a custom report from chat in one press (held)",
422
- "k": "fix"
423
- }
424
- ]
425
426
  }
426
427
  ]
427
428
  }
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@02cdbcfc
1
+ kernel-source: pmos@01e14514
2
2
  assembled: 2026-11-03
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-11-03T03:25:24Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T04:44:41Z"}
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-11-03T03:25:24Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T04:44:41Z"}
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-11-03T03:25:24Z"}
5
+ generated: {by: process:create-pmos, at: "2026-11-03T04:44:41Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,4 +1,30 @@
1
1
  [
2
+ {
3
+ "v": "0.501.0",
4
+ "at": "2026-11-03T04:44:45.129Z",
5
+ "f": 3,
6
+ "prs": [
7
+ {
8
+ "n": 1184,
9
+ "t": "Home's list toggle always opens and closes the list",
10
+ "s": "Home's list toggle always opens and closes the list",
11
+ "k": "fix"
12
+ }
13
+ ]
14
+ },
15
+ {
16
+ "v": "0.500.0",
17
+ "at": "2026-11-03T04:17:03.191Z",
18
+ "f": 3,
19
+ "prs": [
20
+ {
21
+ "n": 1183,
22
+ "t": "Delete a captured meeting-note point from its edit card",
23
+ "s": "Delete a captured meeting-note point from its edit card",
24
+ "k": "new"
25
+ }
26
+ ]
27
+ },
2
28
  {
3
29
  "v": "0.499.0",
4
30
  "at": "2026-11-03T03:25:28.533Z",
@@ -109,6 +109,14 @@ Press an initiative's name in the list, or open `?ini=<id>`. The list stays as i
109
109
  - A linked Jira ticket opens in place too. Its name and status stay Jira's ("In Progress in Jira · read 4 min ago"). Its dates can change, and it can be a milestone. It has no **Delete**.
110
110
  - **Delete a task.** Press the caret under a task's box (on a phone, press and hold the box) and press the trash at the end of the status picker. The task goes at once and the message shows **Undo**. In the Task Viewer, the trash is the last icon of the Status row, and **Delete** is the last entry of its ⋯ menu. You can delete every PMOS task, also a task that Claude planned in the breakdown and a task that a stage's To finish list made. A deleted To finish task does not come back when the stage is read again or when the initiative moves back to that stage. A Jira ticket has no Delete: change it in Jira. A task deleted in a draft leaves only the draft.
111
111
 
112
+ ## Show or hide the list
113
+
114
+ The button with the panel icon shows or hides the list on the left. It works in every layout: with a Viewer open beside the chat, with a Viewer on the full screen, with two Viewers, and in a narrow window.
115
+
116
+ - When a Viewer takes the full screen, the list starts as a narrow column of group icons. Press the panel icon at its top to show the full list. The Viewer stays open and becomes narrower.
117
+ - Press the panel icon in the list's header to hide the list again.
118
+ - PMOS keeps your choice on this computer. After you show or hide the list one time, a Viewer on the full screen does not change it.
119
+
112
120
  ## Back and forward
113
121
 
114
122
  The Viewer keeps one trail on Home, the Board and Insights.
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "Guide: notes"
4
4
  description: "How to take notes in a meeting: one note top to bottom, typed notes as timed lines, key points written each minute and labelled by Jev, delete a note; final notes written after Stop from the whole recording, with speakers and a quote for each item, decisions with their why and who, next steps with an open question, a discussion paragraph, and Wispr Flow's notes merged in and fetched by hand when the fetch fails; 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 for conflicts, capture highlights during the meeting, create tasks from next steps, correct and tag lines, see what changed in your notes, replay the meeting, see screenshots of the shared screen beside the transcript, link a note to its meeting and its initiative, and get a summary."
5
- timestamp: 2026-11-02
5
+ timestamp: 2026-11-03
6
6
  ---
7
7
 
8
8
  # Guide: notes
@@ -238,6 +238,12 @@ Press **Replay** beside the player (in Calendar › Notes, or on the meeting's c
238
238
 
239
239
  The page reads only records on this machine: the note, the file Focus kept for the meeting, and the screen shots.
240
240
 
241
+ ### Fix or delete a key point
242
+
243
+ In a meeting's Viewer, press a decision, a question or an action to open its edit card. Change the kind or the words, then press **Save**. PMOS remembers what you meant for the next notes, unless you press **Only fix this one**.
244
+
245
+ To delete the point, press the trash on the edit card. The point leaves at once, with no question first. **Undo** shows for 8 seconds and puts it back. A deleted point stays deleted after a reload. It is not in **Left out** and it is not counted. A delete does not teach PMOS anything, and nothing is sent outside PMOS.
246
+
241
247
  The replay page was part of the console, which is deleted. A replay link (for example `/c3.html?replay=n-abcd-1234`) now opens Home.
242
248
 
243
249
  ### Clean up the replay sound
@@ -2,8 +2,9 @@
2
2
  /* meeting-points.js — what the PM said about the key points of a meeting's Replay (initiative premium-uncovered-features, area meetings;
3
3
  * MtReplay.dc.html: the bulk bar's Confirm and Not right). The Replay keeps the points that were agreed, owned or left open; the PM ticks
4
4
  * some and confirms them, or marks them not right. A point marked not right leaves the kept list and goes to "Left out".
5
+ * A point the PM deleted (the trash on its edit card, batch 25 item 10) is gone: not kept, not Left out, not counted. Undo is verdict null.
5
6
  *
6
- * pointsOf(dir, note) → { [pointId]: 'confirmed' | 'wrong' } a later line wins; verdict null forgets the point
7
+ * pointsOf(dir, note) → { [pointId]: 'confirmed' | 'wrong' | 'deleted' } a later line wins; verdict null forgets the point
7
8
  * setPoints(dir, { note, ids, verdict }) → { ok, points } | { ok: false, status, error }
8
9
  *
9
10
  * A point's id is the id lib/meeting-replay.js gives it (d1, a2, q1). One line of <runtime>/meeting-points.jsonl per press, file mode,
@@ -14,7 +15,7 @@ const path = require('path');
14
15
  const FILE = 'meeting-points.jsonl';
15
16
  const NOTE_ID = /^[A-Za-z0-9_-]{1,80}$/;
16
17
  const POINT_ID = /^[A-Za-z0-9_-]{1,40}$/;
17
- const VERDICTS = ['confirmed', 'wrong'];
18
+ const VERDICTS = ['confirmed', 'wrong', 'deleted'];
18
19
 
19
20
  function readLines(file) {
20
21
  let text = '';
@@ -43,7 +44,7 @@ function setPoints(dir, body = {}) {
43
44
  const ids = Array.isArray(body.ids) ? [...new Set(body.ids.map(String))] : [];
44
45
  if (!ids.length || ids.length > 200 || !ids.every((x) => POINT_ID.test(x))) return { ok: false, status: 400, error: 'ids are the key points to mark, 1 to 200 of them' };
45
46
  const verdict = body.verdict == null ? null : String(body.verdict);
46
- if (verdict !== null && !VERDICTS.includes(verdict)) return { ok: false, status: 400, error: 'verdict is confirmed, wrong or null' };
47
+ if (verdict !== null && !VERDICTS.includes(verdict)) return { ok: false, status: 400, error: 'verdict is confirmed, wrong, deleted or null' };
47
48
  fs.mkdirSync(dir, { recursive: true });
48
49
  fs.appendFileSync(path.join(dir, FILE), JSON.stringify({ v: 1, note, ids, verdict, at: new Date().toISOString() }) + '\n');
49
50
  return { ok: true, points: pointsOf(dir, note) };
@@ -186,7 +186,7 @@ function reportSchedules(RN, range) {
186
186
  }
187
187
 
188
188
  /** The decisions and next steps of a note, each with confirmed: true when the PM confirmed or corrected it, else false. A corrected
189
- * point carries the PM's words. A point marked not right is left out. */
189
+ * point carries the PM's words. A point marked not right, or deleted, is left out. */
190
190
  function confirmedPoints(runtimeDir, noteId, decisions, actions) {
191
191
  let pts = {}, eds = {};
192
192
  try { pts = require('./meeting-points.js').pointsOf(runtimeDir, noteId); } catch { pts = {}; }
@@ -194,7 +194,7 @@ function confirmedPoints(runtimeDir, noteId, decisions, actions) {
194
194
  const out = { decisions: [], actions: [] };
195
195
  const put = (list, k) => (list || []).forEach((x, i) => {
196
196
  const id = k[0] + (i + 1), e = eds[id];
197
- if (pts[id] === 'wrong') return;
197
+ if (pts[id] === 'wrong' || pts[id] === 'deleted') return;
198
198
  const kind = e && ['decision', 'action'].includes(e.kind) ? e.kind : e ? null : k;
199
199
  if (!kind) return; // corrected into a question: not a decision or a step any more
200
200
  const row = { text: e ? e.text : x && x.text, confirmed: !!e || pts[id] === 'confirmed' };
@@ -2,8 +2,8 @@
2
2
  /* routes-meeting-points.js — the Replay's Confirm and Not right (initiative premium-uncovered-features, area meetings; lib/route-modules.js).
3
3
  * The data is kept by lib/meeting-points.js.
4
4
  *
5
- * GET /api/meeting-points?note=<id> → { ok, points: { [pointId]: 'confirmed' | 'wrong' } }
6
- * POST /api/meeting-points { note, ids: [pointId], verdict } → { ok, points } verdict confirmed | wrong | null (Undo)
5
+ * GET /api/meeting-points?note=<id> → { ok, points: { [pointId]: 'confirmed' | 'wrong' | 'deleted' } }
6
+ * POST /api/meeting-points { note, ids: [pointId], verdict } → { ok, points } verdict confirmed | wrong | deleted | null (Undo)
7
7
  * The GET also answers { edits, lessons }: the PM's corrections to the note's points and what PMOS learned (lib/meeting-learn.js).
8
8
  * POST /api/meeting-points/edit { note, id, kind, text, orig: { kind, text }, learn, items: [{ id, text }] } → { ok, edits, lesson, check }
9
9
  * a corrected point (XvAReplayEdit). learn false is "Only fix this one". check: the other points of the note that use the learned words.
@@ -58,7 +58,7 @@ function runOf(run) {
58
58
  failed: T.filter((t) => t.status === 'failed').slice(0, 3).map((t) => ({ id: t.id, title: clip(t.title, 200), error: clip(t.error, 200) })) };
59
59
  }
60
60
 
61
- /** deps: { events, items, run, notes: [note with final_notes], adapter, points: (noteId) → { [pointId]: 'confirmed' | 'wrong' }, seen: Set of seen ids }
61
+ /** deps: { events, items, run, notes: [note with final_notes], adapter, points: (noteId) → { [pointId]: 'confirmed' | 'wrong' | 'deleted' }, seen: Set of seen ids }
62
62
  * notes are today's non-private notes, read in full. A decision's point id is the Replay's (lib/meeting-replay.js): d1, d2, … in caughtOf order. */
63
63
  function build(query, deps = {}) {
64
64
  const q = query || {};
@@ -76,7 +76,7 @@ function build(query, deps = {}) {
76
76
  caughtOf(n).decision.forEach((x, i) => {
77
77
  const v = pts['d' + (i + 1)];
78
78
  if (v === 'confirmed') decisions.push({ kind: 'decision', text: clip(x.text, 200), sub: clip(n.title || (n.meeting && n.meeting.title) || 'A meeting', 80) });
79
- else if (v !== 'wrong') toConfirm += 1;
79
+ else if (v !== 'wrong' && v !== 'deleted') toConfirm += 1;
80
80
  });
81
81
  }
82
82
  const done = items.filter((i) => i && i.done && i.done_at && localDate(new Date(i.done_at)) === today);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.499.0",
3
+ "version": "0.501.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.499.0",
9
+ "version": "0.501.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.499.0",
3
+ "version": "0.501.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",
@@ -148,8 +148,11 @@ function layout() {
148
148
  if (man && !phone && !S.page) {
149
149
  const room = S.desk.clientWidth - 64 - (S.row.length - 1) * 22; // the desk less its padding and the gaps
150
150
  const list = document.getElementById('hmList'), lw = list ? list.offsetWidth || 316 : 0;
151
- const own = P.owned ? P.owned() : null; // Home owns its list's fold (batch 20 item 6): only full screen takes its room, and it comes back as it was
152
- const fold = own !== null ? own || (S.row.length === 1 && S.row[0] !== C) : !S.keepList && (!S.row.includes(C) || room - lw < S.row.length * PANEL);
151
+ // Home owns its list's fold (batch 20 item 6). A full Viewer folds it only as a starting default: never once the PM has pressed the fold or
152
+ // unfold (remembered), and not after the rail's "Show the list" in this layout (batch 25 item 12: the PM can always open or close it)
153
+ const own = P.owned ? P.owned() : null;
154
+ const startFold = !S.keepList && !(P.chosen && P.chosen()) && S.row.length === 1 && S.row[0] !== C;
155
+ const fold = own !== null ? own || startFold : !S.keepList && (!S.row.includes(C) || room - lw < S.row.length * PANEL);
153
156
  if (fold !== P.railOn()) P.rail(fold);
154
157
  }
155
158
  if (back) P.rest(); else P.relayout();
@@ -122,6 +122,8 @@ const G = { subs: [], card: null, railKey: null, rail: false, lw: 0, hold: 0, ti
122
122
  * a Viewer's width, its reset, open wide and its close never do. null: the Viewer folds the list to a rail as it needs (a page without one). */
123
123
  export function listFold(o) { G.own = o && typeof o.folded === 'function' ? o : null; }
124
124
  const owned = () => (G.own ? !!G.own.folded() : null);
125
+ /** true: the PM has pressed the list's fold or unfold, so a full Viewer keeps the list as the PM left it (no starting fold). */
126
+ const chosen = () => !!(G.own && typeof G.own.chosen === 'function' && G.own.chosen());
125
127
  const mk = (i) => ({ i, el: null, cur: null, back: null, stack: [], fwd: [], wide: false, w: 0, drill: false, over: false, used: 0, ph: false, via: '' });
126
128
  let V = mk(0);
127
129
  const ALL = [V];
@@ -279,7 +281,7 @@ function setRail(on) {
279
281
  bar.innerHTML = `<button type="button" class="cz-vw-ib" data-cz-vw-rail="show" aria-label="Show the list" title="Show the list">${svg(P.list, 18)}</button><div class="cz-vw-railtiles"></div>`;
280
282
  bar.addEventListener('click', (e) => {
281
283
  const b = e.target.closest('[data-cz-vw-rail]'); if (!b) return;
282
- if (b.dataset.czVwRail === 'show' && G.own) { if (G.own.show) G.own.show(); return; } // the page unfolds its own list
284
+ if (b.dataset.czVwRail === 'show' && G.own) { if (G.own.show) G.own.show(); if (G.rail) setRail(false); return; } // the page unfolds its own list; a layout that folded it never holds it folded
283
285
  if (b.dataset.czVwRail === 'show' && G.managed && ALL.some((x) => shown(x) && G.managed('v' + x.i))) { setRail(false); return; } // cozy-panels.js lays it out
284
286
  if (b.dataset.czVwRail === 'show') { V.wide = false; apply(Math.min(V.w, sideMax())); redraw(); return; }
285
287
  // a group's tile: the page's card of that group's rows, beside the tile (batch 18 item 14, FlA1); the rail and the Viewer stay
@@ -785,6 +787,6 @@ export const panels = {
785
787
  relayout() { ALL.filter(shown).forEach((x) => on(x, () => { apply(V.wide ? room().wideMax : V.w || room().def); redraw(); })); },
786
788
  /** Back beside the chat after the engine laid it out: the width its kind keeps, else the default. */
787
789
  rest() { ALL.filter(shown).forEach((x) => on(x, () => { const k = widthOf(V.cur.kind); V.wide = false; apply(k || room().def); redraw(); })); },
788
- rail(v) { setRail(!!v); }, railOn: () => G.rail, owned,
790
+ rail(v) { setRail(!!v); }, railOn: () => G.rail, owned, chosen,
789
791
  };
790
792
  if (typeof window !== 'undefined') window.czViewer = { open, close, back, forward, trail, replaceUrl, drill, setPresses, setChip, setCheck, isOpen, current, body, onChange, placeholder, retitle, czSummary, czSections, widthOf, setWidth, wide, railOn, railCard, railSync, keepWide, ICONS };
@@ -225,6 +225,7 @@
225
225
  .vzp-learn b:empty { display: none; }
226
226
  .vzp-ef { display: flex; align-items: center; gap: 10px; }
227
227
  .vzp-sp { flex: 1; }
228
+ .vzp-del:hover { color: var(--cz-ink); }
228
229
  .vzp-gbtn { min-height: 34px; padding: 0 12px !important; border-radius: 999px; font-size: var(--fs-3); font-weight: 700; }
229
230
 
230
231
  /* one Replay at a time: the phone's below 640px, else the desk's */
@@ -636,8 +636,12 @@ function drawList() {
636
636
  }
637
637
  /** The list's own fold (batch 20 item 6): kept on this computer, the one thing that folds or unfolds the list. With a Viewer open it is the rail. */
638
638
  function setFold(on) {
639
- S.folded = !!on; store.set('pmos.home.fold', on ? '1' : null); drawList();
640
- if (VW.isOpen() && VW.panels) VW.panels.relayout();
639
+ S.folded = !!on; store.set('pmos.home.fold', on ? '1' : '0'); drawList(); // '0': the PM opened it, and a full Viewer keeps it open from now on
640
+ if (VW.isOpen() && VW.panels) {
641
+ VW.panels.relayout();
642
+ // a Viewer the panels lay out (full, two side by side) does not ask the page again: the press folds or unfolds the list now, in every layout
643
+ if (VW.panels.railOn() !== (S.folded && !isPhone())) VW.panels.rail(S.folded && !isPhone());
644
+ }
641
645
  }
642
646
  /* ── the folded rail's card (batch 18 item 14, FlA1–FlA5) ──
643
647
  * With the Viewer wide, Home's list folds to a rail of group tiles (cozy-viewer.js). A tile's press opens a card beside it with that group's
@@ -1907,7 +1911,7 @@ async function todayPress(k, b) {
1907
1911
  }
1908
1912
  $('#hmDesk').insertAdjacentHTML('afterbegin', `<button type="button" class="hm-unfold" id="hmUnfold" aria-label="Show the list" title="Show the list">${SVG(I.fold, 16)}</button>`);
1909
1913
  $('#hmUnfold').onclick = () => setFold(false);
1910
- VW.listFold({ folded: () => S.folded && !isPhone(), show: () => setFold(false) });
1914
+ VW.listFold({ folded: () => S.folded && !isPhone(), chosen: () => store.get('pmos.home.fold') !== null, show: () => setFold(false) });
1911
1915
  function cta() {
1912
1916
  if (S.ini === 'product') { const C = H.productCard(S.D); if (C.sent) openUpdateViewer(); else { select('n:update'); } return; }
1913
1917
  const h = S.H || {};
@@ -63,7 +63,8 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
63
63
  const raw = Array.isArray(rep.raw) ? rep.raw : [];
64
64
  const rawOf = (k) => raw.find((x) => x && (x.id === k.id || (x.k === k.k && x.text === k.text))) || k;
65
65
  const lineAt = (t) => { if (t == null) return null; let best = null; for (const l of rep.lines) if (best === null || Math.abs(l.t - t) < Math.abs(best.t - t)) best = l; return best && Math.abs(best.t - t) <= NEAR ? best : null; };
66
- const all = rep.keys.map((k) => {
66
+ /* a point the PM deleted is gone: not kept, not Left out, not counted (its line still keeps the remark it was caught from out of Left out) */
67
+ const all = rep.keys.filter((k) => points[k.id] !== 'deleted').map((k) => {
67
68
  const r = rawOf(k), ed = edits[k.id] || null, state = ed ? '' : r.state || k.state || '', owner = clip(r.owner || k.owner, 40), line = lineAt(k.t);
68
69
  const quote = line ? clip(line.who, 30) + ': “' + clip(line.text, 90) + '”' : '';
69
70
  const kind0 = k.k === 'action' ? 'action' : k.k === 'question' ? 'question' : 'decision';
@@ -109,7 +110,7 @@ export function replayView(rep, { n = null, points = {}, edits = {}, lessons = [
109
110
  }
110
111
 
111
112
  /* ── html (pure) ── */
112
- const P = { play: 'M8 5v14l11-7z', pause: 'M8 5h3v14H8zM13 5h3v14h-3z', big: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7', chev: 'M10 7l5 5-5 5', chk: 'M5 12l5 5 9-10', task: 'M5 12l4 4L19 6', x: 'M6 6l12 12M18 6L6 18' };
113
+ const P = { play: 'M8 5v14l11-7z', pause: 'M8 5h3v14H8zM13 5h3v14h-3z', big: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7', chev: 'M10 7l5 5-5 5', chk: 'M5 12l5 5 9-10', task: 'M5 12l4 4L19 6', x: 'M6 6l12 12M18 6L6 18', bin: 'M5 7h14M10 7V4h4v3M7 7l1 13h8l1-13' };
113
114
  const ic = (k, w = 16, cls = '') => `<svg aria-hidden="true" width="${w}" height="${w}" viewBox="0 0 24 24"${cls ? ` class="${cls}"` : ''}><path d="${P[k]}"/></svg>`;
114
115
  const pct = (x) => Math.min(100, Math.max(0, x)).toFixed(2);
115
116
 
@@ -139,7 +140,8 @@ function pointRow(p, ui) {
139
140
  + (p.check ? `<small class="vzp-chk2">Check: you said “${esc(p.check.phrase)}” means ${esc(p.check.meaning)}</small>` : '') + '</span></button>';
140
141
  }
141
142
  const stop = (t) => (/[.!?…”]$/.test(t) ? t : t + '.');
142
- /** The correction (XvAReplayEdit): the kind, the old words struck, the new words, what PMOS will remember, Cancel and Save.
143
+ /** The correction (XvAReplayEdit): the kind, the old words struck, the new words, what PMOS will remember, the trash (Delete: no confirm,
144
+ * Undo in the message, like a task's Delete), Cancel and Save.
143
145
  * d = { kind, text, learn }; also = how many other points of this note use the learned words. */
144
146
  export function editHtml(p, d = {}, also = 0) {
145
147
  const kind = d.kind || p.kind, text = d.text == null ? p.text : d.text, learn = d.learn !== false;
@@ -152,7 +154,7 @@ export function editHtml(p, d = {}, also = 0) {
152
154
  + (changed ? `<p class="vzp-was"><s>${esc(KIND_OF[p.orig.kind] + ' · ' + p.orig.text)}</s></p>` : '')
153
155
  + `<textarea class="vzp-eta" data-a="etext" rows="3" aria-label="What was really said">${esc(text)}</textarea>`
154
156
  + `<div class="vzp-learn"><b>${learn && changed ? 'I will remember this.' : ''}</b> <span data-pl="lesson">${esc(lessonText)}</span>${changed ? ` <button type="button" class="vzp-link" data-a="elearn" aria-pressed="${!learn}">${learn ? 'Only fix this one' : 'Remember it'}</button>` : ''}</div>`
155
- + `<div class="vzp-ef"><span class="vzp-mut vzp-sm">${esc([p.time, p.by].filter(Boolean).join(' · '))}</span><span class="vzp-sp"></span><button type="button" class="vzp-gbtn" data-a="ecancel">Cancel</button><button type="button" class="vzp-dark" data-a="esave"${changed && text.trim() ? '' : ' disabled'}>Save</button></div></div>`;
157
+ + `<div class="vzp-ef"><span class="vzp-mut vzp-sm">${esc([p.time, p.by].filter(Boolean).join(' · '))}</span><span class="vzp-sp"></span><button type="button" class="vzp-ib vzp-del" data-a="edel" data-id="${esc(p.id)}" aria-label="Delete" title="Delete">${ic('bin', 16)}</button><button type="button" class="vzp-gbtn" data-a="ecancel">Cancel</button><button type="button" class="vzp-dark" data-a="esave"${changed && text.trim() ? '' : ' disabled'}>Save</button></div></div>`;
156
158
  }
157
159
  /** "In this part · <topic>": the kept points said in the stretch the replay is in. */
158
160
  export function partHtml(V, ui = {}) {
@@ -700,7 +700,7 @@ function keydown(me, e, body) {
700
700
  }
701
701
  const bodyEl = () => (MT && vCur() && vCur().id === MT.vid ? document.querySelector('#czViewer .cz-vw-body') : null); // null while a preview covers the meeting
702
702
  const repaint = (me) => { const b = bodyEl(); if (b && MT === me) paint(me, b); };
703
- function say(me, text, tone, undo) { me.ui.msg = { text, tone, undo }; repaint(me); clearTimeout(me.msgT); me.msgT = setTimeout(() => { if (me.ui.msg && me.ui.msg.text === text) { me.ui.msg = null; repaint(me); } }, 9000); }
703
+ function say(me, text, tone, undo, ms = 9000) { me.ui.msg = { text, tone, undo }; repaint(me); clearTimeout(me.msgT); me.msgT = setTimeout(() => { if (me.ui.msg && me.ui.msg.text === text) { me.ui.msg = null; repaint(me); } }, ms); }
704
704
  function tick(me) {
705
705
  if (MT !== me || !me.model) return;
706
706
  const b = bodyEl(); if (!b) return;
@@ -783,6 +783,7 @@ async function press(me, e) {
783
783
  case 'elearn': ui.draft = { ...ui.draft, learn: ui.draft.learn === false, text: draftText() }; return repaint(me);
784
784
  case 'ecancel': ui.edit = null; ui.draft = null; return repaint(me);
785
785
  case 'esave': return saveEdit(me);
786
+ case 'edel': return delPoint(me, t.dataset.id);
786
787
  /* the summary's quiet line and the lines under it */
787
788
  case 'm-about': if (ui.pop === 'about') { ui.pop = ''; return repaint(me); } return openAbout(me);
788
789
  case 'aboutpick': return saveAbout(me, t.dataset.v);
@@ -941,6 +942,15 @@ async function bulk(me, k, ids) {
941
942
  }
942
943
  return verdict(me, ids, k, k === 'confirmed' ? plural(ids.length, 'point') + ' confirmed.' : plural(ids.length, 'point') + ' moved to Left out.', k === 'wrong');
943
944
  }
945
+ /** Delete on the edit card: the point is gone, here and after a reload (verdict deleted). No confirm: Undo stays 8 s, as a task's Delete. */
946
+ async function delPoint(me, id) {
947
+ if (!me.n || !id) return;
948
+ const r = await pmPost('/api/meeting-points', { note: me.n.id, ids: [id], verdict: 'deleted' });
949
+ if (!r || !r.ok) return say(me, 'Not deleted: ' + ((r && r.error) || 'no answer'), 'bad');
950
+ me.ui.edit = null; me.ui.draft = null; if (me.ui.sel) me.ui.sel.delete(id);
951
+ me.D.points = r.points || {}; draw(me);
952
+ say(me, 'Point deleted.', '', async () => verdict(me, [id], null, 'Put back.'), 8000);
953
+ }
944
954
  async function verdict(me, ids, v, text, undo) {
945
955
  const r = await pmPost('/api/meeting-points', { note: me.n.id, ids, verdict: v });
946
956
  if (!r || !r.ok) return say(me, 'Not saved: ' + ((r && r.error) || 'no answer'), 'bad');