@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,6 +1,6 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import { memo, type ReactNode, useEffect, useMemo, useRef } from 'react'
|
|
3
|
+
import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
GitFileEntry,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type GitTreeFileRow,
|
|
19
19
|
type GitTreeFolderRow,
|
|
20
20
|
} from '../../../state/git-tree'
|
|
21
|
-
import { getCurrentTheme,
|
|
21
|
+
import { getCurrentTheme, useTheme, useTransparent } from '../../theme'
|
|
22
22
|
|
|
23
23
|
interface GitPanelProps {
|
|
24
24
|
collapsedFolders?: Record<string, true>
|
|
@@ -29,13 +29,16 @@ interface GitPanelProps {
|
|
|
29
29
|
pathConfig?: GitPanePathConfig
|
|
30
30
|
diffCountConfig?: GitPaneDiffCountConfig
|
|
31
31
|
headOffset?: number
|
|
32
|
+
// When set, the single changed-files section is a worktree fork-point review
|
|
33
|
+
// ("vs <base>") rather than a HEAD~N history walk.
|
|
34
|
+
baseLabel?: string
|
|
32
35
|
compact?: boolean
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function sectionTitle(section: GitFileSection, headOffset: number): string {
|
|
38
|
+
function sectionTitle(section: GitFileSection, headOffset: number, baseLabel?: string): string {
|
|
36
39
|
switch (section) {
|
|
37
40
|
case 'historical':
|
|
38
|
-
return `HEAD~${headOffset}`
|
|
41
|
+
return baseLabel != null && baseLabel !== '' ? baseLabel : `HEAD~${headOffset}`
|
|
39
42
|
case 'staged':
|
|
40
43
|
return 'Staged Changes'
|
|
41
44
|
case 'unstaged':
|
|
@@ -105,8 +108,8 @@ function renderFileLabel(
|
|
|
105
108
|
const displayPath = transform ? transform(file.path) : file.path
|
|
106
109
|
const { basename, prefix } = splitPath(displayPath)
|
|
107
110
|
const dir = stripTrailingSlash(prefix)
|
|
108
|
-
const showDir = fileListMode === 'flat' && pathConfig.enabled && dir
|
|
109
|
-
if (!file.renamedFrom) {
|
|
111
|
+
const showDir = fileListMode === 'flat' && pathConfig.enabled && dir !== ''
|
|
112
|
+
if (!(file.renamedFrom != null && file.renamedFrom !== '')) {
|
|
110
113
|
return (
|
|
111
114
|
<text selectable={false} wrapMode="none">
|
|
112
115
|
<span fg={t.text}>{basename}</span>
|
|
@@ -164,23 +167,30 @@ function renderDiffCount(
|
|
|
164
167
|
)
|
|
165
168
|
}
|
|
166
169
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
const FolderRow = memo(function FolderRow({
|
|
171
|
+
isSelected,
|
|
172
|
+
row,
|
|
173
|
+
}: {
|
|
174
|
+
row: GitTreeFolderRow
|
|
175
|
+
isSelected: boolean
|
|
176
|
+
}) {
|
|
177
|
+
const t = useTheme()
|
|
178
|
+
const transparent = useTransparent()
|
|
179
|
+
const bg = isSelected && !transparent ? t.backgroundElement : undefined
|
|
180
|
+
const onSelect = useCallback(() => {
|
|
171
181
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
172
|
-
}
|
|
173
|
-
const onToggle = ()
|
|
182
|
+
}, [row.key])
|
|
183
|
+
const onToggle = useCallback(() => {
|
|
174
184
|
dispatchGlobal({ key: row.key, type: 'git-mode-toggle-folder' })
|
|
175
|
-
}
|
|
185
|
+
}, [row.key])
|
|
176
186
|
return (
|
|
177
|
-
<box
|
|
187
|
+
<box flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
178
188
|
<box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
|
|
179
189
|
<box flexDirection="row" gap={1} onMouseDown={onToggle}>
|
|
180
190
|
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
181
191
|
{row.isCollapsed ? '▸' : '▾'}
|
|
182
192
|
</text>
|
|
183
|
-
<text selectable={false} fg={
|
|
193
|
+
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
184
194
|
{row.isCollapsed ? '\uf07b' : '\uf07c'}
|
|
185
195
|
</text>
|
|
186
196
|
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none">
|
|
@@ -190,30 +200,43 @@ function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode
|
|
|
190
200
|
</box>
|
|
191
201
|
</box>
|
|
192
202
|
)
|
|
193
|
-
}
|
|
203
|
+
})
|
|
194
204
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
isSelected
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
const FileRow = memo(function FileRow({
|
|
206
|
+
addedW,
|
|
207
|
+
diffCountConfig,
|
|
208
|
+
fileListMode,
|
|
209
|
+
isSelected,
|
|
210
|
+
pathConfig,
|
|
211
|
+
removedW,
|
|
212
|
+
repoPrefixes,
|
|
213
|
+
row,
|
|
214
|
+
}: {
|
|
215
|
+
row: GitTreeFileRow
|
|
216
|
+
addedW: number
|
|
217
|
+
removedW: number
|
|
218
|
+
isSelected: boolean
|
|
219
|
+
fileListMode: GitFileListMode
|
|
220
|
+
pathConfig: GitPanePathConfig
|
|
221
|
+
diffCountConfig: GitPaneDiffCountConfig
|
|
203
222
|
repoPrefixes: Record<string, string>
|
|
204
|
-
)
|
|
223
|
+
}) {
|
|
224
|
+
const t = useTheme()
|
|
225
|
+
const transparent = useTransparent()
|
|
205
226
|
const file = row.file
|
|
206
227
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
207
|
-
const bg = isSelected && !
|
|
208
|
-
const onSelect = ()
|
|
228
|
+
const bg = isSelected && !transparent ? t.backgroundElement : undefined
|
|
229
|
+
const onSelect = useCallback(() => {
|
|
209
230
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
210
|
-
}
|
|
231
|
+
}, [row.key])
|
|
211
232
|
// Repo disambiguation prefix: only in flat mode, only when the file came
|
|
212
233
|
// from a sub-repo (root repo files get an empty prefix).
|
|
213
234
|
const repoTag =
|
|
214
|
-
fileListMode === 'flat' && file.repoPath
|
|
235
|
+
fileListMode === 'flat' && file.repoPath != null && file.repoPath !== ''
|
|
236
|
+
? (repoPrefixes[file.repoPath] ?? '')
|
|
237
|
+
: ''
|
|
215
238
|
return (
|
|
216
|
-
<box
|
|
239
|
+
<box flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
217
240
|
<box width={2} flexShrink={0} justifyContent="center">
|
|
218
241
|
<text selectable={false} fg={statusColor(file.status)} bg={bg}>
|
|
219
242
|
<strong>{displayStatus(file)}</strong>
|
|
@@ -221,7 +244,7 @@ function renderFileRow(
|
|
|
221
244
|
</box>
|
|
222
245
|
{repoTag ? (
|
|
223
246
|
<box flexShrink={0}>
|
|
224
|
-
<text selectable={false} fg={
|
|
247
|
+
<text selectable={false} fg={t.primary} bg={bg}>
|
|
225
248
|
<strong>{repoTag}</strong>
|
|
226
249
|
</text>
|
|
227
250
|
</box>
|
|
@@ -232,32 +255,44 @@ function renderFileRow(
|
|
|
232
255
|
{renderDiffCount(file, addedW, removedW, bg, diffCountConfig, hasNumstat)}
|
|
233
256
|
</box>
|
|
234
257
|
)
|
|
235
|
-
}
|
|
258
|
+
})
|
|
236
259
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
260
|
+
const TreeSection = memo(function TreeSection({
|
|
261
|
+
addedW,
|
|
262
|
+
diffCountConfig,
|
|
263
|
+
fileListMode,
|
|
264
|
+
files,
|
|
265
|
+
marginTop,
|
|
266
|
+
pathConfig,
|
|
267
|
+
removedW,
|
|
268
|
+
repoPrefixes,
|
|
269
|
+
rows,
|
|
270
|
+
selectedEntryKey,
|
|
271
|
+
showListModeToggle,
|
|
272
|
+
title,
|
|
273
|
+
}: {
|
|
274
|
+
title: string
|
|
275
|
+
files: GitFileEntry[]
|
|
276
|
+
rows: (GitTreeFolderRow | GitTreeFileRow)[]
|
|
277
|
+
addedW: number
|
|
278
|
+
removedW: number
|
|
279
|
+
fileListMode: GitFileListMode
|
|
280
|
+
selectedEntryKey: string | null | undefined
|
|
281
|
+
showListModeToggle: boolean
|
|
282
|
+
pathConfig: GitPanePathConfig
|
|
283
|
+
diffCountConfig: GitPaneDiffCountConfig
|
|
284
|
+
marginTop: number
|
|
250
285
|
repoPrefixes: Record<string, string>
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
const t2 = getCurrentTheme()
|
|
286
|
+
}) {
|
|
287
|
+
const t2 = useTheme()
|
|
254
288
|
const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
|
|
255
|
-
const toggleListMode = ()
|
|
289
|
+
const toggleListMode = useCallback(() => {
|
|
256
290
|
dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
|
|
257
291
|
runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
|
|
258
|
-
}
|
|
292
|
+
}, [nextFileListMode])
|
|
293
|
+
if (files.length === 0) return null
|
|
259
294
|
return (
|
|
260
|
-
<box
|
|
295
|
+
<box flexDirection="column" marginTop={marginTop}>
|
|
261
296
|
<box flexDirection="row" justifyContent="space-between">
|
|
262
297
|
<text selectable={false} fg={t2.text}>
|
|
263
298
|
<strong>
|
|
@@ -279,22 +314,25 @@ function renderTreeSection(
|
|
|
279
314
|
) : null}
|
|
280
315
|
</box>
|
|
281
316
|
{rows.map((row) =>
|
|
282
|
-
row.kind === 'folder'
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
317
|
+
row.kind === 'folder' ? (
|
|
318
|
+
<FolderRow key={row.key} row={row} isSelected={row.key === selectedEntryKey} />
|
|
319
|
+
) : (
|
|
320
|
+
<FileRow
|
|
321
|
+
key={row.key}
|
|
322
|
+
row={row}
|
|
323
|
+
addedW={addedW}
|
|
324
|
+
removedW={removedW}
|
|
325
|
+
isSelected={row.key === selectedEntryKey}
|
|
326
|
+
fileListMode={fileListMode}
|
|
327
|
+
pathConfig={pathConfig}
|
|
328
|
+
diffCountConfig={diffCountConfig}
|
|
329
|
+
repoPrefixes={repoPrefixes}
|
|
330
|
+
/>
|
|
331
|
+
)
|
|
294
332
|
)}
|
|
295
333
|
</box>
|
|
296
334
|
)
|
|
297
|
-
}
|
|
335
|
+
})
|
|
298
336
|
|
|
299
337
|
interface StatusPlaceholder {
|
|
300
338
|
label: string
|
|
@@ -335,8 +373,13 @@ function computeStatusPlaceholder(
|
|
|
335
373
|
|
|
336
374
|
const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
|
|
337
375
|
const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
|
|
376
|
+
const HIDDEN_SCROLLBAR_OPTIONS = { visible: false }
|
|
377
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
378
|
+
const EMPTY_FILES: GitFileEntry[] = []
|
|
379
|
+
const EMPTY_ROWS: (GitTreeFolderRow | GitTreeFileRow)[] = []
|
|
338
380
|
|
|
339
381
|
export const GitPanel = memo(function GitPanel({
|
|
382
|
+
baseLabel,
|
|
340
383
|
collapsedFolders = {},
|
|
341
384
|
compact = false,
|
|
342
385
|
diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
|
|
@@ -350,7 +393,8 @@ export const GitPanel = memo(function GitPanel({
|
|
|
350
393
|
const t = useTheme()
|
|
351
394
|
useTransparent()
|
|
352
395
|
const repoPrefixes = useAppStore((s) => s.multiRepo.prefixes)
|
|
353
|
-
const
|
|
396
|
+
const isSingleSection = headOffset > 0 || (baseLabel != null && baseLabel !== '')
|
|
397
|
+
const sectionOrder = isSingleSection ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
|
|
354
398
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
355
399
|
const tree = useMemo(
|
|
356
400
|
() => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode, compact),
|
|
@@ -361,7 +405,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
361
405
|
[gitPanel.files]
|
|
362
406
|
)
|
|
363
407
|
|
|
364
|
-
const statusNode = renderStatus(gitPanel, !!projectPath)
|
|
408
|
+
const statusNode = renderStatus(gitPanel, !!(projectPath != null && projectPath !== ''))
|
|
365
409
|
|
|
366
410
|
const hasRemoteTracking = gitPanel.ahead > 0 || gitPanel.behind > 0
|
|
367
411
|
const toggleSection =
|
|
@@ -372,7 +416,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
372
416
|
|
|
373
417
|
useEffect(() => {
|
|
374
418
|
const scrollbox = scrollRef.current
|
|
375
|
-
if (!scrollbox || !selectedEntryKey) return
|
|
419
|
+
if (!scrollbox || !(selectedEntryKey != null && selectedEntryKey !== '')) return
|
|
376
420
|
const selectedIndex = tree.visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
377
421
|
if (selectedIndex < 0) return
|
|
378
422
|
const viewportHeight = Math.max(1, scrollbox.viewport.height)
|
|
@@ -392,29 +436,31 @@ export const GitPanel = memo(function GitPanel({
|
|
|
392
436
|
flexGrow={1}
|
|
393
437
|
ref={scrollRef}
|
|
394
438
|
scrollY
|
|
395
|
-
scrollbarOptions={
|
|
439
|
+
scrollbarOptions={HIDDEN_SCROLLBAR_OPTIONS}
|
|
396
440
|
viewportCulling
|
|
397
|
-
contentOptions={
|
|
441
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
398
442
|
>
|
|
399
443
|
{sectionOrder.map((key, idx) => {
|
|
400
444
|
const section = tree.sections.find((entry) => entry.section === key)
|
|
401
445
|
const priorHasFiles = sectionOrder
|
|
402
446
|
.slice(0, idx)
|
|
403
447
|
.some((k) => (tree.sections.find((e) => e.section === k)?.files.length ?? 0) > 0)
|
|
404
|
-
return
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
448
|
+
return (
|
|
449
|
+
<TreeSection
|
|
450
|
+
key={key}
|
|
451
|
+
title={sectionTitle(key, headOffset, baseLabel)}
|
|
452
|
+
files={section?.files ?? EMPTY_FILES}
|
|
453
|
+
rows={section?.rows ?? EMPTY_ROWS}
|
|
454
|
+
addedW={addedW}
|
|
455
|
+
removedW={removedW}
|
|
456
|
+
fileListMode={fileListMode}
|
|
457
|
+
selectedEntryKey={selectedEntryKey}
|
|
458
|
+
showListModeToggle={key === toggleSection}
|
|
459
|
+
pathConfig={pathConfig}
|
|
460
|
+
diffCountConfig={diffCountConfig}
|
|
461
|
+
marginTop={priorHasFiles ? 1 : 0}
|
|
462
|
+
repoPrefixes={repoPrefixes}
|
|
463
|
+
/>
|
|
418
464
|
)
|
|
419
465
|
})}
|
|
420
466
|
</scrollbox>
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
|
|
1
3
|
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
-
import { memo, useEffect, useRef } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
3
5
|
|
|
4
6
|
import type { DiffData, GitDiffView } from '../../../state/types'
|
|
5
7
|
import type { ThemeId } from '../../themes'
|
|
6
8
|
|
|
7
9
|
import { diffHash } from '../../../git/diff-hash'
|
|
10
|
+
import { getMergeBase } from '../../../git/divergence'
|
|
8
11
|
import { fetchDiff } from '../../../git/git-diff'
|
|
9
12
|
import { useGitPanelPolling } from '../../../git/git-poller'
|
|
10
13
|
import { useRepoDiscovery } from '../../../git/use-repo-discovery'
|
|
11
14
|
import { useAppStore } from '../../../state/app-store'
|
|
12
15
|
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
13
16
|
import { getSelectedGitFile, gitFileKey } from '../../../state/git-tree'
|
|
17
|
+
import { getActiveWorktree, getSessionProjectPath } from '../../../state/session-worktrees'
|
|
14
18
|
import { setGitDiffScroller } from '../../git-view-controls'
|
|
15
19
|
import { useTheme } from '../../theme'
|
|
16
20
|
import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
@@ -64,7 +68,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
64
68
|
</box>
|
|
65
69
|
)
|
|
66
70
|
}
|
|
67
|
-
if (diff.errorMessage) {
|
|
71
|
+
if (diff.errorMessage != null && diff.errorMessage !== '') {
|
|
68
72
|
return (
|
|
69
73
|
<box flexGrow={1} padding={1}>
|
|
70
74
|
<text fg={t.error}>{diff.errorMessage}</text>
|
|
@@ -77,7 +81,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
const placeholder = placeholderText(diff)
|
|
80
|
-
if (placeholder) {
|
|
84
|
+
if (placeholder != null && placeholder !== '') {
|
|
81
85
|
return (
|
|
82
86
|
<box flexGrow={1} padding={1}>
|
|
83
87
|
<text fg={t.textMuted}>{placeholder}</text>
|
|
@@ -87,7 +91,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
87
91
|
|
|
88
92
|
return (
|
|
89
93
|
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
90
|
-
{diff.oldPath ? (
|
|
94
|
+
{diff.oldPath != null && diff.oldPath !== '' ? (
|
|
91
95
|
<box paddingLeft={1} paddingRight={1}>
|
|
92
96
|
<text fg={t.textMuted}>
|
|
93
97
|
renamed: {diff.oldPath} → {diff.path}
|
|
@@ -123,13 +127,42 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
123
127
|
const focusMode = useAppStore((s) => s.focusMode)
|
|
124
128
|
const diffRef = useRef<PierreDiffHandle | null>(null)
|
|
125
129
|
|
|
126
|
-
const currentSession =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
const currentSession =
|
|
131
|
+
currentSessionId != null && currentSessionId !== ''
|
|
132
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
133
|
+
: undefined
|
|
134
|
+
const projectPath = getSessionProjectPath(currentSession)
|
|
135
|
+
|
|
136
|
+
// Review-vs-base: when toggled on for a worktree that records a baseRef,
|
|
137
|
+
// resolve the fork point and diff the working tree against it.
|
|
138
|
+
const activeWorktree = getActiveWorktree(currentSession)
|
|
139
|
+
const reviewBaseRef =
|
|
140
|
+
gitMode.reviewBase && activeWorktree?.baseRef != null && activeWorktree.baseRef !== ''
|
|
141
|
+
? activeWorktree.baseRef
|
|
142
|
+
: null
|
|
143
|
+
const [compareRef, setCompareRef] = useState<string | undefined>(undefined)
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (reviewBaseRef == null || projectPath == null || projectPath === '') {
|
|
146
|
+
setCompareRef(undefined)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
let cancelled = false
|
|
150
|
+
void (async () => {
|
|
151
|
+
const mergeBase = await getMergeBase(projectPath, reviewBaseRef)
|
|
152
|
+
if (!cancelled) setCompareRef(mergeBase)
|
|
153
|
+
})()
|
|
154
|
+
return () => {
|
|
155
|
+
cancelled = true
|
|
156
|
+
}
|
|
157
|
+
}, [reviewBaseRef, projectPath])
|
|
130
158
|
|
|
131
159
|
useRepoDiscovery(projectPath)
|
|
132
|
-
useGitPanelPolling({
|
|
160
|
+
useGitPanelPolling({
|
|
161
|
+
compareRef,
|
|
162
|
+
enabled: focusMode === 'git',
|
|
163
|
+
headOffset: gitMode.headOffset,
|
|
164
|
+
projectPath,
|
|
165
|
+
})
|
|
133
166
|
|
|
134
167
|
const fileBarWidth = Math.max(20, Math.floor(dimensions.width * gitPane.diffModeRatio))
|
|
135
168
|
|
|
@@ -140,8 +173,12 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
140
173
|
selectedEntryKey: gitMode.selectedEntryKey,
|
|
141
174
|
})
|
|
142
175
|
const selectedDiffKey = selectedFile ? gitFileKey(selectedFile) : null
|
|
143
|
-
const diff =
|
|
144
|
-
|
|
176
|
+
const diff =
|
|
177
|
+
selectedDiffKey != null && selectedDiffKey !== '' ? gitMode.diffs[selectedDiffKey] : undefined
|
|
178
|
+
const loading =
|
|
179
|
+
selectedDiffKey != null && selectedDiffKey !== ''
|
|
180
|
+
? !!(gitMode.loading[selectedDiffKey] === true)
|
|
181
|
+
: false
|
|
145
182
|
|
|
146
183
|
useEffect(() => {
|
|
147
184
|
setGitDiffScroller((delta: number) => {
|
|
@@ -160,6 +197,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
160
197
|
}, [])
|
|
161
198
|
|
|
162
199
|
useDiffPrefetch(gitMode.selectedEntryKey, gitPane.prefetchRadius, {
|
|
200
|
+
compareRef,
|
|
163
201
|
enabled: focusMode === 'git',
|
|
164
202
|
headOffset: gitMode.headOffset,
|
|
165
203
|
projectPath,
|
|
@@ -168,23 +206,38 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
168
206
|
|
|
169
207
|
useEffect(() => {
|
|
170
208
|
if (focusMode !== 'git') return
|
|
171
|
-
if (!selectedFile || !projectPath) return
|
|
172
|
-
if (!selectedDiffKey) return
|
|
209
|
+
if (!selectedFile || !(projectPath != null && projectPath !== '')) return
|
|
210
|
+
if (!(selectedDiffKey != null && selectedDiffKey !== '')) return
|
|
173
211
|
if (diff || loading) return
|
|
174
212
|
dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
|
|
175
|
-
void
|
|
176
|
-
|
|
213
|
+
void (async () => {
|
|
214
|
+
try {
|
|
215
|
+
const d = await fetchDiff(
|
|
216
|
+
selectedFile.repoPath ?? projectPath,
|
|
217
|
+
selectedFile,
|
|
218
|
+
gitMode.headOffset,
|
|
219
|
+
compareRef
|
|
220
|
+
)
|
|
177
221
|
dispatchGlobal({
|
|
178
222
|
diff: d,
|
|
179
223
|
hash: diffHash(d.rawDiff),
|
|
180
224
|
key: selectedDiffKey,
|
|
181
225
|
type: 'git-mode-set-diff',
|
|
182
226
|
})
|
|
183
|
-
|
|
184
|
-
.catch(() =>
|
|
227
|
+
} catch {
|
|
185
228
|
dispatchGlobal({ key: selectedDiffKey, loading: false, type: 'git-mode-set-loading' })
|
|
186
|
-
|
|
187
|
-
|
|
229
|
+
}
|
|
230
|
+
})()
|
|
231
|
+
}, [
|
|
232
|
+
focusMode,
|
|
233
|
+
projectPath,
|
|
234
|
+
selectedFile,
|
|
235
|
+
selectedDiffKey,
|
|
236
|
+
diff,
|
|
237
|
+
loading,
|
|
238
|
+
gitMode.headOffset,
|
|
239
|
+
compareRef,
|
|
240
|
+
])
|
|
188
241
|
|
|
189
242
|
const pendingPath = gitMode.pendingDeletePath
|
|
190
243
|
const pendingIsUntracked =
|
|
@@ -199,20 +252,30 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
199
252
|
}
|
|
200
253
|
const actionMessage = gitMode.actionMessage
|
|
201
254
|
let footerNode: React.ReactNode = null
|
|
202
|
-
if (pendingHint) {
|
|
255
|
+
if (pendingHint != null && pendingHint !== '') {
|
|
203
256
|
footerNode = (
|
|
204
257
|
<text fg={t.warning}>
|
|
205
258
|
<strong>{pendingHint}</strong>
|
|
206
259
|
</text>
|
|
207
260
|
)
|
|
208
|
-
} else if (actionMessage) {
|
|
261
|
+
} else if (actionMessage != null && actionMessage !== '') {
|
|
209
262
|
footerNode = actionMessage.split('\n').map((line, idx) => (
|
|
263
|
+
// Message lines are positional and may repeat (e.g. blank lines); index is the identity.
|
|
264
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
210
265
|
<text key={idx} fg={t.primary}>
|
|
211
266
|
{line}
|
|
212
267
|
</text>
|
|
213
268
|
))
|
|
214
269
|
}
|
|
215
270
|
|
|
271
|
+
const baseLabel = compareRef != null && reviewBaseRef != null ? `vs ${reviewBaseRef}` : undefined
|
|
272
|
+
|
|
273
|
+
const handleExitDiff = useCallback((event: OtuiMouseEvent) => {
|
|
274
|
+
event.preventDefault()
|
|
275
|
+
event.stopPropagation()
|
|
276
|
+
dispatchGlobal({ type: 'exit-git-mode' })
|
|
277
|
+
}, [])
|
|
278
|
+
|
|
216
279
|
return (
|
|
217
280
|
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
218
281
|
<box flexDirection="row" flexGrow={1}>
|
|
@@ -228,7 +291,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
228
291
|
<text fg={t.text}>
|
|
229
292
|
<strong>aimux · git</strong>
|
|
230
293
|
</text>
|
|
231
|
-
{gitPanel.branch ? (
|
|
294
|
+
{gitPanel.branch != null && gitPanel.branch !== '' ? (
|
|
232
295
|
<box flexDirection="row">
|
|
233
296
|
<text fg={t.text}>{'\u{e702}'} </text>
|
|
234
297
|
<text fg={t.text}>{gitPanel.branch}</text>
|
|
@@ -242,9 +305,18 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
242
305
|
<text fg={t.textMuted}>[ newer · ] older</text>
|
|
243
306
|
</box>
|
|
244
307
|
) : null}
|
|
308
|
+
{baseLabel != null ? (
|
|
309
|
+
<box flexDirection="row" gap={1}>
|
|
310
|
+
<text fg={t.primary}>
|
|
311
|
+
<strong>{baseLabel}</strong>
|
|
312
|
+
</text>
|
|
313
|
+
<text fg={t.textMuted}>b: back</text>
|
|
314
|
+
</box>
|
|
315
|
+
) : null}
|
|
245
316
|
<text fg={t.textMuted}>{'·'.repeat(Math.max(0, fileBarWidth - 2))}</text>
|
|
246
317
|
</box>
|
|
247
318
|
<GitPanel
|
|
319
|
+
baseLabel={baseLabel}
|
|
248
320
|
collapsedFolders={gitMode.collapsedFolders}
|
|
249
321
|
compact={gitPane.treeCompaction}
|
|
250
322
|
fileListMode={gitPane.fileListMode}
|
|
@@ -271,11 +343,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
271
343
|
paddingLeft={1}
|
|
272
344
|
paddingRight={1}
|
|
273
345
|
backgroundColor={actionBg}
|
|
274
|
-
onMouseDown={
|
|
275
|
-
event.preventDefault()
|
|
276
|
-
event.stopPropagation()
|
|
277
|
-
dispatchGlobal({ type: 'exit-git-mode' })
|
|
278
|
-
}}
|
|
346
|
+
onMouseDown={handleExitDiff}
|
|
279
347
|
>
|
|
280
348
|
<text fg={t.text}>
|
|
281
349
|
<strong>Exit diff</strong>
|
|
@@ -292,7 +360,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
292
360
|
/>
|
|
293
361
|
</box>
|
|
294
362
|
</box>
|
|
295
|
-
{footerNode ? (
|
|
363
|
+
{footerNode != null ? (
|
|
296
364
|
<box paddingLeft={1} paddingRight={1} backgroundColor={sidebarBg} flexDirection="column">
|
|
297
365
|
{footerNode}
|
|
298
366
|
</box>
|
|
@@ -76,14 +76,14 @@ export const ImageDiffView = memo(function ImageDiffView({ diff }: ImageDiffView
|
|
|
76
76
|
const showBoth = before && after
|
|
77
77
|
return (
|
|
78
78
|
<box flexDirection="column" flexGrow={1} overflow="hidden" backgroundColor={t.background}>
|
|
79
|
-
{diff.oldPath ? (
|
|
79
|
+
{diff.oldPath != null && diff.oldPath !== '' ? (
|
|
80
80
|
<box paddingLeft={1} paddingRight={1}>
|
|
81
81
|
<text fg={t.textMuted}>
|
|
82
82
|
renamed: {diff.oldPath} → {diff.path}
|
|
83
83
|
</text>
|
|
84
84
|
</box>
|
|
85
85
|
) : null}
|
|
86
|
-
{banner ? (
|
|
86
|
+
{banner != null && banner !== '' ? (
|
|
87
87
|
<box paddingLeft={1} paddingRight={1}>
|
|
88
88
|
<text fg={protocol === 'kitty' ? t.textMuted : t.warning}>{banner}</text>
|
|
89
89
|
</box>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type { BoxRenderable } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
3
4
|
|
|
4
5
|
import { convertToPng, isPng } from '../../../terminal-graphics/format-fallback'
|
|
5
6
|
import {
|
|
@@ -47,7 +48,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
47
48
|
let cancelled = false
|
|
48
49
|
stateRef.current = { imageId: null, lastKey: null, uploaded: false }
|
|
49
50
|
setError(null)
|
|
50
|
-
|
|
51
|
+
void (async () => {
|
|
51
52
|
let pngBytes: Uint8Array | null = null
|
|
52
53
|
if (mime === 'image/png' || isPng(bytes)) {
|
|
53
54
|
pngBytes = bytes
|
|
@@ -60,7 +61,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
60
61
|
}
|
|
61
62
|
pngBytes = result.png
|
|
62
63
|
}
|
|
63
|
-
if (cancelled
|
|
64
|
+
if (cancelled) return
|
|
64
65
|
const id = nextImageId()
|
|
65
66
|
writeRaw(uploadPngEscape(pngBytes, id))
|
|
66
67
|
stateRef.current.imageId = id
|
|
@@ -76,15 +77,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
76
77
|
}
|
|
77
78
|
}, [bytes, mime])
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
return (
|
|
81
|
-
<box flexGrow={1} alignItems="center" justifyContent="center" padding={1}>
|
|
82
|
-
<text fg={t.warning}>({error})</text>
|
|
83
|
-
</box>
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function renderAfter(this: BoxRenderable): void {
|
|
80
|
+
const renderAfter = useCallback(function (this: BoxRenderable): void {
|
|
88
81
|
const state = stateRef.current
|
|
89
82
|
if (!state.uploaded || state.imageId === null) return
|
|
90
83
|
const key = `${this.screenX},${this.screenY},${this.width},${this.height}`
|
|
@@ -93,6 +86,14 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
93
86
|
const seq = buildPlacement(state.imageId, this.screenX, this.screenY)
|
|
94
87
|
// Queue write to land AFTER opentui's native cell flush in this frame.
|
|
95
88
|
process.nextTick(() => writeRaw(seq))
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
if (error != null && error !== '') {
|
|
92
|
+
return (
|
|
93
|
+
<box flexGrow={1} alignItems="center" justifyContent="center" padding={1}>
|
|
94
|
+
<text fg={t.warning}>({error})</text>
|
|
95
|
+
</box>
|
|
96
|
+
)
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
return <box flexGrow={1} renderAfter={renderAfter} />
|