create-pmos 0.52.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/README.md +2 -0
- package/workspace/REMOTE.md +24 -63
- package/workspace/bin/pmos.js +19 -3
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +28 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/launcher.md +51 -0
- package/workspace/guide/layout.md +102 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/proactive-engine.md +21 -5
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +75 -0
- package/workspace/lib/brief.js +20 -2
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/goals.js +165 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +210 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/launcher-cli.js +207 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +380 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +69 -0
- package/workspace/lib/proactive-cli.js +58 -2
- package/workspace/lib/proactive.js +9 -6
- package/workspace/lib/qr.js +293 -0
- package/workspace/lib/remote-access.js +93 -1
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +578 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/icons/apple-touch-icon.png +0 -0
- package/workspace/public/icons/icon-192.png +0 -0
- package/workspace/public/icons/icon-512.png +0 -0
- package/workspace/public/index.html +40 -19
- package/workspace/public/pm/app.js +330 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3135 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +182 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +175 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +99 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +200 -111
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +126 -0
- package/workspace/public/pm/today-goals.js +172 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2877 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +446 -40
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/morning-brief.skill +15 -9
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +21 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-lists/workspace-launcher.json +5 -0
- package/workspace/test-lists/workspace-phone-app.json +5 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* ── What this module owns ───────────────────────────────────────────────────────────────────
|
|
10
10
|
* the tab bar · + session · split · the empty state · which pane hosts exist and where they sit
|
|
11
11
|
* · the scratchpad · the bottom bar (scratchpad toggle, active-tab title, copy)
|
|
12
|
-
* The BODY of a tab belongs to its own module — `mountDoc` (tab-doc.js), `
|
|
13
|
-
*
|
|
12
|
+
* The BODY of a tab belongs to its own module — `mountDoc` (tab-doc.js), `mountTerm` (tab-term.js). This file never
|
|
13
|
+
* renders a tab's contents.
|
|
14
14
|
*
|
|
15
15
|
* ── Why the pane hosts are cached ───────────────────────────────────────────────────────────
|
|
16
16
|
* A term pane hosts a live xterm attached to a real PTY. Rebuilding that element on an unrelated
|
|
@@ -32,10 +32,13 @@ import { isMobile } from './mobile.js';
|
|
|
32
32
|
import * as FX from './fixture.js';
|
|
33
33
|
import { openTab, openTerm as openTermTab, openDoc as openDocTab } from './panel-initiatives.js';
|
|
34
34
|
import { mountDoc, disposeDoc, docText } from './tab-doc.js';
|
|
35
|
-
import { mountTask } from './tab-task.js';
|
|
36
35
|
import { mountWeb, promptWeb } from './tab-web.js';
|
|
37
36
|
import { newSessionBlock, openNewSession } from './panel-newsess.js';
|
|
38
37
|
import { boardOf, currentProduct } from './board-read.js';
|
|
38
|
+
import { field, button, toast, ensureComponentsCss } from './primitives.js';
|
|
39
|
+
import { COPY as PAL } from './palette-copy.js';
|
|
40
|
+
import { askChoice } from './dialogs.js';
|
|
41
|
+
import { icon, ensureSprite } from './icons.js';
|
|
39
42
|
|
|
40
43
|
/* tab-term.js is loaded LAZILY and on purpose: it is another module's file, a static import of a
|
|
41
44
|
* file that is not on disk yet takes the whole panel down with it, and an empty terminal pane is a
|
|
@@ -49,8 +52,20 @@ import('./modes-live.js').then((m) => { liveMod = m; if ((state.tabs || []).some
|
|
|
49
52
|
import('./tab-term.js')
|
|
50
53
|
.then((m) => { termMod = m; setState({}); })
|
|
51
54
|
.catch(() => { termMod = null; });
|
|
55
|
+
/* pm-os-absorb Run 1 — panel-breakdown.js is loaded LAZILY for the same reason, and for one more: it is a 14-module graph
|
|
56
|
+
* (primitives, icons, bd-*, item-sheet, handoff-box, keep-card, undo-hold), and a static import makes THIS module's
|
|
57
|
+
* evaluation wait for all of it. Those fetches queue behind the server's synchronous boot reads (/api/initiative,
|
|
58
|
+
* /api/library), so panel 4 — and with it tab-term.js and the terminal socket — came up after loadRepos instead of
|
|
59
|
+
* beside it, and a reload's socket opened ~1.5 s late. A Breakdown tab restored on reload repaints when it lands. */
|
|
60
|
+
let breakdownMod = null;
|
|
61
|
+
import('./panel-breakdown.js')
|
|
62
|
+
.then((m) => { breakdownMod = m; if ((state.tabs || []).some((t) => t.kind === 'breakdown')) setState({}); })
|
|
63
|
+
.catch(() => { breakdownMod = null; });
|
|
52
64
|
|
|
53
65
|
/** `{{ expr }}` — one interpolated value in its own span, the way the prototype's runtime emits it. */
|
|
66
|
+
/* A dock control's face: a lucide icon from the vendored sprite, then its word (C6: no Unicode glyph controls). */
|
|
67
|
+
const iconLabel = (name, text) => (text ? [icon(name), el('span', null, text)] : [icon(name)]);
|
|
68
|
+
const closeIcon = () => { ensureSprite(); return icon('x'); };
|
|
54
69
|
const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
|
|
55
70
|
|
|
56
71
|
/* ── the board, read exactly the way panels 2 and 3 read it (board-read.js) ─────────────────── */
|
|
@@ -116,25 +131,6 @@ export function openTerm(slug, sess) {
|
|
|
116
131
|
return openTermTab(slug, sess);
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
/**
|
|
120
|
-
* Open a task tab. Accepts the finished tab record panel 3 builds (`openTask(tab)`), or a task
|
|
121
|
-
* plus its initiative (`openTask(task, 'workspace-pm-spine')`), in which case the tab id is the
|
|
122
|
-
* prototype's `task:<slug>:<phase>#<index>` (line 1750).
|
|
123
|
-
*/
|
|
124
|
-
export function openTask(task, initiative) {
|
|
125
|
-
if (!task) return;
|
|
126
|
-
if (initiative === undefined && task.id) return openTab(task);
|
|
127
|
-
const slug = typeof initiative === 'string' ? initiative
|
|
128
|
-
: (initiative && (initiative.slug || initiative.id)) || task.slug;
|
|
129
|
-
const idx = task.index != null ? task.index : 0;
|
|
130
|
-
return openTab({
|
|
131
|
-
id: task.id || FX.taskTabId(slug, task.phase, idx),
|
|
132
|
-
kind: 'task', role: 'TASK', title: task.title, slug, repo: slugProductOf()[slug],
|
|
133
|
-
phase: task.phase, status: task.status, instruction: task.instruction, input: task.input,
|
|
134
|
-
expect: task.expect, evalNote: task.evalNote, output: task.output,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
134
|
/** Close one tab (lines 1971–1974): the last remaining tab takes over, a dangling split is dropped. */
|
|
139
135
|
export function closeTab(id) {
|
|
140
136
|
setState((s) => {
|
|
@@ -330,13 +326,13 @@ export function toggleSplit() {
|
|
|
330
326
|
|
|
331
327
|
/* ── persistent chrome ─────────────────────────────────────────────────────────────────────── */
|
|
332
328
|
|
|
333
|
-
const BAR_STYLE = 'display:flex;gap:2px;align-items:stretch;padding:6px 8px 0;border-bottom:1px solid
|
|
334
|
-
const CHIP_STYLE = 'flex:none;align-self:center;font
|
|
335
|
-
const SPLIT_STYLE = 'flex:none;margin-left:auto;align-self:center;font
|
|
336
|
-
const CHIP_HOVER = 'border-color:
|
|
329
|
+
const BAR_STYLE = 'display:flex;gap:2px;align-items:stretch;padding:6px 8px 0;border-bottom:1px solid var(--border);min-height:48px;min-width:0;overflow-x:auto';
|
|
330
|
+
const CHIP_STYLE = 'flex:none;align-self:center;font:11px var(--mono);color:var(--muted-foreground);padding:5px 11px;min-height:30px;background:var(--secondary);border:1px solid var(--border);white-space:nowrap;margin-left:4px';
|
|
331
|
+
const SPLIT_STYLE = 'flex:none;margin-left:auto;align-self:center;font:11px var(--mono);color:var(--muted-foreground);padding:5px 12px;min-height:30px;background:var(--secondary);border:1px solid var(--border);white-space:nowrap';
|
|
332
|
+
const CHIP_HOVER = 'border-color:var(--ring)';
|
|
337
333
|
|
|
338
|
-
/*
|
|
339
|
-
*
|
|
334
|
+
/* The strip and the bottom bar are chrome: they follow the theme (PRD R27). Only the terminal itself stays dark (D41): its pane
|
|
335
|
+
* paints --term-bg. `data-term-chrome` stays as a hook for the suites. */
|
|
340
336
|
const tabBar = el('div', { 'data-tabs': 'bar', 'data-term-chrome': '', style: BAR_STYLE, onWheel: stripWheel });
|
|
341
337
|
|
|
342
338
|
/* ── the cockpit's tab strip (workspace-cockpit-nav Run 4, planning/design/cockpit-mockup/WsA.dc.html) ─────────────────────────
|
|
@@ -358,9 +354,10 @@ const CK_BAR = 'flex:none;display:flex;align-items:flex-end;gap:4px;padding:0 10
|
|
|
358
354
|
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';
|
|
359
355
|
const tabsMain = el('div', { 'data-tabs': 'main', style: 'flex:1;min-height:0;min-width:0;display:flex;flex-direction:column' });
|
|
360
356
|
const orientLabel = iv('');
|
|
357
|
+
const orientIcon = el('span', null);
|
|
361
358
|
const orientBtn = el('button', {
|
|
362
359
|
onClick: () => setState({ tabsVertical: !state.tabsVertical }), 'data-tabs': 'orient',
|
|
363
|
-
}, orientLabel);
|
|
360
|
+
}, orientIcon, orientLabel);
|
|
364
361
|
/* 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
|
|
365
362
|
* did nothing there left only the thin scrollbar. */
|
|
366
363
|
function stripWheel(e) {
|
|
@@ -375,13 +372,14 @@ const CK_BTN = 'flex:none;margin:0 0 4px;height:30px;display:grid;place-items:ce
|
|
|
375
372
|
|
|
376
373
|
const newSessionBtn = el('button', {
|
|
377
374
|
onClick: () => newSession(), style: CHIP_STYLE, hover: CHIP_HOVER,
|
|
378
|
-
|
|
375
|
+
'aria-label': 'new session',
|
|
376
|
+
}, ...iconLabel('plus', 'session'));
|
|
379
377
|
|
|
380
378
|
/* overhaul-r5 (D69) — open a web page in a tab, by typing its address. Live mode only. */
|
|
381
379
|
const newWebBtn = el('button', {
|
|
382
380
|
onClick: () => promptWeb(), style: CHIP_STYLE, hover: CHIP_HOVER,
|
|
383
|
-
title: 'open a web page in a tab (http/https; it loads only when you open it)', 'data-r5': 'new-web',
|
|
384
|
-
}, '
|
|
381
|
+
title: 'open a web page in a tab (http/https; it loads only when you open it)', 'data-r5': 'new-web', 'aria-label': 'open a web page',
|
|
382
|
+
}, ...iconLabel('globe', 'web'));
|
|
385
383
|
|
|
386
384
|
/* `⋯ options` — the demoted composer, restored (workspace-session-preflight).
|
|
387
385
|
*
|
|
@@ -395,17 +393,17 @@ const newWebBtn = el('button', {
|
|
|
395
393
|
const newSessionOptsBtn = el('button', {
|
|
396
394
|
onClick: () => openNewSession(selSlug()),
|
|
397
395
|
style: CHIP_STYLE, hover: CHIP_HOVER,
|
|
398
|
-
title: 'choose the branch, the agent and the profile before the session starts',
|
|
399
|
-
}, '
|
|
396
|
+
title: 'choose the branch, the agent and the profile before the session starts', 'aria-label': 'session options',
|
|
397
|
+
}, ...iconLabel('ellipsis', 'options'));
|
|
400
398
|
|
|
401
|
-
const splitLabel = iv('
|
|
399
|
+
const splitLabel = iv('split');
|
|
402
400
|
const splitBtn = el('button', {
|
|
403
401
|
onClick: () => toggleSplit(), style: SPLIT_STYLE, hover: CHIP_HOVER,
|
|
404
|
-
}, splitLabel);
|
|
402
|
+
}, icon('columns-2'), splitLabel);
|
|
405
403
|
|
|
406
404
|
const emptyState = el('div', {
|
|
407
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',
|
|
408
|
-
}, "
|
|
406
|
+
}, "Nothing is open. Open an artifact, or press + session to start a session.");
|
|
409
407
|
|
|
410
408
|
const panesRow = el('div', { 'data-tabs': 'panes', style: 'flex:1;display:flex;min-height:0;min-width:0;overflow:hidden' });
|
|
411
409
|
|
|
@@ -422,7 +420,7 @@ const panesRow = el('div', { 'data-tabs': 'panes', style: 'flex:1;display:flex;m
|
|
|
422
420
|
* The store is `state.scratchPads`, keyed by:
|
|
423
421
|
* `sess:<server session id>` — a terminal tab bound to a live PTY. The id is the SERVER's, so two
|
|
424
422
|
* sessions both called `agent 1` in two repositories are two pads.
|
|
425
|
-
* `ws:<workspace id>` — everything else (a doc,
|
|
423
|
+
* `ws:<workspace id>` — everything else (a doc, nothing open): the repository pad.
|
|
426
424
|
* That is exactly the key space `/api/scratchpad` already stores under — the endpoint has been
|
|
427
425
|
* per-session all along, and it is the client half that went missing. `state.scratchText` remains
|
|
428
426
|
* the text ON SCREEN, so the copy button, `⇥ send to terminal` and the fixture render are unchanged.
|
|
@@ -529,6 +527,7 @@ const scratchGrip = el('div', {
|
|
|
529
527
|
/* ── pad identity, loading and autosave ─────────────────────────────────────────────────────── */
|
|
530
528
|
|
|
531
529
|
const padLoading = new Set(); // keys with a read in flight — one read per key, ever
|
|
530
|
+
const padTyped = new Set(); // pads typed into since this page loaded: a late server read never overwrites them
|
|
532
531
|
let padTimer = null;
|
|
533
532
|
let padPending = null; // {key, text} not yet written
|
|
534
533
|
|
|
@@ -571,7 +570,7 @@ function onScratchInput(text) {
|
|
|
571
570
|
scratchText: text,
|
|
572
571
|
scratchPads: key ? { ...s.scratchPads, [key]: text } : s.scratchPads,
|
|
573
572
|
}));
|
|
574
|
-
if (key) schedulePadSave(key, text);
|
|
573
|
+
if (key) { padTyped.add(key); schedulePadSave(key, text); }
|
|
575
574
|
}
|
|
576
575
|
|
|
577
576
|
/**
|
|
@@ -592,6 +591,8 @@ function syncPad() {
|
|
|
592
591
|
padLoading.add(key);
|
|
593
592
|
const landed = (text) => {
|
|
594
593
|
padLoading.delete(key);
|
|
594
|
+
// text typed while the read was in flight is newer than what the read returns: it stays (and is the save in flight)
|
|
595
|
+
if (padTyped.has(key)) return;
|
|
595
596
|
setState((s) => ({
|
|
596
597
|
scratchPads: { ...s.scratchPads, [key]: text },
|
|
597
598
|
// …and onto the screen only if this is still the pad on screen. A PM who switched tabs while
|
|
@@ -671,31 +672,96 @@ const scratchBlock = el('div', {
|
|
|
671
672
|
scratchSendBtn, scratchCopyBtn, scratchClearBtn, scratchNote),
|
|
672
673
|
);
|
|
673
674
|
|
|
675
|
+
/**
|
|
676
|
+
* Paint the scratchpad drawer and the bottom bar's scratchpad toggle for the active tab `act`.
|
|
677
|
+
* renderTabs calls it on one line, so the scratchpad (3C) and the tab strip (3A) are separate hunks.
|
|
678
|
+
*/
|
|
679
|
+
function renderScratch(act) {
|
|
680
|
+
/* `defaultValue` IS the textarea's child text node — the rendered text lives in the DOM, not only
|
|
681
|
+
* in a property, which is how the prototype's runtime leaves it. Writing it costs a typed-in box
|
|
682
|
+
* nothing; `value` is written only when it differs, so the caret stays where the PM put it. */
|
|
683
|
+
const scratchText = state.scratchText || '';
|
|
684
|
+
if (scratchTa.defaultValue !== scratchText) scratchTa.defaultValue = scratchText;
|
|
685
|
+
if (scratchTa.value !== scratchText) scratchTa.value = scratchText;
|
|
686
|
+
/* The drawer's height, real mode only. The style ATTRIBUTE keeps the prototype's string; only the
|
|
687
|
+
* resolved `height` property is written, so the fixture render — which never reaches here — stays
|
|
688
|
+
* byte-identical to the design. Written on every paint rather than only on resize, because the
|
|
689
|
+
* restored value has to land on the first one. */
|
|
690
|
+
if (!FIXTURE) {
|
|
691
|
+
const h = clampScratchH(state.scratchH || SCRATCH_DEFAULT) + 'px';
|
|
692
|
+
if (scratchTa.style.height !== h) scratchTa.style.height = h;
|
|
693
|
+
}
|
|
694
|
+
scratchCopyLabel.textContent = state.scratchCopied ? 'copied ✓' : '⧉ copy';
|
|
695
|
+
const canSend = !!(act && act.kind === 'term');
|
|
696
|
+
scratchSendLabel.textContent = canSend ? '⇥ send to terminal' : 'open a terminal to send';
|
|
697
|
+
scratchSendBtn.setAttribute('style', canSend ? PAD_BTN_PRIMARY : PAD_BTN_OFF);
|
|
698
|
+
scratchClearBtn.disabled = !scratchText;
|
|
699
|
+
scratchClearBtn.style.opacity = scratchText ? '' : '0.5';
|
|
700
|
+
/* The header names whose pad this is, and the note says what keeps it and what Send does. Both are
|
|
701
|
+
* read from the same facts `padKey` uses, so they cannot claim a pad the drawer is not showing. */
|
|
702
|
+
scratchHeadLabel.textContent = FIXTURE ? 'scratchpad'
|
|
703
|
+
: (canSend ? 'scratchpad · this session' : 'scratchpad · this repository');
|
|
704
|
+
scratchNote.textContent = FIXTURE ? 'Kept until the page closes.'
|
|
705
|
+
: (canSend ? 'Kept for this session. Send also presses Enter.' : 'Kept for this repository. Open a terminal to send.');
|
|
706
|
+
scratchToggleLabel.textContent = state.scratchOpen ? '▼ scratchpad' : '▲ scratchpad';
|
|
707
|
+
if (!FIXTURE) paintBottom(act);
|
|
708
|
+
}
|
|
709
|
+
|
|
674
710
|
/* ── the bottom bar (lines 992–996) ─────────────────────────────────────────────────────────── */
|
|
675
711
|
|
|
676
712
|
const scratchToggleLabel = iv('▲ scratchpad');
|
|
677
713
|
const colTitleLabel = iv('scratchpad');
|
|
678
714
|
const copyLabel = iv('copy');
|
|
679
715
|
|
|
680
|
-
const bottomBar = el('div', {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
},
|
|
684
|
-
el('button', {
|
|
685
|
-
onClick: () => setState((s) => ({ scratchOpen: !s.scratchOpen })),
|
|
686
|
-
style: 'flex:none;color:oklch(0.72 0.014 214);font-size:11px;padding:3px 6px;white-space:nowrap',
|
|
687
|
-
hover: 'color:oklch(0.987 0.002 197.1)',
|
|
688
|
-
}, scratchToggleLabel),
|
|
689
|
-
el('span', {
|
|
690
|
-
style: 'font-family:var(--mono);color:#c4ccce;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
|
|
691
|
-
}, colTitleLabel),
|
|
692
|
-
el('button', {
|
|
693
|
-
onClick: () => copyActive(),
|
|
694
|
-
style: 'flex:none;margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--brand);padding:3px 10px;min-height:26px;background:oklch(1 0 0 / 6%);border:1px solid oklch(1 0 0 / 12%);white-space:nowrap',
|
|
695
|
-
hover: CHIP_HOVER,
|
|
696
|
-
}, copyLabel),
|
|
716
|
+
const bottomBar = el('div', { 'data-tabs': 'bottom', 'data-term-chrome': '', class: 'lg-dock-bottom' },
|
|
717
|
+
button({ label: scratchToggleLabel, variant: 'ghost', size: 'sm', onClick: () => setState((s) => ({ scratchOpen: !s.scratchOpen })) }),
|
|
718
|
+
el('span', { class: 'lg-dock-bottom__title' }, colTitleLabel),
|
|
719
|
+
button({ label: copyLabel, variant: 'outline', size: 'sm', onClick: () => copyActive() }),
|
|
697
720
|
);
|
|
698
721
|
|
|
722
|
+
/* pm-os-absorb Run 3 (R11) took the scratchpad toggle out of the bar, and with it the only way to the pad on a desktop.
|
|
723
|
+
* 0.53.1 puts it back: paintBottom lays the bar out as the scratchpad toggle, the session's name and `copy`, or, on a
|
|
724
|
+
* phone, the terminal's compose line (the same per-session pad). Fixture mode keeps the prototype's bar. */
|
|
725
|
+
const [bottomToggle, bottomTitle, bottomCopy] = bottomBar.children;
|
|
726
|
+
bottomToggle.setAttribute('data-scratch', 'toggle');
|
|
727
|
+
|
|
728
|
+
/* The compose line: one message field and Send on the SAME per-session pad as the drawer (padKey), so the text is kept
|
|
729
|
+
* on this machine and never crosses sessions. On a desktop ⌘K "Ask <session>" types to a session (pm/palette.js). */
|
|
730
|
+
const composeInput = field({ label: PAL.compose.label, kind: 'textarea', hideLabel: true, placeholder: PAL.compose.placeholder,
|
|
731
|
+
attrs: { class: 'pl-compose__input', 'data-compose': 'input', rows: 1, enterkeyhint: 'send' } });
|
|
732
|
+
const composeSend = button({ label: PAL.compose.send, variant: 'primary', onClick: () => sendCompose(),
|
|
733
|
+
attrs: { 'data-compose': 'send', 'aria-label': PAL.compose.sendLabel } });
|
|
734
|
+
const composeLine = el('div', { class: 'pl-compose', 'data-compose': 'line' }, el('div', { class: 'pl-compose__field' }, composeInput), composeSend);
|
|
735
|
+
// Send follows the field as it is typed, not only on the next render of the bar
|
|
736
|
+
composeInput.control.addEventListener('input', () => { composeSend.disabled = !composeInput.control.value.trim(); onScratchInput(composeInput.control.value); });
|
|
737
|
+
/* The keyboard's Send key is Enter (enterkeyhint 'send'), so it sends. Shift+Enter, and Enter while a composition is open, stay a newline. */
|
|
738
|
+
composeInput.control.addEventListener('keydown', (e) => {
|
|
739
|
+
if (e.key !== 'Enter' || e.shiftKey || e.isComposing) return;
|
|
740
|
+
e.preventDefault();
|
|
741
|
+
sendCompose();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
/** Send the line to the session and clear it, only when the session took the text: otherwise the message stays and the PM is told. */
|
|
745
|
+
function sendCompose() {
|
|
746
|
+
const act = activeTab();
|
|
747
|
+
if (!act || act.kind !== 'term' || !(state.scratchText || '').trim()) return;
|
|
748
|
+
if (!sendScratchToTerminal({ strict: true })) { toast({ text: PAL.compose.failed(String(act.title || act.id)), tone: 'error' }); return; }
|
|
749
|
+
onScratchInput('');
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** The bar in real mode: the scratchpad toggle, the title and copy; a phone's terminal gets the compose line instead. */
|
|
753
|
+
function paintBottom(act) {
|
|
754
|
+
const compose = isMobile() && !!act && act.kind === 'term';
|
|
755
|
+
bottomTitle.textContent = act ? act.title : '';
|
|
756
|
+
if (compose) {
|
|
757
|
+
ensureComponentsCss();
|
|
758
|
+
const text = state.scratchText || '';
|
|
759
|
+
if (composeInput.control.value !== text) composeInput.control.value = text;
|
|
760
|
+
composeSend.disabled = !text.trim();
|
|
761
|
+
}
|
|
762
|
+
setChildren(bottomBar, compose ? [composeLine] : (act ? [bottomToggle, bottomTitle, bottomCopy] : [bottomToggle, bottomTitle]));
|
|
763
|
+
}
|
|
764
|
+
|
|
699
765
|
/* ── clipboard + scratchpad actions (lines 2098–2118) ───────────────────────────────────────── */
|
|
700
766
|
|
|
701
767
|
function copyToClipboard(txt) {
|
|
@@ -707,19 +773,23 @@ function activeTab() {
|
|
|
707
773
|
return visibleTabs().find((t) => t.id === state.active) || null;
|
|
708
774
|
}
|
|
709
775
|
|
|
710
|
-
/** `⇥ send to terminal` (line 2104): one echoed prompt line per non-empty scratchpad line.
|
|
711
|
-
|
|
776
|
+
/** `⇥ send to terminal` (line 2104): one echoed prompt line per non-empty scratchpad line. Answers whether a real session took the text.
|
|
777
|
+
* `strict` (the compose line) sends first and echoes nothing when the session did not take it. */
|
|
778
|
+
function sendScratchToTerminal({ strict = false } = {}) {
|
|
712
779
|
const act = activeTab();
|
|
713
|
-
if (!act || act.kind !== 'term') return;
|
|
780
|
+
if (!act || act.kind !== 'term') return false;
|
|
714
781
|
const v = (state.scratchText || '').trim();
|
|
715
|
-
if (!v) return;
|
|
782
|
+
if (!v) return false;
|
|
716
783
|
const lines = FX.scratchLines(v);
|
|
717
|
-
setState((s) => ({ termHist: { ...s.termHist, [act.id]: [...(s.termHist[act.id] || []), ...lines] } }));
|
|
718
784
|
/* A real PTY is not a scrollback list: when a terminal module is present it gets the same text
|
|
719
785
|
* as keystrokes, so the paste lands in the shell rather than only in the transcript. */
|
|
786
|
+
let sent = false;
|
|
720
787
|
if (!FIXTURE && termMod && typeof termMod.sendToTerm === 'function') {
|
|
721
|
-
try { termMod.sendToTerm(act, v + '\n'); } catch { /* the transcript still shows it */ }
|
|
788
|
+
try { sent = termMod.sendToTerm(act, v + '\n') === true; } catch { /* the transcript still shows it */ }
|
|
722
789
|
}
|
|
790
|
+
if (strict && !sent) return false;
|
|
791
|
+
setState((s) => ({ termHist: { ...s.termHist, [act.id]: [...(s.termHist[act.id] || []), ...lines] } }));
|
|
792
|
+
return sent;
|
|
723
793
|
}
|
|
724
794
|
|
|
725
795
|
/** `copy` (line 2111): the doc's text, or the attach command, or the scratchpad. */
|
|
@@ -809,7 +879,7 @@ function peerRow(prompt, p) {
|
|
|
809
879
|
(p.agent === 'claude' && p.shown !== 'exited') ? el('button', {
|
|
810
880
|
'data-r12': 'switch-model', title: 'types /model into this pane; effort is not changed',
|
|
811
881
|
onClick: async () => {
|
|
812
|
-
const t =
|
|
882
|
+
const t = await askChoice({ title: 'Switch “' + p.name + '” model', label: 'Tier', value: 'balanced', confirmLabel: 'Switch', options: [{ value: 'fast', label: 'fast' }, { value: 'balanced', label: 'balanced' }, { value: 'deep', label: 'deep' }] });
|
|
813
883
|
if (!t || !t.trim()) return;
|
|
814
884
|
const out = await fetch('/api/session/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: p.id, tier: t.trim() }) })
|
|
815
885
|
.then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
@@ -883,7 +953,7 @@ function startPromptBlock() {
|
|
|
883
953
|
el('button', {
|
|
884
954
|
onClick: () => newSession(prompt.slug, { force: true }),
|
|
885
955
|
style: CH_BTN, hover: CHIP_HOVER,
|
|
886
|
-
}, '
|
|
956
|
+
}, ...iconLabel('plus', 'start another session here anyway')),
|
|
887
957
|
el('button', { onClick: () => dismissStartPrompt(), style: CH_BTN, hover: CHIP_HOVER }, 'cancel'),
|
|
888
958
|
),
|
|
889
959
|
// The limit, where the PM meets the feature — never only in a comment. PMOS hosts the terminal;
|
|
@@ -900,16 +970,21 @@ function startPromptBlock() {
|
|
|
900
970
|
* `flex:1;overflow-y:auto` — and a flex item's automatic minimum is its content, so without this
|
|
901
971
|
* the pane grows to fit a long document instead of letting the document scroll inside it, and the
|
|
902
972
|
* growth escapes upward until the whole page scrolls. See the chain documented in pm/app.js. */
|
|
903
|
-
const PANE_STYLE = 'flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;border-right:1px solid
|
|
973
|
+
const PANE_STYLE = 'flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;border-right:1px solid var(--border)';
|
|
904
974
|
const paneHosts = new Map(); // tab id -> the pane element, kept for the tab's whole life
|
|
905
975
|
|
|
906
976
|
function paneFor(tab) {
|
|
907
977
|
let host = paneHosts.get(tab.id);
|
|
908
978
|
if (!host) { host = el('div', { style: PANE_STYLE }); paneHosts.set(tab.id, host); }
|
|
979
|
+
if (tab.kind === 'dest') {
|
|
980
|
+
// pm-os-absorb Runs 2–5: a destination as a tab until Run 3's frame. The registry arrives after boot (app.js
|
|
981
|
+
// imports it lazily and renders again), so until then the host stays empty.
|
|
982
|
+
const reg = window.pmApp && window.pmApp.registry;
|
|
983
|
+
if (reg && typeof reg.mountDestTab === 'function') reg.mountDestTab(host, tab);
|
|
984
|
+
return host;
|
|
985
|
+
}
|
|
909
986
|
if (tab.kind === 'term') {
|
|
910
987
|
if (termMod && typeof termMod.mountTerm === 'function') termMod.mountTerm(host, tab);
|
|
911
|
-
} else if (tab.kind === 'task') {
|
|
912
|
-
mountTask(host, tab);
|
|
913
988
|
} else if (tab.kind === 'skill') {
|
|
914
989
|
if (liveMod && typeof liveMod.mountSkillTab === 'function') liveMod.mountSkillTab(host, tab);
|
|
915
990
|
} else if (tab.kind === 'transcript') {
|
|
@@ -918,6 +993,9 @@ function paneFor(tab) {
|
|
|
918
993
|
if (cockpitMod && typeof cockpitMod.mountView === 'function') cockpitMod.mountView(host, tab);
|
|
919
994
|
} else if (tab.kind === 'web') {
|
|
920
995
|
mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
|
|
996
|
+
} else if (tab.kind === 'breakdown') {
|
|
997
|
+
// pm-os-absorb Run 1 — an initiative's items (Tree, Board, item sheet); an empty pane until the module lands
|
|
998
|
+
if (breakdownMod && typeof breakdownMod.mountBreakdown === 'function') breakdownMod.mountBreakdown(host, tab);
|
|
921
999
|
} else {
|
|
922
1000
|
mountDoc(host, tab);
|
|
923
1001
|
}
|
|
@@ -932,6 +1010,7 @@ function reapClosedPanes() {
|
|
|
932
1010
|
const tab = { id };
|
|
933
1011
|
if (termMod && typeof termMod.disposeTerm === 'function') { try { termMod.disposeTerm(tab); } catch { /* ignore */ } }
|
|
934
1012
|
try { disposeDoc(id); } catch { /* ignore */ }
|
|
1013
|
+
try { if (window.pmApp && window.pmApp.registry) window.pmApp.registry.disposeDestTab(id); } catch { /* ignore */ }
|
|
935
1014
|
if (host.parentNode) host.parentNode.removeChild(host);
|
|
936
1015
|
paneHosts.delete(id);
|
|
937
1016
|
const slot = offSlots.get(id);
|
|
@@ -997,6 +1076,37 @@ function keepSessionsLive(shown) {
|
|
|
997
1076
|
|
|
998
1077
|
/* ── the tab bar ───────────────────────────────────────────────────────────────────────────── */
|
|
999
1078
|
|
|
1079
|
+
/* pm-os-absorb Run 3 (slice 3A): in the frame this panel is the agent dock. Its strip then carries the dock's own
|
|
1080
|
+
* "+ Session" menu (new session, session with options, and the skills to start from) in place of the one-click chip, and a
|
|
1081
|
+
* control that folds the dock to its rail. dock.js is loaded lazily, like the other siblings here, so panel 4 never waits
|
|
1082
|
+
* for it; until it lands the strip shows the chip it always had. Outside the frame (fixture mode, ?layout=legacy) nothing
|
|
1083
|
+
* here changes. */
|
|
1084
|
+
let dockMod = null;
|
|
1085
|
+
const inFrame = () => !!(window.pmApp && window.pmApp.frame && window.pmApp.frame.on);
|
|
1086
|
+
import('./dock.js').then((m) => { dockMod = m; if (inFrame()) setState({}); }).catch(() => { dockMod = null; });
|
|
1087
|
+
let dockParts = null;
|
|
1088
|
+
function frameDock() {
|
|
1089
|
+
if (!dockMod || !inFrame()) return null;
|
|
1090
|
+
const repo = currentProduct();
|
|
1091
|
+
const ws = repo && dataMod && typeof dataMod.wsOf === 'function' ? dataMod.wsOf(repo) : null;
|
|
1092
|
+
if (!dockParts || dockParts.ws !== ws) {
|
|
1093
|
+
dockParts = {
|
|
1094
|
+
ws,
|
|
1095
|
+
session: dockMod.sessionMenuButton({
|
|
1096
|
+
ws,
|
|
1097
|
+
newSession: () => newSession(),
|
|
1098
|
+
withOptions: (d) => { openNewSession(selSlug(), { agent: d.cli }); setState({ newSessTier: d.tier }); },
|
|
1099
|
+
openSkill: (sk) => openTab({ id: 'skill:' + sk.id, kind: 'skill', title: sk.label || sk.skill || sk.id, skillId: sk.id, repo: state.product || null, slug: null, role: 'SKILL' }),
|
|
1100
|
+
}),
|
|
1101
|
+
fold: dockParts ? dockParts.fold : dockMod.foldButton({ fold: () => window.pmApp.frame.foldDock() }),
|
|
1102
|
+
focus: dockParts ? dockParts.focus : dockMod.focusButton({ toggle: () => window.pmApp.frame.toggleFocus() }),
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
if (isMobile()) return { session: dockParts.session, fold: null, focus: null };
|
|
1106
|
+
dockMod.paintFocus(dockParts.focus, !!window.pmApp.frame.dockMax);
|
|
1107
|
+
return { session: dockParts.session, fold: dockParts.fold, focus: dockParts.focus };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1000
1110
|
/**
|
|
1001
1111
|
* overhaul-r1 R2.2/R2.3 — the line of work a tab is on, or null when it is the main tree (which
|
|
1002
1112
|
* shows nothing extra). A doc tab carries its tree from the search hit that opened it; a terminal
|
|
@@ -1026,7 +1136,7 @@ function cockpitTabNode(t, act, splitTab, vertical) {
|
|
|
1026
1136
|
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1027
1137
|
const meta = t.kind === 'term'
|
|
1028
1138
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1029
|
-
: (t.kind === '
|
|
1139
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1030
1140
|
const br = tabBranch(t);
|
|
1031
1141
|
return el('div', {
|
|
1032
1142
|
'data-tab': t.id, 'data-tab-active': front ? '1' : '0',
|
|
@@ -1054,7 +1164,7 @@ function cockpitTabNode(t, act, splitTab, vertical) {
|
|
|
1054
1164
|
onClick: () => closeTab(t.id), 'data-tab-close': '', 'aria-label': 'close ' + t.title, title: 'close',
|
|
1055
1165
|
style: 'color:var(--muted-foreground)' + (front ? '' : ';position:absolute;right:3px;top:50%;transform:translateY(-50%)'),
|
|
1056
1166
|
hover: 'color:var(--foreground)',
|
|
1057
|
-
},
|
|
1167
|
+
}, closeIcon()),
|
|
1058
1168
|
);
|
|
1059
1169
|
}
|
|
1060
1170
|
|
|
@@ -1063,7 +1173,7 @@ function cockpitTabRow(t, front) {
|
|
|
1063
1173
|
const dotColor = FX.TAB_COLORS.dot[t.kind] || FX.TAB_COLORS.dot.doc;
|
|
1064
1174
|
const meta = t.kind === 'term'
|
|
1065
1175
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1066
|
-
: (t.kind === '
|
|
1176
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1067
1177
|
const br = tabBranch(t);
|
|
1068
1178
|
return el('div', {
|
|
1069
1179
|
'data-tab': t.id, 'data-tab-active': front ? '1' : '0', 'data-tab-row': '',
|
|
@@ -1088,7 +1198,7 @@ function cockpitTabRow(t, front) {
|
|
|
1088
1198
|
el('button', {
|
|
1089
1199
|
onClick: () => closeTab(t.id), 'data-tab-close': '', 'aria-label': 'close ' + t.title, title: 'close',
|
|
1090
1200
|
style: 'flex:none;color:var(--muted-foreground);padding:0 4px', hover: 'color:var(--foreground)',
|
|
1091
|
-
},
|
|
1201
|
+
}, closeIcon()),
|
|
1092
1202
|
);
|
|
1093
1203
|
}
|
|
1094
1204
|
|
|
@@ -1104,7 +1214,7 @@ function tabNode(t, act, splitTab) {
|
|
|
1104
1214
|
* fixture render keeps the design's literal — the comparator reads this text node. */
|
|
1105
1215
|
const meta = t.kind === 'term'
|
|
1106
1216
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
1107
|
-
: (t.kind === '
|
|
1217
|
+
: (t.kind === 'view' ? 'view' : t.kind === 'web' ? 'web' : t.kind === 'skill' ? 'skill' : t.kind === 'transcript' ? 'conversation' : t.role);
|
|
1108
1218
|
|
|
1109
1219
|
return el('div', {
|
|
1110
1220
|
style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
|
|
@@ -1126,10 +1236,10 @@ function tabNode(t, act, splitTab) {
|
|
|
1126
1236
|
),
|
|
1127
1237
|
),
|
|
1128
1238
|
el('button', {
|
|
1129
|
-
onClick: () => closeTab(t.id),
|
|
1239
|
+
onClick: () => closeTab(t.id), 'aria-label': 'close ' + t.title, title: 'close',
|
|
1130
1240
|
style: 'color:oklch(0.6 0.02 213);font-size:12px;padding:2px 3px;align-self:flex-start;margin-top:6px',
|
|
1131
1241
|
hover: 'color:oklch(0.987 0.002 197.1)',
|
|
1132
|
-
},
|
|
1242
|
+
}, closeIcon()),
|
|
1133
1243
|
);
|
|
1134
1244
|
}
|
|
1135
1245
|
|
|
@@ -1153,16 +1263,19 @@ export function renderTabs(host) {
|
|
|
1153
1263
|
|
|
1154
1264
|
// ── tab bar ──
|
|
1155
1265
|
const canSplit = visTabs.length >= 2 || !!state.splitId;
|
|
1156
|
-
splitLabel.textContent = state.splitId ? '
|
|
1266
|
+
splitLabel.textContent = state.splitId ? 'unsplit' : 'split';
|
|
1157
1267
|
const ck = cockpitLook();
|
|
1158
|
-
|
|
1159
|
-
emptyState.textContent =
|
|
1268
|
+
ensureSprite(); // the dock controls draw lucide icons (C6); the sprite loads once
|
|
1269
|
+
emptyState.textContent = 'Nothing is open. Open an artifact, or press + session to start a session.';
|
|
1160
1270
|
/* 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
|
|
1161
1271
|
* crowded strip (or a long list) scrolled by hand jumped back on the next repaint. The offset is kept across the redraw. */
|
|
1162
1272
|
const keepX = tabBar.scrollLeft, keepY = tabBar.scrollTop;
|
|
1163
1273
|
const vertical = ck && !!state.tabsVertical;
|
|
1274
|
+
const fr = frameDock(); // the dock's own controls, in the frame only
|
|
1164
1275
|
host.style.flexDirection = vertical ? 'row' : 'column';
|
|
1165
|
-
orientLabel.textContent = vertical ? '
|
|
1276
|
+
orientLabel.textContent = vertical ? 'tabs on top' : 'tabs at side';
|
|
1277
|
+
const oi = vertical ? 'arrow-left-right' : 'arrow-up-down';
|
|
1278
|
+
if (!orientIcon.firstChild || orientIcon.firstChild.getAttribute('data-icon') !== oi) orientIcon.replaceChildren(icon(oi));
|
|
1166
1279
|
orientBtn.title = vertical ? 'show the tabs as a strip above the panes' : 'show the tabs as a list at the left of the panes';
|
|
1167
1280
|
if (vertical) {
|
|
1168
1281
|
tabBar.setAttribute('style', CK_VBAR);
|
|
@@ -1170,14 +1283,16 @@ export function renderTabs(host) {
|
|
|
1170
1283
|
newSessionBtn.setAttribute('style', V_BTN + ';margin-bottom:6px');
|
|
1171
1284
|
for (const b of [newWebBtn, newSessionOptsBtn, splitBtn, orientBtn]) b.setAttribute('style', V_BTN);
|
|
1172
1285
|
setChildren(tabBar, [
|
|
1173
|
-
newSessionBtn,
|
|
1286
|
+
fr ? fr.session : newSessionBtn,
|
|
1174
1287
|
...visTabs.map((t) => cockpitTabNode(t, act, splitTab, true)),
|
|
1175
1288
|
el('div', { 'data-tabs': 'vcontrols', style: 'margin-top:auto;padding-top:8px;display:flex;flex-direction:column;gap:4px' },
|
|
1176
1289
|
FIXTURE ? null : newWebBtn,
|
|
1177
1290
|
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1178
1291
|
newSessionOptsBtn,
|
|
1179
1292
|
canSplit ? splitBtn : null,
|
|
1180
|
-
orientBtn
|
|
1293
|
+
orientBtn,
|
|
1294
|
+
fr ? fr.focus : null,
|
|
1295
|
+
fr ? fr.fold : null),
|
|
1181
1296
|
]);
|
|
1182
1297
|
} else if (ck) {
|
|
1183
1298
|
tabBar.setAttribute('style', CK_BAR + ';scrollbar-width:thin');
|
|
@@ -1191,14 +1306,14 @@ export function renderTabs(host) {
|
|
|
1191
1306
|
].filter(Boolean);
|
|
1192
1307
|
extras[0].style.marginLeft = 'auto';
|
|
1193
1308
|
// The switch sits after `+ session`, so the strip still ends the mockup's way in the fixture, which does not draw it.
|
|
1194
|
-
setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, newSessionBtn, FIXTURE ? null : orientBtn]);
|
|
1309
|
+
setChildren(tabBar, [...visTabs.map((t) => cockpitTabNode(t, act, splitTab)), ...extras, fr ? fr.session : newSessionBtn, FIXTURE ? null : orientBtn, fr ? fr.focus : null, fr ? fr.fold : null]);
|
|
1195
1310
|
} else {
|
|
1196
1311
|
tabBar.setAttribute('style', BAR_STYLE);
|
|
1197
1312
|
newSessionBtn.setAttribute('style', CHIP_STYLE);
|
|
1198
1313
|
newWebBtn.setAttribute('style', CHIP_STYLE); newSessionOptsBtn.setAttribute('style', CHIP_STYLE); splitBtn.setAttribute('style', SPLIT_STYLE);
|
|
1199
1314
|
setChildren(tabBar, [
|
|
1200
1315
|
...visTabs.map((t) => tabNode(t, act, splitTab)),
|
|
1201
|
-
newSessionBtn,
|
|
1316
|
+
fr ? fr.session : newSessionBtn,
|
|
1202
1317
|
FIXTURE ? null : newWebBtn,
|
|
1203
1318
|
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
1204
1319
|
newSessionOptsBtn,
|
|
@@ -1230,36 +1345,10 @@ export function renderTabs(host) {
|
|
|
1230
1345
|
// …and the sessions that are open but not on screen keep running, off screen (see above).
|
|
1231
1346
|
keepSessionsLive(new Set([act && act.id, splitTab && splitTab.id].filter(Boolean)));
|
|
1232
1347
|
|
|
1233
|
-
// ── scratchpad ──
|
|
1234
|
-
|
|
1235
|
-
* in a property, which is how the prototype's runtime leaves it. Writing it costs a typed-in box
|
|
1236
|
-
* nothing; `value` is written only when it differs, so the caret stays where the PM put it. */
|
|
1237
|
-
const scratchText = state.scratchText || '';
|
|
1238
|
-
if (scratchTa.defaultValue !== scratchText) scratchTa.defaultValue = scratchText;
|
|
1239
|
-
if (scratchTa.value !== scratchText) scratchTa.value = scratchText;
|
|
1240
|
-
/* The drawer's height, real mode only. The style ATTRIBUTE keeps the prototype's string; only the
|
|
1241
|
-
* resolved `height` property is written, so the fixture render — which never reaches here — stays
|
|
1242
|
-
* byte-identical to the design. Written on every paint rather than only on resize, because the
|
|
1243
|
-
* restored value has to land on the first one. */
|
|
1244
|
-
if (!FIXTURE) {
|
|
1245
|
-
const h = clampScratchH(state.scratchH || SCRATCH_DEFAULT) + 'px';
|
|
1246
|
-
if (scratchTa.style.height !== h) scratchTa.style.height = h;
|
|
1247
|
-
}
|
|
1248
|
-
scratchCopyLabel.textContent = state.scratchCopied ? 'copied ✓' : '⧉ copy';
|
|
1249
|
-
const canSend = !!(act && act.kind === 'term');
|
|
1250
|
-
scratchSendLabel.textContent = canSend ? '⇥ send to terminal' : 'open a terminal to send';
|
|
1251
|
-
scratchSendBtn.setAttribute('style', canSend ? PAD_BTN_PRIMARY : PAD_BTN_OFF);
|
|
1252
|
-
scratchClearBtn.disabled = !scratchText;
|
|
1253
|
-
scratchClearBtn.style.opacity = scratchText ? '' : '0.5';
|
|
1254
|
-
/* The header names whose pad this is, and the note says what keeps it and what Send does. Both are
|
|
1255
|
-
* read from the same facts `padKey` uses, so they cannot claim a pad the drawer is not showing. */
|
|
1256
|
-
scratchHeadLabel.textContent = FIXTURE ? 'scratchpad'
|
|
1257
|
-
: (canSend ? 'scratchpad · this session' : 'scratchpad · this repository');
|
|
1258
|
-
scratchNote.textContent = FIXTURE ? 'Kept until the page closes.'
|
|
1259
|
-
: (canSend ? 'Kept for this session. Send also presses Enter.' : 'Kept for this repository. Open a terminal to send.');
|
|
1348
|
+
// ── scratchpad (renderScratch owns the drawer and the toggle label; 3C) ──
|
|
1349
|
+
renderScratch(act);
|
|
1260
1350
|
|
|
1261
1351
|
// ── bottom bar ──
|
|
1262
|
-
scratchToggleLabel.textContent = state.scratchOpen ? '▼ scratchpad' : '▲ scratchpad';
|
|
1263
1352
|
colTitleLabel.textContent = act ? act.title : 'scratchpad';
|
|
1264
1353
|
copyLabel.textContent = state.copied ? 'copied ✓' : 'copy';
|
|
1265
1354
|
|
|
@@ -1276,7 +1365,7 @@ export function renderTabs(host) {
|
|
|
1276
1365
|
newSessionBlock(),
|
|
1277
1366
|
visTabs.length === 0 && !state.startPrompt && !state.newSess ? emptyState : null,
|
|
1278
1367
|
panesRow,
|
|
1279
|
-
state.scratchOpen ? scratchBlock : null,
|
|
1368
|
+
state.scratchOpen && (FIXTURE || !isMobile()) ? scratchBlock : null,
|
|
1280
1369
|
bottomBar,
|
|
1281
1370
|
]);
|
|
1282
1371
|
}
|
|
@@ -17,6 +17,7 @@ import { el } from './dom.js';
|
|
|
17
17
|
import { state, setState, subscribe } from './state.js';
|
|
18
18
|
import { boardOf } from './board-read.js';
|
|
19
19
|
import { openNewSession } from './panel-newsess.js';
|
|
20
|
+
import { askConfirm } from './dialogs.js';
|
|
20
21
|
|
|
21
22
|
const ip = (v) => el('span', null, v);
|
|
22
23
|
const MONO = 'font-family:var(--mono);font-size:10.5px';
|
|
@@ -76,8 +77,10 @@ async function endSession(name, s) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
async function removeTree(name, t) {
|
|
79
|
-
const ok =
|
|
80
|
-
:
|
|
80
|
+
const ok = await askConfirm({
|
|
81
|
+
title: 'Remove the folder for “' + t.branch + '”?', confirmLabel: 'Remove folder', danger: true,
|
|
82
|
+
body: t.dir + '\n\nThis only removes the folder (git worktree remove). The branch is kept and nothing is merged into main — merge through a pull request. A folder with unsaved work, or a running session, is refused: end the session first.',
|
|
83
|
+
});
|
|
81
84
|
if (!ok) return;
|
|
82
85
|
const out = dataMod ? await dataMod.removeWorktree(t.ws, t.dir) : null;
|
|
83
86
|
if (!out || out.ok === false) { note(name, (out && out.error) || 'the folder could not be removed', 'bad'); return; }
|