@brimveyn/aimux 1.3.0 → 1.3.1

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 (49) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/backend-runtime-events.ts +7 -1
  3. package/src/app-runtime/pty-write.ts +7 -4
  4. package/src/app-runtime/side-effects.ts +54 -4
  5. package/src/app-runtime/snippet-actions.ts +3 -2
  6. package/src/app-runtime/use-backend-runtime.ts +7 -2
  7. package/src/app-runtime/use-renderer-bindings.ts +2 -2
  8. package/src/app-runtime/use-terminal-resize.ts +15 -6
  9. package/src/app.tsx +89 -30
  10. package/src/config.ts +11 -0
  11. package/src/daemon/daemon.ts +19 -2
  12. package/src/daemon/session-manager.ts +20 -5
  13. package/src/daemon/session-registry.ts +18 -11
  14. package/src/index.tsx +8 -1
  15. package/src/input/keymap/describe-bindings.ts +68 -0
  16. package/src/input/keymap/key-format.ts +67 -0
  17. package/src/input/modes/bridge.ts +1 -0
  18. package/src/input/modes/transitions.ts +2 -0
  19. package/src/input/modes/types.ts +2 -0
  20. package/src/ipc/manager-protocol.ts +51 -2
  21. package/src/ipc/protocol.ts +43 -2
  22. package/src/pty/pty-manager.ts +29 -3
  23. package/src/session-backend/local-session-backend.ts +12 -5
  24. package/src/session-backend/remote-session-backend.ts +18 -5
  25. package/src/session-backend/types.ts +4 -2
  26. package/src/state/reducers/modal-state.ts +18 -1
  27. package/src/state/reducers/tab-state.ts +20 -2
  28. package/src/state/session-persistence.ts +9 -2
  29. package/src/state/types.ts +23 -0
  30. package/src/state/validation.ts +8 -0
  31. package/src/terminal-manager/manager-client.ts +29 -5
  32. package/src/terminal-manager/terminal-manager.ts +19 -3
  33. package/src/ui/components/create-session-modal.tsx +3 -5
  34. package/src/ui/components/git-commit-modal.tsx +3 -5
  35. package/src/ui/components/help-modal.tsx +49 -68
  36. package/src/ui/components/list-item.tsx +24 -5
  37. package/src/ui/components/new-tab-modal.tsx +8 -9
  38. package/src/ui/components/pending-chord-overlay.tsx +28 -0
  39. package/src/ui/components/session-name-modal.tsx +3 -5
  40. package/src/ui/components/session-picker-modal.tsx +3 -1
  41. package/src/ui/components/snippet-editor-modal.tsx +3 -1
  42. package/src/ui/components/snippet-picker-modal.tsx +3 -1
  43. package/src/ui/components/status-bar.tsx +6 -2
  44. package/src/ui/components/theme-picker-modal.tsx +3 -6
  45. package/src/ui/components/update-available-modal.tsx +42 -0
  46. package/src/ui/keymap-context.ts +39 -0
  47. package/src/ui/root.tsx +12 -0
  48. package/src/ui/status-bar-model.ts +67 -39
  49. package/src/update/version-check.ts +67 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -57,7 +57,7 @@
57
57
  "format:check": "oxfmt --check ."
58
58
  },
59
59
  "dependencies": {
60
- "@brimveyn/aimux-config": "0.1.0",
60
+ "@brimveyn/aimux-config": "0.2.6",
61
61
  "@opentui/core": "^0.1.90",
62
62
  "@opentui/react": "^0.1.90",
63
63
  "@xterm/headless": "^6.0.0",
@@ -51,7 +51,13 @@ export function bindBackendRuntimeEvents({
51
51
  viewportY: viewport.viewportY,
52
52
  })
53
53
 
54
- dispatch({ tabId, terminalModes, type: 'replace-tab-viewport', viewport })
54
+ dispatch({
55
+ source: resizingRef.current ? 'resize' : 'data',
56
+ tabId,
57
+ terminalModes,
58
+ type: 'replace-tab-viewport',
59
+ viewport,
60
+ })
55
61
  if (timeouts.isStartupGraceActive(tabId) || resizingRef.current) {
56
62
  return
57
63
  }
@@ -1,5 +1,5 @@
1
1
  import type { SessionBackend } from '../session-backend/types'
2
- import type { TabSession } from '../state/types'
2
+ import type { AppAction, TabSession } from '../state/types'
3
3
 
4
4
  import { buildPtyPastePayload } from '../input/paste'
5
5
 
@@ -12,10 +12,12 @@ export function writeToTab(
12
12
  backend: SessionBackend,
13
13
  tabId: string,
14
14
  tab: TabSession | undefined,
15
- input: string
15
+ input: string,
16
+ dispatch?: (action: AppAction) => void
16
17
  ): void {
17
18
  if (tab && shouldScrollViewportToBottom(tab)) {
18
19
  backend.scrollViewportToBottom(tabId)
20
+ dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
19
21
  }
20
22
 
21
23
  backend.write(tabId, input)
@@ -25,8 +27,9 @@ export function writePasteToTab(
25
27
  backend: SessionBackend,
26
28
  tabId: string,
27
29
  tab: TabSession | undefined,
28
- text: string
30
+ text: string,
31
+ dispatch?: (action: AppAction) => void
29
32
  ): void {
30
33
  const payload = buildPtyPastePayload(text, tab?.terminalModes.bracketedPasteMode ?? false)
31
- writeToTab(backend, tabId, tab, payload)
34
+ writeToTab(backend, tabId, tab, payload, dispatch)
32
35
  }
@@ -2,7 +2,6 @@ import { $ } from 'bun'
2
2
 
3
3
  import type { SideEffect } from '../input/modes/types'
4
4
  import type { SessionBackend } from '../session-backend/types'
5
- import type { AppAction, AppState, AssistantId, TabSession } from '../state/types'
6
5
 
7
6
  import { loadConfig, saveConfig } from '../config'
8
7
  import { logInputDebug } from '../debug/input-log'
@@ -27,6 +26,13 @@ import {
27
26
  } from '../state/layout-tree'
28
27
  import { filterSessions, filterSnippets } from '../state/selectors'
29
28
  import { createDefaultTerminalModes } from '../state/terminal-modes'
29
+ import {
30
+ type AppAction,
31
+ type AppState,
32
+ type AssistantId,
33
+ DEFAULT_SCROLL_INTENT,
34
+ type TabSession,
35
+ } from '../state/types'
30
36
  import { saveCurrentWorkspace } from '../state/workspace-save'
31
37
  import { scrollGitDiff } from '../ui/git-view-controls'
32
38
  import { applyTheme } from '../ui/theme'
@@ -124,14 +130,14 @@ function pasteSnippetToActiveGroup(ctx: SideEffectContext): void {
124
130
  const groupId = getGroupIdForTab(state.tabGroupMap, state.activeTabId)
125
131
  const groupTree = groupId ? state.layoutTrees[groupId] : null
126
132
  if (!groupTree) {
127
- pasteSnippetToTab(backend, state.activeTabId, activeTab, snippet)
133
+ pasteSnippetToTab(backend, state.activeTabId, activeTab, snippet, ctx.dispatch)
128
134
  return
129
135
  }
130
136
 
131
137
  for (const tabId of allLeafIds(groupTree)) {
132
138
  const tab = state.tabs.find((entry) => entry.id === tabId)
133
139
  if (tab) {
134
- pasteSnippetToTab(backend, tabId, tab, snippet)
140
+ pasteSnippetToTab(backend, tabId, tab, snippet, ctx.dispatch)
135
141
  }
136
142
  }
137
143
  }
@@ -229,6 +235,7 @@ export function createTabSession(
229
235
  buffer: '',
230
236
  command: customCommand ?? option.command,
231
237
  id: createTabId(),
238
+ scrollIntent: DEFAULT_SCROLL_INTENT,
232
239
  status: 'starting',
233
240
  terminalModes: createDefaultTerminalModes(),
234
241
  title: option.label,
@@ -393,7 +400,13 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
393
400
  )
394
401
  return
395
402
  case 'paste-selected-snippet': {
396
- pasteSnippetToTab(backend, state.activeTabId, ctx.activeTab, getSelectedSnippet(state))
403
+ pasteSnippetToTab(
404
+ backend,
405
+ state.activeTabId,
406
+ ctx.activeTab,
407
+ getSelectedSnippet(state),
408
+ dispatch
409
+ )
397
410
  return
398
411
  }
399
412
  case 'paste-snippet-to-group': {
@@ -471,11 +484,48 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
471
484
  void enqueueGitOp(() => runGitPush(ctx))
472
485
  return
473
486
  }
487
+ case 'confirm-update-selection': {
488
+ handleConfirmUpdateSelection(ctx)
489
+ return
490
+ }
474
491
  default:
475
492
  effect satisfies never
476
493
  }
477
494
  }
478
495
 
496
+ function handleConfirmUpdateSelection(ctx: SideEffectContext): void {
497
+ const { state } = ctx
498
+ if (state.modal.type !== 'update-available') {
499
+ return
500
+ }
501
+ const latest = state.modal.latestVersion
502
+ if (state.modal.selectedIndex === 0) {
503
+ runUpdateFromTui(ctx, latest)
504
+ return
505
+ }
506
+ saveConfig({ ...loadConfig(), skippedUpdateVersion: latest })
507
+ }
508
+
509
+ function runUpdateFromTui(ctx: SideEffectContext, latestVersion: string): void {
510
+ saveCurrentWorkspace(ctx.state)
511
+ void ctx.backend.destroy(true)
512
+ ctx.renderer.destroy()
513
+ process.stdout.write(`\nUpdating aimux to ${latestVersion}...\n`)
514
+ const proc = Bun.spawn(['bun', 'update', '-g', '@brimveyn/aimux', '@brimveyn/aimux-config'], {
515
+ stderr: 'inherit',
516
+ stdin: 'inherit',
517
+ stdout: 'inherit',
518
+ })
519
+ void proc.exited.then((code) => {
520
+ if (code === 0) {
521
+ process.stdout.write(`\nUpdated. Run \`aimux\` to start the new version.\n`)
522
+ } else {
523
+ process.stderr.write(`\nUpdate failed (exit code ${code}).\n`)
524
+ }
525
+ process.exit(code ?? 1)
526
+ })
527
+ }
528
+
479
529
  async function runGitAction(
480
530
  ctx: SideEffectContext,
481
531
  args: string[],
@@ -57,13 +57,14 @@ export function pasteSnippetToTab(
57
57
  backend: SessionBackend,
58
58
  activeTabId: string | null,
59
59
  activeTab: TabSession | undefined,
60
- snippet: SnippetRecord | undefined
60
+ snippet: SnippetRecord | undefined,
61
+ dispatch?: (action: AppAction) => void
61
62
  ): void {
62
63
  if (!snippet || !activeTabId || !activeTab) {
63
64
  return
64
65
  }
65
66
 
66
- writePasteToTab(backend, activeTabId, activeTab, snippet.content)
67
+ writePasteToTab(backend, activeTabId, activeTab, snippet.content, dispatch)
67
68
  }
68
69
 
69
70
  export function handleDeleteSnippetEffect(
@@ -1,7 +1,7 @@
1
1
  import { type MutableRefObject, useEffect, useRef } from 'react'
2
2
 
3
3
  import type { SessionBackend } from '../session-backend/types'
4
- import type { AppAction, LayoutState } from '../state/types'
4
+ import type { AppAction, LayoutState, ScrollIntent } from '../state/types'
5
5
 
6
6
  import { attachCurrentSession } from './backend-attach-runtime'
7
7
  import { bindBackendRuntimeEvents } from './backend-runtime-events'
@@ -11,6 +11,7 @@ interface BackendRuntimeOptions {
11
11
  backend: SessionBackend
12
12
  dispatch: (action: AppAction) => void
13
13
  activeTabId: string | null
14
+ activeTabScrollIntentRef: MutableRefObject<ScrollIntent | null>
14
15
  currentSessionId: string | null
15
16
  layoutRef: MutableRefObject<LayoutState>
16
17
  resizingRef: MutableRefObject<boolean>
@@ -25,6 +26,7 @@ export interface TabRuntimeControls {
25
26
 
26
27
  export function useBackendRuntime({
27
28
  activeTabId,
29
+ activeTabScrollIntentRef,
28
30
  backend,
29
31
  currentSessionId,
30
32
  currentSessionWorkspaceSnapshot,
@@ -65,7 +67,10 @@ export function useBackendRuntime({
65
67
  }
66
68
 
67
69
  backend.setActiveTab(activeTabId)
68
- }, [activeTabId, backend, currentSessionId])
70
+ if (activeTabId && activeTabScrollIntentRef.current) {
71
+ backend.reapplyScrollIntent(activeTabId, activeTabScrollIntentRef.current)
72
+ }
73
+ }, [activeTabId, activeTabScrollIntentRef, backend, currentSessionId])
69
74
 
70
75
  useEffect(() => {
71
76
  return bindBackendRuntimeEvents({
@@ -60,7 +60,7 @@ export function useRendererBindings({
60
60
  activeTabRef.current?.terminalModes.bracketedPasteMode ?? false,
61
61
  getFocusMode: () => focusModeRef.current,
62
62
  handleTerminalShortcut,
63
- writeToPty: (tabId, data) => writeToTab(backend, tabId, activeTabRef.current, data),
63
+ writeToPty: (tabId, data) => writeToTab(backend, tabId, activeTabRef.current, data, dispatch),
64
64
  })
65
65
 
66
66
  const handlePasteEvent = (event: { bytes: Uint8Array; defaultPrevented?: boolean }) => {
@@ -96,7 +96,7 @@ export function useRendererBindings({
96
96
  return
97
97
  }
98
98
 
99
- writePasteToTab(backend, tabId, tab, payload)
99
+ writePasteToTab(backend, tabId, tab, payload, dispatch)
100
100
  }
101
101
 
102
102
  const handleSelection = (selection: OtuiSelection) => {
@@ -2,7 +2,7 @@ import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
2
2
 
3
3
  import type { TerminalContentOrigin } from '../input/raw-input-handler'
4
4
  import type { SessionBackend } from '../session-backend/types'
5
- import type { AppAction, AppState } from '../state/types'
5
+ import type { AppAction, AppState, ScrollIntent } from '../state/types'
6
6
 
7
7
  import {
8
8
  createTerminalBounds,
@@ -27,20 +27,21 @@ function resizeSplitTabs(
27
27
  layoutTrees: AppState['layoutTrees'],
28
28
  tabIds: string[],
29
29
  cols: number,
30
- rows: number
30
+ rows: number,
31
+ intents: Map<string, ScrollIntent>
31
32
  ): void {
32
33
  const bounds = getTerminalBounds(cols, rows)
33
34
  const resizedTabIds = new Set<string>()
34
35
 
35
36
  forEachSplitPaneRect(Object.values(layoutTrees), bounds, (tabId, rect) => {
36
37
  const size = toTerminalContentSize(rect)
37
- backend.resizeTab(tabId, size.cols, size.rows)
38
+ backend.resizeTab(tabId, size.cols, size.rows, intents.get(tabId))
38
39
  resizedTabIds.add(tabId)
39
40
  })
40
41
 
41
42
  for (const id of tabIds) {
42
43
  if (!resizedTabIds.has(id)) {
43
- backend.resizeTab(id, cols, rows)
44
+ backend.resizeTab(id, cols, rows, intents.get(id))
44
45
  }
45
46
  }
46
47
  }
@@ -64,6 +65,7 @@ export function useTerminalResize({
64
65
  }: UseTerminalResizeOptions) {
65
66
  const resizingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
66
67
  const tabIdsRef = useRef<string[]>([])
68
+ const intentsRef = useRef<Map<string, ScrollIntent>>(new Map())
67
69
 
68
70
  const currentTabIds = state.tabs.map((t) => t.id)
69
71
  const tabIdsChanged =
@@ -74,6 +76,12 @@ export function useTerminalResize({
74
76
  }
75
77
  const stableTabIds = tabIdsRef.current
76
78
 
79
+ intentsRef.current = new Map(
80
+ state.tabs
81
+ .filter((t): t is typeof t & { scrollIntent: ScrollIntent } => t.scrollIntent !== undefined)
82
+ .map((t) => [t.id, t.scrollIntent])
83
+ )
84
+
77
85
  const terminalSize = useMemo(() => {
78
86
  const sidebarWidth = state.sidebar.visible ? state.sidebar.width + 1 : 0
79
87
  const reservedRows =
@@ -118,10 +126,11 @@ export function useTerminalResize({
118
126
  state.layoutTrees,
119
127
  stableTabIds,
120
128
  terminalSize.cols,
121
- terminalSize.rows
129
+ terminalSize.rows,
130
+ intentsRef.current
122
131
  )
123
132
  } else {
124
- backend.resizeAll(terminalSize.cols, terminalSize.rows)
133
+ backend.resizeAll(terminalSize.cols, terminalSize.rows, intentsRef.current)
125
134
  }
126
135
  resizingTimerRef.current = setTimeout(() => {
127
136
  resizingRef.current = false
package/src/app.tsx CHANGED
@@ -1,6 +1,15 @@
1
- import { getDefaultKeymapConfig } from '@brimveyn/aimux-config'
1
+ import type { ResolvedConfig } from '@brimveyn/aimux-config'
2
+
2
3
  import { useKeyboard, useRenderer, useTerminalDimensions } from '@opentui/react'
3
- import { useCallback, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react'
4
+ import {
5
+ useCallback,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useReducer,
10
+ useRef,
11
+ useState,
12
+ } from 'react'
4
13
 
5
14
  import type { KeyChord } from './input/keymap/key-chord'
6
15
  import type { TrieBinding } from './input/keymap/trie'
@@ -20,19 +29,36 @@ import { deriveModeId } from './input/modes/bridge'
20
29
  import { registerAllModes } from './input/modes/handlers'
21
30
  import { getHandler, transitionTo } from './input/modes/registry'
22
31
  import { type TerminalContentOrigin } from './input/raw-input-handler'
32
+ import { getProfileName } from './profile-paths'
23
33
  import { appStore } from './state/app-store'
24
34
  import { setActiveDispatch } from './state/dispatch-ref'
25
35
  import { loadSessionCatalog } from './state/session-catalog'
26
36
  import { loadSnippetCatalog } from './state/snippet-catalog'
27
37
  import { appReducer, createInitialState } from './state/store'
38
+ import { KeymapContext } from './ui/keymap-context'
28
39
  import { RootView } from './ui/root'
29
40
  import { applyTheme } from './ui/theme'
30
-
31
- const keymapHandlers = registerAllModes(getDefaultKeymapConfig())
41
+ import {
42
+ fetchLatestNpmVersion,
43
+ getCurrentPackageVersion,
44
+ isNewerVersion,
45
+ } from './update/version-check'
32
46
 
33
47
  const WORKSPACE_SAVE_DEBOUNCE_MS = 250
34
48
 
35
- export function App({ backend }: { backend: SessionBackend }) {
49
+ export function App({
50
+ backend,
51
+ resolvedConfig,
52
+ }: {
53
+ backend: SessionBackend
54
+ resolvedConfig: ResolvedConfig
55
+ }) {
56
+ const keymapHandlers = useMemo(
57
+ () => registerAllModes(resolvedConfig.keymaps),
58
+ // Registration has side effects in a global mode registry — run once per app instance.
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ []
61
+ )
36
62
  const renderer = useRenderer()
37
63
  const dimensions = useTerminalDimensions()
38
64
  const [themeId, setThemeId] = useState<ThemeId>(() => {
@@ -59,6 +85,31 @@ export function App({ backend }: { backend: SessionBackend }) {
59
85
  return () => setActiveDispatch(null)
60
86
  }, [dispatch])
61
87
 
88
+ useEffect(() => {
89
+ if (process.env.AIMUX_DISABLE_UPDATE_CHECK === '1') return
90
+ if (getProfileName() === 'dev') return
91
+
92
+ let cancelled = false
93
+ void (async () => {
94
+ const [current, latest] = await Promise.all([
95
+ getCurrentPackageVersion(),
96
+ fetchLatestNpmVersion('@brimveyn/aimux'),
97
+ ])
98
+ if (cancelled || !latest) return
99
+ if (!isNewerVersion(latest, current)) return
100
+ if (loadConfig().skippedUpdateVersion === latest) return
101
+ dispatch({
102
+ currentVersion: current,
103
+ latestVersion: latest,
104
+ type: 'open-update-available-modal',
105
+ })
106
+ })()
107
+
108
+ return () => {
109
+ cancelled = true
110
+ }
111
+ }, [])
112
+
62
113
  const resizingRef = useRef(false)
63
114
  const layoutRef = useRef(state.layout)
64
115
  layoutRef.current = state.layout
@@ -81,6 +132,8 @@ export function App({ backend }: { backend: SessionBackend }) {
81
132
  activeTabIdRef.current = state.activeTabId
82
133
  const activeTabRef = useRef(activeTab)
83
134
  activeTabRef.current = activeTab
135
+ const activeTabScrollIntentRef = useRef(activeTab?.scrollIntent ?? null)
136
+ activeTabScrollIntentRef.current = activeTab?.scrollIntent ?? null
84
137
 
85
138
  const stateRef = useRef(state)
86
139
  stateRef.current = state
@@ -90,6 +143,7 @@ export function App({ backend }: { backend: SessionBackend }) {
90
143
 
91
144
  const { clearIdleTimer, clearStartupGrace, startStartupGrace } = useBackendRuntime({
92
145
  activeTabId: state.activeTabId,
146
+ activeTabScrollIntentRef,
93
147
  backend,
94
148
  currentSessionId: state.currentSessionId,
95
149
  currentSessionWorkspaceSnapshot,
@@ -132,15 +186,18 @@ export function App({ backend }: { backend: SessionBackend }) {
132
186
  // Allows handleTerminalShortcut (a stable callback) to reach the latest closure.
133
187
  const processKeyResultRef = useRef<(result: KeyResult, modeId: ModeId) => void>(() => {})
134
188
 
135
- const handleTerminalShortcut = useCallback((chord: KeyChord): boolean => {
136
- const terminalHandler = keymapHandlers.find((h) => h.id === 'terminal-input')
137
- if (!terminalHandler) return false
138
- const ctx: ModeContext = { state: stateRef.current }
139
- const result = terminalHandler.handleChord(chord, ctx)
140
- if (!result) return false
141
- processKeyResultRef.current(result, 'terminal-input')
142
- return true
143
- }, [])
189
+ const handleTerminalShortcut = useCallback(
190
+ (chord: KeyChord): boolean => {
191
+ const terminalHandler = keymapHandlers.find((h) => h.id === 'terminal-input')
192
+ if (!terminalHandler) return false
193
+ const ctx: ModeContext = { state: stateRef.current }
194
+ const result = terminalHandler.handleChord(chord, ctx)
195
+ if (!result) return false
196
+ processKeyResultRef.current(result, 'terminal-input')
197
+ return true
198
+ },
199
+ [keymapHandlers]
200
+ )
144
201
 
145
202
  useRendererBindings({
146
203
  activeTabId: state.activeTabId,
@@ -233,21 +290,23 @@ export function App({ backend }: { backend: SessionBackend }) {
233
290
  })
234
291
 
235
292
  return (
236
- <RootView
237
- themeId={themeId}
238
- contentOrigin={contentOriginRef.current}
239
- mouseForwardingEnabled={activeMouseForwardingEnabled}
240
- localScrollbackEnabled={activeLocalScrollbackEnabled}
241
- onTerminalMouseEvent={handleTerminalMouseEvent}
242
- onTerminalScrollEvent={handleTerminalScrollEvent}
243
- onTerminalClick={handleTerminalClick}
244
- onPaneActivate={handlePaneActivate}
245
- onSplitResize={handleSplitResize}
246
- onSeparatorDragStart={handleSeparatorDragStart}
247
- onSeparatorDrag={handleSeparatorDrag}
248
- onSeparatorDragEnd={handleSeparatorDragEnd}
249
- terminalCols={terminalSize.cols}
250
- terminalRows={terminalSize.rows}
251
- />
293
+ <KeymapContext.Provider value={resolvedConfig.keymaps}>
294
+ <RootView
295
+ themeId={themeId}
296
+ contentOrigin={contentOriginRef.current}
297
+ mouseForwardingEnabled={activeMouseForwardingEnabled}
298
+ localScrollbackEnabled={activeLocalScrollbackEnabled}
299
+ onTerminalMouseEvent={handleTerminalMouseEvent}
300
+ onTerminalScrollEvent={handleTerminalScrollEvent}
301
+ onTerminalClick={handleTerminalClick}
302
+ onPaneActivate={handlePaneActivate}
303
+ onSplitResize={handleSplitResize}
304
+ onSeparatorDragStart={handleSeparatorDragStart}
305
+ onSeparatorDrag={handleSeparatorDrag}
306
+ onSeparatorDragEnd={handleSeparatorDragEnd}
307
+ terminalCols={terminalSize.cols}
308
+ terminalRows={terminalSize.rows}
309
+ />
310
+ </KeymapContext.Provider>
252
311
  )
253
312
  }
package/src/config.ts CHANGED
@@ -16,6 +16,7 @@ export interface AimuxConfig {
16
16
  gitPanelVisible?: boolean
17
17
  gitPanelRatio?: number
18
18
  workspaceSnapshot?: WorkspaceSnapshotV1
19
+ skippedUpdateVersion?: string
19
20
  }
20
21
 
21
22
  const DEFAULT_CONFIG: AimuxConfig = {
@@ -58,6 +59,7 @@ export function loadConfigResult(): ConfigLoadResult {
58
59
  gitPanelVisible?: unknown
59
60
  gitPanelRatio?: unknown
60
61
  workspaceSnapshot?: unknown
62
+ skippedUpdateVersion?: unknown
61
63
  }
62
64
 
63
65
  const issues: string[] = []
@@ -98,6 +100,14 @@ export function loadConfigResult(): ConfigLoadResult {
98
100
  issues.push('ignored invalid workspaceSnapshot')
99
101
  }
100
102
 
103
+ const validSkippedUpdateVersion =
104
+ typeof parsed.skippedUpdateVersion === 'string' && parsed.skippedUpdateVersion.length > 0
105
+ ? parsed.skippedUpdateVersion
106
+ : undefined
107
+ if (parsed.skippedUpdateVersion !== undefined && validSkippedUpdateVersion === undefined) {
108
+ issues.push('ignored invalid skippedUpdateVersion')
109
+ }
110
+
101
111
  if (issues.length > 0) {
102
112
  logDebug('config.load.validationIssue', { issues, path: CONFIG_PATH })
103
113
  }
@@ -107,6 +117,7 @@ export function loadConfigResult(): ConfigLoadResult {
107
117
  customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
108
118
  gitPanelRatio: validGitPanelRatio,
109
119
  gitPanelVisible: validGitPanelVisible,
120
+ skippedUpdateVersion: validSkippedUpdateVersion,
110
121
  themeId: isThemeId(parsed.themeId) ? parsed.themeId : undefined,
111
122
  version: 2,
112
123
  workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
@@ -256,7 +256,12 @@ export async function runDaemon(): Promise<void> {
256
256
  case 'resizeClient': {
257
257
  const sessionId = requireSession(socket, attachedSessions)
258
258
  requireNegotiatedVersion(socket, negotiatedVersions)
259
- await manager.resize(sessionId, message.payload.cols, message.payload.rows)
259
+ await manager.resize(
260
+ sessionId,
261
+ message.payload.cols,
262
+ message.payload.rows,
263
+ message.payload.intents
264
+ )
260
265
  sendOk(socket, message.id)
261
266
  break
262
267
  }
@@ -267,7 +272,8 @@ export async function runDaemon(): Promise<void> {
267
272
  sessionId,
268
273
  message.payload.tabId,
269
274
  message.payload.cols,
270
- message.payload.rows
275
+ message.payload.rows,
276
+ message.payload.intent
271
277
  )
272
278
  sendOk(socket, message.id)
273
279
  break
@@ -286,6 +292,17 @@ export async function runDaemon(): Promise<void> {
286
292
  sendOk(socket, message.id)
287
293
  break
288
294
  }
295
+ case 'reapplyScrollIntent': {
296
+ const sessionId = requireSession(socket, attachedSessions)
297
+ requireNegotiatedVersion(socket, negotiatedVersions)
298
+ await manager.reapplyScrollIntent(
299
+ sessionId,
300
+ message.payload.tabId,
301
+ message.payload.intent
302
+ )
303
+ sendOk(socket, message.id)
304
+ break
305
+ }
289
306
  case 'setActiveTab': {
290
307
  const sessionId = requireSession(socket, attachedSessions)
291
308
  requireNegotiatedVersion(socket, negotiatedVersions)
@@ -1,6 +1,11 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
 
3
- import type { TerminalModeState, TerminalSnapshot, WorkspaceSnapshotV1 } from '../state/types'
3
+ import type {
4
+ ScrollIntent,
5
+ TerminalModeState,
6
+ TerminalSnapshot,
7
+ WorkspaceSnapshotV1,
8
+ } from '../state/types'
4
9
 
5
10
  import { logDebug } from '../debug/input-log'
6
11
  import { SessionRegistry } from './session-registry'
@@ -62,12 +67,18 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
62
67
  this.getOrCreateRegistry(sessionId).write(tabId, data)
63
68
  }
64
69
 
65
- resize(sessionId: string, cols: number, rows: number): void {
66
- this.getOrCreateRegistry(sessionId).resizeAll(cols, rows)
70
+ resize(sessionId: string, cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
71
+ this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, intents)
67
72
  }
68
73
 
69
- resizeTab(sessionId: string, tabId: string, cols: number, rows: number): void {
70
- this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows)
74
+ resizeTab(
75
+ sessionId: string,
76
+ tabId: string,
77
+ cols: number,
78
+ rows: number,
79
+ intent?: ScrollIntent
80
+ ): void {
81
+ this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, intent)
71
82
  }
72
83
 
73
84
  scroll(sessionId: string, tabId: string, deltaLines: number): void {
@@ -78,6 +89,10 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
78
89
  this.getOrCreateRegistry(sessionId).scrollViewportToBottom(tabId)
79
90
  }
80
91
 
92
+ reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): void {
93
+ this.getOrCreateRegistry(sessionId).reapplyScrollIntent(tabId, intent)
94
+ }
95
+
81
96
  setActiveTab(sessionId: string, tabId: string | null): void {
82
97
  this.getOrCreateRegistry(sessionId).setActiveTab(tabId)
83
98
  }