@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
@@ -0,0 +1,304 @@
1
+ import type { MouseEvent as OtuiMouseEvent, ScrollBoxRenderable } from '@opentui/core'
2
+
3
+ import { memo, type ReactNode, useCallback, useMemo, useRef } from 'react'
4
+
5
+ import type { FocusMode, TabSession } from '../../../state/types'
6
+
7
+ import { useWorktreeDivergencePolling } from '../../../git/worktree-divergence-poller'
8
+ import { useAppStore } from '../../../state/app-store'
9
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
10
+ import { filterTabsForActiveWorktree } from '../../../state/session-worktrees'
11
+ import { buildTabEntries, type GroupEntry } from '../../../state/tab-entries'
12
+ import { useTheme } from '../../theme'
13
+ import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
14
+ import { TabItem } from './sidebar/tab-item'
15
+ import { useTopTabBarAutoScroll } from './sidebar/use-top-tab-bar-auto-scroll'
16
+
17
+ interface TopTabBarProps {
18
+ forceVisible?: boolean
19
+ }
20
+
21
+ const ROW_CONTENT_OPTIONS = {
22
+ flexDirection: 'row' as const,
23
+ gap: 0,
24
+ justifyContent: 'flex-start' as const,
25
+ }
26
+
27
+ function getGroupIndicator(active: boolean, focused: boolean): string {
28
+ if (!active) return '·'
29
+ return focused ? '›' : '•'
30
+ }
31
+
32
+ function getGroupIndicatorColor(
33
+ t: ReturnType<typeof useTheme>,
34
+ active: boolean,
35
+ focused: boolean
36
+ ): string {
37
+ if (!active) return t.textMuted
38
+ return focused ? t.primary : t.text
39
+ }
40
+
41
+ const TopTabCell = memo(function TopTabCell({
42
+ active,
43
+ backgroundColor,
44
+ children,
45
+ entryId,
46
+ onActivate,
47
+ }: {
48
+ entryId: string
49
+ active: boolean
50
+ backgroundColor: string | undefined
51
+ onActivate?: (entryId: string) => void
52
+ children: ReactNode
53
+ }) {
54
+ const handleMouseDown = useCallback(
55
+ (event: OtuiMouseEvent) => {
56
+ event.stopPropagation()
57
+ onActivate?.(entryId)
58
+ },
59
+ [onActivate, entryId]
60
+ )
61
+ return (
62
+ <box
63
+ backgroundColor={backgroundColor}
64
+ flexDirection="row"
65
+ flexShrink={0}
66
+ onMouseDown={handleMouseDown}
67
+ data-active={active ? 'true' : undefined}
68
+ >
69
+ {children}
70
+ </box>
71
+ )
72
+ })
73
+
74
+ function GroupTabItem({
75
+ active,
76
+ entry,
77
+ focused,
78
+ indexLabel,
79
+ }: {
80
+ entry: GroupEntry
81
+ active: boolean
82
+ focused: boolean
83
+ indexLabel?: string
84
+ }) {
85
+ const t = useTheme()
86
+ const indicator = getGroupIndicator(active, focused)
87
+ const indicatorColor = getGroupIndicatorColor(t, active, focused)
88
+
89
+ const closeGroup = useCallback(() => {
90
+ for (const tab of entry.tabs) {
91
+ dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
92
+ runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
93
+ }
94
+ }, [entry.tabs])
95
+
96
+ const handleCloseMouseDown = useCallback(
97
+ (event: OtuiMouseEvent) => {
98
+ event.stopPropagation()
99
+ closeGroup()
100
+ },
101
+ [closeGroup]
102
+ )
103
+
104
+ const rightClickMenu = useMemo<[string, () => void][]>(
105
+ () => [['Close group', closeGroup]],
106
+ [closeGroup]
107
+ )
108
+
109
+ return (
110
+ <ContextMenuBox
111
+ id={`top-tab-${entry.id}`}
112
+ paddingLeft={1}
113
+ paddingRight={1}
114
+ flexDirection="row"
115
+ alignItems="center"
116
+ rightClickMenu={rightClickMenu}
117
+ >
118
+ <text fg={indicatorColor} selectable={false}>
119
+ {indicator}{' '}
120
+ </text>
121
+ {indexLabel != null && indexLabel !== '' ? (
122
+ <text fg={t.textMuted} selectable={false} wrapMode="none">
123
+ {indexLabel}{' '}
124
+ </text>
125
+ ) : null}
126
+ {entry.tabs.map((tab, i) => {
127
+ const isLeafActive = tab.id === entry.activeLeafId
128
+ return (
129
+ <box key={tab.id} flexDirection="row" flexShrink={0}>
130
+ {i > 0 ? (
131
+ <text fg={t.textMuted} selectable={false} wrapMode="none">
132
+ {' | '}
133
+ </text>
134
+ ) : null}
135
+ <text fg={isLeafActive ? t.text : t.textMuted} selectable={false} wrapMode="none">
136
+ {tab.title}
137
+ </text>
138
+ </box>
139
+ )
140
+ })}
141
+ <box paddingLeft={1} onMouseDown={handleCloseMouseDown}>
142
+ <text fg={t.textMuted} selectable={false}>
143
+ ×
144
+ </text>
145
+ </box>
146
+ </ContextMenuBox>
147
+ )
148
+ }
149
+
150
+ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
151
+ const t = useTheme()
152
+ const headerBg = t.backgroundPanel
153
+ const tabs = useAppStore((s) => s.tabs)
154
+ const activeTabId = useAppStore((s) => s.activeTabId)
155
+ const bar = useAppStore((s) => s.sessionBar)
156
+ const sidebar = useAppStore((s) => s.sidebar)
157
+ const currentSessionId = useAppStore((s) => s.currentSessionId)
158
+ const sessions = useAppStore((s) => s.sessions)
159
+ const focusMode: FocusMode = useAppStore((s) => s.focusMode)
160
+ const layoutTrees = useAppStore((s) => s.layoutTrees)
161
+ const tabGroupMap = useAppStore((s) => s.tabGroupMap)
162
+
163
+ // Sidebar now also shows worktree chips with divergence — poll whenever
164
+ // either surface is visible.
165
+ useWorktreeDivergencePolling(bar.visible || sidebar.visible || forceVisible)
166
+
167
+ const currentSession = useMemo(
168
+ () =>
169
+ currentSessionId != null && currentSessionId !== ''
170
+ ? sessions.find((s) => s.id === currentSessionId)
171
+ : undefined,
172
+ [currentSessionId, sessions]
173
+ )
174
+
175
+ const visibleTabs = useMemo(
176
+ () => filterTabsForActiveWorktree(tabs, currentSession),
177
+ [tabs, currentSession]
178
+ )
179
+
180
+ const entries = useMemo(
181
+ () => buildTabEntries(visibleTabs, layoutTrees, tabGroupMap, activeTabId),
182
+ [visibleTabs, layoutTrees, tabGroupMap, activeTabId]
183
+ )
184
+
185
+ const activeEntryId = useMemo(() => {
186
+ for (const entry of entries) {
187
+ if (entry.kind === 'single') {
188
+ if (entry.tab.id === activeTabId) return entry.id
189
+ } else if (entry.tabs.some((tab) => tab.id === activeTabId)) {
190
+ return entry.id
191
+ }
192
+ }
193
+ return null
194
+ }, [entries, activeTabId])
195
+
196
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null)
197
+ useTopTabBarAutoScroll({
198
+ activeTabId: activeEntryId,
199
+ idPrefix: 'top-tab-',
200
+ scrollRef,
201
+ visible: bar.visible || forceVisible,
202
+ })
203
+
204
+ const handleEntryActivate = useCallback(
205
+ (entryId: string) => {
206
+ const entry = entries.find((e) => e.id === entryId)
207
+ if (!entry) return
208
+ const targetTabId = entry.kind === 'single' ? entry.tab.id : entry.activeLeafId
209
+ if (targetTabId !== activeTabId) {
210
+ dispatchGlobal({ tabId: targetTabId, type: 'set-active-tab' })
211
+ }
212
+ dispatchGlobal({ focusMode: 'terminal-input', type: 'set-focus-mode' })
213
+ },
214
+ [entries, activeTabId]
215
+ )
216
+
217
+ const handleNewTab = useCallback((e: OtuiMouseEvent) => {
218
+ e.stopPropagation()
219
+ dispatchGlobal({ type: 'open-new-tab-modal' })
220
+ }, [])
221
+
222
+ if (!bar.visible && !forceVisible) return null
223
+ if (entries.length === 0) return null
224
+
225
+ const isFocused = focusMode === 'terminal-input' || focusMode === 'navigation'
226
+
227
+ return (
228
+ <box
229
+ width="100%"
230
+ height={1}
231
+ flexDirection="row"
232
+ flexShrink={0}
233
+ backgroundColor={headerBg}
234
+ overflow="hidden"
235
+ >
236
+ <scrollbox
237
+ ref={scrollRef}
238
+ height={1}
239
+ flexGrow={1}
240
+ flexShrink={1}
241
+ flexBasis={0}
242
+ scrollX
243
+ viewportCulling
244
+ contentOptions={ROW_CONTENT_OPTIONS}
245
+ >
246
+ {entries.map((entry, index) => {
247
+ // [N] is shown only for the first 9 entries — that's the range
248
+ // Leader+1..9 can address.
249
+ const indexLabel = index < 9 ? `[${index + 1}]` : undefined
250
+ if (entry.kind === 'single') {
251
+ const tab: TabSession = entry.tab
252
+ const isActive = tab.id === activeTabId
253
+ return (
254
+ <TopTabCell
255
+ key={entry.id}
256
+ entryId={entry.id}
257
+ active={isActive}
258
+ onActivate={handleEntryActivate}
259
+ backgroundColor={isActive ? t.backgroundElement : undefined}
260
+ >
261
+ <TabItem
262
+ id={`top-tab-${tab.id}`}
263
+ tab={tab}
264
+ active={isActive}
265
+ focused={isFocused}
266
+ indexLabel={indexLabel}
267
+ alwaysShowClose
268
+ />
269
+ </TopTabCell>
270
+ )
271
+ }
272
+ const isActive = entry.tabs.some((tab) => tab.id === activeTabId)
273
+ return (
274
+ <TopTabCell
275
+ key={entry.id}
276
+ entryId={entry.id}
277
+ active={isActive}
278
+ onActivate={handleEntryActivate}
279
+ backgroundColor={isActive ? t.backgroundElement : undefined}
280
+ >
281
+ <GroupTabItem
282
+ entry={entry}
283
+ active={isActive}
284
+ focused={isFocused}
285
+ indexLabel={indexLabel}
286
+ />
287
+ </TopTabCell>
288
+ )
289
+ })}
290
+ <box
291
+ flexDirection="row"
292
+ flexShrink={0}
293
+ paddingLeft={1}
294
+ paddingRight={1}
295
+ onMouseDown={handleNewTab}
296
+ >
297
+ <text fg={t.textMuted} selectable={false}>
298
+ +
299
+ </text>
300
+ </box>
301
+ </scrollbox>
302
+ </box>
303
+ )
304
+ }
@@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'
3
3
  import type { WorktreeRecord } from '../../../../state/types'
4
4
 
5
5
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
6
+ import { formatDivergence } from '../../../../state/session-worktrees'
6
7
  import { useTheme } from '../../../theme'
7
8
  import { uiTokens } from '../../../ui-tokens'
8
9
  import { ListItem } from '../../primitives/list-item'
@@ -23,14 +24,6 @@ const MOVE_HINTS: [key: string, label: string][] = [
23
24
  ['esc', 'cancel'],
24
25
  ]
25
26
 
26
- function formatDivergence(divergence: { ahead: number; behind: number } | undefined): string {
27
- if (divergence == null) return ''
28
- const parts: string[] = []
29
- if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
30
- if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
31
- return parts.join(' ')
32
- }
33
-
34
27
  export function WorktreeMoveModal({
35
28
  deleteSource,
36
29
  divergence,
@@ -1,4 +1,4 @@
1
- import type { AIUsageTool } from '@brimveyn/aimux-config'
1
+ import type { AIUsageTool, ResolvedTuiTheme } from '@brimveyn/aimux-config'
2
2
 
3
3
  import { useCallback } from 'react'
4
4
 
@@ -6,14 +6,7 @@ import { useAIUsageStore } from '../../../../state/ai-usage-store'
6
6
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
7
7
  import { useTheme } from '../../../theme'
8
8
 
9
- const TOOL_ICON: Record<AIUsageTool, string> = {
10
- claude: 'CC',
11
- codex: 'CO',
12
- }
13
-
14
- const BAR_SEGMENTS = 4
15
- const BAR_FILLED_CHAR = '\u{2501}'
16
- const BAR_EMPTY_CHAR = '\u{2500}'
9
+ const DOT = '●'
17
10
 
18
11
  function formatTokens(total: number): string {
19
12
  if (total >= 1_000_000) return `${(total / 1_000_000).toFixed(1)}M`
@@ -21,36 +14,14 @@ function formatTokens(total: number): string {
21
14
  return String(total)
22
15
  }
23
16
 
24
- function buildBar(percent: number): { empty: string; filled: string } {
25
- let filledCount = 0
26
- for (let i = 0; i < BAR_SEGMENTS; i++) {
27
- if (percent > i * (100 / BAR_SEGMENTS)) filledCount++
28
- }
29
- return {
30
- empty: BAR_EMPTY_CHAR.repeat(BAR_SEGMENTS - filledCount),
31
- filled: BAR_FILLED_CHAR.repeat(filledCount),
32
- }
33
- }
34
-
35
- function formatResetIn(snap: {
36
- resetAt: string | null
37
- timeRemaining: string | null
38
- }): string | null {
39
- if (snap.resetAt != null && snap.resetAt !== '') {
40
- const diffMs = new Date(snap.resetAt).getTime() - Date.now()
41
- if (diffMs > 0) {
42
- const totalMin = Math.round(diffMs / 60_000)
43
- const h = Math.floor(totalMin / 60)
44
- const m = totalMin % 60
45
- return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}h`
46
- }
47
- }
48
- return snap.timeRemaining
17
+ function pickDotColor(t: ResolvedTuiTheme, percent: number): string {
18
+ if (percent >= 85) return t.error
19
+ if (percent >= 60) return t.warning
20
+ return t.success
49
21
  }
50
22
 
51
23
  export function AIUsageIndicator() {
52
24
  const t = useTheme()
53
- const bg = t.backgroundPanel
54
25
  const enabled = useAIUsageStore((s) => s.enabled)
55
26
  const snapshots = useAIUsageStore((s) => s.snapshots)
56
27
 
@@ -72,36 +43,24 @@ export function AIUsageIndicator() {
72
43
 
73
44
  if (entries.length === 0) {
74
45
  return (
75
- <box
76
- flexDirection="row"
77
- paddingLeft={1}
78
- paddingRight={1}
79
- backgroundColor={bg}
80
- onMouseDown={openModal}
81
- >
82
- <text fg={t.textMuted}>…</text>
46
+ <box flexDirection="row" onMouseDown={openModal}>
47
+ <text fg={t.textMuted} selectable={false}>
48
+
49
+ </text>
83
50
  </box>
84
51
  )
85
52
  }
86
53
 
87
54
  return (
88
- <box flexDirection="row" gap={1}>
55
+ <box flexDirection="row" gap={2} onMouseDown={openModal}>
89
56
  {entries.map(({ snap, tool }) => {
90
57
  if (!snap) return null
91
- const icon = TOOL_ICON[tool]
92
58
 
93
59
  if (snap.error != null && snap.error !== '' && !(snap.stale === true)) {
94
60
  return (
95
- <box
96
- key={tool}
97
- flexDirection="row"
98
- paddingLeft={1}
99
- paddingRight={1}
100
- backgroundColor={bg}
101
- onMouseDown={openModal}
102
- >
61
+ <box key={tool} flexDirection="row">
103
62
  <text fg={t.error} selectable={false}>
104
- {`${icon} —`}
63
+ {DOT}
105
64
  </text>
106
65
  </box>
107
66
  )
@@ -109,56 +68,26 @@ export function AIUsageIndicator() {
109
68
 
110
69
  if (snap.percent !== null) {
111
70
  const p = Math.round(snap.percent)
112
- let color = t.success
113
- if (p >= 85) {
114
- color = t.error
115
- } else if (p >= 60) {
116
- color = t.warning
117
- }
118
- const { empty, filled } = buildBar(snap.percent)
119
- const reset = formatResetIn(snap)
120
- const pctText = `${String(p).padStart(2, ' ')}%`
71
+ const color = pickDotColor(t, snap.percent)
121
72
  return (
122
- <box
123
- key={tool}
124
- flexDirection="row"
125
- paddingLeft={1}
126
- paddingRight={1}
127
- backgroundColor={bg}
128
- onMouseDown={openModal}
129
- >
130
- <text fg={color} selectable={false}>
131
- {`${icon} `}
132
- </text>
73
+ <box key={tool} flexDirection="row">
133
74
  <text fg={color} selectable={false}>
134
- {filled}
135
- </text>
136
- <text fg={t.textMuted} selectable={false}>
137
- {empty}
75
+ {DOT}
138
76
  </text>
139
77
  <text fg={t.text} selectable={false}>
140
- {` ${pctText}`}
78
+ {` ${p}%`}
141
79
  </text>
142
- {reset != null && reset !== '' ? (
143
- <text fg={t.textMuted} selectable={false}>
144
- {` · ${reset}`}
145
- </text>
146
- ) : null}
147
80
  </box>
148
81
  )
149
82
  }
150
83
 
151
84
  return (
152
- <box
153
- key={tool}
154
- flexDirection="row"
155
- paddingLeft={1}
156
- paddingRight={1}
157
- backgroundColor={bg}
158
- onMouseDown={openModal}
159
- >
85
+ <box key={tool} flexDirection="row">
86
+ <text fg={t.textMuted} selectable={false}>
87
+ {DOT}
88
+ </text>
160
89
  <text fg={t.textMuted} selectable={false}>
161
- {`${icon} ${formatTokens(snap.tokens.total)}`}
90
+ {` ${formatTokens(snap.tokens.total)}`}
162
91
  </text>
163
92
  </box>
164
93
  )
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 />