@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
1
|
import type { MouseEvent as OtuiMouseEvent } 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
7
|
import type { FocusMode, TabSession } from '../../../state/types'
|
|
@@ -68,15 +70,34 @@ export function SplitLayout({
|
|
|
68
70
|
tabs,
|
|
69
71
|
}: SplitLayoutProps) {
|
|
70
72
|
const t = useTheme()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const isActive = node.tabId === activeTabId
|
|
74
|
-
const paneOrigin: TerminalContentOrigin = {
|
|
73
|
+
const paneOrigin = useMemo<TerminalContentOrigin>(
|
|
74
|
+
() => ({
|
|
75
75
|
cols: Math.max(1, bounds.cols - PANE_CHROME * 2),
|
|
76
76
|
rows: Math.max(1, bounds.rows - PANE_CHROME * 2),
|
|
77
77
|
x: contentOrigin.x + bounds.x + PANE_CHROME,
|
|
78
78
|
y: contentOrigin.y + bounds.y + PANE_CHROME,
|
|
79
|
-
}
|
|
79
|
+
}),
|
|
80
|
+
[bounds, contentOrigin]
|
|
81
|
+
)
|
|
82
|
+
const handleSeparatorMouseDown = useCallback(
|
|
83
|
+
(e: OtuiMouseEvent) => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
if (node.type !== 'split' || !onSeparatorDragStart) return
|
|
86
|
+
const leafId = getFirstLeafId(node.first)
|
|
87
|
+
if (!(leafId != null && leafId !== '')) return
|
|
88
|
+
onSeparatorDragStart({
|
|
89
|
+
direction: node.direction,
|
|
90
|
+
screenStart:
|
|
91
|
+
node.direction === 'vertical' ? contentOrigin.x + bounds.x : contentOrigin.y + bounds.y,
|
|
92
|
+
tabId: leafId,
|
|
93
|
+
totalSize: node.direction === 'vertical' ? bounds.cols : bounds.rows,
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
[bounds, contentOrigin, node, onSeparatorDragStart]
|
|
97
|
+
)
|
|
98
|
+
if (node.type === 'leaf') {
|
|
99
|
+
const tab = tabs.find((t) => t.id === node.tabId)
|
|
100
|
+
const isActive = node.tabId === activeTabId
|
|
80
101
|
logInputDebug('split.paneOrigin', {
|
|
81
102
|
boundsCols: bounds.cols,
|
|
82
103
|
boundsRows: bounds.rows,
|
|
@@ -157,21 +178,7 @@ export function SplitLayout({
|
|
|
157
178
|
minWidth={node.direction === 'vertical' ? 1 : undefined}
|
|
158
179
|
minHeight={node.direction === 'horizontal' ? 1 : undefined}
|
|
159
180
|
backgroundColor={t.border}
|
|
160
|
-
onMouseDown={
|
|
161
|
-
e.preventDefault()
|
|
162
|
-
if (!onSeparatorDragStart) return
|
|
163
|
-
const leafId = getFirstLeafId(node.first)
|
|
164
|
-
if (!(leafId != null && leafId !== '')) return
|
|
165
|
-
onSeparatorDragStart({
|
|
166
|
-
direction: node.direction,
|
|
167
|
-
screenStart:
|
|
168
|
-
node.direction === 'vertical'
|
|
169
|
-
? contentOrigin.x + bounds.x
|
|
170
|
-
: contentOrigin.y + bounds.y,
|
|
171
|
-
tabId: leafId,
|
|
172
|
-
totalSize: node.direction === 'vertical' ? bounds.cols : bounds.rows,
|
|
173
|
-
})
|
|
174
|
-
}}
|
|
181
|
+
onMouseDown={handleSeparatorMouseDown}
|
|
175
182
|
/>
|
|
176
183
|
<box flexGrow={secondGrow} flexDirection="column" overflow="hidden">
|
|
177
184
|
<SplitLayout
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import { memo, type ReactNode } from 'react'
|
|
3
|
+
import { memo, type ReactNode, useCallback, useMemo } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
6
|
import type { FocusMode, TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
@@ -95,6 +95,8 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
95
95
|
return (
|
|
96
96
|
<text fg={t.text}>
|
|
97
97
|
{lines.map((line, lineIndex) => (
|
|
98
|
+
// Terminal rows are a fixed positional grid; the row index is the identity.
|
|
99
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
98
100
|
<span key={`line-${lineIndex}`}>
|
|
99
101
|
{line.spans.map((span, spanIndex) => renderSpan(span, `s-${spanIndex}`))}
|
|
100
102
|
{lineIndex < lines.length - 1 ? '\n' : ''}
|
|
@@ -132,125 +134,169 @@ export function TerminalPane({
|
|
|
132
134
|
const paneIsActive = isActive ?? true
|
|
133
135
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
134
136
|
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
135
|
-
const rightClickMenu
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
event
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
event.
|
|
173
|
-
event.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
event.type === 'down' &&
|
|
179
|
-
event.button === 0 &&
|
|
180
|
-
onLeftEdgeMouseDown &&
|
|
181
|
-
event.x === contentOrigin.x - 1
|
|
182
|
-
) {
|
|
183
|
-
if (onLeftEdgeMouseDown(event)) {
|
|
137
|
+
const rightClickMenu = useMemo<ContextMenuItem[] | undefined>(
|
|
138
|
+
() =>
|
|
139
|
+
tabId != null && tabId !== ''
|
|
140
|
+
? [
|
|
141
|
+
[
|
|
142
|
+
'Split vertically',
|
|
143
|
+
() =>
|
|
144
|
+
runSideEffectGlobal({
|
|
145
|
+
direction: 'vertical',
|
|
146
|
+
sourceTabId: tabId,
|
|
147
|
+
type: 'split-pane',
|
|
148
|
+
}),
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
'Split horizontally',
|
|
152
|
+
() =>
|
|
153
|
+
runSideEffectGlobal({
|
|
154
|
+
direction: 'horizontal',
|
|
155
|
+
sourceTabId: tabId,
|
|
156
|
+
type: 'split-pane',
|
|
157
|
+
}),
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
'Close pane',
|
|
161
|
+
() => {
|
|
162
|
+
dispatchGlobal({ tabId, type: 'close-pane' })
|
|
163
|
+
runSideEffectGlobal({ tabId, type: 'close-tab' })
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
]
|
|
167
|
+
: undefined,
|
|
168
|
+
[tabId]
|
|
169
|
+
)
|
|
170
|
+
const isOnPaneBorder = useCallback(
|
|
171
|
+
(event: OtuiMouseEvent) =>
|
|
172
|
+
event.x === contentOrigin.x - 1 ||
|
|
173
|
+
event.x === contentOrigin.x + contentOrigin.cols ||
|
|
174
|
+
event.y === contentOrigin.y - 1 ||
|
|
175
|
+
event.y === contentOrigin.y + contentOrigin.rows,
|
|
176
|
+
[contentOrigin]
|
|
177
|
+
)
|
|
178
|
+
const forwardMouseEvent = useCallback(
|
|
179
|
+
(event: OtuiMouseEvent) => {
|
|
180
|
+
if (event.type === 'down' && event.button === 2 && rightClickMenu) {
|
|
184
181
|
event.preventDefault()
|
|
185
182
|
event.stopPropagation()
|
|
183
|
+
openContextMenu(event.x, event.y, rightClickMenu)
|
|
186
184
|
return
|
|
187
185
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
logInputDebug('pane.mouseDown', {
|
|
200
|
-
button: event.button,
|
|
201
|
-
canForward: canForwardMouse,
|
|
202
|
-
eventType: event.type,
|
|
203
|
-
tabId,
|
|
204
|
-
willClick: !canForwardMouse && !!tab && event.button === 0 && !!onTerminalClick,
|
|
205
|
-
x: event.x,
|
|
206
|
-
y: event.y,
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
if (event.type === 'drag') {
|
|
210
|
-
if (onTerminalDrag?.(event, contentOrigin, tabId) === true) {
|
|
211
|
-
event.preventDefault()
|
|
212
|
-
return
|
|
186
|
+
if (
|
|
187
|
+
event.type === 'down' &&
|
|
188
|
+
event.button === 0 &&
|
|
189
|
+
onLeftEdgeMouseDown &&
|
|
190
|
+
event.x === contentOrigin.x - 1
|
|
191
|
+
) {
|
|
192
|
+
if (onLeftEdgeMouseDown(event)) {
|
|
193
|
+
event.preventDefault()
|
|
194
|
+
event.stopPropagation()
|
|
195
|
+
return
|
|
196
|
+
}
|
|
213
197
|
}
|
|
214
|
-
|
|
198
|
+
// Absorb left-button clicks on pane borders — they should not focus the
|
|
199
|
+
// pane or be forwarded to the terminal. This keeps borders available for
|
|
200
|
+
// resize actions (split separators, sidebar edge) without conflicting
|
|
201
|
+
// with focus-on-click behavior in the content area.
|
|
202
|
+
if (event.type === 'down' && event.button === 0 && isOnPaneBorder(event)) {
|
|
215
203
|
event.preventDefault()
|
|
204
|
+
event.stopPropagation()
|
|
216
205
|
return
|
|
217
206
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
207
|
+
if (event.type === 'down') {
|
|
208
|
+
logInputDebug('pane.mouseDown', {
|
|
209
|
+
button: event.button,
|
|
210
|
+
canForward: canForwardMouse,
|
|
211
|
+
eventType: event.type,
|
|
212
|
+
tabId,
|
|
213
|
+
willClick: !canForwardMouse && !!tab && event.button === 0 && !!onTerminalClick,
|
|
214
|
+
x: event.x,
|
|
215
|
+
y: event.y,
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
if (event.type === 'drag') {
|
|
219
|
+
if (onTerminalDrag?.(event, contentOrigin, tabId) === true) {
|
|
220
|
+
event.preventDefault()
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (onSeparatorDrag?.(event) === true) {
|
|
224
|
+
event.preventDefault()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (event.type === 'up') {
|
|
229
|
+
if (onTerminalMouseUp?.(event) === true) {
|
|
230
|
+
event.preventDefault()
|
|
231
|
+
}
|
|
232
|
+
onSeparatorDragEnd?.()
|
|
222
233
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
if (tabId != null && tabId !== '' && onPaneActivate && event.type === 'down') {
|
|
235
|
+
onPaneActivate(tabId)
|
|
236
|
+
}
|
|
237
|
+
if (!canForwardMouse) {
|
|
238
|
+
if (tab && event.type === 'down' && event.button === 0 && onTerminalClick) {
|
|
239
|
+
onTerminalClick(event, contentOrigin, tabId)
|
|
240
|
+
}
|
|
241
|
+
return
|
|
231
242
|
}
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
243
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
244
|
+
event.preventDefault()
|
|
245
|
+
event.stopPropagation()
|
|
246
|
+
onTerminalMouseEvent(event, contentOrigin)
|
|
247
|
+
},
|
|
248
|
+
[
|
|
249
|
+
canForwardMouse,
|
|
250
|
+
contentOrigin,
|
|
251
|
+
isOnPaneBorder,
|
|
252
|
+
onLeftEdgeMouseDown,
|
|
253
|
+
onPaneActivate,
|
|
254
|
+
onSeparatorDrag,
|
|
255
|
+
onSeparatorDragEnd,
|
|
256
|
+
onTerminalClick,
|
|
257
|
+
onTerminalDrag,
|
|
258
|
+
onTerminalMouseEvent,
|
|
259
|
+
onTerminalMouseUp,
|
|
260
|
+
rightClickMenu,
|
|
261
|
+
tab,
|
|
262
|
+
tabId,
|
|
263
|
+
]
|
|
264
|
+
)
|
|
265
|
+
const forwardScrollEvent = useCallback(
|
|
266
|
+
(event: OtuiMouseEvent) => {
|
|
267
|
+
if (!canForwardMouse && !canUseLocalScrollback) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
243
270
|
|
|
244
|
-
|
|
245
|
-
|
|
271
|
+
event.preventDefault()
|
|
272
|
+
event.stopPropagation()
|
|
246
273
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
274
|
+
if (canForwardMouse) {
|
|
275
|
+
onTerminalMouseEvent(event, contentOrigin)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
251
278
|
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
onTerminalScrollEvent(event)
|
|
280
|
+
},
|
|
281
|
+
[
|
|
282
|
+
canForwardMouse,
|
|
283
|
+
canUseLocalScrollback,
|
|
284
|
+
contentOrigin,
|
|
285
|
+
onTerminalMouseEvent,
|
|
286
|
+
onTerminalScrollEvent,
|
|
287
|
+
]
|
|
288
|
+
)
|
|
289
|
+
const handleNoTabMouseDown = useCallback((event: OtuiMouseEvent) => {
|
|
290
|
+
event.stopPropagation()
|
|
291
|
+
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
292
|
+
}, [])
|
|
293
|
+
const handleContentMouseDown = useCallback(
|
|
294
|
+
(e: OtuiMouseEvent) => {
|
|
295
|
+
e.stopPropagation()
|
|
296
|
+
forwardMouseEvent(e)
|
|
297
|
+
},
|
|
298
|
+
[forwardMouseEvent]
|
|
299
|
+
)
|
|
254
300
|
return (
|
|
255
301
|
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
256
302
|
<ContextMenuBox
|
|
@@ -282,10 +328,7 @@ export function TerminalPane({
|
|
|
282
328
|
marginTop={1}
|
|
283
329
|
paddingX={2}
|
|
284
330
|
backgroundColor={t.backgroundPanel}
|
|
285
|
-
onMouseDown={
|
|
286
|
-
event.stopPropagation()
|
|
287
|
-
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
288
|
-
}}
|
|
331
|
+
onMouseDown={handleNoTabMouseDown}
|
|
289
332
|
>
|
|
290
333
|
<text fg={t.text}>New assistant</text>
|
|
291
334
|
</box>
|
|
@@ -297,10 +340,7 @@ export function TerminalPane({
|
|
|
297
340
|
flexGrow={1}
|
|
298
341
|
width="100%"
|
|
299
342
|
overflow="hidden"
|
|
300
|
-
onMouseDown={
|
|
301
|
-
e.stopPropagation()
|
|
302
|
-
forwardMouseEvent(e)
|
|
303
|
-
}}
|
|
343
|
+
onMouseDown={handleContentMouseDown}
|
|
304
344
|
onMouseUp={forwardMouseEvent}
|
|
305
345
|
onMouseDrag={forwardMouseEvent}
|
|
306
346
|
onMouseScroll={forwardScrollEvent}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
-
import { useLayoutEffect, useMemo } from 'react'
|
|
3
|
+
import { useCallback, useLayoutEffect, useMemo } from 'react'
|
|
4
4
|
|
|
5
5
|
import { collectHelpEntries, HELP_MODE_LABELS } from '../../../../input/keymap/help-entries'
|
|
6
6
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
@@ -48,22 +48,29 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
48
48
|
dispatchGlobal({ count: filtered.length, type: 'set-help-entry-count' })
|
|
49
49
|
}, [filtered.length])
|
|
50
50
|
|
|
51
|
-
const items
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
const items = useMemo<PickerItem[]>(
|
|
52
|
+
() =>
|
|
53
|
+
filtered.map((entry, index) => ({
|
|
54
|
+
group: entry.modeLabel,
|
|
55
|
+
key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
|
|
56
|
+
title: (
|
|
57
|
+
<text fg={t.textMuted} wrapMode="none">
|
|
58
|
+
{entry.description ?? ''}
|
|
59
|
+
</text>
|
|
60
|
+
),
|
|
61
|
+
trailing: (
|
|
62
|
+
<text fg={t.textMuted} wrapMode="none">
|
|
63
|
+
{entry.keysDisplay}
|
|
64
|
+
</text>
|
|
65
|
+
),
|
|
66
|
+
})),
|
|
67
|
+
[filtered, t]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const handleHover = useCallback(
|
|
71
|
+
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
72
|
+
[]
|
|
73
|
+
)
|
|
67
74
|
|
|
68
75
|
return (
|
|
69
76
|
<Picker
|
|
@@ -79,7 +86,7 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
79
86
|
{filter != null && filter !== '' ? 'No matching bindings.' : 'No bindings registered.'}
|
|
80
87
|
</text>
|
|
81
88
|
}
|
|
82
|
-
onHover={
|
|
89
|
+
onHover={handleHover}
|
|
83
90
|
/>
|
|
84
91
|
)
|
|
85
92
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
|
|
1
3
|
import { isAutoCommitEnabled } from '@brimveyn/aimux-config'
|
|
2
|
-
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
3
5
|
|
|
4
6
|
import type { ModeId } from '../../../../input/modes/types'
|
|
5
7
|
|
|
@@ -99,7 +101,7 @@ export function GitCommitModal({
|
|
|
99
101
|
(s) => s.gitPanel.files.filter((f) => f.section === 'staged').length
|
|
100
102
|
)
|
|
101
103
|
|
|
102
|
-
const onAutoCommitClick = (): void => {
|
|
104
|
+
const onAutoCommitClick = useCallback((): void => {
|
|
103
105
|
const hasTitle = title.trim().length > 0
|
|
104
106
|
if (hasTitle || !(currentSessionId != null && currentSessionId !== '')) {
|
|
105
107
|
dispatchGlobal({ type: 'git-commit-enter-confirm' })
|
|
@@ -113,7 +115,16 @@ export function GitCommitModal({
|
|
|
113
115
|
if (bgKind !== 'generating') {
|
|
114
116
|
runSideEffectGlobal({ sessionId: currentSessionId, type: 'generate-auto-commit-now' })
|
|
115
117
|
}
|
|
116
|
-
}
|
|
118
|
+
}, [bgKind, currentSessionId, title])
|
|
119
|
+
|
|
120
|
+
const handleAutoCommitMouseDown = useCallback(
|
|
121
|
+
(event: OtuiMouseEvent) => {
|
|
122
|
+
event.preventDefault()
|
|
123
|
+
event.stopPropagation()
|
|
124
|
+
onAutoCommitClick()
|
|
125
|
+
},
|
|
126
|
+
[onAutoCommitClick]
|
|
127
|
+
)
|
|
117
128
|
|
|
118
129
|
const modeId = pickModeId(stage)
|
|
119
130
|
const shellTitle = pickShellTitle(stage)
|
|
@@ -169,11 +180,7 @@ export function GitCommitModal({
|
|
|
169
180
|
flexDirection="row"
|
|
170
181
|
gap={1}
|
|
171
182
|
alignItems="center"
|
|
172
|
-
onMouseDown={
|
|
173
|
-
event.preventDefault()
|
|
174
|
-
event.stopPropagation()
|
|
175
|
-
onAutoCommitClick()
|
|
176
|
-
}}
|
|
183
|
+
onMouseDown={handleAutoCommitMouseDown}
|
|
177
184
|
>
|
|
178
185
|
{showBgSpinner ? <text fg={t.primary}>{bgSpinnerFrame}</text> : null}
|
|
179
186
|
<text fg={t.text}>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { DirectoryResult } from '../../../../state/types'
|
|
2
4
|
|
|
3
5
|
import { abbreviatePath } from '../../../path-format'
|
|
@@ -41,15 +43,17 @@ export function CreateSessionModal({
|
|
|
41
43
|
const dirActive = activeField === 'directory'
|
|
42
44
|
const nameActive = activeField === 'name'
|
|
43
45
|
|
|
44
|
-
const items
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
const items = useMemo<FormOptionItem[]>(
|
|
47
|
+
() =>
|
|
48
|
+
results.map((result) => ({
|
|
49
|
+
key: result.path,
|
|
50
|
+
leading: <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>,
|
|
51
|
+
title: (active) => (
|
|
52
|
+
<text fg={active ? t.text : t.textMuted}>{abbreviatePath(result.path)}</text>
|
|
53
|
+
),
|
|
54
|
+
})),
|
|
55
|
+
[results, t]
|
|
56
|
+
)
|
|
53
57
|
|
|
54
58
|
return (
|
|
55
59
|
<Form
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { SessionRecord } from '../../../../state/types'
|
|
2
4
|
|
|
3
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
@@ -44,41 +46,46 @@ export function SessionPickerModal({
|
|
|
44
46
|
sessions,
|
|
45
47
|
}: SessionPickerModalProps) {
|
|
46
48
|
const t = useTheme()
|
|
47
|
-
const ordered = orderSessionsForDisplay(sessions)
|
|
48
|
-
const
|
|
49
|
-
const filtered = filterSessions(ordered, filter)
|
|
49
|
+
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
50
|
+
const filtered = useMemo(() => filterSessions(ordered, filter), [filter, ordered])
|
|
50
51
|
const hasFilter = !!(filter != null && filter !== '')
|
|
51
52
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const items = useMemo<PickerItem[]>(() => {
|
|
54
|
+
const baselineOrder = ordered.map((s) => s.id)
|
|
55
|
+
const sessionItems: PickerItem[] = filtered.map((session, index) => {
|
|
56
|
+
const active = index === selectedIndex
|
|
57
|
+
const displayIndex = baselineOrder.indexOf(session.id) + 1
|
|
58
|
+
return {
|
|
59
|
+
key: session.id,
|
|
60
|
+
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
61
|
+
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
|
|
62
|
+
subtitle:
|
|
63
|
+
session.projectPath != null && session.projectPath !== '' ? (
|
|
64
|
+
<text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
|
|
65
|
+
) : undefined,
|
|
66
|
+
title: (
|
|
67
|
+
<text fg={active ? t.text : t.textMuted}>
|
|
68
|
+
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|
|
69
|
+
</text>
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
const createNewItem: PickerItem = {
|
|
74
|
+
key: '__create-new__',
|
|
57
75
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
58
|
-
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
|
|
59
|
-
subtitle:
|
|
60
|
-
session.projectPath != null && session.projectPath !== '' ? (
|
|
61
|
-
<text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
|
|
62
|
-
) : undefined,
|
|
63
76
|
title: (
|
|
64
|
-
<text fg={
|
|
65
|
-
|
|
77
|
+
<text fg={selectedIndex === filtered.length ? t.text : t.textMuted}>
|
|
78
|
+
Create new workspace
|
|
66
79
|
</text>
|
|
67
80
|
),
|
|
68
81
|
}
|
|
69
|
-
|
|
82
|
+
return [...sessionItems, createNewItem]
|
|
83
|
+
}, [currentSessionId, currentTabCount, filtered, ordered, selectedIndex, t])
|
|
70
84
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<text fg={selectedIndex === filtered.length ? t.text : t.textMuted}>
|
|
76
|
-
Create new workspace
|
|
77
|
-
</text>
|
|
78
|
-
),
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const items = [...sessionItems, createNewItem]
|
|
85
|
+
const handleHover = useCallback(
|
|
86
|
+
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
87
|
+
[]
|
|
88
|
+
)
|
|
82
89
|
|
|
83
90
|
return (
|
|
84
91
|
<Picker
|
|
@@ -95,7 +102,7 @@ export function SessionPickerModal({
|
|
|
95
102
|
<text fg={t.textMuted}>{getEmptyStateMessage(hasFilter)}</text>
|
|
96
103
|
) : undefined
|
|
97
104
|
}
|
|
98
|
-
onHover={
|
|
105
|
+
onHover={handleHover}
|
|
99
106
|
/>
|
|
100
107
|
)
|
|
101
108
|
}
|
|
@@ -147,12 +147,13 @@ export function AutoComplete({
|
|
|
147
147
|
return (
|
|
148
148
|
<ListItem
|
|
149
149
|
key={item.key}
|
|
150
|
+
index={optionIndex}
|
|
150
151
|
active={optionActive}
|
|
151
152
|
leading={resolveItemContent(item.leading, optionActive)}
|
|
152
153
|
title={resolveItemContent(item.title, optionActive)}
|
|
153
154
|
subtitle={resolveItemContent(item.subtitle, optionActive)}
|
|
154
155
|
trailing={resolveItemContent(item.trailing, optionActive)}
|
|
155
|
-
|
|
156
|
+
onHoverIndex={onHover}
|
|
156
157
|
onClick={item.onClick}
|
|
157
158
|
/>
|
|
158
159
|
)
|