@phenx-inc/ctlsurf 0.3.16 → 0.5.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 (32) hide show
  1. package/out/headless/index.mjs +309 -38
  2. package/out/headless/index.mjs.map +4 -4
  3. package/out/main/index.js +277 -7
  4. package/out/preload/index.js +2 -0
  5. package/out/renderer/assets/{cssMode-D5dPwEy5.js → cssMode-DkmdBgO7.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-c5jJjQ9s.js → freemarker2-CI-gkP-3.js} +1 -1
  7. package/out/renderer/assets/{handlebars-BTbmOxx9.js → handlebars-D5tEqanR.js} +1 -1
  8. package/out/renderer/assets/{html-3cIIQcxO.js → html-fH93EYfn.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-DYbpW1yY.js → htmlMode-CRicxcwK.js} +3 -3
  10. package/out/renderer/assets/{index-D2MUZin7.js → index-BOOvUI7u.js} +192 -23
  11. package/out/renderer/assets/{index-6KvOnYL1.css → index-ezC-iarf.css} +40 -0
  12. package/out/renderer/assets/{javascript-CDuCMm-6.js → javascript-D1Baz4fV.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-COLqbq0s.js → jsonMode-Bquqf3QN.js} +3 -3
  14. package/out/renderer/assets/{liquid-BFcqZizB.js → liquid-ByOcPjBF.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-CbkEcL-z.js → lspLanguageFeatures-BxPLl0yy.js} +1 -1
  16. package/out/renderer/assets/{mdx-DyK93oEE.js → mdx-yuNgx0rM.js} +1 -1
  17. package/out/renderer/assets/{python-D4lCwSVr.js → python-2OakgLlA.js} +1 -1
  18. package/out/renderer/assets/{razor-DdkE9XVt.js → razor-DnIVMSwa.js} +1 -1
  19. package/out/renderer/assets/{tsMode-BrQ4Fsc-.js → tsMode-CRIrHuii.js} +1 -1
  20. package/out/renderer/assets/{typescript-BakbYMnC.js → typescript-DJ3C8Yly.js} +1 -1
  21. package/out/renderer/assets/{xml-DHDW9Xhp.js → xml-CalvD5_C.js} +1 -1
  22. package/out/renderer/assets/{yaml-1Ayv_J3q.js → yaml-Cgs8pdVp.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/index.ts +8 -1
  26. package/src/main/orchestrator.ts +35 -7
  27. package/src/main/transcripts.ts +341 -0
  28. package/src/preload/index.ts +4 -0
  29. package/src/renderer/App.tsx +1 -0
  30. package/src/renderer/components/TerminalPanel.tsx +95 -2
  31. package/src/renderer/lib/tableToHtml.ts +146 -0
  32. package/src/renderer/styles.css +40 -0
package/out/main/index.js CHANGED
@@ -6040,6 +6040,248 @@ function trimToLogLimit(str) {
6040
6040
  return `[truncated]
6041
6041
  ${str.slice(str.length - MAX_LOG_CHARS)}`;
6042
6042
  }
6043
+ function supportsTranscriptLogging(agentId) {
6044
+ return agentId === "claude" || agentId === "codex";
6045
+ }
6046
+ const POLL_INTERVAL_MS = 1e3;
6047
+ const DISCOVERY_SLACK_MS = 1e4;
6048
+ const READ_CHUNK_BYTES = 64 * 1024;
6049
+ const MAX_ENTRY_CHARS = 2e4;
6050
+ class TranscriptTailer {
6051
+ agentId;
6052
+ cwd;
6053
+ sink;
6054
+ claudeProjectsDir;
6055
+ codexSessionsDir;
6056
+ files = /* @__PURE__ */ new Map();
6057
+ pollTimer = null;
6058
+ sinceMs = 0;
6059
+ constructor(options) {
6060
+ this.agentId = options.agentId;
6061
+ this.cwd = stripTrailingSep(options.cwd);
6062
+ this.sink = options.sink;
6063
+ this.claudeProjectsDir = options.claudeProjectsDir || path.join(os.homedir(), ".claude", "projects");
6064
+ this.codexSessionsDir = options.codexSessionsDir || path.join(os.homedir(), ".codex", "sessions");
6065
+ }
6066
+ start() {
6067
+ if (this.pollTimer) return;
6068
+ this.sinceMs = Date.now();
6069
+ this.files.clear();
6070
+ this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
6071
+ console.log(`[transcripts] Tailing ${this.agentId} transcripts for ${this.cwd}`);
6072
+ }
6073
+ stop() {
6074
+ if (!this.pollTimer) return;
6075
+ clearInterval(this.pollTimer);
6076
+ this.pollTimer = null;
6077
+ this.poll();
6078
+ this.files.clear();
6079
+ console.log("[transcripts] Stopped");
6080
+ }
6081
+ poll() {
6082
+ try {
6083
+ this.discover();
6084
+ for (const [filePath, tail] of this.files) {
6085
+ if (!tail.excluded) this.drainFile(filePath, tail);
6086
+ }
6087
+ } catch (err) {
6088
+ console.error("[transcripts] Poll error:", err);
6089
+ }
6090
+ }
6091
+ // ─── Discovery ──────────────────────────────────
6092
+ /**
6093
+ * Track every transcript file with recent activity, not just the first
6094
+ * match: /clear (Claude) or /new (Codex) starts a new session file in the
6095
+ * middle of one PTY run, and tailing all active candidates handles the
6096
+ * switch without special cases. Old idle files never match (stale mtime).
6097
+ */
6098
+ discover() {
6099
+ const dirs = this.agentId === "claude" ? [this.claudeProjectsDirForCwd()] : this.codexDateDirs();
6100
+ for (const dir of dirs) {
6101
+ let names;
6102
+ try {
6103
+ names = fs.readdirSync(dir);
6104
+ } catch {
6105
+ continue;
6106
+ }
6107
+ for (const name of names) {
6108
+ if (!name.endsWith(".jsonl")) continue;
6109
+ if (this.agentId === "codex" && !name.startsWith("rollout-")) continue;
6110
+ const filePath = path.join(dir, name);
6111
+ if (this.files.has(filePath)) continue;
6112
+ try {
6113
+ const stat = fs.statSync(filePath);
6114
+ if (stat.mtimeMs >= this.sinceMs - DISCOVERY_SLACK_MS) {
6115
+ this.files.set(filePath, { offset: 0, remainder: "", excluded: false });
6116
+ }
6117
+ } catch {
6118
+ }
6119
+ }
6120
+ }
6121
+ }
6122
+ claudeProjectsDirForCwd() {
6123
+ const slug = this.cwd.replace(/[^a-zA-Z0-9]/g, "-");
6124
+ return path.join(this.claudeProjectsDir, slug);
6125
+ }
6126
+ codexDateDirs() {
6127
+ const dirs = /* @__PURE__ */ new Set();
6128
+ for (const ms of [this.sinceMs, Date.now()]) {
6129
+ const d = new Date(ms);
6130
+ const yyyy = String(d.getFullYear());
6131
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
6132
+ const dd = String(d.getDate()).padStart(2, "0");
6133
+ dirs.add(path.join(this.codexSessionsDir, yyyy, mm, dd));
6134
+ }
6135
+ return [...dirs];
6136
+ }
6137
+ // ─── Tailing ────────────────────────────────────
6138
+ drainFile(filePath, tail) {
6139
+ let size;
6140
+ try {
6141
+ size = fs.statSync(filePath).size;
6142
+ } catch {
6143
+ return;
6144
+ }
6145
+ if (size <= tail.offset) return;
6146
+ let fd;
6147
+ try {
6148
+ fd = fs.openSync(filePath, "r");
6149
+ } catch {
6150
+ return;
6151
+ }
6152
+ try {
6153
+ const buf = Buffer.alloc(READ_CHUNK_BYTES);
6154
+ while (tail.offset < size && !tail.excluded) {
6155
+ const bytesRead = fs.readSync(fd, buf, 0, READ_CHUNK_BYTES, tail.offset);
6156
+ if (bytesRead <= 0) break;
6157
+ tail.offset += bytesRead;
6158
+ tail.remainder += buf.toString("utf-8", 0, bytesRead);
6159
+ const lines = tail.remainder.split("\n");
6160
+ tail.remainder = lines.pop() || "";
6161
+ for (const line of lines) {
6162
+ this.handleLine(line, tail);
6163
+ if (tail.excluded) break;
6164
+ }
6165
+ }
6166
+ } catch (err) {
6167
+ console.error(`[transcripts] Read error for ${filePath}:`, err);
6168
+ } finally {
6169
+ try {
6170
+ fs.closeSync(fd);
6171
+ } catch {
6172
+ }
6173
+ }
6174
+ }
6175
+ handleLine(line, tail) {
6176
+ const trimmed = line.trim();
6177
+ if (!trimmed) return;
6178
+ let obj;
6179
+ try {
6180
+ obj = JSON.parse(trimmed);
6181
+ } catch {
6182
+ return;
6183
+ }
6184
+ const entry = this.agentId === "claude" ? this.parseClaudeLine(obj) : this.parseCodexLine(obj, tail);
6185
+ if (!entry) return;
6186
+ const ms = Date.parse(entry.ts);
6187
+ if (Number.isFinite(ms) && ms < this.sinceMs - DISCOVERY_SLACK_MS) return;
6188
+ this.sink({ ...entry, content: capLength(entry.content) });
6189
+ }
6190
+ // ─── Claude Code format ─────────────────────────
6191
+ parseClaudeLine(obj) {
6192
+ if (!obj || typeof obj !== "object") return null;
6193
+ if (obj.isMeta) return null;
6194
+ if (obj.type !== "user" && obj.type !== "assistant") return null;
6195
+ if (typeof obj.cwd === "string" && stripTrailingSep(obj.cwd) !== this.cwd) return null;
6196
+ if (obj.isSidechain) return null;
6197
+ const message = obj.message;
6198
+ if (!message) return null;
6199
+ const text = extractClaudeText(message.content, obj.type === "user");
6200
+ if (!text) return null;
6201
+ return {
6202
+ ts: typeof obj.timestamp === "string" ? obj.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
6203
+ type: obj.type === "user" ? "user_input" : "terminal_output",
6204
+ content: text
6205
+ };
6206
+ }
6207
+ // ─── Codex CLI format ───────────────────────────
6208
+ parseCodexLine(obj, tail) {
6209
+ if (!obj || typeof obj !== "object") return null;
6210
+ const payload = obj.payload;
6211
+ if (obj.type === "session_meta") {
6212
+ const metaCwd = payload?.cwd;
6213
+ if (typeof metaCwd === "string" && stripTrailingSep(metaCwd) !== this.cwd) {
6214
+ tail.excluded = true;
6215
+ }
6216
+ return null;
6217
+ }
6218
+ if (obj.type !== "event_msg" || !payload || typeof payload !== "object") return null;
6219
+ let type;
6220
+ if (payload.type === "user_message") {
6221
+ type = "user_input";
6222
+ } else if (payload.type === "agent_message") {
6223
+ type = "terminal_output";
6224
+ } else {
6225
+ return null;
6226
+ }
6227
+ const text = typeof payload.message === "string" ? payload.message.trim() : "";
6228
+ if (!text || isCodexNoise(text)) return null;
6229
+ return {
6230
+ ts: typeof obj.timestamp === "string" ? obj.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
6231
+ type,
6232
+ content: text
6233
+ };
6234
+ }
6235
+ }
6236
+ const CLAUDE_NOISE_PREFIXES = [
6237
+ "<local-command-caveat>",
6238
+ "<command-name>",
6239
+ "<command-message>",
6240
+ "<local-command-stdout>",
6241
+ "<bash-input>",
6242
+ "<bash-stdout>",
6243
+ "<bash-stderr>",
6244
+ "<system-reminder>",
6245
+ "<task-notification>",
6246
+ "caveat: the messages below",
6247
+ "[request interrupted"
6248
+ ];
6249
+ const CODEX_NOISE_PREFIXES = [
6250
+ "<environment_context>",
6251
+ "<user_instructions>",
6252
+ "<permissions instructions>",
6253
+ "<turn_aborted>"
6254
+ ];
6255
+ function isClaudeNoise(text) {
6256
+ const lower = text.trimStart().toLowerCase();
6257
+ return CLAUDE_NOISE_PREFIXES.some((p2) => lower.startsWith(p2));
6258
+ }
6259
+ function isCodexNoise(text) {
6260
+ const lower = text.trimStart().toLowerCase();
6261
+ return CODEX_NOISE_PREFIXES.some((p2) => lower.startsWith(p2));
6262
+ }
6263
+ function extractClaudeText(content, isUser) {
6264
+ if (typeof content === "string") {
6265
+ const text = content.trim();
6266
+ return text && !isClaudeNoise(text) ? text : "";
6267
+ }
6268
+ if (!Array.isArray(content)) return "";
6269
+ if (isUser && content.some((b2) => b2?.type === "tool_result")) return "";
6270
+ const parts = [];
6271
+ for (const block of content) {
6272
+ if (block?.type !== "text" || typeof block.text !== "string") continue;
6273
+ const text = block.text.trim();
6274
+ if (text && !isClaudeNoise(text)) parts.push(text);
6275
+ }
6276
+ return parts.join("\n\n");
6277
+ }
6278
+ function stripTrailingSep(p2) {
6279
+ return p2.length > 1 ? p2.replace(/[/\\]+$/, "") : p2;
6280
+ }
6281
+ function capLength(str) {
6282
+ if (str.length <= MAX_ENTRY_CHARS) return str;
6283
+ return str.slice(0, MAX_ENTRY_CHARS) + `… [truncated, ${str.length} total chars]`;
6284
+ }
6043
6285
  var bufferUtil = { exports: {} };
6044
6286
  var constants;
6045
6287
  var hasRequiredConstants;
@@ -10719,6 +10961,7 @@ class Orchestrator {
10719
10961
  noProjectPollTimer = null;
10720
10962
  noProjectPollCwd = null;
10721
10963
  currentProjectName = null;
10964
+ transcriptTailer = null;
10722
10965
  constructor(settingsDir, events) {
10723
10966
  this.settingsDir = settingsDir;
10724
10967
  this.events = events;
@@ -10770,11 +11013,36 @@ class Orchestrator {
10770
11013
  this.saveSettings();
10771
11014
  this.bridge.setLoggingEnabled(enabled);
10772
11015
  if (!enabled) {
10773
- this.bridge.endSession();
11016
+ this.stopChatLogging();
10774
11017
  } else if (this.activeTabId) {
11018
+ const tab = this.tabs.get(this.activeTabId);
11019
+ if (tab) this.startChatLogging(tab.agent, tab.cwd);
11020
+ }
11021
+ }
11022
+ // Agents with native session transcripts (Claude Code, Codex) get exact
11023
+ // chat text tailed from their JSONL logs; everything else falls back to
11024
+ // the terminal screen-scraper bridge.
11025
+ startChatLogging(agent, cwd) {
11026
+ this.stopChatLogging();
11027
+ if (!this.settings.logChat) return;
11028
+ if (supportsTranscriptLogging(agent.id)) {
11029
+ this.transcriptTailer = new TranscriptTailer({
11030
+ agentId: agent.id,
11031
+ cwd,
11032
+ sink: (entry) => this.workerWs.sendChatLog(entry)
11033
+ });
11034
+ this.transcriptTailer.start();
11035
+ } else {
10775
11036
  this.bridge.startSession();
10776
11037
  }
10777
11038
  }
11039
+ stopChatLogging() {
11040
+ if (this.transcriptTailer) {
11041
+ this.transcriptTailer.stop();
11042
+ this.transcriptTailer = null;
11043
+ }
11044
+ this.bridge.endSession();
11045
+ }
10778
11046
  // ─── Settings ───────────────────────────────────
10779
11047
  getActiveProfile() {
10780
11048
  return this.settings.profiles[this.settings.activeProfile] || this.settings.profiles.production || DEFAULT_PROFILES.production;
@@ -10986,7 +11254,7 @@ class Orchestrator {
10986
11254
  this.events.onPtyExit(tabId, exitCode);
10987
11255
  await this.timeTracker.endSession(tabId);
10988
11256
  if (tabId === this.activeTabId) {
10989
- this.bridge.endSession();
11257
+ this.stopChatLogging();
10990
11258
  if (this.currentAgent && isCodingAgent(this.currentAgent)) {
10991
11259
  this.workerWs.disconnect();
10992
11260
  }
@@ -10994,9 +11262,7 @@ class Orchestrator {
10994
11262
  const t = this.tabs.get(tabId);
10995
11263
  if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
10996
11264
  });
10997
- if (this.settings.logChat) {
10998
- this.bridge.startSession();
10999
- }
11265
+ this.startChatLogging(agent, cwd);
11000
11266
  const profile = this.getActiveProfile();
11001
11267
  const shouldTrack = opts?.trackTime !== void 0 ? opts.trackTime : profile.trackTime !== false;
11002
11268
  if (shouldTrack) {
@@ -11036,7 +11302,7 @@ class Orchestrator {
11036
11302
  tab.ptyManager.kill();
11037
11303
  this.tabs.delete(tabId);
11038
11304
  if (tabId === this.activeTabId) {
11039
- this.bridge.endSession();
11305
+ this.stopChatLogging();
11040
11306
  if (isCodingAgent(tab.agent)) {
11041
11307
  this.workerWs.disconnect();
11042
11308
  }
@@ -11187,7 +11453,7 @@ class Orchestrator {
11187
11453
  // ─── Shutdown ───────────────────────────────────
11188
11454
  async shutdown() {
11189
11455
  this.stopNoProjectPolling();
11190
- this.bridge.endSession();
11456
+ this.stopChatLogging();
11191
11457
  await this.timeTracker.endAll();
11192
11458
  for (const [, tab] of this.tabs) {
11193
11459
  if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
@@ -11301,6 +11567,10 @@ function createWindow() {
11301
11567
  mainWindow = null;
11302
11568
  });
11303
11569
  }
11570
+ electron.ipcMain.handle("clipboard:writeTable", (_event, html, text) => {
11571
+ electron.clipboard.write({ html, text });
11572
+ return { ok: true };
11573
+ });
11304
11574
  electron.ipcMain.handle("pty:spawn", async (_event, tabId, agent, cwd) => {
11305
11575
  await orchestrator.spawnAgent(tabId, agent, cwd);
11306
11576
  return { ok: true };
@@ -31,6 +31,8 @@ const api = {
31
31
  return () => electron.ipcRenderer.removeListener("app:projectChanged", listener);
32
32
  },
33
33
  browseCwd: () => electron.ipcRenderer.invoke("app:browseCwd"),
34
+ // Clipboard — write a rich-HTML table (+ plain-text fallback) for email paste.
35
+ copyEmailTable: (html, text) => electron.ipcRenderer.invoke("clipboard:writeTable", html, text),
34
36
  getVersion: () => electron.ipcRenderer.invoke("app:getVersion"),
35
37
  getUpdateInfo: () => electron.ipcRenderer.invoke("app:getUpdateInfo"),
36
38
  onUpdateInfo: (callback) => {
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-D2MUZin7.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-CbkEcL-z.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-CbkEcL-z.js";
1
+ import { c as createWebWorker, l as languages } from "./index-BOOvUI7u.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-BxPLl0yy.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-BxPLl0yy.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-D2MUZin7.js";
1
+ import { l as languages } from "./index-BOOvUI7u.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D2MUZin7.js";
1
+ import { l as languages } from "./index-BOOvUI7u.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-D2MUZin7.js";
1
+ import { l as languages } from "./index-BOOvUI7u.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-D2MUZin7.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-CbkEcL-z.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-CbkEcL-z.js";
1
+ import { c as createWebWorker, l as languages } from "./index-BOOvUI7u.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-BxPLl0yy.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-BxPLl0yy.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {