@phenx-inc/ctlsurf 0.17.2 → 0.17.4
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/README.md +25 -2
- package/bin/ctlsurf-worker.js +243 -44
- package/out/headless/index.mjs +1033 -128
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +933 -17
- package/out/preload/index.js +15 -0
- package/out/renderer/assets/{cssMode-BtQxI2Eb.js → cssMode-BJfoToLX.js} +3 -3
- package/out/renderer/assets/{freemarker2-DRW5dmY5.js → freemarker2-T0eMtP1Z.js} +1 -1
- package/out/renderer/assets/{handlebars-CFLF3cP7.js → handlebars-Dbbib8fi.js} +1 -1
- package/out/renderer/assets/{html-DtnBOkwp.js → html-BRHT_oxc.js} +1 -1
- package/out/renderer/assets/{htmlMode-DMVysDCg.js → htmlMode-BRlZTxME.js} +3 -3
- package/out/renderer/assets/{index-CEjwKjtd.css → index-CBWAeUYW.css} +91 -0
- package/out/renderer/assets/{index-Dm70hk5s.js → index-RCAeuJTr.js} +476 -411
- package/out/renderer/assets/{javascript-ChlyA1sf.js → javascript-BbOZOxq9.js} +2 -2
- package/out/renderer/assets/{jsonMode-D1_oHzeN.js → jsonMode-CUvAeYcT.js} +3 -3
- package/out/renderer/assets/{liquid-D4ftdy1G.js → liquid-DRIACo_s.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Ckqu5ubG.js → lspLanguageFeatures-Ds-3nH9p.js} +1 -1
- package/out/renderer/assets/{mdx-B9wsVYSC.js → mdx-D76jlpLO.js} +1 -1
- package/out/renderer/assets/{python-e23tD6IS.js → python-CMuRPKJi.js} +1 -1
- package/out/renderer/assets/{razor-CrDyXj3d.js → razor-DRoH5mal.js} +1 -1
- package/out/renderer/assets/{tsMode-BwGC5o4O.js → tsMode-dPJzgFD7.js} +1 -1
- package/out/renderer/assets/{typescript-Do-98d_O.js → typescript-D5USKnLj.js} +1 -1
- package/out/renderer/assets/{xml-Dpdu5R3m.js → xml-qYg1wt2P.js} +1 -1
- package/out/renderer/assets/{yaml-B0Zt7LCJ.js → yaml-Uc-CvnvL.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +4 -2
- package/scripts/check-work-map-gpu.cjs +62 -0
- package/scripts/check-work-map-gpu.d.cts +13 -0
- package/scripts/postinstall.cjs +44 -0
- package/scripts/prepare-work-map.py +49 -0
- package/scripts/setup-ui.cjs +161 -0
- package/scripts/test-work-map-capability.cjs +45 -0
- package/scripts/test-work-map-ui.cjs +130 -0
- package/scripts/test-work-map.cjs +352 -0
- package/scripts/work-map-clef.py +81 -0
- package/scripts/work-map-setup.cjs +127 -0
- package/src/main/index.ts +43 -1
- package/src/main/observer/observer.ts +2 -0
- package/src/main/orchestrator.ts +39 -4
- package/src/main/transcripts.ts +35 -16
- package/src/main/workMap/clef.ts +156 -0
- package/src/main/workMap/normalize.ts +172 -0
- package/src/main/workMap/source.ts +28 -0
- package/src/main/workMap/tree.ts +199 -0
- package/src/main/workMap/workMap.ts +244 -0
- package/src/preload/index.ts +17 -0
- package/src/renderer/App.tsx +15 -16
- package/src/renderer/components/ObserverPanel.tsx +3 -0
- package/src/renderer/components/SettingsDialog.tsx +0 -104
- package/src/renderer/components/WorkMapPanel.tsx +235 -0
- package/src/renderer/components/WorkMapToggle.tsx +50 -0
- package/src/renderer/lib/workMapHighlights.ts +41 -0
- package/src/renderer/styles.css +91 -0
- package/src/shared/workMap.d.ts +70 -0
package/out/main/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const os = require("os");
|
|
|
29
29
|
const https = require("https");
|
|
30
30
|
const module$1 = require("module");
|
|
31
31
|
const child_process = require("child_process");
|
|
32
|
+
const string_decoder = require("string_decoder");
|
|
32
33
|
const require$$1 = require("crypto");
|
|
33
34
|
const require$$0$3 = require("events");
|
|
34
35
|
const require$$2 = require("http");
|
|
@@ -38,6 +39,7 @@ const require$$0$2 = require("stream");
|
|
|
38
39
|
const require$$7 = require("url");
|
|
39
40
|
const require$$0 = require("zlib");
|
|
40
41
|
const require$$0$1 = require("buffer");
|
|
42
|
+
const cp = require("node:child_process");
|
|
41
43
|
const NPM_PACKAGE = "@phenx-inc/ctlsurf";
|
|
42
44
|
function compareSemver(a, b2) {
|
|
43
45
|
const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
|
|
@@ -6261,6 +6263,8 @@ const POLL_INTERVAL_MS = 1e3;
|
|
|
6261
6263
|
const DISCOVERY_SLACK_MS = 1e4;
|
|
6262
6264
|
const READ_CHUNK_BYTES = 64 * 1024;
|
|
6263
6265
|
const MAX_ENTRY_CHARS = 2e4;
|
|
6266
|
+
const MAX_READ_PER_POLL = 512 * 1024;
|
|
6267
|
+
const MAX_LINE_CHARS = 1024 * 1024;
|
|
6264
6268
|
class TranscriptTailer {
|
|
6265
6269
|
agentId;
|
|
6266
6270
|
cwd;
|
|
@@ -6328,7 +6332,7 @@ class TranscriptTailer {
|
|
|
6328
6332
|
try {
|
|
6329
6333
|
const stat = fs.statSync(filePath);
|
|
6330
6334
|
if (stat.mtimeMs >= this.sinceMs - DISCOVERY_SLACK_MS) {
|
|
6331
|
-
this.files.set(filePath, { offset: 0, remainder: "", excluded: false });
|
|
6335
|
+
this.files.set(filePath, { offset: 0, remainder: "", excluded: false, decoder: new string_decoder.StringDecoder("utf8"), discardingLine: false, scoped: this.agentId === "claude" });
|
|
6332
6336
|
}
|
|
6333
6337
|
} catch {
|
|
6334
6338
|
}
|
|
@@ -6358,7 +6362,14 @@ class TranscriptTailer {
|
|
|
6358
6362
|
} catch {
|
|
6359
6363
|
return;
|
|
6360
6364
|
}
|
|
6361
|
-
if (size
|
|
6365
|
+
if (size < tail.offset) {
|
|
6366
|
+
tail.offset = 0;
|
|
6367
|
+
tail.remainder = "";
|
|
6368
|
+
tail.decoder = new string_decoder.StringDecoder("utf8");
|
|
6369
|
+
tail.discardingLine = false;
|
|
6370
|
+
tail.scoped = this.agentId === "claude";
|
|
6371
|
+
}
|
|
6372
|
+
if (size === tail.offset) return;
|
|
6362
6373
|
let fd;
|
|
6363
6374
|
try {
|
|
6364
6375
|
fd = fs.openSync(filePath, "r");
|
|
@@ -6367,17 +6378,26 @@ class TranscriptTailer {
|
|
|
6367
6378
|
}
|
|
6368
6379
|
try {
|
|
6369
6380
|
const buf = Buffer.alloc(READ_CHUNK_BYTES);
|
|
6370
|
-
|
|
6371
|
-
|
|
6381
|
+
const end = Math.min(size, tail.offset + MAX_READ_PER_POLL);
|
|
6382
|
+
while (tail.offset < end && !tail.excluded) {
|
|
6383
|
+
const bytesRead = fs.readSync(fd, buf, 0, Math.min(READ_CHUNK_BYTES, end - tail.offset), tail.offset);
|
|
6372
6384
|
if (bytesRead <= 0) break;
|
|
6373
6385
|
tail.offset += bytesRead;
|
|
6374
|
-
tail.remainder += buf.
|
|
6386
|
+
tail.remainder += tail.decoder.write(buf.subarray(0, bytesRead));
|
|
6375
6387
|
const lines = tail.remainder.split("\n");
|
|
6376
6388
|
tail.remainder = lines.pop() || "";
|
|
6377
6389
|
for (const line of lines) {
|
|
6378
|
-
|
|
6390
|
+
if (tail.discardingLine) {
|
|
6391
|
+
tail.discardingLine = false;
|
|
6392
|
+
continue;
|
|
6393
|
+
}
|
|
6394
|
+
if (line.length <= MAX_LINE_CHARS) this.handleLine(line, tail, filePath);
|
|
6379
6395
|
if (tail.excluded) break;
|
|
6380
6396
|
}
|
|
6397
|
+
if (tail.remainder.length > MAX_LINE_CHARS) {
|
|
6398
|
+
tail.remainder = "";
|
|
6399
|
+
tail.discardingLine = true;
|
|
6400
|
+
}
|
|
6381
6401
|
}
|
|
6382
6402
|
} catch (err) {
|
|
6383
6403
|
console.error(`[transcripts] Read error for ${filePath}:`, err);
|
|
@@ -6388,7 +6408,7 @@ class TranscriptTailer {
|
|
|
6388
6408
|
}
|
|
6389
6409
|
}
|
|
6390
6410
|
}
|
|
6391
|
-
handleLine(line, tail) {
|
|
6411
|
+
handleLine(line, tail, filePath) {
|
|
6392
6412
|
const trimmed = line.trim();
|
|
6393
6413
|
if (!trimmed) return;
|
|
6394
6414
|
let obj;
|
|
@@ -6397,16 +6417,18 @@ class TranscriptTailer {
|
|
|
6397
6417
|
} catch {
|
|
6398
6418
|
return;
|
|
6399
6419
|
}
|
|
6420
|
+
const entry = this.agentId === "claude" ? this.parseClaudeLine(obj) : this.parseCodexLine(obj, tail);
|
|
6421
|
+
if (tail.excluded || !tail.scoped || obj?.isSidechain || obj?.isMeta) return;
|
|
6422
|
+
if (this.agentId === "claude" && typeof obj?.cwd === "string" && stripTrailingSep$2(obj.cwd) !== this.cwd) return;
|
|
6400
6423
|
if (this.rawSink) {
|
|
6401
6424
|
const rawMs = typeof obj?.timestamp === "string" ? Date.parse(obj.timestamp) : NaN;
|
|
6402
|
-
if (!Number.isFinite(rawMs) || rawMs >= this.sinceMs - DISCOVERY_SLACK_MS) {
|
|
6425
|
+
if (obj?.type === "session_meta" || !Number.isFinite(rawMs) || rawMs >= this.sinceMs - DISCOVERY_SLACK_MS) {
|
|
6403
6426
|
try {
|
|
6404
|
-
this.rawSink(obj);
|
|
6427
|
+
this.rawSink(obj, { filePath, sinceMs: this.sinceMs });
|
|
6405
6428
|
} catch {
|
|
6406
6429
|
}
|
|
6407
6430
|
}
|
|
6408
6431
|
}
|
|
6409
|
-
const entry = this.agentId === "claude" ? this.parseClaudeLine(obj) : this.parseCodexLine(obj, tail);
|
|
6410
6432
|
if (!entry) return;
|
|
6411
6433
|
const ms = Date.parse(entry.ts);
|
|
6412
6434
|
if (Number.isFinite(ms) && ms < this.sinceMs - DISCOVERY_SLACK_MS) return;
|
|
@@ -6435,9 +6457,10 @@ class TranscriptTailer {
|
|
|
6435
6457
|
const payload = obj.payload;
|
|
6436
6458
|
if (obj.type === "session_meta") {
|
|
6437
6459
|
const metaCwd = payload?.cwd;
|
|
6438
|
-
if (typeof metaCwd
|
|
6460
|
+
if (typeof metaCwd !== "string" || stripTrailingSep$2(metaCwd) !== this.cwd) {
|
|
6439
6461
|
tail.excluded = true;
|
|
6440
6462
|
}
|
|
6463
|
+
tail.scoped = !tail.excluded;
|
|
6441
6464
|
return null;
|
|
6442
6465
|
}
|
|
6443
6466
|
if (obj.type !== "event_msg" || !payload || typeof payload !== "object") return null;
|
|
@@ -12014,7 +12037,7 @@ Your previous output was invalid (${lastError}). Return valid JSON matching the
|
|
|
12014
12037
|
}
|
|
12015
12038
|
}
|
|
12016
12039
|
buildUser(window2) {
|
|
12017
|
-
const
|
|
12040
|
+
const compact2 = window2.map((e) => {
|
|
12018
12041
|
const out = { id: e.id, ts: e.ts, type: e.type };
|
|
12019
12042
|
if (e.text) out.text = truncateForTier1(e.text);
|
|
12020
12043
|
if (e.tool) out.tool = e.tool;
|
|
@@ -12034,7 +12057,7 @@ Your previous output was invalid (${lastError}). Return valid JSON matching the
|
|
|
12034
12057
|
${JSON.stringify(state)}
|
|
12035
12058
|
|
|
12036
12059
|
WINDOW (${window2.length} events):
|
|
12037
|
-
${
|
|
12060
|
+
${compact2.join("\n")}`;
|
|
12038
12061
|
}
|
|
12039
12062
|
parse(text, windowIds) {
|
|
12040
12063
|
const parsed = JSON.parse(text);
|
|
@@ -22756,6 +22779,842 @@ function extractToolResultText(content) {
|
|
|
22756
22779
|
if (!Array.isArray(content)) return "";
|
|
22757
22780
|
return content.filter((b2) => b2?.type === "text" && typeof b2.text === "string").map((b2) => b2.text).join("\n");
|
|
22758
22781
|
}
|
|
22782
|
+
var checkWorkMapGpu = { exports: {} };
|
|
22783
|
+
var hasRequiredCheckWorkMapGpu;
|
|
22784
|
+
function requireCheckWorkMapGpu() {
|
|
22785
|
+
if (hasRequiredCheckWorkMapGpu) return checkWorkMapGpu.exports;
|
|
22786
|
+
hasRequiredCheckWorkMapGpu = 1;
|
|
22787
|
+
(function(module2) {
|
|
22788
|
+
function initialCapability({ desktop = false, platform = process.platform, arch = process.arch } = {}) {
|
|
22789
|
+
if (!desktop) return { state: "unavailable", reason: "Work map is available only in the desktop app." };
|
|
22790
|
+
if (platform !== "darwin" || arch !== "arm64") return { state: "unavailable", reason: "Work map currently requires an Apple Silicon Mac with Metal GPU support." };
|
|
22791
|
+
return { state: "checking", reason: "Checking Apple GPU availability…" };
|
|
22792
|
+
}
|
|
22793
|
+
function appleGpuFromReport(report) {
|
|
22794
|
+
if (!Array.isArray(report?.SPDisplaysDataType)) return null;
|
|
22795
|
+
return report.SPDisplaysDataType.find((gpu) => {
|
|
22796
|
+
const apple = gpu.spdisplays_vendor === "sppci_vendor_Apple" || /^Apple M\d/i.test(gpu.sppci_model || gpu._name || "");
|
|
22797
|
+
const metal = String(gpu.spdisplays_metal || gpu.spdisplays_metalsupport || gpu.spdisplays_mtlgpufamilysupport || "");
|
|
22798
|
+
return apple && /supported|metal/i.test(metal) && !/unsupported|not.supported|unavailable/i.test(metal);
|
|
22799
|
+
}) || null;
|
|
22800
|
+
}
|
|
22801
|
+
function usableElectronGpu(info) {
|
|
22802
|
+
if (info?.auxAttributes?.softwareRendering === true) return false;
|
|
22803
|
+
return Array.isArray(info?.gpuDevice) && info.gpuDevice.some((gpu) => gpu.active === true && (gpu.vendorId === 4203 || /Apple/i.test(gpu.deviceString || gpu.vendorString || "")));
|
|
22804
|
+
}
|
|
22805
|
+
function readGpuReport() {
|
|
22806
|
+
return new Promise((resolve, reject) => {
|
|
22807
|
+
const { execFile } = cp;
|
|
22808
|
+
execFile("/usr/sbin/system_profiler", ["SPDisplaysDataType", "-json"], { timeout: 5e3, maxBuffer: 1024 * 1024 }, (error, output) => {
|
|
22809
|
+
if (error) {
|
|
22810
|
+
reject(error);
|
|
22811
|
+
return;
|
|
22812
|
+
}
|
|
22813
|
+
try {
|
|
22814
|
+
resolve(JSON.parse(output));
|
|
22815
|
+
} catch (error2) {
|
|
22816
|
+
reject(error2);
|
|
22817
|
+
}
|
|
22818
|
+
});
|
|
22819
|
+
});
|
|
22820
|
+
}
|
|
22821
|
+
async function detectCapability(options = {}) {
|
|
22822
|
+
const initial = initialCapability(options);
|
|
22823
|
+
if (initial.state !== "checking") return initial;
|
|
22824
|
+
try {
|
|
22825
|
+
const gpu = appleGpuFromReport(await (options.readGpuReport || readGpuReport)());
|
|
22826
|
+
if (!gpu) return { state: "unavailable", reason: "No Apple GPU with Metal support was detected." };
|
|
22827
|
+
if (options.verifyGpu) {
|
|
22828
|
+
let timer;
|
|
22829
|
+
const usable = await Promise.race([
|
|
22830
|
+
options.verifyGpu(),
|
|
22831
|
+
new Promise((resolve) => {
|
|
22832
|
+
timer = setTimeout(() => resolve(false), 5e3);
|
|
22833
|
+
})
|
|
22834
|
+
]).finally(() => clearTimeout(timer));
|
|
22835
|
+
if (!usable) return { state: "unavailable", reason: "The Apple GPU is unavailable to the desktop app." };
|
|
22836
|
+
}
|
|
22837
|
+
return { state: "available", reason: null, gpuName: String(gpu.sppci_model || gpu._name || "Apple GPU") };
|
|
22838
|
+
} catch {
|
|
22839
|
+
return { state: "unavailable", reason: "Apple GPU availability could not be verified. Work map is disabled for this launch." };
|
|
22840
|
+
}
|
|
22841
|
+
}
|
|
22842
|
+
module2.exports = { initialCapability, appleGpuFromReport, usableElectronGpu, detectCapability };
|
|
22843
|
+
if (require.main === module2) {
|
|
22844
|
+
detectCapability({ desktop: true }).then((capability) => {
|
|
22845
|
+
console.log(capability.state === "available" ? `[ctlsurf] Work map hardware detected: ${capability.gpuName}. The desktop app checks GPU access again at startup.` : `[ctlsurf] ${capability.reason} Other app features remain available.`);
|
|
22846
|
+
}).catch(() => {
|
|
22847
|
+
});
|
|
22848
|
+
}
|
|
22849
|
+
})(checkWorkMapGpu);
|
|
22850
|
+
return checkWorkMapGpu.exports;
|
|
22851
|
+
}
|
|
22852
|
+
var checkWorkMapGpuExports = requireCheckWorkMapGpu();
|
|
22853
|
+
function validateDecisions(value, req) {
|
|
22854
|
+
const answers = value?.answers;
|
|
22855
|
+
if (!answers || Object.keys(answers).sort().join() !== Object.keys(req.questions).sort().join()) throw new Error("Clef returned an invalid decision schema");
|
|
22856
|
+
for (const [key, q] of Object.entries(req.questions)) {
|
|
22857
|
+
const a = answers[key];
|
|
22858
|
+
const allowed = Object.keys(q.criteria).sort();
|
|
22859
|
+
if (!a || !allowed.includes(a.choice) || !a.probabilities || Object.keys(a.probabilities).sort().join() !== allowed.join()) throw new Error("Clef returned an unknown branch or activity");
|
|
22860
|
+
const values = Object.values(a.probabilities);
|
|
22861
|
+
if (values.some((p2) => typeof p2 !== "number" || !Number.isFinite(p2) || p2 < 0 || p2 > 1) || Math.abs(values.reduce((s, p2) => s + p2, 0) - 1) > 2e-3 || a.probabilities[a.choice] < Math.max(...values) - 1e-6) throw new Error("Clef returned invalid probabilities");
|
|
22862
|
+
}
|
|
22863
|
+
return answers;
|
|
22864
|
+
}
|
|
22865
|
+
class ClefWorker {
|
|
22866
|
+
constructor(scriptPath, onChange) {
|
|
22867
|
+
this.scriptPath = scriptPath;
|
|
22868
|
+
this.onChange = onChange;
|
|
22869
|
+
}
|
|
22870
|
+
state = "idle";
|
|
22871
|
+
error = null;
|
|
22872
|
+
child = null;
|
|
22873
|
+
ready = null;
|
|
22874
|
+
rejectReady = null;
|
|
22875
|
+
pending = null;
|
|
22876
|
+
seq = 0;
|
|
22877
|
+
generation = 0;
|
|
22878
|
+
idle = null;
|
|
22879
|
+
loadTimeout = null;
|
|
22880
|
+
requestTimeout = null;
|
|
22881
|
+
stopping = Promise.resolve();
|
|
22882
|
+
change(state, error = null) {
|
|
22883
|
+
this.state = state;
|
|
22884
|
+
this.error = error;
|
|
22885
|
+
this.onChange();
|
|
22886
|
+
}
|
|
22887
|
+
async predict(settings, request) {
|
|
22888
|
+
const generation = this.generation;
|
|
22889
|
+
try {
|
|
22890
|
+
await this.start(settings, generation);
|
|
22891
|
+
if (!this.child || generation !== this.generation) throw new Error("Clef process stopped");
|
|
22892
|
+
if (this.pending) throw new Error("Clef is already processing a batch");
|
|
22893
|
+
if (this.idle) clearTimeout(this.idle);
|
|
22894
|
+
this.change("thinking");
|
|
22895
|
+
const response = await new Promise((resolve, reject) => {
|
|
22896
|
+
this.pending = { id: ++this.seq, resolve, reject };
|
|
22897
|
+
this.requestTimeout = setTimeout(() => this.fail("Clef timed out; turn the map off and on to retry."), 2e4);
|
|
22898
|
+
this.child.stdin.write(JSON.stringify({ id: this.seq, request }) + "\n", (err) => {
|
|
22899
|
+
if (err) this.fail(`Clef input failed: ${err.message}`);
|
|
22900
|
+
});
|
|
22901
|
+
});
|
|
22902
|
+
const answers = validateDecisions(response, request);
|
|
22903
|
+
this.change("ready");
|
|
22904
|
+
this.armIdle();
|
|
22905
|
+
return answers;
|
|
22906
|
+
} catch (err) {
|
|
22907
|
+
if (generation === this.generation) this.fail(err instanceof Error ? err.message : String(err));
|
|
22908
|
+
throw err;
|
|
22909
|
+
}
|
|
22910
|
+
}
|
|
22911
|
+
async start(settings, generation) {
|
|
22912
|
+
await this.stopping;
|
|
22913
|
+
if (generation !== this.generation) throw new Error("Clef process stopped");
|
|
22914
|
+
if (this.ready) return this.ready;
|
|
22915
|
+
if (process.platform !== "darwin" || process.arch !== "arm64") throw new Error("Local Clef-Flash requires an Apple Silicon Mac. Choose Rules on this machine.");
|
|
22916
|
+
if (!fs.existsSync(settings.pythonPath) || !fs.existsSync(path$1.join(settings.modelPath, "clef_mlx.py"))) {
|
|
22917
|
+
throw new Error("Clef runtime not found. Open Local model setup and select the Python executable and downloaded Clef-Flash folder.");
|
|
22918
|
+
}
|
|
22919
|
+
this.change("loading");
|
|
22920
|
+
const child = child_process.spawn(settings.pythonPath, ["-u", this.scriptPath, settings.modelPath], {
|
|
22921
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22922
|
+
env: { ...process.env, HF_HUB_OFFLINE: "1", TRANSFORMERS_OFFLINE: "1", PYTHONDONTWRITEBYTECODE: "1" }
|
|
22923
|
+
});
|
|
22924
|
+
this.child = child;
|
|
22925
|
+
let buffer = "";
|
|
22926
|
+
let stderr = "";
|
|
22927
|
+
this.ready = new Promise((resolve, reject) => {
|
|
22928
|
+
this.rejectReady = reject;
|
|
22929
|
+
this.loadTimeout = setTimeout(() => this.fail("Clef did not load within 60 seconds."), 6e4);
|
|
22930
|
+
child.stdout.on("data", (chunk) => {
|
|
22931
|
+
if (this.child !== child) return;
|
|
22932
|
+
buffer += String(chunk);
|
|
22933
|
+
if (buffer.length > 128e3) {
|
|
22934
|
+
this.fail("Clef response exceeded its output budget");
|
|
22935
|
+
return;
|
|
22936
|
+
}
|
|
22937
|
+
let newline;
|
|
22938
|
+
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
22939
|
+
const line = buffer.slice(0, newline);
|
|
22940
|
+
buffer = buffer.slice(newline + 1);
|
|
22941
|
+
let msg;
|
|
22942
|
+
try {
|
|
22943
|
+
msg = JSON.parse(line);
|
|
22944
|
+
} catch {
|
|
22945
|
+
continue;
|
|
22946
|
+
}
|
|
22947
|
+
if (msg.ready) {
|
|
22948
|
+
if (this.loadTimeout) clearTimeout(this.loadTimeout);
|
|
22949
|
+
this.rejectReady = null;
|
|
22950
|
+
this.change("ready");
|
|
22951
|
+
resolve();
|
|
22952
|
+
} else if (msg.error) {
|
|
22953
|
+
this.fail(String(msg.error));
|
|
22954
|
+
} else if (msg.id === this.pending?.id) {
|
|
22955
|
+
if (this.requestTimeout) clearTimeout(this.requestTimeout);
|
|
22956
|
+
const pending = this.pending;
|
|
22957
|
+
this.pending = null;
|
|
22958
|
+
pending?.resolve(msg);
|
|
22959
|
+
}
|
|
22960
|
+
}
|
|
22961
|
+
});
|
|
22962
|
+
child.stderr.on("data", (chunk) => {
|
|
22963
|
+
stderr = (stderr + String(chunk)).slice(-1e3);
|
|
22964
|
+
});
|
|
22965
|
+
child.on("error", (err) => {
|
|
22966
|
+
if (this.child === child) this.fail(`Cannot start Clef: ${err.message}`);
|
|
22967
|
+
});
|
|
22968
|
+
child.on("exit", () => {
|
|
22969
|
+
if (this.child === child) this.fail(`Clef stopped unexpectedly. ${stderr.slice(-300)}`);
|
|
22970
|
+
});
|
|
22971
|
+
});
|
|
22972
|
+
return this.ready;
|
|
22973
|
+
}
|
|
22974
|
+
armIdle() {
|
|
22975
|
+
if (this.idle) clearTimeout(this.idle);
|
|
22976
|
+
this.idle = setTimeout(() => this.stop(), 6e4);
|
|
22977
|
+
this.idle.unref();
|
|
22978
|
+
}
|
|
22979
|
+
fail(message) {
|
|
22980
|
+
this.stop(message);
|
|
22981
|
+
this.change("error", message.slice(0, 600));
|
|
22982
|
+
}
|
|
22983
|
+
stop(reason = "Clef stopped") {
|
|
22984
|
+
++this.generation;
|
|
22985
|
+
for (const timer of [this.idle, this.loadTimeout, this.requestTimeout]) if (timer) clearTimeout(timer);
|
|
22986
|
+
this.idle = this.loadTimeout = this.requestTimeout = null;
|
|
22987
|
+
this.rejectReady?.(new Error(reason));
|
|
22988
|
+
this.rejectReady = null;
|
|
22989
|
+
this.pending?.reject(new Error(reason));
|
|
22990
|
+
this.pending = null;
|
|
22991
|
+
this.ready = null;
|
|
22992
|
+
const child = this.child;
|
|
22993
|
+
this.child = null;
|
|
22994
|
+
if (child) {
|
|
22995
|
+
this.stopping = new Promise((resolve) => child.once("close", () => resolve()));
|
|
22996
|
+
child.kill("SIGKILL");
|
|
22997
|
+
}
|
|
22998
|
+
this.change("idle");
|
|
22999
|
+
}
|
|
23000
|
+
}
|
|
23001
|
+
const textOf = (value) => {
|
|
23002
|
+
if (typeof value === "string") return value;
|
|
23003
|
+
if (Array.isArray(value)) return value.map((b2) => b2?.text || "").filter(Boolean).join("\n");
|
|
23004
|
+
return "";
|
|
23005
|
+
};
|
|
23006
|
+
const compact = (value) => {
|
|
23007
|
+
if (typeof value === "string") return value.slice(0, 1800);
|
|
23008
|
+
try {
|
|
23009
|
+
return JSON.stringify(value ?? {}).slice(0, 1800);
|
|
23010
|
+
} catch {
|
|
23011
|
+
return "";
|
|
23012
|
+
}
|
|
23013
|
+
};
|
|
23014
|
+
const noise = (text) => /^\s*(?:<environment_context>|<user_instructions>|<permissions instructions>|<system-reminder>|<local-command|<command-|<task-notification>|<turn_aborted>|# AGENTS\.md)/i.test(text);
|
|
23015
|
+
const summaryText = (value) => {
|
|
23016
|
+
if (typeof value === "string") return value;
|
|
23017
|
+
if (!Array.isArray(value)) return "";
|
|
23018
|
+
return value.map((part) => typeof part === "string" ? part : part?.type === "summary_text" ? part.text || "" : "").filter(Boolean).join("\n\n");
|
|
23019
|
+
};
|
|
23020
|
+
function codexThinking(raw) {
|
|
23021
|
+
const p2 = raw?.payload;
|
|
23022
|
+
if (raw?.type === "response_item" && p2?.type === "reasoning") {
|
|
23023
|
+
const text = summaryText(p2.summary);
|
|
23024
|
+
return { text, key: p2.id || text };
|
|
23025
|
+
}
|
|
23026
|
+
if (raw?.type === "event_msg" && p2?.type === "agent_reasoning") {
|
|
23027
|
+
const text = typeof p2.text === "string" ? p2.text : "";
|
|
23028
|
+
return { text, key: p2.id || text };
|
|
23029
|
+
}
|
|
23030
|
+
const item = raw?.type === "event_msg" && p2?.type === "item_completed" ? p2.item : raw?.method === "item/completed" ? raw.params?.item : null;
|
|
23031
|
+
if (item?.type === "Reasoning" || item?.type === "reasoning") {
|
|
23032
|
+
const text = summaryText(item.summary_text ?? item.summaryText ?? item.summary);
|
|
23033
|
+
return { text, key: item.id || text };
|
|
23034
|
+
}
|
|
23035
|
+
return null;
|
|
23036
|
+
}
|
|
23037
|
+
function mapThinkingStatus(raw, agent) {
|
|
23038
|
+
if (agent === "claude") {
|
|
23039
|
+
if (raw?.type !== "assistant" || !Array.isArray(raw.message?.content)) return null;
|
|
23040
|
+
const blocks = raw.message.content.filter((b2) => b2?.type === "thinking" || b2?.type === "redacted_thinking");
|
|
23041
|
+
if (!blocks.length) return null;
|
|
23042
|
+
return blocks.some((b2) => b2.type === "thinking" && typeof b2.thinking === "string" && b2.thinking.trim()) ? "available" : "unavailable";
|
|
23043
|
+
}
|
|
23044
|
+
const thinking = codexThinking(raw);
|
|
23045
|
+
return thinking ? thinking.text.trim() ? "available" : "unavailable" : null;
|
|
23046
|
+
}
|
|
23047
|
+
function normalizeMapEvents(raw, file, agent, includeFullSource = false) {
|
|
23048
|
+
if (!raw || typeof raw !== "object" || raw.isSidechain || raw.isMeta) return [];
|
|
23049
|
+
const ts = typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString();
|
|
23050
|
+
const source = path$1.basename(file);
|
|
23051
|
+
const events = [];
|
|
23052
|
+
const add = (kind, text, key, extra = {}) => {
|
|
23053
|
+
if (!text.trim() || (kind === "prompt" || kind === "message") && noise(text)) return;
|
|
23054
|
+
const hash = require$$1.createHash("sha256").update(`${file}:${key}:${kind}`).digest("hex").slice(0, 20);
|
|
23055
|
+
const bounded = kind === "thinking" && text.length > 1800 ? `${text.slice(0, 300)}
|
|
23056
|
+
…
|
|
23057
|
+
${text.slice(-1490)}` : text.trim().slice(0, 1800);
|
|
23058
|
+
const segments = kind === "thinking" || kind === "message" ? decisionCandidates(text) : void 0;
|
|
23059
|
+
events.push({
|
|
23060
|
+
id: hash,
|
|
23061
|
+
ts,
|
|
23062
|
+
kind,
|
|
23063
|
+
text: bounded,
|
|
23064
|
+
source,
|
|
23065
|
+
...extra,
|
|
23066
|
+
...segments?.length ? { segments } : {},
|
|
23067
|
+
...includeFullSource && (kind === "thinking" || kind === "message") ? { fullText: text.trim() } : {}
|
|
23068
|
+
});
|
|
23069
|
+
};
|
|
23070
|
+
if (agent === "claude") {
|
|
23071
|
+
if (raw.type !== "user" && raw.type !== "assistant") return [];
|
|
23072
|
+
const blocks = raw.message?.content;
|
|
23073
|
+
const key = raw.uuid || raw.message?.id || `${ts}:${compact(blocks)}`;
|
|
23074
|
+
if (typeof blocks === "string") add(raw.type === "user" ? "prompt" : "message", blocks, key);
|
|
23075
|
+
else if (Array.isArray(blocks)) blocks.forEach((b2, i) => {
|
|
23076
|
+
if (b2.type === "text") add(raw.type === "user" ? "prompt" : "message", b2.text || "", `${key}:${i}`);
|
|
23077
|
+
if (raw.type === "assistant" && b2.type === "thinking" && typeof b2.thinking === "string") add("thinking", b2.thinking, `${key}:${i}`);
|
|
23078
|
+
if (b2.type === "tool_use") add("tool", compact(b2.input?.command ?? b2.input?.file_path ?? b2.input), b2.id, { tool: b2.name, toolId: b2.id, status: "running" });
|
|
23079
|
+
if (b2.type === "tool_result") add("result", textOf(b2.content) || "Tool completed", b2.tool_use_id, {
|
|
23080
|
+
toolId: b2.tool_use_id,
|
|
23081
|
+
status: b2.is_error === true ? "failed" : "succeeded"
|
|
23082
|
+
});
|
|
23083
|
+
});
|
|
23084
|
+
return events;
|
|
23085
|
+
}
|
|
23086
|
+
const thinking = codexThinking(raw);
|
|
23087
|
+
if (thinking) {
|
|
23088
|
+
add("thinking", thinking.text, `reasoning:${thinking.key}`);
|
|
23089
|
+
return events;
|
|
23090
|
+
}
|
|
23091
|
+
const p2 = raw.payload;
|
|
23092
|
+
if (!p2) return [];
|
|
23093
|
+
if (raw.type === "event_msg") {
|
|
23094
|
+
if (p2.type === "user_message" || p2.type === "agent_message") {
|
|
23095
|
+
add(p2.type === "user_message" ? "prompt" : "message", textOf(p2.message), `${ts}:${p2.type}:${textOf(p2.message)}`);
|
|
23096
|
+
}
|
|
23097
|
+
const item = p2.type === "item_completed" ? p2.item : null;
|
|
23098
|
+
if (item?.type === "CommandExecution") {
|
|
23099
|
+
const status = item.status === "failed" || typeof item.exit_code === "number" && item.exit_code !== 0 ? "failed" : item.status === "completed" && item.exit_code === 0 ? "succeeded" : "unknown";
|
|
23100
|
+
const command = Array.isArray(item.command) ? item.command.length >= 3 && /^-(?:l?c|cl)$/.test(item.command[1]) ? item.command.slice(2).join(" ") : item.command.join(" ") : compact(item.command);
|
|
23101
|
+
add("tool", command, item.id, { tool: "shell", toolId: item.id, status: "running" });
|
|
23102
|
+
add("result", item.aggregated_output || `Command ended (${item.exit_code ?? item.status})`, item.id, { toolId: item.id, status });
|
|
23103
|
+
}
|
|
23104
|
+
if (item?.type === "FileChange") {
|
|
23105
|
+
add("tool", Object.keys(item.changes || {}).join("\n"), item.id, { tool: "apply_patch", toolId: item.id, status: "unknown" });
|
|
23106
|
+
}
|
|
23107
|
+
}
|
|
23108
|
+
if (raw.type !== "response_item") return events;
|
|
23109
|
+
if (p2.type === "message" && (p2.role === "user" || p2.role === "assistant")) {
|
|
23110
|
+
const text = Array.isArray(p2.content) ? p2.content.filter((b2) => ["input_text", "output_text"].includes(b2.type)).map((b2) => b2.text).join("\n") : "";
|
|
23111
|
+
add(p2.role === "user" ? "prompt" : "message", text, p2.id || `${ts}:${p2.role}:${text}`);
|
|
23112
|
+
}
|
|
23113
|
+
if (p2.type === "function_call" || p2.type === "custom_tool_call") {
|
|
23114
|
+
if (/^(?:functions\.)?exec$/.test(p2.name)) return events;
|
|
23115
|
+
let input = p2.arguments ?? p2.input;
|
|
23116
|
+
try {
|
|
23117
|
+
const obj = JSON.parse(input);
|
|
23118
|
+
input = obj.cmd ?? obj.command ?? obj.file_path ?? obj;
|
|
23119
|
+
} catch {
|
|
23120
|
+
}
|
|
23121
|
+
add("tool", compact(input), p2.call_id, { tool: p2.name, toolId: p2.call_id, status: "running" });
|
|
23122
|
+
}
|
|
23123
|
+
if (p2.type === "function_call_output" || p2.type === "custom_tool_call_output") {
|
|
23124
|
+
const output = textOf(p2.output) || compact(p2.output);
|
|
23125
|
+
const exit = output.match(/(?:Process exited with code|exit_code["']?\s*:)\s*(-?\d+)/i);
|
|
23126
|
+
const status = p2.is_error === true ? "failed" : exit ? Number(exit[1]) === 0 ? "succeeded" : "failed" : "unknown";
|
|
23127
|
+
add("result", output, p2.call_id, { toolId: p2.call_id, status });
|
|
23128
|
+
}
|
|
23129
|
+
return events;
|
|
23130
|
+
}
|
|
23131
|
+
function activityFor(e) {
|
|
23132
|
+
if (e.kind === "thinking") return "thinking";
|
|
23133
|
+
if (e.kind === "prompt") return "planning";
|
|
23134
|
+
if (e.kind === "message") return /\b(will|next|plan|investigat|checking)\b/i.test(e.text) ? "planning" : "reporting";
|
|
23135
|
+
if (/edit|write|apply_patch|filechange/i.test(e.tool || "")) return "editing";
|
|
23136
|
+
if (/\b(?:pytest|vitest|jest|tsc|unittest|npm (?:run )?(?:test|build)|cargo test|go test|node\s+\S*test[-_.])\b/i.test(e.text)) return "testing";
|
|
23137
|
+
return "investigating";
|
|
23138
|
+
}
|
|
23139
|
+
function shortLabel(text, limit2 = 72) {
|
|
23140
|
+
const line = text.replace(/```[\s\S]*?```/g, "").replace(/^[\s#>*-]+/gm, "").replace(/\s+/g, " ").trim();
|
|
23141
|
+
return line.length > limit2 ? `${line.slice(0, limit2 - 1).trimEnd()}…` : line || "Current work";
|
|
23142
|
+
}
|
|
23143
|
+
function topicFor(e) {
|
|
23144
|
+
if (e.kind === "thinking") return "Reasoning & decisions";
|
|
23145
|
+
const paths = e.text.match(/(?:[\w.@-]+\/)+[\w.@-]+\.(?:tsx?|jsx?|py|json|css|md|rs|go|sh|cjs|mjs)\b/g);
|
|
23146
|
+
if (paths?.length) return shortLabel(paths[0].split("/").slice(-2).join("/"), 48);
|
|
23147
|
+
if (activityFor(e) === "testing") return "Build & tests";
|
|
23148
|
+
if (e.kind === "message") return "Approach & findings";
|
|
23149
|
+
return shortLabel(e.tool || "Investigation", 48);
|
|
23150
|
+
}
|
|
23151
|
+
function decisionCandidates(text) {
|
|
23152
|
+
const parts = text.split(/\n+|(?<=[.!?])\s+|,\s+(?=(?:so|because|since|rather|instead)\b)/i).map((s) => s.replace(/^[\s#>*-]+/, "").replace(/\*\*/g, "").trim()).filter((s) => s.length >= 15);
|
|
23153
|
+
const score = (s) => (intentFor(s) !== "context" ? 10 : 0) + (/\b(?:choose|decid|instead|suspect|likely|next|prefer)\w*\b/i.test(s) ? 5 : 0) + (/\b(?:because|since|so|avoid|tradeoff|due to|constraint|means)\b/i.test(s) ? 8 : 0);
|
|
23154
|
+
return [...new Set(parts)].map((text2, index) => ({ text: text2.slice(0, 320), index, score: score(text2) })).sort((a, b2) => b2.score - a.score || a.index - b2.index).slice(0, 6).map((p2) => p2.text);
|
|
23155
|
+
}
|
|
23156
|
+
function intentFor(text) {
|
|
23157
|
+
if (/\b(?:suspect|hypothesis|likely|probably|perhaps|maybe|might|could be|seems)\b/i.test(text)) return "hypothesis";
|
|
23158
|
+
if (/\b(?:alternative|option|versus|weigh|either|could (?:use|try))\b/i.test(text)) return "alternative";
|
|
23159
|
+
if (/\b(?:choose|decided?|prefer|instead|use .+ rather than|let['’]s use)\b/i.test(text)) return "decision";
|
|
23160
|
+
if (/\b(?:building|implementing|keeping) (?:it |this )?(?:as|with|in)\b/i.test(text)) return "decision";
|
|
23161
|
+
if (/\b(?:I['’]ll|I will|we should|need to|next|going to|plan to|first (?:check|test|inspect|read))\b/i.test(text)) return "next_step";
|
|
23162
|
+
return "context";
|
|
23163
|
+
}
|
|
23164
|
+
const ACTIVITIES = {
|
|
23165
|
+
investigating: "Reading files, searching, inspecting or diagnosing",
|
|
23166
|
+
editing: "Changing files, code, dependencies or configuration",
|
|
23167
|
+
testing: "Running tests, builds or validation",
|
|
23168
|
+
planning: "Discussing the approach or deciding what to do next",
|
|
23169
|
+
reporting: "Explaining findings or summarizing completed work"
|
|
23170
|
+
};
|
|
23171
|
+
const INTENTS = {
|
|
23172
|
+
decision: "A stated choice of approach or rejection of another approach",
|
|
23173
|
+
hypothesis: "An unverified explanation or suspicion to investigate",
|
|
23174
|
+
alternative: "Weighing different possible approaches without choosing yet",
|
|
23175
|
+
next_step: "A stated next action or check to perform",
|
|
23176
|
+
context: "General narration with no clear choice, hypothesis or next step"
|
|
23177
|
+
};
|
|
23178
|
+
const MAP_NODE_LIMIT = 240;
|
|
23179
|
+
class WorkMapTree {
|
|
23180
|
+
nodes = [];
|
|
23181
|
+
dropped = 0;
|
|
23182
|
+
goal = null;
|
|
23183
|
+
seq = 0;
|
|
23184
|
+
seen = /* @__PURE__ */ new Set();
|
|
23185
|
+
tools = /* @__PURE__ */ new Map();
|
|
23186
|
+
lastText = null;
|
|
23187
|
+
contextId;
|
|
23188
|
+
ingest(e) {
|
|
23189
|
+
if (this.seen.has(e.id)) return null;
|
|
23190
|
+
this.seen.add(e.id);
|
|
23191
|
+
if (this.seen.size > 1200) this.seen.delete(this.seen.values().next().value);
|
|
23192
|
+
if (e.kind === "result") {
|
|
23193
|
+
const id = this.tools.get(`${e.source}:${e.toolId}`);
|
|
23194
|
+
const node2 = this.nodes.find((n) => n.id === id);
|
|
23195
|
+
if (node2) {
|
|
23196
|
+
node2.status = e.status || "unknown";
|
|
23197
|
+
node2.evidence = [node2.evidence[0], e];
|
|
23198
|
+
}
|
|
23199
|
+
return null;
|
|
23200
|
+
}
|
|
23201
|
+
if (e.kind === "prompt" || e.kind === "message" || e.kind === "thinking") {
|
|
23202
|
+
const key = `${e.source}:${e.kind}:${e.text}`;
|
|
23203
|
+
const time = Date.parse(e.ts);
|
|
23204
|
+
if (this.lastText?.key === key && Math.abs(time - this.lastText.time) < 3e3) return null;
|
|
23205
|
+
this.lastText = { key, time };
|
|
23206
|
+
}
|
|
23207
|
+
if (e.kind === "prompt") {
|
|
23208
|
+
this.contextId = void 0;
|
|
23209
|
+
this.goal = this.add({ parentId: null, kind: "goal", title: shortLabel(e.text, 110), activity: "planning", status: "unknown", ts: e.ts, evidence: [e] }).id;
|
|
23210
|
+
this.trim();
|
|
23211
|
+
return null;
|
|
23212
|
+
}
|
|
23213
|
+
if (!this.goal) this.goal = this.add({ parentId: null, kind: "goal", title: "Session work", activity: "planning", status: "unknown", ts: e.ts, evidence: [] }).id;
|
|
23214
|
+
const topic = topicFor(e);
|
|
23215
|
+
const branch = this.branch(this.goal, topic, e.ts);
|
|
23216
|
+
const activity = activityFor(e);
|
|
23217
|
+
const statement = e.kind === "thinking" || e.kind === "message" ? e.segments?.[0] || decisionCandidates(e.text)[0] || e.text : e.text;
|
|
23218
|
+
const title = e.kind === "tool" ? `${e.tool || "Tool"} · ${shortLabel(e.text, 65)}` : shortLabel(statement, 110);
|
|
23219
|
+
const intent = e.kind === "thinking" || e.kind === "message" ? intentFor(statement) : void 0;
|
|
23220
|
+
const node = this.add({
|
|
23221
|
+
parentId: branch.id,
|
|
23222
|
+
kind: "step",
|
|
23223
|
+
title,
|
|
23224
|
+
activity,
|
|
23225
|
+
status: e.status || "unknown",
|
|
23226
|
+
ts: e.ts,
|
|
23227
|
+
evidence: [e],
|
|
23228
|
+
placement: "rules",
|
|
23229
|
+
intent,
|
|
23230
|
+
...intent && intent !== "context" ? { focus: { text: statement } } : {},
|
|
23231
|
+
...e.kind === "tool" && this.contextId ? { contextId: this.contextId } : {}
|
|
23232
|
+
});
|
|
23233
|
+
if (e.kind === "thinking" || e.kind === "message") this.contextId = node.id;
|
|
23234
|
+
if (e.toolId) this.tools.set(`${e.source}:${e.toolId}`, node.id);
|
|
23235
|
+
this.trim();
|
|
23236
|
+
return node.id;
|
|
23237
|
+
}
|
|
23238
|
+
end() {
|
|
23239
|
+
for (const n of this.nodes) if (n.status === "running") n.status = "interrupted";
|
|
23240
|
+
}
|
|
23241
|
+
request(ids) {
|
|
23242
|
+
const nodes = ids.map((id) => this.nodes.find((n) => n.id === id && n.kind === "step")).filter((n) => !!n);
|
|
23243
|
+
if (!nodes.length) return null;
|
|
23244
|
+
const parent = this.nodes.find((n) => n.id === nodes[0].parentId);
|
|
23245
|
+
if (!parent?.parentId) return null;
|
|
23246
|
+
const goalId = parent.parentId;
|
|
23247
|
+
const sameGoal = nodes.filter((n) => this.nodes.find((b2) => b2.id === n.parentId)?.parentId === goalId);
|
|
23248
|
+
const isIntent = (n) => n?.evidence[0]?.kind === "thinking" || n?.evidence[0]?.kind === "message";
|
|
23249
|
+
const selected = sameGoal.slice(0, isIntent(sameGoal[0]) || isIntent(sameGoal[1]) ? 1 : 2);
|
|
23250
|
+
const goal = this.nodes.find((n) => n.id === goalId);
|
|
23251
|
+
const branches = this.nodes.filter((n) => n.kind === "branch" && n.parentId === goalId).slice(-4);
|
|
23252
|
+
const criteria = Object.fromEntries(branches.map((n) => [n.id, n.title.slice(0, 48)]));
|
|
23253
|
+
criteria.new_topic = "A different work topic; create a new branch";
|
|
23254
|
+
const questions = {};
|
|
23255
|
+
const statements = {};
|
|
23256
|
+
const reasons = {};
|
|
23257
|
+
selected.forEach((node, i) => {
|
|
23258
|
+
questions[`branch_${i}`] = { type: "choice", instructions: `Which work topic does event_${i} belong to? Use the goal and stated reasoning, not incidental file paths.`, criteria };
|
|
23259
|
+
if (isIntent(node)) {
|
|
23260
|
+
const candidates = node.evidence[0].segments || decisionCandidates(node.evidence[0].text);
|
|
23261
|
+
statements[node.id] = candidates.length ? candidates : [node.evidence[0].text.slice(0, 220)];
|
|
23262
|
+
questions[`statement_${i}`] = {
|
|
23263
|
+
type: "choice",
|
|
23264
|
+
instructions: `Which excerpt best captures the agent's choice, hypothesis or next check in event_${i}?`,
|
|
23265
|
+
criteria: Object.fromEntries(statements[node.id].map((s, j) => [`s${j}`, s]))
|
|
23266
|
+
};
|
|
23267
|
+
questions[`intent_${i}`] = { type: "choice", instructions: `What does the selected excerpt express? A tentative explanation is not an established decision.`, criteria: INTENTS };
|
|
23268
|
+
reasons[node.id] = candidates.filter((s) => /\b(?:because|since|so|avoid|tradeoff|due to|constraint|rather|instead)\b/i.test(s)).slice(0, 3);
|
|
23269
|
+
if (reasons[node.id].length) questions[`reason_${i}`] = {
|
|
23270
|
+
type: "choice",
|
|
23271
|
+
instructions: "Which excerpt states why the agent chose its approach? Choose none if no supporting reason is stated.",
|
|
23272
|
+
criteria: { ...Object.fromEntries(reasons[node.id].map((s, j) => [`r${j}`, s])), none: "No supporting reason stated" }
|
|
23273
|
+
};
|
|
23274
|
+
} else {
|
|
23275
|
+
questions[`activity_${i}`] = { type: "choice", instructions: `What activity does event_${i} show?`, criteria: ACTIVITIES };
|
|
23276
|
+
}
|
|
23277
|
+
});
|
|
23278
|
+
return {
|
|
23279
|
+
ids: selected.map((n) => n.id),
|
|
23280
|
+
goalId,
|
|
23281
|
+
statements,
|
|
23282
|
+
reasons,
|
|
23283
|
+
request: {
|
|
23284
|
+
model: "clef-flash",
|
|
23285
|
+
state: JSON.stringify({ goal: goal.title, events: selected.map((n, i) => ({
|
|
23286
|
+
id: `event_${i}`,
|
|
23287
|
+
kind: n.evidence[0]?.kind,
|
|
23288
|
+
tool: n.evidence[0]?.tool,
|
|
23289
|
+
text: n.evidence[0]?.text.slice(0, 450),
|
|
23290
|
+
preceding_intent: this.nodes.find((c) => c.id === n.contextId)?.evidence[0]?.text.slice(-400),
|
|
23291
|
+
current_topic: this.nodes.find((b2) => b2.id === n.parentId)?.title
|
|
23292
|
+
})) }),
|
|
23293
|
+
questions
|
|
23294
|
+
}
|
|
23295
|
+
};
|
|
23296
|
+
}
|
|
23297
|
+
apply(job, answers) {
|
|
23298
|
+
job.ids.forEach((id, i) => {
|
|
23299
|
+
const node = this.nodes.find((n) => n.id === id);
|
|
23300
|
+
if (!node || !this.nodes.some((n) => n.id === job.goalId)) return;
|
|
23301
|
+
const decision = answers[`branch_${i}`];
|
|
23302
|
+
const textSource = node.evidence[0]?.kind === "thinking" || node.evidence[0]?.kind === "message";
|
|
23303
|
+
if (textSource) {
|
|
23304
|
+
const choice = answers[`statement_${i}`]?.choice;
|
|
23305
|
+
const statement = job.statements[node.id]?.[Number(choice?.slice(1))];
|
|
23306
|
+
if (statement) node.title = shortLabel(statement, 110);
|
|
23307
|
+
const statedIntent = intentFor(statement || "");
|
|
23308
|
+
node.intent = statedIntent !== "context" ? statedIntent : answers[`intent_${i}`]?.choice;
|
|
23309
|
+
const reasonChoice = answers[`reason_${i}`]?.choice;
|
|
23310
|
+
const reason = reasonChoice && reasonChoice !== "none" ? job.reasons[node.id]?.[Number(reasonChoice.slice(1))] : void 0;
|
|
23311
|
+
node.focus = statement ? { text: statement, ...reason && reason !== statement ? { reason } : {} } : void 0;
|
|
23312
|
+
}
|
|
23313
|
+
const topic = topicFor(node.evidence[0]);
|
|
23314
|
+
const newTitle = shortLabel(node.evidence[0].kind === "thinking" ? node.title : node.evidence[0].text, 48);
|
|
23315
|
+
const target = decision.choice === "new_topic" ? this.branch(job.goalId, newTitle === topic ? `${node.activity} · ${topic}` : newTitle, node.ts) : this.nodes.find((n) => n.id === decision.choice && n.kind === "branch" && n.parentId === job.goalId);
|
|
23316
|
+
if (!target) return;
|
|
23317
|
+
node.parentId = target.id;
|
|
23318
|
+
node.activity = node.evidence[0]?.kind === "thinking" ? "thinking" : textSource ? node.intent === "context" ? "reporting" : "planning" : answers[`activity_${i}`].choice;
|
|
23319
|
+
node.placement = "clef-flash";
|
|
23320
|
+
node.confidence = decision.probabilities[decision.choice];
|
|
23321
|
+
});
|
|
23322
|
+
this.removeEmptyBranches();
|
|
23323
|
+
this.trim();
|
|
23324
|
+
}
|
|
23325
|
+
add(node) {
|
|
23326
|
+
const result = { ...node, id: `map_${++this.seq}` };
|
|
23327
|
+
this.nodes.push(result);
|
|
23328
|
+
return result;
|
|
23329
|
+
}
|
|
23330
|
+
branch(goal, title, ts) {
|
|
23331
|
+
return this.nodes.find((n) => n.kind === "branch" && n.parentId === goal && n.title === title) || this.add({ parentId: goal, kind: "branch", title, activity: "planning", status: "unknown", ts, evidence: [] });
|
|
23332
|
+
}
|
|
23333
|
+
removeEmptyBranches() {
|
|
23334
|
+
const parents = new Set(this.nodes.map((n) => n.parentId));
|
|
23335
|
+
this.nodes = this.nodes.filter((n) => n.kind !== "branch" || parents.has(n.id));
|
|
23336
|
+
}
|
|
23337
|
+
trim() {
|
|
23338
|
+
while (this.nodes.length > MAP_NODE_LIMIT) {
|
|
23339
|
+
const oldGoal = this.nodes.find((n) => n.kind === "goal" && n.id !== this.goal);
|
|
23340
|
+
if (oldGoal) {
|
|
23341
|
+
const removed = /* @__PURE__ */ new Set([oldGoal.id, ...this.nodes.filter((n) => n.parentId === oldGoal.id).map((n) => n.id)]);
|
|
23342
|
+
const before = this.nodes.length;
|
|
23343
|
+
this.nodes = this.nodes.filter((n) => !removed.has(n.id) && !removed.has(n.parentId || ""));
|
|
23344
|
+
this.dropped += before - this.nodes.length;
|
|
23345
|
+
} else {
|
|
23346
|
+
const oldest = this.nodes.findIndex((n) => n.kind === "step");
|
|
23347
|
+
if (oldest < 0) break;
|
|
23348
|
+
this.nodes.splice(oldest, 1);
|
|
23349
|
+
this.dropped++;
|
|
23350
|
+
this.removeEmptyBranches();
|
|
23351
|
+
}
|
|
23352
|
+
}
|
|
23353
|
+
const retained = new Set(this.nodes.map((n) => n.id));
|
|
23354
|
+
if (this.contextId && !retained.has(this.contextId)) this.contextId = void 0;
|
|
23355
|
+
for (const [key, id] of this.tools) if (!retained.has(id)) this.tools.delete(key);
|
|
23356
|
+
}
|
|
23357
|
+
}
|
|
23358
|
+
async function readMapSource(file, agent, eventId) {
|
|
23359
|
+
const decoder = new string_decoder.StringDecoder("utf8");
|
|
23360
|
+
let remainder = "";
|
|
23361
|
+
let discard = false;
|
|
23362
|
+
const stream2 = fs.createReadStream(file, { highWaterMark: 64 * 1024 });
|
|
23363
|
+
for await (const chunk of stream2) {
|
|
23364
|
+
remainder += decoder.write(chunk);
|
|
23365
|
+
const lines = remainder.split("\n");
|
|
23366
|
+
remainder = lines.pop() || "";
|
|
23367
|
+
for (const line of lines) {
|
|
23368
|
+
if (discard) {
|
|
23369
|
+
discard = false;
|
|
23370
|
+
continue;
|
|
23371
|
+
}
|
|
23372
|
+
if (line.length > 1024 * 1024) continue;
|
|
23373
|
+
let raw;
|
|
23374
|
+
try {
|
|
23375
|
+
raw = JSON.parse(line);
|
|
23376
|
+
} catch {
|
|
23377
|
+
continue;
|
|
23378
|
+
}
|
|
23379
|
+
const event = normalizeMapEvents(raw, file, agent, true).find((e) => e.id === eventId);
|
|
23380
|
+
if (event?.fullText !== void 0) return { text: event.fullText, source: path$1.basename(file) };
|
|
23381
|
+
}
|
|
23382
|
+
if (remainder.length > 1024 * 1024) {
|
|
23383
|
+
remainder = "";
|
|
23384
|
+
discard = true;
|
|
23385
|
+
}
|
|
23386
|
+
}
|
|
23387
|
+
return null;
|
|
23388
|
+
}
|
|
23389
|
+
class WorkMap {
|
|
23390
|
+
constructor(appRoot, onUpdate, roots = {}, capability = checkWorkMapGpuExports.initialCapability()) {
|
|
23391
|
+
this.onUpdate = onUpdate;
|
|
23392
|
+
this.roots = roots;
|
|
23393
|
+
this.capability = capability;
|
|
23394
|
+
const cache = path$1.join(appRoot, "experiments/live-map/.cache");
|
|
23395
|
+
const prepared = path$1.join(os.homedir(), ".ctlsurf/clef-flash");
|
|
23396
|
+
const pilotPython = path$1.join(cache, "envs/clef-flash/bin/python");
|
|
23397
|
+
this.settings = {
|
|
23398
|
+
enabled: false,
|
|
23399
|
+
engine: "clef-flash",
|
|
23400
|
+
pythonPath: process.env.CTLSURF_CLEF_PYTHON || (fs.existsSync(pilotPython) ? pilotPython : path$1.join(prepared, "env/bin/python")),
|
|
23401
|
+
modelPath: process.env.CTLSURF_CLEF_MODEL || (fs.existsSync(pilotPython) ? path$1.join(cache, "models/clef-flash") : path$1.join(prepared, "model"))
|
|
23402
|
+
};
|
|
23403
|
+
this.clef = new ClefWorker(path$1.join(appRoot, "scripts/work-map-clef.py"), () => this.publish());
|
|
23404
|
+
}
|
|
23405
|
+
tabs = /* @__PURE__ */ new Map();
|
|
23406
|
+
owners = /* @__PURE__ */ new Map();
|
|
23407
|
+
settings;
|
|
23408
|
+
clef;
|
|
23409
|
+
revision = 0;
|
|
23410
|
+
epoch = 0;
|
|
23411
|
+
timer = null;
|
|
23412
|
+
publishing = null;
|
|
23413
|
+
working = false;
|
|
23414
|
+
round = 0;
|
|
23415
|
+
configure(value) {
|
|
23416
|
+
++this.epoch;
|
|
23417
|
+
const previous = this.settings;
|
|
23418
|
+
this.settings = {
|
|
23419
|
+
enabled: this.capability.state === "available" && (typeof value.enabled === "boolean" ? value.enabled : previous.enabled),
|
|
23420
|
+
engine: value.engine === "rules" || value.engine === "clef-flash" ? value.engine : previous.engine,
|
|
23421
|
+
pythonPath: typeof value.pythonPath === "string" && value.pythonPath.trim() ? value.pythonPath.trim() : previous.pythonPath,
|
|
23422
|
+
modelPath: typeof value.modelPath === "string" && value.modelPath.trim() ? value.modelPath.trim() : previous.modelPath
|
|
23423
|
+
};
|
|
23424
|
+
this.clef.stop();
|
|
23425
|
+
if (this.timer) clearTimeout(this.timer);
|
|
23426
|
+
this.timer = null;
|
|
23427
|
+
for (const tab of this.tabs.values()) {
|
|
23428
|
+
tab.pending.clear();
|
|
23429
|
+
if (this.settings.enabled && !tab.ended) this.start(tab);
|
|
23430
|
+
else {
|
|
23431
|
+
tab.tailer?.stop();
|
|
23432
|
+
tab.tailer = null;
|
|
23433
|
+
tab.tree.end();
|
|
23434
|
+
}
|
|
23435
|
+
if (this.settings.enabled && this.settings.engine === "clef-flash") {
|
|
23436
|
+
tab.tree.nodes.filter((n) => n.kind === "step" && n.placement !== "clef-flash").slice(-24).forEach((n) => tab.pending.add(n.id));
|
|
23437
|
+
}
|
|
23438
|
+
}
|
|
23439
|
+
this.publish();
|
|
23440
|
+
this.schedule();
|
|
23441
|
+
return { ...this.settings };
|
|
23442
|
+
}
|
|
23443
|
+
attach(tabId, agentId, agentName, cwd) {
|
|
23444
|
+
this.remove(tabId);
|
|
23445
|
+
const tab = { agentId, agentName, cwd, tree: new WorkMapTree(), tailer: null, ended: false, state: "idle", source: null, ignored: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Set(), thinkingStatus: "waiting", sources: /* @__PURE__ */ new Map() };
|
|
23446
|
+
this.tabs.set(tabId, tab);
|
|
23447
|
+
if (this.settings.enabled) this.start(tab);
|
|
23448
|
+
this.publish();
|
|
23449
|
+
}
|
|
23450
|
+
getCapability() {
|
|
23451
|
+
return { ...this.capability };
|
|
23452
|
+
}
|
|
23453
|
+
setCapability(capability) {
|
|
23454
|
+
this.capability = { ...capability };
|
|
23455
|
+
if (capability.state !== "available") this.configure({ enabled: false });
|
|
23456
|
+
this.publish();
|
|
23457
|
+
}
|
|
23458
|
+
end(tabId) {
|
|
23459
|
+
const tab = this.tabs.get(tabId);
|
|
23460
|
+
if (!tab) return;
|
|
23461
|
+
tab.ended = true;
|
|
23462
|
+
tab.tailer?.stop();
|
|
23463
|
+
tab.tailer = null;
|
|
23464
|
+
tab.pending.clear();
|
|
23465
|
+
tab.tree.end();
|
|
23466
|
+
for (const [source, owner] of this.owners) if (owner === tabId) this.owners.delete(source);
|
|
23467
|
+
if (![...this.tabs.values()].some((t) => !t.ended)) this.clef.stop();
|
|
23468
|
+
this.publish();
|
|
23469
|
+
}
|
|
23470
|
+
remove(tabId) {
|
|
23471
|
+
this.end(tabId);
|
|
23472
|
+
this.tabs.delete(tabId);
|
|
23473
|
+
for (const [source, owner] of this.owners) if (owner === tabId) this.owners.delete(source);
|
|
23474
|
+
}
|
|
23475
|
+
clear(tabId) {
|
|
23476
|
+
const tab = this.tabs.get(tabId);
|
|
23477
|
+
if (tab) {
|
|
23478
|
+
tab.tree = new WorkMapTree();
|
|
23479
|
+
tab.pending.clear();
|
|
23480
|
+
}
|
|
23481
|
+
this.publish();
|
|
23482
|
+
}
|
|
23483
|
+
noteState(tabId, state) {
|
|
23484
|
+
const tab = this.tabs.get(tabId);
|
|
23485
|
+
if (tab && tab.state !== state) {
|
|
23486
|
+
tab.state = state;
|
|
23487
|
+
this.publish();
|
|
23488
|
+
}
|
|
23489
|
+
}
|
|
23490
|
+
snapshot(tabId) {
|
|
23491
|
+
const tab = this.tabs.get(tabId);
|
|
23492
|
+
return {
|
|
23493
|
+
revision: ++this.revision,
|
|
23494
|
+
tabId,
|
|
23495
|
+
agentName: tab?.agentName || null,
|
|
23496
|
+
supported: !!tab && supportsTranscriptLogging(tab.agentId),
|
|
23497
|
+
ended: !!tab?.ended,
|
|
23498
|
+
state: tab?.state || "idle",
|
|
23499
|
+
settings: { ...this.settings },
|
|
23500
|
+
runtime: !this.settings.enabled ? "off" : this.settings.engine === "rules" ? "ready" : this.clef.state,
|
|
23501
|
+
error: this.settings.engine === "clef-flash" ? this.clef.error : null,
|
|
23502
|
+
nodes: tab?.tree.nodes || [],
|
|
23503
|
+
dropped: tab?.tree.dropped || 0,
|
|
23504
|
+
thinkingStatus: tab?.thinkingStatus || "waiting",
|
|
23505
|
+
capability: this.getCapability()
|
|
23506
|
+
};
|
|
23507
|
+
}
|
|
23508
|
+
async getSource(tabId, eventId) {
|
|
23509
|
+
const tab = this.tabs.get(tabId);
|
|
23510
|
+
const event = tab?.tree.nodes.flatMap((n) => n.evidence).find((e) => e.id === eventId && (e.kind === "thinking" || e.kind === "message"));
|
|
23511
|
+
const file = event && tab?.sources.get(event.source);
|
|
23512
|
+
if (!event || !file || !tab) return null;
|
|
23513
|
+
return readMapSource(file, tab.agentId, event.id);
|
|
23514
|
+
}
|
|
23515
|
+
start(tab) {
|
|
23516
|
+
if (tab.tailer || !supportsTranscriptLogging(tab.agentId)) return;
|
|
23517
|
+
tab.tailer = new TranscriptTailer({
|
|
23518
|
+
...this.roots,
|
|
23519
|
+
agentId: tab.agentId,
|
|
23520
|
+
cwd: tab.cwd,
|
|
23521
|
+
sink: () => {
|
|
23522
|
+
},
|
|
23523
|
+
rawSink: (raw, source) => {
|
|
23524
|
+
if (!source || !this.settings.enabled || tab.ended) return;
|
|
23525
|
+
const file = source.filePath;
|
|
23526
|
+
if (tab.ignored.has(file)) return;
|
|
23527
|
+
if (raw.type === "session_meta") {
|
|
23528
|
+
const meta = raw.payload || {};
|
|
23529
|
+
if (meta.thread_source && meta.thread_source !== "user" || meta.source && typeof meta.source === "object" || meta.agent_path && meta.agent_path !== "/root") {
|
|
23530
|
+
tab.ignored.add(file);
|
|
23531
|
+
return;
|
|
23532
|
+
}
|
|
23533
|
+
}
|
|
23534
|
+
const tabId = [...this.tabs].find(([, candidate]) => candidate === tab)?.[0];
|
|
23535
|
+
if (!tabId) return;
|
|
23536
|
+
if (this.owners.has(file) && this.owners.get(file) !== tabId) return;
|
|
23537
|
+
if (tab.source && tab.source !== file) {
|
|
23538
|
+
if (raw.type !== "session_meta" || Date.parse(raw.payload?.timestamp || raw.timestamp) < source.sinceMs) return;
|
|
23539
|
+
this.owners.delete(tab.source);
|
|
23540
|
+
}
|
|
23541
|
+
tab.source = file;
|
|
23542
|
+
this.owners.set(file, tabId);
|
|
23543
|
+
const thinking = mapThinkingStatus(raw, tab.agentId);
|
|
23544
|
+
const statusChanged = thinking !== null && thinking !== tab.thinkingStatus && (thinking === "available" || tab.thinkingStatus === "waiting");
|
|
23545
|
+
if (statusChanged) tab.thinkingStatus = thinking;
|
|
23546
|
+
const events = normalizeMapEvents(raw, file, tab.agentId);
|
|
23547
|
+
for (const e of events) {
|
|
23548
|
+
tab.sources.set(e.source, file);
|
|
23549
|
+
const id = tab.tree.ingest(e);
|
|
23550
|
+
if (id && this.settings.engine === "clef-flash") {
|
|
23551
|
+
tab.pending.add(id);
|
|
23552
|
+
if (tab.pending.size > 24) tab.pending.delete(tab.pending.values().next().value);
|
|
23553
|
+
}
|
|
23554
|
+
}
|
|
23555
|
+
const retainedSources = new Set(tab.tree.nodes.flatMap((n) => n.evidence.map((e) => e.source)));
|
|
23556
|
+
for (const name of tab.sources.keys()) if (!retainedSources.has(name)) tab.sources.delete(name);
|
|
23557
|
+
if (events.length || statusChanged) {
|
|
23558
|
+
this.publish();
|
|
23559
|
+
this.schedule();
|
|
23560
|
+
}
|
|
23561
|
+
}
|
|
23562
|
+
});
|
|
23563
|
+
tab.tailer.start();
|
|
23564
|
+
}
|
|
23565
|
+
schedule() {
|
|
23566
|
+
if (this.timer || this.working || !this.settings.enabled || this.settings.engine !== "clef-flash" || this.clef.state === "error") return;
|
|
23567
|
+
if (![...this.tabs.values()].some((t) => t.pending.size)) return;
|
|
23568
|
+
this.timer = setTimeout(() => {
|
|
23569
|
+
this.timer = null;
|
|
23570
|
+
void this.infer();
|
|
23571
|
+
}, 1e3);
|
|
23572
|
+
}
|
|
23573
|
+
async infer() {
|
|
23574
|
+
const candidates = [...this.tabs].filter(([, t]) => t.pending.size && !t.ended);
|
|
23575
|
+
const candidate = candidates[this.round++ % candidates.length];
|
|
23576
|
+
if (!candidate || !this.settings.enabled || this.settings.engine !== "clef-flash") return;
|
|
23577
|
+
const [tabId, tab] = candidate;
|
|
23578
|
+
const tree = tab.tree;
|
|
23579
|
+
const ids = [...tab.pending].slice(0, 2);
|
|
23580
|
+
const job = tree.request(ids);
|
|
23581
|
+
const consumed = job?.ids || ids;
|
|
23582
|
+
consumed.forEach((id) => tab.pending.delete(id));
|
|
23583
|
+
if (!job) {
|
|
23584
|
+
this.schedule();
|
|
23585
|
+
return;
|
|
23586
|
+
}
|
|
23587
|
+
const epoch = this.epoch;
|
|
23588
|
+
this.working = true;
|
|
23589
|
+
try {
|
|
23590
|
+
const answers = await this.clef.predict(this.settings, job.request);
|
|
23591
|
+
if (epoch === this.epoch && this.tabs.get(tabId) === tab && !tab.ended && tab.tree === tree) tree.apply(job, answers);
|
|
23592
|
+
} catch {
|
|
23593
|
+
} finally {
|
|
23594
|
+
this.working = false;
|
|
23595
|
+
this.publish();
|
|
23596
|
+
this.schedule();
|
|
23597
|
+
}
|
|
23598
|
+
}
|
|
23599
|
+
publish() {
|
|
23600
|
+
if (this.publishing) return;
|
|
23601
|
+
this.publishing = setTimeout(() => {
|
|
23602
|
+
this.publishing = null;
|
|
23603
|
+
for (const tabId of this.tabs.keys()) this.onUpdate(this.snapshot(tabId));
|
|
23604
|
+
}, 120);
|
|
23605
|
+
}
|
|
23606
|
+
shutdown() {
|
|
23607
|
+
this.settings.enabled = false;
|
|
23608
|
+
++this.epoch;
|
|
23609
|
+
for (const tab of this.tabs.values()) tab.tailer?.stop();
|
|
23610
|
+
this.clef.stop();
|
|
23611
|
+
if (this.timer) clearTimeout(this.timer);
|
|
23612
|
+
if (this.publishing) clearTimeout(this.publishing);
|
|
23613
|
+
this.timer = this.publishing = null;
|
|
23614
|
+
this.tabs.clear();
|
|
23615
|
+
this.owners.clear();
|
|
23616
|
+
}
|
|
23617
|
+
}
|
|
22759
23618
|
const DEFAULT_IDLE_TIMEOUT_MIN = 15;
|
|
22760
23619
|
const DEFAULT_PROFILES = {
|
|
22761
23620
|
production: {
|
|
@@ -22779,6 +23638,7 @@ class Orchestrator {
|
|
|
22779
23638
|
timeTracker = new TimeTracker(this.ctlsurfApi);
|
|
22780
23639
|
ticketStore = new TicketStore(this.ctlsurfApi);
|
|
22781
23640
|
observer;
|
|
23641
|
+
workMap;
|
|
22782
23642
|
// State
|
|
22783
23643
|
tabs = /* @__PURE__ */ new Map();
|
|
22784
23644
|
activeTabId = null;
|
|
@@ -22798,19 +23658,24 @@ class Orchestrator {
|
|
|
22798
23658
|
// until the next remote spawn for that target consumes them — never persisted.
|
|
22799
23659
|
pendingSshPasswords = /* @__PURE__ */ new Map();
|
|
22800
23660
|
pendingSshPasswordTimers = /* @__PURE__ */ new Map();
|
|
22801
|
-
constructor(settingsDir, events) {
|
|
23661
|
+
constructor(settingsDir, events, options) {
|
|
22802
23662
|
this.settingsDir = settingsDir;
|
|
22803
23663
|
this.events = events;
|
|
23664
|
+
this.workMap = new WorkMap(options?.appRoot || process.cwd(), (snapshot) => this.events.onWorkMapUpdate?.(snapshot), {}, checkWorkMapGpuExports.initialCapability({ desktop: options?.desktop === true }));
|
|
22804
23665
|
this.observer = new Observer({
|
|
22805
23666
|
settingsDir,
|
|
22806
23667
|
api: this.ctlsurfApi,
|
|
22807
|
-
getSettings: () =>
|
|
23668
|
+
getSettings: () => ({
|
|
23669
|
+
...this.settings.observer,
|
|
23670
|
+
enabled: false
|
|
23671
|
+
}),
|
|
22808
23672
|
onUpdate: (item) => this.events.onObserverUpdate?.(item),
|
|
22809
23673
|
onActivity: (reviewing) => this.events.onObserverActivity?.(reviewing)
|
|
22810
23674
|
});
|
|
22811
23675
|
this.stateMonitor = new AgentStateMonitor(({ tabId, state, anyAwaiting }) => {
|
|
22812
23676
|
this.events.onAgentStateChange?.(tabId, state, anyAwaiting);
|
|
22813
23677
|
this.observer.noteAgentState(tabId, state);
|
|
23678
|
+
this.workMap.noteState(tabId, state);
|
|
22814
23679
|
});
|
|
22815
23680
|
this.stateMonitor.start();
|
|
22816
23681
|
this.workerWs = new WorkerWsClient({
|
|
@@ -22882,13 +23747,24 @@ class Orchestrator {
|
|
|
22882
23747
|
this.saveSettings();
|
|
22883
23748
|
this.restartActiveChatLogging();
|
|
22884
23749
|
}
|
|
23750
|
+
configureWorkMap(data) {
|
|
23751
|
+
const capability = this.workMap.getCapability();
|
|
23752
|
+
if (data.enabled && capability.state !== "available") throw new Error(capability.reason || "Work map is unavailable on this machine.");
|
|
23753
|
+
this.settings.workMap = this.workMap.configure(data);
|
|
23754
|
+
this.saveSettings();
|
|
23755
|
+
}
|
|
23756
|
+
setWorkMapCapability(capability) {
|
|
23757
|
+
this.workMap.setCapability(capability);
|
|
23758
|
+
this.workMap.configure({ ...this.settings.workMap, enabled: capability.state === "available" && !!this.settings.workMap?.enabled });
|
|
23759
|
+
this.events.onWorkMapCapability?.(capability);
|
|
23760
|
+
}
|
|
22885
23761
|
// ─── Observer settings ──────────────────────────
|
|
22886
23762
|
/** Summary for the settings UI — never echoes the API key back. */
|
|
22887
23763
|
getObserverSettingsSummary() {
|
|
22888
23764
|
const o = this.settings.observer || {};
|
|
22889
23765
|
const keySource = o.anthropicApiKey ? "settings" : process.env.ANTHROPIC_API_KEY ? "env" : null;
|
|
22890
23766
|
return {
|
|
22891
|
-
enabled:
|
|
23767
|
+
enabled: false,
|
|
22892
23768
|
hasApiKey: keySource !== null,
|
|
22893
23769
|
keySource,
|
|
22894
23770
|
dailyCapUsd: o.dailyCapUsd ?? OBSERVER_DEFAULTS.dailyCapUsd
|
|
@@ -23048,6 +23924,7 @@ class Orchestrator {
|
|
|
23048
23924
|
}
|
|
23049
23925
|
this.applyProfile(this.getActiveProfile());
|
|
23050
23926
|
this.bridge.setLoggingEnabled(!!this.settings.logChat);
|
|
23927
|
+
this.workMap.configure({ ...this.settings.workMap, enabled: !!this.events.onWorkMapUpdate && !!this.settings.workMap?.enabled });
|
|
23051
23928
|
}
|
|
23052
23929
|
saveSettings() {
|
|
23053
23930
|
const settingsPath = path$1.join(this.settingsDir, "settings.json");
|
|
@@ -23194,6 +24071,7 @@ class Orchestrator {
|
|
|
23194
24071
|
this.tabs.delete(tabId);
|
|
23195
24072
|
this.stateMonitor.removeTab(tabId);
|
|
23196
24073
|
void this.observer.endTab(tabId, existing.projectRootPath);
|
|
24074
|
+
this.workMap.remove(tabId);
|
|
23197
24075
|
existing.ptyManager.kill();
|
|
23198
24076
|
await this.timeTracker.endSession(tabId);
|
|
23199
24077
|
}
|
|
@@ -23208,6 +24086,7 @@ class Orchestrator {
|
|
|
23208
24086
|
const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
|
|
23209
24087
|
this.tabs.set(tabId, tab);
|
|
23210
24088
|
this.stateMonitor.trackTab(tabId, agent.id, cwd);
|
|
24089
|
+
this.workMap.attach(tabId, agent.id, agent.name, cwd);
|
|
23211
24090
|
if (isRemoteAgent(agent)) {
|
|
23212
24091
|
const target = agent.args[1];
|
|
23213
24092
|
const pw = target ? this.consumePendingSshPassword(target) : void 0;
|
|
@@ -23229,6 +24108,7 @@ class Orchestrator {
|
|
|
23229
24108
|
if (this.tabs.get(tabId) !== tab) return;
|
|
23230
24109
|
this.events.onPtyExit(tabId, exitCode);
|
|
23231
24110
|
this.observer.endTab(tabId, tab.projectRootPath);
|
|
24111
|
+
this.workMap.end(tabId);
|
|
23232
24112
|
await this.timeTracker.endSession(tabId);
|
|
23233
24113
|
if (this.tabs.get(tabId) !== tab) return;
|
|
23234
24114
|
if (tabId === this.activeTabId) {
|
|
@@ -23279,6 +24159,7 @@ class Orchestrator {
|
|
|
23279
24159
|
}
|
|
23280
24160
|
async killTab(tabId) {
|
|
23281
24161
|
const tab = this.tabs.get(tabId);
|
|
24162
|
+
this.workMap.remove(tabId);
|
|
23282
24163
|
if (!tab) return;
|
|
23283
24164
|
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|
|
23284
24165
|
await this.timeTracker.endSession(tabId);
|
|
@@ -23479,6 +24360,7 @@ class Orchestrator {
|
|
|
23479
24360
|
}
|
|
23480
24361
|
// ─── Shutdown ───────────────────────────────────
|
|
23481
24362
|
async shutdown() {
|
|
24363
|
+
this.workMap.shutdown();
|
|
23482
24364
|
this.stopNoProjectPolling();
|
|
23483
24365
|
this.stopChatLogging();
|
|
23484
24366
|
this.stateMonitor.stop();
|
|
@@ -24024,6 +24906,8 @@ const orchestrator = new Orchestrator(
|
|
|
24024
24906
|
onAgentMessage: (text) => mainWindow?.webContents.send("agent:message", text),
|
|
24025
24907
|
onObserverUpdate: (item) => mainWindow?.webContents.send("observer:update", item),
|
|
24026
24908
|
onObserverActivity: (reviewing) => mainWindow?.webContents.send("observer:activity", reviewing),
|
|
24909
|
+
onWorkMapUpdate: (snapshot) => mainWindow?.webContents.send("workmap:update", snapshot),
|
|
24910
|
+
onWorkMapCapability: (capability) => mainWindow?.webContents.send("workmap:capability", capability),
|
|
24027
24911
|
onAgentStateChange: (tabId, state, anyAwaiting) => {
|
|
24028
24912
|
log(`[agent-state] tab=${tabId} state=${state} anyAwaiting=${anyAwaiting}`);
|
|
24029
24913
|
mainWindow?.webContents.send("agent:state", tabId, state, anyAwaiting);
|
|
@@ -24033,8 +24917,13 @@ const orchestrator = new Orchestrator(
|
|
|
24033
24917
|
mainWindow?.webContents.send("app:cwdChanged");
|
|
24034
24918
|
updateProjectBadge(orchestrator.getActiveTabProjectPath() || orchestrator.cwd);
|
|
24035
24919
|
}
|
|
24036
|
-
}
|
|
24920
|
+
},
|
|
24921
|
+
{ appRoot: workMapRoot(), desktop: true }
|
|
24037
24922
|
);
|
|
24923
|
+
function workMapRoot() {
|
|
24924
|
+
const resources = process.resourcesPath;
|
|
24925
|
+
return fs.existsSync(path$1.join(resources, "scripts", "work-map-clef.py")) ? resources : electron.app.getAppPath();
|
|
24926
|
+
}
|
|
24038
24927
|
function createWindow() {
|
|
24039
24928
|
const saved = orchestrator.settingsData;
|
|
24040
24929
|
if (saved.themeSource === "light" || saved.themeSource === "dark" || saved.themeSource === "system") {
|
|
@@ -24379,6 +25268,30 @@ electron.ipcMain.handle("speak:set", (_event, enabled) => {
|
|
|
24379
25268
|
orchestrator.setSpeakReplies(!!enabled);
|
|
24380
25269
|
return { enabled: orchestrator.speakRepliesEnabled };
|
|
24381
25270
|
});
|
|
25271
|
+
electron.ipcMain.handle("workmap:get", (_event, tabId) => orchestrator.workMap.snapshot(String(tabId)));
|
|
25272
|
+
electron.ipcMain.handle("workmap:capability", () => orchestrator.workMap.getCapability());
|
|
25273
|
+
electron.ipcMain.handle("workmap:source", (_event, tabId, eventId) => orchestrator.workMap.getSource(String(tabId), String(eventId)));
|
|
25274
|
+
electron.ipcMain.handle("workmap:set", (_event, tabId, data) => {
|
|
25275
|
+
if (!data || typeof data !== "object") throw new Error("Invalid work map settings");
|
|
25276
|
+
const input = data;
|
|
25277
|
+
const clean = {
|
|
25278
|
+
...orchestrator.workMap.snapshot(String(tabId)).settings
|
|
25279
|
+
};
|
|
25280
|
+
if (typeof input.enabled === "boolean") clean.enabled = input.enabled;
|
|
25281
|
+
if (input.engine === "rules" || input.engine === "clef-flash") clean.engine = input.engine;
|
|
25282
|
+
for (const key of ["pythonPath", "modelPath"]) {
|
|
25283
|
+
if (typeof input[key] === "string") {
|
|
25284
|
+
if (input[key].length > 4096 || !path$1.isAbsolute(input[key])) throw new Error("Model paths must be absolute local paths");
|
|
25285
|
+
clean[key] = input[key];
|
|
25286
|
+
}
|
|
25287
|
+
}
|
|
25288
|
+
orchestrator.configureWorkMap(clean);
|
|
25289
|
+
return orchestrator.workMap.snapshot(String(tabId));
|
|
25290
|
+
});
|
|
25291
|
+
electron.ipcMain.handle("workmap:clear", (_event, tabId) => {
|
|
25292
|
+
orchestrator.workMap.clear(String(tabId));
|
|
25293
|
+
return orchestrator.workMap.snapshot(String(tabId));
|
|
25294
|
+
});
|
|
24382
25295
|
electron.ipcMain.handle("observer:get", () => orchestrator.getObserverSettingsSummary());
|
|
24383
25296
|
electron.ipcMain.handle("observer:set", (_event, data) => {
|
|
24384
25297
|
orchestrator.saveObserverSettings(data);
|
|
@@ -24449,6 +25362,9 @@ electron.app.whenReady().then(() => {
|
|
|
24449
25362
|
}
|
|
24450
25363
|
orchestrator.loadSettings();
|
|
24451
25364
|
createWindow();
|
|
25365
|
+
void checkWorkMapGpuExports.detectCapability({ desktop: true, verifyGpu: async () => checkWorkMapGpuExports.usableElectronGpu(await electron.app.getGPUInfo("complete")) }).then((capability) => {
|
|
25366
|
+
if (!shutdownStarted) orchestrator.setWorkMapCapability(capability);
|
|
25367
|
+
});
|
|
24452
25368
|
setTimeout(() => {
|
|
24453
25369
|
void checkForUpdate();
|
|
24454
25370
|
}, 3e3);
|