@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,8 +1,8 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
-
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
+
import type { MouseEvent as OtuiMouseEvent, ScrollBoxRenderable } from '@opentui/core'
|
|
3
3
|
|
|
4
4
|
import { useTerminalDimensions } from '@opentui/react'
|
|
5
|
-
import { type ReactNode, useLayoutEffect, useRef } from 'react'
|
|
5
|
+
import { type ReactNode, useCallback, useLayoutEffect, useMemo, useRef } from 'react'
|
|
6
6
|
|
|
7
7
|
import { useTheme } from '../../../theme'
|
|
8
8
|
import { BareInput } from '../../primitives/bare-input'
|
|
@@ -49,35 +49,41 @@ function PickerItemCtas({
|
|
|
49
49
|
onWorktree?: () => void
|
|
50
50
|
}) {
|
|
51
51
|
const t = useTheme()
|
|
52
|
+
const handleEdit = useCallback(
|
|
53
|
+
(event: OtuiMouseEvent) => {
|
|
54
|
+
event.stopPropagation()
|
|
55
|
+
onEdit?.()
|
|
56
|
+
},
|
|
57
|
+
[onEdit]
|
|
58
|
+
)
|
|
59
|
+
const handleWorktree = useCallback(
|
|
60
|
+
(event: OtuiMouseEvent) => {
|
|
61
|
+
event.stopPropagation()
|
|
62
|
+
onWorktree?.()
|
|
63
|
+
},
|
|
64
|
+
[onWorktree]
|
|
65
|
+
)
|
|
66
|
+
const handleDelete = useCallback(
|
|
67
|
+
(event: OtuiMouseEvent) => {
|
|
68
|
+
event.stopPropagation()
|
|
69
|
+
onDelete?.()
|
|
70
|
+
},
|
|
71
|
+
[onDelete]
|
|
72
|
+
)
|
|
52
73
|
return (
|
|
53
74
|
<box flexDirection="row" gap={1}>
|
|
54
75
|
{onEdit ? (
|
|
55
|
-
<box
|
|
56
|
-
onMouseDown={(event) => {
|
|
57
|
-
event.stopPropagation()
|
|
58
|
-
onEdit()
|
|
59
|
-
}}
|
|
60
|
-
>
|
|
76
|
+
<box onMouseDown={handleEdit}>
|
|
61
77
|
<text fg={t.primary}>[edit]</text>
|
|
62
78
|
</box>
|
|
63
79
|
) : null}
|
|
64
80
|
{onWorktree ? (
|
|
65
|
-
<box
|
|
66
|
-
onMouseDown={(event) => {
|
|
67
|
-
event.stopPropagation()
|
|
68
|
-
onWorktree()
|
|
69
|
-
}}
|
|
70
|
-
>
|
|
81
|
+
<box onMouseDown={handleWorktree}>
|
|
71
82
|
<text fg={t.warning}>[WT]</text>
|
|
72
83
|
</box>
|
|
73
84
|
) : null}
|
|
74
85
|
{onDelete ? (
|
|
75
|
-
<box
|
|
76
|
-
onMouseDown={(event) => {
|
|
77
|
-
event.stopPropagation()
|
|
78
|
-
onDelete()
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
86
|
+
<box onMouseDown={handleDelete}>
|
|
81
87
|
<text fg={t.error}>[del]</text>
|
|
82
88
|
</box>
|
|
83
89
|
) : null}
|
|
@@ -109,13 +115,29 @@ export function Picker({
|
|
|
109
115
|
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
110
116
|
const skipNextScrollRef = useRef(false)
|
|
111
117
|
|
|
112
|
-
const resetScrollTimer = () => {
|
|
118
|
+
const resetScrollTimer = useCallback(() => {
|
|
113
119
|
if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current)
|
|
114
120
|
scrollTimerRef.current = setTimeout(() => {
|
|
115
121
|
isScrollingRef.current = false
|
|
116
122
|
scrollTimerRef.current = null
|
|
117
123
|
}, 10)
|
|
118
|
-
}
|
|
124
|
+
}, [])
|
|
125
|
+
|
|
126
|
+
const handleScrollboxScroll = useCallback(() => {
|
|
127
|
+
isScrollingRef.current = true
|
|
128
|
+
resetScrollTimer()
|
|
129
|
+
}, [resetScrollTimer])
|
|
130
|
+
|
|
131
|
+
const handleItemHover = useCallback(
|
|
132
|
+
(index: number) => {
|
|
133
|
+
if (isScrollingRef.current) return
|
|
134
|
+
skipNextScrollRef.current = true
|
|
135
|
+
onHover(index)
|
|
136
|
+
},
|
|
137
|
+
[onHover]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const listContentOptions = useMemo(() => ({ flexDirection: 'column' as const, gap }), [gap])
|
|
119
141
|
|
|
120
142
|
useLayoutEffect(() => {
|
|
121
143
|
if (!scrollboxRef.current) return
|
|
@@ -128,7 +150,7 @@ export function Picker({
|
|
|
128
150
|
isScrollingRef.current = true
|
|
129
151
|
scrollboxRef.current.scrollChildIntoView(target)
|
|
130
152
|
resetScrollTimer()
|
|
131
|
-
}, [selectedIndex, items])
|
|
153
|
+
}, [selectedIndex, items, resetScrollTimer])
|
|
132
154
|
|
|
133
155
|
return (
|
|
134
156
|
<ModalShell
|
|
@@ -144,11 +166,8 @@ export function Picker({
|
|
|
144
166
|
ref={scrollboxRef}
|
|
145
167
|
scrollY
|
|
146
168
|
height={items.length === 0 ? 0 : listHeight}
|
|
147
|
-
contentOptions={
|
|
148
|
-
onMouseScroll={
|
|
149
|
-
isScrollingRef.current = true
|
|
150
|
-
resetScrollTimer()
|
|
151
|
-
}}
|
|
169
|
+
contentOptions={listContentOptions}
|
|
170
|
+
onMouseScroll={handleScrollboxScroll}
|
|
152
171
|
>
|
|
153
172
|
{(() => {
|
|
154
173
|
let prevGroup: string | undefined
|
|
@@ -181,15 +200,12 @@ export function Picker({
|
|
|
181
200
|
<ListItem
|
|
182
201
|
key={item.key}
|
|
183
202
|
id={item.key}
|
|
203
|
+
index={capturedIndex}
|
|
184
204
|
active={active}
|
|
185
205
|
title={item.title}
|
|
186
206
|
subtitle={item.subtitle}
|
|
187
207
|
trailing={trailing}
|
|
188
|
-
|
|
189
|
-
if (isScrollingRef.current) return
|
|
190
|
-
skipNextScrollRef.current = true
|
|
191
|
-
onHover(capturedIndex)
|
|
192
|
-
}}
|
|
208
|
+
onHoverIndex={handleItemHover}
|
|
193
209
|
onClick={item.onClick}
|
|
194
210
|
/>
|
|
195
211
|
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { SnippetRecord } from '../../../../state/types'
|
|
2
4
|
|
|
3
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
@@ -31,35 +33,46 @@ export function SnippetPickerModal({
|
|
|
31
33
|
snippets,
|
|
32
34
|
}: SnippetPickerModalProps) {
|
|
33
35
|
const t = useTheme()
|
|
34
|
-
const filtered = filterSnippets(snippets, filter)
|
|
36
|
+
const filtered = useMemo(() => filterSnippets(snippets, filter), [filter, snippets])
|
|
37
|
+
|
|
38
|
+
const items = useMemo<PickerItem[]>(
|
|
39
|
+
() =>
|
|
40
|
+
filtered.map((snippet, index) => {
|
|
41
|
+
const active = index === selectedIndex
|
|
42
|
+
const fromConfig = isConfigSnippetId(snippet.id)
|
|
43
|
+
return {
|
|
44
|
+
key: snippet.id,
|
|
45
|
+
onClick: () => {
|
|
46
|
+
dispatchGlobal({ type: 'close-modal' })
|
|
47
|
+
runSideEffectGlobal({ type: 'paste-selected-snippet' })
|
|
48
|
+
},
|
|
49
|
+
onDelete: fromConfig
|
|
50
|
+
? undefined
|
|
51
|
+
: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
|
|
52
|
+
onEdit: fromConfig
|
|
53
|
+
? undefined
|
|
54
|
+
: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
|
|
55
|
+
subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
56
|
+
title: (
|
|
57
|
+
<box flexDirection="row">
|
|
58
|
+
<text fg={active ? t.text : t.textMuted}>
|
|
59
|
+
<strong>{snippet.name}</strong>
|
|
60
|
+
</text>
|
|
61
|
+
{snippet.trigger != null && snippet.trigger !== '' ? (
|
|
62
|
+
<text fg={t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
63
|
+
) : null}
|
|
64
|
+
{fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
|
|
65
|
+
</box>
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
[filtered, selectedIndex, t]
|
|
70
|
+
)
|
|
35
71
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
key: snippet.id,
|
|
41
|
-
onClick: () => {
|
|
42
|
-
dispatchGlobal({ type: 'close-modal' })
|
|
43
|
-
runSideEffectGlobal({ type: 'paste-selected-snippet' })
|
|
44
|
-
},
|
|
45
|
-
onDelete: fromConfig
|
|
46
|
-
? undefined
|
|
47
|
-
: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
|
|
48
|
-
onEdit: fromConfig ? undefined : () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
|
|
49
|
-
subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
50
|
-
title: (
|
|
51
|
-
<box flexDirection="row">
|
|
52
|
-
<text fg={active ? t.text : t.textMuted}>
|
|
53
|
-
<strong>{snippet.name}</strong>
|
|
54
|
-
</text>
|
|
55
|
-
{snippet.trigger != null && snippet.trigger !== '' ? (
|
|
56
|
-
<text fg={t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
57
|
-
) : null}
|
|
58
|
-
{fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
|
|
59
|
-
</box>
|
|
60
|
-
),
|
|
61
|
-
}
|
|
62
|
-
})
|
|
72
|
+
const handleHover = useCallback(
|
|
73
|
+
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
74
|
+
[]
|
|
75
|
+
)
|
|
63
76
|
|
|
64
77
|
return (
|
|
65
78
|
<Picker
|
|
@@ -83,7 +96,7 @@ export function SnippetPickerModal({
|
|
|
83
96
|
<text fg={t.error}>{actionMessage}</text>
|
|
84
97
|
) : undefined
|
|
85
98
|
}
|
|
86
|
-
onHover={
|
|
99
|
+
onHover={handleHover}
|
|
87
100
|
/>
|
|
88
101
|
)
|
|
89
102
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { AssistantId, WorktreeRecord } from '../../../../state/types'
|
|
2
4
|
|
|
3
5
|
import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
|
|
@@ -68,11 +70,86 @@ export function NewTabModal({
|
|
|
68
70
|
worktrees,
|
|
69
71
|
}: NewTabModalProps) {
|
|
70
72
|
const t = useTheme()
|
|
73
|
+
const options = useMemo(() => getAllAssistantOptions(customCommands), [customCommands])
|
|
74
|
+
const filtered = useMemo(() => filterAssistants(options, filter), [filter, options])
|
|
75
|
+
|
|
76
|
+
const handleHover = useCallback(
|
|
77
|
+
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
78
|
+
[]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const worktreeItems = useMemo<PickerItem[]>(
|
|
82
|
+
() => [
|
|
83
|
+
...worktrees.map((worktree, index) => {
|
|
84
|
+
const active = index === selectedIndex
|
|
85
|
+
const label = worktree.branch ?? worktree.name
|
|
86
|
+
const blockedReason = getDeleteBlockedReason({ currentSessionId, worktree, worktrees })
|
|
87
|
+
const canDelete = blockedReason == null || blockedReason === ''
|
|
88
|
+
return {
|
|
89
|
+
key: worktree.id,
|
|
90
|
+
onClick: () => {
|
|
91
|
+
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
92
|
+
runSideEffectGlobal({ type: 'launch-selected-assistant' })
|
|
93
|
+
},
|
|
94
|
+
onDelete:
|
|
95
|
+
active && canDelete && currentSessionId != null && currentSessionId !== ''
|
|
96
|
+
? () => {
|
|
97
|
+
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
98
|
+
dispatchGlobal({ message: null, type: 'set-new-tab-worktree-delete-state' })
|
|
99
|
+
runSideEffectGlobal({
|
|
100
|
+
force: worktreeDeleteConfirmId === worktree.id,
|
|
101
|
+
sessionId: currentSessionId,
|
|
102
|
+
type: 'delete-worktree',
|
|
103
|
+
worktreeId: worktree.id,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
: undefined,
|
|
107
|
+
subtitle: (
|
|
108
|
+
<box flexDirection="column">
|
|
109
|
+
<text fg={t.textMuted}>{worktree.path}</text>
|
|
110
|
+
<text fg={t.textMuted}>{worktree.source}</text>
|
|
111
|
+
</box>
|
|
112
|
+
),
|
|
113
|
+
title: <text fg={active ? t.text : t.textMuted}>{label}</text>,
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
{
|
|
117
|
+
key: '__create-worktree__',
|
|
118
|
+
onClick: () => dispatchGlobal({ type: 'enter-new-tab-worktree-create' }),
|
|
119
|
+
subtitle: <text fg={t.textMuted}>Create an Aimux temp worktree</text>,
|
|
120
|
+
title: <text fg={createWorktree ? t.text : t.textMuted}>Create new worktree</text>,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
[createWorktree, currentSessionId, selectedIndex, t, worktreeDeleteConfirmId, worktrees]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const items = useMemo<PickerItem[]>(
|
|
127
|
+
() =>
|
|
128
|
+
filtered.map((option, index) => {
|
|
129
|
+
const active = index === selectedIndex
|
|
130
|
+
const customCmd = customCommands[option.id]
|
|
131
|
+
return {
|
|
132
|
+
key: option.id,
|
|
133
|
+
onClick: () =>
|
|
134
|
+
dispatchGlobal({ assistantId: option.id, type: 'select-new-tab-assistant' }),
|
|
135
|
+
onEdit: () =>
|
|
136
|
+
dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
|
|
137
|
+
subtitle: (
|
|
138
|
+
<box flexDirection="column">
|
|
139
|
+
<text fg={t.textMuted}>{option.description}</text>
|
|
140
|
+
{customCmd != null && customCmd !== '' ? (
|
|
141
|
+
<text fg={t.primary}>{customCmd}</text>
|
|
142
|
+
) : null}
|
|
143
|
+
</box>
|
|
144
|
+
),
|
|
145
|
+
title: <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
|
|
146
|
+
}
|
|
147
|
+
}),
|
|
148
|
+
[customCommands, filtered, selectedIndex, t]
|
|
149
|
+
)
|
|
71
150
|
|
|
72
151
|
if (editingCommand !== null) {
|
|
73
|
-
const option =
|
|
74
|
-
getAllAssistantOptions(customCommands).find((o) => o.id === editingCommand) ??
|
|
75
|
-
getAssistantOption(0)
|
|
152
|
+
const option = options.find((o) => o.id === editingCommand) ?? getAssistantOption(0)
|
|
76
153
|
return (
|
|
77
154
|
<Form
|
|
78
155
|
title={`Edit command — ${option.label}`}
|
|
@@ -90,9 +167,6 @@ export function NewTabModal({
|
|
|
90
167
|
)
|
|
91
168
|
}
|
|
92
169
|
|
|
93
|
-
const options = getAllAssistantOptions(customCommands)
|
|
94
|
-
const filtered = filterAssistants(options, filter)
|
|
95
|
-
|
|
96
170
|
if (step === 'worktree-create') {
|
|
97
171
|
const selectedAssistant =
|
|
98
172
|
options.find((option) => option.id === selectedAssistantId) ?? options[0]
|
|
@@ -139,47 +213,6 @@ export function NewTabModal({
|
|
|
139
213
|
worktree: selectedWorktree,
|
|
140
214
|
worktrees,
|
|
141
215
|
})
|
|
142
|
-
const worktreeItems: PickerItem[] = [
|
|
143
|
-
...worktrees.map((worktree, index) => {
|
|
144
|
-
const active = index === selectedIndex
|
|
145
|
-
const label = worktree.branch ?? worktree.name
|
|
146
|
-
const blockedReason = getDeleteBlockedReason({ currentSessionId, worktree, worktrees })
|
|
147
|
-
const canDelete = blockedReason == null || blockedReason === ''
|
|
148
|
-
return {
|
|
149
|
-
key: worktree.id,
|
|
150
|
-
onClick: () => {
|
|
151
|
-
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
152
|
-
runSideEffectGlobal({ type: 'launch-selected-assistant' })
|
|
153
|
-
},
|
|
154
|
-
onDelete:
|
|
155
|
-
active && canDelete && currentSessionId != null && currentSessionId !== ''
|
|
156
|
-
? () => {
|
|
157
|
-
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
158
|
-
dispatchGlobal({ message: null, type: 'set-new-tab-worktree-delete-state' })
|
|
159
|
-
runSideEffectGlobal({
|
|
160
|
-
force: worktreeDeleteConfirmId === worktree.id,
|
|
161
|
-
sessionId: currentSessionId,
|
|
162
|
-
type: 'delete-worktree',
|
|
163
|
-
worktreeId: worktree.id,
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
: undefined,
|
|
167
|
-
subtitle: (
|
|
168
|
-
<box flexDirection="column">
|
|
169
|
-
<text fg={t.textMuted}>{worktree.path}</text>
|
|
170
|
-
<text fg={t.textMuted}>{worktree.source}</text>
|
|
171
|
-
</box>
|
|
172
|
-
),
|
|
173
|
-
title: <text fg={active ? t.text : t.textMuted}>{label}</text>,
|
|
174
|
-
}
|
|
175
|
-
}),
|
|
176
|
-
{
|
|
177
|
-
key: '__create-worktree__',
|
|
178
|
-
onClick: () => dispatchGlobal({ type: 'enter-new-tab-worktree-create' }),
|
|
179
|
-
subtitle: <text fg={t.textMuted}>Create an Aimux temp worktree</text>,
|
|
180
|
-
title: <text fg={createWorktree ? t.text : t.textMuted}>Create new worktree</text>,
|
|
181
|
-
},
|
|
182
|
-
]
|
|
183
216
|
return (
|
|
184
217
|
<Picker
|
|
185
218
|
title={`New assistant: ${selectedAssistant?.label ?? 'assistant'}`}
|
|
@@ -190,7 +223,7 @@ export function NewTabModal({
|
|
|
190
223
|
items={worktreeItems}
|
|
191
224
|
selectedIndex={selectedIndex}
|
|
192
225
|
emptyState={<text fg={t.textMuted}>No worktrees.</text>}
|
|
193
|
-
onHover={
|
|
226
|
+
onHover={handleHover}
|
|
194
227
|
footer={
|
|
195
228
|
<box flexDirection="column">
|
|
196
229
|
{(worktreeDeleteMessage != null && worktreeDeleteMessage !== '') ||
|
|
@@ -213,23 +246,6 @@ export function NewTabModal({
|
|
|
213
246
|
)
|
|
214
247
|
}
|
|
215
248
|
|
|
216
|
-
const items: PickerItem[] = filtered.map((option, index) => {
|
|
217
|
-
const active = index === selectedIndex
|
|
218
|
-
const customCmd = customCommands[option.id]
|
|
219
|
-
return {
|
|
220
|
-
key: option.id,
|
|
221
|
-
onClick: () => dispatchGlobal({ assistantId: option.id, type: 'select-new-tab-assistant' }),
|
|
222
|
-
onEdit: () => dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
|
|
223
|
-
subtitle: (
|
|
224
|
-
<box flexDirection="column">
|
|
225
|
-
<text fg={t.textMuted}>{option.description}</text>
|
|
226
|
-
{customCmd != null && customCmd !== '' ? <text fg={t.primary}>{customCmd}</text> : null}
|
|
227
|
-
</box>
|
|
228
|
-
),
|
|
229
|
-
title: <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
|
|
233
249
|
return (
|
|
234
250
|
<Picker
|
|
235
251
|
title="New assistant: choose assistant"
|
|
@@ -241,7 +257,7 @@ export function NewTabModal({
|
|
|
241
257
|
items={items}
|
|
242
258
|
selectedIndex={selectedIndex}
|
|
243
259
|
emptyState={<text fg={t.textMuted}>No matching assistants.</text>}
|
|
244
|
-
onHover={
|
|
260
|
+
onHover={handleHover}
|
|
245
261
|
footer={
|
|
246
262
|
<box flexDirection="column">
|
|
247
263
|
<text fg={t.textMuted}>Step 1/2: choose assistant</text>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect, useMemo } from 'react'
|
|
1
|
+
import { useCallback, useLayoutEffect, useMemo } from 'react'
|
|
2
2
|
|
|
3
3
|
import type { ThemeId } from '../../../themes'
|
|
4
4
|
|
|
@@ -37,19 +37,28 @@ export function ThemePickerModal({
|
|
|
37
37
|
|
|
38
38
|
const effectiveIndex = clampSelection(selectedIndex, filtered.length)
|
|
39
39
|
|
|
40
|
-
const items
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
const items = useMemo<PickerItem[]>(
|
|
41
|
+
() =>
|
|
42
|
+
filtered.map((id, rowIndex) => {
|
|
43
|
+
const active = rowIndex === effectiveIndex
|
|
44
|
+
const isCurrent = id === currentThemeId
|
|
45
|
+
return {
|
|
46
|
+
key: id,
|
|
47
|
+
onClick: () => {
|
|
48
|
+
dispatchGlobal({ type: 'close-modal' })
|
|
49
|
+
runSideEffectGlobal({ action: 'confirm', type: 'apply-theme' })
|
|
50
|
+
},
|
|
51
|
+
title: <text fg={active ? t.text : t.textMuted}>{themeDisplayName(id)}</text>,
|
|
52
|
+
trailing: isCurrent ? <text fg={t.primary}>current</text> : undefined,
|
|
53
|
+
}
|
|
54
|
+
}),
|
|
55
|
+
[currentThemeId, effectiveIndex, filtered, t]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const handleHover = useCallback(
|
|
59
|
+
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
60
|
+
[]
|
|
61
|
+
)
|
|
53
62
|
|
|
54
63
|
return (
|
|
55
64
|
<Picker
|
|
@@ -70,7 +79,7 @@ export function ThemePickerModal({
|
|
|
70
79
|
items={items}
|
|
71
80
|
selectedIndex={effectiveIndex}
|
|
72
81
|
emptyState={<text fg={t.textMuted}>No themes available.</text>}
|
|
73
|
-
onHover={
|
|
82
|
+
onHover={handleHover}
|
|
74
83
|
/>
|
|
75
84
|
)
|
|
76
85
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { WorktreeRecord } from '../../../../state/types'
|
|
2
4
|
|
|
3
5
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
@@ -10,8 +12,8 @@ interface WorktreeMoveModalProps {
|
|
|
10
12
|
deleteSource: boolean
|
|
11
13
|
divergence: Record<string, { ahead: number; behind: number }>
|
|
12
14
|
selectedIndex: number
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
sourceWorktreeId: string
|
|
16
|
+
worktrees: WorktreeRecord[]
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const MOVE_HINTS: [key: string, label: string][] = [
|
|
@@ -33,10 +35,24 @@ export function WorktreeMoveModal({
|
|
|
33
35
|
deleteSource,
|
|
34
36
|
divergence,
|
|
35
37
|
selectedIndex,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
sourceWorktreeId,
|
|
39
|
+
worktrees,
|
|
38
40
|
}: WorktreeMoveModalProps) {
|
|
39
41
|
const t = useTheme()
|
|
42
|
+
const source = useMemo(
|
|
43
|
+
() => worktrees.find((w) => w.id === sourceWorktreeId),
|
|
44
|
+
[sourceWorktreeId, worktrees]
|
|
45
|
+
)
|
|
46
|
+
const targets = useMemo(
|
|
47
|
+
() => worktrees.filter((w) => w.id !== sourceWorktreeId),
|
|
48
|
+
[sourceWorktreeId, worktrees]
|
|
49
|
+
)
|
|
50
|
+
const sourceLabel =
|
|
51
|
+
source?.branch != null && source.branch !== '' ? source.branch : (source?.name ?? 'worktree')
|
|
52
|
+
const handleSelectIndex = useCallback(
|
|
53
|
+
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
54
|
+
[]
|
|
55
|
+
)
|
|
40
56
|
return (
|
|
41
57
|
<ModalShell
|
|
42
58
|
title={`Move ${sourceLabel} →`}
|
|
@@ -74,9 +90,10 @@ export function WorktreeMoveModal({
|
|
|
74
90
|
<ListItem
|
|
75
91
|
key={worktree.id}
|
|
76
92
|
id={worktree.id}
|
|
93
|
+
index={index}
|
|
77
94
|
active={active}
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
onHoverIndex={handleSelectIndex}
|
|
96
|
+
onClickIndex={handleSelectIndex}
|
|
80
97
|
title={
|
|
81
98
|
<text fg={active ? t.text : t.textMuted} wrapMode="none">
|
|
82
99
|
{label}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AIUsageTool } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
+
import { useCallback } from 'react'
|
|
4
|
+
|
|
3
5
|
import { useAIUsageStore } from '../../../../state/ai-usage-store'
|
|
4
6
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
5
7
|
import { useTheme } from '../../../theme'
|
|
@@ -52,6 +54,15 @@ export function AIUsageIndicator() {
|
|
|
52
54
|
const enabled = useAIUsageStore((s) => s.enabled)
|
|
53
55
|
const snapshots = useAIUsageStore((s) => s.snapshots)
|
|
54
56
|
|
|
57
|
+
const openModal = useCallback(
|
|
58
|
+
(e: { preventDefault: () => void; stopPropagation: () => void }) => {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
e.stopPropagation()
|
|
61
|
+
dispatchGlobal({ type: 'open-ai-usage-modal' })
|
|
62
|
+
},
|
|
63
|
+
[]
|
|
64
|
+
)
|
|
65
|
+
|
|
55
66
|
if (!enabled) return null
|
|
56
67
|
|
|
57
68
|
const ordered: AIUsageTool[] = ['claude', 'codex']
|
|
@@ -59,12 +70,6 @@ export function AIUsageIndicator() {
|
|
|
59
70
|
.map((tool) => ({ snap: snapshots[tool], tool }))
|
|
60
71
|
.filter((entry) => entry.snap !== undefined)
|
|
61
72
|
|
|
62
|
-
const openModal = (e: { preventDefault: () => void; stopPropagation: () => void }) => {
|
|
63
|
-
e.preventDefault()
|
|
64
|
-
e.stopPropagation()
|
|
65
|
-
dispatchGlobal({ type: 'open-ai-usage-modal' })
|
|
66
|
-
}
|
|
67
|
-
|
|
68
73
|
if (entries.length === 0) {
|
|
69
74
|
return (
|
|
70
75
|
<box
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
import type { BoxProps } from '@opentui/react'
|
|
3
3
|
|
|
4
|
+
import { memo, useCallback } from 'react'
|
|
5
|
+
|
|
4
6
|
import { type ContextMenuItem, openContextMenu } from '../../../context-menu/controller'
|
|
5
7
|
|
|
6
8
|
interface ContextMenuBoxProps extends BoxProps {
|
|
7
9
|
rightClickMenu?: ContextMenuItem[]
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
export const ContextMenuBox = memo(function ContextMenuBox({
|
|
13
|
+
onMouseDown,
|
|
14
|
+
rightClickMenu,
|
|
15
|
+
...boxProps
|
|
16
|
+
}: ContextMenuBoxProps) {
|
|
17
|
+
const handleMouseDown = useCallback(
|
|
18
|
+
function (this: BoxRenderable, event: OtuiMouseEvent): void {
|
|
19
|
+
if (event.button === 2 && rightClickMenu && rightClickMenu.length > 0) {
|
|
20
|
+
event.preventDefault()
|
|
21
|
+
event.stopPropagation()
|
|
22
|
+
openContextMenu(event.x, event.y, rightClickMenu)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
onMouseDown?.call(this, event)
|
|
26
|
+
},
|
|
27
|
+
[onMouseDown, rightClickMenu]
|
|
28
|
+
)
|
|
20
29
|
return <box {...boxProps} onMouseDown={handleMouseDown} />
|
|
21
|
-
}
|
|
30
|
+
})
|