create-pmos 0.180.0 → 0.182.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.180.0",
3
+ "version": "0.182.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@5ed98ca
1
+ kernel-source: pmos@1400593
2
2
  assembled: 2026-10-13
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-13T02:13:11Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T03:18:01Z"}
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-13T02:13:11Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T03:18:01Z"}
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-13T02:13:11Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T03:18:01Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -13,7 +13,7 @@
13
13
  * work a Work task with a follow-up shows the agent line; Review opens the sheet with the draft and "the agent sent nothing";
14
14
  * Keep records the keep; a task with a next-step shows Keep and Drop in the line
15
15
  * setting Board settings has the Agent setting; pressing Shadow posts /api/driver/mode and the ledger holds it
16
- * briefing Needs you shows the agent's two kinds of row and a press opens the initiative
16
+ * briefing Needs you shows the agent's two kinds of row, a long title wraps inside the row, and a press opens the initiative
17
17
  * phone at 400 wide the strip has no horizontal scroll
18
18
  * Every wait polls the page. The page must raise no script error.
19
19
  */
@@ -150,6 +150,11 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
150
150
  await sel('[data-drv-need]');
151
151
  const needs = await st(() => [...document.querySelectorAll('[data-drv-need]')].map((r) => r.dataset.drvNeed + ':' + r.textContent.replace(/\s+/g, ' ')));
152
152
  ok('B1 Needs you shows the agent\'s rows: the drafts left on the initiative, and the initiative waiting on you', needs.some((t) => /^driver-drafts:Agent has 1 draft on Partner API onboarding/.test(t)) && needs.some((t) => /^driver-blocked:.*Partner API onboarding is waiting on you/.test(t)), needs.join(' | '));
153
+ const fit = await st(() => { const r = [...document.querySelectorAll('[data-drv-need="driver-drafts"]')].find((x) => x.offsetParent); const t = r.querySelector('.tt'); const was = t.textContent;
154
+ t.textContent = 'Agent has 2 drafts on I need to create recommendations for Scarlet product strategy, including whether to change it with something else entirely or keep it';
155
+ const nb = r.querySelector('.nb'); const out = { ws: getComputedStyle(t).whiteSpace, over: nb.scrollWidth - nb.clientWidth, right: t.getBoundingClientRect().right - r.getBoundingClientRect().right };
156
+ t.textContent = was; return out; });
157
+ ok('B3 a long title wraps inside the row: the title button is not nowrap and nothing runs past the card', fit.ws !== 'nowrap' && fit.over <= 1 && fit.right <= 1, JSON.stringify(fit));
153
158
  await page.click('[data-drv-need="driver-drafts"] .tt');
154
159
  await until(() => page.$('[data-drv="strip"]'));
155
160
  ok('B2 a press on the row opens the initiative with its Agent strip', !!(await page.$('[data-drv="strip"]')));
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- /* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8).
3
+ /* c3-phone-test.js — the phone companion (initiative c3-phone-companion; PRD done-criteria 1 to 6 and 8): c3-phone.js, its sheets c3-phone-detail.js and its demo notes c3-demo-phone.js.
4
4
  * A real server and a real Chromium drive /c3.html in demo mode (clock frozen at Thu 8 Oct 2026 08:12; ?at=HH:MM moves the companion's clock).
5
5
  *
6
6
  * phone at 400 wide the companion shows and the desktop grid does not
7
7
  * moments 08:12 morning; 10:10 before Checkout sync; 10:45 in the meeting with the Record offer
8
8
  * done Done on the waiting item records a write to /api/pm-items/done
9
9
  * tell text sent from the ask bar records a capture and clears the bar
10
- * record the red button shows the recording line; Stop brings the bar back
10
+ * brief the morning card counts every row the desktop Briefing counts; See all lists them; a row opens its details
11
+ * meeting a Later today row and the morning card open the meeting's sheet with its brief (GET /api/meeting-focus)
12
+ * record the red button shows the recording line and the notes being taken (just heard, decisions); Stop brings the bar back
11
13
  * debrief 11:25 shows the decisions and actions of the recorded Checkout sync; a meeting with no recording asks you to tell PMOS
12
14
  * desk Full console shows the desktop grid and survives a reload; More > Phone companion brings it back
13
15
  * desktop at 1280 wide no companion is drawn
16
+ * 620 the companion shows up to 640 wide, where the phone styling starts
14
17
  * No script error may be raised.
15
18
  */
16
19
  const path = require('path');
@@ -76,6 +79,31 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
76
79
  ok('done: Done records a write to /api/pm-items/done', await waitFor(page, (n) => ((window.__c3demoWrites || []).filter((w) => String(w.path || w.url || '').startsWith('/api/pm-items/done')).length) > n, before, 5000));
77
80
  }
78
81
 
82
+ /* ── the brief: everything the desktop Briefing counts waits here, and each row opens ── */
83
+ ok('brief: the morning card counts what waits on you', await waitFor(page, () => /wait on you|waits on you/.test(document.querySelector('[data-c3ph-moment="morning"]')?.textContent || ''), null, 8000));
84
+ ok('brief: the briefing needs are in the waiting list', await waitFor(page, () => !!document.querySelector('[data-c3ph-all-open]'), null, 8000));
85
+ if (await page.$('[data-c3ph-all-open]')) {
86
+ await page.click('[data-c3ph-all-open]');
87
+ const n = await waitFor(page, () => document.querySelectorAll('[data-c3ph-all] [data-c3ph-all-row]').length > 1, null, 5000);
88
+ ok('brief: See all lists every row that waits', n);
89
+ const nonPm = await page.evaluate(() => [...document.querySelectorAll('[data-c3ph-all-row]')].length);
90
+ await page.click('[data-c3ph-all-row] .pmc-row__open');
91
+ ok('brief: a row opens its details', await waitFor(page, () => !!document.querySelector('[data-c3ph-need]'), null, 5000), nonPm);
92
+ await page.keyboard.press('Escape');
93
+ }
94
+
95
+ /* ── a meeting opens its brief ── */
96
+ const laterRow = await waitFor(page, () => !!document.querySelector('[data-c3ph-later-row] .pmc-row__open'), null, 8000);
97
+ ok('meeting: the Later today rows can be opened', laterRow);
98
+ if (laterRow) {
99
+ await page.click('[data-c3ph-later-row] .pmc-row__open');
100
+ ok('meeting: the sheet shows the meeting', await waitFor(page, () => !!document.querySelector('[data-c3ph-meeting]'), null, 5000));
101
+ ok('meeting: the sheet reads the brief of the meeting', await waitFor(page, () => document.querySelector('[data-c3ph-brief]')?.dataset.c3phBrief === 'ready' && !!document.querySelector('[data-c3ph-sect="gist"]'), null, 8000),
102
+ await page.evaluate(() => document.querySelector('[data-c3ph-brief]')?.outerHTML.slice(0, 300)));
103
+ await page.keyboard.press('Escape');
104
+ }
105
+ ok('meeting: the morning card opens the brief of the next meeting', !!(await page.$('[data-c3ph-moment="morning"] [data-c3ph-meeting-open]')));
106
+
79
107
  /* ── tell PMOS ── */
80
108
  await page.fill('[data-c3ph-input]', 'Ask Maya about the beta date');
81
109
  await page.press('[data-c3ph-input]', 'Enter');
@@ -86,6 +114,8 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
86
114
  await page.click('[data-c3ph-record]');
87
115
  ok('record: the recording line shows with Stop', await waitFor(page, () => !!document.querySelector('[data-c3ph-recbar] [data-c3ph-stop]'), null, 8000));
88
116
  ok('record: the ask bar gives way while recording', !(await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]'))));
117
+ ok('record: the notes being taken show while recording', await waitFor(page, () => { const c = document.querySelector('[data-c3ph-moment="live"]'); return !!c && c.dataset.c3phLive === 'ready' && !!c.querySelector('[data-c3ph-heard]') && !!c.querySelector('[data-c3ph-live-list="decisions"]'); }, null, 10000),
118
+ await page.evaluate(() => document.querySelector('[data-c3ph-moment]')?.outerHTML.slice(0, 300)));
89
119
  if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
90
120
  ok('record: Stop brings the bar back', await waitFor(page, () => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]') && !document.querySelector('[data-c3ph-recbar]'), null, 8000));
91
121
  await page.close();
@@ -131,6 +161,12 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
131
161
  await page.close();
132
162
  await pctx.close();
133
163
 
164
+ /* ── 620 wide: console3's phone styling starts at 640, so the companion must too (no squeezed desktop between) ── */
165
+ const mctx = await ctxOf(620);
166
+ page = await open(mctx, '/c3.html?demo=1');
167
+ ok('phone: the companion shows at 620 wide', await page.evaluate(shown, '[data-c3ph]'));
168
+ await mctx.close();
169
+
134
170
  /* ── desktop ── */
135
171
  const dctx = await ctxOf(1280);
136
172
  page = await open(dctx, '/c3.html?demo=1', () => !!document.querySelector('nav.c3tabs [role="tab"]'));
@@ -2,7 +2,7 @@
2
2
  type: Guide
3
3
  title: "The Initiative Driver: drafts the agent files for you"
4
4
  description: "What the Initiative Driver does when an initiative is created or moved, what stops it, how it files proposals you keep, send back or drop, the board modes, and the pmos driver commands."
5
- timestamp: 2026-10-12
5
+ timestamp: 2026-10-13
6
6
  ---
7
7
 
8
8
  # The Initiative Driver: drafts the agent files for you
@@ -55,7 +55,7 @@ In Discover, the agent writes the problem statement, the evidence, the opportuni
55
55
  - **The Agent strip.** Open an initiative and stay on Overview. The Agent section is at the top. It shows the state of the agent (working, idle, waiting on you, paused or off), the last thing it did, and what stops it ("Stopped by: ..."). Each open proposal is one row with Keep, Send back and Drop. Press a row to read its text and its sources. A source that is a link opens in a new tab. Pause and Resume are in the header. The text "1 of 3 runs used today" opens the list of today's runs.
56
56
  - **Keep, Send back and Drop on a row.** A kept row stays in the strip, marked "Kept by you", until you leave the page. Send back asks for a one-line note. A kept stage move changes the stage on the page at once. These three acts cannot be undone: each adds one line to the ledger.
57
57
  - **One line in Work.** A Work task with an open proposal shows one line under its title, for example "Agent drafted a follow-up to Dana. Nothing is sent." Press Review to read the draft. Keep copies the text so you can send it yourself, and records your decision. If the browser does not allow copying, the text is selected and you press Ctrl+C. A suggested next step shows Keep and Drop in the line. Shadow and Off show no line.
58
- - **Needs you.** In Briefing, the agent has two kinds of row: "Agent has 3 drafts on ..." and "... is waiting on you". Press a row to open the initiative.
58
+ - **Needs you.** In Briefing, the agent has two kinds of row: "Agent has 3 drafts on ..." and "... is waiting on you". Press a row to open the initiative. A long title wraps inside the card.
59
59
  - **The Agent setting.** Open the board, press Board settings, and find Agent. Choose Off, Shadow or Approve. The setting is saved when you press it.
60
60
 
61
61
  ## Keep, send back, drop
@@ -7,7 +7,7 @@ timestamp: 2026-10-13
7
7
 
8
8
  # Guide: the phone companion
9
9
 
10
- When the console (`/`) is 600 pixels wide or less, for example on a phone, it shows the companion and not the desktop pages. The companion is not a small copy of the desktop. It shows the one thing that is useful now, and it keeps decisions for your computer.
10
+ When the console (`/`) is 640 pixels wide or less, for example on a phone, it shows the companion and not the desktop pages. The companion is not a small copy of the desktop. It shows the one thing that is useful now, and it keeps decisions for your computer.
11
11
 
12
12
  ## The moment card
13
13
 
@@ -15,9 +15,9 @@ The top card shows where you are in your day and why it is useful now. PMOS choo
15
15
 
16
16
  | Moment | When | What the card shows |
17
17
  |---|---|---|
18
- | Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of your open items and the number of decisions that wait for your computer |
19
- | Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it, and the items to close first, each with **Done** |
20
- | In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**. When it records, **Open the meeting companion** opens the meeting focus page. |
18
+ | Your day | Before noon, with no meeting in the next 30 minutes | The next meeting, the number of things that wait on you and how many are overdue, and **The brief of this meeting** |
19
+ | Before you walk in | The next meeting starts in 30 minutes or less | What is prepared for it, the items to close first, each with **Done**, and **The brief of this meeting** |
20
+ | In the meeting | A meeting is in progress | **Record this meeting?** with **Record** and **Not this one**, and **Open the meeting companion**, which opens the meeting focus page. |
21
21
  | Just ended | A meeting ended less than 20 minutes ago | **What came out of it?** If you recorded the meeting, the card shows the summary, the decisions, the actions and the open questions from its notes, the same notes as on the Notes page. While PMOS writes the notes, the card says so. If you did not record it, **Tell PMOS** puts the meeting name in the bar so you can type the result. |
22
22
  | Free time | 15 minutes or more before the next meeting | The items that fit in the time: overdue items first, then items due today, at most two. Each has **Done** and **Later today**. |
23
23
  | Wrapping up | From 17:00, or after noon with no meeting left | What is still open and what waits for your computer |
@@ -26,7 +26,15 @@ When the moment changes, a short message shows at the top for 6 seconds. The mes
26
26
 
27
27
  ## Waiting on you
28
28
 
29
- Under the moment card, your own items show one at a time ("1 of 4"). **Done** marks the item done; the message that follows has **Undo**. **Later today** shows the item again later. **Next** shows the next item. Under the card, **Later today** lists the rest of today's meetings.
29
+ Under the moment card, the things that wait on you show one at a time ("1 of 9"). They are the same things that the **Needs you** section of the desktop Briefing counts: your own items, the Jira and plan items assigned to you that are overdue, blocked or due today, the items of the morning brief, and the decisions.
30
+
31
+ - For your own Work items, **Done** marks the item done; the message that follows has **Undo**. **Later today** shows the item again later.
32
+ - For the other rows, **Details** shows why the row is there, its facts and a link to where it lives. Decisions wait for your computer.
33
+ - **Next** shows the next row. **See all** lists every row; tap a row to see its details.
34
+
35
+ ## Meetings
36
+
37
+ Under the card, **Later today** lists the rest of today's meetings. Tap a meeting to see its brief: the time, the people, the initiative, then the same brief as the meeting card of the desktop Calendar (in short, what matters today, what came out of the last one, what to be ready with, and the links). From the brief you can record the meeting or open the meeting companion.
30
38
 
31
39
  ## Tell PMOS
32
40
 
@@ -34,11 +42,11 @@ Type a note, a to-do or an idea in the bar at the bottom and press Enter. PMOS p
34
42
 
35
43
  ## Record
36
44
 
37
- The red button records a conversation or a meeting with the same recorder as **Record** on the desktop. While it records, the bar shows the name, the time and **Stop**.
45
+ The red button records a conversation or a meeting with the same recorder as **Record** on the desktop. While it records, the bar shows the name, the time and **Stop**, and the top card shows the notes that are being taken: the last lines that were heard, and the decisions, actions and open questions that PMOS has found so far. The card reads the note again every 5 seconds.
38
46
 
39
47
  ## The full console
40
48
 
41
- **Full console** at the top shows the desktop pages on this device, and the device keeps this choice. To go back, open **More** and choose **Phone companion**. In a window wider than 600 pixels the console always shows the desktop pages.
49
+ **Full console** at the top shows the desktop pages on this device, and the device keeps this choice. To go back, open **More** and choose **Phone companion**. In a window wider than 640 pixels the console always shows the desktop pages.
42
50
 
43
51
  ## Limits
44
52
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
  /* lib/c3-full-console.js — for browser suites that test the console's desktop layout at phone width.
3
- * Since c3-phone-companion, console3 at 600px or less shows the phone companion. The desktop layout stays reachable there with
3
+ * Since c3-phone-companion, console3 at 640px or less shows the phone companion. The desktop layout stays reachable there with
4
4
  * "Full console", which stores pmos.c3.phone = 'desk' on the device. fullConsole(browser) makes every new context start with
5
5
  * that choice, so a suite that resizes to 400 wide still tests the desktop layout. c3-phone-test.js tests the companion itself. */
6
6
  function fullConsole(browser) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.180.0",
3
+ "version": "0.182.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.180.0",
9
+ "version": "0.182.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.180.0",
3
+ "version": "0.182.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",
@@ -12,3 +12,19 @@ const NOTE = {
12
12
  };
13
13
  export const demoNoteFor = (m) => (m && (m.id === 'm2' || m.title === NOTE.title) ? NOTE : null);
14
14
  export const demoNote = (id) => (id === NOTE.id ? { ok: true, note: NOTE } : null);
15
+
16
+ /* The note of a recording under way (?demo=1): what the phone companion shows while it records. The shapes are the real ones of GET
17
+ * /api/notes/one: transcript lines with their source, and the bullets the live notes pick out, each with its category. */
18
+ const LIVE = {
19
+ state: 'recording', speakers: { mic: 'You', meeting: 'Dara' },
20
+ transcript: [
21
+ { t: 12, source: 'meeting', text: 'The sandbox keys are two days late, so 24 Oct is gone.' },
22
+ { t: 31, source: 'mic', text: 'Then we hold beta on 30 Oct for the two partners already set up.' },
23
+ { t: 48, source: 'meeting', text: 'I can get the new keys date in writing by Friday.' },
24
+ ],
25
+ bullets: [
26
+ { id: 0, cat: 'decision', text: 'Beta stays on 30 Oct for the two partners already set up' },
27
+ { id: 1, cat: 'action', text: 'Dara gets the sandbox keys date in writing by Friday' },
28
+ ],
29
+ };
30
+ export const demoLive = (st) => ({ ok: true, note: { ...LIVE, id: st.id, title: st.title } });
@@ -24,7 +24,7 @@
24
24
  * day(iso) '12 Oct' · dayLong(iso) 'Mon 12 Oct 2026' · ago(iso, today) · daysBetween(a, b) · plural(n, one, many) · trunc(s, n)
25
25
  */
26
26
  import { el, frag } from './dom.js';
27
- import { button } from './primitives.js';
27
+ import { button, ensureComponentsCss } from './primitives.js';
28
28
  import { c3ic } from './pm-work-view.js';
29
29
  import { makeXp, attachExpand } from './c3-expand.js';
30
30
 
@@ -59,9 +59,23 @@ export function put(node, ...kids) {
59
59
  return node;
60
60
  }
61
61
 
62
+ /**
63
+ * Link c3-initiatives.css once, after components.css, so the `ini-b` reset comes after the primitive's own rules. btn() calls it: the
64
+ * Briefing, Work, Reports and Scan pages draw rows with these builders too, and without the reset a row title kept the primitive's nowrap
65
+ * and ran past its card. Every rule in the sheet is under an `ini-` or `in-` class, so it changes nothing else on those pages.
66
+ */
67
+ export function ensureIniCss() {
68
+ if (typeof document === 'undefined' || document.querySelector('link[data-c3-ini]')) return;
69
+ ensureComponentsCss();
70
+ const l = document.createElement('link');
71
+ l.rel = 'stylesheet'; l.href = new URL('./c3-initiatives.css', import.meta.url).href; l.setAttribute('data-c3-ini', '');
72
+ document.head.appendChild(l);
73
+ }
74
+
62
75
  /* ── controls ── */
63
76
  /** A native button drawn as a console3 control. attrs: class (the console3 class), onclick, and any attribute. */
64
77
  export function btn(attrs, ...kids) {
78
+ ensureIniCss();
65
79
  const { onclick, class: cls, disabled, ...rest } = attrs || {};
66
80
  const b = button({ variant: 'ghost', onClick: onclick, disabled: !!disabled, attrs: { ...rest, class: 'ini-b' + (cls ? ' ' + cls : '') } });
67
81
  b.replaceChildren(frag(...kids.filter((k) => k != null && k !== false)));
@@ -43,7 +43,7 @@ import { openSheet, closeSheet } from './c3-sheet.js';
43
43
  import { toast } from './c3-toast.js';
44
44
  import {
45
45
  btn, geo, icon, pbtn, pill, lk, ib, chip, tag, st, note, lbl, mono, sw, jiraLabel, plural, trunc, day, daysBetween, statusChip, healthSt, HEALTH, ownerChip, ownerText,
46
- stageChip, stepsMini, progChip, dateChip, list, empty, sect, card, nrow, crumbs, lh, segTabs, cols, hbs, hbr, kv, chgl, chgli, openMenu, closeMenu, CATS, PORTFOLIO, put
46
+ stageChip, stepsMini, progChip, dateChip, list, empty, sect, card, nrow, crumbs, lh, segTabs, cols, hbs, hbr, kv, chgl, chgli, openMenu, closeMenu, CATS, PORTFOLIO, put, ensureIniCss
47
47
  } from './c3-ini-ui.js';
48
48
  import { TEMPLATES, BOARD_TEMPLATES, isBuildStage, templateBuilds, offered, wfOf, catOf, kindOf, tplName, TOOL_META, toolLabel, toolIcon, loadItems, dropItems, workTlRows, iniTlRow, rescheduleTask, lockedToast } from './c3-ini-work.js';
49
49
  import { timeline, tlInit, tlRange } from './c3-ini-timeline.js';
@@ -102,11 +102,8 @@ export function formatLoc(loc) {
102
102
 
103
103
  function ensureCss() {
104
104
  ensureC3Css();
105
- ensureComponentsCss(); // first, so the reset in c3-initiatives.css comes after the primitive's own rules
106
- if (typeof document === 'undefined' || document.querySelector('link[data-c3-ini]')) return;
107
- const l = document.createElement('link');
108
- l.rel = 'stylesheet'; l.href = new URL('./c3-initiatives.css', import.meta.url).href; l.setAttribute('data-c3-ini', '');
109
- document.head.appendChild(l);
105
+ ensureComponentsCss();
106
+ ensureIniCss(); // after components.css, so the reset in c3-initiatives.css comes after the primitive's own rules
110
107
  }
111
108
  const warnChip = (w) => st('warn', 'alert', w, { title: 'An order rule, shown as a warning. It never blocks.', 'data-ini-warning': w });
112
109
  const buildTag = (b) => (b.build ? el('span', { class: 'tag' }, icon('jira', 'sm'), jiraLabel(b) ? 'Jira · ' + jiraLabel(b) : 'Jira not set') : el('span', { class: 'tag' }, 'PMOS tasks only'));
@@ -0,0 +1,83 @@
1
+ /* pm/c3-phone-detail.js — the phone companion's sheets (initiative c3-phone-companion). Each opens as the console3 bottom sheet (c3-sheet.js).
2
+ * openMeeting(m, opts) a meeting of today: when, who, the initiative, what is prepared, then its brief as GET /api/meeting-focus
3
+ * returns it (lib/calendar-layers.js briefOf, the same brief as the desktop Calendar card): In short, What matters,
4
+ * Since the last one, To prepare, links. opts: { onRecord(m), onFocus(m), recording } adds Record it and
5
+ * Open the meeting companion. A failed read keeps what the companion already knew and says so.
6
+ * openNeed(r, opts) one thing that waits on the PM (a row of waitRows in c3-phone.js): why, the facts, the source link, and
7
+ * Done and Later today when the row is the PM's own Work item. opts: { onDone(r), onLater(r) }
8
+ * openAll(rows, opts) every row that waits, as a list; a press opens that row (openNeed). opts as openNeed.
9
+ * Nothing here writes: the buttons call back into c3-phone.js, which owns the writes and their Undo. */
10
+ import { el } from './dom.js';
11
+ import { button, row } from './primitives.js';
12
+ import { pmGet } from './http.js';
13
+ import { c3ic } from './pm-work-view.js';
14
+ import { openSheet, closeSheet } from './c3-sheet.js';
15
+
16
+ const p2 = (n) => String(n).padStart(2, '0');
17
+ const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p2(d.getMinutes()); };
18
+ const dayOf = (iso) => { const d = new Date(iso); return d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()); };
19
+ const line = (t, attrs) => el('div', { class: 'c3ph-line', ...(attrs || {}) }, t);
20
+ const sect = (key, title, ...kids) => el('div', { class: 'c3ph-fit', 'data-c3ph-sect': key }, el('b', null, title), ...kids.filter(Boolean));
21
+ const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
22
+ const link = (url, label) => el('a', { class: 'chip2', href: /^https?:\/\//.test(url) ? url : 'https://' + url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), label);
23
+
24
+ /** The parts of a briefOf brief, as phone sections. Pure over the brief; null parts are left out. */
25
+ export function briefParts(b) {
26
+ if (!b) return [];
27
+ const out = [];
28
+ if (b.gist && b.gist.length) out.push(sect('gist', 'In short', line(b.gist.join(' '))));
29
+ if (b.matters && b.matters.length) out.push(sect('matters', b.planning ? 'To settle in planning' : 'What matters today', ...b.matters.map((x) => line([x.tag ? x.tag + ': ' : '', x.text, x.meta ? ' · ' + x.meta : ''].join('')))));
30
+ if (b.last) {
31
+ const l = b.last;
32
+ out.push(sect('last', 'Since the last one',
33
+ l.private ? line('That note is private, so only its title shows: ' + (l.title || 'Untitled note') + '.') : line(l.overview || 'That note has no summary yet.'),
34
+ ...(l.private ? [] : (l.decisions || []).map((t) => line('Decision: ' + t))),
35
+ ...(l.private ? [] : (l.actions || []).map((a) => line('From last time: ' + a.text + [a.owner, a.due && 'due ' + a.due].filter(Boolean).map((s) => ' · ' + s).join(''))))));
36
+ }
37
+ if (b.prep) out.push(sect('prep', 'To be ready', line(b.prep)));
38
+ const links = [...(b.links || []).filter((l) => l && l.url).map((l) => link(l.url, l.label)), ...(b.refs || []).filter((r) => r && r.link).map((r) => link(r.link, r.label))];
39
+ if (links.length) out.push(el('div', { class: 'chips2', 'data-c3ph-sect': 'links' }, ...links));
40
+ return out;
41
+ }
42
+
43
+ export function openMeeting(m, opts = {}) {
44
+ const known = [
45
+ line(hm(m.start) + (m.end ? '–' + hm(m.end) : '') + (m.minutes ? ' · ' + m.minutes + ' min' : '')),
46
+ m.people && m.people.length ? line(m.people.join(', ')) : null,
47
+ m.initiative ? line('Initiative: ' + m.initiative) : null,
48
+ ...(m.prepared || []).map((x) => el('div', { class: 'c3ph-prep' }, c3ic(x.kind === 'prep' ? 'spark' : 'doc', 'sm'), x.text)),
49
+ ...(m.before || []).map((x) => line('Before you go: ' + x.title)),
50
+ ].filter(Boolean);
51
+ const brief = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-brief': 'loading' }, line('Reading the brief of this meeting.'));
52
+ const acts = el('div', { class: 'c3ph-acts' },
53
+ opts.onRecord && !opts.recording ? btn('Record it', 'primary', 'mic', { 'data-c3ph-sheet-record': m.id }, () => { closeSheet(); opts.onRecord(m); }) : null,
54
+ opts.onFocus ? btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-sheet-focus': m.id }, () => { closeSheet(); opts.onFocus(m); }) : null);
55
+ const sh = openSheet({ title: m.title, icon: 'calendar', body: [el('div', { class: 'c3ph-sheetbody', 'data-c3ph-meeting': m.id }, ...known), brief, acts] });
56
+ pmGet('/api/meeting-focus?event=' + encodeURIComponent(m.id) + '&date=' + dayOf(m.start))
57
+ .then((j) => {
58
+ const parts = j && j.ok !== false ? briefParts(j.brief) : null;
59
+ brief.dataset.c3phBrief = parts ? (parts.length ? 'ready' : 'empty') : 'error';
60
+ brief.replaceChildren(...(parts === null ? [line('The brief could not be read' + (j && j.error ? ': ' + j.error : '.'))]
61
+ : parts.length ? parts : [line('PMOS holds nothing more on this meeting. Link it to an initiative on your computer, and what matters there shows here.')]));
62
+ }, () => { brief.dataset.c3phBrief = 'error'; brief.replaceChildren(line('The brief could not be read.')); });
63
+ return sh;
64
+ }
65
+
66
+ export function openNeed(r, opts = {}) {
67
+ const body = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-need': r.id },
68
+ el('span', { class: 'c3ph-step' }, r.label.toUpperCase()),
69
+ r.why ? line(r.why) : null,
70
+ ...(r.facts || []).map((f) => line(f)),
71
+ r.url ? el('div', { class: 'chips2' }, link(r.url, r.urlLabel || 'Open the source')) : null,
72
+ r.pm ? el('div', { class: 'c3ph-acts two' },
73
+ btn('Done', 'primary', 'check', { 'data-c3ph-sheet-done': r.id }, () => { closeSheet(); opts.onDone && opts.onDone(r); }),
74
+ btn('Later today', 'ghost', 'clock', { 'data-c3ph-sheet-later': r.id }, () => { closeSheet(); opts.onLater && opts.onLater(r); }))
75
+ : line(r.decide ? 'A decision: make it on your computer, where the options and their evidence are.' : 'Change it where it lives, or on your computer.'));
76
+ return openSheet({ title: r.headline, icon: 'inbox', body });
77
+ }
78
+
79
+ export function openAll(rows, opts = {}) {
80
+ const list = el('div', { class: 'c3ph-sheetbody', 'data-c3ph-all': String(rows.length) },
81
+ ...rows.map((r) => row({ title: r.headline, meta: [r.label], compact: true, onOpen: () => openNeed(r, opts), openLabel: 'Open ' + r.headline, attrs: { 'data-c3ph-all-row': r.id } })));
82
+ return openSheet({ title: rows.length + (rows.length === 1 ? ' thing waits on you' : ' things wait on you'), icon: 'inbox', body: list });
83
+ }
@@ -27,8 +27,6 @@
27
27
  .c3 .c3ph-cnt{display:flex;justify-content:space-between;font-weight:600;font-size:var(--fs-2)}
28
28
  .c3 .c3ph-cnt .dim{font-weight:400;font-family:var(--mono)}
29
29
  .c3 .c3ph-later{display:grid;gap:4px;font-size:var(--fs-2);color:var(--muted-foreground);padding:0 4px}
30
- .c3 .c3ph-later > div{display:flex;gap:10px}
31
- .c3 .c3ph-later .mono{font-family:var(--mono);min-width:42px}
32
30
  .c3 .c3ph-ft{position:sticky;bottom:0;display:flex;align-items:center;gap:10px;padding:8px 12px calc(12px + env(safe-area-inset-bottom));border-top:1px solid var(--border);background:var(--card);z-index:2}
33
31
  .c3 .c3ph-ask{flex:1;display:flex;align-items:center;gap:8px;border:1px solid var(--border);border-radius:8px;padding:0 4px 0 12px;min-height:44px;color:var(--muted-foreground);background:var(--background)}
34
32
  .c3 .c3ph-ask input.c3ph-in{flex:1;min-width:0;border:0;background:transparent;font-family:inherit;font-size:var(--fs-3);color:var(--foreground);padding:10px 0;outline:none;border-radius:0}
@@ -3,12 +3,17 @@
3
3
  *
4
4
  * It has no tabs and no inbox to visit. It brings the one thing that matters now (c3-phone-model.js momentOf), then the PM's own items one
5
5
  * at a time, then what comes later today. Two controls sit at the bottom: "Tell PMOS" and the red Record button.
6
- * moment card meeting under way: "Record this meeting?" (Record / Not this one); recording: the meeting companion (c3-focus.js, compact)
6
+ * moment card meeting under way: "Record this meeting?" (Record / Not this one), and the meeting companion (c3-focus.js, compact)
7
+ * recording (any recording): the live notes of GET /api/notes/one, read every 5 s: what was just heard, and the decisions,
8
+ * actions and questions picked out so far (dest-notes.js partsOf), as the desktop note shows them
7
9
  * before a meeting: when it starts, who, what is prepared, the "before you go" items with Done
8
10
  * just ended: tell PMOS what came out of it a free gap: the own items that fit, with Done and Later today
9
11
  * morning: the next meeting and how much waits wrap: what is still open, what waits for the desk
10
- * waiting card the own items from GET /api/brief-page needs, one at a time: Done (POST /api/pm-items/done, Undo: /reopen),
11
- * Later today and Tomorrow (POST /api/pm-items/notyet). Decisions are not answered on the phone: they wait for the desk.
12
+ * waiting card what waits on the PM, as the desktop Briefing counts it: the own items of GET /api/brief-page, then the needs of
13
+ * GET /api/briefing (assigned Jira and WBS items, Work items, morning brief items, decisions), one at a time, with See all.
14
+ * An own Work item has Done (POST /api/pm-items/done, Undo: /reopen) and Later today (POST /api/pm-items/notyet); any other row
15
+ * opens its details (c3-phone-detail.js openNeed). Decisions are not answered on the phone: they wait for the desk.
16
+ * meetings the next meeting and each row of Later today open the meeting's sheet with its brief (c3-phone-detail.js openMeeting).
12
17
  * Tell PMOS a line of text, filed by POST /api/capture { ws, text }: it lands in the Work inbox, where triage files it. The small mic
13
18
  * points to the keyboard's own dictation; this page does not send audio anywhere.
14
19
  * Record the red button records a conversation or a meeting, the same recorder as the desktop Record control (note-dock.js).
@@ -23,6 +28,9 @@ import { onBriefPage, loadBriefPage } from './c3-brief-data.js';
23
28
  import { onBriefing, loadBriefing } from './shell-ticker.js';
24
29
  import { toast } from './c3-toast.js';
25
30
  import { momentOf, inText } from './c3-phone-model.js';
31
+ import { openMeeting, openNeed, openAll } from './c3-phone-detail.js';
32
+ import { cardView } from './briefing-model.js';
33
+ import { row } from './primitives.js';
26
34
 
27
35
  const CSS_ID = 'c3-phone-css';
28
36
  const p2 = (n) => String(n).padStart(2, '0');
@@ -30,6 +38,7 @@ const hm = (iso) => { const d = new Date(iso); return p2(d.getHours()) + ':' + p
30
38
  const first = (n) => String(n || '').trim().split(/\s+/)[0];
31
39
  const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
32
40
  const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
41
+ const LIVE_MS = 5000; // how often the note being recorded is read again
33
42
  const clockOf = (msec) => { const s = Math.max(0, Math.floor(msec / 1000)); return Math.floor(s / 60) + ':' + p2(s % 60); };
34
43
 
35
44
  function ensureCss() {
@@ -39,7 +48,7 @@ function ensureCss() {
39
48
 
40
49
  /** Is this device in phone mode? Phone width, and the PM has not chosen the full console on it. */
41
50
  export function phoneWanted(store) {
42
- if (typeof matchMedia !== 'function' || !matchMedia('(max-width:600px)').matches) return false;
51
+ if (typeof matchMedia !== 'function' || !matchMedia('(max-width:640px)').matches) return false;
43
52
  return !(store && store.get('pmos.c3.phone') === 'desk');
44
53
  }
45
54
 
@@ -90,6 +99,20 @@ export function mountPhone(root, ctx = {}) {
90
99
  }
91
100
  }
92
101
  if (S.clock) S.clock.textContent = st.mode === 'recording' && st.started ? clockOf(dock.now() - st.started) : st.mode === 'paused' ? 'paused' : '';
102
+ if (st.mode !== 'idle' && st.id && !S.liveBusy && (!S.live || S.live.id !== st.id || Date.now() - S.live.at > LIVE_MS)) readLive(st);
103
+ if (st.mode === 'idle' && S.live) { S.live = null; draw(); }
104
+ }
105
+ /* ── the note being recorded (GET /api/notes/one): what was just heard, and the decisions, actions and questions picked out so far ── */
106
+ async function readLive(st) {
107
+ S.liveBusy = true;
108
+ try {
109
+ const [src, dn, vn] = await Promise.all([demo ? import('./c3-demo-phone.js') : null, import('./dest-notes.js'), import('./view-notes.js')]);
110
+ const one = demo ? src.demoLive(st) : await pmGet('/api/notes/one?id=' + encodeURIComponent(st.id));
111
+ const n = one && one.ok !== false ? one.note : null;
112
+ S.live = { id: st.id, at: Date.now(), note: n, parts: n ? dn.partsOf(n) : null, speakerOf: vn.speakerOf };
113
+ } catch { S.live = { id: st.id, at: Date.now(), note: null, parts: null }; }
114
+ S.liveBusy = false;
115
+ draw();
93
116
  }
94
117
 
95
118
  /* ── writes ── */
@@ -146,24 +169,68 @@ export function mountPhone(root, ctx = {}) {
146
169
  const card = (hero, attrs, ...kids) => el('section', { class: 'card c3ph-card' + (hero ? ' lead' : ''), ...(attrs || {}) }, ...kids);
147
170
  const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
148
171
  const who = (m) => (m.people && m.people.length ? m.people.map(first).join(', ') : '');
149
- const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && !S.gone.has(n.id));
150
- const decisions = () => ((S.briefing && S.briefing.needs) || []).filter((n) => n && (n.card || (n.opts || n.options || []).length) && !n.itemId).length;
172
+ const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && n.kind !== 'decision' && !S.gone.has(n.id));
173
+ /* everything that waits on the PM, as the desktop Briefing's Needs you counts it (c3-brief-needs.js needsCount): the own items of
174
+ * GET /api/brief-page, then the briefing's needs (assigned Jira and WBS items, own Work items, morning brief items, decisions). */
175
+ function waitRows() {
176
+ const out = []; const seen = new Set();
177
+ own().forEach((n) => { seen.add(n.id); out.push({ id: n.id, pm: true, headline: n.headline, raw: n, why: n.evidence && n.evidence.text ? 'Looks done: ' + n.evidence.text + '.' : null,
178
+ label: n.kind === 'overdue' ? (n.days_late === 1 ? '1 day late' : n.days_late + ' days late') : n.kind === 'due-today' ? 'Due today' : n.person ? 'Talk to ' + first(n.person) : 'Outside PMOS' }); });
179
+ ((S.briefing && S.briefing.needs) || []).forEach((n, i) => {
180
+ if (!n) return;
181
+ const pm = n.origin === 'work' && !!n.itemId;
182
+ const id = pm ? n.itemId : String(n.key || 'need' + i);
183
+ if (seen.has(id) || (n.itemId && seen.has(n.itemId)) || S.gone.has(id)) return;
184
+ seen.add(id);
185
+ if (n.card) {
186
+ const v = cardView(n.card);
187
+ out.push({ id, pm: false, decide: true, headline: v.headline, label: v.tag + (v.deadline && v.deadline.text ? ' · ' + v.deadline.text : ''), why: n.card.why && n.card.why.text, url: n.card.url, urlLabel: n.card.url_label, facts: [] });
188
+ return;
189
+ }
190
+ out.push({ id, pm, decide: n.kind === 'Decide', headline: n.headline, label: [n.kind, n.dueText && n.dueText !== n.kind ? n.dueText : null, n.p].filter(Boolean).join(' · '),
191
+ why: n.why, url: n.url, urlLabel: n.url_label, facts: (n.facts || []).map((f) => f && f.text).filter(Boolean) });
192
+ });
193
+ return out;
194
+ }
195
+ const decisions = () => waitRows().filter((r) => !r.pm).length;
196
+ const needOpts = { onDone: (r) => itemDone(r), onLater: (r) => itemLater(r, 'later-today') };
197
+ const focus = (m) => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true })).catch(() => toast('The meeting companion did not open.'));
198
+ /* a meeting's sheet (c3-phone-detail.js): Record it while the meeting has not ended, and the meeting companion */
199
+ function meetingSheet(m) {
200
+ const t = now().getTime(); const ends = Date.parse(m.end || m.start) || 0;
201
+ openMeeting(m, { recording: recState().mode !== 'idle', onRecord: t < ends ? (x) => startRec(x) : null, onFocus: focus });
202
+ }
151
203
 
152
- function itemCard(n, hero) {
153
- const late = n.kind === 'overdue' ? (n.days_late === 1 ? '1 day late' : n.days_late + ' days late') : n.kind === 'due-today' ? 'Due today' : n.person ? 'Talk to ' + first(n.person) : 'Outside PMOS';
154
- return [el('span', { class: 'c3ph-step' }, late.toUpperCase()), el('b', { class: 'c3ph-tt' }, n.headline),
155
- n.evidence && n.evidence.text ? line('Looks done: ' + n.evidence.text + '.') : null,
156
- acts(btn(n.evidence ? 'Confirm done' : 'Done', hero ? 'primary' : 'outline', 'check', { 'data-c3ph-done': n.id }, () => itemDone(n)),
157
- btn('Later today', 'ghost', 'clock', { 'data-c3ph-later': n.id }, () => itemLater(n, 'later-today')))];
204
+ function liveCard(st, mo) {
205
+ const L = S.live && S.live.id === st.id ? S.live : null;
206
+ const n = L && L.note; const p = L && L.parts;
207
+ const ev = (n && n.meeting && n.meeting.event_id) ? { id: n.meeting.event_id, start: (mo.meeting && mo.meeting.start) || now().toISOString() } : (mo.kind === 'meeting' ? mo.meeting : null);
208
+ const heard = n ? (n.transcript || []).filter((x) => x && x.text && !x.dropped).slice(-3) : [];
209
+ const list = (label, rows) => (rows && rows.length ? el('div', { class: 'c3ph-fit', 'data-c3ph-live-list': label.toLowerCase() }, el('b', null, label + ' · ' + rows.length), ...rows.slice(-2).map((x) => line(x.text))) : null);
210
+ const ov = p && p.overview ? (p.overview.length > 220 ? p.overview.slice(0, 217) + '…' : p.overview) : '';
211
+ return card(true, { 'data-c3ph-moment': 'live', 'data-c3ph-live': !L ? 'loading' : n ? 'ready' : 'none' },
212
+ why(st.mode === 'paused' ? 'Paused' : 'Recording now'), big(st.title || 'Conversation'),
213
+ ov ? line(ov) : null,
214
+ p ? list('Decisions', p.decisions) : null, p ? list('Actions', p.actions) : null, p ? list('Open questions', p.questions) : null,
215
+ heard.length ? el('div', { class: 'c3ph-fit', 'data-c3ph-heard': String(heard.length) }, el('b', null, 'Just heard'), ...heard.map((x) => line((L.speakerOf ? L.speakerOf(n, x.source) + ': ' : '') + x.text)))
216
+ : line(!L ? 'Opening the notes.' : n ? 'Listening. What is said shows here, and PMOS picks out the decisions, actions and questions as they come.' : 'The notes of this recording could not be read. They are kept on your computer.'),
217
+ ev ? acts(btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': ev.id }, () => focus(ev))) : null);
218
+ }
219
+
220
+ function itemCard(r, hero) {
221
+ if (!r.label) return itemCard(waitRows().find((x) => x.id === r.id) || { ...r, pm: true, label: 'Yours' }, hero);
222
+ const looks = r.raw && r.raw.evidence;
223
+ return [el('span', { class: 'c3ph-step' }, r.label.toUpperCase()), el('b', { class: 'c3ph-tt' }, r.headline),
224
+ r.why ? line(r.why) : null,
225
+ r.pm ? acts(btn(looks ? 'Confirm done' : 'Done', hero ? 'primary' : 'outline', 'check', { 'data-c3ph-done': r.id }, () => itemDone(r)),
226
+ btn('Later today', 'ghost', 'clock', { 'data-c3ph-later': r.id }, () => itemLater(r, 'later-today')))
227
+ : acts(btn('Details', 'outline', 'chevron-right', { 'data-c3ph-details': r.id }, () => openNeed(r, needOpts)))];
158
228
  }
159
229
 
160
230
  function momentCard(mo) {
161
231
  const m = mo.meeting;
162
232
  if (mo.kind === 'meeting') {
163
- const st = recState();
164
- const recording = st.mode !== 'idle';
165
- const open = btn('Open the meeting companion', recording ? 'primary' : 'outline', 'target', { 'data-c3ph-focus': m.id }, () => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true })).catch(() => toast('The meeting companion did not open.')));
166
- if (recording) return card(true, { 'data-c3ph-moment': 'meeting' }, why('Under way · ends ' + inText(mo.minutes)), big(m.title), line('Recording. PMOS follows the talk and writes the notes.'), acts(open));
233
+ const open = btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': m.id }, () => focus(m));
167
234
  if (S.noRec.has(m.id)) return card(true, { 'data-c3ph-moment': 'meeting' }, why('Under way · ends ' + inText(mo.minutes)), big(m.title), line('Not recording. PMOS follows the agenda and what you mark.'), acts(open, btn('Record from now', 'ghost', 'mic', { 'data-c3ph-rec-meeting': m.id }, () => startRec(m))));
168
235
  return card(true, { 'data-c3ph-moment': 'meeting' }, why('Starting now' + (who(m) ? ' · ' + who(m) : '')), big('Record ' + m.title + '?'),
169
236
  el('div', { class: 'c3ph-rec-why' }, 'Recording lets PMOS follow the talk, bring up what you need at the right topic, and write the notes for you.'),
@@ -177,7 +244,8 @@ export function mountPhone(root, ctx = {}) {
177
244
  ...before.map((x) => el('div', { class: 'c3ph-before', 'data-c3ph-before': x.id }, el('span', null, 'Before you go: ' + x.title),
178
245
  btn('Done', 'outline', 'check', { 'data-c3ph-done': x.id }, () => itemDone({ id: x.id, headline: x.title })))),
179
246
  !(m.prepared || []).length && !before.length ? line('Nothing is prepared for it.') : null,
180
- line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' }));
247
+ line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' }),
248
+ acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': m.id }, () => meetingSheet(m))));
181
249
  }
182
250
  if (mo.kind === 'after') {
183
251
  const db = debriefOf(m);
@@ -208,34 +276,40 @@ export function mountPhone(root, ctx = {}) {
208
276
  ...(fits.length ? fits.map((n) => el('div', { class: 'c3ph-fit', 'data-c3ph-fit': n.id }, ...itemCard(n, false))) : [line('Nothing of yours is due. A good moment to prepare the next meeting.')]));
209
277
  }
210
278
  if (mo.kind === 'morning') {
211
- const o = own();
279
+ const w = waitRows(); const late = w.filter((r) => /overdue|late/i.test(r.label)).length;
212
280
  return card(true, { 'data-c3ph-moment': 'morning' }, why('Your day'), big(mo.next ? mo.next.title + ' at ' + hm(mo.next.start) : 'No meetings today'),
213
- line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null, o.length ? o.length + (o.length === 1 ? ' thing of yours waits.' : ' things of yours wait.') : 'Nothing of yours is due.', decisions() ? decisions() + (decisions() === 1 ? ' decision waits' : ' decisions wait') + ' for your desk.' : null].filter(Boolean).join(' ')));
281
+ line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
282
+ w.length ? w.length + (w.length === 1 ? ' thing waits on you' : ' things wait on you') + (late ? ', ' + late + ' overdue.' : '.') : 'Nothing waits on you.'].filter(Boolean).join(' ')),
283
+ mo.next ? acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next))) : null);
214
284
  }
215
- const o = own();
285
+ const o = waitRows().filter((r) => r.pm);
216
286
  return card(true, { 'data-c3ph-moment': 'wrap' }, why('Wrapping up'), big(o.length ? o.length + ' still open today' : 'Nothing left for today'),
217
287
  line(decisions() ? decisions() + (decisions() === 1 ? ' decision waits' : ' decisions wait') + ' for your desk. It opens first on your Briefing.' : 'Nothing waits for your desk.'));
218
288
  }
219
289
 
220
290
  function waitingCard(mo) {
221
291
  const skipIds = new Set(((mo.kind === 'gap' && mo.fits) || []).map((n) => n.id));
222
- const list = own().filter((n) => !skipIds.has(n.id) && !S.skipped.has(n.id));
292
+ const all = waitRows().filter((r) => !skipIds.has(r.id));
293
+ const list = all.filter((r) => !S.skipped.has(r.id));
223
294
  if (!list.length) return null;
224
295
  const n = list[0];
225
296
  return card(false, { 'data-c3ph-waiting': n.id }, el('div', { class: 'c3ph-cnt' }, el('span', null, 'Waiting on you'), el('span', { class: 'dim' }, '1 of ' + list.length)),
226
- ...itemCard(n, false), list.length > 1 ? button({ label: 'Next', variant: 'ghost', size: 'sm', iconEnd: 'chevron-right', attrs: { 'data-c3ph-skip': n.id }, onClick: () => { S.skipped.add(n.id); draw(); } }) : null);
297
+ ...itemCard(n, false),
298
+ el('div', { class: 'c3ph-acts two' },
299
+ list.length > 1 ? button({ label: 'Next', variant: 'ghost', size: 'sm', iconEnd: 'chevron-right', attrs: { 'data-c3ph-skip': n.id }, onClick: () => { S.skipped.add(n.id); draw(); } }) : null,
300
+ all.length > 1 ? button({ label: 'See all ' + all.length, variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-all-open': '' }, onClick: () => openAll(all, needOpts) }) : null));
227
301
  }
228
302
 
229
303
  function laterList(mo) {
230
304
  const rows = (mo.later || []).slice(0, 3);
231
305
  if (!rows.length) return null;
232
306
  return el('div', { class: 'c3ph-later', 'data-c3ph-later-list': '' }, el('span', { class: 'c3ph-step' }, 'LATER TODAY'),
233
- ...rows.map((m) => el('div', { 'data-c3ph-later-row': m.id }, el('span', { class: 'mono' }, hm(m.start)), el('span', null, m.title))));
307
+ ...rows.map((m) => row({ title: m.title, meta: [hm(m.start), who(m)], compact: true, onOpen: () => meetingSheet(m), openLabel: 'Open ' + m.title, attrs: { 'data-c3ph-later-row': m.id } })));
234
308
  }
235
309
 
236
310
  function headLine() {
237
311
  const d = now();
238
- const n = own().length;
312
+ const n = waitRows().length;
239
313
  head.replaceChildren(el('b', null, WD[d.getDay()] + ' ' + d.getDate() + ' ' + MO[d.getMonth()]),
240
314
  el('span', { class: 'c3ph-hdr' }, n ? el('span', { class: 'c3ph-dotn', 'data-c3ph-count': String(n) }, c3ic('inbox', 'sm'), n + ' waiting') : null,
241
315
  button({ label: 'Full console', variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-desk': '' }, onClick: () => ctx.onDesk && ctx.onDesk() })));
@@ -255,7 +329,8 @@ export function mountPhone(root, ctx = {}) {
255
329
  S.kindKey = key;
256
330
  host.dataset.moment = mo.kind;
257
331
  headLine();
258
- body.replaceChildren(momentCard(mo), waitingCard(mo) || el('span'), laterList(mo) || el('span'));
332
+ const st = recState();
333
+ body.replaceChildren(st.mode !== 'idle' ? liveCard(st, mo) : momentCard(mo), waitingCard(mo) || el('span'), laterList(mo) || el('span'));
259
334
  }
260
335
 
261
336
  const off1 = onBriefPage((j) => { S.page = j; draw(); });
@@ -151,7 +151,7 @@ function toggleMore() {
151
151
  document.addEventListener('keydown', S.menuOff); document.addEventListener('pointerdown', S.menuOff);
152
152
  const first = S.menu.querySelector('.c3pg-mi:not([disabled])'); if (first) first.focus();
153
153
  }
154
- /* The phone companion (c3-phone.js): at 600px or less, unless the PM chose the full console on this device, it fills the page and the
154
+ /* The phone companion (c3-phone.js): at 640px or less, unless the PM chose the full console on this device, it fills the page and the
155
155
  * desktop grid is hidden. "Full console" stores that choice; the More menu's "Phone companion" clears it. */
156
156
  function syncPhone() {
157
157
  const want = phoneWanted(store);
@@ -607,7 +607,7 @@ async function boot() {
607
607
  mountAmbient(root); // the ambient motion canvas, once, behind the grid
608
608
  mountQuiet(root); // the quiet layout: two rows above the content, no teaching sentences (c3-quiet-pages)
609
609
  syncPhone(); // at phone width the companion replaces the desktop frame (c3-phone-companion)
610
- if (typeof matchMedia === 'function') matchMedia('(max-width:600px)').addEventListener('change', syncPhone);
610
+ if (typeof matchMedia === 'function') matchMedia('(max-width:640px)').addEventListener('change', syncPhone);
611
611
 
612
612
  S.dock.setWorkspaces(S.workspaces, S.ws);
613
613
  mountBell(S.bellHost, { go: (_, t) => goTarget(t) });