@brimveyn/aimux 1.13.2 → 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.
- package/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +1 -3
- package/src/app-runtime/multi-click-clipboard-guard.ts +24 -12
- package/src/app-runtime/pty-write.ts +6 -9
- package/src/app-runtime/side-effects.ts +149 -19
- package/src/app-runtime/snippet-actions.ts +2 -3
- package/src/app-runtime/split-drag-controller.ts +3 -1
- package/src/app-runtime/use-backend-runtime.ts +4 -7
- package/src/app-runtime/use-mouse-handlers.ts +30 -1
- package/src/app-runtime/use-renderer-bindings.ts +4 -12
- package/src/app-runtime/use-terminal-resize.ts +7 -22
- package/src/app.tsx +0 -6
- package/src/config.ts +1 -12
- package/src/daemon/daemon.ts +2 -19
- package/src/daemon/session-manager.ts +3 -15
- package/src/daemon/session-registry.ts +11 -30
- package/src/git/worktree-branch-poller.ts +54 -0
- package/src/input/modes/types.ts +2 -0
- package/src/input/terminal-text-extraction.ts +7 -8
- package/src/ipc/manager-protocol.ts +6 -38
- package/src/ipc/protocol.ts +9 -37
- package/src/pty/pty-manager.ts +20 -31
- package/src/pty/terminal-snapshot.ts +43 -0
- package/src/session-backend/local-session-backend.ts +7 -31
- package/src/session-backend/remote-session-backend.ts +5 -32
- package/src/session-backend/types.ts +2 -15
- package/src/state/layout-tree.ts +114 -4
- package/src/state/reducers/session-state.ts +20 -0
- package/src/state/reducers/tab-state.ts +22 -22
- package/src/state/reducers/ui-state.ts +0 -3
- package/src/state/session-persistence.ts +2 -22
- 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 +4 -15
- package/src/state/validation.ts +0 -8
- package/src/state/workspace-save.ts +0 -1
- package/src/terminal-manager/manager-client.ts +5 -29
- package/src/terminal-manager/terminal-manager.ts +2 -17
- 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/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/root.tsx +68 -63
- 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
|
@@ -3,6 +3,7 @@ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
3
3
|
import { memo, type ReactNode, useCallback, useMemo } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
|
+
import type { JunctionEdgeInfo, JunctionEdges } from '../../../state/layout-tree'
|
|
6
7
|
import type { FocusMode, TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
7
8
|
|
|
8
9
|
import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
|
|
@@ -28,8 +29,10 @@ interface TerminalPaneProps {
|
|
|
28
29
|
onPaneActivate?: (tabId: string) => void
|
|
29
30
|
onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
|
|
30
31
|
onSeparatorDragEnd?: () => void
|
|
32
|
+
onSeparatorDragStart?: (info: JunctionEdgeInfo) => void
|
|
31
33
|
onLeftEdgeMouseDown?: (event: OtuiMouseEvent) => boolean
|
|
32
34
|
onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
|
|
35
|
+
junctionEdges?: JunctionEdges
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function getTitle(
|
|
@@ -113,6 +116,7 @@ export function TerminalPane({
|
|
|
113
116
|
contentOrigin,
|
|
114
117
|
focusMode,
|
|
115
118
|
isActive,
|
|
119
|
+
junctionEdges,
|
|
116
120
|
localScrollbackEnabled,
|
|
117
121
|
mouseForwardingEnabled,
|
|
118
122
|
onLeftEdgeMouseDown,
|
|
@@ -120,6 +124,7 @@ export function TerminalPane({
|
|
|
120
124
|
onPaneActivate,
|
|
121
125
|
onSeparatorDrag,
|
|
122
126
|
onSeparatorDragEnd,
|
|
127
|
+
onSeparatorDragStart,
|
|
123
128
|
onTerminalClick,
|
|
124
129
|
onTerminalDrag,
|
|
125
130
|
onTerminalMouseEvent,
|
|
@@ -175,6 +180,16 @@ export function TerminalPane({
|
|
|
175
180
|
event.y === contentOrigin.y + contentOrigin.rows,
|
|
176
181
|
[contentOrigin]
|
|
177
182
|
)
|
|
183
|
+
const getJunctionSideAt = useCallback(
|
|
184
|
+
(event: OtuiMouseEvent): keyof JunctionEdges | null => {
|
|
185
|
+
if (event.x === contentOrigin.x - 1) return 'left'
|
|
186
|
+
if (event.x === contentOrigin.x + contentOrigin.cols) return 'right'
|
|
187
|
+
if (event.y === contentOrigin.y - 1) return 'top'
|
|
188
|
+
if (event.y === contentOrigin.y + contentOrigin.rows) return 'bottom'
|
|
189
|
+
return null
|
|
190
|
+
},
|
|
191
|
+
[contentOrigin]
|
|
192
|
+
)
|
|
178
193
|
const forwardMouseEvent = useCallback(
|
|
179
194
|
(event: OtuiMouseEvent) => {
|
|
180
195
|
if (event.type === 'down' && event.button === 2 && rightClickMenu) {
|
|
@@ -195,6 +210,18 @@ export function TerminalPane({
|
|
|
195
210
|
return
|
|
196
211
|
}
|
|
197
212
|
}
|
|
213
|
+
if (event.type === 'down' && event.button === 0 && junctionEdges && onSeparatorDragStart) {
|
|
214
|
+
const side = getJunctionSideAt(event)
|
|
215
|
+
if (side !== null) {
|
|
216
|
+
const info = junctionEdges[side]
|
|
217
|
+
if (info) {
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
event.stopPropagation()
|
|
220
|
+
onSeparatorDragStart(info)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
198
225
|
// Absorb left-button clicks on pane borders — they should not focus the
|
|
199
226
|
// pane or be forwarded to the terminal. This keeps borders available for
|
|
200
227
|
// resize actions (split separators, sidebar edge) without conflicting
|
|
@@ -248,11 +275,14 @@ export function TerminalPane({
|
|
|
248
275
|
[
|
|
249
276
|
canForwardMouse,
|
|
250
277
|
contentOrigin,
|
|
278
|
+
getJunctionSideAt,
|
|
251
279
|
isOnPaneBorder,
|
|
280
|
+
junctionEdges,
|
|
252
281
|
onLeftEdgeMouseDown,
|
|
253
282
|
onPaneActivate,
|
|
254
283
|
onSeparatorDrag,
|
|
255
284
|
onSeparatorDragEnd,
|
|
285
|
+
onSeparatorDragStart,
|
|
256
286
|
onTerminalClick,
|
|
257
287
|
onTerminalDrag,
|
|
258
288
|
onTerminalMouseEvent,
|
|
@@ -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,
|
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 />
|