@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
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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);
|
|
422
|
-
const
|
|
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={
|
|
145
|
+
flexGrow={listGrow}
|
|
476
146
|
flexShrink={1}
|
|
477
147
|
flexBasis={0}
|
|
478
148
|
overflow="hidden"
|
|
479
149
|
>
|
|
480
|
-
<
|
|
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>
|
|
@@ -17,6 +17,10 @@ interface TabItemProps {
|
|
|
17
17
|
inLayout?: boolean
|
|
18
18
|
/** When set, this tab's worktree can be moved — adds a "Move worktree" entry. */
|
|
19
19
|
moveWorktreeId?: string
|
|
20
|
+
/** 1-based position in the visible tab list — rendered as `[N]` and matches Leader+N. */
|
|
21
|
+
indexLabel?: string
|
|
22
|
+
/** Render the close × unconditionally instead of only on hover. */
|
|
23
|
+
alwaysShowClose?: boolean
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
function getStatusColor(status: TabSession['status']): string {
|
|
@@ -50,31 +54,34 @@ function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean)
|
|
|
50
54
|
return inLayout ? t.textMuted : t.textMuted
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
function
|
|
57
|
+
function BusyGlyph() {
|
|
54
58
|
const t = useTheme()
|
|
55
59
|
const frame = useBusySpinner()
|
|
56
60
|
return (
|
|
57
61
|
<text fg={t.primary} selectable={false}>
|
|
58
|
-
{
|
|
62
|
+
{' '}
|
|
63
|
+
{frame}
|
|
59
64
|
</text>
|
|
60
65
|
)
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
function
|
|
68
|
+
function WaitingGlyph() {
|
|
64
69
|
const t = useTheme()
|
|
65
70
|
return (
|
|
66
71
|
<text fg={t.warning} selectable={false}>
|
|
67
|
-
|
|
72
|
+
{' '}
|
|
73
|
+
?
|
|
68
74
|
</text>
|
|
69
75
|
)
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
function
|
|
78
|
+
function ActivityGlyph({ tab }: { tab: TabSession }) {
|
|
73
79
|
const t = useTheme()
|
|
74
80
|
if (tab.status === 'error') {
|
|
75
81
|
return (
|
|
76
82
|
<text fg={t.error} selectable={false}>
|
|
77
|
-
|
|
83
|
+
{' '}
|
|
84
|
+
✗
|
|
78
85
|
</text>
|
|
79
86
|
)
|
|
80
87
|
}
|
|
@@ -82,37 +89,48 @@ function ActivityIndicator({ tab }: { tab: TabSession }) {
|
|
|
82
89
|
if (tab.status === 'disconnected') {
|
|
83
90
|
return (
|
|
84
91
|
<text fg={t.warning} selectable={false}>
|
|
85
|
-
|
|
92
|
+
{' '}
|
|
93
|
+
⏸
|
|
86
94
|
</text>
|
|
87
95
|
)
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
if (tab.activity === 'working') {
|
|
91
|
-
return <
|
|
99
|
+
return <BusyGlyph />
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
if (tab.activity === 'waiting-input') {
|
|
95
|
-
return <
|
|
103
|
+
return <WaitingGlyph />
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
if (tab.activity === 'idle') {
|
|
99
107
|
return (
|
|
100
108
|
<text fg={t.success} selectable={false}>
|
|
101
|
-
|
|
109
|
+
{' '}
|
|
110
|
+
●
|
|
102
111
|
</text>
|
|
103
112
|
)
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
return (
|
|
107
116
|
<text fg={getStatusColor(tab.status)} selectable={false}>
|
|
108
|
-
{
|
|
117
|
+
{' '}
|
|
118
|
+
·
|
|
109
119
|
</text>
|
|
110
120
|
)
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
export function TabItem({
|
|
123
|
+
export function TabItem({
|
|
124
|
+
active,
|
|
125
|
+
alwaysShowClose,
|
|
126
|
+
focused,
|
|
127
|
+
id,
|
|
128
|
+
indexLabel,
|
|
129
|
+
inLayout,
|
|
130
|
+
moveWorktreeId,
|
|
131
|
+
tab,
|
|
132
|
+
}: TabItemProps) {
|
|
114
133
|
const t = useTheme()
|
|
115
|
-
const label = tab.command.split(' ')[0]
|
|
116
134
|
const isInLayout = inLayout ?? false
|
|
117
135
|
const indicator = getIndicator(active, focused, isInLayout)
|
|
118
136
|
const indicatorColor = getIndicatorColor(active, focused, isInLayout)
|
|
@@ -135,21 +153,19 @@ export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }:
|
|
|
135
153
|
},
|
|
136
154
|
],
|
|
137
155
|
[
|
|
138
|
-
'Move
|
|
156
|
+
'Move left',
|
|
139
157
|
() => {
|
|
140
158
|
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
141
159
|
dispatchGlobal({ delta: -1, type: 'reorder-active-tab' })
|
|
142
160
|
},
|
|
143
161
|
],
|
|
144
162
|
[
|
|
145
|
-
'Move
|
|
163
|
+
'Move right',
|
|
146
164
|
() => {
|
|
147
165
|
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
148
166
|
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
149
167
|
},
|
|
150
168
|
],
|
|
151
|
-
// Move this tab's worktree into another one. Opened from here it overlays
|
|
152
|
-
// the normal view (no git mode) since the open action doesn't touch focus.
|
|
153
169
|
...(moveWorktreeId != null && moveWorktreeId !== ''
|
|
154
170
|
? [
|
|
155
171
|
[
|
|
@@ -181,38 +197,31 @@ export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }:
|
|
|
181
197
|
id={id}
|
|
182
198
|
paddingLeft={1}
|
|
183
199
|
paddingRight={1}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
flexDirection="column"
|
|
187
|
-
gap={0}
|
|
200
|
+
flexDirection="row"
|
|
201
|
+
alignItems="center"
|
|
188
202
|
rightClickMenu={rightClickMenu}
|
|
189
203
|
onMouseOver={handleMouseOver}
|
|
190
204
|
onMouseOut={handleMouseOut}
|
|
191
205
|
>
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
206
|
+
<text fg={indicatorColor} selectable={false}>
|
|
207
|
+
{indicator}{' '}
|
|
208
|
+
</text>
|
|
209
|
+
{indexLabel != null && indexLabel !== '' ? (
|
|
210
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
211
|
+
{indexLabel}{' '}
|
|
195
212
|
</text>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
213
|
+
) : null}
|
|
214
|
+
<text fg={active ? t.text : t.textMuted} selectable={false} wrapMode="none">
|
|
215
|
+
{tab.title}
|
|
216
|
+
</text>
|
|
217
|
+
<ActivityGlyph tab={tab} />
|
|
218
|
+
{alwaysShowClose === true || hovered ? (
|
|
219
|
+
<box paddingLeft={1} onMouseDown={handleCloseMouseDown}>
|
|
220
|
+
<text fg={t.textMuted} selectable={false}>
|
|
221
|
+
×
|
|
199
222
|
</text>
|
|
200
223
|
</box>
|
|
201
|
-
|
|
202
|
-
<box onMouseDown={handleCloseMouseDown}>
|
|
203
|
-
<text fg={t.textMuted} selectable={false}>
|
|
204
|
-
×
|
|
205
|
-
</text>
|
|
206
|
-
</box>
|
|
207
|
-
) : null}
|
|
208
|
-
</box>
|
|
209
|
-
<box flexDirection="row">
|
|
210
|
-
<text fg={t.textMuted} selectable={false}>
|
|
211
|
-
{' '}
|
|
212
|
-
{label}{' '}
|
|
213
|
-
</text>
|
|
214
|
-
<ActivityIndicator tab={tab} />
|
|
215
|
-
</box>
|
|
224
|
+
) : null}
|
|
216
225
|
</ContextMenuBox>
|
|
217
226
|
)
|
|
218
227
|
}
|
|
@@ -1,63 +1,20 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import { useEffect
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
interface UseSidebarAutoScrollOptions {
|
|
5
|
+
interface Options {
|
|
8
6
|
scrollRef: React.RefObject<ScrollBoxRenderable | null>
|
|
9
7
|
visible: boolean
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
tabCount: number
|
|
8
|
+
/** Full id of the active row to bring into view (workspace OR worktree). */
|
|
9
|
+
activeRowId: string | null
|
|
13
10
|
}
|
|
14
11
|
|
|
15
|
-
export function useSidebarAutoScroll({
|
|
16
|
-
activeIndex,
|
|
17
|
-
activeTabId,
|
|
18
|
-
scrollRef,
|
|
19
|
-
tabCount,
|
|
20
|
-
visible,
|
|
21
|
-
}: UseSidebarAutoScrollOptions): void {
|
|
22
|
-
const previousActiveIndexRef = useRef(-1)
|
|
23
|
-
const previousVisibilityRef = useRef(visible)
|
|
24
|
-
|
|
12
|
+
export function useSidebarAutoScroll({ activeRowId, scrollRef, visible }: Options): void {
|
|
25
13
|
useEffect(() => {
|
|
26
|
-
if (!visible)
|
|
27
|
-
|
|
28
|
-
previousActiveIndexRef.current = activeIndex
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
14
|
+
if (!visible) return
|
|
15
|
+
if (activeRowId == null || activeRowId === '') return
|
|
32
16
|
const scrollbox = scrollRef.current
|
|
33
|
-
if (!scrollbox)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!previousVisibilityRef.current && activeTabId != null && activeTabId !== '') {
|
|
40
|
-
scrollbox.scrollChildIntoView(`sidebar-tab-${activeTabId}`)
|
|
41
|
-
previousVisibilityRef.current = true
|
|
42
|
-
previousActiveIndexRef.current = activeIndex
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const scrollTarget = getSidebarScrollTarget({
|
|
47
|
-
nextActiveIndex: activeIndex,
|
|
48
|
-
previousActiveIndex: previousActiveIndexRef.current,
|
|
49
|
-
tabCount,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
if (scrollTarget === 'top') {
|
|
53
|
-
scrollbox.scrollTo({ x: 0, y: 0 })
|
|
54
|
-
} else if (scrollTarget === 'bottom') {
|
|
55
|
-
scrollbox.scrollTo({ x: 0, y: scrollbox.scrollHeight })
|
|
56
|
-
} else if (scrollTarget === 'active-item' && activeTabId != null && activeTabId !== '') {
|
|
57
|
-
scrollbox.scrollChildIntoView(`sidebar-tab-${activeTabId}`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
previousVisibilityRef.current = visible
|
|
61
|
-
previousActiveIndexRef.current = activeIndex
|
|
62
|
-
}, [activeIndex, activeTabId, scrollRef, tabCount, visible])
|
|
17
|
+
if (!scrollbox) return
|
|
18
|
+
scrollbox.scrollChildIntoView(activeRowId)
|
|
19
|
+
}, [activeRowId, scrollRef, visible])
|
|
63
20
|
}
|