@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
package/out/main/index.js CHANGED
@@ -5948,7 +5948,7 @@ class TerminalCapture {
5948
5948
  return lines.join("\n");
5949
5949
  }
5950
5950
  }
5951
- function stripAnsi(str) {
5951
+ function stripAnsi$1(str) {
5952
5952
  return str.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[PX^_][^\x1b]*\x1b\\/g, "").replace(/\x1b[^[\]PX^_](.|$)/g, "").replace(/\x1b/g, "");
5953
5953
  }
5954
5954
  function processBackspaces(str) {
@@ -5963,13 +5963,13 @@ function processBackspaces(str) {
5963
5963
  return result.join("");
5964
5964
  }
5965
5965
  function cleanInput(str) {
5966
- let cleaned = stripAnsi(str);
5966
+ let cleaned = stripAnsi$1(str);
5967
5967
  cleaned = processBackspaces(cleaned);
5968
5968
  cleaned = cleaned.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
5969
5969
  return cleaned.trim();
5970
5970
  }
5971
5971
  function normalizeTerminalText(str) {
5972
- let cleaned = stripAnsi(str);
5972
+ let cleaned = stripAnsi$1(str);
5973
5973
  cleaned = cleaned.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5974
5974
  cleaned = cleaned.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
5975
5975
  cleaned = cleaned.replace(/[ \t]+\n/g, "\n");
@@ -6058,7 +6058,7 @@ class TranscriptTailer {
6058
6058
  sinceMs = 0;
6059
6059
  constructor(options) {
6060
6060
  this.agentId = options.agentId;
6061
- this.cwd = stripTrailingSep(options.cwd);
6061
+ this.cwd = stripTrailingSep$1(options.cwd);
6062
6062
  this.sink = options.sink;
6063
6063
  this.claudeProjectsDir = options.claudeProjectsDir || path.join(os.homedir(), ".claude", "projects");
6064
6064
  this.codexSessionsDir = options.codexSessionsDir || path.join(os.homedir(), ".codex", "sessions");
@@ -6192,7 +6192,7 @@ class TranscriptTailer {
6192
6192
  if (!obj || typeof obj !== "object") return null;
6193
6193
  if (obj.isMeta) return null;
6194
6194
  if (obj.type !== "user" && obj.type !== "assistant") return null;
6195
- if (typeof obj.cwd === "string" && stripTrailingSep(obj.cwd) !== this.cwd) return null;
6195
+ if (typeof obj.cwd === "string" && stripTrailingSep$1(obj.cwd) !== this.cwd) return null;
6196
6196
  if (obj.isSidechain) return null;
6197
6197
  const message = obj.message;
6198
6198
  if (!message) return null;
@@ -6210,7 +6210,7 @@ class TranscriptTailer {
6210
6210
  const payload = obj.payload;
6211
6211
  if (obj.type === "session_meta") {
6212
6212
  const metaCwd = payload?.cwd;
6213
- if (typeof metaCwd === "string" && stripTrailingSep(metaCwd) !== this.cwd) {
6213
+ if (typeof metaCwd === "string" && stripTrailingSep$1(metaCwd) !== this.cwd) {
6214
6214
  tail.excluded = true;
6215
6215
  }
6216
6216
  return null;
@@ -6275,7 +6275,7 @@ function extractClaudeText(content, isUser) {
6275
6275
  }
6276
6276
  return parts.join("\n\n");
6277
6277
  }
6278
- function stripTrailingSep(p2) {
6278
+ function stripTrailingSep$1(p2) {
6279
6279
  return p2.length > 1 ? p2.replace(/[/\\]+$/, "") : p2;
6280
6280
  }
6281
6281
  function capLength(str) {
@@ -6284,7 +6284,7 @@ function capLength(str) {
6284
6284
  }
6285
6285
  function readLatestAssistantReply(opts) {
6286
6286
  try {
6287
- const cwd = stripTrailingSep(opts.cwd);
6287
+ const cwd = stripTrailingSep$1(opts.cwd);
6288
6288
  if (opts.agentId === "claude") {
6289
6289
  const file = mostRecentClaudeFile(cwd, opts.claudeProjectsDir);
6290
6290
  if (!file) return null;
@@ -6376,7 +6376,7 @@ function lastClaudeAssistant(file, cwd) {
6376
6376
  }
6377
6377
  if (!obj || typeof obj !== "object") continue;
6378
6378
  if (obj.isMeta || obj.isSidechain || obj.type !== "assistant") continue;
6379
- if (typeof obj.cwd === "string" && stripTrailingSep(obj.cwd) !== cwd) continue;
6379
+ if (typeof obj.cwd === "string" && stripTrailingSep$1(obj.cwd) !== cwd) continue;
6380
6380
  const text = extractClaudeText(obj.message?.content, false);
6381
6381
  if (text) return text;
6382
6382
  }
@@ -6395,7 +6395,7 @@ function lastCodexAgentMessage(file, cwd) {
6395
6395
  }
6396
6396
  if (obj?.type === "session_meta") {
6397
6397
  const metaCwd = obj.payload?.cwd;
6398
- if (typeof metaCwd === "string" && stripTrailingSep(metaCwd) !== cwd) return null;
6398
+ if (typeof metaCwd === "string" && stripTrailingSep$1(metaCwd) !== cwd) return null;
6399
6399
  break;
6400
6400
  }
6401
6401
  }
@@ -6414,6 +6414,206 @@ function lastCodexAgentMessage(file, cwd) {
6414
6414
  }
6415
6415
  return null;
6416
6416
  }
6417
+ const BUSY_GRACE_MS = 2500;
6418
+ const SETTLE_MS = 1200;
6419
+ const DECISION_TTL_MS = 4e3;
6420
+ const TICK_MS = 400;
6421
+ const HOOK_AUTHORITY_MS = 6e3;
6422
+ const TAIL_KEEP_CHARS = 240;
6423
+ const CLAUDE_SPINNER = /[✶✻✳✺✢✷✸✹❋][^\n]{0,60}(?:…|\.\.\.)/;
6424
+ function hasBusyMarker(text) {
6425
+ return /esc to interrupt/i.test(text) || /\bWorking \(\d+s/i.test(text) || CLAUDE_SPINNER.test(text);
6426
+ }
6427
+ const APPROVAL_PATTERNS = [
6428
+ /\bDo you want to (proceed|continue|create|make this edit|run)\b/i,
6429
+ // Claude Code
6430
+ /❯\s*\d+\.\s*Yes\b/i,
6431
+ // Claude numbered-choice prompt
6432
+ /\bAllow command\b/i,
6433
+ // Codex
6434
+ /\bRun this command\?/i,
6435
+ // Codex
6436
+ /\bApprove\b[^\n]*\?/i,
6437
+ /\(y\/n\)/i,
6438
+ /\[y\/N\]/i
6439
+ ];
6440
+ function hasApprovalMarker(text) {
6441
+ return APPROVAL_PATTERNS.some((re) => re.test(text));
6442
+ }
6443
+ function stripAnsi(str) {
6444
+ return str.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b/g, "");
6445
+ }
6446
+ class AgentStateMonitor {
6447
+ sink;
6448
+ now;
6449
+ tabs = /* @__PURE__ */ new Map();
6450
+ timer = null;
6451
+ lastAnyAwaiting = false;
6452
+ constructor(sink, now = Date.now) {
6453
+ this.sink = sink;
6454
+ this.now = now;
6455
+ }
6456
+ start() {
6457
+ if (this.timer) return;
6458
+ this.timer = setInterval(() => this.tick(), TICK_MS);
6459
+ }
6460
+ stop() {
6461
+ if (this.timer) {
6462
+ clearInterval(this.timer);
6463
+ this.timer = null;
6464
+ }
6465
+ this.tabs.clear();
6466
+ this.lastAnyAwaiting = false;
6467
+ }
6468
+ /** Register a tab so its raw PTY output can be classified. */
6469
+ trackTab(tabId, agentId, cwd) {
6470
+ const now = this.now();
6471
+ this.tabs.set(tabId, {
6472
+ agentId,
6473
+ cwd,
6474
+ state: "idle",
6475
+ everBusy: false,
6476
+ lastBusyAt: 0,
6477
+ lastOutputAt: now,
6478
+ lastDecisionAt: 0,
6479
+ hookState: null,
6480
+ hookAt: 0,
6481
+ tail: ""
6482
+ });
6483
+ }
6484
+ removeTab(tabId) {
6485
+ if (this.tabs.delete(tabId)) this.emitAggregate();
6486
+ }
6487
+ /** Feed a raw PTY output chunk for a tab. */
6488
+ feed(tabId, data) {
6489
+ const rec = this.tabs.get(tabId);
6490
+ if (!rec) return;
6491
+ const now = this.now();
6492
+ rec.lastOutputAt = now;
6493
+ const text = rec.tail + stripAnsi(data);
6494
+ rec.tail = text.length > TAIL_KEEP_CHARS ? text.slice(-TAIL_KEEP_CHARS) : text;
6495
+ if (hasBusyMarker(text)) {
6496
+ rec.lastBusyAt = now;
6497
+ rec.everBusy = true;
6498
+ }
6499
+ if (hasApprovalMarker(text)) {
6500
+ rec.lastDecisionAt = now;
6501
+ }
6502
+ }
6503
+ /**
6504
+ * Note input written to a tab's PTY. Only a *submit* (data containing a
6505
+ * newline) counts as the user engaging — clearing any awaiting state and
6506
+ * resetting the "has worked" flag so a leftover idle box doesn't re-flag.
6507
+ *
6508
+ * Individual keystrokes and, crucially, the terminal's own auto-responses
6509
+ * (cursor-position / device-attribute replies that TUIs constantly trigger,
6510
+ * which xterm.js writes straight back to the PTY) are ignored — otherwise
6511
+ * the state would churn busy↔idle on every control-sequence handshake.
6512
+ */
6513
+ noteInput(tabId, data) {
6514
+ if (!/[\r\n]/.test(data)) return;
6515
+ const rec = this.tabs.get(tabId);
6516
+ if (!rec) return;
6517
+ const now = this.now();
6518
+ rec.everBusy = false;
6519
+ rec.lastBusyAt = now;
6520
+ rec.lastDecisionAt = 0;
6521
+ rec.hookState = null;
6522
+ this.setState(rec, tabId, "busy");
6523
+ }
6524
+ /**
6525
+ * Apply an authoritative state from an agent hook, addressed by cwd. Overrides
6526
+ * the heuristic for HOOK_AUTHORITY_MS. `kind`:
6527
+ * 'awaiting_input' — turn done / idle prompt (Stop, idle_prompt, agent-turn-complete)
6528
+ * 'awaiting_decision' — permission/approval prompt (PermissionRequest, permission_prompt)
6529
+ * 'busy' — user submitted a prompt (UserPromptSubmit)
6530
+ */
6531
+ applyHookEvent(cwd, kind) {
6532
+ const target = stripTrailingSep(cwd);
6533
+ const now = this.now();
6534
+ for (const [tabId, rec] of this.tabs) {
6535
+ if (stripTrailingSep(rec.cwd) !== target) continue;
6536
+ rec.hookState = kind;
6537
+ rec.hookAt = now;
6538
+ if (kind === "busy") {
6539
+ rec.everBusy = false;
6540
+ rec.lastBusyAt = now;
6541
+ }
6542
+ this.setState(rec, tabId, kind);
6543
+ }
6544
+ }
6545
+ tick() {
6546
+ const now = this.now();
6547
+ for (const [tabId, rec] of this.tabs) {
6548
+ if (rec.hookState && now - rec.hookAt < HOOK_AUTHORITY_MS) {
6549
+ this.setState(rec, tabId, rec.hookState);
6550
+ continue;
6551
+ }
6552
+ const busyRecently = rec.lastBusyAt > 0 && now - rec.lastBusyAt < BUSY_GRACE_MS;
6553
+ const settled = now - rec.lastOutputAt >= SETTLE_MS;
6554
+ const decisionRecent = rec.lastDecisionAt > 0 && now - rec.lastDecisionAt < DECISION_TTL_MS;
6555
+ let next = null;
6556
+ if (busyRecently) {
6557
+ next = "busy";
6558
+ } else if (settled && rec.everBusy) {
6559
+ next = rec.state === "awaiting_decision" || decisionRecent ? "awaiting_decision" : "awaiting_input";
6560
+ } else if (settled && !rec.everBusy) {
6561
+ next = "idle";
6562
+ }
6563
+ if (next) this.setState(rec, tabId, next);
6564
+ }
6565
+ this.emitAggregate();
6566
+ }
6567
+ setState(rec, tabId, state) {
6568
+ if (rec.state === state) return;
6569
+ rec.state = state;
6570
+ this.sink({ tabId, state, anyAwaiting: this.computeAnyAwaiting() });
6571
+ this.lastAnyAwaiting = this.computeAnyAwaiting();
6572
+ }
6573
+ emitAggregate() {
6574
+ const anyAwaiting = this.computeAnyAwaiting();
6575
+ if (anyAwaiting === this.lastAnyAwaiting) return;
6576
+ this.lastAnyAwaiting = anyAwaiting;
6577
+ const tabId = this.firstAwaitingTabId() ?? "";
6578
+ const state = tabId ? this.tabs.get(tabId).state : "idle";
6579
+ this.sink({ tabId, state, anyAwaiting });
6580
+ }
6581
+ computeAnyAwaiting() {
6582
+ for (const rec of this.tabs.values()) {
6583
+ if (rec.state === "awaiting_input" || rec.state === "awaiting_decision") return true;
6584
+ }
6585
+ return false;
6586
+ }
6587
+ firstAwaitingTabId() {
6588
+ for (const [tabId, rec] of this.tabs) {
6589
+ if (rec.state === "awaiting_input" || rec.state === "awaiting_decision") return tabId;
6590
+ }
6591
+ return null;
6592
+ }
6593
+ }
6594
+ function stripTrailingSep(p2) {
6595
+ return p2.length > 1 ? p2.replace(/[/\\]+$/, "") : p2;
6596
+ }
6597
+ class AttentionController {
6598
+ awaiting = false;
6599
+ focused = false;
6600
+ acknowledged = false;
6601
+ /** Update awaiting state; returns whether attention should be active. */
6602
+ setAwaiting(on) {
6603
+ if (on && !this.awaiting) this.acknowledged = this.focused;
6604
+ this.awaiting = on;
6605
+ return this.active();
6606
+ }
6607
+ /** Update window focus; returns whether attention should be active. */
6608
+ setFocused(focused) {
6609
+ this.focused = focused;
6610
+ if (focused && this.awaiting) this.acknowledged = true;
6611
+ return this.active();
6612
+ }
6613
+ active() {
6614
+ return this.awaiting && !this.focused && !this.acknowledged;
6615
+ }
6616
+ }
6417
6617
  var bufferUtil = { exports: {} };
6418
6618
  var constants;
6419
6619
  var hasRequiredConstants;
@@ -11094,9 +11294,14 @@ class Orchestrator {
11094
11294
  noProjectPollCwd = null;
11095
11295
  currentProjectName = null;
11096
11296
  transcriptTailer = null;
11297
+ stateMonitor;
11097
11298
  constructor(settingsDir, events) {
11098
11299
  this.settingsDir = settingsDir;
11099
11300
  this.events = events;
11301
+ this.stateMonitor = new AgentStateMonitor(({ tabId, state, anyAwaiting }) => {
11302
+ this.events.onAgentStateChange?.(tabId, state, anyAwaiting);
11303
+ });
11304
+ this.stateMonitor.start();
11100
11305
  this.workerWs = new WorkerWsClient({
11101
11306
  onStatusChange: (status) => {
11102
11307
  log$3(`[worker-ws] Status: ${status}`);
@@ -11108,9 +11313,10 @@ class Orchestrator {
11108
11313
  this.workerWs.sendAck(message.id);
11109
11314
  if (message.type === "prompt" || message.type === "task_dispatch") {
11110
11315
  const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
11111
- if (activeTab) {
11316
+ if (activeTab && this.activeTabId) {
11112
11317
  activeTab.ptyManager.write(message.content + "\r");
11113
11318
  this.bridge.feedInput(message.content + "\r");
11319
+ this.stateMonitor.noteInput(this.activeTabId, message.content + "\r");
11114
11320
  }
11115
11321
  }
11116
11322
  },
@@ -11401,9 +11607,11 @@ class Orchestrator {
11401
11607
  const ptyManager = new PtyManager(agent, cwd);
11402
11608
  const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
11403
11609
  this.tabs.set(tabId, tab);
11610
+ this.stateMonitor.trackTab(tabId, agent.id, cwd);
11404
11611
  ptyManager.onData((data) => {
11405
11612
  this.events.onPtyData(tabId, data);
11406
11613
  this.timeTracker.recordActivity(tabId);
11614
+ this.stateMonitor.feed(tabId, data);
11407
11615
  if (tabId === this.activeTabId) {
11408
11616
  this.bridge.feedOutput(data);
11409
11617
  this.streamTerminalData(tabId, data);
@@ -11420,6 +11628,7 @@ class Orchestrator {
11420
11628
  }
11421
11629
  const t = this.tabs.get(tabId);
11422
11630
  if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
11631
+ this.stateMonitor.removeTab(tabId);
11423
11632
  });
11424
11633
  this.startChatLogging(agent, cwd);
11425
11634
  const profile = this.getActiveProfile();
@@ -11442,6 +11651,7 @@ class Orchestrator {
11442
11651
  }
11443
11652
  writePty(tabId, data) {
11444
11653
  this.tabs.get(tabId)?.ptyManager.write(data);
11654
+ this.stateMonitor.noteInput(tabId, data);
11445
11655
  if (tabId === this.activeTabId) {
11446
11656
  this.bridge.feedInput(data);
11447
11657
  }
@@ -11460,6 +11670,7 @@ class Orchestrator {
11460
11670
  await this.timeTracker.endSession(tabId);
11461
11671
  tab.ptyManager.kill();
11462
11672
  this.tabs.delete(tabId);
11673
+ this.stateMonitor.removeTab(tabId);
11463
11674
  if (tabId === this.activeTabId) {
11464
11675
  this.stopChatLogging();
11465
11676
  if (isCodingAgent(tab.agent)) {
@@ -11613,6 +11824,7 @@ class Orchestrator {
11613
11824
  async shutdown() {
11614
11825
  this.stopNoProjectPolling();
11615
11826
  this.stopChatLogging();
11827
+ this.stateMonitor.stop();
11616
11828
  await this.timeTracker.endAll();
11617
11829
  for (const [, tab] of this.tabs) {
11618
11830
  if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
@@ -11653,39 +11865,121 @@ function getProjectAbbrev(cwdPath) {
11653
11865
  if (word.length <= 3) return word.toUpperCase();
11654
11866
  return word.slice(0, 2).toUpperCase();
11655
11867
  }
11656
- function createOverlayIcon(text) {
11868
+ function createDotOverlay(ready) {
11657
11869
  const size = 32;
11658
11870
  const canvas = Buffer.alloc(size * size * 4, 0);
11871
+ const [r, g2, b2] = ready ? [52, 199, 89] : [122, 162, 247];
11659
11872
  for (let y2 = 0; y2 < size; y2++) {
11660
11873
  for (let x = 0; x < size; x++) {
11661
11874
  const cx = x - size / 2;
11662
11875
  const cy = y2 - size / 2;
11663
- const dist = Math.sqrt(cx * cx + cy * cy);
11664
- if (dist <= size / 2) {
11876
+ if (Math.sqrt(cx * cx + cy * cy) <= size / 2) {
11665
11877
  const idx = (y2 * size + x) * 4;
11666
- canvas[idx] = 122;
11667
- canvas[idx + 1] = 162;
11668
- canvas[idx + 2] = 247;
11669
- canvas[idx + 3] = 220;
11878
+ canvas[idx] = r;
11879
+ canvas[idx + 1] = g2;
11880
+ canvas[idx + 2] = b2;
11881
+ canvas[idx + 3] = 235;
11670
11882
  }
11671
11883
  }
11672
11884
  }
11673
11885
  return electron.nativeImage.createFromBuffer(canvas, { width: size, height: size });
11674
11886
  }
11675
- function updateProjectBadge(cwdPath) {
11676
- if (!cwdPath) return;
11677
- const abbrev = getProjectAbbrev(cwdPath);
11887
+ let baseDockIcon = null;
11888
+ function dockIconWithReadyDot(base) {
11889
+ const { width, height } = base.getSize();
11890
+ if (!width || !height) return base;
11891
+ const bmp = base.toBitmap();
11892
+ const r = Math.round(Math.min(width, height) * 0.17);
11893
+ const ring = Math.max(2, Math.round(r * 0.22));
11894
+ const cx = width - r - Math.round(width * 0.05);
11895
+ const cy = height - r - Math.round(height * 0.05);
11896
+ const put = (x, y2, b2, g2, rr) => {
11897
+ const idx = (y2 * width + x) * 4;
11898
+ bmp[idx] = b2;
11899
+ bmp[idx + 1] = g2;
11900
+ bmp[idx + 2] = rr;
11901
+ bmp[idx + 3] = 255;
11902
+ };
11903
+ for (let y2 = cy - r - ring; y2 <= cy + r + ring; y2++) {
11904
+ if (y2 < 0 || y2 >= height) continue;
11905
+ for (let x = cx - r - ring; x <= cx + r + ring; x++) {
11906
+ if (x < 0 || x >= width) continue;
11907
+ const d = Math.sqrt((x - cx) ** 2 + (y2 - cy) ** 2);
11908
+ if (d <= r) put(x, y2, 89, 199, 52);
11909
+ else if (d <= r + ring) put(x, y2, 24, 26, 31);
11910
+ }
11911
+ }
11912
+ return electron.nativeImage.createFromBitmap(bmp, { width, height });
11913
+ }
11914
+ function setDockReadyDot(on) {
11915
+ if (process.platform !== "darwin" || !electron.app.dock || !baseDockIcon) return;
11916
+ electron.app.dock.setIcon(on ? dockIconWithReadyDot(baseDockIcon) : baseDockIcon);
11917
+ }
11918
+ let projectAbbrev = "";
11919
+ function applySteadyBadge() {
11678
11920
  if (process.platform === "darwin" && electron.app.dock) {
11679
- electron.app.dock.setBadge(abbrev);
11921
+ electron.app.dock.setBadge(projectAbbrev);
11680
11922
  }
11681
11923
  if (process.platform === "win32" && mainWindow) {
11682
- mainWindow.setOverlayIcon(createOverlayIcon(), abbrev);
11924
+ mainWindow.setOverlayIcon(projectAbbrev ? createDotOverlay(false) : null, projectAbbrev);
11683
11925
  }
11926
+ }
11927
+ function updateProjectBadge(cwdPath) {
11928
+ if (!cwdPath) return;
11929
+ projectAbbrev = getProjectAbbrev(cwdPath);
11930
+ applySteadyBadge();
11684
11931
  if (mainWindow) {
11685
11932
  const folderName = cwdPath.split("/").filter(Boolean).pop() || "ctlsurf-worker";
11686
11933
  mainWindow.setTitle(`ctlsurf-worker — ${folderName}`);
11687
11934
  }
11688
11935
  }
11936
+ const attention = new AttentionController();
11937
+ let attentionActive = false;
11938
+ let bounceId = -1;
11939
+ function setAwaitingIndicator(on) {
11940
+ applyAttention(attention.setAwaiting(on));
11941
+ }
11942
+ function applyAttention(active) {
11943
+ if (active) startAttention();
11944
+ else stopAttention();
11945
+ }
11946
+ function startAttention() {
11947
+ if (attentionActive) return;
11948
+ attentionActive = true;
11949
+ log("[agent-state] attention START (green ready dot + bounce)");
11950
+ if (process.platform === "darwin" && electron.app.dock) {
11951
+ setDockReadyDot(true);
11952
+ bounceId = electron.app.dock.bounce("informational");
11953
+ }
11954
+ if (process.platform === "win32" && mainWindow) {
11955
+ mainWindow.setOverlayIcon(createDotOverlay(true), "Ready for you");
11956
+ mainWindow.flashFrame(true);
11957
+ }
11958
+ if (process.platform === "linux") {
11959
+ if (typeof electron.app.badgeCount === "number") electron.app.badgeCount = 1;
11960
+ mainWindow?.flashFrame(true);
11961
+ }
11962
+ }
11963
+ function stopAttention() {
11964
+ if (!attentionActive) return;
11965
+ attentionActive = false;
11966
+ log("[agent-state] attention STOP");
11967
+ if (process.platform === "darwin" && electron.app.dock) {
11968
+ setDockReadyDot(false);
11969
+ if (bounceId >= 0) {
11970
+ electron.app.dock.cancelBounce(bounceId);
11971
+ bounceId = -1;
11972
+ }
11973
+ }
11974
+ if (process.platform === "win32" && mainWindow) {
11975
+ mainWindow.flashFrame(false);
11976
+ }
11977
+ if (process.platform === "linux") {
11978
+ if (typeof electron.app.badgeCount === "number") electron.app.badgeCount = 0;
11979
+ mainWindow?.flashFrame(false);
11980
+ }
11981
+ applySteadyBadge();
11982
+ }
11689
11983
  const orchestrator = new Orchestrator(
11690
11984
  getSettingsDir(),
11691
11985
  {
@@ -11696,6 +11990,11 @@ const orchestrator = new Orchestrator(
11696
11990
  onWorkerRegistered: (data) => mainWindow?.webContents.send("worker:registered", data),
11697
11991
  onProjectChanged: (name) => mainWindow?.webContents.send("app:projectChanged", name),
11698
11992
  onAgentMessage: (text) => mainWindow?.webContents.send("agent:message", text),
11993
+ onAgentStateChange: (tabId, state, anyAwaiting) => {
11994
+ log(`[agent-state] tab=${tabId} state=${state} anyAwaiting=${anyAwaiting}`);
11995
+ mainWindow?.webContents.send("agent:state", tabId, state, anyAwaiting);
11996
+ setAwaitingIndicator(anyAwaiting);
11997
+ },
11699
11998
  onCwdChanged: () => {
11700
11999
  mainWindow?.webContents.send("app:cwdChanged");
11701
12000
  updateProjectBadge(orchestrator.cwd);
@@ -11726,6 +12025,8 @@ function createWindow() {
11726
12025
  } else {
11727
12026
  mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
11728
12027
  }
12028
+ mainWindow.on("focus", () => applyAttention(attention.setFocused(true)));
12029
+ mainWindow.on("blur", () => applyAttention(attention.setFocused(false)));
11729
12030
  mainWindow.on("closed", () => {
11730
12031
  mainWindow = null;
11731
12032
  });
@@ -11989,7 +12290,8 @@ electron.app.whenReady().then(() => {
11989
12290
  if (process.platform === "darwin" && electron.app.dock) {
11990
12291
  const iconPath = path.join(__dirname, "../../resources/icon.png");
11991
12292
  try {
11992
- electron.app.dock.setIcon(electron.nativeImage.createFromPath(iconPath));
12293
+ baseDockIcon = electron.nativeImage.createFromPath(iconPath);
12294
+ electron.app.dock.setIcon(baseDockIcon);
11993
12295
  } catch {
11994
12296
  }
11995
12297
  }
@@ -69,6 +69,13 @@ const api = {
69
69
  electron.ipcRenderer.on("agent:message", listener);
70
70
  return () => electron.ipcRenderer.removeListener("agent:message", listener);
71
71
  },
72
+ // Real-time agent activity state for an in-app indicator dot.
73
+ // state: 'idle' | 'busy' | 'awaiting_input' | 'awaiting_decision'
74
+ onAgentState: (callback) => {
75
+ const listener = (_event, tabId, state, anyAwaiting) => callback(tabId, state, anyAwaiting);
76
+ electron.ipcRenderer.on("agent:state", listener);
77
+ return () => electron.ipcRenderer.removeListener("agent:state", listener);
78
+ },
72
79
  // Latest assistant reply from the active tab's transcript (null if none).
73
80
  getLastReply: () => electron.ipcRenderer.invoke("agent:getLastReply"),
74
81
  // Filesystem
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-Cj7K9-Ba.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-8SIcRAMh.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-8SIcRAMh.js";
1
+ import { c as createWebWorker, l as languages } from "./index-BDlCPzCG.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-Bhdn2Eqy.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-Bhdn2Eqy.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-Cj7K9-Ba.js";
1
+ import { l as languages } from "./index-BDlCPzCG.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-Cj7K9-Ba.js";
1
+ import { l as languages } from "./index-BDlCPzCG.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-Cj7K9-Ba.js";
1
+ import { l as languages } from "./index-BDlCPzCG.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-Cj7K9-Ba.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-8SIcRAMh.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-8SIcRAMh.js";
1
+ import { c as createWebWorker, l as languages } from "./index-BDlCPzCG.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-Bhdn2Eqy.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-Bhdn2Eqy.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {