@phenx-inc/ctlsurf 0.17.1 → 0.17.3

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.
Files changed (52) hide show
  1. package/README.md +25 -2
  2. package/bin/ctlsurf-worker.js +24 -0
  3. package/out/headless/index.mjs +1033 -128
  4. package/out/headless/index.mjs.map +4 -4
  5. package/out/main/index.js +929 -17
  6. package/out/preload/index.js +15 -0
  7. package/out/renderer/assets/{cssMode-BtQxI2Eb.js → cssMode-BJfoToLX.js} +3 -3
  8. package/out/renderer/assets/{freemarker2-DRW5dmY5.js → freemarker2-T0eMtP1Z.js} +1 -1
  9. package/out/renderer/assets/{handlebars-CFLF3cP7.js → handlebars-Dbbib8fi.js} +1 -1
  10. package/out/renderer/assets/{html-DtnBOkwp.js → html-BRHT_oxc.js} +1 -1
  11. package/out/renderer/assets/{htmlMode-DMVysDCg.js → htmlMode-BRlZTxME.js} +3 -3
  12. package/out/renderer/assets/{index-CEjwKjtd.css → index-CBWAeUYW.css} +91 -0
  13. package/out/renderer/assets/{index-Dm70hk5s.js → index-RCAeuJTr.js} +476 -411
  14. package/out/renderer/assets/{javascript-ChlyA1sf.js → javascript-BbOZOxq9.js} +2 -2
  15. package/out/renderer/assets/{jsonMode-D1_oHzeN.js → jsonMode-CUvAeYcT.js} +3 -3
  16. package/out/renderer/assets/{liquid-D4ftdy1G.js → liquid-DRIACo_s.js} +1 -1
  17. package/out/renderer/assets/{lspLanguageFeatures-Ckqu5ubG.js → lspLanguageFeatures-Ds-3nH9p.js} +1 -1
  18. package/out/renderer/assets/{mdx-B9wsVYSC.js → mdx-D76jlpLO.js} +1 -1
  19. package/out/renderer/assets/{python-e23tD6IS.js → python-CMuRPKJi.js} +1 -1
  20. package/out/renderer/assets/{razor-CrDyXj3d.js → razor-DRoH5mal.js} +1 -1
  21. package/out/renderer/assets/{tsMode-BwGC5o4O.js → tsMode-dPJzgFD7.js} +1 -1
  22. package/out/renderer/assets/{typescript-Do-98d_O.js → typescript-D5USKnLj.js} +1 -1
  23. package/out/renderer/assets/{xml-Dpdu5R3m.js → xml-qYg1wt2P.js} +1 -1
  24. package/out/renderer/assets/{yaml-B0Zt7LCJ.js → yaml-Uc-CvnvL.js} +1 -1
  25. package/out/renderer/index.html +2 -2
  26. package/package.json +4 -2
  27. package/scripts/check-work-map-gpu.cjs +62 -0
  28. package/scripts/check-work-map-gpu.d.cts +13 -0
  29. package/scripts/postinstall.cjs +15 -0
  30. package/scripts/prepare-work-map.py +37 -0
  31. package/scripts/test-work-map-capability.cjs +45 -0
  32. package/scripts/test-work-map-ui.cjs +130 -0
  33. package/scripts/test-work-map.cjs +352 -0
  34. package/scripts/work-map-clef.py +81 -0
  35. package/src/main/index.ts +35 -1
  36. package/src/main/observer/observer.ts +2 -0
  37. package/src/main/orchestrator.ts +39 -4
  38. package/src/main/transcripts.ts +35 -16
  39. package/src/main/workMap/clef.ts +156 -0
  40. package/src/main/workMap/normalize.ts +172 -0
  41. package/src/main/workMap/source.ts +28 -0
  42. package/src/main/workMap/tree.ts +199 -0
  43. package/src/main/workMap/workMap.ts +244 -0
  44. package/src/preload/index.ts +17 -0
  45. package/src/renderer/App.tsx +15 -16
  46. package/src/renderer/components/ObserverPanel.tsx +3 -0
  47. package/src/renderer/components/SettingsDialog.tsx +0 -104
  48. package/src/renderer/components/WorkMapPanel.tsx +235 -0
  49. package/src/renderer/components/WorkMapToggle.tsx +50 -0
  50. package/src/renderer/lib/workMapHighlights.ts +41 -0
  51. package/src/renderer/styles.css +91 -0
  52. 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 <= tail.offset) return;
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
- while (tail.offset < size && !tail.excluded) {
6371
- const bytesRead = fs.readSync(fd, buf, 0, READ_CHUNK_BYTES, tail.offset);
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.toString("utf-8", 0, bytesRead);
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
- this.handleLine(line, tail);
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 === "string" && stripTrailingSep$2(metaCwd) !== this.cwd) {
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 compact = window2.map((e) => {
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
- ${compact.join("\n")}`;
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: () => this.settings.observer || {},
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: !!o.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,7 +24917,8 @@ const orchestrator = new Orchestrator(
24033
24917
  mainWindow?.webContents.send("app:cwdChanged");
24034
24918
  updateProjectBadge(orchestrator.getActiveTabProjectPath() || orchestrator.cwd);
24035
24919
  }
24036
- }
24920
+ },
24921
+ { appRoot: electron.app.isPackaged ? process.resourcesPath : electron.app.getAppPath(), desktop: true }
24037
24922
  );
24038
24923
  function createWindow() {
24039
24924
  const saved = orchestrator.settingsData;
@@ -24379,6 +25264,30 @@ electron.ipcMain.handle("speak:set", (_event, enabled) => {
24379
25264
  orchestrator.setSpeakReplies(!!enabled);
24380
25265
  return { enabled: orchestrator.speakRepliesEnabled };
24381
25266
  });
25267
+ electron.ipcMain.handle("workmap:get", (_event, tabId) => orchestrator.workMap.snapshot(String(tabId)));
25268
+ electron.ipcMain.handle("workmap:capability", () => orchestrator.workMap.getCapability());
25269
+ electron.ipcMain.handle("workmap:source", (_event, tabId, eventId) => orchestrator.workMap.getSource(String(tabId), String(eventId)));
25270
+ electron.ipcMain.handle("workmap:set", (_event, tabId, data) => {
25271
+ if (!data || typeof data !== "object") throw new Error("Invalid work map settings");
25272
+ const input = data;
25273
+ const clean = {
25274
+ ...orchestrator.workMap.snapshot(String(tabId)).settings
25275
+ };
25276
+ if (typeof input.enabled === "boolean") clean.enabled = input.enabled;
25277
+ if (input.engine === "rules" || input.engine === "clef-flash") clean.engine = input.engine;
25278
+ for (const key of ["pythonPath", "modelPath"]) {
25279
+ if (typeof input[key] === "string") {
25280
+ if (input[key].length > 4096 || !path$1.isAbsolute(input[key])) throw new Error("Model paths must be absolute local paths");
25281
+ clean[key] = input[key];
25282
+ }
25283
+ }
25284
+ orchestrator.configureWorkMap(clean);
25285
+ return orchestrator.workMap.snapshot(String(tabId));
25286
+ });
25287
+ electron.ipcMain.handle("workmap:clear", (_event, tabId) => {
25288
+ orchestrator.workMap.clear(String(tabId));
25289
+ return orchestrator.workMap.snapshot(String(tabId));
25290
+ });
24382
25291
  electron.ipcMain.handle("observer:get", () => orchestrator.getObserverSettingsSummary());
24383
25292
  electron.ipcMain.handle("observer:set", (_event, data) => {
24384
25293
  orchestrator.saveObserverSettings(data);
@@ -24449,6 +25358,9 @@ electron.app.whenReady().then(() => {
24449
25358
  }
24450
25359
  orchestrator.loadSettings();
24451
25360
  createWindow();
25361
+ void checkWorkMapGpuExports.detectCapability({ desktop: true, verifyGpu: async () => checkWorkMapGpuExports.usableElectronGpu(await electron.app.getGPUInfo("complete")) }).then((capability) => {
25362
+ if (!shutdownStarted) orchestrator.setWorkMapCapability(capability);
25363
+ });
24452
25364
  setTimeout(() => {
24453
25365
  void checkForUpdate();
24454
25366
  }, 3e3);