@phenx-inc/ctlsurf 0.9.0 → 0.11.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 (31) hide show
  1. package/out/headless/index.mjs +196 -2
  2. package/out/headless/index.mjs.map +4 -4
  3. package/out/main/index.js +326 -24
  4. package/out/preload/index.js +7 -0
  5. package/out/renderer/assets/{cssMode-BdYp0fp9.js → cssMode-Bmzy9hKX.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-Cya-9zr-.js → freemarker2-CDAsExTa.js} +1 -1
  7. package/out/renderer/assets/{handlebars-EJs9W5Ub.js → handlebars-CvpBYG1L.js} +1 -1
  8. package/out/renderer/assets/{html-Ex4qy2lh.js → html-CNhEQqr8.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-CCm0aJ6C.js → htmlMode-DD7YPgga.js} +3 -3
  10. package/out/renderer/assets/{index-Cj7K9-Ba.js → index-BDlCPzCG.js} +153 -146
  11. package/out/renderer/assets/{index-nvsNYP5c.css → index-CGHeDwZa.css} +23 -46
  12. package/out/renderer/assets/{javascript-B2niCouI.js → javascript-CsGLsswK.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-BgCUHaFo.js → jsonMode-CTuGgRvC.js} +3 -3
  14. package/out/renderer/assets/{liquid-DOQmSo4G.js → liquid-BrrN4uul.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-8SIcRAMh.js → lspLanguageFeatures-Bhdn2Eqy.js} +1 -1
  16. package/out/renderer/assets/{mdx-DuKdGNKE.js → mdx-DJo-gax4.js} +1 -1
  17. package/out/renderer/assets/{python-BUa4XFG-.js → python-DYHeg5b2.js} +1 -1
  18. package/out/renderer/assets/{razor-BUw8jho0.js → razor-Cqcks14K.js} +1 -1
  19. package/out/renderer/assets/{tsMode-8FLRnobw.js → tsMode-YTLgCayh.js} +1 -1
  20. package/out/renderer/assets/{typescript-rdwAjCi4.js → typescript-oxBn_Vcw.js} +1 -1
  21. package/out/renderer/assets/{xml-HOzT59Qg.js → xml-BEqGMCTc.js} +1 -1
  22. package/out/renderer/assets/{yaml-B81JfLsW.js → yaml-BGSrk5HC.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/agentState.ts +329 -0
  26. package/src/main/index.ts +135 -20
  27. package/src/main/orchestrator.ts +21 -1
  28. package/src/preload/index.ts +8 -0
  29. package/src/renderer/App.tsx +32 -9
  30. package/src/renderer/components/TicketPanel.tsx +20 -25
  31. package/src/renderer/styles.css +23 -46
@@ -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.9.0",
5474
+ version: "0.11.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);