@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/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
- lastSnapshotLines = 0;
3635
- pendingWrites = 0;
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.lastSnapshotLines = 0;
3645
- this.pendingWrites = 0;
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.pendingWrites++;
3658
- this.terminal.write(data, () => {
3659
- this.pendingWrites--;
3660
- this.scheduleFlush();
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 + buf.cursorY + 1;
3694
- if (totalLines <= this.lastSnapshotLines) return;
3695
- const newLines = [];
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
- newLines.push(line.translateToString(true));
3691
+ allLines.push(line.translateToString(true));
3700
3692
  }
3701
3693
  }
3702
- this.lastSnapshotLines = totalLines;
3703
- const content = newLines.join("\n");
3704
- const cleaned = content.replace(/\n{3,}/g, "\n\n").trim();
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
- async endSession() {
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
- clearTimeout(this.flushTimer);
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
- await this.bridge.endSession();
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
- await this.bridge.endSession();
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
- await this.bridge.endSession();
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
- await this.bridge.endSession();
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.8",
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": {
@@ -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 lastSnapshotLines: number = 0
22
- private pendingWrites: number = 0
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.lastSnapshotLines = 0
35
- this.pendingWrites = 0
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.pendingWrites++
50
- this.terminal.write(data, () => {
51
- this.pendingWrites--
52
- this.scheduleFlush()
53
- })
54
- }
56
+ this.terminal.write(data)
57
+ this.bytesAccumulated += data.length
55
58
 
56
- private scheduleFlush(): void {
57
- if (this.flushTimer) {
58
- clearTimeout(this.flushTimer)
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
- const totalLines = buf.baseY + buf.cursorY + 1
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 newLines: string[] = []
97
- for (let i = this.lastSnapshotLines; i < totalLines; 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
- newLines.push(line.translateToString(true))
95
+ allLines.push(line.translateToString(true))
101
96
  }
102
97
  }
103
98
 
104
- this.lastSnapshotLines = totalLines
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
- async endSession(): Promise<void> {
131
+ endSession(): void {
123
132
  if (!this.sessionActive) return
124
133
 
125
- // If writes are pending, give xterm a moment to process them
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
- clearTimeout(this.flushTimer)
138
+ clearInterval(this.flushTimer)
135
139
  this.flushTimer = null
136
140
  }
137
141
 
@@ -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
- await this.bridge.endSession()
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
- await this.bridge.endSession()
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
- await this.bridge.endSession()
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
- await this.bridge.endSession()
394
+ this.bridge.endSession()
395
395
  this.ptyManager?.kill()
396
396
  this.ptyManager = null
397
397
  this.workerWs.disconnect()