@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
package/out/main/index.js CHANGED
@@ -6282,6 +6282,138 @@ 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(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(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(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
+ }
6285
6417
  var bufferUtil = { exports: {} };
6286
6418
  var constants;
6287
6419
  var hasRequiredConstants;
@@ -11008,31 +11140,58 @@ class Orchestrator {
11008
11140
  get logChatEnabled() {
11009
11141
  return !!this.settings.logChat;
11010
11142
  }
11143
+ get speakRepliesEnabled() {
11144
+ return !!this.settings.speakReplies;
11145
+ }
11146
+ // Latest assistant reply from a tab's transcript, for on-demand "read aloud".
11147
+ // Defaults to the active tab. Returns null when the tab's agent has no native
11148
+ // transcript (only Claude Code / Codex do) or nothing has been said yet. Reads
11149
+ // the transcript directly, so it works regardless of the speakReplies setting.
11150
+ getLastReply(tabId) {
11151
+ const id = tabId || this.activeTabId;
11152
+ if (!id) return null;
11153
+ const tab = this.tabs.get(id);
11154
+ if (!tab || !supportsTranscriptLogging(tab.agent.id)) return null;
11155
+ return readLatestAssistantReply({ agentId: tab.agent.id, cwd: tab.cwd });
11156
+ }
11011
11157
  setLogChat(enabled) {
11012
11158
  this.settings.logChat = enabled;
11013
11159
  this.saveSettings();
11014
11160
  this.bridge.setLoggingEnabled(enabled);
11015
- if (!enabled) {
11016
- this.stopChatLogging();
11017
- } else if (this.activeTabId) {
11018
- const tab = this.tabs.get(this.activeTabId);
11019
- if (tab) this.startChatLogging(tab.agent, tab.cwd);
11020
- }
11161
+ this.restartActiveChatLogging();
11162
+ }
11163
+ setSpeakReplies(enabled) {
11164
+ this.settings.speakReplies = enabled;
11165
+ this.saveSettings();
11166
+ this.restartActiveChatLogging();
11167
+ }
11168
+ // Re-evaluate the transcript tailer for the active tab against the current
11169
+ // logChat / speakReplies flags. Both setters funnel through here.
11170
+ restartActiveChatLogging() {
11171
+ this.stopChatLogging();
11172
+ if (!this.activeTabId) return;
11173
+ const tab = this.tabs.get(this.activeTabId);
11174
+ if (tab) this.startChatLogging(tab.agent, tab.cwd);
11021
11175
  }
11022
11176
  // Agents with native session transcripts (Claude Code, Codex) get exact
11023
11177
  // chat text tailed from their JSONL logs; everything else falls back to
11024
11178
  // the terminal screen-scraper bridge.
11025
11179
  startChatLogging(agent, cwd) {
11026
11180
  this.stopChatLogging();
11027
- if (!this.settings.logChat) return;
11181
+ if (!this.settings.logChat && !this.settings.speakReplies) return;
11028
11182
  if (supportsTranscriptLogging(agent.id)) {
11029
11183
  this.transcriptTailer = new TranscriptTailer({
11030
11184
  agentId: agent.id,
11031
11185
  cwd,
11032
- sink: (entry) => this.workerWs.sendChatLog(entry)
11186
+ sink: (entry) => {
11187
+ if (this.settings.logChat) this.workerWs.sendChatLog(entry);
11188
+ if (this.settings.speakReplies && entry.type === "terminal_output") {
11189
+ this.events.onAgentMessage?.(entry.content);
11190
+ }
11191
+ }
11033
11192
  });
11034
11193
  this.transcriptTailer.start();
11035
- } else {
11194
+ } else if (this.settings.logChat) {
11036
11195
  this.bridge.startSession();
11037
11196
  }
11038
11197
  }
@@ -11536,6 +11695,7 @@ const orchestrator = new Orchestrator(
11536
11695
  onWorkerMessage: (message) => mainWindow?.webContents.send("worker:message", message),
11537
11696
  onWorkerRegistered: (data) => mainWindow?.webContents.send("worker:registered", data),
11538
11697
  onProjectChanged: (name) => mainWindow?.webContents.send("app:projectChanged", name),
11698
+ onAgentMessage: (text) => mainWindow?.webContents.send("agent:message", text),
11539
11699
  onCwdChanged: () => {
11540
11700
  mainWindow?.webContents.send("app:cwdChanged");
11541
11701
  updateProjectBadge(orchestrator.cwd);
@@ -11771,6 +11931,12 @@ electron.ipcMain.handle("logchat:set", (_event, enabled) => {
11771
11931
  orchestrator.setLogChat(!!enabled);
11772
11932
  return { enabled: orchestrator.logChatEnabled };
11773
11933
  });
11934
+ electron.ipcMain.handle("speak:get", () => ({ enabled: orchestrator.speakRepliesEnabled }));
11935
+ electron.ipcMain.handle("speak:set", (_event, enabled) => {
11936
+ orchestrator.setSpeakReplies(!!enabled);
11937
+ return { enabled: orchestrator.speakRepliesEnabled };
11938
+ });
11939
+ electron.ipcMain.handle("agent:getLastReply", () => orchestrator.getLastReply());
11774
11940
  electron.ipcMain.handle("tracking:get", () => ({ active: orchestrator.isActiveTabTracking() }));
11775
11941
  electron.ipcMain.handle("tracking:set", async (_event, enabled) => {
11776
11942
  await orchestrator.setActiveTabTracking(enabled);
@@ -61,6 +61,16 @@ const api = {
61
61
  // Chat logging (global)
62
62
  getLogChat: () => electron.ipcRenderer.invoke("logchat:get"),
63
63
  setLogChat: (enabled) => electron.ipcRenderer.invoke("logchat:set", enabled),
64
+ // Speak agent replies (global, Electron-only)
65
+ getSpeakReplies: () => electron.ipcRenderer.invoke("speak:get"),
66
+ setSpeakReplies: (enabled) => electron.ipcRenderer.invoke("speak:set", enabled),
67
+ onAgentMessage: (callback) => {
68
+ const listener = (_event, text) => callback(text);
69
+ electron.ipcRenderer.on("agent:message", listener);
70
+ return () => electron.ipcRenderer.removeListener("agent:message", listener);
71
+ },
72
+ // Latest assistant reply from the active tab's transcript (null if none).
73
+ getLastReply: () => electron.ipcRenderer.invoke("agent:getLastReply"),
64
74
  // Filesystem
65
75
  readDir: (dirPath) => electron.ipcRenderer.invoke("fs:readDir", dirPath),
66
76
  readFile: (filePath) => electron.ipcRenderer.invoke("fs:readFile", filePath),
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-dRvutfbl.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-DqzMqkRk.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-DqzMqkRk.js";
1
+ import { c as createWebWorker, l as languages } from "./index-Cj7K9-Ba.js";
2
+ import { C as CompletionAdapter, H as HoverAdapter, D as DocumentHighlightAdapter, a as DefinitionAdapter, R as ReferenceAdapter, b as DocumentSymbolAdapter, c as RenameAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, e as DiagnosticsAdapter, S as SelectionRangeAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider } from "./lspLanguageFeatures-8SIcRAMh.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-8SIcRAMh.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-dRvutfbl.js";
1
+ import { l as languages } from "./index-Cj7K9-Ba.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-dRvutfbl.js";
1
+ import { l as languages } from "./index-Cj7K9-Ba.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-dRvutfbl.js";
1
+ import { l as languages } from "./index-Cj7K9-Ba.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-dRvutfbl.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-DqzMqkRk.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-DqzMqkRk.js";
1
+ import { c as createWebWorker, l as languages } from "./index-Cj7K9-Ba.js";
2
+ import { H as HoverAdapter, D as DocumentHighlightAdapter, h as DocumentLinkAdapter, F as FoldingRangeAdapter, b as DocumentSymbolAdapter, S as SelectionRangeAdapter, c as RenameAdapter, f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter } from "./lspLanguageFeatures-8SIcRAMh.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-8SIcRAMh.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {