@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.
- package/out/headless/index.mjs +169 -10
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +175 -9
- package/out/preload/index.js +10 -0
- package/out/renderer/assets/{cssMode-eTXVdAkZ.js → cssMode-BdYp0fp9.js} +3 -3
- package/out/renderer/assets/{freemarker2-B5BKaiK4.js → freemarker2-Cya-9zr-.js} +1 -1
- package/out/renderer/assets/{handlebars-BIdLd2wU.js → handlebars-EJs9W5Ub.js} +1 -1
- package/out/renderer/assets/{html-BXL4cnLS.js → html-Ex4qy2lh.js} +1 -1
- package/out/renderer/assets/{htmlMode-46N3XG2c.js → htmlMode-CCm0aJ6C.js} +3 -3
- package/out/renderer/assets/{index-dRvutfbl.js → index-Cj7K9-Ba.js} +823 -137
- package/out/renderer/assets/{index-Cf-RsxoC.css → index-nvsNYP5c.css} +229 -0
- package/out/renderer/assets/{javascript-n_iZZzDX.js → javascript-B2niCouI.js} +2 -2
- package/out/renderer/assets/{jsonMode-DXDczSNu.js → jsonMode-BgCUHaFo.js} +3 -3
- package/out/renderer/assets/{liquid-B1QweUh7.js → liquid-DOQmSo4G.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-DqzMqkRk.js → lspLanguageFeatures-8SIcRAMh.js} +1 -1
- package/out/renderer/assets/{mdx-BCv8lm5e.js → mdx-DuKdGNKE.js} +1 -1
- package/out/renderer/assets/{python-BLNzYwDv.js → python-BUa4XFG-.js} +1 -1
- package/out/renderer/assets/{razor-CvAww8bG.js → razor-BUw8jho0.js} +1 -1
- package/out/renderer/assets/{transformers.web-DtSCnG36.js → transformers.web-_PuZoIho.js} +824 -19
- package/out/renderer/assets/{tsMode-C7m6Kr5E.js → tsMode-8FLRnobw.js} +1 -1
- package/out/renderer/assets/{typescript-DhPw4VVg.js → typescript-rdwAjCi4.js} +1 -1
- package/out/renderer/assets/{xml-B0WLFJ2U.js → xml-HOzT59Qg.js} +1 -1
- package/out/renderer/assets/{yaml-BWyn9Wd7.js → yaml-B81JfLsW.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/index.ts +9 -0
- package/src/main/orchestrator.ts +51 -10
- package/src/main/transcripts.ts +134 -0
- package/src/preload/index.ts +14 -0
- package/src/renderer/App.tsx +30 -11
- package/src/renderer/components/FloatingMic.tsx +48 -0
- package/src/renderer/components/SpeakControls.tsx +235 -0
- package/src/renderer/components/VoiceInput.tsx +183 -140
- package/src/renderer/index.html +62 -0
- package/src/renderer/lib/localWhisper.ts +94 -6
- package/src/renderer/lib/speech.ts +317 -0
- package/src/renderer/styles.css +229 -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.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
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
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) =>
|
|
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
|
}
|