@brimveyn/aimux 1.13.0 → 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.
Files changed (57) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/backend-attach-runtime.ts +12 -14
  3. package/src/app-runtime/multi-click-clipboard-guard.ts +24 -12
  4. package/src/app-runtime/pty-write.ts +6 -9
  5. package/src/app-runtime/side-effects.ts +65 -56
  6. package/src/app-runtime/snippet-actions.ts +2 -3
  7. package/src/app-runtime/use-backend-runtime.ts +4 -7
  8. package/src/app-runtime/use-mouse-handlers.ts +30 -1
  9. package/src/app-runtime/use-renderer-bindings.ts +4 -12
  10. package/src/app-runtime/use-terminal-resize.ts +6 -18
  11. package/src/app.tsx +0 -3
  12. package/src/daemon/daemon.ts +20 -27
  13. package/src/daemon/session-manager.ts +3 -15
  14. package/src/daemon/session-registry.ts +11 -30
  15. package/src/git/command-queue.ts +18 -2
  16. package/src/input/terminal-text-extraction.ts +7 -8
  17. package/src/ipc/manager-protocol.ts +6 -38
  18. package/src/ipc/protocol.ts +9 -37
  19. package/src/pty/pty-manager.ts +20 -31
  20. package/src/pty/terminal-snapshot.ts +43 -0
  21. package/src/session-backend/local-session-backend.ts +7 -31
  22. package/src/session-backend/remote-session-backend.ts +51 -63
  23. package/src/session-backend/types.ts +2 -15
  24. package/src/state/reducers/tab-state.ts +2 -20
  25. package/src/state/session-persistence.ts +2 -22
  26. package/src/state/types.ts +3 -11
  27. package/src/state/validation.ts +0 -8
  28. package/src/terminal-manager/manager-client.ts +5 -29
  29. package/src/terminal-manager/terminal-manager.ts +2 -17
  30. package/src/ui/components/git/diff-renderer/fold-strip.tsx +30 -23
  31. package/src/ui/components/git/diff-renderer/split-view.tsx +33 -7
  32. package/src/ui/components/git/diff-renderer/stacked-view.tsx +23 -3
  33. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +9 -5
  34. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +8 -6
  35. package/src/ui/components/git/git-panel.tsx +114 -75
  36. package/src/ui/components/git/git-view.tsx +26 -18
  37. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +10 -10
  38. package/src/ui/components/layout/session-bar.tsx +134 -116
  39. package/src/ui/components/layout/sidebar/sidebar.tsx +89 -53
  40. package/src/ui/components/layout/sidebar/tab-item.tsx +65 -54
  41. package/src/ui/components/layout/split-layout.tsx +27 -20
  42. package/src/ui/components/layout/terminal-pane.tsx +154 -114
  43. package/src/ui/components/modals/app/help-modal.tsx +25 -18
  44. package/src/ui/components/modals/git/git-commit-modal.tsx +15 -8
  45. package/src/ui/components/modals/sessions/create-session-modal.tsx +13 -9
  46. package/src/ui/components/modals/sessions/session-picker-modal.tsx +35 -28
  47. package/src/ui/components/modals/shared/form.tsx +2 -1
  48. package/src/ui/components/modals/shared/picker.tsx +49 -33
  49. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +42 -29
  50. package/src/ui/components/modals/tabs/new-tab-modal.tsx +82 -66
  51. package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
  52. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +23 -6
  53. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +11 -6
  54. package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
  55. package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
  56. package/src/ui/components/primitives/list-item.tsx +20 -4
  57. package/src/ui/root.tsx +77 -48
@@ -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'
@@ -144,6 +144,17 @@ export class RemoteSessionBackend
144
144
  }
145
145
  }
146
146
 
147
+ /** Fire-and-forget a command, reporting any failure via reportCommandError. */
148
+ private dispatchCommand(request: ClientRequest, context: string, tabId?: string): void {
149
+ void (async () => {
150
+ try {
151
+ await this.sendExpectOk(request)
152
+ } catch (error) {
153
+ this.reportCommandError(context, error, tabId)
154
+ }
155
+ })()
156
+ }
157
+
147
158
  private handleServerEvent(message: ServerEvent): void {
148
159
  logDebug('backend.remote.event', { type: message.type })
149
160
  switch (message.type) {
@@ -352,8 +363,10 @@ export class RemoteSessionBackend
352
363
  return
353
364
  }
354
365
 
355
- void this.sendExpectOk({ id: crypto.randomUUID(), payload: options, type: 'createTab' }).catch(
356
- (error) => this.reportCommandError('createTab', error, options.tabId)
366
+ this.dispatchCommand(
367
+ { id: crypto.randomUUID(), payload: options, type: 'createTab' },
368
+ 'createTab',
369
+ options.tabId
357
370
  )
358
371
  }
359
372
 
@@ -362,99 +375,76 @@ export class RemoteSessionBackend
362
375
  logDebug('backend.remote.skipWriteBeforeAttach', { inputLength: input.length, tabId })
363
376
  return
364
377
  }
365
- void this.sendExpectOk({
366
- id: crypto.randomUUID(),
367
- payload: { data: input, tabId },
368
- type: 'write',
369
- }).catch((error) => this.reportCommandError('write', error, tabId))
378
+ this.dispatchCommand(
379
+ { id: crypto.randomUUID(), payload: { data: input, tabId }, type: 'write' },
380
+ 'write',
381
+ tabId
382
+ )
370
383
  }
371
384
 
372
385
  scrollViewport(tabId: string, deltaLines: number): void {
373
386
  if (!this.attached) {
374
387
  return
375
388
  }
376
- void this.sendExpectOk({
377
- id: crypto.randomUUID(),
378
- payload: { deltaLines, tabId },
379
- type: 'scroll',
380
- }).catch((error) => this.reportCommandError('scroll', error, tabId))
389
+ this.dispatchCommand(
390
+ { id: crypto.randomUUID(), payload: { deltaLines, tabId }, type: 'scroll' },
391
+ 'scroll',
392
+ tabId
393
+ )
381
394
  }
382
395
 
383
396
  scrollViewportToBottom(tabId: string): void {
384
397
  if (!this.attached) {
385
398
  return
386
399
  }
387
- void this.sendExpectOk({
388
- id: crypto.randomUUID(),
389
- payload: { tabId },
390
- type: 'scrollToBottom',
391
- }).catch((error) => this.reportCommandError('scrollToBottom', error, tabId))
392
- }
393
-
394
- reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
395
- if (!this.attached) {
396
- return
397
- }
398
- void this.sendExpectOk({
399
- id: crypto.randomUUID(),
400
- payload: { intent, tabId },
401
- type: 'reapplyScrollIntent',
402
- }).catch((error) => this.reportCommandError('reapplyScrollIntent', error, tabId))
400
+ this.dispatchCommand(
401
+ { id: crypto.randomUUID(), payload: { tabId }, type: 'scrollToBottom' },
402
+ 'scrollToBottom',
403
+ tabId
404
+ )
403
405
  }
404
406
 
405
407
  setActiveTab(tabId: string | null): void {
406
408
  if (!this.attached) {
407
409
  return
408
410
  }
409
- void this.sendExpectOk({
410
- id: crypto.randomUUID(),
411
- payload: { tabId },
412
- type: 'setActiveTab',
413
- }).catch((error) => this.reportCommandError('setActiveTab', error))
411
+ this.dispatchCommand(
412
+ { id: crypto.randomUUID(), payload: { tabId }, type: 'setActiveTab' },
413
+ 'setActiveTab'
414
+ )
414
415
  }
415
416
 
416
- resizeAll(
417
- cols: number,
418
- rows: number,
419
- intents?: Map<string, ScrollIntent>,
420
- _options?: { sync?: boolean }
421
- ): void {
417
+ resizeAll(cols: number, rows: number, _options?: { sync?: boolean }): void {
422
418
  if (!this.attached) {
423
419
  logDebug('backend.remote.skipResizeBeforeAttach', { cols, rows })
424
420
  return
425
421
  }
426
422
  logDebug('backend.remote.resize', { cols, rows, sessionId: this.currentSessionId })
427
- const intentsRecord = intents ? Object.fromEntries(intents.entries()) : undefined
428
- void this.sendExpectOk({
429
- id: crypto.randomUUID(),
430
- payload: { cols, intents: intentsRecord, rows },
431
- type: 'resizeClient',
432
- }).catch((error) => this.reportCommandError('resizeClient', error))
423
+ this.dispatchCommand(
424
+ { id: crypto.randomUUID(), payload: { cols, rows }, type: 'resizeClient' },
425
+ 'resizeClient'
426
+ )
433
427
  }
434
428
 
435
- resizeTab(
436
- tabId: string,
437
- cols: number,
438
- rows: number,
439
- intent?: ScrollIntent,
440
- _options?: { sync?: boolean }
441
- ): void {
429
+ resizeTab(tabId: string, cols: number, rows: number, _options?: { sync?: boolean }): void {
442
430
  if (!this.attached) {
443
431
  return
444
432
  }
445
- void this.sendExpectOk({
446
- id: crypto.randomUUID(),
447
- payload: { cols, intent, rows, tabId },
448
- type: 'resizeTab',
449
- }).catch((error) => this.reportCommandError('resizeTab', error, tabId))
433
+ this.dispatchCommand(
434
+ { id: crypto.randomUUID(), payload: { cols, rows, tabId }, type: 'resizeTab' },
435
+ 'resizeTab',
436
+ tabId
437
+ )
450
438
  }
451
439
 
452
440
  disposeSession(tabId: string): void {
453
441
  if (!this.attached) {
454
442
  return
455
443
  }
456
- void this.sendExpectOk({ id: crypto.randomUUID(), payload: { tabId }, type: 'closeTab' }).catch(
457
- (error) => this.reportCommandError('closeTab', error, tabId)
444
+ this.dispatchCommand(
445
+ { id: crypto.randomUUID(), payload: { tabId }, type: 'closeTab' },
446
+ 'closeTab',
447
+ tabId
458
448
  )
459
449
  }
460
450
 
@@ -462,9 +452,7 @@ export class RemoteSessionBackend
462
452
  if (!this.attached) {
463
453
  return
464
454
  }
465
- void this.sendExpectOk({ id: crypto.randomUUID(), payload: {}, type: 'disposeAll' }).catch(
466
- (error) => this.reportCommandError('disposeAll', error)
467
- )
455
+ this.dispatchCommand({ id: crypto.randomUUID(), payload: {}, type: 'disposeAll' }, 'disposeAll')
468
456
  }
469
457
 
470
458
  async destroy(keepSessions = true): Promise<void> {
@@ -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
@@ -1,3 +1,5 @@
1
+ import type { AppAction, AppState, TabSession } from '../types'
2
+
1
3
  import {
2
4
  allLeafIds,
3
5
  createGroupId,
@@ -15,13 +17,6 @@ import {
15
17
  import { normalizeGroupedTabOrder } from '../session-persistence'
16
18
  import { orderTabsByWorktree, withActiveWorktree } from '../session-worktrees'
17
19
  import { createDefaultTerminalModes } from '../terminal-modes'
18
- import {
19
- type AppAction,
20
- type AppState,
21
- DEFAULT_SCROLL_INTENT,
22
- deriveScrollIntent,
23
- type TabSession,
24
- } from '../types'
25
20
 
26
21
  const MAX_BUFFER_LENGTH = 50_000
27
22
 
@@ -448,7 +443,6 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
448
443
  buffer: '',
449
444
  errorMessage: undefined,
450
445
  exitCode: undefined,
451
- scrollIntent: DEFAULT_SCROLL_INTENT,
452
446
  status: 'starting',
453
447
  terminalModes: createDefaultTerminalModes(),
454
448
  viewport: undefined,
@@ -470,23 +464,11 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
470
464
  ...state,
471
465
  tabs: updateTab(state.tabs, action.tabId, (tab) => ({
472
466
  ...tab,
473
- scrollIntent:
474
- action.source === 'resize' || action.source === 'switch'
475
- ? (tab.scrollIntent ?? DEFAULT_SCROLL_INTENT)
476
- : deriveScrollIntent(action.viewport),
477
467
  status: tab.status === 'starting' ? 'running' : tab.status,
478
468
  terminalModes: action.terminalModes,
479
469
  viewport: action.viewport,
480
470
  })),
481
471
  }
482
- case 'set-scroll-intent':
483
- return {
484
- ...state,
485
- tabs: updateTab(state.tabs, action.tabId, (tab) => ({
486
- ...tab,
487
- scrollIntent: action.intent,
488
- })),
489
- }
490
472
  case 'set-tab-activity':
491
473
  return {
492
474
  ...state,
@@ -1,12 +1,6 @@
1
+ import type { AppState, TabSession, TabStatus, WorkspaceSnapshotV1 } from './types'
2
+
1
3
  import { allLeafIds, createGroupId, type LayoutNode, pruneLayoutTree } from './layout-tree'
2
- import {
3
- type AppState,
4
- DEFAULT_SCROLL_INTENT,
5
- type ScrollIntent,
6
- type TabSession,
7
- type TabStatus,
8
- type WorkspaceSnapshotV1,
9
- } from './types'
10
4
 
11
5
  export function createEmptyWorkspaceSnapshot(): WorkspaceSnapshotV1 {
12
6
  return {
@@ -47,7 +41,6 @@ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
47
41
  errorMessage: tab.errorMessage,
48
42
  exitCode: tab.exitCode,
49
43
  id: tab.id,
50
- scrollIntent: tab.scrollIntent,
51
44
  status: tab.status === 'disconnected' ? 'running' : tab.status,
52
45
  terminalModes: tab.terminalModes,
53
46
  title: tab.title,
@@ -76,7 +69,6 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
76
69
  errorMessage: tab.errorMessage,
77
70
  exitCode: tab.exitCode,
78
71
  id: tab.id,
79
- scrollIntent: tab.scrollIntent ?? DEFAULT_SCROLL_INTENT,
80
72
  status: getDisconnectedStatus(tab.status),
81
73
  terminalModes: tab.terminalModes,
82
74
  title: tab.title,
@@ -85,18 +77,6 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
85
77
  }))
86
78
  }
87
79
 
88
- export function getSnapshotScrollIntents(
89
- snapshot: WorkspaceSnapshotV1 | undefined
90
- ): Map<string, ScrollIntent> {
91
- if (!snapshot || snapshot.version !== 1) {
92
- return new Map()
93
- }
94
-
95
- return new Map(
96
- snapshot.tabs.map((tab) => [tab.id, tab.scrollIntent ?? DEFAULT_SCROLL_INTENT] as const)
97
- )
98
- }
99
-
100
80
  export function restoreLayoutTrees(
101
81
  snapshot: WorkspaceSnapshotV1 | undefined,
102
82
  tabs: TabSession[]
@@ -70,16 +70,11 @@ export interface TerminalSnapshot {
70
70
  cursorVisible: boolean
71
71
  }
72
72
 
73
+ // The scroll position is owned end-to-end by the backend emulator; this type
74
+ // only describes the re-anchor target the backend computes for itself across a
75
+ // reflow. The frontend no longer derives, stores, or sends a scroll intent.
73
76
  export type ScrollIntent = { kind: 'bottom' } | { absoluteLine: number; kind: 'anchor' }
74
77
 
75
- export const DEFAULT_SCROLL_INTENT: ScrollIntent = { kind: 'bottom' }
76
-
77
- export function deriveScrollIntent(viewport: TerminalSnapshot): ScrollIntent {
78
- return viewport.viewportY >= viewport.baseY
79
- ? { kind: 'bottom' }
80
- : { absoluteLine: viewport.viewportY, kind: 'anchor' }
81
- }
82
-
83
78
  export interface TerminalModeState {
84
79
  mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any'
85
80
  sendFocusMode: boolean
@@ -97,7 +92,6 @@ export interface PersistedTabSnapshot {
97
92
  buffer: string
98
93
  viewport?: TerminalSnapshot
99
94
  terminalModes: TerminalModeState
100
- scrollIntent?: ScrollIntent
101
95
  errorMessage?: string
102
96
  exitCode?: number
103
97
  worktreeId?: string
@@ -163,7 +157,6 @@ export interface TabSession {
163
157
  buffer: string
164
158
  viewport?: TerminalSnapshot
165
159
  terminalModes: TerminalModeState
166
- scrollIntent?: ScrollIntent
167
160
  command: string
168
161
  errorMessage?: string
169
162
  exitCode?: number
@@ -583,7 +576,6 @@ export type TabAction =
583
576
  terminalModes: TerminalModeState
584
577
  source?: 'resize' | 'scroll' | 'data' | 'switch'
585
578
  }
586
- | { type: 'set-scroll-intent'; tabId: string; intent: ScrollIntent }
587
579
  | { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
588
580
  | { type: 'set-tab-error'; tabId: string; message: string }
589
581
 
@@ -56,13 +56,6 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
56
56
  )
57
57
  }
58
58
 
59
- function isScrollIntent(value: unknown): boolean {
60
- if (!isObjectRecord(value)) return false
61
- if (value.kind === 'bottom') return true
62
- if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
63
- return false
64
- }
65
-
66
59
  function isTerminalModeState(value: unknown): value is TerminalModeState {
67
60
  return (
68
61
  isObjectRecord(value) &&
@@ -149,7 +142,6 @@ export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapsho
149
142
  isString(tab.buffer) &&
150
143
  isTerminalModeState(tab.terminalModes) &&
151
144
  (tab.viewport === undefined || isTerminalSnapshot(tab.viewport)) &&
152
- (tab.scrollIntent === undefined || isScrollIntent(tab.scrollIntent)) &&
153
145
  (tab.errorMessage === undefined || isString(tab.errorMessage)) &&
154
146
  (tab.exitCode === undefined || isFiniteNumber(tab.exitCode)) &&
155
147
  (tab.worktreeId === undefined || isString(tab.worktreeId))
@@ -1,12 +1,7 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
  import { connect, type Socket } from 'node:net'
3
3
 
4
- import type {
5
- ScrollIntent,
6
- TerminalModeState,
7
- TerminalSnapshot,
8
- WorkspaceSnapshotV1,
9
- } from '../state/types'
4
+ import type { TerminalModeState, TerminalSnapshot, WorkspaceSnapshotV1 } from '../state/types'
10
5
 
11
6
  import { getTerminalManagerSocketPath } from '../daemon/runtime-paths'
12
7
  import { logDebug } from '../debug/input-log'
@@ -306,29 +301,18 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
306
301
  })
307
302
  }
308
303
 
309
- async resize(
310
- sessionId: string,
311
- cols: number,
312
- rows: number,
313
- intents?: Record<string, ScrollIntent>
314
- ): Promise<void> {
304
+ async resize(sessionId: string, cols: number, rows: number): Promise<void> {
315
305
  return this.sendExpectOk({
316
306
  id: crypto.randomUUID(),
317
- payload: { cols, intents, rows, sessionId },
307
+ payload: { cols, rows, sessionId },
318
308
  type: 'resizeClient',
319
309
  })
320
310
  }
321
311
 
322
- async resizeTab(
323
- sessionId: string,
324
- tabId: string,
325
- cols: number,
326
- rows: number,
327
- intent?: ScrollIntent
328
- ): Promise<void> {
312
+ async resizeTab(sessionId: string, tabId: string, cols: number, rows: number): Promise<void> {
329
313
  return this.sendExpectOk({
330
314
  id: crypto.randomUUID(),
331
- payload: { cols, intent, rows, sessionId, tabId },
315
+ payload: { cols, rows, sessionId, tabId },
332
316
  type: 'resizeTab',
333
317
  })
334
318
  }
@@ -349,14 +333,6 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
349
333
  })
350
334
  }
351
335
 
352
- async reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
353
- return this.sendExpectOk({
354
- id: crypto.randomUUID(),
355
- payload: { intent, sessionId, tabId },
356
- type: 'reapplyScrollIntent',
357
- })
358
- }
359
-
360
336
  async setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
361
337
  return this.sendExpectOk({
362
338
  id: crypto.randomUUID(),
@@ -211,15 +211,10 @@ export async function runTerminalManager(): Promise<void> {
211
211
  break
212
212
  case 'resizeClient': {
213
213
  requireNegotiatedVersion(socket, negotiatedVersions)
214
- const intentsRecord = message.payload.intents
215
- const intentsMap = intentsRecord
216
- ? new Map(Object.entries(intentsRecord))
217
- : undefined
218
214
  sessionManager.resize(
219
215
  message.payload.sessionId,
220
216
  message.payload.cols,
221
- message.payload.rows,
222
- intentsMap
217
+ message.payload.rows
223
218
  )
224
219
  sendOk(socket, message.id)
225
220
  break
@@ -230,8 +225,7 @@ export async function runTerminalManager(): Promise<void> {
230
225
  message.payload.sessionId,
231
226
  message.payload.tabId,
232
227
  message.payload.cols,
233
- message.payload.rows,
234
- message.payload.intent
228
+ message.payload.rows
235
229
  )
236
230
  sendOk(socket, message.id)
237
231
  break
@@ -249,15 +243,6 @@ export async function runTerminalManager(): Promise<void> {
249
243
  sessionManager.scrollToBottom(message.payload.sessionId, message.payload.tabId)
250
244
  sendOk(socket, message.id)
251
245
  break
252
- case 'reapplyScrollIntent':
253
- requireNegotiatedVersion(socket, negotiatedVersions)
254
- sessionManager.reapplyScrollIntent(
255
- message.payload.sessionId,
256
- message.payload.tabId,
257
- message.payload.intent
258
- )
259
- sendOk(socket, message.id)
260
- break
261
246
  case 'setActiveTab':
262
247
  requireNegotiatedVersion(socket, negotiatedVersions)
263
248
  sessionManager.setActiveTab(message.payload.sessionId, message.payload.tabId)
@@ -1,3 +1,5 @@
1
+ import { memo, useCallback } from 'react'
2
+
1
3
  import type { FoldDispatch } from './pierre-diff'
2
4
 
3
5
  import { useTheme } from '../../../theme'
@@ -8,7 +10,7 @@ interface Props {
8
10
  dispatch: FoldDispatch
9
11
  }
10
12
 
11
- function Button({ label, onPress }: { label: string; onPress: () => void }) {
13
+ const Button = memo(function Button({ label, onPress }: { label: string; onPress: () => void }) {
12
14
  const t = useTheme()
13
15
  const bg = t.diffContextBg
14
16
  return (
@@ -16,7 +18,7 @@ function Button({ label, onPress }: { label: string; onPress: () => void }) {
16
18
  <text fg={t.primary}>{label}</text>
17
19
  </box>
18
20
  )
19
- }
21
+ })
20
22
 
21
23
  function Spacer() {
22
24
  return <text> </text>
@@ -31,42 +33,47 @@ export function FoldStrip({ dispatch, fold }: Props) {
31
33
  const shrinkUp = Math.min(FOLD_STEP, topExpanded)
32
34
  const shrinkDown = Math.min(FOLD_STEP, bottomExpanded)
33
35
 
36
+ const handleExpandTop = useCallback(
37
+ () => dispatch.adjust(foldId, 'top', stepUp),
38
+ [dispatch, foldId, stepUp]
39
+ )
40
+ const handleExpandBottom = useCallback(
41
+ () => dispatch.adjust(foldId, 'bottom', stepDown),
42
+ [dispatch, foldId, stepDown]
43
+ )
44
+ const handleExpandAll = useCallback(
45
+ () => dispatch.set(foldId, total, 0),
46
+ [dispatch, foldId, total]
47
+ )
48
+ const handleShrinkTop = useCallback(
49
+ () => dispatch.adjust(foldId, 'top', -shrinkUp),
50
+ [dispatch, foldId, shrinkUp]
51
+ )
52
+ const handleShrinkBottom = useCallback(
53
+ () => dispatch.adjust(foldId, 'bottom', -shrinkDown),
54
+ [dispatch, foldId, shrinkDown]
55
+ )
56
+
34
57
  const controls: React.ReactNode[] = []
35
58
  if (hidden > 0) {
36
59
  controls.push(
37
- <Button
38
- key="up"
39
- label={`↑${stepUp}`}
40
- onPress={() => dispatch.adjust(foldId, 'top', stepUp)}
41
- />,
60
+ <Button key="up" label={`↑${stepUp}`} onPress={handleExpandTop} />,
42
61
  <Spacer key="sp1" />,
43
- <Button
44
- key="down"
45
- label={`↓${stepDown}`}
46
- onPress={() => dispatch.adjust(foldId, 'bottom', stepDown)}
47
- />,
62
+ <Button key="down" label={`↓${stepDown}`} onPress={handleExpandBottom} />,
48
63
  <Spacer key="sp2" />,
49
- <Button key="all" label="⇅ all" onPress={() => dispatch.set(foldId, total, 0)} />
64
+ <Button key="all" label="⇅ all" onPress={handleExpandAll} />
50
65
  )
51
66
  }
52
67
  if (shrinkUp > 0) {
53
68
  controls.push(
54
69
  <Spacer key="sp3" />,
55
- <Button
56
- key="shrinkUp"
57
- label={`−↑${shrinkUp}`}
58
- onPress={() => dispatch.adjust(foldId, 'top', -shrinkUp)}
59
- />
70
+ <Button key="shrinkUp" label={`−↑${shrinkUp}`} onPress={handleShrinkTop} />
60
71
  )
61
72
  }
62
73
  if (shrinkDown > 0) {
63
74
  controls.push(
64
75
  <Spacer key="sp4" />,
65
- <Button
66
- key="shrinkDown"
67
- label={`−↓${shrinkDown}`}
68
- onPress={() => dispatch.adjust(foldId, 'bottom', -shrinkDown)}
69
- />
76
+ <Button key="shrinkDown" label={`−↓${shrinkDown}`} onPress={handleShrinkBottom} />
70
77
  )
71
78
  }
72
79