@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,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
|
-
import { connect, Socket } from 'node:net'
|
|
2
|
+
import { connect, type Socket } from 'node:net'
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
ScrollIntent,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
parseManagerMessage,
|
|
24
24
|
} from '../ipc/manager-protocol'
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface ManagerClientEvents {
|
|
27
27
|
render: [
|
|
28
28
|
sessionId: string,
|
|
29
29
|
tabId: string,
|
|
@@ -84,7 +84,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
84
84
|
return this.socket
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
private send(request: ManagerRequest): Promise<ManagerResponse> {
|
|
87
|
+
private async send(request: ManagerRequest): Promise<ManagerResponse> {
|
|
88
88
|
const socket = this.getConnectedSocket()
|
|
89
89
|
logDebug('managerClient.send', { id: request.id, type: request.type })
|
|
90
90
|
return new Promise((resolve, reject) => {
|
|
@@ -286,7 +286,9 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
286
286
|
return response.payload
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
createTab(
|
|
289
|
+
async createTab(
|
|
290
|
+
options: Extract<ManagerRequest, { type: 'createTab' }>['payload']
|
|
291
|
+
): Promise<void> {
|
|
290
292
|
logDebug('managerClient.createTab', {
|
|
291
293
|
command: options.command,
|
|
292
294
|
sessionId: options.sessionId,
|
|
@@ -296,7 +298,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
296
298
|
return this.sendExpectOk({ id: crypto.randomUUID(), payload: options, type: 'createTab' })
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
write(sessionId: string, tabId: string, data: string): Promise<void> {
|
|
301
|
+
async write(sessionId: string, tabId: string, data: string): Promise<void> {
|
|
300
302
|
return this.sendExpectOk({
|
|
301
303
|
id: crypto.randomUUID(),
|
|
302
304
|
payload: { data, sessionId, tabId },
|
|
@@ -304,7 +306,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
304
306
|
})
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
resize(
|
|
309
|
+
async resize(
|
|
308
310
|
sessionId: string,
|
|
309
311
|
cols: number,
|
|
310
312
|
rows: number,
|
|
@@ -317,7 +319,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
317
319
|
})
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
resizeTab(
|
|
322
|
+
async resizeTab(
|
|
321
323
|
sessionId: string,
|
|
322
324
|
tabId: string,
|
|
323
325
|
cols: number,
|
|
@@ -331,7 +333,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
331
333
|
})
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
scroll(sessionId: string, tabId: string, deltaLines: number): Promise<void> {
|
|
336
|
+
async scroll(sessionId: string, tabId: string, deltaLines: number): Promise<void> {
|
|
335
337
|
return this.sendExpectOk({
|
|
336
338
|
id: crypto.randomUUID(),
|
|
337
339
|
payload: { deltaLines, sessionId, tabId },
|
|
@@ -339,7 +341,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
339
341
|
})
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
scrollToBottom(sessionId: string, tabId: string): Promise<void> {
|
|
344
|
+
async scrollToBottom(sessionId: string, tabId: string): Promise<void> {
|
|
343
345
|
return this.sendExpectOk({
|
|
344
346
|
id: crypto.randomUUID(),
|
|
345
347
|
payload: { sessionId, tabId },
|
|
@@ -347,7 +349,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
347
349
|
})
|
|
348
350
|
}
|
|
349
351
|
|
|
350
|
-
reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
|
|
352
|
+
async reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
|
|
351
353
|
return this.sendExpectOk({
|
|
352
354
|
id: crypto.randomUUID(),
|
|
353
355
|
payload: { intent, sessionId, tabId },
|
|
@@ -355,7 +357,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
355
357
|
})
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
|
|
360
|
+
async setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
|
|
359
361
|
return this.sendExpectOk({
|
|
360
362
|
id: crypto.randomUUID(),
|
|
361
363
|
payload: { sessionId, tabId },
|
|
@@ -363,7 +365,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
363
365
|
})
|
|
364
366
|
}
|
|
365
367
|
|
|
366
|
-
closeTab(sessionId: string, tabId: string): Promise<void> {
|
|
368
|
+
async closeTab(sessionId: string, tabId: string): Promise<void> {
|
|
367
369
|
return this.sendExpectOk({
|
|
368
370
|
id: crypto.randomUUID(),
|
|
369
371
|
payload: { sessionId, tabId },
|
|
@@ -371,7 +373,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
371
373
|
})
|
|
372
374
|
}
|
|
373
375
|
|
|
374
|
-
disposeSession(sessionId: string): Promise<void> {
|
|
376
|
+
async disposeSession(sessionId: string): Promise<void> {
|
|
375
377
|
return this.sendExpectOk({
|
|
376
378
|
id: crypto.randomUUID(),
|
|
377
379
|
payload: { sessionId },
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { memo, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { FoldDispatch } from './pierre-diff'
|
|
4
|
+
|
|
1
5
|
import { useTheme } from '../../../theme'
|
|
2
6
|
import { FOLD_STEP, type FoldInfo } from './build-rows'
|
|
3
|
-
import { type FoldDispatch } from './pierre-diff'
|
|
4
7
|
|
|
5
8
|
interface Props {
|
|
6
9
|
fold: FoldInfo
|
|
7
10
|
dispatch: FoldDispatch
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
13
|
+
const Button = memo(function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
11
14
|
const t = useTheme()
|
|
12
15
|
const bg = t.diffContextBg
|
|
13
16
|
return (
|
|
@@ -15,7 +18,7 @@ function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
|
15
18
|
<text fg={t.primary}>{label}</text>
|
|
16
19
|
</box>
|
|
17
20
|
)
|
|
18
|
-
}
|
|
21
|
+
})
|
|
19
22
|
|
|
20
23
|
function Spacer() {
|
|
21
24
|
return <text> </text>
|
|
@@ -30,42 +33,47 @@ export function FoldStrip({ dispatch, fold }: Props) {
|
|
|
30
33
|
const shrinkUp = Math.min(FOLD_STEP, topExpanded)
|
|
31
34
|
const shrinkDown = Math.min(FOLD_STEP, bottomExpanded)
|
|
32
35
|
|
|
36
|
+
const handleExpandTop = useCallback(
|
|
37
|
+
() => dispatch.adjust(foldId, 'top', stepUp),
|
|
38
|
+
[dispatch, foldId, stepUp]
|
|
39
|
+
)
|
|
40
|
+
const handleExpandBottom = useCallback(
|
|
41
|
+
() => dispatch.adjust(foldId, 'bottom', stepDown),
|
|
42
|
+
[dispatch, foldId, stepDown]
|
|
43
|
+
)
|
|
44
|
+
const handleExpandAll = useCallback(
|
|
45
|
+
() => dispatch.set(foldId, total, 0),
|
|
46
|
+
[dispatch, foldId, total]
|
|
47
|
+
)
|
|
48
|
+
const handleShrinkTop = useCallback(
|
|
49
|
+
() => dispatch.adjust(foldId, 'top', -shrinkUp),
|
|
50
|
+
[dispatch, foldId, shrinkUp]
|
|
51
|
+
)
|
|
52
|
+
const handleShrinkBottom = useCallback(
|
|
53
|
+
() => dispatch.adjust(foldId, 'bottom', -shrinkDown),
|
|
54
|
+
[dispatch, foldId, shrinkDown]
|
|
55
|
+
)
|
|
56
|
+
|
|
33
57
|
const controls: React.ReactNode[] = []
|
|
34
58
|
if (hidden > 0) {
|
|
35
59
|
controls.push(
|
|
36
|
-
<Button
|
|
37
|
-
key="up"
|
|
38
|
-
label={`↑${stepUp}`}
|
|
39
|
-
onPress={() => dispatch.adjust(foldId, 'top', stepUp)}
|
|
40
|
-
/>,
|
|
60
|
+
<Button key="up" label={`↑${stepUp}`} onPress={handleExpandTop} />,
|
|
41
61
|
<Spacer key="sp1" />,
|
|
42
|
-
<Button
|
|
43
|
-
key="down"
|
|
44
|
-
label={`↓${stepDown}`}
|
|
45
|
-
onPress={() => dispatch.adjust(foldId, 'bottom', stepDown)}
|
|
46
|
-
/>,
|
|
62
|
+
<Button key="down" label={`↓${stepDown}`} onPress={handleExpandBottom} />,
|
|
47
63
|
<Spacer key="sp2" />,
|
|
48
|
-
<Button key="all" label="⇅ all" onPress={
|
|
64
|
+
<Button key="all" label="⇅ all" onPress={handleExpandAll} />
|
|
49
65
|
)
|
|
50
66
|
}
|
|
51
67
|
if (shrinkUp > 0) {
|
|
52
68
|
controls.push(
|
|
53
69
|
<Spacer key="sp3" />,
|
|
54
|
-
<Button
|
|
55
|
-
key="shrinkUp"
|
|
56
|
-
label={`−↑${shrinkUp}`}
|
|
57
|
-
onPress={() => dispatch.adjust(foldId, 'top', -shrinkUp)}
|
|
58
|
-
/>
|
|
70
|
+
<Button key="shrinkUp" label={`−↑${shrinkUp}`} onPress={handleShrinkTop} />
|
|
59
71
|
)
|
|
60
72
|
}
|
|
61
73
|
if (shrinkDown > 0) {
|
|
62
74
|
controls.push(
|
|
63
75
|
<Spacer key="sp4" />,
|
|
64
|
-
<Button
|
|
65
|
-
key="shrinkDown"
|
|
66
|
-
label={`−↓${shrinkDown}`}
|
|
67
|
-
onPress={() => dispatch.adjust(foldId, 'bottom', -shrinkDown)}
|
|
68
|
-
/>
|
|
76
|
+
<Button key="shrinkDown" label={`−↓${shrinkDown}`} onPress={handleShrinkBottom} />
|
|
69
77
|
)
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -3,11 +3,11 @@ import type { ThemedToken } from 'shiki'
|
|
|
3
3
|
|
|
4
4
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
|
5
5
|
|
|
6
|
+
import type { FoldState } from '../../../../state/types'
|
|
6
7
|
import type { ThemeId } from '../../../themes'
|
|
7
8
|
|
|
8
9
|
import { useAppStore } from '../../../../state/app-store'
|
|
9
10
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
10
|
-
import { type FoldState } from '../../../../state/types'
|
|
11
11
|
import { useTheme } from '../../../theme'
|
|
12
12
|
import { buildDiffSegments, firstChangeSegmentOffset, gutterWidth } from './build-rows'
|
|
13
13
|
import { SplitView, type SplitViewHandle } from './split-view'
|
|
@@ -22,14 +22,14 @@ interface PrepareOptions {
|
|
|
22
22
|
signal?: AbortSignal
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function yieldToEventLoop(): Promise<void> {
|
|
25
|
+
async function yieldToEventLoop(): Promise<void> {
|
|
26
26
|
return new Promise((resolve) => {
|
|
27
27
|
setImmediate(resolve)
|
|
28
28
|
})
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function throwIfAborted(signal?: AbortSignal): void {
|
|
32
|
-
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
32
|
+
if (signal?.aborted === true) throw new DOMException('Aborted', 'AbortError')
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Lifts diff parsing off the synchronous render tree and returns a segmented
|
|
@@ -34,6 +34,30 @@ import { tokenToSpan } from './highlight'
|
|
|
34
34
|
import { useSegmentVirtualization } from './use-segment-virtualization'
|
|
35
35
|
|
|
36
36
|
const OVERSCAN = 24
|
|
37
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
38
|
+
const HIDDEN_SCROLLBAR_OPTIONS = { visible: false }
|
|
39
|
+
|
|
40
|
+
// Stable per-row key derived from the line identities in the row, so rows keep a
|
|
41
|
+
// consistent identity across fold expand/collapse rather than relying on position.
|
|
42
|
+
function splitCellKey(cell: SplitCell): string {
|
|
43
|
+
switch (cell.type) {
|
|
44
|
+
case 'context':
|
|
45
|
+
return `c${cell.lineNumber}`
|
|
46
|
+
case 'addition':
|
|
47
|
+
return `a${cell.lineNumber}`
|
|
48
|
+
case 'deletion':
|
|
49
|
+
return `d${cell.lineNumber}`
|
|
50
|
+
case 'fold':
|
|
51
|
+
return `f${cell.fold.foldId}`
|
|
52
|
+
case 'filler':
|
|
53
|
+
return 'x'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function splitRowKey(row: SplitRowOrHeader): string {
|
|
58
|
+
if (row.type === 'hunk-header') return `hh:${row.spec}`
|
|
59
|
+
return `${splitCellKey(row.left)}|${splitCellKey(row.right)}`
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
export interface SplitViewHandle {
|
|
39
63
|
leftScroll: ScrollBoxRenderable | null
|
|
@@ -154,15 +178,15 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
154
178
|
flexGrow={1}
|
|
155
179
|
scrollY
|
|
156
180
|
viewportCulling
|
|
157
|
-
contentOptions={
|
|
158
|
-
verticalScrollbarOptions={
|
|
181
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
182
|
+
verticalScrollbarOptions={HIDDEN_SCROLLBAR_OPTIONS}
|
|
159
183
|
onMouseScroll={handleScroll}
|
|
160
184
|
>
|
|
161
185
|
{visibleWindow.topSpacer > 0 ? <box height={visibleWindow.topSpacer} /> : null}
|
|
162
186
|
{renderedSegments.map((rendered) =>
|
|
163
|
-
rendered.rows.map((row
|
|
187
|
+
rendered.rows.map((row) => (
|
|
164
188
|
<SideRow
|
|
165
|
-
key={`${rendered.segment.id}:left:${
|
|
189
|
+
key={`${rendered.segment.id}:left:${splitRowKey(row)}`}
|
|
166
190
|
cell={row.type === 'row' ? row.left : null}
|
|
167
191
|
foldDispatch={foldDispatch}
|
|
168
192
|
gw={gw}
|
|
@@ -180,14 +204,14 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
180
204
|
flexGrow={1}
|
|
181
205
|
scrollY
|
|
182
206
|
viewportCulling
|
|
183
|
-
contentOptions={
|
|
207
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
184
208
|
onMouseScroll={handleScroll}
|
|
185
209
|
>
|
|
186
210
|
{visibleWindow.topSpacer > 0 ? <box height={visibleWindow.topSpacer} /> : null}
|
|
187
211
|
{renderedSegments.map((rendered) =>
|
|
188
|
-
rendered.rows.map((row
|
|
212
|
+
rendered.rows.map((row) => (
|
|
189
213
|
<SideRow
|
|
190
|
-
key={`${rendered.segment.id}:right:${
|
|
214
|
+
key={`${rendered.segment.id}:right:${splitRowKey(row)}`}
|
|
191
215
|
cell={row.type === 'row' ? row.right : null}
|
|
192
216
|
foldDispatch={foldDispatch}
|
|
193
217
|
gw={gw}
|
|
@@ -230,7 +254,9 @@ function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-h
|
|
|
230
254
|
return (
|
|
231
255
|
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
232
256
|
<text fg={t.text}>{row.spec}</text>
|
|
233
|
-
{row.context
|
|
257
|
+
{row.context != null && row.context !== '' ? (
|
|
258
|
+
<text fg={t.textMuted}> {row.context}</text>
|
|
259
|
+
) : null}
|
|
234
260
|
</box>
|
|
235
261
|
)
|
|
236
262
|
}
|
|
@@ -285,10 +311,12 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
|
|
|
285
311
|
{tokens.map((tok, i) => {
|
|
286
312
|
const s = tokenToSpan(tok)
|
|
287
313
|
let attributes = 0
|
|
288
|
-
if (s.bold) attributes |= TextAttributes.BOLD
|
|
289
|
-
if (s.italic) attributes |= TextAttributes.ITALIC
|
|
290
|
-
if (s.underline) attributes |= TextAttributes.UNDERLINE
|
|
314
|
+
if (s.bold === true) attributes |= TextAttributes.BOLD
|
|
315
|
+
if (s.italic === true) attributes |= TextAttributes.ITALIC
|
|
316
|
+
if (s.underline === true) attributes |= TextAttributes.UNDERLINE
|
|
291
317
|
return (
|
|
318
|
+
// Syntax tokens are positional within a single line and never reorder.
|
|
319
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
292
320
|
<span key={i} fg={s.fg ?? t.text} attributes={attributes}>
|
|
293
321
|
{s.text}
|
|
294
322
|
</span>
|
|
@@ -33,6 +33,24 @@ import { tokenToSpan } from './highlight'
|
|
|
33
33
|
import { useSegmentVirtualization } from './use-segment-virtualization'
|
|
34
34
|
|
|
35
35
|
const OVERSCAN = 24
|
|
36
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
37
|
+
|
|
38
|
+
// Stable per-row key derived from the line identities in the row, so rows keep a
|
|
39
|
+
// consistent identity across fold expand/collapse rather than relying on position.
|
|
40
|
+
function unifiedRowKey(row: UnifiedRowOrHeader): string {
|
|
41
|
+
switch (row.type) {
|
|
42
|
+
case 'hunk-header':
|
|
43
|
+
return `hh:${row.spec}`
|
|
44
|
+
case 'fold':
|
|
45
|
+
return `f:${row.fold.foldId}`
|
|
46
|
+
case 'context':
|
|
47
|
+
return `c:${row.delLineNumber}:${row.addLineNumber}`
|
|
48
|
+
case 'addition':
|
|
49
|
+
return `a:${row.lineNumber}`
|
|
50
|
+
case 'deletion':
|
|
51
|
+
return `d:${row.lineNumber}`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
36
54
|
|
|
37
55
|
export interface StackedViewHandle {
|
|
38
56
|
scroll: ScrollBoxRenderable | null
|
|
@@ -148,14 +166,14 @@ export const StackedView = forwardRef<StackedViewHandle, Props>(function Stacked
|
|
|
148
166
|
flexGrow={1}
|
|
149
167
|
scrollY
|
|
150
168
|
viewportCulling
|
|
151
|
-
contentOptions={
|
|
169
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
152
170
|
onMouseScroll={handleScroll}
|
|
153
171
|
>
|
|
154
172
|
{visibleWindow.topSpacer > 0 ? <box height={visibleWindow.topSpacer} /> : null}
|
|
155
173
|
{renderedSegments.map((rendered) =>
|
|
156
|
-
rendered.rows.map((row
|
|
174
|
+
rendered.rows.map((row) => (
|
|
157
175
|
<UnifiedRowRender
|
|
158
|
-
key={`${rendered.segment.id}:${
|
|
176
|
+
key={`${rendered.segment.id}:${unifiedRowKey(row)}`}
|
|
159
177
|
foldDispatch={foldDispatch}
|
|
160
178
|
gw={gw}
|
|
161
179
|
highlights={highlights}
|
|
@@ -186,7 +204,9 @@ function UnifiedRowRender({
|
|
|
186
204
|
return (
|
|
187
205
|
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
188
206
|
<text fg={t.text}>{row.spec}</text>
|
|
189
|
-
{row.context
|
|
207
|
+
{row.context != null && row.context !== '' ? (
|
|
208
|
+
<text fg={t.textMuted}> {row.context}</text>
|
|
209
|
+
) : null}
|
|
190
210
|
</box>
|
|
191
211
|
)
|
|
192
212
|
}
|
|
@@ -230,10 +250,12 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
|
|
|
230
250
|
{tokens.map((tok, i) => {
|
|
231
251
|
const s = tokenToSpan(tok)
|
|
232
252
|
let attributes = 0
|
|
233
|
-
if (s.bold) attributes |= TextAttributes.BOLD
|
|
234
|
-
if (s.italic) attributes |= TextAttributes.ITALIC
|
|
235
|
-
if (s.underline) attributes |= TextAttributes.UNDERLINE
|
|
253
|
+
if (s.bold === true) attributes |= TextAttributes.BOLD
|
|
254
|
+
if (s.italic === true) attributes |= TextAttributes.ITALIC
|
|
255
|
+
if (s.underline === true) attributes |= TextAttributes.UNDERLINE
|
|
236
256
|
return (
|
|
257
|
+
// Syntax tokens are positional within a single line and never reorder.
|
|
258
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
237
259
|
<span key={i} fg={s.fg ?? t.text} attributes={attributes}>
|
|
238
260
|
{s.text}
|
|
239
261
|
</span>
|
|
@@ -13,6 +13,7 @@ interface PrefetchOptions {
|
|
|
13
13
|
projectPath: string | undefined
|
|
14
14
|
themeId: string
|
|
15
15
|
headOffset: number
|
|
16
|
+
compareRef?: string
|
|
16
17
|
enabled: boolean
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -66,11 +67,15 @@ class PrefetchQueue {
|
|
|
66
67
|
if (!task) break
|
|
67
68
|
this.inflight.set(task.key, task.controller)
|
|
68
69
|
this.running += 1
|
|
69
|
-
void
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
void (async () => {
|
|
71
|
+
try {
|
|
72
|
+
await this.execute(task)
|
|
73
|
+
} finally {
|
|
74
|
+
this.inflight.delete(task.key)
|
|
75
|
+
this.running -= 1
|
|
76
|
+
this.pump()
|
|
77
|
+
}
|
|
78
|
+
})()
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
}
|
|
@@ -94,15 +99,20 @@ export function useDiffPrefetch(
|
|
|
94
99
|
|
|
95
100
|
const queueRef = useRef<PrefetchQueue | null>(null)
|
|
96
101
|
|
|
97
|
-
const { enabled, headOffset, projectPath } = opts
|
|
102
|
+
const { compareRef, enabled, headOffset, projectPath } = opts
|
|
98
103
|
const runTaskRef = useRef<(task: Task) => Promise<void>>(async () => {})
|
|
99
104
|
|
|
100
105
|
// Keep a ref to the execute function so the queue keeps the latest closure
|
|
101
106
|
// without needing to recreate the queue itself.
|
|
102
107
|
runTaskRef.current = async (task: Task) => {
|
|
103
|
-
if (!projectPath) return
|
|
108
|
+
if (!(projectPath != null && projectPath !== '')) return
|
|
104
109
|
try {
|
|
105
|
-
const diff = await fetchDiff(
|
|
110
|
+
const diff = await fetchDiff(
|
|
111
|
+
task.file.repoPath ?? projectPath,
|
|
112
|
+
task.file,
|
|
113
|
+
headOffset,
|
|
114
|
+
compareRef
|
|
115
|
+
)
|
|
106
116
|
if (task.controller.signal.aborted) return
|
|
107
117
|
const hash = diffHash(diff.rawDiff)
|
|
108
118
|
dispatchGlobal({ diff, hash, key: task.key, type: 'git-mode-set-diff' })
|
|
@@ -122,13 +132,19 @@ export function useDiffPrefetch(
|
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
if (!queueRef.current) {
|
|
125
|
-
queueRef.current = new PrefetchQueue((task) => runTaskRef.current(task), MAX_CONCURRENCY)
|
|
135
|
+
queueRef.current = new PrefetchQueue(async (task) => runTaskRef.current(task), MAX_CONCURRENCY)
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
useEffect(() => {
|
|
129
139
|
const queue = queueRef.current
|
|
130
140
|
if (!queue) return
|
|
131
|
-
if (
|
|
141
|
+
if (
|
|
142
|
+
!enabled ||
|
|
143
|
+
radius <= 0 ||
|
|
144
|
+
projectPath == null ||
|
|
145
|
+
projectPath === '' ||
|
|
146
|
+
!(selectedEntryKey != null && selectedEntryKey !== '')
|
|
147
|
+
) {
|
|
132
148
|
queue.cancelAll()
|
|
133
149
|
return
|
|
134
150
|
}
|
|
@@ -149,7 +165,7 @@ export function useDiffPrefetch(
|
|
|
149
165
|
const key = row.key
|
|
150
166
|
if (diffs[key]) continue
|
|
151
167
|
if (parsed[key]) continue
|
|
152
|
-
if (loading[key]) continue
|
|
168
|
+
if (loading[key] === true) continue
|
|
153
169
|
tasks.push({
|
|
154
170
|
controller: new AbortController(),
|
|
155
171
|
distance: Math.abs(i - selectedIdx),
|
|
@@ -171,6 +187,7 @@ export function useDiffPrefetch(
|
|
|
171
187
|
parsed,
|
|
172
188
|
loading,
|
|
173
189
|
headOffset,
|
|
190
|
+
compareRef,
|
|
174
191
|
])
|
|
175
192
|
|
|
176
193
|
useEffect(() => {
|
|
@@ -124,8 +124,9 @@ export function useDiffPreparation(
|
|
|
124
124
|
const owner = requestOwner
|
|
125
125
|
const version = requestVersionRef.current
|
|
126
126
|
setPreparing(true)
|
|
127
|
-
void
|
|
128
|
-
|
|
127
|
+
void (async () => {
|
|
128
|
+
try {
|
|
129
|
+
const result = await prepareDiff(diff, path, { signal: controller.signal })
|
|
129
130
|
if (
|
|
130
131
|
controller.signal.aborted ||
|
|
131
132
|
requestVersionRef.current !== version ||
|
|
@@ -142,9 +143,9 @@ export function useDiffPreparation(
|
|
|
142
143
|
key: cacheKey,
|
|
143
144
|
type: 'git-mode-set-parsed',
|
|
144
145
|
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
} catch {
|
|
147
|
+
// Aborted or failed preparation; nothing to surface here.
|
|
148
|
+
} finally {
|
|
148
149
|
if (
|
|
149
150
|
!controller.signal.aborted &&
|
|
150
151
|
requestVersionRef.current === version &&
|
|
@@ -152,7 +153,8 @@ export function useDiffPreparation(
|
|
|
152
153
|
) {
|
|
153
154
|
setPreparing(false)
|
|
154
155
|
}
|
|
155
|
-
}
|
|
156
|
+
}
|
|
157
|
+
})()
|
|
156
158
|
|
|
157
159
|
return () => {
|
|
158
160
|
controller.abort()
|
|
@@ -163,11 +165,11 @@ export function useDiffPreparation(
|
|
|
163
165
|
(segments: readonly DiffSegment[]) => {
|
|
164
166
|
const file = parsed.file
|
|
165
167
|
const filetype = parsed.filetype
|
|
166
|
-
if (!file || !filetype) return
|
|
168
|
+
if (!file || !(filetype != null && filetype !== '')) return
|
|
167
169
|
const owner = requestOwner
|
|
168
170
|
const version = requestVersionRef.current
|
|
169
171
|
const activeHash = cachedParsed?.hash ?? (localOwnerMatches ? localHash : null)
|
|
170
|
-
if (!activeHash) return
|
|
172
|
+
if (!(activeHash != null && activeHash !== '')) return
|
|
171
173
|
const next = segments.filter(
|
|
172
174
|
(segment) =>
|
|
173
175
|
(segment.kind === 'context' || segment.kind === 'change') &&
|