create-pmos 0.46.0 → 0.48.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/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +13 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
- package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +189 -18
- package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
- package/payload/okf/core/concepts/frontmatter-format.md +6 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +204 -0
- package/payload/scripts/workflow-state.js +52 -0
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +3 -3
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/initiatives-and-next-steps.md +16 -2
- package/workspace/guide/knowledge-graph.md +89 -2
- package/workspace/guide/layout.md +30 -2
- package/workspace/guide/proactive-engine.md +26 -4
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +17 -1
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +204 -0
- package/workspace/lib/engine/workflow-state.js +52 -0
- package/workspace/lib/jev-categorize.js +399 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-route.js +129 -0
- package/workspace/lib/kg-cli.js +5 -2
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +26 -9
- package/workspace/lib/learn.js +282 -0
- package/workspace/lib/live-notes.js +890 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +58 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +267 -21
- package/workspace/public/pm/panel-cockpit.js +636 -0
- package/workspace/public/pm/panel-detail.js +210 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +145 -27
- package/workspace/public/pm/state.js +45 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +394 -10
- package/workspace/skills/live-notes.skill +58 -0
- package/workspace/skills/morning-brief.skill +1 -0
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import { el, setChildren } from './dom.js';
|
|
53
|
-
import { state, setState, FIXTURE } from './state.js';
|
|
53
|
+
import { state, setState, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
|
|
54
|
+
import { isMobile } from './mobile.js';
|
|
54
55
|
import * as FX from './fixture.js';
|
|
55
56
|
import { openDoc, openTab, openTerm, fileNewModel } from './panel-initiatives.js';
|
|
56
57
|
import { addProductBody } from './panel-repos.js';
|
|
@@ -59,6 +60,10 @@ import { statusBanner, lifecycleNotes } from './status-banner.js';
|
|
|
59
60
|
import { stepActions } from './next-step-ui.js';
|
|
60
61
|
import { boardOf, records, currentProduct } from './board-read.js';
|
|
61
62
|
|
|
63
|
+
/** The cockpit's own look of this panel (workspace-cockpit-nav): the mockup's header, chips and phase rows. A phone and `?layout=legacy`
|
|
64
|
+
* keep the prototype's look, which the older suites and the phone still read. */
|
|
65
|
+
const cockpitLook = () => !LEGACY_LAYOUT_FLAG && !isMobile();
|
|
66
|
+
|
|
62
67
|
/* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
|
|
63
68
|
|
|
64
69
|
/** `{{ expr }}` — an interpolated value, wrapped the way the prototype's runtime wraps it. */
|
|
@@ -374,6 +379,22 @@ function noLadderBlock(ansBg, ansC, ansText, noLadderText) {
|
|
|
374
379
|
* all done → done; some done → doing; else todo. Collapse is per phase PER INITIATIVE (`pkey`), so
|
|
375
380
|
* folding `build` on one initiative does not fold it on the next.
|
|
376
381
|
*/
|
|
382
|
+
/**
|
|
383
|
+
* What the documents say, beside the ladder and never in it (status-from-artifacts, Run 1). One plain
|
|
384
|
+
* line each, with its basis. A flag colours its own dot only; no phase, count or total reads it.
|
|
385
|
+
*/
|
|
386
|
+
function factsBlock(rec) {
|
|
387
|
+
const facts = (rec && rec.facts) || [];
|
|
388
|
+
if (!facts.length) return null;
|
|
389
|
+
return el('div', { 'data-facts': 'status-from-artifacts', style: 'border-top:1px solid var(--border);padding:8px 14px 8px' },
|
|
390
|
+
el('div', { style: 'font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground);padding-bottom:5px' }, iv('What the documents say')),
|
|
391
|
+
...facts.map((f) => el('div', { style: 'display:grid;grid-template-columns:14px 104px 1fr;gap:6px;align-items:baseline;padding:2px 0;font-size:12px;line-height:1.45' },
|
|
392
|
+
el('span', { style: 'font-size:9px;color:' + (FX.TONE[f.tone] || FX.C.mut) }, iv('●')),
|
|
393
|
+
el('span', { style: 'color:var(--muted-foreground)' }, iv(f.label)),
|
|
394
|
+
el('span', { style: 'color:var(--foreground)' }, iv(f.text)),
|
|
395
|
+
)));
|
|
396
|
+
}
|
|
397
|
+
|
|
377
398
|
function phaseBlock(slug, rec) {
|
|
378
399
|
const phases = FX.phasesFor(rec, slug);
|
|
379
400
|
if (!phases.length) return null;
|
|
@@ -735,7 +756,7 @@ function syncSummary(sy, dest) {
|
|
|
735
756
|
const ROLE_CHIP = { 'user stories': 'stories' };
|
|
736
757
|
function roleChip(role) { return ROLE_CHIP[role] || role; }
|
|
737
758
|
|
|
738
|
-
function artifactRow(slug, a) {
|
|
759
|
+
function artifactRow(slug, a, noHead) {
|
|
739
760
|
const key = FX.artKey(slug, a.path);
|
|
740
761
|
// `hasOwnProperty`, not `||`. An unlinked artifact records `null` under its key, and a falsy check
|
|
741
762
|
// would fall straight through to the server's `a.dest` and re-draw the link the PM just withdrew.
|
|
@@ -841,7 +862,7 @@ function artifactRow(slug, a) {
|
|
|
841
862
|
}, '+ ', iv(ls.l))),
|
|
842
863
|
);
|
|
843
864
|
|
|
844
|
-
return el('div', { style: 'min-width:0' }, head, linkRow, openHere ? destForm(openHere, '') : null);
|
|
865
|
+
return el('div', { style: 'min-width:0' }, noHead ? null : head, linkRow, openHere ? destForm(openHere, '') : null);
|
|
845
866
|
}
|
|
846
867
|
|
|
847
868
|
// Linked: the destination pill, the sync button, the PM's own controls, and the status line —
|
|
@@ -909,7 +930,7 @@ function artifactRow(slug, a) {
|
|
|
909
930
|
panel = realSyncPanel(slug, a, key, dest);
|
|
910
931
|
}
|
|
911
932
|
|
|
912
|
-
return el('div', { style: 'min-width:0' }, head, pillRow, openHere ? destForm(openHere, dest.target) : null, panel);
|
|
933
|
+
return el('div', { style: 'min-width:0' }, noHead ? null : head, pillRow, openHere ? destForm(openHere, dest.target) : null, panel);
|
|
913
934
|
}
|
|
914
935
|
|
|
915
936
|
/**
|
|
@@ -1139,6 +1160,98 @@ function footer(rec) {
|
|
|
1139
1160
|
|
|
1140
1161
|
/* ── the `dInit` branch, assembled in the prototype's order ─────────────────────────────────── */
|
|
1141
1162
|
|
|
1163
|
+
/* ── the cockpit look: the mockup's title block and phase rows (planning/design/cockpit-mockup/WsA.dc.html) ────────────────── */
|
|
1164
|
+
|
|
1165
|
+
const CK_BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;color:var(--foreground);white-space:nowrap';
|
|
1166
|
+
const CK_SEP = () => el('div', { style: 'height:1px;background:var(--border)' });
|
|
1167
|
+
|
|
1168
|
+
/** Title, summary and chips: the lane, the anchor and the flow, as pills. */
|
|
1169
|
+
function cockpitTitle(name, tag, pills) {
|
|
1170
|
+
return [
|
|
1171
|
+
el('div', { style: 'font:600 17px var(--sans);overflow-wrap:anywhere' }, iv(name)),
|
|
1172
|
+
el('div', { style: 'color:var(--muted-foreground)' }, iv(tag)),
|
|
1173
|
+
el('div', { style: 'display:flex;gap:6px;flex-wrap:wrap' }, pills.map((pl) => el('span', { style: CK_BADGE }, iv(pl.t)))),
|
|
1174
|
+
];
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const CK_PH_DOT = { done: 'var(--success)', blocked: 'var(--destructive)', doing: 'var(--info)', todo: 'var(--muted-foreground)' };
|
|
1178
|
+
|
|
1179
|
+
/** The phases as the mockup draws them: a row per phase with its count, its tasks beneath while it is open. */
|
|
1180
|
+
function phaseBlockCockpit(slug, rec) {
|
|
1181
|
+
const phases = FX.phasesFor(rec, slug);
|
|
1182
|
+
if (!phases.length) return null;
|
|
1183
|
+
const collP = state.collapsedPhases || {};
|
|
1184
|
+
const repo = repoOf(slug);
|
|
1185
|
+
/* Run 4 (the mockup): only the phase the initiative is at opens by itself — the same pointer the breadcrumb uses (the first
|
|
1186
|
+
* blocked step, else the current one). Every other phase shows its row and its count, and opens on a click. A phase the PM has
|
|
1187
|
+
* opened or folded keeps that choice (`collapsedPhases` holds true or false for it). */
|
|
1188
|
+
const stepAt = Array.isArray(rec.steps) ? FX.targetStepIndex(rec.steps) : -1;
|
|
1189
|
+
const atKey = stepAt >= 0 && rec.steps[stepAt] ? rec.steps[stepAt].k : null;
|
|
1190
|
+
const anyTaskTab = (state.tabs || []).some((t) => t.id === state.active && t.kind === 'task' && t.slug === slug);
|
|
1191
|
+
return el('div', { 'data-cockpit': 'phases', style: 'display:flex;flex-direction:column;gap:2px' }, phases.map((ph) => {
|
|
1192
|
+
const pkey = slug + '|' + ph.key;
|
|
1193
|
+
const open = Object.prototype.hasOwnProperty.call(collP, pkey) ? !collP[pkey] : ph.key === atKey;
|
|
1194
|
+
// the task the initiative is at: the first blocked (else in-progress) task of that phase, marked while no task tab is in front
|
|
1195
|
+
const atTask = ph.key === atKey && !anyTaskTab ? ph.tasks.findIndex((t) => t.status === 'blocked') : -1;
|
|
1196
|
+
const atIdx = atTask >= 0 ? atTask : (ph.key === atKey && !anyTaskTab ? ph.tasks.findIndex((t) => t.status === 'doing') : -1);
|
|
1197
|
+
const head = el('button', {
|
|
1198
|
+
onClick: () => setState((s) => ({ collapsedPhases: { ...s.collapsedPhases, [pkey]: open } })),
|
|
1199
|
+
style: 'display:flex;align-items:center;gap:8px;min-height:32px;font:500 11px var(--mono);letter-spacing:.08em;text-transform:uppercase;text-align:left;width:100%',
|
|
1200
|
+
},
|
|
1201
|
+
el('span', null, open ? '▾' : '▸'),
|
|
1202
|
+
el('span', { style: 'width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;background:' + (CK_PH_DOT[ph.status] || CK_PH_DOT.todo) }),
|
|
1203
|
+
ph.label.charAt(0).toUpperCase() + ph.label.slice(1),
|
|
1204
|
+
el('span', { style: 'margin-left:auto;color:var(--muted-foreground)' }, iv(ph.count)),
|
|
1205
|
+
);
|
|
1206
|
+
const rows = open ? ph.tasks.map((t, ti) => {
|
|
1207
|
+
const tid = FX.taskTabId(slug, ph.key, ti);
|
|
1208
|
+
const active = state.active === tid || ti === atIdx;
|
|
1209
|
+
const [glyph, color] = FX.TDOT[t.status] || FX.TDOT.todo;
|
|
1210
|
+
return el('button', {
|
|
1211
|
+
onClick: () => openTask({
|
|
1212
|
+
id: tid, kind: 'task', role: 'TASK', title: t.title, slug, repo, phase: ph.key,
|
|
1213
|
+
status: t.status, instruction: t.instruction, input: t.input, expect: t.expect,
|
|
1214
|
+
evalNote: t.evalNote, output: t.output,
|
|
1215
|
+
}),
|
|
1216
|
+
'aria-current': ti === atIdx ? 'step' : null,
|
|
1217
|
+
style: 'display:flex;align-items:center;gap:10px;min-height:34px;padding:0 8px 0 24px;border-radius:8px;font:400 13px var(--sans);text-align:left;width:100%;background:' + (active ? 'var(--accent)' : 'transparent'),
|
|
1218
|
+
hover: 'background:var(--accent)',
|
|
1219
|
+
},
|
|
1220
|
+
el('span', { style: 'color:' + color }, glyph),
|
|
1221
|
+
el('span', { style: 'flex-grow:1' }, iv(t.title)),
|
|
1222
|
+
el('span', { style: 'color:var(--muted-foreground)' }, '↗'),
|
|
1223
|
+
);
|
|
1224
|
+
}) : [];
|
|
1225
|
+
return [head, rows];
|
|
1226
|
+
}));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** The blocker card (workspace-cockpit-nav): drawn only when a rung of the ladder is blocking, in the engine's own words. The cockpit
|
|
1230
|
+
* draws it as the mockup's row: the words on the left, and on the right the slot of the Grant button, which stays empty until the
|
|
1231
|
+
* grants ledger behind it exists (PRD Q3) — the slot keeps the card's height, so nothing moves when the button arrives. */
|
|
1232
|
+
function blockerCard(b) {
|
|
1233
|
+
if (cockpitLook()) {
|
|
1234
|
+
const said = el('span', { style: 'color:var(--destructive)' }, iv(b.label + ' blocked' + (b.evidence ? ': ' + b.evidence : '')));
|
|
1235
|
+
return el('div', {
|
|
1236
|
+
'data-cockpit': 'blocker',
|
|
1237
|
+
style: 'border:1px solid color-mix(in oklab, var(--destructive) 50%, transparent);background:color-mix(in oklab, var(--destructive) 8%, transparent);border-radius:8px;padding:10px 12px;display:flex;align-items:center;justify-content:space-between;gap:10px',
|
|
1238
|
+
},
|
|
1239
|
+
b.next
|
|
1240
|
+
? el('div', { style: 'display:flex;flex-direction:column;gap:4px;min-width:0' }, said,
|
|
1241
|
+
el('span', { style: 'color:var(--muted-foreground);font-size:12px;line-height:1.5' }, iv('next: ' + b.next)))
|
|
1242
|
+
: said,
|
|
1243
|
+
el('span', { 'data-cockpit': 'grant-slot', 'aria-hidden': 'true', style: 'flex:none;width:0;height:30px' }),
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
return el('div', {
|
|
1247
|
+
'data-cockpit': 'blocker',
|
|
1248
|
+
style: 'border:1px solid color-mix(in oklab, var(--destructive) 50%, transparent);background:color-mix(in oklab, var(--destructive) 8%, transparent);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px',
|
|
1249
|
+
},
|
|
1250
|
+
el('span', { style: 'color:var(--destructive);font-size:13px' }, iv(b.label + ' blocked' + (b.evidence ? ': ' + b.evidence : ''))),
|
|
1251
|
+
b.next ? el('span', { style: 'color:var(--muted-foreground);font-size:12px;line-height:1.5' }, iv('next: ' + b.next)) : null,
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1142
1255
|
function initBody(slug) {
|
|
1143
1256
|
const rec = slug ? records()[slug] : null;
|
|
1144
1257
|
if (!rec) return [noneBody()];
|
|
@@ -1153,7 +1266,12 @@ function initBody(slug) {
|
|
|
1153
1266
|
return out;
|
|
1154
1267
|
}
|
|
1155
1268
|
|
|
1156
|
-
|
|
1269
|
+
const ck = cockpitLook();
|
|
1270
|
+
if (ck) return cockpitInitBody(slug, rec);
|
|
1271
|
+
{
|
|
1272
|
+
out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null, lanePicker(slug, rec)));
|
|
1273
|
+
if (rec.blocker) out.push(el('div', { style: 'margin:12px 14px 0' }, blockerCard(rec.blocker)));
|
|
1274
|
+
}
|
|
1157
1275
|
{ const sb = statusBanner(repoOf(slug), slug, rec); if (sb) out.push(sb); }
|
|
1158
1276
|
{ const lc = lifecycleRow(repoOf(slug), slug); if (lc) { out.push(lc); const ln = lifecycleNotes(rec); if (ln) out.push(ln); } }
|
|
1159
1277
|
|
|
@@ -1170,8 +1288,9 @@ function initBody(slug) {
|
|
|
1170
1288
|
const nextB = nextStepsBlock(slug, rec);
|
|
1171
1289
|
if (nextB) out.push(nextB);
|
|
1172
1290
|
|
|
1173
|
-
const phases = phaseBlock(slug, rec);
|
|
1174
|
-
|
|
1291
|
+
if (!ck) { const phases = phaseBlock(slug, rec); if (phases) out.push(phases); }
|
|
1292
|
+
|
|
1293
|
+
{ const fb = factsBlock(rec); if (fb) out.push(fb); }
|
|
1175
1294
|
|
|
1176
1295
|
out.push(el('div', { style: 'border-top:1px solid var(--border)' },
|
|
1177
1296
|
linksRow(slug),
|
|
@@ -1182,6 +1301,70 @@ function initBody(slug) {
|
|
|
1182
1301
|
return out;
|
|
1183
1302
|
}
|
|
1184
1303
|
|
|
1304
|
+
/* ── the cockpit's initiative (Run 4: the mockup's column, section for section) ─────────────────────────────────────────────
|
|
1305
|
+
* The detail host is the mockup's column: 16px padding and a 12px gap between its sections (renderDetail sets that). In order:
|
|
1306
|
+
* title, summary, chips, the blocker card, a rule, the phases, then what the mockup does not draw (the anchor line, the lane picker,
|
|
1307
|
+
* the status, the sessions, the next steps, the facts), a rule, the artifacts, the links and the derivation line. A section with
|
|
1308
|
+
* nothing to show is not drawn, so it leaves no gap. The sections the mockup does not draw were laid out for the full width of the
|
|
1309
|
+
* column, so they keep it (`FULL`). */
|
|
1310
|
+
const FULL = 'margin:0 -16px';
|
|
1311
|
+
function cockpitInitBody(slug, rec) {
|
|
1312
|
+
const ph = phaseBlockCockpit(slug, rec);
|
|
1313
|
+
const out = [...cockpitTitle(slug, rec.tag || '', rec.pills || [])];
|
|
1314
|
+
if (rec.blocker) out.push(blockerCard(rec.blocker));
|
|
1315
|
+
if (ph) out.push(CK_SEP(), ph);
|
|
1316
|
+
const more = [crumbBlock(rec.crumb || null), lanePicker(slug, rec)].filter(Boolean);
|
|
1317
|
+
if (more.length) out.push(el('div', null, more));
|
|
1318
|
+
const wide = [];
|
|
1319
|
+
{ const sb = statusBanner(repoOf(slug), slug, rec); if (sb) wide.push(sb); }
|
|
1320
|
+
{ const lc = lifecycleRow(repoOf(slug), slug); if (lc) { wide.push(lc); const ln = lifecycleNotes(rec); if (ln) wide.push(ln); } }
|
|
1321
|
+
if (rec.noLadder) {
|
|
1322
|
+
const ansC = FX.TONE[(rec.answer && rec.answer.tone) || 'info'] || FX.C.mut;
|
|
1323
|
+
wide.push(...noLadderBlock(FX.wash(ansC), ansC, (rec.answer && rec.answer.text) || '', rec.noLadder));
|
|
1324
|
+
}
|
|
1325
|
+
{ const sess = sessionsBlock(slug); if (sess) wide.push(sess); }
|
|
1326
|
+
{ const nextB = nextStepsBlock(slug, rec); if (nextB) wide.push(nextB); }
|
|
1327
|
+
{ const fb = factsBlock(rec); if (fb) wide.push(fb); }
|
|
1328
|
+
if (wide.length) out.push(el('div', { style: FULL }, wide));
|
|
1329
|
+
out.push(CK_SEP(), cockpitArtifacts(slug, rec));
|
|
1330
|
+
// The links row's + controls record a reference through the server; the fixture has no server and no links, so it has none.
|
|
1331
|
+
const links = (rec.links || []).length || ((state.initLinks || {})[slug] || []).length || !FIXTURE ? linksRow(slug) : null;
|
|
1332
|
+
if (links) out.push(el('div', { style: FULL + ';border-top:1px solid var(--border)' }, links));
|
|
1333
|
+
// The derivation line says which working tree the panel was read from and when. The fixture read none, so it has no line.
|
|
1334
|
+
if (!FIXTURE) out.push(el('div', { style: 'margin:auto -16px -16px' }, footer(rec)));
|
|
1335
|
+
return out;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/** The mockup's artifacts: a label, then a row per artifact — its role, its path — with the artifact's link and sync controls
|
|
1339
|
+
* under the row (`data-art-links`). The mockup shows a sync badge in the row ("⇄ in sync"); the app never claims that, because it
|
|
1340
|
+
* has never compared the two documents (see SYNC_NOT_A_SYNC), so the row says what is recorded through the controls instead. */
|
|
1341
|
+
const CK_LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
1342
|
+
function cockpitArtifacts(slug, rec) {
|
|
1343
|
+
const arts = rec.arts || [];
|
|
1344
|
+
const rows = [];
|
|
1345
|
+
for (const a of arts) {
|
|
1346
|
+
rows.push(el('button', {
|
|
1347
|
+
onClick: () => openDoc(slug, a.path, a.role), 'data-art-row': a.path, title: a.path,
|
|
1348
|
+
style: 'display:flex;align-items:center;gap:8px;width:100%;text-align:left', hover: 'background:var(--accent)',
|
|
1349
|
+
},
|
|
1350
|
+
el('span', { style: CK_BADGE + ';font-family:var(--mono)' }, iv(roleChip(a.role))),
|
|
1351
|
+
el('span', { 'data-art-path': '', style: 'flex-grow:1;min-width:0;color:var(--muted-foreground);font:11px var(--mono)' }, iv(shortPath(a.path))),
|
|
1352
|
+
));
|
|
1353
|
+
rows.push(el('div', { 'data-art-links': a.path }, artifactRow(slug, a, true)));
|
|
1354
|
+
}
|
|
1355
|
+
if (!arts.length) rows.push(el('span', { style: 'font-size:11.5px;color:var(--muted-foreground);font-style:italic' }, 'none filed'));
|
|
1356
|
+
const conv = conversationsBlock(slug, rec), git = gitFilesBlock(slug, rec);
|
|
1357
|
+
return el('div', { 'data-cockpit': 'artifacts', style: 'display:flex;flex-direction:column;gap:6px' },
|
|
1358
|
+
el('div', { style: CK_LBL }, 'Artifacts'), rows, conv, git);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/** A path as the mockup writes one: whole when it is short, else its first 24 characters, an ellipsis and its last 7, so the folder and
|
|
1362
|
+
* the end of the name both stay readable ("planning/prd/ooh-weather…-prd.md"). The row's tooltip carries the whole path. */
|
|
1363
|
+
export function shortPath(p) {
|
|
1364
|
+
const s0 = String(p || '');
|
|
1365
|
+
return s0.length <= 32 ? s0 : s0.slice(0, 24) + '…' + s0.slice(-7);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1185
1368
|
/* ── `dNone` (lines 838–840) ────────────────────────────────────────────────────────────────── */
|
|
1186
1369
|
|
|
1187
1370
|
function noneBody() {
|
|
@@ -1439,7 +1622,26 @@ export function renderDetail(host) {
|
|
|
1439
1622
|
else if (kind === 'none') body = [noneBody()];
|
|
1440
1623
|
else body = modeDetailBody(kind);
|
|
1441
1624
|
|
|
1442
|
-
|
|
1625
|
+
// the cockpit draws an initiative without the panel's own title bar (the top bar's breadcrumb names it); its fold control returns with Focus
|
|
1626
|
+
const bare = cockpitLook() && kind === 'init';
|
|
1627
|
+
/* The cockpit's column (outside Focus) is the mockup's: the page's own background, and for an initiative 16px of padding and a 12px
|
|
1628
|
+
* gap between sections. The legacy shell, a phone and Focus keep the panel's sidebar fill and draw their own padding. */
|
|
1629
|
+
const ckCol = cockpitLook() && !state.layoutFocus;
|
|
1630
|
+
host.style.background = ckCol ? 'transparent' : 'var(--sidebar)';
|
|
1631
|
+
host.style.padding = ckCol && bare ? '16px' : '';
|
|
1632
|
+
host.style.gap = ckCol && bare ? '12px' : '';
|
|
1633
|
+
if (bare && state.layoutFocus) {
|
|
1634
|
+
// Focus (Run 2): the detail is a 360px slice with a sticky bar that names what is asked of the PM and leads back to the lists
|
|
1635
|
+
const slug = selectedSlug();
|
|
1636
|
+
const rows = (state.statuses && state.statuses[state.product]) || {};
|
|
1637
|
+
const key = window.PMOSStatusGroups ? window.PMOSStatusGroups.keyOf(rows[slug]) : 'unknown';
|
|
1638
|
+
const ask = key === 'needs-you' ? 'Needs you' : key === 'awaiting-acceptance' ? 'Awaiting your acceptance' : 'Nothing is asked of you';
|
|
1639
|
+
const bar = el('div', { 'data-focus': 'bar', style: 'position:sticky;top:0;z-index:2;display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--sidebar)' },
|
|
1640
|
+
el('span', { style: 'font:500 12px var(--sans);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, ask),
|
|
1641
|
+
el('button', { 'data-focus': 'navigate', onClick: () => setState({ layoutFocus: false }), style: 'margin-left:auto;font:500 11px var(--sans);border:1px solid var(--border);border-radius:6px;padding:2px 8px', hover: 'background:var(--accent)' }, 'Navigate'));
|
|
1642
|
+
return setChildren(host, [bar, ...body]);
|
|
1643
|
+
}
|
|
1644
|
+
return setChildren(host, bare ? body : [stickyHeader, ...body]);
|
|
1443
1645
|
}
|
|
1444
1646
|
|
|
1445
1647
|
export default renderDetail;
|
|
@@ -96,7 +96,7 @@ function baseGroups(product) {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/** GROUPS (lines 1425–1430) — user groups appended, user KRs and user initiatives folded in. */
|
|
99
|
-
function groupsFor(product) {
|
|
99
|
+
export function groupsFor(product) {
|
|
100
100
|
const extraGroups = (state.userGroups && state.userGroups[product]) || [];
|
|
101
101
|
return [...baseGroups(product), ...extraGroups].map((g) => {
|
|
102
102
|
const extra = (state.addedInits && state.addedInits[product + '|' + g.gid]) || [];
|
|
@@ -117,10 +117,10 @@ function firstSlugOf(product) {
|
|
|
117
117
|
return g0 && g0.slugs && g0.slugs.length ? g0.slugs[0] : null;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const selectedSlug = () => (state.sel !== null && state.sel !== undefined ? state.sel : firstSlugOf(currentProduct()));
|
|
120
|
+
export const selectedSlug = () => (state.sel !== null && state.sel !== undefined ? state.sel : firstSlugOf(currentProduct()));
|
|
121
121
|
|
|
122
122
|
/** sessionsOf (line 1398) — a record that failed to read has no sessions, it does not throw. */
|
|
123
|
-
function sessionsOf(slug, DATA) {
|
|
123
|
+
export function sessionsOf(slug, DATA) {
|
|
124
124
|
const i = DATA[slug];
|
|
125
125
|
if (!i || i.readError) return [];
|
|
126
126
|
return [...(i.sessions || []), ...((state.extraSess && state.extraSess[slug]) || [])];
|
|
@@ -157,7 +157,7 @@ function liveSessions() {
|
|
|
157
157
|
* why this returns the whole shape rather than just the map: a running agent PMOS cannot attribute
|
|
158
158
|
* still has to be visible somewhere, and dropping it was half the defect this fixes.
|
|
159
159
|
*/
|
|
160
|
-
function attribution() {
|
|
160
|
+
export function attribution() {
|
|
161
161
|
const empty = { byInitiative: {}, via: {}, unattributed: [] };
|
|
162
162
|
if (FIXTURE) return empty;
|
|
163
163
|
const ws = WS();
|
|
@@ -172,7 +172,7 @@ function attribution() {
|
|
|
172
172
|
* `onBoard` is what stops a tab or a working tree belonging to ANOTHER repository putting a row on
|
|
173
173
|
* this repository's board — `slugProduct()` is the same map panel 4 scopes its tabs by.
|
|
174
174
|
*/
|
|
175
|
-
function workingSet(attr, product) {
|
|
175
|
+
export function workingSet(attr, product) {
|
|
176
176
|
if (FIXTURE) return [];
|
|
177
177
|
const ws = WS();
|
|
178
178
|
if (!ws) return [];
|
|
@@ -198,7 +198,7 @@ function workingSet(attr, product) {
|
|
|
198
198
|
*/
|
|
199
199
|
const HASH = ['#', 'var(--muted-foreground)', ''];
|
|
200
200
|
|
|
201
|
-
function leadOf(slug, live) {
|
|
201
|
+
export function leadOf(slug, live) {
|
|
202
202
|
const rows = live[slug];
|
|
203
203
|
if (!rows || !rows.length) return HASH;
|
|
204
204
|
const ss = SS();
|
|
@@ -397,7 +397,7 @@ function groupNode(g, product) {
|
|
|
397
397
|
* knowledge of what the session is for into a recorded declaration, which is the only way a session
|
|
398
398
|
* started before this field existed can ever join an initiative.
|
|
399
399
|
*/
|
|
400
|
-
function orphanRows(rows) {
|
|
400
|
+
export function orphanRows(rows) {
|
|
401
401
|
const ss = SS();
|
|
402
402
|
const ws = WS();
|
|
403
403
|
const head = el('div', {
|
|
@@ -451,7 +451,7 @@ function orphanRows(rows) {
|
|
|
451
451
|
* `state.extraSess`, the sessions this window started, which have no server row until the next poll.
|
|
452
452
|
* `seen` is what stops one session arriving twice when both paths find it.
|
|
453
453
|
*/
|
|
454
|
-
function searchHits(q, DATA, live) {
|
|
454
|
+
export function searchHits(q, DATA, live) {
|
|
455
455
|
const hits = [];
|
|
456
456
|
if (!q) return hits;
|
|
457
457
|
const seen = new Set();
|
|
@@ -483,7 +483,7 @@ function searchHits(q, DATA, live) {
|
|
|
483
483
|
return hits.slice(0, 8);
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
-
function hitNode(ht) {
|
|
486
|
+
export function hitNode(ht) {
|
|
487
487
|
return el('button', {
|
|
488
488
|
onClick: ht.on,
|
|
489
489
|
style: 'display:block;width:100%;padding:6px 12px;min-height:40px',
|