@phenx-inc/ctlsurf 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/out/headless/index.mjs +549 -116
  2. package/out/headless/index.mjs.map +4 -4
  3. package/out/main/index.js +416 -38
  4. package/out/preload/index.js +3 -0
  5. package/out/renderer/assets/{cssMode-Xx1-SozN.js → cssMode-BtQxI2Eb.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-DM49yPw1.js → freemarker2-DRW5dmY5.js} +1 -1
  7. package/out/renderer/assets/{handlebars-DJJBblpV.js → handlebars-CFLF3cP7.js} +1 -1
  8. package/out/renderer/assets/{html-DnMnJOxw.js → html-DtnBOkwp.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-BG5u2CSH.js → htmlMode-DMVysDCg.js} +3 -3
  10. package/out/renderer/assets/{index-DID1LlMU.css → index-CEjwKjtd.css} +45 -0
  11. package/out/renderer/assets/{index-D65QIRev.js → index-Dm70hk5s.js} +292 -38
  12. package/out/renderer/assets/{javascript-Dci_phmN.js → javascript-ChlyA1sf.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-Dmp3JJSm.js → jsonMode-D1_oHzeN.js} +3 -3
  14. package/out/renderer/assets/{liquid-C9KVysk1.js → liquid-D4ftdy1G.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-IDD0-KWa.js → lspLanguageFeatures-Ckqu5ubG.js} +1 -1
  16. package/out/renderer/assets/{mdx-ascmBFXX.js → mdx-B9wsVYSC.js} +1 -1
  17. package/out/renderer/assets/{python-DcbM4Hy5.js → python-e23tD6IS.js} +1 -1
  18. package/out/renderer/assets/{razor-XJROxpYK.js → razor-CrDyXj3d.js} +1 -1
  19. package/out/renderer/assets/{tsMode-EXbzul3n.js → tsMode-BwGC5o4O.js} +1 -1
  20. package/out/renderer/assets/{typescript-DhTtFBLt.js → typescript-Do-98d_O.js} +1 -1
  21. package/out/renderer/assets/{xml-BLfEkVl1.js → xml-Dpdu5R3m.js} +1 -1
  22. package/out/renderer/assets/{yaml-DpzsqvWa.js → yaml-B0Zt7LCJ.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/scripts/test-terminal-scroll.cjs +64 -0
  26. package/src/main/headless.ts +12 -0
  27. package/src/main/index.ts +8 -0
  28. package/src/main/monitor.ts +540 -0
  29. package/src/main/orchestrator.ts +10 -0
  30. package/src/main/pty.ts +5 -0
  31. package/src/main/tui.ts +37 -3
  32. package/src/preload/index.ts +4 -0
  33. package/src/renderer/App.tsx +9 -1
  34. package/src/renderer/components/Resources.tsx +217 -0
  35. package/src/renderer/components/StatusBar.tsx +6 -1
  36. package/src/renderer/components/TerminalPanel.tsx +54 -20
  37. package/src/renderer/styles.css +45 -0
package/out/main/index.js CHANGED
@@ -180,6 +180,10 @@ class PtyManager {
180
180
  this.process = null;
181
181
  });
182
182
  }
183
+ /** OS pid of the spawned agent (or ssh), null once it has exited. */
184
+ get pid() {
185
+ return this.process?.pid ?? null;
186
+ }
183
187
  write(data) {
184
188
  this.process?.write(data);
185
189
  }
@@ -6038,16 +6042,16 @@ class ConversationBridge {
6038
6042
  this.outputFlushTimer = null;
6039
6043
  if (this.outputBuffer.length === 0) return;
6040
6044
  const candidate = this.terminalCapture.currentCandidate();
6041
- const now = Date.now();
6045
+ const now2 = Date.now();
6042
6046
  if (candidate.length === 0) {
6043
6047
  this.outputBuffer = "";
6044
6048
  return;
6045
6049
  }
6046
6050
  if (candidate !== this.pendingCandidate) {
6047
6051
  this.pendingCandidate = candidate;
6048
- this.pendingCandidateChangedAt = now;
6052
+ this.pendingCandidateChangedAt = now2;
6049
6053
  }
6050
- if (now - this.pendingCandidateChangedAt >= OUTPUT_STABLE_MS) {
6054
+ if (now2 - this.pendingCandidateChangedAt >= OUTPUT_STABLE_MS) {
6051
6055
  this.flush();
6052
6056
  return;
6053
6057
  }
@@ -6557,8 +6561,8 @@ function mostRecentClaudeFile(cwd, root) {
6557
6561
  }
6558
6562
  function recentCodexRollouts(root) {
6559
6563
  const dirs = /* @__PURE__ */ new Set();
6560
- const now = Date.now();
6561
- for (const ms of [now, now - 24 * 60 * 60 * 1e3]) {
6564
+ const now2 = Date.now();
6565
+ for (const ms of [now2, now2 - 24 * 60 * 60 * 1e3]) {
6562
6566
  const d = new Date(ms);
6563
6567
  const yyyy = String(d.getFullYear());
6564
6568
  const mm = String(d.getMonth() + 1).padStart(2, "0");
@@ -6670,9 +6674,9 @@ class AgentStateMonitor {
6670
6674
  tabs = /* @__PURE__ */ new Map();
6671
6675
  timer = null;
6672
6676
  lastAnyAwaiting = false;
6673
- constructor(sink, now = Date.now) {
6677
+ constructor(sink, now2 = Date.now) {
6674
6678
  this.sink = sink;
6675
- this.now = now;
6679
+ this.now = now2;
6676
6680
  }
6677
6681
  start() {
6678
6682
  if (this.timer) return;
@@ -6688,14 +6692,14 @@ class AgentStateMonitor {
6688
6692
  }
6689
6693
  /** Register a tab so its raw PTY output can be classified. */
6690
6694
  trackTab(tabId, agentId, cwd) {
6691
- const now = this.now();
6695
+ const now2 = this.now();
6692
6696
  this.tabs.set(tabId, {
6693
6697
  agentId,
6694
6698
  cwd,
6695
6699
  state: "idle",
6696
6700
  everBusy: false,
6697
6701
  lastBusyAt: 0,
6698
- lastOutputAt: now,
6702
+ lastOutputAt: now2,
6699
6703
  lastDecisionAt: 0,
6700
6704
  hookState: null,
6701
6705
  hookAt: 0,
@@ -6709,16 +6713,16 @@ class AgentStateMonitor {
6709
6713
  feed(tabId, data) {
6710
6714
  const rec = this.tabs.get(tabId);
6711
6715
  if (!rec) return;
6712
- const now = this.now();
6713
- rec.lastOutputAt = now;
6716
+ const now2 = this.now();
6717
+ rec.lastOutputAt = now2;
6714
6718
  const text = rec.tail + stripAnsi(data);
6715
6719
  rec.tail = text.length > TAIL_KEEP_CHARS ? text.slice(-TAIL_KEEP_CHARS) : text;
6716
6720
  if (hasBusyMarker(text)) {
6717
- rec.lastBusyAt = now;
6721
+ rec.lastBusyAt = now2;
6718
6722
  rec.everBusy = true;
6719
6723
  }
6720
6724
  if (hasApprovalMarker(text)) {
6721
- rec.lastDecisionAt = now;
6725
+ rec.lastDecisionAt = now2;
6722
6726
  }
6723
6727
  }
6724
6728
  /**
@@ -6735,9 +6739,9 @@ class AgentStateMonitor {
6735
6739
  if (!/[\r\n]/.test(data)) return;
6736
6740
  const rec = this.tabs.get(tabId);
6737
6741
  if (!rec) return;
6738
- const now = this.now();
6742
+ const now2 = this.now();
6739
6743
  rec.everBusy = false;
6740
- rec.lastBusyAt = now;
6744
+ rec.lastBusyAt = now2;
6741
6745
  rec.lastDecisionAt = 0;
6742
6746
  rec.hookState = null;
6743
6747
  this.setState(rec, tabId, "busy");
@@ -6751,28 +6755,28 @@ class AgentStateMonitor {
6751
6755
  */
6752
6756
  applyHookEvent(cwd, kind) {
6753
6757
  const target = stripTrailingSep$1(cwd);
6754
- const now = this.now();
6758
+ const now2 = this.now();
6755
6759
  for (const [tabId, rec] of this.tabs) {
6756
6760
  if (stripTrailingSep$1(rec.cwd) !== target) continue;
6757
6761
  rec.hookState = kind;
6758
- rec.hookAt = now;
6762
+ rec.hookAt = now2;
6759
6763
  if (kind === "busy") {
6760
6764
  rec.everBusy = false;
6761
- rec.lastBusyAt = now;
6765
+ rec.lastBusyAt = now2;
6762
6766
  }
6763
6767
  this.setState(rec, tabId, kind);
6764
6768
  }
6765
6769
  }
6766
6770
  tick() {
6767
- const now = this.now();
6771
+ const now2 = this.now();
6768
6772
  for (const [tabId, rec] of this.tabs) {
6769
- if (rec.hookState && now - rec.hookAt < HOOK_AUTHORITY_MS) {
6773
+ if (rec.hookState && now2 - rec.hookAt < HOOK_AUTHORITY_MS) {
6770
6774
  this.setState(rec, tabId, rec.hookState);
6771
6775
  continue;
6772
6776
  }
6773
- const busyRecently = rec.lastBusyAt > 0 && now - rec.lastBusyAt < BUSY_GRACE_MS;
6774
- const settled = now - rec.lastOutputAt >= SETTLE_MS;
6775
- const decisionRecent = rec.lastDecisionAt > 0 && now - rec.lastDecisionAt < DECISION_TTL_MS;
6777
+ const busyRecently = rec.lastBusyAt > 0 && now2 - rec.lastBusyAt < BUSY_GRACE_MS;
6778
+ const settled = now2 - rec.lastOutputAt >= SETTLE_MS;
6779
+ const decisionRecent = rec.lastDecisionAt > 0 && now2 - rec.lastDecisionAt < DECISION_TTL_MS;
6776
6780
  let next = null;
6777
6781
  if (busyRecently) {
6778
6782
  next = "busy";
@@ -11095,16 +11099,16 @@ class TimeTracker {
11095
11099
  recordActivity(tabId) {
11096
11100
  const s = this.sessions.get(tabId);
11097
11101
  if (!s || s.ended) return;
11098
- const now = Date.now();
11099
- if (!isSameLocalDay(s.startedAt, now)) {
11102
+ const now2 = Date.now();
11103
+ if (!isSameLocalDay(s.startedAt, now2)) {
11100
11104
  void this.rollover(tabId);
11101
11105
  return;
11102
11106
  }
11103
- const delta = now - s.lastActivity;
11107
+ const delta = now2 - s.lastActivity;
11104
11108
  if (delta < s.idleTimeoutMs) {
11105
11109
  s.activeMs += delta;
11106
11110
  }
11107
- s.lastActivity = now;
11111
+ s.lastActivity = now2;
11108
11112
  }
11109
11113
  async rollover(tabId) {
11110
11114
  if (this.rollingOver.has(tabId)) return;
@@ -11730,10 +11734,10 @@ class SpanTracker {
11730
11734
  lastAccountAt = 0;
11731
11735
  activityMs = { agent: 0, human: 0, idle: 0 };
11732
11736
  started = false;
11733
- constructor(session, sink, now = Date.now) {
11737
+ constructor(session, sink, now2 = Date.now) {
11734
11738
  this.session = session;
11735
11739
  this.sink = sink;
11736
- this.now = now;
11740
+ this.now = now2;
11737
11741
  }
11738
11742
  /** Begin attribution at session start. */
11739
11743
  begin() {
@@ -12180,10 +12184,10 @@ class ObserverBudget {
12180
12184
  capUsd;
12181
12185
  date;
12182
12186
  spentUsd = 0;
12183
- constructor(dir, capUsd, now = () => /* @__PURE__ */ new Date()) {
12187
+ constructor(dir, capUsd, now2 = () => /* @__PURE__ */ new Date()) {
12184
12188
  this.filePath = path$1.join(dir, "budget.json");
12185
12189
  this.capUsd = capUsd;
12186
- this.date = localDay(now());
12190
+ this.date = localDay(now2());
12187
12191
  try {
12188
12192
  const raw = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
12189
12193
  if (raw?.date === this.date && typeof raw.spentUsd === "number") {
@@ -12192,8 +12196,8 @@ class ObserverBudget {
12192
12196
  } catch {
12193
12197
  }
12194
12198
  }
12195
- addUsage(model, usage, now = () => /* @__PURE__ */ new Date()) {
12196
- const today = localDay(now());
12199
+ addUsage(model, usage, now2 = () => /* @__PURE__ */ new Date()) {
12200
+ const today = localDay(now2());
12197
12201
  if (today !== this.date) {
12198
12202
  this.date = today;
12199
12203
  this.spentUsd = 0;
@@ -12205,8 +12209,8 @@ class ObserverBudget {
12205
12209
  } catch {
12206
12210
  }
12207
12211
  }
12208
- overCap(now = () => /* @__PURE__ */ new Date()) {
12209
- if (localDay(now()) !== this.date) return false;
12212
+ overCap(now2 = () => /* @__PURE__ */ new Date()) {
12213
+ if (localDay(now2()) !== this.date) return false;
12210
12214
  return this.spentUsd >= this.capUsd;
12211
12215
  }
12212
12216
  get spent() {
@@ -16235,15 +16239,15 @@ function requireDist() {
16235
16239
  return `v1,${expectedSignature}`;
16236
16240
  }
16237
16241
  verifyTimestamp(timestampHeader) {
16238
- const now = Math.floor(Date.now() / 1e3);
16242
+ const now2 = Math.floor(Date.now() / 1e3);
16239
16243
  const timestamp = parseInt(timestampHeader, 10);
16240
16244
  if (isNaN(timestamp)) {
16241
16245
  throw new WebhookVerificationError("Invalid Signature Headers");
16242
16246
  }
16243
- if (now - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
16247
+ if (now2 - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
16244
16248
  throw new WebhookVerificationError("Message timestamp too old");
16245
16249
  }
16246
- if (timestamp > now + WEBHOOK_TOLERANCE_IN_SECONDS) {
16250
+ if (timestamp > now2 + WEBHOOK_TOLERANCE_IN_SECONDS) {
16247
16251
  throw new WebhookVerificationError("Message timestamp too new");
16248
16252
  }
16249
16253
  return new Date(timestamp * 1e3);
@@ -23306,6 +23310,15 @@ class Orchestrator {
23306
23310
  getTabIds() {
23307
23311
  return [...this.tabs.keys()];
23308
23312
  }
23313
+ // Every open tab's agent and PTY pid, for the resource monitor (monitor.ts).
23314
+ getTabProcesses() {
23315
+ return [...this.tabs].map(([tabId, tab]) => ({
23316
+ tabId,
23317
+ agent: tab.agent,
23318
+ pid: tab.ptyManager.pid,
23319
+ active: tabId === this.activeTabId
23320
+ }));
23321
+ }
23309
23322
  // ─── Tracking control (active tab) ──────────────
23310
23323
  isActiveTabTracking() {
23311
23324
  if (!this.activeTabId) return false;
@@ -23479,6 +23492,368 @@ class Orchestrator {
23479
23492
  this.workerWs.disconnect();
23480
23493
  }
23481
23494
  }
23495
+ const FAST_MS = 3e3;
23496
+ const SLOW_MS = 1e4;
23497
+ const IDLE_STOP_MS = 12e4;
23498
+ const HISTORY_MS = 6e5;
23499
+ const FAST_HOLD_MS = 15e3;
23500
+ const USAGE_SLOW_MS = 3e5;
23501
+ const USAGE_FAST_MS = 6e4;
23502
+ const CODEX_TAIL_BYTES = 512 * 1024;
23503
+ function now() {
23504
+ return (/* @__PURE__ */ new Date()).toISOString();
23505
+ }
23506
+ function round(x, digits = 1) {
23507
+ const f = 10 ** digits;
23508
+ return Math.round(x * f) / f;
23509
+ }
23510
+ function run(cmd, args, opts = {}) {
23511
+ return new Promise((resolve, reject) => {
23512
+ child_process.execFile(cmd, args, { timeout: opts.timeout ?? 5e3, cwd: opts.cwd, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
23513
+ if (err && !stdout) reject(err);
23514
+ else resolve(stdout);
23515
+ });
23516
+ });
23517
+ }
23518
+ function serviceFor(agent) {
23519
+ const cmd = isRemoteAgent(agent) ? agent.args.slice(2).join(" ") : path$1.basename(agent.command);
23520
+ if (/\bcodex\b/.test(cmd)) return "codex";
23521
+ if (/\bclaude\b/.test(cmd)) return "claude";
23522
+ return null;
23523
+ }
23524
+ function cpuTimes() {
23525
+ let idle = 0;
23526
+ let total = 0;
23527
+ for (const c of os.cpus()) {
23528
+ const t = c.times;
23529
+ idle += t.idle;
23530
+ total += t.user + t.nice + t.sys + t.idle + t.irq;
23531
+ }
23532
+ return { idle, total };
23533
+ }
23534
+ async function memory() {
23535
+ const total = os.totalmem();
23536
+ let avail = os.freemem();
23537
+ try {
23538
+ if (process.platform === "darwin") {
23539
+ const out = await run("vm_stat", []);
23540
+ const page = Number(/page size of (\d+) bytes/.exec(out)?.[1]) || 4096;
23541
+ const pages = (name) => Number(new RegExp(`Pages ${name}:\\s+(\\d+)`).exec(out)?.[1] ?? 0);
23542
+ avail = (pages("free") + pages("inactive") + pages("speculative")) * page;
23543
+ } else if (process.platform === "linux") {
23544
+ const m2 = /MemAvailable:\s+(\d+) kB/.exec(await fs.promises.readFile("/proc/meminfo", "utf-8"));
23545
+ if (m2) avail = Number(m2[1]) * 1024;
23546
+ }
23547
+ } catch {
23548
+ }
23549
+ avail = Math.min(avail, total);
23550
+ return { total, used: total - avail, pct: round(100 * (total - avail) / total) };
23551
+ }
23552
+ async function diskUsage() {
23553
+ if (typeof fs.promises.statfs !== "function") return null;
23554
+ try {
23555
+ const s = await fs.promises.statfs(os.homedir());
23556
+ return { total: s.blocks * s.bsize, free: s.bavail * s.bsize };
23557
+ } catch {
23558
+ return null;
23559
+ }
23560
+ }
23561
+ async function appleGpu() {
23562
+ if (process.platform !== "darwin") return null;
23563
+ try {
23564
+ const out = await run("ioreg", ["-r", "-d", "1", "-w", "0", "-c", "IOAccelerator"], { timeout: 3e3 });
23565
+ const m2 = /"Device Utilization %"=(\d+)/.exec(out);
23566
+ return m2 ? Number(m2[1]) : null;
23567
+ } catch {
23568
+ return null;
23569
+ }
23570
+ }
23571
+ async function processTable() {
23572
+ const table = /* @__PURE__ */ new Map();
23573
+ if (process.platform === "win32") return table;
23574
+ try {
23575
+ const out = await run("ps", ["-A", "-o", "pid=,ppid=,pcpu=,rss="]);
23576
+ for (const line of out.split("\n")) {
23577
+ const [pid, ppid, cpu, rss] = line.trim().split(/\s+/).map(Number);
23578
+ if (pid) table.set(pid, { ppid, cpu: cpu || 0, rss: (rss || 0) * 1024 });
23579
+ }
23580
+ } catch {
23581
+ }
23582
+ return table;
23583
+ }
23584
+ function agentProcess(tab, table) {
23585
+ const base = {
23586
+ tabId: tab.tabId,
23587
+ name: tab.agent.name,
23588
+ active: tab.active,
23589
+ remote: isRemoteAgent(tab.agent),
23590
+ service: serviceFor(tab.agent),
23591
+ pid: tab.pid
23592
+ };
23593
+ if (tab.pid == null || !table.has(tab.pid)) return { ...base, cpu: null, mem: null, procs: 0 };
23594
+ const children = /* @__PURE__ */ new Map();
23595
+ for (const [pid, row] of table) {
23596
+ const list = children.get(row.ppid);
23597
+ if (list) list.push(pid);
23598
+ else children.set(row.ppid, [pid]);
23599
+ }
23600
+ let cpu = 0;
23601
+ let mem = 0;
23602
+ let procs = 0;
23603
+ const queue = [tab.pid];
23604
+ while (queue.length) {
23605
+ const pid = queue.pop();
23606
+ const row = table.get(pid);
23607
+ if (!row) continue;
23608
+ cpu += row.cpu;
23609
+ mem += row.rss;
23610
+ procs++;
23611
+ queue.push(...children.get(pid) ?? []);
23612
+ }
23613
+ return { ...base, cpu: round(cpu), mem, procs };
23614
+ }
23615
+ function parseClaudeUsage(text) {
23616
+ const windows = [];
23617
+ for (const line of text.split("\n")) {
23618
+ let m2 = /^\s*Current session:\s*([\d.]+)% used(?:\s*·\s*resets (.+))?/.exec(line);
23619
+ if (m2) {
23620
+ windows.push({ label: "session (5h)", short: "5h", pct: Number(m2[1]), resets: m2[2]?.trim() || null });
23621
+ continue;
23622
+ }
23623
+ m2 = /^\s*Current week \((.+?)\):\s*([\d.]+)% used(?:\s*·\s*resets (.+))?/.exec(line);
23624
+ if (m2) {
23625
+ const all = m2[1].trim().toLowerCase() === "all models";
23626
+ windows.push({
23627
+ label: all ? "week, all models" : `week, ${m2[1].trim()}`,
23628
+ short: all ? "wk" : null,
23629
+ pct: Number(m2[2]),
23630
+ resets: m2[3]?.trim() || null
23631
+ });
23632
+ }
23633
+ }
23634
+ return windows;
23635
+ }
23636
+ async function claudeUsage() {
23637
+ const source = "claude /usage";
23638
+ let text = "";
23639
+ try {
23640
+ const out = await run(
23641
+ "claude",
23642
+ ["-p", "/usage", "--output-format", "json", "--no-session-persistence"],
23643
+ { cwd: os.tmpdir(), timeout: 4e4 }
23644
+ );
23645
+ text = String(JSON.parse(out || "{}").result ?? "");
23646
+ } catch (err) {
23647
+ const msg = err.code === "ENOENT" ? "claude not found on PATH" : String(err);
23648
+ return { ok: false, windows: [], source, ts: now(), error: msg.slice(0, 200) };
23649
+ }
23650
+ const windows = parseClaudeUsage(text);
23651
+ return windows.length ? { ok: true, windows, source, ts: now() } : { ok: false, windows: [], source, ts: now(), error: text.trim().slice(0, 200) || "no usage reported" };
23652
+ }
23653
+ function findKey(obj, key) {
23654
+ if (Array.isArray(obj)) {
23655
+ for (const v2 of obj) {
23656
+ const found = findKey(v2, key);
23657
+ if (found) return found;
23658
+ }
23659
+ } else if (obj && typeof obj === "object") {
23660
+ const rec = obj;
23661
+ if (rec[key] && typeof rec[key] === "object") return rec[key];
23662
+ for (const v2 of Object.values(rec)) {
23663
+ const found = findKey(v2, key);
23664
+ if (found) return found;
23665
+ }
23666
+ }
23667
+ return null;
23668
+ }
23669
+ function codexWindow(minutes) {
23670
+ const m2 = Number(minutes);
23671
+ if (!Number.isFinite(m2)) return { label: "window", short: "win" };
23672
+ if (m2 === 300) return { label: "5h", short: "5h" };
23673
+ if (m2 === 1440) return { label: "day", short: "day" };
23674
+ if (m2 === 10080) return { label: "week", short: "wk" };
23675
+ const s = m2 < 1440 ? `${Math.floor(m2 / 60)}h` : `${Math.floor(m2 / 1440)}d`;
23676
+ return { label: s, short: s };
23677
+ }
23678
+ async function readTail(file, bytes) {
23679
+ const fh = await fs.promises.open(file, "r");
23680
+ try {
23681
+ const { size } = await fh.stat();
23682
+ const start = Math.max(0, size - bytes);
23683
+ const buf = Buffer.alloc(size - start);
23684
+ await fh.read(buf, 0, buf.length, start);
23685
+ return buf.toString("utf-8");
23686
+ } finally {
23687
+ await fh.close();
23688
+ }
23689
+ }
23690
+ async function codexSessionFiles(root, limit2) {
23691
+ const sortedDirs = async (dir) => (await fs.promises.readdir(dir, { withFileTypes: true }).catch(() => [])).filter((d) => d.isDirectory()).map((d) => path$1.join(dir, d.name)).sort().reverse();
23692
+ const days = [];
23693
+ for (const y2 of await sortedDirs(root)) {
23694
+ for (const m2 of await sortedDirs(y2)) {
23695
+ days.push(...await sortedDirs(m2));
23696
+ if (days.length >= 14) break;
23697
+ }
23698
+ if (days.length >= 14) break;
23699
+ }
23700
+ const files = [];
23701
+ for (const day of days.slice(0, 14)) {
23702
+ for (const name of await fs.promises.readdir(day).catch(() => [])) {
23703
+ if (!name.startsWith("rollout-") || !name.endsWith(".jsonl")) continue;
23704
+ const file = path$1.join(day, name);
23705
+ try {
23706
+ files.push({ file, mtime: (await fs.promises.stat(file)).mtimeMs });
23707
+ } catch {
23708
+ }
23709
+ }
23710
+ }
23711
+ return files.sort((a, b2) => b2.mtime - a.mtime).slice(0, limit2).map((f) => f.file);
23712
+ }
23713
+ async function codexUsage(home = os.homedir()) {
23714
+ const source = "your most recent Codex session";
23715
+ for (const file of await codexSessionFiles(path$1.join(home, ".codex", "sessions"), 5)) {
23716
+ let lines;
23717
+ try {
23718
+ lines = (await readTail(file, CODEX_TAIL_BYTES)).split("\n");
23719
+ } catch {
23720
+ continue;
23721
+ }
23722
+ for (let i = lines.length - 1; i >= 0; i--) {
23723
+ if (!lines[i].includes('"rate_limits"')) continue;
23724
+ let rl;
23725
+ try {
23726
+ rl = findKey(JSON.parse(lines[i]), "rate_limits");
23727
+ } catch {
23728
+ continue;
23729
+ }
23730
+ if (!rl) continue;
23731
+ const windows = [];
23732
+ for (const key of ["primary", "secondary"]) {
23733
+ const w2 = rl[key];
23734
+ if (!w2 || typeof w2 !== "object" || w2.used_percent == null) continue;
23735
+ const { label, short } = codexWindow(w2.window_minutes);
23736
+ const resets = typeof w2.resets_at === "number" ? new Date(w2.resets_at * 1e3).toLocaleString(void 0, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : null;
23737
+ windows.push({ label, short, pct: Number(w2.used_percent), resets });
23738
+ }
23739
+ if (windows.length) {
23740
+ const ts = new Date((await fs.promises.stat(file)).mtimeMs).toISOString();
23741
+ return { ok: true, windows, plan: rl.plan_type ?? null, source, ts };
23742
+ }
23743
+ }
23744
+ }
23745
+ return { ok: false, windows: [], source, ts: now(), error: "no Codex session with usage yet" };
23746
+ }
23747
+ const FETCH_USAGE = {
23748
+ claude: claudeUsage,
23749
+ codex: () => codexUsage()
23750
+ };
23751
+ class ResourceMonitor {
23752
+ constructor(tabs) {
23753
+ this.tabs = tabs;
23754
+ }
23755
+ machine = { ok: false, ts: now(), error: "sampling…" };
23756
+ history = [];
23757
+ agents = [];
23758
+ usage = {};
23759
+ usageAt = {};
23760
+ usageBusy = /* @__PURE__ */ new Set();
23761
+ prevCpu = cpuTimes();
23762
+ lastAsk = 0;
23763
+ fastUntil = 0;
23764
+ timer = null;
23765
+ inflight = null;
23766
+ /** Latest sample plus 10 minutes of history. `fast` = the panel is open:
23767
+ * sample every ~3 s and refresh quota every minute. */
23768
+ async snapshot(fast = false) {
23769
+ const t = Date.now();
23770
+ this.lastAsk = t;
23771
+ const speedUp = fast && t >= this.fastUntil;
23772
+ if (fast) this.fastUntil = t + FAST_HOLD_MS;
23773
+ if (!this.timer && !this.inflight || speedUp) await this.tick();
23774
+ else this.refreshUsage(this.tabs());
23775
+ return this.build(fast);
23776
+ }
23777
+ stop() {
23778
+ if (this.timer) clearTimeout(this.timer);
23779
+ this.timer = null;
23780
+ this.lastAsk = 0;
23781
+ }
23782
+ tick() {
23783
+ if (!this.inflight) {
23784
+ if (this.timer) clearTimeout(this.timer);
23785
+ this.timer = null;
23786
+ this.inflight = this.sample().catch((err) => log$5(`[monitor] sample failed: ${err}`)).finally(() => {
23787
+ this.inflight = null;
23788
+ if (Date.now() - this.lastAsk >= IDLE_STOP_MS) return;
23789
+ this.timer = setTimeout(() => {
23790
+ void this.tick();
23791
+ }, Date.now() < this.fastUntil ? FAST_MS : SLOW_MS);
23792
+ this.timer.unref?.();
23793
+ });
23794
+ }
23795
+ return this.inflight;
23796
+ }
23797
+ async sample() {
23798
+ const tabs = this.tabs();
23799
+ this.refreshUsage(tabs);
23800
+ try {
23801
+ const [mem, disk, gpu, table] = await Promise.all([memory(), diskUsage(), appleGpu(), processTable()]);
23802
+ const cur = cpuTimes();
23803
+ const dTotal = cur.total - this.prevCpu.total;
23804
+ const cpuPct = dTotal > 0 ? round(100 * (1 - (cur.idle - this.prevCpu.idle) / dTotal)) : null;
23805
+ this.prevCpu = cur;
23806
+ this.machine = {
23807
+ ok: true,
23808
+ ts: now(),
23809
+ cpu: { pct: cpuPct, cores: os.cpus().length, load: os.loadavg().map((x) => round(x, 2)) },
23810
+ mem,
23811
+ disk,
23812
+ gpu
23813
+ };
23814
+ this.agents = tabs.map((tab) => agentProcess(tab, table));
23815
+ const t = Date.now();
23816
+ this.history.push({ t, cpu: cpuPct, mem: mem.pct, gpu });
23817
+ while (this.history.length && this.history[0].t < t - HISTORY_MS) this.history.shift();
23818
+ } catch (err) {
23819
+ this.machine = { ok: false, ts: now(), error: String(err).slice(0, 200) };
23820
+ }
23821
+ }
23822
+ refreshUsage(tabs) {
23823
+ const every = Date.now() < this.fastUntil ? USAGE_FAST_MS : USAGE_SLOW_MS;
23824
+ for (const s of this.servicesInUse(tabs)) {
23825
+ if (this.usageBusy.has(s) || Date.now() - (this.usageAt[s] ?? 0) < every) continue;
23826
+ this.usageBusy.add(s);
23827
+ FETCH_USAGE[s]().catch((err) => ({ ok: false, windows: [], source: s, ts: now(), error: String(err).slice(0, 200) })).then((u) => {
23828
+ this.usage[s] = u;
23829
+ this.usageAt[s] = Date.now();
23830
+ if (!u.ok) log$5(`[monitor] ${s} usage unavailable: ${u.error}`);
23831
+ }).finally(() => this.usageBusy.delete(s));
23832
+ }
23833
+ }
23834
+ servicesInUse(tabs) {
23835
+ return [...new Set(tabs.map((t) => serviceFor(t.agent)).filter((s) => s !== null))];
23836
+ }
23837
+ build(fast) {
23838
+ const tabs = this.tabs();
23839
+ const active = tabs.find((t) => t.active);
23840
+ const usage = {};
23841
+ for (const s of this.servicesInUse(tabs)) {
23842
+ if (this.usage[s]) usage[s] = this.usage[s];
23843
+ }
23844
+ return {
23845
+ machine: {
23846
+ ...this.machine,
23847
+ label: process.platform === "darwin" ? "This Mac" : os.hostname(),
23848
+ history: [...this.history]
23849
+ },
23850
+ agents: this.agents,
23851
+ activeService: active ? serviceFor(active.agent) : null,
23852
+ usage,
23853
+ interval: fast ? FAST_MS : SLOW_MS
23854
+ };
23855
+ }
23856
+ }
23482
23857
  electron.app.setName("ctlsurf");
23483
23858
  process.title = "ctlsurf";
23484
23859
  process.stdout?.on?.("error", () => {
@@ -23822,6 +24197,8 @@ async function checkForUpdate() {
23822
24197
  }
23823
24198
  electron.ipcMain.handle("app:getVersion", () => electron.app.getVersion());
23824
24199
  electron.ipcMain.handle("app:getUpdateInfo", () => updateInfo);
24200
+ const resourceMonitor = new ResourceMonitor(() => orchestrator.getTabProcesses());
24201
+ electron.ipcMain.handle("monitor:snapshot", (_event, fast) => resourceMonitor.snapshot(!!fast));
23825
24202
  electron.ipcMain.handle("fs:readDir", async (_event, dirPath) => {
23826
24203
  try {
23827
24204
  const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
@@ -24083,6 +24460,7 @@ let shutdownStarted = false;
24083
24460
  function shutdownThenQuit() {
24084
24461
  if (shutdownStarted) return;
24085
24462
  shutdownStarted = true;
24463
+ resourceMonitor.stop();
24086
24464
  void Promise.race([
24087
24465
  orchestrator.shutdown(),
24088
24466
  new Promise((resolve) => setTimeout(resolve, 1e4))
@@ -58,6 +58,9 @@ const api = {
58
58
  electron.ipcRenderer.on("app:updateInfo", listener);
59
59
  return () => electron.ipcRenderer.removeListener("app:updateInfo", listener);
60
60
  },
61
+ // Resources + plan quota for the status bar (shape: MonitorSnapshot in
62
+ // src/main/monitor.ts). `fast` = the details panel is open.
63
+ getResourceSnapshot: (fast) => electron.ipcRenderer.invoke("monitor:snapshot", fast),
61
64
  // Settings (legacy)
62
65
  getSetting: (key) => electron.ipcRenderer.invoke("settings:get", key),
63
66
  setSetting: (key, value) => electron.ipcRenderer.invoke("settings:set", key, value),
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-D65QIRev.js";
2
- import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-IDD0-KWa.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-IDD0-KWa.js";
1
+ import { c as createWebWorker, l as languages } from "./index-Dm70hk5s.js";
2
+ import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-Ckqu5ubG.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-Ckqu5ubG.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-Dm70hk5s.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-Dm70hk5s.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D65QIRev.js";
1
+ import { l as languages } from "./index-Dm70hk5s.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-D65QIRev.js";
2
- import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-IDD0-KWa.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-IDD0-KWa.js";
1
+ import { c as createWebWorker, l as languages } from "./index-Dm70hk5s.js";
2
+ import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-Ckqu5ubG.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-Ckqu5ubG.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {