@phenx-inc/ctlsurf 0.1.21 → 0.3.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 (35) hide show
  1. package/out/headless/index.mjs +409 -99
  2. package/out/headless/index.mjs.map +4 -4
  3. package/out/main/index.js +419 -77
  4. package/out/preload/index.js +12 -8
  5. package/out/renderer/assets/{cssMode-C6bY9C4O.js → cssMode-DiOmyihM.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CkAJiX1K.js → freemarker2-BAfv60yb.js} +1 -1
  7. package/out/renderer/assets/{handlebars-DnLXVUXp.js → handlebars-Ult17NzQ.js} +1 -1
  8. package/out/renderer/assets/{html-Ds5-qvDh.js → html-DCxh4J-1.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-DYFYy4MK.js → htmlMode-CQ5Xenrg.js} +3 -3
  10. package/out/renderer/assets/{index-DwSsD_Xm.js → index-BnCJ1IaZ.js} +308 -101
  11. package/out/renderer/assets/{index-DK9wLFFm.css → index-CrTu3Z4M.css} +132 -0
  12. package/out/renderer/assets/{javascript-CiHhG2a9.js → javascript-U5dsRcHx.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-DdDRlbXP.js → jsonMode-DshPNyVy.js} +3 -3
  14. package/out/renderer/assets/{liquid-BP5mb-uD.js → liquid-jHHLYTlB.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-Dljhj5Gh.js → lspLanguageFeatures-CUafmPGy.js} +1 -1
  16. package/out/renderer/assets/{mdx-D4u3N7dt.js → mdx-Ct-tiY6g.js} +1 -1
  17. package/out/renderer/assets/{python-BQDHXVwp.js → python-wD3UwKPV.js} +1 -1
  18. package/out/renderer/assets/{razor-BfXW9cDc.js → razor-11ECS4oH.js} +1 -1
  19. package/out/renderer/assets/{tsMode-BGTjG8Ow.js → tsMode-D-7JexQ_.js} +1 -1
  20. package/out/renderer/assets/{typescript-422MU_YO.js → typescript-Cvna1mak.js} +1 -1
  21. package/out/renderer/assets/{xml-B6EKhHiy.js → xml-JsEaImjA.js} +1 -1
  22. package/out/renderer/assets/{yaml-LkO_eGYb.js → yaml-B8pCNDb_.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/ctlsurfApi.ts +26 -0
  26. package/src/main/headless.ts +40 -34
  27. package/src/main/index.ts +95 -13
  28. package/src/main/orchestrator.ts +160 -55
  29. package/src/main/timeTracker.ts +223 -0
  30. package/src/main/tui.ts +25 -5
  31. package/src/preload/index.ts +23 -15
  32. package/src/renderer/App.tsx +197 -43
  33. package/src/renderer/components/SettingsDialog.tsx +38 -1
  34. package/src/renderer/components/TerminalPanel.tsx +109 -59
  35. package/src/renderer/styles.css +132 -0
@@ -7504,6 +7504,117 @@ html, body, #root {
7504
7504
  overflow: hidden;
7505
7505
  }
7506
7506
 
7507
+ /* Terminal tabs wrapper */
7508
+ .terminal-tabs-wrapper {
7509
+ display: flex;
7510
+ flex-direction: column;
7511
+ width: 100%;
7512
+ height: 100%;
7513
+ overflow: hidden;
7514
+ }
7515
+
7516
+ .terminal-tab-bar {
7517
+ display: flex;
7518
+ align-items: center;
7519
+ gap: 1px;
7520
+ padding: 2px 4px;
7521
+ background: #16161e;
7522
+ border-bottom: 1px solid #292e42;
7523
+ flex-shrink: 0;
7524
+ overflow-x: auto;
7525
+ min-height: 28px;
7526
+ }
7527
+
7528
+ .terminal-tab {
7529
+ display: flex;
7530
+ align-items: center;
7531
+ gap: 4px;
7532
+ padding: 3px 8px;
7533
+ font-size: 11px;
7534
+ color: #565f89;
7535
+ background: transparent;
7536
+ border-radius: 4px;
7537
+ cursor: pointer;
7538
+ white-space: nowrap;
7539
+ user-select: none;
7540
+ }
7541
+
7542
+ .terminal-tab:hover {
7543
+ background: #1f2335;
7544
+ color: #a9b1d6;
7545
+ }
7546
+
7547
+ .terminal-tab.active {
7548
+ background: #1a1b26;
7549
+ color: #c0caf5;
7550
+ }
7551
+
7552
+ .terminal-tab-label {
7553
+ display: flex;
7554
+ align-items: center;
7555
+ gap: 4px;
7556
+ }
7557
+
7558
+ .terminal-tab-dot {
7559
+ display: inline-block;
7560
+ width: 6px;
7561
+ height: 6px;
7562
+ border-radius: 50%;
7563
+ }
7564
+
7565
+ .terminal-tab-dot.active {
7566
+ background: #9ece6a;
7567
+ }
7568
+
7569
+ .terminal-tab-dot.exited {
7570
+ background: #565f89;
7571
+ }
7572
+
7573
+ .terminal-tab-close {
7574
+ background: none;
7575
+ border: none;
7576
+ color: inherit;
7577
+ font-size: 14px;
7578
+ line-height: 1;
7579
+ cursor: pointer;
7580
+ padding: 0 2px;
7581
+ border-radius: 3px;
7582
+ opacity: 0.5;
7583
+ }
7584
+
7585
+ .terminal-tab-close:hover {
7586
+ opacity: 1;
7587
+ background: rgba(247, 118, 142, 0.2);
7588
+ color: #f7768e;
7589
+ }
7590
+
7591
+ .terminal-tab-add {
7592
+ background: none;
7593
+ border: none;
7594
+ color: #565f89;
7595
+ font-size: 16px;
7596
+ cursor: pointer;
7597
+ padding: 2px 6px;
7598
+ border-radius: 4px;
7599
+ line-height: 1;
7600
+ }
7601
+
7602
+ .terminal-tab-add:hover {
7603
+ background: #1f2335;
7604
+ color: #a9b1d6;
7605
+ }
7606
+
7607
+ .terminal-tabs-content {
7608
+ flex: 1;
7609
+ display: flex;
7610
+ overflow: hidden;
7611
+ }
7612
+
7613
+ .terminal-tab-panel {
7614
+ flex-direction: column;
7615
+ overflow: hidden;
7616
+ }
7617
+
7507
7618
  /* Terminal container */
7508
7619
  .terminal-container {
7509
7620
  width: 100%;
@@ -7793,6 +7904,27 @@ html, body, #root {
7793
7904
  .status-dot.idle { background: #565f89; }
7794
7905
  .status-dot.pending { background: #e0af68; }
7795
7906
 
7907
+ .tracking-dot {
7908
+ width: 6px;
7909
+ height: 6px;
7910
+ border-radius: 50%;
7911
+ display: inline-block;
7912
+ vertical-align: middle;
7913
+ }
7914
+ .tracking-dot.on { background: #9ece6a; box-shadow: 0 0 4px #9ece6a; }
7915
+ .tracking-dot.off { background: #565f89; }
7916
+
7917
+ .titlebar-icon-btn {
7918
+ display: inline-flex;
7919
+ align-items: center;
7920
+ gap: 5px;
7921
+ padding: 0 8px;
7922
+ }
7923
+ .tracking-icon {
7924
+ font-size: 14px;
7925
+ line-height: 1;
7926
+ }
7927
+
7796
7928
  /* Editor panel */
7797
7929
  .editor-panel {
7798
7930
  display: flex;
@@ -1,5 +1,5 @@
1
- import { conf as conf$1, language as language$1 } from "./typescript-422MU_YO.js";
2
- import "./index-DwSsD_Xm.js";
1
+ import { conf as conf$1, language as language$1 } from "./typescript-Cvna1mak.js";
2
+ import "./index-BnCJ1IaZ.js";
3
3
  const conf = conf$1;
4
4
  const language = {
5
5
  // Set defaultToken to invalid to see what you do not tokenize yet
@@ -1,6 +1,6 @@
1
- import { c as createWebWorker, l as languages, e as editor } from "./index-DwSsD_Xm.js";
2
- import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-Dljhj5Gh.js";
3
- import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-Dljhj5Gh.js";
1
+ import { c as createWebWorker, l as languages, e as editor } from "./index-BnCJ1IaZ.js";
2
+ import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-CUafmPGy.js";
3
+ import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-CUafmPGy.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-BnCJ1IaZ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-DwSsD_Xm.js";
1
+ import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-BnCJ1IaZ.js";
2
2
  var DocumentUri;
3
3
  (function(DocumentUri2) {
4
4
  function is(value) {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BnCJ1IaZ.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  blockComment: ["{/*", "*/}"]
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BnCJ1IaZ.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  lineComment: "#",
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BnCJ1IaZ.js";
2
2
  const EMPTY_ELEMENTS = [
3
3
  "area",
4
4
  "base",
@@ -1,4 +1,4 @@
1
- import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-DwSsD_Xm.js";
1
+ import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-BnCJ1IaZ.js";
2
2
  class WorkerManager {
3
3
  constructor(_modeId, _defaults) {
4
4
  this._modeId = _modeId;
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BnCJ1IaZ.js";
2
2
  const conf = {
3
3
  wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
4
4
  comments: {
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BnCJ1IaZ.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  blockComment: ["<!--", "-->"]
@@ -1,4 +1,4 @@
1
- import { l as languages } from "./index-DwSsD_Xm.js";
1
+ import { l as languages } from "./index-BnCJ1IaZ.js";
2
2
  const conf = {
3
3
  comments: {
4
4
  lineComment: "#"
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ctlsurf-worker</title>
7
- <script type="module" crossorigin src="./assets/index-DwSsD_Xm.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-DK9wLFFm.css">
7
+ <script type="module" crossorigin src="./assets/index-BnCJ1IaZ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-CrTu3Z4M.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.1.21",
3
+ "version": "0.3.0",
4
4
  "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
5
  "main": "out/main/index.js",
6
6
  "bin": {
@@ -105,6 +105,32 @@ export class CtlsurfApi {
105
105
  return folder?.pages || []
106
106
  }
107
107
 
108
+ async getFolder(folderId: string): Promise<any> {
109
+ return this.request('GET', `/folders/${folderId}`)
110
+ }
111
+
112
+ // ─── Datastore ───────────────────────────────────────
113
+
114
+ async getPageBlockSummaries(pageId: string): Promise<any[]> {
115
+ return this.request('GET', `/blocks/page/${pageId}/summary`)
116
+ }
117
+
118
+ async addRow(blockId: string, data: Record<string, unknown>): Promise<any> {
119
+ return this.request('POST', `/datastore/${blockId}/rows`, { data })
120
+ }
121
+
122
+ async updateRow(blockId: string, rowId: string, data: Record<string, unknown>): Promise<any> {
123
+ return this.request('PUT', `/datastore/${blockId}/rows/${rowId}`, { data })
124
+ }
125
+
126
+ async getDatastoreSchema(blockId: string): Promise<{ block_id: string; columns: Array<{ id: string; name: string; type: string }> }> {
127
+ return this.request('GET', `/datastore/${blockId}/schema`)
128
+ }
129
+
130
+ async updateDatastoreSchema(blockId: string, columns: Array<{ id: string; name: string; type: string }>): Promise<any> {
131
+ return this.request('PUT', `/datastore/${blockId}/schema`, { columns })
132
+ }
133
+
108
134
  async findFolderByGitRemote(gitRemote: string): Promise<any> {
109
135
  // Search folders by listing all and matching git_remote
110
136
  const folders = await this.request('GET', '/folders')
@@ -70,39 +70,13 @@ async function main() {
70
70
  const tui = new Tui()
71
71
  const agents = getBuiltinAgents()
72
72
 
73
- // ─── Agent selection ────────────────────────────
74
- let agent: AgentConfig
75
-
76
- if (args.agent) {
77
- const found = agents.find(a => a.id === args.agent)
78
- agent = found || {
79
- id: args.agent,
80
- name: args.agent,
81
- command: args.agent,
82
- args: [],
83
- description: `Custom agent: ${args.agent}`,
84
- }
85
- } else {
86
- // Show interactive picker
87
- const selectedIdx = await tui.showAgentPicker(agents)
88
- agent = agents[selectedIdx]
89
- }
90
-
91
- // ─── Start TUI + agent ─────────────────────────
92
-
93
- tui.update({
94
- agentName: agent.name,
95
- cwd: args.cwd,
96
- mode: 'terminal',
97
- })
98
-
99
- tui.init()
73
+ // ─── Orchestrator (loaded early so picker can read profile defaults) ──
100
74
 
101
75
  const orchestrator = new Orchestrator(settingsDir, {
102
- onPtyData: (data) => {
76
+ onPtyData: (_tabId, data) => {
103
77
  tui.writePtyData(data)
104
78
  },
105
- onPtyExit: (code) => {
79
+ onPtyExit: (_tabId, code) => {
106
80
  tui.destroy()
107
81
  console.log(`Agent exited with code ${code}`)
108
82
  orchestrator.shutdown().then(() => process.exit(code))
@@ -125,15 +99,47 @@ async function main() {
125
99
  if (args.apiKey) orchestrator.overrideApiKey(args.apiKey)
126
100
  if (args.baseUrl) orchestrator.overrideBaseUrl(args.baseUrl)
127
101
 
102
+ // ─── Agent selection ────────────────────────────
103
+
104
+ let agent: AgentConfig
105
+ let trackTimeOverride: boolean | undefined
106
+
107
+ if (args.agent) {
108
+ const found = agents.find(a => a.id === args.agent)
109
+ agent = found || {
110
+ id: args.agent,
111
+ name: args.agent,
112
+ command: args.agent,
113
+ args: [],
114
+ description: `Custom agent: ${args.agent}`,
115
+ }
116
+ } else {
117
+ const initialTrackTime = orchestrator.getActiveProfile().trackTime !== false
118
+ const picked = await tui.showAgentPicker(agents, { initialTrackTime })
119
+ agent = agents[picked.agentIdx]
120
+ trackTimeOverride = picked.trackTime
121
+ }
122
+
123
+ // ─── Start TUI + agent ─────────────────────────
124
+
125
+ tui.update({
126
+ agentName: agent.name,
127
+ cwd: args.cwd,
128
+ mode: 'terminal',
129
+ })
130
+
131
+ tui.init()
132
+
128
133
  // Spawn agent with PTY sized to fit the TUI content area
134
+ const HEADLESS_TAB = 'headless'
129
135
  const ptySize = tui.getPtySize()
130
- await orchestrator.spawnAgent(agent, args.cwd)
131
- orchestrator.resizePty(ptySize.cols, ptySize.rows)
136
+ await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd, { trackTime: trackTimeOverride })
137
+ orchestrator.resizePty(HEADLESS_TAB, ptySize.cols, ptySize.rows)
132
138
 
133
139
  // For coding agents, send an initial prompt to kick-start them
134
140
  if (isCodingAgent(agent)) {
135
141
  setTimeout(() => {
136
- orchestrator.writePty('hello\r')
142
+ orchestrator.writePty(HEADLESS_TAB, 'hello\r')
137
143
  }, 1000)
138
144
  }
139
145
 
@@ -160,7 +166,7 @@ async function main() {
160
166
  if (SCROLL_UP_RE.test(str) || SCROLL_DOWN_RE.test(str)) {
161
167
  return
162
168
  }
163
- orchestrator.writePty(str)
169
+ orchestrator.writePty(HEADLESS_TAB, str)
164
170
  })
165
171
  }
166
172
 
@@ -170,7 +176,7 @@ async function main() {
170
176
  const rows = process.stdout.rows || 24
171
177
  tui.resize(cols, rows)
172
178
  const size = tui.getPtySize()
173
- orchestrator.resizePty(size.cols, size.rows)
179
+ orchestrator.resizePty(HEADLESS_TAB, size.cols, size.rows)
174
180
  })
175
181
 
176
182
  // Graceful shutdown
package/src/main/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { app, BrowserWindow, ipcMain, dialog } from 'electron'
1
+ import { app, BrowserWindow, ipcMain, dialog, nativeImage } from 'electron'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
4
 
@@ -19,17 +19,88 @@ import { Orchestrator } from './orchestrator'
19
19
 
20
20
  let mainWindow: BrowserWindow | null = null
21
21
 
22
+ // ─── Project badge on dock/taskbar icon ──────────
23
+
24
+ function getProjectAbbrev(cwdPath: string): string {
25
+ const folderName = cwdPath.split('/').filter(Boolean).pop() || ''
26
+ if (!folderName) return ''
27
+
28
+ // Split on hyphens, underscores, dots, camelCase boundaries
29
+ const words = folderName
30
+ .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → separate words
31
+ .split(/[-_.\s]+/)
32
+ .filter(Boolean)
33
+
34
+ if (words.length >= 2) {
35
+ // Multiple words → take initials (up to 4)
36
+ return words.slice(0, 4).map(w => w[0]).join('').toUpperCase()
37
+ }
38
+
39
+ // Single word: if short (<=3 chars) use as-is, otherwise first 2 chars
40
+ const word = words[0]
41
+ if (word.length <= 3) return word.toUpperCase()
42
+ return word.slice(0, 2).toUpperCase()
43
+ }
44
+
45
+ function createOverlayIcon(text: string): Electron.NativeImage {
46
+ // Create a 32x32 overlay with text for Windows/Linux
47
+ const size = 32
48
+ const canvas = Buffer.alloc(size * size * 4, 0) // RGBA
49
+
50
+ // Fill with a semi-transparent dark background circle
51
+ for (let y = 0; y < size; y++) {
52
+ for (let x = 0; x < size; x++) {
53
+ const cx = x - size / 2
54
+ const cy = y - size / 2
55
+ const dist = Math.sqrt(cx * cx + cy * cy)
56
+ if (dist <= size / 2) {
57
+ const idx = (y * size + x) * 4
58
+ canvas[idx] = 122 // R (blue-ish)
59
+ canvas[idx + 1] = 162 // G
60
+ canvas[idx + 2] = 247 // B
61
+ canvas[idx + 3] = 220 // A
62
+ }
63
+ }
64
+ }
65
+
66
+ return nativeImage.createFromBuffer(canvas, { width: size, height: size })
67
+ }
68
+
69
+ function updateProjectBadge(cwdPath: string | null): void {
70
+ if (!cwdPath) return
71
+ const abbrev = getProjectAbbrev(cwdPath)
72
+
73
+ // macOS: dock badge shows text directly
74
+ if (process.platform === 'darwin' && app.dock) {
75
+ app.dock.setBadge(abbrev)
76
+ }
77
+
78
+ // Windows: overlay icon on taskbar
79
+ if (process.platform === 'win32' && mainWindow) {
80
+ mainWindow.setOverlayIcon(createOverlayIcon(abbrev), abbrev)
81
+ }
82
+
83
+ // All platforms: update window title
84
+ if (mainWindow) {
85
+ const folderName = cwdPath.split('/').filter(Boolean).pop() || 'ctlsurf-worker'
86
+ mainWindow.setTitle(`ctlsurf-worker — ${folderName}`)
87
+ }
88
+ }
89
+
22
90
  // ─── Orchestrator (shared logic) ──────────────────
23
91
 
24
92
  const orchestrator = new Orchestrator(
25
93
  getSettingsDir(true),
26
94
  {
27
- onPtyData: (data) => mainWindow?.webContents.send('pty:data', data),
28
- onPtyExit: (code) => mainWindow?.webContents.send('pty:exit', code),
95
+ onPtyData: (tabId, data) => mainWindow?.webContents.send('pty:data', tabId, data),
96
+ onPtyExit: (tabId, code) => mainWindow?.webContents.send('pty:exit', tabId, code),
29
97
  onWorkerStatus: (status) => mainWindow?.webContents.send('worker:status', status),
30
98
  onWorkerMessage: (message) => mainWindow?.webContents.send('worker:message', message),
31
99
  onWorkerRegistered: (data) => mainWindow?.webContents.send('worker:registered', data),
32
- onCwdChanged: () => mainWindow?.webContents.send('app:cwdChanged'),
100
+ onCwdChanged: () => {
101
+ mainWindow?.webContents.send('app:cwdChanged')
102
+ updateProjectBadge(orchestrator.cwd)
103
+ },
33
104
  }
34
105
  )
35
106
 
@@ -65,21 +136,25 @@ function createWindow(): void {
65
136
 
66
137
  // ─── IPC Handlers ──────────────────────────────────
67
138
 
68
- ipcMain.handle('pty:spawn', async (_event, agent: AgentConfig, cwd: string) => {
69
- await orchestrator.spawnAgent(agent, cwd)
139
+ ipcMain.handle('pty:spawn', async (_event, tabId: string, agent: AgentConfig, cwd: string) => {
140
+ await orchestrator.spawnAgent(tabId, agent, cwd)
70
141
  return { ok: true }
71
142
  })
72
143
 
73
- ipcMain.handle('pty:write', (_event, data: string) => {
74
- orchestrator.writePty(data)
144
+ ipcMain.handle('pty:write', (_event, tabId: string, data: string) => {
145
+ orchestrator.writePty(tabId, data)
146
+ })
147
+
148
+ ipcMain.handle('pty:resize', (_event, tabId: string, cols: number, rows: number) => {
149
+ orchestrator.resizePty(tabId, cols, rows)
75
150
  })
76
151
 
77
- ipcMain.handle('pty:resize', (_event, cols: number, rows: number) => {
78
- orchestrator.resizePty(cols, rows)
152
+ ipcMain.handle('pty:kill', async (_event, tabId: string) => {
153
+ await orchestrator.killTab(tabId)
79
154
  })
80
155
 
81
- ipcMain.handle('pty:kill', async () => {
82
- await orchestrator.killAgent()
156
+ ipcMain.handle('pty:setActiveTab', (_event, tabId: string) => {
157
+ orchestrator.setActiveTab(tabId)
83
158
  })
84
159
 
85
160
  ipcMain.handle('agents:list', () => getBuiltinAgents())
@@ -234,6 +309,14 @@ ipcMain.handle('profiles:save', (_event, id: string, data: any) => {
234
309
  ipcMain.handle('profiles:switch', (_event, id: string) => orchestrator.switchProfile(id))
235
310
  ipcMain.handle('profiles:delete', (_event, id: string) => orchestrator.deleteProfile(id))
236
311
 
312
+ // ─── Tracking IPC ─────────────────────────────────
313
+
314
+ ipcMain.handle('tracking:get', () => ({ active: orchestrator.isActiveTabTracking() }))
315
+ ipcMain.handle('tracking:set', async (_event, enabled: boolean) => {
316
+ await orchestrator.setActiveTabTracking(enabled)
317
+ return { active: orchestrator.isActiveTabTracking() }
318
+ })
319
+
237
320
  // ─── Legacy Settings IPC ──────────────────────────
238
321
 
239
322
  ipcMain.handle('settings:get', (_event, key: string) => {
@@ -279,7 +362,6 @@ app.whenReady().then(() => {
279
362
  if (process.platform === 'darwin' && app.dock) {
280
363
  const iconPath = path.join(__dirname, '../../resources/icon.png')
281
364
  try {
282
- const { nativeImage } = require('electron')
283
365
  app.dock.setIcon(nativeImage.createFromPath(iconPath))
284
366
  } catch { /* ignore */ }
285
367
  }