create-pmos 0.47.0 → 0.49.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 +2 -2
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +14 -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/route.mjs +95 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +104 -19
- package/payload/mods/pmos-cockpit/types/index.d.ts +1 -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 +214 -0
- package/payload/scripts/workflow-state.js +59 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -2
- 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/index.md +2 -1
- package/workspace/guide/initiatives-and-next-steps.md +17 -2
- package/workspace/guide/knowledge-graph.md +94 -1
- package/workspace/guide/layout.md +31 -2
- package/workspace/guide/live-notes.md +78 -0
- package/workspace/guide/proactive-engine.md +6 -2
- package/workspace/guide/validate-with-typesafe.md +31 -2
- 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 +214 -0
- package/workspace/lib/engine/workflow-state.js +59 -2
- package/workspace/lib/jev-categorize.js +433 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-live.js +37 -0
- package/workspace/lib/jev-route.js +139 -0
- package/workspace/lib/kg-cli.js +3 -1
- package/workspace/lib/kg.js +6 -1
- package/workspace/lib/learn.js +5 -3
- package/workspace/lib/live-notes.js +1035 -0
- 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 +78 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +279 -18
- package/workspace/public/pm/panel-cockpit.js +638 -0
- package/workspace/public/pm/panel-detail.js +212 -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 +249 -29
- package/workspace/public/pm/state.js +48 -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-livenotes.js +397 -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 +349 -7
- package/workspace/skills/live-notes.skill +58 -0
|
@@ -48,34 +48,54 @@ const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v
|
|
|
48
48
|
|
|
49
49
|
/* ── styles: design tokens only, no raw hex (workspace-ci's token-hygiene check) ────────────── */
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
/* Drawn from planning/design/cockpit-mockup/ViewNewSession.dc.html (Run 3 of workspace-cockpit-nav):
|
|
52
|
+
* a centred dialog card over a dimmed backdrop, option cards with a radio mark for where the session
|
|
53
|
+
* works, pill chips and badges, and the two buttons bottom right. The dialog is position:fixed, so it
|
|
54
|
+
* takes no room in the panel's flow: the panes below keep their size and their place, which is the
|
|
55
|
+
* same promise the inline block made. It is still a DOM child of panel 4, so every reader that looks
|
|
56
|
+
* for the form there still finds it. */
|
|
57
|
+
const BACKDROP = 'position:fixed;inset:0;z-index:55;display:grid;place-items:center;padding:16px;'
|
|
58
|
+
+ 'background:oklch(0 0 0 / 45%)';
|
|
59
|
+
const WRAP = 'width:640px;max-width:100%;max-height:calc(100vh - 32px);overflow-y:auto;box-sizing:border-box;'
|
|
60
|
+
+ 'display:flex;flex-direction:column;gap:12px;padding:22px;background:var(--card);color:var(--foreground);'
|
|
61
|
+
+ 'border:1px solid var(--input);border-radius:12px;box-shadow:0 16px 48px oklch(0 0 0 / 50%);'
|
|
62
|
+
+ 'font-family:var(--sans);font-size:13px;line-height:1.45';
|
|
63
|
+
const HEAD = 'font-family:var(--sans);font-size:18px;font-weight:600;color:var(--foreground)';
|
|
64
|
+
const TITLE = 'font-size:12.5px;color:var(--muted-foreground)';
|
|
65
|
+
const SUB = 'font-size:12px;color:var(--muted-foreground);line-height:1.5';
|
|
66
|
+
const LABEL = 'font-family:var(--mono);font-size:10px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;'
|
|
67
|
+
+ 'color:var(--muted-foreground);margin-top:2px';
|
|
57
68
|
const ROW = 'display:flex;gap:6px;align-items:center;flex-wrap:wrap';
|
|
58
|
-
const INPUT = 'font-family:var(--mono);font-size:
|
|
59
|
-
+ 'background:var(--
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const CHIP_DEAD = '
|
|
65
|
-
|
|
66
|
-
const CHIP_BAD = 'font-family:var(--mono);font-size:10.5px;padding:4px 9px;min-height:26px;'
|
|
67
|
-
+ 'background:var(--secondary);border:1px solid var(--destructive);color:var(--destructive);opacity:.75';
|
|
69
|
+
const INPUT = 'font-family:var(--mono);font-size:12px;padding:5px 10px;min-height:32px;box-sizing:border-box;'
|
|
70
|
+
+ 'background:var(--background);border:1px solid var(--input);border-radius:8px;color:var(--foreground);min-width:240px';
|
|
71
|
+
const PILL = 'font-family:var(--mono);font-size:11px;padding:0 10px;min-height:26px;border-radius:999px;';
|
|
72
|
+
const CHIP = PILL + 'background:transparent;border:1px solid var(--border);color:var(--foreground)';
|
|
73
|
+
/* `var(--accent)` appears in CHIP_ON and in no other chip rule: pm-tabs-test reads a lit chip off it. */
|
|
74
|
+
const CHIP_ON = PILL + 'background:var(--accent);border:1px solid var(--brand);color:var(--foreground)';
|
|
75
|
+
const CHIP_DEAD = PILL + 'background:transparent;border:1px solid var(--border);color:var(--muted-foreground);opacity:.55';
|
|
76
|
+
const CHIP_BAD = PILL + 'background:transparent;border:1px solid var(--destructive);color:var(--destructive);opacity:.75';
|
|
68
77
|
const HOVER = 'border-color:var(--ring)';
|
|
69
|
-
/*
|
|
70
|
-
|
|
71
|
-
const PRIMARY = '
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const ERR = 'font-size:
|
|
76
|
-
+ 'border:1px solid var(--destructive);padding:7px
|
|
77
|
-
const WARN = 'font-size:
|
|
78
|
-
const MUTED_MONO = 'font-family:var(--mono);font-size:
|
|
78
|
+
/* The board's primary button is the inverted one: foreground on background. */
|
|
79
|
+
const BTN_BASE = 'font-family:var(--sans);font-size:12.5px;font-weight:500;padding:0 14px;min-height:32px;border-radius:8px;';
|
|
80
|
+
const PRIMARY = BTN_BASE + 'background:var(--foreground);border:1px solid var(--foreground);color:var(--background)';
|
|
81
|
+
const PRIMARY_OFF = BTN_BASE + 'background:var(--secondary);border:1px solid var(--border);color:var(--muted-foreground)';
|
|
82
|
+
const SECONDARY = BTN_BASE + 'background:transparent;border:1px solid var(--input);color:var(--foreground)';
|
|
83
|
+
const CLOSE = 'font-size:16px;line-height:1;padding:4px 6px;background:transparent;border:0;color:var(--muted-foreground)';
|
|
84
|
+
const ERR = 'font-size:12px;line-height:1.5;color:var(--destructive);border-radius:8px;'
|
|
85
|
+
+ 'border:1px solid var(--destructive);padding:7px 10px;overflow-wrap:anywhere';
|
|
86
|
+
const WARN = 'font-size:12px;line-height:1.5;color:var(--warning);overflow-wrap:anywhere';
|
|
87
|
+
const MUTED_MONO = 'font-family:var(--mono);font-size:12px;color:var(--muted-foreground);overflow-wrap:anywhere';
|
|
88
|
+
const BADGE = 'font-family:var(--sans);font-size:11px;font-weight:500;border:1px solid var(--border);border-radius:999px;'
|
|
89
|
+
+ 'padding:0 8px;line-height:18px;white-space:nowrap;color:var(--foreground)';
|
|
90
|
+
const BADGE_W = BADGE + ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)';
|
|
91
|
+
const BADGE_S = BADGE + ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)';
|
|
92
|
+
/* An option card: the board's `.opt`, with its radio mark. */
|
|
93
|
+
const OPT = 'display:flex;gap:12px;align-items:flex-start;width:100%;box-sizing:border-box;text-align:left;'
|
|
94
|
+
+ 'padding:12px 14px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--foreground);'
|
|
95
|
+
+ 'font-family:var(--sans);font-size:13px';
|
|
96
|
+
const OPT_ON = OPT + ';border-color:var(--brand);background:color-mix(in oklab, var(--brand) 8%, transparent)';
|
|
97
|
+
const RADIO = 'flex:none;width:16px;height:16px;margin-top:2px;box-sizing:border-box;border-radius:50%;border:1px solid var(--input)';
|
|
98
|
+
const RADIO_ON = RADIO + ';border:5px solid var(--brand)';
|
|
79
99
|
|
|
80
100
|
/* ── open / close ──────────────────────────────────────────────────────────────────────────── */
|
|
81
101
|
|
|
@@ -193,7 +213,8 @@ function absorbBranchBroadcast() {
|
|
|
193
213
|
function branchSection() {
|
|
194
214
|
const info = state.newSessBranches;
|
|
195
215
|
const out = [];
|
|
196
|
-
|
|
216
|
+
// The board calls this "Where it works". The word `branch` stays its own text: suites read it.
|
|
217
|
+
out.push(el('div', { style: LABEL, 'data-r29': 'where' }, el('span', null, 'where it works'), el('span', null, ' · '), el('span', null, 'branch')));
|
|
197
218
|
|
|
198
219
|
if (FIXTURE) {
|
|
199
220
|
out.push(el('div', { style: SUB }, 'fixture mode reads no repository — a session started here applies no branch'));
|
|
@@ -228,13 +249,22 @@ function branchSection() {
|
|
|
228
249
|
}
|
|
229
250
|
|
|
230
251
|
const pick = (v) => setState({ newSessBranch: v, newSessBranchMode: false, newSessError: null });
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
/* The sessions already running in this folder, read from the same list isolateNow reads. The
|
|
253
|
+
* board's "Shared with 1 running session" is this count; nothing else is claimed about them. */
|
|
254
|
+
const sharing = (state.sessions || []).filter((x) => x && x.ws === state.wsId && x.state !== 'exited');
|
|
255
|
+
const stayOn = state.newSessBranch === '' && !state.newSessBranchMode;
|
|
256
|
+
// ALWAYS first and ALWAYS the default: starting a session must never require a git decision.
|
|
257
|
+
out.push(optionCard({
|
|
258
|
+
on: stayOn, onClick: () => pick(''), hook: 'opt-current',
|
|
259
|
+
title: 'stay on current' + (current ? ' (' + current + ')' : ''),
|
|
260
|
+
meta: current ? '⎇ ' + current : null,
|
|
261
|
+
sub: sharing.length
|
|
262
|
+
? 'Shared with ' + sharing.length + ' running session' + (sharing.length === 1 ? '' : 's') + ': '
|
|
263
|
+
+ sharing.map((x) => x.name || x.id).filter(Boolean).slice(0, 3).join(', ') + '.'
|
|
264
|
+
: 'No other session runs in this folder now.',
|
|
265
|
+
badge: sharing.length ? ['they can overwrite each other', BADGE_W] : null,
|
|
266
|
+
}));
|
|
267
|
+
const chips = [];
|
|
238
268
|
for (const b of branches) {
|
|
239
269
|
if (b === current) continue;
|
|
240
270
|
const taken = elsewhere.get(b);
|
|
@@ -259,12 +289,16 @@ function branchSection() {
|
|
|
259
289
|
style: state.newSessBranch === b && !state.newSessBranchMode ? CHIP_ON : CHIP, hover: HOVER,
|
|
260
290
|
}, iv(b)));
|
|
261
291
|
}
|
|
262
|
-
chips.
|
|
292
|
+
if (chips.length) {
|
|
293
|
+
out.push(el('div', { style: SUB }, 'Or move this folder to another branch:'));
|
|
294
|
+
out.push(el('div', { style: ROW, 'data-r29': 'branches' }, chips));
|
|
295
|
+
}
|
|
296
|
+
out.push(optionCard({
|
|
297
|
+
on: !!state.newSessBranchMode, hook: 'opt-new',
|
|
263
298
|
onClick: () => setState({ newSessBranchMode: true, newSessBranch: '', newSessError: null }),
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
out.push(el('div', { style: ROW }, chips));
|
|
299
|
+
title: '+ new branch…',
|
|
300
|
+
sub: 'Creates a branch from the current one. Choose below if it gets its own folder.',
|
|
301
|
+
}));
|
|
268
302
|
|
|
269
303
|
if (state.newSessBranchMode) {
|
|
270
304
|
out.push(el('div', { style: ROW },
|
|
@@ -282,11 +316,12 @@ function branchSection() {
|
|
|
282
316
|
// that is staying put, it would be a checkbox with no meaning.
|
|
283
317
|
if (state.newSessBranchMode || state.newSessBranch) {
|
|
284
318
|
const on = isolateNow();
|
|
285
|
-
out.push(el('div', { style: ROW },
|
|
319
|
+
out.push(el('div', { style: ROW, 'data-r29': 'isolate' },
|
|
286
320
|
el('button', {
|
|
287
321
|
onClick: () => setState({ newSessIsolate: !on }),
|
|
288
322
|
style: on ? CHIP_ON : CHIP, hover: HOVER,
|
|
289
323
|
}, iv((on ? '☑' : '☐') + ' its own folder (worktree)')),
|
|
324
|
+
el('span', { style: on ? BADGE_S : BADGE_W }, iv(on ? 'isolated' : 'shared folder')),
|
|
290
325
|
el('span', { style: SUB }, iv(on
|
|
291
326
|
? 'the branch is checked out in a new folder — sessions already running are untouched'
|
|
292
327
|
: 'this repository is switched to that branch — every session already in it moves too')),
|
|
@@ -295,6 +330,19 @@ function branchSection() {
|
|
|
295
330
|
return out;
|
|
296
331
|
}
|
|
297
332
|
|
|
333
|
+
/** The board's option card: a radio mark, a bold title, a mono aside, a muted line and a badge. The
|
|
334
|
+
* title is its own span, so a reader of the panel's text still finds it as one item. */
|
|
335
|
+
function optionCard({ on, onClick, hook, title, meta, sub, badge }) {
|
|
336
|
+
return el('button', { onClick, style: on ? OPT_ON : OPT, hover: on ? null : HOVER, 'data-r29': hook, 'data-on': on ? 'yes' : 'no' },
|
|
337
|
+
el('span', { style: on ? RADIO_ON : RADIO }),
|
|
338
|
+
el('span', { style: 'display:flex;flex-direction:column;gap:2px;min-width:0' },
|
|
339
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline;flex-wrap:wrap' },
|
|
340
|
+
el('b', { style: 'font-weight:600' }, iv(title)),
|
|
341
|
+
meta ? el('span', { style: MUTED_MONO }, iv(meta)) : null),
|
|
342
|
+
sub ? el('span', { style: SUB }, iv(sub)) : null,
|
|
343
|
+
badge ? el('span', { style: 'margin-top:4px' }, el('span', { style: badge[1] }, iv(badge[0]))) : null));
|
|
344
|
+
}
|
|
345
|
+
|
|
298
346
|
const shortPath = (p) => { const s = String(p || ''); const i = s.lastIndexOf('/'); return i < 0 ? s : s.slice(i + 1); };
|
|
299
347
|
|
|
300
348
|
/** What the isolate checkbox currently reads — the PM's choice if they made one, else the server's
|
|
@@ -505,53 +553,64 @@ export function newSessionBlock() {
|
|
|
505
553
|
const slug = state.newSess.slug;
|
|
506
554
|
const busy = !!state.newSessBusy;
|
|
507
555
|
|
|
508
|
-
|
|
509
|
-
el('
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
el('
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
556
|
+
const badges = [slug, state.product && state.product !== slug ? state.product : null, state.newSessAgent || 'shell']
|
|
557
|
+
.filter(Boolean).map((b) => el('span', { style: BADGE }, iv(b)));
|
|
558
|
+
|
|
559
|
+
return el('div', { style: BACKDROP, 'data-r29': 'newsess' },
|
|
560
|
+
el('div', { style: WRAP, role: 'dialog', 'aria-label': 'New session', 'data-board': 'new-session' },
|
|
561
|
+
el('div', { style: 'display:flex;justify-content:space-between;align-items:center;gap:8px' },
|
|
562
|
+
el('span', { style: HEAD, 'data-r29': 'newsess-title' }, 'New session'),
|
|
563
|
+
el('button', { onClick: () => closeNewSession(), style: CLOSE, hover: 'color:var(--foreground)', disabled: busy,
|
|
564
|
+
'aria-label': 'close', title: 'close without starting', 'data-r29': 'newsess-close' }, '×'),
|
|
565
|
+
),
|
|
566
|
+
el('div', { style: 'display:flex;gap:8px;flex-wrap:wrap', 'data-r29': 'newsess-badges' }, badges),
|
|
567
|
+
el('div', { style: 'display:flex;gap:8px;align-items:baseline;flex-wrap:wrap' },
|
|
568
|
+
el('span', { style: TITLE }, 'start a session — more options'),
|
|
569
|
+
el('span', { style: MUTED_MONO }, iv(slug)),
|
|
570
|
+
),
|
|
571
|
+
el('div', { style: SUB },
|
|
572
|
+
'What this session runs, and where. + session does all of this in one click on the rule; '
|
|
573
|
+
+ 'this is for the times the rule is not what you want.'),
|
|
574
|
+
|
|
575
|
+
el('div', { style: LABEL }, 'name'),
|
|
576
|
+
el('div', { style: ROW },
|
|
577
|
+
el('input', {
|
|
578
|
+
value: state.newSessName, placeholder: slug + ' 1', style: INPUT, disabled: busy,
|
|
579
|
+
onInput: (e) => { state.newSessName = e.target.value; },
|
|
580
|
+
onChange: () => setState({}),
|
|
581
|
+
}),
|
|
582
|
+
),
|
|
583
|
+
|
|
584
|
+
el('div', { style: LABEL }, 'agent'),
|
|
585
|
+
el('div', { style: ROW },
|
|
586
|
+
['shell', 'claude'].map((a) => el('button', {
|
|
587
|
+
onClick: () => setState({ newSessAgent: a, newSessError: null }),
|
|
588
|
+
style: state.newSessAgent === a ? CHIP_ON : CHIP, hover: HOVER,
|
|
589
|
+
}, iv(a))),
|
|
590
|
+
el('span', { style: SUB }, 'a CLI that is not on PATH is refused by the server, and says so'),
|
|
591
|
+
),
|
|
592
|
+
|
|
593
|
+
modelSection(),
|
|
594
|
+
el('div', { style: 'display:flex;flex-direction:column;gap:8px', 'data-r29': 'newsess-where' }, branchSection()),
|
|
595
|
+
el('div', { style: 'display:flex;flex-direction:column;gap:8px', 'data-r29': 'newsess-profile' }, profileSection()),
|
|
596
|
+
|
|
597
|
+
// The refusal, where the question was asked. It is rendered ABOVE the button so the PM reads it
|
|
598
|
+
// before pressing again, and it survives until something changes.
|
|
599
|
+
state.newSessError ? el('div', { style: ERR }, iv(state.newSessError)) : null,
|
|
600
|
+
|
|
601
|
+
el('div', { style: SUB, 'data-r29': 'newsess-advice' },
|
|
602
|
+
'Nothing appears until the server says it started. If it refuses — an unreadable profile, a '
|
|
603
|
+
+ 'branch git will not check out — you will read the reason here and no session, tab or '
|
|
604
|
+
+ 'terminal will have been created.'),
|
|
605
|
+
|
|
606
|
+
el('div', { style: 'display:flex;gap:8px;align-items:center;justify-content:flex-end;flex-wrap:wrap;padding-top:2px', 'data-r29': 'newsess-actions' },
|
|
607
|
+
el('button', { onClick: () => closeNewSession(), style: SECONDARY, hover: HOVER, disabled: busy }, 'cancel'),
|
|
608
|
+
el('button', {
|
|
609
|
+
onClick: () => { if (!busy) onStart(); },
|
|
610
|
+
disabled: busy, style: busy ? PRIMARY_OFF : PRIMARY, hover: busy ? null : 'opacity:.9',
|
|
611
|
+
}, busy ? 'starting…' : 'start session'),
|
|
612
|
+
),
|
|
549
613
|
),
|
|
550
|
-
|
|
551
|
-
el('div', { style: SUB },
|
|
552
|
-
'Nothing appears until the server says it started. If it refuses — an unreadable profile, a '
|
|
553
|
-
+ 'branch git will not check out — you will read the reason here and no session, tab or '
|
|
554
|
-
+ 'terminal will have been created.'),
|
|
555
614
|
);
|
|
556
615
|
}
|
|
557
616
|
|
|
@@ -106,14 +106,14 @@ async function connectData() {
|
|
|
106
106
|
connectData();
|
|
107
107
|
|
|
108
108
|
/** allProds (line 1431) — the base repositories plus anything the PM added in this window. */
|
|
109
|
-
function repoTuples() {
|
|
109
|
+
export function repoTuples() {
|
|
110
110
|
if (FIXTURE) return productsList(state.addedProducts || []);
|
|
111
111
|
const added = (state.addedProducts || []).map((x) => [x.id, x.sub, x.kind || 'github']);
|
|
112
112
|
return [...(realRepos || []), ...added];
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/** `product` (line 1396): the state field, defaulting to the first repository in the list. */
|
|
116
|
-
function activeProduct(tuples) {
|
|
116
|
+
export function activeProduct(tuples) {
|
|
117
117
|
if (state.product) return state.product;
|
|
118
118
|
const list = tuples || repoTuples();
|
|
119
119
|
return list.length ? list[0][0] : null;
|
|
@@ -255,11 +255,11 @@ function repoButton(tuple, product) {
|
|
|
255
255
|
const chipStyle = 'flex:none;font-family:var(--mono);font-size:10px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground);min-height:22px';
|
|
256
256
|
const actStyle = 'flex:none;font-size:12px;padding:2px 6px;color:var(--muted-foreground);min-height:28px';
|
|
257
257
|
|
|
258
|
-
function treesOf(name) {
|
|
258
|
+
export function treesOf(name) {
|
|
259
259
|
return dataMod && typeof dataMod.productTrees === 'function' ? dataMod.productTrees(name) : [];
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
async function renameRepo(name) {
|
|
262
|
+
export async function renameRepo(name) {
|
|
263
263
|
const win = typeof window !== 'undefined' && window.prompt ? window : null;
|
|
264
264
|
const label = win ? win.prompt('Rename “' + name + '” to:', name) : null;
|
|
265
265
|
if (label === null || !String(label).trim()) return;
|
|
@@ -329,10 +329,11 @@ async function removeRepo(name) {
|
|
|
329
329
|
setState({ repoNotice: null });
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
/** What the live app adds under a repository: the chips (trees, sync, profile, audit, rename, remove) and the blocks
|
|
333
|
+
* they open. Empty in fixture mode, where the repository row is the prototype's and carries nothing more. The cockpit
|
|
334
|
+
* list column draws this under the repository's name; the phone's repository list draws it under the row. */
|
|
335
|
+
export function manageParts(name, product) {
|
|
336
|
+
if (FIXTURE || !dataMod) return { tools: [], below: [] };
|
|
336
337
|
const trees = treesOf(name);
|
|
337
338
|
/* The trees list is always available — the main tree is a line of work too, and a repository with
|
|
338
339
|
* one tree still has branches to create. The active repository opens it by default; others on a
|
|
@@ -359,23 +360,39 @@ function repoEntry(tuple, product) {
|
|
|
359
360
|
tools.push(el('button', { onClick: () => renameRepo(name), style: actStyle, hover: 'color:var(--foreground)', title: 'rename this repository', 'data-r1': 'rename' }, '✎'));
|
|
360
361
|
tools.push(el('button', { onClick: () => removeRepo(name), style: actStyle, hover: 'color:var(--destructive)', title: 'remove this repository from PMOS (no folder is deleted)', 'data-r1': 'remove' }, '✕'));
|
|
361
362
|
}
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
{ const
|
|
366
|
-
|
|
367
|
-
{ const tn = treeNoteFor(name); if (tn) kids.push(tn); }
|
|
368
|
-
{ const ab = auditBlock(name); if (ab) kids.push(ab); }
|
|
363
|
+
const below = [];
|
|
364
|
+
{ const bf = branchForm(name); if (bf) below.push(bf); }
|
|
365
|
+
if (open && trees.length) below.push(treesBlock(name, trees, product === name));
|
|
366
|
+
{ const tn = treeNoteFor(name); if (tn) below.push(tn); }
|
|
367
|
+
{ const ab = auditBlock(name); if (ab) below.push(ab); }
|
|
369
368
|
const n = state.repoNotice;
|
|
370
369
|
if (n && n.name === name) {
|
|
371
|
-
|
|
370
|
+
below.push(el('div', {
|
|
372
371
|
style: 'padding:4px 14px 8px 39px;font-size:11px;color:' + (n.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)'),
|
|
373
372
|
'data-r1': 'notice',
|
|
374
373
|
}, ip(n.text)));
|
|
375
374
|
}
|
|
375
|
+
return { tools, below };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function repoEntry(tuple, product) {
|
|
379
|
+
const row = repoButton(tuple, product);
|
|
380
|
+
if (FIXTURE || !dataMod) return row;
|
|
381
|
+
const { tools, below } = manageParts(tuple[0], product);
|
|
382
|
+
const kids = [el('div', { style: 'display:flex;align-items:center' },
|
|
383
|
+
el('div', { style: 'flex:1;min-width:0' }, row),
|
|
384
|
+
tools.length ? el('div', { style: 'display:flex;align-items:center;gap:2px;padding-right:8px' }, tools) : null)];
|
|
385
|
+
for (const b of below) kids.push(b);
|
|
376
386
|
return el('div', null, kids);
|
|
377
387
|
}
|
|
378
388
|
|
|
389
|
+
/** The abbreviation a repository shows on a rail tile: the one the PM set, else its first letters. */
|
|
390
|
+
export function repoInitial(name) {
|
|
391
|
+
const set = dataMod && dataMod.repoInitials ? dataMod.repoInitials(name) : null;
|
|
392
|
+
return (set || String(name).replace(/[^A-Za-z0-9]+/g, ' ').trim().split(' ').filter(Boolean).slice(0, 2)
|
|
393
|
+
.map((w) => w[0]).join('') || String(name).slice(0, 2)).toUpperCase();
|
|
394
|
+
}
|
|
395
|
+
|
|
379
396
|
/**
|
|
380
397
|
* overhaul-r1 R4 — the collapsed repositories rail, one glyph per repository, LIVE mode only (the
|
|
381
398
|
* fixture rail is the prototype's and the comparator reads it). Initials + kind badge; a dot when a
|
|
@@ -447,7 +464,7 @@ export function onAddProduct() { setDetail({ kind: 'addprod' }); }
|
|
|
447
464
|
* resolved. It is deliberately NOT dismissible: the condition is still true after the click.
|
|
448
465
|
* Nothing renders when the state is settled — including in fixture mode, which has no data layer
|
|
449
466
|
* and therefore never sets the key, so the comparator sees the prototype's markup unchanged. */
|
|
450
|
-
function stateHealthBanner() {
|
|
467
|
+
export function stateHealthBanner() {
|
|
451
468
|
const h = state.stateHealth;
|
|
452
469
|
if (!h || h.ok !== false) return null;
|
|
453
470
|
return el('div', {
|