@brimveyn/aimux 1.14.15 → 1.14.16
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 +71 -33
- package/src/app.tsx +26 -1
- package/src/config.ts +4 -0
- package/src/git/worktree.ts +45 -0
- package/src/input/modes/bridge.ts +4 -0
- package/src/input/modes/transitions.ts +8 -1
- package/src/input/modes/types.ts +15 -1
- package/src/state/reducers/modal-state.ts +138 -19
- package/src/state/reducers/session-state.ts +11 -19
- package/src/state/reducers/tab-state.ts +17 -0
- package/src/state/selectors.ts +45 -1
- package/src/state/session-persistence.ts +23 -1
- package/src/state/types.ts +39 -7
- package/src/ui/components/layout/sidebar/worktree-row.tsx +12 -2
- package/src/ui/components/layout/top-tab-bar.tsx +146 -12
- package/src/ui/components/modals/shared/worktree-delete-confirm.tsx +39 -0
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +70 -23
- package/src/ui/root.tsx +14 -2
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
MouseEvent as OtuiMouseEvent,
|
|
4
|
+
ScrollBoxRenderable,
|
|
5
|
+
} from '@opentui/core'
|
|
2
6
|
|
|
3
|
-
import { memo, type ReactNode, useCallback, useMemo, useRef } from 'react'
|
|
7
|
+
import { memo, type ReactNode, useCallback, useMemo, useRef, useState } from 'react'
|
|
4
8
|
|
|
5
9
|
import type { FocusMode, TabSession } from '../../../state/types'
|
|
6
10
|
|
|
@@ -8,7 +12,8 @@ import { useWorktreeDivergencePolling } from '../../../git/worktree-divergence-p
|
|
|
8
12
|
import { useAppStore } from '../../../state/app-store'
|
|
9
13
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
10
14
|
import { filterTabsForActiveWorktree } from '../../../state/session-worktrees'
|
|
11
|
-
import { buildTabEntries, type GroupEntry } from '../../../state/tab-entries'
|
|
15
|
+
import { buildTabEntries, type GroupEntry, type TabEntry } from '../../../state/tab-entries'
|
|
16
|
+
import { moveIdToIdPosition } from '../../session-ordering'
|
|
12
17
|
import { useTheme } from '../../theme'
|
|
13
18
|
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
14
19
|
import { TabItem } from './sidebar/tab-item'
|
|
@@ -43,27 +48,51 @@ const TopTabCell = memo(function TopTabCell({
|
|
|
43
48
|
backgroundColor,
|
|
44
49
|
children,
|
|
45
50
|
entryId,
|
|
46
|
-
|
|
51
|
+
onDrag,
|
|
52
|
+
onDragCancel,
|
|
53
|
+
onDragStart,
|
|
54
|
+
onDrop,
|
|
55
|
+
setCellRef,
|
|
47
56
|
}: {
|
|
48
57
|
entryId: string
|
|
49
58
|
active: boolean
|
|
50
59
|
backgroundColor: string | undefined
|
|
51
|
-
|
|
60
|
+
setCellRef: (entryId: string, ref: BoxRenderable | null) => void
|
|
61
|
+
onDragStart: (entryId: string) => void
|
|
62
|
+
onDrag: (event: OtuiMouseEvent) => void
|
|
63
|
+
onDrop: () => void
|
|
64
|
+
onDragCancel: () => void
|
|
52
65
|
children: ReactNode
|
|
53
66
|
}) {
|
|
67
|
+
const handleRef = useCallback(
|
|
68
|
+
(r: BoxRenderable | null) => setCellRef(entryId, r),
|
|
69
|
+
[setCellRef, entryId]
|
|
70
|
+
)
|
|
54
71
|
const handleMouseDown = useCallback(
|
|
55
72
|
(event: OtuiMouseEvent) => {
|
|
73
|
+
event.preventDefault()
|
|
56
74
|
event.stopPropagation()
|
|
57
|
-
|
|
75
|
+
onDragStart(entryId)
|
|
76
|
+
},
|
|
77
|
+
[onDragStart, entryId]
|
|
78
|
+
)
|
|
79
|
+
const handleMouseUp = useCallback(
|
|
80
|
+
(event: OtuiMouseEvent) => {
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
onDrop()
|
|
58
83
|
},
|
|
59
|
-
[
|
|
84
|
+
[onDrop]
|
|
60
85
|
)
|
|
61
86
|
return (
|
|
62
87
|
<box
|
|
88
|
+
ref={handleRef}
|
|
63
89
|
backgroundColor={backgroundColor}
|
|
64
90
|
flexDirection="row"
|
|
65
91
|
flexShrink={0}
|
|
66
92
|
onMouseDown={handleMouseDown}
|
|
93
|
+
onMouseDrag={onDrag}
|
|
94
|
+
onMouseUp={handleMouseUp}
|
|
95
|
+
onMouseDragEnd={onDragCancel}
|
|
67
96
|
data-active={active ? 'true' : undefined}
|
|
68
97
|
>
|
|
69
98
|
{children}
|
|
@@ -147,6 +176,19 @@ function GroupTabItem({
|
|
|
147
176
|
)
|
|
148
177
|
}
|
|
149
178
|
|
|
179
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
180
|
+
if (a.length !== b.length) return false
|
|
181
|
+
for (let i = 0; i < a.length; i++) {
|
|
182
|
+
if (a[i] !== b[i]) return false
|
|
183
|
+
}
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Flatten an entry into the underlying tab ids, in display order. */
|
|
188
|
+
function entryTabIds(entry: TabEntry): string[] {
|
|
189
|
+
return entry.kind === 'single' ? [entry.tab.id] : entry.tabs.map((tab) => tab.id)
|
|
190
|
+
}
|
|
191
|
+
|
|
150
192
|
export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
151
193
|
const t = useTheme()
|
|
152
194
|
const headerBg = t.backgroundPanel
|
|
@@ -214,6 +256,89 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
214
256
|
[entries, activeTabId]
|
|
215
257
|
)
|
|
216
258
|
|
|
259
|
+
// --- Drag-and-drop reorder of the tab strip ---------------------------------
|
|
260
|
+
// Mirrors the workspace-list drag, but on the horizontal axis: entries are
|
|
261
|
+
// laid out left-to-right inside a scrollX box, so hit-testing is on x/width.
|
|
262
|
+
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
263
|
+
const [dragOrder, setDragOrder] = useState<string[] | null>(null)
|
|
264
|
+
const lastSwapWithRef = useRef<string | null>(null)
|
|
265
|
+
const cellRefs = useRef(new Map<string, BoxRenderable>())
|
|
266
|
+
|
|
267
|
+
const baselineOrder = useMemo(() => entries.map((e) => e.id), [entries])
|
|
268
|
+
|
|
269
|
+
const setCellRef = useCallback((id: string, ref: BoxRenderable | null): void => {
|
|
270
|
+
if (ref) cellRefs.current.set(id, ref)
|
|
271
|
+
else cellRefs.current.delete(id)
|
|
272
|
+
}, [])
|
|
273
|
+
|
|
274
|
+
const findEntryAtX = useCallback((x: number): string | null => {
|
|
275
|
+
for (const [id, ref] of cellRefs.current) {
|
|
276
|
+
if (x >= ref.x && x < ref.x + ref.width) return id
|
|
277
|
+
}
|
|
278
|
+
return null
|
|
279
|
+
}, [])
|
|
280
|
+
|
|
281
|
+
const handleDragStart = useCallback(
|
|
282
|
+
(id: string) => {
|
|
283
|
+
setDraggingId(id)
|
|
284
|
+
setDragOrder(baselineOrder)
|
|
285
|
+
lastSwapWithRef.current = null
|
|
286
|
+
},
|
|
287
|
+
[baselineOrder]
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
const handleDrag = useCallback(
|
|
291
|
+
(event: OtuiMouseEvent) => {
|
|
292
|
+
if (!(draggingId != null && draggingId !== '')) return
|
|
293
|
+
const hit = findEntryAtX(event.x)
|
|
294
|
+
if (hit === null || hit === draggingId) {
|
|
295
|
+
lastSwapWithRef.current = null
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
if (hit === lastSwapWithRef.current) return
|
|
299
|
+
setDragOrder((prev) => (prev ? moveIdToIdPosition(prev, draggingId, hit) : prev))
|
|
300
|
+
lastSwapWithRef.current = hit
|
|
301
|
+
},
|
|
302
|
+
[draggingId, findEntryAtX]
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const commitDrop = useCallback(() => {
|
|
306
|
+
const source = draggingId
|
|
307
|
+
const finalOrder = dragOrder
|
|
308
|
+
setDraggingId(null)
|
|
309
|
+
setDragOrder(null)
|
|
310
|
+
lastSwapWithRef.current = null
|
|
311
|
+
|
|
312
|
+
if (source == null || source === '' || !finalOrder) return
|
|
313
|
+
|
|
314
|
+
if (!arraysEqual(finalOrder, baselineOrder)) {
|
|
315
|
+
// Expand entries (groups collapse multiple tabs) back into a flat tab-id
|
|
316
|
+
// order, then let the reducer rewrite only the visible tabs' slots.
|
|
317
|
+
const byId = new Map(entries.map((e) => [e.id, e]))
|
|
318
|
+
const orderedTabIds = finalOrder.flatMap((id) => {
|
|
319
|
+
const entry = byId.get(id)
|
|
320
|
+
return entry ? entryTabIds(entry) : []
|
|
321
|
+
})
|
|
322
|
+
dispatchGlobal({ orderedTabIds, type: 'reorder-tabs' })
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// No reorder happened → treat as a plain click on the entry.
|
|
327
|
+
handleEntryActivate(source)
|
|
328
|
+
}, [baselineOrder, dragOrder, draggingId, entries, handleEntryActivate])
|
|
329
|
+
|
|
330
|
+
const cancelDrag = useCallback(() => {
|
|
331
|
+
setDraggingId(null)
|
|
332
|
+
setDragOrder(null)
|
|
333
|
+
lastSwapWithRef.current = null
|
|
334
|
+
}, [])
|
|
335
|
+
|
|
336
|
+
const visibleEntries = useMemo(() => {
|
|
337
|
+
if (dragOrder === null) return entries
|
|
338
|
+
const byId = new Map(entries.map((e) => [e.id, e]))
|
|
339
|
+
return dragOrder.map((id) => byId.get(id)).filter((e): e is TabEntry => e != null)
|
|
340
|
+
}, [dragOrder, entries])
|
|
341
|
+
|
|
217
342
|
const handleNewTab = useCallback((e: OtuiMouseEvent) => {
|
|
218
343
|
e.stopPropagation()
|
|
219
344
|
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
@@ -245,10 +370,11 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
245
370
|
viewportCulling
|
|
246
371
|
contentOptions={ROW_CONTENT_OPTIONS}
|
|
247
372
|
>
|
|
248
|
-
{
|
|
373
|
+
{visibleEntries.map((entry, index) => {
|
|
249
374
|
// [N] is shown only for the first 9 entries — that's the range
|
|
250
375
|
// Leader+1..9 can address.
|
|
251
376
|
const indexLabel = index < 9 ? `[${index + 1}]` : undefined
|
|
377
|
+
const dragging = entry.id === draggingId
|
|
252
378
|
if (entry.kind === 'single') {
|
|
253
379
|
const tab: TabSession = entry.tab
|
|
254
380
|
const isActive = tab.id === activeTabId
|
|
@@ -257,8 +383,12 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
257
383
|
key={entry.id}
|
|
258
384
|
entryId={entry.id}
|
|
259
385
|
active={isActive}
|
|
260
|
-
|
|
261
|
-
|
|
386
|
+
setCellRef={setCellRef}
|
|
387
|
+
onDragStart={handleDragStart}
|
|
388
|
+
onDrag={handleDrag}
|
|
389
|
+
onDrop={commitDrop}
|
|
390
|
+
onDragCancel={cancelDrag}
|
|
391
|
+
backgroundColor={isActive || dragging ? t.backgroundElement : undefined}
|
|
262
392
|
>
|
|
263
393
|
<TabItem
|
|
264
394
|
id={`top-tab-${tab.id}`}
|
|
@@ -277,8 +407,12 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
277
407
|
key={entry.id}
|
|
278
408
|
entryId={entry.id}
|
|
279
409
|
active={isActive}
|
|
280
|
-
|
|
281
|
-
|
|
410
|
+
setCellRef={setCellRef}
|
|
411
|
+
onDragStart={handleDragStart}
|
|
412
|
+
onDrag={handleDrag}
|
|
413
|
+
onDrop={commitDrop}
|
|
414
|
+
onDragCancel={cancelDrag}
|
|
415
|
+
backgroundColor={isActive || dragging ? t.backgroundElement : undefined}
|
|
282
416
|
>
|
|
283
417
|
<GroupTabItem
|
|
284
418
|
entry={entry}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { useTheme } from '../../../theme'
|
|
4
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
5
|
+
import { Form } from './form'
|
|
6
|
+
|
|
7
|
+
interface WorktreeDeleteConfirmProps {
|
|
8
|
+
keybindsModeId: ModeId
|
|
9
|
+
reason: string
|
|
10
|
+
worktreeLabel: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared confirmation dialog for a recoverable worktree delete. Used both inside
|
|
15
|
+
* the new-tab picker and as a standalone modal (sidebar "Remove worktree"); the
|
|
16
|
+
* keybinds mode wires Enter/y to confirm and Esc/n to cancel for each context.
|
|
17
|
+
*/
|
|
18
|
+
export function WorktreeDeleteConfirm({
|
|
19
|
+
keybindsModeId,
|
|
20
|
+
reason,
|
|
21
|
+
worktreeLabel,
|
|
22
|
+
}: WorktreeDeleteConfirmProps) {
|
|
23
|
+
const t = useTheme()
|
|
24
|
+
return (
|
|
25
|
+
<Form
|
|
26
|
+
title="Delete worktree?"
|
|
27
|
+
keybindsModeId={keybindsModeId}
|
|
28
|
+
width={uiTokens.modalWidth.md}
|
|
29
|
+
footer={<text fg={t.textMuted}>Enter / y to delete · Esc / n to cancel</text>}
|
|
30
|
+
>
|
|
31
|
+
<box flexDirection="column" gap={1}>
|
|
32
|
+
<text fg={t.text}>
|
|
33
|
+
Delete <strong>{worktreeLabel}</strong>?
|
|
34
|
+
</text>
|
|
35
|
+
<text fg={t.warning}>{reason}</text>
|
|
36
|
+
</box>
|
|
37
|
+
</Form>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -5,11 +5,16 @@ import type { AssistantId, WorktreeRecord } from '../../../../state/types'
|
|
|
5
5
|
|
|
6
6
|
import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
|
|
7
7
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildBaseRefOptions,
|
|
10
|
+
filterAssistants,
|
|
11
|
+
getTemplateNoneOffset,
|
|
12
|
+
} from '../../../../state/selectors'
|
|
9
13
|
import { useTheme } from '../../../theme'
|
|
10
14
|
import { uiTokens } from '../../../ui-tokens'
|
|
11
|
-
import { Form, TextField } from '../shared/form'
|
|
15
|
+
import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
|
|
12
16
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
17
|
+
import { WorktreeDeleteConfirm } from '../shared/worktree-delete-confirm'
|
|
13
18
|
|
|
14
19
|
interface NewTabModalProps {
|
|
15
20
|
selectedIndex: number
|
|
@@ -19,16 +24,18 @@ interface NewTabModalProps {
|
|
|
19
24
|
currentSessionId: string | null
|
|
20
25
|
editingCommand: AssistantId | null
|
|
21
26
|
editBuffer: string
|
|
22
|
-
activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
|
|
27
|
+
activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name' | 'base'
|
|
23
28
|
branchError: string | null
|
|
24
29
|
branchName: string
|
|
25
30
|
createWorktree: boolean
|
|
26
31
|
selectedAssistantId: AssistantId | null
|
|
27
32
|
step: 'assistant' | 'worktree' | 'worktree-create' | 'template'
|
|
28
|
-
|
|
29
|
-
worktreeDeleteMessage: string | null
|
|
33
|
+
worktreeDeletePrompt: { worktreeId: string; reason: string } | null
|
|
30
34
|
worktrees: WorktreeRecord[]
|
|
31
35
|
worktreeName: string
|
|
36
|
+
baseQuery: string
|
|
37
|
+
baseRef: string
|
|
38
|
+
baseBranches: string[]
|
|
32
39
|
worktreeTemplates: WorktreeTemplate[]
|
|
33
40
|
/**
|
|
34
41
|
* True when this modal is the worktree-aware new-tab flow. False when the
|
|
@@ -63,6 +70,9 @@ function getDeleteBlockedReason({
|
|
|
63
70
|
export function NewTabModal({
|
|
64
71
|
activeField,
|
|
65
72
|
allowTemplateShortcut,
|
|
73
|
+
baseBranches,
|
|
74
|
+
baseQuery,
|
|
75
|
+
baseRef,
|
|
66
76
|
branchError,
|
|
67
77
|
branchName,
|
|
68
78
|
createWorktree,
|
|
@@ -75,8 +85,7 @@ export function NewTabModal({
|
|
|
75
85
|
selectedAssistantId,
|
|
76
86
|
selectedIndex,
|
|
77
87
|
step,
|
|
78
|
-
|
|
79
|
-
worktreeDeleteMessage,
|
|
88
|
+
worktreeDeletePrompt,
|
|
80
89
|
worktreeName,
|
|
81
90
|
worktrees,
|
|
82
91
|
worktreeTemplates,
|
|
@@ -107,9 +116,8 @@ export function NewTabModal({
|
|
|
107
116
|
active && canDelete && currentSessionId != null && currentSessionId !== ''
|
|
108
117
|
? () => {
|
|
109
118
|
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
110
|
-
dispatchGlobal({ message: null, type: 'set-new-tab-worktree-delete-state' })
|
|
111
119
|
runSideEffectGlobal({
|
|
112
|
-
force:
|
|
120
|
+
force: false,
|
|
113
121
|
sessionId: currentSessionId,
|
|
114
122
|
type: 'delete-worktree',
|
|
115
123
|
worktreeId: worktree.id,
|
|
@@ -127,12 +135,15 @@ export function NewTabModal({
|
|
|
127
135
|
}),
|
|
128
136
|
{
|
|
129
137
|
key: '__create-worktree__',
|
|
130
|
-
onClick: () =>
|
|
138
|
+
onClick: () => {
|
|
139
|
+
dispatchGlobal({ type: 'enter-new-tab-worktree-create' })
|
|
140
|
+
runSideEffectGlobal({ type: 'load-new-tab-base-branches' })
|
|
141
|
+
},
|
|
131
142
|
subtitle: <text fg={t.textMuted}>Create an Aimux temp worktree</text>,
|
|
132
143
|
title: <text fg={createWorktree ? t.text : t.textMuted}>Create new worktree</text>,
|
|
133
144
|
},
|
|
134
145
|
],
|
|
135
|
-
[createWorktree, currentSessionId, selectedIndex, t,
|
|
146
|
+
[createWorktree, currentSessionId, selectedIndex, t, worktrees]
|
|
136
147
|
)
|
|
137
148
|
|
|
138
149
|
const noneOffset = getTemplateNoneOffset(selectedAssistantId)
|
|
@@ -216,6 +227,24 @@ export function NewTabModal({
|
|
|
216
227
|
]
|
|
217
228
|
}, [customCommands, filtered, showShortcutEntry, selectedIndex, t])
|
|
218
229
|
|
|
230
|
+
const baseItems = useMemo<FormOptionItem[]>(
|
|
231
|
+
() =>
|
|
232
|
+
buildBaseRefOptions(worktrees, baseBranches, baseQuery).map((option) => ({
|
|
233
|
+
key: option.ref,
|
|
234
|
+
leading: (
|
|
235
|
+
<text fg={option.kind === 'worktree' ? t.warning : t.textMuted}>
|
|
236
|
+
{option.kind === 'worktree' ? '\u{e728}' : '\u{e702}'}
|
|
237
|
+
</text>
|
|
238
|
+
),
|
|
239
|
+
subtitle:
|
|
240
|
+
option.kind === 'worktree' ? (
|
|
241
|
+
<text fg={t.textMuted}>worktree: {option.detail}</text>
|
|
242
|
+
) : null,
|
|
243
|
+
title: (active) => <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
|
|
244
|
+
})),
|
|
245
|
+
[baseBranches, baseQuery, t, worktrees]
|
|
246
|
+
)
|
|
247
|
+
|
|
219
248
|
if (editingCommand !== null) {
|
|
220
249
|
const option = options.find((o) => o.id === editingCommand) ?? getAssistantOption(0)
|
|
221
250
|
return (
|
|
@@ -260,6 +289,7 @@ export function NewTabModal({
|
|
|
260
289
|
if (step === 'worktree-create') {
|
|
261
290
|
const selectedAssistant =
|
|
262
291
|
options.find((option) => option.id === selectedAssistantId) ?? options[0]
|
|
292
|
+
const baseActive = activeField === 'base'
|
|
263
293
|
return (
|
|
264
294
|
<Form
|
|
265
295
|
title={`New worktree: ${selectedAssistant?.label ?? 'assistant'}`}
|
|
@@ -286,7 +316,20 @@ export function NewTabModal({
|
|
|
286
316
|
<text fg={t.error}>{branchError}</text>
|
|
287
317
|
) : null}
|
|
288
318
|
</box>
|
|
289
|
-
<
|
|
319
|
+
<AutoComplete
|
|
320
|
+
active={baseActive}
|
|
321
|
+
label="Base (fork from)"
|
|
322
|
+
placeholder="branch or worktree to fork from..."
|
|
323
|
+
value={baseQuery}
|
|
324
|
+
displayValue={baseRef !== '' ? baseRef : 'current branch'}
|
|
325
|
+
items={baseItems}
|
|
326
|
+
selectedIndex={selectedIndex}
|
|
327
|
+
cursorPos={baseActive ? cursorPos : undefined}
|
|
328
|
+
maxVisibleRows={6}
|
|
329
|
+
onHover={handleHover}
|
|
330
|
+
emptyState={<text fg={t.textMuted}>No branches found</text>}
|
|
331
|
+
/>
|
|
332
|
+
<text fg={t.textMuted}>Step 3/3: configure new worktree · Tab switches fields</text>
|
|
290
333
|
</box>
|
|
291
334
|
</Form>
|
|
292
335
|
)
|
|
@@ -297,6 +340,19 @@ export function NewTabModal({
|
|
|
297
340
|
options.find((option) => option.id === selectedAssistantId) ??
|
|
298
341
|
filtered[selectedIndex] ??
|
|
299
342
|
options[0]
|
|
343
|
+
|
|
344
|
+
if (worktreeDeletePrompt != null) {
|
|
345
|
+
const target = worktrees.find((worktree) => worktree.id === worktreeDeletePrompt.worktreeId)
|
|
346
|
+
const label = target?.branch ?? target?.name ?? 'this worktree'
|
|
347
|
+
return (
|
|
348
|
+
<WorktreeDeleteConfirm
|
|
349
|
+
keybindsModeId="modal.new-tab.worktree-delete-confirm"
|
|
350
|
+
reason={worktreeDeletePrompt.reason}
|
|
351
|
+
worktreeLabel={label}
|
|
352
|
+
/>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
300
356
|
const selectedWorktree = worktrees[selectedIndex]
|
|
301
357
|
const deleteBlockedReason = getDeleteBlockedReason({
|
|
302
358
|
currentSessionId,
|
|
@@ -316,17 +372,8 @@ export function NewTabModal({
|
|
|
316
372
|
onHover={handleHover}
|
|
317
373
|
footer={
|
|
318
374
|
<box flexDirection="column">
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
<text
|
|
322
|
-
fg={
|
|
323
|
-
worktreeDeleteMessage != null && worktreeDeleteMessage !== ''
|
|
324
|
-
? t.error
|
|
325
|
-
: t.textMuted
|
|
326
|
-
}
|
|
327
|
-
>
|
|
328
|
-
{worktreeDeleteMessage ?? deleteBlockedReason}
|
|
329
|
-
</text>
|
|
375
|
+
{deleteBlockedReason != null && deleteBlockedReason !== '' ? (
|
|
376
|
+
<text fg={t.textMuted}>{deleteBlockedReason}</text>
|
|
330
377
|
) : null}
|
|
331
378
|
<text fg={t.textMuted}>Step 2/2: choose worktree</text>
|
|
332
379
|
<text fg={t.textMuted}>Enter launches, Ctrl+d deletes selected worktree</text>
|
package/src/ui/root.tsx
CHANGED
|
@@ -34,6 +34,7 @@ import { GitCommitModal } from './components/modals/git/git-commit-modal'
|
|
|
34
34
|
import { CreateSessionModal } from './components/modals/sessions/create-session-modal'
|
|
35
35
|
import { SessionNameModal } from './components/modals/sessions/session-name-modal'
|
|
36
36
|
import { SessionPickerModal } from './components/modals/sessions/session-picker-modal'
|
|
37
|
+
import { WorktreeDeleteConfirm } from './components/modals/shared/worktree-delete-confirm'
|
|
37
38
|
import { SnippetEditorModal } from './components/modals/snippets/snippet-editor-modal'
|
|
38
39
|
import { SnippetPickerModal } from './components/modals/snippets/snippet-picker-modal'
|
|
39
40
|
import { NewTabModal } from './components/modals/tabs/new-tab-modal'
|
|
@@ -46,6 +47,7 @@ import { ToastViewport } from './components/overlays/toast/toast-viewport'
|
|
|
46
47
|
import { useTheme } from './theme'
|
|
47
48
|
|
|
48
49
|
const EMPTY_WORKTREES: WorktreeRecord[] = []
|
|
50
|
+
const EMPTY_BASE_BRANCHES: string[] = []
|
|
49
51
|
|
|
50
52
|
function getCreateSessionFields(modal: ModalState) {
|
|
51
53
|
if (modal.type !== 'create-session') {
|
|
@@ -114,8 +116,10 @@ function renderModal(
|
|
|
114
116
|
createWorktree={modal.type === 'new-tab' ? modal.createWorktree : false}
|
|
115
117
|
selectedAssistantId={modal.type === 'new-tab' ? modal.selectedAssistantId : null}
|
|
116
118
|
step={modal.type === 'new-tab' ? modal.step : 'assistant'}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
baseQuery={modal.type === 'new-tab' ? modal.baseQuery : ''}
|
|
120
|
+
baseRef={modal.type === 'new-tab' ? modal.baseRef : ''}
|
|
121
|
+
baseBranches={modal.type === 'new-tab' ? modal.baseBranches : EMPTY_BASE_BRANCHES}
|
|
122
|
+
worktreeDeletePrompt={modal.type === 'new-tab' ? modal.worktreeDeletePrompt : null}
|
|
119
123
|
worktrees={
|
|
120
124
|
options.currentSessionId != null && options.currentSessionId !== ''
|
|
121
125
|
? (options.sessions.find((session) => session.id === options.currentSessionId)
|
|
@@ -225,6 +229,14 @@ function renderModal(
|
|
|
225
229
|
)
|
|
226
230
|
case 'ai-usage':
|
|
227
231
|
return <AIUsageModal />
|
|
232
|
+
case 'worktree-delete-confirm':
|
|
233
|
+
return (
|
|
234
|
+
<WorktreeDeleteConfirm
|
|
235
|
+
keybindsModeId="modal.worktree-delete-confirm"
|
|
236
|
+
reason={modal.reason}
|
|
237
|
+
worktreeLabel={modal.worktreeLabel}
|
|
238
|
+
/>
|
|
239
|
+
)
|
|
228
240
|
case 'git-commit': {
|
|
229
241
|
const titleText =
|
|
230
242
|
modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
|