@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.
- package/out/headless/index.mjs +91 -57
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +145 -63
- package/out/preload/index.js +9 -8
- package/out/renderer/assets/{cssMode-C6bY9C4O.js → cssMode-D3kH1Kju.js} +3 -3
- package/out/renderer/assets/{freemarker2-CkAJiX1K.js → freemarker2-BCHZUSLb.js} +1 -1
- package/out/renderer/assets/{handlebars-DnLXVUXp.js → handlebars-DKx-Fw-H.js} +1 -1
- package/out/renderer/assets/{html-Ds5-qvDh.js → html-BSCM04uL.js} +1 -1
- package/out/renderer/assets/{htmlMode-DYFYy4MK.js → htmlMode-BucU1MUc.js} +3 -3
- package/out/renderer/assets/{index-DwSsD_Xm.js → index-BsdOeO0U.js} +230 -101
- package/out/renderer/assets/{index-DK9wLFFm.css → index-BzF7I1my.css} +111 -0
- package/out/renderer/assets/{javascript-CiHhG2a9.js → javascript-bPY5C4uq.js} +2 -2
- package/out/renderer/assets/{jsonMode-DdDRlbXP.js → jsonMode-BmJotb6E.js} +3 -3
- package/out/renderer/assets/{liquid-BP5mb-uD.js → liquid-Cja_Pzh3.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Dljhj5Gh.js → lspLanguageFeatures-hoVZfVKv.js} +1 -1
- package/out/renderer/assets/{mdx-D4u3N7dt.js → mdx-C0s81MOq.js} +1 -1
- package/out/renderer/assets/{python-BQDHXVwp.js → python-CulkBOJr.js} +1 -1
- package/out/renderer/assets/{razor-BfXW9cDc.js → razor-czmzhwVZ.js} +1 -1
- package/out/renderer/assets/{tsMode-BGTjG8Ow.js → tsMode-B90EqYGx.js} +1 -1
- package/out/renderer/assets/{typescript-422MU_YO.js → typescript-Ckc6emP2.js} +1 -1
- package/out/renderer/assets/{xml-B6EKhHiy.js → xml-CKh-JyGN.js} +1 -1
- package/out/renderer/assets/{yaml-LkO_eGYb.js → yaml-B49zLim4.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/headless.ts +8 -7
- package/src/main/index.ts +87 -13
- package/src/main/orchestrator.ts +98 -54
- package/src/preload/index.ts +16 -14
- package/src/renderer/App.tsx +161 -43
- package/src/renderer/components/TerminalPanel.tsx +101 -59
- package/src/renderer/styles.css +111 -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%;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-Ckc6emP2.js";
|
|
2
|
+
import "./index-BsdOeO0U.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-
|
|
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-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-BsdOeO0U.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-hoVZfVKv.js";
|
|
3
|
+
import { a, D, h, R, c, 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) {
|
package/out/renderer/assets/{lspLanguageFeatures-Dljhj5Gh.js → lspLanguageFeatures-hoVZfVKv.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-BsdOeO0U.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -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-
|
|
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-BsdOeO0U.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BsdOeO0U.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BzF7I1my.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.
|
|
3
|
+
"version": "0.2.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": {
|
package/src/main/headless.ts
CHANGED
|
@@ -99,10 +99,10 @@ async function main() {
|
|
|
99
99
|
tui.init()
|
|
100
100
|
|
|
101
101
|
const orchestrator = new Orchestrator(settingsDir, {
|
|
102
|
-
onPtyData: (data) => {
|
|
102
|
+
onPtyData: (_tabId, data) => {
|
|
103
103
|
tui.writePtyData(data)
|
|
104
104
|
},
|
|
105
|
-
onPtyExit: (code) => {
|
|
105
|
+
onPtyExit: (_tabId, code) => {
|
|
106
106
|
tui.destroy()
|
|
107
107
|
console.log(`Agent exited with code ${code}`)
|
|
108
108
|
orchestrator.shutdown().then(() => process.exit(code))
|
|
@@ -126,14 +126,15 @@ async function main() {
|
|
|
126
126
|
if (args.baseUrl) orchestrator.overrideBaseUrl(args.baseUrl)
|
|
127
127
|
|
|
128
128
|
// Spawn agent with PTY sized to fit the TUI content area
|
|
129
|
+
const HEADLESS_TAB = 'headless'
|
|
129
130
|
const ptySize = tui.getPtySize()
|
|
130
|
-
await orchestrator.spawnAgent(agent, args.cwd)
|
|
131
|
-
orchestrator.resizePty(ptySize.cols, ptySize.rows)
|
|
131
|
+
await orchestrator.spawnAgent(HEADLESS_TAB, agent, args.cwd)
|
|
132
|
+
orchestrator.resizePty(HEADLESS_TAB, ptySize.cols, ptySize.rows)
|
|
132
133
|
|
|
133
134
|
// For coding agents, send an initial prompt to kick-start them
|
|
134
135
|
if (isCodingAgent(agent)) {
|
|
135
136
|
setTimeout(() => {
|
|
136
|
-
orchestrator.writePty('hello\r')
|
|
137
|
+
orchestrator.writePty(HEADLESS_TAB, 'hello\r')
|
|
137
138
|
}, 1000)
|
|
138
139
|
}
|
|
139
140
|
|
|
@@ -160,7 +161,7 @@ async function main() {
|
|
|
160
161
|
if (SCROLL_UP_RE.test(str) || SCROLL_DOWN_RE.test(str)) {
|
|
161
162
|
return
|
|
162
163
|
}
|
|
163
|
-
orchestrator.writePty(str)
|
|
164
|
+
orchestrator.writePty(HEADLESS_TAB, str)
|
|
164
165
|
})
|
|
165
166
|
}
|
|
166
167
|
|
|
@@ -170,7 +171,7 @@ async function main() {
|
|
|
170
171
|
const rows = process.stdout.rows || 24
|
|
171
172
|
tui.resize(cols, rows)
|
|
172
173
|
const size = tui.getPtySize()
|
|
173
|
-
orchestrator.resizePty(size.cols, size.rows)
|
|
174
|
+
orchestrator.resizePty(HEADLESS_TAB, size.cols, size.rows)
|
|
174
175
|
})
|
|
175
176
|
|
|
176
177
|
// 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: () =>
|
|
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:
|
|
78
|
-
orchestrator.
|
|
152
|
+
ipcMain.handle('pty:kill', async (_event, tabId: string) => {
|
|
153
|
+
await orchestrator.killTab(tabId)
|
|
79
154
|
})
|
|
80
155
|
|
|
81
|
-
ipcMain.handle('pty:
|
|
82
|
-
|
|
156
|
+
ipcMain.handle('pty:setActiveTab', (_event, tabId: string) => {
|
|
157
|
+
orchestrator.setActiveTab(tabId)
|
|
83
158
|
})
|
|
84
159
|
|
|
85
160
|
ipcMain.handle('agents:list', () => getBuiltinAgents())
|
|
@@ -279,7 +354,6 @@ app.whenReady().then(() => {
|
|
|
279
354
|
if (process.platform === 'darwin' && app.dock) {
|
|
280
355
|
const iconPath = path.join(__dirname, '../../resources/icon.png')
|
|
281
356
|
try {
|
|
282
|
-
const { nativeImage } = require('electron')
|
|
283
357
|
app.dock.setIcon(nativeImage.createFromPath(iconPath))
|
|
284
358
|
} catch { /* ignore */ }
|
|
285
359
|
}
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -30,14 +30,22 @@ export interface SettingsData {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface OrchestratorEvents {
|
|
33
|
-
onPtyData: (data: string) => void
|
|
34
|
-
onPtyExit: (code: number) => void
|
|
33
|
+
onPtyData: (tabId: string, data: string) => void
|
|
34
|
+
onPtyExit: (tabId: string, code: number) => void
|
|
35
35
|
onWorkerStatus: (status: string) => void
|
|
36
36
|
onWorkerMessage: (message: IncomingMessage) => void
|
|
37
37
|
onWorkerRegistered: (data: { worker_id: string; folder_id: string | null; status: string }) => void
|
|
38
38
|
onCwdChanged: () => void
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface TabState {
|
|
42
|
+
ptyManager: PtyManager
|
|
43
|
+
agent: AgentConfig
|
|
44
|
+
cwd: string
|
|
45
|
+
termStreamBuffer: string
|
|
46
|
+
termStreamTimer: ReturnType<typeof setTimeout> | null
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
// ─── Orchestrator ─────────────────────────────────
|
|
42
50
|
|
|
43
51
|
const DEFAULT_PROFILES: Record<string, Profile> = {
|
|
@@ -61,7 +69,8 @@ export class Orchestrator {
|
|
|
61
69
|
readonly workerWs: WorkerWsClient
|
|
62
70
|
|
|
63
71
|
// State
|
|
64
|
-
private
|
|
72
|
+
private tabs = new Map<string, TabState>()
|
|
73
|
+
private activeTabId: string | null = null
|
|
65
74
|
private currentAgent: AgentConfig | null = null
|
|
66
75
|
private currentCwd: string | null = null
|
|
67
76
|
private settings: SettingsData = {
|
|
@@ -69,10 +78,6 @@ export class Orchestrator {
|
|
|
69
78
|
profiles: { ...DEFAULT_PROFILES },
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
// Terminal stream batching
|
|
73
|
-
private termStreamBuffer = ''
|
|
74
|
-
private termStreamTimer: ReturnType<typeof setTimeout> | null = null
|
|
75
|
-
|
|
76
81
|
constructor(settingsDir: string, events: OrchestratorEvents) {
|
|
77
82
|
this.settingsDir = settingsDir
|
|
78
83
|
this.events = events
|
|
@@ -88,8 +93,9 @@ export class Orchestrator {
|
|
|
88
93
|
this.workerWs.sendAck(message.id)
|
|
89
94
|
|
|
90
95
|
if (message.type === 'prompt' || message.type === 'task_dispatch') {
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null
|
|
97
|
+
if (activeTab) {
|
|
98
|
+
activeTab.ptyManager.write(message.content + '\r')
|
|
93
99
|
this.bridge.feedInput(message.content)
|
|
94
100
|
}
|
|
95
101
|
}
|
|
@@ -102,7 +108,8 @@ export class Orchestrator {
|
|
|
102
108
|
}
|
|
103
109
|
},
|
|
104
110
|
onTerminalInput: (data: string) => {
|
|
105
|
-
this.
|
|
111
|
+
const activeTab = this.activeTabId ? this.tabs.get(this.activeTabId) : null
|
|
112
|
+
activeTab?.ptyManager.write(data)
|
|
106
113
|
},
|
|
107
114
|
})
|
|
108
115
|
|
|
@@ -280,37 +287,48 @@ export class Orchestrator {
|
|
|
280
287
|
return { ok: true }
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
// ─── PTY & Agent
|
|
290
|
+
// ─── PTY & Agent (multi-tab) ─────────────────────
|
|
284
291
|
|
|
285
|
-
async spawnAgent(agent: AgentConfig, cwd: string): Promise<void> {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
async spawnAgent(tabId: string, agent: AgentConfig, cwd: string): Promise<void> {
|
|
293
|
+
// Kill existing PTY on this tab if any
|
|
294
|
+
const existing = this.tabs.get(tabId)
|
|
295
|
+
if (existing) {
|
|
296
|
+
if (existing.termStreamTimer) clearTimeout(existing.termStreamTimer)
|
|
297
|
+
existing.ptyManager.kill()
|
|
298
|
+
this.tabs.delete(tabId)
|
|
289
299
|
}
|
|
290
300
|
|
|
291
301
|
this.currentAgent = agent
|
|
292
302
|
const prevCwd = this.currentCwd
|
|
293
303
|
this.currentCwd = cwd
|
|
304
|
+
this.activeTabId = tabId
|
|
294
305
|
if (prevCwd !== cwd) {
|
|
295
306
|
this.events.onCwdChanged()
|
|
296
307
|
}
|
|
297
308
|
|
|
298
|
-
|
|
309
|
+
const ptyManager = new PtyManager(agent, cwd)
|
|
310
|
+
const tab: TabState = { ptyManager, agent, cwd, termStreamBuffer: '', termStreamTimer: null }
|
|
311
|
+
this.tabs.set(tabId, tab)
|
|
299
312
|
|
|
300
|
-
|
|
301
|
-
this.events.onPtyData(data)
|
|
302
|
-
this.
|
|
303
|
-
|
|
313
|
+
ptyManager.onData((data: string) => {
|
|
314
|
+
this.events.onPtyData(tabId, data)
|
|
315
|
+
if (tabId === this.activeTabId) {
|
|
316
|
+
this.bridge.feedOutput(data)
|
|
317
|
+
this.streamTerminalData(tabId, data)
|
|
318
|
+
}
|
|
304
319
|
})
|
|
305
320
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
321
|
+
ptyManager.onExit(async (exitCode: number) => {
|
|
322
|
+
this.events.onPtyExit(tabId, exitCode)
|
|
323
|
+
if (tabId === this.activeTabId) {
|
|
324
|
+
this.bridge.endSession()
|
|
325
|
+
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
326
|
+
this.workerWs.disconnect()
|
|
327
|
+
}
|
|
313
328
|
}
|
|
329
|
+
// Clean up tab state
|
|
330
|
+
const t = this.tabs.get(tabId)
|
|
331
|
+
if (t?.termStreamTimer) clearTimeout(t.termStreamTimer)
|
|
314
332
|
})
|
|
315
333
|
|
|
316
334
|
this.bridge.startSession()
|
|
@@ -323,26 +341,51 @@ export class Orchestrator {
|
|
|
323
341
|
}
|
|
324
342
|
}
|
|
325
343
|
|
|
326
|
-
writePty(data: string): void {
|
|
327
|
-
this.ptyManager
|
|
328
|
-
this.
|
|
344
|
+
writePty(tabId: string, data: string): void {
|
|
345
|
+
this.tabs.get(tabId)?.ptyManager.write(data)
|
|
346
|
+
if (tabId === this.activeTabId) {
|
|
347
|
+
this.bridge.feedInput(data)
|
|
348
|
+
}
|
|
329
349
|
}
|
|
330
350
|
|
|
331
|
-
resizePty(cols: number, rows: number): void {
|
|
332
|
-
this.ptyManager
|
|
333
|
-
this.
|
|
334
|
-
|
|
351
|
+
resizePty(tabId: string, cols: number, rows: number): void {
|
|
352
|
+
this.tabs.get(tabId)?.ptyManager.resize(cols, rows)
|
|
353
|
+
if (tabId === this.activeTabId) {
|
|
354
|
+
this.bridge.resize(cols, rows)
|
|
355
|
+
this.workerWs.sendTerminalResize(cols, rows)
|
|
356
|
+
}
|
|
335
357
|
}
|
|
336
358
|
|
|
337
|
-
async
|
|
338
|
-
this.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
359
|
+
async killTab(tabId: string): Promise<void> {
|
|
360
|
+
const tab = this.tabs.get(tabId)
|
|
361
|
+
if (!tab) return
|
|
362
|
+
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer)
|
|
363
|
+
tab.ptyManager.kill()
|
|
364
|
+
this.tabs.delete(tabId)
|
|
365
|
+
if (tabId === this.activeTabId) {
|
|
366
|
+
this.bridge.endSession()
|
|
367
|
+
if (isCodingAgent(tab.agent)) {
|
|
368
|
+
this.workerWs.disconnect()
|
|
369
|
+
}
|
|
370
|
+
// Switch active to another tab if available
|
|
371
|
+
const remaining = [...this.tabs.keys()]
|
|
372
|
+
this.activeTabId = remaining.length > 0 ? remaining[remaining.length - 1] : null
|
|
343
373
|
}
|
|
344
374
|
}
|
|
345
375
|
|
|
376
|
+
setActiveTab(tabId: string): void {
|
|
377
|
+
this.activeTabId = tabId
|
|
378
|
+
const tab = this.tabs.get(tabId)
|
|
379
|
+
if (tab) {
|
|
380
|
+
this.currentAgent = tab.agent
|
|
381
|
+
this.currentCwd = tab.cwd
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getTabIds(): string[] {
|
|
386
|
+
return [...this.tabs.keys()]
|
|
387
|
+
}
|
|
388
|
+
|
|
346
389
|
// ─── Worker WebSocket ───────────────────────────
|
|
347
390
|
|
|
348
391
|
connectWorkerWs(agent: AgentConfig, cwd: string): void {
|
|
@@ -375,15 +418,17 @@ export class Orchestrator {
|
|
|
375
418
|
}
|
|
376
419
|
}
|
|
377
420
|
|
|
378
|
-
private streamTerminalData(data: string): void {
|
|
379
|
-
this.
|
|
380
|
-
if (!
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
421
|
+
private streamTerminalData(tabId: string, data: string): void {
|
|
422
|
+
const tab = this.tabs.get(tabId)
|
|
423
|
+
if (!tab) return
|
|
424
|
+
tab.termStreamBuffer += data
|
|
425
|
+
if (!tab.termStreamTimer) {
|
|
426
|
+
tab.termStreamTimer = setTimeout(() => {
|
|
427
|
+
if (tab.termStreamBuffer) {
|
|
428
|
+
this.workerWs.sendTerminalData(tab.termStreamBuffer)
|
|
429
|
+
tab.termStreamBuffer = ''
|
|
385
430
|
}
|
|
386
|
-
|
|
431
|
+
tab.termStreamTimer = null
|
|
387
432
|
}, TERM_STREAM_INTERVAL_MS)
|
|
388
433
|
}
|
|
389
434
|
}
|
|
@@ -392,12 +437,11 @@ export class Orchestrator {
|
|
|
392
437
|
|
|
393
438
|
async shutdown(): Promise<void> {
|
|
394
439
|
this.bridge.endSession()
|
|
395
|
-
this.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (this.termStreamTimer) {
|
|
399
|
-
clearTimeout(this.termStreamTimer)
|
|
400
|
-
this.termStreamTimer = null
|
|
440
|
+
for (const [, tab] of this.tabs) {
|
|
441
|
+
if (tab.termStreamTimer) clearTimeout(tab.termStreamTimer)
|
|
442
|
+
tab.ptyManager.kill()
|
|
401
443
|
}
|
|
444
|
+
this.tabs.clear()
|
|
445
|
+
this.workerWs.disconnect()
|
|
402
446
|
}
|
|
403
447
|
}
|