create-pmos 0.502.0 → 0.504.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 +39 -27
- 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/briefing-premium-calendar-2-test.js +4 -3
- package/workspace/briefing-premium-calendar-3-test.js +2 -3
- package/workspace/changelog.json +38 -0
- package/workspace/fixtures/premium-viewer-fixtures.js +3 -2
- package/workspace/guide/briefing-premium.md +1 -1
- package/workspace/guide/home.md +1 -0
- package/workspace/guide/notes.md +21 -1
- package/workspace/lib/meeting-terms.js +53 -0
- package/workspace/lib/routes-meeting-points.js +7 -4
- package/workspace/lib/transcript-read.js +38 -3
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-chat-line-test.js +3 -3
- package/workspace/premium-vw-meet-test.js +28 -17
- package/workspace/public/pm/bd-cols.js +5 -2
- package/workspace/public/pm/bd-deliveries.js +3 -3
- package/workspace/public/pm/bd-pin-card.js +3 -3
- package/workspace/public/pm/bd-pins.js +10 -0
- package/workspace/public/pm/bd-span.js +11 -9
- package/workspace/public/pm/cozy-vw-mtg.css +81 -0
- package/workspace/public/pm/home-line.css +2 -2
- package/workspace/public/pm/home-line.js +42 -13
- package/workspace/public/pm/premium-mtg-replay.js +155 -67
- package/workspace/public/pm/premium-vw-mtg.js +119 -25
|
@@ -43,7 +43,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
43
43
|
const press = (p, sel) => p.$eval('#czViewer ' + sel, (b) => b.click());
|
|
44
44
|
const folds = (p) => p.evaluate(() => [...document.querySelectorAll('#czViewer [data-czs-fold]')].map((x) => x.dataset.czsFold + (x.getAttribute('aria-expanded') === 'true' ? '*' : '')).join());
|
|
45
45
|
const unfold = async (p, id) => { if (await p.evaluate((k) => document.querySelector(`#czViewer [data-czs-fold="${k}"]`).getAttribute('aria-expanded') !== 'true', id)) await press(p, `[data-czs-fold="${id}"]`); };
|
|
46
|
-
const sum = (p) => p.evaluate(() => { const s = document.querySelector('#czViewer .czs-sum'); return s ? { line: s.querySelector('.czs-line').innerText, text: (s.querySelector('.czs-p') || {}).innerText || '', meta: (s.querySelector('.czs-meta') || {}).innerText || '', pri: (s.querySelector('[data-czs-act]') || {}).textContent || '' } : null; });
|
|
46
|
+
const sum = (p) => p.evaluate(() => { const s = [...document.querySelectorAll('#czViewer .czs-sum')].find((x) => x.offsetParent) || document.querySelector('#czViewer .czs-sum'); return s ? { line: (s.querySelector('.czs-line') || {}).innerText || '', text: (s.querySelector('.czs-p') || {}).innerText || '', meta: (s.querySelector('.czs-meta') || {}).innerText || '', pri: (s.querySelector('[data-czs-act]') || {}).textContent || '' } : null; });
|
|
47
47
|
|
|
48
48
|
/* an ended meeting with notes and a recording */
|
|
49
49
|
const A = await F.open(browser, BASE, '/premium-viewer.html?open=meeting¬e=n1');
|
|
@@ -54,9 +54,12 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
54
54
|
|
|
55
55
|
/* S1 summary first */
|
|
56
56
|
const s1 = await sum(p);
|
|
57
|
-
|
|
58
|
-
ok('S1 the summary
|
|
59
|
-
ok('S1
|
|
57
|
+
/* batch 25 item 11 (MvC): the summary first in the left column, the kept points with Confirm N in their header, the evidence, one strip, the docked timeline */
|
|
58
|
+
ok('S1 the summary: the overview and the meta line, no primary (Confirm sits in the kept header)', /opens next to the chat/.test(s1.text) && !s1.pri, JSON.stringify(s1));
|
|
59
|
+
ok('S1 the left column starts with the summary, then the kept points: "N kept" and Confirm N; the evidence and the docked timeline', await p.evaluate(() => { const l = document.querySelector('#czViewer .vzr-l'); return !!l && l.firstElementChild.classList.contains('czs-sum') && /^\d+ kept$/.test(l.querySelector('.vzr-ph h3').textContent) && /^Confirm \d+$/.test(l.querySelector('[data-a="confirmall"]').textContent) && !!document.querySelector('#czViewer .vzr-ev') && !!document.querySelector('#czViewer .vzp-card .vzr-dock.vzp-pl'); }));
|
|
60
|
+
const chips = await p.evaluate(() => [...document.querySelectorAll('#czViewer .vzr-fold [data-a="strip"]')].map((x) => x.dataset.k).join());
|
|
61
|
+
ok('S1 one strip: Left out, Next steps, Since the last one, Facts, Names and terms, Prep and material, Transcript; no tabs, no checkbox, no filter', chips === 'left,steps,since,facts,names,prep,tr' && !(await p.$('#czViewer [role="tablist"]')) && !(await p.$('#czViewer .vzr [role="checkbox"]')) && !(await p.$('#czViewer [data-a="pf"]')), chips);
|
|
62
|
+
ok('S1 the Viewer opens wide for the two columns', await p.evaluate(() => document.getElementById('czViewer').offsetWidth >= 900 && getComputedStyle(document.querySelector('#czViewer .vzr-cols')).gridTemplateColumns.split(' ').length === 2));
|
|
60
63
|
ok('S1 no Detail or Conversation switch, no #dlg; the note was read from GET /api/notes/one', await p.evaluate(() => !document.querySelector('#dlg, #dTabs, .stream')) && A.S.gets.some((g) => g.startsWith('/api/notes/one?id=n1')));
|
|
61
64
|
|
|
62
65
|
/* S2 the header and ⋯ */
|
|
@@ -70,17 +73,17 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
70
73
|
ok('S2 Copy as Markdown copies the notes', /^# Viewer sync/.test(clip || '') && /Diffs can be approved/.test(clip || ''), String(clip).slice(0, 200));
|
|
71
74
|
|
|
72
75
|
/* S3 After */
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(
|
|
76
|
-
ok('S3 the
|
|
76
|
+
const strip = (k) => press(p, `.vzr-fold [data-a="strip"][data-k="${k}"]`);
|
|
77
|
+
await strip('since'); const since = await body(p); await strip('facts'); const facts = await body(p);
|
|
78
|
+
ok('S3 Since the last one and Facts mentioned, Matches and Not checked', /Raised again/.test(since) && /Matches/.test(facts) && /Not checked/.test(facts), facts.slice(0, 600));
|
|
79
|
+
ok('S3 the evidence has the screen captures, with no Waiting for design marker', await until(() => p.evaluate(() => document.querySelectorAll('#czViewer .vzp-card .vzp-th').length > 0 && !document.querySelector('#czViewer [data-cz-placeholder]'))));
|
|
77
80
|
await press(p, '.vzp-card .vzp-th[data-a="seekto"]');
|
|
78
81
|
ok('S3 a thumbnail moves the replay to its time', await until(() => p.evaluate(() => /^03:10/.test((document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent || ''))), await p.evaluate(() => (document.querySelector('#czViewer [data-clk="pos"]') || {}).textContent));
|
|
79
|
-
await
|
|
80
|
-
ok('S3 the Transcript
|
|
82
|
+
await strip('tr');
|
|
83
|
+
ok('S3 the Transcript in the strip shows the lines, Dita named', await until(async () => /The Viewer sits beside the chat now/.test(await body(p)) && /Dita/.test(await body(p))));
|
|
81
84
|
|
|
82
85
|
/* S4 Make task: a work-item becomes V.4 Drafted in PMOS */
|
|
83
|
-
await
|
|
86
|
+
await strip('steps');
|
|
84
87
|
await until(() => p.evaluate(() => !!document.querySelector('#czViewer [data-a="makewi"][data-i="0"]')));
|
|
85
88
|
ok('S4 each step offers Make task and an icon-only work-item press', await p.evaluate(() => !!document.querySelector('#czViewer [data-a="maketask"][data-i="0"]') && !document.querySelector('#czViewer [data-a="makewi"][data-i="0"]').textContent.trim()));
|
|
86
89
|
await press(p, '[data-a="makewi"][data-i="0"]');
|
|
@@ -90,9 +93,10 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
90
93
|
ok('S4 Make task writes one plan item for the step', await until(() => A.S.posts.some((x) => x.path === '/api/plan-items' && JSON.stringify(x.body).includes('Move saved views'))));
|
|
91
94
|
|
|
92
95
|
/* S5 a correction, and what PMOS learns (XvAReplayEdit) */
|
|
93
|
-
await
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
const pid = await p.evaluate(() => { const r = document.querySelectorAll('#czViewer .vzr-row [data-a="psel"]')[1]; r.click(); return r.dataset.id; });
|
|
97
|
+
ok('S5 pressing a row selects it and the evidence follows it: its time, its quote', await until(() => p.evaluate((id) => !!document.querySelector(`#czViewer .vzr-row.sel [data-a="psel"][data-id="${id}"]`) && !!document.querySelector(`#czViewer .vzr-ev [data-a="pedit"][data-id="${id}"]`), pid)), pid);
|
|
98
|
+
await press(p, '.vzr-ev [data-a="pedit"]');
|
|
99
|
+
ok('S5 Fix in the evidence opens the editor in place: the kinds, the words, what PMOS will remember', !!pid && await until(() => p.evaluate(() => !!document.querySelector('#czViewer .vzp-ed [data-a="etext"]') && document.querySelectorAll('#czViewer .vzp-ed [data-a="ekind"]').length === 3 && document.querySelector('#czViewer .vzp-ed [data-a="esave"]').disabled)), pid);
|
|
96
100
|
const kNow = await p.evaluate(() => document.querySelector('#czViewer .vzp-ed [data-a="ekind"][aria-checked="true"]').dataset.k);
|
|
97
101
|
const kNew = kNow === 'question' ? 'action' : 'question';
|
|
98
102
|
await press(p, `.vzp-ed [data-a="ekind"][data-k="${kNew}"]`);
|
|
@@ -103,15 +107,22 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
103
107
|
ok('S5 the page says Fixed and what it will remember, with Undo', await until(async () => /Fixed\./.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))), (await body(p)).slice(0, 300));
|
|
104
108
|
await press(p, '.vz-msg [data-a="msgundo"]');
|
|
105
109
|
ok('S5 Undo forgets the lesson (POST /api/meeting-points/forget)', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 'l0123456789')), JSON.stringify(A.S.posts.map((x) => x.path)));
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
/* MvF: a word heard wrong, selected in the quote, fixed in every place; Undo forgets it */
|
|
111
|
+
await p.evaluate(() => { const q = document.querySelector('#czViewer .vzr-ev .vzr-q'); const tn = [...q.childNodes].find((x) => x.nodeType === 3 && /print/.test(x.textContent)) || q.firstChild; const i = tn.textContent.indexOf('print'); const r = document.createRange(); r.setStart(tn, i); r.setEnd(tn, i + 5); const s = getSelection(); s.removeAllRanges(); s.addRange(r); q.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); });
|
|
112
|
+
ok('S5 a selected word becomes a field with ✓', await until(() => p.evaluate(() => { const f = document.querySelector('#czViewer .vzr-fe'); return !!f && f.dataset.w === 'print' && document.activeElement === f.querySelector('input'); })));
|
|
113
|
+
await p.fill('#czViewer .vzr-fe input', 'Print'); await p.press('#czViewer .vzr-fe input', 'Enter');
|
|
114
|
+
ok('S5 Enter posts the word fix and it shows fixed, with Undo', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/term' && x.body.heard === 'print' && x.body.means === 'Print')) && await until(async () => !!(await p.$('#czViewer .vzr-fx')) && /Fixed/.test(await body(p)) && !!(await p.$('#czViewer .vz-msg [data-a="msgundo"]'))));
|
|
115
|
+
await press(p, '.vz-msg [data-a="msgundo"]');
|
|
116
|
+
ok('S5 Undo forgets the word fix', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points/forget' && x.body.lesson === 't0123456789')) && await until(async () => !(await p.$('#czViewer .vzr-fx'))));
|
|
117
|
+
await press(p, '[data-a="confirmall"]');
|
|
118
|
+
ok('S5 Confirm N in the kept header confirms what is left to confirm', await until(() => A.S.posts.some((x) => x.path === '/api/meeting-points' && x.body.verdict === 'confirmed' && x.body.ids.length > 0)), JSON.stringify(A.S.posts.filter((x) => x.path === '/api/meeting-points')));
|
|
108
119
|
|
|
109
120
|
/* S8 series membership */
|
|
110
121
|
await F.more(p, 'Not part of this series');
|
|
111
122
|
ok('S8 ⋯ Not part of this series posts key off', await until(() => A.S.posts.some((x) => x.path === '/api/notes/series-member' && x.body.key === 'off')));
|
|
112
123
|
|
|
113
124
|
/* S9 writes: only the presses */
|
|
114
|
-
const allowed = ['/api/viewer-meeting/work-item', '/api/plan-items', '/api/notes/series-member', '/api/meeting-points/edit', '/api/meeting-points/forget', '/api/meeting-points'];
|
|
125
|
+
const allowed = ['/api/meeting-points/term', '/api/viewer-meeting/work-item', '/api/plan-items', '/api/notes/series-member', '/api/meeting-points/edit', '/api/meeting-points/forget', '/api/meeting-points'];
|
|
115
126
|
ok('S9 the page wrote only what was pressed', A.S.posts.every((x) => allowed.includes(x.path)), JSON.stringify(A.S.posts.map((x) => x.path)));
|
|
116
127
|
ok('S9 no page error', A.errors.length === 0, A.errors.join(' | '));
|
|
117
128
|
await A.ctx.close();
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* · On a narrow Board the columns drop from right to left until the timeline keeps MIN_TRACK px. A phone has no columns (StsA3Phone).
|
|
11
11
|
* Pure markup and state; pm/bd-span.js draws with it, pm/premium-board.js wires the menu and the sort. */
|
|
12
12
|
import { esc, store } from './premium-shell.js';
|
|
13
|
+
import { endStage } from './bd-pins.js';
|
|
13
14
|
import { stageOfRow, wfOf, wfIcon, catOfRow, catLabel, CAT_ORDER } from './bd-wf.js';
|
|
14
15
|
import { offTone, md, dy } from './bd-span.js';
|
|
15
16
|
|
|
@@ -58,13 +59,15 @@ export function setSort(k) { sortBy = byK[k] ? k : null; }
|
|
|
58
59
|
const midnight = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
59
60
|
const day = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
|
|
60
61
|
/** A row's next milestone: the earliest milestone not done (a missed one first, it is the earliest), else the end the row stands for
|
|
61
|
-
* (Deliver, or Evaluate past it). → { name, d, late, over } or null. over: days past the date. */
|
|
62
|
+
* (its workflow's ◆ stage: Deliver, or Evaluate past it, on Product feature; none on a workflow with no ◆ stage). → { name, d, late, over } or null. over: days past the date. */
|
|
62
63
|
export function nextMs(r, now = new Date()) {
|
|
63
64
|
const t = midnight(now);
|
|
64
65
|
const M = (r.milestones || []).filter((m) => !m.done && day(m.date)).sort((a, b) => day(a.date) - day(b.date));
|
|
65
66
|
let x = M.length ? { name: M[0].name, d: day(M[0].date) } : null;
|
|
66
67
|
const k = r.track ? r.track.k : -1;
|
|
67
|
-
|
|
68
|
+
const es = endStage(r);
|
|
69
|
+
// a row with dated milestones is named by them (batch 25 item 13: never Deliver beside a named milestone); the plain end only without any
|
|
70
|
+
if (!x && es && !(r.milestones || []).some((m) => day(m.date)) && day(r.end) && k < 8 && !(r.track && r.track.prog && r.track.prog.total > 0 && r.track.prog.done >= r.track.prog.total)) x = { name: es, d: day(r.end) };
|
|
68
71
|
if (!x) return null;
|
|
69
72
|
const over = Math.round((t - x.d) / 864e5);
|
|
70
73
|
return { ...x, late: over > 0, over: Math.max(0, over) };
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* · A phone shows the next 3 above the rows (missed first), with "All".
|
|
13
13
|
* · Nothing to show (Filter = None, nothing pinned): no lane at all.
|
|
14
14
|
* Pure apart from the remembered fold and wire(), which hooks the hover and the fold onto the Board's body. */
|
|
15
|
-
import { shownMs, msMode } from './bd-pins.js';
|
|
15
|
+
import { shownMs, msMode, endStage } from './bd-pins.js';
|
|
16
16
|
import { faceOr } from './cozy-adler.js'; // Adler once in the lane's header when a delivery is missed (batch 17 item 4-A, canvas AdM2)
|
|
17
17
|
const TICK = '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l5 5 9-10"/></svg>'; // the delivered mark, drawn (conformance R28-13: no glyph controls)
|
|
18
18
|
|
|
@@ -54,7 +54,7 @@ export function setFolded(on) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const openHold = (r) => (Array.isArray(r.holds) ? r.holds : []).find((h) => h && day(h.from) && !day(h.to)) || null;
|
|
57
|
-
const endName =
|
|
57
|
+
const endName = endStage; // its workflow's ◆ stage; '' when the workflow has none (batch 25 item 13)
|
|
58
58
|
/** The slip in words: "+1 wk", "+3d". */
|
|
59
59
|
const slipWords = (n) => (n >= 7 ? '+' + Math.round(n / 7) + ' wk' : '+' + n + 'd');
|
|
60
60
|
|
|
@@ -83,7 +83,7 @@ export function entriesOf(o) {
|
|
|
83
83
|
};
|
|
84
84
|
const dated = (r.milestones || []).filter((m) => m && day(m.date));
|
|
85
85
|
if (dated.length) { for (const m of shownMs(r, mode)) one(m.id, m.name, day(m.date), m.done, atEnd(day(m.date))); continue; }
|
|
86
|
-
if (e) { one('', endName(r), e, !!(r.track && (r.track.k >= 6 && endName(r) === 'Deliver')), true); continue; }
|
|
86
|
+
if (e && endName(r)) { one('', endName(r), e, !!(r.track && (r.track.k >= 6 && endName(r) === 'Deliver')), true); continue; }
|
|
87
87
|
// only Jira has a date (XcalmA6Grill 8): "≈" and the forecast; PMOS never presents a forecast as a commitment
|
|
88
88
|
if (fc) out.push({ ...base, id: '', name: r.title, date: fc, done: false, late: false, risk: false, fc: true, planned: null, slip: 0 });
|
|
89
89
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { pmPost } from './http.js';
|
|
8
8
|
import { esc } from './premium-shell.js';
|
|
9
9
|
import * as Pop from './bd-pop.js';
|
|
10
|
-
import { isPinned, shownMs, msMode, setMsMode, MS_MODES } from './bd-pins.js';
|
|
10
|
+
import { isPinned, shownMs, msMode, setMsMode, MS_MODES, endStage } from './bd-pins.js';
|
|
11
11
|
|
|
12
12
|
const PIN = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 17v5M8 3h8l-1 5 3 4H6l3-4z"/></svg>';
|
|
13
13
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
@@ -27,9 +27,9 @@ function cardHtml(r, msId) {
|
|
|
27
27
|
if (!L.length) return `<p class="bd-pc-h">${esc(r.title)}</p><p class="bd-amut bd-pc-sub">No milestone yet. Flag a task as a milestone ◇ in the initiative's Tasks.</p>`;
|
|
28
28
|
const m0 = L.find((m) => m.id === msId) || null, on = shownMs(r).length;
|
|
29
29
|
const head = m0 ? `<p class="bd-pc-h">${esc(m0.name)}</p><p class="bd-amut bd-pc-sub">${esc(when(m0.date))} · ${on} of ${L.length} ${L.length === 1 ? 'milestone' : 'milestones'} on the timeline</p>`
|
|
30
|
-
: r.end && /^\d{4}-\d{2}-\d{2}/.test(String(r.end))
|
|
30
|
+
: r.end && /^\d{4}-\d{2}-\d{2}/.test(String(r.end)) && endStage(r)
|
|
31
31
|
// the row's plain end ◇ (no milestone task stands for it): the card says what the row's label says, "Deliver Jun 9 ’27" (batch 14 item 1)
|
|
32
|
-
? `<p class="bd-pc-h">${esc(r
|
|
32
|
+
? `<p class="bd-pc-h">${esc(endStage(r))} ${esc(when(String(r.end).slice(0, 10)))}</p><p class="bd-amut bd-pc-sub">The end of ${esc(r.title)} · ${on} of ${L.length} ${L.length === 1 ? 'milestone' : 'milestones'} on the timeline</p>`
|
|
33
33
|
: `<p class="bd-pc-h">${esc(r.title)}</p><p class="bd-amut bd-pc-sub">${on} of ${L.length} ${L.length === 1 ? 'milestone' : 'milestones'} on the timeline</p>`;
|
|
34
34
|
const row = (m) => {
|
|
35
35
|
const p = isPinned(r, m), late = !m.done && m.date < today;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the last one. Filter switches the whole Board: "Milestones on the timeline: Pinned · All · None", remembered per person.
|
|
4
4
|
* isPinned(r, m) a milestone's own pin (m.pinned true or false) wins; else only the row's last dated milestone is pinned
|
|
5
5
|
* shownMs(r, mode) the row's dated milestones the timeline draws, by date
|
|
6
|
+
* endStage(r) the name of the row's plain end ◇: Deliver or Evaluate on Product feature, the own ◆ stage elsewhere, else ''
|
|
6
7
|
* msMode() · setMsMode the Filter choice: 'pinned' (the default), 'all' or 'none'
|
|
7
8
|
* Pure apart from the remembered choice. */
|
|
8
9
|
const MODE_KEY = 'pmos.board.msMode';
|
|
@@ -22,6 +23,15 @@ export function shownMs(r, mode = msMode()) {
|
|
|
22
23
|
const M = datedMs(r);
|
|
23
24
|
return mode === 'all' ? M : M.filter((m) => isPinned(r, m));
|
|
24
25
|
}
|
|
26
|
+
/** What a row's plain end ◇ stands for when no milestone task names it (batch 25 item 13). Product feature: its lifecycle's Deliver, or
|
|
27
|
+
* Evaluate past it. Another workflow (D99): its own ◆ stage, the first one not done, else the last. A workflow with no ◆ stage: '' (zero is
|
|
28
|
+
* silence: no labelled ◇, never a lifecycle name that the workflow does not have). */
|
|
29
|
+
export function endStage(r) {
|
|
30
|
+
if (!r || !r.wf || r.wf.id === 'product-feature') return r && r.track && r.track.k >= 6 ? 'Evaluate' : 'Deliver';
|
|
31
|
+
const S = (Array.isArray(r.wfStages) ? r.wfStages : []).filter((s) => s && s.milestone && s.status !== 'skipped');
|
|
32
|
+
const x = S.find((s) => s.status !== 'done') || S[S.length - 1];
|
|
33
|
+
return x ? String(x.name || '') : '';
|
|
34
|
+
}
|
|
25
35
|
let MODE = null;
|
|
26
36
|
export function msMode() {
|
|
27
37
|
if (MODE === null) { let v = null; try { v = localStorage.getItem(MODE_KEY); } catch { v = null; } MODE = MS_MODES.some(([k]) => k === v) ? v : 'pinned'; }
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* the hairline and » only; its row sheet lists them. A gap never changes the row's dot or risk.
|
|
29
29
|
* Calmer timeline (feedback batch 13 item 4, A: XcalmA1Desk, A4Dark, A3Phone, A2Folded, A5Milestones): say it once, few marks, quiet rows.
|
|
30
30
|
* · One label per bar end: what lands, then when ("Tender award Nov 3"; the year only when it is not this year). The name is the end milestone's,
|
|
31
|
-
* else a milestone close enough to merge with the end ◇, else the row's latest milestone;
|
|
31
|
+
* else a milestone close enough to merge with the end ◇, else the row's latest milestone; a row with none says the ◆ stage of its own workflow (Deliver or Evaluate on Product feature), and on a workflow with no ◆ stage draws no labelled ◇ (batch 25 item 13). The date turns amber at risk and orange late; the rest (days over, the forecast, a slip, a clash) is its hover.
|
|
32
32
|
* With no room at the right edge the label sits before the ◇, on a card chip. No date under a ◇ and no second date.
|
|
33
33
|
* · Marks: the bar (done dark, to do light), a 2 px hairline for any gap or hold (a hold's hairline opens the hold card), one ◇ (filled done,
|
|
34
34
|
* orange late), the dashed Jira forecast, Today. The dotted hold, ⏸, the amber tint, the blue ◆ and the hatched out-of-window band are gone.
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { esc } from './premium-shell.js';
|
|
47
47
|
import { mergeClose } from './program-model.js';
|
|
48
48
|
import { stagesOfRow } from './bd-wf.js';
|
|
49
|
-
import { shownMs } from './bd-pins.js';
|
|
49
|
+
import { shownMs, endStage } from './bd-pins.js';
|
|
50
50
|
|
|
51
51
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
52
52
|
const day = (v) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(v || '')); return m ? new Date(+m[1], +m[2] - 1, +m[3]) : null; };
|
|
@@ -195,8 +195,8 @@ function extentOf(r, now) {
|
|
|
195
195
|
|
|
196
196
|
/** A date as the calm timeline says it (batch 13 item 4): "Nov 3", and the year only when it is not this year ("Jun 10 ’27"). */
|
|
197
197
|
export const dy = (d, now) => md(d) + (d.getFullYear() !== now.getFullYear() ? ' ’' + String(d.getFullYear()).slice(2) : '');
|
|
198
|
-
/** What lands at a row's end: its end milestone's name, else the stage the end stands for. */
|
|
199
|
-
const landsOf = (r, et) => (et ? et.name : r
|
|
198
|
+
/** What lands at a row's end: its end milestone's name, else the ◆ stage of its own workflow the end stands for, else '' (no labelled ◇). */
|
|
199
|
+
const landsOf = (r, et) => (et ? et.name : endStage(r));
|
|
200
200
|
/** The one label at a bar's end (XcalmA1Desk): what lands, then when; tone '' | 'risk' | 'late' | 'q' colours the date. At the right edge, with
|
|
201
201
|
* no room after the mark, it sits before it on a card chip. hover: the words a pointer reads (days over, the forecast, a slip, a clash). */
|
|
202
202
|
function endLabel(at, name, d, tone, now, pxPct, hover, slip) {
|
|
@@ -235,6 +235,8 @@ function qBar(r, X, now, phone, fmt, pxPct, ms = {}) {
|
|
|
235
235
|
// the ◇ and its label carry the milestone's own date; the initiative's end, when it differs, is the hover (batch 14 item 1)
|
|
236
236
|
const ed = et ? day(et.date) : e; if (+ed !== +e) hover.push('Initiative ends ' + dy(e, now));
|
|
237
237
|
const name = landsOf(r, et), eTitle = name + ' · ' + md(ed) + (et && et.stage ? ' · ◆ ' + et.stage : '') + (et && et.done ? ' · done' : isLate ? ' · late' : '');
|
|
238
|
+
// nothing lands (batch 25 item 13): a workflow with no ◆ stage and no dated milestone draws no ◇ and no label; the bar's own end says it
|
|
239
|
+
if (!name) return out + planMarks(r, X, phone, fmt);
|
|
238
240
|
out += `<button type="button" class="tg-end${et && et.done ? ' dn' : ''}${isLate ? ' late' : ''}" data-ms-pick="${esc(r.key)}" data-ms-id="${esc(et ? et.id : '')}" style="left:${P(X(ed))}" aria-haspopup="dialog" aria-label="${esc(eTitle + '. Milestones of ' + r.title)}" title="${esc(eTitle + (clash ? '. ' + clash : ''))}"></button>`;
|
|
239
241
|
// with the Next milestone column on, the column names it and the bar's end has no label (StsA8Board); the ◇ keeps the words as its hover
|
|
240
242
|
if (!ms.noLabel) out += endLabel(Math.max(X(e), X(ed), over ? X(now) : 0, late ? X(fc) : 0, fr ? X(fr.high) : 0), name, ed, isLate ? 'late' : risk, now, pxPct, hover, slipHtml(r));
|
|
@@ -244,7 +246,7 @@ function qBar(r, X, now, phone, fmt, pxPct, ms = {}) {
|
|
|
244
246
|
out += draftBar(r, X, phone, fmt, now, pxPct);
|
|
245
247
|
} else {
|
|
246
248
|
const d = s || e;
|
|
247
|
-
out += `<button type="button" class="tg-end" data-ms-pick="${esc(r.key)}" data-ms-id="" style="left:${P(X(d))}" aria-haspopup="dialog" aria-label="${esc((s ? 'Starts ' : 'Ends ') + md(d) + '. Milestones of ' + r.title)}" title="${s ? 'Starts' : 'Ends'} ${esc(md(d))}"></button>` + (e && !ms.noLabel ? endLabel(X(d), landsOf(r, et), d, risk, now, pxPct, [], '') : '');
|
|
249
|
+
out += `<button type="button" class="tg-end" data-ms-pick="${esc(r.key)}" data-ms-id="" style="left:${P(X(d))}" aria-haspopup="dialog" aria-label="${esc((s ? 'Starts ' : 'Ends ') + md(d) + '. Milestones of ' + r.title)}" title="${s ? 'Starts' : 'Ends'} ${esc(md(d))}"></button>` + (e && !ms.noLabel && landsOf(r, et) ? endLabel(X(d), landsOf(r, et), d, risk, now, pxPct, [], '') : '');
|
|
248
250
|
}
|
|
249
251
|
return out + planMarks(r, X, phone, fmt);
|
|
250
252
|
}
|
|
@@ -348,7 +350,7 @@ function gapsHtml(r, X, now, phone, fmt, pxPct, s, e, tone, note = '', lands = n
|
|
|
348
350
|
}
|
|
349
351
|
// the end ◇'s own name and date when a milestone task is it (the timeline-label hotfix: never a stage name for a milestone)
|
|
350
352
|
const ob = H.find((x) => x.kind === 'open' && x.b > (lands ? lands.d : e));
|
|
351
|
-
const clash = ob ? (lands ? lands.name : r
|
|
353
|
+
const clash = ob ? (lands ? lands.name : endStage(r) || 'The end') + ' is before the return, ' + md(ob.b) : null;
|
|
352
354
|
return { html, clash };
|
|
353
355
|
}
|
|
354
356
|
const iso = (d) => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
@@ -363,7 +365,7 @@ const msStage = (r, m) => { if (!m.sec) return ''; const st = (r.wfStages || [])
|
|
|
363
365
|
* zoom's ~40 px: the timeline-label hotfix, "[Tender] 12.0 Award contract" on Nov 2 sat under the end ◇ labelled "Deliver Nov 4"; one mark says
|
|
364
366
|
* the milestone's own name and date), else the row's latest milestone task however far from the end (Wawan 2026-10-28, "End always names a
|
|
365
367
|
* milestone": a plain "Deliver Nov 11" whose card listed only "Tech blueprint approved, Apr 29" read as two answers). It takes that
|
|
366
|
-
* task's name, date and done, and is not drawn twice. Only a row with no dated milestone says
|
|
368
|
+
* task's name, date and done, and is not drawn twice. Only a row with no dated milestone says its workflow's ◆ stage (pm/bd-pins.js endStage). M: its dated milestones. */
|
|
367
369
|
function endTask(r, M, reach = 0) {
|
|
368
370
|
const e = day(r.end); if (!e || !M.length) return null;
|
|
369
371
|
const S = M.slice().sort((a, b) => day(a.date) - day(b.date));
|
|
@@ -404,9 +406,9 @@ export function laneMilestones(rows, now, reach = 0) {
|
|
|
404
406
|
for (const r of rows || []) {
|
|
405
407
|
const M = (r.milestones || []).filter((m) => day(m.date)), s = day(r.start), e = day(r.end), et = endTask(r, M, reach);
|
|
406
408
|
for (const m of M) if (!et || m.id !== et.id) out.push({ id: m.id, name: m.name, date: m.date, done: !!m.done, late: msLate(m, now), row: r });
|
|
407
|
-
if (!s || !e) continue;
|
|
409
|
+
if (!s || !e || (!et && !endStage(r))) continue;
|
|
408
410
|
const ob = openBack(r, now), late = (r.over && now > e) || (ob && ob > e) || (et && msLate(et, now));
|
|
409
|
-
out.push({ id: et ? et.id : '', name: et ? et.name : r
|
|
411
|
+
out.push({ id: et ? et.id : '', name: et ? et.name : endStage(r), date: et ? String(et.date).slice(0, 10) : iso(e), done: !!(et && et.done), late: !!late, row: r });
|
|
410
412
|
}
|
|
411
413
|
return out.sort((a, b) => day(a.date) - day(b.date));
|
|
412
414
|
}
|
|
@@ -228,6 +228,87 @@
|
|
|
228
228
|
.vzp-del:hover { color: var(--cz-ink); }
|
|
229
229
|
.vzp-gbtn { min-height: 34px; padding: 0 12px !important; border-radius: 999px; font-size: var(--fs-3); font-weight: 700; }
|
|
230
230
|
|
|
231
|
+
/* ── the recording view (batch 25 item 11, MvC/MvF/MvN): summary and lean kept rows left, the evidence right, the timeline docked ── */
|
|
232
|
+
/* the shell's wide grid (summary left, 75ch right) does not apply: this view is its own two columns */
|
|
233
|
+
.cz-vw-w2 .cz-vw-body:has(> .vz-rec) { display: block; }
|
|
234
|
+
.cz-vw-body > .vz.vz-rec { max-width: none; }
|
|
235
|
+
.vzr { container-type: inline-size; padding: 0; background: none; gap: 14px; }
|
|
236
|
+
.vzr-mbonly { display: none; }
|
|
237
|
+
.vzr-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
238
|
+
@container (min-width: 860px) { .vzr-cols { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: 20px; } .vzr-ev { position: sticky; top: 0; } }
|
|
239
|
+
.vzr-l { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
|
240
|
+
.vzr-sum .czs-p { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: var(--fs-3); line-height: 1.55; margin: 0; }
|
|
241
|
+
.vzr-sum.all .czs-p { display: block; }
|
|
242
|
+
.vzr-sm { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
243
|
+
.vzr-sm .czs-meta { margin: 0 0 0 auto; }
|
|
244
|
+
.vzr-more2 { font-size: var(--fs-2); font-weight: 800; color: var(--cz-blueink); padding: 4px 0 !important; }
|
|
245
|
+
.vzr-pts { border: 1px solid var(--cz-hair); border-radius: 20px; background: var(--cz-card); overflow: hidden; }
|
|
246
|
+
.vzr-ph { display: flex; align-items: center; gap: 12px; padding: 12px 14px 8px 18px; flex-wrap: wrap; }
|
|
247
|
+
.vzr-ph h3 { margin: 0; font-size: var(--fs-4); font-weight: 800; }
|
|
248
|
+
.vzr-cnt { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-2); font-weight: 700; color: var(--cz-ink2); }
|
|
249
|
+
.vzr-cfall { display: inline-flex !important; align-items: center; gap: 6px; }
|
|
250
|
+
.vzr-k { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; flex: none; color: var(--cz-ink2); }
|
|
251
|
+
.vzr-k[data-k="decision"]::before { content: ''; width: 10px; height: 10px; border-radius: 50%; background: var(--cz-ok); }
|
|
252
|
+
.vzr-k[data-k="question"] { color: var(--cz-late); }
|
|
253
|
+
.vzr-k svg, .vzr-bx { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
|
254
|
+
.vzp-kr.vzr-row { grid-template-columns: 20px minmax(0, 1fr) auto 32px; gap: 0 12px; align-items: start; padding: 10px 12px 10px 18px; }
|
|
255
|
+
.vzp-kr.vzr-row .vzr-k { margin-top: 1px; }
|
|
256
|
+
.vzp-kr.vzr-row.sel { margin: 2px 6px; padding-left: 12px; padding-right: 6px; }
|
|
257
|
+
.vzr-row .vzp-kt b { font-weight: 800; line-height: 1.35; }
|
|
258
|
+
.vzr-row .vzp-tm { padding-top: 2px !important; min-width: 44px; }
|
|
259
|
+
.vzr-cf { width: 30px; height: 30px; border-radius: 50%; border: 1.6px solid var(--cz-hair) !important; display: inline-grid !important; place-items: center; color: var(--cz-ink3); margin-top: -3px; }
|
|
260
|
+
.vzr-cf:hover { color: var(--cz-ok); border-color: var(--cz-ok) !important; }
|
|
261
|
+
.vzr-cf.on { background: var(--cz-okbg) !important; border-color: transparent !important; color: var(--cz-ok); }
|
|
262
|
+
.vzr-fu { grid-column: 2 / 5; display: flex; align-items: center; gap: 10px; margin-top: 6px; font-size: var(--fs-2); color: var(--cz-ink2); }
|
|
263
|
+
.vzr-fu.vzr-sub { margin-top: 2px; }
|
|
264
|
+
.vzr-bx { color: var(--cz-ink3); flex: none; }
|
|
265
|
+
.vzr-grow { flex: 1; min-width: 0; }
|
|
266
|
+
.vzr-who { color: var(--cz-ink3); }
|
|
267
|
+
.vzr-tk { display: inline-flex; align-items: center; height: 22px; padding: 0 9px; border-radius: 999px; background: var(--cz-bluebg); color: var(--cz-blueink); font-size: var(--fs-1); font-weight: 800; }
|
|
268
|
+
.vzr-ghost { background: var(--cz-well) !important; }
|
|
269
|
+
.vzr-chk { grid-column: 2 / 5; margin: 4px 0 0; font-size: var(--fs-2); color: var(--cz-late); }
|
|
270
|
+
.vzr-pad { padding: 4px 18px 14px; }
|
|
271
|
+
.vzr-fold { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
272
|
+
.vzr-ch { min-height: 34px; padding: 0 13px !important; border-radius: 999px; background: var(--cz-well) !important; display: inline-flex !important; align-items: center; gap: 7px; font-size: var(--fs-2); color: var(--cz-ink2); }
|
|
273
|
+
.vzr-ch b { font-weight: 800; color: var(--cz-ink); }
|
|
274
|
+
.vzr-ch span { color: var(--cz-ink3); }
|
|
275
|
+
.vzr-ch[aria-expanded="true"] { background: var(--cz-ink) !important; color: var(--cz-card); }
|
|
276
|
+
.vzr-ch[aria-expanded="true"] b, .vzr-ch[aria-expanded="true"] span { color: var(--cz-card); }
|
|
277
|
+
.vzr-panel { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 18px; border: 1px solid var(--cz-hair); background: var(--cz-card); }
|
|
278
|
+
.vzr-ev { display: flex; flex-direction: column; gap: 12px; padding: 14px; border-radius: 20px; border: 1px solid var(--cz-hair); background: var(--cz-card); min-width: 0; }
|
|
279
|
+
.vzr-shot { aspect-ratio: 16 / 9; background: var(--cz-well) !important; }
|
|
280
|
+
.vzr-noshot { display: grid; place-items: center; min-height: 120px; border-radius: 12px; background: var(--cz-well); padding: 12px; text-align: center; }
|
|
281
|
+
.vzr-acts { display: flex; align-items: center; gap: 10px; }
|
|
282
|
+
.vzr-pri { width: 40px !important; height: 40px !important; background: var(--cz-blueink) !important; color: var(--cz-card) !important; }
|
|
283
|
+
.vzr-at { font-size: var(--fs-3); font-weight: 800; color: var(--cz-blueink); font-variant-numeric: tabular-nums; }
|
|
284
|
+
.vzr-q { margin: 0; font-size: var(--fs-3); line-height: 1.5; color: var(--cz-ink2); }
|
|
285
|
+
.vzr-film { display: flex; gap: 6px; align-items: center; overflow: hidden; }
|
|
286
|
+
.vzr-film.all { flex-wrap: wrap; }
|
|
287
|
+
.vzr-film .vzp-th { width: 72px; height: 44px; }
|
|
288
|
+
.vzr-more { height: 44px; min-width: 44px; padding: 0 8px !important; border-radius: 6px; background: var(--cz-well) !important; color: var(--cz-ink2); font-size: var(--fs-2); font-weight: 800; display: inline-grid !important; place-items: center; }
|
|
289
|
+
.vzr-dock { position: sticky; bottom: 0; z-index: 2; flex-direction: row !important; align-items: center; gap: 14px !important; padding: 10px 16px !important; border-radius: 22px; background: var(--cz-card) !important; box-shadow: 0 18px 44px -18px color-mix(in oklab, var(--cz-ink) 40%, transparent), 0 0 0 1px var(--cz-hair); }
|
|
290
|
+
.vzr-dock .vzp-ctl { flex: none; }
|
|
291
|
+
.vzr-dock .vzp-ctl .vzp-mut { flex: none; }
|
|
292
|
+
.vzr-tl { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
293
|
+
.vzr-tops { align-items: flex-end; }
|
|
294
|
+
.vzr-tops .vzp-top { white-space: normal; text-overflow: clip; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-height: 1.2; font-weight: 700; color: var(--cz-ink2); padding: 0 6px 0 0 !important; text-align: left !important; }
|
|
295
|
+
.vzr-tops .vzp-top[aria-current="true"] { color: var(--cz-ink); font-weight: 800; }
|
|
296
|
+
.vzp-kd[data-k="decision"] { fill: var(--cz-ok); }
|
|
297
|
+
.vzp-kd[data-k="action"] { fill: var(--cz-blue); }
|
|
298
|
+
.vzp-kd[data-k="question"] { fill: var(--cz-late); }
|
|
299
|
+
/* a word fixed in place (MvF): fixed in green, a guess underlined dotted, the field in place */
|
|
300
|
+
.vzr-fx { background: var(--cz-okbg); border-radius: 5px; padding: 0 3px; font-weight: 700; color: var(--cz-ink); }
|
|
301
|
+
.vzr-fl { display: inline !important; padding: 0 !important; font: inherit; color: var(--cz-ink); font-weight: 700; border-bottom: 2px dotted var(--cz-late) !important; border-radius: 0 !important; cursor: pointer; }
|
|
302
|
+
.vzr-fe { display: inline-flex; align-items: center; gap: 4px; padding: 0 2px 0 8px; border-radius: 10px; background: var(--cz-card); box-shadow: 0 0 0 2px var(--cz-blue); vertical-align: middle; }
|
|
303
|
+
.vzr-fe input { width: 12ch; height: 28px; border: 0; outline: 0; background: transparent; font: inherit; font-weight: 800; color: var(--cz-ink); }
|
|
304
|
+
.vzr-ok { width: 26px; height: 26px; border-radius: 50%; display: inline-grid !important; place-items: center; background: var(--cz-blueink) !important; color: var(--cz-card); }
|
|
305
|
+
.vzr-ok svg, .vzr-ev .vzp-ib svg, .vzr-pts svg, .vzr-cfall svg { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
306
|
+
.vzr-ev .vzp-fill, .vzr-pri svg { fill: currentColor; stroke: none; }
|
|
307
|
+
@media (max-width: 640px) {
|
|
308
|
+
.vzr-mbonly { display: block; }
|
|
309
|
+
.vzr-mbonly > .czs-secs { margin-top: 0; }
|
|
310
|
+
}
|
|
311
|
+
|
|
231
312
|
/* one Replay at a time: the phone's below 640px, else the desk's */
|
|
232
313
|
.vzp.vzp-mb { display: none; }
|
|
233
314
|
@media (max-width: 640px) {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
.pl-sl { margin-top: 2px; }
|
|
21
21
|
.pl-now { display: flex; align-items: center; gap: 8px; margin: 4px 0 0; padding-top: 10px; border-top: 1px solid var(--cz-hair); font-size: var(--fs-2); min-width: 0; }
|
|
22
22
|
.pl-now b { flex: none; font-weight: 800; }
|
|
23
|
-
.pl-
|
|
24
|
-
.hm-pl.open .pl-
|
|
23
|
+
.pl-nt { min-width: 0; color: var(--cz-ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
24
|
+
.hm-pl.open .pl-nt { white-space: normal; }
|
|
25
25
|
.pl-acts { display: flex; gap: 8px; margin-top: 10px; }
|
|
26
26
|
.pl-ib { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--cz-hair); background: var(--cz-card); color: var(--cz-ink2); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; }
|
|
27
27
|
.pl-ib.pri { background: var(--cz-solid); color: var(--cz-onsolid); border-color: transparent; }
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* and models (the first call in a page adds /pm/home-line.css once): Node tests them (chat-line.test.js).
|
|
3
3
|
*
|
|
4
4
|
* One line at the top of a chat shows the progress of the goal or of the session (one thing: Wawan, 2026-10-25). Folded it shows a spinner,
|
|
5
|
-
* the step it is on, "1 of 4 done" and Stop. A tap opens it in place on a phone (the goal's points, a Now row with the
|
|
6
|
-
* Watch its terminal) and in the Viewer on a desktop (viewHtml). A session that never wrote a step list
|
|
7
|
-
*
|
|
5
|
+
* the step it is on, "1 of 4 done" and Stop. A tap opens it in place on a phone (the goal's points, a Now row with what the session does
|
|
6
|
+
* in plain words, Watch its terminal) and in the Viewer on a desktop (viewHtml). A session that never wrote a step list leads with the turn's
|
|
7
|
+
* goal, Claude's newest one-line status ("Checking which programs exist · 0:12"), else "Working · 3:12", never an invented count.
|
|
8
|
+
* Raw tool calls ("Bash · cat …") never show on the line or in its body: they stay behind the terminal icon (batch 25 item 14). When Claude needs the PM the line turns blue ("Claude asks before it goes on"). When the run ends the line goes away and a
|
|
8
9
|
* divider stays where it finished (dividerHtml): "Goal done", or in amber why it stopped.
|
|
9
10
|
*
|
|
10
11
|
* goalModel(goal, conv, o) a working goal (state started or waiting) → model, else null
|
|
@@ -48,6 +49,28 @@ export function clockOf(conv, now = Date.now()) {
|
|
|
48
49
|
const s = Math.max(0, Math.round((now - t0) / 1000));
|
|
49
50
|
return Math.floor(s / 60) + ':' + pad(s % 60);
|
|
50
51
|
}
|
|
52
|
+
const MD = (t) => t.replace(/[*_`]+/g, '').replace(/^\s*(?:#+|[-*•>]|\d+[.)])\s*/, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').trim();
|
|
53
|
+
/** The turn's goal in plain words: the last line of Claude's newest message in the turn that runs now (its status before the tool call),
|
|
54
|
+
* without Markdown, a trailing colon or code; a long line keeps its last sentence. '' when Claude wrote nothing since the PM's message. */
|
|
55
|
+
export function turnGoal(conv) {
|
|
56
|
+
const ms = (conv && conv.messages) || [];
|
|
57
|
+
for (let i = ms.length - 1; i >= 0; i--) {
|
|
58
|
+
if (ms[i].role === 'user') return '';
|
|
59
|
+
if (ms[i].role !== 'assistant' || !ms[i].text) continue;
|
|
60
|
+
let fence = false;
|
|
61
|
+
const lines = String(ms[i].text).split('\n').filter((l) => { if (/^\s*(```|~~~)/.test(l)) { fence = !fence; return false; } return !fence && !/^\s*\|/.test(l); })
|
|
62
|
+
.map(MD).filter((l) => /[a-z]/i.test(l));
|
|
63
|
+
let t = (lines.pop() || '').replace(/[::]\s*$/, '');
|
|
64
|
+
if (t.length > 120) t = t.split(/(?<=[.!?])\s+/).filter(Boolean).pop() || t;
|
|
65
|
+
if (t) return t.length > 120 ? t.slice(0, 119).trimEnd() + '…' : t;
|
|
66
|
+
}
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
/** What the session does now, in plain words: the to-do it marked in progress, else the turn's goal. Never a raw tool call. */
|
|
70
|
+
export function nowText(conv) {
|
|
71
|
+
const st = conv && Array.isArray(conv.steps) ? conv.steps.find((s) => s && s.state === 'now') : null;
|
|
72
|
+
return (st && st.text) || turnGoal(conv);
|
|
73
|
+
}
|
|
51
74
|
/** The newest tool call of the turn that runs now ("Edit · planning/prd/x.md"), or '' once the PM wrote after it. */
|
|
52
75
|
export function nowCall(conv) {
|
|
53
76
|
const ms = (conv && conv.messages) || [];
|
|
@@ -113,7 +136,7 @@ export function goalModel(goal, conv, o = {}) {
|
|
|
113
136
|
count: asking ? '' : `${nMet} of ${count} done`, clock: '',
|
|
114
137
|
title: d.goal || 'Goal',
|
|
115
138
|
head: `${nMet} of ${count} points met · round ${Math.max(1, (goal.rounds || 0) + 1)} of ${limit}${d.by ? ' · due ' + d.by : ''} · Claude checks its own points`,
|
|
116
|
-
items: w.items, more: w.more, now: run ? nowCall(conv) : '', term: !!o.term,
|
|
139
|
+
items: w.items, more: w.more, now: run && !asking ? nowText(conv) : '', call: run ? nowCall(conv) : '', term: !!o.term,
|
|
117
140
|
open: !!o.open, on: !!o.on, frac: count ? nMet / count : 0,
|
|
118
141
|
};
|
|
119
142
|
}
|
|
@@ -124,20 +147,23 @@ export function sessionModel(conv, o = {}) {
|
|
|
124
147
|
const steps = Array.isArray(conv.steps) && conv.steps.length ? conv.steps : null;
|
|
125
148
|
const asking = !!o.asking;
|
|
126
149
|
const clock = clockOf(conv, o.now);
|
|
150
|
+
const call = running(conv) ? nowCall(conv) : '';
|
|
127
151
|
if (!steps) {
|
|
128
|
-
|
|
129
|
-
|
|
152
|
+
// no to-do list: the turn's goal leads (Claude's newest status line), the clock sits at the end; with none, "Working · 3:12"
|
|
153
|
+
const goal = asking ? '' : turnGoal(conv);
|
|
154
|
+
return { kind: 'session', icon: asking ? 'none' : 'spin', tone: asking ? 'you' : '', text: asking ? 'Claude asks before it goes on' : goal || 'Working', clock: asking ? '' : clock, count: '',
|
|
155
|
+
clockEnd: !!goal, title: o.title || 'Working', head: goal && clock ? 'Working · ' + clock : '', items: [], more: 0, now: '', call, term: !!o.term, open: !!o.open, on: !!o.on, frac: 0, plain: true };
|
|
130
156
|
}
|
|
131
157
|
const cur = steps.findIndex((s) => s.state === 'now');
|
|
132
158
|
const done = steps.filter((s) => s.state === 'done').length;
|
|
133
|
-
const all = steps.map((s) => ({ text: s.text, state: s.state === 'done' ? 'done' : s.state === 'now' && !asking ? 'now' : 'next', sub: s.sub || ''
|
|
159
|
+
const all = steps.map((s) => ({ text: s.text, state: s.state === 'done' ? 'done' : s.state === 'now' && !asking ? 'now' : 'next', sub: s.sub || '' }));
|
|
134
160
|
const w = window5(all, cur, o.all);
|
|
135
161
|
return {
|
|
136
162
|
kind: 'session', icon: asking ? 'none' : 'spin', tone: asking ? 'you' : '',
|
|
137
163
|
text: asking ? 'Claude asks before it goes on' : cur >= 0 ? steps[cur].text : 'Working',
|
|
138
164
|
count: asking ? '' : `${done} of ${steps.length} done`, clock: '',
|
|
139
165
|
title: o.title || 'Steps', head: `${done} of ${steps.length} done${clock ? ' · ' + clock : ''}`,
|
|
140
|
-
items: w.items, more: w.more, now: cur >= 0 && steps[cur].call
|
|
166
|
+
items: w.items, more: w.more, now: '', call: (cur >= 0 && steps[cur].call) || call, term: !!o.term,
|
|
141
167
|
open: !!o.open, on: !!o.on, frac: steps.length ? done / steps.length : 0,
|
|
142
168
|
};
|
|
143
169
|
}
|
|
@@ -148,7 +174,9 @@ function listHtml(m, label) {
|
|
|
148
174
|
owner: x.avatar ? { kind: 'person', initials: x.avatar.initial, name: x.avatar.name } : null, act: x.act || null }));
|
|
149
175
|
return stepsHtml(steps, { label, cls: 'pl-sl', more: m.more ? { n: m.more, attr: 'data-pl="all"', label: 'Show all ' + m.more } : null });
|
|
150
176
|
}
|
|
151
|
-
const nowRow = (m) => (m.now ? `<p class="pl-now">${spinHtml(14)}<b>Now</b><
|
|
177
|
+
const nowRow = (m) => (m.now ? `<p class="pl-now">${spinHtml(14)}<b>Now</b><span class="pl-nt">${esc(m.now)}</span></p>` : '');
|
|
178
|
+
/** Watch its terminal: the raw tool call is only behind it, as the icon's tooltip. */
|
|
179
|
+
const termBtn = (m) => `<button type="button" class="pl-ib" data-pl="term" aria-label="Watch its terminal" title="${esc('Watch its terminal' + (m.call ? ' · ' + m.call : ''))}">${SVG(P.term, 18, 1.8)}</button>`;
|
|
152
180
|
const leadOf = (m) => (m.icon === 'spin' ? spinHtml(16) : m.icon === 'clock' ? `<span class="pl-clk">${SVG(P.clock, 16, 2)}</span>` : m.tone === 'you' ? '<i class="pl-dot" aria-hidden="true"></i>' : '');
|
|
153
181
|
|
|
154
182
|
/** The line. Folded: one row; open: the head line, the items, the Now row and Watch its terminal. */
|
|
@@ -156,13 +184,14 @@ export function lineHtml(m) {
|
|
|
156
184
|
if (!m) return '';
|
|
157
185
|
ensureCss();
|
|
158
186
|
const tail = m.count ? `<span class="pl-n">${esc(m.count)}</span>` : m.clock ? `<span class="pl-n">${esc(m.clock)}</span>` : '';
|
|
159
|
-
const
|
|
187
|
+
const inl = m.clock && !m.count && !m.clockEnd; // "Working · 3:12" reads as one; a goal line keeps its clock at the end
|
|
188
|
+
const text = inl && m.icon !== 'none' ? m.text + ' · ' + m.clock : m.text;
|
|
160
189
|
const say = [m.open ? m.title : text, m.count].filter(Boolean).join(', ');
|
|
161
190
|
const stop = `<button type="button" class="pl-stop" data-pl="stop" aria-label="${esc(m.stopLabel || 'Stop')}" title="${esc(m.stopLabel || 'Stop')}">${STOP_SQ}</button>`;
|
|
162
191
|
const row = `<div class="pl-row"><button type="button" class="pl-hit" data-pl="toggle" aria-expanded="${!!m.open}" aria-label="${esc(say)}">${leadOf(m)}`
|
|
163
|
-
+ `<span class="pl-t">${esc(m.open && m.kind === 'goal' ? m.title : text)}</span>${m.open ? '' : (
|
|
192
|
+
+ `<span class="pl-t">${esc(m.open && m.kind === 'goal' ? m.title : text)}</span>${m.open ? '' : (inl ? '' : tail)}<span class="pl-cv">${SVG(m.open ? P.up : P.down, 14, 2.2)}</span></button>${stop}</div>`;
|
|
164
193
|
const body = m.open && m.tone !== 'you' ? `<div class="pl-body">${m.head ? `<p class="pl-head">${esc(m.head)}</p>` : ''}${m.items.length ? listHtml(m, m.kind === 'goal' ? 'The goal’s points' : 'Steps') : ''}${nowRow(m)}`
|
|
165
|
-
+ (m.term ? `<div class="pl-acts"
|
|
194
|
+
+ (m.term ? `<div class="pl-acts">${termBtn(m)}</div>` : '') + '</div>' : '';
|
|
166
195
|
const cls = ['hm-pl', m.tone === 'you' ? 'you' : '', m.open ? 'open' : '', m.on ? 'on' : '', m.plain ? 'plain' : ''].filter(Boolean).join(' ');
|
|
167
196
|
return `<div class="${cls}" role="status" data-kind="${esc(m.kind || '')}">${row}${body}</div>`;
|
|
168
197
|
}
|
|
@@ -174,7 +203,7 @@ export function viewHtml(m) {
|
|
|
174
203
|
return `<div class="vwdoc pl-vw"><div class="pl-vwh"><b>${esc(m.title)}</b><button type="button" class="pl-stop" data-pl="stop" aria-label="${esc(m.stopLabel || 'Stop')}" title="${esc(m.stopLabel || 'Stop')}">${STOP_SQ}</button></div>`
|
|
175
204
|
+ (m.head ? `<p class="pl-head">${esc(m.head)}</p>` : '')
|
|
176
205
|
+ (m.items.length ? listHtml({ ...m, more: 0 }, m.kind === 'goal' ? 'The goal’s points' : 'Steps') : '') + nowRow(m)
|
|
177
|
-
+ (m.term || m.change ? `<div class="pl-acts">${m.term ?
|
|
206
|
+
+ (m.term || m.change ? `<div class="pl-acts">${m.term ? termBtn(m) : ''}`
|
|
178
207
|
// m.change: the pencil that opens the plan editor for a working goal (home-goal.js; the change applies after the current step)
|
|
179
208
|
+ `${m.change ? `<button type="button" class="pl-ib" data-goal="change" aria-label="Change the plan" title="Change the plan">${SVG('M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4', 18, 1.8)}</button>` : ''}</div>` : '') + '</div>';
|
|
180
209
|
}
|