@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.
Files changed (35) 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 +2 -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/status-bar.tsx +168 -33
  26. package/src/ui/components/layout/terminal-pane.tsx +30 -0
  27. package/src/ui/components/layout/top-tab-bar.tsx +304 -0
  28. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +1 -8
  29. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +22 -93
  30. package/src/ui/root.tsx +68 -63
  31. package/src/ui/status-bar-model.ts +36 -37
  32. package/src/ui/components/layout/session-bar.tsx +0 -296
  33. package/src/ui/components/layout/sidebar/sidebar-group-metadata.ts +0 -44
  34. package/src/ui/components/layout/sidebar/sidebar-scroll.ts +0 -33
  35. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +0 -36
@@ -3,13 +3,21 @@ import { createHash } from 'node:crypto'
3
3
  import { existsSync } from 'node:fs'
4
4
  import { basename } from 'node:path'
5
5
 
6
- import type { SessionRecord, TabSession, WorktreeRecord } from './types'
6
+ import type { BranchDivergence, SessionRecord, TabSession, WorktreeRecord } from './types'
7
7
 
8
8
  import { createPrefixedId } from '../platform/id'
9
9
  import { isInsideAimuxWorktreeRoot } from '../platform/worktree-paths'
10
10
 
11
11
  const WORKTREE_COLORS = ['#7dd3fc', '#86efac', '#facc15', '#f0abfc', '#fb7185', '#c4b5fd']
12
12
 
13
+ export function formatDivergence(divergence: BranchDivergence | undefined): string {
14
+ if (divergence == null) return ''
15
+ const parts: string[] = []
16
+ if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
17
+ if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
18
+ return parts.join(' ')
19
+ }
20
+
13
21
  export function getWorktreeColor(worktreeId: string): string {
14
22
  let hash = 0
15
23
  for (let i = 0; i < worktreeId.length; i++) {
@@ -98,7 +106,24 @@ function mergeExistingGitWorktrees(worktrees: WorktreeRecord[], now: string): Wo
98
106
  ? primaryRepoRoot
99
107
  : (live[0]?.path ?? anchor.repoRoot)
100
108
  for (const entry of live) {
101
- if (byPath.has(entry.path)) continue
109
+ const existing = byPath.get(entry.path)
110
+ if (existing) {
111
+ // Patch branch / commitSha from git for entries we already track —
112
+ // notably the primary worktree, whose `branch` is never set at creation
113
+ // time (createPrimaryWorktree doesn't run `git`). Without this, the UI
114
+ // and the divergence poller have no branch for primary worktrees.
115
+ const patchedBranch = entry.branch ?? existing.branch
116
+ const patchedSha = entry.head ?? existing.commitSha
117
+ if (patchedBranch !== existing.branch || patchedSha !== existing.commitSha) {
118
+ byPath.set(entry.path, {
119
+ ...existing,
120
+ branch: patchedBranch,
121
+ commitSha: patchedSha,
122
+ updatedAt: now,
123
+ })
124
+ }
125
+ continue
126
+ }
102
127
  if (isInsideAimuxWorktreeRoot(entry.path)) continue
103
128
  const worktree: WorktreeRecord = {
104
129
  branch: entry.branch,
@@ -204,6 +229,24 @@ export function getRenderedTabWorktreeId(
204
229
  return tab.worktreeId ?? worktrees[0]?.id ?? '__main__'
205
230
  }
206
231
 
232
+ export function filterTabsForActiveWorktree(
233
+ tabs: TabSession[],
234
+ session: SessionRecord | undefined
235
+ ): TabSession[] {
236
+ if (!session) return tabs
237
+ const activeWorktreeId = session.activeWorktreeId
238
+ if (activeWorktreeId == null || activeWorktreeId === '') return tabs
239
+ const worktrees = session.worktrees ?? []
240
+ const primaryId = worktrees[0]?.id
241
+ const activeIsPrimary = primaryId != null && primaryId === activeWorktreeId
242
+ return tabs.filter((tab) => {
243
+ const owned = tab.worktreeId != null && tab.worktreeId !== ''
244
+ if (owned) return tab.worktreeId === activeWorktreeId
245
+ // Unbound (legacy) tabs surface only under the primary worktree.
246
+ return activeIsPrimary
247
+ })
248
+ }
249
+
207
250
  export function orderTabsByWorktree(
208
251
  tabs: TabSession[],
209
252
  session: SessionRecord | undefined
@@ -16,7 +16,6 @@ import {
16
16
  type GitPaneMode,
17
17
  type GitPanePosition,
18
18
  type GitPaneState,
19
- type SessionBarPosition,
20
19
  type SessionRecord,
21
20
  type SnippetRecord,
22
21
  } from './types'
@@ -32,7 +31,6 @@ export interface InitialStateOverrides {
32
31
  gitPane?: Partial<GitPaneState>
33
32
  sidebar?: Pick<AppState['sidebar'], 'visible' | 'width'>
34
33
  sessionBarVisible?: boolean
35
- sessionBarPosition?: SessionBarPosition
36
34
  }
37
35
 
38
36
  const DEFAULT_GIT_PANE: GitPaneState = {
@@ -103,7 +101,6 @@ export function createInitialState(
103
101
  multiRepo: EMPTY_MULTI_REPO_STATE,
104
102
  pendingChords: null,
105
103
  sessionBar: {
106
- position: overrides.sessionBarPosition ?? 'top',
107
104
  visible: overrides.sessionBarVisible ?? true,
108
105
  },
109
106
  sessions,
@@ -0,0 +1,74 @@
1
+ import type { TabSession } from './types'
2
+
3
+ import { allLeafIds, type LayoutNode } from './layout-tree'
4
+
5
+ export interface SingleEntry {
6
+ kind: 'single'
7
+ id: string
8
+ tab: TabSession
9
+ }
10
+
11
+ export interface GroupEntry {
12
+ kind: 'group'
13
+ id: string
14
+ groupId: string
15
+ tabs: TabSession[]
16
+ activeLeafId: string
17
+ }
18
+
19
+ export type TabEntry = SingleEntry | GroupEntry
20
+
21
+ /**
22
+ * Collapse a list of visible tabs into "tab strip entries": one entry per
23
+ * standalone tab, one entry per multi-leaf layout group (split). Tabs that
24
+ * belong to a layout tree with only one leaf are treated as standalone.
25
+ *
26
+ * Within a group entry, `activeLeafId` is `activeTabId` when it falls inside
27
+ * the group, otherwise the first leaf in the underlying tabs order.
28
+ */
29
+ export function buildTabEntries(
30
+ visibleTabs: TabSession[],
31
+ layoutTrees: Record<string, LayoutNode>,
32
+ tabGroupMap: Record<string, string>,
33
+ activeTabId: string | null
34
+ ): TabEntry[] {
35
+ // Set of groupIds that are real splits (>= 2 leaves).
36
+ const splitGroupIds = new Set<string>()
37
+ for (const [groupId, tree] of Object.entries(layoutTrees)) {
38
+ if (allLeafIds(tree).length >= 2) {
39
+ splitGroupIds.add(groupId)
40
+ }
41
+ }
42
+
43
+ const entries: TabEntry[] = []
44
+ const emittedGroupIds = new Set<string>()
45
+
46
+ for (const tab of visibleTabs) {
47
+ const groupId = tabGroupMap[tab.id]
48
+ const inSplit = groupId != null && groupId !== '' && splitGroupIds.has(groupId)
49
+
50
+ if (!inSplit) {
51
+ entries.push({ id: tab.id, kind: 'single', tab })
52
+ continue
53
+ }
54
+
55
+ if (emittedGroupIds.has(groupId)) continue
56
+ emittedGroupIds.add(groupId)
57
+
58
+ const members = visibleTabs.filter((t) => tabGroupMap[t.id] === groupId)
59
+ const activeLeafId =
60
+ activeTabId != null && members.some((t) => t.id === activeTabId)
61
+ ? activeTabId
62
+ : (members[0]?.id ?? tab.id)
63
+
64
+ entries.push({
65
+ activeLeafId,
66
+ groupId,
67
+ id: groupId,
68
+ kind: 'group',
69
+ tabs: members,
70
+ })
71
+ }
72
+
73
+ return entries
74
+ }
@@ -141,11 +141,8 @@ export interface SessionRecord {
141
141
  activeWorktreeId?: string
142
142
  }
143
143
 
144
- export type SessionBarPosition = 'top' | 'bottom'
145
-
146
144
  export interface SessionBarState {
147
145
  visible: boolean
148
- position: SessionBarPosition
149
146
  }
150
147
 
151
148
  export interface TabSession {
@@ -539,6 +536,7 @@ export type SessionAction =
539
536
  | { type: 'rename-session-record'; sessionId: string; name: string }
540
537
  | { type: 'delete-session-record'; sessionId: string; openSessionPicker?: boolean }
541
538
  | { type: 'reorder-sessions'; orderedIds: string[] }
539
+ | { type: 'reorder-active-session'; delta: number }
542
540
  | { type: 'set-session-status'; sessionId: string; status: SessionStatus }
543
541
  | { type: 'add-worktree-record'; sessionId: string; worktree: WorktreeRecord; activate?: boolean }
544
542
  | { type: 'remove-worktree-record'; sessionId: string; worktreeId: string }
@@ -619,7 +617,6 @@ export type UIAction =
619
617
  | { type: 'set-git-pane-position'; position: GitPanePosition }
620
618
  | { type: 'set-pending-chords'; chords: string[] | null }
621
619
  | { type: 'toggle-session-bar' }
622
- | { type: 'set-session-bar-position'; position: SessionBarPosition }
623
620
 
624
621
  // -- Git panel actions --
625
622
  export interface GitRefreshPayload {
@@ -33,7 +33,6 @@ export function saveCurrentWorkspace(state: AppState): void {
33
33
  position: state.gitPane.position,
34
34
  visible: state.gitPane.visible,
35
35
  },
36
- sessionBarPosition: state.sessionBar.position,
37
36
  sessionBarVisible: state.sessionBar.visible,
38
37
  sidebar: {
39
38
  visible: state.sidebar.visible,
@@ -1,34 +1,16 @@
1
- import type {
2
- BoxRenderable,
3
- MouseEvent as OtuiMouseEvent,
4
- ScrollBoxRenderable,
5
- } from '@opentui/core'
1
+ import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
6
2
 
7
- import { memo, type ReactNode, useCallback, useMemo, useRef } from 'react'
3
+ import { useCallback, useMemo, useRef } from 'react'
8
4
 
9
- import type { BranchDivergence } from '../../../../state/types'
10
-
11
- import { useWorktreeDivergencePolling } from '../../../../git/worktree-divergence-poller'
12
5
  import { useAppStore } from '../../../../state/app-store'
13
6
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
14
- import {
15
- getActiveWorktree,
16
- getRenderedTabWorktreeId,
17
- getSessionProjectPath,
18
- getWorktreeColor,
19
- orderTabsByWorktree,
20
- } from '../../../../state/session-worktrees'
21
- import { getCurrentTheme, type ResolvedTuiTheme, useTheme } from '../../../theme'
7
+ import { useTheme } from '../../../theme'
22
8
  import { buildGitPaneContextMenu } from '../../git/pane/git-pane-context-menu'
23
9
  import { GitPaneWidget } from '../../git/pane/git-pane-widget'
24
10
  import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
25
- import { buildTabGroupInfo } from './sidebar-group-metadata'
26
- import { TabItem } from './tab-item'
27
- import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
28
- import { useSidebarBranch } from './use-sidebar-branch'
11
+ import { WorkspaceList } from './workspace-list'
29
12
 
30
13
  interface SidebarProps {
31
- onTabActivate?: (tabId: string) => void
32
14
  onResizeDrag?: (event: OtuiMouseEvent) => boolean
33
15
  onResizeDragEnd?: () => void
34
16
  onEmbeddedGitResizeStart?: (info: {
@@ -38,314 +20,9 @@ interface SidebarProps {
38
20
  }) => void
39
21
  }
40
22
 
41
- const GUTTER_START = '╭'
42
- const GUTTER_MIDDLE = '├'
43
- const GUTTER_END = '╰'
44
- const GUTTER_PAD = '│'
45
23
  const RESIZE_HANDLE = '─'
46
- const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
47
-
48
- // Left accent strip + trailing space for a worktree group header.
49
- const WORKTREE_STRIP = '▍ '
50
-
51
- // Compact "↑ahead ↓behind" label for a worktree group header; empty when the
52
- // branch is level with its base (or divergence isn't known yet).
53
- function formatDivergence(divergence: BranchDivergence | undefined): string {
54
- if (divergence == null) return ''
55
- const parts: string[] = []
56
- if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
57
- if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
58
- return parts.join(' ')
59
- }
60
-
61
- // Fit a worktree label into `max` columns, ellipsizing when it would overflow.
62
- // Branch names are ASCII, so character count tracks rendered column width.
63
- function truncateLabel(label: string, max: number): string {
64
- if (max <= 0) return ''
65
- if (label.length <= max) return label
66
- if (max === 1) return '…'
67
- return `${label.slice(0, max - 1)}…`
68
- }
69
-
70
- function getRowBackground({
71
- alternate,
72
- isActive,
73
- t,
74
- }: {
75
- isActive: boolean
76
- alternate: boolean
77
- t: ResolvedTuiTheme
78
- }): string | undefined {
79
- if (isActive) return t.backgroundElement
80
- if (alternate) return t.backgroundPanel
81
- return t.backgroundPanel
82
- }
83
-
84
- const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: number }) {
85
- const t = useTheme()
86
- const currentSessionId = useAppStore((s) => s.currentSessionId)
87
- const sessions = useAppStore((s) => s.sessions)
88
- const currentSession =
89
- currentSessionId != null && currentSessionId !== ''
90
- ? sessions.find((s) => s.id === currentSessionId)
91
- : undefined
92
- const activeWorktree = getActiveWorktree(currentSession)
93
- const projectPath = getSessionProjectPath(currentSession)
94
- const branch = useSidebarBranch(projectPath)
95
-
96
- const handleNewAssistant = useCallback((e: OtuiMouseEvent) => {
97
- e.stopPropagation()
98
- dispatchGlobal({ type: 'open-new-tab-modal' })
99
- }, [])
100
-
101
- return (
102
- <box flexDirection="column" flexShrink={0} gap={0}>
103
- <text fg={t.text} selectable={false}>
104
- <strong>aimux</strong>
105
- </text>
106
- <text fg={t.text} selectable={false}>
107
- {currentSession ? currentSession.name : 'No workspace selected'}
108
- </text>
109
- {(branch != null && branch !== '') || activeWorktree ? (
110
- <box flexDirection="row">
111
- <text fg={t.text} selectable={false}>
112
- {'\u{e702}'}{' '}
113
- </text>
114
- <text fg={t.text} selectable={false}>
115
- {branch ?? activeWorktree?.branch ?? activeWorktree?.name}
116
- </text>
117
- {activeWorktree?.source === 'aimux-temp' ? (
118
- <text fg={t.textMuted} selectable={false}>
119
- {' '}
120
- tmp
121
- </text>
122
- ) : null}
123
- </box>
124
- ) : null}
125
- <box
126
- flexDirection="row"
127
- paddingY={1}
128
- backgroundColor={t.backgroundPanel}
129
- justifyContent="center"
130
- marginTop={1}
131
- onMouseDown={handleNewAssistant}
132
- >
133
- <text fg={t.text} selectable={false}>
134
- + New assistant
135
- </text>
136
- </box>
137
- <text fg={t.textMuted} selectable={false}>
138
- {'·'.repeat(Math.max(0, contentWidth - 2))}
139
- </text>
140
- </box>
141
- )
142
- })
143
-
144
- function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGroupEnd: boolean) {
145
- const t = getCurrentTheme()
146
- return (
147
- <box flexDirection="column" width={1} overflow="hidden">
148
- <text fg={t.border} selectable={false}>
149
- {/* oxlint-disable-next-line no-nested-ternary */}
150
- {isGroupStart ? GUTTER_START : isGroupMiddle ? GUTTER_MIDDLE : GUTTER_PAD}
151
- </text>
152
- <text fg={t.border} selectable={false}>
153
- {isGroupEnd ? GUTTER_END : GUTTER_PAD}
154
- </text>
155
- </box>
156
- )
157
- }
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
-
184
- interface TabsBodyProps {
185
- onTabActivate?: (tabId: string) => void
186
- contentWidth: number
187
- }
188
-
189
- const TabsBody = memo(function TabsBody({ contentWidth, onTabActivate }: TabsBodyProps) {
190
- const t = useTheme()
191
- const tabs = useAppStore((s) => s.tabs)
192
- const activeTabId = useAppStore((s) => s.activeTabId)
193
- const focusMode = useAppStore((s) => s.focusMode)
194
- const layoutTrees = useAppStore((s) => s.layoutTrees)
195
- const sidebarVisible = useAppStore((s) => s.sidebar.visible)
196
- const currentSessionId = useAppStore((s) => s.currentSessionId)
197
- const sessions = useAppStore((s) => s.sessions)
198
- const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
199
- const currentSession =
200
- currentSessionId != null && currentSessionId !== ''
201
- ? sessions.find((s) => s.id === currentSessionId)
202
- : undefined
203
- const worktrees = useMemo(() => currentSession?.worktrees ?? [], [currentSession?.worktrees])
204
- const worktreeById = useMemo(
205
- () => new Map(worktrees.map((worktree) => [worktree.id, worktree])),
206
- [worktrees]
207
- )
208
- const groupedTabs = useMemo(() => {
209
- return orderTabsByWorktree(tabs, currentSession)
210
- }, [currentSession, tabs])
211
- const showWorktreeSeparators = useMemo(() => {
212
- const ids = new Set<string>()
213
- for (const tab of groupedTabs) {
214
- ids.add(getRenderedTabWorktreeId(tab, worktrees))
215
- }
216
- const activeWorktreeId =
217
- currentSession?.activeWorktreeId ?? getActiveWorktree(currentSession)?.id
218
- if (activeWorktreeId != null && activeWorktreeId !== '') ids.add(activeWorktreeId)
219
- return ids.size >= 2
220
- }, [currentSession, groupedTabs, worktrees])
221
-
222
- const scrollRef = useRef<ScrollBoxRenderable | null>(null)
223
- const activeIndex = groupedTabs.findIndex((tab) => tab.id === activeTabId)
224
- const tabGroupInfo = useMemo(
225
- () => buildTabGroupInfo(layoutTrees, groupedTabs),
226
- [layoutTrees, groupedTabs]
227
- )
228
-
229
- useSidebarAutoScroll({
230
- activeIndex,
231
- activeTabId,
232
- scrollRef,
233
- tabCount: groupedTabs.length,
234
- visible: sidebarVisible,
235
- })
236
-
237
- return (
238
- <scrollbox
239
- paddingTop={0}
240
- ref={scrollRef}
241
- flexGrow={1}
242
- scrollY
243
- viewportCulling
244
- contentOptions={COLUMN_CONTENT_OPTIONS}
245
- >
246
- {tabs.length === 0 ? (
247
- <box paddingTop={1}>
248
- <text fg={t.textMuted} selectable={false}>
249
- No tabs yet. Press Ctrl+n.
250
- </text>
251
- </box>
252
- ) : (
253
- groupedTabs.map((tab, index) => {
254
- const isActive = tab.id === activeTabId
255
- const alternate = index % 2 === 1
256
- const info = tabGroupInfo.get(tab.id)
257
- const inLayout = !!(info?.inLayout === true)
258
- const inGroup = info ? index >= info.groupStart && index <= info.groupEnd : false
259
- const isGroupStart = info ? index === info.groupStart : false
260
- const isGroupEnd = info ? index === info.groupEnd : false
261
- const isGroupMiddle = inGroup && !isGroupStart && !isGroupEnd
262
- const tabOwnWorktree =
263
- tab.worktreeId != null && tab.worktreeId !== ''
264
- ? worktreeById.get(tab.worktreeId)
265
- : undefined
266
- let tabWorktree = tabOwnWorktree
267
- if (!tabWorktree && worktrees.length > 1) {
268
- tabWorktree = worktrees[0]
269
- }
270
- // A tab's worktree can be moved when it has a branch (squash needs one,
271
- // so not the primary) and there's at least one other worktree to land in.
272
- const moveWorktreeId =
273
- tabOwnWorktree?.branch != null && tabOwnWorktree.branch !== '' && worktrees.length > 1
274
- ? tabOwnWorktree.id
275
- : undefined
276
- const prevTab = groupedTabs[index - 1]
277
- const startsWorktreeGroup =
278
- !prevTab ||
279
- getRenderedTabWorktreeId(prevTab, worktrees) !==
280
- getRenderedTabWorktreeId(tab, worktrees)
281
- const worktreeColor = tabWorktree
282
- ? (tabWorktree.color ?? getWorktreeColor(tabWorktree.id))
283
- : t.textMuted
284
- const worktreeLabel = tabWorktree?.branch ?? tabWorktree?.name ?? 'main'
285
- const aheadBehind = formatDivergence(
286
- tabWorktree != null ? worktreeDivergence[tabWorktree.id] : undefined
287
- )
288
- const aheadBehindText = aheadBehind === '' ? '' : ` ${aheadBehind}`
289
- // Reserve room for the strip, the ahead/behind chip and a 1-col gap so
290
- // the header never wraps; the divider fills whatever remains (min 0).
291
- const labelBudget = Math.max(
292
- 0,
293
- contentWidth - WORKTREE_STRIP.length - aheadBehindText.length - 1
294
- )
295
- const headerLabel = truncateLabel(worktreeLabel, labelBudget)
296
24
 
297
- return (
298
- <box key={tab.id} flexDirection="column">
299
- {showWorktreeSeparators && startsWorktreeGroup ? (
300
- <box flexDirection="row" overflow="hidden" paddingTop={index === 0 ? 0 : 1}>
301
- <text selectable={false} wrapMode="none" flexShrink={0}>
302
- <span fg={worktreeColor}>
303
- {WORKTREE_STRIP}
304
- {headerLabel}
305
- </span>
306
- {aheadBehindText !== '' ? (
307
- <span fg={t.textMuted}>{aheadBehindText}</span>
308
- ) : null}
309
- </text>
310
- <box flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
311
- <text fg={t.textMuted} selectable={false} wrapMode="none">
312
- {' '}
313
- {'─'.repeat(contentWidth)}
314
- </text>
315
- </box>
316
- </box>
317
- ) : null}
318
- <TabRowButton
319
- tabId={tab.id}
320
- onActivate={onTabActivate}
321
- backgroundColor={getRowBackground({ alternate, isActive, t })}
322
- >
323
- {inGroup ? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd) : null}
324
- <box flexGrow={1}>
325
- <TabItem
326
- id={`sidebar-tab-${tab.id}`}
327
- tab={tab}
328
- active={isActive}
329
- focused={focusMode === 'navigation'}
330
- inLayout={inLayout}
331
- moveWorktreeId={moveWorktreeId}
332
- />
333
- </box>
334
- </TabRowButton>
335
- </box>
336
- )
337
- })
338
- )}
339
- </scrollbox>
340
- )
341
- })
342
-
343
- export function Sidebar({
344
- onEmbeddedGitResizeStart,
345
- onResizeDrag,
346
- onResizeDragEnd,
347
- onTabActivate,
348
- }: SidebarProps) {
25
+ export function Sidebar({ onEmbeddedGitResizeStart, onResizeDrag, onResizeDragEnd }: SidebarProps) {
349
26
  const t = useTheme()
350
27
  const sidebarBg = t.background
351
28
  const sidebarVisible = useAppStore((s) => s.sidebar.visible)
@@ -353,7 +30,6 @@ export function Sidebar({
353
30
  const gitPane = useAppStore((s) => s.gitPane)
354
31
  const focusMode = useAppStore((s) => s.focusMode)
355
32
  const bodyRef = useRef<BoxRenderable | null>(null)
356
- useWorktreeDivergencePolling(sidebarVisible)
357
33
 
358
34
  const handleSidebarMouseDown = useCallback(() => {
359
35
  if (focusMode === 'terminal-input') {
@@ -418,15 +94,10 @@ export function Sidebar({
418
94
  }
419
95
  )
420
96
 
421
- // flex-grow scaled by 100 (integer preferred); tabs gets (1-ratio), git gets ratio.
422
- const tabsGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.embeddedRatio) * 100)) : 1
97
+ // flex-grow scaled by 100 (integer preferred); workspace list gets (1-ratio), git gets ratio.
98
+ const listGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.embeddedRatio) * 100)) : 1
423
99
  const gitGrow = gitEmbedded ? Math.max(1, Math.round(gitPane.embeddedRatio * 100)) : 0
424
100
 
425
- const separator = (
426
- <text fg={t.textMuted} selectable={false}>
427
- {'·'.repeat(Math.max(0, contentWidth - 2))}
428
- </text>
429
- )
430
101
  const gitBody = gitEmbedded ? (
431
102
  <ContextMenuBox
432
103
  flexDirection="column"
@@ -462,7 +133,6 @@ export function Sidebar({
462
133
  >
463
134
  <box flexDirection="row" width={sidebarWidth} flexGrow={1} overflow="hidden">
464
135
  <box width={contentWidth} flexGrow={1} flexDirection="column" overflow="hidden">
465
- <SidebarTop contentWidth={contentWidth} />
466
136
  <box ref={bodyRef} flexDirection="column" flexGrow={1} overflow="hidden">
467
137
  {gitOnTop ? (
468
138
  <>
@@ -472,12 +142,12 @@ export function Sidebar({
472
142
  ) : null}
473
143
  <box
474
144
  flexDirection="column"
475
- flexGrow={tabsGrow}
145
+ flexGrow={listGrow}
476
146
  flexShrink={1}
477
147
  flexBasis={0}
478
148
  overflow="hidden"
479
149
  >
480
- <TabsBody onTabActivate={onTabActivate} contentWidth={contentWidth} />
150
+ <WorkspaceList contentWidth={contentWidth} />
481
151
  </box>
482
152
  {gitOnBottom ? (
483
153
  <>
@@ -486,7 +156,6 @@ export function Sidebar({
486
156
  </>
487
157
  ) : null}
488
158
  </box>
489
- {!gitEmbedded ? separator : null}
490
159
  </box>
491
160
  </box>
492
161
  </ContextMenuBox>