@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.
Files changed (32) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/side-effects.ts +145 -1
  3. package/src/app-runtime/split-drag-controller.ts +3 -1
  4. package/src/app-runtime/use-terminal-resize.ts +1 -4
  5. package/src/app.tsx +0 -3
  6. package/src/config.ts +1 -12
  7. package/src/git/worktree-branch-poller.ts +54 -0
  8. package/src/input/modes/types.ts +2 -0
  9. package/src/state/layout-tree.ts +114 -4
  10. package/src/state/reducers/session-state.ts +20 -0
  11. package/src/state/reducers/tab-state.ts +20 -2
  12. package/src/state/reducers/ui-state.ts +0 -3
  13. package/src/state/session-worktrees.ts +45 -2
  14. package/src/state/store.ts +0 -3
  15. package/src/state/tab-entries.ts +74 -0
  16. package/src/state/types.ts +1 -4
  17. package/src/state/workspace-save.ts +0 -1
  18. package/src/ui/components/layout/sidebar/sidebar.tsx +9 -340
  19. package/src/ui/components/layout/sidebar/tab-item.tsx +51 -42
  20. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +10 -53
  21. package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +30 -0
  22. package/src/ui/components/layout/sidebar/workspace-list.tsx +398 -0
  23. package/src/ui/components/layout/sidebar/worktree-row.tsx +92 -0
  24. package/src/ui/components/layout/split-layout.tsx +20 -39
  25. package/src/ui/components/layout/terminal-pane.tsx +30 -0
  26. package/src/ui/components/layout/top-tab-bar.tsx +304 -0
  27. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +1 -8
  28. package/src/ui/root.tsx +68 -63
  29. package/src/ui/components/layout/session-bar.tsx +0 -296
  30. package/src/ui/components/layout/sidebar/sidebar-group-metadata.ts +0 -44
  31. package/src/ui/components/layout/sidebar/sidebar-scroll.ts +0 -33
  32. 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
- {sessionBarPosition === 'top' && <SessionBar forceVisible />}
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
- {gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left' ? (
434
- <GitPaneInPaneMode
435
- position="left"
436
- ratio={gitPaneRatio}
437
- onGitPaneResizeStart={onGitPaneResizeStart}
438
- />
439
- ) : null}
440
- {activeTree && activeTree.type === 'split' ? (
441
- <SplitLayout
442
- node={activeTree}
443
- tabs={tabs}
444
- activeTabId={activeTabId}
445
- focusMode={focusMode}
446
- contentOrigin={splitContentOrigin}
447
- mouseForwardingEnabled={mouseForwardingEnabled}
448
- localScrollbackEnabled={localScrollbackEnabled}
449
- onTerminalMouseEvent={onTerminalMouseEvent}
450
- onTerminalScrollEvent={onTerminalScrollEvent}
451
- onTerminalClick={onTerminalClick}
452
- onTerminalDrag={onTerminalDrag}
453
- onTerminalMouseUp={onTerminalMouseUp}
454
- onPaneActivate={onPaneActivate}
455
- onSplitResize={onSplitResize}
456
- onSeparatorDragStart={onSeparatorDragStart}
457
- onSeparatorDrag={onSeparatorDrag}
458
- onSeparatorDragEnd={onSeparatorDragEnd}
459
- onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
460
- onMeasure={onMeasure}
461
- bounds={splitBounds}
462
- />
463
- ) : (
464
- <TerminalPane
465
- tab={activeTab}
466
- tabId={activeTabId ?? undefined}
467
- isActive
468
- focusMode={focusMode}
469
- contentOrigin={contentOrigin}
470
- mouseForwardingEnabled={mouseForwardingEnabled}
471
- localScrollbackEnabled={localScrollbackEnabled}
472
- onTerminalMouseEvent={onTerminalMouseEvent}
473
- onTerminalScrollEvent={onTerminalScrollEvent}
474
- onTerminalClick={onTerminalClick}
475
- onTerminalDrag={onTerminalDrag}
476
- onTerminalMouseUp={onTerminalMouseUp}
477
- onPaneActivate={onPaneActivate}
478
- onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
479
- onMeasure={onMeasure}
480
- />
481
- )}
482
- {gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'right' ? (
483
- <GitPaneInPaneMode
484
- position="right"
485
- ratio={gitPaneRatio}
486
- onGitPaneResizeStart={onGitPaneResizeStart}
487
- />
488
- ) : null}
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
- }