create-pmos 0.190.0 → 0.191.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 +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-phone-test.js +22 -1
- package/workspace/guide/phone-companion.md +4 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-focus.css +51 -0
- package/workspace/public/pm/c3-focus.js +83 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.191.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": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T07:07:45Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T07:07:45Z"}
|
|
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-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-13T07:07:45Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* nodesk there is no Full console; a phone that chose it before 2026-10-13 shows the companion again
|
|
17
17
|
* focus Record on a meeting opens focus in one column: the clock, the now line, Topics, Captured, Note and Your slides as sheets,
|
|
18
18
|
* a context chip in a sheet, Ask and Stop at the foot, nothing wider than the screen; Back to your day closes it and recording goes on
|
|
19
|
+
* two sides focus is two fixed halves that never scroll (v6): the room's latest line large over the one before it, and PMOS's newest
|
|
20
|
+
* note alone below; ‹ › steps back through earlier notes, "N newer" returns; held notes open in a sheet and pull one in
|
|
19
21
|
* narrow a long title and a long list of people never widen the companion past the screen
|
|
20
22
|
* desktop at 1280 wide no companion is drawn
|
|
21
23
|
* 620 the companion shows up to 640 wide, where the phone styling starts
|
|
@@ -141,7 +143,8 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
141
143
|
await waitFor(page, () => !document.querySelector('[data-shell="sheet"] [data-fc="note-sheet"]'), null, 3000);
|
|
142
144
|
await page.fill('[data-c3focus] [data-fc="ask-input"]', 'What did Budi ask?');
|
|
143
145
|
await page.press('[data-c3focus] [data-fc="ask-input"]', 'Enter');
|
|
144
|
-
ok('record: Ask PMOS quietly
|
|
146
|
+
ok('record: Ask PMOS quietly shows the answer with your question, as the newest note', await waitFor(page, () => { const s = document.querySelector('[data-c3focus] [data-fc="stream"]'); const at = s?.querySelector('[data-fc="pg-at"]')?.textContent || ''; const k = at.match(/^(\d+) of (\d+)$/);
|
|
147
|
+
return /You asked: What did Budi ask/.test(s?.textContent || '') && !!k && k[1] === k[2]; }, null, 5000));
|
|
145
148
|
await page.click('[data-c3focus] [data-fc="leave"]');
|
|
146
149
|
ok('record: Back to your day closes focus and keeps recording', await waitFor(page, () => !document.querySelector('[data-c3focus]') && !!document.querySelector('[data-c3ph-recbar] [data-c3ph-stop]'), null, 5000));
|
|
147
150
|
ok('record: the bar has Focus', !!(await page.$('[data-c3ph-recbar] [data-c3ph-focus-bar]')));
|
|
@@ -172,6 +175,24 @@ const writes = (p) => (window.__c3demoWrites || []).filter((w) => w.path === p |
|
|
|
172
175
|
ok('focus: no stage on the page itself, no desktop Leave focus or End meeting', await page.evaluate(() => !document.querySelector('[data-c3focus] [data-fc="stage"]') && !/Leave focus|End meeting/.test(document.querySelector('[data-c3focus]').textContent)));
|
|
173
176
|
ok('focus: nothing is wider than the phone', await page.evaluate(() => { const f = document.querySelector('[data-c3focus]'); return f.scrollWidth <= innerWidth + 1 && [...f.querySelectorAll('*')].every((n) => n.getBoundingClientRect().right <= innerWidth + 1 || n.closest('.fc-cxs')); }),
|
|
174
177
|
await page.evaluate(() => [...document.querySelectorAll('[data-c3focus] *')].filter((n) => n.getBoundingClientRect().right > innerWidth + 1 && !n.closest('.fc-cxs')).slice(0, 3).map((n) => n.className + ':' + Math.round(n.getBoundingClientRect().right)).join(' ')));
|
|
178
|
+
ok('two sides: the room shows its latest line large, the one before it faint', await page.evaluate(() => { const r = document.querySelector('[data-c3focus] [data-fc="room"]'); const now = r && r.querySelector('[data-fc="said-now"]'); const prev = r && r.querySelector('[data-fc="said-prev"]');
|
|
179
|
+
return !!now && !!prev && parseFloat(getComputedStyle(now).fontSize) > parseFloat(getComputedStyle(prev).fontSize) && !!r.querySelector('[data-fc="gist"]'); }));
|
|
180
|
+
ok('two sides: PMOS shows one note, the newest, with a pager', await page.evaluate(() => { const m = document.querySelector('[data-c3focus] [data-fc="pmos"]'); const at = m.querySelector('[data-fc="pg-at"]')?.textContent || ''; const k = at.match(/^(\d+) of (\d+)$/);
|
|
181
|
+
return m.querySelectorAll('[data-fc="note-card"]').length === 1 && !!k && k[1] === k[2] && +k[2] > 1; }));
|
|
182
|
+
ok('two sides: neither half scrolls and the page fits the screen', await page.evaluate(() => { const f = document.querySelector('[data-c3focus]'); const ft = f.querySelector('[data-fc="phone-foot"]').getBoundingClientRect();
|
|
183
|
+
return ['room', 'pmos'].every((k) => { const n = f.querySelector('[data-fc="' + k + '"]'); return n.scrollHeight <= n.clientHeight + 1 && getComputedStyle(n).overflowY !== 'auto'; }) && ft.bottom <= innerHeight + 1; }));
|
|
184
|
+
const firstNote = await page.textContent('[data-c3focus] [data-fc="note-card"] .fc-b');
|
|
185
|
+
await page.click('[data-c3focus] [data-fc="pg-prev"]');
|
|
186
|
+
ok('two sides: ‹ steps back to an earlier note and offers the newer one', await waitFor(page, (t) => { const c = document.querySelector('[data-c3focus] [data-fc="note-card"] .fc-b'); return !!c && c.textContent !== t && !!document.querySelector('[data-c3focus] [data-fc="pg-newest"]'); }, firstNote, 3000));
|
|
187
|
+
await page.click('[data-c3focus] [data-fc="pg-newest"]');
|
|
188
|
+
ok('two sides: "newer" goes back to the newest note', await waitFor(page, (t) => document.querySelector('[data-c3focus] [data-fc="note-card"] .fc-b')?.textContent === t && !document.querySelector('[data-c3focus] [data-fc="pg-newest"]'), firstNote, 3000));
|
|
189
|
+
if (await page.$('[data-c3focus] [data-fc="held"]')) {
|
|
190
|
+
const before = await page.textContent('[data-c3focus] [data-fc="pg-at"]');
|
|
191
|
+
await page.click('[data-c3focus] [data-fc="held"]');
|
|
192
|
+
ok('two sides: the held chip opens what PMOS is holding', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="held-sheet"] [data-fc="pull"]'), null, 3000));
|
|
193
|
+
await page.click('[data-shell="sheet"] [data-fc="pull"]');
|
|
194
|
+
ok('two sides: pulling one in shows it as the newest note', await waitFor(page, (b) => { const at = document.querySelector('[data-c3focus] [data-fc="pg-at"]')?.textContent || ''; const k = at.match(/^(\d+) of (\d+)$/); return !document.querySelector('[data-shell="sheet"] [data-fc="held-sheet"]') && at !== b && !!k && k[1] === k[2] && /early/.test(document.querySelector('[data-c3focus] [data-fc="note-card"]')?.textContent || ''); }, before, 3000));
|
|
195
|
+
} else ok('two sides: the demo meeting holds a note', false);
|
|
175
196
|
ok('focus: the address does not change', await page.evaluate(() => !new URL(location.href).searchParams.has('focus')));
|
|
176
197
|
await page.click('[data-c3focus] [data-fc="topics"]');
|
|
177
198
|
ok('focus: Topics opens as a sheet over focus', await waitFor(page, () => !!document.querySelector('[data-shell="sheet"] [data-fc="topics-sheet"]') && !!document.querySelector('[data-c3focus]'), null, 5000));
|
|
@@ -46,14 +46,14 @@ The red button records a conversation or a meeting with the same recorder as **R
|
|
|
46
46
|
|
|
47
47
|
## Focus
|
|
48
48
|
|
|
49
|
-
Focus
|
|
49
|
+
Focus shows the meeting on one screen. You do not scroll it. From the top:
|
|
50
50
|
|
|
51
51
|
- **Back to your day**, the title, and the clock: the time since the start and the time left. Under them, the bar of what the room talked about.
|
|
52
52
|
- The topic that the room is on now, and how long it has been on it.
|
|
53
53
|
- **Topics**, **Captured**, **Note** and **Your slides** (or **Material**). Each one opens a sheet over focus. **Note** shows what the live note has found so far, or the last lines of the transcript.
|
|
54
|
-
- **What's being said**, with context. Tap a context to see its detail.
|
|
55
|
-
-
|
|
56
|
-
- At the bottom, **Ask PMOS quietly** and **Stop**. Only you see your question and the answer. **Stop** asks before it stops the recording.
|
|
54
|
+
- **The room**: the last line that was said, in large text, with the line before it in faint text. It changes when someone speaks. Under it, **What's being said**, with context. Tap a context to see its detail.
|
|
55
|
+
- **PMOS, to you**: only the newest note that PMOS wrote for you, in large text, with the line that it answers. Tap **‹** to see earlier notes. If a new note comes while you look at an earlier one, tap **1 newer** to go to it. PMOS holds some notes for the right moment. Tap **Holding** to see them, and tap **Now** to see one now.
|
|
56
|
+
- At the bottom, **Ask PMOS quietly** and **Stop**. Only you see your question and the answer. The answer shows as the newest note, under your question. **Stop** asks before it stops the recording.
|
|
57
57
|
|
|
58
58
|
A recording that is not on the calendar, for example a talk in the corridor, opens the same focus without topics and slides.
|
|
59
59
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.191.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.191.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -251,3 +251,54 @@
|
|
|
251
251
|
[data-shell="sheet"] [data-fc="stage"] .fc-card { display: flex; gap: var(--sp-2); align-items: center; margin-top: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: 8px; background: var(--card); flex-wrap: wrap; }
|
|
252
252
|
[data-shell="sheet"] [data-fc="stage"] .fc-card .nb { display: grid; gap: 2px; flex: 1 1 160px; min-width: 0; overflow-wrap: anywhere; }
|
|
253
253
|
[data-shell="sheet"] [data-fc="stage"] .seg { flex-wrap: wrap; }
|
|
254
|
+
/* phone mode, two sides (v6): the room on top, PMOS to you below, each showing only its latest. Nothing scrolls. */
|
|
255
|
+
.c3focus.fc-phone .fc-page { gap: 6px; }
|
|
256
|
+
.c3focus.fc-phone .fc-side { display: flex; align-items: center; gap: 6px; font-size: 11px; letter-spacing: .6px; text-transform: uppercase; color: var(--muted-foreground); flex: none; }
|
|
257
|
+
.c3focus.fc-phone .fc-side .fc-dot { width: 6px; height: 6px; }
|
|
258
|
+
.c3focus.fc-phone .fc-room, .c3focus.fc-phone .fc-me { min-height: 0; display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; border-radius: 8px; overflow: hidden; }
|
|
259
|
+
.c3focus.fc-phone .fc-room { flex: 1.15 1 0; border: 1px solid var(--border); background: var(--card); }
|
|
260
|
+
.c3focus.fc-phone .fc-said { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; gap: 4px; overflow: hidden; container-type: size; }
|
|
261
|
+
.c3focus.fc-phone .fc-said p { margin: 0; flex: none; }
|
|
262
|
+
.c3focus.fc-phone .fc-sp { font-size: var(--fs-1); color: var(--muted-foreground); }
|
|
263
|
+
.c3focus.fc-phone .fc-said-prev { display: flex; gap: 6px; align-items: baseline; opacity: .55; font-size: var(--fs-2); min-width: 0; }
|
|
264
|
+
.c3focus.fc-phone .fc-said-prev.you { justify-content: flex-end; }
|
|
265
|
+
.c3focus.fc-phone .fc-said-prev .fc-st { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
266
|
+
.c3focus.fc-phone .fc-said-now { display: grid; gap: 1px; font-size: 20px; line-height: 1.3; font-weight: 500; animation: fcsaidin .35s ease-out; }
|
|
267
|
+
.c3focus.fc-phone .fc-said-now .fc-sp { font-size: var(--fs-2); font-weight: 600; color: var(--foreground); }
|
|
268
|
+
.c3focus.fc-phone .fc-said-now .fc-st { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
269
|
+
.c3focus.fc-phone .fc-said-now.you { text-align: right; }
|
|
270
|
+
.c3focus.fc-phone .fc-said-now.you .fc-sp { color: var(--acc); }
|
|
271
|
+
.c3focus.fc-phone .fc-said-now.dim { font-size: var(--fs-2); font-weight: 400; }
|
|
272
|
+
@container (max-height: 96px) { .c3focus.fc-phone .fc-said-prev { display: none; } }
|
|
273
|
+
@keyframes fcsaidin { from { opacity: 0; transform: translateY(6px); } }
|
|
274
|
+
.c3focus.fc-phone .fc-room .fc-bandwrap { flex: none; border-top: 1px solid var(--border); padding-top: 6px; }
|
|
275
|
+
.c3focus.fc-phone .fc-room .fc-gist { padding: 0; border: 0; background: none; }
|
|
276
|
+
.c3focus.fc-phone .fc-room .fc-gist p > span { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
277
|
+
.c3focus.fc-phone .fc-room .fc-gist p > .ic { display: none; }
|
|
278
|
+
.c3focus.fc-phone .fc-room .fc-gl { display: inline; font-size: 11px; letter-spacing: .6px; text-transform: uppercase; }
|
|
279
|
+
.c3focus.fc-phone .fc-me { flex: 1.5 1 0; background: color-mix(in oklab, var(--acc) 9%, var(--card)); border: 1px solid color-mix(in oklab, var(--acc) 30%, var(--border)); }
|
|
280
|
+
.c3focus.fc-phone .fc-me.fc-arr { animation: fcarr .9s ease-out; }
|
|
281
|
+
@keyframes fcarr { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--acc) 45%, transparent); } 100% { box-shadow: 0 0 0 10px transparent; } }
|
|
282
|
+
@media (prefers-reduced-motion: reduce) { .c3focus.fc-phone .fc-me.fc-arr, .c3focus.fc-phone .fc-said-now { animation: none; } }
|
|
283
|
+
.c3focus.fc-phone .fc-one { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
|
|
284
|
+
.c3focus.fc-phone .fc-one > * { flex: none; }
|
|
285
|
+
.c3focus.fc-phone .fc-mh { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
286
|
+
.c3focus.fc-phone .fc-mh .fc-side { color: var(--acc); }
|
|
287
|
+
.c3focus.fc-phone .fc-pg { display: flex; align-items: center; gap: 2px; font-size: var(--fs-1); color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
|
288
|
+
.c3focus.fc-phone .fc-pg .ib { width: 32px; height: 32px; min-width: 0; padding: 0; }
|
|
289
|
+
.c3focus.fc-phone .fc-pg .ib:disabled { opacity: .3; }
|
|
290
|
+
.c3focus.fc-phone .fc-onote { flex: 1 1 auto !important; min-height: 0; display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
|
|
291
|
+
.c3focus.fc-phone .fc-onote > * { flex: none; }
|
|
292
|
+
.c3focus.fc-phone .fc-cath { display: flex; align-items: center; gap: 8px; font-size: var(--fs-2); }
|
|
293
|
+
.c3focus.fc-phone .fc-cath b { color: var(--acc); }
|
|
294
|
+
.c3focus.fc-phone .fc-newer { margin-left: auto; min-height: 28px; padding: 2px 10px; border-radius: 6px; background: var(--acc); color: var(--background); font-size: var(--fs-1); font-weight: 600; }
|
|
295
|
+
.c3focus.fc-phone .fc-onote .fc-re { font-size: var(--fs-1); font-style: italic; color: var(--muted-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
296
|
+
.c3focus.fc-phone .fc-one .fc-b { margin: 0; font-size: 16px; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
|
297
|
+
.c3focus.fc-phone .fc-onote .fc-acts2 { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
298
|
+
.c3focus.fc-phone .fc-onote .fc-meta { font-size: var(--fs-1); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
299
|
+
.c3focus.fc-phone .fc-onote .fc-meta > span + span::before { content: ' · '; }
|
|
300
|
+
.c3focus.fc-phone .fc-dots { display: flex; justify-content: center; gap: 5px; margin-top: auto; }
|
|
301
|
+
.c3focus.fc-phone .fc-pdot { width: 6px; height: 6px; border-radius: 50%; background: color-mix(in oklab, var(--acc) 25%, transparent); }
|
|
302
|
+
.c3focus.fc-phone .fc-on.fc-pdot { background: var(--acc); }
|
|
303
|
+
.c3focus.fc-phone .fc-heldc { align-self: flex-start; min-height: 30px; padding: 2px 10px; border-radius: 6px; border: 1px dashed color-mix(in oklab, var(--acc) 45%, var(--border)); font-size: var(--fs-1); color: var(--muted-foreground); }
|
|
304
|
+
.c3focus.fc-phone:not([data-fc-state="ok"]) .fc-room, .c3focus.fc-phone:not([data-fc-state="ok"]) .fc-me { display: none; }
|
|
@@ -110,7 +110,12 @@ export function openFocus({ event, date, pop, back, phone, note, meet, mtitle }
|
|
|
110
110
|
? el('div', { class: 'fc-page' },
|
|
111
111
|
r.top = el('header', { class: 'fc-phd', 'data-fc': 'phone-head' }),
|
|
112
112
|
r.now = el('div', { class: 'fc-phnow', 'data-fc': 'now-line' }),
|
|
113
|
-
|
|
113
|
+
el('section', { class: 'fc-room', 'data-fc': 'room', 'aria-label': 'The room' },
|
|
114
|
+
el('span', { class: 'fc-side mono' }, el('span', { class: 'fc-dot' }), 'The room'),
|
|
115
|
+
r.tick = el('div', { class: 'fc-said', 'aria-live': 'polite', 'data-fc': 'ticker' }),
|
|
116
|
+
r.band = el('div', { class: 'fc-bandwrap' })),
|
|
117
|
+
r.me = el('section', { class: 'fc-me', 'data-fc': 'pmos', 'aria-label': 'PMOS to you. Only you see it.' },
|
|
118
|
+
r.stream = el('div', { class: 'fc-one', 'data-fc': 'stream' })),
|
|
114
119
|
el('div', { class: 'fc-phft', 'data-fc': 'phone-foot' }, r.form, r.act = el('span', { class: 'fc-phact' })))
|
|
115
120
|
: pop
|
|
116
121
|
? el('div', { class: 'fc-page' },
|
|
@@ -128,7 +133,7 @@ export function openFocus({ event, date, pop, back, phone, note, meet, mtitle }
|
|
|
128
133
|
el('div', { class: 'fc-s' }, r.stage = el('div', { class: 'fc-stagewrap' }), r.cap = el('div', { class: 'fc-capwrap' })),
|
|
129
134
|
el('aside', { class: 'fc-w', 'aria-label': 'Conversation' }, convo)));
|
|
130
135
|
root.append(page);
|
|
131
|
-
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 },
|
|
136
|
+
F = { id, date: day, ev0: ev, root, r, sig: {}, data: null, offset: 0, ui: { ni: null, 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 },
|
|
132
137
|
pop: !!pop, back: !!back, phone: !!phone, noteId: id ? '' : String(note || ''), meet: String(meet || ''), mtitle: String(mtitle || ''), choices: null,
|
|
133
138
|
timers: {}, askTimers: new Map(), sheet: null, sheetOpen: false, status: 'reading', error: '',
|
|
134
139
|
opener: document.activeElement && document.activeElement !== document.body ? document.activeElement : null, seenNotes: 0 };
|
|
@@ -567,6 +572,7 @@ function askBubble(a, err) {
|
|
|
567
572
|
}
|
|
568
573
|
|
|
569
574
|
function drawConversation(d) {
|
|
575
|
+
if (F.phone) { drawPhRoom(d); drawPhMe(d); return; }
|
|
570
576
|
const tail = (d.tail || []).slice(F.pop ? -1 : -2);
|
|
571
577
|
region('tick', tail, () => tail.map((x) => el('p', null, el('span', { class: 'mono' }, x.who), ' ' + x.text)));
|
|
572
578
|
const notes = d.notes || [];
|
|
@@ -606,6 +612,76 @@ function drawConversation(d) {
|
|
|
606
612
|
F.seenNotes = nNotes;
|
|
607
613
|
}
|
|
608
614
|
|
|
615
|
+
/* ── phone mode: two sides, like a translator app in conversation mode (v6) ─────────────────────────────────────────
|
|
616
|
+
The scrolling conversation was hard to follow live, so nothing scrolls. The room gets the top: its latest line, large,
|
|
617
|
+
and the one before it, faint. PMOS gets the bottom: only its newest note, large. ‹ › steps back through earlier notes;
|
|
618
|
+
a note that arrives while you look back shows as "1 newer". Held notes wait behind one chip. */
|
|
619
|
+
|
|
620
|
+
function drawPhRoom(d) {
|
|
621
|
+
const tail = (d.tail || []).slice(-2);
|
|
622
|
+
region('tick', tail, () => {
|
|
623
|
+
if (!tail.length) return [el('p', { class: 'fc-said-now dim', 'data-fc': 'said-now' }, recState(d) === 'recording' ? 'Listening to the room.' : 'PMOS hears the room when it records.')];
|
|
624
|
+
const line = (x, cls, fc) => el('p', { class: cls + (x.who === 'You' ? ' you' : ''), 'data-fc': fc }, el('span', { class: 'fc-sp mono' }, x.who), el('span', { class: 'fc-st' }, x.text));
|
|
625
|
+
const cur = tail[tail.length - 1]; const prev = tail.length > 1 ? tail[0] : null;
|
|
626
|
+
return [prev ? line(prev, 'fc-said-prev', 'said-prev') : null, line(cur, 'fc-said-now', 'said-now')];
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/** What PMOS has shown you, oldest first: notes, notes you pulled in early, and questions not answered yet (an answer carries its question). */
|
|
630
|
+
function phItems(d) {
|
|
631
|
+
const notes = d.notes || [];
|
|
632
|
+
const pulled = F.ui.pulled.map((id) => notes.find((n) => n.id === id)).filter(Boolean);
|
|
633
|
+
const shown = notes.filter((n) => n.show !== 'hold').concat(pulled.map((n) => ({ ...n, at: F.pulledAt && F.pulledAt[n.id] || n.at, pulledWhy: 'You asked for it early. PMOS was holding it ' + (n.when || 'for later') + '.' })));
|
|
634
|
+
const asks = (d.asks || []).slice();
|
|
635
|
+
for (const [id, p] of F.ui.pending) if (!asks.some((a) => a.id === id)) asks.push({ id, at: p.at, question: p.question, state: 'working' });
|
|
636
|
+
const answered = new Set(shown.map((n) => n.ask).filter(Boolean));
|
|
637
|
+
const qOf = new Map(asks.map((a) => [a.id, a.question]));
|
|
638
|
+
return [...shown.map((n) => ({ kind: 'note', at: n.at, n, q: n.ask ? qOf.get(n.ask) || '' : '' })), ...asks.filter((a) => !answered.has(a.id) && a.state !== 'answered').map((a) => ({ kind: 'ask', at: a.at, a }))]
|
|
639
|
+
.map((x, i) => ({ ...x, i })).sort((a, b) => a.at - b.at || a.i - b.i);
|
|
640
|
+
}
|
|
641
|
+
function drawPhMe(d) {
|
|
642
|
+
const items = phItems(d); const N = items.length; const last = N - 1;
|
|
643
|
+
const i = F.ui.ni == null ? last : Math.max(0, Math.min(F.ui.ni, last));
|
|
644
|
+
const newer = F.ui.ni == null ? 0 : last - i;
|
|
645
|
+
const held = (d.notes || []).filter((n) => n.show === 'hold' && !F.ui.pulled.includes(n.id));
|
|
646
|
+
const comp = d.companion || {}; const recording = recState(d) === 'recording';
|
|
647
|
+
const x = items[i];
|
|
648
|
+
const sig = [items.map((y) => y.kind === 'note' ? [y.n.id, y.n.pulledWhy ? 1 : 0, y.q] : [y.a.id, y.a.state]), i, newer, held.map((n) => n.id), recording, !!comp.on, comp.error || '', [...F.ui.failed]];
|
|
649
|
+
region('stream', sig, () => {
|
|
650
|
+
const head = el('div', { class: 'fc-mh' }, el('span', { class: 'fc-side mono' }, icon('spark', 'sm'), 'PMOS, to you'),
|
|
651
|
+
N > 1 ? el('span', { class: 'fc-pg' }, ib('Earlier note', 'prev', { 'data-fc': 'pg-prev', ...(i === 0 ? { disabled: '' } : {}) }), el('span', { class: 'mono', 'data-fc': 'pg-at' }, (i + 1) + ' of ' + N), ib('Later note', 'next', { 'data-fc': 'pg-next', ...(i === last ? { disabled: '' } : {}) })) : null);
|
|
652
|
+
let body;
|
|
653
|
+
if (!x) body = el('p', { class: 'fc-b dim', 'data-fc': 'listening' }, !recording ? 'PMOS listens when the meeting records.' : comp.on === false ? 'PMOS is listening to the transcript only. The companion is off, so it adds no notes.' : 'Listening. A note shows here when it helps.');
|
|
654
|
+
else if (x.kind === 'ask') {
|
|
655
|
+
const err = F.ui.failed.get(x.a.id);
|
|
656
|
+
body = el('div', { class: 'fc-onote', 'data-fc': 'ask-q' }, el('span', { class: 'fc-re' }, 'You asked: ' + x.a.question),
|
|
657
|
+
el('p', { class: 'fc-b dim', role: 'status' }, err ? 'PMOS could not answer: ' + err : 'PMOS is reading the record.'));
|
|
658
|
+
} else {
|
|
659
|
+
const n = x.n; const why = n.pulledWhy || n.why;
|
|
660
|
+
body = el('div', { class: 'fc-onote', 'data-fc': 'note-card', 'data-cat': n.cat },
|
|
661
|
+
el('span', { class: 'fc-cath' }, el('b', null, n.cat), el('span', { class: 'dim mono' }, hhmm(n.at)),
|
|
662
|
+
newer ? button({ variant: 'ghost', label: [newer + ' newer', icon('next', 'sm')], attrs: { class: 'fc-newer', 'data-fc': 'pg-newest', title: 'Show the newest note' } }) : null),
|
|
663
|
+
x.q ? el('span', { class: 'fc-re' }, 'You asked: ' + x.q) : n.heard ? el('span', { class: 'fc-re' }, n.heard) : null,
|
|
664
|
+
el('p', { class: 'fc-b' }, n.body),
|
|
665
|
+
(n.acts || []).length ? el('div', { class: 'fc-acts2' }, ...n.acts.map(actBtn)) : null,
|
|
666
|
+
el('span', { class: 'fc-meta dim' }, why ? el('span', null, el('b', null, 'Why now'), ' ' + why) : null, n.src ? el('span', null, 'from ' + n.src) : null));
|
|
667
|
+
}
|
|
668
|
+
return [head, body,
|
|
669
|
+
N > 1 ? el('div', { class: 'fc-dots', 'aria-hidden': 'true' }, ...items.map((_, k) => el('i', { class: 'fc-pdot' + (k === i ? ' fc-on' : '') }))) : null,
|
|
670
|
+
held.length ? button({ variant: 'ghost', label: [icon('clock', 'sm'), 'Holding ' + held.length + ' for later'], attrs: { class: 'fc-heldc', 'data-fc': 'held', title: 'What PMOS is holding for the right moment' } }) : null,
|
|
671
|
+
comp.error ? el('p', { class: 'dim fc-note', role: 'status' }, comp.error) : null];
|
|
672
|
+
});
|
|
673
|
+
if (N > F.seenNotes && F.ui.ni == null && F.seenNotes) { const m = F.r.me; m.classList.remove('fc-arr'); void m.offsetWidth; m.classList.add('fc-arr'); }
|
|
674
|
+
F.seenNotes = N;
|
|
675
|
+
}
|
|
676
|
+
function heldBuild() {
|
|
677
|
+
const held = (F.data.notes || []).filter((n) => n.show === 'hold' && !F.ui.pulled.includes(n.id));
|
|
678
|
+
return [el('div', { class: 'fc-heldsheet', 'data-fc': 'held-sheet' },
|
|
679
|
+
el('p', { class: 'dim fc-note' }, 'Each one waits for the moment it fits. Pull one in now if you want it early.'),
|
|
680
|
+
held.length ? el('ul', { class: 'list', role: 'list' }, ...held.map((n) => el('li', { class: 'nrow' }, el('span', { class: 'ki' }, icon('clock')),
|
|
681
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, n.cat), n.when ? el('div', { class: 'dim fc-note' }, 'Shows ' + n.when) : null),
|
|
682
|
+
pbtn('outline', 'Now', null, { 'data-fc': 'pull', 'data-id': n.id, title: 'Show it now' })))) : el('p', { class: 'dim fc-note' }, 'Nothing is held.'))];
|
|
683
|
+
}
|
|
684
|
+
|
|
609
685
|
function askForm() {
|
|
610
686
|
const input = el('input', { type: 'text', 'aria-label': 'Ask PMOS, only you see it', placeholder: 'Ask PMOS quietly', maxlength: '500', autocomplete: 'off', 'data-fc': 'ask-input' });
|
|
611
687
|
const form = el('form', { class: 'fc-in', 'data-fc': 'ask-form' }, input, ib('Ask', 'next', { type: 'submit', 'data-fc': 'ask' }));
|
|
@@ -622,7 +698,7 @@ async function ask(question) {
|
|
|
622
698
|
const r = await pmPost('/api/meeting-focus/ask', body);
|
|
623
699
|
if (F !== f) return;
|
|
624
700
|
if (!r || !r.ok) { toast({ text: (r && r.error) || 'The question was not sent.', tone: 'error' }); return; }
|
|
625
|
-
f.ui.pending.set(r.id, { question, at });
|
|
701
|
+
f.ui.pending.set(r.id, { question, at }); f.ui.ni = null;
|
|
626
702
|
draw();
|
|
627
703
|
const t = setInterval(async () => {
|
|
628
704
|
const s = await pmGet('/api/meeting-focus/ask?id=' + encodeURIComponent(r.id));
|
|
@@ -800,6 +876,9 @@ function onClick(ev) {
|
|
|
800
876
|
if (k === 'open-link' || k === 'present') { openUrl(t.dataset.url); return; }
|
|
801
877
|
if (k === 'start') { startRecording(); return; }
|
|
802
878
|
if (k === 'end') { endMeeting(); return; }
|
|
879
|
+
if (k === 'pg-prev' || k === 'pg-next') { const N = phItems(d).length; const i = F.ui.ni == null ? N - 1 : F.ui.ni; const j = k === 'pg-prev' ? Math.max(0, i - 1) : i + 1; F.ui.ni = j >= N - 1 ? null : j; drawConversation(d); return; }
|
|
880
|
+
if (k === 'pg-newest') { F.ui.ni = null; drawConversation(d); return; }
|
|
881
|
+
if (k === 'held' && F.phone) { showSheet('held', 'PMOS is holding', 'clock', heldBuild); return; }
|
|
803
882
|
if (k === 'fold') { F.ui.folded.add(t.dataset.id); drawConversation(d); return; }
|
|
804
883
|
if (k === 'unfold') { const id = t.dataset.id; if (F.ui.open.has(id)) F.ui.open.delete(id); else F.ui.open.add(id); F.ui.folded.delete(id); drawConversation(d); return; }
|
|
805
884
|
if (k === 'pull') {
|
|
@@ -830,5 +909,6 @@ document.addEventListener('click', (ev) => {
|
|
|
830
909
|
if (k === 'material-pick') { F.ui.stage = Number(t.dataset.i) || 0; refreshSheet(true); return; }
|
|
831
910
|
if (k === 'note-tab') { F.ui.noteTab = t.dataset.tab === 'said' ? 'said' : 'note'; refreshSheet(true); return; }
|
|
832
911
|
if (k === 'stop-keep') { closeSheet(); return; }
|
|
912
|
+
if (k === 'pull') { const id = t.dataset.id; F.ui.pulled.push(id); (F.pulledAt ||= {})[id] = nowMs(); F.ui.ni = null; closeSheet(); drawConversation(F.data); return; }
|
|
833
913
|
if (k === 'stop-ok') { closeSheet(); endMeeting().then(() => { if (F && F.phone) closeFocus(); }); return; }
|
|
834
914
|
});
|