@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.
- package/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +12 -14
- 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 +65 -56
- 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 +20 -27
- package/src/daemon/session-manager.ts +3 -15
- package/src/daemon/session-registry.ts +11 -30
- package/src/git/command-queue.ts +18 -2
- 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 +51 -63
- 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/src/ui/components/git/diff-renderer/fold-strip.tsx +30 -23
- package/src/ui/components/git/diff-renderer/split-view.tsx +33 -7
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +23 -3
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +9 -5
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +8 -6
- package/src/ui/components/git/git-panel.tsx +114 -75
- package/src/ui/components/git/git-view.tsx +26 -18
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +10 -10
- package/src/ui/components/layout/session-bar.tsx +134 -116
- package/src/ui/components/layout/sidebar/sidebar.tsx +89 -53
- package/src/ui/components/layout/sidebar/tab-item.tsx +65 -54
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +154 -114
- package/src/ui/components/modals/app/help-modal.tsx +25 -18
- package/src/ui/components/modals/git/git-commit-modal.tsx +15 -8
- package/src/ui/components/modals/sessions/create-session-modal.tsx +13 -9
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +35 -28
- package/src/ui/components/modals/shared/form.tsx +2 -1
- package/src/ui/components/modals/shared/picker.tsx +49 -33
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +42 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +82 -66
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +23 -6
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +11 -6
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/primitives/list-item.tsx +20 -4
- package/src/ui/root.tsx +77 -48
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
-
import type { AssistantId,
|
|
3
|
+
import type { AssistantId, WorkspaceSnapshotV1 } from '../state/types'
|
|
4
4
|
import type { SessionBackend, SessionBackendEvents } from './types'
|
|
5
5
|
|
|
6
6
|
import { SessionManager } from '../daemon/session-manager'
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
getSnapshotTrees,
|
|
13
13
|
toTerminalContentSize,
|
|
14
14
|
} from '../state/layout-resize'
|
|
15
|
-
import { getSnapshotScrollIntents } from '../state/session-persistence'
|
|
16
15
|
|
|
17
16
|
export class LocalSessionBackend
|
|
18
17
|
extends EventEmitter<SessionBackendEvents>
|
|
@@ -67,22 +66,15 @@ export class LocalSessionBackend
|
|
|
67
66
|
})
|
|
68
67
|
this.currentSessionId = options.sessionId
|
|
69
68
|
const trees = getSnapshotTrees(options.workspaceSnapshot)
|
|
70
|
-
const intents = getSnapshotScrollIntents(options.workspaceSnapshot)
|
|
71
69
|
const splitTrees = trees.filter((t) => t.type === 'split')
|
|
72
70
|
if (splitTrees.length > 0) {
|
|
73
71
|
const bounds = createTerminalBounds(options.cols, options.rows)
|
|
74
72
|
forEachSplitPaneRect(splitTrees, bounds, (tabId, rect) => {
|
|
75
73
|
const size = toTerminalContentSize(rect)
|
|
76
|
-
this.sessionManager.resizeTab(
|
|
77
|
-
options.sessionId,
|
|
78
|
-
tabId,
|
|
79
|
-
size.cols,
|
|
80
|
-
size.rows,
|
|
81
|
-
intents.get(tabId)
|
|
82
|
-
)
|
|
74
|
+
this.sessionManager.resizeTab(options.sessionId, tabId, size.cols, size.rows)
|
|
83
75
|
})
|
|
84
76
|
} else {
|
|
85
|
-
this.sessionManager.resize(options.sessionId, options.cols, options.rows
|
|
77
|
+
this.sessionManager.resize(options.sessionId, options.cols, options.rows)
|
|
86
78
|
}
|
|
87
79
|
const attachResult = this.sessionManager.attachSession(
|
|
88
80
|
options.sessionId,
|
|
@@ -149,36 +141,20 @@ export class LocalSessionBackend
|
|
|
149
141
|
this.sessionManager.scrollToBottom(this.currentSessionId, tabId)
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
153
|
-
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
154
|
-
this.sessionManager.reapplyScrollIntent(this.currentSessionId, tabId, intent)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
144
|
setActiveTab(tabId: string | null): void {
|
|
158
145
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
159
146
|
logDebug('backend.local.setActiveTab', { sessionId: this.currentSessionId, tabId })
|
|
160
147
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
161
148
|
}
|
|
162
149
|
|
|
163
|
-
resizeAll(
|
|
164
|
-
cols: number,
|
|
165
|
-
rows: number,
|
|
166
|
-
intents?: Map<string, ScrollIntent>,
|
|
167
|
-
options?: { sync?: boolean }
|
|
168
|
-
): void {
|
|
150
|
+
resizeAll(cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
169
151
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
170
|
-
this.sessionManager.resize(this.currentSessionId, cols, rows,
|
|
152
|
+
this.sessionManager.resize(this.currentSessionId, cols, rows, options)
|
|
171
153
|
}
|
|
172
154
|
|
|
173
|
-
resizeTab(
|
|
174
|
-
tabId: string,
|
|
175
|
-
cols: number,
|
|
176
|
-
rows: number,
|
|
177
|
-
intent?: ScrollIntent,
|
|
178
|
-
options?: { sync?: boolean }
|
|
179
|
-
): void {
|
|
155
|
+
resizeTab(tabId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
180
156
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
181
|
-
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows,
|
|
157
|
+
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, options)
|
|
182
158
|
}
|
|
183
159
|
|
|
184
160
|
disposeSession(tabId: string): void {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
import { connect, type Socket } from 'node:net'
|
|
3
3
|
|
|
4
|
-
import type { AssistantId,
|
|
4
|
+
import type { AssistantId, WorkspaceSnapshotV1 } from '../state/types'
|
|
5
5
|
import type { SessionBackend, SessionBackendEvents } from './types'
|
|
6
6
|
|
|
7
7
|
import { getIpcDaemonSocketPath } from '../daemon/runtime-paths'
|
|
@@ -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
|
-
|
|
356
|
-
(
|
|
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
|
-
|
|
366
|
-
id: crypto.randomUUID(),
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
377
|
-
id: crypto.randomUUID(),
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
388
|
-
id: crypto.randomUUID(),
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
410
|
-
id: crypto.randomUUID(),
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
446
|
-
id: crypto.randomUUID(),
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
457
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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[]
|
package/src/state/types.ts
CHANGED
|
@@ -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
|
|
package/src/state/validation.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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={
|
|
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
|
|