@phenx-inc/ctlsurf 0.1.21 → 0.2.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 (31) hide show
  1. package/out/headless/index.mjs +91 -57
  2. package/out/headless/index.mjs.map +2 -2
  3. package/out/main/index.js +145 -63
  4. package/out/preload/index.js +9 -8
  5. package/out/renderer/assets/{cssMode-C6bY9C4O.js → cssMode-D3kH1Kju.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CkAJiX1K.js → freemarker2-BCHZUSLb.js} +1 -1
  7. package/out/renderer/assets/{handlebars-DnLXVUXp.js → handlebars-DKx-Fw-H.js} +1 -1
  8. package/out/renderer/assets/{html-Ds5-qvDh.js → html-BSCM04uL.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-DYFYy4MK.js → htmlMode-BucU1MUc.js} +3 -3
  10. package/out/renderer/assets/{index-DwSsD_Xm.js → index-BsdOeO0U.js} +230 -101
  11. package/out/renderer/assets/{index-DK9wLFFm.css → index-BzF7I1my.css} +111 -0
  12. package/out/renderer/assets/{javascript-CiHhG2a9.js → javascript-bPY5C4uq.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-DdDRlbXP.js → jsonMode-BmJotb6E.js} +3 -3
  14. package/out/renderer/assets/{liquid-BP5mb-uD.js → liquid-Cja_Pzh3.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-Dljhj5Gh.js → lspLanguageFeatures-hoVZfVKv.js} +1 -1
  16. package/out/renderer/assets/{mdx-D4u3N7dt.js → mdx-C0s81MOq.js} +1 -1
  17. package/out/renderer/assets/{python-BQDHXVwp.js → python-CulkBOJr.js} +1 -1
  18. package/out/renderer/assets/{razor-BfXW9cDc.js → razor-czmzhwVZ.js} +1 -1
  19. package/out/renderer/assets/{tsMode-BGTjG8Ow.js → tsMode-B90EqYGx.js} +1 -1
  20. package/out/renderer/assets/{typescript-422MU_YO.js → typescript-Ckc6emP2.js} +1 -1
  21. package/out/renderer/assets/{xml-B6EKhHiy.js → xml-CKh-JyGN.js} +1 -1
  22. package/out/renderer/assets/{yaml-LkO_eGYb.js → yaml-B49zLim4.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/headless.ts +8 -7
  26. package/src/main/index.ts +87 -13
  27. package/src/main/orchestrator.ts +98 -54
  28. package/src/preload/index.ts +16 -14
  29. package/src/renderer/App.tsx +161 -43
  30. package/src/renderer/components/TerminalPanel.tsx +101 -59
  31. package/src/renderer/styles.css +111 -0
package/out/main/index.js CHANGED
@@ -4454,16 +4454,14 @@ class Orchestrator {
4454
4454
  bridge = new ConversationBridge();
4455
4455
  workerWs;
4456
4456
  // State
4457
- ptyManager = null;
4457
+ tabs = /* @__PURE__ */ new Map();
4458
+ activeTabId = null;
4458
4459
  currentAgent = null;
4459
4460
  currentCwd = null;
4460
4461
  settings = {
4461
4462
  activeProfile: "production",
4462
4463
  profiles: { ...DEFAULT_PROFILES }
4463
4464
  };
4464
- // Terminal stream batching
4465
- termStreamBuffer = "";
4466
- termStreamTimer = null;
4467
4465
  constructor(settingsDir, events) {
4468
4466
  this.settingsDir = settingsDir;
4469
4467
  this.events = events;
@@ -4477,8 +4475,9 @@ class Orchestrator {
4477
4475
  events.onWorkerMessage(message);
4478
4476
  this.workerWs.sendAck(message.id);
4479
4477
  if (message.type === "prompt" || message.type === "task_dispatch") {
4480
- if (this.ptyManager) {
4481
- this.ptyManager.write(message.content + "\r");
4478
+ const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
4479
+ if (activeTab) {
4480
+ activeTab.ptyManager.write(message.content + "\r");
4482
4481
  this.bridge.feedInput(message.content);
4483
4482
  }
4484
4483
  }
@@ -4491,7 +4490,8 @@ class Orchestrator {
4491
4490
  }
4492
4491
  },
4493
4492
  onTerminalInput: (data) => {
4494
- this.ptyManager?.write(data);
4493
+ const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null;
4494
+ activeTab?.ptyManager.write(data);
4495
4495
  }
4496
4496
  });
4497
4497
  this.bridge.setWsClient(this.workerWs);
@@ -4645,31 +4645,41 @@ class Orchestrator {
4645
4645
  this.saveSettings();
4646
4646
  return { ok: true };
4647
4647
  }
4648
- // ─── PTY & Agent ────────────────────────────────
4649
- async spawnAgent(agent, cwd) {
4650
- if (this.ptyManager) {
4651
- this.bridge.endSession();
4652
- this.ptyManager.kill();
4648
+ // ─── PTY & Agent (multi-tab) ─────────────────────
4649
+ async spawnAgent(tabId, agent, cwd) {
4650
+ const existing = this.tabs.get(tabId);
4651
+ if (existing) {
4652
+ if (existing.termStreamTimer) clearTimeout(existing.termStreamTimer);
4653
+ existing.ptyManager.kill();
4654
+ this.tabs.delete(tabId);
4653
4655
  }
4654
4656
  this.currentAgent = agent;
4655
4657
  const prevCwd = this.currentCwd;
4656
4658
  this.currentCwd = cwd;
4659
+ this.activeTabId = tabId;
4657
4660
  if (prevCwd !== cwd) {
4658
4661
  this.events.onCwdChanged();
4659
4662
  }
4660
- this.ptyManager = new PtyManager(agent, cwd);
4661
- this.ptyManager.onData((data) => {
4662
- this.events.onPtyData(data);
4663
- this.bridge.feedOutput(data);
4664
- this.streamTerminalData(data);
4663
+ const ptyManager = new PtyManager(agent, cwd);
4664
+ const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
4665
+ this.tabs.set(tabId, tab);
4666
+ ptyManager.onData((data) => {
4667
+ this.events.onPtyData(tabId, data);
4668
+ if (tabId === this.activeTabId) {
4669
+ this.bridge.feedOutput(data);
4670
+ this.streamTerminalData(tabId, data);
4671
+ }
4665
4672
  });
4666
- const thisPtyManager = this.ptyManager;
4667
- this.ptyManager.onExit(async (exitCode) => {
4668
- this.events.onPtyExit(exitCode);
4669
- this.bridge.endSession();
4670
- if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
4671
- this.workerWs.disconnect();
4673
+ ptyManager.onExit(async (exitCode) => {
4674
+ this.events.onPtyExit(tabId, exitCode);
4675
+ if (tabId === this.activeTabId) {
4676
+ this.bridge.endSession();
4677
+ if (this.currentAgent && isCodingAgent(this.currentAgent)) {
4678
+ this.workerWs.disconnect();
4679
+ }
4672
4680
  }
4681
+ const t = this.tabs.get(tabId);
4682
+ if (t?.termStreamTimer) clearTimeout(t.termStreamTimer);
4673
4683
  });
4674
4684
  this.bridge.startSession();
4675
4685
  if (isCodingAgent(agent)) {
@@ -4679,23 +4689,45 @@ class Orchestrator {
4679
4689
  this.checkProjectStatus(cwd);
4680
4690
  }
4681
4691
  }
4682
- writePty(data) {
4683
- this.ptyManager?.write(data);
4684
- this.bridge.feedInput(data);
4692
+ writePty(tabId, data) {
4693
+ this.tabs.get(tabId)?.ptyManager.write(data);
4694
+ if (tabId === this.activeTabId) {
4695
+ this.bridge.feedInput(data);
4696
+ }
4685
4697
  }
4686
- resizePty(cols, rows) {
4687
- this.ptyManager?.resize(cols, rows);
4688
- this.bridge.resize(cols, rows);
4689
- this.workerWs.sendTerminalResize(cols, rows);
4698
+ resizePty(tabId, cols, rows) {
4699
+ this.tabs.get(tabId)?.ptyManager.resize(cols, rows);
4700
+ if (tabId === this.activeTabId) {
4701
+ this.bridge.resize(cols, rows);
4702
+ this.workerWs.sendTerminalResize(cols, rows);
4703
+ }
4690
4704
  }
4691
- async killAgent() {
4692
- this.bridge.endSession();
4693
- this.ptyManager?.kill();
4694
- this.ptyManager = null;
4695
- if (this.currentAgent && isCodingAgent(this.currentAgent)) {
4696
- this.workerWs.disconnect();
4705
+ async killTab(tabId) {
4706
+ const tab = this.tabs.get(tabId);
4707
+ if (!tab) return;
4708
+ if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
4709
+ tab.ptyManager.kill();
4710
+ this.tabs.delete(tabId);
4711
+ if (tabId === this.activeTabId) {
4712
+ this.bridge.endSession();
4713
+ if (isCodingAgent(tab.agent)) {
4714
+ this.workerWs.disconnect();
4715
+ }
4716
+ const remaining = [...this.tabs.keys()];
4717
+ this.activeTabId = remaining.length > 0 ? remaining[remaining.length - 1] : null;
4718
+ }
4719
+ }
4720
+ setActiveTab(tabId) {
4721
+ this.activeTabId = tabId;
4722
+ const tab = this.tabs.get(tabId);
4723
+ if (tab) {
4724
+ this.currentAgent = tab.agent;
4725
+ this.currentCwd = tab.cwd;
4697
4726
  }
4698
4727
  }
4728
+ getTabIds() {
4729
+ return [...this.tabs.keys()];
4730
+ }
4699
4731
  // ─── Worker WebSocket ───────────────────────────
4700
4732
  connectWorkerWs(agent, cwd) {
4701
4733
  const profile = this.getActiveProfile();
@@ -4724,28 +4756,29 @@ class Orchestrator {
4724
4756
  this.events.onWorkerStatus("no_project");
4725
4757
  }
4726
4758
  }
4727
- streamTerminalData(data) {
4728
- this.termStreamBuffer += data;
4729
- if (!this.termStreamTimer) {
4730
- this.termStreamTimer = setTimeout(() => {
4731
- if (this.termStreamBuffer) {
4732
- this.workerWs.sendTerminalData(this.termStreamBuffer);
4733
- this.termStreamBuffer = "";
4759
+ streamTerminalData(tabId, data) {
4760
+ const tab = this.tabs.get(tabId);
4761
+ if (!tab) return;
4762
+ tab.termStreamBuffer += data;
4763
+ if (!tab.termStreamTimer) {
4764
+ tab.termStreamTimer = setTimeout(() => {
4765
+ if (tab.termStreamBuffer) {
4766
+ this.workerWs.sendTerminalData(tab.termStreamBuffer);
4767
+ tab.termStreamBuffer = "";
4734
4768
  }
4735
- this.termStreamTimer = null;
4769
+ tab.termStreamTimer = null;
4736
4770
  }, TERM_STREAM_INTERVAL_MS);
4737
4771
  }
4738
4772
  }
4739
4773
  // ─── Shutdown ───────────────────────────────────
4740
4774
  async shutdown() {
4741
4775
  this.bridge.endSession();
4742
- this.ptyManager?.kill();
4743
- this.ptyManager = null;
4744
- this.workerWs.disconnect();
4745
- if (this.termStreamTimer) {
4746
- clearTimeout(this.termStreamTimer);
4747
- this.termStreamTimer = null;
4776
+ for (const [, tab] of this.tabs) {
4777
+ if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer);
4778
+ tab.ptyManager.kill();
4748
4779
  }
4780
+ this.tabs.clear();
4781
+ this.workerWs.disconnect();
4749
4782
  }
4750
4783
  }
4751
4784
  process.stdout?.on?.("error", () => {
@@ -4766,15 +4799,62 @@ function log(...args) {
4766
4799
  }
4767
4800
  }
4768
4801
  let mainWindow = null;
4802
+ function getProjectAbbrev(cwdPath) {
4803
+ const folderName = cwdPath.split("/").filter(Boolean).pop() || "";
4804
+ if (!folderName) return "";
4805
+ const words = folderName.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[-_.\s]+/).filter(Boolean);
4806
+ if (words.length >= 2) {
4807
+ return words.slice(0, 4).map((w) => w[0]).join("").toUpperCase();
4808
+ }
4809
+ const word = words[0];
4810
+ if (word.length <= 3) return word.toUpperCase();
4811
+ return word.slice(0, 2).toUpperCase();
4812
+ }
4813
+ function createOverlayIcon(text) {
4814
+ const size = 32;
4815
+ const canvas = Buffer.alloc(size * size * 4, 0);
4816
+ for (let y = 0; y < size; y++) {
4817
+ for (let x = 0; x < size; x++) {
4818
+ const cx = x - size / 2;
4819
+ const cy = y - size / 2;
4820
+ const dist = Math.sqrt(cx * cx + cy * cy);
4821
+ if (dist <= size / 2) {
4822
+ const idx = (y * size + x) * 4;
4823
+ canvas[idx] = 122;
4824
+ canvas[idx + 1] = 162;
4825
+ canvas[idx + 2] = 247;
4826
+ canvas[idx + 3] = 220;
4827
+ }
4828
+ }
4829
+ }
4830
+ return electron.nativeImage.createFromBuffer(canvas, { width: size, height: size });
4831
+ }
4832
+ function updateProjectBadge(cwdPath) {
4833
+ if (!cwdPath) return;
4834
+ const abbrev = getProjectAbbrev(cwdPath);
4835
+ if (process.platform === "darwin" && electron.app.dock) {
4836
+ electron.app.dock.setBadge(abbrev);
4837
+ }
4838
+ if (process.platform === "win32" && mainWindow) {
4839
+ mainWindow.setOverlayIcon(createOverlayIcon(), abbrev);
4840
+ }
4841
+ if (mainWindow) {
4842
+ const folderName = cwdPath.split("/").filter(Boolean).pop() || "ctlsurf-worker";
4843
+ mainWindow.setTitle(`ctlsurf-worker — ${folderName}`);
4844
+ }
4845
+ }
4769
4846
  const orchestrator = new Orchestrator(
4770
4847
  getSettingsDir(),
4771
4848
  {
4772
- onPtyData: (data) => mainWindow?.webContents.send("pty:data", data),
4773
- onPtyExit: (code) => mainWindow?.webContents.send("pty:exit", code),
4849
+ onPtyData: (tabId, data) => mainWindow?.webContents.send("pty:data", tabId, data),
4850
+ onPtyExit: (tabId, code) => mainWindow?.webContents.send("pty:exit", tabId, code),
4774
4851
  onWorkerStatus: (status) => mainWindow?.webContents.send("worker:status", status),
4775
4852
  onWorkerMessage: (message) => mainWindow?.webContents.send("worker:message", message),
4776
4853
  onWorkerRegistered: (data) => mainWindow?.webContents.send("worker:registered", data),
4777
- onCwdChanged: () => mainWindow?.webContents.send("app:cwdChanged")
4854
+ onCwdChanged: () => {
4855
+ mainWindow?.webContents.send("app:cwdChanged");
4856
+ updateProjectBadge(orchestrator.cwd);
4857
+ }
4778
4858
  }
4779
4859
  );
4780
4860
  function createWindow() {
@@ -4802,18 +4882,21 @@ function createWindow() {
4802
4882
  mainWindow = null;
4803
4883
  });
4804
4884
  }
4805
- electron.ipcMain.handle("pty:spawn", async (_event, agent, cwd) => {
4806
- await orchestrator.spawnAgent(agent, cwd);
4885
+ electron.ipcMain.handle("pty:spawn", async (_event, tabId, agent, cwd) => {
4886
+ await orchestrator.spawnAgent(tabId, agent, cwd);
4807
4887
  return { ok: true };
4808
4888
  });
4809
- electron.ipcMain.handle("pty:write", (_event, data) => {
4810
- orchestrator.writePty(data);
4889
+ electron.ipcMain.handle("pty:write", (_event, tabId, data) => {
4890
+ orchestrator.writePty(tabId, data);
4891
+ });
4892
+ electron.ipcMain.handle("pty:resize", (_event, tabId, cols, rows) => {
4893
+ orchestrator.resizePty(tabId, cols, rows);
4811
4894
  });
4812
- electron.ipcMain.handle("pty:resize", (_event, cols, rows) => {
4813
- orchestrator.resizePty(cols, rows);
4895
+ electron.ipcMain.handle("pty:kill", async (_event, tabId) => {
4896
+ await orchestrator.killTab(tabId);
4814
4897
  });
4815
- electron.ipcMain.handle("pty:kill", async () => {
4816
- await orchestrator.killAgent();
4898
+ electron.ipcMain.handle("pty:setActiveTab", (_event, tabId) => {
4899
+ orchestrator.setActiveTab(tabId);
4817
4900
  });
4818
4901
  electron.ipcMain.handle("agents:list", () => getBuiltinAgents());
4819
4902
  electron.ipcMain.handle("agents:default", () => getDefaultAgent());
@@ -4975,8 +5058,7 @@ electron.app.whenReady().then(() => {
4975
5058
  if (process.platform === "darwin" && electron.app.dock) {
4976
5059
  const iconPath = path.join(__dirname, "../../resources/icon.png");
4977
5060
  try {
4978
- const { nativeImage } = require("electron");
4979
- electron.app.dock.setIcon(nativeImage.createFromPath(iconPath));
5061
+ electron.app.dock.setIcon(electron.nativeImage.createFromPath(iconPath));
4980
5062
  } catch {
4981
5063
  }
4982
5064
  }
@@ -1,19 +1,20 @@
1
1
  "use strict";
2
2
  const electron = require("electron");
3
3
  const api = {
4
- // Pty operations
5
- spawnAgent: (agent, cwd) => electron.ipcRenderer.invoke("pty:spawn", agent, cwd),
6
- writePty: (data) => electron.ipcRenderer.invoke("pty:write", data),
7
- resizePty: (cols, rows) => electron.ipcRenderer.invoke("pty:resize", cols, rows),
8
- killPty: () => electron.ipcRenderer.invoke("pty:kill"),
9
- // Pty events
4
+ // Pty operations (multi-tab: all take tabId)
5
+ spawnAgent: (tabId, agent, cwd) => electron.ipcRenderer.invoke("pty:spawn", tabId, agent, cwd),
6
+ writePty: (tabId, data) => electron.ipcRenderer.invoke("pty:write", tabId, data),
7
+ resizePty: (tabId, cols, rows) => electron.ipcRenderer.invoke("pty:resize", tabId, cols, rows),
8
+ killPty: (tabId) => electron.ipcRenderer.invoke("pty:kill", tabId),
9
+ setActiveTab: (tabId) => electron.ipcRenderer.invoke("pty:setActiveTab", tabId),
10
+ // Pty events (multi-tab: callbacks receive tabId)
10
11
  onPtyData: (callback) => {
11
- const listener = (_event, data) => callback(data);
12
+ const listener = (_event, tabId, data) => callback(tabId, data);
12
13
  electron.ipcRenderer.on("pty:data", listener);
13
14
  return () => electron.ipcRenderer.removeListener("pty:data", listener);
14
15
  },
15
16
  onPtyExit: (callback) => {
16
- const listener = (_event, code) => callback(code);
17
+ const listener = (_event, tabId, code) => callback(tabId, code);
17
18
  electron.ipcRenderer.on("pty:exit", listener);
18
19
  return () => electron.ipcRenderer.removeListener("pty:exit", listener);
19
20
  },
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages } from "./index-DwSsD_Xm.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-Dljhj5Gh.js";
3
- import { h, i, j, t, k } from "./lspLanguageFeatures-Dljhj5Gh.js";
1
+ import { c as createWebWorker, l as languages } from "./index-BsdOeO0U.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-hoVZfVKv.js";
3
+ import { h, i, j, t, k } from "./lspLanguageFeatures-hoVZfVKv.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-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BsdOeO0U.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "assign",
4
4
  "flush",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BsdOeO0U.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BsdOeO0U.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-DwSsD_Xm.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-Dljhj5Gh.js";
3
- import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-Dljhj5Gh.js";
1
+ import { c as createWebWorker, l as languages } from "./index-BsdOeO0U.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-hoVZfVKv.js";
3
+ import { a, e, d, R, i, j, t, k } from "./lspLanguageFeatures-hoVZfVKv.js";
4
4
  const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
5
5
  class WorkerManager {
6
6
  constructor(defaults) {