@phenx-inc/ctlsurf 0.1.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 (133) hide show
  1. package/bin/ctlsurf-worker.js +173 -0
  2. package/electron-vite.config.ts +34 -0
  3. package/out/headless/index.mjs +1364 -0
  4. package/out/headless/index.mjs.map +7 -0
  5. package/out/main/index.js +1131 -0
  6. package/out/preload/index.js +67 -0
  7. package/out/renderer/assets/abap-D5KwWAsZ.js +1399 -0
  8. package/out/renderer/assets/apex-DVGUZ64i.js +331 -0
  9. package/out/renderer/assets/azcli-BEAhqcuE.js +69 -0
  10. package/out/renderer/assets/bat-Bqkp9Cfu.js +101 -0
  11. package/out/renderer/assets/bicep-DIlfshcM.js +110 -0
  12. package/out/renderer/assets/cameligo-CLaaYNMV.js +175 -0
  13. package/out/renderer/assets/clojure-fcgFaMHx.js +762 -0
  14. package/out/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
  15. package/out/renderer/assets/coffee-CzJ5oEdj.js +233 -0
  16. package/out/renderer/assets/cpp-CcN6f0ik.js +390 -0
  17. package/out/renderer/assets/csharp-BJeIuvde.js +327 -0
  18. package/out/renderer/assets/csp-D_3BK2Wp.js +54 -0
  19. package/out/renderer/assets/css-i3rI3_64.js +186 -0
  20. package/out/renderer/assets/css.worker-umuuUiIb.js +53567 -0
  21. package/out/renderer/assets/cssMode-DL0XItGB.js +208 -0
  22. package/out/renderer/assets/cypher-D0--_GAN.js +264 -0
  23. package/out/renderer/assets/dart-vLMHv35g.js +282 -0
  24. package/out/renderer/assets/dockerfile--oxj0cAH.js +131 -0
  25. package/out/renderer/assets/ecl-CeuUgzaZ.js +457 -0
  26. package/out/renderer/assets/editor.worker-CNgWLVu7.js +13695 -0
  27. package/out/renderer/assets/elixir-eLfY1jWH.js +570 -0
  28. package/out/renderer/assets/flow9-ZSTChSMd.js +143 -0
  29. package/out/renderer/assets/freemarker2-CrOEuDcF.js +995 -0
  30. package/out/renderer/assets/fsharp-D2uoxuLH.js +218 -0
  31. package/out/renderer/assets/go-brnMpFrj.js +219 -0
  32. package/out/renderer/assets/graphql-BeiGgjIU.js +152 -0
  33. package/out/renderer/assets/handlebars-D4QYaBof.js +414 -0
  34. package/out/renderer/assets/hcl-CrX1Es2W.js +184 -0
  35. package/out/renderer/assets/html-B2Dqk2ai.js +303 -0
  36. package/out/renderer/assets/html.worker-BT47iy49.js +29777 -0
  37. package/out/renderer/assets/htmlMode-CdZ0Prhd.js +224 -0
  38. package/out/renderer/assets/index-CJ6RsQWP.css +8108 -0
  39. package/out/renderer/assets/index-pZmE1QXB.js +211777 -0
  40. package/out/renderer/assets/ini-BcQysCTb.js +72 -0
  41. package/out/renderer/assets/java-Dt3iMn2o.js +233 -0
  42. package/out/renderer/assets/javascript-CK8zNQXj.js +72 -0
  43. package/out/renderer/assets/json.worker-D4JVmXIe.js +21424 -0
  44. package/out/renderer/assets/jsonMode-Cewaellc.js +931 -0
  45. package/out/renderer/assets/julia-Cm3ItYL_.js +512 -0
  46. package/out/renderer/assets/kotlin-Ddo1SjA5.js +253 -0
  47. package/out/renderer/assets/less-B7Qaxw-O.js +162 -0
  48. package/out/renderer/assets/lexon-C1U0m2n9.js +158 -0
  49. package/out/renderer/assets/liquid-Bd3GPNs2.js +235 -0
  50. package/out/renderer/assets/lspLanguageFeatures-DSDH7BnA.js +1841 -0
  51. package/out/renderer/assets/lua-hNsuGJkO.js +163 -0
  52. package/out/renderer/assets/m3-6ko6q9-_.js +211 -0
  53. package/out/renderer/assets/markdown-B0YTnTxW.js +230 -0
  54. package/out/renderer/assets/mdx-CCPVCrXC.js +159 -0
  55. package/out/renderer/assets/mips-CJm71dS3.js +199 -0
  56. package/out/renderer/assets/msdax-BBeIktCY.js +376 -0
  57. package/out/renderer/assets/mysql-BWiizXSn.js +879 -0
  58. package/out/renderer/assets/objective-c-B1L1C5EC.js +184 -0
  59. package/out/renderer/assets/pascal-DMQyD4Xk.js +252 -0
  60. package/out/renderer/assets/pascaligo-VA_LQ1oU.js +165 -0
  61. package/out/renderer/assets/perl-DC0Z0tlO.js +627 -0
  62. package/out/renderer/assets/pgsql-DaSGFTLp.js +852 -0
  63. package/out/renderer/assets/php-Bkx1qpkQ.js +501 -0
  64. package/out/renderer/assets/pla-DEV89yYj.js +138 -0
  65. package/out/renderer/assets/postiats-CVVurEnu.js +908 -0
  66. package/out/renderer/assets/powerquery-BQ_t1ZiQ.js +891 -0
  67. package/out/renderer/assets/powershell-BXiKvz7Z.js +240 -0
  68. package/out/renderer/assets/protobuf-CndvAUGu.js +421 -0
  69. package/out/renderer/assets/pug-BxCXwerb.js +403 -0
  70. package/out/renderer/assets/python-34jOtlcC.js +295 -0
  71. package/out/renderer/assets/qsharp-BWK6YLKm.js +302 -0
  72. package/out/renderer/assets/r-CtqYUQ6l.js +244 -0
  73. package/out/renderer/assets/razor-DXRw694z.js +545 -0
  74. package/out/renderer/assets/redis-O7gSt3oh.js +303 -0
  75. package/out/renderer/assets/redshift-CvYMMYZY.js +810 -0
  76. package/out/renderer/assets/restructuredtext-B-KQCVu_.js +175 -0
  77. package/out/renderer/assets/ruby-DCd4DmAr.js +512 -0
  78. package/out/renderer/assets/rust-B1c0VCeq.js +344 -0
  79. package/out/renderer/assets/sb-Chfc_wZF.js +116 -0
  80. package/out/renderer/assets/scala-DbVzH-3O.js +371 -0
  81. package/out/renderer/assets/scheme-D7PxodDG.js +109 -0
  82. package/out/renderer/assets/scss-B42qMyAu.js +261 -0
  83. package/out/renderer/assets/shell-vZEubQ82.js +222 -0
  84. package/out/renderer/assets/solidity-yHOxYChb.js +1368 -0
  85. package/out/renderer/assets/sophia-D7pU0Y1d.js +200 -0
  86. package/out/renderer/assets/sparql-DxuVdnRl.js +202 -0
  87. package/out/renderer/assets/sql-BAGepFCR.js +854 -0
  88. package/out/renderer/assets/st-C-b0Dh53.js +417 -0
  89. package/out/renderer/assets/swift-BmOZGynf.js +313 -0
  90. package/out/renderer/assets/systemverilog-BOC0OOdC.js +577 -0
  91. package/out/renderer/assets/tcl-Bb4GCwBr.js +233 -0
  92. package/out/renderer/assets/ts.worker-C7hW3aY-.js +225330 -0
  93. package/out/renderer/assets/tsMode-CmND5_wB.js +1265 -0
  94. package/out/renderer/assets/twig-DvgEGWAV.js +393 -0
  95. package/out/renderer/assets/typescript-BNNI0Euv.js +337 -0
  96. package/out/renderer/assets/typespec-R77Ln7Jb.js +128 -0
  97. package/out/renderer/assets/vb-Bm6ESA0Q.js +373 -0
  98. package/out/renderer/assets/wgsl-_KPae5vw.js +454 -0
  99. package/out/renderer/assets/xml-CgdndrNB.js +89 -0
  100. package/out/renderer/assets/yaml-DNWPIf1s.js +200 -0
  101. package/out/renderer/index.html +13 -0
  102. package/package.json +67 -0
  103. package/resources/icon.icns +0 -0
  104. package/resources/icon.ico +0 -0
  105. package/resources/icon.png +0 -0
  106. package/src/main/agents.ts +46 -0
  107. package/src/main/bridge.ts +180 -0
  108. package/src/main/ctlsurfApi.ts +142 -0
  109. package/src/main/detectMode.ts +17 -0
  110. package/src/main/headless.ts +182 -0
  111. package/src/main/index.ts +300 -0
  112. package/src/main/orchestrator.ts +404 -0
  113. package/src/main/pty.ts +65 -0
  114. package/src/main/settingsDir.ts +17 -0
  115. package/src/main/tui.ts +366 -0
  116. package/src/main/workerWs.ts +312 -0
  117. package/src/preload/index.ts +114 -0
  118. package/src/renderer/App.tsx +275 -0
  119. package/src/renderer/components/CtlsurfPanel.tsx +49 -0
  120. package/src/renderer/components/EditorPanel.tsx +232 -0
  121. package/src/renderer/components/MultiSplitPane.tsx +251 -0
  122. package/src/renderer/components/PaneLayout.tsx +419 -0
  123. package/src/renderer/components/SettingsDialog.tsx +204 -0
  124. package/src/renderer/components/SplitPane.tsx +82 -0
  125. package/src/renderer/components/StatusBar.tsx +73 -0
  126. package/src/renderer/components/TerminalPanel.tsx +140 -0
  127. package/src/renderer/index.html +12 -0
  128. package/src/renderer/main.tsx +10 -0
  129. package/src/renderer/styles.css +722 -0
  130. package/tsconfig.json +8 -0
  131. package/tsconfig.main.json +15 -0
  132. package/tsconfig.preload.json +14 -0
  133. package/tsconfig.renderer.json +15 -0
@@ -0,0 +1,1364 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+ var __commonJS = (cb, mod) => function __require2() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
12
+
13
+ // node_modules/electron/index.js
14
+ var require_electron = __commonJS({
15
+ "node_modules/electron/index.js"(exports, module) {
16
+ var fs2 = __require("fs");
17
+ var path3 = __require("path");
18
+ var pathFile = path3.join(__dirname, "path.txt");
19
+ function getElectronPath() {
20
+ let executablePath;
21
+ if (fs2.existsSync(pathFile)) {
22
+ executablePath = fs2.readFileSync(pathFile, "utf-8");
23
+ }
24
+ if (process.env.ELECTRON_OVERRIDE_DIST_PATH) {
25
+ return path3.join(process.env.ELECTRON_OVERRIDE_DIST_PATH, executablePath || "electron");
26
+ }
27
+ if (executablePath) {
28
+ return path3.join(__dirname, "dist", executablePath);
29
+ } else {
30
+ throw new Error("Electron failed to install correctly, please delete node_modules/electron and try installing again");
31
+ }
32
+ }
33
+ module.exports = getElectronPath();
34
+ }
35
+ });
36
+
37
+ // src/main/orchestrator.ts
38
+ import path from "path";
39
+ import fs from "fs";
40
+ import os2 from "os";
41
+
42
+ // src/main/pty.ts
43
+ import { createRequire } from "module";
44
+ var require2 = createRequire(import.meta.url);
45
+ var pty = require2("node-pty");
46
+ var PtyManager = class {
47
+ process = null;
48
+ dataCallbacks = [];
49
+ exitCallbacks = [];
50
+ constructor(agent, cwd) {
51
+ const shell = agent.command;
52
+ const args = agent.args || [];
53
+ try {
54
+ console.log(`[pty] Spawning: ${shell} ${args.join(" ")} in ${cwd}`);
55
+ } catch {
56
+ }
57
+ this.process = pty.spawn(shell, args, {
58
+ name: "xterm-256color",
59
+ cwd,
60
+ env: process.env,
61
+ cols: 80,
62
+ rows: 24
63
+ });
64
+ this.process.onData((data) => {
65
+ for (const cb of this.dataCallbacks) {
66
+ cb(data);
67
+ }
68
+ });
69
+ this.process.onExit(({ exitCode }) => {
70
+ for (const cb of this.exitCallbacks) {
71
+ cb(exitCode);
72
+ }
73
+ this.process = null;
74
+ });
75
+ }
76
+ write(data) {
77
+ this.process?.write(data);
78
+ }
79
+ resize(cols, rows) {
80
+ this.process?.resize(cols, rows);
81
+ }
82
+ kill() {
83
+ this.process?.kill();
84
+ this.process = null;
85
+ }
86
+ onData(cb) {
87
+ this.dataCallbacks.push(cb);
88
+ }
89
+ onExit(cb) {
90
+ this.exitCallbacks.push(cb);
91
+ }
92
+ };
93
+
94
+ // src/main/agents.ts
95
+ function getShellCommand() {
96
+ if (process.platform === "win32") return "powershell.exe";
97
+ return process.env.SHELL || "/bin/zsh";
98
+ }
99
+ function getBuiltinAgents() {
100
+ return [
101
+ {
102
+ id: "shell",
103
+ name: "Shell",
104
+ command: getShellCommand(),
105
+ args: ["-l"],
106
+ // login shell to load PATH
107
+ description: "Default system shell"
108
+ },
109
+ {
110
+ id: "claude",
111
+ name: "Claude Code",
112
+ command: "claude",
113
+ args: [],
114
+ description: "Anthropic Claude Code CLI"
115
+ },
116
+ {
117
+ id: "codex",
118
+ name: "Codex CLI",
119
+ command: "codex",
120
+ args: [],
121
+ description: "OpenAI Codex CLI"
122
+ }
123
+ ];
124
+ }
125
+ function isCodingAgent(agent) {
126
+ return agent.id !== "shell";
127
+ }
128
+
129
+ // src/main/ctlsurfApi.ts
130
+ var CTLSURF_BASE_URL = "https://app.ctlsurf.com/api";
131
+ var CtlsurfApi = class {
132
+ baseUrl;
133
+ apiKey = null;
134
+ constructor(baseUrl) {
135
+ this.baseUrl = baseUrl || CTLSURF_BASE_URL;
136
+ }
137
+ setApiKey(key) {
138
+ this.apiKey = key;
139
+ }
140
+ setBaseUrl(url) {
141
+ this.baseUrl = url.endsWith("/api") ? url : `${url}/api`;
142
+ }
143
+ getApiKey() {
144
+ return this.apiKey;
145
+ }
146
+ headers() {
147
+ const h = { "Content-Type": "application/json" };
148
+ if (this.apiKey) {
149
+ h["Authorization"] = `Bearer ${this.apiKey}`;
150
+ }
151
+ return h;
152
+ }
153
+ async request(method, path3, body) {
154
+ const url = `${this.baseUrl}${path3}`;
155
+ const opts = {
156
+ method,
157
+ headers: this.headers()
158
+ };
159
+ if (body) {
160
+ opts.body = JSON.stringify(body);
161
+ }
162
+ const res = await fetch(url, opts);
163
+ if (!res.ok) {
164
+ const text = await res.text();
165
+ throw new Error(`ctlsurf API ${method} ${path3}: ${res.status} ${text}`);
166
+ }
167
+ return res.json();
168
+ }
169
+ // ─── Pages ───────────────────────────────────────────
170
+ async createPage(params) {
171
+ return this.request("POST", "/pages", params);
172
+ }
173
+ async findPageByRootPath(rootPath) {
174
+ return this.request("POST", "/pages/find-by-root-path", { root_path: rootPath });
175
+ }
176
+ // ─── Blocks ──────────────────────────────────────────
177
+ async createBlock(pageId, params) {
178
+ return this.request("POST", `/blocks/page/${pageId}`, params);
179
+ }
180
+ async getBlock(blockId) {
181
+ return this.request("GET", `/blocks/${blockId}`);
182
+ }
183
+ async updateBlock(blockId, params) {
184
+ return this.request("PUT", `/blocks/${blockId}`, params);
185
+ }
186
+ // ─── Folders ────────────────────────────────────────
187
+ async createFolder(params) {
188
+ return this.request("POST", "/folders", params);
189
+ }
190
+ // ─── Workers ────────────────────────────────────────
191
+ async getAuthCode() {
192
+ return this.request("POST", "/workers/token-exchange");
193
+ }
194
+ async findFolderByPath(rootPath) {
195
+ return this.request("POST", "/folders/find-by-path", { root_path: rootPath });
196
+ }
197
+ async getFolderPages(folderId) {
198
+ const folder = await this.request("GET", `/folders/${folderId}`);
199
+ return folder?.pages || [];
200
+ }
201
+ async findFolderByGitRemote(gitRemote) {
202
+ const folders = await this.request("GET", "/folders");
203
+ return folders?.find((f) => f.git_remote === gitRemote || f.root_path === gitRemote) || null;
204
+ }
205
+ // ─── Log convenience ─────────────────────────────────
206
+ async appendLog(blockId, action, message, data) {
207
+ const block = await this.getBlock(blockId);
208
+ const props = block.props || {};
209
+ const entries = Array.isArray(props.entries) ? [...props.entries] : [];
210
+ const maxEntries = props.max_entries || 1e3;
211
+ const entry = {
212
+ _id: `log_${entries.length}`,
213
+ _timestamp: (/* @__PURE__ */ new Date()).toISOString(),
214
+ action,
215
+ message
216
+ };
217
+ if (data) {
218
+ entry.data = data;
219
+ }
220
+ entries.push(entry);
221
+ const trimmed = entries.length > maxEntries ? entries.slice(-maxEntries) : entries;
222
+ return this.updateBlock(blockId, {
223
+ props: { ...props, entries: trimmed }
224
+ });
225
+ }
226
+ };
227
+
228
+ // src/main/bridge.ts
229
+ var ConversationBridge = class {
230
+ api;
231
+ logBlockId = null;
232
+ pageId = null;
233
+ buffer = "";
234
+ flushTimer = null;
235
+ flushIntervalMs = 3e3;
236
+ // flush every 3 seconds
237
+ agentName = "shell";
238
+ sessionActive = false;
239
+ inputBuffer = "";
240
+ constructor(api) {
241
+ this.api = api;
242
+ }
243
+ /**
244
+ * Start a new logging session.
245
+ * Creates a log block on the given dataspace page.
246
+ */
247
+ async startSession(dataspacePageId, agentName, cwd) {
248
+ if (!this.api.getApiKey()) {
249
+ console.log("[bridge] No API key set, skipping session logging");
250
+ return;
251
+ }
252
+ this.pageId = dataspacePageId;
253
+ this.agentName = agentName;
254
+ this.buffer = "";
255
+ this.inputBuffer = "";
256
+ try {
257
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
258
+ const block = await this.api.createBlock(dataspacePageId, {
259
+ type: "log",
260
+ title: `${agentName} \u2014 ${timestamp} \u2014 ${cwd}`,
261
+ props: {
262
+ entries: [],
263
+ max_entries: 1e3
264
+ }
265
+ });
266
+ this.logBlockId = block.id;
267
+ this.sessionActive = true;
268
+ await this.api.appendLog(this.logBlockId, "session_start", `Started ${agentName} session`, {
269
+ agent: agentName,
270
+ cwd,
271
+ timestamp
272
+ });
273
+ console.log(`[bridge] Session started, log block: ${this.logBlockId}`);
274
+ } catch (err) {
275
+ console.error(`[bridge] Failed to start session:`, err.message);
276
+ this.sessionActive = false;
277
+ }
278
+ }
279
+ /**
280
+ * Feed terminal output data into the bridge.
281
+ * Buffers and flushes periodically.
282
+ */
283
+ feedOutput(data) {
284
+ if (!this.sessionActive) return;
285
+ this.buffer += data;
286
+ if (this.flushTimer) {
287
+ clearTimeout(this.flushTimer);
288
+ }
289
+ this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs);
290
+ }
291
+ /**
292
+ * Feed user input data into the bridge.
293
+ */
294
+ feedInput(data) {
295
+ if (!this.sessionActive) return;
296
+ this.inputBuffer += data;
297
+ if (data.includes("\r") || data.includes("\n")) {
298
+ const input = this.inputBuffer.trim();
299
+ if (input.length > 0) {
300
+ this.logEntry("user_input", input);
301
+ }
302
+ this.inputBuffer = "";
303
+ }
304
+ }
305
+ /**
306
+ * Flush buffered output to ctlsurf.
307
+ */
308
+ async flush() {
309
+ if (!this.logBlockId || this.buffer.length === 0) return;
310
+ const chunk = this.buffer;
311
+ this.buffer = "";
312
+ const cleaned = stripAnsi(chunk);
313
+ if (cleaned.trim().length === 0) return;
314
+ try {
315
+ await this.api.appendLog(this.logBlockId, "terminal_output", cleaned);
316
+ } catch (err) {
317
+ console.error(`[bridge] Failed to append log:`, err.message);
318
+ }
319
+ }
320
+ /**
321
+ * Log a specific entry immediately.
322
+ */
323
+ async logEntry(action, message, data) {
324
+ if (!this.logBlockId) return;
325
+ try {
326
+ await this.api.appendLog(this.logBlockId, action, message, data);
327
+ } catch (err) {
328
+ console.error(`[bridge] Failed to log entry:`, err.message);
329
+ }
330
+ }
331
+ /**
332
+ * End the current session.
333
+ */
334
+ async endSession(exitCode) {
335
+ if (!this.sessionActive || !this.logBlockId) return;
336
+ await this.flush();
337
+ try {
338
+ await this.api.appendLog(this.logBlockId, "session_end", `Session ended (exit code: ${exitCode ?? "unknown"})`, {
339
+ agent: this.agentName,
340
+ exitCode
341
+ });
342
+ } catch (err) {
343
+ console.error(`[bridge] Failed to log session end:`, err.message);
344
+ }
345
+ if (this.flushTimer) {
346
+ clearTimeout(this.flushTimer);
347
+ this.flushTimer = null;
348
+ }
349
+ this.sessionActive = false;
350
+ this.logBlockId = null;
351
+ this.buffer = "";
352
+ this.inputBuffer = "";
353
+ console.log("[bridge] Session ended");
354
+ }
355
+ };
356
+ function stripAnsi(str) {
357
+ return str.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "").replace(/\x1b[^[\]](.|$)/g, "").replace(/\x1b/g, "").replace(/\r/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
358
+ }
359
+
360
+ // src/main/workerWs.ts
361
+ import os from "os";
362
+ import crypto from "crypto";
363
+ function log(...args) {
364
+ try {
365
+ console.log(...args);
366
+ } catch {
367
+ }
368
+ }
369
+ var HEARTBEAT_INTERVAL_MS = 3e4;
370
+ var RECONNECT_DELAY_MS = 5e3;
371
+ var MAX_RECONNECT_DELAY_MS = 6e4;
372
+ var WorkerWsClient = class {
373
+ ws = null;
374
+ apiKey = null;
375
+ baseUrl;
376
+ events;
377
+ heartbeatTimer = null;
378
+ reconnectTimer = null;
379
+ reconnectDelay = RECONNECT_DELAY_MS;
380
+ registration = null;
381
+ workerId = null;
382
+ _status = "disconnected";
383
+ shouldReconnect = false;
384
+ fingerprint;
385
+ constructor(events, baseUrl) {
386
+ this.events = events;
387
+ this.baseUrl = baseUrl || "wss://app.ctlsurf.com";
388
+ this.fingerprint = this.generateFingerprint();
389
+ }
390
+ get status() {
391
+ return this._status;
392
+ }
393
+ get currentWorkerId() {
394
+ return this.workerId;
395
+ }
396
+ setApiKey(key) {
397
+ this.apiKey = key;
398
+ }
399
+ setBaseUrl(url) {
400
+ this.baseUrl = url;
401
+ }
402
+ generateFingerprint() {
403
+ const data = `${os.hostname()}:${os.userInfo().username}:${os.platform()}:${os.arch()}`;
404
+ return crypto.createHash("sha256").update(data).digest("hex").slice(0, 32);
405
+ }
406
+ setStatus(status) {
407
+ if (this._status !== status) {
408
+ this._status = status;
409
+ this.events.onStatusChange(status);
410
+ }
411
+ }
412
+ connect(registration) {
413
+ this.registration = { ...registration, fingerprint: this.fingerprint };
414
+ this.shouldReconnect = true;
415
+ this.doConnect();
416
+ }
417
+ disconnect() {
418
+ this.shouldReconnect = false;
419
+ this.clearTimers();
420
+ if (this.ws) {
421
+ const oldWs = this.ws;
422
+ this.ws = null;
423
+ oldWs.onopen = null;
424
+ oldWs.onmessage = null;
425
+ oldWs.onclose = null;
426
+ oldWs.onerror = null;
427
+ try {
428
+ oldWs.close(1e3, "client disconnect");
429
+ } catch {
430
+ }
431
+ }
432
+ this.setStatus("disconnected");
433
+ }
434
+ sendResponse(parentId, content, metadata) {
435
+ this.send({
436
+ type: "response",
437
+ parent_id: parentId,
438
+ content,
439
+ metadata
440
+ });
441
+ }
442
+ sendStatusUpdate(status) {
443
+ this.send({ type: "status_update", status });
444
+ }
445
+ sendAck(messageId) {
446
+ this.send({ type: "ack", message_id: messageId });
447
+ }
448
+ sendTerminalData(data) {
449
+ this.send({ type: "terminal_stream", data });
450
+ }
451
+ sendTerminalResize(cols, rows) {
452
+ this.send({ type: "terminal_resize", cols, rows });
453
+ }
454
+ doConnect() {
455
+ if (!this.apiKey || !this.registration) {
456
+ log("[worker-ws] No API key or registration, skipping connect");
457
+ return;
458
+ }
459
+ this.clearTimers();
460
+ if (this.ws) {
461
+ const oldWs = this.ws;
462
+ this.ws = null;
463
+ oldWs.onopen = null;
464
+ oldWs.onmessage = null;
465
+ oldWs.onclose = null;
466
+ oldWs.onerror = null;
467
+ try {
468
+ oldWs.close();
469
+ } catch {
470
+ }
471
+ setTimeout(() => this.doConnectNow(), 500);
472
+ return;
473
+ }
474
+ this.doConnectNow();
475
+ }
476
+ doConnectNow() {
477
+ if (!this.apiKey || !this.registration) return;
478
+ if (!this.shouldReconnect) {
479
+ log("[worker-ws] shouldReconnect is false, aborting connect");
480
+ return;
481
+ }
482
+ this.setStatus("connecting");
483
+ const wsBase = this.baseUrl.replace(/^http/, "ws");
484
+ const url = `${wsBase}/api/ws/worker?token=${encodeURIComponent(this.apiKey)}`;
485
+ log(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
486
+ try {
487
+ this.ws = new WebSocket(url);
488
+ } catch (err) {
489
+ log("[worker-ws] Failed to create WebSocket:", err);
490
+ this.scheduleReconnect();
491
+ return;
492
+ }
493
+ this.ws.onopen = () => {
494
+ log("[worker-ws] Connected, sending register");
495
+ this.reconnectDelay = RECONNECT_DELAY_MS;
496
+ this.send({
497
+ type: "register",
498
+ ...this.registration
499
+ });
500
+ this.startHeartbeat();
501
+ };
502
+ this.ws.onmessage = (event) => {
503
+ try {
504
+ const data = JSON.parse(String(event.data));
505
+ this.handleMessage(data);
506
+ } catch (err) {
507
+ log("[worker-ws] Failed to parse message:", err);
508
+ }
509
+ };
510
+ this.ws.onclose = (event) => {
511
+ log(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
512
+ this.ws = null;
513
+ this.clearHeartbeat();
514
+ this.setStatus("disconnected");
515
+ if (this.shouldReconnect) {
516
+ this.scheduleReconnect();
517
+ }
518
+ };
519
+ this.ws.onerror = () => {
520
+ log("[worker-ws] WebSocket error");
521
+ };
522
+ }
523
+ handleMessage(data) {
524
+ const msgType = data.type;
525
+ switch (msgType) {
526
+ case "registered": {
527
+ this.workerId = data.worker_id;
528
+ const workerStatus = data.status;
529
+ console.log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
530
+ if (workerStatus === "pending_approval") {
531
+ this.setStatus("pending_approval");
532
+ } else {
533
+ this.setStatus("connected");
534
+ }
535
+ const pendingMessages = data.pending_messages || [];
536
+ this.events.onRegistered({
537
+ worker_id: this.workerId,
538
+ folder_id: data.folder_id,
539
+ status: workerStatus,
540
+ pending_messages: pendingMessages
541
+ });
542
+ for (const msg of pendingMessages) {
543
+ this.events.onMessage(msg);
544
+ }
545
+ break;
546
+ }
547
+ case "approved": {
548
+ log("[worker-ws] Worker approved!");
549
+ this.setStatus("connected");
550
+ break;
551
+ }
552
+ case "message": {
553
+ const msg = data.message;
554
+ if (msg) {
555
+ console.log(`[worker-ws] Received message: ${msg.id}`);
556
+ this.events.onMessage(msg);
557
+ }
558
+ break;
559
+ }
560
+ case "terminal_input": {
561
+ const inputData = data.data;
562
+ if (inputData && this.events.onTerminalInput) {
563
+ this.events.onTerminalInput(inputData);
564
+ }
565
+ break;
566
+ }
567
+ case "heartbeat_ack":
568
+ break;
569
+ default:
570
+ console.log(`[worker-ws] Unknown message type: ${msgType}`);
571
+ }
572
+ }
573
+ send(data) {
574
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
575
+ this.ws.send(JSON.stringify(data));
576
+ }
577
+ }
578
+ startHeartbeat() {
579
+ this.clearHeartbeat();
580
+ this.heartbeatTimer = setInterval(() => {
581
+ this.send({ type: "heartbeat" });
582
+ }, HEARTBEAT_INTERVAL_MS);
583
+ }
584
+ clearHeartbeat() {
585
+ if (this.heartbeatTimer) {
586
+ clearInterval(this.heartbeatTimer);
587
+ this.heartbeatTimer = null;
588
+ }
589
+ }
590
+ scheduleReconnect() {
591
+ if (!this.shouldReconnect) return;
592
+ console.log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
593
+ this.reconnectTimer = setTimeout(() => {
594
+ this.doConnect();
595
+ }, this.reconnectDelay);
596
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
597
+ }
598
+ clearTimers() {
599
+ this.clearHeartbeat();
600
+ if (this.reconnectTimer) {
601
+ clearTimeout(this.reconnectTimer);
602
+ this.reconnectTimer = null;
603
+ }
604
+ }
605
+ };
606
+
607
+ // src/main/orchestrator.ts
608
+ function log2(...args) {
609
+ try {
610
+ console.log(...args);
611
+ } catch {
612
+ }
613
+ }
614
+ var DEFAULT_PROFILES = {
615
+ production: {
616
+ name: "Production",
617
+ apiKey: "",
618
+ baseUrl: "https://app.ctlsurf.com",
619
+ dataspacePageId: ""
620
+ }
621
+ };
622
+ var TERM_STREAM_INTERVAL_MS = 50;
623
+ var Orchestrator = class {
624
+ settingsDir;
625
+ events;
626
+ // Core services
627
+ ctlsurfApi = new CtlsurfApi();
628
+ bridge = new ConversationBridge(this.ctlsurfApi);
629
+ workerWs;
630
+ // State
631
+ ptyManager = null;
632
+ currentAgent = null;
633
+ currentCwd = null;
634
+ settings = {
635
+ activeProfile: "production",
636
+ profiles: { ...DEFAULT_PROFILES }
637
+ };
638
+ // Terminal stream batching
639
+ termStreamBuffer = "";
640
+ termStreamTimer = null;
641
+ constructor(settingsDir, events) {
642
+ this.settingsDir = settingsDir;
643
+ this.events = events;
644
+ this.workerWs = new WorkerWsClient({
645
+ onStatusChange: (status) => {
646
+ log2(`[worker-ws] Status: ${status}`);
647
+ events.onWorkerStatus(status);
648
+ },
649
+ onMessage: (message) => {
650
+ log2(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
651
+ events.onWorkerMessage(message);
652
+ this.workerWs.sendAck(message.id);
653
+ if (message.type === "prompt" || message.type === "task_dispatch") {
654
+ if (this.ptyManager) {
655
+ this.ptyManager.write(message.content + "\r");
656
+ this.bridge.feedInput(message.content);
657
+ }
658
+ }
659
+ },
660
+ onRegistered: (data) => {
661
+ log2(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
662
+ events.onWorkerRegistered(data);
663
+ if (!data.folder_id) {
664
+ events.onWorkerStatus("no_project");
665
+ }
666
+ },
667
+ onTerminalInput: (data) => {
668
+ this.ptyManager?.write(data);
669
+ }
670
+ });
671
+ }
672
+ // ─── Settings ───────────────────────────────────
673
+ getActiveProfile() {
674
+ return this.settings.profiles[this.settings.activeProfile] || this.settings.profiles.production || DEFAULT_PROFILES.production;
675
+ }
676
+ get settingsData() {
677
+ return this.settings;
678
+ }
679
+ get cwd() {
680
+ return this.currentCwd;
681
+ }
682
+ get agent() {
683
+ return this.currentAgent;
684
+ }
685
+ applyProfile(profile) {
686
+ const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY || "";
687
+ if (apiKey) {
688
+ this.ctlsurfApi.setApiKey(apiKey);
689
+ this.workerWs.setApiKey(apiKey);
690
+ } else {
691
+ this.ctlsurfApi.setApiKey("");
692
+ this.workerWs.setApiKey(null);
693
+ }
694
+ const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
695
+ this.ctlsurfApi.setBaseUrl(baseUrl);
696
+ this.workerWs.setBaseUrl(baseUrl);
697
+ log2(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
698
+ }
699
+ loadSettings() {
700
+ try {
701
+ fs.mkdirSync(this.settingsDir, { recursive: true });
702
+ } catch {
703
+ }
704
+ const settingsPath = path.join(this.settingsDir, "settings.json");
705
+ try {
706
+ if (fs.existsSync(settingsPath)) {
707
+ const raw = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
708
+ if (!raw.profiles) {
709
+ this.settings = {
710
+ activeProfile: "production",
711
+ profiles: {
712
+ production: {
713
+ name: "Production",
714
+ apiKey: raw.ctlsurfApiKey || "",
715
+ baseUrl: raw.ctlsurfBaseUrl || "https://app.ctlsurf.com",
716
+ dataspacePageId: raw.ctlsurfDataspacePageId || ""
717
+ }
718
+ }
719
+ };
720
+ this.saveSettings();
721
+ log2("[settings] Migrated legacy settings to profiles");
722
+ } else {
723
+ this.settings = raw;
724
+ if (!this.settings.profiles.production) {
725
+ this.settings.profiles.production = { ...DEFAULT_PROFILES.production };
726
+ }
727
+ }
728
+ }
729
+ } catch {
730
+ this.settings = {
731
+ activeProfile: "production",
732
+ profiles: { ...DEFAULT_PROFILES }
733
+ };
734
+ }
735
+ this.applyProfile(this.getActiveProfile());
736
+ }
737
+ saveSettings() {
738
+ const settingsPath = path.join(this.settingsDir, "settings.json");
739
+ try {
740
+ fs.mkdirSync(this.settingsDir, { recursive: true });
741
+ fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
742
+ } catch (err) {
743
+ log2("[settings] Failed to save:", err.message);
744
+ }
745
+ }
746
+ overrideApiKey(key) {
747
+ this.ctlsurfApi.setApiKey(key);
748
+ this.workerWs.setApiKey(key);
749
+ }
750
+ overrideBaseUrl(url) {
751
+ this.ctlsurfApi.setBaseUrl(url);
752
+ this.workerWs.setBaseUrl(url);
753
+ }
754
+ // ─── Profile CRUD ───────────────────────────────
755
+ listProfiles() {
756
+ return {
757
+ activeProfile: this.settings.activeProfile,
758
+ profiles: Object.entries(this.settings.profiles).map(([id, p]) => ({
759
+ id,
760
+ name: p.name,
761
+ baseUrl: p.baseUrl,
762
+ hasApiKey: !!p.apiKey,
763
+ dataspacePageId: p.dataspacePageId || null
764
+ }))
765
+ };
766
+ }
767
+ getProfile(profileId) {
768
+ const p = this.settings.profiles[profileId];
769
+ if (!p) return null;
770
+ return {
771
+ id: profileId,
772
+ name: p.name,
773
+ baseUrl: p.baseUrl,
774
+ hasApiKey: !!p.apiKey,
775
+ dataspacePageId: p.dataspacePageId || ""
776
+ };
777
+ }
778
+ saveProfile(profileId, data) {
779
+ const existing = this.settings.profiles[profileId];
780
+ this.settings.profiles[profileId] = {
781
+ name: data.name,
782
+ apiKey: data.apiKey !== void 0 ? data.apiKey : existing?.apiKey || "",
783
+ baseUrl: data.baseUrl || "https://app.ctlsurf.com",
784
+ dataspacePageId: data.dataspacePageId || ""
785
+ };
786
+ this.saveSettings();
787
+ if (profileId === this.settings.activeProfile) {
788
+ this.applyProfile(this.settings.profiles[profileId]);
789
+ if (this.currentAgent && this.currentCwd) {
790
+ this.workerWs.disconnect();
791
+ this.connectWorkerWs(this.currentAgent, this.currentCwd);
792
+ }
793
+ }
794
+ }
795
+ switchProfile(profileId) {
796
+ if (!this.settings.profiles[profileId]) return { ok: false, error: "Profile not found" };
797
+ this.workerWs.disconnect();
798
+ this.settings.activeProfile = profileId;
799
+ this.saveSettings();
800
+ this.applyProfile(this.getActiveProfile());
801
+ if (this.currentAgent && this.currentCwd) {
802
+ this.connectWorkerWs(this.currentAgent, this.currentCwd);
803
+ }
804
+ return { ok: true };
805
+ }
806
+ deleteProfile(profileId) {
807
+ if (profileId === "production") return { ok: false, error: "Cannot delete Production profile" };
808
+ if (!this.settings.profiles[profileId]) return { ok: false, error: "Profile not found" };
809
+ if (this.settings.activeProfile === profileId) {
810
+ this.workerWs.disconnect();
811
+ this.settings.activeProfile = "production";
812
+ this.applyProfile(this.getActiveProfile());
813
+ if (this.currentAgent && this.currentCwd) {
814
+ this.connectWorkerWs(this.currentAgent, this.currentCwd);
815
+ }
816
+ }
817
+ delete this.settings.profiles[profileId];
818
+ this.saveSettings();
819
+ return { ok: true };
820
+ }
821
+ // ─── PTY & Agent ────────────────────────────────
822
+ async spawnAgent(agent, cwd) {
823
+ if (this.ptyManager) {
824
+ await this.bridge.endSession();
825
+ this.ptyManager.kill();
826
+ }
827
+ this.currentAgent = agent;
828
+ const prevCwd = this.currentCwd;
829
+ this.currentCwd = cwd;
830
+ if (prevCwd !== cwd) {
831
+ this.events.onCwdChanged();
832
+ }
833
+ this.ptyManager = new PtyManager(agent, cwd);
834
+ this.ptyManager.onData((data) => {
835
+ this.events.onPtyData(data);
836
+ this.bridge.feedOutput(data);
837
+ this.streamTerminalData(data);
838
+ });
839
+ const thisPtyManager = this.ptyManager;
840
+ this.ptyManager.onExit(async (exitCode) => {
841
+ this.events.onPtyExit(exitCode);
842
+ await this.bridge.endSession(exitCode);
843
+ if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
844
+ this.workerWs.disconnect();
845
+ }
846
+ });
847
+ const profile = this.getActiveProfile();
848
+ const dataspacePageId = profile.dataspacePageId || process.env.CTLSURF_DATASPACE_PAGE_ID || "";
849
+ if (dataspacePageId && this.ctlsurfApi.getApiKey()) {
850
+ await this.bridge.startSession(dataspacePageId, agent.name, cwd);
851
+ }
852
+ if (isCodingAgent(agent)) {
853
+ this.connectWorkerWs(agent, cwd);
854
+ } else {
855
+ this.workerWs.disconnect();
856
+ this.checkProjectStatus(cwd);
857
+ }
858
+ }
859
+ writePty(data) {
860
+ this.ptyManager?.write(data);
861
+ this.bridge.feedInput(data);
862
+ }
863
+ resizePty(cols, rows) {
864
+ this.ptyManager?.resize(cols, rows);
865
+ this.workerWs.sendTerminalResize(cols, rows);
866
+ }
867
+ async killAgent() {
868
+ await this.bridge.endSession();
869
+ this.ptyManager?.kill();
870
+ this.ptyManager = null;
871
+ if (this.currentAgent && isCodingAgent(this.currentAgent)) {
872
+ this.workerWs.disconnect();
873
+ }
874
+ }
875
+ // ─── Worker WebSocket ───────────────────────────
876
+ connectWorkerWs(agent, cwd) {
877
+ const profile = this.getActiveProfile();
878
+ const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
879
+ if (!apiKey) {
880
+ log2("[worker-ws] No API key, skipping WS connect");
881
+ return;
882
+ }
883
+ this.workerWs.connect({
884
+ machine: os2.hostname(),
885
+ cwd,
886
+ agent: agent.name
887
+ });
888
+ }
889
+ async checkProjectStatus(cwd) {
890
+ if (!this.ctlsurfApi.getApiKey()) {
891
+ this.events.onWorkerStatus("no_project");
892
+ return;
893
+ }
894
+ try {
895
+ const folder = await this.ctlsurfApi.findFolderByPath(cwd);
896
+ if (!folder?.id) {
897
+ this.events.onWorkerStatus("no_project");
898
+ }
899
+ } catch {
900
+ this.events.onWorkerStatus("no_project");
901
+ }
902
+ }
903
+ streamTerminalData(data) {
904
+ this.termStreamBuffer += data;
905
+ if (!this.termStreamTimer) {
906
+ this.termStreamTimer = setTimeout(() => {
907
+ if (this.termStreamBuffer) {
908
+ this.workerWs.sendTerminalData(this.termStreamBuffer);
909
+ this.termStreamBuffer = "";
910
+ }
911
+ this.termStreamTimer = null;
912
+ }, TERM_STREAM_INTERVAL_MS);
913
+ }
914
+ }
915
+ // ─── Shutdown ───────────────────────────────────
916
+ async shutdown() {
917
+ await this.bridge.endSession();
918
+ this.ptyManager?.kill();
919
+ this.ptyManager = null;
920
+ this.workerWs.disconnect();
921
+ if (this.termStreamTimer) {
922
+ clearTimeout(this.termStreamTimer);
923
+ this.termStreamTimer = null;
924
+ }
925
+ }
926
+ };
927
+
928
+ // src/main/settingsDir.ts
929
+ import path2 from "path";
930
+ import os3 from "os";
931
+ function getSettingsDir(useElectron) {
932
+ if (useElectron) {
933
+ const { app } = require_electron();
934
+ return app.getPath("userData");
935
+ }
936
+ if (process.platform === "darwin") {
937
+ return path2.join(os3.homedir(), "Library", "Application Support", "ctlsurf-worker");
938
+ }
939
+ return path2.join(
940
+ process.env.XDG_CONFIG_HOME || path2.join(os3.homedir(), ".config"),
941
+ "ctlsurf-worker"
942
+ );
943
+ }
944
+
945
+ // src/main/tui.ts
946
+ var ESC = "\x1B";
947
+ var CSI = `${ESC}[`;
948
+ var BG_BAR = `${CSI}48;2;22;22;30m`;
949
+ var FG_TITLE = `${CSI}38;2;192;202;245m`;
950
+ var FG_DIM = `${CSI}38;2;86;95;137m`;
951
+ var FG_ACCENT = `${CSI}38;2;122;162;247m`;
952
+ var FG_GREEN = `${CSI}38;2;158;206;106m`;
953
+ var FG_RED = `${CSI}38;2;247;118;142m`;
954
+ var FG_YELLOW = `${CSI}38;2;224;175;104m`;
955
+ var FG_WHITE = `${CSI}38;2;169;177;214m`;
956
+ var BG_MODAL = `${CSI}48;2;31;35;53m`;
957
+ var BG_SELECTED = `${CSI}48;2;42;43;61m`;
958
+ var RESET = `${CSI}0m`;
959
+ var Tui = class {
960
+ rows = 0;
961
+ cols = 0;
962
+ state = {
963
+ agentName: "",
964
+ cwd: "",
965
+ wsStatus: "disconnected",
966
+ workerId: null,
967
+ mode: "terminal"
968
+ };
969
+ constructor() {
970
+ this.rows = process.stdout.rows || 24;
971
+ this.cols = process.stdout.columns || 80;
972
+ }
973
+ /**
974
+ * Initialize the TUI: alternate screen, hide cursor reporting, set scroll region
975
+ */
976
+ init() {
977
+ this.write(`${CSI}?1049h`);
978
+ this.setScrollRegion();
979
+ this.moveToPtyArea();
980
+ this.drawTitleBar();
981
+ this.drawStatusBar();
982
+ }
983
+ /**
984
+ * Restore terminal to normal state
985
+ */
986
+ destroy() {
987
+ this.write(`${CSI}r`);
988
+ this.write(`${CSI}?1049l`);
989
+ this.write(`${CSI}?25h`);
990
+ }
991
+ /**
992
+ * Handle terminal resize
993
+ */
994
+ resize(cols, rows) {
995
+ this.cols = cols;
996
+ this.rows = rows;
997
+ this.setScrollRegion();
998
+ this.drawTitleBar();
999
+ this.drawStatusBar();
1000
+ this.moveToPtyArea();
1001
+ }
1002
+ /**
1003
+ * Get the PTY dimensions (main area minus title + status bars)
1004
+ */
1005
+ getPtySize() {
1006
+ return {
1007
+ cols: this.cols,
1008
+ rows: Math.max(1, this.rows - 2)
1009
+ };
1010
+ }
1011
+ /**
1012
+ * Update state and redraw bars
1013
+ */
1014
+ update(partial) {
1015
+ Object.assign(this.state, partial);
1016
+ this.write(`${CSI}s`);
1017
+ this.drawTitleBar();
1018
+ this.drawStatusBar();
1019
+ this.write(`${CSI}u`);
1020
+ }
1021
+ /**
1022
+ * Write PTY output to the scroll region.
1023
+ * Cursor is assumed to be in the PTY area already.
1024
+ */
1025
+ writePtyData(data) {
1026
+ this.write(data);
1027
+ }
1028
+ /**
1029
+ * Update the terminal window/tab title via OSC escape sequence.
1030
+ * Works in passthrough mode (no chrome) — doesn't conflict with the agent's TUI.
1031
+ */
1032
+ setTerminalTitle(title) {
1033
+ this.write(`${ESC}]0;${title}\x07`);
1034
+ }
1035
+ /**
1036
+ * Build a title string from current state for the terminal tab.
1037
+ */
1038
+ updateTerminalTitle() {
1039
+ const { agentName, wsStatus, cwd } = this.state;
1040
+ const displayCwd = this.shortenPath(cwd);
1041
+ const statusIcon = {
1042
+ connected: "\u25CF",
1043
+ connecting: "\u25CB",
1044
+ disconnected: "\u25CB",
1045
+ pending_approval: "\u25CB",
1046
+ no_project: "\u25CB"
1047
+ }[wsStatus] || "\u25CB";
1048
+ const statusLabel = {
1049
+ connected: "Connected",
1050
+ connecting: "Connecting...",
1051
+ disconnected: "Disconnected",
1052
+ pending_approval: "Pending",
1053
+ no_project: "No Project"
1054
+ }[wsStatus] || wsStatus;
1055
+ this.setTerminalTitle(`ctlsurf \xB7 ${agentName} \xB7 ${statusIcon} ${statusLabel} \xB7 ${displayCwd}`);
1056
+ }
1057
+ /**
1058
+ * Show an interactive agent picker modal.
1059
+ * Returns a promise that resolves with the selected agent index.
1060
+ */
1061
+ showAgentPicker(agents) {
1062
+ return new Promise((resolve) => {
1063
+ let selected = 0;
1064
+ const modalWidth = 44;
1065
+ const modalHeight = agents.length + 4;
1066
+ const startCol = Math.max(1, Math.floor((this.cols - modalWidth) / 2));
1067
+ const startRow = Math.max(1, Math.floor((this.rows - modalHeight) / 2));
1068
+ this.write(`${CSI}?1049h`);
1069
+ this.write(`${CSI}?25l`);
1070
+ const drawModal = () => {
1071
+ const topBorder = "\u250C" + "\u2500".repeat(modalWidth - 2) + "\u2510";
1072
+ const botBorder = "\u2514" + "\u2500".repeat(modalWidth - 2) + "\u2518";
1073
+ const emptyLine = "\u2502" + " ".repeat(modalWidth - 2) + "\u2502";
1074
+ for (let r = 0; r < this.rows; r++) {
1075
+ this.write(`${CSI}${r + 1};1H${BG_BAR}${CSI}2K${RESET}`);
1076
+ }
1077
+ const brand = "ctlsurf";
1078
+ const brandCol = Math.max(1, Math.floor((this.cols - brand.length) / 2));
1079
+ this.write(`${CSI}${startRow - 2};${brandCol}H${FG_ACCENT}${brand}${RESET}`);
1080
+ this.write(`${CSI}${startRow};${startCol}H${BG_MODAL}${FG_DIM}${topBorder}${RESET}`);
1081
+ const title = " Select Agent";
1082
+ const titlePad = " ".repeat(Math.max(0, modalWidth - 2 - title.length));
1083
+ this.write(`${CSI}${startRow + 1};${startCol}H${BG_MODAL}${FG_DIM}\u2502${RESET}${BG_MODAL}${FG_TITLE}${title}${titlePad}${FG_DIM}\u2502${RESET}`);
1084
+ const sep = "\u251C" + "\u2500".repeat(modalWidth - 2) + "\u2524";
1085
+ this.write(`${CSI}${startRow + 2};${startCol}H${BG_MODAL}${FG_DIM}${sep}${RESET}`);
1086
+ for (let i = 0; i < agents.length; i++) {
1087
+ const agent = agents[i];
1088
+ const row = startRow + 3 + i;
1089
+ const isSelected = i === selected;
1090
+ const bg = isSelected ? BG_SELECTED : BG_MODAL;
1091
+ const pointer = isSelected ? `${FG_ACCENT}\u25B8 ` : " ";
1092
+ const nameFg = isSelected ? FG_ACCENT : FG_WHITE;
1093
+ const descFg = FG_DIM;
1094
+ const nameStr = agent.name;
1095
+ const descStr = agent.description ? ` ${FG_DIM}\u2014 ${agent.description.slice(0, 20)}` : "";
1096
+ const content = `${pointer}${nameFg}${nameStr}${descStr}`;
1097
+ const contentLen = (isSelected ? 2 : 2) + nameStr.length + (agent.description ? 3 + Math.min(20, agent.description.length) : 0);
1098
+ const pad = " ".repeat(Math.max(0, modalWidth - 2 - contentLen));
1099
+ this.write(`${CSI}${row};${startCol}H${bg}${FG_DIM}\u2502${RESET}${bg}${content}${pad}${RESET}${BG_MODAL}${FG_DIM}\u2502${RESET}`);
1100
+ }
1101
+ const botRow = startRow + 3 + agents.length;
1102
+ this.write(`${CSI}${botRow};${startCol}H${BG_MODAL}${FG_DIM}${botBorder}${RESET}`);
1103
+ const hint = "\u2191\u2193 navigate \xB7 Enter select \xB7 q quit";
1104
+ const hintCol = Math.max(1, Math.floor((this.cols - hint.length) / 2));
1105
+ this.write(`${CSI}${botRow + 2};${hintCol}H${FG_DIM}${hint}${RESET}`);
1106
+ };
1107
+ drawModal();
1108
+ if (process.stdin.isTTY) {
1109
+ process.stdin.setRawMode(true);
1110
+ }
1111
+ process.stdin.resume();
1112
+ const onKey = (data) => {
1113
+ const key = data.toString();
1114
+ if (key === "\x1B[A" || key === "k") {
1115
+ selected = (selected - 1 + agents.length) % agents.length;
1116
+ drawModal();
1117
+ } else if (key === "\x1B[B" || key === "j") {
1118
+ selected = (selected + 1) % agents.length;
1119
+ drawModal();
1120
+ } else if (key === "\r" || key === "\n") {
1121
+ cleanup();
1122
+ resolve(selected);
1123
+ } else if (key === "q" || key === "\x1B" || key === "") {
1124
+ cleanup();
1125
+ this.write(`${CSI}?25h`);
1126
+ this.write(`${CSI}?1049l`);
1127
+ process.exit(0);
1128
+ }
1129
+ };
1130
+ const cleanup = () => {
1131
+ process.stdin.removeListener("data", onKey);
1132
+ this.write(`${CSI}?25h`);
1133
+ };
1134
+ process.stdin.on("data", onKey);
1135
+ });
1136
+ }
1137
+ // ─── Internal ───────────────────────────────────
1138
+ write(data) {
1139
+ try {
1140
+ process.stdout.write(data);
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ setScrollRegion() {
1145
+ this.write(`${CSI}2;${this.rows - 1}r`);
1146
+ }
1147
+ moveToPtyArea() {
1148
+ this.write(`${CSI}2;1H`);
1149
+ }
1150
+ drawTitleBar() {
1151
+ const { agentName, cwd, wsStatus } = this.state;
1152
+ this.write(`${CSI}1;1H`);
1153
+ this.write(`${BG_BAR}${CSI}2K`);
1154
+ const displayCwd = this.shortenPath(cwd);
1155
+ const statusColor = {
1156
+ connected: FG_GREEN,
1157
+ connecting: FG_YELLOW,
1158
+ disconnected: FG_RED,
1159
+ pending_approval: FG_YELLOW,
1160
+ no_project: FG_DIM
1161
+ }[wsStatus] || FG_DIM;
1162
+ const statusLabel = {
1163
+ connected: "Connected",
1164
+ connecting: "Connecting...",
1165
+ disconnected: "Disconnected",
1166
+ pending_approval: "Pending",
1167
+ no_project: "No Project"
1168
+ }[wsStatus] || wsStatus;
1169
+ const wsIndicator = `${statusColor}\u25CF ${statusLabel}${RESET}${BG_BAR}`;
1170
+ const left = ` ${FG_ACCENT}ctlsurf${RESET}${BG_BAR} ${FG_DIM}\u2502${RESET}${BG_BAR} ${FG_TITLE}${agentName || "starting..."}${RESET}${BG_BAR} ${FG_DIM}\u2502${RESET}${BG_BAR} ${FG_DIM}${displayCwd}${RESET}${BG_BAR}`;
1171
+ const right = `${wsIndicator} ${RESET}${BG_BAR}`;
1172
+ this.write(left);
1173
+ const pad = Math.max(0, this.cols - this.visibleLen(left) - this.visibleLen(right));
1174
+ this.write(" ".repeat(pad));
1175
+ this.write(right);
1176
+ this.write(RESET);
1177
+ }
1178
+ drawStatusBar() {
1179
+ const { wsStatus, workerId, cwd } = this.state;
1180
+ this.write(`${CSI}${this.rows};1H`);
1181
+ this.write(`${BG_BAR}${CSI}2K`);
1182
+ const statusColor = {
1183
+ connected: FG_GREEN,
1184
+ connecting: FG_YELLOW,
1185
+ disconnected: FG_RED,
1186
+ pending_approval: FG_YELLOW,
1187
+ no_project: FG_DIM
1188
+ }[wsStatus] || FG_DIM;
1189
+ const statusDot = `${statusColor}\u25CF${RESET}${BG_BAR}`;
1190
+ const statusLabel = {
1191
+ connected: "Connected",
1192
+ connecting: "Connecting...",
1193
+ disconnected: "Disconnected",
1194
+ pending_approval: "Pending Approval",
1195
+ no_project: "No Project"
1196
+ }[wsStatus] || wsStatus;
1197
+ const displayCwd = this.shortenPath(cwd);
1198
+ const left = ` ${statusDot} ${FG_DIM}${statusLabel}${RESET}${BG_BAR}`;
1199
+ const right = `${FG_DIM}Ctrl+\\ exit${RESET}${BG_BAR} ${FG_DIM}${displayCwd} ${RESET}${BG_BAR}`;
1200
+ this.write(left);
1201
+ const pad = Math.max(0, this.cols - this.visibleLen(left) - this.visibleLen(right));
1202
+ this.write(" ".repeat(pad));
1203
+ this.write(right);
1204
+ this.write(RESET);
1205
+ }
1206
+ shortenPath(p) {
1207
+ if (!p) return "";
1208
+ const home = process.env.HOME || "";
1209
+ if (home && p.startsWith(home)) {
1210
+ return "~" + p.slice(home.length);
1211
+ }
1212
+ return p;
1213
+ }
1214
+ visibleLen(s) {
1215
+ return s.replace(/\x1b\[[^m]*m/g, "").length;
1216
+ }
1217
+ };
1218
+
1219
+ // src/main/headless.ts
1220
+ process.stdout?.on?.("error", () => {
1221
+ });
1222
+ process.stderr?.on?.("error", () => {
1223
+ });
1224
+ process.on("uncaughtException", (err) => {
1225
+ if (err.message === "write EPIPE") return;
1226
+ try {
1227
+ console.error("[uncaught]", err);
1228
+ } catch {
1229
+ }
1230
+ });
1231
+ function parseArgs(argv) {
1232
+ const args = {
1233
+ agent: null,
1234
+ cwd: process.env.CTLSURF_WORKER_CWD || process.cwd(),
1235
+ apiKey: null,
1236
+ baseUrl: null,
1237
+ profile: null
1238
+ };
1239
+ for (let i = 0; i < argv.length; i++) {
1240
+ const arg = argv[i];
1241
+ const next = argv[i + 1];
1242
+ switch (arg) {
1243
+ case "--agent":
1244
+ args.agent = next;
1245
+ i++;
1246
+ break;
1247
+ case "--cwd":
1248
+ args.cwd = next;
1249
+ i++;
1250
+ break;
1251
+ case "--api-key":
1252
+ args.apiKey = next;
1253
+ i++;
1254
+ break;
1255
+ case "--base-url":
1256
+ args.baseUrl = next;
1257
+ i++;
1258
+ break;
1259
+ case "--profile":
1260
+ args.profile = next;
1261
+ i++;
1262
+ break;
1263
+ case "--terminal":
1264
+ break;
1265
+ case "--desktop":
1266
+ break;
1267
+ }
1268
+ }
1269
+ return args;
1270
+ }
1271
+ async function main() {
1272
+ const args = parseArgs(process.argv.slice(2));
1273
+ const settingsDir = getSettingsDir(false);
1274
+ const tui = new Tui();
1275
+ const agents = getBuiltinAgents();
1276
+ let agent;
1277
+ if (args.agent) {
1278
+ const found = agents.find((a) => a.id === args.agent);
1279
+ agent = found || {
1280
+ id: args.agent,
1281
+ name: args.agent,
1282
+ command: args.agent,
1283
+ args: [],
1284
+ description: `Custom agent: ${args.agent}`
1285
+ };
1286
+ } else {
1287
+ const selectedIdx = await tui.showAgentPicker(agents);
1288
+ agent = agents[selectedIdx];
1289
+ }
1290
+ tui.update({
1291
+ agentName: agent.name,
1292
+ cwd: args.cwd,
1293
+ mode: "terminal"
1294
+ });
1295
+ tui.init();
1296
+ const orchestrator = new Orchestrator(settingsDir, {
1297
+ onPtyData: (data) => {
1298
+ tui.writePtyData(data);
1299
+ },
1300
+ onPtyExit: (code) => {
1301
+ tui.destroy();
1302
+ console.log(`Agent exited with code ${code}`);
1303
+ orchestrator.shutdown().then(() => process.exit(code));
1304
+ },
1305
+ onWorkerStatus: (status) => {
1306
+ tui.update({ wsStatus: status });
1307
+ },
1308
+ onWorkerMessage: () => {
1309
+ },
1310
+ onWorkerRegistered: () => {
1311
+ tui.update({ wsStatus: "connected" });
1312
+ },
1313
+ onCwdChanged: () => {
1314
+ tui.update({ cwd: orchestrator.cwd || "" });
1315
+ }
1316
+ });
1317
+ orchestrator.loadSettings();
1318
+ if (args.profile) orchestrator.switchProfile(args.profile);
1319
+ if (args.apiKey) orchestrator.overrideApiKey(args.apiKey);
1320
+ if (args.baseUrl) orchestrator.overrideBaseUrl(args.baseUrl);
1321
+ const ptySize = tui.getPtySize();
1322
+ await orchestrator.spawnAgent(agent, args.cwd);
1323
+ orchestrator.resizePty(ptySize.cols, ptySize.rows);
1324
+ if (isCodingAgent(agent)) {
1325
+ setTimeout(() => {
1326
+ orchestrator.writePty("hello\r");
1327
+ }, 1e3);
1328
+ }
1329
+ if (process.stdin.isTTY) {
1330
+ process.stdin.setRawMode(true);
1331
+ process.stdin.resume();
1332
+ process.stdin.on("data", (data) => {
1333
+ const str = data.toString();
1334
+ if (str === "") {
1335
+ shutdown();
1336
+ return;
1337
+ }
1338
+ orchestrator.writePty(str);
1339
+ });
1340
+ }
1341
+ process.stdout.on("resize", () => {
1342
+ const cols = process.stdout.columns || 80;
1343
+ const rows = process.stdout.rows || 24;
1344
+ tui.resize(cols, rows);
1345
+ const size = tui.getPtySize();
1346
+ orchestrator.resizePty(size.cols, size.rows);
1347
+ });
1348
+ const shutdown = async () => {
1349
+ if (process.stdin.isTTY) {
1350
+ process.stdin.setRawMode(false);
1351
+ }
1352
+ tui.destroy();
1353
+ await orchestrator.shutdown();
1354
+ process.exit(0);
1355
+ };
1356
+ process.on("SIGINT", shutdown);
1357
+ process.on("SIGTERM", shutdown);
1358
+ }
1359
+ main().catch((err) => {
1360
+ process.stdout.write("\x1B[?1049l");
1361
+ console.error("Fatal error:", err);
1362
+ process.exit(1);
1363
+ });
1364
+ //# sourceMappingURL=index.mjs.map