@phenx-inc/ctlsurf 0.1.8 → 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 +33 -38
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +33 -38
- package/package.json +1 -1
- package/src/main/bridge.ts +43 -39
- 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
|
-
|
|
3635
|
-
|
|
3633
|
+
lastSnapshot = "";
|
|
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
|
}
|
|
@@ -3641,10 +3642,12 @@ class ConversationBridge {
|
|
|
3641
3642
|
}
|
|
3642
3643
|
startSession() {
|
|
3643
3644
|
this.terminal.reset();
|
|
3644
|
-
this.
|
|
3645
|
-
this.
|
|
3645
|
+
this.lastSnapshot = "";
|
|
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,23 +3682,25 @@ 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
|
-
const totalLines = buf.baseY +
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
for (let i = this.lastSnapshotLines; i < totalLines; i++) {
|
|
3686
|
+
const totalLines = buf.baseY + this.terminal.rows;
|
|
3687
|
+
const allLines = [];
|
|
3688
|
+
for (let i = 0; i < totalLines; i++) {
|
|
3697
3689
|
const line = buf.getLine(i);
|
|
3698
3690
|
if (line) {
|
|
3699
|
-
|
|
3691
|
+
allLines.push(line.translateToString(true));
|
|
3700
3692
|
}
|
|
3701
3693
|
}
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3694
|
+
const currentSnapshot = allLines.join("\n");
|
|
3695
|
+
let newContent;
|
|
3696
|
+
if (currentSnapshot.startsWith(this.lastSnapshot)) {
|
|
3697
|
+
newContent = currentSnapshot.slice(this.lastSnapshot.length);
|
|
3698
|
+
} else {
|
|
3699
|
+
newContent = currentSnapshot;
|
|
3700
|
+
}
|
|
3701
|
+
this.lastSnapshot = currentSnapshot;
|
|
3702
|
+
const cleaned = newContent.replace(/\n{3,}/g, "\n\n").trim();
|
|
3703
|
+
this.bytesAccumulated = 0;
|
|
3705
3704
|
if (cleaned.length === 0) return;
|
|
3706
3705
|
this.sendEntry("terminal_output", cleaned);
|
|
3707
3706
|
}
|
|
@@ -3713,15 +3712,11 @@ class ConversationBridge {
|
|
|
3713
3712
|
content
|
|
3714
3713
|
});
|
|
3715
3714
|
}
|
|
3716
|
-
|
|
3715
|
+
endSession() {
|
|
3717
3716
|
if (!this.sessionActive) return;
|
|
3718
|
-
if (this.pendingWrites > 0) {
|
|
3719
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3720
|
-
}
|
|
3721
|
-
this.pendingWrites = 0;
|
|
3722
3717
|
this.flush();
|
|
3723
3718
|
if (this.flushTimer) {
|
|
3724
|
-
|
|
3719
|
+
clearInterval(this.flushTimer);
|
|
3725
3720
|
this.flushTimer = null;
|
|
3726
3721
|
}
|
|
3727
3722
|
this.sessionActive = false;
|
|
@@ -7850,7 +7845,7 @@ class Orchestrator {
|
|
|
7850
7845
|
// ─── PTY & Agent ────────────────────────────────
|
|
7851
7846
|
async spawnAgent(agent, cwd) {
|
|
7852
7847
|
if (this.ptyManager) {
|
|
7853
|
-
|
|
7848
|
+
this.bridge.endSession();
|
|
7854
7849
|
this.ptyManager.kill();
|
|
7855
7850
|
}
|
|
7856
7851
|
this.currentAgent = agent;
|
|
@@ -7868,7 +7863,7 @@ class Orchestrator {
|
|
|
7868
7863
|
const thisPtyManager = this.ptyManager;
|
|
7869
7864
|
this.ptyManager.onExit(async (exitCode) => {
|
|
7870
7865
|
this.events.onPtyExit(exitCode);
|
|
7871
|
-
|
|
7866
|
+
this.bridge.endSession();
|
|
7872
7867
|
if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
7873
7868
|
this.workerWs.disconnect();
|
|
7874
7869
|
}
|
|
@@ -7891,7 +7886,7 @@ class Orchestrator {
|
|
|
7891
7886
|
this.workerWs.sendTerminalResize(cols, rows);
|
|
7892
7887
|
}
|
|
7893
7888
|
async killAgent() {
|
|
7894
|
-
|
|
7889
|
+
this.bridge.endSession();
|
|
7895
7890
|
this.ptyManager?.kill();
|
|
7896
7891
|
this.ptyManager = null;
|
|
7897
7892
|
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
@@ -7940,7 +7935,7 @@ class Orchestrator {
|
|
|
7940
7935
|
}
|
|
7941
7936
|
// ─── Shutdown ───────────────────────────────────
|
|
7942
7937
|
async shutdown() {
|
|
7943
|
-
|
|
7938
|
+
this.bridge.endSession();
|
|
7944
7939
|
this.ptyManager?.kill();
|
|
7945
7940
|
this.ptyManager = null;
|
|
7946
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
|
-
private
|
|
22
|
-
|
|
20
|
+
private lastSnapshot: string = ''
|
|
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 })
|
|
@@ -31,10 +33,15 @@ export class ConversationBridge {
|
|
|
31
33
|
|
|
32
34
|
startSession(): void {
|
|
33
35
|
this.terminal.reset()
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
+
this.lastSnapshot = ''
|
|
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,29 +84,36 @@ 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
|
-
// Writes still processing — reschedule
|
|
87
|
-
this.scheduleFlush()
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
87
|
const buf = this.terminal.buffer.active
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (totalLines <= this.lastSnapshotLines) return
|
|
88
|
+
// Read all lines: scrollback (baseY) + visible area (rows)
|
|
89
|
+
const totalLines = buf.baseY + this.terminal.rows
|
|
95
90
|
|
|
96
|
-
const
|
|
97
|
-
for (let i =
|
|
91
|
+
const allLines: string[] = []
|
|
92
|
+
for (let i = 0; i < totalLines; i++) {
|
|
98
93
|
const line = buf.getLine(i)
|
|
99
94
|
if (line) {
|
|
100
|
-
|
|
95
|
+
allLines.push(line.translateToString(true))
|
|
101
96
|
}
|
|
102
97
|
}
|
|
103
98
|
|
|
104
|
-
|
|
99
|
+
const currentSnapshot = allLines.join('\n')
|
|
100
|
+
|
|
101
|
+
// Diff: find new content since last snapshot
|
|
102
|
+
let newContent: string
|
|
103
|
+
if (currentSnapshot.startsWith(this.lastSnapshot)) {
|
|
104
|
+
// Common case: new content appended
|
|
105
|
+
newContent = currentSnapshot.slice(this.lastSnapshot.length)
|
|
106
|
+
} else {
|
|
107
|
+
// Screen was cleared/rewritten — send full current state
|
|
108
|
+
newContent = currentSnapshot
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.lastSnapshot = currentSnapshot
|
|
112
|
+
|
|
113
|
+
const cleaned = newContent.replace(/\n{3,}/g, '\n\n').trim()
|
|
114
|
+
|
|
115
|
+
this.bytesAccumulated = 0
|
|
105
116
|
|
|
106
|
-
const content = newLines.join('\n')
|
|
107
|
-
const cleaned = content.replace(/\n{3,}/g, '\n\n').trim()
|
|
108
117
|
if (cleaned.length === 0) return
|
|
109
118
|
|
|
110
119
|
this.sendEntry('terminal_output', cleaned)
|
|
@@ -119,19 +128,14 @@ export class ConversationBridge {
|
|
|
119
128
|
})
|
|
120
129
|
}
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
endSession(): void {
|
|
123
132
|
if (!this.sessionActive) return
|
|
124
133
|
|
|
125
|
-
//
|
|
126
|
-
if (this.pendingWrites > 0) {
|
|
127
|
-
await new Promise<void>(resolve => setTimeout(resolve, 500))
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
this.pendingWrites = 0
|
|
134
|
+
// Give xterm a moment to process any remaining writes
|
|
131
135
|
this.flush()
|
|
132
136
|
|
|
133
137
|
if (this.flushTimer) {
|
|
134
|
-
|
|
138
|
+
clearInterval(this.flushTimer)
|
|
135
139
|
this.flushTimer = null
|
|
136
140
|
}
|
|
137
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()
|