create-pmos 0.36.0 → 0.38.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/bin/create-pmos.js +22 -3
- package/package.json +1 -1
- package/payload/AGENTS.md +6 -0
- package/payload/KIT-VERSION +2 -2
- package/payload/okf/core/concepts/frontmatter-format.md +27 -61
- package/payload/okf/core/templates/decision-record-template.md +34 -0
- package/payload/okf/core/templates/index.md +7 -2
- package/payload/okf/core/templates/pm-brief-template.md +34 -0
- package/payload/okf/core/templates/project-plan-template.md +43 -0
- package/payload/okf/core/templates/report-weekly-template.md +35 -0
- package/payload/okf/core/templates/strategy-long-range-template.md +37 -0
- package/payload/okf/core/templates/strategy-one-pager-template.md +34 -0
- package/payload/planning/evals/eval-calibration.md +8 -0
- package/payload/planning/okrs/health-budgets.md +8 -0
- package/payload/scripts/anchor-resolve.js +56 -6
- package/payload/scripts/gate-initiative.sh +32 -0
- package/payload/scripts/okf-upgrade.js +232 -0
- package/payload/scripts/pmos-memory.js +375 -0
- package/payload/scripts/pmos-model.js +239 -0
- package/payload/scripts/pmos-skills.js +274 -0
- package/payload/scripts/workflow-state.js +178 -10
- package/payload/scripts/workflow-sweep.js +2 -1
- package/payload/state/discoveries.md +8 -0
- package/payload/state/index.md +7 -0
- package/payload/state/log.md +7 -0
- package/workspace/README.md +61 -204
- package/workspace/lib/board-cache.js +18 -8
- package/workspace/lib/console-state.js +2 -0
- package/workspace/lib/engine/anchor-resolve.js +56 -6
- package/workspace/lib/engine/pmos-memory.js +375 -0
- package/workspace/lib/engine/pmos-model.js +239 -0
- package/workspace/lib/engine/pmos-skills.js +274 -0
- package/workspace/lib/engine/workflow-state.js +178 -10
- package/workspace/lib/focus.js +5 -6
- package/workspace/lib/memory.js +54 -0
- package/workspace/lib/models.js +50 -0
- package/workspace/lib/preview-kind.js +12 -1
- package/workspace/lib/schedule-logic.js +31 -2
- package/workspace/lib/skill-store.js +5 -1
- package/workspace/lib/skills-registry.js +74 -0
- package/workspace/lib/sync.js +132 -0
- package/workspace/lib/tree-share.js +2 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +3 -3
- package/workspace/public/pm/app.js +32 -2
- package/workspace/public/pm/data.js +160 -10
- package/workspace/public/pm/lifecycle.js +105 -0
- package/workspace/public/pm/markdown.js +10 -1
- package/workspace/public/pm/modes-live.js +371 -0
- package/workspace/public/pm/modes.js +7 -4
- package/workspace/public/pm/panel-detail.js +22 -2
- package/workspace/public/pm/panel-initiatives.js +57 -12
- package/workspace/public/pm/panel-newsess.js +25 -0
- package/workspace/public/pm/panel-repos.js +130 -4
- package/workspace/public/pm/panel-tabs.js +49 -2
- package/workspace/public/pm/state.js +11 -1
- package/workspace/public/pm/tab-doc.js +160 -26
- package/workspace/public/pm/tab-term.js +2 -0
- package/workspace/public/pm/tab-web.js +89 -0
- package/workspace/public/pm/tokens.css +4 -0
- package/workspace/public/pm/xlsx-read.js +117 -0
- package/workspace/server.js +690 -40
- package/workspace/skills/gather-requirements.skill +53 -0
- package/workspace/skills/memory-groom.skill +46 -0
- package/workspace/skills/report-biweekly.skill +53 -0
- package/workspace/skills/report-daily.skill +53 -0
- package/workspace/skills/report-monthly.skill +53 -0
- package/workspace/skills/report-special.skill +49 -0
- package/workspace/skills/report-weekly.skill +53 -0
- package/workspace/skills/skill-discover.skill +49 -0
- package/workspace/skills/skill-generate.skill +33 -0
|
@@ -295,6 +295,28 @@ function isolateNow() {
|
|
|
295
295
|
return live.length > 0;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/* ── the model chips (D75) ──────────────────────────────────────────────────────────────────
|
|
299
|
+
* · only a Claude session has a model control PMOS has checked, so only it is offered a choice;
|
|
300
|
+
* · `auto` sends nothing: the server routes by the policy, or asks for no model at all;
|
|
301
|
+
* · the chips say what the PM is choosing (a tier), never a model name that will age. */
|
|
302
|
+
function modelSection() {
|
|
303
|
+
if (state.newSessAgent !== 'claude') {
|
|
304
|
+
return el('div', { 'data-r12': 'model-note', style: SUB }, iv('model: only a Claude session can be given a model by PMOS — a shell, or any other CLI, runs its own default'));
|
|
305
|
+
}
|
|
306
|
+
const cur = state.newSessTier || 'auto';
|
|
307
|
+
return el('div', { 'data-r12': 'model' },
|
|
308
|
+
el('div', { style: LABEL }, 'model'),
|
|
309
|
+
el('div', { style: ROW },
|
|
310
|
+
['auto', 'fast', 'balanced', 'deep'].map((t) => el('button', {
|
|
311
|
+
'data-r12': 'tier-' + t,
|
|
312
|
+
onClick: () => setState({ newSessTier: t, newSessError: null }),
|
|
313
|
+
style: cur === t ? CHIP_ON : CHIP, hover: HOVER,
|
|
314
|
+
}, iv(t))),
|
|
315
|
+
el('span', { style: SUB }, cur === 'auto'
|
|
316
|
+
? 'auto: PMOS asks for no model unless your policy routes this work to one'
|
|
317
|
+
: 'a ' + cur + ' tier — the model and effort it maps to are in your models policy')));
|
|
318
|
+
}
|
|
319
|
+
|
|
298
320
|
/* ── the profile chips ─────────────────────────────────────────────────────────────────────── */
|
|
299
321
|
|
|
300
322
|
/**
|
|
@@ -419,6 +441,8 @@ async function onStart() {
|
|
|
419
441
|
// value from liveSessionCount, and its own comment asks the client to state what the PM saw.
|
|
420
442
|
isolate: branch ? isolateNow() : null,
|
|
421
443
|
profile: state.preflightProfile === undefined ? null : state.preflightProfile,
|
|
444
|
+
// D75 — only for a Claude session; 'auto' sends nothing and lets the policy route.
|
|
445
|
+
tier: (state.newSessAgent === 'claude' && state.newSessTier && state.newSessTier !== 'auto') ? state.newSessTier : undefined,
|
|
422
446
|
});
|
|
423
447
|
|
|
424
448
|
if (!res || !res.ok) {
|
|
@@ -495,6 +519,7 @@ export function newSessionBlock() {
|
|
|
495
519
|
el('span', { style: SUB }, 'a CLI that is not on PATH is refused by the server, and says so'),
|
|
496
520
|
),
|
|
497
521
|
|
|
522
|
+
modelSection(),
|
|
498
523
|
branchSection(),
|
|
499
524
|
profileSection(),
|
|
500
525
|
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { el, setChildren } from './dom.js';
|
|
27
|
+
import { syncButton } from './lifecycle.js';
|
|
27
28
|
import { state, setState, FIXTURE } from './state.js';
|
|
28
29
|
import {
|
|
29
30
|
productsList, repoRow, firstSlugOf, MODE_META, SETTINGS_GROUPS, ADDABLE_PRODUCTS,
|
|
@@ -166,13 +167,25 @@ export function selectRepo(id) {
|
|
|
166
167
|
* belt and braces — it makes the absence a property of THIS module, so it cannot regress by someone
|
|
167
168
|
* adding an id to the seed list.
|
|
168
169
|
*/
|
|
170
|
+
/* overhaul-r1 R6.1 — in LIVE mode, a mode with no server behind it is not offered. `agents`,
|
|
171
|
+
* `memory` and `connections` start empty and every edit is a client-only setState that no server
|
|
172
|
+
* receives (modes.js `seedOr`), so they looked broken because they were. They come back in a later
|
|
173
|
+
* release wired or replaced. Fixture mode keeps them: it is the prototype's recorded state, and the
|
|
174
|
+
* design comparison reads it. Exported so the live-mode test can assert the list, not a screenshot. */
|
|
175
|
+
export const UNBACKED_MODES = ['agent', 'connect']; // mode ids (labels: agents · connections). `memory` left this list in R9 (D72): the repository's ledger backs it.
|
|
176
|
+
/* overhaul-r4 — the PORTFOLIO mode joins "all repositories" in live mode only (the fixture drawer is the
|
|
177
|
+
* prototype's, and the design comparison reads it). Its meta lives here, not in fixture.js, for the
|
|
178
|
+
* same reason. */
|
|
179
|
+
const META = Object.assign({}, MODE_META, { portfolio: ['▦', 'portfolio'] });
|
|
180
|
+
const LIVE_EXTRA = { 'all repositories': ['portfolio'] };
|
|
169
181
|
const DRAWER_GROUPS = SETTINGS_GROUPS.map((g) => ({
|
|
170
182
|
label: g.label,
|
|
171
|
-
items:
|
|
172
|
-
|
|
183
|
+
items: (FIXTURE ? [] : LIVE_EXTRA[g.label] || []).concat(
|
|
184
|
+
g.items.filter((id) => id !== 'workflow' && MODE_META[id] && (FIXTURE || !UNBACKED_MODES.includes(id)))),
|
|
185
|
+
})).filter((g) => g.items.length);
|
|
173
186
|
|
|
174
187
|
function drawerButton(id) {
|
|
175
|
-
const [glyph, label] =
|
|
188
|
+
const [glyph, label] = META[id];
|
|
176
189
|
const on = state.mode === id;
|
|
177
190
|
return el('button', {
|
|
178
191
|
onClick: () => setState({ mode: id, settingsOpen: false, col1: true }),
|
|
@@ -233,6 +246,119 @@ function repoButton(tuple, product) {
|
|
|
233
246
|
);
|
|
234
247
|
}
|
|
235
248
|
|
|
249
|
+
/* ── overhaul-r1: lines of work, rename, remove (LIVE mode only) ─────────────────────────────
|
|
250
|
+
* Fixture mode renders the prototype's row unchanged. In live mode the row gains a `⑂ N` chip
|
|
251
|
+
* when the repository has linked worktrees (R2.1), and the ACTIVE row gains rename and remove
|
|
252
|
+
* (R3.1/R3.2). The server's answer to a remove is shown under the row in its own words (R3.3).
|
|
253
|
+
*/
|
|
254
|
+
const chipStyle = 'flex:none;font-family:var(--mono);font-size:10px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground);min-height:22px';
|
|
255
|
+
const actStyle = 'flex:none;font-size:12px;padding:2px 6px;color:var(--muted-foreground);min-height:28px';
|
|
256
|
+
|
|
257
|
+
function treesOf(name) {
|
|
258
|
+
return dataMod && typeof dataMod.productTrees === 'function' ? dataMod.productTrees(name) : [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function renameRepo(name) {
|
|
262
|
+
const label = typeof window !== 'undefined' && window.prompt ? window.prompt('Rename “' + name + '” to:', name) : null;
|
|
263
|
+
if (label === null || !String(label).trim() || String(label).trim() === name) return;
|
|
264
|
+
const out = await dataMod.renameRepository(name, String(label).trim());
|
|
265
|
+
if (!out || out.ok === false) { setState({ repoNotice: { name, tone: 'bad', text: (out && out.error) || 'rename refused' } }); return; }
|
|
266
|
+
await connectData();
|
|
267
|
+
setState({ product: String(label).trim(), repoNotice: null });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function removeRepo(name) {
|
|
271
|
+
const ok = typeof window === 'undefined' || !window.confirm ? true
|
|
272
|
+
: window.confirm('Remove “' + name + '” from PMOS?\n\nThis forgets the repository and its worktree rows. No folder is deleted.');
|
|
273
|
+
if (!ok) return;
|
|
274
|
+
const out = await dataMod.removeRepository(name);
|
|
275
|
+
if (!out || out.ok === false) { setState({ repoNotice: { name, tone: 'bad', text: (out && out.error) || 'remove refused' } }); return; }
|
|
276
|
+
realRepos = (realRepos || []).filter((t) => t[0] !== name);
|
|
277
|
+
setState({ repoNotice: null });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function repoEntry(tuple, product) {
|
|
281
|
+
const row = repoButton(tuple, product);
|
|
282
|
+
if (FIXTURE || !dataMod) return row;
|
|
283
|
+
const name = tuple[0];
|
|
284
|
+
const trees = treesOf(name);
|
|
285
|
+
const linked = trees.filter((t) => !t.main);
|
|
286
|
+
const open = !!(state.treesOpen || {})[name];
|
|
287
|
+
const tools = [];
|
|
288
|
+
if (linked.length) {
|
|
289
|
+
tools.push(el('button', {
|
|
290
|
+
onClick: () => setState((s) => ({ treesOpen: { ...(s.treesOpen || {}), [name]: !open } })),
|
|
291
|
+
style: chipStyle, hover: 'color:var(--foreground)',
|
|
292
|
+
title: linked.length + ' linked worktree' + (linked.length === 1 ? '' : 's') + ' — show the lines of work',
|
|
293
|
+
'data-r1': 'trees-chip',
|
|
294
|
+
}, ip('⑂ ' + linked.length)));
|
|
295
|
+
}
|
|
296
|
+
if (product === name) {
|
|
297
|
+
{ const sb = syncButton(name); if (sb) tools.push(sb); }
|
|
298
|
+
tools.push(el('button', { onClick: () => renameRepo(name), style: actStyle, hover: 'color:var(--foreground)', title: 'rename this repository', 'data-r1': 'rename' }, '✎'));
|
|
299
|
+
tools.push(el('button', { onClick: () => removeRepo(name), style: actStyle, hover: 'color:var(--destructive)', title: 'remove this repository from PMOS (no folder is deleted)', 'data-r1': 'remove' }, '✕'));
|
|
300
|
+
}
|
|
301
|
+
const kids = [el('div', { style: 'display:flex;align-items:center' },
|
|
302
|
+
el('div', { style: 'flex:1;min-width:0' }, row),
|
|
303
|
+
tools.length ? el('div', { style: 'display:flex;align-items:center;gap:2px;padding-right:8px' }, tools) : null)];
|
|
304
|
+
if (open && trees.length) {
|
|
305
|
+
kids.push(el('div', { style: 'padding:2px 14px 8px 39px', 'data-r1': 'trees' },
|
|
306
|
+
trees.map((t) => el('div', {
|
|
307
|
+
style: 'display:flex;gap:8px;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground);padding:2px 0',
|
|
308
|
+
title: t.initiative ? 'initiative read from the branch name (initiative/<id>) — not a recorded attribution' : '',
|
|
309
|
+
},
|
|
310
|
+
el('span', { style: 'color:var(--foreground)' }, ip((t.main ? '⎇ ' : '⑂ ') + (t.branch || '(detached)'))),
|
|
311
|
+
el('span', null, ip(t.sessions + ' session' + (t.sessions === 1 ? '' : 's'))),
|
|
312
|
+
t.initiative ? el('span', null, ip('#' + t.initiative)) : null,
|
|
313
|
+
)),
|
|
314
|
+
));
|
|
315
|
+
}
|
|
316
|
+
const n = state.repoNotice;
|
|
317
|
+
if (n && n.name === name) {
|
|
318
|
+
kids.push(el('div', {
|
|
319
|
+
style: 'padding:4px 14px 8px 39px;font-size:11px;color:' + (n.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)'),
|
|
320
|
+
'data-r1': 'notice',
|
|
321
|
+
}, ip(n.text)));
|
|
322
|
+
}
|
|
323
|
+
return el('div', null, kids);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* overhaul-r1 R4 — the collapsed repositories rail, one glyph per repository, LIVE mode only (the
|
|
328
|
+
* fixture rail is the prototype's and the comparator reads it). Initials + kind badge; a dot when a
|
|
329
|
+
* session on any of its lines of work needs the PM. Click selects WITHOUT expanding the panel. The
|
|
330
|
+
* active glyph is marked by a left bar and a filled box — shape, not colour alone.
|
|
331
|
+
*/
|
|
332
|
+
export function railGlyphs() {
|
|
333
|
+
if (FIXTURE) return [];
|
|
334
|
+
const tuples = repoTuples();
|
|
335
|
+
const product = activeProduct(tuples);
|
|
336
|
+
const sess = Array.isArray(state.sessions) ? state.sessions : [];
|
|
337
|
+
return tuples.map((t) => {
|
|
338
|
+
const rp = repoRow(t);
|
|
339
|
+
const name = rp.name;
|
|
340
|
+
const wss = new Set(treesOf(name).map((x) => x.ws));
|
|
341
|
+
const needs = sess.some((x) => x && wss.has(x.ws) && x.shown === 'needs');
|
|
342
|
+
const active = product === name;
|
|
343
|
+
const ini = (String(name).replace(/[^A-Za-z0-9]+/g, ' ').trim().split(' ').filter(Boolean).slice(0, 2)
|
|
344
|
+
.map((w) => w[0]).join('') || String(name).slice(0, 2)).toUpperCase();
|
|
345
|
+
const said = name + ' — ' + rp.kindLabel + ' repository' + (needs ? ' · a session needs you' : '');
|
|
346
|
+
return el('button', {
|
|
347
|
+
onClick: (e) => { if (e && e.stopPropagation) e.stopPropagation(); selectRepo(name); },
|
|
348
|
+
title: said, 'aria-label': said,
|
|
349
|
+
'data-r1': 'rail-glyph', 'data-repo': name, 'data-active': active ? '1' : '0',
|
|
350
|
+
'data-ini': ini, 'data-kind': rp.kindGlyph,
|
|
351
|
+
style: 'position:relative;flex:none;width:28px;height:28px;margin:3px auto;display:flex;align-items:center;justify-content:center;'
|
|
352
|
+
+ 'font-family:var(--mono);font-size:10px;font-weight:600;border:1px solid ' + (active ? 'var(--brand)' : 'var(--border)')
|
|
353
|
+
+ ';background:' + (active ? 'var(--accent)' : 'transparent') + ';color:var(--foreground);'
|
|
354
|
+
+ 'box-shadow:' + (active ? 'inset 3px 0 0 var(--brand)' : 'none'),
|
|
355
|
+
hover: 'background:var(--accent)',
|
|
356
|
+
},
|
|
357
|
+
needs ? el('span', { style: 'position:absolute;right:-3px;top:-3px;width:7px;height:7px;border-radius:50%;background:var(--warning, var(--destructive))', 'data-r1': 'needs-dot' }) : null,
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
236
362
|
/** setDetail (line 1401) — open panel 3 on a detail kind, un-collapsing it if the PM folded it. */
|
|
237
363
|
const setDetail = (d) => setState({ detail: d, col2: true });
|
|
238
364
|
|
|
@@ -293,7 +419,7 @@ export function renderRepos(host) {
|
|
|
293
419
|
|
|
294
420
|
const scroll = el('div', { style: 'flex:1;overflow-y:auto;min-height:0' },
|
|
295
421
|
stateHealthBanner(),
|
|
296
|
-
tuples.map((t) =>
|
|
422
|
+
tuples.map((t) => repoEntry(t, product)),
|
|
297
423
|
el('button', {
|
|
298
424
|
onClick: onAddProduct,
|
|
299
425
|
style: 'display:flex;align-items:center;gap:9px;width:100%;padding:9px 14px;min-height:40px;color:var(--muted-foreground)',
|
|
@@ -32,6 +32,7 @@ import * as FX from './fixture.js';
|
|
|
32
32
|
import { openTab, openTerm as openTermTab, openDoc as openDocTab } from './panel-initiatives.js';
|
|
33
33
|
import { mountDoc, disposeDoc, docText } from './tab-doc.js';
|
|
34
34
|
import { mountTask } from './tab-task.js';
|
|
35
|
+
import { mountWeb, promptWeb } from './tab-web.js';
|
|
35
36
|
import { newSessionBlock, openNewSession } from './panel-newsess.js';
|
|
36
37
|
import { boardOf, currentProduct } from './board-read.js';
|
|
37
38
|
|
|
@@ -300,6 +301,12 @@ const newSessionBtn = el('button', {
|
|
|
300
301
|
onClick: () => newSession(), style: CHIP_STYLE, hover: CHIP_HOVER,
|
|
301
302
|
}, '+ session');
|
|
302
303
|
|
|
304
|
+
/* overhaul-r5 (D69) — open a web page in a tab, by typing its address. Live mode only. */
|
|
305
|
+
const newWebBtn = el('button', {
|
|
306
|
+
onClick: () => promptWeb(), style: CHIP_STYLE, hover: CHIP_HOVER,
|
|
307
|
+
title: 'open a web page in a tab (http/https; it loads only when you open it)', 'data-r5': 'new-web',
|
|
308
|
+
}, '🌐 web');
|
|
309
|
+
|
|
303
310
|
/* `⋯ options` — the demoted composer, restored (workspace-session-preflight).
|
|
304
311
|
*
|
|
305
312
|
* `workspace-repo-first`'s accepted PRD says the six-question composer "remains reachable as *more
|
|
@@ -661,6 +668,7 @@ function peerRow(prompt, p) {
|
|
|
661
668
|
p.doing ? el('div', {
|
|
662
669
|
style: 'font-family:var(--mono);font-size:10.5px;color:oklch(0.62 0.02 213);overflow:hidden;text-overflow:ellipsis;white-space:nowrap',
|
|
663
670
|
}, iv(p.doing)) : null,
|
|
671
|
+
p.modelLine ? el('div', { 'data-r12': 'model-line', style: 'font-family:var(--mono);font-size:10.5px;color:oklch(0.62 0.02 213)' }, iv(p.modelLine)) : null,
|
|
664
672
|
el('div', { style: 'font-size:11px;color:' + RISK_COLOR[p.risk.level] + ';line-height:1.45' }, iv(p.risk.why)),
|
|
665
673
|
/* WHY THE END DID NOT HAPPEN, in the row the PM was looking at. A refusal recorded in state
|
|
666
674
|
* and rendered nowhere would be the same silence in a new place — the whole defect here was a
|
|
@@ -679,6 +687,18 @@ function peerRow(prompt, p) {
|
|
|
679
687
|
onClick: () => joinSession(prompt.slug, p),
|
|
680
688
|
style: CH_BTN + ';color:var(--brand)', hover: CHIP_HOVER,
|
|
681
689
|
}, 'open it'),
|
|
690
|
+
// D75 — switch a RUNNING Claude session's model, on a press: PMOS types the harness's own /model command into the pane.
|
|
691
|
+
(p.agent === 'claude' && p.shown !== 'exited') ? el('button', {
|
|
692
|
+
'data-r12': 'switch-model', title: 'types /model into this pane; effort is not changed',
|
|
693
|
+
onClick: async () => {
|
|
694
|
+
const t = typeof window !== 'undefined' && window.prompt ? window.prompt('Switch “' + p.name + '” to which tier? fast, balanced or deep', 'balanced') : null;
|
|
695
|
+
if (!t || !t.trim()) return;
|
|
696
|
+
const out = await fetch('/api/session/model', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: p.id, tier: t.trim() }) })
|
|
697
|
+
.then((r) => r.json()).catch((e) => ({ ok: false, error: String((e && e.message) || e) }));
|
|
698
|
+
setState({ endRefusal: out && out.ok ? null : { id: p.id, why: 'model not switched — ' + ((out && out.error) || 'no answer') } });
|
|
699
|
+
},
|
|
700
|
+
style: CH_BTN, hover: CHIP_HOVER,
|
|
701
|
+
}, 'switch model') : null,
|
|
682
702
|
el('button', {
|
|
683
703
|
onClick: () => {
|
|
684
704
|
if (!armed) { setState({ endConfirm: p.id, endRefusal: null }); return; }
|
|
@@ -772,6 +792,8 @@ function paneFor(tab) {
|
|
|
772
792
|
if (termMod && typeof termMod.mountTerm === 'function') termMod.mountTerm(host, tab);
|
|
773
793
|
} else if (tab.kind === 'task') {
|
|
774
794
|
mountTask(host, tab);
|
|
795
|
+
} else if (tab.kind === 'web') {
|
|
796
|
+
mountWeb(host, tab); // overhaul-r5 — never falls through to an empty doc tab
|
|
775
797
|
} else {
|
|
776
798
|
mountDoc(host, tab);
|
|
777
799
|
}
|
|
@@ -851,6 +873,29 @@ function keepSessionsLive(shown) {
|
|
|
851
873
|
|
|
852
874
|
/* ── the tab bar ───────────────────────────────────────────────────────────────────────────── */
|
|
853
875
|
|
|
876
|
+
/**
|
|
877
|
+
* overhaul-r1 R2.2/R2.3 — the line of work a tab is on, or null when it is the main tree (which
|
|
878
|
+
* shows nothing extra). A doc tab carries its tree from the search hit that opened it; a terminal
|
|
879
|
+
* tab is on whatever tree its session's workspace is. Live mode only; fixture tabs have neither.
|
|
880
|
+
*/
|
|
881
|
+
export function tabBranch(t, includeMain) {
|
|
882
|
+
if (FIXTURE || !t || !dataMod) return null;
|
|
883
|
+
let ws = null;
|
|
884
|
+
if (t.kind === 'doc') ws = t.ws || null;
|
|
885
|
+
else if (t.kind === 'term' && t.sessionId) {
|
|
886
|
+
const s = (Array.isArray(state.sessions) ? state.sessions : []).find((x) => x && x.id === t.sessionId);
|
|
887
|
+
ws = s ? s.ws : null;
|
|
888
|
+
}
|
|
889
|
+
if (ws && dataMod.isLinkedTree(ws)) return (t.kind === 'doc' && t.branch) || dataMod.branchOfWs(ws) || 'detached';
|
|
890
|
+
if (!includeMain) return null;
|
|
891
|
+
// the status bar (R2.3) also names the MAIN tree's branch, but only where the repository has other
|
|
892
|
+
// lines of work — in a repository with one tree the branch answers no question anyone asked
|
|
893
|
+
const prod = t.repo || currentProduct();
|
|
894
|
+
const trees = prod && typeof dataMod.productTrees === 'function' ? dataMod.productTrees(prod) : [];
|
|
895
|
+
const main = trees.find((x) => x.main);
|
|
896
|
+
return trees.length > 1 && main ? main.branch : null;
|
|
897
|
+
}
|
|
898
|
+
|
|
854
899
|
function tabNode(t, act, splitTab) {
|
|
855
900
|
const selected = (act && act.id === t.id) || (splitTab && splitTab.id === t.id);
|
|
856
901
|
const bg = selected ? FX.TAB_COLORS.activeBg : 'transparent';
|
|
@@ -863,7 +908,7 @@ function tabNode(t, act, splitTab) {
|
|
|
863
908
|
* fixture render keeps the design's literal — the comparator reads this text node. */
|
|
864
909
|
const meta = t.kind === 'term'
|
|
865
910
|
? ((!FIXTURE && t.sessionId) ? 'session ' + t.sessionId : 'session')
|
|
866
|
-
: (t.kind === 'task' ? 'task' : t.role);
|
|
911
|
+
: (t.kind === 'task' ? 'task' : t.kind === 'web' ? 'web' : t.role);
|
|
867
912
|
|
|
868
913
|
return el('div', {
|
|
869
914
|
style: 'flex:none;display:flex;align-items:center;gap:8px;padding:0 10px;background:' + bg
|
|
@@ -881,7 +926,7 @@ function tabNode(t, act, splitTab) {
|
|
|
881
926
|
}, iv(t.title)),
|
|
882
927
|
el('span', {
|
|
883
928
|
style: 'font-family:var(--mono);font-size:9px;color:oklch(0.52 0.02 213);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2',
|
|
884
|
-
}, iv(t.slug), ' · ', iv(meta)),
|
|
929
|
+
}, iv(t.slug), ' · ', iv(meta), tabBranch(t) ? el('span', { 'data-r1': 'tab-branch' }, iv(' · ⑂ ' + tabBranch(t))) : null),
|
|
885
930
|
),
|
|
886
931
|
),
|
|
887
932
|
el('button', {
|
|
@@ -914,6 +959,8 @@ export function renderTabs(host) {
|
|
|
914
959
|
setChildren(tabBar, [
|
|
915
960
|
...visTabs.map((t) => tabNode(t, act, splitTab)),
|
|
916
961
|
newSessionBtn,
|
|
962
|
+
FIXTURE ? null : newWebBtn,
|
|
963
|
+
!FIXTURE && state.webNotice ? el('span', { 'data-r5': 'notice', style: 'align-self:center;font-size:11px;color:var(--destructive);padding:0 6px' }, state.webNotice) : null,
|
|
917
964
|
newSessionOptsBtn,
|
|
918
965
|
canSplit ? splitBtn : null,
|
|
919
966
|
]);
|
|
@@ -36,6 +36,9 @@ function initial() {
|
|
|
36
36
|
userGroups: {}, userKrs: {}, addedInits: {}, userInits: {}, selTask: null, collapsedPhases: {},
|
|
37
37
|
detail: { kind: 'init' },
|
|
38
38
|
tabs: [], active: null, splitId: null,
|
|
39
|
+
// overhaul-r1: reader measure per tab (R5), which repositories show their lines of work (R2.1),
|
|
40
|
+
// and the last remove/rename answer, shown on the row it is about (R3.3)
|
|
41
|
+
docNarrow: {}, treesOpen: {}, repoNotice: null,
|
|
39
42
|
initLinks: {}, artLinks: {}, artSyncOpen: {}, artSyncState: {},
|
|
40
43
|
/* The sync half of panel 3, all per-window and none of it persisted. `artSyncTask` holds the
|
|
41
44
|
* task text the server composed for a direction the PM just chose; `linkGone` holds the
|
|
@@ -143,7 +146,12 @@ const isFlagMap = (v) =>
|
|
|
143
146
|
* id, so a restored terminal re-attaches to exactly the PTY it was showing rather than re-running
|
|
144
147
|
* the name join — which is what made a reloaded tab land on a session the PM could not identify. */
|
|
145
148
|
const TAB_FIELDS = ['id', 'kind', 'title', 'slug', 'repo', 'sess', 'sessionId', 'state', 'profile',
|
|
146
|
-
'path', 'role', 'phase', 'status'
|
|
149
|
+
'path', 'role', 'phase', 'status',
|
|
150
|
+
// the line of work a doc tab was opened from (overhaul-r1 R1.3/R2.2) — a reload must keep
|
|
151
|
+
// reading the branch-only file from ITS tree, not fall back to the main tree
|
|
152
|
+
'ws', 'branch',
|
|
153
|
+
// a web tab's address (overhaul-r5, D69) — restored, re-validated, and loaded only on a PM press
|
|
154
|
+
'url'];
|
|
147
155
|
const MAX_TABS = 40;
|
|
148
156
|
|
|
149
157
|
function packTab(t) {
|
|
@@ -169,6 +177,8 @@ const PERSIST = {
|
|
|
169
177
|
sel: isSlug,
|
|
170
178
|
collapsedGroups: isFlagMap,
|
|
171
179
|
collapsedPhases: isFlagMap,
|
|
180
|
+
// per doc tab: true = 72ch measure, false = full width; absent = the viewport default (R5.1)
|
|
181
|
+
docNarrow: isFlagMap,
|
|
172
182
|
tabs: isTabList,
|
|
173
183
|
active: isSlug,
|
|
174
184
|
splitId: isSlug,
|