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.
- package/package.json +38 -14
- 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-adddoc-test.js +2 -1
- package/workspace/briefing-premium-board-test.js +5 -4
- package/workspace/briefing-premium-initiative-test.js +2 -1
- package/workspace/changelog.json +37 -0
- package/workspace/guide/briefing-premium.md +2 -1
- package/workspace/guide/initiatives-screen.md +1 -1
- package/workspace/guide/report-slides.md +6 -6
- package/workspace/guide/reports.md +1 -1
- package/workspace/guide/workflows.md +15 -3
- package/workspace/ini-workflow-test.js +2 -1
- package/workspace/lib/engine/workflows.js +19 -6
- package/workspace/lib/initiative-cli.js +15 -1
- package/workspace/lib/open-mission.js +14 -0
- package/workspace/lib/report-export.js +5 -3
- package/workspace/lib/report-pyramid.js +134 -33
- package/workspace/lib/report-templates.js +33 -3
- package/workspace/lib/routes-boards.js +35 -2
- package/workspace/lib/routes-brief-page.js +2 -1
- package/workspace/lib/routes-initiatives.js +1 -0
- package/workspace/lib/routes-report-design.js +1 -1
- package/workspace/lib/routes-report-flow.js +11 -4
- package/workspace/lib/routes-report-setups.js +3 -2
- package/workspace/lib/routes-workflows.js +6 -1
- package/workspace/lib/workflow-cli.js +2 -2
- package/workspace/lib/workflows-store.js +15 -1
- package/workspace/metric-drafts-ui-test.js +2 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-board-actions-test.js +2 -1
- package/workspace/premium-vw-backlog-test.js +3 -2
- package/workspace/premium-vw-item-test.js +3 -2
- package/workspace/premium-vw-jira-test.js +3 -2
- package/workspace/program-panel-test.js +2 -1
- package/workspace/public/pm/cozy-home.css +1 -0
- package/workspace/public/pm/home-app.js +3 -1
- package/workspace/public/pm/home-chat.js +49 -8
- package/workspace/public/pm/ini-view.js +2 -2
- package/workspace/public/pm/premium-shell.js +57 -6
- package/workspace/public/pm/premium.css +1 -0
- package/workspace/public/pm/wf-page.css +8 -0
- package/workspace/public/pm/wf-page.js +33 -5
- package/workspace/skills/report-template.skill +4 -2
- package/workspace/split-merge-ui-test.js +3 -2
- package/workspace/stage-card-test.js +2 -1
- package/workspace/task-press-test.js +2 -1
- 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.
|
|
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.
|
|
9
|
+
"version": "0.495.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
-
|
|
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; }
|
|
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:
|
|
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
|
-
|
|
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,
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `<
|
|
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
|
-
|
|
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
|
|
393
|
-
|
|
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:
|
|
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
|
-
-
|
|
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)));
|