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
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { el, setChildren } from './dom.js';
|
|
30
|
-
import { state, setState, FIXTURE } from './state.js';
|
|
30
|
+
import { state, setState, FIXTURE, LEGACY_LAYOUT_FLAG } from './state.js';
|
|
31
|
+
import { isMobile } from './mobile.js';
|
|
31
32
|
import * as FX from './fixture.js';
|
|
32
33
|
import { openTab, openTerm as openTermTab, openDoc as openDocTab } from './panel-initiatives.js';
|
|
33
34
|
import { mountDoc, disposeDoc, docText } from './tab-doc.js';
|
|
@@ -41,7 +42,9 @@ import { boardOf, currentProduct } from './board-read.js';
|
|
|
41
42
|
* far smaller failure than a dead panel 4. When it lands, the panel repaints itself. */
|
|
42
43
|
let termMod = null;
|
|
43
44
|
let liveMod = null;
|
|
45
|
+
let cockpitMod = null;
|
|
44
46
|
/* Loaded lazily; a repaint is asked for only when a skill tab is already open (restored on reload) and waiting for it. */
|
|
47
|
+
import('./panel-cockpit.js').then((m) => { cockpitMod = m; if ((state.tabs || []).some((t) => t.kind === 'view')) setState({}); }).catch(() => { cockpitMod = null; });
|
|
45
48
|
import('./modes-live.js').then((m) => { liveMod = m; if ((state.tabs || []).some((t) => t.kind === 'skill' || t.kind === 'transcript')) setState({}); }).catch(() => { liveMod = null; });
|
|
46
49
|
import('./tab-term.js')
|
|
47
50
|
.then((m) => { termMod = m; setState({}); })
|
|
@@ -332,7 +335,41 @@ const CHIP_STYLE = 'flex:none;align-self:center;font-family:var(--mono);font-siz
|
|
|
332
335
|
const SPLIT_STYLE = 'flex:none;margin-left:auto;align-self:center;font-family:var(--mono);font-size:11px;color:oklch(0.85 0.01 214);padding:5px 12px;min-height:30px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%);white-space:nowrap';
|
|
333
336
|
const CHIP_HOVER = 'border-color:oklch(0.56 0.021 213.5)';
|
|
334
337
|
|
|
335
|
-
const tabBar = el('div', { style: BAR_STYLE });
|
|
338
|
+
const tabBar = el('div', { 'data-tabs': 'bar', style: BAR_STYLE, onWheel: stripWheel });
|
|
339
|
+
|
|
340
|
+
/* ── the cockpit's tab strip (workspace-cockpit-nav Run 4, planning/design/cockpit-mockup/WsA.dc.html) ─────────────────────────
|
|
341
|
+
* On a desktop the strip is the mockup's: a 44px band whose tabs sit on its floor, the tab in front a card with rounded top corners,
|
|
342
|
+
* the others plain, each a dot and a title in 12px mono, and `+ session` at the right end. A phone and `?layout=legacy` keep the
|
|
343
|
+
* prototype's strip above. What the mockup does not draw is kept (R12): a tab's second line (its initiative and session) sits under
|
|
344
|
+
* its title in the tab's own lower padding, so the title stays where the mockup puts it; a tab that is not in front keeps its close
|
|
345
|
+
* control, in its right padding; the web, options and split controls sit to the left of `+ session`. */
|
|
346
|
+
const cockpitLook = () => !LEGACY_LAYOUT_FLAG && !isMobile();
|
|
347
|
+
const CK_BAR = 'flex:none;display:flex;align-items:flex-end;gap:4px;padding:0 10px;height:45px;border-bottom:1px solid var(--border);min-width:0;overflow-x:auto';
|
|
348
|
+
/* ── the tabs as a vertical list (the PM's request after testing 0.48.0: the strip gets crowded with many tabs open) ─────────────
|
|
349
|
+
* `state.tabsVertical` puts the open tabs in a column on the left of the panes, one row per tab with its title, its second line and
|
|
350
|
+
* its close control, and the session controls under them. The choice is stored with the layout, so it survives a reload. It is a
|
|
351
|
+
* cockpit (desktop) choice only: a phone and `?layout=legacy` keep the strip.
|
|
352
|
+
*
|
|
353
|
+
* NOTHING MOVES WHEN IT IS SWITCHED. The panes, the chooser, the scratchpad and the bottom bar live in one persistent column
|
|
354
|
+
* (`tabsMain`) that is always the second child of the host; the switch only turns the host's flex direction and restyles the bar.
|
|
355
|
+
* A live terminal is never re-parented by it. */
|
|
356
|
+
const CK_VBAR = 'flex:none;width:224px;display:flex;flex-direction:column;align-items:stretch;gap:2px;padding:8px 6px;border-right:1px solid var(--border);min-height:0;overflow-y:auto;overflow-x:hidden';
|
|
357
|
+
const tabsMain = el('div', { 'data-tabs': 'main', style: 'flex:1;min-height:0;min-width:0;display:flex;flex-direction:column' });
|
|
358
|
+
const orientLabel = iv('');
|
|
359
|
+
const orientBtn = el('button', {
|
|
360
|
+
onClick: () => setState({ tabsVertical: !state.tabsVertical }), 'data-tabs': 'orient',
|
|
361
|
+
}, orientLabel);
|
|
362
|
+
/* In the strip, a mouse wheel scrolls the tabs sideways: with many tabs open the strip is wider than the panel, and a wheel that
|
|
363
|
+
* did nothing there left only the thin scrollbar. */
|
|
364
|
+
function stripWheel(e) {
|
|
365
|
+
if (state.tabsVertical || !e.deltaY || Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
|
|
366
|
+
const bar = e.currentTarget;
|
|
367
|
+
if (bar.scrollWidth <= bar.clientWidth) return;
|
|
368
|
+
bar.scrollLeft += e.deltaY;
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const CK_BTN = 'flex:none;margin:0 0 4px;height:30px;display:grid;place-items:center;font:500 12.5px var(--sans);line-height:normal;background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;white-space:nowrap';
|
|
336
373
|
|
|
337
374
|
const newSessionBtn = el('button', {
|
|
338
375
|
onClick: () => newSession(), style: CHIP_STYLE, hover: CHIP_HOVER,
|
|
@@ -368,7 +405,7 @@ const emptyState = el('div', {
|
|
|
368
405
|
style: 'flex:1;display:grid;place-items:center;color:oklch(0.55 0.02 213);font-size:12.5px;padding:20px;text-align:center',
|
|
369
406
|
}, "nothing open — click a task's output or an artifact, or + session above");
|
|
370
407
|
|
|
371
|
-
const panesRow = el('div', { style: 'flex:1;display:flex;min-height:0;min-width:0;overflow:hidden' });
|
|
408
|
+
const panesRow = el('div', { 'data-tabs': 'panes', style: 'flex:1;display:flex;min-height:0;min-width:0;overflow:hidden' });
|
|
372
409
|
|
|
373
410
|
/* ── the scratchpad (lines 983–991) ──────────────────────────────────────────────────────────
|
|
374
411
|
* One textarea, created once. Its text is written back as a CHILD TEXT NODE as well as a value,
|
|
@@ -395,9 +432,16 @@ const SCRATCH_PLACEHOLDER = FIXTURE
|
|
|
395
432
|
? 'scratchpad — survives tab switches, dies with the page'
|
|
396
433
|
: 'scratchpad — one pad per session, saved on this machine';
|
|
397
434
|
|
|
435
|
+
/* workspace-cockpit-nav Run 3 (PRD R10) — the drawer takes the form of the cockpit board
|
|
436
|
+
* `planning/design/cockpit-mockup/ViewFilesPad.dc.html`: a labelled header with a fold control, the pad
|
|
437
|
+
* as a bordered field, and one row of buttons with a short note. The textarea stays a DIRECT child of
|
|
438
|
+
* `scratchBlock` (the height ceiling and pm-tabs-test measure through `parentElement`), and every
|
|
439
|
+
* behaviour below — the per-session pad, the autosave, the height handle — is unchanged. */
|
|
398
440
|
const scratchTa = el('textarea', {
|
|
399
441
|
placeholder: SCRATCH_PLACEHOLDER,
|
|
400
|
-
|
|
442
|
+
'data-scratch': 'text',
|
|
443
|
+
style: 'height:96px;flex:none;width:100%;border:1px solid var(--input);border-radius:8px;background:var(--background);'
|
|
444
|
+
+ 'color:var(--foreground);padding:10px 12px;font:12.5px/1.55 var(--mono)',
|
|
401
445
|
onInput: (e) => onScratchInput(e.target.value),
|
|
402
446
|
});
|
|
403
447
|
|
|
@@ -457,9 +501,10 @@ const scratchGrip = el('div', {
|
|
|
457
501
|
'aria-orientation': 'horizontal',
|
|
458
502
|
'aria-label': 'resize the scratchpad — drag, or ↑ / ↓',
|
|
459
503
|
title: 'drag to resize · ↑ / ↓ to nudge · double-click to reset',
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
504
|
+
'data-scratch': 'grip',
|
|
505
|
+
// full width across the drawer's padding, on its top edge
|
|
506
|
+
style: 'flex:none;height:7px;margin:-12px -18px 0;cursor:ns-resize;background:var(--sidebar);touch-action:none',
|
|
507
|
+
hover: 'background:var(--border)',
|
|
463
508
|
onPointerdown: (e) => {
|
|
464
509
|
dragFrom = { y: e.clientY, h: state.scratchH || SCRATCH_DEFAULT };
|
|
465
510
|
try { e.target.setPointerCapture(e.pointerId); } catch { /* not captured — the move still works */ }
|
|
@@ -564,6 +609,13 @@ if (typeof window !== 'undefined' && !FIXTURE) {
|
|
|
564
609
|
document.addEventListener('visibilitychange', () => { if (document.hidden) flushPad(); });
|
|
565
610
|
}
|
|
566
611
|
|
|
612
|
+
/* The board's two button forms, on the token system: `.btnp` (the one primary action) and `.btn`. */
|
|
613
|
+
const PAD_BTN = 'flex:none;font:500 12.5px var(--sans);border-radius:8px;padding:0 12px;height:28px;'
|
|
614
|
+
+ 'display:inline-grid;place-items:center;white-space:nowrap;';
|
|
615
|
+
const PAD_BTN_PRIMARY = PAD_BTN + 'background:var(--foreground);color:var(--background);border:0';
|
|
616
|
+
const PAD_BTN_OUTLINE = PAD_BTN + 'background:transparent;color:var(--foreground);border:1px solid var(--input)';
|
|
617
|
+
const PAD_BTN_OFF = PAD_BTN + 'background:transparent;color:var(--muted-foreground);border:1px solid var(--input)';
|
|
618
|
+
|
|
567
619
|
const scratchCopyLabel = iv('⧉ copy');
|
|
568
620
|
const scratchCopyBtn = el('button', {
|
|
569
621
|
onClick: () => {
|
|
@@ -571,24 +623,50 @@ const scratchCopyBtn = el('button', {
|
|
|
571
623
|
setState({ scratchCopied: true });
|
|
572
624
|
setTimeout(() => setState({ scratchCopied: false }), 1200);
|
|
573
625
|
},
|
|
574
|
-
|
|
575
|
-
|
|
626
|
+
'data-scratch': 'copy',
|
|
627
|
+
style: PAD_BTN_OUTLINE,
|
|
628
|
+
hover: 'background:var(--accent)',
|
|
576
629
|
}, scratchCopyLabel);
|
|
577
630
|
|
|
578
631
|
const scratchSendLabel = iv('open a terminal to send');
|
|
579
632
|
const scratchSendBtn = el('button', {
|
|
580
633
|
onClick: () => sendScratchToTerminal(),
|
|
581
|
-
|
|
582
|
-
|
|
634
|
+
'data-scratch': 'send',
|
|
635
|
+
style: PAD_BTN_OFF,
|
|
583
636
|
}, scratchSendLabel);
|
|
584
637
|
|
|
638
|
+
/* Clear — the board's second button. It empties the pad through the same path as typing, so the
|
|
639
|
+
* empty pad is saved for this session like any other edit. Disabled when there is nothing to clear. */
|
|
640
|
+
const scratchClearBtn = el('button', {
|
|
641
|
+
onClick: () => { if (state.scratchText) onScratchInput(''); },
|
|
642
|
+
'data-scratch': 'clear',
|
|
643
|
+
style: PAD_BTN_OUTLINE,
|
|
644
|
+
hover: 'background:var(--accent)',
|
|
645
|
+
}, 'clear');
|
|
646
|
+
|
|
647
|
+
const scratchHeadLabel = el('span', {
|
|
648
|
+
'data-scratch': 'label',
|
|
649
|
+
style: 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)',
|
|
650
|
+
}, 'scratchpad');
|
|
651
|
+
const scratchFoldBtn = el('button', {
|
|
652
|
+
onClick: () => setState({ scratchOpen: false }),
|
|
653
|
+
'data-scratch': 'fold', 'aria-label': 'close the scratchpad', title: 'Close the scratchpad. The text is kept.',
|
|
654
|
+
style: 'flex:none;color:var(--muted-foreground);padding:0 4px', hover: 'color:var(--foreground)',
|
|
655
|
+
}, '▾');
|
|
656
|
+
const scratchNote = el('span', {
|
|
657
|
+
'data-scratch': 'note',
|
|
658
|
+
style: 'min-width:0;color:var(--muted-foreground);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
|
|
659
|
+
});
|
|
660
|
+
|
|
585
661
|
const scratchBlock = el('div', {
|
|
586
|
-
|
|
662
|
+
'data-scratch': 'pad',
|
|
663
|
+
style: 'display:flex;flex-direction:column;gap:8px;padding:12px 18px;border-top:1px solid var(--border);background:var(--sidebar)',
|
|
587
664
|
},
|
|
588
665
|
FIXTURE ? null : scratchGrip,
|
|
666
|
+
el('div', { style: 'display:flex;align-items:center;justify-content:space-between' }, scratchHeadLabel, scratchFoldBtn),
|
|
589
667
|
scratchTa,
|
|
590
|
-
el('div', { style: 'display:flex;
|
|
591
|
-
scratchCopyBtn,
|
|
668
|
+
el('div', { style: 'display:flex;align-items:center;gap:8px;min-width:0' },
|
|
669
|
+
scratchSendBtn, scratchCopyBtn, scratchClearBtn, scratchNote),
|
|
592
670
|
);
|
|
593
671
|
|
|
594
672
|
/* ── the bottom bar (lines 992–996) ─────────────────────────────────────────────────────────── */
|
|
@@ -598,6 +676,7 @@ const colTitleLabel = iv('scratchpad');
|
|
|
598
676
|
const copyLabel = iv('copy');
|
|
599
677
|
|
|
600
678
|
const bottomBar = el('div', {
|
|
679
|
+
'data-tabs': 'bottom',
|
|
601
680
|
style: 'display:flex;gap:10px;align-items:center;padding:5px 12px;border-top:1px solid oklch(1 0 0 / 10%);font-size:11px;color:oklch(0.6 0.02 213)',
|
|
602
681
|
},
|
|
603
682
|
el('button', {
|
|
@@ -833,6 +912,8 @@ function paneFor(tab) {
|
|
|
833
912
|
if (liveMod && typeof liveMod.mountSkillTab === 'function') liveMod.mountSkillTab(host, tab);
|
|
834
913
|
} else if (tab.kind === 'transcript') {
|
|
835
914
|
if (liveMod && typeof liveMod.mountTranscriptTab === 'function') liveMod.mountTranscriptTab(host, tab);
|
|
915
|
+
} else if (tab.kind === 'view') {
|
|
916
|
+
if (cockpitMod && typeof cockpitMod.mountView === 'function') cockpitMod.mountView(host, tab);
|
|
836
917
|
} else if (tab.kind === 'web') {
|
|
837
918
|
mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
|
|
838
919
|
} else {
|
|
@@ -937,6 +1018,78 @@ export function tabBranch(t, includeMain) {
|
|
|
937
1018
|
return trees.length > 1 && main ? main.branch : null;
|
|
938
1019
|
}
|
|
939
1020
|
|
|
1021
|
+
function cockpitTabNode(t, act, splitTab, vertical) {
|
|
1022
|
+
const front = (act && act.id === t.id) || (splitTab && splitTab.id === t.id);
|
|
1023
|
+
if (vertical) return cockpitTabRow(t, front);
|
|
1024
|
+
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1025
|
+
const meta = t.kind === 'term'
|
|
1026
|
+
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1027
|
+
: (t.kind === 'task' ? 'task' : t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1028
|
+
const br = tabBranch(t);
|
|
1029
|
+
return el('div', {
|
|
1030
|
+
'data-tab': t.id, 'data-tab-active': front ? '1' : '0',
|
|
1031
|
+
// A tab may shrink to 96px when the strip is crowded; the title is clipped with an ellipsis and the full title is its tooltip.
|
|
1032
|
+
style: 'position:relative;flex:0 1 auto;min-width:96px;display:flex;align-items:center;gap:8px;padding:0 14px;font:12px var(--mono);'
|
|
1033
|
+
+ (front ? 'height:37px;border:1px solid var(--border);border-bottom:0;border-radius:8px 8px 0 0;background:var(--card);color:var(--foreground)'
|
|
1034
|
+
: 'height:36px;color:var(--muted-foreground)'),
|
|
1035
|
+
},
|
|
1036
|
+
el('button', {
|
|
1037
|
+
onClick: () => setState({ active: t.id }),
|
|
1038
|
+
title: t.title + (t.slug ? '\n' + t.slug + ' · ' + meta : ''),
|
|
1039
|
+
style: 'display:flex;gap:8px;align-items:center;align-self:stretch;color:inherit;min-width:0',
|
|
1040
|
+
},
|
|
1041
|
+
el('span', { 'data-tab-dot': t.kind, style: 'width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;background:' + dotColor + ';color:' + dotColor }),
|
|
1042
|
+
el('span', { style: 'display:flex;flex-direction:column;min-width:0;align-items:flex-start' },
|
|
1043
|
+
el('span', { style: 'max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(t.title)),
|
|
1044
|
+
el('span', {
|
|
1045
|
+
'data-tab-sub': '',
|
|
1046
|
+
// under the title, inside the tab's own width (the tab is its containing block), clipped to it
|
|
1047
|
+
style: 'position:absolute;left:30px;right:14px;bottom:1px;font:9px var(--mono);line-height:10px;color:oklch(0.52 0.02 213);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:left',
|
|
1048
|
+
}, iv(t.slug), ' · ', iv(meta), br ? el('span', { 'data-r1': 'tab-branch' }, iv(' · ⑂ ' + br)) : null),
|
|
1049
|
+
),
|
|
1050
|
+
),
|
|
1051
|
+
el('button', {
|
|
1052
|
+
onClick: () => closeTab(t.id), 'data-tab-close': '', 'aria-label': 'close ' + t.title, title: 'close',
|
|
1053
|
+
style: 'color:var(--muted-foreground)' + (front ? '' : ';position:absolute;right:3px;top:50%;transform:translateY(-50%)'),
|
|
1054
|
+
hover: 'color:var(--foreground)',
|
|
1055
|
+
}, '×'),
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** One tab as a row of the vertical list: the dot, the title over its second line, and the close control, all inside the row. */
|
|
1060
|
+
function cockpitTabRow(t, front) {
|
|
1061
|
+
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1062
|
+
const meta = t.kind === 'term'
|
|
1063
|
+
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1064
|
+
: (t.kind === 'task' ? 'task' : t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1065
|
+
const br = tabBranch(t);
|
|
1066
|
+
return el('div', {
|
|
1067
|
+
'data-tab': t.id, 'data-tab-active': front ? '1' : '0', 'data-tab-row': '',
|
|
1068
|
+
style: 'flex:none;display:flex;align-items:center;gap:6px;padding:5px 6px 5px 10px;border-radius:6px;font:12px var(--mono);border:1px solid '
|
|
1069
|
+
+ (front ? 'var(--border);background:var(--card);color:var(--foreground)' : 'transparent;color:var(--muted-foreground)'),
|
|
1070
|
+
hover: front ? null : 'background:var(--accent)',
|
|
1071
|
+
},
|
|
1072
|
+
el('button', {
|
|
1073
|
+
onClick: () => setState({ active: t.id }),
|
|
1074
|
+
title: t.title + (t.slug ? '\n' + t.slug + ' · ' + meta : ''),
|
|
1075
|
+
style: 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;color:inherit;text-align:left',
|
|
1076
|
+
},
|
|
1077
|
+
el('span', { 'data-tab-dot': t.kind, style: 'width:8px;height:8px;border-radius:50%;display:inline-block;flex-shrink:0;background:' + dotColor + ';color:' + dotColor }),
|
|
1078
|
+
el('span', { style: 'flex:1;min-width:0;display:flex;flex-direction:column;align-items:stretch' },
|
|
1079
|
+
el('span', { style: 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(t.title)),
|
|
1080
|
+
el('span', {
|
|
1081
|
+
'data-tab-sub': '',
|
|
1082
|
+
style: 'font:9.5px var(--mono);line-height:12px;color:oklch(0.52 0.02 213);overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
|
|
1083
|
+
}, iv(t.slug), ' · ', iv(meta), br ? el('span', { 'data-r1': 'tab-branch' }, iv(' · ⑂ ' + br)) : null),
|
|
1084
|
+
),
|
|
1085
|
+
),
|
|
1086
|
+
el('button', {
|
|
1087
|
+
onClick: () => closeTab(t.id), 'data-tab-close': '', 'aria-label': 'close ' + t.title, title: 'close',
|
|
1088
|
+
style: 'flex:none;color:var(--muted-foreground);padding:0 4px', hover: 'color:var(--foreground)',
|
|
1089
|
+
}, '×'),
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
940
1093
|
function tabNode(t, act, splitTab) {
|
|
941
1094
|
const selected = (act && act.id === t.id) || (splitTab && splitTab.id === t.id);
|
|
942
1095
|
const bg = selected ? FX.TAB_COLORS.activeBg : 'transparent';
|
|
@@ -949,7 +1102,7 @@ function tabNode(t, act, splitTab) {
|
|
|
949
1102
|
* fixture render keeps the design's literal — the comparator reads this text node. */
|
|
950
1103
|
const meta = t.kind === 'term'
|
|
951
1104
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
952
|
-
: (t.kind === 'task' ? 'task' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1105
|
+
: (t.kind === 'task' ? 'task' : t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
953
1106
|
|
|
954
1107
|
return el('div', {
|
|
955
1108
|
style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
|
|
@@ -985,6 +1138,8 @@ function tabNode(t, act, splitTab) {
|
|
|
985
1138
|
* prototype's, in the prototype's order: tab bar · (empty state) · panes · (scratchpad) · bottom
|
|
986
1139
|
* bar. The panes row renders even when nothing is open, exactly as the design does.
|
|
987
1140
|
*/
|
|
1141
|
+
let lastFrontId = null;
|
|
1142
|
+
|
|
988
1143
|
export function renderTabs(host) {
|
|
989
1144
|
// Which pad the drawer is on is decided from the active tab, before anything is painted from it.
|
|
990
1145
|
syncPad();
|
|
@@ -997,14 +1152,74 @@ export function renderTabs(host) {
|
|
|
997
1152
|
// ── tab bar ──
|
|
998
1153
|
const canSplit = visTabs.length >= 2 || !!state.splitId;
|
|
999
1154
|
splitLabel.textContent = state.splitId ? '⫽ unsplit' : '⫽ split';
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1155
|
+
const ck = cockpitLook();
|
|
1156
|
+
newSessionBtn.textContent = ck ? '+ session' : '+ session';
|
|
1157
|
+
emptyState.textContent = "nothing open — click a task's output or an artifact, or " + (ck ? '+' : '+') + ' session above';
|
|
1158
|
+
/* The tabs are drawn anew on every paint, so the bar is empty for a moment and its scroll would snap back to the start: a
|
|
1159
|
+
* crowded strip (or a long list) scrolled by hand jumped back on the next repaint. The offset is kept across the redraw. */
|
|
1160
|
+
const keepX = tabBar.scrollLeft, keepY = tabBar.scrollTop;
|
|
1161
|
+
const vertical = ck && !!state.tabsVertical;
|
|
1162
|
+
host.style.flexDirection = vertical ? 'row' : 'column';
|
|
1163
|
+
orientLabel.textContent = vertical ? '⇆ tabs on top' : '⇅ tabs at side';
|
|
1164
|
+
orientBtn.title = vertical ? 'show the tabs as a strip above the panes' : 'show the tabs as a list at the left of the panes';
|
|
1165
|
+
if (vertical) {
|
|
1166
|
+
tabBar.setAttribute('style', CK_VBAR);
|
|
1167
|
+
const V_BTN = CK_BTN.replace('margin:0 0 4px', 'margin:0') + ';justify-content:start';
|
|
1168
|
+
newSessionBtn.setAttribute('style', V_BTN + ';margin-bottom:6px');
|
|
1169
|
+
for (const b of [newWebBtn, newSessionOptsBtn, splitBtn, orientBtn]) b.setAttribute('style', V_BTN);
|
|
1170
|
+
setChildren(tabBar, [
|
|
1171
|
+
newSessionBtn,
|
|
1172
|
+
...visTabs.map((t) => cockpitTabNode(t, act, splitTab, true)),
|
|
1173
|
+
el('div', { 'data-tabs': 'vcontrols', style: 'margin-top:auto;padding-top:8px;display:flex;flex-direction:column;gap:4px' },
|
|
1174
|
+
FIXTURE ? null : newWebBtn,
|
|
1175
|
+
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1176
|
+
newSessionOptsBtn,
|
|
1177
|
+
canSplit ? splitBtn : null,
|
|
1178
|
+
orientBtn),
|
|
1179
|
+
]);
|
|
1180
|
+
} else if (ck) {
|
|
1181
|
+
tabBar.setAttribute('style', CK_BAR + ';scrollbar-width:thin');
|
|
1182
|
+
newSessionBtn.setAttribute('style', CK_BTN + ';margin-left:0');
|
|
1183
|
+
for (const b of [newWebBtn, newSessionOptsBtn, splitBtn, orientBtn]) b.setAttribute('style', CK_BTN);
|
|
1184
|
+
const extras = [
|
|
1185
|
+
FIXTURE ? null : newWebBtn,
|
|
1186
|
+
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1187
|
+
newSessionOptsBtn,
|
|
1188
|
+
canSplit ? splitBtn : null,
|
|
1189
|
+
].filter(Boolean);
|
|
1190
|
+
extras[0].style.marginLeft = 'auto';
|
|
1191
|
+
// The switch sits after `+ session`, so the strip still ends the mockup's way in the fixture, which does not draw it.
|
|
1192
|
+
setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, newSessionBtn, FIXTURE ? null : orientBtn]);
|
|
1193
|
+
} else {
|
|
1194
|
+
tabBar.setAttribute('style', BAR_STYLE);
|
|
1195
|
+
newSessionBtn.setAttribute('style', CHIP_STYLE);
|
|
1196
|
+
newWebBtn.setAttribute('style', CHIP_STYLE); newSessionOptsBtn.setAttribute('style', CHIP_STYLE); splitBtn.setAttribute('style', SPLIT_STYLE);
|
|
1197
|
+
setChildren(tabBar, [
|
|
1198
|
+
...visTabs.map((t) => tabNode(t, act, splitTab)),
|
|
1199
|
+
newSessionBtn,
|
|
1200
|
+
FIXTURE ? null : newWebBtn,
|
|
1201
|
+
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1202
|
+
newSessionOptsBtn,
|
|
1203
|
+
canSplit ? splitBtn : null,
|
|
1204
|
+
]);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (tabBar.scrollLeft !== keepX) tabBar.scrollLeft = keepX;
|
|
1208
|
+
if (tabBar.scrollTop !== keepY) tabBar.scrollTop = keepY;
|
|
1209
|
+
// When the tab in front changes, the bar scrolls just enough to show it, so a tab opened at the far end of a crowded strip or a
|
|
1210
|
+
// long list is seen. Only the bar's own offset is moved (not scrollIntoView, which would also scroll the panels around it).
|
|
1211
|
+
if (ck && act && act.id !== lastFrontId) {
|
|
1212
|
+
const n = [...tabBar.querySelectorAll('[data-tab]')].find((x) => x.getAttribute('data-tab') === act.id);
|
|
1213
|
+
if (n) {
|
|
1214
|
+
const b = tabBar.getBoundingClientRect(), r = n.getBoundingClientRect();
|
|
1215
|
+
if (vertical) {
|
|
1216
|
+
if (r.top < b.top) tabBar.scrollTop -= b.top - r.top;
|
|
1217
|
+
else if (r.bottom > b.bottom) tabBar.scrollTop += r.bottom - b.bottom;
|
|
1218
|
+
} else if (r.left < b.left) tabBar.scrollLeft -= b.left - r.left;
|
|
1219
|
+
else if (r.right > b.right) tabBar.scrollLeft += r.right - b.right;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
lastFrontId = act ? act.id : null;
|
|
1008
1223
|
|
|
1009
1224
|
// ── panes ──
|
|
1010
1225
|
reapClosedPanes();
|
|
@@ -1031,18 +1246,23 @@ export function renderTabs(host) {
|
|
|
1031
1246
|
scratchCopyLabel.textContent = state.scratchCopied ? 'copied ✓' : '⧉ copy';
|
|
1032
1247
|
const canSend = !!(act && act.kind === 'term');
|
|
1033
1248
|
scratchSendLabel.textContent = canSend ? '⇥ send to terminal' : 'open a terminal to send';
|
|
1034
|
-
scratchSendBtn.setAttribute('style',
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1249
|
+
scratchSendBtn.setAttribute('style', canSend ? PAD_BTN_PRIMARY : PAD_BTN_OFF);
|
|
1250
|
+
scratchClearBtn.disabled = !scratchText;
|
|
1251
|
+
scratchClearBtn.style.opacity = scratchText ? '' : '0.5';
|
|
1252
|
+
/* The header names whose pad this is, and the note says what keeps it and what Send does. Both are
|
|
1253
|
+
* read from the same facts `padKey` uses, so they cannot claim a pad the drawer is not showing. */
|
|
1254
|
+
scratchHeadLabel.textContent = FIXTURE ? 'scratchpad'
|
|
1255
|
+
: (canSend ? 'scratchpad · this session' : 'scratchpad · this repository');
|
|
1256
|
+
scratchNote.textContent = FIXTURE ? 'Kept until the page closes.'
|
|
1257
|
+
: (canSend ? 'Kept for this session. Send also presses Enter.' : 'Kept for this repository. Open a terminal to send.');
|
|
1038
1258
|
|
|
1039
1259
|
// ── bottom bar ──
|
|
1040
1260
|
scratchToggleLabel.textContent = state.scratchOpen ? '▼ scratchpad' : '▲ scratchpad';
|
|
1041
1261
|
colTitleLabel.textContent = act ? act.title : 'scratchpad';
|
|
1042
1262
|
copyLabel.textContent = state.copied ? 'copied ✓' : 'copy';
|
|
1043
1263
|
|
|
1044
|
-
setChildren(host, [
|
|
1045
|
-
|
|
1264
|
+
setChildren(host, [tabBar, tabsMain]);
|
|
1265
|
+
setChildren(tabsMain, [
|
|
1046
1266
|
// Above the panes and below the tab bar: the chooser is about the click that was just made in
|
|
1047
1267
|
// that bar, and putting it here leaves every open pane — and every live PTY — exactly where it
|
|
1048
1268
|
// was. Replacing the panes with it would re-parent a terminal, which is the quiet regression
|
|
@@ -14,15 +14,47 @@
|
|
|
14
14
|
/* Fixture mode is decided from the URL, not from another module: the persistence bypass below is
|
|
15
15
|
* the whole reason the comparator can trust a run. Reading it here keeps state.js loadable on its
|
|
16
16
|
* own and stops a data-layer import from deciding whether the PM's layout is written to disk. */
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/* `?fixture=proto` is the prototype's own seed data; `?fixture=cockpit` is the cockpit mockup's (planning/design/cockpit-mockup/): the
|
|
18
|
+
* same fixture paths, a different seed. Both are the offline demo and the parity script's like-for-like content. */
|
|
19
|
+
const FIXTURE_KIND = (() => {
|
|
20
|
+
try { const f = new URLSearchParams(location.search).get('fixture'); return f === 'proto' || f === 'cockpit' ? f : null; }
|
|
21
|
+
catch { return null; }
|
|
20
22
|
})();
|
|
23
|
+
export const FIXTURE = FIXTURE_KIND !== null;
|
|
24
|
+
/** `?layout=legacy` draws the three-column shell on a desktop, so the suites of the panels that shell uses keep guarding them until
|
|
25
|
+
* Run 4 moves the phone onto the cockpit. Nothing offers it. */
|
|
26
|
+
export const LEGACY_LAYOUT_FLAG = (() => { try { return new URLSearchParams(location.search).get('layout') === 'legacy'; } catch { return false; } })();
|
|
27
|
+
export const FIXTURE_COCKPIT = FIXTURE_KIND === 'cockpit';
|
|
28
|
+
|
|
29
|
+
/** What the cockpit fixture starts with beyond its seed data: the derived statuses (so the list can group), the open tabs. */
|
|
30
|
+
const COCKPIT_START = {
|
|
31
|
+
statuses: {
|
|
32
|
+
microsites: {
|
|
33
|
+
'ooh-weather-expansion': { status: 'blocked' }, 'psle-paywall-revamp': { status: 'delivered' }, 'smrt-news-feed': { status: 'building' },
|
|
34
|
+
'lightbox-ga-tagging': { status: 'accepted' }, 'login-metric-tagging': { status: 'accepted' }, 'shortlink-redirects': { status: 'accepted' },
|
|
35
|
+
},
|
|
36
|
+
// five things wait on the person, as the mockup's "Today 5" says: two here, three in pmos (the tile with the dot), none in scarlet
|
|
37
|
+
pmos: { 'cockpit-terminal-mod': { status: 'blocked' }, 'workspace-cockpit-nav': { status: 'blocked' }, 'kit-publish': { status: 'delivered' } },
|
|
38
|
+
// the mockup's third tile carries no dot: nothing in that repository is waiting on the person
|
|
39
|
+
scarlet: { 'scarlet-onboarding': { status: 'building' } },
|
|
40
|
+
},
|
|
41
|
+
product: 'microsites', sel: 'ooh-weather-expansion',
|
|
42
|
+
tabs: [
|
|
43
|
+
{ id: 'term:ooh-weather-expansion:claude · merge PR 41', kind: 'term', title: 'claude · merge PR 41', slug: 'ooh-weather-expansion', repo: 'microsites', sess: 'claude · merge PR 41', state: 'working', role: 'TERM' },
|
|
44
|
+
{ id: 'doc:planning/prd/ooh-weather-expansion-prd.md', kind: 'doc', title: '…-prd.md', slug: 'ooh-weather-expansion', path: 'planning/prd/ooh-weather-expansion-prd.md', role: 'DOC' },
|
|
45
|
+
{ id: 'view:memory:microsites', kind: 'view', view: 'memory', title: 'tool · memory', slug: 'microsites', repo: 'microsites' },
|
|
46
|
+
],
|
|
47
|
+
active: 'term:ooh-weather-expansion:claude · merge PR 41',
|
|
48
|
+
};
|
|
21
49
|
|
|
22
50
|
const STORE_KEY = 'pmos.workspace.pm.v1';
|
|
23
51
|
|
|
24
52
|
/** The initial state — the prototype's `state = {…}` block, key for key. */
|
|
25
53
|
function initial() {
|
|
54
|
+
const base = initialBase();
|
|
55
|
+
return FIXTURE_COCKPIT ? { ...base, ...JSON.parse(JSON.stringify(COCKPIT_START)) } : base;
|
|
56
|
+
}
|
|
57
|
+
function initialBase() {
|
|
26
58
|
return {
|
|
27
59
|
theme: 'dark', sel: null, q: '', mode: 'product', collapsedGroups: {}, groupBy: 'objective', activeOnly: false, legendOpen: false, settingsOpen: false,
|
|
28
60
|
product: null, intgSynced: {},
|
|
@@ -35,6 +67,13 @@ function initial() {
|
|
|
35
67
|
newType: 'init', newName: '', newSlug: '', newAnchor: null, newFlow: 'adlc-feature',
|
|
36
68
|
userGroups: {}, userKrs: {}, addedInits: {}, userInits: {}, selTask: null, collapsedPhases: {},
|
|
37
69
|
detail: { kind: 'init' },
|
|
70
|
+
// the rail's menu (workspace-cockpit-nav): open or shut, never persisted
|
|
71
|
+
menuOpen: false,
|
|
72
|
+
paletteOpen: false, // Run 2: the ⌘K palette — never persisted
|
|
73
|
+
layoutFocus: false, // Run 2: the Focus layout — never persisted (distinct from `focus`, the focus-mode data)
|
|
74
|
+
todayScope: 'repo',
|
|
75
|
+
filesOpen: false, // Run 3: the Work list shows the repository's files — never persisted
|
|
76
|
+
todayDismissed: {},
|
|
38
77
|
tabs: [], active: null, splitId: null,
|
|
39
78
|
// overhaul-r1: reader measure per tab (R5), which repositories show their lines of work (R2.1),
|
|
40
79
|
// and the last remove/rename answer, shown on the row it is about (R3.3)
|
|
@@ -91,6 +130,7 @@ function initial() {
|
|
|
91
130
|
newSessIsolate: null, newSessError: null, newSessBusy: false,
|
|
92
131
|
newSessBranches: null, preflightProfile: null, profiles: null, branchInfo: null,
|
|
93
132
|
col0: true, col1: true, col2: true, scratchOpen: false, scratchText: '', copied: false,
|
|
133
|
+
tabsVertical: false, // the cockpit's tabs as a vertical list instead of a strip — persisted, per browser
|
|
94
134
|
/* The scratchpad drawer's height in px (panel-tabs.js). 96 is the prototype's literal and stays
|
|
95
135
|
* the default; the PM can drag it, and the drag is clamped there rather than here — this store
|
|
96
136
|
* only has to refuse a value that could not have come from the clamp. */
|
|
@@ -155,7 +195,9 @@ const TAB_FIELDS = ['id', 'kind', 'title', 'slug', 'repo', 'sess', 'sessionId',
|
|
|
155
195
|
// a skill tab's skill id — a reload reopens the same skill
|
|
156
196
|
'skillId',
|
|
157
197
|
// a conversation tab's session id — a reload reopens the same transcript
|
|
158
|
-
'convId'
|
|
198
|
+
'convId',
|
|
199
|
+
// a view tab's view key (workspace-cockpit-nav) — a reload reopens the same tool
|
|
200
|
+
'view'];
|
|
159
201
|
const MAX_TABS = 40;
|
|
160
202
|
|
|
161
203
|
function packTab(t) {
|
|
@@ -193,6 +235,8 @@ const PERSIST = {
|
|
|
193
235
|
active: isSlug,
|
|
194
236
|
splitId: isSlug,
|
|
195
237
|
scratchOpen: isBool,
|
|
238
|
+
// the cockpit's tabs as a vertical list (true) or the horizontal strip (false, the default)
|
|
239
|
+
tabsVertical: isBool,
|
|
196
240
|
/* A restored height is re-validated on the way IN, not merely on the way out: a corrupted or
|
|
197
241
|
* hand-edited record must not be able to render a drawer with no textarea in it, or one taller
|
|
198
242
|
* than any window it could be restored into. The upper bound here is deliberately generous (the
|