@brimveyn/aimux 1.13.0 → 1.13.2
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 +11 -11
- package/src/app-runtime/side-effects.ts +61 -38
- package/src/daemon/daemon.ts +18 -8
- package/src/git/command-queue.ts +18 -2
- package/src/session-backend/remote-session-backend.ts +57 -42
- 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,5 +1,7 @@
|
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
|
|
1
3
|
import { useKeyboard } from '@opentui/react'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
3
5
|
|
|
4
6
|
import { useAppStore } from '../../../../state/app-store'
|
|
5
7
|
import {
|
|
@@ -9,6 +11,50 @@ import {
|
|
|
9
11
|
} from '../../../context-menu/controller'
|
|
10
12
|
import { useTheme } from '../../../theme'
|
|
11
13
|
|
|
14
|
+
const MenuItem = memo(function MenuItem({
|
|
15
|
+
active,
|
|
16
|
+
index,
|
|
17
|
+
label,
|
|
18
|
+
onHover,
|
|
19
|
+
onSelect,
|
|
20
|
+
width,
|
|
21
|
+
}: {
|
|
22
|
+
active: boolean
|
|
23
|
+
index: number
|
|
24
|
+
label: string
|
|
25
|
+
onHover: (index: number) => void
|
|
26
|
+
onSelect: () => void
|
|
27
|
+
width: number
|
|
28
|
+
}) {
|
|
29
|
+
const t = useTheme()
|
|
30
|
+
const handleMouseOver = useCallback(() => onHover(index), [index, onHover])
|
|
31
|
+
const handleMouseDown = useCallback(
|
|
32
|
+
(e: OtuiMouseEvent) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
e.stopPropagation()
|
|
35
|
+
if (e.button !== 0) return
|
|
36
|
+
closeContextMenu()
|
|
37
|
+
onSelect()
|
|
38
|
+
},
|
|
39
|
+
[onSelect]
|
|
40
|
+
)
|
|
41
|
+
return (
|
|
42
|
+
<box
|
|
43
|
+
width={width - 2}
|
|
44
|
+
flexShrink={0}
|
|
45
|
+
paddingLeft={1}
|
|
46
|
+
paddingRight={1}
|
|
47
|
+
backgroundColor={active ? t.backgroundElement : undefined}
|
|
48
|
+
onMouseOver={handleMouseOver}
|
|
49
|
+
onMouseDown={handleMouseDown}
|
|
50
|
+
>
|
|
51
|
+
<text fg={active ? t.text : t.textMuted} selectable={false}>
|
|
52
|
+
{label}
|
|
53
|
+
</text>
|
|
54
|
+
</box>
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
12
58
|
export function ContextMenuOverlay() {
|
|
13
59
|
const [menu, setMenu] = useState<ContextMenuState | null>(null)
|
|
14
60
|
const [selected, setSelected] = useState(0)
|
|
@@ -48,6 +94,15 @@ export function ContextMenuOverlay() {
|
|
|
48
94
|
}
|
|
49
95
|
})
|
|
50
96
|
|
|
97
|
+
const handleBackdropMouseDown = useCallback((e: OtuiMouseEvent) => {
|
|
98
|
+
e.preventDefault()
|
|
99
|
+
e.stopPropagation()
|
|
100
|
+
closeContextMenu()
|
|
101
|
+
}, [])
|
|
102
|
+
const handleMenuMouseDown = useCallback((e: OtuiMouseEvent) => {
|
|
103
|
+
e.stopPropagation()
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
51
106
|
if (!menu) return null
|
|
52
107
|
|
|
53
108
|
const maxLabel = Math.max(...menu.items.map(([label]) => label.length))
|
|
@@ -66,11 +121,7 @@ export function ContextMenuOverlay() {
|
|
|
66
121
|
left={0}
|
|
67
122
|
width="100%"
|
|
68
123
|
height="100%"
|
|
69
|
-
onMouseDown={
|
|
70
|
-
e.preventDefault()
|
|
71
|
-
e.stopPropagation()
|
|
72
|
-
closeContextMenu()
|
|
73
|
-
}}
|
|
124
|
+
onMouseDown={handleBackdropMouseDown}
|
|
74
125
|
/>
|
|
75
126
|
<box
|
|
76
127
|
position="absolute"
|
|
@@ -81,35 +132,19 @@ export function ContextMenuOverlay() {
|
|
|
81
132
|
border
|
|
82
133
|
borderColor={t.border}
|
|
83
134
|
backgroundColor={t.backgroundPanel}
|
|
84
|
-
onMouseDown={
|
|
85
|
-
e.stopPropagation()
|
|
86
|
-
}}
|
|
135
|
+
onMouseDown={handleMenuMouseDown}
|
|
87
136
|
>
|
|
88
|
-
{menu.items.map(([label, onSelect], index) =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
onMouseDown={(e) => {
|
|
100
|
-
e.preventDefault()
|
|
101
|
-
e.stopPropagation()
|
|
102
|
-
if (e.button !== 0) return
|
|
103
|
-
closeContextMenu()
|
|
104
|
-
onSelect()
|
|
105
|
-
}}
|
|
106
|
-
>
|
|
107
|
-
<text fg={active ? t.text : t.textMuted} selectable={false}>
|
|
108
|
-
{label}
|
|
109
|
-
</text>
|
|
110
|
-
</box>
|
|
111
|
-
)
|
|
112
|
-
})}
|
|
137
|
+
{menu.items.map(([label, onSelect], index) => (
|
|
138
|
+
<MenuItem
|
|
139
|
+
key={label}
|
|
140
|
+
index={index}
|
|
141
|
+
label={label}
|
|
142
|
+
onHover={setSelected}
|
|
143
|
+
onSelect={onSelect}
|
|
144
|
+
active={index === selected}
|
|
145
|
+
width={width}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
113
148
|
</box>
|
|
114
149
|
</box>
|
|
115
150
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { memo, type ReactNode, useMemo } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
import { Surface } from './surface'
|
|
@@ -9,27 +9,43 @@ interface ListItemProps {
|
|
|
9
9
|
active: boolean
|
|
10
10
|
direction?: ListItemDirection
|
|
11
11
|
id?: string
|
|
12
|
+
index?: number
|
|
12
13
|
leading?: ReactNode
|
|
13
14
|
subtitle?: ReactNode
|
|
14
15
|
title: ReactNode
|
|
15
16
|
trailing?: ReactNode
|
|
16
17
|
onHover?: () => void
|
|
17
18
|
onClick?: () => void
|
|
19
|
+
onHoverIndex?: (index: number) => void
|
|
20
|
+
onClickIndex?: (index: number) => void
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
export function ListItem({
|
|
23
|
+
export const ListItem = memo(function ListItem({
|
|
21
24
|
active,
|
|
22
25
|
direction = 'column',
|
|
23
26
|
id,
|
|
27
|
+
index,
|
|
24
28
|
leading,
|
|
25
29
|
onClick,
|
|
30
|
+
onClickIndex,
|
|
26
31
|
onHover,
|
|
32
|
+
onHoverIndex,
|
|
27
33
|
subtitle,
|
|
28
34
|
title,
|
|
29
35
|
trailing,
|
|
30
36
|
}: ListItemProps) {
|
|
31
37
|
const t = useTheme()
|
|
32
38
|
const isRow = direction === 'row'
|
|
39
|
+
// Build per-item handlers from the stable index-based callbacks so callers can
|
|
40
|
+
// avoid creating fresh closures inside their list `.map()`.
|
|
41
|
+
const handleHover = useMemo<(() => void) | undefined>(() => {
|
|
42
|
+
if (onHoverIndex && index !== undefined) return () => onHoverIndex(index)
|
|
43
|
+
return onHover
|
|
44
|
+
}, [index, onHover, onHoverIndex])
|
|
45
|
+
const handleClick = useMemo<(() => void) | undefined>(() => {
|
|
46
|
+
if (onClickIndex && index !== undefined) return () => onClickIndex(index)
|
|
47
|
+
return onClick
|
|
48
|
+
}, [index, onClick, onClickIndex])
|
|
33
49
|
const inner = (
|
|
34
50
|
<box flexDirection="column">
|
|
35
51
|
<box flexDirection="row">
|
|
@@ -48,7 +64,7 @@ export function ListItem({
|
|
|
48
64
|
</box>
|
|
49
65
|
)
|
|
50
66
|
return (
|
|
51
|
-
<box id={id} onMouseOver={
|
|
67
|
+
<box id={id} onMouseOver={handleHover} onMouseDown={handleClick}>
|
|
52
68
|
{active ? (
|
|
53
69
|
<Surface tone="selected" paddingLeft={isRow ? 2 : 1} paddingRight={isRow ? 2 : 1}>
|
|
54
70
|
{inner}
|
|
@@ -60,4 +76,4 @@ export function ListItem({
|
|
|
60
76
|
)}
|
|
61
77
|
</box>
|
|
62
78
|
)
|
|
63
|
-
}
|
|
79
|
+
})
|
package/src/ui/root.tsx
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { MouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
+
import { useCallback, useMemo } from 'react'
|
|
4
|
+
|
|
3
5
|
import type { MeasuredPaneRect } from '../app-runtime/use-pane-size-report'
|
|
4
6
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
5
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
FocusMode,
|
|
9
|
+
ModalState,
|
|
10
|
+
SessionRecord,
|
|
11
|
+
SnippetRecord,
|
|
12
|
+
WorktreeRecord,
|
|
13
|
+
} from '../state/types'
|
|
6
14
|
import type { ThemeId } from './themes'
|
|
7
15
|
|
|
8
16
|
import { useAppStore } from '../state/app-store'
|
|
@@ -35,6 +43,8 @@ import { PendingChordOverlay } from './components/overlays/pending-chord-overlay
|
|
|
35
43
|
import { ToastViewport } from './components/overlays/toast/toast-viewport'
|
|
36
44
|
import { useTheme } from './theme'
|
|
37
45
|
|
|
46
|
+
const EMPTY_WORKTREES: WorktreeRecord[] = []
|
|
47
|
+
|
|
38
48
|
function getCreateSessionFields(modal: ModalState) {
|
|
39
49
|
if (modal.type !== 'create-session') {
|
|
40
50
|
return { directoryQuery: '', sessionName: '' }
|
|
@@ -106,8 +116,8 @@ function renderModal(
|
|
|
106
116
|
worktrees={
|
|
107
117
|
options.currentSessionId != null && options.currentSessionId !== ''
|
|
108
118
|
? (options.sessions.find((session) => session.id === options.currentSessionId)
|
|
109
|
-
?.worktrees ??
|
|
110
|
-
:
|
|
119
|
+
?.worktrees ?? EMPTY_WORKTREES)
|
|
120
|
+
: EMPTY_WORKTREES
|
|
111
121
|
}
|
|
112
122
|
worktreeName={modal.type === 'new-tab' ? modal.worktreeName : ''}
|
|
113
123
|
/>
|
|
@@ -189,21 +199,13 @@ function renderModal(
|
|
|
189
199
|
options.currentSessionId != null && options.currentSessionId !== ''
|
|
190
200
|
? options.sessions.find((entry) => entry.id === options.currentSessionId)
|
|
191
201
|
: undefined
|
|
192
|
-
const worktrees = session?.worktrees ?? []
|
|
193
|
-
const sourceId = modal.sourceWorktreeId
|
|
194
|
-
const source = worktrees.find((w) => w.id === sourceId)
|
|
195
|
-
const targets = worktrees.filter((w) => w.id !== sourceId)
|
|
196
|
-
const sourceLabel =
|
|
197
|
-
source?.branch != null && source.branch !== ''
|
|
198
|
-
? source.branch
|
|
199
|
-
: (source?.name ?? 'worktree')
|
|
200
202
|
return (
|
|
201
203
|
<WorktreeMoveModal
|
|
202
204
|
deleteSource={modal.type === 'worktree-move' ? modal.deleteSource : false}
|
|
203
205
|
divergence={options.worktreeDivergence}
|
|
204
206
|
selectedIndex={modal.selectedIndex}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
sourceWorktreeId={modal.sourceWorktreeId}
|
|
208
|
+
worktrees={session?.worktrees ?? EMPTY_WORKTREES}
|
|
207
209
|
/>
|
|
208
210
|
)
|
|
209
211
|
}
|
|
@@ -322,14 +324,59 @@ export function RootView({
|
|
|
322
324
|
const sidebarVisible = useAppStore((s) => s.sidebar.visible)
|
|
323
325
|
|
|
324
326
|
const gitPaneInPaneOnLeft = gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left'
|
|
327
|
+
const splitChrome = PANE_BORDER * 2
|
|
328
|
+
|
|
329
|
+
const handleSidebarEdgeResize = useCallback(
|
|
330
|
+
(event: MouseEvent): boolean => {
|
|
331
|
+
onSidebarResizeStart?.({ initialWidth: sidebarWidth, screenStart: event.x })
|
|
332
|
+
return true
|
|
333
|
+
},
|
|
334
|
+
[onSidebarResizeStart, sidebarWidth]
|
|
335
|
+
)
|
|
325
336
|
const handleTerminalLeftEdgeMouseDown =
|
|
326
337
|
sidebarVisible && !gitPaneInPaneOnLeft && onSidebarResizeStart
|
|
327
|
-
?
|
|
328
|
-
onSidebarResizeStart({ initialWidth: sidebarWidth, screenStart: event.x })
|
|
329
|
-
return true
|
|
330
|
-
}
|
|
338
|
+
? handleSidebarEdgeResize
|
|
331
339
|
: undefined
|
|
332
340
|
|
|
341
|
+
const handleRootMouseDrag = useCallback(
|
|
342
|
+
(event: MouseEvent) => {
|
|
343
|
+
if (onSeparatorDrag?.(event) === true) {
|
|
344
|
+
event.preventDefault()
|
|
345
|
+
event.stopPropagation()
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
[onSeparatorDrag]
|
|
349
|
+
)
|
|
350
|
+
const handleRootMouseUp = useCallback(
|
|
351
|
+
(event: MouseEvent) => {
|
|
352
|
+
// Catch releases that land outside any TerminalPane (sidebar, gap,
|
|
353
|
+
// status bar, …) so an in-flight multi-click drag — and its
|
|
354
|
+
// auto-scroll interval — is always finalised.
|
|
355
|
+
onTerminalMouseUp?.(event)
|
|
356
|
+
onSeparatorDragEnd?.()
|
|
357
|
+
},
|
|
358
|
+
[onSeparatorDragEnd, onTerminalMouseUp]
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
const splitContentOrigin = useMemo(
|
|
362
|
+
() => ({
|
|
363
|
+
cols: terminalCols + splitChrome,
|
|
364
|
+
rows: terminalRows + splitChrome,
|
|
365
|
+
x: contentOrigin.x - PANE_BORDER,
|
|
366
|
+
y: contentOrigin.y - PANE_BORDER,
|
|
367
|
+
}),
|
|
368
|
+
[contentOrigin, splitChrome, terminalCols, terminalRows]
|
|
369
|
+
)
|
|
370
|
+
const splitBounds = useMemo(
|
|
371
|
+
() => ({
|
|
372
|
+
cols: terminalCols + splitChrome,
|
|
373
|
+
rows: terminalRows + splitChrome,
|
|
374
|
+
x: 0,
|
|
375
|
+
y: 0,
|
|
376
|
+
}),
|
|
377
|
+
[splitChrome, terminalCols, terminalRows]
|
|
378
|
+
)
|
|
379
|
+
|
|
333
380
|
const activeTab = tabs.find((tab) => tab.id === activeTabId)
|
|
334
381
|
const activeTree =
|
|
335
382
|
activeTabId != null && activeTabId !== ''
|
|
@@ -337,7 +384,6 @@ export function RootView({
|
|
|
337
384
|
: null
|
|
338
385
|
const createSessionFields = getCreateSessionFields(modal)
|
|
339
386
|
const snippetEditorFields = getSnippetEditorFields(modal)
|
|
340
|
-
const splitChrome = PANE_BORDER * 2
|
|
341
387
|
|
|
342
388
|
const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
|
|
343
389
|
if (inGitMode) {
|
|
@@ -373,19 +419,8 @@ export function RootView({
|
|
|
373
419
|
width="100%"
|
|
374
420
|
height="100%"
|
|
375
421
|
backgroundColor={editorBg}
|
|
376
|
-
onMouseDrag={
|
|
377
|
-
|
|
378
|
-
event.preventDefault()
|
|
379
|
-
event.stopPropagation()
|
|
380
|
-
}
|
|
381
|
-
}}
|
|
382
|
-
onMouseUp={(event) => {
|
|
383
|
-
// Catch releases that land outside any TerminalPane (sidebar, gap,
|
|
384
|
-
// status bar, …) so an in-flight multi-click drag — and its
|
|
385
|
-
// auto-scroll interval — is always finalised.
|
|
386
|
-
onTerminalMouseUp?.(event)
|
|
387
|
-
onSeparatorDragEnd?.()
|
|
388
|
-
}}
|
|
422
|
+
onMouseDrag={handleRootMouseDrag}
|
|
423
|
+
onMouseUp={handleRootMouseUp}
|
|
389
424
|
>
|
|
390
425
|
{sessionBarPosition === 'top' && <SessionBar />}
|
|
391
426
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
@@ -408,12 +443,7 @@ export function RootView({
|
|
|
408
443
|
tabs={tabs}
|
|
409
444
|
activeTabId={activeTabId}
|
|
410
445
|
focusMode={focusMode}
|
|
411
|
-
contentOrigin={
|
|
412
|
-
cols: terminalCols + splitChrome,
|
|
413
|
-
rows: terminalRows + splitChrome,
|
|
414
|
-
x: contentOrigin.x - PANE_BORDER,
|
|
415
|
-
y: contentOrigin.y - PANE_BORDER,
|
|
416
|
-
}}
|
|
446
|
+
contentOrigin={splitContentOrigin}
|
|
417
447
|
mouseForwardingEnabled={mouseForwardingEnabled}
|
|
418
448
|
localScrollbackEnabled={localScrollbackEnabled}
|
|
419
449
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
@@ -428,12 +458,7 @@ export function RootView({
|
|
|
428
458
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
429
459
|
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
430
460
|
onMeasure={onMeasure}
|
|
431
|
-
bounds={
|
|
432
|
-
cols: terminalCols + splitChrome,
|
|
433
|
-
rows: terminalRows + splitChrome,
|
|
434
|
-
x: 0,
|
|
435
|
-
y: 0,
|
|
436
|
-
}}
|
|
461
|
+
bounds={splitBounds}
|
|
437
462
|
/>
|
|
438
463
|
) : (
|
|
439
464
|
<TerminalPane
|
|
@@ -509,16 +534,20 @@ function GitPaneInPaneMode({
|
|
|
509
534
|
dispatchGlobal({ position: nextPosition, type: 'set-git-pane-position' })
|
|
510
535
|
}
|
|
511
536
|
)
|
|
537
|
+
const handleResizeMouseDown = useCallback(
|
|
538
|
+
(event: MouseEvent) => {
|
|
539
|
+
event.preventDefault()
|
|
540
|
+
event.stopPropagation()
|
|
541
|
+
onGitPaneResizeStart?.({ initialWidth: width, screenStart: event.x, side: position })
|
|
542
|
+
},
|
|
543
|
+
[onGitPaneResizeStart, position, width]
|
|
544
|
+
)
|
|
512
545
|
const handle = (
|
|
513
546
|
<box
|
|
514
547
|
width={1}
|
|
515
548
|
flexShrink={0}
|
|
516
549
|
backgroundColor={tokens.border}
|
|
517
|
-
onMouseDown={
|
|
518
|
-
event.preventDefault()
|
|
519
|
-
event.stopPropagation()
|
|
520
|
-
onGitPaneResizeStart?.({ initialWidth: width, screenStart: event.x, side: position })
|
|
521
|
-
}}
|
|
550
|
+
onMouseDown={handleResizeMouseDown}
|
|
522
551
|
/>
|
|
523
552
|
)
|
|
524
553
|
return (
|