@brimveyn/aimux 1.13.2 → 1.14.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.
- 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 +149 -19
- package/src/app-runtime/snippet-actions.ts +2 -3
- package/src/app-runtime/split-drag-controller.ts +3 -1
- 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 +7 -22
- package/src/app.tsx +0 -6
- package/src/config.ts +1 -12
- 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/git/worktree-branch-poller.ts +54 -0
- package/src/input/modes/types.ts +2 -0
- 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/layout-tree.ts +114 -4
- package/src/state/reducers/session-state.ts +20 -0
- package/src/state/reducers/tab-state.ts +22 -22
- package/src/state/reducers/ui-state.ts +0 -3
- package/src/state/session-persistence.ts +2 -22
- package/src/state/session-worktrees.ts +45 -2
- package/src/state/store.ts +0 -3
- package/src/state/tab-entries.ts +74 -0
- package/src/state/types.ts +4 -15
- package/src/state/validation.ts +0 -8
- package/src/state/workspace-save.ts +0 -1
- package/src/terminal-manager/manager-client.ts +5 -29
- package/src/terminal-manager/terminal-manager.ts +2 -17
- package/src/ui/components/layout/sidebar/sidebar.tsx +9 -340
- package/src/ui/components/layout/sidebar/tab-item.tsx +51 -42
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +10 -53
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +30 -0
- package/src/ui/components/layout/sidebar/workspace-list.tsx +398 -0
- package/src/ui/components/layout/sidebar/worktree-row.tsx +92 -0
- package/src/ui/components/layout/split-layout.tsx +20 -39
- package/src/ui/components/layout/terminal-pane.tsx +30 -0
- package/src/ui/components/layout/top-tab-bar.tsx +304 -0
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +1 -8
- package/src/ui/root.tsx +68 -63
- package/src/ui/components/layout/session-bar.tsx +0 -296
- package/src/ui/components/layout/sidebar/sidebar-group-metadata.ts +0 -44
- package/src/ui/components/layout/sidebar/sidebar-scroll.ts +0 -33
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +0 -36
|
@@ -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
|
)
|
|
@@ -185,8 +175,7 @@ export function useTerminalResize({
|
|
|
185
175
|
const terminalSize = useMemo(() => {
|
|
186
176
|
const sidebarWidth = state.sidebar.visible ? state.sidebar.width : 0
|
|
187
177
|
const sessionBarRows = state.sessionBar.visible ? 1 : 0
|
|
188
|
-
const sessionBarTopOffset =
|
|
189
|
-
state.sessionBar.visible && state.sessionBar.position === 'top' ? 1 : 0
|
|
178
|
+
const sessionBarTopOffset = sessionBarRows
|
|
190
179
|
const reservedRows =
|
|
191
180
|
MAIN_AREA_VERTICAL_PADDING +
|
|
192
181
|
STATUS_BAR_HEIGHT +
|
|
@@ -215,7 +204,6 @@ export function useTerminalResize({
|
|
|
215
204
|
state.sidebar.visible,
|
|
216
205
|
state.sidebar.width,
|
|
217
206
|
state.sessionBar.visible,
|
|
218
|
-
state.sessionBar.position,
|
|
219
207
|
gitPaneInPaneMode,
|
|
220
208
|
state.gitPane.position,
|
|
221
209
|
state.gitPane.paneRatio,
|
|
@@ -230,7 +218,6 @@ export function useTerminalResize({
|
|
|
230
218
|
backend,
|
|
231
219
|
cols: terminalSize.cols,
|
|
232
220
|
dispatch,
|
|
233
|
-
intents: intentsRef.current,
|
|
234
221
|
layoutTrees: state.layoutTrees,
|
|
235
222
|
resizingRef,
|
|
236
223
|
resizingTimerRef,
|
|
@@ -244,7 +231,6 @@ export function useTerminalResize({
|
|
|
244
231
|
state.sidebar.visible,
|
|
245
232
|
state.sidebar.width,
|
|
246
233
|
state.sessionBar.visible,
|
|
247
|
-
state.sessionBar.position,
|
|
248
234
|
gitPaneInPaneMode,
|
|
249
235
|
state.gitPane.position,
|
|
250
236
|
state.gitPane.paneRatio,
|
|
@@ -259,7 +245,6 @@ export function useTerminalResize({
|
|
|
259
245
|
backend,
|
|
260
246
|
cols: terminalSize.cols,
|
|
261
247
|
dispatch,
|
|
262
|
-
intents: intentsRef.current,
|
|
263
248
|
layoutTrees: state.layoutTrees,
|
|
264
249
|
resizingRef,
|
|
265
250
|
resizingTimerRef,
|
package/src/app.tsx
CHANGED
|
@@ -100,8 +100,6 @@ export function App({
|
|
|
100
100
|
const json = loadConfig()
|
|
101
101
|
const sessionBarVisible =
|
|
102
102
|
resolvedConfig.sessionBar?.initialVisible ?? json.sessionBarVisible ?? true
|
|
103
|
-
const sessionBarPosition =
|
|
104
|
-
resolvedConfig.sessionBar?.initialPosition ?? json.sessionBarPosition ?? 'top'
|
|
105
103
|
const sidebarOverrides = json.sidebar
|
|
106
104
|
|
|
107
105
|
// Merge config-file gitPane (persisted prefs) with user's resolved gitPane
|
|
@@ -149,7 +147,6 @@ export function App({
|
|
|
149
147
|
sessionCatalog.length === 0,
|
|
150
148
|
{
|
|
151
149
|
gitPane: gitPaneOverrides,
|
|
152
|
-
sessionBarPosition,
|
|
153
150
|
sessionBarVisible,
|
|
154
151
|
sidebar: sidebarOverrides,
|
|
155
152
|
}
|
|
@@ -258,8 +255,6 @@ export function App({
|
|
|
258
255
|
activeTabIdRef.current = state.activeTabId
|
|
259
256
|
const activeTabRef = useRef(activeTab)
|
|
260
257
|
activeTabRef.current = activeTab
|
|
261
|
-
const activeTabScrollIntentRef = useRef(activeTab?.scrollIntent ?? null)
|
|
262
|
-
activeTabScrollIntentRef.current = activeTab?.scrollIntent ?? null
|
|
263
258
|
|
|
264
259
|
const stateRef = useRef(state)
|
|
265
260
|
stateRef.current = state
|
|
@@ -307,7 +302,6 @@ export function App({
|
|
|
307
302
|
|
|
308
303
|
const { clearIdleTimer, clearStartupGrace, startStartupGrace } = useBackendRuntime({
|
|
309
304
|
activeTabId: state.activeTabId,
|
|
310
|
-
activeTabScrollIntentRef,
|
|
311
305
|
backend,
|
|
312
306
|
currentSessionId: state.currentSessionId,
|
|
313
307
|
currentSessionWorkspaceSnapshot,
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
-
import type { GitFileListMode,
|
|
3
|
+
import type { GitFileListMode, WorkspaceSnapshotV1 } from './state/types'
|
|
4
4
|
|
|
5
5
|
import { logDebug } from './debug/input-log'
|
|
6
6
|
import { getProfileConfigDir } from './profile-paths'
|
|
@@ -41,7 +41,6 @@ export interface AimuxConfig {
|
|
|
41
41
|
gitPane?: PersistedGitPane
|
|
42
42
|
sidebar?: PersistedSidebar
|
|
43
43
|
sessionBarVisible?: boolean
|
|
44
|
-
sessionBarPosition?: SessionBarPosition
|
|
45
44
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
46
45
|
skippedUpdateVersion?: string
|
|
47
46
|
}
|
|
@@ -161,7 +160,6 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
161
160
|
gitPanelVisible?: unknown
|
|
162
161
|
gitPanelRatio?: unknown
|
|
163
162
|
sessionBarVisible?: unknown
|
|
164
|
-
sessionBarPosition?: unknown
|
|
165
163
|
workspaceSnapshot?: unknown
|
|
166
164
|
skippedUpdateVersion?: unknown
|
|
167
165
|
}
|
|
@@ -233,14 +231,6 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
233
231
|
issues.push('ignored invalid sessionBarVisible')
|
|
234
232
|
}
|
|
235
233
|
|
|
236
|
-
const validSessionBarPosition =
|
|
237
|
-
parsed.sessionBarPosition === 'top' || parsed.sessionBarPosition === 'bottom'
|
|
238
|
-
? parsed.sessionBarPosition
|
|
239
|
-
: undefined
|
|
240
|
-
if (parsed.sessionBarPosition !== undefined && validSessionBarPosition === undefined) {
|
|
241
|
-
issues.push('ignored invalid sessionBarPosition')
|
|
242
|
-
}
|
|
243
|
-
|
|
244
234
|
if (
|
|
245
235
|
parsed.workspaceSnapshot !== undefined &&
|
|
246
236
|
!isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
|
|
@@ -264,7 +254,6 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
264
254
|
config: {
|
|
265
255
|
customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
|
|
266
256
|
gitPane: validGitPane,
|
|
267
|
-
sessionBarPosition: validSessionBarPosition,
|
|
268
257
|
sessionBarVisible: validSessionBarVisible,
|
|
269
258
|
sidebar: validSidebar,
|
|
270
259
|
skippedUpdateVersion: validSkippedUpdateVersion,
|
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
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
TabSession,
|
|
5
|
+
TerminalModeState,
|
|
6
|
+
TerminalSnapshot,
|
|
7
|
+
WorkspaceSnapshotV1,
|
|
8
|
+
} from '../state/types'
|
|
9
|
+
|
|
3
10
|
import { logDebug } from '../debug/input-log'
|
|
4
11
|
import { PtyManager } from '../pty/pty-manager'
|
|
5
12
|
import {
|
|
@@ -8,14 +15,6 @@ import {
|
|
|
8
15
|
restoreTabsFromWorkspace,
|
|
9
16
|
} from '../state/session-persistence'
|
|
10
17
|
import { createDefaultTerminalModes } from '../state/terminal-modes'
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_SCROLL_INTENT,
|
|
13
|
-
type ScrollIntent,
|
|
14
|
-
type TabSession,
|
|
15
|
-
type TerminalModeState,
|
|
16
|
-
type TerminalSnapshot,
|
|
17
|
-
type WorkspaceSnapshotV1,
|
|
18
|
-
} from '../state/types'
|
|
19
18
|
|
|
20
19
|
interface SessionRegistryEvents {
|
|
21
20
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
@@ -91,7 +90,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
91
90
|
const existing = this.tabs.get(persisted.id)
|
|
92
91
|
if (existing) {
|
|
93
92
|
existing.title = persisted.title
|
|
94
|
-
existing.scrollIntent = persisted.scrollIntent ?? DEFAULT_SCROLL_INTENT
|
|
95
93
|
existing.worktreeId = persisted.worktreeId
|
|
96
94
|
}
|
|
97
95
|
}
|
|
@@ -147,7 +145,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
147
145
|
buffer: '',
|
|
148
146
|
command: [options.command, ...(options.args ?? [])].join(' '),
|
|
149
147
|
id: options.tabId,
|
|
150
|
-
scrollIntent: DEFAULT_SCROLL_INTENT,
|
|
151
148
|
status: 'starting',
|
|
152
149
|
terminalModes: createDefaultTerminalModes(),
|
|
153
150
|
title: options.title,
|
|
@@ -159,7 +156,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
159
156
|
existing.exitCode = undefined
|
|
160
157
|
existing.viewport = undefined
|
|
161
158
|
existing.terminalModes = createDefaultTerminalModes()
|
|
162
|
-
existing.scrollIntent = DEFAULT_SCROLL_INTENT
|
|
163
159
|
existing.assistant = options.assistant
|
|
164
160
|
existing.title = options.title
|
|
165
161
|
existing.command = [options.command, ...(options.args ?? [])].join(' ')
|
|
@@ -173,23 +169,12 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
173
169
|
this.ptyManager.write(tabId, data)
|
|
174
170
|
}
|
|
175
171
|
|
|
176
|
-
resizeAll(
|
|
177
|
-
cols
|
|
178
|
-
rows: number,
|
|
179
|
-
intents?: Map<string, ScrollIntent>,
|
|
180
|
-
options?: { sync?: boolean }
|
|
181
|
-
): void {
|
|
182
|
-
this.ptyManager.resizeAll(cols, rows, intents, options)
|
|
172
|
+
resizeAll(cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
173
|
+
this.ptyManager.resizeAll(cols, rows, options)
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
resizeTab(
|
|
186
|
-
tabId
|
|
187
|
-
cols: number,
|
|
188
|
-
rows: number,
|
|
189
|
-
intent?: ScrollIntent,
|
|
190
|
-
options?: { sync?: boolean }
|
|
191
|
-
): void {
|
|
192
|
-
this.ptyManager.resizeSession(tabId, cols, rows, intent, options)
|
|
176
|
+
resizeTab(tabId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
|
|
177
|
+
this.ptyManager.resizeSession(tabId, cols, rows, options)
|
|
193
178
|
}
|
|
194
179
|
|
|
195
180
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
@@ -200,10 +185,6 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
200
185
|
this.ptyManager.scrollViewportToBottom(tabId)
|
|
201
186
|
}
|
|
202
187
|
|
|
203
|
-
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
204
|
-
this.ptyManager.reapplyScrollIntent(tabId, intent)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
188
|
setActiveTab(tabId: string | null): void {
|
|
208
189
|
if (tabId === null) {
|
|
209
190
|
this.activeTabId = null
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
import { appStore } from '../state/app-store'
|
|
4
|
+
import { dispatchGlobal } from '../state/dispatch-ref'
|
|
5
|
+
import { getCurrentBranch } from '../ui/git-branch'
|
|
6
|
+
|
|
7
|
+
const INTERVAL_MS = 4000
|
|
8
|
+
|
|
9
|
+
// Single source of truth for "what git branch is each worktree on right now".
|
|
10
|
+
// Polls every known worktree's path and dispatches update-worktree-record
|
|
11
|
+
// when the live branch differs from the stored one. Components read
|
|
12
|
+
// `worktree.branch` from state — no per-component polling, no flickers.
|
|
13
|
+
//
|
|
14
|
+
// Runs once when enabled; reads sessions from the store on each tick so
|
|
15
|
+
// session updates do NOT re-create the effect (which would otherwise feedback
|
|
16
|
+
// off our own dispatches).
|
|
17
|
+
export function useWorktreeBranchPolling(enabled: boolean): void {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!enabled) return
|
|
20
|
+
|
|
21
|
+
let cancelled = false
|
|
22
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
23
|
+
|
|
24
|
+
const tick = async () => {
|
|
25
|
+
const sessions = appStore.getState().sessions
|
|
26
|
+
await Promise.all(
|
|
27
|
+
sessions.flatMap((session) =>
|
|
28
|
+
(session.worktrees ?? []).map(async (worktree) => {
|
|
29
|
+
if (worktree.path == null || worktree.path === '') return
|
|
30
|
+
const branch = await getCurrentBranch(worktree.path)
|
|
31
|
+
if (cancelled) return
|
|
32
|
+
if (branch != null && branch !== worktree.branch) {
|
|
33
|
+
dispatchGlobal({
|
|
34
|
+
patch: { branch },
|
|
35
|
+
sessionId: session.id,
|
|
36
|
+
type: 'update-worktree-record',
|
|
37
|
+
worktreeId: worktree.id,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
if (cancelled) return
|
|
44
|
+
timer = setTimeout(() => void tick(), INTERVAL_MS)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void tick()
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
cancelled = true
|
|
51
|
+
if (timer != null) clearTimeout(timer)
|
|
52
|
+
}
|
|
53
|
+
}, [enabled])
|
|
54
|
+
}
|
package/src/input/modes/types.ts
CHANGED
|
@@ -68,6 +68,8 @@ export type SideEffect =
|
|
|
68
68
|
| { type: 'git-push' }
|
|
69
69
|
| { type: 'confirm-update-selection' }
|
|
70
70
|
| { type: 'switch-session-by-index'; index: number }
|
|
71
|
+
| { type: 'cycle-sidebar-item'; direction: 1 | -1 }
|
|
72
|
+
| { type: 'switch-tab-by-index'; index: number }
|
|
71
73
|
| { type: 'delete-session'; sessionId: string }
|
|
72
74
|
| { type: 'delete-worktree'; sessionId: string; worktreeId: string; force?: boolean }
|
|
73
75
|
| {
|
|
@@ -7,13 +7,12 @@ export function getLineText(line: TerminalLine): string {
|
|
|
7
7
|
/**
|
|
8
8
|
* Extract text from a range of terminal lines as a single string.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* spaces
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* explicitly dragged over are preserved.
|
|
10
|
+
* Trailing `[ \t]+` is stripped from each joined segment to drop the viewport
|
|
11
|
+
* padding the snapshot layer fills blank cells with (every rendered row is now
|
|
12
|
+
* padded to the full terminal width to prevent ghosting). Without this, shell
|
|
13
|
+
* line continuations (`\` + padding spaces + `\n`) paste as escaped-space
|
|
14
|
+
* sequences, and a single-line copy dragged to/past end-of-line would pick up
|
|
15
|
+
* the synthetic padding spaces.
|
|
17
16
|
*/
|
|
18
17
|
export function extractStreamText(
|
|
19
18
|
lines: TerminalLine[],
|
|
@@ -38,7 +37,7 @@ export function extractStreamText(
|
|
|
38
37
|
for (let row = clampedStart; row <= clampedEnd; row++) {
|
|
39
38
|
const text = getLineText(lines[row] as TerminalLine)
|
|
40
39
|
if (row === startRow && row === endRow) {
|
|
41
|
-
parts.push(text.slice(Math.max(0, startCol), Math.max(0, endCol)))
|
|
40
|
+
parts.push(rtrim(text.slice(Math.max(0, startCol), Math.max(0, endCol))))
|
|
42
41
|
} else if (row === startRow) {
|
|
43
42
|
parts.push(rtrim(text.slice(Math.max(0, startCol))))
|
|
44
43
|
} else if (row === endRow) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ScrollIntent,
|
|
3
2
|
TabSession,
|
|
4
3
|
TerminalModeState,
|
|
5
4
|
TerminalSnapshot,
|
|
@@ -14,8 +13,12 @@ import {
|
|
|
14
13
|
negotiateProtocolVersion,
|
|
15
14
|
} from './protocol'
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// v5: backend owns scroll position end to end. Removed the per-tab scroll
|
|
17
|
+
// `intent`/`intents` from resize messages and the `reapplyScrollIntent`
|
|
18
|
+
// message. Min is raised in lockstep so a pre-v5 peer (which could still send
|
|
19
|
+
// the dropped message) can't negotiate a now-incompatible version.
|
|
20
|
+
export const MANAGER_PROTOCOL_MIN_VERSION = 5
|
|
21
|
+
export const MANAGER_PROTOCOL_VERSION = 5
|
|
19
22
|
/**
|
|
20
23
|
* Minimum version required to send `setBroadcastEnabled`. Older TMs (v3) will
|
|
21
24
|
* not understand the message; the daemon must check the negotiated version
|
|
@@ -75,7 +78,6 @@ export type ManagerRequest =
|
|
|
75
78
|
sessionId: string
|
|
76
79
|
cols: number
|
|
77
80
|
rows: number
|
|
78
|
-
intents?: Record<string, ScrollIntent>
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
| {
|
|
@@ -86,7 +88,6 @@ export type ManagerRequest =
|
|
|
86
88
|
tabId: string
|
|
87
89
|
cols: number
|
|
88
90
|
rows: number
|
|
89
|
-
intent?: ScrollIntent
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
| {
|
|
@@ -99,11 +100,6 @@ export type ManagerRequest =
|
|
|
99
100
|
type: 'scroll'
|
|
100
101
|
payload: { sessionId: string; tabId: string; deltaLines: number }
|
|
101
102
|
}
|
|
102
|
-
| {
|
|
103
|
-
id: string
|
|
104
|
-
type: 'reapplyScrollIntent'
|
|
105
|
-
payload: { sessionId: string; tabId: string; intent: ScrollIntent }
|
|
106
|
-
}
|
|
107
103
|
| { id: string; type: 'setActiveTab'; payload: { sessionId: string; tabId: string | null } }
|
|
108
104
|
| { id: string; type: 'closeTab'; payload: { sessionId: string; tabId: string } }
|
|
109
105
|
| { id: string; type: 'disposeSession'; payload: { sessionId: string } }
|
|
@@ -178,18 +174,6 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
|
178
174
|
)
|
|
179
175
|
}
|
|
180
176
|
|
|
181
|
-
function isScrollIntent(value: unknown): value is ScrollIntent {
|
|
182
|
-
if (!isObjectRecord(value)) return false
|
|
183
|
-
if (value.kind === 'bottom') return true
|
|
184
|
-
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
185
|
-
return false
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function isScrollIntentRecord(value: unknown): value is Record<string, ScrollIntent> {
|
|
189
|
-
if (!isObjectRecord(value)) return false
|
|
190
|
-
return Object.values(value).every(isScrollIntent)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
177
|
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
194
178
|
return (
|
|
195
179
|
isObjectRecord(value) &&
|
|
@@ -303,20 +287,12 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
|
303
287
|
assert(isString(value.payload.sessionId), 'resizeClient.sessionId must be a string')
|
|
304
288
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
305
289
|
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
306
|
-
assert(
|
|
307
|
-
value.payload.intents === undefined || isScrollIntentRecord(value.payload.intents),
|
|
308
|
-
'resizeClient.intents must be a scroll-intent record'
|
|
309
|
-
)
|
|
310
290
|
return value as ManagerRequest
|
|
311
291
|
case 'resizeTab':
|
|
312
292
|
assert(isString(value.payload.sessionId), 'resizeTab.sessionId must be a string')
|
|
313
293
|
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
314
294
|
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
315
295
|
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
316
|
-
assert(
|
|
317
|
-
value.payload.intent === undefined || isScrollIntent(value.payload.intent),
|
|
318
|
-
'resizeTab.intent must be a scroll intent'
|
|
319
|
-
)
|
|
320
296
|
return value as ManagerRequest
|
|
321
297
|
case 'scroll':
|
|
322
298
|
assert(isString(value.payload.sessionId), 'scroll.sessionId must be a string')
|
|
@@ -327,14 +303,6 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
|
327
303
|
assert(isString(value.payload.sessionId), 'scrollToBottom.sessionId must be a string')
|
|
328
304
|
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
329
305
|
return value as ManagerRequest
|
|
330
|
-
case 'reapplyScrollIntent':
|
|
331
|
-
assert(isString(value.payload.sessionId), 'reapplyScrollIntent.sessionId must be a string')
|
|
332
|
-
assert(isString(value.payload.tabId), 'reapplyScrollIntent.tabId must be a string')
|
|
333
|
-
assert(
|
|
334
|
-
isScrollIntent(value.payload.intent),
|
|
335
|
-
'reapplyScrollIntent.intent must be a scroll intent'
|
|
336
|
-
)
|
|
337
|
-
return value as ManagerRequest
|
|
338
306
|
case 'setActiveTab':
|
|
339
307
|
assert(isString(value.payload.sessionId), 'setActiveTab.sessionId must be a string')
|
|
340
308
|
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
ScrollIntent,
|
|
3
2
|
SessionStatus,
|
|
4
3
|
TabActivity,
|
|
5
4
|
TabSession,
|
|
@@ -10,8 +9,13 @@ import type {
|
|
|
10
9
|
|
|
11
10
|
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// v9: scroll position is owned entirely by the backend emulator. Dropped the
|
|
13
|
+
// per-tab scroll `intent`/`intents` from resize messages and removed the
|
|
14
|
+
// `reapplyScrollIntent` message — the frontend no longer derives or sends it.
|
|
15
|
+
// (v8 was the unfilled-viewport status-detector change; this is a further
|
|
16
|
+
// breaking wire change, so the version steps again.)
|
|
17
|
+
export const IPC_PROTOCOL_MIN_VERSION = 9
|
|
18
|
+
export const IPC_PROTOCOL_VERSION = 9
|
|
15
19
|
|
|
16
20
|
export interface ProtocolHelloRequest {
|
|
17
21
|
minVersion: number
|
|
@@ -67,20 +71,15 @@ export type ClientRequest =
|
|
|
67
71
|
| {
|
|
68
72
|
id: string
|
|
69
73
|
type: 'resizeClient'
|
|
70
|
-
payload: { cols: number; rows: number
|
|
74
|
+
payload: { cols: number; rows: number }
|
|
71
75
|
}
|
|
72
76
|
| {
|
|
73
77
|
id: string
|
|
74
78
|
type: 'resizeTab'
|
|
75
|
-
payload: { tabId: string; cols: number; rows: number
|
|
79
|
+
payload: { tabId: string; cols: number; rows: number }
|
|
76
80
|
}
|
|
77
81
|
| { id: string; type: 'scrollToBottom'; payload: { tabId: string } }
|
|
78
82
|
| { id: string; type: 'scroll'; payload: { tabId: string; deltaLines: number } }
|
|
79
|
-
| {
|
|
80
|
-
id: string
|
|
81
|
-
type: 'reapplyScrollIntent'
|
|
82
|
-
payload: { tabId: string; intent: ScrollIntent }
|
|
83
|
-
}
|
|
84
83
|
| { id: string; type: 'setActiveTab'; payload: { tabId: string | null } }
|
|
85
84
|
| { id: string; type: 'closeTab'; payload: { tabId: string } }
|
|
86
85
|
| { id: string; type: 'disposeAll'; payload: Record<string, never> }
|
|
@@ -180,18 +179,6 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
|
180
179
|
)
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
function isScrollIntent(value: unknown): value is ScrollIntent {
|
|
184
|
-
if (!isObjectRecord(value)) return false
|
|
185
|
-
if (value.kind === 'bottom') return true
|
|
186
|
-
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
187
|
-
return false
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function isScrollIntentRecord(value: unknown): value is Record<string, ScrollIntent> {
|
|
191
|
-
if (!isObjectRecord(value)) return false
|
|
192
|
-
return Object.values(value).every(isScrollIntent)
|
|
193
|
-
}
|
|
194
|
-
|
|
195
182
|
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
196
183
|
return (
|
|
197
184
|
isObjectRecord(value) &&
|
|
@@ -328,19 +315,11 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
328
315
|
case 'resizeClient':
|
|
329
316
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
330
317
|
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
331
|
-
assert(
|
|
332
|
-
value.payload.intents === undefined || isScrollIntentRecord(value.payload.intents),
|
|
333
|
-
'resizeClient.intents must be a scroll-intent record'
|
|
334
|
-
)
|
|
335
318
|
return value as ClientRequest
|
|
336
319
|
case 'resizeTab':
|
|
337
320
|
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
338
321
|
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
339
322
|
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
340
|
-
assert(
|
|
341
|
-
value.payload.intent === undefined || isScrollIntent(value.payload.intent),
|
|
342
|
-
'resizeTab.intent must be a scroll intent'
|
|
343
|
-
)
|
|
344
323
|
return value as ClientRequest
|
|
345
324
|
case 'scroll':
|
|
346
325
|
assert(isString(value.payload.tabId), 'scroll.tabId must be a string')
|
|
@@ -349,13 +328,6 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
349
328
|
case 'scrollToBottom':
|
|
350
329
|
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
351
330
|
return value as ClientRequest
|
|
352
|
-
case 'reapplyScrollIntent':
|
|
353
|
-
assert(isString(value.payload.tabId), 'reapplyScrollIntent.tabId must be a string')
|
|
354
|
-
assert(
|
|
355
|
-
isScrollIntent(value.payload.intent),
|
|
356
|
-
'reapplyScrollIntent.intent must be a scroll intent'
|
|
357
|
-
)
|
|
358
|
-
return value as ClientRequest
|
|
359
331
|
case 'setActiveTab':
|
|
360
332
|
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
|
361
333
|
return value as ClientRequest
|