@phenx-inc/ctlsurf 0.8.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 +339 -2
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +441 -20
- package/out/preload/index.js +9 -0
- package/out/renderer/assets/{cssMode-BQN8v2ok.js → cssMode-BbCVOTaJ.js} +3 -3
- package/out/renderer/assets/{freemarker2-DbxGYYVp.js → freemarker2-DryLywQv.js} +1 -1
- package/out/renderer/assets/{handlebars-3auU1CAd.js → handlebars-CdUMtSdr.js} +1 -1
- package/out/renderer/assets/{html-D8xFiRmI.js → html-Cwkf2GMH.js} +1 -1
- package/out/renderer/assets/{htmlMode-M3MApZ4n.js → htmlMode-C7PzhXxH.js} +3 -3
- package/out/renderer/assets/{index-B-iM7dFC.css → index-BIceGcz3.css} +52 -0
- package/out/renderer/assets/{index---H6cxNl.js → index-Dg1vMspf.js} +274 -244
- package/out/renderer/assets/{javascript-BO_ViZM5.js → javascript-Cy7ROXJl.js} +2 -2
- package/out/renderer/assets/{jsonMode-CKp2zvZu.js → jsonMode-BtZtt3QW.js} +3 -3
- package/out/renderer/assets/{liquid-C1eHcrht.js → liquid-xReAQeYs.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-CHWJx_Tl.js → lspLanguageFeatures-Dmx0L0Bw.js} +1 -1
- package/out/renderer/assets/{mdx-Qqdtk7fL.js → mdx-YMNFO1Zz.js} +1 -1
- package/out/renderer/assets/{python-DKu7rNbs.js → python-BExaDVxv.js} +1 -1
- package/out/renderer/assets/{razor-BOMpCo6z.js → razor-HL8ANmQ4.js} +1 -1
- package/out/renderer/assets/{transformers.web-DtSCnG36.js → transformers.web-_PuZoIho.js} +824 -19
- package/out/renderer/assets/{tsMode-yAjlPR-D.js → tsMode-Dqd8vdLE.js} +1 -1
- package/out/renderer/assets/{typescript-BiJRCUcL.js → typescript-Dysz1PTX.js} +1 -1
- package/out/renderer/assets/{xml-D4PvYeQq.js → xml-BHp5Z9nc.js} +1 -1
- package/out/renderer/assets/{yaml-BeHVkmnS.js → yaml-CPDHxVup.js} +1 -1
- package/out/renderer/index.html +64 -2
- package/out/repro-toggle.mjs +305 -0
- package/package.json +1 -1
- package/src/main/agentState.ts +329 -0
- package/src/main/index.ts +107 -19
- package/src/main/orchestrator.ts +34 -2
- package/src/main/transcripts.ts +134 -0
- package/src/preload/index.ts +11 -0
- package/src/renderer/App.tsx +48 -13
- package/src/renderer/components/FloatingMic.tsx +48 -0
- package/src/renderer/components/VoiceInput.tsx +36 -149
- package/src/renderer/index.html +62 -0
- package/src/renderer/lib/localWhisper.ts +46 -2
- package/src/renderer/lib/speech.ts +18 -0
- package/src/renderer/styles.css +52 -0
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,13 +6275,345 @@ 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) {
|
|
6282
6282
|
if (str.length <= MAX_ENTRY_CHARS) return str;
|
|
6283
6283
|
return str.slice(0, MAX_ENTRY_CHARS) + `… [truncated, ${str.length} total chars]`;
|
|
6284
6284
|
}
|
|
6285
|
+
function readLatestAssistantReply(opts) {
|
|
6286
|
+
try {
|
|
6287
|
+
const cwd = stripTrailingSep$1(opts.cwd);
|
|
6288
|
+
if (opts.agentId === "claude") {
|
|
6289
|
+
const file = mostRecentClaudeFile(cwd, opts.claudeProjectsDir);
|
|
6290
|
+
if (!file) return null;
|
|
6291
|
+
const text = lastClaudeAssistant(file, cwd);
|
|
6292
|
+
return text ? capLength(text) : null;
|
|
6293
|
+
}
|
|
6294
|
+
const codexRoot = opts.codexSessionsDir || path.join(os.homedir(), ".codex", "sessions");
|
|
6295
|
+
for (const file of recentCodexRollouts(codexRoot)) {
|
|
6296
|
+
const text = lastCodexAgentMessage(file, cwd);
|
|
6297
|
+
if (text) return capLength(text);
|
|
6298
|
+
}
|
|
6299
|
+
return null;
|
|
6300
|
+
} catch {
|
|
6301
|
+
return null;
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
function readLines(file) {
|
|
6305
|
+
try {
|
|
6306
|
+
return fs.readFileSync(file, "utf-8").split("\n");
|
|
6307
|
+
} catch {
|
|
6308
|
+
return [];
|
|
6309
|
+
}
|
|
6310
|
+
}
|
|
6311
|
+
function mostRecentClaudeFile(cwd, root) {
|
|
6312
|
+
const projectsRoot = root || path.join(os.homedir(), ".claude", "projects");
|
|
6313
|
+
const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
6314
|
+
const dir = path.join(projectsRoot, slug);
|
|
6315
|
+
let names;
|
|
6316
|
+
try {
|
|
6317
|
+
names = fs.readdirSync(dir);
|
|
6318
|
+
} catch {
|
|
6319
|
+
return null;
|
|
6320
|
+
}
|
|
6321
|
+
let best = null;
|
|
6322
|
+
let bestMs = -Infinity;
|
|
6323
|
+
for (const name of names) {
|
|
6324
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
6325
|
+
const fp = path.join(dir, name);
|
|
6326
|
+
try {
|
|
6327
|
+
const ms = fs.statSync(fp).mtimeMs;
|
|
6328
|
+
if (ms > bestMs) {
|
|
6329
|
+
bestMs = ms;
|
|
6330
|
+
best = fp;
|
|
6331
|
+
}
|
|
6332
|
+
} catch {
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
return best;
|
|
6336
|
+
}
|
|
6337
|
+
function recentCodexRollouts(root) {
|
|
6338
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6339
|
+
const now = Date.now();
|
|
6340
|
+
for (const ms of [now, now - 24 * 60 * 60 * 1e3]) {
|
|
6341
|
+
const d = new Date(ms);
|
|
6342
|
+
const yyyy = String(d.getFullYear());
|
|
6343
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
6344
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
6345
|
+
dirs.add(path.join(root, yyyy, mm, dd));
|
|
6346
|
+
}
|
|
6347
|
+
const files = [];
|
|
6348
|
+
for (const dir of dirs) {
|
|
6349
|
+
let names;
|
|
6350
|
+
try {
|
|
6351
|
+
names = fs.readdirSync(dir);
|
|
6352
|
+
} catch {
|
|
6353
|
+
continue;
|
|
6354
|
+
}
|
|
6355
|
+
for (const name of names) {
|
|
6356
|
+
if (!name.startsWith("rollout-") || !name.endsWith(".jsonl")) continue;
|
|
6357
|
+
const fp = path.join(dir, name);
|
|
6358
|
+
try {
|
|
6359
|
+
files.push({ fp, ms: fs.statSync(fp).mtimeMs });
|
|
6360
|
+
} catch {
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
return files.sort((a, b2) => b2.ms - a.ms).map((f) => f.fp);
|
|
6365
|
+
}
|
|
6366
|
+
function lastClaudeAssistant(file, cwd) {
|
|
6367
|
+
const lines = readLines(file);
|
|
6368
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6369
|
+
const trimmed = lines[i].trim();
|
|
6370
|
+
if (!trimmed) continue;
|
|
6371
|
+
let obj;
|
|
6372
|
+
try {
|
|
6373
|
+
obj = JSON.parse(trimmed);
|
|
6374
|
+
} catch {
|
|
6375
|
+
continue;
|
|
6376
|
+
}
|
|
6377
|
+
if (!obj || typeof obj !== "object") continue;
|
|
6378
|
+
if (obj.isMeta || obj.isSidechain || obj.type !== "assistant") continue;
|
|
6379
|
+
if (typeof obj.cwd === "string" && stripTrailingSep$1(obj.cwd) !== cwd) continue;
|
|
6380
|
+
const text = extractClaudeText(obj.message?.content, false);
|
|
6381
|
+
if (text) return text;
|
|
6382
|
+
}
|
|
6383
|
+
return null;
|
|
6384
|
+
}
|
|
6385
|
+
function lastCodexAgentMessage(file, cwd) {
|
|
6386
|
+
const lines = readLines(file);
|
|
6387
|
+
for (const line of lines) {
|
|
6388
|
+
const trimmed = line.trim();
|
|
6389
|
+
if (!trimmed) continue;
|
|
6390
|
+
let obj;
|
|
6391
|
+
try {
|
|
6392
|
+
obj = JSON.parse(trimmed);
|
|
6393
|
+
} catch {
|
|
6394
|
+
continue;
|
|
6395
|
+
}
|
|
6396
|
+
if (obj?.type === "session_meta") {
|
|
6397
|
+
const metaCwd = obj.payload?.cwd;
|
|
6398
|
+
if (typeof metaCwd === "string" && stripTrailingSep$1(metaCwd) !== cwd) return null;
|
|
6399
|
+
break;
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6403
|
+
const trimmed = lines[i].trim();
|
|
6404
|
+
if (!trimmed) continue;
|
|
6405
|
+
let obj;
|
|
6406
|
+
try {
|
|
6407
|
+
obj = JSON.parse(trimmed);
|
|
6408
|
+
} catch {
|
|
6409
|
+
continue;
|
|
6410
|
+
}
|
|
6411
|
+
if (obj?.type !== "event_msg" || obj.payload?.type !== "agent_message") continue;
|
|
6412
|
+
const text = typeof obj.payload.message === "string" ? obj.payload.message.trim() : "";
|
|
6413
|
+
if (text && !isCodexNoise(text)) return text;
|
|
6414
|
+
}
|
|
6415
|
+
return null;
|
|
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
|
+
}
|
|
6285
6617
|
var bufferUtil = { exports: {} };
|
|
6286
6618
|
var constants;
|
|
6287
6619
|
var hasRequiredConstants;
|
|
@@ -10962,9 +11294,14 @@ class Orchestrator {
|
|
|
10962
11294
|
noProjectPollCwd = null;
|
|
10963
11295
|
currentProjectName = null;
|
|
10964
11296
|
transcriptTailer = null;
|
|
11297
|
+
stateMonitor;
|
|
10965
11298
|
constructor(settingsDir, events) {
|
|
10966
11299
|
this.settingsDir = settingsDir;
|
|
10967
11300
|
this.events = events;
|
|
11301
|
+
this.stateMonitor = new AgentStateMonitor(({ tabId, state, anyAwaiting }) => {
|
|
11302
|
+
this.events.onAgentStateChange?.(tabId, state, anyAwaiting);
|
|
11303
|
+
});
|
|
11304
|
+
this.stateMonitor.start();
|
|
10968
11305
|
this.workerWs = new WorkerWsClient({
|
|
10969
11306
|
onStatusChange: (status) => {
|
|
10970
11307
|
log$3(`[worker-ws] Status: ${status}`);
|
|
@@ -10976,9 +11313,10 @@ class Orchestrator {
|
|
|
10976
11313
|
this.workerWs.sendAck(message.id);
|
|
10977
11314
|
if (message.type === "prompt" || message.type === "task_dispatch") {
|
|
10978
11315
|
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
|
|
10979
|
-
if (activeTab) {
|
|
11316
|
+
if (activeTab && this.activeTabId) {
|
|
10980
11317
|
activeTab.ptyManager.write(message.content + "\r");
|
|
10981
11318
|
this.bridge.feedInput(message.content + "\r");
|
|
11319
|
+
this.stateMonitor.noteInput(this.activeTabId, message.content + "\r");
|
|
10982
11320
|
}
|
|
10983
11321
|
}
|
|
10984
11322
|
},
|
|
@@ -11011,6 +11349,17 @@ class Orchestrator {
|
|
|
11011
11349
|
get speakRepliesEnabled() {
|
|
11012
11350
|
return !!this.settings.speakReplies;
|
|
11013
11351
|
}
|
|
11352
|
+
// Latest assistant reply from a tab's transcript, for on-demand "read aloud".
|
|
11353
|
+
// Defaults to the active tab. Returns null when the tab's agent has no native
|
|
11354
|
+
// transcript (only Claude Code / Codex do) or nothing has been said yet. Reads
|
|
11355
|
+
// the transcript directly, so it works regardless of the speakReplies setting.
|
|
11356
|
+
getLastReply(tabId) {
|
|
11357
|
+
const id = tabId || this.activeTabId;
|
|
11358
|
+
if (!id) return null;
|
|
11359
|
+
const tab = this.tabs.get(id);
|
|
11360
|
+
if (!tab || !supportsTranscriptLogging(tab.agent.id)) return null;
|
|
11361
|
+
return readLatestAssistantReply({ agentId: tab.agent.id, cwd: tab.cwd });
|
|
11362
|
+
}
|
|
11014
11363
|
setLogChat(enabled) {
|
|
11015
11364
|
this.settings.logChat = enabled;
|
|
11016
11365
|
this.saveSettings();
|
|
@@ -11258,9 +11607,11 @@ class Orchestrator {
|
|
|
11258
11607
|
const ptyManager = new PtyManager(agent, cwd);
|
|
11259
11608
|
const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
|
|
11260
11609
|
this.tabs.set(tabId, tab);
|
|
11610
|
+
this.stateMonitor.trackTab(tabId, agent.id, cwd);
|
|
11261
11611
|
ptyManager.onData((data) => {
|
|
11262
11612
|
this.events.onPtyData(tabId, data);
|
|
11263
11613
|
this.timeTracker.recordActivity(tabId);
|
|
11614
|
+
this.stateMonitor.feed(tabId, data);
|
|
11264
11615
|
if (tabId === this.activeTabId) {
|
|
11265
11616
|
this.bridge.feedOutput(data);
|
|
11266
11617
|
this.streamTerminalData(tabId, data);
|
|
@@ -11277,6 +11628,7 @@ class Orchestrator {
|
|
|
11277
11628
|
}
|
|
11278
11629
|
const t = this.tabs.get(tabId);
|
|
11279
11630
|
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
|
|
11631
|
+
this.stateMonitor.removeTab(tabId);
|
|
11280
11632
|
});
|
|
11281
11633
|
this.startChatLogging(agent, cwd);
|
|
11282
11634
|
const profile = this.getActiveProfile();
|
|
@@ -11299,6 +11651,7 @@ class Orchestrator {
|
|
|
11299
11651
|
}
|
|
11300
11652
|
writePty(tabId, data) {
|
|
11301
11653
|
this.tabs.get(tabId)?.ptyManager.write(data);
|
|
11654
|
+
this.stateMonitor.noteInput(tabId, data);
|
|
11302
11655
|
if (tabId === this.activeTabId) {
|
|
11303
11656
|
this.bridge.feedInput(data);
|
|
11304
11657
|
}
|
|
@@ -11317,6 +11670,7 @@ class Orchestrator {
|
|
|
11317
11670
|
await this.timeTracker.endSession(tabId);
|
|
11318
11671
|
tab.ptyManager.kill();
|
|
11319
11672
|
this.tabs.delete(tabId);
|
|
11673
|
+
this.stateMonitor.removeTab(tabId);
|
|
11320
11674
|
if (tabId === this.activeTabId) {
|
|
11321
11675
|
this.stopChatLogging();
|
|
11322
11676
|
if (isCodingAgent(tab.agent)) {
|
|
@@ -11470,6 +11824,7 @@ class Orchestrator {
|
|
|
11470
11824
|
async shutdown() {
|
|
11471
11825
|
this.stopNoProjectPolling();
|
|
11472
11826
|
this.stopChatLogging();
|
|
11827
|
+
this.stateMonitor.stop();
|
|
11473
11828
|
await this.timeTracker.endAll();
|
|
11474
11829
|
for (const [, tab] of this.tabs) {
|
|
11475
11830
|
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|
|
@@ -11510,39 +11865,97 @@ function getProjectAbbrev(cwdPath) {
|
|
|
11510
11865
|
if (word.length <= 3) return word.toUpperCase();
|
|
11511
11866
|
return word.slice(0, 2).toUpperCase();
|
|
11512
11867
|
}
|
|
11513
|
-
function
|
|
11868
|
+
function createDotOverlay(red) {
|
|
11514
11869
|
const size = 32;
|
|
11515
11870
|
const canvas = Buffer.alloc(size * size * 4, 0);
|
|
11871
|
+
const [r, g2, b2] = red ? [235, 64, 52] : [122, 162, 247];
|
|
11516
11872
|
for (let y2 = 0; y2 < size; y2++) {
|
|
11517
11873
|
for (let x = 0; x < size; x++) {
|
|
11518
11874
|
const cx = x - size / 2;
|
|
11519
11875
|
const cy = y2 - size / 2;
|
|
11520
|
-
|
|
11521
|
-
if (dist <= size / 2) {
|
|
11876
|
+
if (Math.sqrt(cx * cx + cy * cy) <= size / 2) {
|
|
11522
11877
|
const idx = (y2 * size + x) * 4;
|
|
11523
|
-
canvas[idx] =
|
|
11524
|
-
canvas[idx + 1] =
|
|
11525
|
-
canvas[idx + 2] =
|
|
11526
|
-
canvas[idx + 3] =
|
|
11878
|
+
canvas[idx] = r;
|
|
11879
|
+
canvas[idx + 1] = g2;
|
|
11880
|
+
canvas[idx + 2] = b2;
|
|
11881
|
+
canvas[idx + 3] = 235;
|
|
11527
11882
|
}
|
|
11528
11883
|
}
|
|
11529
11884
|
}
|
|
11530
11885
|
return electron.nativeImage.createFromBuffer(canvas, { width: size, height: size });
|
|
11531
11886
|
}
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
const abbrev = getProjectAbbrev(cwdPath);
|
|
11887
|
+
let projectAbbrev = "";
|
|
11888
|
+
function applySteadyBadge() {
|
|
11535
11889
|
if (process.platform === "darwin" && electron.app.dock) {
|
|
11536
|
-
electron.app.dock.setBadge(
|
|
11890
|
+
electron.app.dock.setBadge(projectAbbrev);
|
|
11537
11891
|
}
|
|
11538
11892
|
if (process.platform === "win32" && mainWindow) {
|
|
11539
|
-
mainWindow.setOverlayIcon(
|
|
11893
|
+
mainWindow.setOverlayIcon(projectAbbrev ? createDotOverlay(false) : null, projectAbbrev);
|
|
11540
11894
|
}
|
|
11895
|
+
}
|
|
11896
|
+
function updateProjectBadge(cwdPath) {
|
|
11897
|
+
if (!cwdPath) return;
|
|
11898
|
+
projectAbbrev = getProjectAbbrev(cwdPath);
|
|
11899
|
+
if (!blinkTimer) applySteadyBadge();
|
|
11541
11900
|
if (mainWindow) {
|
|
11542
11901
|
const folderName = cwdPath.split("/").filter(Boolean).pop() || "ctlsurf-worker";
|
|
11543
11902
|
mainWindow.setTitle(`ctlsurf-worker — ${folderName}`);
|
|
11544
11903
|
}
|
|
11545
11904
|
}
|
|
11905
|
+
const attention = new AttentionController();
|
|
11906
|
+
let blinkTimer = null;
|
|
11907
|
+
let blinkOn = false;
|
|
11908
|
+
let bounceId = -1;
|
|
11909
|
+
function setAwaitingIndicator(on) {
|
|
11910
|
+
applyAttention(attention.setAwaiting(on));
|
|
11911
|
+
}
|
|
11912
|
+
function applyAttention(active) {
|
|
11913
|
+
if (active) startAttention();
|
|
11914
|
+
else stopAttention();
|
|
11915
|
+
}
|
|
11916
|
+
function startAttention() {
|
|
11917
|
+
if (blinkTimer) return;
|
|
11918
|
+
log("[agent-state] attention START (blink + bounce)");
|
|
11919
|
+
if (process.platform === "darwin" && electron.app.dock) {
|
|
11920
|
+
bounceId = electron.app.dock.bounce("informational");
|
|
11921
|
+
}
|
|
11922
|
+
if (process.platform === "win32" && mainWindow) {
|
|
11923
|
+
mainWindow.setOverlayIcon(createDotOverlay(true), "Awaiting input");
|
|
11924
|
+
mainWindow.flashFrame(true);
|
|
11925
|
+
}
|
|
11926
|
+
if (process.platform === "linux") {
|
|
11927
|
+
if (typeof electron.app.badgeCount === "number") electron.app.badgeCount = 1;
|
|
11928
|
+
mainWindow?.flashFrame(true);
|
|
11929
|
+
}
|
|
11930
|
+
blinkOn = true;
|
|
11931
|
+
blinkTimer = setInterval(() => {
|
|
11932
|
+
blinkOn = !blinkOn;
|
|
11933
|
+
if (process.platform === "darwin" && electron.app.dock) {
|
|
11934
|
+
electron.app.dock.setBadge(blinkOn ? projectAbbrev || "●" : "");
|
|
11935
|
+
}
|
|
11936
|
+
}, 600);
|
|
11937
|
+
}
|
|
11938
|
+
function stopAttention() {
|
|
11939
|
+
if (blinkTimer) {
|
|
11940
|
+
clearInterval(blinkTimer);
|
|
11941
|
+
blinkTimer = null;
|
|
11942
|
+
log("[agent-state] attention STOP");
|
|
11943
|
+
}
|
|
11944
|
+
if (process.platform === "darwin" && electron.app.dock) {
|
|
11945
|
+
if (bounceId >= 0) {
|
|
11946
|
+
electron.app.dock.cancelBounce(bounceId);
|
|
11947
|
+
bounceId = -1;
|
|
11948
|
+
}
|
|
11949
|
+
}
|
|
11950
|
+
if (process.platform === "win32" && mainWindow) {
|
|
11951
|
+
mainWindow.flashFrame(false);
|
|
11952
|
+
}
|
|
11953
|
+
if (process.platform === "linux") {
|
|
11954
|
+
if (typeof electron.app.badgeCount === "number") electron.app.badgeCount = 0;
|
|
11955
|
+
mainWindow?.flashFrame(false);
|
|
11956
|
+
}
|
|
11957
|
+
applySteadyBadge();
|
|
11958
|
+
}
|
|
11546
11959
|
const orchestrator = new Orchestrator(
|
|
11547
11960
|
getSettingsDir(),
|
|
11548
11961
|
{
|
|
@@ -11553,6 +11966,11 @@ const orchestrator = new Orchestrator(
|
|
|
11553
11966
|
onWorkerRegistered: (data) => mainWindow?.webContents.send("worker:registered", data),
|
|
11554
11967
|
onProjectChanged: (name) => mainWindow?.webContents.send("app:projectChanged", name),
|
|
11555
11968
|
onAgentMessage: (text) => mainWindow?.webContents.send("agent:message", text),
|
|
11969
|
+
onAgentStateChange: (tabId, state, anyAwaiting) => {
|
|
11970
|
+
log(`[agent-state] tab=${tabId} state=${state} anyAwaiting=${anyAwaiting}`);
|
|
11971
|
+
mainWindow?.webContents.send("agent:state", tabId, state, anyAwaiting);
|
|
11972
|
+
setAwaitingIndicator(anyAwaiting);
|
|
11973
|
+
},
|
|
11556
11974
|
onCwdChanged: () => {
|
|
11557
11975
|
mainWindow?.webContents.send("app:cwdChanged");
|
|
11558
11976
|
updateProjectBadge(orchestrator.cwd);
|
|
@@ -11583,6 +12001,8 @@ function createWindow() {
|
|
|
11583
12001
|
} else {
|
|
11584
12002
|
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
|
11585
12003
|
}
|
|
12004
|
+
mainWindow.on("focus", () => applyAttention(attention.setFocused(true)));
|
|
12005
|
+
mainWindow.on("blur", () => applyAttention(attention.setFocused(false)));
|
|
11586
12006
|
mainWindow.on("closed", () => {
|
|
11587
12007
|
mainWindow = null;
|
|
11588
12008
|
});
|
|
@@ -11793,6 +12213,7 @@ electron.ipcMain.handle("speak:set", (_event, enabled) => {
|
|
|
11793
12213
|
orchestrator.setSpeakReplies(!!enabled);
|
|
11794
12214
|
return { enabled: orchestrator.speakRepliesEnabled };
|
|
11795
12215
|
});
|
|
12216
|
+
electron.ipcMain.handle("agent:getLastReply", () => orchestrator.getLastReply());
|
|
11796
12217
|
electron.ipcMain.handle("tracking:get", () => ({ active: orchestrator.isActiveTabTracking() }));
|
|
11797
12218
|
electron.ipcMain.handle("tracking:set", async (_event, enabled) => {
|
|
11798
12219
|
await orchestrator.setActiveTabTracking(enabled);
|
package/out/preload/index.js
CHANGED
|
@@ -69,6 +69,15 @@ 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
|
+
},
|
|
79
|
+
// Latest assistant reply from the active tab's transcript (null if none).
|
|
80
|
+
getLastReply: () => electron.ipcRenderer.invoke("agent:getLastReply"),
|
|
72
81
|
// Filesystem
|
|
73
82
|
readDir: (dirPath) => electron.ipcRenderer.invoke("fs:readDir", dirPath),
|
|
74
83
|
readFile: (filePath) => electron.ipcRenderer.invoke("fs:readFile", filePath),
|
|
@@ -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-Dg1vMspf.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-Dmx0L0Bw.js";
|
|
3
|
+
import { h, i, j, t, k } from "./lspLanguageFeatures-Dmx0L0Bw.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-Dg1vMspf.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-Dmx0L0Bw.js";
|
|
3
|
+
import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-Dmx0L0Bw.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|