@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/headless/index.mjs
CHANGED
|
@@ -5471,7 +5471,7 @@ var require_package = __commonJS({
|
|
|
5471
5471
|
"package.json"(exports, module) {
|
|
5472
5472
|
module.exports = {
|
|
5473
5473
|
name: "@phenx-inc/ctlsurf",
|
|
5474
|
-
version: "0.
|
|
5474
|
+
version: "0.10.0",
|
|
5475
5475
|
description: "Agent-agnostic terminal and desktop app for ctlsurf \u2014 run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5476
5476
|
main: "out/main/index.js",
|
|
5477
5477
|
bin: {
|
|
@@ -6337,6 +6337,320 @@ function capLength(str) {
|
|
|
6337
6337
|
if (str.length <= MAX_ENTRY_CHARS) return str;
|
|
6338
6338
|
return str.slice(0, MAX_ENTRY_CHARS) + `\u2026 [truncated, ${str.length} total chars]`;
|
|
6339
6339
|
}
|
|
6340
|
+
function readLatestAssistantReply(opts) {
|
|
6341
|
+
try {
|
|
6342
|
+
const cwd = stripTrailingSep(opts.cwd);
|
|
6343
|
+
if (opts.agentId === "claude") {
|
|
6344
|
+
const file = mostRecentClaudeFile(cwd, opts.claudeProjectsDir);
|
|
6345
|
+
if (!file) return null;
|
|
6346
|
+
const text = lastClaudeAssistant(file, cwd);
|
|
6347
|
+
return text ? capLength(text) : null;
|
|
6348
|
+
}
|
|
6349
|
+
const codexRoot = opts.codexSessionsDir || path.join(os.homedir(), ".codex", "sessions");
|
|
6350
|
+
for (const file of recentCodexRollouts(codexRoot)) {
|
|
6351
|
+
const text = lastCodexAgentMessage(file, cwd);
|
|
6352
|
+
if (text) return capLength(text);
|
|
6353
|
+
}
|
|
6354
|
+
return null;
|
|
6355
|
+
} catch {
|
|
6356
|
+
return null;
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
function readLines(file) {
|
|
6360
|
+
try {
|
|
6361
|
+
return fs.readFileSync(file, "utf-8").split("\n");
|
|
6362
|
+
} catch {
|
|
6363
|
+
return [];
|
|
6364
|
+
}
|
|
6365
|
+
}
|
|
6366
|
+
function mostRecentClaudeFile(cwd, root) {
|
|
6367
|
+
const projectsRoot = root || path.join(os.homedir(), ".claude", "projects");
|
|
6368
|
+
const slug = cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
6369
|
+
const dir = path.join(projectsRoot, slug);
|
|
6370
|
+
let names;
|
|
6371
|
+
try {
|
|
6372
|
+
names = fs.readdirSync(dir);
|
|
6373
|
+
} catch {
|
|
6374
|
+
return null;
|
|
6375
|
+
}
|
|
6376
|
+
let best = null;
|
|
6377
|
+
let bestMs = -Infinity;
|
|
6378
|
+
for (const name of names) {
|
|
6379
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
6380
|
+
const fp = path.join(dir, name);
|
|
6381
|
+
try {
|
|
6382
|
+
const ms = fs.statSync(fp).mtimeMs;
|
|
6383
|
+
if (ms > bestMs) {
|
|
6384
|
+
bestMs = ms;
|
|
6385
|
+
best = fp;
|
|
6386
|
+
}
|
|
6387
|
+
} catch {
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6390
|
+
return best;
|
|
6391
|
+
}
|
|
6392
|
+
function recentCodexRollouts(root) {
|
|
6393
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
6394
|
+
const now = Date.now();
|
|
6395
|
+
for (const ms of [now, now - 24 * 60 * 60 * 1e3]) {
|
|
6396
|
+
const d = new Date(ms);
|
|
6397
|
+
const yyyy = String(d.getFullYear());
|
|
6398
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
6399
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
6400
|
+
dirs.add(path.join(root, yyyy, mm, dd));
|
|
6401
|
+
}
|
|
6402
|
+
const files = [];
|
|
6403
|
+
for (const dir of dirs) {
|
|
6404
|
+
let names;
|
|
6405
|
+
try {
|
|
6406
|
+
names = fs.readdirSync(dir);
|
|
6407
|
+
} catch {
|
|
6408
|
+
continue;
|
|
6409
|
+
}
|
|
6410
|
+
for (const name of names) {
|
|
6411
|
+
if (!name.startsWith("rollout-") || !name.endsWith(".jsonl")) continue;
|
|
6412
|
+
const fp = path.join(dir, name);
|
|
6413
|
+
try {
|
|
6414
|
+
files.push({ fp, ms: fs.statSync(fp).mtimeMs });
|
|
6415
|
+
} catch {
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
return files.sort((a, b) => b.ms - a.ms).map((f) => f.fp);
|
|
6420
|
+
}
|
|
6421
|
+
function lastClaudeAssistant(file, cwd) {
|
|
6422
|
+
const lines = readLines(file);
|
|
6423
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6424
|
+
const trimmed = lines[i].trim();
|
|
6425
|
+
if (!trimmed) continue;
|
|
6426
|
+
let obj;
|
|
6427
|
+
try {
|
|
6428
|
+
obj = JSON.parse(trimmed);
|
|
6429
|
+
} catch {
|
|
6430
|
+
continue;
|
|
6431
|
+
}
|
|
6432
|
+
if (!obj || typeof obj !== "object") continue;
|
|
6433
|
+
if (obj.isMeta || obj.isSidechain || obj.type !== "assistant") continue;
|
|
6434
|
+
if (typeof obj.cwd === "string" && stripTrailingSep(obj.cwd) !== cwd) continue;
|
|
6435
|
+
const text = extractClaudeText(obj.message?.content, false);
|
|
6436
|
+
if (text) return text;
|
|
6437
|
+
}
|
|
6438
|
+
return null;
|
|
6439
|
+
}
|
|
6440
|
+
function lastCodexAgentMessage(file, cwd) {
|
|
6441
|
+
const lines = readLines(file);
|
|
6442
|
+
for (const line of lines) {
|
|
6443
|
+
const trimmed = line.trim();
|
|
6444
|
+
if (!trimmed) continue;
|
|
6445
|
+
let obj;
|
|
6446
|
+
try {
|
|
6447
|
+
obj = JSON.parse(trimmed);
|
|
6448
|
+
} catch {
|
|
6449
|
+
continue;
|
|
6450
|
+
}
|
|
6451
|
+
if (obj?.type === "session_meta") {
|
|
6452
|
+
const metaCwd = obj.payload?.cwd;
|
|
6453
|
+
if (typeof metaCwd === "string" && stripTrailingSep(metaCwd) !== cwd) return null;
|
|
6454
|
+
break;
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
6458
|
+
const trimmed = lines[i].trim();
|
|
6459
|
+
if (!trimmed) continue;
|
|
6460
|
+
let obj;
|
|
6461
|
+
try {
|
|
6462
|
+
obj = JSON.parse(trimmed);
|
|
6463
|
+
} catch {
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
if (obj?.type !== "event_msg" || obj.payload?.type !== "agent_message") continue;
|
|
6467
|
+
const text = typeof obj.payload.message === "string" ? obj.payload.message.trim() : "";
|
|
6468
|
+
if (text && !isCodexNoise(text)) return text;
|
|
6469
|
+
}
|
|
6470
|
+
return null;
|
|
6471
|
+
}
|
|
6472
|
+
|
|
6473
|
+
// src/main/agentState.ts
|
|
6474
|
+
var BUSY_GRACE_MS = 2500;
|
|
6475
|
+
var SETTLE_MS = 1200;
|
|
6476
|
+
var DECISION_TTL_MS = 4e3;
|
|
6477
|
+
var TICK_MS = 400;
|
|
6478
|
+
var HOOK_AUTHORITY_MS = 6e3;
|
|
6479
|
+
var TAIL_KEEP_CHARS = 240;
|
|
6480
|
+
var CLAUDE_SPINNER = /[✶✻✳✺✢✷✸✹❋][^\n]{0,60}(?:…|\.\.\.)/;
|
|
6481
|
+
function hasBusyMarker(text) {
|
|
6482
|
+
return /esc to interrupt/i.test(text) || /\bWorking \(\d+s/i.test(text) || CLAUDE_SPINNER.test(text);
|
|
6483
|
+
}
|
|
6484
|
+
var APPROVAL_PATTERNS = [
|
|
6485
|
+
/\bDo you want to (proceed|continue|create|make this edit|run)\b/i,
|
|
6486
|
+
// Claude Code
|
|
6487
|
+
/❯\s*\d+\.\s*Yes\b/i,
|
|
6488
|
+
// Claude numbered-choice prompt
|
|
6489
|
+
/\bAllow command\b/i,
|
|
6490
|
+
// Codex
|
|
6491
|
+
/\bRun this command\?/i,
|
|
6492
|
+
// Codex
|
|
6493
|
+
/\bApprove\b[^\n]*\?/i,
|
|
6494
|
+
/\(y\/n\)/i,
|
|
6495
|
+
/\[y\/N\]/i
|
|
6496
|
+
];
|
|
6497
|
+
function hasApprovalMarker(text) {
|
|
6498
|
+
return APPROVAL_PATTERNS.some((re) => re.test(text));
|
|
6499
|
+
}
|
|
6500
|
+
function stripAnsi2(str) {
|
|
6501
|
+
return str.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b/g, "");
|
|
6502
|
+
}
|
|
6503
|
+
var AgentStateMonitor = class {
|
|
6504
|
+
sink;
|
|
6505
|
+
now;
|
|
6506
|
+
tabs = /* @__PURE__ */ new Map();
|
|
6507
|
+
timer = null;
|
|
6508
|
+
lastAnyAwaiting = false;
|
|
6509
|
+
constructor(sink, now = Date.now) {
|
|
6510
|
+
this.sink = sink;
|
|
6511
|
+
this.now = now;
|
|
6512
|
+
}
|
|
6513
|
+
start() {
|
|
6514
|
+
if (this.timer) return;
|
|
6515
|
+
this.timer = setInterval(() => this.tick(), TICK_MS);
|
|
6516
|
+
}
|
|
6517
|
+
stop() {
|
|
6518
|
+
if (this.timer) {
|
|
6519
|
+
clearInterval(this.timer);
|
|
6520
|
+
this.timer = null;
|
|
6521
|
+
}
|
|
6522
|
+
this.tabs.clear();
|
|
6523
|
+
this.lastAnyAwaiting = false;
|
|
6524
|
+
}
|
|
6525
|
+
/** Register a tab so its raw PTY output can be classified. */
|
|
6526
|
+
trackTab(tabId, agentId, cwd) {
|
|
6527
|
+
const now = this.now();
|
|
6528
|
+
this.tabs.set(tabId, {
|
|
6529
|
+
agentId,
|
|
6530
|
+
cwd,
|
|
6531
|
+
state: "idle",
|
|
6532
|
+
everBusy: false,
|
|
6533
|
+
lastBusyAt: 0,
|
|
6534
|
+
lastOutputAt: now,
|
|
6535
|
+
lastDecisionAt: 0,
|
|
6536
|
+
hookState: null,
|
|
6537
|
+
hookAt: 0,
|
|
6538
|
+
tail: ""
|
|
6539
|
+
});
|
|
6540
|
+
}
|
|
6541
|
+
removeTab(tabId) {
|
|
6542
|
+
if (this.tabs.delete(tabId)) this.emitAggregate();
|
|
6543
|
+
}
|
|
6544
|
+
/** Feed a raw PTY output chunk for a tab. */
|
|
6545
|
+
feed(tabId, data) {
|
|
6546
|
+
const rec = this.tabs.get(tabId);
|
|
6547
|
+
if (!rec) return;
|
|
6548
|
+
const now = this.now();
|
|
6549
|
+
rec.lastOutputAt = now;
|
|
6550
|
+
const text = rec.tail + stripAnsi2(data);
|
|
6551
|
+
rec.tail = text.length > TAIL_KEEP_CHARS ? text.slice(-TAIL_KEEP_CHARS) : text;
|
|
6552
|
+
if (hasBusyMarker(text)) {
|
|
6553
|
+
rec.lastBusyAt = now;
|
|
6554
|
+
rec.everBusy = true;
|
|
6555
|
+
}
|
|
6556
|
+
if (hasApprovalMarker(text)) {
|
|
6557
|
+
rec.lastDecisionAt = now;
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
/**
|
|
6561
|
+
* Note input written to a tab's PTY. Only a *submit* (data containing a
|
|
6562
|
+
* newline) counts as the user engaging — clearing any awaiting state and
|
|
6563
|
+
* resetting the "has worked" flag so a leftover idle box doesn't re-flag.
|
|
6564
|
+
*
|
|
6565
|
+
* Individual keystrokes and, crucially, the terminal's own auto-responses
|
|
6566
|
+
* (cursor-position / device-attribute replies that TUIs constantly trigger,
|
|
6567
|
+
* which xterm.js writes straight back to the PTY) are ignored — otherwise
|
|
6568
|
+
* the state would churn busy↔idle on every control-sequence handshake.
|
|
6569
|
+
*/
|
|
6570
|
+
noteInput(tabId, data) {
|
|
6571
|
+
if (!/[\r\n]/.test(data)) return;
|
|
6572
|
+
const rec = this.tabs.get(tabId);
|
|
6573
|
+
if (!rec) return;
|
|
6574
|
+
const now = this.now();
|
|
6575
|
+
rec.everBusy = false;
|
|
6576
|
+
rec.lastBusyAt = now;
|
|
6577
|
+
rec.lastDecisionAt = 0;
|
|
6578
|
+
rec.hookState = null;
|
|
6579
|
+
this.setState(rec, tabId, "busy");
|
|
6580
|
+
}
|
|
6581
|
+
/**
|
|
6582
|
+
* Apply an authoritative state from an agent hook, addressed by cwd. Overrides
|
|
6583
|
+
* the heuristic for HOOK_AUTHORITY_MS. `kind`:
|
|
6584
|
+
* 'awaiting_input' — turn done / idle prompt (Stop, idle_prompt, agent-turn-complete)
|
|
6585
|
+
* 'awaiting_decision' — permission/approval prompt (PermissionRequest, permission_prompt)
|
|
6586
|
+
* 'busy' — user submitted a prompt (UserPromptSubmit)
|
|
6587
|
+
*/
|
|
6588
|
+
applyHookEvent(cwd, kind) {
|
|
6589
|
+
const target = stripTrailingSep2(cwd);
|
|
6590
|
+
const now = this.now();
|
|
6591
|
+
for (const [tabId, rec] of this.tabs) {
|
|
6592
|
+
if (stripTrailingSep2(rec.cwd) !== target) continue;
|
|
6593
|
+
rec.hookState = kind;
|
|
6594
|
+
rec.hookAt = now;
|
|
6595
|
+
if (kind === "busy") {
|
|
6596
|
+
rec.everBusy = false;
|
|
6597
|
+
rec.lastBusyAt = now;
|
|
6598
|
+
}
|
|
6599
|
+
this.setState(rec, tabId, kind);
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
tick() {
|
|
6603
|
+
const now = this.now();
|
|
6604
|
+
for (const [tabId, rec] of this.tabs) {
|
|
6605
|
+
if (rec.hookState && now - rec.hookAt < HOOK_AUTHORITY_MS) {
|
|
6606
|
+
this.setState(rec, tabId, rec.hookState);
|
|
6607
|
+
continue;
|
|
6608
|
+
}
|
|
6609
|
+
const busyRecently = rec.lastBusyAt > 0 && now - rec.lastBusyAt < BUSY_GRACE_MS;
|
|
6610
|
+
const settled = now - rec.lastOutputAt >= SETTLE_MS;
|
|
6611
|
+
const decisionRecent = rec.lastDecisionAt > 0 && now - rec.lastDecisionAt < DECISION_TTL_MS;
|
|
6612
|
+
let next = null;
|
|
6613
|
+
if (busyRecently) {
|
|
6614
|
+
next = "busy";
|
|
6615
|
+
} else if (settled && rec.everBusy) {
|
|
6616
|
+
next = rec.state === "awaiting_decision" || decisionRecent ? "awaiting_decision" : "awaiting_input";
|
|
6617
|
+
} else if (settled && !rec.everBusy) {
|
|
6618
|
+
next = "idle";
|
|
6619
|
+
}
|
|
6620
|
+
if (next) this.setState(rec, tabId, next);
|
|
6621
|
+
}
|
|
6622
|
+
this.emitAggregate();
|
|
6623
|
+
}
|
|
6624
|
+
setState(rec, tabId, state) {
|
|
6625
|
+
if (rec.state === state) return;
|
|
6626
|
+
rec.state = state;
|
|
6627
|
+
this.sink({ tabId, state, anyAwaiting: this.computeAnyAwaiting() });
|
|
6628
|
+
this.lastAnyAwaiting = this.computeAnyAwaiting();
|
|
6629
|
+
}
|
|
6630
|
+
emitAggregate() {
|
|
6631
|
+
const anyAwaiting = this.computeAnyAwaiting();
|
|
6632
|
+
if (anyAwaiting === this.lastAnyAwaiting) return;
|
|
6633
|
+
this.lastAnyAwaiting = anyAwaiting;
|
|
6634
|
+
const tabId = this.firstAwaitingTabId() ?? "";
|
|
6635
|
+
const state = tabId ? this.tabs.get(tabId).state : "idle";
|
|
6636
|
+
this.sink({ tabId, state, anyAwaiting });
|
|
6637
|
+
}
|
|
6638
|
+
computeAnyAwaiting() {
|
|
6639
|
+
for (const rec of this.tabs.values()) {
|
|
6640
|
+
if (rec.state === "awaiting_input" || rec.state === "awaiting_decision") return true;
|
|
6641
|
+
}
|
|
6642
|
+
return false;
|
|
6643
|
+
}
|
|
6644
|
+
firstAwaitingTabId() {
|
|
6645
|
+
for (const [tabId, rec] of this.tabs) {
|
|
6646
|
+
if (rec.state === "awaiting_input" || rec.state === "awaiting_decision") return tabId;
|
|
6647
|
+
}
|
|
6648
|
+
return null;
|
|
6649
|
+
}
|
|
6650
|
+
};
|
|
6651
|
+
function stripTrailingSep2(p) {
|
|
6652
|
+
return p.length > 1 ? p.replace(/[/\\]+$/, "") : p;
|
|
6653
|
+
}
|
|
6340
6654
|
|
|
6341
6655
|
// src/main/workerWs.ts
|
|
6342
6656
|
import os2 from "os";
|
|
@@ -7133,9 +7447,14 @@ var Orchestrator = class {
|
|
|
7133
7447
|
noProjectPollCwd = null;
|
|
7134
7448
|
currentProjectName = null;
|
|
7135
7449
|
transcriptTailer = null;
|
|
7450
|
+
stateMonitor;
|
|
7136
7451
|
constructor(settingsDir, events) {
|
|
7137
7452
|
this.settingsDir = settingsDir;
|
|
7138
7453
|
this.events = events;
|
|
7454
|
+
this.stateMonitor = new AgentStateMonitor(({ tabId, state, anyAwaiting }) => {
|
|
7455
|
+
this.events.onAgentStateChange?.(tabId, state, anyAwaiting);
|
|
7456
|
+
});
|
|
7457
|
+
this.stateMonitor.start();
|
|
7139
7458
|
this.workerWs = new WorkerWsClient({
|
|
7140
7459
|
onStatusChange: (status) => {
|
|
7141
7460
|
log(`[worker-ws] Status: ${status}`);
|
|
@@ -7147,9 +7466,10 @@ var Orchestrator = class {
|
|
|
7147
7466
|
this.workerWs.sendAck(message.id);
|
|
7148
7467
|
if (message.type === "prompt" || message.type === "task_dispatch") {
|
|
7149
7468
|
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
|
|
7150
|
-
if (activeTab) {
|
|
7469
|
+
if (activeTab && this.activeTabId) {
|
|
7151
7470
|
activeTab.ptyManager.write(message.content + "\r");
|
|
7152
7471
|
this.bridge.feedInput(message.content + "\r");
|
|
7472
|
+
this.stateMonitor.noteInput(this.activeTabId, message.content + "\r");
|
|
7153
7473
|
}
|
|
7154
7474
|
}
|
|
7155
7475
|
},
|
|
@@ -7182,6 +7502,17 @@ var Orchestrator = class {
|
|
|
7182
7502
|
get speakRepliesEnabled() {
|
|
7183
7503
|
return !!this.settings.speakReplies;
|
|
7184
7504
|
}
|
|
7505
|
+
// Latest assistant reply from a tab's transcript, for on-demand "read aloud".
|
|
7506
|
+
// Defaults to the active tab. Returns null when the tab's agent has no native
|
|
7507
|
+
// transcript (only Claude Code / Codex do) or nothing has been said yet. Reads
|
|
7508
|
+
// the transcript directly, so it works regardless of the speakReplies setting.
|
|
7509
|
+
getLastReply(tabId) {
|
|
7510
|
+
const id = tabId || this.activeTabId;
|
|
7511
|
+
if (!id) return null;
|
|
7512
|
+
const tab = this.tabs.get(id);
|
|
7513
|
+
if (!tab || !supportsTranscriptLogging(tab.agent.id)) return null;
|
|
7514
|
+
return readLatestAssistantReply({ agentId: tab.agent.id, cwd: tab.cwd });
|
|
7515
|
+
}
|
|
7185
7516
|
setLogChat(enabled) {
|
|
7186
7517
|
this.settings.logChat = enabled;
|
|
7187
7518
|
this.saveSettings();
|
|
@@ -7429,9 +7760,11 @@ var Orchestrator = class {
|
|
|
7429
7760
|
const ptyManager = new PtyManager(agent, cwd);
|
|
7430
7761
|
const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
|
|
7431
7762
|
this.tabs.set(tabId, tab);
|
|
7763
|
+
this.stateMonitor.trackTab(tabId, agent.id, cwd);
|
|
7432
7764
|
ptyManager.onData((data) => {
|
|
7433
7765
|
this.events.onPtyData(tabId, data);
|
|
7434
7766
|
this.timeTracker.recordActivity(tabId);
|
|
7767
|
+
this.stateMonitor.feed(tabId, data);
|
|
7435
7768
|
if (tabId === this.activeTabId) {
|
|
7436
7769
|
this.bridge.feedOutput(data);
|
|
7437
7770
|
this.streamTerminalData(tabId, data);
|
|
@@ -7448,6 +7781,7 @@ var Orchestrator = class {
|
|
|
7448
7781
|
}
|
|
7449
7782
|
const t = this.tabs.get(tabId);
|
|
7450
7783
|
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
|
|
7784
|
+
this.stateMonitor.removeTab(tabId);
|
|
7451
7785
|
});
|
|
7452
7786
|
this.startChatLogging(agent, cwd);
|
|
7453
7787
|
const profile = this.getActiveProfile();
|
|
@@ -7470,6 +7804,7 @@ var Orchestrator = class {
|
|
|
7470
7804
|
}
|
|
7471
7805
|
writePty(tabId, data) {
|
|
7472
7806
|
this.tabs.get(tabId)?.ptyManager.write(data);
|
|
7807
|
+
this.stateMonitor.noteInput(tabId, data);
|
|
7473
7808
|
if (tabId === this.activeTabId) {
|
|
7474
7809
|
this.bridge.feedInput(data);
|
|
7475
7810
|
}
|
|
@@ -7488,6 +7823,7 @@ var Orchestrator = class {
|
|
|
7488
7823
|
await this.timeTracker.endSession(tabId);
|
|
7489
7824
|
tab.ptyManager.kill();
|
|
7490
7825
|
this.tabs.delete(tabId);
|
|
7826
|
+
this.stateMonitor.removeTab(tabId);
|
|
7491
7827
|
if (tabId === this.activeTabId) {
|
|
7492
7828
|
this.stopChatLogging();
|
|
7493
7829
|
if (isCodingAgent(tab.agent)) {
|
|
@@ -7641,6 +7977,7 @@ var Orchestrator = class {
|
|
|
7641
7977
|
async shutdown() {
|
|
7642
7978
|
this.stopNoProjectPolling();
|
|
7643
7979
|
this.stopChatLogging();
|
|
7980
|
+
this.stateMonitor.stop();
|
|
7644
7981
|
await this.timeTracker.endAll();
|
|
7645
7982
|
for (const [, tab] of this.tabs) {
|
|
7646
7983
|
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
|