@phenx-inc/ctlsurf 0.1.18 → 0.1.21
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 +21 -62
- package/out/headless/index.mjs.map +2 -2
- package/out/renderer/assets/{cssMode-Cxe23-tB.js → cssMode-C6bY9C4O.js} +3 -3
- package/out/renderer/assets/{freemarker2-Be0nj7Oa.js → freemarker2-CkAJiX1K.js} +1 -1
- package/out/renderer/assets/{handlebars-C0It7_Nu.js → handlebars-DnLXVUXp.js} +1 -1
- package/out/renderer/assets/{html-BW6LB-7J.js → html-Ds5-qvDh.js} +1 -1
- package/out/renderer/assets/{htmlMode-D_V-1VlE.js → htmlMode-DYFYy4MK.js} +3 -3
- package/out/renderer/assets/{index-D568SpEt.js → index-DwSsD_Xm.js} +41 -24
- package/out/renderer/assets/{javascript-D_LoeNc7.js → javascript-CiHhG2a9.js} +2 -2
- package/out/renderer/assets/{jsonMode-K3WSinSE.js → jsonMode-DdDRlbXP.js} +3 -3
- package/out/renderer/assets/{liquid-BqfOd6m8.js → liquid-BP5mb-uD.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bjf28WU6.js → lspLanguageFeatures-Dljhj5Gh.js} +1 -1
- package/out/renderer/assets/{mdx-BoESjI38.js → mdx-D4u3N7dt.js} +1 -1
- package/out/renderer/assets/{python-DlafOOgB.js → python-BQDHXVwp.js} +1 -1
- package/out/renderer/assets/{razor-CB6E9DBD.js → razor-BfXW9cDc.js} +1 -1
- package/out/renderer/assets/{tsMode-DYu1z_nn.js → tsMode-BGTjG8Ow.js} +1 -1
- package/out/renderer/assets/{typescript-CDjkh0d5.js → typescript-422MU_YO.js} +1 -1
- package/out/renderer/assets/{xml-C-XlilKZ.js → xml-B6EKhHiy.js} +1 -1
- package/out/renderer/assets/{yaml-BTrtxLEo.js → yaml-LkO_eGYb.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
- package/src/main/headless.ts +12 -0
- package/src/main/tui.ts +31 -107
- package/src/renderer/components/TerminalPanel.tsx +27 -4
package/src/main/tui.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal UI (TUI) renderer
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Minimal status bar at the bottom, full PTY passthrough above.
|
|
5
|
+
* No alternate screen — the PTY (Claude Code) owns the screen and
|
|
6
|
+
* handles its own scrolling, mouse events, and display management.
|
|
7
|
+
* Status info also goes to the terminal tab title via OSC.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
const ESC = '\x1b'
|
|
@@ -10,13 +12,13 @@ const CSI = `${ESC}[`
|
|
|
10
12
|
|
|
11
13
|
// Colors (Tokyo Night palette)
|
|
12
14
|
const BG_BAR = `${CSI}48;2;22;22;30m` // #16161e
|
|
13
|
-
const FG_TITLE = `${CSI}38;2;192;202;245m` // #c0caf5
|
|
14
15
|
const FG_DIM = `${CSI}38;2;86;95;137m` // #565f89
|
|
15
16
|
const FG_ACCENT = `${CSI}38;2;122;162;247m` // #7aa2f7
|
|
16
17
|
const FG_GREEN = `${CSI}38;2;158;206;106m` // #9ece6a
|
|
17
18
|
const FG_RED = `${CSI}38;2;247;118;142m` // #f7768e
|
|
18
19
|
const FG_YELLOW = `${CSI}38;2;224;175;104m` // #e0af68
|
|
19
20
|
const FG_WHITE = `${CSI}38;2;169;177;214m` // #a9b1d6
|
|
21
|
+
const FG_TITLE = `${CSI}38;2;192;202;245m` // #c0caf5
|
|
20
22
|
const BG_MODAL = `${CSI}48;2;31;35;53m` // #1f2335
|
|
21
23
|
const BG_SELECTED = `${CSI}48;2;42;43;61m` // #2a2b3d
|
|
22
24
|
const RESET = `${CSI}0m`
|
|
@@ -46,28 +48,18 @@ export class Tui {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
* Initialize the TUI:
|
|
51
|
+
* Initialize the TUI: scroll region leaving last row for status bar.
|
|
52
|
+
* No alternate screen — PTY fully owns the display.
|
|
50
53
|
*/
|
|
51
54
|
init(): void {
|
|
52
|
-
//
|
|
53
|
-
this.
|
|
54
|
-
// Set scroll region: lines 2 to (rows - 1), leaving line 1 for title, line rows for status
|
|
55
|
-
this.setScrollRegion()
|
|
56
|
-
// Move cursor to the PTY area
|
|
57
|
-
this.moveToPtyArea()
|
|
58
|
-
// Draw chrome
|
|
59
|
-
this.drawTitleBar()
|
|
60
|
-
this.drawStatusBar()
|
|
55
|
+
// Just set tab title — the PTY agent owns the full screen
|
|
56
|
+
this.updateTerminalTitle()
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
/**
|
|
64
60
|
* Restore terminal to normal state
|
|
65
61
|
*/
|
|
66
62
|
destroy(): void {
|
|
67
|
-
// Reset scroll region
|
|
68
|
-
this.write(`${CSI}r`)
|
|
69
|
-
// Leave alternate screen
|
|
70
|
-
this.write(`${CSI}?1049l`)
|
|
71
63
|
// Show cursor
|
|
72
64
|
this.write(`${CSI}?25h`)
|
|
73
65
|
}
|
|
@@ -78,46 +70,35 @@ export class Tui {
|
|
|
78
70
|
resize(cols: number, rows: number): void {
|
|
79
71
|
this.cols = cols
|
|
80
72
|
this.rows = rows
|
|
81
|
-
this.setScrollRegion()
|
|
82
|
-
this.drawTitleBar()
|
|
83
|
-
this.drawStatusBar()
|
|
84
|
-
this.moveToPtyArea()
|
|
85
73
|
}
|
|
86
74
|
|
|
87
75
|
/**
|
|
88
|
-
* Get the PTY dimensions (
|
|
76
|
+
* Get the PTY dimensions (all rows minus status bar)
|
|
89
77
|
*/
|
|
90
78
|
getPtySize(): { cols: number; rows: number } {
|
|
91
79
|
return {
|
|
92
80
|
cols: this.cols,
|
|
93
|
-
rows:
|
|
81
|
+
rows: this.rows,
|
|
94
82
|
}
|
|
95
83
|
}
|
|
96
84
|
|
|
97
85
|
/**
|
|
98
|
-
* Update state and redraw
|
|
86
|
+
* Update state and redraw status bar
|
|
99
87
|
*/
|
|
100
88
|
update(partial: Partial<TuiState>): void {
|
|
101
89
|
Object.assign(this.state, partial)
|
|
102
|
-
|
|
103
|
-
this.write(`${CSI}s`) // save cursor
|
|
104
|
-
this.drawTitleBar()
|
|
105
|
-
this.drawStatusBar()
|
|
106
|
-
this.write(`${CSI}u`) // restore cursor
|
|
90
|
+
this.updateTerminalTitle()
|
|
107
91
|
}
|
|
108
92
|
|
|
109
93
|
/**
|
|
110
|
-
* Write PTY output
|
|
111
|
-
* Cursor is assumed to be in the PTY area already.
|
|
94
|
+
* Write PTY output — full passthrough.
|
|
112
95
|
*/
|
|
113
96
|
writePtyData(data: string): void {
|
|
114
|
-
// PTY data goes straight to stdout — it's already in the scroll region
|
|
115
97
|
this.write(data)
|
|
116
98
|
}
|
|
117
99
|
|
|
118
100
|
/**
|
|
119
101
|
* Update the terminal window/tab title via OSC escape sequence.
|
|
120
|
-
* Works in passthrough mode (no chrome) — doesn't conflict with the agent's TUI.
|
|
121
102
|
*/
|
|
122
103
|
setTerminalTitle(title: string): void {
|
|
123
104
|
this.write(`${ESC}]0;${title}\x07`)
|
|
@@ -149,49 +130,41 @@ export class Tui {
|
|
|
149
130
|
|
|
150
131
|
/**
|
|
151
132
|
* Show an interactive agent picker modal.
|
|
152
|
-
*
|
|
133
|
+
* Uses alternate screen just for the picker, then exits back to normal.
|
|
153
134
|
*/
|
|
154
135
|
showAgentPicker(agents: { name: string; description: string }[]): Promise<number> {
|
|
155
136
|
return new Promise((resolve) => {
|
|
156
137
|
let selected = 0
|
|
157
138
|
const modalWidth = 44
|
|
158
|
-
const modalHeight = agents.length + 4
|
|
139
|
+
const modalHeight = agents.length + 4
|
|
159
140
|
const startCol = Math.max(1, Math.floor((this.cols - modalWidth) / 2))
|
|
160
141
|
const startRow = Math.max(1, Math.floor((this.rows - modalHeight) / 2))
|
|
161
142
|
|
|
162
|
-
// Enter alternate screen
|
|
143
|
+
// Enter alternate screen just for the picker
|
|
163
144
|
this.write(`${CSI}?1049h`)
|
|
164
|
-
// Hide cursor
|
|
165
145
|
this.write(`${CSI}?25l`)
|
|
166
146
|
|
|
167
147
|
const drawModal = () => {
|
|
168
148
|
const topBorder = '\u250c' + '\u2500'.repeat(modalWidth - 2) + '\u2510'
|
|
169
149
|
const botBorder = '\u2514' + '\u2500'.repeat(modalWidth - 2) + '\u2518'
|
|
170
|
-
const emptyLine = '\u2502' + ' '.repeat(modalWidth - 2) + '\u2502'
|
|
171
150
|
|
|
172
|
-
// Draw background fill
|
|
173
151
|
for (let r = 0; r < this.rows; r++) {
|
|
174
152
|
this.write(`${CSI}${r + 1};1H${BG_BAR}${CSI}2K${RESET}`)
|
|
175
153
|
}
|
|
176
154
|
|
|
177
|
-
// Draw logo/branding centered
|
|
178
155
|
const brand = 'ctlsurf'
|
|
179
156
|
const brandCol = Math.max(1, Math.floor((this.cols - brand.length) / 2))
|
|
180
157
|
this.write(`${CSI}${startRow - 2};${brandCol}H${FG_ACCENT}${brand}${RESET}`)
|
|
181
158
|
|
|
182
|
-
// Top border
|
|
183
159
|
this.write(`${CSI}${startRow};${startCol}H${BG_MODAL}${FG_DIM}${topBorder}${RESET}`)
|
|
184
160
|
|
|
185
|
-
// Title
|
|
186
161
|
const title = ' Select Agent'
|
|
187
162
|
const titlePad = ' '.repeat(Math.max(0, modalWidth - 2 - title.length))
|
|
188
163
|
this.write(`${CSI}${startRow + 1};${startCol}H${BG_MODAL}${FG_DIM}\u2502${RESET}${BG_MODAL}${FG_TITLE}${title}${titlePad}${FG_DIM}\u2502${RESET}`)
|
|
189
164
|
|
|
190
|
-
// Separator
|
|
191
165
|
const sep = '\u251c' + '\u2500'.repeat(modalWidth - 2) + '\u2524'
|
|
192
166
|
this.write(`${CSI}${startRow + 2};${startCol}H${BG_MODAL}${FG_DIM}${sep}${RESET}`)
|
|
193
167
|
|
|
194
|
-
// Agent items
|
|
195
168
|
for (let i = 0; i < agents.length; i++) {
|
|
196
169
|
const agent = agents[i]
|
|
197
170
|
const row = startRow + 3 + i
|
|
@@ -199,28 +172,24 @@ export class Tui {
|
|
|
199
172
|
const bg = isSelected ? BG_SELECTED : BG_MODAL
|
|
200
173
|
const pointer = isSelected ? `${FG_ACCENT}\u25B8 ` : ' '
|
|
201
174
|
const nameFg = isSelected ? FG_ACCENT : FG_WHITE
|
|
202
|
-
const descFg = FG_DIM
|
|
203
175
|
const nameStr = agent.name
|
|
204
|
-
const descStr = agent.description ? ` ${FG_DIM}
|
|
176
|
+
const descStr = agent.description ? ` ${FG_DIM}\u2014 ${agent.description.slice(0, 20)}` : ''
|
|
205
177
|
const content = `${pointer}${nameFg}${nameStr}${descStr}`
|
|
206
|
-
const contentLen =
|
|
178
|
+
const contentLen = 2 + nameStr.length + (agent.description ? 3 + Math.min(20, agent.description.length) : 0)
|
|
207
179
|
const pad = ' '.repeat(Math.max(0, modalWidth - 2 - contentLen))
|
|
208
180
|
this.write(`${CSI}${row};${startCol}H${bg}${FG_DIM}\u2502${RESET}${bg}${content}${pad}${RESET}${BG_MODAL}${FG_DIM}\u2502${RESET}`)
|
|
209
181
|
}
|
|
210
182
|
|
|
211
|
-
// Bottom border
|
|
212
183
|
const botRow = startRow + 3 + agents.length
|
|
213
184
|
this.write(`${CSI}${botRow};${startCol}H${BG_MODAL}${FG_DIM}${botBorder}${RESET}`)
|
|
214
185
|
|
|
215
|
-
|
|
216
|
-
const hint = '\u2191\u2193 navigate · Enter select · q quit'
|
|
186
|
+
const hint = '\u2191\u2193 navigate \u00B7 Enter select \u00B7 q quit'
|
|
217
187
|
const hintCol = Math.max(1, Math.floor((this.cols - hint.length) / 2))
|
|
218
188
|
this.write(`${CSI}${botRow + 2};${hintCol}H${FG_DIM}${hint}${RESET}`)
|
|
219
189
|
}
|
|
220
190
|
|
|
221
191
|
drawModal()
|
|
222
192
|
|
|
223
|
-
// Set raw mode to capture keypresses
|
|
224
193
|
if (process.stdin.isTTY) {
|
|
225
194
|
process.stdin.setRawMode(true)
|
|
226
195
|
}
|
|
@@ -230,31 +199,27 @@ export class Tui {
|
|
|
230
199
|
const key = data.toString()
|
|
231
200
|
|
|
232
201
|
if (key === '\x1b[A' || key === 'k') {
|
|
233
|
-
// Up
|
|
234
202
|
selected = (selected - 1 + agents.length) % agents.length
|
|
235
203
|
drawModal()
|
|
236
204
|
} else if (key === '\x1b[B' || key === 'j') {
|
|
237
|
-
// Down
|
|
238
205
|
selected = (selected + 1) % agents.length
|
|
239
206
|
drawModal()
|
|
240
207
|
} else if (key === '\r' || key === '\n') {
|
|
241
|
-
// Enter — select
|
|
242
208
|
cleanup()
|
|
243
209
|
resolve(selected)
|
|
244
210
|
} else if (key === 'q' || key === '\x1b' || key === '\x03') {
|
|
245
|
-
// q, Escape, Ctrl+C — quit
|
|
246
211
|
cleanup()
|
|
247
|
-
this.write(`${CSI}?25h`)
|
|
248
|
-
this.write(`${CSI}?1049l`)
|
|
212
|
+
this.write(`${CSI}?25h`)
|
|
213
|
+
this.write(`${CSI}?1049l`)
|
|
249
214
|
process.exit(0)
|
|
250
215
|
}
|
|
251
216
|
}
|
|
252
217
|
|
|
253
218
|
const cleanup = () => {
|
|
254
219
|
process.stdin.removeListener('data', onKey)
|
|
255
|
-
// Show cursor again
|
|
256
220
|
this.write(`${CSI}?25h`)
|
|
257
|
-
//
|
|
221
|
+
// Leave alternate screen — back to normal mode for the agent
|
|
222
|
+
this.write(`${CSI}?1049l`)
|
|
258
223
|
}
|
|
259
224
|
|
|
260
225
|
process.stdin.on('data', onKey)
|
|
@@ -270,58 +235,19 @@ export class Tui {
|
|
|
270
235
|
}
|
|
271
236
|
|
|
272
237
|
private setScrollRegion(): void {
|
|
273
|
-
//
|
|
274
|
-
this.write(`${CSI}
|
|
238
|
+
// PTY gets rows 1 through (rows - 1), status bar on last row
|
|
239
|
+
this.write(`${CSI}1;${this.rows - 1}r`)
|
|
275
240
|
}
|
|
276
241
|
|
|
277
|
-
private
|
|
278
|
-
|
|
279
|
-
this.write(`${CSI}2;1H`)
|
|
280
|
-
}
|
|
242
|
+
private drawStatusBar(): void {
|
|
243
|
+
const { agentName, wsStatus, cwd } = this.state
|
|
281
244
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Move to line 1
|
|
285
|
-
this.write(`${CSI}1;1H`)
|
|
286
|
-
// Clear line and draw
|
|
245
|
+
// Move to last line (outside scroll region)
|
|
246
|
+
this.write(`${CSI}${this.rows};1H`)
|
|
287
247
|
this.write(`${BG_BAR}${CSI}2K`)
|
|
288
248
|
|
|
289
249
|
const displayCwd = this.shortenPath(cwd)
|
|
290
250
|
|
|
291
|
-
// WS status indicator
|
|
292
|
-
const statusColor = {
|
|
293
|
-
connected: FG_GREEN,
|
|
294
|
-
connecting: FG_YELLOW,
|
|
295
|
-
disconnected: FG_RED,
|
|
296
|
-
pending_approval: FG_YELLOW,
|
|
297
|
-
no_project: FG_DIM,
|
|
298
|
-
}[wsStatus] || FG_DIM
|
|
299
|
-
const statusLabel = {
|
|
300
|
-
connected: 'Connected',
|
|
301
|
-
connecting: 'Connecting...',
|
|
302
|
-
disconnected: 'Disconnected',
|
|
303
|
-
pending_approval: 'Pending',
|
|
304
|
-
no_project: 'No Project',
|
|
305
|
-
}[wsStatus] || wsStatus
|
|
306
|
-
const wsIndicator = `${statusColor}\u25CF ${statusLabel}${RESET}${BG_BAR}`
|
|
307
|
-
|
|
308
|
-
const left = ` ${FG_ACCENT}ctlsurf${RESET}${BG_BAR} ${FG_DIM}\u2502${RESET}${BG_BAR} ${FG_TITLE}${agentName || 'starting...'}${RESET}${BG_BAR} ${FG_DIM}\u2502${RESET}${BG_BAR} ${FG_DIM}${displayCwd}${RESET}${BG_BAR}`
|
|
309
|
-
const right = `${wsIndicator} ${RESET}${BG_BAR}`
|
|
310
|
-
|
|
311
|
-
this.write(left)
|
|
312
|
-
const pad = Math.max(0, this.cols - this.visibleLen(left) - this.visibleLen(right))
|
|
313
|
-
this.write(' '.repeat(pad))
|
|
314
|
-
this.write(right)
|
|
315
|
-
this.write(RESET)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
private drawStatusBar(): void {
|
|
319
|
-
const { wsStatus, workerId, cwd } = this.state
|
|
320
|
-
// Move to last line
|
|
321
|
-
this.write(`${CSI}${this.rows};1H`)
|
|
322
|
-
// Clear line and draw
|
|
323
|
-
this.write(`${BG_BAR}${CSI}2K`)
|
|
324
|
-
|
|
325
251
|
const statusColor = {
|
|
326
252
|
connected: FG_GREEN,
|
|
327
253
|
connecting: FG_YELLOW,
|
|
@@ -339,8 +265,7 @@ export class Tui {
|
|
|
339
265
|
no_project: 'No Project',
|
|
340
266
|
}[wsStatus] || wsStatus
|
|
341
267
|
|
|
342
|
-
const
|
|
343
|
-
const left = ` ${statusDot} ${FG_DIM}${statusLabel}${RESET}${BG_BAR}`
|
|
268
|
+
const left = ` ${FG_ACCENT}ctlsurf${RESET}${BG_BAR} ${statusDot} ${FG_DIM}${statusLabel}${RESET}${BG_BAR} ${FG_DIM}\u2502${RESET}${BG_BAR} ${FG_DIM}${agentName || '...'}${RESET}${BG_BAR}`
|
|
344
269
|
const right = `${FG_DIM}Ctrl+\\ exit${RESET}${BG_BAR} ${FG_DIM}${displayCwd} ${RESET}${BG_BAR}`
|
|
345
270
|
|
|
346
271
|
this.write(left)
|
|
@@ -360,7 +285,6 @@ export class Tui {
|
|
|
360
285
|
}
|
|
361
286
|
|
|
362
287
|
private visibleLen(s: string): number {
|
|
363
|
-
// Strip ANSI codes to get visible length
|
|
364
288
|
return s.replace(/\x1b\[[^m]*m/g, '').length
|
|
365
289
|
}
|
|
366
290
|
}
|
|
@@ -27,6 +27,20 @@ let _unsubData: (() => void) | null = null
|
|
|
27
27
|
let _unsubExit: (() => void) | null = null
|
|
28
28
|
let _currentAgentId: string | null = null
|
|
29
29
|
|
|
30
|
+
// Scroll-pinning: true means user is at/near bottom, auto-scroll is allowed
|
|
31
|
+
let _pinnedToBottom = true
|
|
32
|
+
|
|
33
|
+
function isAtBottom(terminal: Terminal): boolean {
|
|
34
|
+
const buf = terminal.buffer.active
|
|
35
|
+
return buf.viewportY >= buf.baseY
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scrollIfPinned(terminal: Terminal) {
|
|
39
|
+
if (_pinnedToBottom) {
|
|
40
|
+
terminal.scrollToBottom()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
function getOrCreateTerminal(onExit: () => void): { terminal: Terminal; fitAddon: FitAddon } {
|
|
31
45
|
if (_terminal && _fitAddon) {
|
|
32
46
|
return { terminal: _terminal, fitAddon: _fitAddon }
|
|
@@ -67,14 +81,22 @@ function getOrCreateTerminal(onExit: () => void): { terminal: Terminal; fitAddon
|
|
|
67
81
|
terminal.loadAddon(fitAddon)
|
|
68
82
|
terminal.loadAddon(new WebLinksAddon())
|
|
69
83
|
|
|
70
|
-
//
|
|
84
|
+
// Track scroll position — detect when user scrolls away from bottom
|
|
85
|
+
terminal.onScroll(() => {
|
|
86
|
+
_pinnedToBottom = isAtBottom(terminal)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Send keystrokes to pty — re-pin to bottom on user input
|
|
71
90
|
terminal.onData((data) => {
|
|
91
|
+
_pinnedToBottom = true
|
|
92
|
+
terminal.scrollToBottom()
|
|
72
93
|
window.worker.writePty(data)
|
|
73
94
|
})
|
|
74
95
|
|
|
75
|
-
// Receive pty output —
|
|
96
|
+
// Receive pty output — auto-scroll only if pinned
|
|
76
97
|
_unsubData = window.worker.onPtyData((data) => {
|
|
77
98
|
terminal.write(data)
|
|
99
|
+
scrollIfPinned(terminal)
|
|
78
100
|
})
|
|
79
101
|
|
|
80
102
|
// Handle pty exit
|
|
@@ -108,7 +130,7 @@ export function TerminalPanel({ agent, onSpawn, onExit }: TerminalPanelProps) {
|
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
fitAddon.fit()
|
|
111
|
-
terminal
|
|
133
|
+
scrollIfPinned(terminal)
|
|
112
134
|
|
|
113
135
|
// Resize handling
|
|
114
136
|
let resizeTimeout: ReturnType<typeof setTimeout>
|
|
@@ -117,7 +139,7 @@ export function TerminalPanel({ agent, onSpawn, onExit }: TerminalPanelProps) {
|
|
|
117
139
|
resizeTimeout = setTimeout(() => {
|
|
118
140
|
if (_fitAddon && _terminal) {
|
|
119
141
|
_fitAddon.fit()
|
|
120
|
-
_terminal
|
|
142
|
+
scrollIfPinned(_terminal)
|
|
121
143
|
const { cols, rows } = _terminal
|
|
122
144
|
window.worker.resizePty(cols, rows)
|
|
123
145
|
}
|
|
@@ -147,6 +169,7 @@ export function TerminalPanel({ agent, onSpawn, onExit }: TerminalPanelProps) {
|
|
|
147
169
|
onSpawn(agent).then(() => {
|
|
148
170
|
if (_fitAddon && _terminal) {
|
|
149
171
|
_fitAddon.fit()
|
|
172
|
+
_pinnedToBottom = true
|
|
150
173
|
_terminal.scrollToBottom()
|
|
151
174
|
const { cols, rows } = _terminal
|
|
152
175
|
window.worker.resizePty(cols, rows)
|