@phenx-inc/ctlsurf 0.16.1 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ctlsurf-worker.js +39 -6
- package/out/headless/index.mjs +549 -116
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +416 -38
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{cssMode-DwHD6JSn.js → cssMode-BtQxI2Eb.js} +3 -3
- package/out/renderer/assets/{freemarker2-Na9-bXYw.js → freemarker2-DRW5dmY5.js} +1 -1
- package/out/renderer/assets/{handlebars-D7gzKLS8.js → handlebars-CFLF3cP7.js} +1 -1
- package/out/renderer/assets/{html-DDK7xN27.js → html-DtnBOkwp.js} +1 -1
- package/out/renderer/assets/{htmlMode-BfbLv-NB.js → htmlMode-DMVysDCg.js} +3 -3
- package/out/renderer/assets/{index-DID1LlMU.css → index-CEjwKjtd.css} +45 -0
- package/out/renderer/assets/{index-HoDaBul3.js → index-Dm70hk5s.js} +252 -26
- package/out/renderer/assets/{javascript-DJjWOxID.js → javascript-ChlyA1sf.js} +2 -2
- package/out/renderer/assets/{jsonMode-BileWbig.js → jsonMode-D1_oHzeN.js} +3 -3
- package/out/renderer/assets/{liquid-DvNU_dI9.js → liquid-D4ftdy1G.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bb27EY8c.js → lspLanguageFeatures-Ckqu5ubG.js} +1 -1
- package/out/renderer/assets/{mdx-CLkNPVgD.js → mdx-B9wsVYSC.js} +1 -1
- package/out/renderer/assets/{python-DOJdL4kC.js → python-e23tD6IS.js} +1 -1
- package/out/renderer/assets/{razor-CSr8UlJc.js → razor-CrDyXj3d.js} +1 -1
- package/out/renderer/assets/{tsMode-sbsAvk21.js → tsMode-BwGC5o4O.js} +1 -1
- package/out/renderer/assets/{typescript-r5QB4yRW.js → typescript-Do-98d_O.js} +1 -1
- package/out/renderer/assets/{xml-CYhKh3Pi.js → xml-Dpdu5R3m.js} +1 -1
- package/out/renderer/assets/{yaml-B8DsoaYW.js → yaml-B0Zt7LCJ.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/scripts/extract-electron.js +105 -0
- package/src/main/headless.ts +12 -0
- package/src/main/index.ts +8 -0
- package/src/main/monitor.ts +540 -0
- package/src/main/orchestrator.ts +10 -0
- package/src/main/pty.ts +5 -0
- package/src/main/tui.ts +37 -3
- package/src/preload/index.ts +4 -0
- package/src/renderer/App.tsx +9 -1
- package/src/renderer/components/Resources.tsx +217 -0
- package/src/renderer/components/StatusBar.tsx +6 -1
- 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
|
|
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 =
|
|
6052
|
+
this.pendingCandidateChangedAt = now2;
|
|
6049
6053
|
}
|
|
6050
|
-
if (
|
|
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
|
|
6561
|
-
for (const ms of [
|
|
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,
|
|
6677
|
+
constructor(sink, now2 = Date.now) {
|
|
6674
6678
|
this.sink = sink;
|
|
6675
|
-
this.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
|
|
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:
|
|
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
|
|
6713
|
-
rec.lastOutputAt =
|
|
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 =
|
|
6721
|
+
rec.lastBusyAt = now2;
|
|
6718
6722
|
rec.everBusy = true;
|
|
6719
6723
|
}
|
|
6720
6724
|
if (hasApprovalMarker(text)) {
|
|
6721
|
-
rec.lastDecisionAt =
|
|
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
|
|
6742
|
+
const now2 = this.now();
|
|
6739
6743
|
rec.everBusy = false;
|
|
6740
|
-
rec.lastBusyAt =
|
|
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
|
|
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 =
|
|
6762
|
+
rec.hookAt = now2;
|
|
6759
6763
|
if (kind === "busy") {
|
|
6760
6764
|
rec.everBusy = false;
|
|
6761
|
-
rec.lastBusyAt =
|
|
6765
|
+
rec.lastBusyAt = now2;
|
|
6762
6766
|
}
|
|
6763
6767
|
this.setState(rec, tabId, kind);
|
|
6764
6768
|
}
|
|
6765
6769
|
}
|
|
6766
6770
|
tick() {
|
|
6767
|
-
const
|
|
6771
|
+
const now2 = this.now();
|
|
6768
6772
|
for (const [tabId, rec] of this.tabs) {
|
|
6769
|
-
if (rec.hookState &&
|
|
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 &&
|
|
6774
|
-
const settled =
|
|
6775
|
-
const decisionRecent = rec.lastDecisionAt > 0 &&
|
|
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
|
|
11099
|
-
if (!isSameLocalDay(s.startedAt,
|
|
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 =
|
|
11107
|
+
const delta = now2 - s.lastActivity;
|
|
11104
11108
|
if (delta < s.idleTimeoutMs) {
|
|
11105
11109
|
s.activeMs += delta;
|
|
11106
11110
|
}
|
|
11107
|
-
s.lastActivity =
|
|
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,
|
|
11737
|
+
constructor(session, sink, now2 = Date.now) {
|
|
11734
11738
|
this.session = session;
|
|
11735
11739
|
this.sink = sink;
|
|
11736
|
-
this.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,
|
|
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(
|
|
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,
|
|
12196
|
-
const today = localDay(
|
|
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(
|
|
12209
|
-
if (localDay(
|
|
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
|
|
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 (
|
|
16247
|
+
if (now2 - timestamp > WEBHOOK_TOLERANCE_IN_SECONDS) {
|
|
16244
16248
|
throw new WebhookVerificationError("Message timestamp too old");
|
|
16245
16249
|
}
|
|
16246
|
-
if (timestamp >
|
|
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))
|
package/out/preload/index.js
CHANGED
|
@@ -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-
|
|
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-
|
|
3
|
-
import { h, i, j, t, k } from "./lspLanguageFeatures-
|
|
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,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages } from "./index-
|
|
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-
|
|
3
|
-
import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-
|
|
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) {
|