@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.
@@ -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: number,
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: string,
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
- * Multi-row selections are lossy: trailing `[ \t]+` is stripped from each
11
- * joined segment to drop the viewport padding the snapshot layer fills blank
12
- * cells with. Without this, shell line continuations (`\` followed by padding
13
- * spaces then `\n`) paste as escaped-space sequences instead of continuations.
14
- *
15
- * Single-row selections are returned verbatim — trailing spaces the user
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
- export const MANAGER_PROTOCOL_MIN_VERSION = 3
18
- export const MANAGER_PROTOCOL_VERSION = 4
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')
@@ -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
- export const IPC_PROTOCOL_MIN_VERSION = 8
14
- export const IPC_PROTOCOL_VERSION = 8
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; intents?: Record<string, ScrollIntent> }
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; intent?: ScrollIntent }
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
@@ -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
- session: SessionHandle,
379
- cols: number,
380
- rows: number,
381
- intent: ScrollIntent | undefined,
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, intents?.get(session.tabId), options?.sync ?? false)
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
- reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
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.applyScrollIntent(session, intent)
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, ScrollIntent, WorkspaceSnapshotV1 } from '../state/types'
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, intents)
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, intents, options)
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, intent, options)
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, ScrollIntent, WorkspaceSnapshotV1 } from '../state/types'
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, intent, rows, tabId }, type: 'resizeTab' },
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
- cols: number,
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