create-pmos 0.37.0 → 0.38.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/bin/create-pmos.js +49 -9
- package/package.json +1 -1
- package/payload/AGENTS.md +4 -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/bin/pmos.js +23 -4
- 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 +1 -1
- 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 +437 -0
- package/workspace/public/pm/modes.js +8 -5
- 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 +736 -42
- 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
|
@@ -265,9 +265,15 @@ export function openTerm(slug, sess) {
|
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
export function openDoc(slug, path, role) {
|
|
268
|
+
export function openDoc(slug, path, role, tree) {
|
|
269
|
+
/* `tree` — the line of work a search hit came from (overhaul-r1 R1.3). A hit on a LINKED worktree
|
|
270
|
+
* opens a tab that carries that tree's `ws` and `branch`, under its own id, so it is read from and
|
|
271
|
+
* saved to that tree and can sit beside the main tree's copy of the same path. A main-tree hit
|
|
272
|
+
* opens exactly the tab it always did. */
|
|
273
|
+
const linked = !!(tree && tree.ws && tree.linked);
|
|
269
274
|
openTab({
|
|
270
|
-
id: 'doc:' + path, kind: 'doc', title: path.split('/').pop(), slug,
|
|
275
|
+
id: 'doc:' + (linked ? tree.ws + ':' : '') + path, kind: 'doc', title: path.split('/').pop(), slug,
|
|
276
|
+
...(linked ? { ws: tree.ws, branch: tree.branch || null } : {}),
|
|
271
277
|
/* A file opened from the FOLDER search belongs to no initiative, so `slugProduct()` has no
|
|
272
278
|
* answer for it — and `panel-tabs.visibleTabs` shows a tab whose repo is `undefined` in EVERY
|
|
273
279
|
* repository, which would hang this workspace's file in another one's tab strip and read its
|
|
@@ -522,15 +528,20 @@ function dataModule() {
|
|
|
522
528
|
return null;
|
|
523
529
|
}
|
|
524
530
|
|
|
531
|
+
/* overhaul-r4 B2 — "all repositories": the same box, widened to every registered repository. The
|
|
532
|
+
* cache is keyed by scope too, so the two answers never mix. */
|
|
533
|
+
const scopeKey = (q) => (state.searchAll ? 'all\u0000' : '') + q;
|
|
525
534
|
function requestFiles(q) {
|
|
526
|
-
|
|
535
|
+
const key = scopeKey(q);
|
|
536
|
+
const all = !!state.searchAll;
|
|
537
|
+
fileWanted = key;
|
|
527
538
|
if (fileTimer) clearTimeout(fileTimer);
|
|
528
539
|
fileTimer = setTimeout(() => {
|
|
529
540
|
fileTimer = null;
|
|
530
541
|
const mod = dataModule();
|
|
531
542
|
if (!mod || typeof mod.searchFiles !== 'function') return;
|
|
532
|
-
const asked =
|
|
533
|
-
Promise.resolve(mod.searchFiles(
|
|
543
|
+
const asked = key;
|
|
544
|
+
Promise.resolve(mod.searchFiles(q, undefined, all)).then(
|
|
534
545
|
(r) => {
|
|
535
546
|
if (fileWanted !== asked) return; // the PM has typed past this query
|
|
536
547
|
fileCache.set(asked, (r && r.ok === false)
|
|
@@ -539,6 +550,8 @@ function requestFiles(q) {
|
|
|
539
550
|
hits: (r && r.hits) || [], truncated: !!(r && r.truncated),
|
|
540
551
|
contentTruncated: !!(r && r.content_truncated),
|
|
541
552
|
scanned: (r && r.scanned) || 0, contentScanned: (r && r.content_scanned) || 0, error: null,
|
|
553
|
+
trees: Array.isArray(r && r.trees) ? r.trees : [],
|
|
554
|
+
repos: Array.isArray(r && r.repos) ? r.repos : null,
|
|
542
555
|
});
|
|
543
556
|
if (fileCache.size > 40) fileCache.delete(fileCache.keys().next().value);
|
|
544
557
|
setState({});
|
|
@@ -555,12 +568,34 @@ function requestFiles(q) {
|
|
|
555
568
|
/** The result for `q`, or null while nothing has come back yet. Kicks the fetch off on a miss. */
|
|
556
569
|
function fileResults(q) {
|
|
557
570
|
if (!q || FIXTURE) return null;
|
|
558
|
-
if (fileCache.has(q)) return fileCache.get(q);
|
|
571
|
+
if (fileCache.has(scopeKey(q))) return fileCache.get(scopeKey(q));
|
|
559
572
|
requestFiles(q);
|
|
560
573
|
return null;
|
|
561
574
|
}
|
|
562
575
|
|
|
563
|
-
|
|
576
|
+
/* overhaul-r1 R1.2 — every hit names its line of work. A hit merged across trees (identical
|
|
577
|
+
* content) names every branch; a hit from a linked worktree names its branch with `⑂`; a hit only
|
|
578
|
+
* on the main tree of a repository with no linked trees says nothing extra. */
|
|
579
|
+
const isLinked = (ws) => !!(dataMod && typeof dataMod.isLinkedTree === 'function' && dataMod.isLinkedTree(ws));
|
|
580
|
+
const hitTree = (h) => (h && h.ws ? { ws: h.ws, branch: h.branch || null, linked: isLinked(h.ws) } : null);
|
|
581
|
+
function branchTag(h) {
|
|
582
|
+
const bs = Array.isArray(h.branches) ? h.branches.filter(Boolean) : [];
|
|
583
|
+
if (bs.length > 1) return ' · on ' + bs.join(', ');
|
|
584
|
+
if (h.ws && isLinked(h.ws)) return ' · ⑂ ' + (h.branch || 'detached');
|
|
585
|
+
return '';
|
|
586
|
+
}
|
|
587
|
+
/* A hit from ANOTHER repository (all-repositories scope) selects that repository first, then opens
|
|
588
|
+
* the file from the hit's own tree, so it is read where it was found. */
|
|
589
|
+
async function openHit(h) {
|
|
590
|
+
if (h.repo && h.repo !== state.product) {
|
|
591
|
+
const repos = await import('./panel-repos.js');
|
|
592
|
+
repos.selectRepo(h.repo);
|
|
593
|
+
openDoc(null, h.path, 'FILE', h.ws ? { ws: h.ws, branch: h.branch || null, linked: true } : null);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
openDoc(null, h.path, 'FILE', hitTree(h));
|
|
597
|
+
}
|
|
598
|
+
const fileLabel = (h) => (h.repo && state.searchAll ? h.repo + ' · ' : '') + 'file · ' + (h.kind || 'file') + (h.match === 'content' ? ' · line ' + h.line : '') + branchTag(h);
|
|
564
599
|
|
|
565
600
|
function fileHitNode(h) {
|
|
566
601
|
const where = h.dir ? h.dir + '/' : '';
|
|
@@ -575,7 +610,7 @@ function fileHitNode(h) {
|
|
|
575
610
|
);
|
|
576
611
|
}
|
|
577
612
|
return el('button', {
|
|
578
|
-
onClick: () =>
|
|
613
|
+
onClick: () => openHit(h),
|
|
579
614
|
style: 'display:block;width:100%;padding:6px 12px;min-height:40px;text-align:left',
|
|
580
615
|
hover: 'background:var(--accent)',
|
|
581
616
|
title: h.path,
|
|
@@ -593,10 +628,17 @@ function fileHitNode(h) {
|
|
|
593
628
|
export function fileHitsBlock(q) {
|
|
594
629
|
if (!q || FIXTURE) return null;
|
|
595
630
|
const r = fileResults(q);
|
|
631
|
+
const scope = state.searchAll ? 'files in every repository' : 'files in this folder';
|
|
596
632
|
const head = (text, tone) => el('div', {
|
|
597
|
-
style: 'padding:4px 12px 4px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;'
|
|
633
|
+
style: 'display:flex;gap:8px;align-items:center;padding:4px 12px 4px;font-family:var(--mono);font-size:10px;letter-spacing:.12em;'
|
|
598
634
|
+ 'text-transform:uppercase;color:' + (tone || 'var(--muted-foreground)') + ';border-bottom:1px solid var(--border)',
|
|
599
|
-
}, iv(text))
|
|
635
|
+
}, iv(String(text).replace(/^files in this folder/, scope)),
|
|
636
|
+
// overhaul-r4 B2 — widen the same search to every registered repository
|
|
637
|
+
el('button', {
|
|
638
|
+
onClick: () => setState((st) => ({ searchAll: !st.searchAll })),
|
|
639
|
+
style: 'margin-left:auto;letter-spacing:0;text-transform:none;font-size:10px;padding:1px 6px;border:1px solid var(--border);color:var(--muted-foreground)',
|
|
640
|
+
hover: 'color:var(--foreground)', 'data-r4': 'search-all', title: 'search every registered repository',
|
|
641
|
+
}, iv((state.searchAll ? '☑' : '☐') + ' all repositories')));
|
|
600
642
|
|
|
601
643
|
if (!r) return el('div', { 'data-pm-files': '' }, head('files in this folder · searching…'));
|
|
602
644
|
if (r.error) {
|
|
@@ -608,8 +650,11 @@ export function fileHitsBlock(q) {
|
|
|
608
650
|
* conclude from an empty list. `contentTruncated` means every file was NAMED and some bodies
|
|
609
651
|
* were not read, which is the ordinary state of any repository holding a lock file. Folding the
|
|
610
652
|
* second into the first would light the serious flag permanently and thereby retire it. */
|
|
653
|
+
// overhaul-r1 R1.4: with several lines of work, say WHICH walk was cut short
|
|
654
|
+
const cut = (r.trees || []).length > 1 ? (r.trees || []).filter((t) => t.truncated).map((t) => t.branch || 'detached') : [];
|
|
655
|
+
const lines = (r.trees || []).length > 1 ? ' across ' + r.trees.length + ' lines of work' : '';
|
|
611
656
|
const bound = r.truncated
|
|
612
|
-
? ' · BOUNDED — files beyond the walk limit were not looked at'
|
|
657
|
+
? ' · BOUNDED — files beyond the walk limit were not looked at' + (cut.length ? ' (on ' + cut.join(', ') + ')' : '')
|
|
613
658
|
: (r.contentTruncated ? ' · all ' + r.scanned + ' names searched; ' + r.contentScanned + ' bodies read' : '');
|
|
614
659
|
|
|
615
660
|
if (!r.hits.length) {
|
|
@@ -620,7 +665,7 @@ export function fileHitsBlock(q) {
|
|
|
620
665
|
? 'no match in the ' + r.scanned + ' files searched — the walk was BOUNDED, more were not looked at'
|
|
621
666
|
: 'no file matches — a successful read of nothing' + (r.contentTruncated ? bound : ''))));
|
|
622
667
|
}
|
|
623
|
-
const kids = [head('files in this folder · ' + r.hits.length + ' of ' + r.scanned + ' searched' + bound)];
|
|
668
|
+
const kids = [head('files in this folder · ' + r.hits.length + ' of ' + r.scanned + ' searched' + lines + bound)];
|
|
624
669
|
for (const h of r.hits) kids.push(fileHitNode(h));
|
|
625
670
|
return el('div', { 'data-pm-files': '' }, kids);
|
|
626
671
|
}
|
|
@@ -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,
|