@brimveyn/aimux 1.14.0 → 1.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/app-runtime/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 +2 -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/status-bar.tsx +168 -33
- 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/components/overlays/ai-usage/ai-usage-indicator.tsx +22 -93
- package/src/ui/root.tsx +68 -63
- package/src/ui/status-bar-model.ts +36 -37
- 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,36 +1,38 @@
|
|
|
1
1
|
import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
-
import type { AppState
|
|
3
|
+
import type { AppState } from '../state/types'
|
|
4
4
|
|
|
5
5
|
import { describeBindings } from '../input/keymap/describe-bindings'
|
|
6
6
|
import { getSessionProjectPath } from '../state/session-worktrees'
|
|
7
7
|
import { buildHintText } from './keymap-context'
|
|
8
8
|
import { abbreviatePath } from './path-format'
|
|
9
9
|
|
|
10
|
+
export interface IdentitySegment {
|
|
11
|
+
id: string
|
|
12
|
+
text: string
|
|
13
|
+
tone: 'primary' | 'muted'
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
export interface StatusBarModel {
|
|
11
|
-
left: string
|
|
12
|
-
right: string
|
|
13
17
|
help: string
|
|
18
|
+
right: string
|
|
19
|
+
sessionSegments: IdentitySegment[]
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
const MAX_TAB_LABEL_LENGTH = 24
|
|
17
22
|
const HINT_LIMIT = 6
|
|
18
23
|
const HELP_DESCRIPTION = 'Help'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
const SEP = ' · '
|
|
25
|
+
|
|
26
|
+
function sessionSegments(
|
|
27
|
+
sessionName: string,
|
|
28
|
+
sessionPath: string | null | undefined
|
|
29
|
+
): IdentitySegment[] {
|
|
30
|
+
const segs: IdentitySegment[] = [{ id: 'session', text: sessionName, tone: 'primary' }]
|
|
31
|
+
if (sessionPath != null && sessionPath !== '') {
|
|
32
|
+
segs.push({ id: 'sep-session-path', text: SEP, tone: 'muted' })
|
|
33
|
+
segs.push({ id: 'path', text: abbreviatePath(sessionPath), tone: 'muted' })
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
return `${label.slice(0, MAX_TAB_LABEL_LENGTH - 3)}...`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getActiveTabLabel(tab?: TabSession): string {
|
|
29
|
-
if (!tab) {
|
|
30
|
-
return 'no tab'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return `${truncateLabel(tab.title)} (${tab.status})`
|
|
35
|
+
return segs
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const STAGING_DESCRIPTIONS = ['Stage', 'Unstage/delete', 'Commit', 'Push']
|
|
@@ -54,64 +56,61 @@ function helpHintForMode(config: ResolvedKeymapConfig, modeId: ModeId): string {
|
|
|
54
56
|
return `${helpBinding.keysDisplay} ${helpBinding.description ?? ''}`.trim()
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
export function getStatusBarModel(
|
|
58
|
-
state: AppState,
|
|
59
|
-
activeTab: TabSession | undefined,
|
|
60
|
-
config: ResolvedKeymapConfig
|
|
61
|
-
): StatusBarModel {
|
|
59
|
+
export function getStatusBarModel(state: AppState, config: ResolvedKeymapConfig): StatusBarModel {
|
|
62
60
|
const currentSession =
|
|
63
61
|
state.currentSessionId != null && state.currentSessionId !== ''
|
|
64
62
|
? state.sessions.find((session) => session.id === state.currentSessionId)
|
|
65
63
|
: undefined
|
|
66
64
|
const sessionName = currentSession?.name ?? 'no workspace'
|
|
67
65
|
const sessionPath = getSessionProjectPath(currentSession)
|
|
68
|
-
const
|
|
69
|
-
sessionPath != null && sessionPath !== ''
|
|
70
|
-
? `${sessionName} (${abbreviatePath(sessionPath)})`
|
|
71
|
-
: sessionName
|
|
72
|
-
|
|
73
|
-
// \u{f0b1} = nf-fa-briefcase (session icon)
|
|
74
|
-
const sessionIcon = '\u{f0b1}'
|
|
66
|
+
const sessionSegs = sessionSegments(sessionName, sessionPath)
|
|
75
67
|
|
|
76
68
|
switch (state.focusMode) {
|
|
77
69
|
case 'terminal-input':
|
|
78
70
|
return {
|
|
79
71
|
help: '',
|
|
80
|
-
left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
|
|
81
72
|
right: hintForMode(config, 'terminal-input'),
|
|
73
|
+
sessionSegments: sessionSegs,
|
|
82
74
|
}
|
|
83
75
|
case 'modal': {
|
|
84
76
|
const modalMode = deriveModalModeId(state.modal.type)
|
|
85
77
|
return {
|
|
86
78
|
help: '',
|
|
87
|
-
left: `${sessionIcon} ${sessionLabel}`,
|
|
88
79
|
right: modalMode ? hintForMode(config, modalMode) : '',
|
|
80
|
+
sessionSegments: sessionSegs,
|
|
89
81
|
}
|
|
90
82
|
}
|
|
91
83
|
case 'git': {
|
|
92
84
|
const headOffset = state.gitMode.headOffset
|
|
93
|
-
const
|
|
94
|
-
|
|
85
|
+
const extras: IdentitySegment[] = []
|
|
86
|
+
if (headOffset > 0) {
|
|
87
|
+
extras.push({ id: 'sep-head-offset', text: SEP, tone: 'muted' })
|
|
88
|
+
extras.push({ id: 'head-offset', text: `HEAD~${headOffset}`, tone: 'muted' })
|
|
89
|
+
}
|
|
90
|
+
if (state.gitMode.reviewBase) {
|
|
91
|
+
extras.push({ id: 'sep-review-base', text: SEP, tone: 'muted' })
|
|
92
|
+
extras.push({ id: 'review-base', text: 'vs base', tone: 'muted' })
|
|
93
|
+
}
|
|
95
94
|
return {
|
|
96
95
|
help: helpHintForMode(config, 'git-mode'),
|
|
97
|
-
left: `${sessionIcon} ${sessionLabel}${offsetTag}${reviewTag}`,
|
|
98
96
|
right: hintForGitMode(config, headOffset),
|
|
97
|
+
sessionSegments: [...sessionSegs, ...extras],
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
case 'command-edit': {
|
|
102
101
|
const commandEditMode = deriveCommandEditModeId(state.modal.type)
|
|
103
102
|
return {
|
|
104
103
|
help: '',
|
|
105
|
-
left: `${sessionIcon} ${sessionLabel}`,
|
|
106
104
|
right: commandEditMode ? hintForMode(config, commandEditMode) : '',
|
|
105
|
+
sessionSegments: sessionSegs,
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
case 'navigation':
|
|
110
109
|
default:
|
|
111
110
|
return {
|
|
112
111
|
help: helpHintForMode(config, 'navigation'),
|
|
113
|
-
left: `${sessionIcon} ${sessionLabel} ${getActiveTabLabel(activeTab)}`,
|
|
114
112
|
right: hintForMode(config, 'navigation'),
|
|
113
|
+
sessionSegments: sessionSegs,
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
}
|
|
@@ -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
|
-
}
|