@brimveyn/aimux 1.12.2 → 1.13.0
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 +18 -2
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/pty-write.ts +52 -0
- 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 +598 -102
- package/src/app-runtime/snippet-actions.ts +25 -11
- 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 +143 -8
- package/src/app.tsx +25 -9
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +3 -3
- 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 +1 -1
- 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 +13 -1
- package/src/input/raw-input-handler.ts +34 -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 +18 -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 +8 -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 +3 -3
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- 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 +385 -24
- 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/snippet-catalog.ts +65 -4
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +108 -16
- package/src/state/validation.ts +47 -3
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
- 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 +6 -4
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
- package/src/ui/components/git/git-panel.tsx +17 -10
- package/src/ui/components/git/git-view.tsx +79 -19
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
- 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 +3 -3
- package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
- package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
- 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 +1 -1
- package/src/ui/components/layout/terminal-pane.tsx +43 -40
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +4 -2
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
- package/src/ui/components/modals/shared/form.tsx +4 -5
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +28 -5
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
- 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 +3 -3
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +65 -14
- 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
|
+
|
|
6
7
|
import { memo, useMemo, useRef } from 'react'
|
|
7
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'
|
|
@@ -33,6 +44,28 @@ const GUTTER_END = '╰'
|
|
|
33
44
|
const GUTTER_PAD = '│'
|
|
34
45
|
const RESIZE_HANDLE = '─'
|
|
35
46
|
|
|
47
|
+
// Left accent strip + trailing space for a worktree group header.
|
|
48
|
+
const WORKTREE_STRIP = '▍ '
|
|
49
|
+
|
|
50
|
+
// Compact "↑ahead ↓behind" label for a worktree group header; empty when the
|
|
51
|
+
// branch is level with its base (or divergence isn't known yet).
|
|
52
|
+
function formatDivergence(divergence: BranchDivergence | undefined): string {
|
|
53
|
+
if (divergence == null) return ''
|
|
54
|
+
const parts: string[] = []
|
|
55
|
+
if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
|
|
56
|
+
if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
|
|
57
|
+
return parts.join(' ')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fit a worktree label into `max` columns, ellipsizing when it would overflow.
|
|
61
|
+
// Branch names are ASCII, so character count tracks rendered column width.
|
|
62
|
+
function truncateLabel(label: string, max: number): string {
|
|
63
|
+
if (max <= 0) return ''
|
|
64
|
+
if (label.length <= max) return label
|
|
65
|
+
if (max === 1) return '…'
|
|
66
|
+
return `${label.slice(0, max - 1)}…`
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
function getRowBackground({
|
|
37
70
|
alternate,
|
|
38
71
|
isActive,
|
|
@@ -51,10 +84,12 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
51
84
|
const t = useTheme()
|
|
52
85
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
53
86
|
const sessions = useAppStore((s) => s.sessions)
|
|
54
|
-
const currentSession =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
const currentSession =
|
|
88
|
+
currentSessionId != null && currentSessionId !== ''
|
|
89
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
90
|
+
: undefined
|
|
91
|
+
const activeWorktree = getActiveWorktree(currentSession)
|
|
92
|
+
const projectPath = getSessionProjectPath(currentSession)
|
|
58
93
|
const branch = useSidebarBranch(projectPath)
|
|
59
94
|
|
|
60
95
|
return (
|
|
@@ -65,14 +100,20 @@ const SidebarTop = memo(function SidebarTop({ contentWidth }: { contentWidth: nu
|
|
|
65
100
|
<text fg={t.text} selectable={false}>
|
|
66
101
|
{currentSession ? currentSession.name : 'No workspace selected'}
|
|
67
102
|
</text>
|
|
68
|
-
{branch ? (
|
|
103
|
+
{(branch != null && branch !== '') || activeWorktree ? (
|
|
69
104
|
<box flexDirection="row">
|
|
70
105
|
<text fg={t.text} selectable={false}>
|
|
71
106
|
{'\u{e702}'}{' '}
|
|
72
107
|
</text>
|
|
73
108
|
<text fg={t.text} selectable={false}>
|
|
74
|
-
{branch}
|
|
109
|
+
{branch ?? activeWorktree?.branch ?? activeWorktree?.name}
|
|
75
110
|
</text>
|
|
111
|
+
{activeWorktree?.source === 'aimux-temp' ? (
|
|
112
|
+
<text fg={t.textMuted} selectable={false}>
|
|
113
|
+
{' '}
|
|
114
|
+
tmp
|
|
115
|
+
</text>
|
|
116
|
+
) : null}
|
|
76
117
|
</box>
|
|
77
118
|
) : null}
|
|
78
119
|
<box
|
|
@@ -114,25 +155,54 @@ function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGrou
|
|
|
114
155
|
|
|
115
156
|
interface TabsBodyProps {
|
|
116
157
|
onTabActivate?: (tabId: string) => void
|
|
158
|
+
contentWidth: number
|
|
117
159
|
}
|
|
118
160
|
|
|
119
|
-
const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
161
|
+
const TabsBody = memo(function TabsBody({ contentWidth, onTabActivate }: TabsBodyProps) {
|
|
120
162
|
const t = useTheme()
|
|
121
163
|
const tabs = useAppStore((s) => s.tabs)
|
|
122
164
|
const activeTabId = useAppStore((s) => s.activeTabId)
|
|
123
165
|
const focusMode = useAppStore((s) => s.focusMode)
|
|
124
166
|
const layoutTrees = useAppStore((s) => s.layoutTrees)
|
|
125
167
|
const sidebarVisible = useAppStore((s) => s.sidebar.visible)
|
|
168
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
169
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
170
|
+
const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
|
|
171
|
+
const currentSession =
|
|
172
|
+
currentSessionId != null && currentSessionId !== ''
|
|
173
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
174
|
+
: undefined
|
|
175
|
+
const worktrees = useMemo(() => currentSession?.worktrees ?? [], [currentSession?.worktrees])
|
|
176
|
+
const worktreeById = useMemo(
|
|
177
|
+
() => new Map(worktrees.map((worktree) => [worktree.id, worktree])),
|
|
178
|
+
[worktrees]
|
|
179
|
+
)
|
|
180
|
+
const groupedTabs = useMemo(() => {
|
|
181
|
+
return orderTabsByWorktree(tabs, currentSession)
|
|
182
|
+
}, [currentSession, tabs])
|
|
183
|
+
const showWorktreeSeparators = useMemo(() => {
|
|
184
|
+
const ids = new Set<string>()
|
|
185
|
+
for (const tab of groupedTabs) {
|
|
186
|
+
ids.add(getRenderedTabWorktreeId(tab, worktrees))
|
|
187
|
+
}
|
|
188
|
+
const activeWorktreeId =
|
|
189
|
+
currentSession?.activeWorktreeId ?? getActiveWorktree(currentSession)?.id
|
|
190
|
+
if (activeWorktreeId != null && activeWorktreeId !== '') ids.add(activeWorktreeId)
|
|
191
|
+
return ids.size >= 2
|
|
192
|
+
}, [currentSession, groupedTabs, worktrees])
|
|
126
193
|
|
|
127
194
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
128
|
-
const activeIndex =
|
|
129
|
-
const tabGroupInfo = useMemo(
|
|
195
|
+
const activeIndex = groupedTabs.findIndex((tab) => tab.id === activeTabId)
|
|
196
|
+
const tabGroupInfo = useMemo(
|
|
197
|
+
() => buildTabGroupInfo(layoutTrees, groupedTabs),
|
|
198
|
+
[layoutTrees, groupedTabs]
|
|
199
|
+
)
|
|
130
200
|
|
|
131
201
|
useSidebarAutoScroll({
|
|
132
202
|
activeIndex,
|
|
133
203
|
activeTabId,
|
|
134
204
|
scrollRef,
|
|
135
|
-
tabCount:
|
|
205
|
+
tabCount: groupedTabs.length,
|
|
136
206
|
visible: sidebarVisible,
|
|
137
207
|
})
|
|
138
208
|
|
|
@@ -152,35 +222,90 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
152
222
|
</text>
|
|
153
223
|
</box>
|
|
154
224
|
) : (
|
|
155
|
-
|
|
225
|
+
groupedTabs.map((tab, index) => {
|
|
156
226
|
const isActive = tab.id === activeTabId
|
|
157
227
|
const alternate = index % 2 === 1
|
|
158
228
|
const info = tabGroupInfo.get(tab.id)
|
|
159
|
-
const inLayout = !!info?.inLayout
|
|
229
|
+
const inLayout = !!(info?.inLayout === true)
|
|
160
230
|
const inGroup = info ? index >= info.groupStart && index <= info.groupEnd : false
|
|
161
231
|
const isGroupStart = info ? index === info.groupStart : false
|
|
162
232
|
const isGroupEnd = info ? index === info.groupEnd : false
|
|
163
233
|
const isGroupMiddle = inGroup && !isGroupStart && !isGroupEnd
|
|
234
|
+
const tabOwnWorktree =
|
|
235
|
+
tab.worktreeId != null && tab.worktreeId !== ''
|
|
236
|
+
? worktreeById.get(tab.worktreeId)
|
|
237
|
+
: undefined
|
|
238
|
+
let tabWorktree = tabOwnWorktree
|
|
239
|
+
if (!tabWorktree && worktrees.length > 1) {
|
|
240
|
+
tabWorktree = worktrees[0]
|
|
241
|
+
}
|
|
242
|
+
// A tab's worktree can be moved when it has a branch (squash needs one,
|
|
243
|
+
// so not the primary) and there's at least one other worktree to land in.
|
|
244
|
+
const moveWorktreeId =
|
|
245
|
+
tabOwnWorktree?.branch != null && tabOwnWorktree.branch !== '' && worktrees.length > 1
|
|
246
|
+
? tabOwnWorktree.id
|
|
247
|
+
: undefined
|
|
248
|
+
const prevTab = groupedTabs[index - 1]
|
|
249
|
+
const startsWorktreeGroup =
|
|
250
|
+
!prevTab ||
|
|
251
|
+
getRenderedTabWorktreeId(prevTab, worktrees) !==
|
|
252
|
+
getRenderedTabWorktreeId(tab, worktrees)
|
|
253
|
+
const worktreeColor = tabWorktree
|
|
254
|
+
? (tabWorktree.color ?? getWorktreeColor(tabWorktree.id))
|
|
255
|
+
: t.textMuted
|
|
256
|
+
const worktreeLabel = tabWorktree?.branch ?? tabWorktree?.name ?? 'main'
|
|
257
|
+
const aheadBehind = formatDivergence(
|
|
258
|
+
tabWorktree != null ? worktreeDivergence[tabWorktree.id] : undefined
|
|
259
|
+
)
|
|
260
|
+
const aheadBehindText = aheadBehind === '' ? '' : ` ${aheadBehind}`
|
|
261
|
+
// Reserve room for the strip, the ahead/behind chip and a 1-col gap so
|
|
262
|
+
// the header never wraps; the divider fills whatever remains (min 0).
|
|
263
|
+
const labelBudget = Math.max(
|
|
264
|
+
0,
|
|
265
|
+
contentWidth - WORKTREE_STRIP.length - aheadBehindText.length - 1
|
|
266
|
+
)
|
|
267
|
+
const headerLabel = truncateLabel(worktreeLabel, labelBudget)
|
|
164
268
|
|
|
165
269
|
return (
|
|
166
|
-
<box
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
270
|
+
<box key={tab.id} flexDirection="column">
|
|
271
|
+
{showWorktreeSeparators && startsWorktreeGroup ? (
|
|
272
|
+
<box flexDirection="row" overflow="hidden" paddingTop={index === 0 ? 0 : 1}>
|
|
273
|
+
<text selectable={false} wrapMode="none" flexShrink={0}>
|
|
274
|
+
<span fg={worktreeColor}>
|
|
275
|
+
{WORKTREE_STRIP}
|
|
276
|
+
{headerLabel}
|
|
277
|
+
</span>
|
|
278
|
+
{aheadBehindText !== '' ? (
|
|
279
|
+
<span fg={t.textMuted}>{aheadBehindText}</span>
|
|
280
|
+
) : null}
|
|
281
|
+
</text>
|
|
282
|
+
<box flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
|
|
283
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
284
|
+
{' '}
|
|
285
|
+
{'─'.repeat(contentWidth)}
|
|
286
|
+
</text>
|
|
287
|
+
</box>
|
|
288
|
+
</box>
|
|
289
|
+
) : null}
|
|
290
|
+
<box
|
|
291
|
+
backgroundColor={getRowBackground({ alternate, isActive, t })}
|
|
292
|
+
flexDirection="row"
|
|
293
|
+
onMouseDown={(event) => {
|
|
294
|
+
event.stopPropagation()
|
|
295
|
+
onTabActivate?.(tab.id)
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
298
|
+
{inGroup ? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd) : null}
|
|
299
|
+
<box flexGrow={1}>
|
|
300
|
+
<TabItem
|
|
301
|
+
id={`sidebar-tab-${tab.id}`}
|
|
302
|
+
tab={tab}
|
|
303
|
+
active={isActive}
|
|
304
|
+
focused={focusMode === 'navigation'}
|
|
305
|
+
inLayout={inLayout}
|
|
306
|
+
moveWorktreeId={moveWorktreeId}
|
|
307
|
+
/>
|
|
308
|
+
</box>
|
|
184
309
|
</box>
|
|
185
310
|
</box>
|
|
186
311
|
)
|
|
@@ -203,6 +328,7 @@ export function Sidebar({
|
|
|
203
328
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
204
329
|
const focusMode = useAppStore((s) => s.focusMode)
|
|
205
330
|
const bodyRef = useRef<BoxRenderable | null>(null)
|
|
331
|
+
useWorktreeDivergencePolling(sidebarVisible)
|
|
206
332
|
|
|
207
333
|
if (!sidebarVisible) {
|
|
208
334
|
return null
|
|
@@ -289,7 +415,7 @@ export function Sidebar({
|
|
|
289
415
|
}
|
|
290
416
|
}}
|
|
291
417
|
onMouseDrag={(event) => {
|
|
292
|
-
if (onResizeDrag?.(event)) {
|
|
418
|
+
if (onResizeDrag?.(event) === true) {
|
|
293
419
|
event.preventDefault()
|
|
294
420
|
event.stopPropagation()
|
|
295
421
|
}
|
|
@@ -315,7 +441,7 @@ export function Sidebar({
|
|
|
315
441
|
flexBasis={0}
|
|
316
442
|
overflow="hidden"
|
|
317
443
|
>
|
|
318
|
-
<TabsBody onTabActivate={onTabActivate} />
|
|
444
|
+
<TabsBody onTabActivate={onTabActivate} contentWidth={contentWidth} />
|
|
319
445
|
</box>
|
|
320
446
|
{gitOnBottom ? (
|
|
321
447
|
<>
|
|
@@ -13,6 +13,8 @@ interface TabItemProps {
|
|
|
13
13
|
active: boolean
|
|
14
14
|
focused: boolean
|
|
15
15
|
inLayout?: boolean
|
|
16
|
+
/** When set, this tab's worktree can be moved — adds a "Move worktree" entry. */
|
|
17
|
+
moveWorktreeId?: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function getStatusColor(status: TabSession['status']): string {
|
|
@@ -106,7 +108,7 @@ function ActivityIndicator({ tab }: { tab: TabSession }) {
|
|
|
106
108
|
)
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
111
|
+
export function TabItem({ active, focused, id, inLayout, moveWorktreeId, tab }: TabItemProps) {
|
|
110
112
|
const t = useTheme()
|
|
111
113
|
const label = tab.command.split(' ')[0]
|
|
112
114
|
const isInLayout = inLayout ?? false
|
|
@@ -152,6 +154,20 @@ export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
|
152
154
|
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
153
155
|
},
|
|
154
156
|
],
|
|
157
|
+
// Move this tab's worktree into another one. Opened from here it overlays
|
|
158
|
+
// the normal view (no git mode) since the open action doesn't touch focus.
|
|
159
|
+
...(moveWorktreeId != null && moveWorktreeId !== ''
|
|
160
|
+
? [
|
|
161
|
+
[
|
|
162
|
+
'Move worktree',
|
|
163
|
+
() =>
|
|
164
|
+
dispatchGlobal({
|
|
165
|
+
sourceWorktreeId: moveWorktreeId,
|
|
166
|
+
type: 'open-worktree-move-modal',
|
|
167
|
+
}),
|
|
168
|
+
] as [string, () => void],
|
|
169
|
+
]
|
|
170
|
+
: []),
|
|
155
171
|
]}
|
|
156
172
|
onMouseOver={() => setHovered(true)}
|
|
157
173
|
onMouseOut={() => setHovered(false)}
|
|
@@ -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
|
}
|
|
@@ -161,7 +161,7 @@ export function SplitLayout({
|
|
|
161
161
|
e.preventDefault()
|
|
162
162
|
if (!onSeparatorDragStart) return
|
|
163
163
|
const leafId = getFirstLeafId(node.first)
|
|
164
|
-
if (!leafId) return
|
|
164
|
+
if (!(leafId != null && leafId !== '')) return
|
|
165
165
|
onSeparatorDragStart({
|
|
166
166
|
direction: node.direction,
|
|
167
167
|
screenStart:
|
|
@@ -3,7 +3,7 @@ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
3
3
|
import { memo, type ReactNode } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
|
-
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
6
|
+
import type { FocusMode, TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
7
7
|
|
|
8
8
|
import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
|
|
9
9
|
import { logInputDebug } from '../../../debug/input-log'
|
|
@@ -15,7 +15,7 @@ import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
|
15
15
|
interface TerminalPaneProps {
|
|
16
16
|
tab?: TabSession
|
|
17
17
|
tabId?: string
|
|
18
|
-
focusMode:
|
|
18
|
+
focusMode: FocusMode
|
|
19
19
|
isActive?: boolean
|
|
20
20
|
contentOrigin: TerminalContentOrigin
|
|
21
21
|
mouseForwardingEnabled: boolean
|
|
@@ -61,15 +61,15 @@ function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMo
|
|
|
61
61
|
function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
62
62
|
let node: ReactNode = span.text
|
|
63
63
|
|
|
64
|
-
if (span.underline) {
|
|
64
|
+
if (span.underline === true) {
|
|
65
65
|
node = <u>{node}</u>
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
if (span.italic) {
|
|
68
|
+
if (span.italic === true) {
|
|
69
69
|
node = <em>{node}</em>
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
if (span.bold) {
|
|
72
|
+
if (span.bold === true) {
|
|
73
73
|
node = <strong>{node}</strong>
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -132,35 +132,36 @@ export function TerminalPane({
|
|
|
132
132
|
const paneIsActive = isActive ?? true
|
|
133
133
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
134
134
|
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
135
|
-
const rightClickMenu: ContextMenuItem[] | undefined =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
135
|
+
const rightClickMenu: ContextMenuItem[] | undefined =
|
|
136
|
+
tabId != null && tabId !== ''
|
|
137
|
+
? [
|
|
138
|
+
[
|
|
139
|
+
'Split vertically',
|
|
140
|
+
() =>
|
|
141
|
+
runSideEffectGlobal({
|
|
142
|
+
direction: 'vertical',
|
|
143
|
+
sourceTabId: tabId,
|
|
144
|
+
type: 'split-pane',
|
|
145
|
+
}),
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
'Split horizontally',
|
|
149
|
+
() =>
|
|
150
|
+
runSideEffectGlobal({
|
|
151
|
+
direction: 'horizontal',
|
|
152
|
+
sourceTabId: tabId,
|
|
153
|
+
type: 'split-pane',
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
156
|
+
[
|
|
157
|
+
'Close pane',
|
|
158
|
+
() => {
|
|
159
|
+
dispatchGlobal({ tabId, type: 'close-pane' })
|
|
160
|
+
runSideEffectGlobal({ tabId, type: 'close-tab' })
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
]
|
|
164
|
+
: undefined
|
|
164
165
|
const isOnPaneBorder = (event: OtuiMouseEvent) =>
|
|
165
166
|
event.x === contentOrigin.x - 1 ||
|
|
166
167
|
event.x === contentOrigin.x + contentOrigin.cols ||
|
|
@@ -206,22 +207,22 @@ export function TerminalPane({
|
|
|
206
207
|
})
|
|
207
208
|
}
|
|
208
209
|
if (event.type === 'drag') {
|
|
209
|
-
if (onTerminalDrag?.(event, contentOrigin, tabId)) {
|
|
210
|
+
if (onTerminalDrag?.(event, contentOrigin, tabId) === true) {
|
|
210
211
|
event.preventDefault()
|
|
211
212
|
return
|
|
212
213
|
}
|
|
213
|
-
if (onSeparatorDrag?.(event)) {
|
|
214
|
+
if (onSeparatorDrag?.(event) === true) {
|
|
214
215
|
event.preventDefault()
|
|
215
216
|
return
|
|
216
217
|
}
|
|
217
218
|
}
|
|
218
219
|
if (event.type === 'up') {
|
|
219
|
-
if (onTerminalMouseUp?.(event)) {
|
|
220
|
+
if (onTerminalMouseUp?.(event) === true) {
|
|
220
221
|
event.preventDefault()
|
|
221
222
|
}
|
|
222
223
|
onSeparatorDragEnd?.()
|
|
223
224
|
}
|
|
224
|
-
if (tabId && onPaneActivate && event.type === 'down') {
|
|
225
|
+
if (tabId != null && tabId !== '' && onPaneActivate && event.type === 'down') {
|
|
225
226
|
onPaneActivate(tabId)
|
|
226
227
|
}
|
|
227
228
|
if (!canForwardMouse) {
|
|
@@ -308,7 +309,7 @@ export function TerminalPane({
|
|
|
308
309
|
</box>
|
|
309
310
|
)}
|
|
310
311
|
</ContextMenuBox>
|
|
311
|
-
{tab?.status === 'disconnected' || tab?.errorMessage ? (
|
|
312
|
+
{tab?.status === 'disconnected' || (tab?.errorMessage != null && tab.errorMessage !== '') ? (
|
|
312
313
|
// Absolutely positioned so it overlays the bordered box instead of
|
|
313
314
|
// consuming a flex row. If it took a row, the rendered terminal area
|
|
314
315
|
// would be one line shorter than the size sent to the PTY/xterm,
|
|
@@ -317,7 +318,9 @@ export function TerminalPane({
|
|
|
317
318
|
{tab?.status === 'disconnected' ? (
|
|
318
319
|
<text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this workspace.</text>
|
|
319
320
|
) : null}
|
|
320
|
-
{tab?.errorMessage
|
|
321
|
+
{tab?.errorMessage != null && tab?.errorMessage !== '' ? (
|
|
322
|
+
<text fg={t.error}>{tab.errorMessage}</text>
|
|
323
|
+
) : null}
|
|
321
324
|
</box>
|
|
322
325
|
) : null}
|
|
323
326
|
</box>
|
|
@@ -93,7 +93,7 @@ interface ToolSectionProps {
|
|
|
93
93
|
|
|
94
94
|
function ToolSection({ snap, tool }: ToolSectionProps) {
|
|
95
95
|
const t = useTheme()
|
|
96
|
-
const isHardError = Boolean(snap.error) && !snap.stale
|
|
96
|
+
const isHardError = Boolean(snap.error) && !(snap.stale === true)
|
|
97
97
|
const relative = formatRelative(snap.lastUpdated)
|
|
98
98
|
|
|
99
99
|
let body: ReactNode
|
|
@@ -119,7 +119,7 @@ function ToolSection({ snap, tool }: ToolSectionProps) {
|
|
|
119
119
|
<text fg={t.text} selectable={false}>
|
|
120
120
|
{TOOL_TITLE[tool]}
|
|
121
121
|
</text>
|
|
122
|
-
{snap.planTier ? (
|
|
122
|
+
{snap.planTier != null && snap.planTier !== '' ? (
|
|
123
123
|
<text fg={t.textMuted} selectable={false}>
|
|
124
124
|
{snap.planTier}
|
|
125
125
|
</text>
|
|
@@ -145,7 +145,10 @@ function WindowRow({ window }: { window: UsageWindow }) {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
const pctText = percent === null ? '—' : `${Math.round(percent)}% used`
|
|
148
|
-
const resetText =
|
|
148
|
+
const resetText =
|
|
149
|
+
window.timeRemaining != null && window.timeRemaining !== ''
|
|
150
|
+
? `Resets in ${window.timeRemaining}`
|
|
151
|
+
: null
|
|
149
152
|
|
|
150
153
|
return (
|
|
151
154
|
<box flexDirection="column" paddingTop={1}>
|
|
@@ -164,7 +167,7 @@ function WindowRow({ window }: { window: UsageWindow }) {
|
|
|
164
167
|
<text fg={t.textMuted} selectable={false}>
|
|
165
168
|
{pctText}
|
|
166
169
|
</text>
|
|
167
|
-
{resetText ? (
|
|
170
|
+
{resetText != null && resetText !== '' ? (
|
|
168
171
|
<text fg={t.textMuted} selectable={false}>
|
|
169
172
|
{resetText}
|
|
170
173
|
</text>
|
|
@@ -181,7 +184,7 @@ function PaceLine({ pace }: { pace: NonNullable<UsageWindow['pace']> }) {
|
|
|
181
184
|
if (paceStageIsBehind(pace.stage)) color = t.warning
|
|
182
185
|
else if (paceStageIsAhead(pace.stage)) color = t.success
|
|
183
186
|
|
|
184
|
-
const suffix = pace.rightText ? ` · ${pace.rightText}` : ''
|
|
187
|
+
const suffix = pace.rightText != null && pace.rightText !== '' ? ` · ${pace.rightText}` : ''
|
|
185
188
|
return (
|
|
186
189
|
<text fg={color} selectable={false}>
|
|
187
190
|
{`Pace: ${pace.label}${suffix}`}
|
|
@@ -19,7 +19,7 @@ interface HelpModalProps {
|
|
|
19
19
|
function matchesFilter(needle: string, ...fields: (string | undefined)[]): boolean {
|
|
20
20
|
if (!needle) return true
|
|
21
21
|
const lower = needle.toLowerCase()
|
|
22
|
-
return fields.some((f) => f?.toLowerCase().includes(lower))
|
|
22
|
+
return fields.some((f) => f?.toLowerCase().includes(lower) === true)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModalProps) {
|
|
@@ -75,7 +75,9 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
75
75
|
items={items}
|
|
76
76
|
selectedIndex={selectedIndex}
|
|
77
77
|
emptyState={
|
|
78
|
-
<text fg={t.textMuted}>
|
|
78
|
+
<text fg={t.textMuted}>
|
|
79
|
+
{filter != null && filter !== '' ? 'No matching bindings.' : 'No bindings registered.'}
|
|
80
|
+
</text>
|
|
79
81
|
}
|
|
80
82
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
81
83
|
/>
|
|
@@ -90,7 +90,7 @@ export function GitCommitModal({
|
|
|
90
90
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
91
91
|
const bgKind = useAppStore((s) => {
|
|
92
92
|
const id = s.currentSessionId
|
|
93
|
-
return id ? s.autoCommit.bySession[id]?.kind : undefined
|
|
93
|
+
return id != null && id !== '' ? s.autoCommit.bySession[id]?.kind : undefined
|
|
94
94
|
})
|
|
95
95
|
const isBgGenerating = bgKind === 'generating'
|
|
96
96
|
const showBgSpinner = isBgGenerating && !isConfirm && !isGenerating
|
|
@@ -101,7 +101,7 @@ export function GitCommitModal({
|
|
|
101
101
|
|
|
102
102
|
const onAutoCommitClick = (): void => {
|
|
103
103
|
const hasTitle = title.trim().length > 0
|
|
104
|
-
if (hasTitle || !currentSessionId) {
|
|
104
|
+
if (hasTitle || !(currentSessionId != null && currentSessionId !== '')) {
|
|
105
105
|
dispatchGlobal({ type: 'git-commit-enter-confirm' })
|
|
106
106
|
return
|
|
107
107
|
}
|
|
@@ -62,7 +62,11 @@ export function CreateSessionModal({
|
|
|
62
62
|
label="Search projects"
|
|
63
63
|
placeholder="Type a project name..."
|
|
64
64
|
value={directoryQuery}
|
|
65
|
-
displayValue={
|
|
65
|
+
displayValue={
|
|
66
|
+
pendingProjectPath != null && pendingProjectPath !== ''
|
|
67
|
+
? abbreviatePath(pendingProjectPath)
|
|
68
|
+
: directoryQuery
|
|
69
|
+
}
|
|
66
70
|
items={items}
|
|
67
71
|
selectedIndex={selectedIndex}
|
|
68
72
|
maxVisibleRows={VISIBLE_ROWS}
|
|
@@ -47,7 +47,7 @@ export function SessionPickerModal({
|
|
|
47
47
|
const ordered = orderSessionsForDisplay(sessions)
|
|
48
48
|
const baselineOrder = ordered.map((s) => s.id)
|
|
49
49
|
const filtered = filterSessions(ordered, filter)
|
|
50
|
-
const hasFilter = !!filter
|
|
50
|
+
const hasFilter = !!(filter != null && filter !== '')
|
|
51
51
|
|
|
52
52
|
const sessionItems: PickerItem[] = filtered.map((session, index) => {
|
|
53
53
|
const active = index === selectedIndex
|
|
@@ -56,9 +56,10 @@ export function SessionPickerModal({
|
|
|
56
56
|
key: session.id,
|
|
57
57
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
58
58
|
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
|
|
59
|
-
subtitle:
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
subtitle:
|
|
60
|
+
session.projectPath != null && session.projectPath !== '' ? (
|
|
61
|
+
<text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
|
|
62
|
+
) : undefined,
|
|
62
63
|
title: (
|
|
63
64
|
<text fg={active ? t.text : t.textMuted}>
|
|
64
65
|
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|