create-pmos 0.494.0 → 0.495.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.
Files changed (51) hide show
  1. package/package.json +38 -14
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/briefing-premium-adddoc-test.js +2 -1
  7. package/workspace/briefing-premium-board-test.js +5 -4
  8. package/workspace/briefing-premium-initiative-test.js +2 -1
  9. package/workspace/changelog.json +37 -0
  10. package/workspace/guide/briefing-premium.md +2 -1
  11. package/workspace/guide/initiatives-screen.md +1 -1
  12. package/workspace/guide/report-slides.md +6 -6
  13. package/workspace/guide/reports.md +1 -1
  14. package/workspace/guide/workflows.md +15 -3
  15. package/workspace/ini-workflow-test.js +2 -1
  16. package/workspace/lib/engine/workflows.js +19 -6
  17. package/workspace/lib/initiative-cli.js +15 -1
  18. package/workspace/lib/open-mission.js +14 -0
  19. package/workspace/lib/report-export.js +5 -3
  20. package/workspace/lib/report-pyramid.js +134 -33
  21. package/workspace/lib/report-templates.js +33 -3
  22. package/workspace/lib/routes-boards.js +35 -2
  23. package/workspace/lib/routes-brief-page.js +2 -1
  24. package/workspace/lib/routes-initiatives.js +1 -0
  25. package/workspace/lib/routes-report-design.js +1 -1
  26. package/workspace/lib/routes-report-flow.js +11 -4
  27. package/workspace/lib/routes-report-setups.js +3 -2
  28. package/workspace/lib/routes-workflows.js +6 -1
  29. package/workspace/lib/workflow-cli.js +2 -2
  30. package/workspace/lib/workflows-store.js +15 -1
  31. package/workspace/metric-drafts-ui-test.js +2 -1
  32. package/workspace/package-lock.json +2 -2
  33. package/workspace/package.json +1 -1
  34. package/workspace/premium-board-actions-test.js +2 -1
  35. package/workspace/premium-vw-backlog-test.js +3 -2
  36. package/workspace/premium-vw-item-test.js +3 -2
  37. package/workspace/premium-vw-jira-test.js +3 -2
  38. package/workspace/program-panel-test.js +2 -1
  39. package/workspace/public/pm/cozy-home.css +1 -0
  40. package/workspace/public/pm/home-app.js +3 -1
  41. package/workspace/public/pm/home-chat.js +49 -8
  42. package/workspace/public/pm/ini-view.js +2 -2
  43. package/workspace/public/pm/premium-shell.js +57 -6
  44. package/workspace/public/pm/premium.css +1 -0
  45. package/workspace/public/pm/wf-page.css +8 -0
  46. package/workspace/public/pm/wf-page.js +33 -5
  47. package/workspace/skills/report-template.skill +4 -2
  48. package/workspace/split-merge-ui-test.js +3 -2
  49. package/workspace/stage-card-test.js +2 -1
  50. package/workspace/task-press-test.js +2 -1
  51. package/workspace/tk-tasks-test.js +2 -1
@@ -12,6 +12,7 @@ const path = require('path');
12
12
  const fs = require('fs');
13
13
  const { spawn } = require('child_process');
14
14
  const { findBrowser } = require('./lib/find-browser');
15
+ const { openMission } = require('./lib/open-mission');
15
16
  const T = require('./test-run').configure({ name: 'metric-drafts-ui' });
16
17
  const BP = require('./lib/board-premium.js');
17
18
  const F = require('./fixtures/board-premium-facts.js');
@@ -68,7 +69,7 @@ const INITIATIVE = () => ({
68
69
  const shot = async (p, name) => { if (SHOTS) { await sleep(450); await p.screenshot({ path: path.join(SHOTS, name + '.png') }); } };
69
70
 
70
71
  const open = async ({ w = 1440, h = 900, drafts = { program: PROGRAM(), initiative: INITIATIVE() }, dark = false } = {}) => {
71
- const c = await browser.newContext({ viewport: { width: w, height: h }, colorScheme: dark ? 'dark' : 'light' });
72
+ const c = await openMission(await browser.newContext({ viewport: { width: w, height: h }, colorScheme: dark ? 'dark' : 'light' }));
72
73
  const page = await c.newPage();
73
74
  const errors = [], posts = [], gets = [];
74
75
  page.on('pageerror', (e) => errors.push(String((e && e.stack) || e)));
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.494.0",
3
+ "version": "0.495.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.494.0",
9
+ "version": "0.495.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@anthropic-ai/sdk": "^1.3.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.494.0",
3
+ "version": "0.495.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { spawn } = require('child_process');
12
12
  const { findBrowser } = require('./lib/find-browser');
13
+ const { openMission } = require('./lib/open-mission');
13
14
  const T = require('./test-run').configure({ name: 'premium-board-actions' });
14
15
  const { makeRepos, codeRepo, write, git } = require('./plan-test-repos');
15
16
  const BP = require('./lib/board-premium.js');
@@ -71,7 +72,7 @@ const ALLOWED = /^\/api\/(?:boards\/(?:asks\/(?:add|accept|decline)|move|archive
71
72
 
72
73
  browser = await chromium.launch({ executablePath: exe });
73
74
  const open = async (url, { w = 1280, h = 900 } = {}) => {
74
- const ctx = await browser.newContext({ viewport: { width: w, height: h } });
75
+ const ctx = await openMission(await browser.newContext({ viewport: { width: w, height: h } }));
75
76
  await ctx.addInitScript(() => { try { localStorage.setItem('pmos.premium.openAs', 'popup'); } catch { /* none */ } });
76
77
  const page = await ctx.newPage(), errors = [], posts = [];
77
78
  page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
@@ -7,6 +7,7 @@
7
7
  const path = require('path');
8
8
  const { spawn } = require('child_process');
9
9
  const { findBrowser } = require('./lib/find-browser');
10
+ const { openMission } = require('./lib/open-mission');
10
11
  const T = require('./test-run').configure({ name: 'premium-vw-backlog' });
11
12
  const BP = require('./lib/board-premium.js');
12
13
  const F = require('./fixtures/board-premium-facts.js');
@@ -46,7 +47,7 @@ const SPRINTS = { ok: true, source: 'jira', readAt: '2026-10-19T09:00:00Z', velo
46
47
  browser = await chromium.launch({ executablePath: exe });
47
48
 
48
49
  const open = async ({ w = 1280, h = 900, theme = 'light', sprints = SPRINTS } = {}) => {
49
- const ctx = await browser.newContext({ viewport: { width: w, height: h } });
50
+ const ctx = await openMission(await browser.newContext({ viewport: { width: w, height: h } }));
50
51
  const page = await ctx.newPage();
51
52
  const S = { errors: [], reads: [] };
52
53
  page.on('pageerror', (e) => S.errors.push(String((e && e.message) || e)));
@@ -120,7 +121,7 @@ const SPRINTS = { ok: true, source: 'jira', readAt: '2026-10-19T09:00:00Z', velo
120
121
 
121
122
  /* the Board (premium-uncovered-features, BdSprint) carries no Sprint card and no dialog; its sprint chip opens the Sprints and releases popover, whose Backlog row opens the backlog in the side panel */
122
123
  {
123
- const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }), page = await ctx.newPage(), errs = [];
124
+ const ctx = await openMission(await browser.newContext({ viewport: { width: 1280, height: 900 } })), page = await ctx.newPage(), errs = [];
124
125
  page.on('pageerror', (e) => errs.push(String((e && e.message) || e)));
125
126
  const model = BP.build(F.facts({ sprints: { source: 'jira', readAt: SPRINTS.readAt, sprints: [SPRINT] } }));
126
127
  await page.route('**/api/board-premium*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model, as_of: Date.now() }) }));
@@ -8,6 +8,7 @@
8
8
  const path = require('path');
9
9
  const { spawn } = require('child_process');
10
10
  const { findBrowser } = require('./lib/find-browser');
11
+ const { openMission } = require('./lib/open-mission');
11
12
  const T = require('./test-run').configure({ name: 'premium-vw-item' });
12
13
  const IP = require('./lib/initiative-premium.js');
13
14
  const F = require('./fixtures/initiative-premium-facts.js');
@@ -41,7 +42,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
41
42
  const MODEL = IP.build(F.facts());
42
43
 
43
44
  const open = async ({ w = 1280, h = 900, theme = 'light', answer = null } = {}) => {
44
- const ctx = await browser.newContext({ viewport: { width: w, height: h } });
45
+ const ctx = await openMission(await browser.newContext({ viewport: { width: w, height: h } }));
45
46
  const page = await ctx.newPage();
46
47
  const S = { errors: [], gets: [], vgets: [] }; // vgets: the Viewer's own reads (no page headers), apart from Home's
47
48
  page.on('pageerror', (e) => S.errors.push(String((e && e.message) || e)));
@@ -106,7 +107,7 @@ const openCard = async (pg, uid, V = '#czViewer') => {
106
107
  /* R9 the Board (premium-agreed-design): the Agreed Board opens an initiative in its side panel and never the dialog; its expand leads to the
107
108
  * initiative (Home's initiative layout), where the record is. R7 floors at 390 in dark: the same record, opened from Home's Overview. */
108
109
  {
109
- const ctx = await browser.newContext({ viewport: { width: 390, height: 844 } });
110
+ const ctx = await openMission(await browser.newContext({ viewport: { width: 390, height: 844 } }));
110
111
  const page = await ctx.newPage(); const errors = [];
111
112
  page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
112
113
  await page.route('**/api/board-premium*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model: BP.build(BF.facts()), as_of: Date.now() }) }));
@@ -10,6 +10,7 @@
10
10
  const path = require('path');
11
11
  const { spawn } = require('child_process');
12
12
  const { findBrowser } = require('./lib/find-browser');
13
+ const { openMission } = require('./lib/open-mission');
13
14
  const T = require('./test-run').configure({ name: 'premium-vw-jira' });
14
15
  const BP = require('./lib/board-premium.js');
15
16
  const F = require('./fixtures/board-premium-facts.js');
@@ -75,7 +76,7 @@ const TICKETS = {
75
76
  const overview = async (pg) => { await (async (g) => { const P = '#czViewer:not([hidden]) [data-hi-v="page"]', r = '#hmList [data-hm-sel^="i:"][aria-current="true"]'; for (let i = 0; i < 4 && !(await g.$(P)) && (await g.$('#czViewer:not([hidden]) [data-cz-vw="back"]')); i++) { await g.click('#czViewer [data-cz-vw="back"]'); await g.waitForTimeout(150); } if (await g.$(P)) return; if (await g.$(r)) await g.click(r); else await g.reload(); await g.waitForSelector(P + ' .iv', { timeout: 30000 }); })(pg); /* ← back to the initiative's page, else its row, else the address again (an initiative the list does not show) */ /* the page holds the Tasks (batch 11 item 20) */ return until(() => pg.evaluate(() => !!document.querySelector('#czViewer:not([hidden]) [data-hi-v]'))); };
76
77
  const openSprint = async (pg) => { if (!(await pg.$('#czViewer:not([hidden]) [data-hi-v]'))) await overview(pg); /* open() already showed the Overview */ await viaMore(pg, 'Sprint'); };
77
78
  const open = async ({ w = 1280, h = 900, theme = 'light', sprints = SPRINTS, tickets = TICKETS } = {}) => {
78
- const ctx = await browser.newContext({ viewport: { width: w, height: h }, colorScheme: theme });
79
+ const ctx = await openMission(await browser.newContext({ viewport: { width: w, height: h }, colorScheme: theme }));
79
80
  await ctx.addInitScript((t) => { try { localStorage.setItem('pmos.c3.theme', t); } catch { /* none */ } }, theme);
80
81
  const page = await ctx.newPage(), errors = [], posts = [], reads = [];
81
82
  page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
@@ -193,7 +194,7 @@ const TICKETS = {
193
194
 
194
195
  /* A the Board (premium-uncovered-features, BdSprint): its sprint chip names the sprint and opens the Sprints and releases popover; it opens no dialog */
195
196
  {
196
- const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }), pg = await ctx.newPage(), errs = [];
197
+ const ctx = await openMission(await browser.newContext({ viewport: { width: 1280, height: 900 } })), pg = await ctx.newPage(), errs = [];
197
198
  pg.on('pageerror', (e) => errs.push(String((e && e.message) || e)));
198
199
  await pg.route('**/api/board-premium*', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, state: 'ready', model: model({ source: 'jira', readAt: new Date().toISOString(), sprints: [{ name: 'Sprint 14', state: 'active', start: '2026-10-07', end: '2026-10-21', counts: { todo: 2, doing: 1, done: 1 }, issues: [] }] }), as_of: Date.now() }) }));
199
200
  await pg.goto(BASE + '/premium-board.html');
@@ -15,6 +15,7 @@
15
15
  const path = require('path');
16
16
  const { spawn } = require('child_process');
17
17
  const { findBrowser } = require('./lib/find-browser');
18
+ const { openMission } = require('./lib/open-mission');
18
19
  const T = require('./test-run').configure({ name: 'program-panel' });
19
20
  const BP = require('./lib/board-premium.js');
20
21
  const F = require('./fixtures/board-premium-facts.js');
@@ -46,7 +47,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
46
47
  browser = await chromium.launch({ executablePath: exe });
47
48
 
48
49
  const open = async ({ w = 1440, h = 900, qs = '?ws=idpf&program=project-ascent', ctx = null, programs = F.programs() } = {}) => {
49
- const c = ctx || await browser.newContext({ viewport: { width: w, height: h } });
50
+ const c = ctx || await openMission(await browser.newContext({ viewport: { width: w, height: h } }));
50
51
  const page = await c.newPage();
51
52
  const errors = [], posts = [];
52
53
  page.on('pageerror', (e) => errors.push(String((e && e.stack) || e)));
@@ -232,6 +232,7 @@ body.cz-home { min-height: 100vh; overflow: hidden; }
232
232
  .hm-u { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
233
233
  .hm-bub { max-width: 80%; padding: 10px 16px; border-radius: 18px 18px 8px 18px; background: var(--cz-bubble); font-size: var(--fs-3); line-height: 1.5; color: var(--cz-ink); }
234
234
  .hm-you { font-size: var(--fs-1); color: var(--cz-ink3); }
235
+ .hm-pend.sending .hm-bub { opacity: .72; } /* batch 24 item 2: the bubble shows at once, a touch lighter until PMOS takes it */
235
236
  .hm-worked { padding-left: 38px; }
236
237
  .hm-wk { display: flex; align-items: center; gap: 8px; max-width: 100%; min-height: 32px; padding: 0 4px; border: 0; border-radius: 8px; background: none; color: var(--cz-ink3); font-size: var(--fs-2); cursor: pointer; text-align: left; } /* a plain grey line (AgChatAsk) */
237
238
  .hm-wk:hover { color: var(--cz-ink2); }
@@ -843,7 +843,9 @@ function sessionTarget(s) {
843
843
  // a program's or a product's own chat says so in its scope, never "Loose" (batch 22 item 17)
844
844
  const pm = /^chat:program:([^:]+)/.exec(key || ''), pr = pm ? (S.D.programs || []).find((p) => p && p.id === pm[1]) : null;
845
845
  const prod = !ini && !pr && /^product(:|$)/.test(key || '') ? H.productName(S.D) : '';
846
- const home = pr ? { text: pr.title, icon: TD.P.layers } : prod ? { text: prod, icon: I.home } : null;
846
+ // a New workflow chat (Settings › Workflows) says so, never "Loose" (batch 24 item 4); its draft stays on the Workflows list
847
+ const wfNew = !ini && !pr && !prod && /^pop:workflow:/.test(key || '');
848
+ const home = pr ? { text: pr.title, icon: TD.P.layers } : prod ? { text: prod, icon: I.home } : wfNew ? { text: 'New workflow', icon: I.chat } : null;
847
849
  const t = { title: H.nameOf(s), ses: s.id, sub: [role.name, ini ? iniTitle(ini) : home ? home.text : 'Loose'].join(' · '), ini, role, scope: ini ? scopeOf(ini) : home || { text: 'Loose', icon: I.chat, loose: true }, rich: rich(), to: rich() ? role : null, also: alsoOf(ini) };
848
850
  if (key && /^(decision|blocked|waited|src|ev|work|board|init|doc|ins|chat|pop):/.test(key)) { t.key = key; t.sid = undefined; } else t.sid = s.id;
849
851
  if (!t.key) t.sid = s.id;
@@ -58,7 +58,7 @@ export const adlerAvHtml = (mood) => `<span class="hm-av hm-adler" aria-hidden="
58
58
 
59
59
  export function mountChat(el, host) {
60
60
  const esc = host.esc;
61
- const C = { t: null, th: null, conv: null, timer: null, show: 12, plOpen: false, plAll: false, open: new Set(), seq: 0, sending: false, files: [], seenAt: null, lastKey: null, picked: {}, auto: { key: null, ctx: new Set(), rep: null, goal: null }, slashAt: 0 };
61
+ const C = { t: null, th: null, conv: null, timer: null, show: 12, plOpen: false, plAll: false, open: new Set(), seq: 0, sending: false, files: [], seenAt: null, lastKey: null, picked: {}, pend: [], auto: { key: null, ctx: new Set(), rep: null, goal: null }, slashAt: 0 };
62
62
  el.innerHTML = `<button type="button" class="hm-reopen" id="hmReopen" aria-label="Open the Viewer" title="Open the Viewer" hidden>${PANE}</button>`
63
63
  + '<div class="hm-chd" id="hmCh" hidden></div><div class="hm-sug" id="hmSug" hidden></div><div class="hm-plw" id="hmPl" hidden></div><div class="hm-log" id="hmLog" role="log" aria-live="polite"></div><div class="hm-foot" id="hmFoot"></div>';
64
64
  const log = el.querySelector('#hmLog'), foot = el.querySelector('#hmFoot'), reopen = el.querySelector('#hmReopen'), head = el.querySelector('#hmCh'), sug = el.querySelector('#hmSug'), pl = el.querySelector('#hmPl');
@@ -175,6 +175,32 @@ export function mountChat(el, host) {
175
175
  return q.filter((x) => !read(x)).map((x) => `<div class="hm-q"><div class="hm-qb">${userHtml(x.text)}</div><span class="hm-qn">${esc(role().name)} reads it after this tool call, in seconds<button type="button" class="hm-qx" data-hm-qdel="${esc(x.at)}" aria-label="Take it back" title="Take it back">${SVG(XX, 13)}</button></span></div>`).join('');
176
176
  }
177
177
  const sid = () => (C.t && C.t.sid) || (C.th && C.th.session) || null;
178
+ /* A sent message shows at once (batch 24 item 2: "enter the prompt is so long to appear on the chat"). Its bubble draws the moment the PM
179
+ * presses Enter, says "Sending…" until PMOS takes it, then "You · HH:MM". It leaves when the transcript holds the PM's turn (the same words,
180
+ * or any PM turn from that moment on), and after 3 minutes at most, so a bubble never stays as a ghost. A message that was not sent leaves at
181
+ * once and its words go back in the box. */
182
+ const PEND_MAX = 180000;
183
+ function settlePend() {
184
+ if (!C.pend.length) return;
185
+ const key = (t) => splitFiles(shownUser(t)).text.replace(/\s+/g, ' ').trim();
186
+ const users = ((C.conv && C.conv.messages) || []).filter((m) => m.role === 'user');
187
+ const lastUser = users.reduce((n, m) => Math.max(n, Date.parse(m.at) || 0), 0), now = Date.now();
188
+ // a message for a working session is queued by the host and drawn as "reads it after this tool call": the bubble gives way to it
189
+ const queued = new Set((host.queued ? host.queued(sid()) : []).map((x) => x.text));
190
+ C.pend = C.pend.filter((p) => {
191
+ if (!p.sent) return true;
192
+ if (queued.has(p.text)) return false;
193
+ const t0 = Date.parse(p.at);
194
+ if (now - t0 > PEND_MAX) return false;
195
+ if (lastUser && lastUser >= t0 - 2000) return false;
196
+ return !users.slice(p.n).some((m) => key(m.text) === key(p.text));
197
+ });
198
+ }
199
+ function pendHtml() {
200
+ settlePend();
201
+ return C.pend.map((p) => `<div class="hm-u hm-pend${p.sent ? '' : ' sending'}"><div class="hm-bub">${filesHtml(p.files)}${p.text ? userHtml(p.text) : ''}</div>`
202
+ + `<span class="hm-you" role="status">${p.sent ? 'You · ' + esc(hm(p.at)) : 'Sending…'}</span></div>`).join('');
203
+ }
178
204
  // A loose chat that reads like one initiative gets one quiet chip at its top: "Looks like <initiative> · Link it ×" (XvALinkSuggest).
179
205
  function drawSug() {
180
206
  const t = C.t, g = t && t.scope && t.scope.loose && t.sid && host.suggestLink ? host.suggestLink(t, firstUser()) : null;
@@ -205,7 +231,7 @@ export function mountChat(el, host) {
205
231
  const goal = C.th && C.th.goal;
206
232
  const parts = items.map((it, i) => msgHtml(it, i, R));
207
233
  placeDivider(parts, items, goal);
208
- let html = (t.intro && !(t.intro.onlyEmpty && items.length) ? introHtml(t.intro, !items.length) : '') + parts.join('') + askHtml(R) + (R.stop && R.lastAgent < 0 ? dividerHtml(R.stop) : '') + queuedHtml();
234
+ let html = (t.intro && !(t.intro.onlyEmpty && items.length) ? introHtml(t.intro, !items.length) : '') + parts.join('') + askHtml(R) + (R.stop && R.lastAgent < 0 ? dividerHtml(R.stop) : '') + pendHtml() + queuedHtml();
209
235
  html += GU.choiceHtml(); // /goal while a goal works here: In a new chat, or Here instead
210
236
  if (C.th && C.th.working) html += `<p class="hm-note" role="status">The agent is answering. You can write again when it has.</p>`;
211
237
  if (C.conv && C.conv.state === 'ended') html += '<p class="hm-note">This session has ended. Your conversation is kept here, and your next message picks it up.</p>';
@@ -418,20 +444,31 @@ export function mountChat(el, host) {
418
444
  async function send(text) {
419
445
  const t = C.t; text = String(text || '').trim();
420
446
  if (!t || (!text && !C.files.length) || C.sending) return false;
447
+ if (t.key && C.files.some((f) => f.state === 'saving')) { host.toast('A file is still being saved. Send again in a moment.'); return false; }
421
448
  C.sending = true;
422
449
  let r;
423
450
  const was = running();
451
+ // the bubble first, then the server (batch 24 item 2): the files leave the box with the words, and come back if the send fails
452
+ const sentFiles = C.files;
453
+ const p = { text, files: sentFiles.map((f) => f.path || f.saved).filter(Boolean), at: new Date().toISOString(), sent: false, n: ((C.conv && C.conv.messages) || []).filter((m) => m.role === 'user').length };
454
+ C.pend.push(p); C.files = []; C.jump = true; draw();
424
455
  if (t.key) {
425
- if (C.files.some((f) => f.state === 'saving')) { C.sending = false; host.toast('A file is still being saved. Send again in a moment.'); return false; }
426
- const files = C.files.map((f) => f.saved);
456
+ const files = sentFiles.map((f) => f.saved);
427
457
  r = await host.pmPost('/api/briefing-premium/ask' + (t.ws ? '?ws=' + encodeURIComponent(t.ws) : ''), files.length ? { key: t.key, text, files, ...(t.context ? { context: t.context } : {}) } : { key: t.key, text, ...(t.context ? { context: t.context } : {}) }).catch(() => null);
428
458
  } else r = await host.pmPost('/api/session/say', { id: t.sid, text }).catch(() => null);
429
459
  C.sending = false;
430
- if (!r || r.ok !== true) { host.toast((r && r.error) || 'The message was not sent.'); return false; }
460
+ if (!r || r.ok !== true) {
461
+ C.pend = C.pend.filter((x) => x !== p);
462
+ if (C.t === t && !C.files.length) C.files = sentFiles;
463
+ if (C.t === t) draw();
464
+ host.toast((r && r.error) || 'The message was not sent.'); return false;
465
+ }
431
466
  const s = sid() || r.session;
432
467
  if (host.touch) host.touch(s);
433
468
  if (was && s && host.queue) host.queue(s, text);
434
- C.files.forEach((f) => { if (f.url) URL.revokeObjectURL(f.url); }); C.files = []; C.jump = true;
469
+ p.sent = true;
470
+ sentFiles.forEach((f) => { if (f.url) URL.revokeObjectURL(f.url); }); C.jump = true;
471
+ if (C.t === t) draw();
435
472
  if (t.onSent) t.onSent(r);
436
473
  read();
437
474
  return true;
@@ -527,7 +564,11 @@ export function mountChat(el, host) {
527
564
  drawFoot({ role: role() });
528
565
  }
529
566
  };
530
- el.addEventListener('submit', (e) => { e.preventDefault(); const ta = foot.querySelector('textarea'); const v = ta.value; if (GU.submit(v)) { ta.value = ''; grow(ta); return; } send(v).then((ok) => { if (ok) { ta.value = ''; grow(ta); } }); });
567
+ el.addEventListener('submit', (e) => { e.preventDefault(); const ta = foot.querySelector('textarea'); const v = ta.value; if (GU.submit(v)) { ta.value = ''; grow(ta); return; }
568
+ // the box empties at once and the bubble shows (batch 24 item 2); a send that fails puts the words back, unless the PM typed again
569
+ if (!v.trim() && !C.files.length) return;
570
+ ta.value = ''; grow(ta);
571
+ send(v).then((ok) => { const box = foot.querySelector('textarea'); if (!ok && box && !box.value) { box.value = v; grow(box); } }); });
531
572
  const shutAdd = (focus) => { if (!C.addOpen) return; C.addOpen = false; drawFoot({ role: role() }); if (focus) { const a = foot.querySelector('#hmAdd'); if (a) a.focus(); } };
532
573
  document.addEventListener('click', (e) => { if (C.addOpen && e.target.isConnected && !e.target.closest('.hm-addw')) shutAdd(false); }, true);
533
574
  el.addEventListener('keydown', (e) => {
@@ -623,7 +664,7 @@ export function mountChat(el, host) {
623
664
  if (notesM) notesM.then((M) => setTimeout(() => M.syncNotes(), 0)); // the Notes open in the Viewer belonged to the conversation before
624
665
  const same = C.t && t && C.t.key === t.key && C.t.sid === t.sid;
625
666
  C.t = t;
626
- if (!same) { C.addOpen = false; C.th = null; C.conv = null; C.show = 12; C.open = new Set(); C.files = []; C.marked = null; C.touched = null; C.picked = {}; C.plOpen = false; C.plAll = false; C.seenAt = t.key ? host.seen(t.key) : null; put(log, introHtml(t.intro)); put(pl, ''); pl.hidden = true; C.jump = true; peek(null); }
667
+ if (!same) { C.addOpen = false; C.th = null; C.conv = null; C.show = 12; C.open = new Set(); C.files = []; C.marked = null; C.touched = null; C.picked = {}; C.pend = []; C.plOpen = false; C.plAll = false; C.seenAt = t.key ? host.seen(t.key) : null; put(log, introHtml(t.intro)); put(pl, ''); pl.hidden = true; C.jump = true; peek(null); }
627
668
  draw(); read();
628
669
  },
629
670
  refresh() { if (C.t) draw(); },
@@ -75,11 +75,11 @@ const SHOWN = new Map(); // id → the view last drawn, for the Kanban's stage
75
75
  const MCS = new Map(); // id → the PM's own changes to { mc, gx, ab } (true = open), kept in the browser
76
76
  const MCK = 'pmos.iv.mc.';
77
77
  const store = () => { try { return typeof localStorage !== 'undefined' ? localStorage : null; } catch { return null; } };
78
- /** The three folds of one initiative: the PM's own, else the defaults (the way to finish is open on a wide screen, About is folded; on a phone both are folded; About opens by itself while a success measure waits on a ✓). */
78
+ /** The three folds of one initiative: the PM's own, else the defaults (the telemetry card starts hidden in its banner; the way to finish is open on a wide screen, About is folded; on a phone both are folded; About opens by itself while a success measure waits on a ✓). */
79
79
  export function mcState(id, o = {}) {
80
80
  let mine = MCS.get(id);
81
81
  if (!mine) { mine = {}; try { const s = store(), j = s && JSON.parse(s.getItem(MCK + id) || 'null'); if (j && typeof j === 'object') for (const k of ['mc', 'gx', 'ab']) if (typeof j[k] === 'boolean') mine[k] = j[k]; } catch { mine = {}; } MCS.set(id, mine); }
82
- const dflt = { mc: true, gx: !phone(), ab: !!o.proposal };
82
+ const dflt = { mc: false, gx: !phone(), ab: !!o.proposal }; // the telemetry starts hidden in its banner (Wawan, batch 24 item 1)
83
83
  return { mc: 'mc' in mine ? mine.mc : dflt.mc, gx: 'gx' in mine ? mine.gx : dflt.gx, ab: 'ab' in mine ? mine.ab : dflt.ab };
84
84
  }
85
85
  export function mcToggle(id, key, o) {
@@ -538,14 +538,50 @@ $('#dBody').addEventListener('click', async (e) => {
538
538
  const b = e.target.closest('[data-lead="refresh"]'); if (!b || !S.key || !S.key.startsWith('pop:')) return;
539
539
  b.disabled = true; await sendText(S.key, leadAsk(S.key));
540
540
  });
541
+ /* A sent message shows at once (batch 24 item 2: "enter the prompt is so long to appear on the chat"): its turn draws the moment the PM presses
542
+ * Enter, lighter until PMOS takes it. It leaves when the transcript holds the PM's turn (the same words, or any PM turn from that moment on),
543
+ * and after 3 minutes at most. A message that was not sent leaves at once and its words go back in the box. Keyed by row key, or 'sid:' + id. */
544
+ const PEND = { key: null, list: [], th: null }, PEND_MAX = 180000;
545
+ const pendOf = (key) => { if (PEND.key !== key) { PEND.key = key; PEND.list = []; PEND.th = null; } return PEND.list; };
546
+ function pendAdd(key, text, files) {
547
+ const conv = key.startsWith('sid:') ? PEND.sidConv : S.conv;
548
+ const p = { text, files: files || [], at: new Date().toISOString(), sent: false, n: ((conv && conv.messages) || []).filter((m) => m.role === 'user').length };
549
+ pendOf(key).push(p); return p;
550
+ }
551
+ function pendHtml(key, conv) {
552
+ const list = pendOf(key); if (!list.length) return '';
553
+ const k = (t) => splitFiles(shownUser(t)).text.replace(/\s+/g, ' ').trim();
554
+ const users = ((conv && conv.messages) || []).filter((m) => m.role === 'user');
555
+ const lastUser = users.reduce((n, m) => Math.max(n, Date.parse(m.at) || 0), 0), now = Date.now();
556
+ PEND.list = list.filter((p) => {
557
+ if (!p.sent) return true;
558
+ const t0 = Date.parse(p.at);
559
+ return now - t0 <= PEND_MAX && !(lastUser && lastUser >= t0 - 2000) && !users.slice(p.n).some((m) => k(m.text) === k(p.text));
560
+ });
561
+ return PEND.list.map((p) => `<div class="turn u pend${p.sent ? '' : ' sending'}"><div class="ub">${filesHtml(p.files)}${p.text ? userHtml(p.text) : ''}</div></div>`).join('');
562
+ }
563
+ /** Draws the waiting turns now, without a read: the thread as last read plus the new turn, and the stream scrolled to its end. */
564
+ function pendDraw(key) {
565
+ if (key.startsWith('sid:')) { if (S.sid !== key.slice(4)) return; }
566
+ else if (S.key !== key) return;
567
+ const box = $('#dBody .stream'); if (!box) return;
568
+ if (!key.startsWith('sid:') && PEND.th) drawThread(PEND.th, key, S.conv);
569
+ else {
570
+ let w = $('#pendW', box); if (!w) { box.insertAdjacentHTML('beforeend', '<div id="pendW"></div>'); w = $('#pendW', box); }
571
+ putHtml(w, pendHtml(key, key.startsWith('sid:') ? PEND.sidConv : S.conv));
572
+ }
573
+ const b = $('#dBody'); b.scrollTop = b.scrollHeight;
574
+ }
541
575
  export function drawThread(th, key, conv) {
542
576
  if (S.key !== key) return;
577
+ pendOf(key); PEND.th = th;
543
578
  const box = $('#dBody .stream'); if (!box) return;
544
579
  drawLead(box, th);
545
580
  let t = $('#thr', box); if (!t) { box.insertAdjacentHTML('beforeend', '<div id="thr"></div>'); t = $('#thr', box); }
546
581
  const b = $('#dBody'), atEnd = b.scrollHeight - b.scrollTop - b.clientHeight < 80;
547
582
  S.conv = conv;
548
- putHtml(t, threadHtml(th) + sessionHtml(conv, th.goal, { answer: true }));
583
+ const pw = $('#pendW', box); if (pw) pw.remove(); // a first message drew its turn before the thread was read
584
+ putHtml(t, threadHtml(th) + sessionHtml(conv, th.goal, { answer: true }) + pendHtml(key, conv));
549
585
  ctxAuto(key, conv);
550
586
  S.goal = th.goal || null; S.draft = readyDraft(conv); S.session = th.session || null;
551
587
  syncTerm(termTarget({ live: th.session, command: th.resume, ws: new URLSearchParams(location.search).get('ws') }));
@@ -773,15 +809,29 @@ $('#dBody').addEventListener('submit', async (e) => {
773
809
  const inp = $('#askIn'), text = inp.value.trim(), key = S.key;
774
810
  if (!key && S.sid) { // premium-session-list: a session the list opened that has no row of its own
775
811
  if (!text) { $('#askNote').textContent = 'Write a message for Claude.'; return; }
776
- const id = S.sid, r = await pmPost('/api/session/say', { id, text });
777
- if (!r || !r.ok) { $('#askNote').textContent = (r && r.error) || 'The message was not sent.'; return; }
778
- inp.value = ''; $('#askNote').textContent = 'Sent.'; drawSlash(); setTimeout(() => loadSid(id), 400); return;
812
+ const id = S.sid, pk = 'sid:' + id, p = pendAdd(pk, text);
813
+ inp.value = ''; drawSlash(); growAsk(); pendDraw(pk); // the turn shows at once (batch 24 item 2)
814
+ const r = await pmPost('/api/session/say', { id, text }).catch(() => null);
815
+ if (!r || !r.ok) {
816
+ PEND.list = pendOf(pk).filter((x) => x !== p); pendDraw(pk);
817
+ if (S.sid === id && !inp.value) { inp.value = text; growAsk(); }
818
+ $('#askNote').textContent = (r && r.error) || 'The message was not sent.'; return;
819
+ }
820
+ p.sent = true; pendDraw(pk);
821
+ $('#askNote').textContent = 'Sent.'; setTimeout(() => loadSid(id), 400); return;
779
822
  }
780
823
  if (!key) { $('#askNote').textContent = 'This row has no conversation, so the message was not sent.'; return; }
781
824
  const files = attachNames();
782
825
  if (files === null) { $('#askNote').textContent = 'A file is still saving. Send when its chip shows its size.'; return; }
783
826
  if (!text && !files.length) { $('#askNote').textContent = 'Write a message for Claude.'; return; }
784
- if (await sendText(key, text, files)) { inp.value = ''; inp.placeholder = 'Message Claude, or type /'; clearAttach(); drawSlash(); growAsk(); }
827
+ // the turn shows and the box empties at once (batch 24 item 2); a send that fails takes the turn away and puts the words back
828
+ const p = pendAdd(key, text, files);
829
+ inp.value = ''; drawSlash(); growAsk(); pendDraw(key);
830
+ if (await sendText(key, text, files)) { p.sent = true; inp.placeholder = 'Message Claude, or type /'; clearAttach(); pendDraw(key); }
831
+ else {
832
+ PEND.list = pendOf(key).filter((x) => x !== p); pendDraw(key);
833
+ if (S.key === key && !inp.value) { inp.value = text; growAsk(); }
834
+ }
785
835
  });
786
836
 
787
837
  /* ── on scroll: a card's head sticks while its card is on screen. The old hero no longer folds into a dock (premium-agreed-design). ── */
@@ -1424,7 +1474,8 @@ async function loadSid(id) {
1424
1474
  syncTerm(termTarget({ live: c && c.ok && c.state !== 'ended' ? id : null }));
1425
1475
  const box = $('#dBody .stream'); if (!box) return;
1426
1476
  const b = $('#dBody'), atEnd = b.scrollHeight - b.scrollTop - b.clientHeight < 80;
1427
- putHtml(box, c && c.ok ? sessionHtml(c) : turnA(ptx('The conversation of this session could not be read.')));
1477
+ if (c && c.ok) PEND.sidConv = c;
1478
+ putHtml(box, c && c.ok ? sessionHtml(c) + pendHtml('sid:' + id, c) : turnA(ptx('The conversation of this session could not be read.')));
1428
1479
  $('#askNote').textContent = c && c.ask ? 'Claude is waiting for your answer above.' : 'Your message goes to this session.';
1429
1480
  if (atEnd) b.scrollTop = b.scrollHeight;
1430
1481
  if (c && c.ok && c.state !== 'ended' && $('#dlg').classList.contains('on')) R.sidTimer = setTimeout(() => loadSid(id), 2000);
@@ -285,6 +285,7 @@ a.round{color:inherit;text-decoration:none}
285
285
  .tc{min-width:0}
286
286
  .tt{margin:0;font-size:var(--fs-3);line-height:1.55;color:var(--ink);max-width:58ch}
287
287
  .turn.u{justify-items:end}
288
+ .turn.u.pend.sending .ub{opacity:.72} /* batch 24 item 2: a sent turn shows at once, a touch lighter until PMOS takes it */
288
289
  .ub{background:var(--solid);color:var(--on-solid);padding:11px 16px;border-radius:18px 18px 8px 18px;font-size:var(--fs-3);line-height:1.45;max-width:86%}
289
290
  .acard{margin-left:40px;background:var(--well);border-radius:var(--r-in);padding:20px 22px;display:grid;gap:14px;animation:rise .6s var(--ease) both}
290
291
  .still .turn,.still .acard{animation:none} /* a redraw of a live chat keeps its turns still (premium-shell.js putHtml) */
@@ -23,6 +23,14 @@ body.wf-page { min-height: 100vh; overflow: hidden; font-family: var(--cz-sans);
23
23
  .wfl .t { font-size: var(--fs-3); font-weight: 700; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
24
24
  .wfl .n { font-size: var(--fs-2); color: var(--cz-ink3); font-weight: 600; }
25
25
  .wfl.arch .t { color: var(--cz-ink3); }
26
+ .wfl .wfb { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; text-align: left; }
27
+ .wfl .wfb:focus-visible, .wfl .wfs:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; border-radius: 8px; }
28
+ /* the star (batch 24 item 3, canvas DfA): filled on the default; an outline only on hover or focus elsewhere, and always on touch */
29
+ .wfl .wfs { width: 30px; height: 30px; margin: -4px 0; border-radius: 999px; display: inline-grid; place-items: center; border: 0; padding: 0; background: none; color: var(--cz-ink3); cursor: pointer; flex: none; opacity: 0; }
30
+ .wfl:hover .wfs, .wfl:focus-within .wfs, .wfl .wfs.on { opacity: 1; }
31
+ .wfl .wfs:hover { background: var(--cz-card); color: var(--cz-ink2); box-shadow: 0 0 0 1px var(--cz-hair); }
32
+ .wfl .wfs.on, .wfl .wfs.on:hover { color: var(--cz-risk); }
33
+ @media (hover: none) { .wfl .wfs { opacity: 1; } }
26
34
  .wf-sub { font-size: var(--fs-1); font-weight: 800; color: var(--cz-ink3); padding: 14px 10px 4px; }
27
35
  .wf-page .wfi { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 6px; background: var(--cz-well); color: var(--cz-ink2); flex: none; }
28
36
  .wfl .wfi, .wf-head .wfi { background: var(--cz-well2); }
@@ -1,6 +1,7 @@
1
1
  /* pm/wf-page.js — Settings › Workflows (initiative workflows per initiative, D99; canvas WfAModel, WfAStage, WfACond, WfARemove, WfANew,
2
2
  * WfADraft). One page, reached from Settings:
3
- * the list every workflow with its count of initiatives and +; Product feature first, archived ones last and quiet
3
+ * the list every workflow with its count of initiatives and +; Product feature first, archived ones last and quiet. The star on a row
4
+ * (batch 24 item 3, canvas DfA) makes that workflow the default a new initiative starts on (POST /api/workflows/default)
4
5
  * the picked workflow one numbered list across the To do, In progress and Done columns; drag a stage to reorder it or to move it to
5
6
  * another column; Add a stage per column; the dashed Won't do · Archived column (in every workflow); ⋯ = Copy, Archive.
6
7
  * The name is the rename press (RnA*: tap it, one field with a check).
@@ -35,6 +36,8 @@ const DRAG = 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01', PLUS = '
35
36
  const S = { list: null, sel: null, draft: null, edit: null, err: '', msg: '', renaming: false, renErr: '', busy: false, pop: null, chat: null, poll: 0, phoneOpen: false };
36
37
  const wfOf = (id) => (S.edit && id === S.edit.id ? S.edit : (S.list || []).find((w) => w.id === id) || null);
37
38
  const cur = () => (S.sel && S.sel !== 'new' ? wfOf(S.sel) : null);
39
+ /** A New workflow chat is still going (its key is kept until Create), or PMOS has drafted one. */
40
+ const draftLive = () => !!(store.get(CHAT_KEY) || S.draft);
38
41
  const readOnly = (wf) => !!wf && (wf.builtin || wf.id === PRODUCT_FEATURE_ID);
39
42
 
40
43
  function toast(text, undo) {
@@ -62,19 +65,30 @@ async function readDraft() {
62
65
  const d = r && r.ok && r.draft ? r.draft.draft || null : null; // { draft, note, at } | null
63
66
  const was = JSON.stringify(S.draft), now = JSON.stringify(d);
64
67
  S.draft = d;
68
+ if (was !== now && S.sel !== 'new') drawList(); // the draft row's name follows the draft
65
69
  if (was !== now && S.sel === 'new') { drawList(); if (d) showDraft(); else if (VW.current() && VW.current().id === 'wf-draft') VW.close(); }
66
70
  }
67
71
 
68
72
  /* ── the list (WfAModel's left column) ── */
73
+ // the star (batch 24 item 3, canvas DfA): gold and filled on the default, an outline on hover or focus elsewhere; archived rows have none
74
+ const STAR = 'M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.9 6.8 19.6l1-5.8-4.3-4.1 5.9-.9z';
75
+ function starHtml(w) {
76
+ if (w.archived) return '';
77
+ const label = w.default ? 'Default for new initiatives' : 'Make ' + w.name + ' the default for new initiatives';
78
+ return `<button type="button" class="wfs${w.default ? ' on' : ''}" data-wf-star="${esc(w.id)}" aria-pressed="${w.default ? 'true' : 'false'}" aria-label="${esc(label)}" title="${w.default ? 'Default for new initiatives' : 'Make default'}">`
79
+ + `<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="${w.default ? 'currentColor' : 'none'}" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="${STAR}"/></svg></button>`;
80
+ }
69
81
  function rowHtml(w) {
70
82
  const n = w.count > 0 ? `<span class="n">${w.count}</span>` : ''; // zero is silence
71
- return `<button type="button" class="wfl${S.sel === w.id ? ' sel' : ''}${w.archived ? ' arch' : ''}" data-wf="${esc(w.id)}"${S.sel === w.id ? ' aria-current="true"' : ''}>${wfIcon(w, 22)}<span class="t">${esc(w.name)}</span>${n}</button>`;
83
+ return `<div class="wfl${S.sel === w.id ? ' sel' : ''}${w.archived ? ' arch' : ''}"><button type="button" class="wfb" data-wf="${esc(w.id)}"${S.sel === w.id ? ' aria-current="true"' : ''}>${wfIcon(w, 22)}<span class="t">${esc(w.name)}</span></button>${starHtml(w)}${n}</div>`;
72
84
  }
73
85
  function drawList() {
74
86
  const el = $('#wfList'); if (!el) return;
75
87
  const L = S.list;
76
88
  const on = (L || []).filter((w) => !w.archived), off = (L || []).filter((w) => w.archived);
77
- const newRow = S.sel === 'new' ? `<button type="button" class="wfl sel" data-wf="new" aria-current="true"><span class="wfi new" aria-hidden="true">${svg(PLUS, 14)}</span><span class="t">${esc((S.draft && S.draft.name) || 'New workflow')}</span><span class="n">draft</span></button>` : '';
89
+ // a live draft keeps its row while another workflow is open, so its chat and draft are one press away (batch 24 item 4)
90
+ const onNew = S.sel === 'new';
91
+ const newRow = onNew || draftLive() ? `<button type="button" class="wfl${onNew ? ' sel' : ''}" data-wf="new"${onNew ? ' aria-current="true"' : ''}><span class="wfi new" aria-hidden="true">${svg(PLUS, 14)}</span><span class="t">${esc((S.draft && S.draft.name) || 'New workflow')}</span><span class="n">draft</span></button>` : '';
78
92
  el.innerHTML = `<div class="lhead"><h2>Workflows</h2><button type="button" class="ib sm" data-wf-new aria-label="New workflow: describe it and PMOS drafts it" title="New workflow: describe it and PMOS drafts it">${svg(PLUS, 16)}</button></div>`
79
93
  + (L === null ? '<p class="wf-mut wf-small wf-pad">Reading the workflows.</p>' : on.map(rowHtml).join('') + newRow
80
94
  + (off.length ? `<p class="wf-sub">Archived</p>${off.map(rowHtml).join('')}` : ''));
@@ -234,6 +248,17 @@ async function archive(wf, on) {
234
248
  await load();
235
249
  toast(on ? 'Archived ' + wf.name + '.' : 'Restored ' + wf.name + '.', () => archive(wf, !on));
236
250
  }
251
+ async function makeDefault(id, undoTo) {
252
+ const w = (S.list || []).find((x) => x.id === id);
253
+ if (!w || (w.default && !undoTo)) return;
254
+ const was = (S.list || []).find((x) => x.default);
255
+ const r = await post('/api/workflows/default', { id });
256
+ if (!(r && r.ok)) { S.msg = (r && r.error) || 'Not changed.'; drawMain(); return; }
257
+ for (const x of S.list) { if (x.id === id) x.default = true; else delete x.default; }
258
+ drawList();
259
+ const b = document.querySelector(`#wfList [data-wf-star="${CSS.escape(id)}"]`); if (b) b.focus();
260
+ if (!undoTo) toast('New initiatives start on ' + w.name + '.', was ? () => makeDefault(was.id, true) : null);
261
+ }
237
262
  async function rename(wf, name) {
238
263
  const v = String(name || '').trim();
239
264
  if (!v || v === wf.name) { S.renaming = false; S.renErr = ''; drawMain(); return; }
@@ -389,8 +414,10 @@ function bindDrag(main) {
389
414
  function bind() {
390
415
  $('#wfList').addEventListener('click', (e) => {
391
416
  if (e.target.closest('[data-wf-new]')) { startNew(); return; }
392
- const b = e.target.closest('[data-wf]'); if (!b) return;
393
- if (b.dataset.wf === 'new') return;
417
+ const star = e.target.closest('[data-wf-star]'); if (star) { makeDefault(star.dataset.wfStar); return; }
418
+ const row = e.target.closest('.wfl');
419
+ const b = e.target.closest('[data-wf]') || (row && row.querySelector('[data-wf]')); if (!b) return;
420
+ if (b.dataset.wf === 'new') { if (S.sel !== 'new') startNew(); return; } // back to the live draft: its chat and draft viewer
394
421
  select(b.dataset.wf);
395
422
  });
396
423
  const main = $('#wfMain');
@@ -431,5 +458,6 @@ export function boot() {
431
458
  draw();
432
459
  load();
433
460
  if (new URLSearchParams(location.search).get('new') === '1') startNew();
461
+ else if (store.get(CHAT_KEY)) readDraft(); // a draft from before shows as its row, by name
434
462
  }
435
463
  if (typeof document !== 'undefined' && document.getElementById('wfMain')) boot();
@@ -46,7 +46,9 @@ When the lines after this procedure say "This report has a saved structure" (a s
46
46
  When the lines after this procedure name a draft and a change ("The draft: <path> (slot <slot>)" and "The change the PM asked for"), this run is a change, not a new cycle. Steps 4 to 6 change as follows, and every rule stays:
47
47
  - Work on THAT file only. Do not draft a new slot. Never run `pmos report draft <id>` without the slot: it drafts today's slot, which is a second file.
48
48
  - If the named file does not exist, stop and say so in one line. Do not write it yourself.
49
- - A change to the words: rewrite the note or notes that the change is about, even when they already have text. Keep every other note as it is. Rules 3, 6 and 7 still apply.
49
+ - A change to the words: edit only the note or notes that the change is about, in the file, between their markers, even when they already have text. Keep every other note and every line outside the notes as it is. Rules 3, 6 and 7 still apply.
50
+ - Do NOT redraft for a change to the words. A redraft reads today's data and rewrites every block, so the numbers, the block titles and the place of the notes can change. Run `pmos report draft <id> <slot> --redraft`, with the slot from the lines, only when the PM asks for fresh numbers (for example "refresh", "update the numbers", "use today's data"). Then read the file once more.
51
+ - KEEP EVERY CAVEAT. A sentence that says something is provisional, not decided, unverified, at risk, not reached or a warning stays, word for word, also when the change asks for shorter text. Make the other sentences shorter. If the change cannot be made without removing a caveat, keep the caveat and say so.
50
52
  - A change to a number, a date, a section, a slide or the order: you cannot make it, because the script writes every number and the template sets the sections (rules 6 and 7). Change nothing. Say in one line which setting it needs: a structured report changes in the studio (it applies from the next cycle); other reports change in the template.
51
- - Then run `pmos report draft <id> <slot> --redraft`, with the slot from the lines, so the script rewrites the file, keeps every note and writes every number. Read the file once more.
53
+ - When you finish, say in one line which notes you changed, and quote each sentence you removed.
52
54
  - A message that starts with `[PMOS report change]` is the same: it names the draft, its slot and the change.
@@ -14,6 +14,7 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { spawn } = require('child_process');
16
16
  const { findBrowser } = require('./lib/find-browser');
17
+ const { openMission } = require('./lib/open-mission');
17
18
  const T = require('./test-run').configure({ name: 'split-merge-ui' });
18
19
  const { makeRepos, codeRepo, write, git } = require('./plan-test-repos');
19
20
 
@@ -68,7 +69,7 @@ const shot = (p, name) => (SHOTS ? p.screenshot({ path: path.join(SHOTS, name) }
68
69
 
69
70
  browser = await chromium.launch({ executablePath: exe });
70
71
  const open = async (o = {}) => {
71
- const ctx = await browser.newContext({ viewport: { width: o.w || 1440, height: o.h || 900 } });
72
+ const ctx = await openMission(await browser.newContext({ viewport: { width: o.w || 1440, height: o.h || 900 } }));
72
73
  const page = await ctx.newPage();
73
74
  const errors = [];
74
75
  page.on('pageerror', (e) => errors.push(String((e && e.stack) || e)));
@@ -133,7 +134,7 @@ const shot = (p, name) => (SHOTS ? p.screenshot({ path: path.join(SHOTS, name) }
133
134
 
134
135
  /* H — the merged one on Home */
135
136
  {
136
- const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
137
+ const ctx = await openMission(await browser.newContext({ viewport: { width: 1440, height: 900 } }));
137
138
  const p = await ctx.newPage();
138
139
  const errors = [];
139
140
  p.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
@@ -13,6 +13,7 @@ const fs = require('fs');
13
13
  const { spawn } = require('child_process');
14
14
  const { pathToFileURL } = require('url');
15
15
  const { findBrowser } = require('./lib/find-browser');
16
+ const { openMission } = require('./lib/open-mission');
16
17
  const T = require('./test-run').configure({ name: 'stage-card' });
17
18
  const IP = require('./lib/initiative-premium.js');
18
19
  const F = require('./fixtures/initiative-premium-facts.js');
@@ -100,7 +101,7 @@ async function pure() {
100
101
  // the board the page reads (the one view draws the board's row): the scenario's stage, so the page and the board agree as the records do
101
102
  const boardAt = (stage) => { const f = BF.facts(); f.rows = f.rows.map((r) => (r.id === 'self-serve-v2' ? { ...r, stage } : r)); return BP.build(f); };
102
103
  const open = async ({ w = 1280, h = 900, theme = 'light', model = SCEN.build(), board = BOARD, sessions = null, trail = null } = {}) => {
103
- const ctx = await browser.newContext({ viewport: { width: w, height: h } });
104
+ const ctx = await openMission(await browser.newContext({ viewport: { width: w, height: h } }));
104
105
  const page = await ctx.newPage();
105
106
  const errors = [], posts = [];
106
107
  page.on('pageerror', (e) => errors.push(String(e)));
@@ -10,6 +10,7 @@
10
10
  const path = require('path');
11
11
  const { spawn } = require('child_process');
12
12
  const { findBrowser } = require('./lib/find-browser');
13
+ const { openMission } = require('./lib/open-mission');
13
14
  const T = require('./test-run').configure({ name: 'task-press' });
14
15
  const IP = require('./lib/initiative-premium.js');
15
16
  const F = require('./fixtures/initiative-premium-facts.js');
@@ -47,7 +48,7 @@ const json = (r, b, status = 200) => r.fulfill({ status, contentType: 'applicati
47
48
  const shot = async (p, name) => { if (SHOTS) await p.screenshot({ path: path.join(SHOTS, name) }).catch(() => {}); };
48
49
 
49
50
  async function page(url) {
50
- const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
51
+ const ctx = await openMission(await browser.newContext({ viewport: { width: 1440, height: 900 } }));
51
52
  const p = await ctx.newPage();
52
53
  const errors = [], posts = [];
53
54
  p.on('pageerror', (e) => errors.push(String((e && e.message) || e)));