@brimveyn/aimux 1.13.0 → 1.14.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/backend-attach-runtime.ts +12 -14
- package/src/app-runtime/multi-click-clipboard-guard.ts +24 -12
- package/src/app-runtime/pty-write.ts +6 -9
- package/src/app-runtime/side-effects.ts +65 -56
- package/src/app-runtime/snippet-actions.ts +2 -3
- package/src/app-runtime/use-backend-runtime.ts +4 -7
- package/src/app-runtime/use-mouse-handlers.ts +30 -1
- package/src/app-runtime/use-renderer-bindings.ts +4 -12
- package/src/app-runtime/use-terminal-resize.ts +6 -18
- package/src/app.tsx +0 -3
- package/src/daemon/daemon.ts +20 -27
- package/src/daemon/session-manager.ts +3 -15
- package/src/daemon/session-registry.ts +11 -30
- package/src/git/command-queue.ts +18 -2
- package/src/input/terminal-text-extraction.ts +7 -8
- package/src/ipc/manager-protocol.ts +6 -38
- package/src/ipc/protocol.ts +9 -37
- package/src/pty/pty-manager.ts +20 -31
- package/src/pty/terminal-snapshot.ts +43 -0
- package/src/session-backend/local-session-backend.ts +7 -31
- package/src/session-backend/remote-session-backend.ts +51 -63
- package/src/session-backend/types.ts +2 -15
- package/src/state/reducers/tab-state.ts +2 -20
- package/src/state/session-persistence.ts +2 -22
- package/src/state/types.ts +3 -11
- package/src/state/validation.ts +0 -8
- package/src/terminal-manager/manager-client.ts +5 -29
- package/src/terminal-manager/terminal-manager.ts +2 -17
- 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,6 +1,6 @@
|
|
|
1
1
|
import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import { useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { SessionRecord, SessionStatus } from '../../../state/types'
|
|
6
6
|
|
|
@@ -31,52 +31,49 @@ export function SessionBar({ forceVisible = false }: SessionBarProps) {
|
|
|
31
31
|
const chipRefs = useRef(new Map<string, BoxRenderable>())
|
|
32
32
|
|
|
33
33
|
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
34
|
-
|
|
34
|
+
const baselineOrder = useMemo(() => ordered.map((s) => s.id), [ordered])
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
dragOrder !== null
|
|
38
|
-
? dragOrder
|
|
39
|
-
.map((id) => ordered.find((s) => s.id === id))
|
|
40
|
-
.filter((s): s is SessionRecord => !!s)
|
|
41
|
-
: ordered
|
|
42
|
-
|
|
43
|
-
const baselineOrder = ordered.map((s) => s.id)
|
|
44
|
-
|
|
45
|
-
function setChipRef(id: string, ref: BoxRenderable | null): void {
|
|
36
|
+
const setChipRef = useCallback((id: string, ref: BoxRenderable | null): void => {
|
|
46
37
|
if (ref) chipRefs.current.set(id, ref)
|
|
47
38
|
else chipRefs.current.delete(id)
|
|
48
|
-
}
|
|
39
|
+
}, [])
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
const findChipAtX = useCallback((x: number): string | null => {
|
|
51
42
|
for (const [id, ref] of chipRefs.current) {
|
|
52
43
|
if (x >= ref.x && x < ref.x + ref.width) return id
|
|
53
44
|
}
|
|
54
45
|
return null
|
|
55
|
-
}
|
|
46
|
+
}, [])
|
|
56
47
|
|
|
57
|
-
const handleMouseDown = (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const handleMouseDrag = (event: OtuiMouseEvent) => {
|
|
64
|
-
if (!(draggingId != null && draggingId !== '')) return
|
|
65
|
-
const hit = findChipAtX(event.x)
|
|
66
|
-
if (hit === null) {
|
|
67
|
-
lastSwapWithRef.current = null
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
if (hit === draggingId) {
|
|
48
|
+
const handleMouseDown = useCallback(
|
|
49
|
+
(id: string) => {
|
|
50
|
+
setDraggingId(id)
|
|
51
|
+
setDragOrder(baselineOrder)
|
|
71
52
|
lastSwapWithRef.current = null
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
setDragOrder((prev) => (prev ? moveIdToIdPosition(prev, draggingId, hit) : prev))
|
|
76
|
-
lastSwapWithRef.current = hit
|
|
77
|
-
}
|
|
53
|
+
},
|
|
54
|
+
[baselineOrder]
|
|
55
|
+
)
|
|
78
56
|
|
|
79
|
-
const
|
|
57
|
+
const handleMouseDrag = useCallback(
|
|
58
|
+
(event: OtuiMouseEvent) => {
|
|
59
|
+
if (!(draggingId != null && draggingId !== '')) return
|
|
60
|
+
const hit = findChipAtX(event.x)
|
|
61
|
+
if (hit === null) {
|
|
62
|
+
lastSwapWithRef.current = null
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (hit === draggingId) {
|
|
66
|
+
lastSwapWithRef.current = null
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (hit === lastSwapWithRef.current) return
|
|
70
|
+
setDragOrder((prev) => (prev ? moveIdToIdPosition(prev, draggingId, hit) : prev))
|
|
71
|
+
lastSwapWithRef.current = hit
|
|
72
|
+
},
|
|
73
|
+
[draggingId, findChipAtX]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const commitDrop = useCallback(() => {
|
|
80
77
|
const source = draggingId
|
|
81
78
|
const finalOrder = dragOrder
|
|
82
79
|
setDraggingId(null)
|
|
@@ -95,13 +92,27 @@ export function SessionBar({ forceVisible = false }: SessionBarProps) {
|
|
|
95
92
|
if (idx >= 0) {
|
|
96
93
|
runSideEffectGlobal({ index: idx + 1, type: 'switch-session-by-index' })
|
|
97
94
|
}
|
|
98
|
-
}
|
|
95
|
+
}, [baselineOrder, dragOrder, draggingId])
|
|
99
96
|
|
|
100
|
-
const cancelDrag = () => {
|
|
97
|
+
const cancelDrag = useCallback(() => {
|
|
101
98
|
setDraggingId(null)
|
|
102
99
|
setDragOrder(null)
|
|
103
100
|
lastSwapWithRef.current = null
|
|
104
|
-
}
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
const handleNewSession = useCallback((e: OtuiMouseEvent) => {
|
|
104
|
+
e.stopPropagation()
|
|
105
|
+
dispatchGlobal({ returnToSessionPicker: false, type: 'open-create-session-modal' })
|
|
106
|
+
}, [])
|
|
107
|
+
|
|
108
|
+
if ((!bar.visible && !forceVisible) || ordered.length === 0) return null
|
|
109
|
+
|
|
110
|
+
const visibleSessions =
|
|
111
|
+
dragOrder !== null
|
|
112
|
+
? dragOrder
|
|
113
|
+
.map((id) => ordered.find((s) => s.id === id))
|
|
114
|
+
.filter((s): s is SessionRecord => !!s)
|
|
115
|
+
: ordered
|
|
105
116
|
|
|
106
117
|
return (
|
|
107
118
|
<box
|
|
@@ -112,50 +123,28 @@ export function SessionBar({ forceVisible = false }: SessionBarProps) {
|
|
|
112
123
|
paddingRight={1}
|
|
113
124
|
backgroundColor={headerBg}
|
|
114
125
|
>
|
|
115
|
-
{visibleSessions.map((session) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
rightClickMenu={[
|
|
131
|
-
[
|
|
132
|
-
'Rename workspace',
|
|
133
|
-
() =>
|
|
134
|
-
dispatchGlobal({
|
|
135
|
-
initialName: session.name,
|
|
136
|
-
returnToSessionPicker: false,
|
|
137
|
-
sessionTargetId: session.id,
|
|
138
|
-
type: 'open-session-name-modal',
|
|
139
|
-
}),
|
|
140
|
-
],
|
|
141
|
-
[
|
|
142
|
-
'Delete workspace',
|
|
143
|
-
() => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
|
|
144
|
-
],
|
|
145
|
-
]}
|
|
146
|
-
/>
|
|
147
|
-
)
|
|
148
|
-
})}
|
|
126
|
+
{visibleSessions.map((session) => (
|
|
127
|
+
<SessionChip
|
|
128
|
+
key={session.id}
|
|
129
|
+
session={session}
|
|
130
|
+
index={baselineOrder.indexOf(session.id) + 1}
|
|
131
|
+
active={session.id === currentId}
|
|
132
|
+
status={statusMap[session.id] ?? IDLE_SESSION_STATUS}
|
|
133
|
+
dragging={draggingId === session.id}
|
|
134
|
+
setChipRef={setChipRef}
|
|
135
|
+
onDragStart={handleMouseDown}
|
|
136
|
+
onDrag={handleMouseDrag}
|
|
137
|
+
onDrop={commitDrop}
|
|
138
|
+
onDragCancel={cancelDrag}
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
149
141
|
<box flexGrow={1} />
|
|
150
142
|
<box
|
|
151
143
|
flexDirection="row"
|
|
152
144
|
paddingLeft={1}
|
|
153
145
|
paddingRight={1}
|
|
154
146
|
backgroundColor={t.backgroundElement}
|
|
155
|
-
onMouseDown={
|
|
156
|
-
e.stopPropagation()
|
|
157
|
-
dispatchGlobal({ returnToSessionPicker: false, type: 'open-create-session-modal' })
|
|
158
|
-
}}
|
|
147
|
+
onMouseDown={handleNewSession}
|
|
159
148
|
>
|
|
160
149
|
<text fg={t.text} selectable={false}>
|
|
161
150
|
+ New
|
|
@@ -179,25 +168,23 @@ interface SessionChipProps {
|
|
|
179
168
|
active: boolean
|
|
180
169
|
status: SessionStatus
|
|
181
170
|
dragging: boolean
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
rightClickMenu: [string, () => void][]
|
|
171
|
+
setChipRef: (id: string, ref: BoxRenderable | null) => void
|
|
172
|
+
onDragStart: (id: string) => void
|
|
173
|
+
onDrag: (event: OtuiMouseEvent) => void
|
|
174
|
+
onDrop: () => void
|
|
175
|
+
onDragCancel: () => void
|
|
188
176
|
}
|
|
189
177
|
|
|
190
|
-
function SessionChip({
|
|
178
|
+
const SessionChip = memo(function SessionChip({
|
|
191
179
|
active,
|
|
192
180
|
dragging,
|
|
193
181
|
index,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
onRef,
|
|
199
|
-
rightClickMenu,
|
|
182
|
+
onDrag,
|
|
183
|
+
onDragCancel,
|
|
184
|
+
onDragStart,
|
|
185
|
+
onDrop,
|
|
200
186
|
session,
|
|
187
|
+
setChipRef,
|
|
201
188
|
status,
|
|
202
189
|
}: SessionChipProps) {
|
|
203
190
|
const t = useTheme()
|
|
@@ -212,30 +199,68 @@ function SessionChip({
|
|
|
212
199
|
const waitingColor = t.warning
|
|
213
200
|
const [hovered, setHovered] = useState(false)
|
|
214
201
|
|
|
202
|
+
const handleRef = useCallback(
|
|
203
|
+
(r: BoxRenderable | null) => setChipRef(session.id, r),
|
|
204
|
+
[setChipRef, session.id]
|
|
205
|
+
)
|
|
206
|
+
const handleMouseDown = useCallback(
|
|
207
|
+
(e: OtuiMouseEvent) => {
|
|
208
|
+
e.preventDefault()
|
|
209
|
+
onDragStart(session.id)
|
|
210
|
+
},
|
|
211
|
+
[onDragStart, session.id]
|
|
212
|
+
)
|
|
213
|
+
const handleMouseUp = useCallback(
|
|
214
|
+
(e: OtuiMouseEvent) => {
|
|
215
|
+
e.preventDefault()
|
|
216
|
+
onDrop()
|
|
217
|
+
},
|
|
218
|
+
[onDrop]
|
|
219
|
+
)
|
|
220
|
+
const handleMouseOver = useCallback(() => setHovered(true), [])
|
|
221
|
+
const handleMouseOut = useCallback(() => setHovered(false), [])
|
|
222
|
+
const handleDeleteMouseDown = useCallback(
|
|
223
|
+
(event: OtuiMouseEvent) => {
|
|
224
|
+
event.preventDefault()
|
|
225
|
+
event.stopPropagation()
|
|
226
|
+
runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' })
|
|
227
|
+
},
|
|
228
|
+
[session.id]
|
|
229
|
+
)
|
|
230
|
+
const rightClickMenu = useMemo<[string, () => void][]>(
|
|
231
|
+
() => [
|
|
232
|
+
[
|
|
233
|
+
'Rename workspace',
|
|
234
|
+
() =>
|
|
235
|
+
dispatchGlobal({
|
|
236
|
+
initialName: session.name,
|
|
237
|
+
returnToSessionPicker: false,
|
|
238
|
+
sessionTargetId: session.id,
|
|
239
|
+
type: 'open-session-name-modal',
|
|
240
|
+
}),
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
'Delete workspace',
|
|
244
|
+
() => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
|
|
245
|
+
],
|
|
246
|
+
],
|
|
247
|
+
[session.id, session.name]
|
|
248
|
+
)
|
|
249
|
+
|
|
215
250
|
return (
|
|
216
251
|
<ContextMenuBox
|
|
217
|
-
ref={
|
|
252
|
+
ref={handleRef}
|
|
218
253
|
flexDirection="row"
|
|
219
254
|
paddingLeft={1}
|
|
220
255
|
paddingRight={1}
|
|
221
256
|
backgroundColor={bgColor}
|
|
222
257
|
rightClickMenu={rightClickMenu}
|
|
223
|
-
onMouseOver={
|
|
224
|
-
onMouseOut={
|
|
225
|
-
onMouseDown={
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
onMouseDrag={(e) => {
|
|
230
|
-
onMouseDrag(e)
|
|
231
|
-
}}
|
|
232
|
-
onMouseUp={(e) => {
|
|
233
|
-
e.preventDefault()
|
|
234
|
-
onMouseUp(e)
|
|
235
|
-
}}
|
|
236
|
-
onMouseDragEnd={(e) => {
|
|
237
|
-
onMouseDragEnd(e)
|
|
238
|
-
}}
|
|
258
|
+
onMouseOver={handleMouseOver}
|
|
259
|
+
onMouseOut={handleMouseOut}
|
|
260
|
+
onMouseDown={handleMouseDown}
|
|
261
|
+
onMouseDrag={onDrag}
|
|
262
|
+
onMouseUp={handleMouseUp}
|
|
263
|
+
onMouseDragEnd={onDragCancel}
|
|
239
264
|
>
|
|
240
265
|
{showWaiting ? (
|
|
241
266
|
<text fg={waitingColor} selectable={false}>
|
|
@@ -256,14 +281,7 @@ function SessionChip({
|
|
|
256
281
|
[{index}] {session.name}
|
|
257
282
|
</text>
|
|
258
283
|
{hovered ? (
|
|
259
|
-
<box
|
|
260
|
-
paddingLeft={1}
|
|
261
|
-
onMouseDown={(event) => {
|
|
262
|
-
event.preventDefault()
|
|
263
|
-
event.stopPropagation()
|
|
264
|
-
runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' })
|
|
265
|
-
}}
|
|
266
|
-
>
|
|
284
|
+
<box paddingLeft={1} onMouseDown={handleDeleteMouseDown}>
|
|
267
285
|
<text fg={t.textMuted} selectable={false}>
|
|
268
286
|
×
|
|
269
287
|
</text>
|
|
@@ -275,4 +293,4 @@ function SessionChip({
|
|
|
275
293
|
)}
|
|
276
294
|
</ContextMenuBox>
|
|
277
295
|
)
|
|
278
|
-
}
|
|
296
|
+
})
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
ScrollBoxRenderable,
|
|
5
5
|
} from '@opentui/core'
|
|
6
6
|
|
|
7
|
-
import { memo, useMemo, useRef } from 'react'
|
|
7
|
+
import { memo, type ReactNode, useCallback, useMemo, useRef } from 'react'
|
|
8
8
|
|
|
9
9
|
import type { BranchDivergence } from '../../../../state/types'
|
|
10
10
|
|
|
@@ -43,6 +43,7 @@ const GUTTER_MIDDLE = '├'
|
|
|
43
43
|
const GUTTER_END = '╰'
|
|
44
44
|
const GUTTER_PAD = '│'
|
|
45
45
|
const RESIZE_HANDLE = '─'
|
|
46
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
46
47
|
|
|
47
48
|
// Left accent strip + trailing space for a worktree group header.
|
|
48
49
|
const WORKTREE_STRIP = '▍ '
|
|
@@ -92,6 +93,11 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
92
93
|
const projectPath = getSessionProjectPath(currentSession)
|
|
93
94
|
const branch = useSidebarBranch(projectPath)
|
|
94
95
|
|
|
96
|
+
const handleNewAssistant = useCallback((e: OtuiMouseEvent) => {
|
|
97
|
+
e.stopPropagation()
|
|
98
|
+
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
95
101
|
return (
|
|
96
102
|
<box flexDirection="column" flexShrink={0} gap={0}>
|
|
97
103
|
<text fg={t.text} selectable={false}>
|
|
@@ -122,10 +128,7 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
122
128
|
backgroundColor={t.backgroundPanel}
|
|
123
129
|
justifyContent="center"
|
|
124
130
|
marginTop={1}
|
|
125
|
-
onMouseDown={
|
|
126
|
-
e.stopPropagation()
|
|
127
|
-
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
128
|
-
}}
|
|
131
|
+
onMouseDown={handleNewAssistant}
|
|
129
132
|
>
|
|
130
133
|
<text fg={t.text} selectable={false}>
|
|
131
134
|
+ New assistant
|
|
@@ -153,6 +156,31 @@ function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGrou
|
|
|
153
156
|
)
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
const TabRowButton = memo(function TabRowButton({
|
|
160
|
+
backgroundColor,
|
|
161
|
+
children,
|
|
162
|
+
onActivate,
|
|
163
|
+
tabId,
|
|
164
|
+
}: {
|
|
165
|
+
tabId: string
|
|
166
|
+
backgroundColor: string | undefined
|
|
167
|
+
onActivate?: (tabId: string) => void
|
|
168
|
+
children: ReactNode
|
|
169
|
+
}) {
|
|
170
|
+
const handleMouseDown = useCallback(
|
|
171
|
+
(event: OtuiMouseEvent) => {
|
|
172
|
+
event.stopPropagation()
|
|
173
|
+
onActivate?.(tabId)
|
|
174
|
+
},
|
|
175
|
+
[onActivate, tabId]
|
|
176
|
+
)
|
|
177
|
+
return (
|
|
178
|
+
<box backgroundColor={backgroundColor} flexDirection="row" onMouseDown={handleMouseDown}>
|
|
179
|
+
{children}
|
|
180
|
+
</box>
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
156
184
|
interface TabsBodyProps {
|
|
157
185
|
onTabActivate?: (tabId: string) => void
|
|
158
186
|
contentWidth: number
|
|
@@ -213,7 +241,7 @@ const TabsBody = memo(function TabsBody({ contentWidth, onTabActivate }: TabsBod
|
|
|
213
241
|
flexGrow={1}
|
|
214
242
|
scrollY
|
|
215
243
|
viewportCulling
|
|
216
|
-
contentOptions={
|
|
244
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
217
245
|
>
|
|
218
246
|
{tabs.length === 0 ? (
|
|
219
247
|
<box paddingTop={1}>
|
|
@@ -287,13 +315,10 @@ const TabsBody = memo(function TabsBody({ contentWidth, onTabActivate }: TabsBod
|
|
|
287
315
|
</box>
|
|
288
316
|
</box>
|
|
289
317
|
) : null}
|
|
290
|
-
<
|
|
318
|
+
<TabRowButton
|
|
319
|
+
tabId={tab.id}
|
|
320
|
+
onActivate={onTabActivate}
|
|
291
321
|
backgroundColor={getRowBackground({ alternate, isActive, t })}
|
|
292
|
-
flexDirection="row"
|
|
293
|
-
onMouseDown={(event) => {
|
|
294
|
-
event.stopPropagation()
|
|
295
|
-
onTabActivate?.(tab.id)
|
|
296
|
-
}}
|
|
297
322
|
>
|
|
298
323
|
{inGroup ? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd) : null}
|
|
299
324
|
<box flexGrow={1}>
|
|
@@ -306,7 +331,7 @@ const TabsBody = memo(function TabsBody({ contentWidth, onTabActivate }: TabsBod
|
|
|
306
331
|
moveWorktreeId={moveWorktreeId}
|
|
307
332
|
/>
|
|
308
333
|
</box>
|
|
309
|
-
</
|
|
334
|
+
</TabRowButton>
|
|
310
335
|
</box>
|
|
311
336
|
)
|
|
312
337
|
})
|
|
@@ -330,6 +355,52 @@ export function Sidebar({
|
|
|
330
355
|
const bodyRef = useRef<BoxRenderable | null>(null)
|
|
331
356
|
useWorktreeDivergencePolling(sidebarVisible)
|
|
332
357
|
|
|
358
|
+
const handleSidebarMouseDown = useCallback(() => {
|
|
359
|
+
if (focusMode === 'terminal-input') {
|
|
360
|
+
dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
|
|
361
|
+
}
|
|
362
|
+
}, [focusMode])
|
|
363
|
+
const handleSidebarMouseDrag = useCallback(
|
|
364
|
+
(event: OtuiMouseEvent) => {
|
|
365
|
+
if (onResizeDrag?.(event) === true) {
|
|
366
|
+
event.preventDefault()
|
|
367
|
+
event.stopPropagation()
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
[onResizeDrag]
|
|
371
|
+
)
|
|
372
|
+
const handleSidebarMouseUp = useCallback(() => {
|
|
373
|
+
onResizeDragEnd?.()
|
|
374
|
+
}, [onResizeDragEnd])
|
|
375
|
+
const handleEmbeddedHandleMouseDown = useCallback(
|
|
376
|
+
(event: OtuiMouseEvent) => {
|
|
377
|
+
const body = bodyRef.current
|
|
378
|
+
if (!body) return
|
|
379
|
+
event.preventDefault()
|
|
380
|
+
event.stopPropagation()
|
|
381
|
+
onEmbeddedGitResizeStart?.({
|
|
382
|
+
containerStart: body.y,
|
|
383
|
+
position: gitPane.position === 'top' ? 'top' : 'bottom',
|
|
384
|
+
totalSize: Math.max(1, body.height),
|
|
385
|
+
})
|
|
386
|
+
},
|
|
387
|
+
[gitPane.position, onEmbeddedGitResizeStart]
|
|
388
|
+
)
|
|
389
|
+
const sidebarMenu = useMemo<[string, () => void][]>(
|
|
390
|
+
() => [
|
|
391
|
+
['Hide sidebar', () => dispatchGlobal({ type: 'toggle-sidebar' })],
|
|
392
|
+
['Toggle git pane', () => dispatchGlobal({ type: 'toggle-git-pane' })],
|
|
393
|
+
[
|
|
394
|
+
'Toggle diff mode',
|
|
395
|
+
() => {
|
|
396
|
+
dispatchGlobal({ type: 'enter-git-mode' })
|
|
397
|
+
dispatchGlobal({ type: 'git-mode-toggle-diff-view' })
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
],
|
|
401
|
+
[]
|
|
402
|
+
)
|
|
403
|
+
|
|
333
404
|
if (!sidebarVisible) {
|
|
334
405
|
return null
|
|
335
406
|
}
|
|
@@ -369,21 +440,7 @@ export function Sidebar({
|
|
|
369
440
|
</ContextMenuBox>
|
|
370
441
|
) : null
|
|
371
442
|
const embeddedHandle = gitEmbedded ? (
|
|
372
|
-
<box
|
|
373
|
-
minHeight={1}
|
|
374
|
-
flexShrink={0}
|
|
375
|
-
onMouseDown={(event) => {
|
|
376
|
-
const body = bodyRef.current
|
|
377
|
-
if (!body) return
|
|
378
|
-
event.preventDefault()
|
|
379
|
-
event.stopPropagation()
|
|
380
|
-
onEmbeddedGitResizeStart?.({
|
|
381
|
-
containerStart: body.y,
|
|
382
|
-
position: gitPane.position === 'top' ? 'top' : 'bottom',
|
|
383
|
-
totalSize: Math.max(1, body.height),
|
|
384
|
-
})
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
443
|
+
<box minHeight={1} flexShrink={0} onMouseDown={handleEmbeddedHandleMouseDown}>
|
|
387
444
|
<text fg={t.border} selectable={false}>
|
|
388
445
|
{RESIZE_HANDLE.repeat(Math.max(1, contentWidth))}
|
|
389
446
|
</text>
|
|
@@ -398,31 +455,10 @@ export function Sidebar({
|
|
|
398
455
|
backgroundColor={sidebarBg}
|
|
399
456
|
gap={0}
|
|
400
457
|
overflow="hidden"
|
|
401
|
-
rightClickMenu={
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
'Toggle diff mode',
|
|
406
|
-
() => {
|
|
407
|
-
dispatchGlobal({ type: 'enter-git-mode' })
|
|
408
|
-
dispatchGlobal({ type: 'git-mode-toggle-diff-view' })
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
]}
|
|
412
|
-
onMouseDown={() => {
|
|
413
|
-
if (focusMode === 'terminal-input') {
|
|
414
|
-
dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
|
|
415
|
-
}
|
|
416
|
-
}}
|
|
417
|
-
onMouseDrag={(event) => {
|
|
418
|
-
if (onResizeDrag?.(event) === true) {
|
|
419
|
-
event.preventDefault()
|
|
420
|
-
event.stopPropagation()
|
|
421
|
-
}
|
|
422
|
-
}}
|
|
423
|
-
onMouseUp={() => {
|
|
424
|
-
onResizeDragEnd?.()
|
|
425
|
-
}}
|
|
458
|
+
rightClickMenu={sidebarMenu}
|
|
459
|
+
onMouseDown={handleSidebarMouseDown}
|
|
460
|
+
onMouseDrag={handleSidebarMouseDrag}
|
|
461
|
+
onMouseUp={handleSidebarMouseUp}
|
|
426
462
|
>
|
|
427
463
|
<box flexDirection="row" width={sidebarWidth} flexGrow={1} overflow="hidden">
|
|
428
464
|
<box width={contentWidth} flexGrow={1} flexDirection="column" overflow="hidden">
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
2
4
|
|
|
3
5
|
import type { TabSession } from '../../../../state/types'
|
|
4
6
|
|
|
@@ -116,6 +118,64 @@ export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }:
|
|
|
116
118
|
const indicatorColor = getIndicatorColor(active, focused, isInLayout)
|
|
117
119
|
const [hovered, setHovered] = useState(false)
|
|
118
120
|
|
|
121
|
+
const rightClickMenu = useMemo<[string, () => void][]>(
|
|
122
|
+
() => [
|
|
123
|
+
[
|
|
124
|
+
'Rename',
|
|
125
|
+
() => {
|
|
126
|
+
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
127
|
+
dispatchGlobal({ type: 'open-rename-tab-modal' })
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
'Close',
|
|
132
|
+
() => {
|
|
133
|
+
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
134
|
+
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
'Move up',
|
|
139
|
+
() => {
|
|
140
|
+
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
141
|
+
dispatchGlobal({ delta: -1, type: 'reorder-active-tab' })
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
'Move down',
|
|
146
|
+
() => {
|
|
147
|
+
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
148
|
+
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
// Move this tab's worktree into another one. Opened from here it overlays
|
|
152
|
+
// the normal view (no git mode) since the open action doesn't touch focus.
|
|
153
|
+
...(moveWorktreeId != null && moveWorktreeId !== ''
|
|
154
|
+
? [
|
|
155
|
+
[
|
|
156
|
+
'Move worktree',
|
|
157
|
+
() =>
|
|
158
|
+
dispatchGlobal({
|
|
159
|
+
sourceWorktreeId: moveWorktreeId,
|
|
160
|
+
type: 'open-worktree-move-modal',
|
|
161
|
+
}),
|
|
162
|
+
] as [string, () => void],
|
|
163
|
+
]
|
|
164
|
+
: []),
|
|
165
|
+
],
|
|
166
|
+
[moveWorktreeId, tab.id]
|
|
167
|
+
)
|
|
168
|
+
const handleMouseOver = useCallback(() => setHovered(true), [])
|
|
169
|
+
const handleMouseOut = useCallback(() => setHovered(false), [])
|
|
170
|
+
const handleCloseMouseDown = useCallback(
|
|
171
|
+
(event: OtuiMouseEvent) => {
|
|
172
|
+
event.stopPropagation()
|
|
173
|
+
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
174
|
+
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
175
|
+
},
|
|
176
|
+
[tab.id]
|
|
177
|
+
)
|
|
178
|
+
|
|
119
179
|
return (
|
|
120
180
|
<ContextMenuBox
|
|
121
181
|
id={id}
|
|
@@ -125,52 +185,9 @@ export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }:
|
|
|
125
185
|
paddingBottom={0}
|
|
126
186
|
flexDirection="column"
|
|
127
187
|
gap={0}
|
|
128
|
-
rightClickMenu={
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
() => {
|
|
132
|
-
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
133
|
-
dispatchGlobal({ type: 'open-rename-tab-modal' })
|
|
134
|
-
},
|
|
135
|
-
],
|
|
136
|
-
[
|
|
137
|
-
'Close',
|
|
138
|
-
() => {
|
|
139
|
-
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
140
|
-
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
[
|
|
144
|
-
'Move up',
|
|
145
|
-
() => {
|
|
146
|
-
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
147
|
-
dispatchGlobal({ delta: -1, type: 'reorder-active-tab' })
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
[
|
|
151
|
-
'Move down',
|
|
152
|
-
() => {
|
|
153
|
-
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
154
|
-
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
// Move this tab's worktree into another one. Opened from here it overlays
|
|
158
|
-
// the normal view (no git mode) since the open action doesn't touch focus.
|
|
159
|
-
...(moveWorktreeId != null && moveWorktreeId !== ''
|
|
160
|
-
? [
|
|
161
|
-
[
|
|
162
|
-
'Move worktree',
|
|
163
|
-
() =>
|
|
164
|
-
dispatchGlobal({
|
|
165
|
-
sourceWorktreeId: moveWorktreeId,
|
|
166
|
-
type: 'open-worktree-move-modal',
|
|
167
|
-
}),
|
|
168
|
-
] as [string, () => void],
|
|
169
|
-
]
|
|
170
|
-
: []),
|
|
171
|
-
]}
|
|
172
|
-
onMouseOver={() => setHovered(true)}
|
|
173
|
-
onMouseOut={() => setHovered(false)}
|
|
188
|
+
rightClickMenu={rightClickMenu}
|
|
189
|
+
onMouseOver={handleMouseOver}
|
|
190
|
+
onMouseOut={handleMouseOut}
|
|
174
191
|
>
|
|
175
192
|
<box flexDirection="row" alignItems="center">
|
|
176
193
|
<text fg={indicatorColor} selectable={false}>
|
|
@@ -182,13 +199,7 @@ export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }:
|
|
|
182
199
|
</text>
|
|
183
200
|
</box>
|
|
184
201
|
{hovered ? (
|
|
185
|
-
<box
|
|
186
|
-
onMouseDown={(event) => {
|
|
187
|
-
event.stopPropagation()
|
|
188
|
-
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
189
|
-
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
190
|
-
}}
|
|
191
|
-
>
|
|
202
|
+
<box onMouseDown={handleCloseMouseDown}>
|
|
192
203
|
<text fg={t.textMuted} selectable={false}>
|
|
193
204
|
×
|
|
194
205
|
</text>
|