@brimveyn/aimux 1.13.2 → 1.14.1
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 +1 -3
- 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 +149 -19
- package/src/app-runtime/snippet-actions.ts +2 -3
- package/src/app-runtime/split-drag-controller.ts +3 -1
- 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 +7 -22
- package/src/app.tsx +0 -6
- package/src/config.ts +1 -12
- package/src/daemon/daemon.ts +2 -19
- package/src/daemon/session-manager.ts +3 -15
- package/src/daemon/session-registry.ts +11 -30
- package/src/git/worktree-branch-poller.ts +54 -0
- package/src/input/modes/types.ts +2 -0
- 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 +5 -32
- package/src/session-backend/types.ts +2 -15
- package/src/state/layout-tree.ts +114 -4
- package/src/state/reducers/session-state.ts +20 -0
- package/src/state/reducers/tab-state.ts +22 -22
- package/src/state/reducers/ui-state.ts +0 -3
- package/src/state/session-persistence.ts +2 -22
- package/src/state/session-worktrees.ts +45 -2
- package/src/state/store.ts +0 -3
- package/src/state/tab-entries.ts +74 -0
- package/src/state/types.ts +4 -15
- package/src/state/validation.ts +0 -8
- package/src/state/workspace-save.ts +0 -1
- package/src/terminal-manager/manager-client.ts +5 -29
- package/src/terminal-manager/terminal-manager.ts +2 -17
- package/src/ui/components/layout/sidebar/sidebar.tsx +9 -340
- package/src/ui/components/layout/sidebar/tab-item.tsx +51 -42
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +10 -53
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +30 -0
- package/src/ui/components/layout/sidebar/workspace-list.tsx +398 -0
- package/src/ui/components/layout/sidebar/worktree-row.tsx +92 -0
- package/src/ui/components/layout/split-layout.tsx +20 -39
- package/src/ui/components/layout/terminal-pane.tsx +30 -0
- package/src/ui/components/layout/top-tab-bar.tsx +304 -0
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +1 -8
- package/src/ui/root.tsx +68 -63
- package/src/ui/components/layout/session-bar.tsx +0 -296
- package/src/ui/components/layout/sidebar/sidebar-group-metadata.ts +0 -44
- package/src/ui/components/layout/sidebar/sidebar-scroll.ts +0 -33
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +0 -36
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
-
|
|
3
|
-
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
|
4
|
-
|
|
5
|
-
import type { SessionRecord, SessionStatus } from '../../../state/types'
|
|
6
|
-
|
|
7
|
-
import { useAppStore } from '../../../state/app-store'
|
|
8
|
-
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
9
|
-
// eslint-disable-next-line no-duplicate-imports
|
|
10
|
-
import { IDLE_SESSION_STATUS } from '../../../state/types'
|
|
11
|
-
import { useBusySpinner } from '../../hooks/use-busy-spinner'
|
|
12
|
-
import { moveIdToIdPosition, orderSessionsForDisplay } from '../../session-ordering'
|
|
13
|
-
import { useTheme } from '../../theme'
|
|
14
|
-
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
15
|
-
|
|
16
|
-
interface SessionBarProps {
|
|
17
|
-
forceVisible?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function SessionBar({ forceVisible = false }: SessionBarProps) {
|
|
21
|
-
const t = useTheme()
|
|
22
|
-
const headerBg = t.backgroundPanel
|
|
23
|
-
const sessions = useAppStore((s) => s.sessions)
|
|
24
|
-
const currentId = useAppStore((s) => s.currentSessionId)
|
|
25
|
-
const bar = useAppStore((s) => s.sessionBar)
|
|
26
|
-
const statusMap = useAppStore((s) => s.sessionStatuses)
|
|
27
|
-
|
|
28
|
-
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
29
|
-
const [dragOrder, setDragOrder] = useState<string[] | null>(null)
|
|
30
|
-
const lastSwapWithRef = useRef<string | null>(null)
|
|
31
|
-
const chipRefs = useRef(new Map<string, BoxRenderable>())
|
|
32
|
-
|
|
33
|
-
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
34
|
-
const baselineOrder = useMemo(() => ordered.map((s) => s.id), [ordered])
|
|
35
|
-
|
|
36
|
-
const setChipRef = useCallback((id: string, ref: BoxRenderable | null): void => {
|
|
37
|
-
if (ref) chipRefs.current.set(id, ref)
|
|
38
|
-
else chipRefs.current.delete(id)
|
|
39
|
-
}, [])
|
|
40
|
-
|
|
41
|
-
const findChipAtX = useCallback((x: number): string | null => {
|
|
42
|
-
for (const [id, ref] of chipRefs.current) {
|
|
43
|
-
if (x >= ref.x && x < ref.x + ref.width) return id
|
|
44
|
-
}
|
|
45
|
-
return null
|
|
46
|
-
}, [])
|
|
47
|
-
|
|
48
|
-
const handleMouseDown = useCallback(
|
|
49
|
-
(id: string) => {
|
|
50
|
-
setDraggingId(id)
|
|
51
|
-
setDragOrder(baselineOrder)
|
|
52
|
-
lastSwapWithRef.current = null
|
|
53
|
-
},
|
|
54
|
-
[baselineOrder]
|
|
55
|
-
)
|
|
56
|
-
|
|
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(() => {
|
|
77
|
-
const source = draggingId
|
|
78
|
-
const finalOrder = dragOrder
|
|
79
|
-
setDraggingId(null)
|
|
80
|
-
setDragOrder(null)
|
|
81
|
-
lastSwapWithRef.current = null
|
|
82
|
-
|
|
83
|
-
if (source == null || source === '' || !finalOrder) return
|
|
84
|
-
|
|
85
|
-
const changed = !arraysEqual(finalOrder, baselineOrder)
|
|
86
|
-
if (changed) {
|
|
87
|
-
dispatchGlobal({ orderedIds: finalOrder, type: 'reorder-sessions' })
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const idx = baselineOrder.indexOf(source)
|
|
92
|
-
if (idx >= 0) {
|
|
93
|
-
runSideEffectGlobal({ index: idx + 1, type: 'switch-session-by-index' })
|
|
94
|
-
}
|
|
95
|
-
}, [baselineOrder, dragOrder, draggingId])
|
|
96
|
-
|
|
97
|
-
const cancelDrag = useCallback(() => {
|
|
98
|
-
setDraggingId(null)
|
|
99
|
-
setDragOrder(null)
|
|
100
|
-
lastSwapWithRef.current = null
|
|
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
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<box
|
|
119
|
-
width="100%"
|
|
120
|
-
flexDirection="row"
|
|
121
|
-
flexShrink={0}
|
|
122
|
-
paddingLeft={1}
|
|
123
|
-
paddingRight={1}
|
|
124
|
-
backgroundColor={headerBg}
|
|
125
|
-
>
|
|
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
|
-
))}
|
|
141
|
-
<box flexGrow={1} />
|
|
142
|
-
<box
|
|
143
|
-
flexDirection="row"
|
|
144
|
-
paddingLeft={1}
|
|
145
|
-
paddingRight={1}
|
|
146
|
-
backgroundColor={t.backgroundElement}
|
|
147
|
-
onMouseDown={handleNewSession}
|
|
148
|
-
>
|
|
149
|
-
<text fg={t.text} selectable={false}>
|
|
150
|
-
+ New
|
|
151
|
-
</text>
|
|
152
|
-
</box>
|
|
153
|
-
</box>
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function arraysEqual(a: string[], b: string[]): boolean {
|
|
158
|
-
if (a.length !== b.length) return false
|
|
159
|
-
for (let i = 0; i < a.length; i++) {
|
|
160
|
-
if (a[i] !== b[i]) return false
|
|
161
|
-
}
|
|
162
|
-
return true
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
interface SessionChipProps {
|
|
166
|
-
session: SessionRecord
|
|
167
|
-
index: number
|
|
168
|
-
active: boolean
|
|
169
|
-
status: SessionStatus
|
|
170
|
-
dragging: boolean
|
|
171
|
-
setChipRef: (id: string, ref: BoxRenderable | null) => void
|
|
172
|
-
onDragStart: (id: string) => void
|
|
173
|
-
onDrag: (event: OtuiMouseEvent) => void
|
|
174
|
-
onDrop: () => void
|
|
175
|
-
onDragCancel: () => void
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const SessionChip = memo(function SessionChip({
|
|
179
|
-
active,
|
|
180
|
-
dragging,
|
|
181
|
-
index,
|
|
182
|
-
onDrag,
|
|
183
|
-
onDragCancel,
|
|
184
|
-
onDragStart,
|
|
185
|
-
onDrop,
|
|
186
|
-
session,
|
|
187
|
-
setChipRef,
|
|
188
|
-
status,
|
|
189
|
-
}: SessionChipProps) {
|
|
190
|
-
const t = useTheme()
|
|
191
|
-
const selectionBg = t.backgroundElement
|
|
192
|
-
const showSpinner = status.working
|
|
193
|
-
const showWaiting = status.waiting
|
|
194
|
-
const spinner = useBusySpinner(showSpinner)
|
|
195
|
-
const labelColor = active ? t.text : t.textMuted
|
|
196
|
-
const bgColor = dragging || active ? selectionBg : undefined
|
|
197
|
-
const idleColor = t.success
|
|
198
|
-
const workingColor = t.primary
|
|
199
|
-
const waitingColor = t.warning
|
|
200
|
-
const [hovered, setHovered] = useState(false)
|
|
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
|
-
|
|
250
|
-
return (
|
|
251
|
-
<ContextMenuBox
|
|
252
|
-
ref={handleRef}
|
|
253
|
-
flexDirection="row"
|
|
254
|
-
paddingLeft={1}
|
|
255
|
-
paddingRight={1}
|
|
256
|
-
backgroundColor={bgColor}
|
|
257
|
-
rightClickMenu={rightClickMenu}
|
|
258
|
-
onMouseOver={handleMouseOver}
|
|
259
|
-
onMouseOut={handleMouseOut}
|
|
260
|
-
onMouseDown={handleMouseDown}
|
|
261
|
-
onMouseDrag={onDrag}
|
|
262
|
-
onMouseUp={handleMouseUp}
|
|
263
|
-
onMouseDragEnd={onDragCancel}
|
|
264
|
-
>
|
|
265
|
-
{showWaiting ? (
|
|
266
|
-
<text fg={waitingColor} selectable={false}>
|
|
267
|
-
?{' '}
|
|
268
|
-
</text>
|
|
269
|
-
) : null}
|
|
270
|
-
{showSpinner ? (
|
|
271
|
-
<text fg={workingColor} selectable={false}>
|
|
272
|
-
{spinner}{' '}
|
|
273
|
-
</text>
|
|
274
|
-
) : null}
|
|
275
|
-
{!showWaiting && !showSpinner ? (
|
|
276
|
-
<text fg={active ? workingColor : idleColor} selectable={false}>
|
|
277
|
-
{'● '}
|
|
278
|
-
</text>
|
|
279
|
-
) : null}
|
|
280
|
-
<text fg={labelColor} selectable={false}>
|
|
281
|
-
[{index}] {session.name}
|
|
282
|
-
</text>
|
|
283
|
-
{hovered ? (
|
|
284
|
-
<box paddingLeft={1} onMouseDown={handleDeleteMouseDown}>
|
|
285
|
-
<text fg={t.textMuted} selectable={false}>
|
|
286
|
-
×
|
|
287
|
-
</text>
|
|
288
|
-
</box>
|
|
289
|
-
) : (
|
|
290
|
-
<text fg={t.textMuted} selectable={false}>
|
|
291
|
-
{' '}
|
|
292
|
-
</text>
|
|
293
|
-
)}
|
|
294
|
-
</ContextMenuBox>
|
|
295
|
-
)
|
|
296
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { TabSession } from '../../../../state/types'
|
|
2
|
-
|
|
3
|
-
import { allLeafIds, type LayoutNode } from '../../../../state/layout-tree'
|
|
4
|
-
|
|
5
|
-
export interface TabGroupInfo {
|
|
6
|
-
inLayout: boolean
|
|
7
|
-
groupStart: number
|
|
8
|
-
groupEnd: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function buildTabGroupInfo(
|
|
12
|
-
layoutTrees: Record<string, LayoutNode>,
|
|
13
|
-
tabs: TabSession[]
|
|
14
|
-
): Map<string, TabGroupInfo> {
|
|
15
|
-
const tabGroupInfo = new Map<string, TabGroupInfo>()
|
|
16
|
-
|
|
17
|
-
for (const tree of Object.values(layoutTrees)) {
|
|
18
|
-
const ids = allLeafIds(tree)
|
|
19
|
-
if (ids.length <= 1) {
|
|
20
|
-
continue
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const idSet = new Set(ids)
|
|
24
|
-
let groupStart = -1
|
|
25
|
-
let groupEnd = -1
|
|
26
|
-
|
|
27
|
-
for (const [index, tab] of tabs.entries()) {
|
|
28
|
-
if (!idSet.has(tab.id)) {
|
|
29
|
-
continue
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (groupStart === -1) {
|
|
33
|
-
groupStart = index
|
|
34
|
-
}
|
|
35
|
-
groupEnd = index
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
for (const id of ids) {
|
|
39
|
-
tabGroupInfo.set(id, { groupEnd, groupStart, inLayout: true })
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return tabGroupInfo
|
|
44
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export type SidebarScrollTarget = 'none' | 'active-item' | 'top' | 'bottom'
|
|
2
|
-
|
|
3
|
-
interface SidebarScrollTargetOptions {
|
|
4
|
-
previousActiveIndex: number
|
|
5
|
-
nextActiveIndex: number
|
|
6
|
-
tabCount: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getSidebarScrollTarget({
|
|
10
|
-
nextActiveIndex,
|
|
11
|
-
previousActiveIndex,
|
|
12
|
-
tabCount,
|
|
13
|
-
}: SidebarScrollTargetOptions): SidebarScrollTarget {
|
|
14
|
-
if (tabCount === 0 || nextActiveIndex < 0) {
|
|
15
|
-
return 'none'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (previousActiveIndex < 0 || previousActiveIndex === nextActiveIndex) {
|
|
19
|
-
return previousActiveIndex < 0 ? 'active-item' : 'none'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const lastIndex = tabCount - 1
|
|
23
|
-
|
|
24
|
-
if (previousActiveIndex === lastIndex && nextActiveIndex === 0) {
|
|
25
|
-
return 'top'
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (previousActiveIndex === 0 && nextActiveIndex === lastIndex) {
|
|
29
|
-
return 'bottom'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return 'active-item'
|
|
33
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import { getCurrentBranch } from '../../../git-branch'
|
|
4
|
-
|
|
5
|
-
const BRANCH_POLL_INTERVAL_MS = 5_000
|
|
6
|
-
|
|
7
|
-
export function useSidebarBranch(projectPath: string | undefined): string | null {
|
|
8
|
-
const [branch, setBranch] = useState<string | null>(null)
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
if (!(projectPath != null && projectPath !== '')) {
|
|
12
|
-
setBranch(null)
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let isCurrent = true
|
|
17
|
-
const updateBranch = async () => {
|
|
18
|
-
const nextBranch = await getCurrentBranch(projectPath)
|
|
19
|
-
if (isCurrent) {
|
|
20
|
-
setBranch(nextBranch)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
void updateBranch()
|
|
25
|
-
const interval = setInterval(() => {
|
|
26
|
-
void updateBranch()
|
|
27
|
-
}, BRANCH_POLL_INTERVAL_MS)
|
|
28
|
-
|
|
29
|
-
return () => {
|
|
30
|
-
isCurrent = false
|
|
31
|
-
clearInterval(interval)
|
|
32
|
-
}
|
|
33
|
-
}, [projectPath])
|
|
34
|
-
|
|
35
|
-
return branch
|
|
36
|
-
}
|