create-pmos 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- package/payload/mods/pmos-cockpit/README.md +13 -0
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +2 -1
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +41 -11
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +228 -8
- package/payload/mods/pmos-cockpit/hooks/register.tsx +11 -3
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/plan-items-stage.js +271 -0
- package/payload/scripts/pmos-log.sh +18 -7
- package/payload/scripts/workflow-state.js +28 -2
- package/payload/state/discoveries.md +1 -1
- package/payload/state/log.md +1 -1
- package/workspace/bin/pmos.js +15 -1
- package/workspace/cut-stub.js +40 -0
- package/workspace/first-paint-audit.js +145 -0
- package/workspace/fixtures/capture/requirements/capture-2026-10-04-1.md +10 -0
- package/workspace/fixtures/initiatives-api/list.json +176 -0
- package/workspace/fixtures/initiatives-api/post-403.json +5 -0
- package/workspace/fixtures/initiatives-api/post-ok.json +10 -0
- package/workspace/fixtures/initiatives-api/roadmap.json +208 -0
- package/workspace/fixtures/initiatives-api/sheet.json +112 -0
- package/workspace/fixtures/report-api/draft.json +177 -0
- package/workspace/fixtures/reports-page/draft-biweekly-2026-10-16.md +97 -0
- package/workspace/fixtures/today-api/provider-rows.json +204 -0
- package/workspace/fixtures/today-api/seam-probe-kinds.json +5 -0
- package/workspace/guide/board.md +81 -0
- package/workspace/guide/breakdown.md +154 -0
- package/workspace/guide/command-palette.md +75 -0
- package/workspace/guide/index.md +27 -17
- package/workspace/guide/initiatives-roadmap.md +130 -0
- package/workspace/guide/initiatives-screen.md +89 -0
- package/workspace/guide/initiatives-timeline.md +57 -0
- package/workspace/guide/integrated-work.md +94 -0
- package/workspace/guide/knowledge-graph.md +3 -68
- package/workspace/guide/layout.md +98 -32
- package/workspace/guide/live-notes.md +3 -3
- package/workspace/guide/pm-only-acts.md +89 -0
- package/workspace/guide/pm-only-lines.md +61 -0
- package/workspace/guide/reports-page.md +80 -0
- package/workspace/guide/reports.md +73 -0
- package/workspace/guide/timeline.md +70 -0
- package/workspace/guide/today.md +85 -0
- package/workspace/guide/validate-with-typesafe.md +1 -1
- package/workspace/lib/actor.js +91 -0
- package/workspace/lib/board-cache.js +32 -2
- package/workspace/lib/board-cli.js +108 -0
- package/workspace/lib/board-fold.js +232 -0
- package/workspace/lib/breakdown-export.js +80 -0
- package/workspace/lib/capture.js +98 -0
- package/workspace/lib/engine/plan-items-stage.js +271 -0
- package/workspace/lib/engine/workflow-state.js +28 -2
- package/workspace/lib/factory-join.js +158 -0
- package/workspace/lib/grant-mint.js +113 -0
- package/workspace/lib/health-rules-factory.js +92 -0
- package/workspace/lib/health-rules-plan.js +164 -0
- package/workspace/lib/health.js +79 -0
- package/workspace/lib/initiative-cli.js +222 -0
- package/workspace/lib/initiative-display.js +183 -0
- package/workspace/lib/initiative-needs.js +127 -0
- package/workspace/lib/initiative-rows.js +206 -0
- package/workspace/lib/initiative-writer.js +141 -0
- package/workspace/lib/issues.js +206 -0
- package/workspace/lib/item-cli.js +195 -0
- package/workspace/lib/item-spans.js +137 -0
- package/workspace/lib/measure-cli.js +100 -0
- package/workspace/lib/measurements.js +189 -0
- package/workspace/lib/mobile-nav.js +57 -53
- package/workspace/lib/needs-providers.js +120 -0
- package/workspace/lib/placements.js +440 -0
- package/workspace/lib/plan-items.js +922 -0
- package/workspace/lib/plan-ledgers.js +85 -0
- package/workspace/lib/plan-rules.js +72 -0
- package/workspace/lib/plan-tree.js +367 -0
- package/workspace/lib/pm-guard.js +174 -0
- package/workspace/lib/pm-lines-needs.js +331 -0
- package/workspace/lib/pmos-verbs.js +66 -0
- package/workspace/lib/proactive.js +7 -4
- package/workspace/lib/report-cli.js +152 -0
- package/workspace/lib/report-draft.js +579 -0
- package/workspace/lib/report-needs.js +213 -0
- package/workspace/lib/route-modules.js +128 -0
- package/workspace/lib/routes-board.js +91 -0
- package/workspace/lib/routes-capture.js +52 -0
- package/workspace/lib/routes-initiatives.js +371 -0
- package/workspace/lib/routes-reports.js +163 -0
- package/workspace/lib/routes-timeline.js +197 -0
- package/workspace/lib/routes-today.js +32 -0
- package/workspace/lib/today-cli.js +99 -0
- package/workspace/lib/today.js +333 -0
- package/workspace/lib/triage.js +163 -0
- package/workspace/lib/verify-work-cli.js +81 -0
- package/workspace/lib/work-check-needs.js +99 -0
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +4 -5
- package/workspace/public/index.html +29 -19
- package/workspace/public/pm/app.js +299 -30
- package/workspace/public/pm/bd-board.js +113 -0
- package/workspace/public/pm/bd-drag.js +59 -0
- package/workspace/public/pm/bd-model.js +152 -0
- package/workspace/public/pm/bd-timeline.js +233 -0
- package/workspace/public/pm/bd-tree.js +244 -0
- package/workspace/public/pm/board-copy.js +57 -0
- package/workspace/public/pm/breakdown-api.js +58 -0
- package/workspace/public/pm/breakdown-copy.js +138 -0
- package/workspace/public/pm/components.css +3054 -0
- package/workspace/public/pm/cycle-review.js +125 -0
- package/workspace/public/pm/data.js +40 -187
- package/workspace/public/pm/dest-board.js +438 -0
- package/workspace/public/pm/dest-initiatives.js +134 -0
- package/workspace/public/pm/dest-reports.js +270 -0
- package/workspace/public/pm/dest-settings.js +217 -0
- package/workspace/public/pm/dest-today.js +176 -0
- package/workspace/public/pm/dialogs.js +88 -0
- package/workspace/public/pm/display-word.js +56 -0
- package/workspace/public/pm/dock.js +105 -0
- package/workspace/public/pm/fixture.js +8 -227
- package/workspace/public/pm/frame-copy.js +94 -0
- package/workspace/public/pm/handoff-box.js +82 -0
- package/workspace/public/pm/http.js +88 -0
- package/workspace/public/pm/icons.js +53 -0
- package/workspace/public/pm/initiative-list.js +218 -0
- package/workspace/public/pm/initiative-sheet.js +261 -0
- package/workspace/public/pm/initiatives-api.js +59 -0
- package/workspace/public/pm/initiatives-copy.js +215 -0
- package/workspace/public/pm/initiatives-timeline-copy.js +59 -0
- package/workspace/public/pm/initiatives-timeline.js +410 -0
- package/workspace/public/pm/initiatives-ui.js +123 -0
- package/workspace/public/pm/item-sheet.js +251 -0
- package/workspace/public/pm/keep-card.js +204 -0
- package/workspace/public/pm/lifecycle.js +7 -1
- package/workspace/public/pm/mobile.css +49 -20
- package/workspace/public/pm/mobile.js +157 -29
- package/workspace/public/pm/modes-live.js +7 -883
- package/workspace/public/pm/modes.js +32 -1024
- package/workspace/public/pm/new-initiative-sheet.js +74 -0
- package/workspace/public/pm/next-step-ui.js +3 -2
- package/workspace/public/pm/palette-copy.js +51 -0
- package/workspace/public/pm/palette.js +312 -0
- package/workspace/public/pm/panel-breakdown.js +702 -0
- package/workspace/public/pm/panel-cockpit.js +63 -295
- package/workspace/public/pm/panel-detail.js +25 -172
- package/workspace/public/pm/panel-initiatives.js +1 -19
- package/workspace/public/pm/panel-repos.js +31 -81
- package/workspace/public/pm/panel-tabs.js +197 -110
- package/workspace/public/pm/panel-trees.js +5 -2
- package/workspace/public/pm/portfolio-report.js +61 -0
- package/workspace/public/pm/primitives.js +656 -0
- package/workspace/public/pm/print.css +33 -0
- package/workspace/public/pm/provider-cards.js +136 -0
- package/workspace/public/pm/registry.js +194 -0
- package/workspace/public/pm/release-card.js +110 -0
- package/workspace/public/pm/report-view.js +309 -0
- package/workspace/public/pm/reports-copy.js +114 -0
- package/workspace/public/pm/roadmap.js +271 -0
- package/workspace/public/pm/sig-watch.js +41 -0
- package/workspace/public/pm/state.js +8 -4
- package/workspace/public/pm/status-banner.js +13 -5
- package/workspace/public/pm/tab-doc.js +2 -4
- package/workspace/public/pm/tab-term.js +12 -2
- package/workspace/public/pm/tab-web.js +3 -2
- package/workspace/public/pm/timeline-copy.js +48 -0
- package/workspace/public/pm/today-cards.js +244 -0
- package/workspace/public/pm/today-copy.js +97 -0
- package/workspace/public/pm/tokens.css +16 -18
- package/workspace/public/pm/topbar.js +102 -0
- package/workspace/public/pm/triage-card.js +60 -0
- package/workspace/public/pm/undo-hold.js +103 -0
- package/workspace/public/pm/view-addrepo.js +2 -1
- package/workspace/public/pm/view-livenotes.js +3 -1
- package/workspace/public/pm/view-today.js +7 -162
- package/workspace/public/vendor/Sortable.min.js +2 -0
- package/workspace/public/vendor/lucide-sprite.js +79 -0
- package/workspace/replaced-tests.json +2864 -0
- package/workspace/run-tests.js +56 -5
- package/workspace/server.js +312 -36
- package/workspace/sheet-answer.js +61 -0
- package/workspace/skills/measure.skill +41 -0
- package/workspace/skills/report-biweekly.skill +23 -25
- package/workspace/skills/tracker-refresh.skill +50 -0
- package/workspace/test-files.json +19 -3
- package/workspace/test-lists/base.json +6 -0
- package/workspace/test-lists/run2-a.json +6 -0
- package/workspace/test-lists/run2-b.json +13 -0
- package/workspace/test-lists/run2-c.json +19 -0
- package/workspace/test-lists/run2-d.json +22 -0
- package/workspace/test-lists/run3-0.json +5 -0
- package/workspace/test-lists/run3-a.json +5 -0
- package/workspace/test-lists/run3-b.json +6 -0
- package/workspace/test-lists/run3-c.json +15 -0
- package/workspace/test-lists/run3-d.json +6 -0
- package/workspace/test-lists/run4-a.json +8 -0
- package/workspace/test-lists/run4-b.json +6 -0
- package/workspace/test-lists/run4-c.json +13 -0
- package/workspace/test-lists/run4-d.json +9 -0
- package/workspace/test-lists/run5-a.json +6 -0
- package/workspace/test-lists/run5-b.json +6 -0
- package/workspace/test-lists/run5-c.json +15 -0
- package/workspace/test-run.js +27 -1
- package/workspace/third-party/README.md +44 -0
- package/workspace/third-party/lucide-static-1.52.0.LICENSE +43 -0
- package/workspace/third-party/sortablejs-1.15.7.LICENSE +21 -0
- package/workspace/design-parity.js +0 -673
- package/workspace/public/pm/tab-task.js +0 -86
- package/workspace/public/pm/view-guide.js +0 -141
- package/workspace/public/pm/view-knowledge.js +0 -225
- package/workspace/public/pm/view-memory.js +0 -122
- package/workspace/public/pm/view-portfolio.js +0 -153
- package/workspace/public/pm/view-reports.js +0 -190
- package/workspace/public/pm/view-scheduled.js +0 -161
- package/workspace/public/pm/view-skills.js +0 -216
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
/* modes-live.js — the
|
|
1
|
+
/* modes-live.js — the live renderers that stay after the R29 cuts (overhaul-r4, trimmed in pm-os-absorb Run 3, slice 3D).
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
3
|
+
* validate — the Validate page's list: the repository's consent, Jev's plan and run, and the claims ledger. Reached
|
|
4
|
+
* from Settings.
|
|
5
|
+
* skill tab — mountSkillTab / skillDetailLive: one skill, read or edited in a dock tab (started from + Session).
|
|
6
|
+
* transcript — mountTranscriptTab: one recorded conversation, read-only.
|
|
13
7
|
*
|
|
8
|
+
* The Portfolio, Scheduled, Memory, Skills and Knowledge lists, the guide list and the morning brief are gone (R29).
|
|
14
9
|
* Every list states its read: loading, failed (with the reason), or a successful read of nothing.
|
|
15
10
|
* Fixture mode never reaches this module (modes.js routes here only when !FIXTURE).
|
|
16
11
|
*/
|
|
@@ -50,9 +45,6 @@ function read(key, url) {
|
|
|
50
45
|
return hit;
|
|
51
46
|
}
|
|
52
47
|
const reload = (key) => { gen.set(key, (gen.get(key) || 0) + 1); cache.delete(key); };
|
|
53
|
-
/* refetch on the next render but keep showing the last answer meanwhile, so a list does not blank while it updates.
|
|
54
|
-
* It follows a write too, so it bumps the generation like `reload`: a read already in flight is older than the write. */
|
|
55
|
-
const refresh = (key) => { gen.set(key, (gen.get(key) || 0) + 1); const h = cache.get(key); if (h && h.state === 'ok') h.at = 0; else cache.delete(key); };
|
|
56
48
|
/* A route this server does not have answers 404 with the plain text `not found`. Parsing that as JSON
|
|
57
49
|
* showed the PM "Unexpected token 'o', \"not found\" is not valid JSON" — true, and no help. The usual
|
|
58
50
|
* cause is a server still running code older than the page, so say that and the one command. */
|
|
@@ -82,342 +74,8 @@ async function openDocIn(ws, path, repo) {
|
|
|
82
74
|
ini.openDoc(null, path, 'FILE', tree);
|
|
83
75
|
}
|
|
84
76
|
|
|
85
|
-
/* ── portfolio ──────────────────────────────────────────────────────────────────────────────── */
|
|
86
|
-
let reportNote = null;
|
|
87
|
-
async function writeReport(kind) {
|
|
88
|
-
let q = '';
|
|
89
|
-
if (kind === 'query') {
|
|
90
|
-
q = typeof window !== 'undefined' && window.prompt ? window.prompt('Report on initiatives matching:', state.q || '') : '';
|
|
91
|
-
if (!q || !q.trim()) return;
|
|
92
|
-
}
|
|
93
|
-
reportNote = { tone: 'mut', text: 'writing the ' + kind + ' report …' };
|
|
94
|
-
setState({});
|
|
95
|
-
const out = await post('/api/reports', { kind, q });
|
|
96
|
-
if (!out || out.ok === false) { reportNote = { tone: 'bad', text: (out && out.error) || 'the report was not written' }; setState({}); return; }
|
|
97
|
-
reportNote = { tone: 'ok', text: 'written to ' + out.path + (out.unread && out.unread.length ? ' · not fully read: ' + out.unread.join(', ') : '') };
|
|
98
|
-
const pf = cache.get('portfolio');
|
|
99
|
-
const hubName = pf && pf.data && pf.data.hub ? pf.data.hub.name : null;
|
|
100
|
-
await openDocIn(out.ws, out.path, hubName);
|
|
101
|
-
// selecting the hub to open the report switches to its initiatives; stay on the portfolio, where
|
|
102
|
-
// the PM pressed the button and where the note saying what was written is shown
|
|
103
|
-
reload('portfolio');
|
|
104
|
-
setState({ mode: 'portfolio', col1: true });
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function land(row) {
|
|
108
|
-
const repos = await import('./panel-repos.js');
|
|
109
|
-
repos.selectRepo(row.repo);
|
|
110
|
-
setState({ sel: row.id, mode: 'product', detail: { kind: 'init' }, col2: true });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function portfolioList() {
|
|
114
|
-
const r = read('portfolio', '/api/portfolio');
|
|
115
|
-
const kids = [];
|
|
116
|
-
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r4': 'report-bar' },
|
|
117
|
-
el('span', { style: 'font-family:var(--mono);font-size:10px;color:var(--muted-foreground);align-self:center;margin-right:4px' }, 'report →'),
|
|
118
|
-
['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))),
|
|
119
|
-
el('button', { onClick: () => { reload('portfolio'); setState({}); }, style: BTN + ';margin-left:auto', hover: 'color:var(--foreground)', title: 'read again' }, '↻')));
|
|
120
|
-
if (reportNote) kids.push(el('div', { 'data-r4': 'report-note', style: NOTE + ';color:' + (reportNote.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(reportNote.text)));
|
|
121
|
-
const st = status(r, 'the portfolio');
|
|
122
|
-
if (st) return [...kids, st];
|
|
123
|
-
const pf = r.data || { repos: [], rows: [] };
|
|
124
|
-
if (!pf.hub) {
|
|
125
|
-
const here = state.product;
|
|
126
|
-
kids.push(el('div', { style: NOTE, 'data-r15': 'no-hub' }, iv('No hub yet — reports need a portfolio.yaml in a registered repository (D-3). A report is never written to a guessed place.')));
|
|
127
|
-
if (here) kids.push(el('div', { style: 'padding:0 14px 6px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r15': 'make-hub',
|
|
128
|
-
title: 'writes a starter portfolio.yaml in ' + here + ' listing your registered repositories; edit the domains afterwards',
|
|
129
|
-
onClick: async () => {
|
|
130
|
-
const w = dataMod ? dataMod.wsOf(here) : null;
|
|
131
|
-
const out = w ? await post('/api/portfolio/init', { ws: w }) : null;
|
|
132
|
-
reportNote = out && out.ok ? { tone: 'ok', text: 'wrote portfolio.yaml in ' + out.hub + ' — it is now the hub; edit the domains there' } : { tone: 'bad', text: (out && out.error) || 'the hub was not created' };
|
|
133
|
-
reload('portfolio'); setState({});
|
|
134
|
-
} }, 'make “' + here + '” the hub')));
|
|
135
|
-
}
|
|
136
|
-
const q = String(state.q || '').trim().toLowerCase();
|
|
137
|
-
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)));
|
|
138
|
-
// coverage first: what was read, how fresh, and what was NOT read
|
|
139
|
-
kids.push(el('div', { style: LABEL }, iv('coverage · ' + (pf.repos || []).length + ' repositories')));
|
|
140
|
-
for (const rp of pf.repos || []) {
|
|
141
|
-
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);
|
|
142
|
-
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)') },
|
|
143
|
-
iv(rp.name + (rp.domain ? ' (' + rp.domain + ')' : '') + ' · ' + rp.count + ' · ' + s)));
|
|
144
|
-
}
|
|
145
|
-
// then the initiatives, domain › repository
|
|
146
|
-
const byDomain = new Map();
|
|
147
|
-
for (const x of rows) {
|
|
148
|
-
const d = x.domain || 'no domain declared';
|
|
149
|
-
if (!byDomain.has(d)) byDomain.set(d, new Map());
|
|
150
|
-
const m = byDomain.get(d);
|
|
151
|
-
if (!m.has(x.repo)) m.set(x.repo, []);
|
|
152
|
-
m.get(x.repo).push(x);
|
|
153
|
-
}
|
|
154
|
-
for (const [dom, repos] of [...byDomain.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
155
|
-
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r4': 'domain' }, iv('domain · ' + dom)));
|
|
156
|
-
for (const [repo, list] of repos) {
|
|
157
|
-
kids.push(el('div', { style: LABEL + ';padding-top:4px' }, iv(repo + ' · ' + list.length)));
|
|
158
|
-
for (const x of list) {
|
|
159
|
-
const stage = x.accepted ? 'accepted' : x.stage || 'not derived';
|
|
160
|
-
kids.push(el('button', { onClick: () => land(x), style: ROW, hover: 'background:var(--accent)', 'data-r4': 'row', 'data-id': x.id, 'data-repo': x.repo },
|
|
161
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
162
|
-
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv('#' + x.id)),
|
|
163
|
-
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 : '')))),
|
|
164
|
-
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 : '')))));
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (!rows.length) kids.push(el('div', { style: NOTE }, iv(q ? 'no initiative matches "' + q + '"' : 'no initiatives in the repositories that were read')));
|
|
169
|
-
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/* ── the user guide (status-clarity R6): pages that ship with the app, opened read-only in a doc tab ───────────── */
|
|
173
|
-
async function openGuide(pg) {
|
|
174
|
-
const ini = await import('./panel-initiatives.js');
|
|
175
|
-
ini.openTab({ id: 'guide:' + pg.page, kind: 'doc', title: pg.title || pg.page, guide: true, readonly: true, path: pg.page, slug: null, repo: state.product || null, role: 'GUIDE' });
|
|
176
|
-
}
|
|
177
|
-
export function guideList() {
|
|
178
|
-
const r = read('guide', '/api/guide');
|
|
179
|
-
const st = status(r, 'the guide');
|
|
180
|
-
if (st) return [st];
|
|
181
|
-
const pages = (r.data && r.data.pages) || [];
|
|
182
|
-
if (!pages.length) return [el('div', { style: NOTE, 'data-r18': 'guide-empty' }, iv('The guide has no pages in this install.'))];
|
|
183
|
-
return [el('div', { style: 'padding:2px 0 8px', 'data-r18': 'guide-list' },
|
|
184
|
-
el('div', { style: NOTE }, iv('How this workspace works. These pages ship with the app and are updated with it.')),
|
|
185
|
-
pages.map((pg) => el('button', { onClick: () => openGuide(pg), style: ROW, hover: 'background:var(--accent)', 'data-r18': 'guide-page', 'data-page': pg.page },
|
|
186
|
-
el('span', { style: 'font-size:12.5px' }, iv(pg.title)))))];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/* ── D71 — set up the recommended schedules for the selected repository, on a press ───────────── */
|
|
190
|
-
let seedNote = null;
|
|
191
|
-
function seedRow(set, label, hint) {
|
|
192
|
-
const product = state.product;
|
|
193
|
-
const note = seedNote && seedNote.set === set ? seedNote.text : null;
|
|
194
|
-
return el('div', { 'data-r7': 'seed-' + set, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
195
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', title: hint, onClick: async () => {
|
|
196
|
-
const data = await import('./data.js');
|
|
197
|
-
const ws = product ? data.wsOf(product) : null;
|
|
198
|
-
if (!ws) { seedNote = { set, text: 'select a repository first' }; setState({}); return; }
|
|
199
|
-
const out = await post('/api/schedules/seed', { ws, set });
|
|
200
|
-
seedNote = { set, text: !out || out.ok === false ? ((out && out.error) || 'not set up')
|
|
201
|
-
: out.created.length ? 'scheduled: ' + out.created.map((c) => c.name + ' (' + c.cadence + ' ' + c.time + ')').join(', ')
|
|
202
|
-
: 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ') };
|
|
203
|
-
reload('schedules'); setState({});
|
|
204
|
-
} }, label),
|
|
205
|
-
note ? el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(note)) : null);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/* ── R11 — schedule ONE special report: what it is for, a date and a time ─────────────────────── */
|
|
209
|
-
let specialNote = null;
|
|
210
|
-
function specialRow() {
|
|
211
|
-
return el('div', { 'data-r11': 'special', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
212
|
-
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',
|
|
213
|
-
onClick: async () => {
|
|
214
|
-
const product = state.product;
|
|
215
|
-
const d = await import('./data.js');
|
|
216
|
-
const ws = product ? d.wsOf(product) : null;
|
|
217
|
-
if (!ws) { specialNote = 'select a repository first'; setState({}); return; }
|
|
218
|
-
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;
|
|
219
|
-
if (!what || !what.trim()) return;
|
|
220
|
-
const dflt = new Date(Date.now() + 86400000).toISOString().slice(0, 10);
|
|
221
|
-
const date = window.prompt('Which day should the draft be ready? (YYYY-MM-DD)', dflt);
|
|
222
|
-
if (!date) return;
|
|
223
|
-
const time = window.prompt('At what time? (HH:MM, this machine\'s clock)', '07:30');
|
|
224
|
-
if (!time) return;
|
|
225
|
-
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() });
|
|
226
|
-
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');
|
|
227
|
-
reload('schedules'); setState({});
|
|
228
|
-
} }, 'schedule a special report'),
|
|
229
|
-
specialNote ? el('span', { 'data-r11': 'special-note', style: 'display:block;font-size:11px;color:var(--muted-foreground);margin-top:4px' }, iv(specialNote)) : null);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/* Run a schedule now and SAY what happened. The handlers used to discard the answer, so a session that
|
|
233
|
-
* failed to start (no agent binary, a refused model, a missing schedule) looked like a button that did nothing. */
|
|
234
|
-
let runNote = null;
|
|
235
|
-
async function runSchedule(s, name) {
|
|
236
|
-
runNote = { tone: 'mut', text: 'starting ' + (name || 'the task') + ' …' };
|
|
237
|
-
setState({});
|
|
238
|
-
const out = await post('/api/schedules/run', { id: s.id });
|
|
239
|
-
const r = out && out.result;
|
|
240
|
-
if (!out || out.ok === false) runNote = { tone: 'bad', text: (out && out.error) || 'the workspace server did not answer — nothing was started' };
|
|
241
|
-
else if (r && r.ok === false) runNote = { tone: 'bad', text: 'did not start — ' + (r.reason || 'the server gave no reason') };
|
|
242
|
-
else if (r && r.sessionId) {
|
|
243
|
-
runNote = { tone: 'ok', text: (name || 'task') + ' started as a session — opened in the tabs' };
|
|
244
|
-
try { const ini = await import('./panel-initiatives.js'); ini.openTerm(null, { id: r.sessionId, name: name || 'scheduled run', st: 'working', profile: null }); } catch {}
|
|
245
|
-
} else runNote = { tone: 'ok', text: (name || 'task') + ' ran — the result is in needs-you' };
|
|
246
|
-
reload('schedules');
|
|
247
|
-
setState({});
|
|
248
|
-
}
|
|
249
|
-
const runNoteEl = () => (runNote ? el('div', { 'data-r4': 'run-note', style: NOTE + ';color:' + (runNote.tone === 'bad' ? 'var(--destructive)' : 'var(--muted-foreground)') }, iv(runNote.text)) : null);
|
|
250
|
-
|
|
251
|
-
/* ── scheduled ──────────────────────────────────────────────────────────────────────────────── */
|
|
252
|
-
export function scheduledLive() {
|
|
253
|
-
const r = read('schedules', '/api/schedules?all=1');
|
|
254
|
-
const st = status(r, 'the schedules');
|
|
255
|
-
if (st) return [st];
|
|
256
|
-
const list = (r.data && r.data.schedules) || [];
|
|
257
|
-
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'),
|
|
258
|
-
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.'),
|
|
259
|
-
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'),
|
|
260
|
-
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'),
|
|
261
|
-
specialRow(), runNoteEl()];
|
|
262
|
-
for (const s of list) {
|
|
263
|
-
const name = s.label || s.name || s.task || s.skill || s.id;
|
|
264
|
-
const cadence = (s.cadence || '') + (s.day ? ' ' + s.day : '') + (s.time ? ' at ' + s.time : '');
|
|
265
|
-
kids.push(el('div', { style: ROW, 'data-r4': 'schedule' },
|
|
266
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
267
|
-
el('span', { style: 'font-size:12.5px' }, iv(name)),
|
|
268
|
-
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' }, iv(s.enabled === false ? 'disabled' : cadence))),
|
|
269
|
-
el('span', { style: 'display:block;font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground)' },
|
|
270
|
-
iv('next ' + (s.next ? String(s.next).slice(0, 16).replace('T', ' ') : '—') + ' · last ' + (s.lastRun ? String(s.lastRun).slice(0, 16).replace('T', ' ') : 'never'))),
|
|
271
|
-
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
272
|
-
el('button', { onClick: () => runSchedule(s, name), style: BTN, hover: 'color:var(--foreground)', 'data-r4': 'run' }, 'run now'),
|
|
273
|
-
el('button', { onClick: async () => {
|
|
274
|
-
if (typeof window !== 'undefined' && window.confirm && !window.confirm('Delete the schedule "' + name + '"?')) return;
|
|
275
|
-
await post('/api/schedules/delete', { id: s.id }); reload('schedules'); setState({});
|
|
276
|
-
}, style: BTN, hover: 'color:var(--destructive)', 'data-r4': 'delete' }, 'delete'))));
|
|
277
|
-
}
|
|
278
|
-
if (!list.length) kids.push(el('div', { style: NOTE }, iv('No scheduled tasks — a successful read of nothing.')));
|
|
279
|
-
return [el('div', { style: 'padding:6px 0' }, kids)];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/* ── memory (D72) — the repository's curated memory, for real ─────────────────────────────────── */
|
|
283
|
-
let memNote = null;
|
|
284
|
-
async function memWs() { const d = await import('./data.js'); return state.product ? d.wsOf(state.product) : null; }
|
|
285
|
-
async function memDo(body) {
|
|
286
|
-
const ws = await memWs();
|
|
287
|
-
if (!ws) { memNote = 'select a repository first'; setState({}); return; }
|
|
288
|
-
const out = await post('/api/memory', { ws, ...body });
|
|
289
|
-
memNote = out && out.ok ? (out.staged ? 'held for approval' : null) : ((out && out.error) || 'not saved');
|
|
290
|
-
if (out && !out.ok && out.entries && out.entries.length) memNote += ' — current entries: ' + out.entries.map((e) => '“' + e.text.slice(0, 40) + '”').join(', ');
|
|
291
|
-
// the list caches its read per workspace, so the reload names that key; a bare 'memory' left the old list on screen for 15s
|
|
292
|
-
reload('memory:' + ws); reload('memory'); setState({});
|
|
293
|
-
}
|
|
294
|
-
const ask = (q, v) => (typeof window !== 'undefined' && window.prompt ? window.prompt(q, v || '') : null);
|
|
295
77
|
let dataMod = null;
|
|
296
78
|
import('./data.js').then((m) => { dataMod = m; setState({}); }).catch(() => { dataMod = null; });
|
|
297
|
-
export function memoryLive() {
|
|
298
|
-
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
299
|
-
const ws = wsId || 'none';
|
|
300
|
-
if (!wsId) return [el('div', { style: NOTE }, iv(state.product ? 'reading the repository …' : 'select a repository to see its memory'))];
|
|
301
|
-
const r = read('memory:' + ws, '/api/memory?ws=' + encodeURIComponent(wsId));
|
|
302
|
-
const st = status(r, 'the memory');
|
|
303
|
-
const kids = [];
|
|
304
|
-
const d = r.data || {};
|
|
305
|
-
const actions = el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:8px 14px 4px', 'data-r9': 'memory-bar' },
|
|
306
|
-
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'),
|
|
307
|
-
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'),
|
|
308
|
-
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',
|
|
309
|
-
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({}); } },
|
|
310
|
-
d.wired ? 'wired ✓' : 'wire into CLAUDE.md'),
|
|
311
|
-
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',
|
|
312
|
-
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({}); } },
|
|
313
|
-
'approval ' + (d.config && d.config.write_approval ? 'on' : 'off')));
|
|
314
|
-
kids.push(actions);
|
|
315
|
-
if (memNote) kids.push(el('div', { 'data-r9': 'note', style: NOTE + ';color:var(--destructive)' }, iv(memNote)));
|
|
316
|
-
if (st) return [...kids, st];
|
|
317
|
-
for (const t of ['memory', 'user']) {
|
|
318
|
-
const u = (d.usage || {})[t] || { used: 0, limit: 0, percent: 0 };
|
|
319
|
-
kids.push(el('div', { style: LABEL, 'data-r9': 'usage-' + t }, iv((t === 'memory' ? 'repository memory' : 'about you') + ' · ' + u.percent + '% · ' + u.used + '/' + u.limit)));
|
|
320
|
-
const es = (d.entries || []).filter((e) => e.target === t);
|
|
321
|
-
if (!es.length) kids.push(el('div', { style: NOTE, 'data-r9': 'empty' }, iv(t === 'memory' && !d.wired
|
|
322
|
-
? 'Nothing recorded yet. Sessions read this memory — and agents add to it — only once the repository is wired: press “wire into CLAUDE.md”. Nothing is added automatically; an agent’s fact stays unconfirmed until you confirm it.'
|
|
323
|
-
: 'Nothing recorded yet.')));
|
|
324
|
-
for (const e of es) {
|
|
325
|
-
kids.push(el('div', { style: ROW, 'data-r9': 'entry', 'data-id': e.id },
|
|
326
|
-
el('span', { style: 'display:block;font-size:12px;line-height:1.45' }, iv(e.text)),
|
|
327
|
-
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground);margin-top:2px' },
|
|
328
|
-
iv([e.confidence, e.stale ? 'stale' : null, e.source ? 'source ' + e.source : null, e.by, String(e.at).slice(0, 10)].filter(Boolean).join(' · '))),
|
|
329
|
-
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
330
|
-
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,
|
|
331
|
-
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'),
|
|
332
|
-
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'))));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
if ((d.elsewhere || []).length) {
|
|
336
|
-
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r9': 'elsewhere-label', title: 'the ledger lives in each checkout; these arrive in this tree when their branch merges' }, iv('recorded on other branches · ' + d.elsewhere.length + ' · read only')));
|
|
337
|
-
for (const e of d.elsewhere) {
|
|
338
|
-
kids.push(el('div', { style: ROW, 'data-r9': 'elsewhere', 'data-id': e.id },
|
|
339
|
-
el('span', { style: 'display:block;font-size:12px;line-height:1.45' }, iv(e.text)),
|
|
340
|
-
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground);margin-top:2px' },
|
|
341
|
-
iv([e.target === 'user' ? 'about you' : null, e.confidence, e.branch ? '⑂ ' + e.branch : null, e.by, String(e.at).slice(0, 10)].filter(Boolean).join(' · ')))));
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if ((d.pending || []).length) {
|
|
345
|
-
kids.push(el('div', { style: LABEL + ';color:var(--foreground)', 'data-r9': 'pending-label' }, iv('waiting for your approval · ' + d.pending.length)));
|
|
346
|
-
for (const p of d.pending) {
|
|
347
|
-
kids.push(el('div', { style: ROW, 'data-r9': 'pending', 'data-eid': p.eid },
|
|
348
|
-
el('span', { style: 'display:block;font-size:12px' }, iv(p.op + ' · ' + (p.text || p.id))),
|
|
349
|
-
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', ' '))),
|
|
350
|
-
el('span', { style: 'display:flex;gap:4px;margin-top:4px' },
|
|
351
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r9': 'approve', onClick: () => memDo({ op: 'approve', ref: p.eid }) }, 'approve'),
|
|
352
|
-
el('button', { style: BTN, hover: 'color:var(--destructive)', 'data-r9': 'reject', onClick: () => memDo({ op: 'reject', ref: p.eid }) }, 'reject'))));
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if ((d.blocked || []).length) {
|
|
356
|
-
kids.push(el('div', { style: LABEL + ';color:var(--destructive)', 'data-r9': 'blocked-label' }, iv('blocked by the scan · ' + d.blocked.length)));
|
|
357
|
-
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')));
|
|
358
|
-
}
|
|
359
|
-
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.')));
|
|
360
|
-
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/* ── skills ─────────────────────────────────────────────────────────────────────────────────── */
|
|
364
|
-
let candNote = null; const openText = new Map();
|
|
365
|
-
async function candDo(ws, body) {
|
|
366
|
-
const out = await post('/api/skills/decide', { ws, ...body });
|
|
367
|
-
candNote = out && out.ok ? (out.note || null) : ((out && out.error) || 'not done');
|
|
368
|
-
if (out && out.ok && body.op === 'read') openText.set(body.id + '@' + body.sha, out);
|
|
369
|
-
reload('skillcands:' + ws); reload('skills:' + ws); setState({});
|
|
370
|
-
return out;
|
|
371
|
-
}
|
|
372
|
-
function candidatesBlock(wsId) {
|
|
373
|
-
if (!wsId) return [];
|
|
374
|
-
const r = read('skillcands:' + wsId, '/api/skills/candidates?ws=' + encodeURIComponent(wsId));
|
|
375
|
-
const d = r.data || {};
|
|
376
|
-
const rows = d.candidates || [];
|
|
377
|
-
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'))];
|
|
378
|
-
if (candNote) kids.push(el('div', { 'data-r13': 'cand-note', style: NOTE }, iv(candNote)));
|
|
379
|
-
if (r.state === 'error') kids.push(el('div', { style: NOTE + ';color:var(--destructive)' }, iv('the candidates could not be read — ' + (r.error || ''))));
|
|
380
|
-
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.')));
|
|
381
|
-
for (const c of rows) {
|
|
382
|
-
const key = c.id + '@' + c.sha; const shown = openText.get(key);
|
|
383
|
-
const soft = (c.scan && c.scan.soft) || [];
|
|
384
|
-
kids.push(el('div', { style: ROW, 'data-r13': 'cand', 'data-id': c.id, 'data-state': c.state + (c.running ? ':running' : '') },
|
|
385
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
386
|
-
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(c.id)),
|
|
387
|
-
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)' : '')))),
|
|
388
|
-
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(c.why)),
|
|
389
|
-
el('span', { style: 'display:block;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' },
|
|
390
|
-
iv([c.origin, c.license, c.author, c.source_url, '#' + c.sha12].filter(Boolean).join(' · '))),
|
|
391
|
-
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,
|
|
392
|
-
(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,
|
|
393
|
-
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,
|
|
394
|
-
el('span', { style: 'display:flex;gap:4px;margin-top:4px;flex-wrap:wrap' },
|
|
395
|
-
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'),
|
|
396
|
-
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',
|
|
397
|
-
onClick: async () => {
|
|
398
|
-
let ack = false;
|
|
399
|
-
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; }
|
|
400
|
-
await candDo(wsId, { op: 'approve', id: c.id, sha: c.sha, ack_license: ack });
|
|
401
|
-
} }, 'approve') : null,
|
|
402
|
-
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,
|
|
403
|
-
(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),
|
|
404
|
-
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));
|
|
405
|
-
}
|
|
406
|
-
return kids;
|
|
407
|
-
}
|
|
408
|
-
/* Discovery is its own section at the TOP: it used to sit under the whole skill list, below the fold,
|
|
409
|
-
* so a PM who scrolled nothing saw no discovery at all. */
|
|
410
|
-
function discoveryBlock(wsId) {
|
|
411
|
-
const sch = read('schedules', '/api/schedules?all=1');
|
|
412
|
-
const mine = ((sch.data && sch.data.schedules) || []).find((x) => x.skill === 'skill-discover' || x.task === 'skill-discover');
|
|
413
|
-
const kids = [el('div', { style: LABEL + ';color:var(--foreground)', 'data-r13': 'discovery-label' }, iv('skill discovery')),
|
|
414
|
-
el('div', { style: NOTE }, iv('Finds skills in your own repository and from sources you name. Nothing it finds runs until you read it and approve it.')),
|
|
415
|
-
el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:0 14px 6px' },
|
|
416
|
-
mine ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r13': 'discover-now', onClick: () => runSchedule(mine, 'skill discovery') }, 'find skills now') : null,
|
|
417
|
-
mine ? el('span', { style: 'font-family:var(--mono);font-size:10.5px;color:var(--muted-foreground);align-self:center' }, iv('weekly · next ' + (mine.next ? String(mine.next).slice(0, 16).replace('T', ' ') : '—')))
|
|
418
|
-
: 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'))];
|
|
419
|
-
return [...kids, runNoteEl(), ...candidatesBlock(wsId)];
|
|
420
|
-
}
|
|
421
79
|
/** A skill opens in a tab, like a Markdown file: it sits beside the terminals and survives a mode switch. */
|
|
422
80
|
async function openSkillTab(sk) {
|
|
423
81
|
const ini = await import('./panel-initiatives.js');
|
|
@@ -448,23 +106,6 @@ export function mountSkillTab(host, tab) {
|
|
|
448
106
|
setChildren(host, [body]);
|
|
449
107
|
return host;
|
|
450
108
|
}
|
|
451
|
-
export function skillsLive() {
|
|
452
|
-
const wsId = dataMod && state.product ? dataMod.wsOf(state.product) : null;
|
|
453
|
-
const r = read('skills:' + (wsId || ''), '/api/skills' + (wsId ? '?ws=' + encodeURIComponent(wsId) : ''));
|
|
454
|
-
const st = status(r, 'the skills');
|
|
455
|
-
const head = discoveryBlock(wsId);
|
|
456
|
-
if (st) return [el('div', { style: 'padding:6px 0' }, [...head, st])];
|
|
457
|
-
const list = (r.data && r.data.skills) || [];
|
|
458
|
-
const kids = list.map((sk) => el('button', { style: ROW + (state.active === 'skill:' + sk.id ? ';background:var(--accent)' : ''), hover: 'background:var(--accent)', 'data-r4': 'skill', 'data-id': sk.id,
|
|
459
|
-
onClick: () => openSkillTab(sk) },
|
|
460
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
461
|
-
el('span', { style: 'font-family:var(--mono);font-size:12px' }, iv(sk.label || sk.skill || sk.id)),
|
|
462
|
-
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(sk.source))),
|
|
463
|
-
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)' }, iv(sk.description))));
|
|
464
|
-
if (!list.length) kids.push(el('div', { style: NOTE }, iv('No skills — a successful read of nothing.')));
|
|
465
|
-
return [el('div', { style: 'padding:6px 0' }, [...head, el('div', { style: LABEL + ';color:var(--foreground)' }, iv('skills · ' + list.length + ' · click one to read it')), ...kids])];
|
|
466
|
-
}
|
|
467
|
-
|
|
468
109
|
/* The detail of one skill (panel 3). The text is read from the server on open. Your own skills are
|
|
469
110
|
* editable and save in place; a builtin or an approved discovered skill is read-only, and "make my own
|
|
470
111
|
* copy" writes an editable one beside it, so the original is never changed under a review. */
|
|
@@ -506,235 +147,6 @@ export function skillDetailLive(det) {
|
|
|
506
147
|
return out;
|
|
507
148
|
}
|
|
508
149
|
|
|
509
|
-
/* ── knowledge ──────────────────────────────────────────────────────────────────────────────── */
|
|
510
|
-
function flatten(node, out, depth) {
|
|
511
|
-
for (const d of node.docs || []) out.push({ path: d.path, name: d.name, depth, category: d.category || null, basis: d.category_basis || null, why: d.category_why || null, kind: d.category_kind || null, jev: d.category_jev || null, disagrees: d.category_disagrees || [] });
|
|
512
|
-
if (depth < 3) for (const sub of node.dirs || []) { out.push({ dir: sub.name || sub.path, depth }); flatten(sub, out, depth + 1); }
|
|
513
|
-
return out;
|
|
514
|
-
}
|
|
515
|
-
/* jev-doc-categorizer Run 1 — each document's category, derived locally beside its own `type`, with the basis that decided it.
|
|
516
|
-
* `undetermined` is its own value: the file has no evidence, and it is shown as that, never as `other`. A press on a category
|
|
517
|
-
* narrows the list; a second press on it shows everything again. Nothing is written. */
|
|
518
|
-
let knCat = '';
|
|
519
|
-
const catTitle = (it) => it.category ? 'category ' + it.category + (it.kind ? ' (' + it.kind + ')' : '') + (it.basis ? ' · decided by ' + it.basis : ' · no evidence') + (it.why ? ' (' + it.why + ')' : '')
|
|
520
|
-
+ (it.disagrees && it.disagrees.length ? ' · ' + it.disagrees.map((d) => d.basis + ' says ' + d.category).join(', ') : '') + ' · advisory' : '';
|
|
521
|
-
const catChip = (it) => it.category ? el('span', { 'data-cat': it.category, 'data-cat-basis': it.basis || '', title: catTitle(it), style: 'margin-left:8px;font-size:9.5px;letter-spacing:.06em;color:var(--muted-foreground)' }, iv('· ' + it.category + (it.kind ? ' (' + it.kind + ')' : '') + (it.basis === 'jev' ? ' (jev)' : it.jev === 'unsure' ? ' (jev unsure)' : ''))) : null;
|
|
522
|
-
function categoryBar(trees) {
|
|
523
|
-
const counts = {};
|
|
524
|
-
for (const t of trees) for (const it of flatten(t, [], 0)) if (it.path && it.category) counts[it.category] = (counts[it.category] || 0) + 1;
|
|
525
|
-
const names = Object.keys(counts).sort((a, b) => counts[b] - counts[a]);
|
|
526
|
-
if (!names.length) return null;
|
|
527
|
-
return el('div', { 'data-cat-bar': '1', style: 'display:flex;flex-wrap:wrap;gap:4px;padding:2px 14px 6px' },
|
|
528
|
-
names.map((n) => el('button', { 'data-cat-filter': n, 'aria-pressed': knCat === n ? 'true' : 'false', title: 'show only ' + n + ' documents', onClick: () => { knCat = knCat === n ? '' : n; setState({}); },
|
|
529
|
-
style: 'font-family:var(--mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);' + (knCat === n ? 'background:var(--accent);' : '') }, iv(n + ' ' + counts[n]))));
|
|
530
|
-
}
|
|
531
|
-
/* D82 — Jev on documents the local rules leave undetermined. The switch is off until the PM turns it on; the first run shows
|
|
532
|
-
* its request count and sends only after the PM confirms that number; afterwards opening this list sends only new or changed
|
|
533
|
-
* files. Jev's categories are marked "(jev)" and are advisory. */
|
|
534
|
-
let dPlan = null; // the plan the PM is looking at: { ws, plan }
|
|
535
|
-
let dNote = null;
|
|
536
|
-
const dSay = (text) => { dNote = text; setState({}); };
|
|
537
|
-
async function dSwitch(ws, on) {
|
|
538
|
-
const out = await post('/api/doccat/jev', { ws, categorize: on });
|
|
539
|
-
reload('library:' + ws); dPlan = null;
|
|
540
|
-
dSay(out && out.ok ? (on ? 'categorizing with Jev is on — review the first run’s count, then confirm it; nothing is sent before that' : 'categorizing with Jev is off') : ((out && out.error) || 'not saved'));
|
|
541
|
-
}
|
|
542
|
-
async function dResume(ws) {
|
|
543
|
-
const out = await post('/api/doccat/jev/resume', { ws });
|
|
544
|
-
reload('library:' + ws);
|
|
545
|
-
dSay(out && out.ok ? 'categorizing with Jev resumed' : ((out && out.error) || 'not resumed'));
|
|
546
|
-
}
|
|
547
|
-
async function dPlanFor(ws) {
|
|
548
|
-
const out = await post('/api/doccat/jev/run', { ws });
|
|
549
|
-
if (!out || out.ok === false) { dPlan = null; return dSay((out && out.error) || 'no plan'); }
|
|
550
|
-
dPlan = { ws, plan: out.plan }; dSay('review the count, then confirm to send');
|
|
551
|
-
}
|
|
552
|
-
async function dRun() {
|
|
553
|
-
if (!dPlan) return;
|
|
554
|
-
const { ws, plan } = dPlan;
|
|
555
|
-
dSay('sending ' + plan.requests + ' request(s) to ' + plan.host + ' …');
|
|
556
|
-
const out = await post('/api/doccat/jev/run', { ws, confirm: true, expect_requests: plan.requests, expect_fingerprint: plan.fingerprint });
|
|
557
|
-
dPlan = null; reload('library:' + ws);
|
|
558
|
-
dSay(!out || out.ok === false ? ((out && out.error) || 'the run failed') : 'asked ' + out.sent + ' · ' + out.decided + ' categorized · ' + out.unsure + ' unsure · ' + (out.unreadable || 0) + ' unreadable · ' + out.cached + ' answered from the cache · ' + out.failed + ' failed');
|
|
559
|
-
}
|
|
560
|
-
/* Run 4 — what the last pass did to each undetermined file, so the PM can see which ones Jev categorized, which stayed
|
|
561
|
-
* undetermined and why, and which failed. Problems first. The record is the server's (per user, paths and outcomes only). */
|
|
562
|
-
let dFilesAll = false;
|
|
563
|
-
const D_ORDER = ['failed', 'not-asked', 'unreadable', 'unsure', 'withheld', 'excluded', 'categorized'];
|
|
564
|
-
const D_LABEL = { failed: 'failed', 'not-asked': 'not asked', unreadable: 'unreadable', unsure: 'unsure', withheld: 'withheld', excluded: 'excluded', categorized: 'categorized' };
|
|
565
|
-
const D_TONE = { failed: 'var(--destructive)', 'not-asked': 'var(--destructive)', unreadable: 'var(--destructive)', categorized: 'var(--foreground)' };
|
|
566
|
-
const KEEPS = ' The file keeps the local result: undetermined.';
|
|
567
|
-
function dOutcomeWords(f) {
|
|
568
|
-
const cached = f.cached ? ' Answer from the cache.' : '';
|
|
569
|
-
if (f.outcome === 'categorized') return 'Jev: ' + (f.category === 'ticket' ? 'ticket (' + (f.ticket_kind || 'other') + ')' : f.category) + '. ' + (f.sentence || '') + cached;
|
|
570
|
-
if (f.outcome === 'unsure') return 'Jev was unsure (' + (f.leading || []).join(' or ') + ').' + KEEPS + cached;
|
|
571
|
-
if (f.outcome === 'unreadable') return 'Jev’s answer could not be read.' + KEEPS + ' It is asked again only when the file changes.' + cached;
|
|
572
|
-
if (f.outcome === 'failed') return 'The request failed: ' + (f.reason || 'no reason given') + '.' + KEEPS + ' It is asked again on the next pass.';
|
|
573
|
-
if (f.outcome === 'withheld') return 'Not sent: the credential scan matched (' + (f.rules || []).join(', ') + ').';
|
|
574
|
-
if (f.outcome === 'excluded') return 'Not sent: the path is excluded.';
|
|
575
|
-
if (f.outcome === 'not-asked') return 'Not asked: ' + (f.reason || 'the pass stopped first');
|
|
576
|
-
return f.outcome;
|
|
577
|
-
}
|
|
578
|
-
function dLibraryLine(trees) {
|
|
579
|
-
let local = 0, jev = 0, undet = 0;
|
|
580
|
-
for (const t of trees || []) for (const it of flatten(t, [], 0)) {
|
|
581
|
-
if (!it.path || !it.category) continue;
|
|
582
|
-
if (it.category === 'undetermined') undet++; else if (it.basis === 'jev') jev++; else local++;
|
|
583
|
-
}
|
|
584
|
-
if (!local && !jev && !undet) return null;
|
|
585
|
-
return el('div', { 'data-doccat': 'library-line', style: NOTE }, iv('In this list: ' + local + ' decided by the local rules or your corrections · ' + jev + ' categorized by Jev · ' + undet + ' undetermined.'));
|
|
586
|
-
}
|
|
587
|
-
function dResultsBlock(ws, res) {
|
|
588
|
-
if (!res || !Array.isArray(res.files)) return [];
|
|
589
|
-
const counts = {};
|
|
590
|
-
for (const f of res.files) counts[f.outcome] = (counts[f.outcome] || 0) + 1;
|
|
591
|
-
const parts = D_ORDER.filter((k) => counts[k]).map((k) => counts[k] + ' ' + D_LABEL[k]);
|
|
592
|
-
const when = String(res.at || '').replace('T', ' ').slice(0, 16);
|
|
593
|
-
const out = [el('div', { 'data-doccat': 'results-summary', style: NOTE + ';color:var(--foreground)' },
|
|
594
|
-
iv('Last Jev pass (' + (res.kind === 'confirmed' ? 'confirmed by you' : 'automatic') + ', ' + when + '), ' + res.total + ' undetermined file(s): ' + (parts.join(' · ') || 'none') + '.'
|
|
595
|
-
+ (res.total > res.files.length ? ' Only the first ' + res.files.length + ' are listed.' : '')))];
|
|
596
|
-
const rows = res.files.slice().sort((a, b) => D_ORDER.indexOf(a.outcome) - D_ORDER.indexOf(b.outcome) || a.doc.localeCompare(b.doc));
|
|
597
|
-
const shown = dFilesAll ? rows : rows.slice(0, 50);
|
|
598
|
-
for (const f of shown) {
|
|
599
|
-
out.push(el('div', { 'data-doccat-file': f.doc, 'data-outcome': f.outcome, style: 'display:flex;gap:8px;align-items:baseline;padding:2px 14px 2px 20px;font-size:11px' },
|
|
600
|
-
el('span', { 'data-doccat-outcome': f.outcome, style: 'flex:none;width:76px;font-family:var(--mono);font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:' + (D_TONE[f.outcome] || 'var(--muted-foreground)') }, iv(D_LABEL[f.outcome] || f.outcome)),
|
|
601
|
-
el('span', { style: 'flex:1 1 auto;min-width:0' },
|
|
602
|
-
el('button', { style: 'font-family:var(--mono);font-size:11px;text-decoration:underline;color:var(--foreground);text-align:left', onClick: () => openDocIn(null, f.doc) }, iv(f.doc)),
|
|
603
|
-
el('span', { 'data-doccat-why': '1', style: 'display:block;color:var(--muted-foreground)' }, iv(dOutcomeWords(f))))));
|
|
604
|
-
}
|
|
605
|
-
if (rows.length > 50) out.push(el('div', { style: 'padding:2px 14px 6px 20px' },
|
|
606
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-doccat': 'files-all', onClick: () => { dFilesAll = !dFilesAll; setState({}); } }, dFilesAll ? 'show the first 50' : 'show all ' + rows.length)));
|
|
607
|
-
return out;
|
|
608
|
-
}
|
|
609
|
-
function doccatBlock(ws, j, trees) {
|
|
610
|
-
if (!ws || !j) return [];
|
|
611
|
-
const kids = [];
|
|
612
|
-
kids.push(el('div', { 'data-doccat': 'explain', style: NOTE }, iv('Files with no local evidence are “undetermined”. Jev can be asked to categorize them from a short snippet: the title, the headings and the opening, never the whole file. Its answers are marked (jev), are advisory, and an unsure answer stays undetermined.')));
|
|
613
|
-
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
614
|
-
el('button', { style: BTN + (j.switch ? ';color:var(--foreground);border-color:var(--foreground)' : ''), hover: 'color:var(--foreground)', 'data-doccat': 'switch', onClick: () => dSwitch(ws, !j.switch) },
|
|
615
|
-
iv(j.switch ? 'categorize with Jev: ON — turn off' : 'categorize with Jev: OFF — turn on')),
|
|
616
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', disabled: j.open ? null : 'true', 'data-doccat': 'plan',
|
|
617
|
-
title: 'Shows how many requests the undetermined files would take, and sends only after you confirm that number.', onClick: () => dPlanFor(ws) }, 'ask Jev about undetermined files'),
|
|
618
|
-
dPlan && dPlan.ws === ws ? el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', 'data-doccat': 'run', onClick: dRun }, 'confirm and send ' + dPlan.plan.requests + ' request(s)') : null));
|
|
619
|
-
kids.push(el('div', { 'data-doccat': 'gate', 'data-open': j.open ? 'yes' : 'no', style: NOTE + (j.open ? '' : ';color:var(--foreground)') },
|
|
620
|
-
iv(j.open ? (j.confirmed ? 'Active: new or changed undetermined files are sent when the workspace reads this repository’s documents (when it starts, and when this list opens), at most once a minute.' : 'Ready: nothing is sent until you confirm the first run’s count.') : 'Not active: ' + j.sentence + ' All three must hold before anything is sent.')));
|
|
621
|
-
if (j.paused) kids.push(el('div', { 'data-doccat': 'paused', style: 'display:flex;gap:8px;align-items:center;padding:2px 14px 6px;font-size:11.5px;color:var(--destructive)' },
|
|
622
|
-
el('span', null, iv(j.paused.reason)),
|
|
623
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-doccat': 'resume', onClick: () => dResume(ws) }, 'resume')));
|
|
624
|
-
const l = j.last;
|
|
625
|
-
if (l && l.waiting) kids.push(el('div', { 'data-doccat': 'waiting', style: NOTE + ';color:var(--foreground)' }, iv(l.waiting + ' new or changed file(s) are waiting: more than an automatic pass sends. Ask Jev and confirm the count to send them.')));
|
|
626
|
-
else if (l && !j.results) kids.push(el('div', { 'data-doccat': 'last', style: NOTE }, iv('Last Jev pass ' + String(l.at).replace('T', ' ').slice(0, 16) + ': sent ' + l.sent + ' · categorized ' + l.decided + ' · unsure ' + l.unsure + ' · unreadable ' + (l.unreadable || 0) + ' · withheld ' + (l.withheld_count || 0) + ' · excluded ' + l.excluded + ' · failed ' + l.failed + '.')));
|
|
627
|
-
if (dPlan && dPlan.ws === ws) {
|
|
628
|
-
const p = dPlan.plan;
|
|
629
|
-
kids.push(el('div', { 'data-doccat': 'plan-text', style: NOTE }, iv(p.requests + ' request(s), about ' + p.characters.toLocaleString() + ' characters, to ' + p.host + ' · ' + p.candidates + ' undetermined file(s) · ' + p.cached + ' already answered · ' + p.excluded + ' excluded · ' + p.withheld_count + ' withheld by the credential scan.')));
|
|
630
|
-
}
|
|
631
|
-
if (dNote) kids.push(el('div', { 'data-doccat': 'note', style: NOTE }, iv(dNote)));
|
|
632
|
-
const line = dLibraryLine(trees); if (line) kids.push(line);
|
|
633
|
-
kids.push(...dResultsBlock(ws, j.results));
|
|
634
|
-
return kids;
|
|
635
|
-
}
|
|
636
|
-
/* jev-doc-categorizer Run 3 (C7) — the PM corrects a category. `fix` on a row opens a small control: a category, a ticket kind for
|
|
637
|
-
* a ticket, `save correction` and `clear correction`. A correction is kept per user, outside the repository, and never edits the
|
|
638
|
-
* file. `write it into the file` is a separate press: it shows the one-line diff first, and `apply` writes it only if the file is
|
|
639
|
-
* still the text the diff was made from (the server compares the sha1 and answers 409 when it is not). The server checks every
|
|
640
|
-
* value again; these two lists only fill the menus. */
|
|
641
|
-
const CATS = ['prd', 'meeting-notes', 'report', 'ticket', 'knowledge', 'spec-artifact', 'other'];
|
|
642
|
-
const KINDS = ['design', 'engineering', 'other'];
|
|
643
|
-
let knFix = null; // { path, category, ticket_kind } — the row whose control is open
|
|
644
|
-
let knPlan = null; // { path, category, ticket_kind, diff, sha, unchanged } — the write-back diff on screen
|
|
645
|
-
let knNote = null; // { path, tone, text }
|
|
646
|
-
let knBusy = false;
|
|
647
|
-
const FIX = 'font-family:var(--mono);font-size:9.5px;padding:1px 6px;margin-right:10px;border:1px solid var(--border);color:var(--muted-foreground);flex:none';
|
|
648
|
-
const SEL = 'font-family:var(--mono);font-size:10.5px;padding:2px 4px;border:1px solid var(--border);background:var(--background);color:var(--foreground)';
|
|
649
|
-
const knSay = (path, tone, text) => { knNote = { path, tone, text }; knBusy = false; setState({}); };
|
|
650
|
-
function knOpen(it) {
|
|
651
|
-
if (knFix && knFix.path === it.path) { knFix = null; knPlan = null; knNote = null; setState({}); return; }
|
|
652
|
-
const cat = CATS.includes(it.category) ? it.category : 'other';
|
|
653
|
-
knFix = { path: it.path, category: cat, ticket_kind: cat === 'ticket' ? (it.kind || 'other') : null };
|
|
654
|
-
knPlan = null; knNote = null; setState({});
|
|
655
|
-
}
|
|
656
|
-
async function knSave(ws, clear) {
|
|
657
|
-
if (!knFix || knBusy) return;
|
|
658
|
-
knBusy = true; setState({});
|
|
659
|
-
const body = clear ? { ws, path: knFix.path, clear: true } : { ws, path: knFix.path, category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
|
|
660
|
-
const res = await post('/api/doccat/correct', body);
|
|
661
|
-
refresh('library:' + ws);
|
|
662
|
-
if (res && res.ok) knSay(knFix.path, 'ok', clear ? (res.cleared ? 'correction cleared — the rules decide this file again' : 'this file had no correction') : 'saved — your correction wins over every rule. The file is not changed.');
|
|
663
|
-
else knSay(knFix.path, 'bad', (res && res.error) || 'not saved');
|
|
664
|
-
}
|
|
665
|
-
async function knWriteback(ws, confirm) {
|
|
666
|
-
if (!knFix || knBusy) return;
|
|
667
|
-
knBusy = true; setState({});
|
|
668
|
-
const want = { category: knFix.category, ticket_kind: knFix.category === 'ticket' ? knFix.ticket_kind : null };
|
|
669
|
-
if (!confirm) {
|
|
670
|
-
const res = await post('/api/doccat/writeback', { ws, path: knFix.path, ...want });
|
|
671
|
-
if (res && res.ok) { knPlan = { path: knFix.path, ...want, diff: res.diff, sha: res.sha, next_sha: res.next_sha, unchanged: !!res.unchanged }; knSay(knFix.path, 'mut', res.unchanged ? 'the file already says this — nothing to write' : 'this is the change. Press apply to write it into the file.'); }
|
|
672
|
-
else { knPlan = null; knSay(knFix.path, 'bad', (res && res.error) || 'no diff'); }
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
if (!knPlan) { knBusy = false; return; }
|
|
676
|
-
const res = await post('/api/doccat/writeback', { ws, path: knPlan.path, category: knPlan.category, ticket_kind: knPlan.ticket_kind, confirm: true, expect_sha: knPlan.sha, expect_next_sha: knPlan.next_sha });
|
|
677
|
-
knPlan = null;
|
|
678
|
-
refresh('library:' + ws);
|
|
679
|
-
if (res && res.ok) knSay(knFix.path, 'ok', res.written ? 'written into the file\'s frontmatter' : 'the file already says this — nothing was written');
|
|
680
|
-
else knSay(knFix.path, 'bad', (res && res.error) || 'not written');
|
|
681
|
-
}
|
|
682
|
-
function knPanel(ws, it) {
|
|
683
|
-
const f = knFix;
|
|
684
|
-
const pad = 'padding:2px 14px 8px ' + (20 + it.depth * 12) + 'px';
|
|
685
|
-
const btn = (attr, label, fn, disabled) => el('button', { [attr]: '1', disabled: knBusy || disabled, onClick: fn, style: BTN, hover: 'color:var(--foreground)' }, label);
|
|
686
|
-
const kids = [
|
|
687
|
-
el('select', { 'data-cat-select': '1', 'aria-label': 'category', style: SEL, onChange: (e) => { f.category = e.target.value; f.ticket_kind = f.category === 'ticket' ? (f.ticket_kind || 'other') : null; knPlan = null; setState({}); } },
|
|
688
|
-
CATS.map((c) => el('option', { value: c, selected: c === f.category }, c))),
|
|
689
|
-
f.category === 'ticket' ? el('select', { 'data-cat-kind': '1', 'aria-label': 'ticket kind', style: SEL, onChange: (e) => { f.ticket_kind = e.target.value; knPlan = null; setState({}); } },
|
|
690
|
-
KINDS.map((k) => el('option', { value: k, selected: k === f.ticket_kind }, k))) : null,
|
|
691
|
-
btn('data-cat-save', 'save correction', () => knSave(ws, false)),
|
|
692
|
-
it.basis === 'pm correction' ? btn('data-cat-clear', 'clear correction', () => knSave(ws, true)) : null,
|
|
693
|
-
btn('data-cat-writeback', 'write it into the file', () => knWriteback(ws, false)),
|
|
694
|
-
];
|
|
695
|
-
const out = [el('div', { 'data-cat-panel': it.path, style: pad + ';display:flex;flex-wrap:wrap;gap:6px;align-items:center' }, kids)];
|
|
696
|
-
if (knPlan && knPlan.path === it.path && !knPlan.unchanged) {
|
|
697
|
-
out.push(el('pre', { 'data-cat-diff': '1', style: pad.replace('padding:2px', 'margin:0;padding:6px') + ';font-family:var(--mono);font-size:10.5px;white-space:pre-wrap;color:var(--foreground);background:var(--accent)' }, iv(knPlan.diff)));
|
|
698
|
-
out.push(el('div', { style: pad + ';display:flex;gap:6px' }, btn('data-cat-apply', 'apply', () => knWriteback(ws, true)), btn('data-cat-cancel', 'cancel', () => { knPlan = null; knNote = null; setState({}); })));
|
|
699
|
-
}
|
|
700
|
-
if (knNote && knNote.path === it.path) out.push(el('div', { 'data-cat-note': knNote.tone, style: pad + ';font-size:11px;color:' + TONE[knNote.tone] }, iv(knNote.text)));
|
|
701
|
-
return out;
|
|
702
|
-
}
|
|
703
|
-
export function knowledgeLive() {
|
|
704
|
-
const ws = state.wsId || '';
|
|
705
|
-
const r = read('library:' + ws, '/api/library?ws=' + encodeURIComponent(ws));
|
|
706
|
-
const st = status(r, 'the knowledge');
|
|
707
|
-
if (st) return [st];
|
|
708
|
-
const trees = (r.data && r.data.knowledge) || [];
|
|
709
|
-
const kids = [el('div', { style: NOTE }, iv('This repository\'s knowledge: the OKF bundle, skills and build-skills. Open a document to read it.'))];
|
|
710
|
-
const bar = categoryBar(trees); if (bar) kids.push(bar);
|
|
711
|
-
kids.push(...doccatBlock(ws, r.data && r.data.doccat, trees));
|
|
712
|
-
for (const t of trees) {
|
|
713
|
-
kids.push(el('div', { style: LABEL }, iv(t.path || t.name || 'knowledge')));
|
|
714
|
-
for (const it of flatten(t, [], 0)) {
|
|
715
|
-
if (knCat && (it.dir || it.category !== knCat)) continue;
|
|
716
|
-
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; }
|
|
717
|
-
const open = !!(knFix && knFix.path === it.path);
|
|
718
|
-
kids.push(el('div', { 'data-cat-row': it.path, style: 'display:flex;align-items:center' },
|
|
719
|
-
el('button', { onClick: () => openDocIn(null, it.path), style: 'display:block;flex:1 1 auto;min-width:0;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), catChip(it)),
|
|
720
|
-
el('button', { 'data-cat-fix': it.path, 'aria-expanded': open ? 'true' : 'false', title: 'correct the category of this document', onClick: () => knOpen(it), style: FIX + (open ? ';background:var(--accent);color:var(--foreground)' : ''), hover: 'color:var(--foreground)' }, 'fix')));
|
|
721
|
-
if (open) kids.push(...knPanel(ws, it));
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
// R11 — the notes PMOS and its agents wrote here (requirement notes, report drafts and definitions, memory).
|
|
725
|
-
const notes = (r.data && r.data.notes) || [];
|
|
726
|
-
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.')));
|
|
727
|
-
for (const t of notes) {
|
|
728
|
-
kids.push(el('div', { style: LABEL, 'data-r11': 'notes-label' }, iv(t.path || t.name || 'notes')));
|
|
729
|
-
for (const it of flatten(t, [], 0)) {
|
|
730
|
-
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; }
|
|
731
|
-
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)));
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
if (!trees.length && !notes.length) kids.push(el('div', { style: NOTE }, iv('No knowledge bundle in this repository — a successful read of nothing.')));
|
|
735
|
-
return [el('div', { style: 'padding:2px 0 8px' }, kids)];
|
|
736
|
-
}
|
|
737
|
-
|
|
738
150
|
/* ── D79 — validate the selected repository with TypeSafe's Jev, on a press ───────────────────────────────────
|
|
739
151
|
* Four things on one page, in the order the PM meets them: the key (never shown again once saved), this
|
|
740
152
|
* repository's consent switch (off until the PM turns it on), the plan (what would be sent, before anything
|
|
@@ -1038,292 +450,4 @@ export function validateList() {
|
|
|
1038
450
|
return [el('div', { style: 'padding:2px 0 8px', 'data-r19': 'validate' }, kids)];
|
|
1039
451
|
}
|
|
1040
452
|
|
|
1041
|
-
|
|
1042
|
-
let tNote = null;
|
|
1043
|
-
let tMissed = false;
|
|
1044
|
-
/* The text inputs of the Today list are kept across redraws. The list is drawn again on every state change (the 4-second retry of
|
|
1045
|
-
* an unread repository is one), and a new input on each draw threw away what the person was typing and the caret with it. Each
|
|
1046
|
-
* input is made once; a redraw writes the stored value into it only while the person has not edited it since the last save. */
|
|
1047
|
-
const tInputs = new Map();
|
|
1048
|
-
function tInput(key, attrs, stored) {
|
|
1049
|
-
let n = tInputs.get(key);
|
|
1050
|
-
if (!n) {
|
|
1051
|
-
n = el('input', { ...attrs, onInput: (e) => { e.target.dataset.dirty = '1'; } });
|
|
1052
|
-
tInputs.set(key, n);
|
|
1053
|
-
}
|
|
1054
|
-
if (!n.dataset.dirty && n.value !== stored) n.value = stored;
|
|
1055
|
-
// The redraw moves the input into a new row, and moving a node blurs it: give the focus and the caret back once the draw is done.
|
|
1056
|
-
if (typeof document !== 'undefined' && document.activeElement === n) {
|
|
1057
|
-
let a = null, b = null;
|
|
1058
|
-
try { a = n.selectionStart; b = n.selectionEnd; } catch { /* a number input has no selection */ }
|
|
1059
|
-
queueMicrotask(() => {
|
|
1060
|
-
if (!n.isConnected || document.activeElement === n) return;
|
|
1061
|
-
n.focus();
|
|
1062
|
-
try { if (a !== null) n.setSelectionRange(a, b); } catch { /* as above */ }
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
return n;
|
|
1066
|
-
}
|
|
1067
|
-
/** Save a settings patch from kept inputs; on success their text is the stored value again. */
|
|
1068
|
-
async function tSaveFrom(inputs, patch) {
|
|
1069
|
-
const out = await tSettings(patch);
|
|
1070
|
-
if (out && out.ok !== false) for (const n of inputs) delete n.dataset.dirty;
|
|
1071
|
-
setState({});
|
|
1072
|
-
}
|
|
1073
|
-
let tRetry = null;
|
|
1074
|
-
const tSay = (tone, text) => { tNote = { tone, text }; setState({}); };
|
|
1075
|
-
const OWNER_WORD = { pm: 'yours', agent: 'the agent\'s' };
|
|
1076
|
-
async function tFeedback(body, say) {
|
|
1077
|
-
const out = await post('/api/proactive/feedback', body);
|
|
1078
|
-
if (!out || out.ok === false) return tSay('bad', (out && out.error) || 'feedback was not saved');
|
|
1079
|
-
reload('learn');
|
|
1080
|
-
tSay('ok', say);
|
|
1081
|
-
}
|
|
1082
|
-
async function tOpen(c) {
|
|
1083
|
-
tFeedback({ candidate_id: c.id, event: 'opened' }, 'opened — recorded');
|
|
1084
|
-
const o = c.open || {};
|
|
1085
|
-
if (o.type === 'schedule') { setState({ mode: 'scheduled', col0: true }); return; }
|
|
1086
|
-
if (c.repo && c.initiative_id) await land({ repo: c.repo, id: c.initiative_id });
|
|
1087
|
-
}
|
|
1088
|
-
async function tScan() {
|
|
1089
|
-
const out = await post('/api/proactive/scan', {});
|
|
1090
|
-
reload('today'); reload('brief');
|
|
1091
|
-
tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'the scan failed') : 'scanned — ' + out.found + ' waiting, ' + out.appended + ' new');
|
|
1092
|
-
}
|
|
1093
|
-
async function tSettings(patch) {
|
|
1094
|
-
const out = await post('/api/proactive/settings', patch);
|
|
1095
|
-
reload('today'); reload('brief'); reload('prepare'); reload('learn');
|
|
1096
|
-
tSay(!out || out.ok === false ? 'bad' : 'ok', !out || out.ok === false ? ((out && out.error) || 'not saved') : 'saved');
|
|
1097
|
-
return out;
|
|
1098
|
-
}
|
|
1099
|
-
function tCard(c) {
|
|
1100
|
-
const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
|
|
1101
|
-
return el('div', { 'data-r22': 'card', 'data-id': c.id, 'data-kind': c.kind, 'data-repo': c.repo || '', style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
1102
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
1103
|
-
el('span', { style: 'font-size:12.5px' }, iv(c.title)),
|
|
1104
|
-
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(c.kind + ' · ' + (OWNER_WORD[c.owner] || c.owner)))),
|
|
1105
|
-
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(c.why + (c.evidence ? ' — ' + c.evidence : ''))),
|
|
1106
|
-
c.adjust ? el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground);margin:0 0 4px', 'data-r26': 'adjust' }, iv('Learned from your feedback: ' + c.adjust_why + '.')) : null,
|
|
1107
|
-
el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
|
|
1108
|
-
c.open ? b('open', 'open', () => tOpen(c)) : null,
|
|
1109
|
-
c.initiative_id && c.ws ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'prepare', onClick: () => bgShowPlan(c) }, 'prepare in the background') : null,
|
|
1110
|
-
b('useful', 'useful', () => tFeedback({ candidate_id: c.id, verdict: 'useful' }, 'thanks — recorded as useful')),
|
|
1111
|
-
b('not useful', 'not-useful', () => tFeedback({ candidate_id: c.id, verdict: 'not-useful' }, 'recorded as not useful')),
|
|
1112
|
-
b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: c.id, verdict: 'wrong-priority' }, 'recorded as wrong priority')),
|
|
1113
|
-
b('dismiss 7 days', 'dismiss', async () => { await tFeedback({ candidate_id: c.id, event: 'dismissed' }, 'dismissed for 7 days'); reload('today'); setState({}); })));
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
/* the morning brief (proactive-engine D2): submitted by a scheduled skill in a session, only checked and stored by the server */
|
|
1117
|
-
let bNote = null;
|
|
1118
|
-
async function bSchedule() {
|
|
1119
|
-
const data = await import('./data.js');
|
|
1120
|
-
const ws = state.product ? data.wsOf(state.product) : null;
|
|
1121
|
-
if (!ws) { bNote = 'select a repository first'; setState({}); return; }
|
|
1122
|
-
const out = await post('/api/schedules/seed', { ws, set: 'brief' });
|
|
1123
|
-
bNote = !out || out.ok === false ? ((out && out.error) || 'not scheduled')
|
|
1124
|
-
: out.created.length ? 'scheduled: ' + out.created[0].name + ' (' + out.created[0].cadence + ' ' + out.created[0].time + ') — it runs only while the workspace server runs. Made after that time today, it also runs once within a minute.'
|
|
1125
|
-
: 'nothing to add — ' + out.kept.map((k) => k.skill + ': ' + k.why).join('; ');
|
|
1126
|
-
reload('brief'); setState({});
|
|
1127
|
-
}
|
|
1128
|
-
function bCard(x, ask) {
|
|
1129
|
-
const b = (label, attr, fn) => el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': attr, onClick: fn }, label);
|
|
1130
|
-
return el('div', { 'data-r22': ask ? 'ask-card' : 'brief-card', 'data-id': x.id, 'data-source': x.source, style: 'padding:8px 14px;border-bottom:1px solid var(--border)' },
|
|
1131
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
1132
|
-
el('span', { style: 'font-size:12.5px' }, iv(x.title)),
|
|
1133
|
-
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(x.source + ' · ' + (OWNER_WORD[x.owner] || x.owner) + ' · ' + x.effort + ' · ' + x.confidence))),
|
|
1134
|
-
el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(x.why_now + (x.evidence ? ' — ' + x.evidence : ''))),
|
|
1135
|
-
el('span', { style: 'display:flex;gap:4px;flex-wrap:wrap' },
|
|
1136
|
-
b('useful', 'useful', () => tFeedback({ candidate_id: x.id, verdict: 'useful' }, 'thanks — recorded as useful')),
|
|
1137
|
-
b('not useful', 'not-useful', () => tFeedback({ candidate_id: x.id, verdict: 'not-useful' }, 'recorded as not useful')),
|
|
1138
|
-
b('wrong priority', 'wrong-priority', () => tFeedback({ candidate_id: x.id, verdict: 'wrong-priority' }, 'recorded as wrong priority'))));
|
|
1139
|
-
}
|
|
1140
|
-
function briefSection() {
|
|
1141
|
-
const r = read('brief', '/api/proactive/brief');
|
|
1142
|
-
const st = status(r, 'the brief');
|
|
1143
|
-
if (st) return [st];
|
|
1144
|
-
const d = r.data; const br = d.brief;
|
|
1145
|
-
const kids = [el('div', { 'data-r22': 'greeting', style: 'padding:10px 14px 2px;font-size:15px;color:var(--foreground)' }, iv(d.greeting))];
|
|
1146
|
-
if (!br) {
|
|
1147
|
-
kids.push(el('div', { style: NOTE, 'data-r22': 'no-brief' }, iv('There is no brief yet. A scheduled skill in an agent session reads your calendar and other sources and submits it. This server only checks and stores it.' + (d.scheduled.length ? ' It is scheduled for ' + d.scheduled.map((x) => x.time).join(', ') + ' and runs only while the workspace server runs.' : ''))));
|
|
1148
|
-
if (!d.scheduled.length) kids.push(el('div', { style: 'padding:0 14px 6px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'schedule-brief', onClick: bSchedule }, 'schedule the morning brief (06:45 daily)')));
|
|
1149
|
-
if (bNote) kids.push(el('div', { style: NOTE, 'data-r22': 'brief-note' }, iv(bNote)));
|
|
1150
|
-
return kids;
|
|
1151
|
-
}
|
|
1152
|
-
kids.push(el('div', { style: NOTE, 'data-r22': 'brief-date' }, iv('Brief for ' + br.date + ' · made ' + String(br.generated_at).replace('T', ' ').slice(0, 16) + ' UTC · typed scoring ' + (br.judgment.used ? 'used (the brief says so; the server cannot check it)' : 'not used'))));
|
|
1153
|
-
if (d.stale) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'stale' }, iv('This brief is for ' + br.date + ', not today (' + d.today + '). It is out of date.')));
|
|
1154
|
-
kids.push(el('div', { style: NOTE, 'data-r22': 'sources' }, iv('Sources: ' + Object.entries(br.sources).map(([k, v]) => k + ' ' + v).join(' · '))));
|
|
1155
|
-
kids.push(el('div', { style: LABEL }, iv('calendar')));
|
|
1156
|
-
if (!br.calendar.length) kids.push(el('div', { style: NOTE, 'data-r22': 'no-events' }, iv(br.sources.calendar === 'read' ? 'No events today.' : 'The calendar was not read, so this says nothing about your day.')));
|
|
1157
|
-
for (const e of br.calendar) kids.push(el('div', { 'data-r22': 'event', style: 'padding:3px 14px;font-size:12px' }, iv(new Date(e.start).toTimeString().slice(0, 5) + ' ' + e.title + (e.with ? ' — ' + e.with : ''))));
|
|
1158
|
-
kids.push(el('div', { style: LABEL }, iv('priorities')));
|
|
1159
|
-
if (!br.priorities.length) kids.push(el('div', { style: NOTE, 'data-r22': 'no-priorities' }, iv('No priorities in this brief.')));
|
|
1160
|
-
for (const x of br.priorities) kids.push(bCard(x, false));
|
|
1161
|
-
if (br.worth_asking.length) {
|
|
1162
|
-
kids.push(el('div', { style: LABEL }, iv('worth asking · the brief is unsure')));
|
|
1163
|
-
for (const x of br.worth_asking) kids.push(bCard(x, true));
|
|
1164
|
-
}
|
|
1165
|
-
return kids;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
/* background preparation (proactive-engine D3): the PM approves one plan; the workspace starts it later, under the same rules as a next-step go */
|
|
1169
|
-
let bgPlan = null; // { cid, state: 'loading'|'ok'|'error', data, error }
|
|
1170
|
-
let bgNote = null;
|
|
1171
|
-
async function bgShowPlan(c) {
|
|
1172
|
-
if (bgPlan && bgPlan.cid === c.id) { bgPlan = null; setState({}); return; }
|
|
1173
|
-
bgPlan = { cid: c.id, state: 'loading' }; setState({});
|
|
1174
|
-
const out = await post('/api/proactive/prepare/plan', { ws: c.ws, initiative_id: c.initiative_id });
|
|
1175
|
-
bgPlan = !out || out.ok === false ? { cid: c.id, state: 'error', error: (out && out.error) || 'no plan' } : { cid: c.id, state: 'ok', data: out, c };
|
|
1176
|
-
setState({});
|
|
1177
|
-
}
|
|
1178
|
-
async function bgQueue(c, d) {
|
|
1179
|
-
const out = await post('/api/proactive/prepare/queue', { ws: d.ws, initiative_id: d.initiative_id, item_id: d.item.id, prompt_sha: d.prompt_sha, candidate_id: c.id, confirm: true });
|
|
1180
|
-
bgNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not queued' } : { tone: 'ok', text: 'queued — the workspace starts it when the budget allows' + (d.background.quiet ? ', after quiet hours' : '') + '. It expires in ' + d.background.expire_hours + ' hours.' };
|
|
1181
|
-
if (out && out.ok) bgPlan = null;
|
|
1182
|
-
reload('prepare'); setState({});
|
|
1183
|
-
}
|
|
1184
|
-
async function bgCancel(e) {
|
|
1185
|
-
const out = await post('/api/proactive/prepare/cancel', { id: e.id });
|
|
1186
|
-
bgNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not cancelled' } : { tone: 'ok', text: 'cancelled' };
|
|
1187
|
-
reload('prepare'); setState({});
|
|
1188
|
-
}
|
|
1189
|
-
async function bgRun() {
|
|
1190
|
-
const out = await post('/api/proactive/prepare/run', {});
|
|
1191
|
-
bgNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'the run failed' }
|
|
1192
|
-
: { tone: 'ok', text: out.results.length ? out.results.map((x) => x.action + (x.reason ? ' (' + x.reason + ')' : '')).join('; ') : 'nothing is queued' };
|
|
1193
|
-
reload('prepare'); setState({});
|
|
1194
|
-
}
|
|
1195
|
-
function bgPlanView() {
|
|
1196
|
-
if (!bgPlan) return null;
|
|
1197
|
-
if (bgPlan.state === 'loading') return el('div', { style: NOTE, 'data-r23': 'plan-loading' }, iv('reading the plan …'));
|
|
1198
|
-
if (bgPlan.state === 'error') return el('div', { style: NOTE + ';color:var(--destructive)', 'data-r23': 'plan-error' }, iv(bgPlan.error));
|
|
1199
|
-
const d = bgPlan.data; const c = bgPlan.c;
|
|
1200
|
-
const kids = [el('div', { style: 'font-size:12px', 'data-r23': 'plan-step' }, iv('Step: ' + d.item.key + ' — ' + d.item.text))];
|
|
1201
|
-
if (d.refused) { kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r23': 'plan-refused' }, iv(d.refused.why))); }
|
|
1202
|
-
else {
|
|
1203
|
-
kids.push(el('div', { style: NOTE, 'data-r23': 'plan-where' }, iv(d.plan.why || d.plan.plan)));
|
|
1204
|
-
kids.push(el('div', { style: NOTE, 'data-r23': 'plan-budget' }, iv(d.budget.running + ' of ' + d.budget.limits.concurrent + ' prepared sessions running · ' + d.budget.startedToday + ' of ' + d.budget.limits.perDay + ' started today. The session proposes only: no push, pull request, send, post, acceptance or merge. No outside source and no typed scoring.')));
|
|
1205
|
-
kids.push(el('pre', { 'data-r23': 'plan-prompt', style: 'white-space:pre-wrap;font-family:var(--mono);font-size:10.5px;max-height:140px;overflow:auto;border:1px solid var(--border);padding:6px;margin:4px 0' }, iv(d.prompt)));
|
|
1206
|
-
if (!d.background.enabled) kids.push(el('div', { style: NOTE, 'data-r23': 'plan-off' }, iv('Background prepare is off. Turn it on in the settings below to queue this.')));
|
|
1207
|
-
else kids.push(el('button', { style: BTN + ';color:var(--foreground);border-color:var(--foreground)', hover: 'background:var(--accent)', 'data-r23': 'queue', onClick: () => bgQueue(c, d) }, 'queue it (plan ' + d.prompt_sha + ')'));
|
|
1208
|
-
}
|
|
1209
|
-
return el('div', { 'data-r23': 'plan', style: 'padding:6px 14px 8px;border-bottom:1px solid var(--border)' }, kids);
|
|
1210
|
-
}
|
|
1211
|
-
function backgroundSection() {
|
|
1212
|
-
const r = read('prepare', '/api/proactive/prepare');
|
|
1213
|
-
const st = status(r, 'the background queue');
|
|
1214
|
-
const kids = [el('div', { style: LABEL }, iv('background preparation'))];
|
|
1215
|
-
if (st) return [...kids, st];
|
|
1216
|
-
const d = r.data;
|
|
1217
|
-
kids.push(el('div', { style: NOTE, 'data-r23': 'bg-state' }, iv((d.enabled ? 'On' : 'Off') + '. ' + d.budget.running + ' of ' + d.budget.limits.concurrent + ' prepared sessions running · ' + d.budget.startedToday + ' of ' + d.budget.limits.perDay + ' started today' + (d.quiet ? ' · quiet hours now' : '') + '. An approval is for one plan and expires in ' + d.expire_hours + ' hours. It runs only while the workspace server runs.')));
|
|
1218
|
-
if (bgNote) kids.push(el('div', { 'data-r23': 'bg-note', style: NOTE + ';color:' + (bgNote.tone === 'bad' ? 'var(--destructive)' : 'var(--foreground)') }, iv(bgNote.text)));
|
|
1219
|
-
if (!d.entries.length) kids.push(el('div', { style: NOTE, 'data-r23': 'bg-empty' }, iv('Nothing has been queued.')));
|
|
1220
|
-
for (const e of d.entries) {
|
|
1221
|
-
kids.push(el('div', { 'data-r23': 'entry', 'data-id': e.id, 'data-status': e.status, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1222
|
-
el('span', { style: 'display:flex;gap:8px;align-items:baseline' },
|
|
1223
|
-
el('span', { style: 'font-size:12px' }, iv(e.label)),
|
|
1224
|
-
el('span', { style: 'margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted-foreground)' }, iv(e.status))),
|
|
1225
|
-
e.reason ? el('span', { style: 'display:block;font-size:11px;color:var(--muted-foreground)', 'data-r23': 'entry-reason' }, iv(e.reason)) : null,
|
|
1226
|
-
el('span', { style: 'display:flex;gap:4px;margin-top:3px' },
|
|
1227
|
-
e.status === 'queued' ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'cancel', onClick: () => bgCancel(e) }, 'cancel') : null,
|
|
1228
|
-
e.status === 'started' && e.session_id ? el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r23': 'switch', onClick: async () => { const ini = await import('./panel-initiatives.js'); ini.openTerm(null, { id: e.session_id, name: e.label }); } }, 'switch to it') : null)));
|
|
1229
|
-
}
|
|
1230
|
-
if (d.entries.some((e) => e.status === 'queued')) kids.push(el('div', { style: 'padding:4px 14px' }, el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'look at the queue now; ignores quiet hours only', 'data-r23': 'run', onClick: bgRun }, 'run the queue now')));
|
|
1231
|
-
return kids;
|
|
1232
|
-
}
|
|
1233
|
-
/* learning (proactive-engine D4): bounded weights, a weekly rollup, lessons that wait for the PM's press, triggers left off */
|
|
1234
|
-
let lNote = null;
|
|
1235
|
-
async function lPost(url, body, say) {
|
|
1236
|
-
const out = await post(url, body);
|
|
1237
|
-
lNote = !out || out.ok === false ? { tone: 'bad', text: (out && out.error) || 'not done — nothing changed' } : { tone: 'ok', text: typeof say === 'function' ? say(out) : say };
|
|
1238
|
-
reload('learn'); reload('today'); setState({});
|
|
1239
|
-
}
|
|
1240
|
-
const lRate = (x, what) => x.rate === null ? what + ': not enough evidence (' + x.n + ' of ' + x.min + ' needed)' : what + ': ' + Math.round(x.rate * 100) + '% (' + x.n + ' counted)';
|
|
1241
|
-
function learningSection() {
|
|
1242
|
-
const r = read('learn', '/api/proactive/learn');
|
|
1243
|
-
const st = status(r, 'what it learned');
|
|
1244
|
-
const kids = [el('div', { style: LABEL }, iv('learning'))];
|
|
1245
|
-
if (st) return [...kids, st];
|
|
1246
|
-
const d = r.data; const ro = d.rollup;
|
|
1247
|
-
const b = (label, attr, fn, title) => el('button', { style: BTN, hover: 'color:var(--foreground)', title: title || null, 'data-r26': attr, onClick: fn }, label);
|
|
1248
|
-
kids.push(el('div', { style: NOTE, 'data-r26': 'state' }, iv((d.paused ? 'Paused: nothing is weighted, narrowed or proposed, and feedback is still recorded. ' : 'On. ') + d.evidenceCount + ' piece(s) of evidence' + (d.resetAt ? ' since you reset it' : '') + '. Computed from your own feedback and review decisions on this computer; no model is called. It cannot lower or hide what blocks on you.')));
|
|
1249
|
-
kids.push(el('div', { style: NOTE, 'data-r26': 'rollup' }, iv('Last ' + ro.days + ' days — ' + lRate(ro.hit, 'hit rate') + ' · ' + lRate(ro.asIs, 'used as is') + ' · ' + lRate(ro.corrections, 'corrections per prepared run') + ' · ' + ro.missed + ' reported missed')));
|
|
1250
|
-
kids.push(el('div', { style: NOTE + (ro.watermelon.flag ? ';color:var(--destructive)' : ''), 'data-r26': 'watermelon' }, iv('Watermelon check: ' + ro.watermelon.note + (ro.watermelon.reasons.length ? ' ' + ro.watermelon.reasons.join('; ') + '.' : ''))));
|
|
1251
|
-
const cells = d.cells.filter((c) => c.delta);
|
|
1252
|
-
if (!cells.length) kids.push(el('div', { style: NOTE, 'data-r26': 'no-weights' }, iv('No weight has moved yet. A cell moves after ' + r.data.min_cell + ' events, by at most ' + d.caps.kind + ' points per trigger, ' + d.caps.lane + ' per lane and ' + d.caps.time + ' per trigger and time of day, ' + d.caps.total + ' in all.')));
|
|
1253
|
-
for (const c of cells) kids.push(el('div', { style: 'padding:2px 14px;font-size:11.5px', 'data-r26': 'weight' }, iv(c.dim + ' ' + c.key.replace('|', ' in the ') + ': ' + (c.delta > 0 ? '+' : '') + c.delta + ' (' + c.n + ' events, net ' + c.sum + ')')));
|
|
1254
|
-
for (const n of d.narrowed) kids.push(el('div', { 'data-r26': 'narrowed', 'data-kind': n.kind, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1255
|
-
el('span', { style: 'display:block;font-size:12px' }, iv('Left off Today: ' + n.label.toLowerCase() + '. Useful ' + Math.round(n.rate * 100) + '% over ' + n.n + ' ratings in ' + n.days + ' days; the floor is ' + Math.round(n.floor * 100) + '%. They are still scanned.')),
|
|
1256
|
-
b('restore', 'restore', () => lPost('/api/proactive/learn/restore', { kind: n.kind, confirm: true }, 'restored — it stays until 10 new ratings of it have arrived'))));
|
|
1257
|
-
const proposed = d.lessons.filter((x) => x.status === 'proposed');
|
|
1258
|
-
kids.push(el('div', { style: LABEL }, iv('lessons to approve')));
|
|
1259
|
-
if (!proposed.length) kids.push(el('div', { style: NOTE, 'data-r26': 'no-lessons' }, iv('None waiting. A weekly review proposes lessons; none enters anything until you approve it.')));
|
|
1260
|
-
for (const l of proposed) kids.push(el('div', { 'data-r26': 'lesson', 'data-id': l.id, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1261
|
-
el('span', { style: 'display:block;font-size:12px' }, iv(l.text)),
|
|
1262
|
-
el('span', { style: 'display:block;font-size:10.5px;color:var(--muted-foreground);margin:2px 0 4px' }, iv(l.evidence)),
|
|
1263
|
-
el('span', { style: 'display:flex;gap:4px' },
|
|
1264
|
-
b('approve', 'approve', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'approve', confirm: true }, 'approved — the morning-brief session will read it'), 'the morning-brief session reads it; nothing else does'),
|
|
1265
|
-
b('reject', 'reject', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'reject', confirm: true }, 'rejected — it is not proposed again for 28 days')))));
|
|
1266
|
-
if (d.approved.length) kids.push(el('div', { style: LABEL }, iv('approved lessons (' + d.approved.length + ' of ' + d.max_approved + ')')));
|
|
1267
|
-
for (const l of d.approved) kids.push(el('div', { 'data-r26': 'approved', 'data-id': l.id, style: 'padding:6px 14px;border-bottom:1px solid var(--border)' },
|
|
1268
|
-
el('span', { style: 'display:block;font-size:12px' }, iv(l.text)),
|
|
1269
|
-
b('retire', 'retire', () => lPost('/api/proactive/learn/lesson', { id: l.id, decision: 'retire', confirm: true }, 'retired'))));
|
|
1270
|
-
if (lNote) kids.push(el('div', { 'data-r26': 'note', style: NOTE + ';color:' + (lNote.tone === 'bad' ? 'var(--destructive)' : 'var(--foreground)') }, iv(lNote.text)));
|
|
1271
|
-
const last = d.snapshots[0];
|
|
1272
|
-
kids.push(el('div', { style: NOTE, 'data-r26': 'last-run' }, iv(last ? 'Last weekly review: ' + String(last.at).replace('T', ' ').slice(0, 16) + ' UTC. It runs about every 7 days, only while the workspace server runs, and does not catch up.' : 'No weekly review has run yet. It runs about every 7 days once there is feedback, only while the workspace server runs, and does not catch up.')));
|
|
1273
|
-
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:2px 14px 6px' },
|
|
1274
|
-
b('run the weekly review now', 'run', () => lPost('/api/proactive/learn/run', {}, (o) => 'review taken — ' + o.proposed.length + ' lesson(s) proposed'), 'refused while learning is paused'),
|
|
1275
|
-
b('forget what it learned', 'reset', () => { if (typeof window !== 'undefined' && window.confirm && !window.confirm('Forget what it learned? Earlier feedback stops counting. Nothing is deleted.')) return; lPost('/api/proactive/learn/reset', { confirm: true }, 'reset — earlier feedback is ignored from now on, not deleted'); }, 'earlier feedback stops counting; nothing is deleted')));
|
|
1276
|
-
return kids;
|
|
1277
|
-
}
|
|
1278
|
-
export function todayList() {
|
|
1279
|
-
const r = read('today', '/api/proactive/today');
|
|
1280
|
-
const kids = [el('div', { style: NOTE, 'data-r22': 'intro' }, iv('What is waiting for you, worked out from the ledgers and git on this computer. It runs only while the workspace server runs, reads no mail or calendar, and changes nothing: your feedback only changes how this list is ordered later.'))];
|
|
1281
|
-
kids.push(...briefSection());
|
|
1282
|
-
kids.push(el('div', { style: LABEL }, iv('waiting on records')));
|
|
1283
|
-
kids.push(el('div', { style: 'display:flex;gap:4px;flex-wrap:wrap;padding:4px 14px' },
|
|
1284
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'scan', onClick: tScan }, 'scan now'),
|
|
1285
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', title: 'read the brief and the list again', 'data-r22': 'refresh', onClick: () => { reload('today'); reload('brief'); reload('learn'); setState({}); } }, '↻ refresh'),
|
|
1286
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed', onClick: () => { tMissed = !tMissed; setState({}); } }, 'something was missed')));
|
|
1287
|
-
if (tMissed) {
|
|
1288
|
-
const ta = tInput('missed', { type: 'text', placeholder: 'what should it have shown you?', 'data-r22': 'missed-input', style: 'flex:1;min-width:0;font-size:11.5px;padding:4px 8px;border:1px solid var(--border);background:var(--background);color:var(--foreground);min-height:28px' }, '');
|
|
1289
|
-
kids.push(el('div', { style: 'display:flex;gap:4px;padding:2px 14px 6px' }, ta,
|
|
1290
|
-
el('button', { style: BTN, hover: 'color:var(--foreground)', 'data-r22': 'missed-save', onClick: async () => { await tFeedback({ verdict: 'missed', note: ta.value }, 'recorded as missed'); if (tNote && tNote.tone === 'ok') { tMissed = false; delete ta.dataset.dirty; } setState({}); } }, 'save')));
|
|
1291
|
-
}
|
|
1292
|
-
if (tNote) kids.push(el('div', { 'data-r22': 'note', style: NOTE + ';color:' + (tNote.tone === 'bad' ? 'var(--destructive)' : tNote.tone === 'ok' ? 'var(--foreground)' : 'var(--muted-foreground)') }, iv(tNote.text)));
|
|
1293
|
-
const st = status(r, 'Today');
|
|
1294
|
-
if (st) return [...kids, st];
|
|
1295
|
-
const d = r.data;
|
|
1296
|
-
const s = d.settings;
|
|
1297
|
-
if (d.off) kids.push(el('div', { style: NOTE, 'data-r22': 'off' }, iv('The engine is switched off. Nothing is scanned automatically; "scan now" still works.')));
|
|
1298
|
-
if (d.quiet) kids.push(el('div', { style: NOTE, 'data-r22': 'quiet' }, iv('Quiet hours (' + s.quiet.start + '–' + s.quiet.end + '): no automatic scans now.')));
|
|
1299
|
-
// a repository still being read is asked again in a few seconds, so the list fills in without a press
|
|
1300
|
-
if ((d.unreadable || []).length && !tRetry && typeof setTimeout === 'function') tRetry = setTimeout(() => { tRetry = null; reload('today'); setState({}); }, 4000);
|
|
1301
|
-
for (const u of d.unreadable || []) kids.push(el('div', { style: NOTE + ';color:var(--destructive)', 'data-r22': 'unreadable' }, iv('Not read — ' + (u.name || u.repo || 'a repository') + ': ' + (u.error || 'unknown reason') + '. Nothing is shown for it, which is not the same as nothing waiting.')));
|
|
1302
|
-
if (d.empty) kids.push(el('div', { style: NOTE, 'data-r22': 'empty' }, iv('Nothing is waiting on any repository that was read.')));
|
|
1303
|
-
for (const c of d.candidates || []) { kids.push(tCard(c)); if (bgPlan && bgPlan.cid === c.id) kids.push(bgPlanView()); }
|
|
1304
|
-
for (const n of d.narrowed || []) kids.push(el('div', { style: NOTE, 'data-r26': 'narrowed-note' }, iv(n.hidden + ' ' + n.label.toLowerCase() + ' hidden: you rated them useful only ' + Math.round(n.rate * 100) + '% of the time. Restore them under learning below.')));
|
|
1305
|
-
kids.push(...backgroundSection());
|
|
1306
|
-
if (d.dismissed) kids.push(el('div', { style: NOTE, 'data-r22': 'dismissed' }, iv(d.dismissed + ' dismissed item(s) hidden for 7 days.')));
|
|
1307
|
-
// settings
|
|
1308
|
-
kids.push(el('div', { style: LABEL }, iv('settings')));
|
|
1309
|
-
const chk = (label, on, attr, patch) => el('label', { style: 'display:flex;gap:8px;align-items:center;padding:2px 14px;font-size:12px' },
|
|
1310
|
-
el('input', { type: 'checkbox', checked: on ? 'checked' : null, 'data-r22': attr, onChange: () => tSettings(patch(!on)) }), iv(label));
|
|
1311
|
-
kids.push(chk('engine on', s.enabled, 'set-enabled', (v) => ({ enabled: v })));
|
|
1312
|
-
for (const t of Object.keys(s.triggers)) kids.push(chk('watch: ' + t, s.triggers[t], 'set-' + t, (v) => ({ triggers: { [t]: v } })));
|
|
1313
|
-
kids.push(chk('background prepare (starts approved sessions by itself)', s.backgroundPrepare, 'set-bg', (v) => ({ backgroundPrepare: v })));
|
|
1314
|
-
kids.push(el('div', { style: LABEL }, iv('morning brief')));
|
|
1315
|
-
for (const k of Object.keys(s.briefSources || {})) kids.push(chk('read ' + k + (k === 'mail' ? ' (titles and senders only)' : ''), s.briefSources[k], 'set-src-' + k, (v) => ({ briefSources: { [k]: v } })));
|
|
1316
|
-
kids.push(chk('typed scoring (sends titles and metadata to the Typesafe service)', s.briefScoring, 'set-scoring', (v) => ({ briefScoring: v })));
|
|
1317
|
-
const nm = tInput('name', { type: 'text', maxlength: '40', placeholder: 'your name for the greeting', 'data-r22': 'set-name', style: 'width:180px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.name || '');
|
|
1318
|
-
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;padding:4px 14px;font-size:12px' }, nm, el('button', { style: BTN, 'data-r22': 'set-name-save', onClick: () => tSaveFrom([nm], { name: nm.value }) }, 'save')));
|
|
1319
|
-
kids.push(...learningSection());
|
|
1320
|
-
kids.push(chk('pause learning (no weights, no lessons, nothing narrowed)', s.pauseLearning, 'set-pause', (v) => ({ pauseLearning: v })));
|
|
1321
|
-
const days = tInput('stalled', { type: 'number', min: '1', max: '90', 'data-r22': 'set-stalled-days', style: 'width:60px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, String(s.stalledDays));
|
|
1322
|
-
const qs = tInput('quiet-start', { type: 'text', placeholder: '22:00', 'data-r22': 'set-quiet-start', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.quiet ? s.quiet.start : '');
|
|
1323
|
-
const qe = tInput('quiet-end', { type: 'text', placeholder: '07:00', 'data-r22': 'set-quiet-end', style: 'width:56px;font-size:11.5px;padding:2px 6px;border:1px solid var(--border);background:var(--background);color:var(--foreground)' }, s.quiet ? s.quiet.end : '');
|
|
1324
|
-
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px;font-size:12px' }, iv('stalled after'), days, iv('days'),
|
|
1325
|
-
el('button', { style: BTN, 'data-r22': 'set-stalled-save', onClick: () => tSaveFrom([days], { stalledDays: Number(days.value) }) }, 'save')));
|
|
1326
|
-
kids.push(el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap;padding:4px 14px 8px;font-size:12px' }, iv('quiet hours'), qs, iv('to'), qe,
|
|
1327
|
-
el('button', { style: BTN, 'data-r22': 'set-quiet-save', onClick: () => tSaveFrom([qs, qe], { quiet: qs.value.trim() || qe.value.trim() ? { start: qs.value.trim(), end: qe.value.trim() } : null }) }, 'save')));
|
|
1328
|
-
return [el('div', { style: 'padding:2px 0 8px', 'data-r22': 'today' }, kids)];
|
|
1329
|
-
}
|
|
453
|
+
// { cid, state: 'loading'|'ok'|'error', data, error }
|