@phenx-inc/ctlsurf 0.9.0 → 0.10.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.
- package/out/headless/index.mjs +196 -2
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +300 -23
- package/out/preload/index.js +7 -0
- package/out/renderer/assets/{cssMode-BdYp0fp9.js → cssMode-BbCVOTaJ.js} +3 -3
- package/out/renderer/assets/{freemarker2-Cya-9zr-.js → freemarker2-DryLywQv.js} +1 -1
- package/out/renderer/assets/{handlebars-EJs9W5Ub.js → handlebars-CdUMtSdr.js} +1 -1
- package/out/renderer/assets/{html-Ex4qy2lh.js → html-Cwkf2GMH.js} +1 -1
- package/out/renderer/assets/{htmlMode-CCm0aJ6C.js → htmlMode-C7PzhXxH.js} +3 -3
- package/out/renderer/assets/{index-nvsNYP5c.css → index-BIceGcz3.css} +18 -0
- package/out/renderer/assets/{index-Cj7K9-Ba.js → index-Dg1vMspf.js} +31 -24
- package/out/renderer/assets/{javascript-B2niCouI.js → javascript-Cy7ROXJl.js} +2 -2
- package/out/renderer/assets/{jsonMode-BgCUHaFo.js → jsonMode-BtZtt3QW.js} +3 -3
- package/out/renderer/assets/{liquid-DOQmSo4G.js → liquid-xReAQeYs.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-8SIcRAMh.js → lspLanguageFeatures-Dmx0L0Bw.js} +1 -1
- package/out/renderer/assets/{mdx-DuKdGNKE.js → mdx-YMNFO1Zz.js} +1 -1
- package/out/renderer/assets/{python-BUa4XFG-.js → python-BExaDVxv.js} +1 -1
- package/out/renderer/assets/{razor-BUw8jho0.js → razor-HL8ANmQ4.js} +1 -1
- package/out/renderer/assets/{tsMode-8FLRnobw.js → tsMode-Dqd8vdLE.js} +1 -1
- package/out/renderer/assets/{typescript-rdwAjCi4.js → typescript-Dysz1PTX.js} +1 -1
- package/out/renderer/assets/{xml-HOzT59Qg.js → xml-BHp5Z9nc.js} +1 -1
- package/out/renderer/assets/{yaml-B81JfLsW.js → yaml-CPDHxVup.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/agentState.ts +329 -0
- package/src/main/index.ts +105 -19
- package/src/main/orchestrator.ts +21 -1
- package/src/preload/index.ts +8 -0
- package/src/renderer/App.tsx +23 -2
- package/src/renderer/styles.css +18 -0
package/out/headless/index.mjs
CHANGED
|
@@ -5471,7 +5471,7 @@ var require_package = __commonJS({
|
|
|
5471
5471
|
"package.json"(exports, module) {
|
|
5472
5472
|
module.exports = {
|
|
5473
5473
|
name: "@phenx-inc/ctlsurf",
|
|
5474
|
-
version: "0.
|
|
5474
|
+
version: "0.10.0",
|
|
5475
5475
|
description: "Agent-agnostic terminal and desktop app for ctlsurf \u2014 run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5476
5476
|
main: "out/main/index.js",
|
|
5477
5477
|
bin: {
|
|
@@ -6470,6 +6470,188 @@ function lastCodexAgentMessage(file, cwd) {
|
|
|
6470
6470
|
return null;
|
|
6471
6471
|
}
|
|
6472
6472
|
|
|
6473
|
+
// src/main/agentState.ts
|
|
6474
|
+
var BUSY_GRACE_MS = 2500;
|
|
6475
|
+
var SETTLE_MS = 1200;
|
|
6476
|
+
var DECISION_TTL_MS = 4e3;
|
|
6477
|
+
var TICK_MS = 400;
|
|
6478
|
+
var HOOK_AUTHORITY_MS = 6e3;
|
|
6479
|
+
var TAIL_KEEP_CHARS = 240;
|
|
6480
|
+
var CLAUDE_SPINNER = /[✶✻✳✺✢✷✸✹❋][^\n]{0,60}(?:…|\.\.\.)/;
|
|
6481
|
+
function hasBusyMarker(text) {
|
|
6482
|
+
return /esc to interrupt/i.test(text) || /\bWorking \(\d+s/i.test(text) || CLAUDE_SPINNER.test(text);
|
|
6483
|
+
}
|
|
6484
|
+
var APPROVAL_PATTERNS = [
|
|
6485
|
+
/\bDo you want to (proceed|continue|create|make this edit|run)\b/i,
|
|
6486
|
+
// Claude Code
|
|
6487
|
+
/❯\s*\d+\.\s*Yes\b/i,
|
|
6488
|
+
// Claude numbered-choice prompt
|
|
6489
|
+
/\bAllow command\b/i,
|
|
6490
|
+
// Codex
|
|
6491
|
+
/\bRun this command\?/i,
|
|
6492
|
+
// Codex
|
|
6493
|
+
/\bApprove\b[^\n]*\?/i,
|
|
6494
|
+
/\(y\/n\)/i,
|
|
6495
|
+
/\[y\/N\]/i
|
|
6496
|
+
];
|
|
6497
|
+
function hasApprovalMarker(text) {
|
|
6498
|
+
return APPROVAL_PATTERNS.some((re) => re.test(text));
|
|
6499
|
+
}
|
|
6500
|
+
function stripAnsi2(str) {
|
|
6501
|
+
return str.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b/g, "");
|
|
6502
|
+
}
|
|
6503
|
+
var AgentStateMonitor = class {
|
|
6504
|
+
sink;
|
|
6505
|
+
now;
|
|
6506
|
+
tabs = /* @__PURE__ */ new Map();
|
|
6507
|
+
timer = null;
|
|
6508
|
+
lastAnyAwaiting = false;
|
|
6509
|
+
constructor(sink, now = Date.now) {
|
|
6510
|
+
this.sink = sink;
|
|
6511
|
+
this.now = now;
|
|
6512
|
+
}
|
|
6513
|
+
start() {
|
|
6514
|
+
if (this.timer) return;
|
|
6515
|
+
this.timer = setInterval(() => this.tick(), TICK_MS);
|
|
6516
|
+
}
|
|
6517
|
+
stop() {
|
|
6518
|
+
if (this.timer) {
|
|
6519
|
+
clearInterval(this.timer);
|
|
6520
|
+
this.timer = null;
|
|
6521
|
+
}
|
|
6522
|
+
this.tabs.clear();
|
|
6523
|
+
this.lastAnyAwaiting = false;
|
|
6524
|
+
}
|
|
6525
|
+
/** Register a tab so its raw PTY output can be classified. */
|
|
6526
|
+
trackTab(tabId, agentId, cwd) {
|
|
6527
|
+
const now = this.now();
|
|
6528
|
+
this.tabs.set(tabId, {
|
|
6529
|
+
agentId,
|
|
6530
|
+
cwd,
|
|
6531
|
+
state: "idle",
|
|
6532
|
+
everBusy: false,
|
|
6533
|
+
lastBusyAt: 0,
|
|
6534
|
+
lastOutputAt: now,
|
|
6535
|
+
lastDecisionAt: 0,
|
|
6536
|
+
hookState: null,
|
|
6537
|
+
hookAt: 0,
|
|
6538
|
+
tail: ""
|
|
6539
|
+
});
|
|
6540
|
+
}
|
|
6541
|
+
removeTab(tabId) {
|
|
6542
|
+
if (this.tabs.delete(tabId)) this.emitAggregate();
|
|
6543
|
+
}
|
|
6544
|
+
/** Feed a raw PTY output chunk for a tab. */
|
|
6545
|
+
feed(tabId, data) {
|
|
6546
|
+
const rec = this.tabs.get(tabId);
|
|
6547
|
+
if (!rec) return;
|
|
6548
|
+
const now = this.now();
|
|
6549
|
+
rec.lastOutputAt = now;
|
|
6550
|
+
const text = rec.tail + stripAnsi2(data);
|
|
6551
|
+
rec.tail = text.length > TAIL_KEEP_CHARS ? text.slice(-TAIL_KEEP_CHARS) : text;
|
|
6552
|
+
if (hasBusyMarker(text)) {
|
|
6553
|
+
rec.lastBusyAt = now;
|
|
6554
|
+
rec.everBusy = true;
|
|
6555
|
+
}
|
|
6556
|
+
if (hasApprovalMarker(text)) {
|
|
6557
|
+
rec.lastDecisionAt = now;
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
/**
|
|
6561
|
+
* Note input written to a tab's PTY. Only a *submit* (data containing a
|
|
6562
|
+
* newline) counts as the user engaging — clearing any awaiting state and
|
|
6563
|
+
* resetting the "has worked" flag so a leftover idle box doesn't re-flag.
|
|
6564
|
+
*
|
|
6565
|
+
* Individual keystrokes and, crucially, the terminal's own auto-responses
|
|
6566
|
+
* (cursor-position / device-attribute replies that TUIs constantly trigger,
|
|
6567
|
+
* which xterm.js writes straight back to the PTY) are ignored — otherwise
|
|
6568
|
+
* the state would churn busy↔idle on every control-sequence handshake.
|
|
6569
|
+
*/
|
|
6570
|
+
noteInput(tabId, data) {
|
|
6571
|
+
if (!/[\r\n]/.test(data)) return;
|
|
6572
|
+
const rec = this.tabs.get(tabId);
|
|
6573
|
+
if (!rec) return;
|
|
6574
|
+
const now = this.now();
|
|
6575
|
+
rec.everBusy = false;
|
|
6576
|
+
rec.lastBusyAt = now;
|
|
6577
|
+
rec.lastDecisionAt = 0;
|
|
6578
|
+
rec.hookState = null;
|
|
6579
|
+
this.setState(rec, tabId, "busy");
|
|
6580
|
+
}
|
|
6581
|
+
/**
|
|
6582
|
+
* Apply an authoritative state from an agent hook, addressed by cwd. Overrides
|
|
6583
|
+
* the heuristic for HOOK_AUTHORITY_MS. `kind`:
|
|
6584
|
+
* 'awaiting_input' — turn done / idle prompt (Stop, idle_prompt, agent-turn-complete)
|
|
6585
|
+
* 'awaiting_decision' — permission/approval prompt (PermissionRequest, permission_prompt)
|
|
6586
|
+
* 'busy' — user submitted a prompt (UserPromptSubmit)
|
|
6587
|
+
*/
|
|
6588
|
+
applyHookEvent(cwd, kind) {
|
|
6589
|
+
const target = stripTrailingSep2(cwd);
|
|
6590
|
+
const now = this.now();
|
|
6591
|
+
for (const [tabId, rec] of this.tabs) {
|
|
6592
|
+
if (stripTrailingSep2(rec.cwd) !== target) continue;
|
|
6593
|
+
rec.hookState = kind;
|
|
6594
|
+
rec.hookAt = now;
|
|
6595
|
+
if (kind === "busy") {
|
|
6596
|
+
rec.everBusy = false;
|
|
6597
|
+
rec.lastBusyAt = now;
|
|
6598
|
+
}
|
|
6599
|
+
this.setState(rec, tabId, kind);
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
tick() {
|
|
6603
|
+
const now = this.now();
|
|
6604
|
+
for (const [tabId, rec] of this.tabs) {
|
|
6605
|
+
if (rec.hookState && now - rec.hookAt < HOOK_AUTHORITY_MS) {
|
|
6606
|
+
this.setState(rec, tabId, rec.hookState);
|
|
6607
|
+
continue;
|
|
6608
|
+
}
|
|
6609
|
+
const busyRecently = rec.lastBusyAt > 0 && now - rec.lastBusyAt < BUSY_GRACE_MS;
|
|
6610
|
+
const settled = now - rec.lastOutputAt >= SETTLE_MS;
|
|
6611
|
+
const decisionRecent = rec.lastDecisionAt > 0 && now - rec.lastDecisionAt < DECISION_TTL_MS;
|
|
6612
|
+
let next = null;
|
|
6613
|
+
if (busyRecently) {
|
|
6614
|
+
next = "busy";
|
|
6615
|
+
} else if (settled && rec.everBusy) {
|
|
6616
|
+
next = rec.state === "awaiting_decision" || decisionRecent ? "awaiting_decision" : "awaiting_input";
|
|
6617
|
+
} else if (settled && !rec.everBusy) {
|
|
6618
|
+
next = "idle";
|
|
6619
|
+
}
|
|
6620
|
+
if (next) this.setState(rec, tabId, next);
|
|
6621
|
+
}
|
|
6622
|
+
this.emitAggregate();
|
|
6623
|
+
}
|
|
6624
|
+
setState(rec, tabId, state) {
|
|
6625
|
+
if (rec.state === state) return;
|
|
6626
|
+
rec.state = state;
|
|
6627
|
+
this.sink({ tabId, state, anyAwaiting: this.computeAnyAwaiting() });
|
|
6628
|
+
this.lastAnyAwaiting = this.computeAnyAwaiting();
|
|
6629
|
+
}
|
|
6630
|
+
emitAggregate() {
|
|
6631
|
+
const anyAwaiting = this.computeAnyAwaiting();
|
|
6632
|
+
if (anyAwaiting === this.lastAnyAwaiting) return;
|
|
6633
|
+
this.lastAnyAwaiting = anyAwaiting;
|
|
6634
|
+
const tabId = this.firstAwaitingTabId() ?? "";
|
|
6635
|
+
const state = tabId ? this.tabs.get(tabId).state : "idle";
|
|
6636
|
+
this.sink({ tabId, state, anyAwaiting });
|
|
6637
|
+
}
|
|
6638
|
+
computeAnyAwaiting() {
|
|
6639
|
+
for (const rec of this.tabs.values()) {
|
|
6640
|
+
if (rec.state === "awaiting_input" || rec.state === "awaiting_decision") return true;
|
|
6641
|
+
}
|
|
6642
|
+
return false;
|
|
6643
|
+
}
|
|
6644
|
+
firstAwaitingTabId() {
|
|
6645
|
+
for (const [tabId, rec] of this.tabs) {
|
|
6646
|
+
if (rec.state === "awaiting_input" || rec.state === "awaiting_decision") return tabId;
|
|
6647
|
+
}
|
|
6648
|
+
return null;
|
|
6649
|
+
}
|
|
6650
|
+
};
|
|
6651
|
+
function stripTrailingSep2(p) {
|
|
6652
|
+
return p.length > 1 ? p.replace(/[/\\]+$/, "") : p;
|
|
6653
|
+
}
|
|
6654
|
+
|
|
6473
6655
|
// src/main/workerWs.ts
|
|
6474
6656
|
import os2 from "os";
|
|
6475
6657
|
import crypto from "crypto";
|
|
@@ -7265,9 +7447,14 @@ var Orchestrator = class {
|
|
|
7265
7447
|
noProjectPollCwd = null;
|
|
7266
7448
|
currentProjectName = null;
|
|
7267
7449
|
transcriptTailer = null;
|
|
7450
|
+
stateMonitor;
|
|
7268
7451
|
constructor(settingsDir, events) {
|
|
7269
7452
|
this.settingsDir = settingsDir;
|
|
7270
7453
|
this.events = events;
|
|
7454
|
+
this.stateMonitor = new AgentStateMonitor(({ tabId, state, anyAwaiting }) => {
|
|
7455
|
+
this.events.onAgentStateChange?.(tabId, state, anyAwaiting);
|
|
7456
|
+
});
|
|
7457
|
+
this.stateMonitor.start();
|
|
7271
7458
|
this.workerWs = new WorkerWsClient({
|
|
7272
7459
|
onStatusChange: (status) => {
|
|
7273
7460
|
log(`[worker-ws] Status: ${status}`);
|
|
@@ -7279,9 +7466,10 @@ var Orchestrator = class {
|
|
|
7279
7466
|
this.workerWs.sendAck(message.id);
|
|
7280
7467
|
if (message.type === "prompt" || message.type === "task_dispatch") {
|
|
7281
7468
|
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
|
|
7282
|
-
if (activeTab) {
|
|
7469
|
+
if (activeTab && this.activeTabId) {
|
|
7283
7470
|
activeTab.ptyManager.write(message.content + "\r");
|
|
7284
7471
|
this.bridge.feedInput(message.content + "\r");
|
|
7472
|
+
this.stateMonitor.noteInput(this.activeTabId, message.content + "\r");
|
|
7285
7473
|
}
|
|
7286
7474
|
}
|
|
7287
7475
|
},
|
|
@@ -7572,9 +7760,11 @@ var Orchestrator = class {
|
|
|
7572
7760
|
const ptyManager = new PtyManager(agent, cwd);
|
|
7573
7761
|
const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
|
|
7574
7762
|
this.tabs.set(tabId, tab);
|
|
7763
|
+
this.stateMonitor.trackTab(tabId, agent.id, cwd);
|
|
7575
7764
|
ptyManager.onData((data) => {
|
|
7576
7765
|
this.events.onPtyData(tabId, data);
|
|
7577
7766
|
this.timeTracker.recordActivity(tabId);
|
|
7767
|
+
this.stateMonitor.feed(tabId, data);
|
|
7578
7768
|
if (tabId === this.activeTabId) {
|
|
7579
7769
|
this.bridge.feedOutput(data);
|
|
7580
7770
|
this.streamTerminalData(tabId, data);
|
|
@@ -7591,6 +7781,7 @@ var Orchestrator = class {
|
|
|
7591
7781
|
}
|
|
7592
7782
|
const t = this.tabs.get(tabId);
|
|
7593
7783
|
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
|
|
7784
|
+
this.stateMonitor.removeTab(tabId);
|
|
7594
7785
|
});
|
|
7595
7786
|
this.startChatLogging(agent, cwd);
|
|
7596
7787
|
const profile = this.getActiveProfile();
|
|
@@ -7613,6 +7804,7 @@ var Orchestrator = class {
|
|
|
7613
7804
|
}
|
|
7614
7805
|
writePty(tabId, data) {
|
|
7615
7806
|
this.tabs.get(tabId)?.ptyManager.write(data);
|
|
7807
|
+
this.stateMonitor.noteInput(tabId, data);
|
|
7616
7808
|
if (tabId === this.activeTabId) {
|
|
7617
7809
|
this.bridge.feedInput(data);
|
|
7618
7810
|
}
|
|
@@ -7631,6 +7823,7 @@ var Orchestrator = class {
|
|
|
7631
7823
|
await this.timeTracker.endSession(tabId);
|
|
7632
7824
|
tab.ptyManager.kill();
|
|
7633
7825
|
this.tabs.delete(tabId);
|
|
7826
|
+
this.stateMonitor.removeTab(tabId);
|
|
7634
7827
|
if (tabId === this.activeTabId) {
|
|
7635
7828
|
this.stopChatLogging();
|
|
7636
7829
|
if (isCodingAgent(tab.agent)) {
|
|
@@ -7784,6 +7977,7 @@ var Orchestrator = class {
|
|
|
7784
7977
|
async shutdown() {
|
|
7785
7978
|
this.stopNoProjectPolling();
|
|
7786
7979
|
this.stopChatLogging();
|
|
7980
|
+
this.stateMonitor.stop();
|
|
7787
7981
|
await this.timeTracker.endAll();
|
|
7788
7982
|
for (const [, tab] of this.tabs) {
|
|
7789
7983
|
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|