@brimveyn/aimux 1.13.2 → 1.14.0
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/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +1 -3
- package/src/app-runtime/multi-click-clipboard-guard.ts +24 -12
- package/src/app-runtime/pty-write.ts +6 -9
- package/src/app-runtime/side-effects.ts +4 -18
- package/src/app-runtime/snippet-actions.ts +2 -3
- package/src/app-runtime/use-backend-runtime.ts +4 -7
- package/src/app-runtime/use-mouse-handlers.ts +30 -1
- package/src/app-runtime/use-renderer-bindings.ts +4 -12
- package/src/app-runtime/use-terminal-resize.ts +6 -18
- package/src/app.tsx +0 -3
- package/src/daemon/daemon.ts +2 -19
- package/src/daemon/session-manager.ts +3 -15
- package/src/daemon/session-registry.ts +11 -30
- package/src/input/terminal-text-extraction.ts +7 -8
- package/src/ipc/manager-protocol.ts +6 -38
- package/src/ipc/protocol.ts +9 -37
- package/src/pty/pty-manager.ts +20 -31
- package/src/pty/terminal-snapshot.ts +43 -0
- package/src/session-backend/local-session-backend.ts +7 -31
- package/src/session-backend/remote-session-backend.ts +5 -32
- package/src/session-backend/types.ts +2 -15
- package/src/state/reducers/tab-state.ts +2 -20
- package/src/state/session-persistence.ts +2 -22
- package/src/state/types.ts +3 -11
- package/src/state/validation.ts +0 -8
- package/src/terminal-manager/manager-client.ts +5 -29
- package/src/terminal-manager/terminal-manager.ts +2 -17
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
TabSession,
|
|
5
|
+
TerminalModeState,
|
|
6
|
+
TerminalSnapshot,
|
|
7
|
+
WorkspaceSnapshotV1,
|
|
8
|
+
} from '../state/types'
|
|
9
|
+
|
|
3
10
|
import { logDebug } from '../debug/input-log'
|
|
4
11
|
import { PtyManager } from '../pty/pty-manager'
|
|
5
12
|
import {
|
|
@@ -8,14 +15,6 @@ import {
|
|
|
8
15
|
restoreTabsFromWorkspace,
|
|
9
16
|
} from '../state/session-persistence'
|
|
10
17
|
import { createDefaultTerminalModes } from '../state/terminal-modes'
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_SCROLL_INTENT,
|
|
13
|
-
type ScrollIntent,
|
|
14
|
-
type TabSession,
|
|
15
|
-
type TerminalModeState,
|
|
16
|
-
type TerminalSnapshot,
|
|
17
|
-
type WorkspaceSnapshotV1,
|
|
18
|
-
} from '../state/types'
|
|
19
18
|
|
|
20
19
|
interface SessionRegistryEvents {
|
|
21
20
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
@@ -91,7 +90,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
91
90
|
const existing = this.tabs.get(persisted.id)
|
|
92
91
|
if (existing) {
|
|
93
92
|
existing.title = persisted.title
|
|
94
|
-
existing.scrollIntent = persisted.scrollIntent ?? DEFAULT_SCROLL_INTENT
|
|
95
93
|
existing.worktreeId = persisted.worktreeId
|
|
96
94
|
}
|
|
97
95
|
}
|
|
@@ -147,7 +145,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
147
145
|
buffer: '',
|
|
148
146
|
command: [options.command, ...(options.args ?? [])].join(' '),
|
|
149
147
|
id: options.tabId,
|
|
150
|
-
scrollIntent: DEFAULT_SCROLL_INTENT,
|
|
151
148
|
status: 'starting',
|
|
152
149
|
terminalModes: createDefaultTerminalModes(),
|
|
153
150
|
title: options.title,
|
|
@@ -159,7 +156,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
159
156
|
existing.exitCode = undefined
|
|
160
157
|
existing.viewport = undefined
|
|
161
158
|
existing.terminalModes = createDefaultTerminalModes()
|
|
162
|
-
existing.scrollIntent = DEFAULT_SCROLL_INTENT
|
|
163
159
|
existing.assistant = options.assistant
|
|
164
160
|
existing.title = options.title
|
|
165
161
|
existing.command = [options.command, ...(options.args ?? [])].join(' ')
|
|
@@ -173,23 +169,12 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
173
169
|
this.ptyManager.write(tabId, data)
|
|
174
170
|
}
|
|
175
171
|
|
|
176
|
-
resizeAll(
|
|
177
|
-
cols
|
|
178
|
-
rows: number,
|
|
179
|
-
intents?: Map<string, ScrollIntent>,
|
|
180
|
-
options?: { sync?: boolean }
|
|
181
|
-
): void {
|
|
182
|
-
this.ptyManager.resizeAll(cols, rows, intents, options)
|
|
172
|
+
resizeAll(cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
173
|
+
this.ptyManager.resizeAll(cols, rows, options)
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
resizeTab(
|
|
186
|
-
tabId
|
|
187
|
-
cols: number,
|
|
188
|
-
rows: number,
|
|
189
|
-
intent?: ScrollIntent,
|
|
190
|
-
options?: { sync?: boolean }
|
|
191
|
-
): void {
|
|
192
|
-
this.ptyManager.resizeSession(tabId, cols, rows, intent, options)
|
|
176
|
+
resizeTab(tabId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
177
|
+
this.ptyManager.resizeSession(tabId, cols, rows, options)
|
|
193
178
|
}
|
|
194
179
|
|
|
195
180
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
@@ -200,10 +185,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
200
185
|
this.ptyManager.scrollViewportToBottom(tabId)
|
|
201
186
|
}
|
|
202
187
|
|
|
203
|
-
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
204
|
-
this.ptyManager.reapplyScrollIntent(tabId, intent)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
188
|
setActiveTab(tabId: string | null): void {
|
|
208
189
|
if (tabId === null) {
|
|
209
190
|
this.activeTabId = null
|
|
@@ -7,13 +7,12 @@ export function getLineText(line: TerminalLine): string {
|
|
|
7
7
|
/**
|
|
8
8
|
* Extract text from a range of terminal lines as a single string.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* spaces
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* explicitly dragged over are preserved.
|
|
10
|
+
* Trailing `[ \t]+` is stripped from each joined segment to drop the viewport
|
|
11
|
+
* padding the snapshot layer fills blank cells with (every rendered row is now
|
|
12
|
+
* padded to the full terminal width to prevent ghosting). Without this, shell
|
|
13
|
+
* line continuations (`\` + padding spaces + `\n`) paste as escaped-space
|
|
14
|
+
* sequences, and a single-line copy dragged to/past end-of-line would pick up
|
|
15
|
+
* the synthetic padding spaces.
|
|
17
16
|
*/
|
|
18
17
|
export function extractStreamText(
|
|
19
18
|
lines: TerminalLine[],
|
|
@@ -38,7 +37,7 @@ export function extractStreamText(
|
|
|
38
37
|
for (let row = clampedStart; row <= clampedEnd; row++) {
|
|
39
38
|
const text = getLineText(lines[row] as TerminalLine)
|
|
40
39
|
if (row === startRow && row === endRow) {
|
|
41
|
-
parts.push(text.slice(Math.max(0, startCol), Math.max(0, endCol)))
|
|
40
|
+
parts.push(rtrim(text.slice(Math.max(0, startCol), Math.max(0, endCol))))
|
|
42
41
|
} else if (row === startRow) {
|
|
43
42
|
parts.push(rtrim(text.slice(Math.max(0, startCol))))
|
|
44
43
|
} else if (row === endRow) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ScrollIntent,
|
|
3
2
|
TabSession,
|
|
4
3
|
TerminalModeState,
|
|
5
4
|
TerminalSnapshot,
|
|
@@ -14,8 +13,12 @@ import {
|
|
|
14
13
|
negotiateProtocolVersion,
|
|
15
14
|
} from './protocol'
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// v5: backend owns scroll position end to end. Removed the per-tab scroll
|
|
17
|
+
// `intent`/`intents` from resize messages and the `reapplyScrollIntent`
|
|
18
|
+
// message. Min is raised in lockstep so a pre-v5 peer (which could still send
|
|
19
|
+
// the dropped message) can't negotiate a now-incompatible version.
|
|
20
|
+
export const MANAGER_PROTOCOL_MIN_VERSION = 5
|
|
21
|
+
export const MANAGER_PROTOCOL_VERSION = 5
|
|
19
22
|
/**
|
|
20
23
|
* Minimum version required to send `setBroadcastEnabled`. Older TMs (v3) will
|
|
21
24
|
* not understand the message; the daemon must check the negotiated version
|
|
@@ -75,7 +78,6 @@ export type ManagerRequest =
|
|
|
75
78
|
sessionId: string
|
|
76
79
|
cols: number
|
|
77
80
|
rows: number
|
|
78
|
-
intents?: Record<string, ScrollIntent>
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
| {
|
|
@@ -86,7 +88,6 @@ export type ManagerRequest =
|
|
|
86
88
|
tabId: string
|
|
87
89
|
cols: number
|
|
88
90
|
rows: number
|
|
89
|
-
intent?: ScrollIntent
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
| {
|
|
@@ -99,11 +100,6 @@ export type ManagerRequest =
|
|
|
99
100
|
type: 'scroll'
|
|
100
101
|
payload: { sessionId: string; tabId: string; deltaLines: number }
|
|
101
102
|
}
|
|
102
|
-
| {
|
|
103
|
-
id: string
|
|
104
|
-
type: 'reapplyScrollIntent'
|
|
105
|
-
payload: { sessionId: string; tabId: string; intent: ScrollIntent }
|
|
106
|
-
}
|
|
107
103
|
| { id: string; type: 'setActiveTab'; payload: { sessionId: string; tabId: string | null } }
|
|
108
104
|
| { id: string; type: 'closeTab'; payload: { sessionId: string; tabId: string } }
|
|
109
105
|
| { id: string; type: 'disposeSession'; payload: { sessionId: string } }
|
|
@@ -178,18 +174,6 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
|
178
174
|
)
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
function isScrollIntent(value: unknown): value is ScrollIntent {
|
|
182
|
-
if (!isObjectRecord(value)) return false
|
|
183
|
-
if (value.kind === 'bottom') return true
|
|
184
|
-
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
185
|
-
return false
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function isScrollIntentRecord(value: unknown): value is Record<string, ScrollIntent> {
|
|
189
|
-
if (!isObjectRecord(value)) return false
|
|
190
|
-
return Object.values(value).every(isScrollIntent)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
177
|
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
194
178
|
return (
|
|
195
179
|
isObjectRecord(value) &&
|
|
@@ -303,20 +287,12 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
|
303
287
|
assert(isString(value.payload.sessionId), 'resizeClient.sessionId must be a string')
|
|
304
288
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
305
289
|
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
306
|
-
assert(
|
|
307
|
-
value.payload.intents === undefined || isScrollIntentRecord(value.payload.intents),
|
|
308
|
-
'resizeClient.intents must be a scroll-intent record'
|
|
309
|
-
)
|
|
310
290
|
return value as ManagerRequest
|
|
311
291
|
case 'resizeTab':
|
|
312
292
|
assert(isString(value.payload.sessionId), 'resizeTab.sessionId must be a string')
|
|
313
293
|
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
314
294
|
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
315
295
|
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
316
|
-
assert(
|
|
317
|
-
value.payload.intent === undefined || isScrollIntent(value.payload.intent),
|
|
318
|
-
'resizeTab.intent must be a scroll intent'
|
|
319
|
-
)
|
|
320
296
|
return value as ManagerRequest
|
|
321
297
|
case 'scroll':
|
|
322
298
|
assert(isString(value.payload.sessionId), 'scroll.sessionId must be a string')
|
|
@@ -327,14 +303,6 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
|
327
303
|
assert(isString(value.payload.sessionId), 'scrollToBottom.sessionId must be a string')
|
|
328
304
|
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
329
305
|
return value as ManagerRequest
|
|
330
|
-
case 'reapplyScrollIntent':
|
|
331
|
-
assert(isString(value.payload.sessionId), 'reapplyScrollIntent.sessionId must be a string')
|
|
332
|
-
assert(isString(value.payload.tabId), 'reapplyScrollIntent.tabId must be a string')
|
|
333
|
-
assert(
|
|
334
|
-
isScrollIntent(value.payload.intent),
|
|
335
|
-
'reapplyScrollIntent.intent must be a scroll intent'
|
|
336
|
-
)
|
|
337
|
-
return value as ManagerRequest
|
|
338
306
|
case 'setActiveTab':
|
|
339
307
|
assert(isString(value.payload.sessionId), 'setActiveTab.sessionId must be a string')
|
|
340
308
|
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ScrollIntent,
|
|
3
2
|
SessionStatus,
|
|
4
3
|
TabActivity,
|
|
5
4
|
TabSession,
|
|
@@ -10,8 +9,13 @@ import type {
|
|
|
10
9
|
|
|
11
10
|
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// v9: scroll position is owned entirely by the backend emulator. Dropped the
|
|
13
|
+
// per-tab scroll `intent`/`intents` from resize messages and removed the
|
|
14
|
+
// `reapplyScrollIntent` message — the frontend no longer derives or sends it.
|
|
15
|
+
// (v8 was the unfilled-viewport status-detector change; this is a further
|
|
16
|
+
// breaking wire change, so the version steps again.)
|
|
17
|
+
export const IPC_PROTOCOL_MIN_VERSION = 9
|
|
18
|
+
export const IPC_PROTOCOL_VERSION = 9
|
|
15
19
|
|
|
16
20
|
export interface ProtocolHelloRequest {
|
|
17
21
|
minVersion: number
|
|
@@ -67,20 +71,15 @@ export type ClientRequest =
|
|
|
67
71
|
| {
|
|
68
72
|
id: string
|
|
69
73
|
type: 'resizeClient'
|
|
70
|
-
payload: { cols: number; rows: number
|
|
74
|
+
payload: { cols: number; rows: number }
|
|
71
75
|
}
|
|
72
76
|
| {
|
|
73
77
|
id: string
|
|
74
78
|
type: 'resizeTab'
|
|
75
|
-
payload: { tabId: string; cols: number; rows: number
|
|
79
|
+
payload: { tabId: string; cols: number; rows: number }
|
|
76
80
|
}
|
|
77
81
|
| { id: string; type: 'scrollToBottom'; payload: { tabId: string } }
|
|
78
82
|
| { id: string; type: 'scroll'; payload: { tabId: string; deltaLines: number } }
|
|
79
|
-
| {
|
|
80
|
-
id: string
|
|
81
|
-
type: 'reapplyScrollIntent'
|
|
82
|
-
payload: { tabId: string; intent: ScrollIntent }
|
|
83
|
-
}
|
|
84
83
|
| { id: string; type: 'setActiveTab'; payload: { tabId: string | null } }
|
|
85
84
|
| { id: string; type: 'closeTab'; payload: { tabId: string } }
|
|
86
85
|
| { id: string; type: 'disposeAll'; payload: Record<string, never> }
|
|
@@ -180,18 +179,6 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
|
180
179
|
)
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
function isScrollIntent(value: unknown): value is ScrollIntent {
|
|
184
|
-
if (!isObjectRecord(value)) return false
|
|
185
|
-
if (value.kind === 'bottom') return true
|
|
186
|
-
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
187
|
-
return false
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function isScrollIntentRecord(value: unknown): value is Record<string, ScrollIntent> {
|
|
191
|
-
if (!isObjectRecord(value)) return false
|
|
192
|
-
return Object.values(value).every(isScrollIntent)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
182
|
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
196
183
|
return (
|
|
197
184
|
isObjectRecord(value) &&
|
|
@@ -328,19 +315,11 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
328
315
|
case 'resizeClient':
|
|
329
316
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
330
317
|
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
331
|
-
assert(
|
|
332
|
-
value.payload.intents === undefined || isScrollIntentRecord(value.payload.intents),
|
|
333
|
-
'resizeClient.intents must be a scroll-intent record'
|
|
334
|
-
)
|
|
335
318
|
return value as ClientRequest
|
|
336
319
|
case 'resizeTab':
|
|
337
320
|
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
338
321
|
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
339
322
|
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
340
|
-
assert(
|
|
341
|
-
value.payload.intent === undefined || isScrollIntent(value.payload.intent),
|
|
342
|
-
'resizeTab.intent must be a scroll intent'
|
|
343
|
-
)
|
|
344
323
|
return value as ClientRequest
|
|
345
324
|
case 'scroll':
|
|
346
325
|
assert(isString(value.payload.tabId), 'scroll.tabId must be a string')
|
|
@@ -349,13 +328,6 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
349
328
|
case 'scrollToBottom':
|
|
350
329
|
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
351
330
|
return value as ClientRequest
|
|
352
|
-
case 'reapplyScrollIntent':
|
|
353
|
-
assert(isString(value.payload.tabId), 'reapplyScrollIntent.tabId must be a string')
|
|
354
|
-
assert(
|
|
355
|
-
isScrollIntent(value.payload.intent),
|
|
356
|
-
'reapplyScrollIntent.intent must be a scroll intent'
|
|
357
|
-
)
|
|
358
|
-
return value as ClientRequest
|
|
359
331
|
case 'setActiveTab':
|
|
360
332
|
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
|
361
333
|
return value as ClientRequest
|
package/src/pty/pty-manager.ts
CHANGED
|
@@ -85,6 +85,17 @@ function getTerminalModes(emulator: XTerm, alternateScrollMode: boolean): Termin
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Read the scroll position straight from the emulator that owns it. This is the
|
|
89
|
+
// single source of truth for re-anchoring across a resize: deriving it here
|
|
90
|
+
// (zero latency) instead of accepting a frontend-supplied intent avoids the
|
|
91
|
+
// stale-mirror drift that desynced selection/copy from the rendered viewport.
|
|
92
|
+
function deriveEmulatorScrollIntent(emulator: XTerm): ScrollIntent {
|
|
93
|
+
const buffer = emulator.buffer.active
|
|
94
|
+
return buffer.viewportY >= buffer.baseY
|
|
95
|
+
? { kind: 'bottom' }
|
|
96
|
+
: { absoluteLine: buffer.viewportY, kind: 'anchor' }
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
function envInt(name: string, fallback: number): number {
|
|
89
100
|
const raw = process.env[name]
|
|
90
101
|
if (raw === undefined) return fallback
|
|
@@ -374,13 +385,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
374
385
|
session.emulator.scrollToLine(Math.max(0, intent.absoluteLine))
|
|
375
386
|
}
|
|
376
387
|
|
|
377
|
-
private applyResize(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
intent
|
|
382
|
-
sync: boolean
|
|
383
|
-
): void {
|
|
388
|
+
private applyResize(session: SessionHandle, cols: number, rows: number, sync: boolean): void {
|
|
389
|
+
// Capture the scroll position from the emulator *before* reflow, so the
|
|
390
|
+
// re-anchor restores where the user actually was. The frontend no longer
|
|
391
|
+
// supplies an intent — the backend owns scroll position end to end.
|
|
392
|
+
const intent = deriveEmulatorScrollIntent(session.emulator)
|
|
384
393
|
const safeCols = Math.max(20, cols)
|
|
385
394
|
const safeRows = Math.max(8, rows)
|
|
386
395
|
session.pty.resize(safeCols, safeRows)
|
|
@@ -406,38 +415,18 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
406
415
|
}
|
|
407
416
|
}
|
|
408
417
|
|
|
409
|
-
resizeAll(
|
|
410
|
-
cols: number,
|
|
411
|
-
rows: number,
|
|
412
|
-
intents?: Map<string, ScrollIntent>,
|
|
413
|
-
options?: { sync?: boolean }
|
|
414
|
-
): void {
|
|
418
|
+
resizeAll(cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
415
419
|
for (const session of this.sessions.values()) {
|
|
416
|
-
this.applyResize(session, cols, rows,
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
resizeSession(
|
|
421
|
-
tabId: string,
|
|
422
|
-
cols: number,
|
|
423
|
-
rows: number,
|
|
424
|
-
intent?: ScrollIntent,
|
|
425
|
-
options?: { sync?: boolean }
|
|
426
|
-
): void {
|
|
427
|
-
const session = this.sessions.get(tabId)
|
|
428
|
-
if (!session) {
|
|
429
|
-
return
|
|
420
|
+
this.applyResize(session, cols, rows, options?.sync ?? false)
|
|
430
421
|
}
|
|
431
|
-
this.applyResize(session, cols, rows, intent, options?.sync ?? false)
|
|
432
422
|
}
|
|
433
423
|
|
|
434
|
-
|
|
424
|
+
resizeSession(tabId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
435
425
|
const session = this.sessions.get(tabId)
|
|
436
426
|
if (!session) {
|
|
437
427
|
return
|
|
438
428
|
}
|
|
439
|
-
this.
|
|
440
|
-
this.scheduleRender(session)
|
|
429
|
+
this.applyResize(session, cols, rows, options?.sync ?? false)
|
|
441
430
|
}
|
|
442
431
|
|
|
443
432
|
disposeSession(tabId: string): void {
|
|
@@ -100,6 +100,7 @@ function buildLine(
|
|
|
100
100
|
|
|
101
101
|
const cell = terminal.buffer.active.getNullCell()
|
|
102
102
|
const spans: TerminalSpan[] = []
|
|
103
|
+
let visualColumns = 0
|
|
103
104
|
|
|
104
105
|
for (let column = 0; column < terminal.cols; column += 1) {
|
|
105
106
|
const current = line.getCell(column, cell)
|
|
@@ -138,6 +139,48 @@ function buildLine(
|
|
|
138
139
|
text,
|
|
139
140
|
underline: current.isUnderline() ? true : undefined,
|
|
140
141
|
})
|
|
142
|
+
|
|
143
|
+
visualColumns += current.getWidth()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Pad the row to the full terminal width so opentui overwrites every cell.
|
|
147
|
+
// Without padding, cells past the last written character retain content
|
|
148
|
+
// from the previous frame (opentui's `blendCells` preserves the existing
|
|
149
|
+
// char when an overlay space lands on it — see modal-shell.tsx for the
|
|
150
|
+
// same class of bug solved differently for transparent modals).
|
|
151
|
+
if (visualColumns < terminal.cols) {
|
|
152
|
+
const padCount = terminal.cols - visualColumns
|
|
153
|
+
const cursorInPad =
|
|
154
|
+
cursorVisible &&
|
|
155
|
+
cursorColumn !== null &&
|
|
156
|
+
cursorColumn >= visualColumns &&
|
|
157
|
+
cursorColumn < terminal.cols
|
|
158
|
+
|
|
159
|
+
if (cursorInPad && cursorColumn !== null) {
|
|
160
|
+
// cursorColumn is a buffer column while visualColumns is a sum of cell
|
|
161
|
+
// widths; they diverge when wide glyphs precede the gap. Clamp the offset
|
|
162
|
+
// into the pad so leading + cursor + trailing always equals padCount and
|
|
163
|
+
// the row never over/undershoots terminal.cols (cursor may be a column
|
|
164
|
+
// off in pathological wide-char rows, but the row width stays correct).
|
|
165
|
+
const cursorOffset = Math.min(cursorColumn - visualColumns, padCount - 1)
|
|
166
|
+
const leading = cursorOffset
|
|
167
|
+
const trailing = padCount - cursorOffset - 1
|
|
168
|
+
if (leading > 0) {
|
|
169
|
+
pushSpan(spans, { text: ' '.repeat(leading) })
|
|
170
|
+
}
|
|
171
|
+
const tokens = getCurrentTheme()
|
|
172
|
+
pushSpan(spans, {
|
|
173
|
+
bg: tokens.text,
|
|
174
|
+
cursor: true,
|
|
175
|
+
fg: tokens.background,
|
|
176
|
+
text: ' ',
|
|
177
|
+
})
|
|
178
|
+
if (trailing > 0) {
|
|
179
|
+
pushSpan(spans, { text: ' '.repeat(trailing) })
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
pushSpan(spans, { text: ' '.repeat(padCount) })
|
|
183
|
+
}
|
|
141
184
|
}
|
|
142
185
|
|
|
143
186
|
return { spans }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
-
import type { AssistantId,
|
|
3
|
+
import type { AssistantId, WorkspaceSnapshotV1 } from '../state/types'
|
|
4
4
|
import type { SessionBackend, SessionBackendEvents } from './types'
|
|
5
5
|
|
|
6
6
|
import { SessionManager } from '../daemon/session-manager'
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
getSnapshotTrees,
|
|
13
13
|
toTerminalContentSize,
|
|
14
14
|
} from '../state/layout-resize'
|
|
15
|
-
import { getSnapshotScrollIntents } from '../state/session-persistence'
|
|
16
15
|
|
|
17
16
|
export class LocalSessionBackend
|
|
18
17
|
extends EventEmitter<SessionBackendEvents>
|
|
@@ -67,22 +66,15 @@ export class LocalSessionBackend
|
|
|
67
66
|
})
|
|
68
67
|
this.currentSessionId = options.sessionId
|
|
69
68
|
const trees = getSnapshotTrees(options.workspaceSnapshot)
|
|
70
|
-
const intents = getSnapshotScrollIntents(options.workspaceSnapshot)
|
|
71
69
|
const splitTrees = trees.filter((t) => t.type === 'split')
|
|
72
70
|
if (splitTrees.length > 0) {
|
|
73
71
|
const bounds = createTerminalBounds(options.cols, options.rows)
|
|
74
72
|
forEachSplitPaneRect(splitTrees, bounds, (tabId, rect) => {
|
|
75
73
|
const size = toTerminalContentSize(rect)
|
|
76
|
-
this.sessionManager.resizeTab(
|
|
77
|
-
options.sessionId,
|
|
78
|
-
tabId,
|
|
79
|
-
size.cols,
|
|
80
|
-
size.rows,
|
|
81
|
-
intents.get(tabId)
|
|
82
|
-
)
|
|
74
|
+
this.sessionManager.resizeTab(options.sessionId, tabId, size.cols, size.rows)
|
|
83
75
|
})
|
|
84
76
|
} else {
|
|
85
|
-
this.sessionManager.resize(options.sessionId, options.cols, options.rows
|
|
77
|
+
this.sessionManager.resize(options.sessionId, options.cols, options.rows)
|
|
86
78
|
}
|
|
87
79
|
const attachResult = this.sessionManager.attachSession(
|
|
88
80
|
options.sessionId,
|
|
@@ -149,36 +141,20 @@ export class LocalSessionBackend
|
|
|
149
141
|
this.sessionManager.scrollToBottom(this.currentSessionId, tabId)
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
153
|
-
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
154
|
-
this.sessionManager.reapplyScrollIntent(this.currentSessionId, tabId, intent)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
144
|
setActiveTab(tabId: string | null): void {
|
|
158
145
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
159
146
|
logDebug('backend.local.setActiveTab', { sessionId: this.currentSessionId, tabId })
|
|
160
147
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
161
148
|
}
|
|
162
149
|
|
|
163
|
-
resizeAll(
|
|
164
|
-
cols: number,
|
|
165
|
-
rows: number,
|
|
166
|
-
intents?: Map<string, ScrollIntent>,
|
|
167
|
-
options?: { sync?: boolean }
|
|
168
|
-
): void {
|
|
150
|
+
resizeAll(cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
169
151
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
170
|
-
this.sessionManager.resize(this.currentSessionId, cols, rows,
|
|
152
|
+
this.sessionManager.resize(this.currentSessionId, cols, rows, options)
|
|
171
153
|
}
|
|
172
154
|
|
|
173
|
-
resizeTab(
|
|
174
|
-
tabId: string,
|
|
175
|
-
cols: number,
|
|
176
|
-
rows: number,
|
|
177
|
-
intent?: ScrollIntent,
|
|
178
|
-
options?: { sync?: boolean }
|
|
179
|
-
): void {
|
|
155
|
+
resizeTab(tabId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
180
156
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
181
|
-
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows,
|
|
157
|
+
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, options)
|
|
182
158
|
}
|
|
183
159
|
|
|
184
160
|
disposeSession(tabId: string): void {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import { connect, type Socket } from 'node:net'
|
|
3
3
|
|
|
4
|
-
import type { AssistantId,
|
|
4
|
+
import type { AssistantId, WorkspaceSnapshotV1 } from '../state/types'
|
|
5
5
|
import type { SessionBackend, SessionBackendEvents } from './types'
|
|
6
6
|
|
|
7
7
|
import { getIpcDaemonSocketPath } from '../daemon/runtime-paths'
|
|
@@ -404,17 +404,6 @@ export class RemoteSessionBackend
|
|
|
404
404
|
)
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
408
|
-
if (!this.attached) {
|
|
409
|
-
return
|
|
410
|
-
}
|
|
411
|
-
this.dispatchCommand(
|
|
412
|
-
{ id: crypto.randomUUID(), payload: { intent, tabId }, type: 'reapplyScrollIntent' },
|
|
413
|
-
'reapplyScrollIntent',
|
|
414
|
-
tabId
|
|
415
|
-
)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
407
|
setActiveTab(tabId: string | null): void {
|
|
419
408
|
if (!this.attached) {
|
|
420
409
|
return
|
|
@@ -425,40 +414,24 @@ export class RemoteSessionBackend
|
|
|
425
414
|
)
|
|
426
415
|
}
|
|
427
416
|
|
|
428
|
-
resizeAll(
|
|
429
|
-
cols: number,
|
|
430
|
-
rows: number,
|
|
431
|
-
intents?: Map<string, ScrollIntent>,
|
|
432
|
-
_options?: { sync?: boolean }
|
|
433
|
-
): void {
|
|
417
|
+
resizeAll(cols: number, rows: number, _options?: { sync?: boolean }): void {
|
|
434
418
|
if (!this.attached) {
|
|
435
419
|
logDebug('backend.remote.skipResizeBeforeAttach', { cols, rows })
|
|
436
420
|
return
|
|
437
421
|
}
|
|
438
422
|
logDebug('backend.remote.resize', { cols, rows, sessionId: this.currentSessionId })
|
|
439
|
-
const intentsRecord = intents ? Object.fromEntries(intents.entries()) : undefined
|
|
440
423
|
this.dispatchCommand(
|
|
441
|
-
{
|
|
442
|
-
id: crypto.randomUUID(),
|
|
443
|
-
payload: { cols, intents: intentsRecord, rows },
|
|
444
|
-
type: 'resizeClient',
|
|
445
|
-
},
|
|
424
|
+
{ id: crypto.randomUUID(), payload: { cols, rows }, type: 'resizeClient' },
|
|
446
425
|
'resizeClient'
|
|
447
426
|
)
|
|
448
427
|
}
|
|
449
428
|
|
|
450
|
-
resizeTab(
|
|
451
|
-
tabId: string,
|
|
452
|
-
cols: number,
|
|
453
|
-
rows: number,
|
|
454
|
-
intent?: ScrollIntent,
|
|
455
|
-
_options?: { sync?: boolean }
|
|
456
|
-
): void {
|
|
429
|
+
resizeTab(tabId: string, cols: number, rows: number, _options?: { sync?: boolean }): void {
|
|
457
430
|
if (!this.attached) {
|
|
458
431
|
return
|
|
459
432
|
}
|
|
460
433
|
this.dispatchCommand(
|
|
461
|
-
{ id: crypto.randomUUID(), payload: { cols,
|
|
434
|
+
{ id: crypto.randomUUID(), payload: { cols, rows, tabId }, type: 'resizeTab' },
|
|
462
435
|
'resizeTab',
|
|
463
436
|
tabId
|
|
464
437
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
ScrollIntent,
|
|
5
4
|
SessionStatus,
|
|
6
5
|
TabActivity,
|
|
7
6
|
TabSession,
|
|
@@ -49,21 +48,9 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
|
|
|
49
48
|
write(tabId: string, input: string): void
|
|
50
49
|
scrollViewport(tabId: string, deltaLines: number): void
|
|
51
50
|
scrollViewportToBottom(tabId: string): void
|
|
52
|
-
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void
|
|
53
51
|
setActiveTab(tabId: string | null): void
|
|
54
|
-
resizeAll(
|
|
55
|
-
|
|
56
|
-
rows: number,
|
|
57
|
-
intents?: Map<string, ScrollIntent>,
|
|
58
|
-
options?: { sync?: boolean }
|
|
59
|
-
): void
|
|
60
|
-
resizeTab(
|
|
61
|
-
tabId: string,
|
|
62
|
-
cols: number,
|
|
63
|
-
rows: number,
|
|
64
|
-
intent?: ScrollIntent,
|
|
65
|
-
options?: { sync?: boolean }
|
|
66
|
-
): void
|
|
52
|
+
resizeAll(cols: number, rows: number, options?: { sync?: boolean }): void
|
|
53
|
+
resizeTab(tabId: string, cols: number, rows: number, options?: { sync?: boolean }): void
|
|
67
54
|
disposeSession(tabId: string): void
|
|
68
55
|
disposeAll(): void
|
|
69
56
|
destroy(keepSessions?: boolean): Promise<void> | void
|