create-pmos 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/bin/pmos.js +15 -1
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +27 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/layout.md +98 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +80 -0
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +206 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +367 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +66 -0
- package/workspace/lib/proactive.js +7 -4
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +579 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/index.html +29 -19
- package/workspace/public/pm/app.js +299 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3054 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +176 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +105 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +94 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +197 -110
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +97 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2864 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +312 -36
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +19 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* pm/dest-today.js — the Today destination (pm-os-absorb Run 3, slice 3B, PRD R12, F8).
|
|
2
|
+
*
|
|
3
|
+
* Today lists what waits for the PM, as cards of eight kinds, ranked. The cards come from the registry's Today sources,
|
|
4
|
+
* the same way for every run: todayCards() draws them and todayCount() counts them, so this header and the nav badge add
|
|
5
|
+
* the same sources and cannot disagree. This module also registers the source for the cards the server makes itself
|
|
6
|
+
* (kind 1 acceptance and outcome, kind 2 a waiting session, kind 3 a returned step): `today-core`.
|
|
7
|
+
*
|
|
8
|
+
* mountDestination(host, ctx) runs on every render with the same host. It keeps one root in the host and replaces the root's
|
|
9
|
+
* children only when what it shows changed: the cards' own markup, the scope, the count, the notice, the fold. A card the
|
|
10
|
+
* PM is typing in is rebuilt from the card's own saved state (today-cards.js), and the focus returns to the same control.
|
|
11
|
+
*
|
|
12
|
+
* The legacy Today view (view-today.js) calls mountToday on its own host, so both surfaces draw the one list.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { el } from './dom.js';
|
|
16
|
+
import { empty, tabs, button, toast, ensureComponentsCss } from './primitives.js';
|
|
17
|
+
import { registerTodaySource, todayCards, todayCount, openInitiative } from './registry.js';
|
|
18
|
+
import { repoTuples } from './panel-repos.js';
|
|
19
|
+
import { currentProduct } from './board-read.js';
|
|
20
|
+
import { wsOf } from './data.js';
|
|
21
|
+
import { state, setState, FIXTURE } from './state.js';
|
|
22
|
+
import { COPY } from './today-copy.js';
|
|
23
|
+
import { todayCard, acceptCard, rankNodes, scopeCtx, ensureFeed, feedOf, feedErrorOf, bannerOf, onFeed } from './today-cards.js';
|
|
24
|
+
|
|
25
|
+
const SHOWN = 3; // cards drawn before "Show more": the first paint holds at most 17 controls (first-paint-today.test.js)
|
|
26
|
+
let expanded = false;
|
|
27
|
+
|
|
28
|
+
/* ── the source for the cards the server makes itself ─────────────────────────────────────── */
|
|
29
|
+
|
|
30
|
+
function coreRows(ctx) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const repo of (ctx && ctx.repos) || []) {
|
|
33
|
+
const ws = wsOf(repo);
|
|
34
|
+
if (!ws) continue;
|
|
35
|
+
ensureFeed(ws);
|
|
36
|
+
const d = feedOf(ws);
|
|
37
|
+
for (const c of (d && d.cards) || []) if (c && c.source === 'today') out.push({ repo, c });
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function openSession(c) {
|
|
43
|
+
const id = String(c.run_id || '').replace(/^session:/, '');
|
|
44
|
+
const s = (state.sessions || []).find((x) => x && x.id === id);
|
|
45
|
+
if (!s) { toast({ text: COPY.sessionGone }); return; }
|
|
46
|
+
try { const m = await import('./handoff-box.js'); await m.openSessionTab(c.initiative_id || null, s); } catch (e) { console.error('[pm] could not open the session:', e); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function drawCore({ repo, c }, ctx) {
|
|
50
|
+
const rerender = () => (ctx && ctx.setState ? ctx.setState({}) : setState({}));
|
|
51
|
+
if (c.row_kind === 'accept') return acceptCard(c, { repo, rerender });
|
|
52
|
+
const kind = Number(c.kind);
|
|
53
|
+
return todayCard({
|
|
54
|
+
kind, id: c.id, repo, ws: c.ws, initiative: c.initiative_id, cause: c.cause, evidence: c.evidence, act: c.act,
|
|
55
|
+
attrs: { 'data-today-row': c.row_kind || null },
|
|
56
|
+
onAct: () => {
|
|
57
|
+
if (c.row_kind === 'session') { openSession(c); return; }
|
|
58
|
+
if (c.initiative_id) openInitiative(repo, c.initiative_id); else toast({ text: COPY.noInitiative });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!FIXTURE) {
|
|
64
|
+
registerTodaySource({
|
|
65
|
+
id: 'today-core', kind: null, rank: 10,
|
|
66
|
+
count: (ctx) => coreRows(ctx).length,
|
|
67
|
+
cards: (ctx) => coreRows(ctx).map((x) => drawCore(x, ctx)),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── the board ─────────────────────────────────────────────────────────────────────────────── */
|
|
72
|
+
|
|
73
|
+
const roots = new WeakMap(); // host -> { root, sig, off }
|
|
74
|
+
|
|
75
|
+
function scopeToggle(all) {
|
|
76
|
+
const box = tabs({
|
|
77
|
+
variant: 'segmented', role: 'radiogroup', ariaLabel: COPY.scope.label, value: all ? 'all' : 'repo',
|
|
78
|
+
items: [{ id: 'repo', label: COPY.scope.repo }, { id: 'all', label: COPY.scope.all }],
|
|
79
|
+
onChange: (id) => setState({ todayScope: id === 'all' ? 'all' : 'repo' }),
|
|
80
|
+
attrs: { 'data-today': 'scope' },
|
|
81
|
+
});
|
|
82
|
+
const btns = box.querySelectorAll('button');
|
|
83
|
+
['repo', 'all'].forEach((id, i) => { if (btns[i]) btns[i].setAttribute('data-today-scope', id); });
|
|
84
|
+
return box;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function countText(ctx, n, ready) {
|
|
88
|
+
if (!ready && n === 0) return COPY.reading;
|
|
89
|
+
return ctx.all ? COPY.countAcross(n, ctx.repos.length) : COPY.countIn(n, ctx.repo);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function nextLabel(repos) {
|
|
93
|
+
for (const repo of repos) { const d = feedOf(wsOf(repo)); if (d && d.next && d.next.label) return d.next.label; }
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function build(ctx0) {
|
|
98
|
+
const st = ctx0.state || state;
|
|
99
|
+
const sc = scopeCtx(st, repoTuples(), currentProduct());
|
|
100
|
+
const regCtx = { all: sc.all, repos: sc.repos, setState: ctx0.setState || setState, open: (r, slug) => openInitiative(r, slug) };
|
|
101
|
+
const n = todayCount(regCtx);
|
|
102
|
+
const nodes = rankNodes(todayCards(regCtx));
|
|
103
|
+
const ready = sc.repos.length > 0 && sc.repos.every((r) => { const ws = wsOf(r); return !ws || feedOf(ws) !== null || feedErrorOf(ws) !== null; });
|
|
104
|
+
const banner = bannerOf(sc.repos);
|
|
105
|
+
nodes.forEach((node, i) => { if (!expanded && i >= SHOWN) node.hidden = true; });
|
|
106
|
+
|
|
107
|
+
const head = el('div', { class: 'td-head' },
|
|
108
|
+
el('h2', { class: 'td-count', 'data-today': 'count', 'aria-live': 'polite' }, countText(sc, n, ready)),
|
|
109
|
+
scopeToggle(sc.all));
|
|
110
|
+
const health = banner.length ? el('div', { class: 'td-health', 'data-today': 'health', role: 'status' },
|
|
111
|
+
el('p', { class: 'td-health__title' }, COPY.health.title(banner.length)),
|
|
112
|
+
el('ul', { class: 'td-health__list' }, ...banner.map((b) => el('li', null, COPY.health.line(sc.all ? b.repo + ' · ' + b.source : b.source, b.error))))) : null;
|
|
113
|
+
const list = nodes.length
|
|
114
|
+
? el('div', { class: 'td-list', 'data-today': 'needs', role: 'list', 'aria-label': COPY.aria.list }, ...nodes)
|
|
115
|
+
: el('div', { class: 'td-list', 'data-today': 'needs' }, ready && !banner.some((b) => b.source === COPY.health.todaySource) ? empty({ text: COPY.empty(nextLabel(sc.repos)), attrs: { 'data-today': 'empty' } }) : null);
|
|
116
|
+
const more = nodes.length > SHOWN
|
|
117
|
+
? button({ label: expanded ? COPY.fewer : COPY.more(nodes.length - SHOWN), variant: 'ghost', attrs: { 'data-today': 'more' }, onClick: () => { expanded = !expanded; setState({}); } })
|
|
118
|
+
: null;
|
|
119
|
+
nodes.forEach((node) => node.setAttribute('role', 'listitem'));
|
|
120
|
+
return { children: [head, health, list, more], sig: JSON.stringify([sc.all, sc.repo, n, ready, expanded, banner]) + nodes.map((x) => x.outerHTML).join('') };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The key of the control that has the focus inside the board, so it can be focused again after a rebuild. */
|
|
124
|
+
function focusKey(root) {
|
|
125
|
+
const a = document.activeElement;
|
|
126
|
+
if (!a || !root.contains(a)) return null;
|
|
127
|
+
const card = a.closest('[data-today-id]');
|
|
128
|
+
const attr = ['data-today-act', 'data-today-more', 'data-today-add-note', 'data-today-accept', 'data-today-return'].find((k) => a.hasAttribute(k));
|
|
129
|
+
if (card && attr) return { id: card.getAttribute('data-today-id'), attr };
|
|
130
|
+
if (a.closest('[data-today-note]')) return { id: card && card.getAttribute('data-today-id'), note: true };
|
|
131
|
+
if (a.hasAttribute('data-today-scope')) return { scope: a.getAttribute('data-today-scope') };
|
|
132
|
+
if (a.hasAttribute('data-today') && a.getAttribute('data-today') === 'more') return { more: true };
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function refocus(root, k) {
|
|
136
|
+
if (!k) return;
|
|
137
|
+
const card = k.id ? [...root.querySelectorAll('[data-today-id]')].find((x) => x.getAttribute('data-today-id') === k.id) : null;
|
|
138
|
+
let t = null;
|
|
139
|
+
if (k.note && card) t = card.querySelector('[data-today-note] input, [data-today-note] textarea, input[data-today-note], textarea[data-today-note]');
|
|
140
|
+
else if (k.attr && card) t = card.querySelector('[' + k.attr + ']');
|
|
141
|
+
else if (k.scope) t = root.querySelector('[data-today-scope="' + k.scope + '"]');
|
|
142
|
+
else if (k.more) t = root.querySelector('[data-today="more"]');
|
|
143
|
+
if (t && !t.hidden) { try { t.focus({ preventScroll: true }); } catch { /* not focusable */ } }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Draw Today into `host`. Safe to call on every render: nothing changes unless what Today shows changed. */
|
|
147
|
+
export function mountToday(host, ctx) {
|
|
148
|
+
if (!host) return;
|
|
149
|
+
ensureComponentsCss();
|
|
150
|
+
let r = roots.get(host);
|
|
151
|
+
if (!r) {
|
|
152
|
+
const root = el('section', { class: 'td-board', 'data-today': 'board', 'data-board': 'today', 'aria-label': COPY.title });
|
|
153
|
+
host.replaceChildren(root);
|
|
154
|
+
r = { root, sig: null, off: null };
|
|
155
|
+
roots.set(host, r);
|
|
156
|
+
// a host the old shell threw away stops listening: its next render makes a new one
|
|
157
|
+
r.off = onFeed(() => {
|
|
158
|
+
if (host.isConnected) (ctx && ctx.setState ? ctx.setState : setState)({});
|
|
159
|
+
else { r.off(); roots.delete(host); }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!r.root.isConnected) host.replaceChildren(r.root);
|
|
163
|
+
const out = build(ctx || {});
|
|
164
|
+
if (out.sig === r.sig) return;
|
|
165
|
+
const k = focusKey(r.root);
|
|
166
|
+
r.sig = out.sig;
|
|
167
|
+
r.root.replaceChildren(...out.children.filter(Boolean));
|
|
168
|
+
refocus(r.root, k);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function mountDestination(host, ctx) { mountToday(host, ctx); }
|
|
172
|
+
export function disposeDestination(host) {
|
|
173
|
+
const r = roots.get(host);
|
|
174
|
+
if (r && r.off) r.off();
|
|
175
|
+
roots.delete(host);
|
|
176
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* pm/dialogs.js — the questions the workspace asks, as sheets (pm-os-absorb Run 3, slice 3D; PRD R27, R29).
|
|
2
|
+
*
|
|
3
|
+
* `prompt`, `confirm` and `alert` block the page, cannot be styled, cannot be reached by a test without a browser hook, and on
|
|
4
|
+
* a phone they take the whole screen. Every question the workspace used to ask with one is asked here with the `sheet` primitive
|
|
5
|
+
* instead: the same words, one answer.
|
|
6
|
+
*
|
|
7
|
+
* askConfirm({ title, body, confirmLabel, danger }) → Promise<boolean> true on the confirm press, false on anything else
|
|
8
|
+
* askText({ title, body, label, value, ... }) → Promise<string | null> the text, or null when the PM backed out
|
|
9
|
+
* askFields({ title, body, fields, confirmLabel }) → Promise<object | null> { key: text } for each field, or null
|
|
10
|
+
* askChoice({ title, body, options, value, ... }) → Promise<string | null> the chosen option's value, or null
|
|
11
|
+
*
|
|
12
|
+
* Escape, the backdrop and the close control all answer "no" (false or null). The sheet carries `data-dialog="<kind>"` so a
|
|
13
|
+
* suite can find it, and its confirm control carries `data-dialog-confirm`. There is no browser fallback: when the sheet cannot
|
|
14
|
+
* be drawn (no document) the question is answered "no", so a destructive action never runs unasked.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { sheet, button, field, dialogText, ensureComponentsCss } from './primitives.js';
|
|
18
|
+
|
|
19
|
+
/** The paragraphs of a message: blank lines separate them, single newlines stay line breaks. */
|
|
20
|
+
function paragraphs(text) {
|
|
21
|
+
return String(text || '').split(/\n\s*\n/).filter((p) => p.trim()).map((p) => dialogText(p.trim()));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The one builder. `kind` names the sheet for suites; `controls` are the fields drawn above the buttons; `read()` returns the
|
|
26
|
+
* answer when the PM confirms. Resolves once, on the first close.
|
|
27
|
+
*/
|
|
28
|
+
function ask({ kind, title, sub, body, controls, confirmLabel, cancelLabel, danger, read }) {
|
|
29
|
+
if (typeof document === 'undefined') return Promise.resolve(read ? null : false);
|
|
30
|
+
ensureComponentsCss();
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
let settled = false;
|
|
33
|
+
let handle = null;
|
|
34
|
+
const finish = (answer) => { if (settled) return; settled = true; resolve(answer); };
|
|
35
|
+
const submit = () => { finish(read ? read() : true); if (handle) handle.close(); };
|
|
36
|
+
const confirmBtn = button({
|
|
37
|
+
label: confirmLabel || 'Confirm', variant: danger ? 'danger' : 'primary', onClick: submit,
|
|
38
|
+
attrs: { 'data-dialog-confirm': '' },
|
|
39
|
+
});
|
|
40
|
+
const cancelBtn = button({
|
|
41
|
+
label: cancelLabel || 'Cancel', variant: 'outline', onClick: () => { finish(read ? null : false); if (handle) handle.close(); },
|
|
42
|
+
attrs: { 'data-dialog-cancel': '' },
|
|
43
|
+
});
|
|
44
|
+
handle = sheet({
|
|
45
|
+
mode: 'auto', title, sub, body: [...paragraphs(body), ...(controls || [])], actions: [cancelBtn, confirmBtn],
|
|
46
|
+
attrs: { 'data-dialog': kind },
|
|
47
|
+
onClose: () => finish(read ? null : false),
|
|
48
|
+
});
|
|
49
|
+
const first = handle.dialog.querySelector('input, textarea, select');
|
|
50
|
+
if (first) { try { first.focus({ preventScroll: true }); if (first.select) first.select(); } catch { /* not focusable yet */ } }
|
|
51
|
+
else { try { confirmBtn.focus({ preventScroll: true }); } catch { /* not focusable yet */ } }
|
|
52
|
+
for (const c of handle.dialog.querySelectorAll('input')) {
|
|
53
|
+
c.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); submit(); } });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function askConfirm({ title, body, confirmLabel, cancelLabel, danger } = {}) {
|
|
59
|
+
return ask({ kind: 'confirm', title, body, confirmLabel, cancelLabel, danger });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function askFields({ title, sub, body, fields, confirmLabel, cancelLabel, danger } = {}) {
|
|
63
|
+
const made = (fields || []).map((f) => ({
|
|
64
|
+
key: f.key,
|
|
65
|
+
node: field({ label: f.label, value: f.value || '', placeholder: f.placeholder, attrs: { 'data-dialog-field': f.key } }),
|
|
66
|
+
}));
|
|
67
|
+
return ask({
|
|
68
|
+
kind: 'fields', title, sub, body, confirmLabel, cancelLabel, danger,
|
|
69
|
+
controls: made.map((m) => m.node),
|
|
70
|
+
read: () => Object.fromEntries(made.map((m) => [m.key, m.node.control.value])),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function askText({ title, sub, body, label, value, placeholder, confirmLabel, cancelLabel, danger } = {}) {
|
|
75
|
+
return askFields({
|
|
76
|
+
title, sub, body, confirmLabel, cancelLabel, danger,
|
|
77
|
+
fields: [{ key: 'text', label: label || title || 'Answer', value, placeholder }],
|
|
78
|
+
}).then((a) => (a ? a.text : null));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function askChoice({ title, sub, body, label, options, value, confirmLabel, cancelLabel } = {}) {
|
|
82
|
+
const node = field({ label: label || title || 'Choice', kind: 'select', value, options: options || [], attrs: { 'data-dialog-field': 'choice' } });
|
|
83
|
+
return ask({
|
|
84
|
+
kind: 'choice', title, sub, body, confirmLabel, cancelLabel,
|
|
85
|
+
controls: [node],
|
|
86
|
+
read: () => node.control.value,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* pm/display-word.js — the one word an initiative shows in the old shells (pm-os-absorb Run 3, slice 3D; PRD R3).
|
|
2
|
+
*
|
|
3
|
+
* Nobody types a status. The cockpit list, the breadcrumb and the status banner used to carry their own table of words (a
|
|
4
|
+
* `WORD` map in panel-cockpit.js, a `TONE` map in status-banner.js, a rung-to-word map). They now read the one mapping every
|
|
5
|
+
* surface reads, `displayOf` in lib/initiative-display.js (window.PMOSInitiativeDisplay), so a word cannot differ from one
|
|
6
|
+
* surface to the next:
|
|
7
|
+
*
|
|
8
|
+
* wordOf(statusRow, rec) → the word: the stage, or "Accepted" (Run 2 adds the rules: idle, waiting on, held, closed)
|
|
9
|
+
* stageOf(rec) → the stage the initiative is at (the first blocked step, else the current one), or null
|
|
10
|
+
* toneOf(statusRow) → 'd' (destructive), 'w' (warning) or null: how loudly the word is drawn
|
|
11
|
+
*
|
|
12
|
+
* `statusRow` is the derived `{ status, lifecycle }` row the server sends for the initiative (state.statuses[repo][slug]);
|
|
13
|
+
* `rec` is the initiative record the board reads (records()[slug]), whose `steps` are the workflow ladder. Both are read-only
|
|
14
|
+
* here: this file stores nothing and writes nothing. Pure over its two arguments and the display module.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { targetStepIndex } from './fixture.js';
|
|
18
|
+
|
|
19
|
+
const display = () => (typeof window !== 'undefined' ? window.PMOSInitiativeDisplay : null);
|
|
20
|
+
const groups = () => (typeof window !== 'undefined' ? window.PMOSStatusGroups : null);
|
|
21
|
+
|
|
22
|
+
/** The lifecycle state `displayOf` reads, for a derived group key that is a PM-set one. */
|
|
23
|
+
const LIFECYCLE = { 'on-hold': 'held', closed: 'closed', cancelled: 'cancelled' };
|
|
24
|
+
|
|
25
|
+
/** The stage an initiative is at: the first blocked step of its ladder, else the current one. Null when no ladder is read. */
|
|
26
|
+
export function stageOf(rec) {
|
|
27
|
+
const steps = rec && Array.isArray(rec.steps) ? rec.steps : null;
|
|
28
|
+
const ti = steps ? targetStepIndex(steps) : -1;
|
|
29
|
+
const k = ti >= 0 && steps[ti] ? String(steps[ti].k || '') : '';
|
|
30
|
+
return k || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The derived group key of one status row (`unknown` when nothing was derived). */
|
|
34
|
+
export function keyOf(statusRow) {
|
|
35
|
+
const sg = groups();
|
|
36
|
+
return sg ? sg.keyOf(statusRow) : 'unknown';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The word `displayOf` gives this initiative, or null when the display module is not loaded. */
|
|
40
|
+
export function wordOf(statusRow, rec) {
|
|
41
|
+
const d = display();
|
|
42
|
+
if (!d || typeof d.displayOf !== 'function') return null;
|
|
43
|
+
const key = keyOf(statusRow);
|
|
44
|
+
const row = { stage: stageOf(rec), accepted: key === 'accepted', last_activity: (statusRow && statusRow.last_activity) || null };
|
|
45
|
+
const lifecycle = LIFECYCLE[key] ? { state: LIFECYCLE[key], reason: (statusRow && statusRow.lifecycle && statusRow.lifecycle.reason) || '', at: null } : null;
|
|
46
|
+
const out = d.displayOf(row, { lifecycle, now: Date.now() });
|
|
47
|
+
return out && out.word ? String(out.word) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** How loudly the word is drawn: what needs the PM is destructive, what waits on their acceptance is a warning. */
|
|
51
|
+
export function toneOf(statusRow) {
|
|
52
|
+
const key = keyOf(statusRow);
|
|
53
|
+
if (key === 'needs-you' || key === 'unknown') return 'd';
|
|
54
|
+
if (key === 'awaiting-acceptance') return 'w';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* pm/dock.js — the frame's agent dock (pm-os-absorb Run 3, slice 3A; PRD R10).
|
|
2
|
+
*
|
|
3
|
+
* The dock IS panel 4: the host app.js has always kept for the tabs (#pm-tabs), with its live terminals, documents and
|
|
4
|
+
* the Web tab (D69). The frame never re-creates or re-parents it; it only shows it open (on Today) or folds it to a 44px
|
|
5
|
+
* rail (everywhere else), and the PM toggles it with ⌘J or the rail. It never opens by itself: a tab that opens while
|
|
6
|
+
* the dock is a rail waits in the dock until the PM opens it.
|
|
7
|
+
*
|
|
8
|
+
* This module holds the parts of the dock that are new in the frame:
|
|
9
|
+
* createDockRail({ open, label }) the 44px rail: one control that opens the dock
|
|
10
|
+
* foldButton({ fold }) the control in the open dock's strip that folds it back to the rail
|
|
11
|
+
* sessionMenuButton({ newSession, withOptions, openSkill, ws })
|
|
12
|
+
* "+ Session": New session, Session with options, then the skills to start
|
|
13
|
+
* from (R10: Skills start from the dock's +session menu; the Skills view goes)
|
|
14
|
+
* sessionDefaults() / setSessionDefaults(patch)
|
|
15
|
+
* the default CLI and model that Settings sets and the session menu applies
|
|
16
|
+
*
|
|
17
|
+
* The defaults live in this browser (localStorage), like the dock's other per-window choices; a fixture page reads and
|
|
18
|
+
* writes nothing.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { el } from './dom.js';
|
|
22
|
+
import { FIXTURE } from './state.js';
|
|
23
|
+
import { pmGet } from './http.js';
|
|
24
|
+
import { button, iconButton, menu } from './primitives.js';
|
|
25
|
+
import { COPY } from './frame-copy.js';
|
|
26
|
+
|
|
27
|
+
/* ── the default CLI and model ────────────────────────────────────────────────────────────────────────────────────── */
|
|
28
|
+
|
|
29
|
+
const DEFAULTS_KEY = 'pmos.workspace.pm.session-defaults';
|
|
30
|
+
export const CLIS = Object.freeze(['shell', 'claude']);
|
|
31
|
+
export const TIERS = Object.freeze(['auto', 'fast', 'balanced', 'deep']);
|
|
32
|
+
|
|
33
|
+
/** { cli: 'shell'|'claude', tier: 'auto'|'fast'|'balanced'|'deep' }. An unreadable or unknown value is the default. */
|
|
34
|
+
export function sessionDefaults() {
|
|
35
|
+
let raw = null;
|
|
36
|
+
if (!FIXTURE) { try { raw = JSON.parse(localStorage.getItem(DEFAULTS_KEY) || 'null'); } catch { raw = null; } }
|
|
37
|
+
const r = raw && typeof raw === 'object' ? raw : {};
|
|
38
|
+
return { cli: CLIS.includes(r.cli) ? r.cli : 'shell', tier: TIERS.includes(r.tier) ? r.tier : 'auto' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setSessionDefaults(patch) {
|
|
42
|
+
const next = { ...sessionDefaults(), ...(patch || {}) };
|
|
43
|
+
const clean = { cli: CLIS.includes(next.cli) ? next.cli : 'shell', tier: TIERS.includes(next.tier) ? next.tier : 'auto' };
|
|
44
|
+
if (!FIXTURE) { try { localStorage.setItem(DEFAULTS_KEY, JSON.stringify(clean)); } catch { /* private mode: the default stands */ } }
|
|
45
|
+
return clean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── the rail and the fold control ────────────────────────────────────────────────────────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
export function createDockRail({ open } = {}) {
|
|
51
|
+
const b = iconButton({ icon: 'bot', ariaLabel: COPY.dock.open, onClick: () => open && open(), attrs: { 'data-fr': 'dock-open', 'aria-keyshortcuts': 'Meta+J Control+J' } });
|
|
52
|
+
return el('div', { class: 'fr-dock-rail', 'data-fr': 'dock-rail' }, b);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function foldButton({ fold } = {}) {
|
|
56
|
+
return iconButton({ icon: 'chevron-right', ariaLabel: COPY.dock.fold, onClick: () => fold && fold(), attrs: { class: 'fr-dock-fold', 'data-fr': 'dock-fold', 'aria-keyshortcuts': 'Meta+J Control+J' } });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── + Session ────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
const skillsByWs = new Map(); // ws -> { at, list | null, error }
|
|
62
|
+
const SKILLS_TTL = 60000;
|
|
63
|
+
|
|
64
|
+
function loadSkills(ws) {
|
|
65
|
+
const key = ws || '';
|
|
66
|
+
const have = skillsByWs.get(key);
|
|
67
|
+
if (have && (have.pending || Date.now() - have.at < SKILLS_TTL)) return have.pending || Promise.resolve(have);
|
|
68
|
+
const entry = { at: Date.now(), list: have ? have.list : null, error: null };
|
|
69
|
+
entry.pending = pmGet('/api/skills' + (ws ? '?ws=' + encodeURIComponent(ws) : '')).then((j) => {
|
|
70
|
+
entry.list = j && Array.isArray(j.skills) ? j.skills : null;
|
|
71
|
+
entry.error = entry.list ? null : ((j && j.error) || COPY.dock.skillsUnread);
|
|
72
|
+
entry.at = Date.now();
|
|
73
|
+
delete entry.pending;
|
|
74
|
+
return entry;
|
|
75
|
+
});
|
|
76
|
+
skillsByWs.set(key, entry);
|
|
77
|
+
return entry.pending;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The dock's "+ Session" control. A press opens a menu: New session (the one-click start, or the options form when the
|
|
82
|
+
* default CLI is not the shell, because only the form can start another CLI), Session with options, and one item per
|
|
83
|
+
* skill of the repository. A skill opens in its own tab in the dock, where it is read and started.
|
|
84
|
+
*/
|
|
85
|
+
export function sessionMenuButton({ newSession, withOptions, openSkill, ws } = {}) {
|
|
86
|
+
if (!FIXTURE) loadSkills(ws);
|
|
87
|
+
const b = button({
|
|
88
|
+
label: COPY.dock.session, variant: 'outline', size: 'sm', icon: 'plus', iconEnd: 'chevron-down',
|
|
89
|
+
attrs: { class: 'fr-session', 'data-fr': 'session', 'aria-haspopup': 'menu', 'aria-label': COPY.dock.sessionMenu },
|
|
90
|
+
onClick: async () => {
|
|
91
|
+
const got = FIXTURE ? null : await loadSkills(ws);
|
|
92
|
+
const d = sessionDefaults();
|
|
93
|
+
const items = [
|
|
94
|
+
{ label: COPY.dock.newSession, onSelect: () => (d.cli === 'shell' ? newSession && newSession() : withOptions && withOptions(d)) },
|
|
95
|
+
{ label: COPY.dock.withOptions, onSelect: () => withOptions && withOptions(d) },
|
|
96
|
+
{ sep: true },
|
|
97
|
+
];
|
|
98
|
+
const list = got && got.list ? got.list : [];
|
|
99
|
+
if (!list.length) items.push({ label: got && got.error ? COPY.dock.skillsUnread : COPY.dock.noSkills, onSelect: () => {} });
|
|
100
|
+
for (const sk of list) items.push({ label: COPY.dock.skillsHead + ': ' + (sk.label || sk.skill || sk.id), onSelect: () => openSkill && openSkill(sk) });
|
|
101
|
+
menu({ anchor: b, items, ariaLabel: COPY.dock.sessionMenu });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return b;
|
|
105
|
+
}
|