@brimveyn/aimux 1.14.0 → 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/side-effects.ts +145 -1
- package/src/app-runtime/split-drag-controller.ts +3 -1
- package/src/app-runtime/use-terminal-resize.ts +1 -4
- package/src/app.tsx +0 -3
- package/src/config.ts +1 -12
- package/src/git/worktree-branch-poller.ts +54 -0
- package/src/input/modes/types.ts +2 -0
- package/src/state/layout-tree.ts +114 -4
- package/src/state/reducers/session-state.ts +20 -0
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/reducers/ui-state.ts +0 -3
- 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 +1 -4
- package/src/state/workspace-save.ts +0 -1
- 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
package/src/ui/root.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from '../state/types'
|
|
14
14
|
import type { ThemeId } from './themes'
|
|
15
15
|
|
|
16
|
+
import { useWorktreeBranchPolling } from '../git/worktree-branch-poller'
|
|
16
17
|
import { useAppStore } from '../state/app-store'
|
|
17
18
|
import { dispatchGlobal } from '../state/dispatch-ref'
|
|
18
19
|
import { getGitPaneWidthFromRatio } from '../state/git-pane-sizing'
|
|
@@ -20,11 +21,11 @@ import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout
|
|
|
20
21
|
import { GitView } from './components/git/git-view'
|
|
21
22
|
import { buildGitPaneContextMenu } from './components/git/pane/git-pane-context-menu'
|
|
22
23
|
import { GitPaneWidget } from './components/git/pane/git-pane-widget'
|
|
23
|
-
import { SessionBar } from './components/layout/session-bar'
|
|
24
24
|
import { Sidebar } from './components/layout/sidebar/sidebar'
|
|
25
25
|
import { SplitLayout } from './components/layout/split-layout'
|
|
26
26
|
import { StatusBar } from './components/layout/status-bar'
|
|
27
27
|
import { TerminalPane } from './components/layout/terminal-pane'
|
|
28
|
+
import { TopTabBar } from './components/layout/top-tab-bar'
|
|
28
29
|
import { AIUsageModal } from './components/modals/app/ai-usage-modal'
|
|
29
30
|
import { HelpModal } from './components/modals/app/help-modal'
|
|
30
31
|
import { UpdateAvailableModal } from './components/modals/app/update-available-modal'
|
|
@@ -315,7 +316,6 @@ export function RootView({
|
|
|
315
316
|
const sessions = useAppStore((s) => s.sessions)
|
|
316
317
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
317
318
|
const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
|
|
318
|
-
const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
|
|
319
319
|
const gitPaneMode = useAppStore((s) => s.gitPane.mode)
|
|
320
320
|
const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
|
|
321
321
|
const gitPanePosition = useAppStore((s) => s.gitPane.position)
|
|
@@ -326,6 +326,10 @@ export function RootView({
|
|
|
326
326
|
const gitPaneInPaneOnLeft = gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left'
|
|
327
327
|
const splitChrome = PANE_BORDER * 2
|
|
328
328
|
|
|
329
|
+
// Keep every worktree's `branch` in state synchronized with the on-disk
|
|
330
|
+
// HEAD so the sidebar (and divergence calc) never reads a stale value.
|
|
331
|
+
useWorktreeBranchPolling(true)
|
|
332
|
+
|
|
329
333
|
const handleSidebarEdgeResize = useCallback(
|
|
330
334
|
(event: MouseEvent): boolean => {
|
|
331
335
|
onSidebarResizeStart?.({ initialWidth: sidebarWidth, screenStart: event.x })
|
|
@@ -389,9 +393,8 @@ export function RootView({
|
|
|
389
393
|
if (inGitMode) {
|
|
390
394
|
return (
|
|
391
395
|
<box flexDirection="column" width="100%" height="100%" backgroundColor={editorBg}>
|
|
392
|
-
|
|
396
|
+
<TopTabBar forceVisible />
|
|
393
397
|
<GitView themeId={themeId} />
|
|
394
|
-
{sessionBarPosition === 'bottom' && <SessionBar forceVisible />}
|
|
395
398
|
<StatusBar />
|
|
396
399
|
<PendingChordOverlay />
|
|
397
400
|
<ContextMenuOverlay />
|
|
@@ -422,72 +425,74 @@ export function RootView({
|
|
|
422
425
|
onMouseDrag={handleRootMouseDrag}
|
|
423
426
|
onMouseUp={handleRootMouseUp}
|
|
424
427
|
>
|
|
425
|
-
{sessionBarPosition === 'top' && <SessionBar />}
|
|
426
428
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
427
429
|
<Sidebar
|
|
428
|
-
onTabActivate={onPaneActivate}
|
|
429
430
|
onEmbeddedGitResizeStart={onEmbeddedGitResizeStart}
|
|
430
431
|
onResizeDrag={onSeparatorDrag}
|
|
431
432
|
onResizeDragEnd={onSeparatorDragEnd}
|
|
432
433
|
/>
|
|
433
|
-
|
|
434
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
434
|
+
<box flexDirection="column" flexGrow={1}>
|
|
435
|
+
<TopTabBar />
|
|
436
|
+
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
437
|
+
{gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left' ? (
|
|
438
|
+
<GitPaneInPaneMode
|
|
439
|
+
position="left"
|
|
440
|
+
ratio={gitPaneRatio}
|
|
441
|
+
onGitPaneResizeStart={onGitPaneResizeStart}
|
|
442
|
+
/>
|
|
443
|
+
) : null}
|
|
444
|
+
{activeTree && activeTree.type === 'split' ? (
|
|
445
|
+
<SplitLayout
|
|
446
|
+
node={activeTree}
|
|
447
|
+
tabs={tabs}
|
|
448
|
+
activeTabId={activeTabId}
|
|
449
|
+
focusMode={focusMode}
|
|
450
|
+
contentOrigin={splitContentOrigin}
|
|
451
|
+
mouseForwardingEnabled={mouseForwardingEnabled}
|
|
452
|
+
localScrollbackEnabled={localScrollbackEnabled}
|
|
453
|
+
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
454
|
+
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
455
|
+
onTerminalClick={onTerminalClick}
|
|
456
|
+
onTerminalDrag={onTerminalDrag}
|
|
457
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
458
|
+
onPaneActivate={onPaneActivate}
|
|
459
|
+
onSplitResize={onSplitResize}
|
|
460
|
+
onSeparatorDragStart={onSeparatorDragStart}
|
|
461
|
+
onSeparatorDrag={onSeparatorDrag}
|
|
462
|
+
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
463
|
+
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
464
|
+
onMeasure={onMeasure}
|
|
465
|
+
bounds={splitBounds}
|
|
466
|
+
/>
|
|
467
|
+
) : (
|
|
468
|
+
<TerminalPane
|
|
469
|
+
tab={activeTab}
|
|
470
|
+
tabId={activeTabId ?? undefined}
|
|
471
|
+
isActive
|
|
472
|
+
focusMode={focusMode}
|
|
473
|
+
contentOrigin={contentOrigin}
|
|
474
|
+
mouseForwardingEnabled={mouseForwardingEnabled}
|
|
475
|
+
localScrollbackEnabled={localScrollbackEnabled}
|
|
476
|
+
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
477
|
+
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
478
|
+
onTerminalClick={onTerminalClick}
|
|
479
|
+
onTerminalDrag={onTerminalDrag}
|
|
480
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
481
|
+
onPaneActivate={onPaneActivate}
|
|
482
|
+
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
483
|
+
onMeasure={onMeasure}
|
|
484
|
+
/>
|
|
485
|
+
)}
|
|
486
|
+
{gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'right' ? (
|
|
487
|
+
<GitPaneInPaneMode
|
|
488
|
+
position="right"
|
|
489
|
+
ratio={gitPaneRatio}
|
|
490
|
+
onGitPaneResizeStart={onGitPaneResizeStart}
|
|
491
|
+
/>
|
|
492
|
+
) : null}
|
|
493
|
+
</box>
|
|
494
|
+
</box>
|
|
489
495
|
</box>
|
|
490
|
-
{sessionBarPosition === 'bottom' && <SessionBar />}
|
|
491
496
|
<StatusBar />
|
|
492
497
|
<PendingChordOverlay />
|
|
493
498
|
<ContextMenuOverlay />
|
|
@@ -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
|
-
}
|