create-pmos 0.37.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 +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/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 +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
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/* modes-live.js — the drawer modes that render SERVER data in live mode (overhaul-r4).
|
|
2
|
+
*
|
|
3
|
+
* modes.js renders the prototype's seeded lists, and in live mode those lists start empty and every
|
|
4
|
+
* edit is a client-only setState (seedOr). R1 hid the three modes with no server behind them; this
|
|
5
|
+
* module gives the other three their server truth, and adds the portfolio:
|
|
6
|
+
*
|
|
7
|
+
* portfolio — GET /api/portfolio: every registered repository's initiatives, grouped by domain
|
|
8
|
+
* then repository, freshness stated, unread repositories listed; a report bar that
|
|
9
|
+
* writes a weekly / OKR / query report into the hub (POST /api/reports, a PM press).
|
|
10
|
+
* scheduled — GET /api/schedules; run / delete through the existing routes.
|
|
11
|
+
* skills — GET /api/skills; a row with a file opens it.
|
|
12
|
+
* knowledge — GET /api/library's knowledge trees (OKF, skills, build-skills); a row opens it.
|
|
13
|
+
*
|
|
14
|
+
* Every list states its read: loading, failed (with the reason), or a successful read of nothing.
|
|
15
|
+
* Fixture mode never reaches this module (modes.js routes here only when !FIXTURE).
|
|
16
|
+
*/
|
|
17
|
+
import { el } from './dom.js';
|
|
18
|
+
import { state, setState } from './state.js';
|
|
19
|
+
|
|
20
|
+
const iv = (v) => el('span', null, v === null || v === undefined ? '' : String(v));
|
|
21
|
+
const LABEL = 'padding:10px 14px 4px;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
22
|
+
const ROW = 'display:block;width:100%;padding:7px 14px;min-height:40px;text-align:left;border-bottom:1px solid oklch(1 0 0 / 5%)';
|
|
23
|
+
const BTN = 'font-family:var(--mono);font-size:10.5px;padding:3px 9px;border:1px solid var(--border);color:var(--muted-foreground);min-height:26px';
|
|
24
|
+
const NOTE = 'padding:8px 14px;font-size:11.5px;color:var(--muted-foreground);line-height:1.5';
|
|
25
|
+
|
|
26
|
+
/* one cached read per endpoint; `reload(key)` drops it so the next render refetches */
|
|
27
|
+
const cache = new Map();
|
|
28
|
+
function read(key, url) {
|
|
29
|
+
const hit = cache.get(key);
|
|
30
|
+
if (hit && (hit.state !== 'ok' || Date.now() - hit.at < 15000)) return hit;
|
|
31
|
+
if (!hit || hit.state === 'ok') {
|
|
32
|
+
const next = { state: hit ? hit.state : 'loading', data: hit ? hit.data : null, at: hit ? hit.at : 0 };
|
|
33
|
+
cache.set(key, next);
|
|
34
|
+
fetch(url).then((r) => r.json()).then(
|
|
35
|
+
(data) => { cache.set(key, { state: data && data.ok === false ? 'error' : 'ok', data, error: data && data.error, at: Date.now() }); setState({}); },
|
|
36
|
+
(e) => { cache.set(key, { state: 'error', error: (e && e.message) || String(e), at: Date.now() }); setState({}); },
|
|
37
|
+
);
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
return hit;
|
|
41
|
+
}
|
|
42
|
+
const reload = (key) => cache.delete(key);
|
|
43
|
+
const post = (url, body) => fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
44
|
+
.then((r) => r.json()).catch((e) => ({ ok: false, error: (e && e.message) || String(e) }));
|
|
45
|
+
function status(r, what) {
|
|
46
|
+
if (r.state === 'loading' && !r.data) return el('div', { style: NOTE }, iv('reading ' + what + ' …'));
|
|
47
|
+
if (r.state === 'error') return el('div', { style: NOTE + ';color:var(--destructive)' }, iv(what + ' could not be read — ' + (r.error || 'error')));
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
async function openDocIn(ws, path, repo) {
|
|
51
|
+
const ini = await import('./panel-initiatives.js');
|
|
52
|
+
const tree = ws ? { ws, branch: null, linked: true } : null;
|
|
53
|
+
if (repo && repo !== state.product) {
|
|
54
|
+
const repos = await import('./panel-repos.js');
|
|
55
|
+
repos.selectRepo(repo);
|
|
56
|
+
}
|
|
57
|
+
ini.openDoc(null, path, 'FILE', tree);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── portfolio ──────────────────────────────────────────────────────────────────────────────── */
|
|
61
|
+
let reportNote = null;
|
|
62
|
+
async function writeReport(kind) {
|
|
63
|
+
let q = '';
|
|
64
|
+
if (kind === 'query') {
|
|
65
|
+
q = typeof window !== 'undefined' && window.prompt ? window.prompt('Report on initiatives matching:', state.q || '') : '';
|
|
66
|
+
if (!q || !q.trim()) return;
|
|
67
|
+
}
|
|
68
|
+
reportNote = { tone: 'mut', text: 'writing the ' + kind + ' report …' };
|
|
69
|
+
setState({});
|
|
70
|
+
const out = await post('/api/reports', { kind, q });
|
|
71
|
+
if (!out || out.ok === false) { reportNote = { tone: 'bad', text: (out && out.error) || 'the report was not written' }; setState({}); return; }
|
|
72
|
+
reportNote = { tone: 'ok', text: 'written to ' + out.path + (out.unread && out.unread.length ? ' · not fully read: ' + out.unread.join(', ') : '') };
|
|
73
|
+
const pf = cache.get('portfolio');
|
|
74
|
+
const hubName = pf && pf.data && pf.data.hub ? pf.data.hub.name : null;
|
|
75
|
+
await openDocIn(out.ws, out.path, hubName);
|
|
76
|
+
// selecting the hub to open the report switches to its initiatives; stay on the portfolio, where
|
|
77
|
+
// the PM pressed the button and where the note saying what was written is shown
|
|
78
|
+
reload('portfolio');
|
|
79
|
+
setState({ mode: 'portfolio', col1: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function land(row) {
|
|
83
|
+
const repos = await import('./panel-repos.js');
|
|
84
|
+
repos.selectRepo(row.repo);
|
|
85
|
+
setState({ sel: row.id, mode: 'product', detail: { kind: 'init' }, col2: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function portfolioList() {
|
|
89
|
+
const r = read('portfolio', '/api/portfolio');
|
|
90
|
+
const kids = [];
|
|
91
|
+
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r4': 'report-bar' },
|
|
92
|
+
el('span', { style: 'font-family:var(--mono);font-size:10px;color:var(--muted-foreground);align-self:center;margin-right:4px' }, 'report →'),
|
|
93
|
+
['weekly', 'okr', 'query'].map((k) => el('button', { onClick: () => writeReport(k), style: BTN, hover: 'color:var(--foreground)', 'data-r4': 'report-' + k }, iv(k === 'okr' ? 'OKR' : k))),
|
|
94
|
+
el('button', { onClick: () => { reload('portfolio'); setState({}); }, style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', title: 'read again' }, '↻')));
|
|
95
|
+
if (reportNote) kids.push(el('div', { 'data-r4': 'report-note', style: NOTE + ';color:' + (reportNote.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(reportNote.text)));
|
|
96
|
+
const st = status(r, 'the portfolio');
|
|
97
|
+
if (st) return [...kids, st];
|
|
98
|
+
const pf = r.data || { repos: [], rows: [] };
|
|
99
|
+
if (!pf.hub) kids.push(el('div', { style: NOTE }, iv('No hub yet — reports need a portfolio.yaml in your PM repository (D-3).')));
|
|
100
|
+
const q = String(state.q || '').trim().toLowerCase();
|
|
101
|
+
const rows = (pf.rows || []).filter((x) => !q || [x.id, x.title, x.group, x.repo, x.domain].some((v) => v && String(v).toLowerCase().includes(q)));
|
|
102
|
+
// coverage first: what was read, how fresh, and what was NOT read
|
|
103
|
+
kids.push(el('div', { style: LABEL }, iv('coverage · ' + (pf.repos || []).length + ' repositories')));
|
|
104
|
+
for (const rp of pf.repos || []) {
|
|
105
|
+
const s = rp.error ? 'NOT READ — ' + rp.error : rp.pending ? 'board pending — stages arriving' : rp.stale ? 'stale · as of ' + String(rp.as_of).slice(11, 16) : 'fresh · as of ' + String(rp.as_of || '').slice(11, 16);
|
|
106
|
+
kids.push(el('div', { 'data-r4': 'coverage', style: 'padding:2px 14px;font-family:var(--mono);font-size:10.5px;color:' + (rp.error ? 'var(--destructive)' : 'var(--muted-foreground)') },
|
|
107
|
+
iv(rp.name + (rp.domain ? ' (' + rp.domain + ')' : '') + ' · ' + rp.count + ' · ' + s)));
|
|
108
|
+
}
|
|
109
|
+
// then the initiatives, domain › repository
|
|
110
|
+
const byDomain = new Map();
|
|
111
|
+
for (const x of rows) {
|
|
112
|
+
const d = x.domain || 'no domain declared';
|
|
113
|
+
if (!byDomain.has(d)) byDomain.set(d, new Map());
|
|
114
|
+
const m = byDomain.get(d);
|
|
115
|
+
if (!m.has(x.repo)) m.set(x.repo, []);
|
|
116
|
+
m.get(x.repo).push(x);
|
|
117
|
+
}
|
|
118
|
+
for (const [dom, repos] of [...byDomain.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
119
|
+
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r4': 'domain' }, iv('domain · ' + dom)));
|
|
120
|
+
for (const [repo, list] of repos) {
|
|
121
|
+
kids.push(el('div', { style: LABEL + ';padding-top:4px' }, iv(repo + ' · ' + list.length)));
|
|
122
|
+
for (const x of list) {
|
|
123
|
+
const stage = x.accepted ? 'accepted' : x.stage || 'not derived';
|
|
124
|
+
kids.push(el('button', { onClick: () => land(x), style: ROW, hover: 'background:var(--accent)', 'data-r4': 'row', 'data-id': x.id, 'data-repo': x.repo },
|
|
125
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
126
|
+
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv('#' + x.id)),
|
|
127
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:' + (x.blocked ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(stage + (x.blocked ? ' ✗' : '') + (x.sessions ? ' · ● ' + x.sessions : '')))),
|
|
128
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(x.group + (x.next ? ' — next: ' + x.next : '')))));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!rows.length) kids.push(el('div', { style: NOTE }, iv(q ? 'no initiative matches "' + q + '"' : 'no initiatives in the repositories that were read')));
|
|
133
|
+
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── D71 — set up the recommended schedules for the selected repository, on a press ───────────── */
|
|
137
|
+
let seedNote = null;
|
|
138
|
+
function seedRow(set, label, hint) {
|
|
139
|
+
const product = state.product;
|
|
140
|
+
const note = seedNote && seedNote.set === set ? seedNote.text : null;
|
|
141
|
+
return el('div', { 'data-r7': 'seed-' + set, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
142
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', title: hint, onClick: async () => {
|
|
143
|
+
const data = await import('./data.js');
|
|
144
|
+
const ws = product ? data.wsOf(product) : null;
|
|
145
|
+
if (!ws) { seedNote = { set, text: 'select a repository first' }; setState({}); return; }
|
|
146
|
+
const out = await post('/api/schedules/seed', { ws, set });
|
|
147
|
+
seedNote = { set, text: !out || out.ok === false ? ((out && out.error) || 'not set up')
|
|
148
|
+
: out.created.length ? 'scheduled: ' + out.created.map((c) => c.name + ' (' + c.cadence + ' ' + c.time + ')').join(', ')
|
|
149
|
+
: 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ') };
|
|
150
|
+
reload('schedules'); setState({});
|
|
151
|
+
} }, label),
|
|
152
|
+
note ? el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(note)) : null);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── R11 — schedule ONE special report: what it is for, a date and a time ─────────────────────── */
|
|
156
|
+
let specialNote = null;
|
|
157
|
+
function specialRow() {
|
|
158
|
+
return el('div', { 'data-r11': 'special', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
159
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'a one-off report: say what it is for and when you want the draft; it runs once',
|
|
160
|
+
onClick: async () => {
|
|
161
|
+
const product = state.product;
|
|
162
|
+
const d = await import('./data.js');
|
|
163
|
+
const ws = product ? d.wsOf(product) : null;
|
|
164
|
+
if (!ws) { specialNote = 'select a repository first'; setState({}); return; }
|
|
165
|
+
const what = typeof window !== 'undefined' && window.prompt ? window.prompt('What is the special report for? Who reads it, what period or topic, roughly how long?') : null;
|
|
166
|
+
if (!what || !what.trim()) return;
|
|
167
|
+
const dflt = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
|
168
|
+
const date = window.prompt('Which day should the draft be ready? (YYYY-MM-DD)', dflt);
|
|
169
|
+
if (!date) return;
|
|
170
|
+
const time = window.prompt('At what time? (HH:MM, this machine\'s clock)', '07:30');
|
|
171
|
+
if (!time) return;
|
|
172
|
+
const out = await post('/api/schedules', { ws, name: 'Special report — ' + what.trim().slice(0, 40), cadence: 'once', date: date.trim(), time: time.trim(), skill: 'report-special', task: 'report-special', args: what.trim() });
|
|
173
|
+
specialNote = out && out.ok ? 'scheduled once for ' + date.trim() + ' at ' + time.trim() + ' — it fires only while the workspace is running' : ((out && out.error) || 'not scheduled');
|
|
174
|
+
reload('schedules'); setState({});
|
|
175
|
+
} }, 'schedule a special report'),
|
|
176
|
+
specialNote ? el('span', { 'data-r11': 'special-note', style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(specialNote)) : null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ── scheduled ──────────────────────────────────────────────────────────────────────────────── */
|
|
180
|
+
export function scheduledLive() {
|
|
181
|
+
const r = read('schedules', '/api/schedules?all=1');
|
|
182
|
+
const st = status(r, 'the schedules');
|
|
183
|
+
if (st) return [st];
|
|
184
|
+
const list = (r.data && r.data.schedules) || [];
|
|
185
|
+
const kids = [seedRow('requirements', 'set up daily requirement gathering', 'schedules the gather-requirements skill daily at 07:30 for the selected repository; notes are filed in that repository'),
|
|
186
|
+
seedRow('reports', 'set up report drafts (daily · weekly · bi-weekly · monthly)', 'schedules the four report skills for the selected repository; each follows your own definition and files a draft in that repository. A special report is run on demand.'),
|
|
187
|
+
seedRow('skills', 'set up weekly skill discovery', 'schedules the skill-discover skill weekly (Wednesday 09:00) for the selected repository; it files candidates for you to read and never approves them'),
|
|
188
|
+
seedRow('memory', 'set up weekly memory grooming', 'schedules the memory-groom skill weekly (Monday 08:00) for the selected repository; it proposes changes and never makes them'),
|
|
189
|
+
specialRow()];
|
|
190
|
+
for (const s of list) {
|
|
191
|
+
const name = s.label || s.name || s.task || s.skill || s.id;
|
|
192
|
+
const cadence = (s.cadence || '') + (s.day ? ' ' + s.day : '') + (s.time ? ' at ' + s.time : '');
|
|
193
|
+
kids.push(el('div', { style: ROW, 'data-r4': 'schedule' },
|
|
194
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
195
|
+
el('span', { style: 'font-size:12.5px' }, iv(name)),
|
|
196
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' }, iv(s.enabled === false ? 'disabled' : cadence))),
|
|
197
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' },
|
|
198
|
+
iv('next ' + (s.next ? String(s.next).slice(0, 16).replace('T', ' ') : '—') + ' · last ' + (s.lastRun ? String(s.lastRun).slice(0, 16).replace('T', ' ') : 'never'))),
|
|
199
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
200
|
+
el('button', { onClick: async () => { await post('/api/schedules/run', { id: s.id }); reload('schedules'); setState({}); }, style: BTN, hover: 'color:var(--foreground)', 'data-r4': 'run' }, 'run now'),
|
|
201
|
+
el('button', { onClick: async () => {
|
|
202
|
+
if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete the schedule "' + name + '"?')) return;
|
|
203
|
+
await post('/api/schedules/delete', { id: s.id }); reload('schedules'); setState({});
|
|
204
|
+
}, style: BTN, hover: 'color:var(--destructive)', 'data-r4': 'delete' }, 'delete'))));
|
|
205
|
+
}
|
|
206
|
+
if (!list.length) kids.push(el('div', { style: NOTE }, iv('No scheduled tasks — a successful read of nothing.')));
|
|
207
|
+
return [el('div', { style: 'padding:6px 0' }, kids)];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* ── memory (D72) — the repository's curated memory, for real ─────────────────────────────────── */
|
|
211
|
+
let memNote = null;
|
|
212
|
+
async function memWs() { const d = await import('./data.js'); return state.product ? d.wsOf(state.product) : null; }
|
|
213
|
+
async function memDo(body) {
|
|
214
|
+
const ws = await memWs();
|
|
215
|
+
if (!ws) { memNote = 'select a repository first'; setState({}); return; }
|
|
216
|
+
const out = await post('/api/memory', { ws, ...body });
|
|
217
|
+
memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
|
|
218
|
+
if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
|
|
219
|
+
reload('memory'); setState({});
|
|
220
|
+
}
|
|
221
|
+
const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
|
|
222
|
+
let dataMod = null;
|
|
223
|
+
import('./data.js').then((m) => { dataMod = m; setState({}); }).catch(() => { dataMod = null; });
|
|
224
|
+
export function memoryLive() {
|
|
225
|
+
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
226
|
+
const ws = wsId || 'none';
|
|
227
|
+
if (!wsId) return [el('div', { style: NOTE }, iv(state.product ? 'reading the repository …' : 'select a repository to see its memory'))];
|
|
228
|
+
const r = read('memory:' + ws, '/api/memory?ws=' + encodeURIComponent(wsId));
|
|
229
|
+
const st = status(r, 'the memory');
|
|
230
|
+
const kids = [];
|
|
231
|
+
const d = r.data || {};
|
|
232
|
+
const actions = el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r9': 'memory-bar' },
|
|
233
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'add', onClick: () => { const t = ask('Add to repository memory (one short fact):'); if (t && t.trim()) memDo({ op: 'add', target: 'memory', text: t.trim() }); } }, '+ memory'),
|
|
234
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'add-user', onClick: () => { const t = ask('Add about you (a preference or how you work):'); if (t && t.trim()) memDo({ op: 'add', target: 'user', text: t.trim() }); } }, '+ about me'),
|
|
235
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'wire', title: 'add a block to this repository\'s CLAUDE.md so each session reads the memory at start',
|
|
236
|
+
onClick: async () => { const w = await memWs(); const out = w ? await post('/api/memory/wire', { ws: w }) : null; memNote = out && out.ok ? (out.changed ? 'CLAUDE.md now imports the memory' : 'already wired') : ((out && out.error) || 'not wired'); reload('memory:' + ws); setState({}); } },
|
|
237
|
+
d.wired ? 'wired ✓' : 'wire into CLAUDE.md'),
|
|
238
|
+
el('button', { style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', 'data-r9': 'approval', title: 'when on, writes by an agent wait here for your approval',
|
|
239
|
+
onClick: async () => { const w = await memWs(); if (!w) return; await post('/api/memory/config', { ws: w, write_approval: !(d.config && d.config.write_approval) }); reload('memory:' + ws); setState({}); } },
|
|
240
|
+
'approval ' + (d.config && d.config.write_approval ? 'on' : 'off')));
|
|
241
|
+
kids.push(actions);
|
|
242
|
+
if (memNote) kids.push(el('div', { 'data-r9': 'note', style: NOTE + ';color:var(--destructive)' }, iv(memNote)));
|
|
243
|
+
if (st) return [...kids, st];
|
|
244
|
+
for (const t of ['memory', 'user']) {
|
|
245
|
+
const u = (d.usage || {})[t] || { used: 0, limit: 0, percent: 0 };
|
|
246
|
+
kids.push(el('div', { style: LABEL, 'data-r9': 'usage-' + t }, iv((t === 'memory' ? 'repository memory' : 'about you') + ' · ' + u.percent + '% · ' + u.used + '/' + u.limit)));
|
|
247
|
+
const es = (d.entries || []).filter((e) => e.target === t);
|
|
248
|
+
if (!es.length) kids.push(el('div', { style: NOTE }, iv('Nothing recorded yet.')));
|
|
249
|
+
for (const e of es) {
|
|
250
|
+
kids.push(el('div', { style: ROW, 'data-r9': 'entry', 'data-id': e.id },
|
|
251
|
+
el('span', { style: 'display:block;font-size:12px;line-height:1.45' }, iv(e.text)),
|
|
252
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground);margin-top:2px' },
|
|
253
|
+
iv([e.confidence, e.stale ? 'stale' : null, e.source ? 'source ' + e.source : null, e.by, String(e.at).slice(0, 10)].filter(Boolean).join(' · '))),
|
|
254
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
255
|
+
e.confidence === 'unconfirmed' ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'confirm', onClick: () => memDo({ op: 'confirm', target: t, old: e.text }) }, 'confirm') : null,
|
|
256
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'edit', onClick: () => { const nt = ask('Edit this entry:', e.text); if (nt && nt.trim() && nt.trim() !== e.text) memDo({ op: 'replace', target: t, old: e.text, text: nt.trim() }); } }, 'edit'),
|
|
257
|
+
el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r9': 'remove', onClick: () => { if (window.confirm && !window.confirm('Remove this entry?')) return; memDo({ op: 'remove', target: t, old: e.text }); } }, 'remove'))));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if ((d.pending || []).length) {
|
|
261
|
+
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r9': 'pending-label' }, iv('waiting for your approval · ' + d.pending.length)));
|
|
262
|
+
for (const p of d.pending) {
|
|
263
|
+
kids.push(el('div', { style: ROW, 'data-r9': 'pending', 'data-eid': p.eid },
|
|
264
|
+
el('span', { style: 'display:block;font-size:12px' }, iv(p.op + ' · ' + (p.text || p.id))),
|
|
265
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(p.by + ' · ' + String(p.at).slice(0, 16).replace('T', ' '))),
|
|
266
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
267
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'approve', onClick: () => memDo({ op: 'approve', ref: p.eid }) }, 'approve'),
|
|
268
|
+
el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r9': 'reject', onClick: () => memDo({ op: 'reject', ref: p.eid }) }, 'reject'))));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if ((d.blocked || []).length) {
|
|
272
|
+
kids.push(el('div', { style: LABEL + ';color:var(--destructive)', 'data-r9': 'blocked-label' }, iv('blocked by the scan · ' + d.blocked.length)));
|
|
273
|
+
for (const b of d.blocked) kids.push(el('div', { style: NOTE, 'data-r9': 'blocked' }, iv(b.reason + ' · ' + b.by + ' · ' + String(b.at).slice(0, 10) + ' · not shown to any session')));
|
|
274
|
+
}
|
|
275
|
+
kids.push(el('div', { style: NOTE }, iv('Sessions read this once, when they start. Unconfirmed items are included and labelled. It travels with the repository when you Sync.')));
|
|
276
|
+
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── skills ─────────────────────────────────────────────────────────────────────────────────── */
|
|
280
|
+
let candNote = null; const openText = new Map();
|
|
281
|
+
async function candDo(ws, body) {
|
|
282
|
+
const out = await post('/api/skills/decide', { ws, ...body });
|
|
283
|
+
candNote = out && out.ok ? (out.note || null) : ((out && out.error) || 'not done');
|
|
284
|
+
if (out && out.ok && body.op === 'read') openText.set(body.id + '@' + body.sha, out);
|
|
285
|
+
reload('skillcands:' + ws); reload('skills:' + ws); setState({});
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
function candidatesBlock(wsId) {
|
|
289
|
+
if (!wsId) return [];
|
|
290
|
+
const r = read('skillcands:' + wsId, '/api/skills/candidates?ws=' + encodeURIComponent(wsId));
|
|
291
|
+
const d = r.data || {};
|
|
292
|
+
const rows = d.candidates || [];
|
|
293
|
+
const kids = [el('div', { style: LABEL + ';color:var(--foreground)', 'data-r13': 'cands-label' }, iv('found or generated · ' + rows.length + ' · none runs until you approve it'))];
|
|
294
|
+
if (candNote) kids.push(el('div', { 'data-r13': 'cand-note', style: NOTE }, iv(candNote)));
|
|
295
|
+
if (r.state === 'error') kids.push(el('div', { style: NOTE + ';color:var(--destructive)' }, iv('the candidates could not be read — ' + (r.error || ''))));
|
|
296
|
+
if (!rows.length && r.state !== 'error') kids.push(el('div', { style: NOTE }, iv('No candidates yet. Set up weekly discovery, or ask for skills to be generated from your repeated work.')));
|
|
297
|
+
for (const c of rows) {
|
|
298
|
+
const key = c.id + '@' + c.sha; const shown = openText.get(key);
|
|
299
|
+
const soft = (c.scan && c.scan.soft) || [];
|
|
300
|
+
kids.push(el('div', { style: ROW, 'data-r13': 'cand', 'data-id': c.id, 'data-state': c.state + (c.running ? ':running' : '') },
|
|
301
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
302
|
+
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(c.id)),
|
|
303
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.running ? 'approved · running' : c.state + (c.approved ? ' (approved here)' : '')))),
|
|
304
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(c.why)),
|
|
305
|
+
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' },
|
|
306
|
+
iv([c.origin, c.license, c.author, c.source_url, '#' + c.sha12].filter(Boolean).join(' · '))),
|
|
307
|
+
soft.length ? el('span', { 'data-r13': 'findings', style: 'display:block;font-size:10.5px;color:var(--destructive)' }, iv('flagged: ' + soft.map((f) => f.rule + ' (line ' + f.line + ')').join(', '))) : null,
|
|
308
|
+
(c.ignored_keys || []).length ? el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground)' }, iv('its own ' + c.ignored_keys.join(', ') + ' will be ignored — it runs as an agent skill on claude')) : null,
|
|
309
|
+
c.collides ? el('span', { style: 'display:block;font-size:10.5px;color:var(--destructive)' }, iv('a skill with this name already exists and wins over it')) : null,
|
|
310
|
+
el('span', { style: 'display:flex;gap:4px;margin-top:4px;flex-wrap:wrap' },
|
|
311
|
+
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'read', onClick: () => { if (shown) { openText.delete(key); setState({}); } else candDo(wsId, { op: 'read', id: c.id, sha: c.sha }); } }, shown ? 'hide the text' : 'read it'),
|
|
312
|
+
c.state === 'quarantined' && !c.running ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'approve', disabled: c.read ? null : 'disabled', title: c.read ? 'approve exactly this text, on this machine' : 'open the full text first',
|
|
313
|
+
onClick: async () => {
|
|
314
|
+
let ack = false;
|
|
315
|
+
if (c.needs_license_ack) { ack = !!(window.confirm && window.confirm('Its licence is "' + c.license + '". Have you read it, and do you accept it?')); if (!ack) return; }
|
|
316
|
+
await candDo(wsId, { op: 'approve', id: c.id, sha: c.sha, ack_license: ack });
|
|
317
|
+
} }, 'approve') : null,
|
|
318
|
+
c.state === 'quarantined' ? el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r13': 'reject', onClick: () => { const why = window.prompt ? window.prompt('Why reject “' + c.id + '”? (optional)', '') : ''; if (why === null) return; candDo(wsId, { op: 'reject', id: c.id, sha: c.sha, reason: why }); } }, 'reject') : null,
|
|
319
|
+
(c.running || c.approved) && c.state === 'quarantined' ? el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r13': 'retire', onClick: () => candDo(wsId, { op: 'retire', id: c.id, sha: c.sha, reason: 'retired by the PM' }) }, 'retire') : null),
|
|
320
|
+
shown ? el('pre', { 'data-r13': 'text', style: 'white-space:pre-wrap;overflow-wrap:anywhere;font-family:var(--mono);font-size:11px;line-height:1.5;background:var(--secondary);border:1px solid var(--border);padding:8px;margin:6px 0 0;max-height:320px;overflow:auto' }, iv(shown.text)) : null));
|
|
321
|
+
}
|
|
322
|
+
return kids;
|
|
323
|
+
}
|
|
324
|
+
export function skillsLive() {
|
|
325
|
+
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
326
|
+
const r = read('skills:' + (wsId || ''), '/api/skills' + (wsId ? '?ws=' + encodeURIComponent(wsId) : ''));
|
|
327
|
+
const st = status(r, 'the skills');
|
|
328
|
+
if (st) return [st];
|
|
329
|
+
const list = (r.data && r.data.skills) || [];
|
|
330
|
+
const kids = list.map((sk) => el('div', { style: ROW, 'data-r4': 'skill' },
|
|
331
|
+
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
332
|
+
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(sk.label || sk.skill || sk.id)),
|
|
333
|
+
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(sk.source))),
|
|
334
|
+
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(sk.description))));
|
|
335
|
+
if (!list.length) kids.push(el('div', { style: NOTE }, iv('No skills — a successful read of nothing.')));
|
|
336
|
+
return [el('div', { style: 'padding:6px 0' }, [...kids, ...candidatesBlock(wsId)])];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
|
|
340
|
+
function flatten(node, out, depth) {
|
|
341
|
+
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth });
|
|
342
|
+
if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
export function knowledgeLive() {
|
|
346
|
+
const ws = state.wsId || '';
|
|
347
|
+
const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
|
|
348
|
+
const st = status(r, 'the knowledge');
|
|
349
|
+
if (st) return [st];
|
|
350
|
+
const trees = (r.data && r.data.knowledge) || [];
|
|
351
|
+
const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
|
|
352
|
+
for (const t of trees) {
|
|
353
|
+
kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
|
|
354
|
+
for (const it of flatten(t, [], 0)) {
|
|
355
|
+
if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
|
|
356
|
+
kids.push(el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;width:100%;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11.5px', hover: 'background:var(--accent)', 'data-r4': 'know' }, iv(it.name)));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
|
|
360
|
+
const notes = (r.data && r.data.notes) || [];
|
|
361
|
+
if (notes.length) kids.push(el('div', { style: NOTE, 'data-r11': 'notes-intro' }, iv('Notes PMOS and your agents wrote in this repository — requirement notes, report drafts and definitions, memory.')));
|
|
362
|
+
for (const t of notes) {
|
|
363
|
+
kids.push(el('div', { style: LABEL, 'data-r11': 'notes-label' }, iv(t.path || t.name || 'notes')));
|
|
364
|
+
for (const it of flatten(t, [], 0)) {
|
|
365
|
+
if (it.dir) { kids.push(el('div', { style: 'padding:3px 14px 1px ' + (14 + it.depth * 12) + 'px;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(it.dir + '/'))); continue; }
|
|
366
|
+
kids.push(el('button', { onClick: () => openDocIn(null, it.path), 'data-r11': 'note', style: 'display:block;width:100%;text-align:left;padding:4px 14px 4px ' + (20 + it.depth * 12) + 'px;font-family:var(--mono);font-size:11px;min-height:26px' }, iv(it.name || it.path)));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!trees.length && !notes.length) kids.push(el('div', { style: NOTE }, iv('No knowledge bundle in this repository — a successful read of nothing.')));
|
|
370
|
+
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
371
|
+
}
|
|
@@ -48,6 +48,7 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
48
48
|
import * as FX from './fixture.js';
|
|
49
49
|
import { openDoc, openTerm } from './panel-initiatives.js';
|
|
50
50
|
import { boardOf, records, currentProduct } from './board-read.js';
|
|
51
|
+
import * as LIVE from './modes-live.js'; // overhaul-r4: server-backed renderers, live mode only
|
|
51
52
|
|
|
52
53
|
/* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
|
|
53
54
|
|
|
@@ -1037,13 +1038,15 @@ function intgBody(det) {
|
|
|
1037
1038
|
|
|
1038
1039
|
const LISTS = {
|
|
1039
1040
|
focus: focusList,
|
|
1040
|
-
|
|
1041
|
+
// overhaul-r4: in live mode these three render their server routes; the fixture keeps the prototype's
|
|
1042
|
+
scheduled: () => (FIXTURE ? scheduledList() : LIVE.scheduledLive()),
|
|
1041
1043
|
workflow: workflowList,
|
|
1042
|
-
skills: skillsList,
|
|
1043
|
-
knowledge: knowledgeList,
|
|
1044
|
+
skills: () => (FIXTURE ? skillsList() : LIVE.skillsLive()),
|
|
1045
|
+
knowledge: () => (FIXTURE ? knowledgeList() : LIVE.knowledgeLive()),
|
|
1046
|
+
portfolio: () => (FIXTURE ? [] : LIVE.portfolioList()),
|
|
1044
1047
|
agent: agentsList,
|
|
1045
1048
|
connect: connectList,
|
|
1046
|
-
memory: memoryList,
|
|
1049
|
+
memory: () => (FIXTURE ? memoryList() : LIVE.memoryLive()),
|
|
1047
1050
|
};
|
|
1048
1051
|
|
|
1049
1052
|
const BODIES = {
|
|
@@ -54,6 +54,7 @@ import { state, setState, FIXTURE } from './state.js';
|
|
|
54
54
|
import * as FX from './fixture.js';
|
|
55
55
|
import { openDoc, openTab, openTerm, fileNewModel } from './panel-initiatives.js';
|
|
56
56
|
import { addProductBody } from './panel-repos.js';
|
|
57
|
+
import { lifecycleRow } from './lifecycle.js';
|
|
57
58
|
import { boardOf, records, currentProduct } from './board-read.js';
|
|
58
59
|
|
|
59
60
|
/* ── the prototype's small helpers ─────────────────────────────────────────────────────────── */
|
|
@@ -271,7 +272,24 @@ function sessionsBlock(slug) {
|
|
|
271
272
|
|
|
272
273
|
/* ── 1 · HEADER — slug, tag, pills (lines 338–346) ──────────────────────────────────────────── */
|
|
273
274
|
|
|
274
|
-
|
|
275
|
+
/* overhaul-r2 B3 — the breadcrumb, under the pills. Gaps are drawn muted and italic and SAY what is
|
|
276
|
+
* missing; they are never an empty slot. The KR's baseline → target line and a domain's standing
|
|
277
|
+
* goal sit beneath it, because "which metric does this move?" is the question the crumb answers. */
|
|
278
|
+
function crumbBlock(c) {
|
|
279
|
+
if (!c || !c.parts || !c.parts.length) return null;
|
|
280
|
+
const kids = [];
|
|
281
|
+
c.parts.forEach((pt, i) => {
|
|
282
|
+
if (i) kids.push(el('span', { style: 'color:var(--muted-foreground)' }, ' › '));
|
|
283
|
+
kids.push(el('span', { title: pt.k, style: pt.gap ? 'font-style:italic;color:var(--muted-foreground)' : 'color:var(--foreground)' }, iv(pt.v)));
|
|
284
|
+
});
|
|
285
|
+
return el('div', { 'data-r2': 'crumb', style: 'margin-top:8px;font-size:11.5px;line-height:1.5' },
|
|
286
|
+
el('div', null, kids),
|
|
287
|
+
c.line ? el('div', { 'data-r2': 'kr-line', style: 'font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground);margin-top:3px' }, iv('measure · ' + c.line)) : null,
|
|
288
|
+
c.goal ? el('div', { 'data-r2': 'domain-goal', style: 'font-size:11px;color:var(--muted-foreground);margin-top:3px' }, iv('standing goal · ' + c.goal)) : null,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function headerBlock(name, tag, pills, crumb) {
|
|
275
293
|
return el('div', { style: 'padding:13px 14px 11px;border-bottom:1px solid var(--border)' },
|
|
276
294
|
el('div', { style: 'font-family:var(--mono);font-size:15px;font-weight:600;letter-spacing:-.01em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, iv(name)),
|
|
277
295
|
el('div', { style: 'font-size:12px;color:var(--muted-foreground);margin-top:2px' }, iv(tag)),
|
|
@@ -279,6 +297,7 @@ function headerBlock(name, tag, pills) {
|
|
|
279
297
|
pills.map((pl) => el('span', {
|
|
280
298
|
style: 'font-family:var(--mono);font-size:10px;padding:2px 6px;background:var(--secondary);border:1px solid var(--border);color:' + pl.c,
|
|
281
299
|
}, iv(pl.t)))),
|
|
300
|
+
crumbBlock(crumb),
|
|
282
301
|
);
|
|
283
302
|
}
|
|
284
303
|
|
|
@@ -982,7 +1001,8 @@ function initBody(slug) {
|
|
|
982
1001
|
return out;
|
|
983
1002
|
}
|
|
984
1003
|
|
|
985
|
-
out.push(headerBlock(slug, rec.tag || '', rec.pills || []));
|
|
1004
|
+
out.push(headerBlock(slug, rec.tag || '', rec.pills || [], rec.crumb || null));
|
|
1005
|
+
{ const lc = lifecycleRow(repoOf(slug), slug); if (lc) out.push(lc); }
|
|
986
1006
|
|
|
987
1007
|
if (rec.noLadder) {
|
|
988
1008
|
const ansC = FX.TONE[(rec.answer && rec.answer.tone) || 'info'] || FX.C.mut;
|
|
@@ -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
|
}
|