@phenx-inc/ctlsurf 0.7.0 → 0.9.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 (38) hide show
  1. package/out/headless/index.mjs +169 -10
  2. package/out/headless/index.mjs.map +2 -2
  3. package/out/main/index.js +175 -9
  4. package/out/preload/index.js +10 -0
  5. package/out/renderer/assets/{cssMode-eTXVdAkZ.js → cssMode-BdYp0fp9.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-B5BKaiK4.js → freemarker2-Cya-9zr-.js} +1 -1
  7. package/out/renderer/assets/{handlebars-BIdLd2wU.js → handlebars-EJs9W5Ub.js} +1 -1
  8. package/out/renderer/assets/{html-BXL4cnLS.js → html-Ex4qy2lh.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-46N3XG2c.js → htmlMode-CCm0aJ6C.js} +3 -3
  10. package/out/renderer/assets/{index-dRvutfbl.js → index-Cj7K9-Ba.js} +823 -137
  11. package/out/renderer/assets/{index-Cf-RsxoC.css → index-nvsNYP5c.css} +229 -0
  12. package/out/renderer/assets/{javascript-n_iZZzDX.js → javascript-B2niCouI.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-DXDczSNu.js → jsonMode-BgCUHaFo.js} +3 -3
  14. package/out/renderer/assets/{liquid-B1QweUh7.js → liquid-DOQmSo4G.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-DqzMqkRk.js → lspLanguageFeatures-8SIcRAMh.js} +1 -1
  16. package/out/renderer/assets/{mdx-BCv8lm5e.js → mdx-DuKdGNKE.js} +1 -1
  17. package/out/renderer/assets/{python-BLNzYwDv.js → python-BUa4XFG-.js} +1 -1
  18. package/out/renderer/assets/{razor-CvAww8bG.js → razor-BUw8jho0.js} +1 -1
  19. package/out/renderer/assets/{transformers.web-DtSCnG36.js → transformers.web-_PuZoIho.js} +824 -19
  20. package/out/renderer/assets/{tsMode-C7m6Kr5E.js → tsMode-8FLRnobw.js} +1 -1
  21. package/out/renderer/assets/{typescript-DhPw4VVg.js → typescript-rdwAjCi4.js} +1 -1
  22. package/out/renderer/assets/{xml-B0WLFJ2U.js → xml-HOzT59Qg.js} +1 -1
  23. package/out/renderer/assets/{yaml-BWyn9Wd7.js → yaml-B81JfLsW.js} +1 -1
  24. package/out/renderer/index.html +64 -2
  25. package/out/repro-toggle.mjs +305 -0
  26. package/package.json +1 -1
  27. package/src/main/index.ts +9 -0
  28. package/src/main/orchestrator.ts +51 -10
  29. package/src/main/transcripts.ts +134 -0
  30. package/src/preload/index.ts +14 -0
  31. package/src/renderer/App.tsx +30 -11
  32. package/src/renderer/components/FloatingMic.tsx +48 -0
  33. package/src/renderer/components/SpeakControls.tsx +235 -0
  34. package/src/renderer/components/VoiceInput.tsx +183 -140
  35. package/src/renderer/index.html +62 -0
  36. package/src/renderer/lib/localWhisper.ts +94 -6
  37. package/src/renderer/lib/speech.ts +317 -0
  38. package/src/renderer/styles.css +229 -0
@@ -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.7.0",
5474
+ version: "0.9.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,138 @@ 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
+ }
6340
6472
 
6341
6473
  // src/main/workerWs.ts
6342
6474
  import os2 from "os";
@@ -7179,31 +7311,58 @@ var Orchestrator = class {
7179
7311
  get logChatEnabled() {
7180
7312
  return !!this.settings.logChat;
7181
7313
  }
7314
+ get speakRepliesEnabled() {
7315
+ return !!this.settings.speakReplies;
7316
+ }
7317
+ // Latest assistant reply from a tab's transcript, for on-demand "read aloud".
7318
+ // Defaults to the active tab. Returns null when the tab's agent has no native
7319
+ // transcript (only Claude Code / Codex do) or nothing has been said yet. Reads
7320
+ // the transcript directly, so it works regardless of the speakReplies setting.
7321
+ getLastReply(tabId) {
7322
+ const id = tabId || this.activeTabId;
7323
+ if (!id) return null;
7324
+ const tab = this.tabs.get(id);
7325
+ if (!tab || !supportsTranscriptLogging(tab.agent.id)) return null;
7326
+ return readLatestAssistantReply({ agentId: tab.agent.id, cwd: tab.cwd });
7327
+ }
7182
7328
  setLogChat(enabled) {
7183
7329
  this.settings.logChat = enabled;
7184
7330
  this.saveSettings();
7185
7331
  this.bridge.setLoggingEnabled(enabled);
7186
- if (!enabled) {
7187
- this.stopChatLogging();
7188
- } else if (this.activeTabId) {
7189
- const tab = this.tabs.get(this.activeTabId);
7190
- if (tab) this.startChatLogging(tab.agent, tab.cwd);
7191
- }
7332
+ this.restartActiveChatLogging();
7333
+ }
7334
+ setSpeakReplies(enabled) {
7335
+ this.settings.speakReplies = enabled;
7336
+ this.saveSettings();
7337
+ this.restartActiveChatLogging();
7338
+ }
7339
+ // Re-evaluate the transcript tailer for the active tab against the current
7340
+ // logChat / speakReplies flags. Both setters funnel through here.
7341
+ restartActiveChatLogging() {
7342
+ this.stopChatLogging();
7343
+ if (!this.activeTabId) return;
7344
+ const tab = this.tabs.get(this.activeTabId);
7345
+ if (tab) this.startChatLogging(tab.agent, tab.cwd);
7192
7346
  }
7193
7347
  // Agents with native session transcripts (Claude Code, Codex) get exact
7194
7348
  // chat text tailed from their JSONL logs; everything else falls back to
7195
7349
  // the terminal screen-scraper bridge.
7196
7350
  startChatLogging(agent, cwd) {
7197
7351
  this.stopChatLogging();
7198
- if (!this.settings.logChat) return;
7352
+ if (!this.settings.logChat && !this.settings.speakReplies) return;
7199
7353
  if (supportsTranscriptLogging(agent.id)) {
7200
7354
  this.transcriptTailer = new TranscriptTailer({
7201
7355
  agentId: agent.id,
7202
7356
  cwd,
7203
- sink: (entry) => this.workerWs.sendChatLog(entry)
7357
+ sink: (entry) => {
7358
+ if (this.settings.logChat) this.workerWs.sendChatLog(entry);
7359
+ if (this.settings.speakReplies && entry.type === "terminal_output") {
7360
+ this.events.onAgentMessage?.(entry.content);
7361
+ }
7362
+ }
7204
7363
  });
7205
7364
  this.transcriptTailer.start();
7206
- } else {
7365
+ } else if (this.settings.logChat) {
7207
7366
  this.bridge.startSession();
7208
7367
  }
7209
7368
  }