@phenx-inc/ctlsurf 0.1.11 → 0.1.13
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 +20 -3466
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +47 -3474
- package/package.json +1 -2
- package/src/main/bridge.ts +40 -75
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
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": {
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
"@monaco-editor/react": "^4.7.0",
|
|
47
47
|
"@xterm/addon-fit": "^0.10.0",
|
|
48
48
|
"@xterm/addon-web-links": "^0.11.0",
|
|
49
|
-
"@xterm/headless": "^5.5.0",
|
|
50
49
|
"@xterm/xterm": "^5.5.0",
|
|
51
50
|
"electron-store": "^10.0.0",
|
|
52
51
|
"esbuild": "^0.27.4",
|
package/src/main/bridge.ts
CHANGED
|
@@ -1,63 +1,38 @@
|
|
|
1
|
-
import { Terminal } from '@xterm/headless'
|
|
2
1
|
import { WorkerWsClient } from './workerWs'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Conversation Bridge
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Captures PTY output, strips ANSI codes and terminal artifacts,
|
|
7
|
+
* and sends cleaned text to the backend via WebSocket for chat logging.
|
|
8
|
+
* Uses a simple buffer + byte threshold approach (no timers, since
|
|
9
|
+
* setTimeout/setInterval don't reliably fire under TUI raw mode).
|
|
11
10
|
*/
|
|
12
11
|
export class ConversationBridge {
|
|
13
12
|
private wsClient: WorkerWsClient | null = null
|
|
14
13
|
private sessionActive: boolean = false
|
|
15
14
|
private inputBuffer: string = ''
|
|
16
|
-
private
|
|
15
|
+
private outputBuffer: string = ''
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
private terminal: Terminal
|
|
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
|
|
25
|
-
|
|
26
|
-
constructor() {
|
|
27
|
-
this.terminal = new Terminal({ cols: 120, rows: 50, scrollback: 10000, allowProposedApi: true })
|
|
28
|
-
}
|
|
17
|
+
private readonly FLUSH_BYTES = 500
|
|
29
18
|
|
|
30
19
|
setWsClient(ws: WorkerWsClient): void {
|
|
31
20
|
this.wsClient = ws
|
|
32
21
|
}
|
|
33
22
|
|
|
34
23
|
startSession(): void {
|
|
35
|
-
this.
|
|
36
|
-
this.lastSnapshot = ''
|
|
37
|
-
this.bytesAccumulated = 0
|
|
24
|
+
this.outputBuffer = ''
|
|
38
25
|
this.inputBuffer = ''
|
|
39
26
|
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
|
-
|
|
45
27
|
console.log('[bridge] Session started')
|
|
46
28
|
}
|
|
47
29
|
|
|
48
|
-
/**
|
|
49
|
-
* Feed terminal output data into the bridge.
|
|
50
|
-
* xterm.write() is async — we track pending writes and only flush
|
|
51
|
-
* when all writes have been processed into the buffer.
|
|
52
|
-
*/
|
|
53
30
|
feedOutput(data: string): void {
|
|
54
31
|
if (!this.sessionActive) return
|
|
55
32
|
|
|
56
|
-
this.
|
|
57
|
-
this.bytesAccumulated += data.length
|
|
33
|
+
this.outputBuffer += data
|
|
58
34
|
|
|
59
|
-
|
|
60
|
-
if (this.bytesAccumulated >= this.FLUSH_BYTES) {
|
|
35
|
+
if (this.outputBuffer.length >= this.FLUSH_BYTES) {
|
|
61
36
|
this.flush()
|
|
62
37
|
}
|
|
63
38
|
}
|
|
@@ -75,45 +50,17 @@ export class ConversationBridge {
|
|
|
75
50
|
}
|
|
76
51
|
}
|
|
77
52
|
|
|
78
|
-
resize(
|
|
79
|
-
|
|
53
|
+
resize(_cols: number, _rows: number): void {
|
|
54
|
+
// no-op now (was used for xterm)
|
|
80
55
|
}
|
|
81
56
|
|
|
82
|
-
/**
|
|
83
|
-
* Read new content from the terminal buffer since the last flush.
|
|
84
|
-
* Only flushes when all pending writes have completed.
|
|
85
|
-
*/
|
|
86
57
|
private flush(): void {
|
|
87
|
-
|
|
88
|
-
// Read all lines: scrollback (baseY) + visible area (rows)
|
|
89
|
-
const totalLines = buf.baseY + this.terminal.rows
|
|
90
|
-
|
|
91
|
-
const allLines: string[] = []
|
|
92
|
-
for (let i = 0; i < totalLines; i++) {
|
|
93
|
-
const line = buf.getLine(i)
|
|
94
|
-
if (line) {
|
|
95
|
-
allLines.push(line.translateToString(true))
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
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
|
|
58
|
+
if (this.outputBuffer.length === 0) return
|
|
112
59
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
this.bytesAccumulated = 0
|
|
60
|
+
const raw = this.outputBuffer
|
|
61
|
+
this.outputBuffer = ''
|
|
116
62
|
|
|
63
|
+
const cleaned = cleanOutput(raw)
|
|
117
64
|
if (cleaned.length === 0) return
|
|
118
65
|
|
|
119
66
|
this.sendEntry('terminal_output', cleaned)
|
|
@@ -131,16 +78,11 @@ export class ConversationBridge {
|
|
|
131
78
|
endSession(): void {
|
|
132
79
|
if (!this.sessionActive) return
|
|
133
80
|
|
|
134
|
-
// Give xterm a moment to process any remaining writes
|
|
135
81
|
this.flush()
|
|
136
82
|
|
|
137
|
-
if (this.flushTimer) {
|
|
138
|
-
clearInterval(this.flushTimer)
|
|
139
|
-
this.flushTimer = null
|
|
140
|
-
}
|
|
141
|
-
|
|
142
83
|
this.sessionActive = false
|
|
143
84
|
this.inputBuffer = ''
|
|
85
|
+
this.outputBuffer = ''
|
|
144
86
|
console.log('[bridge] Session ended')
|
|
145
87
|
}
|
|
146
88
|
}
|
|
@@ -150,9 +92,15 @@ export class ConversationBridge {
|
|
|
150
92
|
*/
|
|
151
93
|
function stripAnsi(str: string): string {
|
|
152
94
|
return str
|
|
95
|
+
// CSI sequences (e.g. \x1b[0m, \x1b[?2004h, \x1b[1;32m, \x1b[38;2;r;g;bm)
|
|
153
96
|
.replace(/\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, '')
|
|
97
|
+
// OSC sequences (e.g. \x1b]0;title\x07, \x1b]...\x1b\\)
|
|
154
98
|
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
155
|
-
|
|
99
|
+
// DCS/PM/APC sequences
|
|
100
|
+
.replace(/\x1b[PX^_][^\x1b]*\x1b\\/g, '')
|
|
101
|
+
// Other escape sequences (charset, keypad mode, etc.)
|
|
102
|
+
.replace(/\x1b[^[\]PX^_](.|$)/g, '')
|
|
103
|
+
// Remaining single ESC
|
|
156
104
|
.replace(/\x1b/g, '')
|
|
157
105
|
}
|
|
158
106
|
|
|
@@ -181,3 +129,20 @@ function cleanInput(str: string): string {
|
|
|
181
129
|
cleaned = cleaned.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
182
130
|
return cleaned.trim()
|
|
183
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Clean terminal output: strip ANSI, remove control chars, collapse noise.
|
|
135
|
+
*/
|
|
136
|
+
function cleanOutput(str: string): string {
|
|
137
|
+
let cleaned = stripAnsi(str)
|
|
138
|
+
// Remove carriage returns
|
|
139
|
+
cleaned = cleaned.replace(/\r/g, '')
|
|
140
|
+
// Remove control characters except newline/tab
|
|
141
|
+
// eslint-disable-next-line no-control-regex
|
|
142
|
+
cleaned = cleaned.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
143
|
+
// Collapse 3+ consecutive newlines into 2
|
|
144
|
+
cleaned = cleaned.replace(/\n{3,}/g, '\n\n')
|
|
145
|
+
// Remove lines that are only whitespace
|
|
146
|
+
cleaned = cleaned.split('\n').filter(line => line.trim().length > 0).join('\n')
|
|
147
|
+
return cleaned.trim()
|
|
148
|
+
}
|