@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
|
@@ -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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{` ${
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
396
|
+
<TopTabBar forceVisible />
|
|
393
397
|
<GitView themeId={themeId} />
|
|
394
|
-
{sessionBarPosition === 'bottom' && <SessionBar forceVisible />}
|
|
395
398
|
<StatusBar />
|
|
396
399
|
<PendingChordOverlay />
|
|
397
400
|
<ContextMenuOverlay />
|
|
@@ -422,72 +425,74 @@ export function RootView({
|
|
|
422
425
|
onMouseDrag={handleRootMouseDrag}
|
|
423
426
|
onMouseUp={handleRootMouseUp}
|
|
424
427
|
>
|
|
425
|
-
{sessionBarPosition === 'top' && <SessionBar />}
|
|
426
428
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
427
429
|
<Sidebar
|
|
428
|
-
onTabActivate={onPaneActivate}
|
|
429
430
|
onEmbeddedGitResizeStart={onEmbeddedGitResizeStart}
|
|
430
431
|
onResizeDrag={onSeparatorDrag}
|
|
431
432
|
onResizeDragEnd={onSeparatorDragEnd}
|
|
432
433
|
/>
|
|
433
|
-
|
|
434
|
-
<
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
434
|
+
<box flexDirection="column" flexGrow={1}>
|
|
435
|
+
<TopTabBar />
|
|
436
|
+
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
437
|
+
{gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left' ? (
|
|
438
|
+
<GitPaneInPaneMode
|
|
439
|
+
position="left"
|
|
440
|
+
ratio={gitPaneRatio}
|
|
441
|
+
onGitPaneResizeStart={onGitPaneResizeStart}
|
|
442
|
+
/>
|
|
443
|
+
) : null}
|
|
444
|
+
{activeTree && activeTree.type === 'split' ? (
|
|
445
|
+
<SplitLayout
|
|
446
|
+
node={activeTree}
|
|
447
|
+
tabs={tabs}
|
|
448
|
+
activeTabId={activeTabId}
|
|
449
|
+
focusMode={focusMode}
|
|
450
|
+
contentOrigin={splitContentOrigin}
|
|
451
|
+
mouseForwardingEnabled={mouseForwardingEnabled}
|
|
452
|
+
localScrollbackEnabled={localScrollbackEnabled}
|
|
453
|
+
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
454
|
+
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
455
|
+
onTerminalClick={onTerminalClick}
|
|
456
|
+
onTerminalDrag={onTerminalDrag}
|
|
457
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
458
|
+
onPaneActivate={onPaneActivate}
|
|
459
|
+
onSplitResize={onSplitResize}
|
|
460
|
+
onSeparatorDragStart={onSeparatorDragStart}
|
|
461
|
+
onSeparatorDrag={onSeparatorDrag}
|
|
462
|
+
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
463
|
+
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
464
|
+
onMeasure={onMeasure}
|
|
465
|
+
bounds={splitBounds}
|
|
466
|
+
/>
|
|
467
|
+
) : (
|
|
468
|
+
<TerminalPane
|
|
469
|
+
tab={activeTab}
|
|
470
|
+
tabId={activeTabId ?? undefined}
|
|
471
|
+
isActive
|
|
472
|
+
focusMode={focusMode}
|
|
473
|
+
contentOrigin={contentOrigin}
|
|
474
|
+
mouseForwardingEnabled={mouseForwardingEnabled}
|
|
475
|
+
localScrollbackEnabled={localScrollbackEnabled}
|
|
476
|
+
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
477
|
+
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
478
|
+
onTerminalClick={onTerminalClick}
|
|
479
|
+
onTerminalDrag={onTerminalDrag}
|
|
480
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
481
|
+
onPaneActivate={onPaneActivate}
|
|
482
|
+
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
483
|
+
onMeasure={onMeasure}
|
|
484
|
+
/>
|
|
485
|
+
)}
|
|
486
|
+
{gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'right' ? (
|
|
487
|
+
<GitPaneInPaneMode
|
|
488
|
+
position="right"
|
|
489
|
+
ratio={gitPaneRatio}
|
|
490
|
+
onGitPaneResizeStart={onGitPaneResizeStart}
|
|
491
|
+
/>
|
|
492
|
+
) : null}
|
|
493
|
+
</box>
|
|
494
|
+
</box>
|
|
489
495
|
</box>
|
|
490
|
-
{sessionBarPosition === 'bottom' && <SessionBar />}
|
|
491
496
|
<StatusBar />
|
|
492
497
|
<PendingChordOverlay />
|
|
493
498
|
<ContextMenuOverlay />
|