@phenx-inc/ctlsurf 0.1.9 → 0.1.10
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 +18 -28
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +18 -28
- package/package.json +1 -1
- package/src/main/bridge.ts +22 -27
- package/src/main/orchestrator.ts +4 -4
package/out/main/index.js
CHANGED
|
@@ -3625,14 +3625,15 @@ function requireXtermHeadless() {
|
|
|
3625
3625
|
var xtermHeadlessExports = requireXtermHeadless();
|
|
3626
3626
|
class ConversationBridge {
|
|
3627
3627
|
wsClient = null;
|
|
3628
|
-
flushTimer = null;
|
|
3629
|
-
flushIntervalMs = 3e3;
|
|
3630
3628
|
sessionActive = false;
|
|
3631
3629
|
inputBuffer = "";
|
|
3630
|
+
bytesAccumulated = 0;
|
|
3632
3631
|
// Virtual terminal for processing escape sequences into rendered text
|
|
3633
3632
|
terminal;
|
|
3634
3633
|
lastSnapshot = "";
|
|
3635
|
-
|
|
3634
|
+
// Flush after accumulating enough data or on a timer
|
|
3635
|
+
FLUSH_BYTES = 2e3;
|
|
3636
|
+
flushTimer = null;
|
|
3636
3637
|
constructor() {
|
|
3637
3638
|
this.terminal = new xtermHeadlessExports.Terminal({ cols: 120, rows: 50, scrollback: 1e4 });
|
|
3638
3639
|
}
|
|
@@ -3642,9 +3643,11 @@ class ConversationBridge {
|
|
|
3642
3643
|
startSession() {
|
|
3643
3644
|
this.terminal.reset();
|
|
3644
3645
|
this.lastSnapshot = "";
|
|
3645
|
-
this.
|
|
3646
|
+
this.bytesAccumulated = 0;
|
|
3646
3647
|
this.inputBuffer = "";
|
|
3647
3648
|
this.sessionActive = true;
|
|
3649
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
3650
|
+
this.flushTimer = setInterval(() => this.flush(), 5e3);
|
|
3648
3651
|
console.log("[bridge] Session started");
|
|
3649
3652
|
}
|
|
3650
3653
|
/**
|
|
@@ -3654,17 +3657,11 @@ class ConversationBridge {
|
|
|
3654
3657
|
*/
|
|
3655
3658
|
feedOutput(data) {
|
|
3656
3659
|
if (!this.sessionActive) return;
|
|
3657
|
-
this.
|
|
3658
|
-
this.
|
|
3659
|
-
|
|
3660
|
-
this.
|
|
3661
|
-
});
|
|
3662
|
-
}
|
|
3663
|
-
scheduleFlush() {
|
|
3664
|
-
if (this.flushTimer) {
|
|
3665
|
-
clearTimeout(this.flushTimer);
|
|
3660
|
+
this.terminal.write(data);
|
|
3661
|
+
this.bytesAccumulated += data.length;
|
|
3662
|
+
if (this.bytesAccumulated >= this.FLUSH_BYTES) {
|
|
3663
|
+
this.flush();
|
|
3666
3664
|
}
|
|
3667
|
-
this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs);
|
|
3668
3665
|
}
|
|
3669
3666
|
feedInput(data) {
|
|
3670
3667
|
if (!this.sessionActive) return;
|
|
@@ -3685,10 +3682,6 @@ class ConversationBridge {
|
|
|
3685
3682
|
* Only flushes when all pending writes have completed.
|
|
3686
3683
|
*/
|
|
3687
3684
|
flush() {
|
|
3688
|
-
if (this.pendingWrites > 0) {
|
|
3689
|
-
this.scheduleFlush();
|
|
3690
|
-
return;
|
|
3691
|
-
}
|
|
3692
3685
|
const buf = this.terminal.buffer.active;
|
|
3693
3686
|
const totalLines = buf.baseY + this.terminal.rows;
|
|
3694
3687
|
const allLines = [];
|
|
@@ -3707,6 +3700,7 @@ class ConversationBridge {
|
|
|
3707
3700
|
}
|
|
3708
3701
|
this.lastSnapshot = currentSnapshot;
|
|
3709
3702
|
const cleaned = newContent.replace(/\n{3,}/g, "\n\n").trim();
|
|
3703
|
+
this.bytesAccumulated = 0;
|
|
3710
3704
|
if (cleaned.length === 0) return;
|
|
3711
3705
|
this.sendEntry("terminal_output", cleaned);
|
|
3712
3706
|
}
|
|
@@ -3718,15 +3712,11 @@ class ConversationBridge {
|
|
|
3718
3712
|
content
|
|
3719
3713
|
});
|
|
3720
3714
|
}
|
|
3721
|
-
|
|
3715
|
+
endSession() {
|
|
3722
3716
|
if (!this.sessionActive) return;
|
|
3723
|
-
if (this.pendingWrites > 0) {
|
|
3724
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3725
|
-
}
|
|
3726
|
-
this.pendingWrites = 0;
|
|
3727
3717
|
this.flush();
|
|
3728
3718
|
if (this.flushTimer) {
|
|
3729
|
-
|
|
3719
|
+
clearInterval(this.flushTimer);
|
|
3730
3720
|
this.flushTimer = null;
|
|
3731
3721
|
}
|
|
3732
3722
|
this.sessionActive = false;
|
|
@@ -7855,7 +7845,7 @@ class Orchestrator {
|
|
|
7855
7845
|
// ─── PTY & Agent ────────────────────────────────
|
|
7856
7846
|
async spawnAgent(agent, cwd) {
|
|
7857
7847
|
if (this.ptyManager) {
|
|
7858
|
-
|
|
7848
|
+
this.bridge.endSession();
|
|
7859
7849
|
this.ptyManager.kill();
|
|
7860
7850
|
}
|
|
7861
7851
|
this.currentAgent = agent;
|
|
@@ -7873,7 +7863,7 @@ class Orchestrator {
|
|
|
7873
7863
|
const thisPtyManager = this.ptyManager;
|
|
7874
7864
|
this.ptyManager.onExit(async (exitCode) => {
|
|
7875
7865
|
this.events.onPtyExit(exitCode);
|
|
7876
|
-
|
|
7866
|
+
this.bridge.endSession();
|
|
7877
7867
|
if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
7878
7868
|
this.workerWs.disconnect();
|
|
7879
7869
|
}
|
|
@@ -7896,7 +7886,7 @@ class Orchestrator {
|
|
|
7896
7886
|
this.workerWs.sendTerminalResize(cols, rows);
|
|
7897
7887
|
}
|
|
7898
7888
|
async killAgent() {
|
|
7899
|
-
|
|
7889
|
+
this.bridge.endSession();
|
|
7900
7890
|
this.ptyManager?.kill();
|
|
7901
7891
|
this.ptyManager = null;
|
|
7902
7892
|
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
@@ -7945,7 +7935,7 @@ class Orchestrator {
|
|
|
7945
7935
|
}
|
|
7946
7936
|
// ─── Shutdown ───────────────────────────────────
|
|
7947
7937
|
async shutdown() {
|
|
7948
|
-
|
|
7938
|
+
this.bridge.endSession();
|
|
7949
7939
|
this.ptyManager?.kill();
|
|
7950
7940
|
this.ptyManager = null;
|
|
7951
7941
|
this.workerWs.disconnect();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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/bridge.ts
CHANGED
|
@@ -11,15 +11,17 @@ import { WorkerWsClient } from './workerWs'
|
|
|
11
11
|
*/
|
|
12
12
|
export class ConversationBridge {
|
|
13
13
|
private wsClient: WorkerWsClient | null = null
|
|
14
|
-
private flushTimer: ReturnType<typeof setTimeout> | null = null
|
|
15
|
-
private flushIntervalMs: number = 3000
|
|
16
14
|
private sessionActive: boolean = false
|
|
17
15
|
private inputBuffer: string = ''
|
|
16
|
+
private bytesAccumulated: number = 0
|
|
18
17
|
|
|
19
18
|
// Virtual terminal for processing escape sequences into rendered text
|
|
20
19
|
private terminal: Terminal
|
|
21
20
|
private lastSnapshot: string = ''
|
|
22
|
-
|
|
21
|
+
|
|
22
|
+
// Flush after accumulating enough data or on a timer
|
|
23
|
+
private readonly FLUSH_BYTES = 2000
|
|
24
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null
|
|
23
25
|
|
|
24
26
|
constructor() {
|
|
25
27
|
this.terminal = new Terminal({ cols: 120, rows: 50, scrollback: 10000 })
|
|
@@ -32,9 +34,14 @@ export class ConversationBridge {
|
|
|
32
34
|
startSession(): void {
|
|
33
35
|
this.terminal.reset()
|
|
34
36
|
this.lastSnapshot = ''
|
|
35
|
-
this.
|
|
37
|
+
this.bytesAccumulated = 0
|
|
36
38
|
this.inputBuffer = ''
|
|
37
39
|
this.sessionActive = true
|
|
40
|
+
|
|
41
|
+
// Periodic flush via setInterval (setTimeout may not fire under raw-mode TUI)
|
|
42
|
+
if (this.flushTimer) clearInterval(this.flushTimer)
|
|
43
|
+
this.flushTimer = setInterval(() => this.flush(), 5000)
|
|
44
|
+
|
|
38
45
|
console.log('[bridge] Session started')
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -46,18 +53,13 @@ export class ConversationBridge {
|
|
|
46
53
|
feedOutput(data: string): void {
|
|
47
54
|
if (!this.sessionActive) return
|
|
48
55
|
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
this.pendingWrites--
|
|
52
|
-
this.scheduleFlush()
|
|
53
|
-
})
|
|
54
|
-
}
|
|
56
|
+
this.terminal.write(data)
|
|
57
|
+
this.bytesAccumulated += data.length
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
if (this.
|
|
58
|
-
|
|
59
|
+
// Flush when enough data accumulated
|
|
60
|
+
if (this.bytesAccumulated >= this.FLUSH_BYTES) {
|
|
61
|
+
this.flush()
|
|
59
62
|
}
|
|
60
|
-
this.flushTimer = setTimeout(() => this.flush(), this.flushIntervalMs)
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
feedInput(data: string): void {
|
|
@@ -82,11 +84,6 @@ export class ConversationBridge {
|
|
|
82
84
|
* Only flushes when all pending writes have completed.
|
|
83
85
|
*/
|
|
84
86
|
private flush(): void {
|
|
85
|
-
if (this.pendingWrites > 0) {
|
|
86
|
-
this.scheduleFlush()
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
87
|
const buf = this.terminal.buffer.active
|
|
91
88
|
// Read all lines: scrollback (baseY) + visible area (rows)
|
|
92
89
|
const totalLines = buf.baseY + this.terminal.rows
|
|
@@ -114,6 +111,9 @@ export class ConversationBridge {
|
|
|
114
111
|
this.lastSnapshot = currentSnapshot
|
|
115
112
|
|
|
116
113
|
const cleaned = newContent.replace(/\n{3,}/g, '\n\n').trim()
|
|
114
|
+
|
|
115
|
+
this.bytesAccumulated = 0
|
|
116
|
+
|
|
117
117
|
if (cleaned.length === 0) return
|
|
118
118
|
|
|
119
119
|
this.sendEntry('terminal_output', cleaned)
|
|
@@ -128,19 +128,14 @@ export class ConversationBridge {
|
|
|
128
128
|
})
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
endSession(): void {
|
|
132
132
|
if (!this.sessionActive) return
|
|
133
133
|
|
|
134
|
-
//
|
|
135
|
-
if (this.pendingWrites > 0) {
|
|
136
|
-
await new Promise<void>(resolve => setTimeout(resolve, 500))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
this.pendingWrites = 0
|
|
134
|
+
// Give xterm a moment to process any remaining writes
|
|
140
135
|
this.flush()
|
|
141
136
|
|
|
142
137
|
if (this.flushTimer) {
|
|
143
|
-
|
|
138
|
+
clearInterval(this.flushTimer)
|
|
144
139
|
this.flushTimer = null
|
|
145
140
|
}
|
|
146
141
|
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -284,7 +284,7 @@ export class Orchestrator {
|
|
|
284
284
|
|
|
285
285
|
async spawnAgent(agent: AgentConfig, cwd: string): Promise<void> {
|
|
286
286
|
if (this.ptyManager) {
|
|
287
|
-
|
|
287
|
+
this.bridge.endSession()
|
|
288
288
|
this.ptyManager.kill()
|
|
289
289
|
}
|
|
290
290
|
|
|
@@ -307,7 +307,7 @@ export class Orchestrator {
|
|
|
307
307
|
|
|
308
308
|
this.ptyManager.onExit(async (exitCode: number) => {
|
|
309
309
|
this.events.onPtyExit(exitCode)
|
|
310
|
-
|
|
310
|
+
this.bridge.endSession()
|
|
311
311
|
if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
312
312
|
this.workerWs.disconnect()
|
|
313
313
|
}
|
|
@@ -335,7 +335,7 @@ export class Orchestrator {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
async killAgent(): Promise<void> {
|
|
338
|
-
|
|
338
|
+
this.bridge.endSession()
|
|
339
339
|
this.ptyManager?.kill()
|
|
340
340
|
this.ptyManager = null
|
|
341
341
|
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
@@ -391,7 +391,7 @@ export class Orchestrator {
|
|
|
391
391
|
// ─── Shutdown ───────────────────────────────────
|
|
392
392
|
|
|
393
393
|
async shutdown(): Promise<void> {
|
|
394
|
-
|
|
394
|
+
this.bridge.endSession()
|
|
395
395
|
this.ptyManager?.kill()
|
|
396
396
|
this.ptyManager = null
|
|
397
397
|
this.workerWs.disconnect()
|