@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"bump": "bun run scripts/bump.ts"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@brimveyn/aimux-config": "0.6.
|
|
63
|
+
"@brimveyn/aimux-config": "0.6.3",
|
|
64
64
|
"@opentui/core": "^0.1.90",
|
|
65
65
|
"@opentui/react": "^0.1.90",
|
|
66
66
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
getSnapshotTrees,
|
|
11
11
|
toTerminalContentSize,
|
|
12
12
|
} from '../state/layout-resize'
|
|
13
|
-
import { getSnapshotScrollIntents } from '../state/session-persistence'
|
|
14
13
|
|
|
15
14
|
export function resizeSnapshotPanes(
|
|
16
15
|
snapshot: WorkspaceSnapshotV1 | undefined,
|
|
@@ -22,14 +21,13 @@ export function resizeSnapshotPanes(
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
const trees = getSnapshotTrees(snapshot)
|
|
25
|
-
const intents = getSnapshotScrollIntents(snapshot)
|
|
26
24
|
const bounds = createTerminalBounds(
|
|
27
25
|
layoutRef.current.terminalCols,
|
|
28
26
|
layoutRef.current.terminalRows
|
|
29
27
|
)
|
|
30
28
|
forEachSplitPaneRect(trees, bounds, (tabId, rect) => {
|
|
31
29
|
const size = toTerminalContentSize(rect)
|
|
32
|
-
backend.resizeTab(tabId, size.cols, size.rows
|
|
30
|
+
backend.resizeTab(tabId, size.cols, size.rows)
|
|
33
31
|
})
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
// On multi-click selection mouseUp opentui's finishSelection() fires the
|
|
2
2
|
// 'selection' event *after* our handleTerminalMouseUp has already written
|
|
3
|
-
// the clipboard from drag.capturedLines (anchored
|
|
4
|
-
// handler would then call computeStreamSelectedText against the
|
|
5
|
-
// tab.viewport.lines and overwrite our write with a value that can
|
|
6
|
-
// by a few rows when output
|
|
3
|
+
// the clipboard from drag.capturedLines (anchored in absolute buffer rows).
|
|
4
|
+
// The event handler would then call computeStreamSelectedText against the
|
|
5
|
+
// *current* tab.viewport.lines and overwrite our write with a value that can
|
|
6
|
+
// diverge by a few rows when output/auto-scroll moved the viewport between
|
|
7
|
+
// mouseDown and mouseUp.
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
// finishSelection() emits exactly one 'selection' event, so we suppress
|
|
10
|
+
// exactly one — a consume-once flag rather than a time window (which could
|
|
11
|
+
// either expire before the event arrived, letting a wrong value through, or
|
|
12
|
+
// swallow an unrelated selection that happened to land in the window).
|
|
13
|
+
//
|
|
14
|
+
// A short guard window bounds the flag: if the expected event never arrives
|
|
15
|
+
// (no finishSelection fired), the flag self-heals instead of silently eating
|
|
16
|
+
// the user's next genuine selection.
|
|
17
|
+
const ARM_WINDOW_MS = 250
|
|
18
|
+
let armedAt = 0
|
|
14
19
|
|
|
15
20
|
export function recordMultiClickClipboardWrite(): void {
|
|
16
|
-
|
|
21
|
+
armedAt = Date.now()
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export function shouldSuppressSelectionCopy(): boolean {
|
|
20
|
-
|
|
25
|
+
if (armedAt === 0) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
const withinWindow = Date.now() - armedAt < ARM_WINDOW_MS
|
|
29
|
+
// Consume the arm regardless: the one finishSelection event is now handled,
|
|
30
|
+
// and a stale arm must not suppress a later, unrelated selection.
|
|
31
|
+
armedAt = 0
|
|
32
|
+
return withinWindow
|
|
21
33
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionBackend } from '../session-backend/types'
|
|
2
|
-
import type {
|
|
2
|
+
import type { TabSession } from '../state/types'
|
|
3
3
|
|
|
4
4
|
import { logInputDebug } from '../debug/input-log'
|
|
5
5
|
import { buildPtyPastePayload } from '../input/paste'
|
|
@@ -18,7 +18,6 @@ export function writeToTab(
|
|
|
18
18
|
tabId: string,
|
|
19
19
|
tab: TabSession | undefined,
|
|
20
20
|
input: string,
|
|
21
|
-
dispatch?: (action: AppAction) => void,
|
|
22
21
|
options?: PtyWriteOptions
|
|
23
22
|
): void {
|
|
24
23
|
logInputDebug('ptyWrite.writeToTab', {
|
|
@@ -28,8 +27,9 @@ export function writeToTab(
|
|
|
28
27
|
tabId,
|
|
29
28
|
})
|
|
30
29
|
if ((options?.autoBottom ?? true) && tab && shouldScrollViewportToBottom(tab)) {
|
|
30
|
+
// Backend owns the scroll position; the snapshot it emits reflects the new
|
|
31
|
+
// bottom, so the frontend mirror updates without a separate dispatch.
|
|
31
32
|
backend.scrollViewportToBottom(tabId)
|
|
32
|
-
dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
backend.write(tabId, input)
|
|
@@ -39,11 +39,10 @@ export function writePasteToTab(
|
|
|
39
39
|
backend: SessionBackend,
|
|
40
40
|
tabId: string,
|
|
41
41
|
tab: TabSession | undefined,
|
|
42
|
-
text: string
|
|
43
|
-
dispatch?: (action: AppAction) => void
|
|
42
|
+
text: string
|
|
44
43
|
): void {
|
|
45
44
|
const payload = buildPtyPastePayload(text, tab?.terminalModes.bracketedPasteMode ?? false)
|
|
46
|
-
writeToTab(backend, tabId, tab, payload,
|
|
45
|
+
writeToTab(backend, tabId, tab, payload, { autoBottom: true })
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
const DEL = '\x7f'
|
|
@@ -63,12 +62,10 @@ export function writeMacroExpansionToTab(
|
|
|
63
62
|
tab: TabSession | undefined,
|
|
64
63
|
eraseCount: number,
|
|
65
64
|
expandedText: string,
|
|
66
|
-
cursorOffset: number
|
|
67
|
-
dispatch?: (action: AppAction) => void
|
|
65
|
+
cursorOffset: number
|
|
68
66
|
): void {
|
|
69
67
|
if (tab && shouldScrollViewportToBottom(tab)) {
|
|
70
68
|
backend.scrollViewportToBottom(tabId)
|
|
71
|
-
dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
const erase = eraseCount > 0 ? DEL.repeat(eraseCount) : ''
|
|
@@ -13,6 +13,7 @@ import { dirname, join as joinPath, resolve as resolvePath } from 'node:path'
|
|
|
13
13
|
|
|
14
14
|
import type { SideEffect } from '../input/modes/types'
|
|
15
15
|
import type { SessionBackend } from '../session-backend/types'
|
|
16
|
+
import type { AppAction, AppState, AssistantId, TabSession, WorktreeRecord } from '../state/types'
|
|
16
17
|
import type { ThemeId } from '../ui/themes'
|
|
17
18
|
|
|
18
19
|
import { loadConfig, saveConfig } from '../config'
|
|
@@ -59,14 +60,6 @@ import { getActiveWorktree, getSessionProjectPath } from '../state/session-workt
|
|
|
59
60
|
import { getSnippetsCatalogPath, isConfigSnippetId } from '../state/snippet-catalog'
|
|
60
61
|
import { createDefaultTerminalModes } from '../state/terminal-modes'
|
|
61
62
|
import { toast } from '../state/toast-store'
|
|
62
|
-
import {
|
|
63
|
-
type AppAction,
|
|
64
|
-
type AppState,
|
|
65
|
-
type AssistantId,
|
|
66
|
-
DEFAULT_SCROLL_INTENT,
|
|
67
|
-
type TabSession,
|
|
68
|
-
type WorktreeRecord,
|
|
69
|
-
} from '../state/types'
|
|
70
63
|
import { saveCurrentWorkspace } from '../state/workspace-save'
|
|
71
64
|
import { filterThemeIds } from '../ui/filter-themes'
|
|
72
65
|
import { scrollGitDiff } from '../ui/git-view-controls'
|
|
@@ -173,14 +166,14 @@ function pasteSnippetToActiveGroup(ctx: SideEffectContext): void {
|
|
|
173
166
|
const groupId = getGroupIdForTab(state.tabGroupMap, state.activeTabId)
|
|
174
167
|
const groupTree = groupId != null && groupId !== '' ? state.layoutTrees[groupId] : null
|
|
175
168
|
if (!groupTree) {
|
|
176
|
-
pasteSnippetToTab(backend, state.activeTabId, activeTab, snippet
|
|
169
|
+
pasteSnippetToTab(backend, state.activeTabId, activeTab, snippet)
|
|
177
170
|
return
|
|
178
171
|
}
|
|
179
172
|
|
|
180
173
|
for (const tabId of allLeafIds(groupTree)) {
|
|
181
174
|
const tab = state.tabs.find((entry) => entry.id === tabId)
|
|
182
175
|
if (tab) {
|
|
183
|
-
pasteSnippetToTab(backend, tabId, tab, snippet
|
|
176
|
+
pasteSnippetToTab(backend, tabId, tab, snippet)
|
|
184
177
|
}
|
|
185
178
|
}
|
|
186
179
|
}
|
|
@@ -294,7 +287,6 @@ export function createTabSession(
|
|
|
294
287
|
buffer: '',
|
|
295
288
|
command: customCommand ?? option.command,
|
|
296
289
|
id: createTabId(),
|
|
297
|
-
scrollIntent: DEFAULT_SCROLL_INTENT,
|
|
298
290
|
status: 'starting',
|
|
299
291
|
terminalModes: createDefaultTerminalModes(),
|
|
300
292
|
title: option.label,
|
|
@@ -620,13 +612,7 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
620
612
|
)
|
|
621
613
|
return
|
|
622
614
|
case 'paste-selected-snippet': {
|
|
623
|
-
pasteSnippetToTab(
|
|
624
|
-
backend,
|
|
625
|
-
state.activeTabId,
|
|
626
|
-
ctx.activeTab,
|
|
627
|
-
getSelectedSnippet(state),
|
|
628
|
-
dispatch
|
|
629
|
-
)
|
|
615
|
+
pasteSnippetToTab(backend, state.activeTabId, ctx.activeTab, getSelectedSnippet(state))
|
|
630
616
|
return
|
|
631
617
|
}
|
|
632
618
|
case 'paste-snippet-to-group': {
|
|
@@ -71,14 +71,13 @@ export function pasteSnippetToTab(
|
|
|
71
71
|
backend: SessionBackend,
|
|
72
72
|
activeTabId: string | null,
|
|
73
73
|
activeTab: TabSession | undefined,
|
|
74
|
-
snippet: SnippetRecord | undefined
|
|
75
|
-
dispatch?: (action: AppAction) => void
|
|
74
|
+
snippet: SnippetRecord | undefined
|
|
76
75
|
): void {
|
|
77
76
|
if (!snippet || activeTabId == null || activeTabId === '' || !activeTab) {
|
|
78
77
|
return
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
writePasteToTab(backend, activeTabId, activeTab, snippet.content
|
|
80
|
+
writePasteToTab(backend, activeTabId, activeTab, snippet.content)
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
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
|
|
4
|
+
import type { AppAction, LayoutState } from '../state/types'
|
|
5
5
|
|
|
6
6
|
import { attachCurrentSession } from './backend-attach-runtime'
|
|
7
7
|
import { bindBackendRuntimeEvents } from './backend-runtime-events'
|
|
@@ -11,7 +11,6 @@ interface BackendRuntimeOptions {
|
|
|
11
11
|
backend: SessionBackend
|
|
12
12
|
dispatch: (action: AppAction) => void
|
|
13
13
|
activeTabId: string | null
|
|
14
|
-
activeTabScrollIntentRef: MutableRefObject<ScrollIntent | null>
|
|
15
14
|
currentSessionId: string | null
|
|
16
15
|
layoutRef: MutableRefObject<LayoutState>
|
|
17
16
|
resizingRef: MutableRefObject<boolean>
|
|
@@ -27,7 +26,6 @@ export interface TabRuntimeControls {
|
|
|
27
26
|
|
|
28
27
|
export function useBackendRuntime({
|
|
29
28
|
activeTabId,
|
|
30
|
-
activeTabScrollIntentRef,
|
|
31
29
|
backend,
|
|
32
30
|
currentSessionId,
|
|
33
31
|
currentSessionWorkspaceSnapshot,
|
|
@@ -61,11 +59,10 @@ export function useBackendRuntime({
|
|
|
61
59
|
return
|
|
62
60
|
}
|
|
63
61
|
|
|
62
|
+
// The backend owns each tab's scroll position; a backgrounded tab keeps it
|
|
63
|
+
// in its own emulator, so switching back needs no re-anchor from here.
|
|
64
64
|
backend.setActiveTab(activeTabId)
|
|
65
|
-
|
|
66
|
-
backend.reapplyScrollIntent(activeTabId, activeTabScrollIntentRef.current)
|
|
67
|
-
}
|
|
68
|
-
}, [activeTabId, activeTabScrollIntentRef, backend, currentSessionId])
|
|
65
|
+
}, [activeTabId, backend, currentSessionId])
|
|
69
66
|
|
|
70
67
|
useEffect(() => {
|
|
71
68
|
return bindBackendRuntimeEvents({
|
|
@@ -60,6 +60,16 @@ interface MultiClickDragState {
|
|
|
60
60
|
capturedLines: Map<number, TerminalLine>
|
|
61
61
|
autoScrollTimer: ReturnType<typeof setInterval> | null
|
|
62
62
|
autoScrollDir: -1 | 0 | 1
|
|
63
|
+
// A scroll command was sent to the backend and we are still waiting for the
|
|
64
|
+
// snapshot it produces. Gates the next auto-scroll tick so the backend's
|
|
65
|
+
// viewportY can never run ahead of the frontend mirror (which all the
|
|
66
|
+
// selection math reads) — the source of the copy-offset drift.
|
|
67
|
+
autoScrollPending: boolean
|
|
68
|
+
// The drag tab's viewport object last handled by the snapshot effect. Used
|
|
69
|
+
// to ignore snapshot churn from *other* tabs: updateTab keeps unchanged tabs
|
|
70
|
+
// by reference, so an unrelated tab's output must not clear autoScrollPending
|
|
71
|
+
// or re-extend the selection.
|
|
72
|
+
lastProcessedViewport: TabSession['viewport']
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
interface UseMouseHandlersOptions {
|
|
@@ -161,6 +171,7 @@ export function useMouseHandlers({
|
|
|
161
171
|
}
|
|
162
172
|
drag.autoScrollTimer = null
|
|
163
173
|
drag.autoScrollDir = 0
|
|
174
|
+
drag.autoScrollPending = false
|
|
164
175
|
}
|
|
165
176
|
|
|
166
177
|
const canScroll = (tab: TabSession | undefined, dir: -1 | 1): boolean => {
|
|
@@ -174,14 +185,20 @@ export function useMouseHandlers({
|
|
|
174
185
|
clearAutoScroll(drag)
|
|
175
186
|
drag.autoScrollDir = dir
|
|
176
187
|
drag.autoScrollTimer = setInterval(() => {
|
|
188
|
+
// Wait for the previous scroll's snapshot before issuing the next one, so
|
|
189
|
+
// the backend viewportY stays in lockstep with the frontend mirror.
|
|
190
|
+
if (drag.autoScrollPending) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
177
193
|
const tab = stateRef.current.tabs.find((t: TabSession) => t.id === drag.tabId)
|
|
178
194
|
if (!canScroll(tab, dir)) {
|
|
179
195
|
clearAutoScroll(drag)
|
|
180
196
|
return
|
|
181
197
|
}
|
|
198
|
+
drag.autoScrollPending = true
|
|
182
199
|
backend.scrollViewport(drag.tabId, dir)
|
|
183
200
|
// The snapshot dispatch will trigger the viewport-change effect below,
|
|
184
|
-
// which recaptures lines and re-extends
|
|
201
|
+
// which clears autoScrollPending, recaptures lines, and re-extends focus.
|
|
185
202
|
}, AUTO_SCROLL_INTERVAL_MS)
|
|
186
203
|
}
|
|
187
204
|
|
|
@@ -216,6 +233,16 @@ export function useMouseHandlers({
|
|
|
216
233
|
const tab = stateRef.current.tabs.find((t: TabSession) => t.id === drag.tabId)
|
|
217
234
|
if (!tab?.viewport) return
|
|
218
235
|
|
|
236
|
+
// The effect fires on any tab's snapshot (state.tabs is a fresh array), but
|
|
237
|
+
// the drag tab's viewport object only changes when *its* snapshot lands.
|
|
238
|
+
// Ignore churn from other tabs so the auto-scroll lockstep below stays 1:1.
|
|
239
|
+
if (tab.viewport === drag.lastProcessedViewport) return
|
|
240
|
+
drag.lastProcessedViewport = tab.viewport
|
|
241
|
+
|
|
242
|
+
// A fresh snapshot for the drag tab landed: the backend has caught up, so
|
|
243
|
+
// the auto-scroll loop may issue its next step.
|
|
244
|
+
drag.autoScrollPending = false
|
|
245
|
+
|
|
219
246
|
captureViewportLines(drag, tab)
|
|
220
247
|
|
|
221
248
|
const viewportRows = tab.viewport.lines.length
|
|
@@ -425,6 +452,7 @@ export function useMouseHandlers({
|
|
|
425
452
|
anchorEndCol: selection.endCol,
|
|
426
453
|
anchorStartCol: selection.startCol,
|
|
427
454
|
autoScrollDir: 0,
|
|
455
|
+
autoScrollPending: false,
|
|
428
456
|
autoScrollTimer: null,
|
|
429
457
|
baseX: anchor.baseX,
|
|
430
458
|
baseY: anchor.baseY,
|
|
@@ -433,6 +461,7 @@ export function useMouseHandlers({
|
|
|
433
461
|
focusCol: selection.endCol,
|
|
434
462
|
lastEventScreenX: event.x,
|
|
435
463
|
lastEventScreenY: event.y,
|
|
464
|
+
lastProcessedViewport: tab.viewport,
|
|
436
465
|
mode: selection.mode,
|
|
437
466
|
tabId: targetTabId,
|
|
438
467
|
target: selection.target,
|
|
@@ -121,15 +121,7 @@ export function useRendererBindings({
|
|
|
121
121
|
cwd,
|
|
122
122
|
now,
|
|
123
123
|
})
|
|
124
|
-
writeMacroExpansionToTab(
|
|
125
|
-
backend,
|
|
126
|
-
tabId,
|
|
127
|
-
tab,
|
|
128
|
-
match.triggerText.length,
|
|
129
|
-
text,
|
|
130
|
-
cursorOffset,
|
|
131
|
-
dispatch
|
|
132
|
-
)
|
|
124
|
+
writeMacroExpansionToTab(backend, tabId, tab, match.triggerText.length, text, cursorOffset)
|
|
133
125
|
registerUndo(text, cursorOffset)
|
|
134
126
|
return
|
|
135
127
|
}
|
|
@@ -154,7 +146,7 @@ export function useRendererBindings({
|
|
|
154
146
|
now,
|
|
155
147
|
})
|
|
156
148
|
// Trigger already erased above; eraseCount = 0 here.
|
|
157
|
-
writeMacroExpansionToTab(backend, tabId, tab, 0, text, cursorOffset
|
|
149
|
+
writeMacroExpansionToTab(backend, tabId, tab, 0, text, cursorOffset)
|
|
158
150
|
registerUndo(text, cursorOffset)
|
|
159
151
|
} finally {
|
|
160
152
|
inFlightAsyncExpansionRef.current.delete(tabId)
|
|
@@ -194,7 +186,7 @@ export function useRendererBindings({
|
|
|
194
186
|
resetTrigger: (tabId) => triggerDetectorsRef.current.get(tabId)?.reset(),
|
|
195
187
|
tryConsumeMacroUndo,
|
|
196
188
|
writeToPty: (tabId, data, options) =>
|
|
197
|
-
writeToTab(backend, tabId, activeTabRef.current, data,
|
|
189
|
+
writeToTab(backend, tabId, activeTabRef.current, data, options),
|
|
198
190
|
})
|
|
199
191
|
|
|
200
192
|
const handlePasteEvent = (event: { bytes: Uint8Array; defaultPrevented?: boolean }) => {
|
|
@@ -230,7 +222,7 @@ export function useRendererBindings({
|
|
|
230
222
|
return
|
|
231
223
|
}
|
|
232
224
|
|
|
233
|
-
writePasteToTab(backend, tabId, tab, payload
|
|
225
|
+
writePasteToTab(backend, tabId, tab, payload)
|
|
234
226
|
}
|
|
235
227
|
|
|
236
228
|
const handleSelection = (selection: OtuiSelection) => {
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
12
12
|
import type { SessionBackend } from '../session-backend/types'
|
|
13
|
-
import type { AppAction, AppState
|
|
13
|
+
import type { AppAction, AppState } from '../state/types'
|
|
14
14
|
import type { MeasuredPaneRect } from './use-pane-size-report'
|
|
15
15
|
|
|
16
16
|
import { getGitPaneWidthFromRatio } from '../state/git-pane-sizing'
|
|
@@ -44,7 +44,6 @@ function resizeSplitTabs(
|
|
|
44
44
|
tabIds: string[],
|
|
45
45
|
cols: number,
|
|
46
46
|
rows: number,
|
|
47
|
-
intents: Map<string, ScrollIntent>,
|
|
48
47
|
options?: { sync?: boolean }
|
|
49
48
|
): void {
|
|
50
49
|
const bounds = getTerminalBounds(cols, rows)
|
|
@@ -52,13 +51,13 @@ function resizeSplitTabs(
|
|
|
52
51
|
|
|
53
52
|
forEachSplitPaneRect(Object.values(layoutTrees), bounds, (tabId, rect) => {
|
|
54
53
|
const size = toTerminalContentSize(rect)
|
|
55
|
-
backend.resizeTab(tabId, size.cols, size.rows,
|
|
54
|
+
backend.resizeTab(tabId, size.cols, size.rows, options)
|
|
56
55
|
resizedTabIds.add(tabId)
|
|
57
56
|
})
|
|
58
57
|
|
|
59
58
|
for (const id of tabIds) {
|
|
60
59
|
if (!resizedTabIds.has(id)) {
|
|
61
|
-
backend.resizeTab(id, cols, rows,
|
|
60
|
+
backend.resizeTab(id, cols, rows, options)
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
}
|
|
@@ -81,7 +80,6 @@ interface RunResizeCascadeArgs {
|
|
|
81
80
|
rows: number
|
|
82
81
|
layoutTrees: AppState['layoutTrees']
|
|
83
82
|
stableTabIds: string[]
|
|
84
|
-
intents: Map<string, ScrollIntent>
|
|
85
83
|
sync: boolean
|
|
86
84
|
}
|
|
87
85
|
|
|
@@ -89,7 +87,6 @@ function runResizeCascade({
|
|
|
89
87
|
backend,
|
|
90
88
|
cols,
|
|
91
89
|
dispatch,
|
|
92
|
-
intents,
|
|
93
90
|
layoutTrees,
|
|
94
91
|
resizingRef,
|
|
95
92
|
resizingTimerRef,
|
|
@@ -107,9 +104,9 @@ function runResizeCascade({
|
|
|
107
104
|
clearTimeout(resizingTimerRef.current)
|
|
108
105
|
}
|
|
109
106
|
if (hasSplits) {
|
|
110
|
-
resizeSplitTabs(backend, layoutTrees, stableTabIds, cols, rows,
|
|
107
|
+
resizeSplitTabs(backend, layoutTrees, stableTabIds, cols, rows, options)
|
|
111
108
|
} else {
|
|
112
|
-
backend.resizeAll(cols, rows,
|
|
109
|
+
backend.resizeAll(cols, rows, options)
|
|
113
110
|
}
|
|
114
111
|
resizingTimerRef.current = setTimeout(() => {
|
|
115
112
|
resizingRef.current = false
|
|
@@ -133,7 +130,6 @@ export function useTerminalResize({
|
|
|
133
130
|
}: UseTerminalResizeOptions) {
|
|
134
131
|
const resizingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
135
132
|
const tabIdsRef = useRef<string[]>([])
|
|
136
|
-
const intentsRef = useRef<Map<string, ScrollIntent>>(new Map())
|
|
137
133
|
const handledBySyncRef = useRef(false)
|
|
138
134
|
const sidebarMountedRef = useRef(false)
|
|
139
135
|
|
|
@@ -146,12 +142,6 @@ export function useTerminalResize({
|
|
|
146
142
|
}
|
|
147
143
|
const stableTabIds = tabIdsRef.current
|
|
148
144
|
|
|
149
|
-
intentsRef.current = new Map(
|
|
150
|
-
state.tabs
|
|
151
|
-
.filter((t): t is typeof t & { scrollIntent: ScrollIntent } => t.scrollIntent !== undefined)
|
|
152
|
-
.map((t) => [t.id, t.scrollIntent])
|
|
153
|
-
)
|
|
154
|
-
|
|
155
145
|
const activeTabIdRef = useRef(state.activeTabId)
|
|
156
146
|
activeTabIdRef.current = state.activeTabId
|
|
157
147
|
// Last size we pushed to the backend per tab, used to dedupe the measurement
|
|
@@ -176,7 +166,7 @@ export function useTerminalResize({
|
|
|
176
166
|
return
|
|
177
167
|
}
|
|
178
168
|
measuredRef.current.set(tabId, { cols, rows })
|
|
179
|
-
backend.resizeTab(tabId, cols, rows
|
|
169
|
+
backend.resizeTab(tabId, cols, rows)
|
|
180
170
|
},
|
|
181
171
|
[backend, contentOriginRef]
|
|
182
172
|
)
|
|
@@ -230,7 +220,6 @@ export function useTerminalResize({
|
|
|
230
220
|
backend,
|
|
231
221
|
cols: terminalSize.cols,
|
|
232
222
|
dispatch,
|
|
233
|
-
intents: intentsRef.current,
|
|
234
223
|
layoutTrees: state.layoutTrees,
|
|
235
224
|
resizingRef,
|
|
236
225
|
resizingTimerRef,
|
|
@@ -259,7 +248,6 @@ export function useTerminalResize({
|
|
|
259
248
|
backend,
|
|
260
249
|
cols: terminalSize.cols,
|
|
261
250
|
dispatch,
|
|
262
|
-
intents: intentsRef.current,
|
|
263
251
|
layoutTrees: state.layoutTrees,
|
|
264
252
|
resizingRef,
|
|
265
253
|
resizingTimerRef,
|
package/src/app.tsx
CHANGED
|
@@ -258,8 +258,6 @@ export function App({
|
|
|
258
258
|
activeTabIdRef.current = state.activeTabId
|
|
259
259
|
const activeTabRef = useRef(activeTab)
|
|
260
260
|
activeTabRef.current = activeTab
|
|
261
|
-
const activeTabScrollIntentRef = useRef(activeTab?.scrollIntent ?? null)
|
|
262
|
-
activeTabScrollIntentRef.current = activeTab?.scrollIntent ?? null
|
|
263
261
|
|
|
264
262
|
const stateRef = useRef(state)
|
|
265
263
|
stateRef.current = state
|
|
@@ -307,7 +305,6 @@ export function App({
|
|
|
307
305
|
|
|
308
306
|
const { clearIdleTimer, clearStartupGrace, startStartupGrace } = useBackendRuntime({
|
|
309
307
|
activeTabId: state.activeTabId,
|
|
310
|
-
activeTabScrollIntentRef,
|
|
311
308
|
backend,
|
|
312
309
|
currentSessionId: state.currentSessionId,
|
|
313
310
|
currentSessionWorkspaceSnapshot,
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -472,12 +472,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
472
472
|
case 'resizeClient': {
|
|
473
473
|
const sessionId = requireSession(socket, attachedSessions)
|
|
474
474
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
475
|
-
await manager.resize(
|
|
476
|
-
sessionId,
|
|
477
|
-
message.payload.cols,
|
|
478
|
-
message.payload.rows,
|
|
479
|
-
message.payload.intents
|
|
480
|
-
)
|
|
475
|
+
await manager.resize(sessionId, message.payload.cols, message.payload.rows)
|
|
481
476
|
sendOk(socket, message.id)
|
|
482
477
|
break
|
|
483
478
|
}
|
|
@@ -488,8 +483,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
488
483
|
sessionId,
|
|
489
484
|
message.payload.tabId,
|
|
490
485
|
message.payload.cols,
|
|
491
|
-
message.payload.rows
|
|
492
|
-
message.payload.intent
|
|
486
|
+
message.payload.rows
|
|
493
487
|
)
|
|
494
488
|
sendOk(socket, message.id)
|
|
495
489
|
break
|
|
@@ -508,17 +502,6 @@ export async function runDaemon(): Promise<void> {
|
|
|
508
502
|
sendOk(socket, message.id)
|
|
509
503
|
break
|
|
510
504
|
}
|
|
511
|
-
case 'reapplyScrollIntent': {
|
|
512
|
-
const sessionId = requireSession(socket, attachedSessions)
|
|
513
|
-
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
514
|
-
await manager.reapplyScrollIntent(
|
|
515
|
-
sessionId,
|
|
516
|
-
message.payload.tabId,
|
|
517
|
-
message.payload.intent
|
|
518
|
-
)
|
|
519
|
-
sendOk(socket, message.id)
|
|
520
|
-
break
|
|
521
|
-
}
|
|
522
505
|
case 'setActiveTab': {
|
|
523
506
|
const sessionId = requireSession(socket, attachedSessions)
|
|
524
507
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
ScrollIntent,
|
|
5
4
|
TabSession,
|
|
6
5
|
TerminalModeState,
|
|
7
6
|
TerminalSnapshot,
|
|
@@ -68,14 +67,8 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
68
67
|
this.getOrCreateRegistry(sessionId).write(tabId, data)
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
resize(
|
|
72
|
-
sessionId
|
|
73
|
-
cols: number,
|
|
74
|
-
rows: number,
|
|
75
|
-
intents?: Map<string, ScrollIntent>,
|
|
76
|
-
options?: { sync?: boolean }
|
|
77
|
-
): void {
|
|
78
|
-
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, intents, options)
|
|
70
|
+
resize(sessionId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
71
|
+
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, options)
|
|
79
72
|
}
|
|
80
73
|
|
|
81
74
|
resizeTab(
|
|
@@ -83,10 +76,9 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
83
76
|
tabId: string,
|
|
84
77
|
cols: number,
|
|
85
78
|
rows: number,
|
|
86
|
-
intent?: ScrollIntent,
|
|
87
79
|
options?: { sync?: boolean }
|
|
88
80
|
): void {
|
|
89
|
-
this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows,
|
|
81
|
+
this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, options)
|
|
90
82
|
}
|
|
91
83
|
|
|
92
84
|
scroll(sessionId: string, tabId: string, deltaLines: number): void {
|
|
@@ -97,10 +89,6 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
97
89
|
this.getOrCreateRegistry(sessionId).scrollViewportToBottom(tabId)
|
|
98
90
|
}
|
|
99
91
|
|
|
100
|
-
reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): void {
|
|
101
|
-
this.getOrCreateRegistry(sessionId).reapplyScrollIntent(tabId, intent)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
92
|
setActiveTab(sessionId: string, tabId: string | null): void {
|
|
105
93
|
this.getOrCreateRegistry(sessionId).setActiveTab(tabId)
|
|
106
94
|
}
|