create-pmos 0.502.0 → 0.503.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.
@@ -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; only a row with none says Deliver or Evaluate. The date turns amber at risk and orange late; the rest (days over, the forecast, a slip, a clash) is its hover.
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.track && r.track.k >= 6 ? 'Evaluate' : 'Deliver');
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.track && r.track.k >= 6 ? 'Evaluate' : 'Deliver') + ' is before the return, ' + md(ob.b) : null;
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 Deliver or Evaluate. M: its dated milestones. */
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.track && r.track.k >= 6 ? 'Evaluate' : 'Deliver', date: et ? String(et.date).slice(0, 10) : iso(e), done: !!(et && et.done), late: !!late, row: 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-now code { min-width: 0; font-family: var(--cz-mono); font-size: var(--fs-2); color: var(--cz-ink2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
24
- .hm-pl.open .pl-now code { white-space: normal; word-break: break-all; }
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 newest tool call,
6
- * Watch its terminal) and in the Viewer on a desktop (viewHtml). A session that never wrote a step list shows "Working · 3:12", never an
7
- * invented count. 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
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
- return { kind: 'session', icon: asking ? 'none' : 'spin', tone: asking ? 'you' : '', text: asking ? 'Claude asks before it goes on' : 'Working', clock: asking ? '' : clock, count: '',
129
- title: o.title || 'Working', head: '', items: [], more: 0, now: running(conv) ? nowCall(conv) : '', term: !!o.term, open: !!o.open, on: !!o.on, frac: 0, plain: true };
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 || '', code: s.state === 'now' ? s.call || '' : '' }));
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 ? '' : nowCall(conv), term: !!o.term,
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><code>${esc(m.now)}</code></p>` : '');
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 text = m.clock && !m.count && m.icon !== 'none' ? m.text + ' · ' + m.clock : m.text;
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 ? '' : (m.clock && !m.count ? '' : tail)}<span class="pl-cv">${SVG(m.open ? P.up : P.down, 14, 2.2)}</span></button>${stop}</div>`;
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"><button type="button" class="pl-ib" data-pl="term" aria-label="Watch its terminal" title="Watch its terminal">${SVG(P.term, 18, 1.8)}</button></div>` : '') + '</div>' : '';
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 ? `<button type="button" class="pl-ib" data-pl="term" aria-label="Watch its terminal" title="Watch its terminal">${SVG(P.term, 18, 1.8)}</button>` : ''}`
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
  }