create-pmos 0.181.0 → 0.183.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.181.0",
3
+ "version": "0.183.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@b6a9241
1
+ kernel-source: pmos@ab3a131
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-13T03:13:28Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T03:53:55Z"}
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-13T03:13:28Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T03:53:55Z"}
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-13T03:13:28Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-13T03:53:55Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -1,16 +1,21 @@
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
- * desk Full console shows the desktop grid and survives a reload; More > Phone companion brings it back
14
+ * nodesk there is no Full console; a phone that chose it before 2026-10-13 shows the companion again
15
+ * focus the meeting companion opens over the companion and Back to your day closes it
16
+ * narrow a long title and a long list of people never widen the companion past the screen
13
17
  * desktop at 1280 wide no companion is drawn
18
+ * 620 the companion shows up to 640 wide, where the phone styling starts
14
19
  * No script error may be raised.
15
20
  */
16
21
  const path = require('path');
@@ -76,6 +81,31 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
76
81
  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
82
  }
78
83
 
84
+ /* ── the brief: everything the desktop Briefing counts waits here, and each row opens ── */
85
+ 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));
86
+ ok('brief: the briefing needs are in the waiting list', await waitFor(page, () => !!document.querySelector('[data-c3ph-all-open]'), null, 8000));
87
+ if (await page.$('[data-c3ph-all-open]')) {
88
+ await page.click('[data-c3ph-all-open]');
89
+ const n = await waitFor(page, () => document.querySelectorAll('[data-c3ph-all] [data-c3ph-all-row]').length > 1, null, 5000);
90
+ ok('brief: See all lists every row that waits', n);
91
+ const nonPm = await page.evaluate(() => [...document.querySelectorAll('[data-c3ph-all-row]')].length);
92
+ await page.click('[data-c3ph-all-row] .pmc-row__open');
93
+ ok('brief: a row opens its details', await waitFor(page, () => !!document.querySelector('[data-c3ph-need]'), null, 5000), nonPm);
94
+ await page.keyboard.press('Escape');
95
+ }
96
+
97
+ /* ── a meeting opens its brief ── */
98
+ const laterRow = await waitFor(page, () => !!document.querySelector('[data-c3ph-later-row] .pmc-row__open'), null, 8000);
99
+ ok('meeting: the Later today rows can be opened', laterRow);
100
+ if (laterRow) {
101
+ await page.click('[data-c3ph-later-row] .pmc-row__open');
102
+ ok('meeting: the sheet shows the meeting', await waitFor(page, () => !!document.querySelector('[data-c3ph-meeting]'), null, 5000));
103
+ 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),
104
+ await page.evaluate(() => document.querySelector('[data-c3ph-brief]')?.outerHTML.slice(0, 300)));
105
+ await page.keyboard.press('Escape');
106
+ }
107
+ ok('meeting: the morning card opens the brief of the next meeting', !!(await page.$('[data-c3ph-moment="morning"] [data-c3ph-meeting-open]')));
108
+
79
109
  /* ── tell PMOS ── */
80
110
  await page.fill('[data-c3ph-input]', 'Ask Maya about the beta date');
81
111
  await page.press('[data-c3ph-input]', 'Enter');
@@ -86,6 +116,8 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
86
116
  await page.click('[data-c3ph-record]');
87
117
  ok('record: the recording line shows with Stop', await waitFor(page, () => !!document.querySelector('[data-c3ph-recbar] [data-c3ph-stop]'), null, 8000));
88
118
  ok('record: the ask bar gives way while recording', !(await page.evaluate(() => !!document.querySelector('[data-c3ph-foot] [data-c3ph-input]'))));
119
+ 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),
120
+ await page.evaluate(() => document.querySelector('[data-c3ph-moment]')?.outerHTML.slice(0, 300)));
89
121
  if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
90
122
  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
123
  await page.close();
@@ -101,6 +133,15 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
101
133
  await page.click('[data-c3ph-rec-meeting]');
102
134
  ok('moments: Record starts recording Checkout sync', await waitFor(page, () => /Checkout sync/.test(document.querySelector('[data-c3ph-recbar]')?.textContent || ''), null, 8000));
103
135
  ok('moments: while recording, the card opens the meeting companion', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment] [data-c3ph-focus]'), null, 5000));
136
+ const fbtn = await waitFor(page, () => !!document.querySelector('[data-c3ph-focus]'), null, 8000);
137
+ ok('focus: the Open the meeting companion button is found', fbtn);
138
+ if (fbtn) {
139
+ await page.click('[data-c3ph-focus]');
140
+ ok('focus: the compact focus page shows with Back to your day', await waitFor(page, () => !!document.querySelector('[data-c3focus][data-fc-pop] [data-fc="leave"]') && !document.querySelector('[data-c3focus] [data-fc="pop-open"]'), null, 8000));
141
+ await page.click('[data-c3focus] [data-fc="pop-head"] [data-fc="leave"]');
142
+ ok('focus: Back to your day closes it and the companion shows', await waitFor(page, () => !document.querySelector('[data-c3focus]') && !!document.querySelector('[data-c3ph-moment]'), null, 5000));
143
+ }
144
+
104
145
  if (await page.$('[data-c3ph-stop]')) await page.click('[data-c3ph-stop]');
105
146
 
106
147
  await page.close();
@@ -115,22 +156,29 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
115
156
  await page.close();
116
157
  page = await open(pctx, '/c3.html?demo=1&at=10:45');
117
158
 
118
- /* ── full console and back ── */
119
- await page.click('[data-c3ph-desk]');
120
- ok('desk: Full console shows the desktop grid', await waitFor(page, () => { const g = document.querySelector('.c3grid'); return !!g && getComputedStyle(g).display !== 'none' && !document.querySelector('[data-c3ph]'); }, null, 5000));
159
+ /* ── the meeting companion and back ── */
160
+ ok('nodesk: the companion offers no Full console', await page.evaluate(() => !document.querySelector('[data-c3ph-desk]') && !/Full console/.test(document.querySelector('[data-c3ph]').textContent)));
161
+ /* ── a long title and a long list of people stay inside the screen ── */
162
+ ok('narrow: long text does not widen the companion', await page.evaluate(() => {
163
+ const long = 'SC-01 Technical blueprint and estimate: ideal build versus the minimum viable path for the partner programme, MTGA41, Victor, Ho, Jasmine, Cheng, Cara, Chai, Tan, Lee';
164
+ document.querySelectorAll('[data-c3ph] .c3ph-big, [data-c3ph] .c3ph-tt, [data-c3ph] .pmc-title__text, [data-c3ph] .pmc-row__meta > span').forEach((n) => { n.textContent = long; });
165
+ const host = document.querySelector('[data-c3ph]');
166
+ return host.scrollWidth <= innerWidth + 1 && document.documentElement.scrollWidth <= innerWidth + 1;
167
+ }));
168
+
169
+ /* ── a phone that chose Full console before it was dropped shows the companion again ── */
170
+ await page.evaluate(() => localStorage.setItem('pmos.c3.phone', 'desk'));
121
171
  await page.reload({ waitUntil: 'load' });
122
- await waitFor(page, () => !!document.querySelector('nav.c3tabs [role="tab"]'));
123
- ok('desk: the choice survives a reload', await page.evaluate(() => !document.querySelector('[data-c3ph]')));
124
- const more = await page.$('[data-c3pg="more"]');
125
- if (more) {
126
- await more.click();
127
- const item = await page.$('[data-a="phcomp"]');
128
- ok('desk: More offers Phone companion', !!item);
129
- if (item) { await item.click(); ok('desk: Phone companion brings the companion back', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment]'), null, 5000)); }
130
- } else ok('desk: the More button is found', false);
172
+ ok('nodesk: an old Full console choice is cleared and the companion shows', await waitFor(page, () => !!document.querySelector('[data-c3ph-moment]') && !localStorage.getItem('pmos.c3.phone'), null, 8000));
131
173
  await page.close();
132
174
  await pctx.close();
133
175
 
176
+ /* ── 620 wide: console3's phone styling starts at 640, so the companion must too (no squeezed desktop between) ── */
177
+ const mctx = await ctxOf(620);
178
+ page = await open(mctx, '/c3.html?demo=1');
179
+ ok('phone: the companion shows at 620 wide', await page.evaluate(shown, '[data-c3ph]'));
180
+ await mctx.close();
181
+
134
182
  /* ── desktop ── */
135
183
  const dctx = await ctxOf(1280);
136
184
  page = await open(dctx, '/c3.html?demo=1', () => !!document.querySelector('nav.c3tabs [role="tab"]'));
@@ -9,7 +9,7 @@
9
9
  * more Backlog, Releases, Docs and Sprints show only after More; each still opens
10
10
  * calendar one Layers button shows and hides the layer chips
11
11
  * reports no deck is drawn
12
- * phone at 400 wide with Full console chosen, nothing changes: the title shows and no dot is drawn (c3-phone-test.js tests the companion)
12
+ * phone at 400 wide on the desktop layout (lib/c3-full-console.js), nothing changes: the title shows and no dot is drawn (c3-phone-test.js tests the companion)
13
13
  * No script error may be raised.
14
14
  */
15
15
  const path = require('path');
@@ -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,13 @@ 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
+ A phone always shows the companion. There is no way to the desktop pages on a phone: use your computer for them. In a window wider than 640 pixels the console always shows the desktop pages.
50
+
51
+ **Open the meeting companion** opens the meeting helper over the companion. To go back, press **Back to your day** at the top or the bottom. The meeting continues to record.
42
52
 
43
53
  ## Limits
44
54
 
@@ -1,13 +1,13 @@
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
4
- * "Full console", which stores pmos.c3.phone = 'desk' on the device. fullConsole(browser) makes every new context start with
5
- * that choice, so a suite that resizes to 400 wide still tests the desktop layout. c3-phone-test.js tests the companion itself. */
3
+ * Since c3-phone-companion, console3 at 640px or less shows the phone companion. The PM has no way to the desktop layout there
4
+ * (Full console was dropped on 2026-10-13); pmos.c3.testDesk = 'desk' keeps it, for suites only. fullConsole(browser) makes every
5
+ * new context start with it, 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) {
7
7
  const make = browser.newContext.bind(browser);
8
8
  browser.newContext = async (...args) => {
9
9
  const ctx = await make(...args);
10
- await ctx.addInitScript(() => { try { localStorage.setItem('pmos.c3.phone', 'desk'); } catch { /* no storage */ } });
10
+ await ctx.addInitScript(() => { try { localStorage.setItem('pmos.c3.testDesk', 'desk'); } catch { /* no storage */ } });
11
11
  return ctx;
12
12
  };
13
13
  return browser;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.181.0",
3
+ "version": "0.183.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.181.0",
9
+ "version": "0.183.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.181.0",
3
+ "version": "0.183.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 } });
@@ -1,5 +1,5 @@
1
1
  /* pm/c3-focus.js — the Meeting focus page of console3 (initiative c3-meeting-focus; the mockup the PM approved on 2026-10-12, version 7).
2
- * openFocus({ event, date, pop, meet, mtitle }) closeFocus()
2
+ * openFocus({ event, date, pop, back, meet, mtitle }) closeFocus()
3
3
  * One calm full-screen page over console3, inside #c3-root: it covers the header, the tabs and the dock (they stay in the DOM, set inert) and Leave
4
4
  * focus gives them back. Recording is not touched by leaving. It reads GET /api/meeting-focus every 3 seconds while it is open:
5
5
  * header title, time, people, initiative; the clock (starts in, or elapsed and left); REC with Start recording or End meeting (the note dock);
@@ -12,7 +12,8 @@
12
12
  * In demo mode (?demo=1) c3-demo-focus.js answers the reads with the mockup's Partner API review.
13
13
  * Compact mode (?pop=1, initiative c3-meet-extension): the page the Chrome extension frames over a Google Meet call. Only a head (PMOS, the topic or "Starts in",
14
14
  * the time left, a fold button), What's being said, the conversation and a foot (Open focus in PMOS, only you see this). The stage, the bar of the talk, the now line and
15
- * the captured items are not drawn, and there is no Leave focus. ?focus=now first asks GET /api/meeting-focus/now (meet = the Meet code, mtitle = the tab's title) which
15
+ * the captured items are not drawn, and there is no Leave focus. With back (the phone companion opens it so) the head and the foot have
16
+ * "Back to your day" in place of Open focus in PMOS, which closes the page as Leave focus does. ?focus=now first asks GET /api/meeting-focus/now (meet = the Meet code, mtitle = the tab's title) which
16
17
  * meeting the call is; when none fits, the page says so and lists today's meetings to pick from. Fold shows the head only and tells the frame's parent with one
17
18
  * message, { pmosFocus: 'fold' | 'open' }, that carries no data. */
18
19
  import { el } from './dom.js';
@@ -80,7 +81,7 @@ export function closeFocus() {
80
81
  if (f.opener && document.contains(f.opener) && typeof f.opener.focus === 'function') { try { f.opener.focus({ preventScroll: true }); } catch { /* not focusable any more */ } }
81
82
  }
82
83
 
83
- export function openFocus({ event, date, pop, meet, mtitle } = {}) {
84
+ export function openFocus({ event, date, pop, back, meet, mtitle } = {}) {
84
85
  closeFocus();
85
86
  const ev = event || {};
86
87
  const want = String(ev.id || '');
@@ -102,7 +103,8 @@ export function openFocus({ event, date, pop, meet, mtitle } = {}) {
102
103
  el('div', { class: 'fc-popbody', 'data-fc': 'pop-body' },
103
104
  r.band = el('div', { class: 'fc-bandwrap' }), convo,
104
105
  r.foot = el('div', { class: 'fc-foot', 'data-fc': 'pop-foot' },
105
- lk({ 'data-fc': 'pop-open', title: 'Opens the full focus page in a new tab' }, icon('out', 'sm'), 'Open focus in PMOS'), el('span', { class: 'dim mono' }, 'only you see this'))))
106
+ back ? pbtn('ghost', 'Back to your day', 'prev', { 'data-fc': 'leave', title: 'Back to the companion. The meeting keeps recording.' })
107
+ : lk({ 'data-fc': 'pop-open', title: 'Opens the full focus page in a new tab' }, icon('out', 'sm'), 'Open focus in PMOS'), el('span', { class: 'dim mono' }, 'only you see this'))))
106
108
  : el('div', { class: 'fc-page' },
107
109
  r.top = el('header', { class: 'card fc-top', 'aria-label': 'Meeting' }),
108
110
  r.now = el('div', { class: 'fc-nowl', 'data-fc': 'now-line' }),
@@ -112,7 +114,7 @@ export function openFocus({ event, date, pop, meet, mtitle } = {}) {
112
114
  el('aside', { class: 'fc-w', 'aria-label': 'Conversation' }, convo)));
113
115
  root.append(page);
114
116
  F = { id, date: day, ev0: ev, root, r, sig: {}, data: null, offset: 0, ui: { open: new Set(), folded: new Set(), pulled: [], stage: 0, picker: false, newTopic: false, addTopic: false, heldOpen: false, pending: new Map(), failed: new Map(), pill: false },
115
- pop: !!pop, meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
117
+ pop: !!pop, back: !!back, meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
116
118
  timers: {}, askTimers: new Map(), sheet: null, sheetOpen: false, status: 'reading', error: '',
117
119
  opener: document.activeElement && document.activeElement !== document.body ? document.activeElement : null, seenNotes: 0 };
118
120
  // a fixed box inside an element with container-type is placed against that element, not the screen: lift it while the page is open
@@ -232,7 +234,7 @@ function drawPopHead(d, ph) {
232
234
  : ph === 'after' ? (e.title || 'Meeting') + ' has ended' : cur ? cur.text : e.title;
233
235
  const left = d && ph === 'during' && Number.isFinite(f) ? el('span', { class: 'mono dim', 'data-fc': 'left', title: 'Time left' }, clockText((f - nowMs()) / 1000)) : null;
234
236
  const lbl = F.ui.pill ? 'Open' : 'Fold to a pill';
235
- return [el('b', { class: 'mono' }, 'PMOS'), el('span', { class: 'fc-ph-topic', 'data-fc': 'pop-topic', title: typeof label === 'string' ? label : '' }, label), left,
237
+ return [F.back ? ib('Back to your day', 'prev', { 'data-fc': 'leave' }) : null, el('b', { class: 'mono' }, 'PMOS'), el('span', { class: 'fc-ph-topic', 'data-fc': 'pop-topic', title: typeof label === 'string' ? label : '' }, label), left,
236
238
  ib(lbl, F.ui.pill ? 'next' : 'fold', { 'data-fc': 'pill', 'aria-pressed': String(F.ui.pill) })];
237
239
  });
238
240
  }
@@ -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
+ }
@@ -4,10 +4,14 @@
4
4
  * Everything is scoped under .c3 and uses tokens only. */
5
5
  .c3.c3-phone-on .c3grid{display:none !important}
6
6
  .c3.c3-phone-on{min-height:100dvh}
7
- .c3 .c3ph{display:flex;flex-direction:column;min-height:100dvh;background:var(--background)}
7
+ .c3 .c3ph{display:flex;flex-direction:column;min-height:100dvh;width:100%;max-width:100vw;min-width:0;overflow-x:hidden;background:var(--background)}
8
+ /* a long title or a long list of people wraps or is cut with an ellipsis; it never widens the companion past the screen */
9
+ .c3 .c3ph *{min-width:0}
10
+ .c3 .c3ph-body,.c3 .c3ph-card,.c3 .c3ph-later,.c3 .c3ph-fit,.c3 .c3ph-sheetbody{grid-template-columns:minmax(0,1fr)}
11
+ .c3 .c3ph-big,.c3 .c3ph-tt,.c3 .c3ph-line,.c3 .c3ph-hd b{overflow-wrap:anywhere}
8
12
  .c3 .c3ph-hd{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:calc(10px + env(safe-area-inset-top)) 16px 10px;position:sticky;top:0;background:var(--background);z-index:2}
9
- .c3 .c3ph-hd b{font-size:var(--fs-4)}
10
- .c3 .c3ph-hdr{display:flex;align-items:center;gap:8px}
13
+ .c3 .c3ph-hd b{font-size:var(--fs-4);flex:1;min-width:0}
14
+ .c3 .c3ph-hdr{display:flex;align-items:center;gap:8px;flex:none}
11
15
  .c3 .c3ph-dotn{display:inline-flex;align-items:center;gap:4px;font:var(--fs-1) var(--mono);color:var(--muted-foreground)}
12
16
  .c3 .c3ph-body{flex:1;display:grid;gap:12px;align-content:start;padding:0 12px 16px}
13
17
  .c3 .c3ph-card{display:grid;gap:8px;padding:14px}
@@ -20,15 +24,13 @@
20
24
  .c3 .c3ph-rec-why{font-size:var(--fs-2);line-height:1.45;background:var(--muted);border-radius:8px;padding:8px 10px}
21
25
  .c3 .c3ph-prep{display:flex;gap:6px;align-items:flex-start;font-size:var(--fs-2);line-height:1.45}
22
26
  .c3 .c3ph-before,.c3 .c3ph-fit{display:grid;gap:6px;padding-top:8px;border-top:1px solid var(--border);font-size:var(--fs-2)}
23
- .c3 .c3ph-before{grid-template-columns:1fr auto;align-items:center}
27
+ .c3 .c3ph-before{grid-template-columns:minmax(0,1fr) auto;align-items:center}
24
28
  .c3 .c3ph-acts{display:grid;gap:8px}
25
29
  .c3 .c3ph-acts.two{grid-template-columns:1fr 1fr}
26
30
  .c3 .c3ph-b{min-height:44px;justify-content:center}
27
31
  .c3 .c3ph-cnt{display:flex;justify-content:space-between;font-weight:600;font-size:var(--fs-2)}
28
32
  .c3 .c3ph-cnt .dim{font-weight:400;font-family:var(--mono)}
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}
33
+ .c3 .c3ph-later{display:grid;overflow:hidden;gap:4px;font-size:var(--fs-2);color:var(--muted-foreground);padding:0 4px}
32
34
  .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
35
  .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
36
  .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,17 +3,22 @@
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).
15
20
  * While it records, the bar shows the clock and Stop. In demo mode it drives the simulated recorder and saves nothing.
16
- * "Full console" switches this device to the desktop layout (kept on the device); the desktop More menu has "Phone companion" to come back.
21
+ * There is no way to the desktop pages on a phone. The meeting companion opens as the compact focus page with "Back to your day".
17
22
  * The moment is worked out again every minute; when it changes, a short banner says so. ?at=HH:MM sets the clock in demo mode only. */
18
23
  import { el } from './dom.js';
19
24
  import { button, iconButton } from './primitives.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() {
@@ -37,10 +46,11 @@ function ensureCss() {
37
46
  document.head.appendChild(el('link', { id: CSS_ID, rel: 'stylesheet', href: '/pm/c3-phone.css' }));
38
47
  }
39
48
 
40
- /** Is this device in phone mode? Phone width, and the PM has not chosen the full console on it. */
49
+ /** Is this device in phone mode? Phone width: there is no choice to show the desktop pages on a phone (Wawan, 2026-10-13).
50
+ * pmos.c3.testDesk is for the browser suites that test the desktop layout at phone width (lib/c3-full-console.js); nothing offers it. */
41
51
  export function phoneWanted(store) {
42
- if (typeof matchMedia !== 'function' || !matchMedia('(max-width:600px)').matches) return false;
43
- return !(store && store.get('pmos.c3.phone') === 'desk');
52
+ if (typeof matchMedia !== 'function' || !matchMedia('(max-width:640px)').matches) return false;
53
+ return !(store && store.get('pmos.c3.testDesk') === 'desk');
44
54
  }
45
55
 
46
56
  export function mountPhone(root, ctx = {}) {
@@ -90,6 +100,20 @@ export function mountPhone(root, ctx = {}) {
90
100
  }
91
101
  }
92
102
  if (S.clock) S.clock.textContent = st.mode === 'recording' && st.started ? clockOf(dock.now() - st.started) : st.mode === 'paused' ? 'paused' : '';
103
+ if (st.mode !== 'idle' && st.id && !S.liveBusy && (!S.live || S.live.id !== st.id || Date.now() - S.live.at > LIVE_MS)) readLive(st);
104
+ if (st.mode === 'idle' && S.live) { S.live = null; draw(); }
105
+ }
106
+ /* ── the note being recorded (GET /api/notes/one): what was just heard, and the decisions, actions and questions picked out so far ── */
107
+ async function readLive(st) {
108
+ S.liveBusy = true;
109
+ try {
110
+ const [src, dn, vn] = await Promise.all([demo ? import('./c3-demo-phone.js') : null, import('./dest-notes.js'), import('./view-notes.js')]);
111
+ const one = demo ? src.demoLive(st) : await pmGet('/api/notes/one?id=' + encodeURIComponent(st.id));
112
+ const n = one && one.ok !== false ? one.note : null;
113
+ S.live = { id: st.id, at: Date.now(), note: n, parts: n ? dn.partsOf(n) : null, speakerOf: vn.speakerOf };
114
+ } catch { S.live = { id: st.id, at: Date.now(), note: null, parts: null }; }
115
+ S.liveBusy = false;
116
+ draw();
93
117
  }
94
118
 
95
119
  /* ── writes ── */
@@ -146,24 +170,68 @@ export function mountPhone(root, ctx = {}) {
146
170
  const card = (hero, attrs, ...kids) => el('section', { class: 'card c3ph-card' + (hero ? ' lead' : ''), ...(attrs || {}) }, ...kids);
147
171
  const btn = (label, variant, icon, attrs, onClick) => button({ label, variant, icon, attrs: { ...(attrs || {}), class: 'c3ph-b' }, onClick });
148
172
  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;
173
+ const own = () => ((S.page && S.page.needs) || []).filter((n) => n && n.id && n.kind !== 'decision' && !S.gone.has(n.id));
174
+ /* everything that waits on the PM, as the desktop Briefing's Needs you counts it (c3-brief-needs.js needsCount): the own items of
175
+ * GET /api/brief-page, then the briefing's needs (assigned Jira and WBS items, own Work items, morning brief items, decisions). */
176
+ function waitRows() {
177
+ const out = []; const seen = new Set();
178
+ 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,
179
+ 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' }); });
180
+ ((S.briefing && S.briefing.needs) || []).forEach((n, i) => {
181
+ if (!n) return;
182
+ const pm = n.origin === 'work' && !!n.itemId;
183
+ const id = pm ? n.itemId : String(n.key || 'need' + i);
184
+ if (seen.has(id) || (n.itemId && seen.has(n.itemId)) || S.gone.has(id)) return;
185
+ seen.add(id);
186
+ if (n.card) {
187
+ const v = cardView(n.card);
188
+ 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: [] });
189
+ return;
190
+ }
191
+ 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(' · '),
192
+ why: n.why, url: n.url, urlLabel: n.url_label, facts: (n.facts || []).map((f) => f && f.text).filter(Boolean) });
193
+ });
194
+ return out;
195
+ }
196
+ const decisions = () => waitRows().filter((r) => !r.pm).length;
197
+ const needOpts = { onDone: (r) => itemDone(r), onLater: (r) => itemLater(r, 'later-today') };
198
+ const focus = (m) => import('./c3-focus.js').then((f) => f.openFocus({ event: { id: m.id, start: m.start }, pop: true, back: true })).catch(() => toast('The meeting companion did not open.'));
199
+ /* a meeting's sheet (c3-phone-detail.js): Record it while the meeting has not ended, and the meeting companion */
200
+ function meetingSheet(m) {
201
+ const t = now().getTime(); const ends = Date.parse(m.end || m.start) || 0;
202
+ openMeeting(m, { recording: recState().mode !== 'idle', onRecord: t < ends ? (x) => startRec(x) : null, onFocus: focus });
203
+ }
151
204
 
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')))];
205
+ function liveCard(st, mo) {
206
+ const L = S.live && S.live.id === st.id ? S.live : null;
207
+ const n = L && L.note; const p = L && L.parts;
208
+ 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);
209
+ const heard = n ? (n.transcript || []).filter((x) => x && x.text && !x.dropped).slice(-3) : [];
210
+ 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);
211
+ const ov = p && p.overview ? (p.overview.length > 220 ? p.overview.slice(0, 217) + '…' : p.overview) : '';
212
+ return card(true, { 'data-c3ph-moment': 'live', 'data-c3ph-live': !L ? 'loading' : n ? 'ready' : 'none' },
213
+ why(st.mode === 'paused' ? 'Paused' : 'Recording now'), big(st.title || 'Conversation'),
214
+ ov ? line(ov) : null,
215
+ p ? list('Decisions', p.decisions) : null, p ? list('Actions', p.actions) : null, p ? list('Open questions', p.questions) : null,
216
+ 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)))
217
+ : 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.'),
218
+ ev ? acts(btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': ev.id }, () => focus(ev))) : null);
219
+ }
220
+
221
+ function itemCard(r, hero) {
222
+ if (!r.label) return itemCard(waitRows().find((x) => x.id === r.id) || { ...r, pm: true, label: 'Yours' }, hero);
223
+ const looks = r.raw && r.raw.evidence;
224
+ return [el('span', { class: 'c3ph-step' }, r.label.toUpperCase()), el('b', { class: 'c3ph-tt' }, r.headline),
225
+ r.why ? line(r.why) : null,
226
+ r.pm ? acts(btn(looks ? 'Confirm done' : 'Done', hero ? 'primary' : 'outline', 'check', { 'data-c3ph-done': r.id }, () => itemDone(r)),
227
+ btn('Later today', 'ghost', 'clock', { 'data-c3ph-later': r.id }, () => itemLater(r, 'later-today')))
228
+ : acts(btn('Details', 'outline', 'chevron-right', { 'data-c3ph-details': r.id }, () => openNeed(r, needOpts)))];
158
229
  }
159
230
 
160
231
  function momentCard(mo) {
161
232
  const m = mo.meeting;
162
233
  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));
234
+ const open = btn('Open the meeting companion', 'outline', 'target', { 'data-c3ph-focus': m.id }, () => focus(m));
167
235
  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
236
  return card(true, { 'data-c3ph-moment': 'meeting' }, why('Starting now' + (who(m) ? ' · ' + who(m) : '')), big('Record ' + m.title + '?'),
169
237
  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 +245,8 @@ export function mountPhone(root, ctx = {}) {
177
245
  ...before.map((x) => el('div', { class: 'c3ph-before', 'data-c3ph-before': x.id }, el('span', null, 'Before you go: ' + x.title),
178
246
  btn('Done', 'outline', 'check', { 'data-c3ph-done': x.id }, () => itemDone({ id: x.id, headline: x.title })))),
179
247
  !(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': '' }));
248
+ line('When it starts, PMOS asks to record it.', { 'data-c3ph-recnote': '' }),
249
+ acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': m.id }, () => meetingSheet(m))));
181
250
  }
182
251
  if (mo.kind === 'after') {
183
252
  const db = debriefOf(m);
@@ -208,37 +277,42 @@ export function mountPhone(root, ctx = {}) {
208
277
  ...(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
278
  }
210
279
  if (mo.kind === 'morning') {
211
- const o = own();
280
+ const w = waitRows(); const late = w.filter((r) => /overdue|late/i.test(r.label)).length;
212
281
  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(' ')));
282
+ line([mo.next ? 'First up, ' + inText((Date.parse(mo.next.start) - now().getTime()) / 60000) + '.' : null,
283
+ 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(' ')),
284
+ mo.next ? acts(btn('The brief of this meeting', 'outline', 'calendar', { 'data-c3ph-meeting-open': mo.next.id }, () => meetingSheet(mo.next))) : null);
214
285
  }
215
- const o = own();
286
+ const o = waitRows().filter((r) => r.pm);
216
287
  return card(true, { 'data-c3ph-moment': 'wrap' }, why('Wrapping up'), big(o.length ? o.length + ' still open today' : 'Nothing left for today'),
217
288
  line(decisions() ? decisions() + (decisions() === 1 ? ' decision waits' : ' decisions wait') + ' for your desk. It opens first on your Briefing.' : 'Nothing waits for your desk.'));
218
289
  }
219
290
 
220
291
  function waitingCard(mo) {
221
292
  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));
293
+ const all = waitRows().filter((r) => !skipIds.has(r.id));
294
+ const list = all.filter((r) => !S.skipped.has(r.id));
223
295
  if (!list.length) return null;
224
296
  const n = list[0];
225
297
  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);
298
+ ...itemCard(n, false),
299
+ el('div', { class: 'c3ph-acts two' },
300
+ 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,
301
+ all.length > 1 ? button({ label: 'See all ' + all.length, variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-all-open': '' }, onClick: () => openAll(all, needOpts) }) : null));
227
302
  }
228
303
 
229
304
  function laterList(mo) {
230
305
  const rows = (mo.later || []).slice(0, 3);
231
306
  if (!rows.length) return null;
232
307
  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))));
308
+ ...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
309
  }
235
310
 
236
311
  function headLine() {
237
312
  const d = now();
238
- const n = own().length;
313
+ const n = waitRows().length;
239
314
  head.replaceChildren(el('b', null, WD[d.getDay()] + ' ' + d.getDate() + ' ' + MO[d.getMonth()]),
240
- el('span', { class: 'c3ph-hdr' }, n ? el('span', { class: 'c3ph-dotn', 'data-c3ph-count': String(n) }, c3ic('inbox', 'sm'), n + ' waiting') : null,
241
- button({ label: 'Full console', variant: 'ghost', size: 'sm', attrs: { 'data-c3ph-desk': '' }, onClick: () => ctx.onDesk && ctx.onDesk() })));
315
+ el('span', { class: 'c3ph-hdr' }, n ? el('span', { class: 'c3ph-dotn', 'data-c3ph-count': String(n) }, c3ic('inbox', 'sm'), n + ' waiting') : null));
242
316
  }
243
317
 
244
318
  const BANNER = { meeting: (m) => m.title + ' has started', before: (m, mo) => m.title + ' starts ' + inText(mo.minutes), after: (m) => m.title + ' just ended', gap: (m, mo) => (mo.minutes || 0) + ' free minutes', wrap: () => 'Time to wrap up', morning: () => 'Good morning' };
@@ -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(); });
@@ -138,7 +138,6 @@ function toggleMore() {
138
138
  item('alert', 'Incident day', () => toggleIncident(), S.incident, S.demo ? { 'data-a': 'inc' } : { 'data-a': 'inc', disabled: true, title: 'No incident feed is connected, so this page shows no incident.' }),
139
139
  item('spark', 'Agent playbooks', () => openPlaybooks(), false, { 'data-a': 'pbopen' }),
140
140
  item('phone', 'Phone preview', () => togglePreview(), !!S.preview, { 'data-a': 'phone' }),
141
- S.root.classList.contains('ph') && store.get('pmos.c3.phone') === 'desk' ? item('phone', 'Phone companion', () => { store.set('pmos.c3.phone', ''); syncPhone(); }, false, { 'data-a': 'phcomp' }) : null,
142
141
  item(S.theme === 'dark' ? 'sun' : 'moon', S.theme === 'dark' ? 'Light theme' : 'Dark theme', () => setTheme(S.theme === 'dark' ? 'light' : 'dark')),
143
142
  ((b) => { b.firstChild.replaceWith(gearIcon('sm')); return b; })(item('info', 'Settings', () => showTab('settings', true), false, { 'data-a': 'settings' })),
144
143
  item('term', S.dockOpen ? 'Fold the dock' : 'Open the dock', () => { S.dockOpen = !S.dockOpen; keepDock(); syncDock(); }, false, { 'data-a': 'dockt' }, 'Ctrl J'),
@@ -151,13 +150,14 @@ function toggleMore() {
151
150
  document.addEventListener('keydown', S.menuOff); document.addEventListener('pointerdown', S.menuOff);
152
151
  const first = S.menu.querySelector('.c3pg-mi:not([disabled])'); if (first) first.focus();
153
152
  }
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
155
- * desktop grid is hidden. "Full console" stores that choice; the More menu's "Phone companion" clears it. */
153
+ /* The phone companion (c3-phone.js): at 640px or less it fills the page and the desktop grid is hidden. There is no Full console:
154
+ * the old choice (pmos.c3.phone = 'desk', stored before 2026-10-13) is cleared so no phone stays on the desktop pages. */
156
155
  function syncPhone() {
156
+ if (store.get('pmos.c3.phone')) store.set('pmos.c3.phone', '');
157
157
  const want = phoneWanted(store);
158
158
  if (want && !S.phone) {
159
159
  S.root.classList.add('c3-phone-on');
160
- S.phone = mountPhone(S.root, { ws: S.ws, demo: S.demo, onDesk: () => { store.set('pmos.c3.phone', 'desk'); syncPhone(); } });
160
+ S.phone = mountPhone(S.root, { ws: S.ws, demo: S.demo });
161
161
  } else if (!want && S.phone) {
162
162
  S.phone.dispose(); S.phone = null;
163
163
  S.root.classList.remove('c3-phone-on');
@@ -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) });