@brimveyn/aimux 1.20.3 → 1.21.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/side-effects.ts +3 -48
- package/src/app-runtime/split-drag-controller.ts +4 -8
- package/src/app-runtime/use-mouse-handlers.ts +27 -42
- package/src/app-runtime/use-terminal-resize.ts +11 -20
- package/src/app.tsx +9 -37
- package/src/config.ts +98 -8
- package/src/git/pr-merge.ts +64 -0
- package/src/git/pr-status-poller.ts +57 -0
- package/src/git/pr-status.ts +227 -0
- package/src/index.tsx +7 -2
- package/src/platform/clipboard.ts +140 -4
- package/src/platform/open-url.ts +39 -0
- package/src/services/ai-usage/spawn.ts +3 -1
- package/src/state/bars.ts +75 -0
- package/src/state/git-pane-sizing.ts +0 -9
- package/src/state/pr-status-store.ts +39 -0
- package/src/state/reducers/git-panel-state.ts +0 -47
- package/src/state/reducers/ui-state.ts +83 -13
- package/src/state/session-persistence.ts +5 -7
- package/src/state/store.ts +28 -35
- package/src/state/types.ts +24 -19
- package/src/state/workspace-save.ts +5 -7
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
- package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
- package/src/ui/components/git/pane/git-pane-widget.tsx +30 -16
- package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
- package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
- package/src/ui/components/layout/bar.tsx +191 -0
- package/src/ui/components/layout/top-tab-bar.tsx +3 -2
- package/src/ui/root.tsx +25 -109
- package/src/ui/widgets/registry.tsx +18 -0
- package/src/ui/widgets/widget-context-menu.ts +69 -0
- package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
- package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode, Kimi side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"bump:terminal_manager": "bun run scripts/bump-protocol.ts terminal-manager"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@brimveyn/aimux-config": "0.8.
|
|
69
|
+
"@brimveyn/aimux-config": "0.8.6",
|
|
70
70
|
"@opentui/core": "^0.1.90",
|
|
71
71
|
"@opentui/react": "^0.1.90",
|
|
72
72
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -913,62 +913,17 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
913
913
|
}
|
|
914
914
|
case 'persist-git-diff-mode-ratio': {
|
|
915
915
|
const config = loadConfig()
|
|
916
|
-
|
|
917
|
-
const paneRatio = persistedGitPane?.paneRatio ?? persistedGitPane?.ratio ?? 0.5
|
|
918
|
-
const embeddedRatio = persistedGitPane?.embeddedRatio ?? persistedGitPane?.ratio ?? 0.5
|
|
919
|
-
saveConfig({
|
|
920
|
-
...config,
|
|
921
|
-
gitPane: {
|
|
922
|
-
diffModeRatio: effect.ratio,
|
|
923
|
-
embeddedRatio,
|
|
924
|
-
fileListMode: persistedGitPane?.fileListMode,
|
|
925
|
-
mode: persistedGitPane?.mode ?? 'embedded',
|
|
926
|
-
paneRatio,
|
|
927
|
-
position: persistedGitPane?.position ?? 'bottom',
|
|
928
|
-
treeCompaction: persistedGitPane?.treeCompaction,
|
|
929
|
-
visible: persistedGitPane?.visible ?? true,
|
|
930
|
-
},
|
|
931
|
-
})
|
|
916
|
+
saveConfig({ ...config, gitPane: { ...config.gitPane, diffModeRatio: effect.ratio } })
|
|
932
917
|
return
|
|
933
918
|
}
|
|
934
919
|
case 'persist-git-file-list-mode': {
|
|
935
920
|
const config = loadConfig()
|
|
936
|
-
|
|
937
|
-
const paneRatio = persistedGitPane?.paneRatio ?? persistedGitPane?.ratio ?? 0.5
|
|
938
|
-
const embeddedRatio = persistedGitPane?.embeddedRatio ?? persistedGitPane?.ratio ?? 0.5
|
|
939
|
-
saveConfig({
|
|
940
|
-
...config,
|
|
941
|
-
gitPane: {
|
|
942
|
-
diffModeRatio: persistedGitPane?.diffModeRatio,
|
|
943
|
-
embeddedRatio,
|
|
944
|
-
fileListMode: effect.mode,
|
|
945
|
-
mode: persistedGitPane?.mode ?? 'embedded',
|
|
946
|
-
paneRatio,
|
|
947
|
-
position: persistedGitPane?.position ?? 'bottom',
|
|
948
|
-
treeCompaction: persistedGitPane?.treeCompaction,
|
|
949
|
-
visible: persistedGitPane?.visible ?? true,
|
|
950
|
-
},
|
|
951
|
-
})
|
|
921
|
+
saveConfig({ ...config, gitPane: { ...config.gitPane, fileListMode: effect.mode } })
|
|
952
922
|
return
|
|
953
923
|
}
|
|
954
924
|
case 'persist-git-tree-compaction': {
|
|
955
925
|
const config = loadConfig()
|
|
956
|
-
|
|
957
|
-
const paneRatio = persistedGitPane?.paneRatio ?? persistedGitPane?.ratio ?? 0.5
|
|
958
|
-
const embeddedRatio = persistedGitPane?.embeddedRatio ?? persistedGitPane?.ratio ?? 0.5
|
|
959
|
-
saveConfig({
|
|
960
|
-
...config,
|
|
961
|
-
gitPane: {
|
|
962
|
-
diffModeRatio: persistedGitPane?.diffModeRatio,
|
|
963
|
-
embeddedRatio,
|
|
964
|
-
fileListMode: persistedGitPane?.fileListMode,
|
|
965
|
-
mode: persistedGitPane?.mode ?? 'embedded',
|
|
966
|
-
paneRatio,
|
|
967
|
-
position: persistedGitPane?.position ?? 'bottom',
|
|
968
|
-
treeCompaction: effect.enabled,
|
|
969
|
-
visible: persistedGitPane?.visible ?? true,
|
|
970
|
-
},
|
|
971
|
-
})
|
|
926
|
+
saveConfig({ ...config, gitPane: { ...config.gitPane, treeCompaction: effect.enabled } })
|
|
972
927
|
return
|
|
973
928
|
}
|
|
974
929
|
case 'git-stage': {
|
|
@@ -16,8 +16,7 @@ export interface AxisDragState {
|
|
|
16
16
|
screenStart: number
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface
|
|
20
|
-
anchor: 'start' | 'end'
|
|
19
|
+
export interface RatioDragState {
|
|
21
20
|
axis: ScreenAxis
|
|
22
21
|
screenStart: number
|
|
23
22
|
totalSize: number
|
|
@@ -38,10 +37,7 @@ export function getAxisDeltaFromDrag(event: OtuiMouseEvent, drag: AxisDragState)
|
|
|
38
37
|
return getScreenPosition(event, drag.axis) - drag.screenStart
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
drag
|
|
44
|
-
): number {
|
|
45
|
-
const offset = getScreenPosition(event, drag.axis) - drag.screenStart
|
|
46
|
-
return drag.anchor === 'start' ? offset / drag.totalSize : 1 - offset / drag.totalSize
|
|
40
|
+
/** Cursor position as a 0..1 fraction of the dragged container. */
|
|
41
|
+
export function getRatioFromDrag(event: OtuiMouseEvent, drag: RatioDragState): number {
|
|
42
|
+
return (getScreenPosition(event, drag.axis) - drag.screenStart) / drag.totalSize
|
|
47
43
|
}
|
|
@@ -5,7 +5,7 @@ import { useEffect, useRef } from 'react'
|
|
|
5
5
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
6
6
|
import type { SessionBackend } from '../session-backend/types'
|
|
7
7
|
import type { SplitDirection } from '../state/layout-tree'
|
|
8
|
-
import type { AppAction, AppState, TabSession, TerminalLine } from '../state/types'
|
|
8
|
+
import type { AppAction, AppState, BarSide, TabSession, TerminalLine } from '../state/types'
|
|
9
9
|
|
|
10
10
|
import { logInputDebug } from '../debug/input-log'
|
|
11
11
|
import { MultiClickDetector } from '../input/multi-click-detector'
|
|
@@ -23,20 +23,19 @@ import { recordMultiClickClipboardWrite } from './multi-click-clipboard-guard'
|
|
|
23
23
|
import { requestRenderUpTree } from './render-invalidation'
|
|
24
24
|
import { shouldWriteSelectionToClipboard } from './selection-clipboard-dedup'
|
|
25
25
|
import {
|
|
26
|
-
type AnchoredRatioDragState,
|
|
27
26
|
type AxisDragState,
|
|
28
|
-
getAnchoredRatioFromDrag,
|
|
29
27
|
getAxisDeltaFromDrag,
|
|
28
|
+
getRatioFromDrag,
|
|
30
29
|
getSplitRatioFromDrag,
|
|
30
|
+
type RatioDragState,
|
|
31
31
|
type SplitDragState,
|
|
32
32
|
} from './split-drag-controller'
|
|
33
33
|
import { getForwardedMouseSequence, getScrollViewportDelta } from './terminal-mouse-adapter'
|
|
34
34
|
|
|
35
35
|
type ResizeDragState =
|
|
36
36
|
| ({ kind: 'split' } & SplitDragState)
|
|
37
|
-
| ({ initialWidth: number; kind: '
|
|
38
|
-
| ({
|
|
39
|
-
| ({ kind: 'embedded-git'; position: 'top' | 'bottom' } & AnchoredRatioDragState)
|
|
37
|
+
| ({ initialWidth: number; kind: 'bar'; side: BarSide } & AxisDragState)
|
|
38
|
+
| ({ index: number; kind: 'bar-boundary'; side: BarSide } & RatioDragState)
|
|
40
39
|
|
|
41
40
|
interface MultiClickDragState {
|
|
42
41
|
mode: MultiClickMode
|
|
@@ -322,40 +321,32 @@ export function useMouseHandlers({
|
|
|
322
321
|
separatorDragRef.current = { kind: 'split', ...info }
|
|
323
322
|
}
|
|
324
323
|
|
|
325
|
-
const
|
|
326
|
-
separatorDragRef.current = {
|
|
327
|
-
axis: 'x',
|
|
328
|
-
initialWidth: info.initialWidth,
|
|
329
|
-
kind: 'sidebar',
|
|
330
|
-
screenStart: info.screenStart,
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const handleGitPaneResizeStart = (info: {
|
|
324
|
+
const handleBarResizeStart = (info: {
|
|
335
325
|
initialWidth: number
|
|
336
326
|
screenStart: number
|
|
337
|
-
side:
|
|
327
|
+
side: BarSide
|
|
338
328
|
}) => {
|
|
339
329
|
separatorDragRef.current = {
|
|
340
330
|
axis: 'x',
|
|
341
331
|
initialWidth: info.initialWidth,
|
|
342
|
-
kind: '
|
|
332
|
+
kind: 'bar',
|
|
343
333
|
screenStart: info.screenStart,
|
|
344
334
|
side: info.side,
|
|
345
335
|
}
|
|
346
336
|
}
|
|
347
337
|
|
|
348
|
-
const
|
|
338
|
+
const handleBarBoundaryResizeStart = (info: {
|
|
349
339
|
containerStart: number
|
|
350
|
-
|
|
340
|
+
index: number
|
|
341
|
+
side: BarSide
|
|
351
342
|
totalSize: number
|
|
352
343
|
}) => {
|
|
353
344
|
separatorDragRef.current = {
|
|
354
|
-
anchor: info.position === 'top' ? 'start' : 'end',
|
|
355
345
|
axis: 'y',
|
|
356
|
-
|
|
357
|
-
|
|
346
|
+
index: info.index,
|
|
347
|
+
kind: 'bar-boundary',
|
|
358
348
|
screenStart: info.containerStart,
|
|
349
|
+
side: info.side,
|
|
359
350
|
totalSize: info.totalSize,
|
|
360
351
|
}
|
|
361
352
|
}
|
|
@@ -377,27 +368,22 @@ export function useMouseHandlers({
|
|
|
377
368
|
})
|
|
378
369
|
break
|
|
379
370
|
}
|
|
380
|
-
case '
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
break
|
|
384
|
-
}
|
|
385
|
-
case 'git-pane': {
|
|
386
|
-
const delta = getAxisDeltaFromDrag(event, drag)
|
|
387
|
-
const direction = drag.side === 'left' ? 1 : -1
|
|
388
|
-
const nextWidth = drag.initialWidth + delta * direction
|
|
371
|
+
case 'bar': {
|
|
372
|
+
// The right bar grows as the cursor moves left, hence the flipped sign.
|
|
373
|
+
const delta = getAxisDeltaFromDrag(event, drag) * (drag.side === 'left' ? 1 : -1)
|
|
389
374
|
dispatch({
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
375
|
+
side: drag.side,
|
|
376
|
+
type: 'set-bar-width',
|
|
377
|
+
width: Math.round(drag.initialWidth + delta),
|
|
393
378
|
})
|
|
394
379
|
break
|
|
395
380
|
}
|
|
396
|
-
case '
|
|
381
|
+
case 'bar-boundary': {
|
|
397
382
|
dispatch({
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
383
|
+
index: drag.index,
|
|
384
|
+
ratio: getRatioFromDrag(event, drag),
|
|
385
|
+
side: drag.side,
|
|
386
|
+
type: 'set-bar-boundary',
|
|
401
387
|
})
|
|
402
388
|
break
|
|
403
389
|
}
|
|
@@ -586,13 +572,12 @@ export function useMouseHandlers({
|
|
|
586
572
|
}
|
|
587
573
|
|
|
588
574
|
return {
|
|
589
|
-
|
|
590
|
-
|
|
575
|
+
handleBarBoundaryResizeStart,
|
|
576
|
+
handleBarResizeStart,
|
|
591
577
|
handlePaneActivate,
|
|
592
578
|
handleSeparatorDrag,
|
|
593
579
|
handleSeparatorDragEnd,
|
|
594
580
|
handleSeparatorDragStart,
|
|
595
|
-
handleSidebarResizeStart,
|
|
596
581
|
handleSplitResize,
|
|
597
582
|
handleTerminalClick,
|
|
598
583
|
handleTerminalDrag,
|
|
@@ -13,7 +13,7 @@ import type { SessionBackend } from '../session-backend/types'
|
|
|
13
13
|
import type { AppAction, AppState } from '../state/types'
|
|
14
14
|
import type { MeasuredPaneRect } from './use-pane-size-report'
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { getBarWidth } from '../state/bars'
|
|
17
17
|
import {
|
|
18
18
|
createTerminalBounds,
|
|
19
19
|
forEachSplitPaneRect,
|
|
@@ -223,9 +223,12 @@ export function useTerminalResize({
|
|
|
223
223
|
[backend, contentOriginRef]
|
|
224
224
|
)
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
// `getBarWidth` is the single authority on bar width — the Bar component
|
|
227
|
+
// renders from the same function. A mismatch silently corrupts PTY columns
|
|
228
|
+
// and mouse hit-testing.
|
|
229
|
+
const leftBarWidth = getBarWidth(state.bars.left)
|
|
230
|
+
const rightBarWidth = getBarWidth(state.bars.right)
|
|
227
231
|
const terminalSize = useMemo(() => {
|
|
228
|
-
const sidebarWidth = state.sidebar.visible ? state.sidebar.width : 0
|
|
229
232
|
const sessionBarRows = state.sessionBar.visible ? 1 : 0
|
|
230
233
|
const sessionBarTopOffset = sessionBarRows
|
|
231
234
|
const reservedRows =
|
|
@@ -233,18 +236,16 @@ export function useTerminalResize({
|
|
|
233
236
|
STATUS_BAR_HEIGHT +
|
|
234
237
|
TERMINAL_PANE_VERTICAL_CHROME +
|
|
235
238
|
sessionBarRows
|
|
236
|
-
const gitPaneWidth = gitPaneInPaneMode ? getGitPaneWidthFromRatio(state.gitPane.paneRatio) : 0
|
|
237
|
-
const gitOnLeft = gitPaneInPaneMode && state.gitPane.position === 'left'
|
|
238
239
|
const cols = Math.max(
|
|
239
240
|
MIN_TERMINAL_COLS,
|
|
240
|
-
Math.floor(dimensions.width -
|
|
241
|
+
Math.floor(dimensions.width - leftBarWidth - rightBarWidth - MAIN_AREA_HORIZONTAL_CHROME)
|
|
241
242
|
)
|
|
242
243
|
const rows = Math.max(MIN_TERMINAL_ROWS, Math.floor(dimensions.height - reservedRows))
|
|
243
244
|
|
|
244
245
|
contentOriginRef.current = {
|
|
245
246
|
cols,
|
|
246
247
|
rows,
|
|
247
|
-
x:
|
|
248
|
+
x: leftBarWidth + 1,
|
|
248
249
|
y: 1 + sessionBarTopOffset,
|
|
249
250
|
}
|
|
250
251
|
|
|
@@ -253,12 +254,9 @@ export function useTerminalResize({
|
|
|
253
254
|
contentOriginRef,
|
|
254
255
|
dimensions.height,
|
|
255
256
|
dimensions.width,
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
leftBarWidth,
|
|
258
|
+
rightBarWidth,
|
|
258
259
|
state.sessionBar.visible,
|
|
259
|
-
gitPaneInPaneMode,
|
|
260
|
-
state.gitPane.position,
|
|
261
|
-
state.gitPane.paneRatio,
|
|
262
260
|
])
|
|
263
261
|
|
|
264
262
|
useLayoutEffect(() => {
|
|
@@ -280,14 +278,7 @@ export function useTerminalResize({
|
|
|
280
278
|
})
|
|
281
279
|
handledBySyncRef.current = true
|
|
282
280
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
283
|
-
}, [
|
|
284
|
-
state.sidebar.visible,
|
|
285
|
-
state.sidebar.width,
|
|
286
|
-
state.sessionBar.visible,
|
|
287
|
-
gitPaneInPaneMode,
|
|
288
|
-
state.gitPane.position,
|
|
289
|
-
state.gitPane.paneRatio,
|
|
290
|
-
])
|
|
281
|
+
}, [leftBarWidth, rightBarWidth, state.sessionBar.visible])
|
|
291
282
|
|
|
292
283
|
useEffect(() => {
|
|
293
284
|
if (handledBySyncRef.current) {
|
package/src/app.tsx
CHANGED
|
@@ -106,40 +106,14 @@ export function App({
|
|
|
106
106
|
const json = loadConfig()
|
|
107
107
|
const sessionBarVisible =
|
|
108
108
|
resolvedConfig.sessionBar?.initialVisible ?? json.sessionBarVisible ?? true
|
|
109
|
-
const sidebarOverrides = json.sidebar
|
|
110
|
-
|
|
111
109
|
// Merge config-file gitPane (persisted prefs) with user's resolved gitPane
|
|
112
110
|
// (programmatic config). User config wins; file provides persisted prior state.
|
|
113
111
|
const userGitPane = resolvedConfig.gitPane
|
|
114
|
-
const fileListMode = userGitPane?.initialFileListMode ?? json.gitPane?.fileListMode ?? 'tree'
|
|
115
|
-
const diffModeRatio = userGitPane?.initialDiffModeRatio ?? json.gitPane?.diffModeRatio ?? 0.35
|
|
116
|
-
const treeCompaction =
|
|
117
|
-
userGitPane?.initialTreeCompaction ?? json.gitPane?.treeCompaction ?? true
|
|
118
|
-
const prefetchRadius = userGitPane?.prefetchRadius ?? json.gitPane?.prefetchRadius ?? 5
|
|
119
|
-
const persistedPaneRatio = json.gitPane?.paneRatio ?? json.gitPane?.ratio ?? 0.5
|
|
120
|
-
const persistedEmbeddedRatio = json.gitPane?.embeddedRatio ?? json.gitPane?.ratio ?? 0.5
|
|
121
112
|
const gitPaneOverrides = {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
? userGitPane.initialRatio
|
|
127
|
-
: persistedEmbeddedRatio,
|
|
128
|
-
fileListMode,
|
|
129
|
-
paneRatio:
|
|
130
|
-
userGitPane?.initialMode === 'pane' && userGitPane?.initialRatio !== undefined
|
|
131
|
-
? userGitPane.initialRatio
|
|
132
|
-
: persistedPaneRatio,
|
|
133
|
-
prefetchRadius,
|
|
134
|
-
treeCompaction,
|
|
135
|
-
...(userGitPane?.initialVisible !== undefined ? { visible: userGitPane.initialVisible } : {}),
|
|
136
|
-
...(userGitPane?.initialMode !== undefined ? { mode: userGitPane.initialMode } : {}),
|
|
137
|
-
...(userGitPane?.initialPosition !== undefined
|
|
138
|
-
? { position: userGitPane.initialPosition }
|
|
139
|
-
: {}),
|
|
140
|
-
...(userGitPane?.initialDiffModeRatio !== undefined
|
|
141
|
-
? { diffModeRatio: userGitPane.initialDiffModeRatio }
|
|
142
|
-
: {}),
|
|
113
|
+
diffModeRatio: userGitPane?.initialDiffModeRatio ?? json.gitPane?.diffModeRatio ?? 0.35,
|
|
114
|
+
fileListMode: userGitPane?.initialFileListMode ?? json.gitPane?.fileListMode ?? 'tree',
|
|
115
|
+
prefetchRadius: userGitPane?.prefetchRadius ?? json.gitPane?.prefetchRadius ?? 5,
|
|
116
|
+
treeCompaction: userGitPane?.initialTreeCompaction ?? json.gitPane?.treeCompaction ?? true,
|
|
143
117
|
...(userGitPane?.path !== undefined ? { path: userGitPane.path } : {}),
|
|
144
118
|
...(userGitPane?.diffCount !== undefined ? { diffCount: userGitPane.diffCount } : {}),
|
|
145
119
|
}
|
|
@@ -152,9 +126,9 @@ export function App({
|
|
|
152
126
|
mergedSnippets,
|
|
153
127
|
sessionCatalog.length === 0,
|
|
154
128
|
{
|
|
129
|
+
bars: json.bars,
|
|
155
130
|
gitPane: gitPaneOverrides,
|
|
156
131
|
sessionBarVisible,
|
|
157
|
-
sidebar: sidebarOverrides,
|
|
158
132
|
worktreeTemplates:
|
|
159
133
|
resolvedConfig.worktreeTemplates.length > 0
|
|
160
134
|
? resolvedConfig.worktreeTemplates
|
|
@@ -382,13 +356,12 @@ export function App({
|
|
|
382
356
|
})
|
|
383
357
|
|
|
384
358
|
const {
|
|
385
|
-
|
|
386
|
-
|
|
359
|
+
handleBarBoundaryResizeStart,
|
|
360
|
+
handleBarResizeStart,
|
|
387
361
|
handlePaneActivate,
|
|
388
362
|
handleSeparatorDrag,
|
|
389
363
|
handleSeparatorDragEnd,
|
|
390
364
|
handleSeparatorDragStart,
|
|
391
|
-
handleSidebarResizeStart,
|
|
392
365
|
handleSplitResize,
|
|
393
366
|
handleTerminalClick,
|
|
394
367
|
handleTerminalDrag,
|
|
@@ -563,12 +536,11 @@ export function App({
|
|
|
563
536
|
onTerminalMouseUp={handleTerminalMouseUp}
|
|
564
537
|
onPaneActivate={handlePaneActivate}
|
|
565
538
|
onSplitResize={handleSplitResize}
|
|
566
|
-
|
|
567
|
-
|
|
539
|
+
onBarBoundaryResizeStart={handleBarBoundaryResizeStart}
|
|
540
|
+
onBarResizeStart={handleBarResizeStart}
|
|
568
541
|
onSeparatorDragStart={handleSeparatorDragStart}
|
|
569
542
|
onSeparatorDrag={handleSeparatorDrag}
|
|
570
543
|
onSeparatorDragEnd={handleSeparatorDragEnd}
|
|
571
|
-
onSidebarResizeStart={handleSidebarResizeStart}
|
|
572
544
|
onMeasure={terminalSize.onMeasure}
|
|
573
545
|
terminalCols={terminalSize.cols}
|
|
574
546
|
terminalRows={terminalSize.rows}
|
package/src/config.ts
CHANGED
|
@@ -22,24 +22,48 @@ export function getConfigPath(): string {
|
|
|
22
22
|
return `${getProfileConfigDir()}/aimux.json`
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* `visible` / `mode` / `position` / `*Ratio` are legacy placement fields, read
|
|
27
|
+
* once by `deriveBarsFromLegacy` and no longer written. They must stay
|
|
28
|
+
* optional: rejecting the whole block would silently drop the git *content*
|
|
29
|
+
* preferences (`fileListMode`, `diffModeRatio`, `treeCompaction`).
|
|
30
|
+
*/
|
|
25
31
|
export interface PersistedGitPane {
|
|
26
32
|
diffModeRatio?: number
|
|
27
33
|
fileListMode?: GitFileListMode
|
|
28
34
|
treeCompaction?: boolean
|
|
29
35
|
prefetchRadius?: number
|
|
30
|
-
visible
|
|
31
|
-
mode
|
|
32
|
-
position
|
|
36
|
+
visible?: boolean
|
|
37
|
+
mode?: 'embedded' | 'pane'
|
|
38
|
+
position?: 'top' | 'bottom' | 'left' | 'right'
|
|
33
39
|
paneRatio?: number
|
|
34
40
|
embeddedRatio?: number
|
|
35
41
|
ratio?: number
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
/** Legacy; still written for downgrade safety, read only as a bars fallback. */
|
|
38
45
|
export interface PersistedSidebar {
|
|
39
46
|
visible: boolean
|
|
40
47
|
width: number
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
export interface PersistedBarWidget {
|
|
51
|
+
id: string
|
|
52
|
+
grow: number
|
|
53
|
+
visible: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PersistedBar {
|
|
57
|
+
visible: boolean
|
|
58
|
+
width: number
|
|
59
|
+
widgets: PersistedBarWidget[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PersistedBars {
|
|
63
|
+
left: PersistedBar
|
|
64
|
+
right: PersistedBar
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
export interface WorktreeTemplatePane {
|
|
44
68
|
id: string
|
|
45
69
|
assistant: string
|
|
@@ -67,6 +91,7 @@ export interface AimuxConfig {
|
|
|
67
91
|
themeTransparent?: boolean
|
|
68
92
|
themeMode?: ThemeMode
|
|
69
93
|
gitPane?: PersistedGitPane
|
|
94
|
+
bars?: PersistedBars
|
|
70
95
|
sidebar?: PersistedSidebar
|
|
71
96
|
sessionBarVisible?: boolean
|
|
72
97
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
@@ -79,8 +104,9 @@ export interface AimuxConfig {
|
|
|
79
104
|
function isPersistedGitPane(value: unknown): value is PersistedGitPane {
|
|
80
105
|
if (typeof value !== 'object' || value === null) return false
|
|
81
106
|
const v = value as Record<string, unknown>
|
|
82
|
-
const modeOk = v.mode === 'embedded' || v.mode === 'pane'
|
|
107
|
+
const modeOk = v.mode === undefined || v.mode === 'embedded' || v.mode === 'pane'
|
|
83
108
|
const positionOk =
|
|
109
|
+
v.position === undefined ||
|
|
84
110
|
v.position === 'top' ||
|
|
85
111
|
v.position === 'bottom' ||
|
|
86
112
|
v.position === 'left' ||
|
|
@@ -106,7 +132,7 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
|
|
|
106
132
|
Number.isFinite(v.diffModeRatio) &&
|
|
107
133
|
v.diffModeRatio > 0 &&
|
|
108
134
|
v.diffModeRatio < 1)
|
|
109
|
-
const visibleOk = typeof v.visible === 'boolean'
|
|
135
|
+
const visibleOk = v.visible === undefined || typeof v.visible === 'boolean'
|
|
110
136
|
const fileListModeOk =
|
|
111
137
|
v.fileListMode === undefined || v.fileListMode === 'tree' || v.fileListMode === 'flat'
|
|
112
138
|
const treeCompactionOk = v.treeCompaction === undefined || typeof v.treeCompaction === 'boolean'
|
|
@@ -130,12 +156,69 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
|
|
|
130
156
|
) {
|
|
131
157
|
return false
|
|
132
158
|
}
|
|
133
|
-
// cross-field coherence: embedded => top|bottom; pane => left|right
|
|
134
|
-
if (v.mode === 'embedded' && v.position !== 'top' && v.position !== 'bottom') return false
|
|
135
|
-
if (v.mode === 'pane' && v.position !== 'left' && v.position !== 'right') return false
|
|
136
159
|
return true
|
|
137
160
|
}
|
|
138
161
|
|
|
162
|
+
function isPersistedBar(value: unknown): value is PersistedBar {
|
|
163
|
+
if (typeof value !== 'object' || value === null) return false
|
|
164
|
+
const v = value as Record<string, unknown>
|
|
165
|
+
if (typeof v.visible !== 'boolean') return false
|
|
166
|
+
if (typeof v.width !== 'number' || !Number.isFinite(v.width) || v.width <= 0) return false
|
|
167
|
+
if (!Array.isArray(v.widgets)) return false
|
|
168
|
+
return v.widgets.every((raw) => {
|
|
169
|
+
if (typeof raw !== 'object' || raw === null) return false
|
|
170
|
+
const widget = raw as Record<string, unknown>
|
|
171
|
+
return (
|
|
172
|
+
typeof widget.id === 'string' &&
|
|
173
|
+
widget.id.length > 0 &&
|
|
174
|
+
typeof widget.grow === 'number' &&
|
|
175
|
+
Number.isFinite(widget.grow) &&
|
|
176
|
+
widget.grow > 0 &&
|
|
177
|
+
typeof widget.visible === 'boolean'
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function isPersistedBars(value: unknown): value is PersistedBars {
|
|
183
|
+
if (typeof value !== 'object' || value === null) return false
|
|
184
|
+
const v = value as Record<string, unknown>
|
|
185
|
+
return isPersistedBar(v.left) && isPersistedBar(v.right)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* One-time upgrade from the pre-bars layout (a left sidebar plus a git pane
|
|
190
|
+
* with its own mode/position). Runs whenever `bars` is absent so an existing
|
|
191
|
+
* install keeps the layout it had.
|
|
192
|
+
*/
|
|
193
|
+
export function deriveBarsFromLegacy(
|
|
194
|
+
sidebar: PersistedSidebar | undefined,
|
|
195
|
+
gitPane: PersistedGitPane | undefined
|
|
196
|
+
): PersistedBars {
|
|
197
|
+
const gitVisible = gitPane?.visible ?? true
|
|
198
|
+
const embeddedRatio = gitPane?.embeddedRatio ?? gitPane?.ratio ?? 0.5
|
|
199
|
+
const gitGrow = Math.max(1, Math.round(embeddedRatio * 100))
|
|
200
|
+
const git = { grow: gitGrow, id: 'git', visible: gitVisible }
|
|
201
|
+
const workspaces = { grow: Math.max(1, 100 - gitGrow), id: 'workspaces', visible: true }
|
|
202
|
+
|
|
203
|
+
const left: PersistedBar = {
|
|
204
|
+
visible: sidebar?.visible ?? true,
|
|
205
|
+
widgets: gitPane?.position === 'top' ? [git, workspaces] : [workspaces, git],
|
|
206
|
+
width: sidebar?.width ?? 28,
|
|
207
|
+
}
|
|
208
|
+
const right: PersistedBar = { visible: false, widgets: [], width: 40 }
|
|
209
|
+
|
|
210
|
+
// A legacy `pane` git panel on the right was already its own right-hand
|
|
211
|
+
// column — it maps straight onto the right bar. On the left it shared the
|
|
212
|
+
// edge with the sidebar, which the single-bar model expresses as a stack.
|
|
213
|
+
if (gitPane?.mode === 'pane' && gitPane.position === 'right') {
|
|
214
|
+
left.widgets = [{ ...workspaces, grow: 100 }]
|
|
215
|
+
right.visible = true
|
|
216
|
+
right.widgets = [{ ...git, grow: 100 }]
|
|
217
|
+
right.width = Math.round((gitPane.paneRatio ?? gitPane.ratio ?? 0.5) * 80)
|
|
218
|
+
}
|
|
219
|
+
return { left, right }
|
|
220
|
+
}
|
|
221
|
+
|
|
139
222
|
function isRatioValid(value: unknown): boolean {
|
|
140
223
|
return typeof value === 'number' && Number.isFinite(value) && value > 0.15 && value < 0.85
|
|
141
224
|
}
|
|
@@ -259,6 +342,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
259
342
|
themeTransparent?: unknown
|
|
260
343
|
themeMode?: unknown
|
|
261
344
|
gitPane?: unknown
|
|
345
|
+
bars?: unknown
|
|
262
346
|
sidebar?: unknown
|
|
263
347
|
gitPanelVisible?: unknown
|
|
264
348
|
gitPanelRatio?: unknown
|
|
@@ -305,6 +389,11 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
305
389
|
issues.push('ignored invalid sidebar')
|
|
306
390
|
}
|
|
307
391
|
|
|
392
|
+
const validBars = isPersistedBars(parsed.bars) ? parsed.bars : undefined
|
|
393
|
+
if (parsed.bars !== undefined && validBars === undefined) {
|
|
394
|
+
issues.push('ignored invalid bars')
|
|
395
|
+
}
|
|
396
|
+
|
|
308
397
|
// Legacy migration: previous schema stored gitPanelVisible/gitPanelRatio at
|
|
309
398
|
// top level. If the new `gitPane` field is absent, synthesize it from legacy
|
|
310
399
|
// keys so users don't lose their toggle/ratio on upgrade.
|
|
@@ -359,6 +448,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
359
448
|
|
|
360
449
|
return {
|
|
361
450
|
config: {
|
|
451
|
+
bars: validBars ?? deriveBarsFromLegacy(validSidebar, validGitPane),
|
|
362
452
|
customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
|
|
363
453
|
gitPane: validGitPane,
|
|
364
454
|
prunedOrphanAimuxBranches: parsed.prunedOrphanAimuxBranches === true ? true : undefined,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { logDebug } from '../debug/input-log'
|
|
2
|
+
import { runCli } from '../services/ai-usage/spawn'
|
|
3
|
+
|
|
4
|
+
export interface RepoMergeMethods {
|
|
5
|
+
mergeCommitAllowed: boolean
|
|
6
|
+
squashMergeAllowed: boolean
|
|
7
|
+
rebaseMergeAllowed: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mirrors the method GitHub pre-selects on the merge button: a merge commit
|
|
12
|
+
* when the repo allows one, then squash, then rebase. `gh pr merge` refuses to
|
|
13
|
+
* guess when several are enabled, so we have to name one explicitly.
|
|
14
|
+
*/
|
|
15
|
+
export function pickMergeFlag(methods: RepoMergeMethods): string | null {
|
|
16
|
+
if (methods.mergeCommitAllowed) return '--merge'
|
|
17
|
+
if (methods.squashMergeAllowed) return '--squash'
|
|
18
|
+
if (methods.rebaseMergeAllowed) return '--rebase'
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type MergeResult = { ok: true } | { ok: false; message: string }
|
|
23
|
+
|
|
24
|
+
async function repoMergeMethods(gh: string, cwd: string): Promise<RepoMergeMethods | null> {
|
|
25
|
+
const result = await runCli(
|
|
26
|
+
gh,
|
|
27
|
+
['repo', 'view', '--json', 'mergeCommitAllowed,squashMergeAllowed,rebaseMergeAllowed'],
|
|
28
|
+
15_000,
|
|
29
|
+
cwd
|
|
30
|
+
)
|
|
31
|
+
if (!result.ok) return null
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(result.stdout) as Record<string, unknown>
|
|
34
|
+
return {
|
|
35
|
+
mergeCommitAllowed: raw.mergeCommitAllowed === true,
|
|
36
|
+
rebaseMergeAllowed: raw.rebaseMergeAllowed === true,
|
|
37
|
+
squashMergeAllowed: raw.squashMergeAllowed === true,
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function approveAndMergePr(cwd: string): Promise<MergeResult> {
|
|
45
|
+
const gh = Bun.which('gh')
|
|
46
|
+
if (gh === null) return { message: 'gh CLI not found', ok: false }
|
|
47
|
+
|
|
48
|
+
const methods = await repoMergeMethods(gh, cwd)
|
|
49
|
+
if (methods === null) return { message: 'could not read repo merge settings', ok: false }
|
|
50
|
+
const flag = pickMergeFlag(methods)
|
|
51
|
+
if (flag === null) return { message: 'repo allows no merge method', ok: false }
|
|
52
|
+
|
|
53
|
+
// GitHub rejects approving your own PR, and a missing approval is not a
|
|
54
|
+
// reason to skip the merge — best effort, log it, move on.
|
|
55
|
+
const approve = await runCli(gh, ['pr', 'review', '--approve'], 15_000, cwd)
|
|
56
|
+
if (!approve.ok) logDebug('git.pr.approveSkipped', { error: approve.error })
|
|
57
|
+
|
|
58
|
+
const merge = await runCli(gh, ['pr', 'merge', flag], 60_000, cwd)
|
|
59
|
+
if (!merge.ok) {
|
|
60
|
+
const detail = merge.stderr.trim().split('\n')[0] ?? 'merge failed'
|
|
61
|
+
return { message: detail.slice(0, 160), ok: false }
|
|
62
|
+
}
|
|
63
|
+
return { ok: true }
|
|
64
|
+
}
|