@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.
- package/out/headless/index.mjs +196 -2
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +326 -24
- package/out/preload/index.js +7 -0
- package/out/renderer/assets/{cssMode-BdYp0fp9.js → cssMode-Bmzy9hKX.js} +3 -3
- package/out/renderer/assets/{freemarker2-Cya-9zr-.js → freemarker2-CDAsExTa.js} +1 -1
- package/out/renderer/assets/{handlebars-EJs9W5Ub.js → handlebars-CvpBYG1L.js} +1 -1
- package/out/renderer/assets/{html-Ex4qy2lh.js → html-CNhEQqr8.js} +1 -1
- package/out/renderer/assets/{htmlMode-CCm0aJ6C.js → htmlMode-DD7YPgga.js} +3 -3
- package/out/renderer/assets/{index-Cj7K9-Ba.js → index-BDlCPzCG.js} +153 -146
- package/out/renderer/assets/{index-nvsNYP5c.css → index-CGHeDwZa.css} +23 -46
- package/out/renderer/assets/{javascript-B2niCouI.js → javascript-CsGLsswK.js} +2 -2
- package/out/renderer/assets/{jsonMode-BgCUHaFo.js → jsonMode-CTuGgRvC.js} +3 -3
- package/out/renderer/assets/{liquid-DOQmSo4G.js → liquid-BrrN4uul.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-8SIcRAMh.js → lspLanguageFeatures-Bhdn2Eqy.js} +1 -1
- package/out/renderer/assets/{mdx-DuKdGNKE.js → mdx-DJo-gax4.js} +1 -1
- package/out/renderer/assets/{python-BUa4XFG-.js → python-DYHeg5b2.js} +1 -1
- package/out/renderer/assets/{razor-BUw8jho0.js → razor-Cqcks14K.js} +1 -1
- package/out/renderer/assets/{tsMode-8FLRnobw.js → tsMode-YTLgCayh.js} +1 -1
- package/out/renderer/assets/{typescript-rdwAjCi4.js → typescript-oxBn_Vcw.js} +1 -1
- package/out/renderer/assets/{xml-HOzT59Qg.js → xml-BEqGMCTc.js} +1 -1
- package/out/renderer/assets/{yaml-B81JfLsW.js → yaml-BGSrk5HC.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 +135 -20
- package/src/main/orchestrator.ts +21 -1
- package/src/preload/index.ts +8 -0
- package/src/renderer/App.tsx +32 -9
- package/src/renderer/components/TicketPanel.tsx +20 -25
- 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
|
|
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
|
-
|
|
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] =
|
|
11667
|
-
canvas[idx + 1] =
|
|
11668
|
-
canvas[idx + 2] =
|
|
11669
|
-
canvas[idx + 3] =
|
|
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
|
-
|
|
11676
|
-
|
|
11677
|
-
const
|
|
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(
|
|
11921
|
+
electron.app.dock.setBadge(projectAbbrev);
|
|
11680
11922
|
}
|
|
11681
11923
|
if (process.platform === "win32" && mainWindow) {
|
|
11682
|
-
mainWindow.setOverlayIcon(
|
|
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.
|
|
12293
|
+
baseDockIcon = electron.nativeImage.createFromPath(iconPath);
|
|
12294
|
+
electron.app.dock.setIcon(baseDockIcon);
|
|
11993
12295
|
} catch {
|
|
11994
12296
|
}
|
|
11995
12297
|
}
|
package/out/preload/index.js
CHANGED
|
@@ -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-
|
|
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-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,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-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) {
|