geneseed 4.9.0 → 4.9.2
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/CHANGELOG.md +40 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +7 -1
- package/adapters/opencode/plugins/geneseed-guard.js +139 -27
- package/adapters/opencode/plugins/geneseed-learn.js +7 -1
- package/docs/concepts/hooks.md +1 -1
- package/docs/web-ui.md +3 -2
- package/harness.config.json +1 -1
- package/js/README.md +30 -9
- package/js/build/args.mjs +9 -8
- package/js/build/driver.mjs +21 -21
- package/js/build/emit-claude.mjs +3 -3
- package/js/build/generate.mjs +4 -5
- package/js/build/source.mjs +18 -22
- package/js/hosts/gitref.mjs +5 -3
- package/js/hosts/hooks-done.mjs +21 -9
- package/js/hosts/hooks-learn.mjs +23 -10
- package/js/hosts/hooks-prims.mjs +25 -0
- package/js/hosts/hooks.mjs +17 -8
- package/js/hosts/installs.mjs +5 -4
- package/js/hosts/loss.mjs +124 -18
- package/js/hosts/native.mjs +3 -1
- package/js/hosts/owned.mjs +34 -6
- package/js/hosts/settings.mjs +5 -4
- package/js/inspect/cost.mjs +37 -5
- package/js/inspect/diff.mjs +132 -61
- package/js/inspect/doctor.mjs +4 -3
- package/js/inspect/validate.mjs +2 -2
- package/js/lib/fs.mjs +73 -2
- package/js/maintain/setup.mjs +31 -7
- package/js/maintain/uninstall.mjs +1 -1
- package/js/maintain/update.mjs +1 -1
- package/js/web/actions.mjs +38 -4
- package/js/web/api.mjs +21 -13
- package/js/web/handler.mjs +5 -17
- package/js/web/jobs.mjs +5 -3
- package/js/web/routes.mjs +3 -1
- package/js/web/server.mjs +3 -1
- package/js/web/state.mjs +35 -8
- package/package.json +1 -1
- package/web/dist/assets/{Activity-DZ3O_DsJ.js → Activity-D7Igwuoo.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BQSWwoJ-.js → ActivityDetail-53rp05hx.js} +1 -1
- package/web/dist/assets/Dashboard-BoEH5qyY.js +2 -0
- package/web/dist/assets/{Docs-DUxizutI.js → Docs-BM56RCT8.js} +3 -3
- package/web/dist/assets/GreenhouseView-BTMvGVco.js +2 -0
- package/web/dist/assets/Installs-Dh1tB1NK.js +2 -0
- package/web/dist/assets/JournalView-F4hlWTVB.js +1 -0
- package/web/dist/assets/Laws-iEheocNI.js +3 -0
- package/web/dist/assets/Library-d4_uOGmC.js +1 -0
- package/web/dist/assets/{Loops-Bzb2Y96W.js → Loops-BbYeWl70.js} +2 -2
- package/web/dist/assets/{OperatorHudView-D-nzB6_r.js → OperatorHudView-Dki9Q6W3.js} +1 -1
- package/web/dist/assets/Personal-B_YQJjA_.js +1 -0
- package/web/dist/assets/{Tabs-DkPfjG3H.js → Tabs-CAl6oFwG.js} +1 -1
- package/web/dist/assets/api-CiXlkbe0.js +1 -0
- package/web/dist/assets/drawable-BdTDG5oI.js +1 -0
- package/web/dist/assets/index-BZgEGRbL.js +2 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +60 -353
- package/web/dist/assets/Dashboard-COLskkOJ.js +0 -2
- package/web/dist/assets/GreenhouseView-BSzVX_l7.js +0 -2
- package/web/dist/assets/Installs-Ck4YyEzs.js +0 -2
- package/web/dist/assets/JournalView-BbbACsYK.js +0 -1
- package/web/dist/assets/Laws-BZ_M86r3.js +0 -3
- package/web/dist/assets/Library-D_4LbUg2.js +0 -1
- package/web/dist/assets/Personal-C66284wd.js +0 -1
- package/web/dist/assets/api-Bp5eW_6m.js +0 -1
- package/web/dist/assets/drawable-DlrXDOJ7.js +0 -1
- package/web/dist/assets/index-DJorAjeH.js +0 -2
package/js/inspect/doctor.mjs
CHANGED
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* that this tool never shells back to the generator — is asserted directly instead.
|
|
26
26
|
*
|
|
27
27
|
* WHAT `doctorCollect` DOES NOT REACH, MEASURED RATHER THAN ASSUMED. It takes `groups` and
|
|
28
|
-
* `cmdDoctor` does not pass it: that parameter is the WEB's, and `
|
|
29
|
-
* `js/web/
|
|
30
|
-
* keep the Doctor page's per-check cards a parameter rather
|
|
28
|
+
* `cmdDoctor` does not pass it: that parameter is the WEB's, and `runDoctor` in
|
|
29
|
+
* `js/web/state.mjs` (the cache `apiDoctor` serves) is its only caller. `ran` exists for it —
|
|
30
|
+
* four lines of accumulator that keep the Doctor page's per-check cards a parameter rather
|
|
31
|
+
* than a rewrite.
|
|
31
32
|
*/
|
|
32
33
|
import path from 'node:path';
|
|
33
34
|
import { emitGlobalInto, emitProjectInto, main as driverMain } from '../build/driver.mjs';
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -130,8 +130,8 @@ export function cmdValidate(args) {
|
|
|
130
130
|
const opts = {
|
|
131
131
|
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
132
132
|
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
133
|
-
excludeSkills: args.excludeSkills,
|
|
134
|
-
|
|
133
|
+
excludeSkills: args.excludeSkills,
|
|
134
|
+
...Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, args[g.arg] ? false : null])),
|
|
135
135
|
};
|
|
136
136
|
let scanDirs;
|
|
137
137
|
try {
|
package/js/lib/fs.mjs
CHANGED
|
@@ -29,8 +29,32 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import {
|
|
31
31
|
appendFileSync, writeFileSync, readFileSync, copyFileSync, statSync, utimesSync,
|
|
32
|
+
openSync, fstatSync, readSync, closeSync,
|
|
32
33
|
} from 'node:fs';
|
|
33
34
|
import { EOL } from 'node:os';
|
|
35
|
+
import { resolve } from 'node:path';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What `writeText` wrote while an emit has armed this (`withWriteRecord` below, around the
|
|
39
|
+
* three manifest-writing emits in `js/build/driver.mjs`): resolved path ->
|
|
40
|
+
* `{ text, size, mtimeMs }`, the translated text and a stat taken right after the write.
|
|
41
|
+
* `ownedHashes` (`js/hosts/owned.mjs`) hashes that text instead of reading the file back —
|
|
42
|
+
* the read-back was ~25% of `doctor --all` — and only while the file's size and mtime still
|
|
43
|
+
* match, so a later rewrite by any writer that does not record (a raw `writeFileSync`, a
|
|
44
|
+
* rename) falls back to the read. `copyFile` and `appendText` drop the entry outright.
|
|
45
|
+
* `null`, the default, records nothing: no other caller pays the stat or retains the text.
|
|
46
|
+
*/
|
|
47
|
+
export const textWrites = { map: null };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run one emit with `textWrites` armed, and disarm it on EVERY exit: an emit that throws inside
|
|
51
|
+
* a long-lived process (the web daemon emits in-process) would otherwise leave every later
|
|
52
|
+
* `writeText` paying a stat and keeping its text in memory.
|
|
53
|
+
*/
|
|
54
|
+
export function withWriteRecord(fn) {
|
|
55
|
+
textWrites.map = new Map();
|
|
56
|
+
try { return fn(); } finally { textWrites.map = null; }
|
|
57
|
+
}
|
|
34
58
|
/**
|
|
35
59
|
* `Path.write_text(text, encoding="utf-8")`.
|
|
36
60
|
*
|
|
@@ -49,7 +73,12 @@ import { EOL } from 'node:os';
|
|
|
49
73
|
* The same vendored skill folder can contain files from both categories.
|
|
50
74
|
*/
|
|
51
75
|
export function writeText(path, text) {
|
|
52
|
-
|
|
76
|
+
const out = EOL === '\n' ? text : text.replaceAll('\n', EOL);
|
|
77
|
+
writeFileSync(path, out, 'utf8');
|
|
78
|
+
if (textWrites.map) {
|
|
79
|
+
const st = statSync(path);
|
|
80
|
+
textWrites.map.set(resolve(path), { text: out, size: st.size, mtimeMs: st.mtimeMs });
|
|
81
|
+
}
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
/**
|
|
@@ -62,6 +91,7 @@ export function writeText(path, text) {
|
|
|
62
91
|
* invisible in an editor and a whole-file difference to the gate.
|
|
63
92
|
*/
|
|
64
93
|
export function appendText(path, text) {
|
|
94
|
+
textWrites.map?.delete(resolve(path));
|
|
65
95
|
appendFileSync(path, EOL === '\n' ? text : text.replaceAll('\n', EOL), 'utf8');
|
|
66
96
|
}
|
|
67
97
|
|
|
@@ -79,6 +109,46 @@ export function readText(path) {
|
|
|
79
109
|
return readFileSync(path, 'utf8').replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
80
110
|
}
|
|
81
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The last `bytes` of a file, decoded as `readText` decodes it, and the byte offset the window
|
|
114
|
+
* began at (0: the whole file). A window that starts mid-line drops that partial line, cut at a
|
|
115
|
+
* newline BYTE before decoding — so a multibyte character the window split falls in the dropped
|
|
116
|
+
* part, and a `\r\n` split between its two bytes leaves only an empty line. One byte before the
|
|
117
|
+
* window is read too, so a window that starts exactly on a line keeps it. Throws as
|
|
118
|
+
* `readFileSync` does (missing, unreadable, a directory).
|
|
119
|
+
*/
|
|
120
|
+
export function readTail(path, bytes) {
|
|
121
|
+
const fd = openSync(path, 'r');
|
|
122
|
+
try {
|
|
123
|
+
const st = fstatSync(fd);
|
|
124
|
+
// Windows opens a directory without complaint and reports size 0: refuse it as readFileSync does.
|
|
125
|
+
if (st.isDirectory()) {
|
|
126
|
+
throw Object.assign(new Error(`EISDIR: illegal operation on a directory, read '${path}'`),
|
|
127
|
+
{ code: 'EISDIR' });
|
|
128
|
+
}
|
|
129
|
+
const size = st.size;
|
|
130
|
+
const start = Math.max(0, size - bytes);
|
|
131
|
+
const from = start > 0 ? start - 1 : 0;
|
|
132
|
+
const buf = Buffer.alloc(size - from);
|
|
133
|
+
let got = 0;
|
|
134
|
+
while (got < buf.length) {
|
|
135
|
+
const n = readSync(fd, buf, got, buf.length - got, from + got);
|
|
136
|
+
if (!n) break;
|
|
137
|
+
got += n;
|
|
138
|
+
}
|
|
139
|
+
let cut = 0;
|
|
140
|
+
if (start > 0) {
|
|
141
|
+
const read = buf.subarray(0, got);
|
|
142
|
+
const ends = [read.indexOf(0x0a), read.indexOf(0x0d)].filter((i) => i !== -1);
|
|
143
|
+
cut = ends.length ? Math.min(...ends) + 1 : got; // no line end: nothing whole to decode
|
|
144
|
+
}
|
|
145
|
+
const text = buf.toString('utf8', cut, got).replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
146
|
+
return { text, start };
|
|
147
|
+
} finally {
|
|
148
|
+
closeSync(fd);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
82
152
|
/**
|
|
83
153
|
* `shutil.copy2(src, dest)`.
|
|
84
154
|
*
|
|
@@ -94,6 +164,7 @@ export function readText(path) {
|
|
|
94
164
|
* mode the Python side has and nothing depends on. Mode is not copied here.
|
|
95
165
|
*/
|
|
96
166
|
export function copyFile(src, dest) {
|
|
167
|
+
textWrites.map?.delete(resolve(dest));
|
|
97
168
|
copyFileSync(src, dest);
|
|
98
169
|
const st = statSync(src);
|
|
99
170
|
utimesSync(dest, st.atime, st.mtime);
|
|
@@ -216,7 +287,7 @@ export function withPlatformNewlines(fn) {
|
|
|
216
287
|
*/
|
|
217
288
|
const NOT_THERE = new Set(['ENOENT', 'ENOTDIR', 'ELOOP', 'EBADF']);
|
|
218
289
|
|
|
219
|
-
function statOrNull(p) {
|
|
290
|
+
export function statOrNull(p) {
|
|
220
291
|
try {
|
|
221
292
|
return statSync(p);
|
|
222
293
|
} catch (e) {
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -46,13 +46,13 @@ import path from 'node:path';
|
|
|
46
46
|
|
|
47
47
|
import { main as driverMain, resolveOut } from '../build/driver.mjs';
|
|
48
48
|
import { playLine } from '../ui/anim.mjs';
|
|
49
|
-
import { discoverNames, PACK_ORDER } from '../build/source.mjs';
|
|
49
|
+
import { discoverNames, GATE_SWITCHES, PACK_ORDER } from '../build/source.mjs';
|
|
50
50
|
import { exportImprovements } from '../inspect/diff.mjs';
|
|
51
51
|
import { cmdDoctor } from '../inspect/doctor.mjs';
|
|
52
52
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
53
53
|
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
54
54
|
import {
|
|
55
|
-
claudeCfg, defaultMode, defaultPosture, defaultTheme, EMIT_HOST_SCOPE, installedDefaults,
|
|
55
|
+
claudeCfg, defaultMode, defaultPosture, defaultTheme, EMIT_HOST_SCOPE, gatesOfDir, installedDefaults,
|
|
56
56
|
readJsonMaybe, themeFiles,
|
|
57
57
|
} from '../hosts/installs.mjs';
|
|
58
58
|
import { printOut, printErr } from '../lib/fs.mjs';
|
|
@@ -242,6 +242,24 @@ function askDoctrines(deployed = null) {
|
|
|
242
242
|
* whole, `null` included, because `askDoctrines` distinguishes "no carrier said" from "a
|
|
243
243
|
* carrier said none" and the two open the question differently.
|
|
244
244
|
*/
|
|
245
|
+
/**
|
|
246
|
+
* The gate switches of the one install a setup answer re-runs: the chosen emit's global config
|
|
247
|
+
* dir, or the project/bundle root it names. Never another host's install — carrying its `off`
|
|
248
|
+
* across would disarm a gate nobody switched off here. FAILS CLOSED: no install there, or a dir
|
|
249
|
+
* that cannot be resolved, reads as all armed (`gatesOfDir` already fails closed per line).
|
|
250
|
+
*/
|
|
251
|
+
function targetGates(emit, out) {
|
|
252
|
+
const [host, scope] = EMIT_HOST_SCOPE.get(emit) ?? [];
|
|
253
|
+
try {
|
|
254
|
+
if (!host) return gatesOfDir(resolveOut(out || 'Harness'));
|
|
255
|
+
const dir = scope === 'global'
|
|
256
|
+
? HOSTS.find((h) => h.host === host).configDir() : resolveOut(out || '.');
|
|
257
|
+
return gatesOfDir(dir, host);
|
|
258
|
+
} catch {
|
|
259
|
+
return Object.fromEntries(GATE_SWITCHES.map((g) => [g.key, true]));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
245
263
|
export function collectSetupLines() {
|
|
246
264
|
printOut('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
|
|
247
265
|
const inst = installedDefaults();
|
|
@@ -279,14 +297,20 @@ export function collectSetupLines() {
|
|
|
279
297
|
const excludeRules = inst.excludeRules?.length ? inst.excludeRules : null;
|
|
280
298
|
// Skills left out are preserved the same way, and elide when there are none.
|
|
281
299
|
const excludeSkills = inst.excludeSkills?.length ? inst.excludeSkills : null;
|
|
300
|
+
// The gate switches too: a re-run that dropped them would re-arm every gate its owner switched
|
|
301
|
+
// off. Read off the install THIS answer targets, never the machine walk above (`targetGates`).
|
|
302
|
+
// `setupBuildArgs` elides an armed gate, so an all-armed install prints what it always did.
|
|
303
|
+
const gates = targetGates(emit, out);
|
|
304
|
+
const gated = Object.values(gates).some((on) => on === false);
|
|
282
305
|
printOut(`\nAbout to run: geneseed build ${
|
|
283
306
|
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
|
|
284
|
-
excludeRules, undefined, excludeSkills).join(' ')}\n`);
|
|
307
|
+
excludeRules, undefined, excludeSkills, gates).join(' ')}\n`);
|
|
285
308
|
if (!confirm('Proceed?', true)) return null;
|
|
286
|
-
// The key only when there is something to carry, so a selection that excludes no skill
|
|
287
|
-
// the same object it was before
|
|
309
|
+
// The key only when there is something to carry, so a selection that excludes no skill and
|
|
310
|
+
// switches off no gate is the same object it was before either existed.
|
|
288
311
|
return {
|
|
289
312
|
theme, posture, mode, doctrines, excludeRules, ...(excludeSkills ? { excludeSkills } : {}),
|
|
313
|
+
...(gated ? { gates } : {}),
|
|
290
314
|
emit, out, root, footprint,
|
|
291
315
|
};
|
|
292
316
|
}
|
|
@@ -470,7 +494,7 @@ export function setupLines() {
|
|
|
470
494
|
}
|
|
471
495
|
const {
|
|
472
496
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
473
|
-
doctrines = null, excludeRules = null, excludeSkills = null,
|
|
497
|
+
doctrines = null, excludeRules = null, excludeSkills = null, gates = {},
|
|
474
498
|
} = sel;
|
|
475
499
|
if (emit === 'opencode-global') {
|
|
476
500
|
// The build below overwrites the deployed global harness, and the self-improvement loops
|
|
@@ -489,7 +513,7 @@ export function setupLines() {
|
|
|
489
513
|
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
490
514
|
// one command and run another.
|
|
491
515
|
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
492
|
-
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills);
|
|
516
|
+
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates);
|
|
493
517
|
printOut(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
494
518
|
const rc = driverMain(argv);
|
|
495
519
|
if (rc !== 0) {
|
|
@@ -1179,7 +1179,7 @@ function remergeClaudeHooks(cfg, root = cfg, host = 'claude') {
|
|
|
1179
1179
|
const gates = Object.fromEntries(Object.keys(atRoot).map((k) => [k, atRoot[k] && atCfg[k]]));
|
|
1180
1180
|
const [, claims] = mergeClaudeSettings(settingsFile(cfg, managed),
|
|
1181
1181
|
managed.settings_hooks ?? null, hookRunnerEntry(), doctrines, excluded, host, cfg,
|
|
1182
|
-
gates
|
|
1182
|
+
gates);
|
|
1183
1183
|
// `and data` — a manifest that did not parse is not one to write back.
|
|
1184
1184
|
if (!deepEquals(claims, managed.settings_hooks ?? null) && Object.keys(data).length > 0) {
|
|
1185
1185
|
managed.settings_hooks = claims;
|
package/js/maintain/update.mjs
CHANGED
|
@@ -632,7 +632,7 @@ export async function installAxes(dir, { installProfile, setupBuildArgs }, notif
|
|
|
632
632
|
if (legacyProcessCarrierOfDir(String(dir), 'opencode')) return null;
|
|
633
633
|
const p = installProfile('opencode', 'global', String(dir), notify);
|
|
634
634
|
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
635
|
-
undefined, p.excludeRules, p.trust, p.excludeSkills, p
|
|
635
|
+
undefined, p.excludeRules, p.trust, p.excludeSkills, p).slice(4);
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
/**
|
package/js/web/actions.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
} from '../build/source.mjs';
|
|
39
39
|
import { withStdoutSwallowed } from '../inspect/diff.mjs';
|
|
40
40
|
import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
|
|
41
|
-
import { setupBuildArgs } from '../build/generate.mjs';
|
|
41
|
+
import { installProfile, setupBuildArgs } from '../build/generate.mjs';
|
|
42
42
|
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
43
43
|
import { activeLoops, setLoopPreset } from '../loop/registry.mjs';
|
|
44
44
|
import {
|
|
@@ -61,6 +61,7 @@ import { stripWhitespace } from '../lib/text.mjs';
|
|
|
61
61
|
import { memoryDelete } from '../maintain/memory.mjs';
|
|
62
62
|
import { installDeactivate, installReactivate, installUninstall } from '../maintain/uninstall.mjs';
|
|
63
63
|
import { emitChoices, themeChoices, viewCfg } from './api.mjs';
|
|
64
|
+
import { actionCommands } from './jobs.mjs';
|
|
64
65
|
import { NotFound, deployed, memoryDir } from './catalog.mjs';
|
|
65
66
|
|
|
66
67
|
/** One body field, `dflt` when the body is not an object or does not carry the key. */
|
|
@@ -307,7 +308,8 @@ export function apiMemoryDelete(state, name) {
|
|
|
307
308
|
// CASE-FOLDED: this endpoint's own compared `MEMORY` exactly, so on Windows and macOS `memory`
|
|
308
309
|
// resolved to MEMORY.md, passed, and deleted the index the agent reads at session start.
|
|
309
310
|
if (memoryDelete(d, name) === null) throw new NotFound(name);
|
|
310
|
-
|
|
311
|
+
// The doctor reads nothing in the memory store, so its cached verdict survives (`refresh`).
|
|
312
|
+
state.refresh({ doctor: false });
|
|
311
313
|
return { archived: name };
|
|
312
314
|
}
|
|
313
315
|
|
|
@@ -438,6 +440,38 @@ export function buildOverride(state, body) {
|
|
|
438
440
|
return [themes.has(t) ? t : state.theme, emits.has(e) ? e : state.emit];
|
|
439
441
|
}
|
|
440
442
|
|
|
443
|
+
/**
|
|
444
|
+
* The job steps for a Build POST.
|
|
445
|
+
*
|
|
446
|
+
* A DEPLOYED INSTALL REBUILT IN ITS OWN EMIT TAKES THE ARGV ITS PROFILE OWNS (`installProfile`,
|
|
447
|
+
* the one reader `rebuild-all`, `status` and `upgrade` share), with only the theme the picker
|
|
448
|
+
* chose swapped in. This path used to re-assemble the axes it happened to carry — no gate
|
|
449
|
+
* switches, `--exclude-rules none`, no excluded skills — so a console Build silently re-armed
|
|
450
|
+
* every switched-off gate and handed back every rule and skill its owner had left out.
|
|
451
|
+
*
|
|
452
|
+
* Nothing deployed (the first-deploy Onboarding form) or a re-target to another emit has no
|
|
453
|
+
* install of that emit to honour — a re-target is a FRESH target, by decision (2026-10-18), so it
|
|
454
|
+
* deliberately carries none of this install's switches or exclusions — and keeps the defaults it
|
|
455
|
+
* always had: footprint, posture
|
|
456
|
+
* and mode off the viewed dir, and ALL packs when unknown — never `harness.config.json`, which
|
|
457
|
+
* would drop the consent gate.
|
|
458
|
+
*/
|
|
459
|
+
export function buildActionCmds(state, body) {
|
|
460
|
+
const [theme, emit] = buildOverride(state, body);
|
|
461
|
+
const [host, scope] = EMIT_HOST_SCOPE.get(state.emit || '') ?? [];
|
|
462
|
+
if (host && deployed(state) && emit === state.emit) {
|
|
463
|
+
// `setupBuildArgs` always leads with `--theme <theme>`, so the swap is the head alone.
|
|
464
|
+
// `state.root`, not `target`: a project install's markers live at the repo root.
|
|
465
|
+
const [, , ...rest] = installProfile(host, scope, state.root).argv;
|
|
466
|
+
return actionCommands('build', { buildArgv: ['--theme', theme, ...rest] });
|
|
467
|
+
}
|
|
468
|
+
return actionCommands('build', {
|
|
469
|
+
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
470
|
+
trust: state.trust,
|
|
471
|
+
doctrines: doctrinesForBuild(state.target),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
441
475
|
/**
|
|
442
476
|
* The global emit matching a deployed install's `.geneseed-emit`, so the EXPECTED render
|
|
443
477
|
* uses the install's own host dialect.
|
|
@@ -594,7 +628,7 @@ export function apiInstallCmd(state, body) {
|
|
|
594
628
|
const gates = bodyGates(body, root, host);
|
|
595
629
|
const out = scope === 'global' ? null : String(root);
|
|
596
630
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
597
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates
|
|
631
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
|
|
598
632
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
599
633
|
}
|
|
600
634
|
|
|
@@ -696,6 +730,6 @@ export function apiDeployCmd(state, body) {
|
|
|
696
730
|
const gates = bodyGates(body, root, host);
|
|
697
731
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
698
732
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
699
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates
|
|
733
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
|
|
700
734
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
701
735
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -15,8 +15,7 @@ import { CONFIG, ROOT, THEMES, discoverNames, makeCfg } from '../build/source.mj
|
|
|
15
15
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
16
16
|
import { sourceReleaseVersion } from '../hosts/opencode.mjs';
|
|
17
17
|
import { alwaysOnCost } from '../inspect/cost.mjs';
|
|
18
|
-
import { diffCollect } from '../inspect/diff.mjs';
|
|
19
|
-
import { doctorCollect } from '../inspect/doctor.mjs';
|
|
18
|
+
import { diffCollect, renderMemo } from '../inspect/diff.mjs';
|
|
20
19
|
import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
21
20
|
import { CLAUDE_STYLE, HOSTS, VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
|
|
22
21
|
import {
|
|
@@ -79,13 +78,15 @@ export function apiSetup(state) {
|
|
|
79
78
|
};
|
|
80
79
|
}
|
|
81
80
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
/**
|
|
82
|
+
* The same engine as the `doctor` verb, grouped per check — served from the state's cached run
|
|
83
|
+
* (a lens visit or a page mount no longer pays ~3.6 s), and re-run only when `fresh` is asked
|
|
84
|
+
* for: the Doctor page's Re-run button, as `GET /api/doctor?fresh=1`.
|
|
85
|
+
*/
|
|
86
|
+
export function apiDoctor(state, fresh = false) {
|
|
87
|
+
if (fresh) state.runDoctor();
|
|
88
|
+
const { ok, problems, checked_at } = state.doctor;
|
|
89
|
+
return { ...state._doctorRun, ok, problems, checked_at };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
/**
|
|
@@ -129,6 +130,12 @@ export function* activeInstalls(targets) {
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The state's memo of `expected` renders, made on first use: overview and fleet diff the same
|
|
135
|
+
* install on every refresh, and only the compare has to run again (see `renderMemo`).
|
|
136
|
+
*/
|
|
137
|
+
const memoOf = (state) => (state.renderMemo ??= renderMemo());
|
|
138
|
+
|
|
132
139
|
/** The edited/added/missing tally of a `diffCollect` file list. */
|
|
133
140
|
const driftCounts = (files) => ({
|
|
134
141
|
edited: files.filter((f) => f.status === 'edited').length,
|
|
@@ -178,8 +185,9 @@ export function apiInstalls(state) {
|
|
|
178
185
|
*
|
|
179
186
|
* Its own route, not more fields on `/api/installs`: the sidebar refetches that list on every
|
|
180
187
|
* data revision, and the drift check below re-renders a whole emit per global install.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
188
|
+
* Each global install's `expected` render is memoised on the state (`renderMemo`, keyed on the
|
|
189
|
+
* source fingerprint computed once per request and every axis), so a refresh re-runs only the
|
|
190
|
+
* compare; the memo holds a handful of installs, enough for any machine seen so far.
|
|
183
191
|
*
|
|
184
192
|
* Each row reads ITS OWN `.geneseed-version`. `statusData().installed_fp` is the first carrier
|
|
185
193
|
* a fixed walk finds, which describes the wrong install as soon as two hosts are deployed.
|
|
@@ -196,7 +204,7 @@ export function apiFleet(state) {
|
|
|
196
204
|
let drift = null;
|
|
197
205
|
if (st === 'active') {
|
|
198
206
|
try {
|
|
199
|
-
const { files } = diffCollect({ target: dir });
|
|
207
|
+
const { files } = diffCollect({ target: dir, memo: memoOf(state), sourceFp });
|
|
200
208
|
if (files !== null) drift = driftCounts(files);
|
|
201
209
|
} catch { /* an install that cannot be re-rendered reports no drift figure, not zero */ }
|
|
202
210
|
}
|
|
@@ -225,7 +233,7 @@ export function apiOverview(state) {
|
|
|
225
233
|
let diff = null;
|
|
226
234
|
if (deployed(state)) {
|
|
227
235
|
const { files } = diffCollect({
|
|
228
|
-
target: state.target, theme: state.theme, emit: state.emit });
|
|
236
|
+
target: state.target, theme: state.theme, emit: state.emit, memo: memoOf(state) });
|
|
229
237
|
if (files !== null) diff = driftCounts(files);
|
|
230
238
|
}
|
|
231
239
|
let buildTime = null;
|
package/js/web/handler.mjs
CHANGED
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
* declared next door in `routes.mjs`; the daemon that starts it is in `daemon.mjs`.
|
|
12
12
|
*/
|
|
13
13
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
14
|
-
import { doctrinesForBuild } from '../hosts/installs.mjs';
|
|
15
14
|
import { isTruthy, jsonDumpsCompact } from '../lib/json.mjs';
|
|
16
15
|
import { parseIntStrict, percentDecode } from '../lib/text.mjs';
|
|
17
16
|
import { preflight } from '../maintain/update.mjs';
|
|
18
|
-
import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore,
|
|
17
|
+
import { apiDeployCmd, apiInstallCmd, apiPickFolder, apiRestore, buildActionCmds, mcpTargetPaths } from './actions.mjs';
|
|
19
18
|
import { NotFound } from './catalog.mjs';
|
|
20
19
|
import { openUrl, requestRestart } from './daemon.mjs';
|
|
21
20
|
import { apiDocs, apiDocsPage } from './docs.mjs';
|
|
@@ -130,7 +129,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
130
129
|
|
|
131
130
|
function doGet(req, res, path, ae) {
|
|
132
131
|
const route = STATE_ROUTES[path];
|
|
133
|
-
if (route !== undefined) return sendJson(res, route(state), 200, ae);
|
|
132
|
+
if (route !== undefined) return sendJson(res, route(state, req.url), 200, ae);
|
|
134
133
|
// `pid` is what a restarting client waits on: the OLD server keeps answering this ping for
|
|
135
134
|
// a moment after `/api/restart` returns, and a reload onto it fetches the old token.
|
|
136
135
|
if (path === '/api/ping') {
|
|
@@ -301,20 +300,9 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
301
300
|
if (jid === null) return sendJson(res, { error: 'busy' }, 409, ae);
|
|
302
301
|
return sendJson(res, { job_id: jid }, 202, ae);
|
|
303
302
|
}
|
|
304
|
-
// Build
|
|
305
|
-
// the deployed theme downstream.
|
|
306
|
-
|
|
307
|
-
const [theme, emit] = action === 'build'
|
|
308
|
-
? buildOverride(state, body) : [state.theme, state.emit];
|
|
309
|
-
// The pack selection follows the install like footprint/posture/mode, and is read here
|
|
310
|
-
// rather than left to the table's default: a console Build must re-emit the constitution
|
|
311
|
-
// the deployment already carries. Unknown (no `Active packs:` marker) resolves to ALL
|
|
312
|
-
// packs — never to `harness.config.json`, which would drop the consent gate.
|
|
313
|
-
const cmds = actionCommands(action, {
|
|
314
|
-
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
315
|
-
trust: state.trust,
|
|
316
|
-
doctrines: doctrinesForBuild(state.target),
|
|
317
|
-
});
|
|
303
|
+
// Build resolves its argv off the install (`buildActionCmds`); the other actions take no
|
|
304
|
+
// axes and self-resolve the deployed theme downstream.
|
|
305
|
+
const cmds = action === 'build' ? buildActionCmds(state, body) : actionCommands(action);
|
|
318
306
|
if (!isTruthy(cmds)) {
|
|
319
307
|
return sendJson(res, { error: `unknown action ${action}` }, 404, ae);
|
|
320
308
|
}
|
package/js/web/jobs.mjs
CHANGED
|
@@ -421,14 +421,16 @@ export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
|
421
421
|
* `harness.config.json` exactly as the pack list does, so `{"excludeRules":["process.5"]}`
|
|
422
422
|
* there would take the consent gate out of a console Build that never asked for it. `[]` is
|
|
423
423
|
* spelled `--exclude-rules none` and says "exclude nothing" out loud.
|
|
424
|
+
*
|
|
425
|
+
* `buildArgv`, when given, is used whole and the axes above are ignored: it is the argv a
|
|
426
|
+
* deployed install's profile owns (`buildActionCmds`), switches and exclusions included.
|
|
424
427
|
*/
|
|
425
428
|
function actionTable({
|
|
426
429
|
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
427
430
|
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER], excludeRules = [],
|
|
428
|
-
trust = DEFAULT_PRESET,
|
|
431
|
+
trust = DEFAULT_PRESET, buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture,
|
|
432
|
+
mode, doctrines, PACK_ORDER, excludeRules, trust),
|
|
429
433
|
} = {}) {
|
|
430
|
-
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
|
|
431
|
-
doctrines, PACK_ORDER, excludeRules, trust);
|
|
432
434
|
return {
|
|
433
435
|
doctor: [[NODE(), CLI(), 'doctor']],
|
|
434
436
|
build: [[NODE(), GEN(), ...buildArgv]],
|
package/js/web/routes.mjs
CHANGED
|
@@ -146,7 +146,9 @@ export const STATE_ROUTES = {
|
|
|
146
146
|
'/api/recent': apiRecent,
|
|
147
147
|
'/api/themes': apiThemes,
|
|
148
148
|
'/api/setup': apiSetup,
|
|
149
|
-
|
|
149
|
+
// The one route here that reads its query: `?fresh=1` re-runs instead of serving the cache.
|
|
150
|
+
'/api/doctor': (state, url) => apiDoctor(state,
|
|
151
|
+
new URL(url || '', 'http://x').searchParams.has('fresh')),
|
|
150
152
|
'/api/installs': apiInstalls,
|
|
151
153
|
'/api/fleet': apiFleet,
|
|
152
154
|
'/api/update': apiUpdate,
|
package/js/web/server.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { isFile, printOut } from '../lib/fs.mjs';
|
|
|
27
27
|
import { which } from '../lib/paths.mjs';
|
|
28
28
|
import { parseIntStrict } from '../lib/text.mjs';
|
|
29
29
|
import { promptLine } from '../lib/prompt.mjs';
|
|
30
|
-
import { webState } from './state.mjs';
|
|
30
|
+
import { warmDoctor, webState } from './state.mjs';
|
|
31
31
|
import { clearDaemon, codeStamp, openUrl, readDaemon, restartDaemon, startDaemon, statusDaemon, stopDaemon, writeDaemon } from './daemon.mjs';
|
|
32
32
|
import { makeHandler } from './handler.mjs';
|
|
33
33
|
import { JobManager } from './jobs.mjs';
|
|
@@ -165,6 +165,8 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
165
165
|
printOut(`[web] Geneseed UI on ${url} (theme: ${state.theme})\n`);
|
|
166
166
|
printOut(daemon ? '[web] daemon ready.\n' : '[web] Ctrl-C to stop.\n');
|
|
167
167
|
if (openBrowser) openUrl(url);
|
|
168
|
+
// After the record and the ready line, so `web start`'s wait loop already has its URL.
|
|
169
|
+
warmDoctor(state);
|
|
168
170
|
// Node kills on SIGINT by default, which would skip this message and the record
|
|
169
171
|
// cleanup in `srv.on('close', ...)` below — a foreground `--daemon-internal` server
|
|
170
172
|
// stopped with Ctrl-C would otherwise leave a record pointing at a dead port.
|
package/js/web/state.mjs
CHANGED
|
@@ -40,7 +40,10 @@ export function stampMinute(ms) {
|
|
|
40
40
|
* THE CACHING IS BEHAVIOUR, NOT AN OPTIMISATION: `doctorCollect` renders a theme per call
|
|
41
41
|
* and the console GETs `/api/overview` on mount and again after every mutation, so an
|
|
42
42
|
* uncached `doctor` would put a full build on each of those requests. `refresh()` is what a mutation calls to
|
|
43
|
-
* drop both
|
|
43
|
+
* drop both — except where the mutation cannot change anything the doctor reads (the checkout,
|
|
44
|
+
* the hook shim, the hook shell, the OpenCode config dirs, the deployed install): a memory
|
|
45
|
+
* delete passes `{ doctor: false }` and keeps the ~3.6 s verdict. Rule and profile writes do not
|
|
46
|
+
* call `refresh()` at all. Every other caller keeps the default, so a new one is slow, never stale.
|
|
44
47
|
*/
|
|
45
48
|
export function webState(theme = null, target = null) {
|
|
46
49
|
const st = {
|
|
@@ -56,6 +59,13 @@ export function webState(theme = null, target = null) {
|
|
|
56
59
|
trust: null,
|
|
57
60
|
_inv: null,
|
|
58
61
|
_doctor: null,
|
|
62
|
+
// The Doctor page's per-check cards and theme list, from the SAME run as `_doctor` — kept
|
|
63
|
+
// beside it rather than inside so `/api/overview` (fetched after every mutation) does not
|
|
64
|
+
// carry every group.
|
|
65
|
+
_doctorRun: null,
|
|
66
|
+
// `diffCollect`'s memo of `expected` renders, made on first use by `api.mjs` (`memoOf`);
|
|
67
|
+
// it re-renders on its own key change, so `refresh()` leaves it alone.
|
|
68
|
+
renderMemo: null,
|
|
59
69
|
};
|
|
60
70
|
// The INSTALL ROOT (== build --out). For globals it IS the data dir; a claude/bob/openclaude
|
|
61
71
|
// PROJECT install keeps its data under <repo>/.claude|.bob|.openclaude while the markers
|
|
@@ -87,15 +97,19 @@ export function webState(theme = null, target = null) {
|
|
|
87
97
|
});
|
|
88
98
|
Object.defineProperty(st, 'doctor', {
|
|
89
99
|
get() {
|
|
90
|
-
if (st._doctor === null)
|
|
91
|
-
const [, problems] = doctorCollect({ theme: st.theme });
|
|
92
|
-
st.stampDoctor(problems);
|
|
93
|
-
}
|
|
100
|
+
if (st._doctor === null) st.runDoctor();
|
|
94
101
|
return st._doctor;
|
|
95
102
|
},
|
|
96
103
|
});
|
|
97
|
-
|
|
104
|
+
/** One fresh doctor run, cached whole: the verdict and the grouped detail of the same run. */
|
|
105
|
+
st.runDoctor = () => {
|
|
106
|
+
const groups = [];
|
|
107
|
+
const [themes, problems] = doctorCollect({ theme: st.theme, groups });
|
|
108
|
+
st.stampDoctor(problems, themes, groups);
|
|
109
|
+
};
|
|
110
|
+
st.stampDoctor = (problems, themes = [], groups = []) => {
|
|
98
111
|
st._doctor = { ok: !problems.length, problems, checked_at: stampMinute(Date.now()) };
|
|
112
|
+
st._doctorRun = { themes, groups };
|
|
99
113
|
};
|
|
100
114
|
/**
|
|
101
115
|
* The CURRENT install's mode, from its `.geneseed-emit` marker: the ROOT first (where
|
|
@@ -139,9 +153,9 @@ export function webState(theme = null, target = null) {
|
|
|
139
153
|
st._inv = null;
|
|
140
154
|
st._doctor = null;
|
|
141
155
|
};
|
|
142
|
-
st.refresh = () => {
|
|
156
|
+
st.refresh = ({ doctor = true } = {}) => {
|
|
143
157
|
st._inv = null;
|
|
144
|
-
st._doctor = null;
|
|
158
|
+
if (doctor) st._doctor = null;
|
|
145
159
|
st.emit = st.detectEmit() || st.emit;
|
|
146
160
|
const host = hostOf();
|
|
147
161
|
st.theme = themeOfDir(st.root, host) || themeOfDir(st.target, host) || st.theme;
|
|
@@ -149,3 +163,16 @@ export function webState(theme = null, target = null) {
|
|
|
149
163
|
};
|
|
150
164
|
return st;
|
|
151
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run the doctor once in the background, right after the server binds, so the first
|
|
169
|
+
* `/api/overview` finds a verdict instead of paying the ~3.6 s itself. Still synchronous once it
|
|
170
|
+
* starts — the event loop is blocked for that run — so a request landing mid-run waits for it,
|
|
171
|
+
* no longer than it would have. The catch is not optional: a throw inside `setImmediate` is
|
|
172
|
+
* uncaught and would kill the daemon; a failed warm-up just leaves the cache cold.
|
|
173
|
+
*/
|
|
174
|
+
export function warmDoctor(st) {
|
|
175
|
+
setImmediate(() => {
|
|
176
|
+
try { if (st._doctor === null) st.runDoctor(); } catch { /* cold cache, computed on demand */ }
|
|
177
|
+
});
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
|
|
3
3
|
"name": "geneseed",
|
|
4
|
-
"version": "4.9.
|
|
4
|
+
"version": "4.9.2",
|
|
5
5
|
"description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ai",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{s as e}from"./sections-BET3c1WT.js";import{i as t,t as n}from"./react-BOF6yL40.js";import{t as r}from"./api-
|
|
1
|
+
import{s as e}from"./sections-BET3c1WT.js";import{i as t,t as n}from"./react-BOF6yL40.js";import{t as r}from"./api-CiXlkbe0.js";import{C as i,N as a,Q as o,R as s,x as c}from"./index-BZgEGRbL.js";import{a as l,i as u,n as d,o as f,r as p,t as m}from"./activity-DvPxoS4Y.js";import{t as h}from"./usePoll-NSTzLOF4.js";var g=e(t(),1),_=n();function v({s:e}){let t=p[e.status]||p.idle,n=e.files,r=e.todos,i=r&&r.total>0||e.error||e.blocked_on;return(0,_.jsxs)(`a`,{className:`card pad-md act-card`,href:`#/activity/${encodeURIComponent(e.session_id)}`,children:[(0,_.jsxs)(`div`,{className:`row between wrap gap-12`,children:[(0,_.jsxs)(`div`,{className:`row gap-10`,style:{minWidth:0},children:[(0,_.jsx)(`span`,{className:`feed-dot ${t.cls||`acc`}`,style:{width:9,height:9}}),(0,_.jsxs)(`div`,{style:{minWidth:0},children:[(0,_.jsx)(`h3`,{style:{margin:0},children:e.title||l(e.cwd)||e.session_id}),e.cwd&&(0,_.jsx)(`div`,{className:`dim mono`,style:{fontSize:12,...m},title:e.cwd,children:e.cwd})]})]}),(0,_.jsxs)(`div`,{className:`row gap-8`,style:{flexShrink:0},children:[e.model&&(0,_.jsx)(`span`,{className:`badge mono`,title:`model`,children:e.model}),e.agent&&(0,_.jsx)(`span`,{className:`badge`,children:e.agent}),(0,_.jsxs)(`span`,{className:`badge ${t.cls}`,children:[(0,_.jsx)(`span`,{className:`dot`}),t.label]}),(0,_.jsx)(a,{name:`chevron`,className:`glyph act-chev`})]})]}),e.phase&&(0,_.jsxs)(`div`,{className:`row gap-8`,style:{marginTop:10,fontSize:14},children:[(0,_.jsx)(a,{name:`activity`}),(0,_.jsx)(`span`,{style:m,children:e.phase})]}),(e.tokens>0||e.cost>0||e.turn_started_at||n&&n.count>0)&&(0,_.jsxs)(`div`,{className:`row wrap`,style:{gap:16,marginTop:12,fontSize:13,color:`var(--text-2)`},children:[e.tokens>0&&(0,_.jsxs)(`span`,{children:[f(e.tokens),` tok`]}),e.cost>0&&(0,_.jsxs)(`span`,{children:[`$`,e.cost.toFixed(2)]}),e.turn_started_at?(0,_.jsx)(`span`,{children:(0,_.jsx)(d,{startedAt:e.turn_started_at})}):null,n&&n.count>0&&(0,_.jsxs)(`span`,{children:[n.count,` file`,n.count===1?``:`s`,` `,(0,_.jsxs)(`span`,{style:{color:`var(--good)`},children:[`+`,n.additions]}),` `,(0,_.jsxs)(`span`,{style:{color:`var(--bad)`},children:[`−`,n.deletions]})]})]}),i&&(0,_.jsxs)(`div`,{className:`row between wrap gap-12`,style:{marginTop:12,paddingTop:10,borderTop:`1px solid var(--line)`},children:[r&&r.total>0?(0,_.jsxs)(`span`,{className:`row gap-8 dim`,style:{fontSize:13},children:[(0,_.jsx)(u,{done:r.done,total:r.total}),`plan `,r.done,`/`,r.total]}):(0,_.jsx)(`span`,{}),e.error?(0,_.jsx)(`span`,{className:`badge bad`,title:e.error,style:{maxWidth:300,...m},children:e.error}):e.blocked_on?(0,_.jsx)(`span`,{className:`badge bad`,title:e.blocked_on,style:{maxWidth:300,...m},children:e.blocked_on}):null]}),(0,_.jsxs)(`div`,{className:`dim mono`,style:{fontSize:11,marginTop:8},children:[`updated `,s(e.updated_at),` ago`]})]})}function y(){let[e,t]=(0,g.useState)(null),[n,a]=(0,g.useState)(``);h(e=>r.activity().then(n=>{e()&&(t(n),a(``))},t=>{e()&&a(t.message)}),2e3,[]);let s=!e||e.enabled!==!1,l=e?.activity||[],u=o(),d=async()=>{let e=!s;try{let n=await r.activityToggle(e);t(e=>({...e||{},enabled:n.enabled,activity:n.enabled&&e?.activity||[]})),a(``)}catch(e){a(e.message)}};return(0,_.jsxs)(`div`,{className:`narrow`,children:[(0,_.jsxs)(`div`,{className:`head-row mb-16`,children:[(0,_.jsxs)(`div`,{children:[(0,_.jsx)(`h1`,{className:`h`,children:`Activity`}),(0,_.jsx)(`p`,{className:`sub`,children:`Live OpenCode sessions this harness is running: what each one is doing right now. Refreshes every couple of seconds; sessions drop off when their process exits.`})]}),(0,_.jsxs)(`div`,{className:`row gap-10`,style:{alignItems:`center`,flexShrink:0},children:[(0,_.jsx)(`span`,{className:`dim mono`,style:{fontSize:12},children:s?`tracking on`:`tracking off`}),(0,_.jsx)(`div`,{className:`sw-toggle${s?` on`:``}`,role:`switch`,"aria-checked":s,"aria-label":`Toggle activity tracking`,tabIndex:0,onClick:d,onKeyDown:e=>{(e.key===`Enter`||e.key===` `)&&(e.key===` `&&e.preventDefault(),d())}})]})]}),(0,_.jsx)(c,{error:n,style:{marginBottom:12}}),e?s?l.length===0?(0,_.jsxs)(`div`,{className:`empty`,children:[(0,_.jsx)(`div`,{className:`big`,children:`No active sessions`}),`Run `,(0,_.jsx)(`code`,{children:`opencode`}),` and the sessions it spins up appear here.`]}):(0,_.jsx)(`div`,{className:`stack gap-12`,ref:u,children:l.map(e=>(0,_.jsx)(`div`,{"data-flip":e.session_id,children:(0,_.jsx)(v,{s:e})},e.session_id))}):(0,_.jsxs)(`div`,{className:`empty`,children:[(0,_.jsx)(`div`,{className:`big`,children:`Activity tracking is off`}),`The plugin isn't recording sessions. Flip the switch to turn it back on.`]}):(0,_.jsx)(i,{label:`Reading live sessions…`})]})}export{y as default};
|