@brimveyn/aimux 1.12.6 → 1.13.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/README.md +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +29 -13
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +563 -85
- package/src/app-runtime/snippet-actions.ts +3 -3
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +21 -11
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +19 -3
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +11 -1
- package/src/input/raw-input-handler.ts +1 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +4 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +2 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +60 -45
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +355 -21
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +32 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +39 -11
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
- package/src/ui/components/git/git-panel.tsx +129 -83
- package/src/ui/components/git/git-view.tsx +96 -28
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +14 -13
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +135 -117
- package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
- package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +163 -120
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +29 -20
- package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
- package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
- package/src/ui/components/modals/shared/form.tsx +6 -6
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +71 -32
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +23 -7
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +121 -38
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
MouseEvent as OtuiMouseEvent,
|
|
4
|
+
ScrollBoxRenderable,
|
|
5
5
|
} from '@opentui/core'
|
|
6
|
-
import { memo, useMemo, useRef } from 'react'
|
|
7
6
|
|
|
7
|
+
import { memo, type ReactNode, useCallback, useMemo, useRef } from 'react'
|
|
8
|
+
|
|
9
|
+
import type { BranchDivergence } from '../../../../state/types'
|
|
10
|
+
|
|
11
|
+
import { useWorktreeDivergencePolling } from '../../../../git/worktree-divergence-poller'
|
|
8
12
|
import { useAppStore } from '../../../../state/app-store'
|
|
9
13
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
14
|
+
import {
|
|
15
|
+
getActiveWorktree,
|
|
16
|
+
getRenderedTabWorktreeId,
|
|
17
|
+
getSessionProjectPath,
|
|
18
|
+
getWorktreeColor,
|
|
19
|
+
orderTabsByWorktree,
|
|
20
|
+
} from '../../../../state/session-worktrees'
|
|
10
21
|
import { getCurrentTheme, type ResolvedTuiTheme, useTheme } from '../../../theme'
|
|
11
22
|
import { buildGitPaneContextMenu } from '../../git/pane/git-pane-context-menu'
|
|
12
23
|
import { GitPaneWidget } from '../../git/pane/git-pane-widget'
|
|
@@ -32,6 +43,29 @@ const GUTTER_MIDDLE = '├'
|
|
|
32
43
|
const GUTTER_END = '╰'
|
|
33
44
|
const GUTTER_PAD = '│'
|
|
34
45
|
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
|
+
}
|
|
35
69
|
|
|
36
70
|
function getRowBackground({
|
|
37
71
|
alternate,
|
|
@@ -51,12 +85,19 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
51
85
|
const t = useTheme()
|
|
52
86
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
53
87
|
const sessions = useAppStore((s) => s.sessions)
|
|
54
|
-
const currentSession =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
58
94
|
const branch = useSidebarBranch(projectPath)
|
|
59
95
|
|
|
96
|
+
const handleNewAssistant = useCallback((e: OtuiMouseEvent) => {
|
|
97
|
+
e.stopPropagation()
|
|
98
|
+
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
60
101
|
return (
|
|
61
102
|
<box flexDirection="column" flexShrink={0} gap={0}>
|
|
62
103
|
<text fg={t.text} selectable={false}>
|
|
@@ -65,14 +106,20 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
65
106
|
<text fg={t.text} selectable={false}>
|
|
66
107
|
{currentSession ? currentSession.name : 'No workspace selected'}
|
|
67
108
|
</text>
|
|
68
|
-
{branch ? (
|
|
109
|
+
{(branch != null && branch !== '') || activeWorktree ? (
|
|
69
110
|
<box flexDirection="row">
|
|
70
111
|
<text fg={t.text} selectable={false}>
|
|
71
112
|
{'\u{e702}'}{' '}
|
|
72
113
|
</text>
|
|
73
114
|
<text fg={t.text} selectable={false}>
|
|
74
|
-
{branch}
|
|
115
|
+
{branch ?? activeWorktree?.branch ?? activeWorktree?.name}
|
|
75
116
|
</text>
|
|
117
|
+
{activeWorktree?.source === 'aimux-temp' ? (
|
|
118
|
+
<text fg={t.textMuted} selectable={false}>
|
|
119
|
+
{' '}
|
|
120
|
+
tmp
|
|
121
|
+
</text>
|
|
122
|
+
) : null}
|
|
76
123
|
</box>
|
|
77
124
|
) : null}
|
|
78
125
|
<box
|
|
@@ -81,10 +128,7 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
81
128
|
backgroundColor={t.backgroundPanel}
|
|
82
129
|
justifyContent="center"
|
|
83
130
|
marginTop={1}
|
|
84
|
-
onMouseDown={
|
|
85
|
-
e.stopPropagation()
|
|
86
|
-
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
87
|
-
}}
|
|
131
|
+
onMouseDown={handleNewAssistant}
|
|
88
132
|
>
|
|
89
133
|
<text fg={t.text} selectable={false}>
|
|
90
134
|
+ New assistant
|
|
@@ -112,27 +156,81 @@ function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGrou
|
|
|
112
156
|
)
|
|
113
157
|
}
|
|
114
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
|
+
|
|
115
184
|
interface TabsBodyProps {
|
|
116
185
|
onTabActivate?: (tabId: string) => void
|
|
186
|
+
contentWidth: number
|
|
117
187
|
}
|
|
118
188
|
|
|
119
|
-
const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
189
|
+
const TabsBody = memo(function TabsBody({ contentWidth, onTabActivate }: TabsBodyProps) {
|
|
120
190
|
const t = useTheme()
|
|
121
191
|
const tabs = useAppStore((s) => s.tabs)
|
|
122
192
|
const activeTabId = useAppStore((s) => s.activeTabId)
|
|
123
193
|
const focusMode = useAppStore((s) => s.focusMode)
|
|
124
194
|
const layoutTrees = useAppStore((s) => s.layoutTrees)
|
|
125
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])
|
|
126
221
|
|
|
127
222
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
128
|
-
const activeIndex =
|
|
129
|
-
const tabGroupInfo = useMemo(
|
|
223
|
+
const activeIndex = groupedTabs.findIndex((tab) => tab.id === activeTabId)
|
|
224
|
+
const tabGroupInfo = useMemo(
|
|
225
|
+
() => buildTabGroupInfo(layoutTrees, groupedTabs),
|
|
226
|
+
[layoutTrees, groupedTabs]
|
|
227
|
+
)
|
|
130
228
|
|
|
131
229
|
useSidebarAutoScroll({
|
|
132
230
|
activeIndex,
|
|
133
231
|
activeTabId,
|
|
134
232
|
scrollRef,
|
|
135
|
-
tabCount:
|
|
233
|
+
tabCount: groupedTabs.length,
|
|
136
234
|
visible: sidebarVisible,
|
|
137
235
|
})
|
|
138
236
|
|
|
@@ -143,7 +241,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
143
241
|
flexGrow={1}
|
|
144
242
|
scrollY
|
|
145
243
|
viewportCulling
|
|
146
|
-
contentOptions={
|
|
244
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
147
245
|
>
|
|
148
246
|
{tabs.length === 0 ? (
|
|
149
247
|
<box paddingTop={1}>
|
|
@@ -152,36 +250,88 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
152
250
|
</text>
|
|
153
251
|
</box>
|
|
154
252
|
) : (
|
|
155
|
-
|
|
253
|
+
groupedTabs.map((tab, index) => {
|
|
156
254
|
const isActive = tab.id === activeTabId
|
|
157
255
|
const alternate = index % 2 === 1
|
|
158
256
|
const info = tabGroupInfo.get(tab.id)
|
|
159
|
-
const inLayout = !!info?.inLayout
|
|
257
|
+
const inLayout = !!(info?.inLayout === true)
|
|
160
258
|
const inGroup = info ? index >= info.groupStart && index <= info.groupEnd : false
|
|
161
259
|
const isGroupStart = info ? index === info.groupStart : false
|
|
162
260
|
const isGroupEnd = info ? index === info.groupEnd : false
|
|
163
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)
|
|
164
296
|
|
|
165
297
|
return (
|
|
166
|
-
<box
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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>
|
|
185
335
|
</box>
|
|
186
336
|
)
|
|
187
337
|
})
|
|
@@ -203,6 +353,53 @@ export function Sidebar({
|
|
|
203
353
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
204
354
|
const focusMode = useAppStore((s) => s.focusMode)
|
|
205
355
|
const bodyRef = useRef<BoxRenderable | null>(null)
|
|
356
|
+
useWorktreeDivergencePolling(sidebarVisible)
|
|
357
|
+
|
|
358
|
+
const handleSidebarMouseDown = useCallback(() => {
|
|
359
|
+
if (focusMode === 'terminal-input') {
|
|
360
|
+
dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
|
|
361
|
+
}
|
|
362
|
+
}, [focusMode])
|
|
363
|
+
const handleSidebarMouseDrag = useCallback(
|
|
364
|
+
(event: OtuiMouseEvent) => {
|
|
365
|
+
if (onResizeDrag?.(event) === true) {
|
|
366
|
+
event.preventDefault()
|
|
367
|
+
event.stopPropagation()
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
[onResizeDrag]
|
|
371
|
+
)
|
|
372
|
+
const handleSidebarMouseUp = useCallback(() => {
|
|
373
|
+
onResizeDragEnd?.()
|
|
374
|
+
}, [onResizeDragEnd])
|
|
375
|
+
const handleEmbeddedHandleMouseDown = useCallback(
|
|
376
|
+
(event: OtuiMouseEvent) => {
|
|
377
|
+
const body = bodyRef.current
|
|
378
|
+
if (!body) return
|
|
379
|
+
event.preventDefault()
|
|
380
|
+
event.stopPropagation()
|
|
381
|
+
onEmbeddedGitResizeStart?.({
|
|
382
|
+
containerStart: body.y,
|
|
383
|
+
position: gitPane.position === 'top' ? 'top' : 'bottom',
|
|
384
|
+
totalSize: Math.max(1, body.height),
|
|
385
|
+
})
|
|
386
|
+
},
|
|
387
|
+
[gitPane.position, onEmbeddedGitResizeStart]
|
|
388
|
+
)
|
|
389
|
+
const sidebarMenu = useMemo<[string, () => void][]>(
|
|
390
|
+
() => [
|
|
391
|
+
['Hide sidebar', () => dispatchGlobal({ type: 'toggle-sidebar' })],
|
|
392
|
+
['Toggle git pane', () => dispatchGlobal({ type: 'toggle-git-pane' })],
|
|
393
|
+
[
|
|
394
|
+
'Toggle diff mode',
|
|
395
|
+
() => {
|
|
396
|
+
dispatchGlobal({ type: 'enter-git-mode' })
|
|
397
|
+
dispatchGlobal({ type: 'git-mode-toggle-diff-view' })
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
],
|
|
401
|
+
[]
|
|
402
|
+
)
|
|
206
403
|
|
|
207
404
|
if (!sidebarVisible) {
|
|
208
405
|
return null
|
|
@@ -243,21 +440,7 @@ export function Sidebar({
|
|
|
243
440
|
</ContextMenuBox>
|
|
244
441
|
) : null
|
|
245
442
|
const embeddedHandle = gitEmbedded ? (
|
|
246
|
-
<box
|
|
247
|
-
minHeight={1}
|
|
248
|
-
flexShrink={0}
|
|
249
|
-
onMouseDown={(event) => {
|
|
250
|
-
const body = bodyRef.current
|
|
251
|
-
if (!body) return
|
|
252
|
-
event.preventDefault()
|
|
253
|
-
event.stopPropagation()
|
|
254
|
-
onEmbeddedGitResizeStart?.({
|
|
255
|
-
containerStart: body.y,
|
|
256
|
-
position: gitPane.position === 'top' ? 'top' : 'bottom',
|
|
257
|
-
totalSize: Math.max(1, body.height),
|
|
258
|
-
})
|
|
259
|
-
}}
|
|
260
|
-
>
|
|
443
|
+
<box minHeight={1} flexShrink={0} onMouseDown={handleEmbeddedHandleMouseDown}>
|
|
261
444
|
<text fg={t.border} selectable={false}>
|
|
262
445
|
{RESIZE_HANDLE.repeat(Math.max(1, contentWidth))}
|
|
263
446
|
</text>
|
|
@@ -272,31 +455,10 @@ export function Sidebar({
|
|
|
272
455
|
backgroundColor={sidebarBg}
|
|
273
456
|
gap={0}
|
|
274
457
|
overflow="hidden"
|
|
275
|
-
rightClickMenu={
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
'Toggle diff mode',
|
|
280
|
-
() => {
|
|
281
|
-
dispatchGlobal({ type: 'enter-git-mode' })
|
|
282
|
-
dispatchGlobal({ type: 'git-mode-toggle-diff-view' })
|
|
283
|
-
},
|
|
284
|
-
],
|
|
285
|
-
]}
|
|
286
|
-
onMouseDown={() => {
|
|
287
|
-
if (focusMode === 'terminal-input') {
|
|
288
|
-
dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
|
|
289
|
-
}
|
|
290
|
-
}}
|
|
291
|
-
onMouseDrag={(event) => {
|
|
292
|
-
if (onResizeDrag?.(event)) {
|
|
293
|
-
event.preventDefault()
|
|
294
|
-
event.stopPropagation()
|
|
295
|
-
}
|
|
296
|
-
}}
|
|
297
|
-
onMouseUp={() => {
|
|
298
|
-
onResizeDragEnd?.()
|
|
299
|
-
}}
|
|
458
|
+
rightClickMenu={sidebarMenu}
|
|
459
|
+
onMouseDown={handleSidebarMouseDown}
|
|
460
|
+
onMouseDrag={handleSidebarMouseDrag}
|
|
461
|
+
onMouseUp={handleSidebarMouseUp}
|
|
300
462
|
>
|
|
301
463
|
<box flexDirection="row" width={sidebarWidth} flexGrow={1} overflow="hidden">
|
|
302
464
|
<box width={contentWidth} flexGrow={1} flexDirection="column" overflow="hidden">
|
|
@@ -315,7 +477,7 @@ export function Sidebar({
|
|
|
315
477
|
flexBasis={0}
|
|
316
478
|
overflow="hidden"
|
|
317
479
|
>
|
|
318
|
-
<TabsBody onTabActivate={onTabActivate} />
|
|
480
|
+
<TabsBody onTabActivate={onTabActivate} contentWidth={contentWidth} />
|
|
319
481
|
</box>
|
|
320
482
|
{gitOnBottom ? (
|
|
321
483
|
<>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
2
4
|
|
|
3
5
|
import type { TabSession } from '../../../../state/types'
|
|
4
6
|
|
|
@@ -13,6 +15,8 @@ interface TabItemProps {
|
|
|
13
15
|
active: boolean
|
|
14
16
|
focused: boolean
|
|
15
17
|
inLayout?: boolean
|
|
18
|
+
/** When set, this tab's worktree can be moved — adds a "Move worktree" entry. */
|
|
19
|
+
moveWorktreeId?: string
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function getStatusColor(status: TabSession['status']): string {
|
|
@@ -106,7 +110,7 @@ function ActivityIndicator({ tab }: { tab: TabSession }) {
|
|
|
106
110
|
)
|
|
107
111
|
}
|
|
108
112
|
|
|
109
|
-
export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
113
|
+
export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }: TabItemProps) {
|
|
110
114
|
const t = useTheme()
|
|
111
115
|
const label = tab.command.split(' ')[0]
|
|
112
116
|
const isInLayout = inLayout ?? false
|
|
@@ -114,6 +118,64 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
|
114
118
|
const indicatorColor = getIndicatorColor(active, focused, isInLayout)
|
|
115
119
|
const [hovered, setHovered] = useState(false)
|
|
116
120
|
|
|
121
|
+
const rightClickMenu = useMemo<[string, () => void][]>(
|
|
122
|
+
() => [
|
|
123
|
+
[
|
|
124
|
+
'Rename',
|
|
125
|
+
() => {
|
|
126
|
+
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
127
|
+
dispatchGlobal({ type: 'open-rename-tab-modal' })
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
'Close',
|
|
132
|
+
() => {
|
|
133
|
+
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
134
|
+
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
'Move up',
|
|
139
|
+
() => {
|
|
140
|
+
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
141
|
+
dispatchGlobal({ delta: -1, type: 'reorder-active-tab' })
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
'Move down',
|
|
146
|
+
() => {
|
|
147
|
+
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
148
|
+
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
149
|
+
},
|
|
150
|
+
],
|
|
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
|
+
...(moveWorktreeId != null && moveWorktreeId !== ''
|
|
154
|
+
? [
|
|
155
|
+
[
|
|
156
|
+
'Move worktree',
|
|
157
|
+
() =>
|
|
158
|
+
dispatchGlobal({
|
|
159
|
+
sourceWorktreeId: moveWorktreeId,
|
|
160
|
+
type: 'open-worktree-move-modal',
|
|
161
|
+
}),
|
|
162
|
+
] as [string, () => void],
|
|
163
|
+
]
|
|
164
|
+
: []),
|
|
165
|
+
],
|
|
166
|
+
[moveWorktreeId, tab.id]
|
|
167
|
+
)
|
|
168
|
+
const handleMouseOver = useCallback(() => setHovered(true), [])
|
|
169
|
+
const handleMouseOut = useCallback(() => setHovered(false), [])
|
|
170
|
+
const handleCloseMouseDown = useCallback(
|
|
171
|
+
(event: OtuiMouseEvent) => {
|
|
172
|
+
event.stopPropagation()
|
|
173
|
+
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
174
|
+
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
175
|
+
},
|
|
176
|
+
[tab.id]
|
|
177
|
+
)
|
|
178
|
+
|
|
117
179
|
return (
|
|
118
180
|
<ContextMenuBox
|
|
119
181
|
id={id}
|
|
@@ -123,38 +185,9 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
|
123
185
|
paddingBottom={0}
|
|
124
186
|
flexDirection="column"
|
|
125
187
|
gap={0}
|
|
126
|
-
rightClickMenu={
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
() => {
|
|
130
|
-
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
131
|
-
dispatchGlobal({ type: 'open-rename-tab-modal' })
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
[
|
|
135
|
-
'Close',
|
|
136
|
-
() => {
|
|
137
|
-
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
138
|
-
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
[
|
|
142
|
-
'Move up',
|
|
143
|
-
() => {
|
|
144
|
-
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
145
|
-
dispatchGlobal({ delta: -1, type: 'reorder-active-tab' })
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
[
|
|
149
|
-
'Move down',
|
|
150
|
-
() => {
|
|
151
|
-
dispatchGlobal({ tabId: tab.id, type: 'set-active-tab' })
|
|
152
|
-
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
]}
|
|
156
|
-
onMouseOver={() => setHovered(true)}
|
|
157
|
-
onMouseOut={() => setHovered(false)}
|
|
188
|
+
rightClickMenu={rightClickMenu}
|
|
189
|
+
onMouseOver={handleMouseOver}
|
|
190
|
+
onMouseOut={handleMouseOut}
|
|
158
191
|
>
|
|
159
192
|
<box flexDirection="row" alignItems="center">
|
|
160
193
|
<text fg={indicatorColor} selectable={false}>
|
|
@@ -166,13 +199,7 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
|
166
199
|
</text>
|
|
167
200
|
</box>
|
|
168
201
|
{hovered ? (
|
|
169
|
-
<box
|
|
170
|
-
onMouseDown={(event) => {
|
|
171
|
-
event.stopPropagation()
|
|
172
|
-
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
173
|
-
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
174
|
-
}}
|
|
175
|
-
>
|
|
202
|
+
<box onMouseDown={handleCloseMouseDown}>
|
|
176
203
|
<text fg={t.textMuted} selectable={false}>
|
|
177
204
|
×
|
|
178
205
|
</text>
|
|
@@ -36,7 +36,7 @@ export function useSidebarAutoScroll({
|
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if (!previousVisibilityRef.current && activeTabId) {
|
|
39
|
+
if (!previousVisibilityRef.current && activeTabId != null && activeTabId !== '') {
|
|
40
40
|
scrollbox.scrollChildIntoView(`sidebar-tab-${activeTabId}`)
|
|
41
41
|
previousVisibilityRef.current = true
|
|
42
42
|
previousActiveIndexRef.current = activeIndex
|
|
@@ -53,7 +53,7 @@ export function useSidebarAutoScroll({
|
|
|
53
53
|
scrollbox.scrollTo({ x: 0, y: 0 })
|
|
54
54
|
} else if (scrollTarget === 'bottom') {
|
|
55
55
|
scrollbox.scrollTo({ x: 0, y: scrollbox.scrollHeight })
|
|
56
|
-
} else if (scrollTarget === 'active-item' && activeTabId) {
|
|
56
|
+
} else if (scrollTarget === 'active-item' && activeTabId != null && activeTabId !== '') {
|
|
57
57
|
scrollbox.scrollChildIntoView(`sidebar-tab-${activeTabId}`)
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -8,7 +8,7 @@ export function useSidebarBranch(projectPath: string | undefined): string | null
|
|
|
8
8
|
const [branch, setBranch] = useState<string | null>(null)
|
|
9
9
|
|
|
10
10
|
useEffect(() => {
|
|
11
|
-
if (!projectPath) {
|
|
11
|
+
if (!(projectPath != null && projectPath !== '')) {
|
|
12
12
|
setBranch(null)
|
|
13
13
|
return
|
|
14
14
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
+
import { useCallback, useMemo } from 'react'
|
|
4
|
+
|
|
3
5
|
import type { MeasuredPaneRect } from '../../../app-runtime/use-pane-size-report'
|
|
4
6
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
5
7
|
import type { FocusMode, TabSession } from '../../../state/types'
|
|
@@ -68,15 +70,34 @@ export function SplitLayout({
|
|
|
68
70
|
tabs,
|
|
69
71
|
}: SplitLayoutProps) {
|
|
70
72
|
const t = useTheme()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const isActive = node.tabId === activeTabId
|
|
74
|
-
const paneOrigin: TerminalContentOrigin = {
|
|
73
|
+
const paneOrigin = useMemo<TerminalContentOrigin>(
|
|
74
|
+
() => ({
|
|
75
75
|
cols: Math.max(1, bounds.cols - PANE_CHROME * 2),
|
|
76
76
|
rows: Math.max(1, bounds.rows - PANE_CHROME * 2),
|
|
77
77
|
x: contentOrigin.x + bounds.x + PANE_CHROME,
|
|
78
78
|
y: contentOrigin.y + bounds.y + PANE_CHROME,
|
|
79
|
-
}
|
|
79
|
+
}),
|
|
80
|
+
[bounds, contentOrigin]
|
|
81
|
+
)
|
|
82
|
+
const handleSeparatorMouseDown = useCallback(
|
|
83
|
+
(e: OtuiMouseEvent) => {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
if (node.type !== 'split' || !onSeparatorDragStart) return
|
|
86
|
+
const leafId = getFirstLeafId(node.first)
|
|
87
|
+
if (!(leafId != null && leafId !== '')) return
|
|
88
|
+
onSeparatorDragStart({
|
|
89
|
+
direction: node.direction,
|
|
90
|
+
screenStart:
|
|
91
|
+
node.direction === 'vertical' ? contentOrigin.x + bounds.x : contentOrigin.y + bounds.y,
|
|
92
|
+
tabId: leafId,
|
|
93
|
+
totalSize: node.direction === 'vertical' ? bounds.cols : bounds.rows,
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
[bounds, contentOrigin, node, onSeparatorDragStart]
|
|
97
|
+
)
|
|
98
|
+
if (node.type === 'leaf') {
|
|
99
|
+
const tab = tabs.find((t) => t.id === node.tabId)
|
|
100
|
+
const isActive = node.tabId === activeTabId
|
|
80
101
|
logInputDebug('split.paneOrigin', {
|
|
81
102
|
boundsCols: bounds.cols,
|
|
82
103
|
boundsRows: bounds.rows,
|
|
@@ -157,21 +178,7 @@ export function SplitLayout({
|
|
|
157
178
|
minWidth={node.direction === 'vertical' ? 1 : undefined}
|
|
158
179
|
minHeight={node.direction === 'horizontal' ? 1 : undefined}
|
|
159
180
|
backgroundColor={t.border}
|
|
160
|
-
onMouseDown={
|
|
161
|
-
e.preventDefault()
|
|
162
|
-
if (!onSeparatorDragStart) return
|
|
163
|
-
const leafId = getFirstLeafId(node.first)
|
|
164
|
-
if (!leafId) return
|
|
165
|
-
onSeparatorDragStart({
|
|
166
|
-
direction: node.direction,
|
|
167
|
-
screenStart:
|
|
168
|
-
node.direction === 'vertical'
|
|
169
|
-
? contentOrigin.x + bounds.x
|
|
170
|
-
: contentOrigin.y + bounds.y,
|
|
171
|
-
tabId: leafId,
|
|
172
|
-
totalSize: node.direction === 'vertical' ? bounds.cols : bounds.rows,
|
|
173
|
-
})
|
|
174
|
-
}}
|
|
181
|
+
onMouseDown={handleSeparatorMouseDown}
|
|
175
182
|
/>
|
|
176
183
|
<box flexGrow={secondGrow} flexDirection="column" overflow="hidden">
|
|
177
184
|
<SplitLayout
|