@brimveyn/aimux 1.9.0 → 1.9.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/package.json +2 -2
- package/src/app-runtime/side-effects.ts +13 -3
- package/src/app.tsx +26 -17
- package/src/git/git-poller.ts +56 -3
- package/src/git/repo-discovery.ts +95 -0
- package/src/git/use-repo-discovery.ts +28 -0
- package/src/input/keymap/describe-bindings.ts +63 -2
- package/src/pty/terminal-snapshot.ts +7 -5
- package/src/state/reducers/git-mode-state.ts +58 -0
- package/src/state/reducers/git-panel-state.ts +3 -0
- package/src/state/reducers/multi-repo-state.ts +60 -0
- package/src/state/store.ts +6 -0
- package/src/state/types.ts +38 -6
- package/src/ui/breaking-update-screen.tsx +2 -2
- package/src/ui/components/git/diff-renderer/build-rows.ts +467 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/fold-strip.tsx +1 -1
- package/src/ui/components/git/diff-renderer/highlight.ts +102 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/pierre-diff.tsx +23 -15
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +62 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/split-view.tsx +122 -47
- package/src/ui/components/{diff-renderer → git/diff-renderer}/stacked-view.tsx +106 -35
- package/src/ui/components/{diff-renderer → git/diff-renderer}/use-diff-prefetch.ts +9 -18
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +240 -0
- package/src/ui/components/git/diff-renderer/use-segment-virtualization.ts +95 -0
- package/src/ui/components/{git-panel.tsx → git/git-panel.tsx} +32 -10
- package/src/ui/components/{git-view.tsx → git/git-view.tsx} +73 -38
- package/src/ui/components/{git-pane-context-menu.ts → git/pane/git-pane-context-menu.ts} +2 -2
- package/src/ui/components/{git-pane-widget.tsx → git/pane/git-pane-widget.tsx} +6 -4
- package/src/ui/components/{session-bar.tsx → layout/session-bar.tsx} +36 -13
- package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts} +2 -2
- package/src/ui/components/{sidebar.tsx → layout/sidebar/sidebar.tsx} +6 -6
- package/src/ui/components/{tab-item.tsx → layout/sidebar/tab-item.tsx} +5 -5
- package/src/ui/components/{use-sidebar-branch.ts → layout/sidebar/use-sidebar-branch.ts} +1 -1
- package/src/ui/components/{split-layout.tsx → layout/split-layout.tsx} +5 -5
- package/src/ui/components/{status-bar.tsx → layout/status-bar.tsx} +7 -7
- package/src/ui/components/{terminal-pane.tsx → layout/terminal-pane.tsx} +8 -8
- package/src/ui/components/{ai-usage-modal.tsx → modals/app/ai-usage-modal.tsx} +10 -6
- package/src/ui/components/{help-modal.tsx → modals/app/help-modal.tsx} +6 -6
- package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx} +4 -4
- package/src/ui/components/{git-commit-modal.tsx → modals/git/git-commit-modal.tsx} +8 -8
- package/src/ui/components/modals/sessions/create-session-modal.tsx +79 -0
- package/src/ui/components/modals/sessions/session-name-modal.tsx +10 -0
- package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx} +8 -8
- package/src/ui/components/modals/shared/form.tsx +164 -0
- package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx} +7 -7
- package/src/ui/components/{modal-shell.tsx → modals/shared/modal-shell.tsx} +1 -1
- package/src/ui/components/{picker.tsx → modals/shared/picker.tsx} +3 -3
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +30 -0
- package/src/ui/components/{snippet-picker-modal.tsx → modals/snippets/snippet-picker-modal.tsx} +6 -6
- package/src/ui/components/{new-tab-modal.tsx → modals/tabs/new-tab-modal.tsx} +17 -20
- package/src/ui/components/{theme-picker-modal.tsx → modals/themes/theme-picker-modal.tsx} +6 -6
- package/src/ui/components/{ai-usage-indicator.tsx → overlays/ai-usage/ai-usage-indicator.tsx} +3 -3
- package/src/ui/components/{context-menu-box.tsx → overlays/context-menu/context-menu-box.tsx} +1 -1
- package/src/ui/components/{context-menu-overlay.tsx → overlays/context-menu/context-menu-overlay.tsx} +3 -3
- package/src/ui/components/{pending-chord-overlay.tsx → overlays/pending-chord-overlay.tsx} +6 -6
- package/src/ui/components/{bare-input.tsx → primitives/bare-input.tsx} +2 -2
- package/src/ui/components/{input-field.tsx → primitives/input-field.tsx} +2 -2
- package/src/ui/components/{list-item.tsx → primitives/list-item.tsx} +1 -1
- package/src/ui/components/{surface.tsx → primitives/surface.tsx} +1 -1
- package/src/ui/root.tsx +24 -22
- package/src/ui/theme-store.ts +30 -26
- package/src/ui/themes.ts +1 -0
- package/src/ui/components/create-session-modal.tsx +0 -96
- package/src/ui/components/diff-renderer/build-rows.ts +0 -349
- package/src/ui/components/diff-renderer/highlight.ts +0 -44
- package/src/ui/components/diff-renderer/prepare-diff.ts +0 -65
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +0 -106
- package/src/ui/components/session-name-modal.tsx +0 -11
- package/src/ui/components/snippet-editor-modal.tsx +0 -40
- /package/src/ui/components/{diff-renderer → git/diff-renderer}/filetype.ts +0 -0
- /package/src/ui/components/{diff-renderer → git/diff-renderer}/index.ts +0 -0
- /package/src/ui/components/{sidebar-scroll.ts → layout/sidebar/sidebar-scroll.ts} +0 -0
- /package/src/ui/components/{use-sidebar-auto-scroll.ts → layout/sidebar/use-sidebar-auto-scroll.ts} +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { ThemedToken } from 'shiki'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { DiffSegment } from './build-rows'
|
|
6
|
+
|
|
7
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
8
|
+
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
9
|
+
import { getHighlightsTokens, getParsedPayload } from '../../../../state/types'
|
|
10
|
+
import { type HighlightPatch, tokenizeSegment } from './highlight'
|
|
11
|
+
import { prepareDiff, type PreparedParsedDiff } from './prepare-diff'
|
|
12
|
+
|
|
13
|
+
export interface DiffPreparation {
|
|
14
|
+
file: PreparedParsedDiff['file']
|
|
15
|
+
filetype: string | null
|
|
16
|
+
firstChangeOffset: number
|
|
17
|
+
highlights: { add: ThemedToken[][]; del: ThemedToken[][] }
|
|
18
|
+
preparing: boolean
|
|
19
|
+
ready: boolean
|
|
20
|
+
requestSegmentHighlights: (segments: readonly DiffSegment[]) => void
|
|
21
|
+
segments: DiffSegment[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const EMPTY_HIGHLIGHTS = { add: [] as ThemedToken[][], del: [] as ThemedToken[][] }
|
|
25
|
+
const EMPTY_PARSED: PreparedParsedDiff = {
|
|
26
|
+
file: null,
|
|
27
|
+
filetype: null,
|
|
28
|
+
firstChangeOffset: -1,
|
|
29
|
+
segments: [],
|
|
30
|
+
}
|
|
31
|
+
const MAX_HIGHLIGHT_SEGMENTS_PER_PASS = 8
|
|
32
|
+
|
|
33
|
+
function applyHighlightPatches(
|
|
34
|
+
source: { add: ThemedToken[][]; del: ThemedToken[][] },
|
|
35
|
+
add: readonly HighlightPatch[],
|
|
36
|
+
del: readonly HighlightPatch[]
|
|
37
|
+
): { add: ThemedToken[][]; del: ThemedToken[][] } {
|
|
38
|
+
const nextAdd = [...source.add]
|
|
39
|
+
const nextDel = [...source.del]
|
|
40
|
+
for (const patch of add) {
|
|
41
|
+
for (let i = 0; i < patch.tokens.length; i++) nextAdd[patch.start + i] = patch.tokens[i] ?? []
|
|
42
|
+
}
|
|
43
|
+
for (const patch of del) {
|
|
44
|
+
for (let i = 0; i < patch.tokens.length; i++) nextDel[patch.start + i] = patch.tokens[i] ?? []
|
|
45
|
+
}
|
|
46
|
+
return { add: nextAdd, del: nextDel }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function segmentHasHighlights(
|
|
50
|
+
segment: DiffSegment,
|
|
51
|
+
highlights: { add: ThemedToken[][]; del: ThemedToken[][] }
|
|
52
|
+
): boolean {
|
|
53
|
+
if (segment.kind === 'context') {
|
|
54
|
+
for (let i = 0; i < segment.count; i++) {
|
|
55
|
+
if (!highlights.add[segment.addStart + i] || !highlights.del[segment.delStart + i])
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
if (segment.kind === 'change') {
|
|
61
|
+
for (let i = 0; i < segment.additions; i++) {
|
|
62
|
+
if (!highlights.add[segment.addStart + i]) return false
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < segment.deletions; i++) {
|
|
65
|
+
if (!highlights.del[segment.delStart + i]) return false
|
|
66
|
+
}
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useDiffPreparation(
|
|
73
|
+
cacheKey: string,
|
|
74
|
+
diff: string,
|
|
75
|
+
path: string,
|
|
76
|
+
themeId: string
|
|
77
|
+
): DiffPreparation {
|
|
78
|
+
const requestOwner = `${cacheKey}|${themeId}|${diff}`
|
|
79
|
+
const cachedParsed = useAppStore((s) => s.gitMode.parsedFiles[cacheKey])
|
|
80
|
+
const highlightKey = `${cacheKey}|${themeId}`
|
|
81
|
+
const cachedHighlights = useAppStore((s) => s.gitMode.highlights[highlightKey])
|
|
82
|
+
|
|
83
|
+
const [preparing, setPreparing] = useState(false)
|
|
84
|
+
const [localParsed, setLocalParsed] = useState<PreparedParsedDiff | null>(null)
|
|
85
|
+
const [localHighlights, setLocalHighlights] = useState(EMPTY_HIGHLIGHTS)
|
|
86
|
+
const [localHash, setLocalHash] = useState<string | null>(null)
|
|
87
|
+
const completedSegmentsRef = useRef(new Set<string>())
|
|
88
|
+
const currentOwnerRef = useRef(requestOwner)
|
|
89
|
+
const pendingSegmentsRef = useRef(new Set<string>())
|
|
90
|
+
const requestVersionRef = useRef(0)
|
|
91
|
+
const localOwnerMatches = currentOwnerRef.current === requestOwner
|
|
92
|
+
|
|
93
|
+
const parsed = useMemo(() => {
|
|
94
|
+
return (
|
|
95
|
+
getParsedPayload<PreparedParsedDiff>(cachedParsed) ??
|
|
96
|
+
(localOwnerMatches ? localParsed : null) ??
|
|
97
|
+
EMPTY_PARSED
|
|
98
|
+
)
|
|
99
|
+
}, [cachedParsed, localOwnerMatches, localParsed])
|
|
100
|
+
|
|
101
|
+
const highlights = useMemo(() => {
|
|
102
|
+
const cached = getHighlightsTokens(cachedHighlights)
|
|
103
|
+
if (cached && cachedHighlights?.hash === cachedParsed?.hash) return cached
|
|
104
|
+
return localOwnerMatches ? localHighlights : EMPTY_HIGHLIGHTS
|
|
105
|
+
}, [cachedHighlights, cachedParsed, localHighlights, localOwnerMatches])
|
|
106
|
+
|
|
107
|
+
const ready = !!parsed.file
|
|
108
|
+
|
|
109
|
+
useLayoutEffect(() => {
|
|
110
|
+
currentOwnerRef.current = requestOwner
|
|
111
|
+
requestVersionRef.current += 1
|
|
112
|
+
completedSegmentsRef.current.clear()
|
|
113
|
+
pendingSegmentsRef.current.clear()
|
|
114
|
+
setLocalParsed(null)
|
|
115
|
+
setLocalHighlights(EMPTY_HIGHLIGHTS)
|
|
116
|
+
setLocalHash(null)
|
|
117
|
+
setPreparing(false)
|
|
118
|
+
}, [cacheKey, diff, path, requestOwner, themeId])
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (cachedParsed) return
|
|
122
|
+
|
|
123
|
+
const controller = new AbortController()
|
|
124
|
+
const owner = requestOwner
|
|
125
|
+
const version = requestVersionRef.current
|
|
126
|
+
setPreparing(true)
|
|
127
|
+
void prepareDiff(diff, path, { signal: controller.signal })
|
|
128
|
+
.then((result) => {
|
|
129
|
+
if (
|
|
130
|
+
controller.signal.aborted ||
|
|
131
|
+
requestVersionRef.current !== version ||
|
|
132
|
+
currentOwnerRef.current !== owner
|
|
133
|
+
) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
setLocalParsed(result.parsed)
|
|
137
|
+
setLocalHighlights(result.highlights)
|
|
138
|
+
setLocalHash(result.hash)
|
|
139
|
+
dispatchGlobal({
|
|
140
|
+
file: result.parsed,
|
|
141
|
+
hash: result.hash,
|
|
142
|
+
key: cacheKey,
|
|
143
|
+
type: 'git-mode-set-parsed',
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {})
|
|
147
|
+
.finally(() => {
|
|
148
|
+
if (
|
|
149
|
+
!controller.signal.aborted &&
|
|
150
|
+
requestVersionRef.current === version &&
|
|
151
|
+
currentOwnerRef.current === owner
|
|
152
|
+
) {
|
|
153
|
+
setPreparing(false)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return () => {
|
|
158
|
+
controller.abort()
|
|
159
|
+
}
|
|
160
|
+
}, [cacheKey, cachedParsed, diff, path, requestOwner, themeId])
|
|
161
|
+
|
|
162
|
+
const requestSegmentHighlights = useCallback(
|
|
163
|
+
(segments: readonly DiffSegment[]) => {
|
|
164
|
+
const file = parsed.file
|
|
165
|
+
const filetype = parsed.filetype
|
|
166
|
+
if (!file || !filetype) return
|
|
167
|
+
const owner = requestOwner
|
|
168
|
+
const version = requestVersionRef.current
|
|
169
|
+
const activeHash = cachedParsed?.hash ?? (localOwnerMatches ? localHash : null)
|
|
170
|
+
if (!activeHash) return
|
|
171
|
+
const next = segments.filter(
|
|
172
|
+
(segment) =>
|
|
173
|
+
(segment.kind === 'context' || segment.kind === 'change') &&
|
|
174
|
+
!completedSegmentsRef.current.has(segment.id) &&
|
|
175
|
+
!pendingSegmentsRef.current.has(segment.id) &&
|
|
176
|
+
!segmentHasHighlights(segment, highlights)
|
|
177
|
+
)
|
|
178
|
+
const queue = next.slice(0, MAX_HIGHLIGHT_SEGMENTS_PER_PASS)
|
|
179
|
+
if (queue.length === 0) return
|
|
180
|
+
for (const segment of queue) pendingSegmentsRef.current.add(segment.id)
|
|
181
|
+
void (async () => {
|
|
182
|
+
const add: HighlightPatch[] = []
|
|
183
|
+
const del: HighlightPatch[] = []
|
|
184
|
+
try {
|
|
185
|
+
for (const segment of queue) {
|
|
186
|
+
if (requestVersionRef.current !== version || currentOwnerRef.current !== owner) return
|
|
187
|
+
const patch = await tokenizeSegment(file, segment, filetype)
|
|
188
|
+
add.push(...patch.add)
|
|
189
|
+
del.push(...patch.del)
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
if (requestVersionRef.current === version && currentOwnerRef.current === owner) {
|
|
193
|
+
for (const segment of queue) {
|
|
194
|
+
pendingSegmentsRef.current.delete(segment.id)
|
|
195
|
+
completedSegmentsRef.current.add(segment.id)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (
|
|
200
|
+
requestVersionRef.current !== version ||
|
|
201
|
+
currentOwnerRef.current !== owner ||
|
|
202
|
+
(add.length === 0 && del.length === 0)
|
|
203
|
+
) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
setLocalHighlights((current) => applyHighlightPatches(current, add, del))
|
|
207
|
+
dispatchGlobal({
|
|
208
|
+
add,
|
|
209
|
+
del,
|
|
210
|
+
hash: activeHash,
|
|
211
|
+
key: cacheKey,
|
|
212
|
+
themeId,
|
|
213
|
+
type: 'git-mode-merge-highlights',
|
|
214
|
+
})
|
|
215
|
+
})()
|
|
216
|
+
},
|
|
217
|
+
[
|
|
218
|
+
cacheKey,
|
|
219
|
+
cachedParsed?.hash,
|
|
220
|
+
highlights,
|
|
221
|
+
localHash,
|
|
222
|
+
localOwnerMatches,
|
|
223
|
+
parsed.file,
|
|
224
|
+
parsed.filetype,
|
|
225
|
+
requestOwner,
|
|
226
|
+
themeId,
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
file: parsed.file,
|
|
232
|
+
filetype: parsed.filetype,
|
|
233
|
+
firstChangeOffset: parsed.firstChangeOffset,
|
|
234
|
+
highlights,
|
|
235
|
+
preparing,
|
|
236
|
+
ready,
|
|
237
|
+
requestSegmentHighlights,
|
|
238
|
+
segments: parsed.segments,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { DiffSegment } from './build-rows'
|
|
6
|
+
|
|
7
|
+
interface VirtualizedSegment<T extends DiffSegment> {
|
|
8
|
+
bottomSpacer: number
|
|
9
|
+
topSpacer: number
|
|
10
|
+
totalHeight: number
|
|
11
|
+
visible: T[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Options<T extends DiffSegment> {
|
|
15
|
+
estimateHeight: (segment: T) => number
|
|
16
|
+
overscan: number
|
|
17
|
+
scrollRef: React.RefObject<ScrollBoxRenderable | null>
|
|
18
|
+
segments: readonly T[]
|
|
19
|
+
version: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ViewportState {
|
|
23
|
+
height: number
|
|
24
|
+
scrollTop: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sameViewport(a: ViewportState, b: ViewportState): boolean {
|
|
28
|
+
return a.height === b.height && a.scrollTop === b.scrollTop
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useSegmentVirtualization<T extends DiffSegment>({
|
|
32
|
+
estimateHeight,
|
|
33
|
+
overscan,
|
|
34
|
+
scrollRef,
|
|
35
|
+
segments,
|
|
36
|
+
version,
|
|
37
|
+
}: Options<T>): VirtualizedSegment<T> {
|
|
38
|
+
const [viewport, setViewport] = useState<ViewportState>({ height: 0, scrollTop: 0 })
|
|
39
|
+
const estimateHeightRef = useRef(estimateHeight)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
estimateHeightRef.current = estimateHeight
|
|
43
|
+
}, [estimateHeight])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let frame = 0
|
|
47
|
+
const tick = (): void => {
|
|
48
|
+
const node = scrollRef.current
|
|
49
|
+
const next = {
|
|
50
|
+
height: Math.max(1, node?.viewport.height ?? 0),
|
|
51
|
+
scrollTop: Math.max(0, node?.scrollTop ?? 0),
|
|
52
|
+
}
|
|
53
|
+
setViewport((prev) => (sameViewport(prev, next) ? prev : next))
|
|
54
|
+
frame = requestAnimationFrame(tick)
|
|
55
|
+
}
|
|
56
|
+
frame = requestAnimationFrame(tick)
|
|
57
|
+
return () => cancelAnimationFrame(frame)
|
|
58
|
+
}, [scrollRef])
|
|
59
|
+
|
|
60
|
+
return useMemo(() => {
|
|
61
|
+
void version
|
|
62
|
+
if (segments.length === 0) {
|
|
63
|
+
return { bottomSpacer: 0, topSpacer: 0, totalHeight: 0, visible: [] }
|
|
64
|
+
}
|
|
65
|
+
const topTarget = Math.max(0, viewport.scrollTop - overscan)
|
|
66
|
+
const bottomTarget = viewport.scrollTop + viewport.height + overscan
|
|
67
|
+
let totalHeight = 0
|
|
68
|
+
let topSpacer = 0
|
|
69
|
+
let start = 0
|
|
70
|
+
while (start < segments.length) {
|
|
71
|
+
const height = estimateHeightRef.current(segments[start] as T)
|
|
72
|
+
if (totalHeight + height > topTarget) break
|
|
73
|
+
topSpacer += height
|
|
74
|
+
totalHeight += height
|
|
75
|
+
start += 1
|
|
76
|
+
}
|
|
77
|
+
let end = start
|
|
78
|
+
while (end < segments.length && totalHeight < bottomTarget) {
|
|
79
|
+
totalHeight += estimateHeightRef.current(segments[end] as T)
|
|
80
|
+
end += 1
|
|
81
|
+
}
|
|
82
|
+
let bottomSpacer = 0
|
|
83
|
+
for (let i = end; i < segments.length; i++) {
|
|
84
|
+
bottomSpacer += estimateHeightRef.current(segments[i] as T)
|
|
85
|
+
}
|
|
86
|
+
totalHeight = topSpacer + bottomSpacer
|
|
87
|
+
for (let i = start; i < end; i++) totalHeight += estimateHeightRef.current(segments[i] as T)
|
|
88
|
+
return {
|
|
89
|
+
bottomSpacer,
|
|
90
|
+
topSpacer,
|
|
91
|
+
totalHeight,
|
|
92
|
+
visible: segments.slice(start, end) as T[],
|
|
93
|
+
}
|
|
94
|
+
}, [overscan, segments, version, viewport])
|
|
95
|
+
}
|
|
@@ -9,11 +9,16 @@ import type {
|
|
|
9
9
|
GitPaneDiffCountConfig,
|
|
10
10
|
GitPanelState,
|
|
11
11
|
GitPanePathConfig,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../../../state/types'
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { useAppStore } from '../../../state/app-store'
|
|
15
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
16
|
+
import {
|
|
17
|
+
buildGitTreeRows,
|
|
18
|
+
type GitTreeFileRow,
|
|
19
|
+
type GitTreeFolderRow,
|
|
20
|
+
} from '../../../state/git-tree'
|
|
21
|
+
import { getCurrentTokens, getTransparent, useTokens, useTransparent } from '../../theme'
|
|
17
22
|
|
|
18
23
|
interface GitPanelProps {
|
|
19
24
|
collapsedFolders?: Record<string, true>
|
|
@@ -185,14 +190,20 @@ function renderFileRow(
|
|
|
185
190
|
isSelected: boolean,
|
|
186
191
|
fileListMode: GitFileListMode,
|
|
187
192
|
pathConfig: GitPanePathConfig,
|
|
188
|
-
diffCountConfig: GitPaneDiffCountConfig
|
|
193
|
+
diffCountConfig: GitPaneDiffCountConfig,
|
|
194
|
+
repoPrefixes: Record<string, string>
|
|
189
195
|
): ReactNode {
|
|
196
|
+
const t = getCurrentTokens()
|
|
190
197
|
const file = row.file
|
|
191
198
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
192
|
-
const bg = isSelected && !getTransparent() ?
|
|
199
|
+
const bg = isSelected && !getTransparent() ? t.selected : undefined
|
|
193
200
|
const onSelect = (): void => {
|
|
194
201
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
195
202
|
}
|
|
203
|
+
// Repo disambiguation prefix: only in flat mode, only when the file came
|
|
204
|
+
// from a sub-repo (root repo files get an empty prefix).
|
|
205
|
+
const repoTag =
|
|
206
|
+
fileListMode === 'flat' && file.repoPath ? (repoPrefixes[file.repoPath] ?? '') : ''
|
|
196
207
|
return (
|
|
197
208
|
<box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
198
209
|
<box width={2} flexShrink={0} justifyContent="center">
|
|
@@ -200,6 +211,13 @@ function renderFileRow(
|
|
|
200
211
|
<strong>{displayStatus(file)}</strong>
|
|
201
212
|
</text>
|
|
202
213
|
</box>
|
|
214
|
+
{repoTag ? (
|
|
215
|
+
<box flexShrink={0}>
|
|
216
|
+
<text fg={t.palette.primary} bg={bg}>
|
|
217
|
+
<strong>{repoTag}</strong>
|
|
218
|
+
</text>
|
|
219
|
+
</box>
|
|
220
|
+
) : null}
|
|
203
221
|
<box flexGrow={1} overflow="hidden" paddingLeft={fileListMode === 'tree' ? row.depth * 2 : 0}>
|
|
204
222
|
{renderFileLabel(file, pathConfig, fileListMode)}
|
|
205
223
|
</box>
|
|
@@ -220,7 +238,8 @@ function renderTreeSection(
|
|
|
220
238
|
showListModeToggle: boolean,
|
|
221
239
|
pathConfig: GitPanePathConfig,
|
|
222
240
|
diffCountConfig: GitPaneDiffCountConfig,
|
|
223
|
-
marginTop: number
|
|
241
|
+
marginTop: number,
|
|
242
|
+
repoPrefixes: Record<string, string>
|
|
224
243
|
): ReactNode {
|
|
225
244
|
if (files.length === 0) return null
|
|
226
245
|
const t = getCurrentTokens()
|
|
@@ -255,7 +274,8 @@ function renderTreeSection(
|
|
|
255
274
|
row.key === selectedEntryKey,
|
|
256
275
|
fileListMode,
|
|
257
276
|
pathConfig,
|
|
258
|
-
diffCountConfig
|
|
277
|
+
diffCountConfig,
|
|
278
|
+
repoPrefixes
|
|
259
279
|
)
|
|
260
280
|
)}
|
|
261
281
|
</box>
|
|
@@ -313,6 +333,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
313
333
|
}: GitPanelProps) {
|
|
314
334
|
const t = useTokens()
|
|
315
335
|
useTransparent()
|
|
336
|
+
const repoPrefixes = useAppStore((s) => s.multiRepo.prefixes)
|
|
316
337
|
const sectionOrder = headOffset > 0 ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
|
|
317
338
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
318
339
|
const tree = useMemo(
|
|
@@ -344,7 +365,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
344
365
|
}, [selectedEntryKey, tree.visibleRows])
|
|
345
366
|
|
|
346
367
|
return (
|
|
347
|
-
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
368
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden" gap={0}>
|
|
348
369
|
{hasRemoteTracking ? (
|
|
349
370
|
<text fg={t.muted}>
|
|
350
371
|
↑{gitPanel.ahead} ↓{gitPanel.behind}
|
|
@@ -376,7 +397,8 @@ export const GitPanel = memo(function GitPanel({
|
|
|
376
397
|
key === toggleSection,
|
|
377
398
|
pathConfig,
|
|
378
399
|
diffCountConfig,
|
|
379
|
-
priorHasFiles ? 1 : 0
|
|
400
|
+
priorHasFiles ? 1 : 0,
|
|
401
|
+
repoPrefixes
|
|
380
402
|
)
|
|
381
403
|
})}
|
|
382
404
|
</scrollbox>
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { useTerminalDimensions } from '@opentui/react'
|
|
2
2
|
import { memo, useEffect, useRef } from 'react'
|
|
3
3
|
|
|
4
|
-
import type { DiffData, GitDiffView } from '
|
|
5
|
-
import type { ThemeId } from '
|
|
4
|
+
import type { DiffData, GitDiffView } from '../../../state/types'
|
|
5
|
+
import type { ThemeId } from '../../themes'
|
|
6
6
|
|
|
7
|
-
import { diffHash } from '
|
|
8
|
-
import { fetchDiff } from '
|
|
9
|
-
import { useGitPanelPolling } from '
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
7
|
+
import { diffHash } from '../../../git/diff-hash'
|
|
8
|
+
import { fetchDiff } from '../../../git/git-diff'
|
|
9
|
+
import { useGitPanelPolling } from '../../../git/git-poller'
|
|
10
|
+
import { useRepoDiscovery } from '../../../git/use-repo-discovery'
|
|
11
|
+
import { useAppStore } from '../../../state/app-store'
|
|
12
|
+
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
13
|
+
import { getSelectedGitFile, gitFileKey } from '../../../state/git-tree'
|
|
14
|
+
import { setGitDiffScroller } from '../../git-view-controls'
|
|
15
|
+
import { useBg, useTokens } from '../../theme'
|
|
15
16
|
import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
16
17
|
import { useDiffPrefetch } from './diff-renderer/use-diff-prefetch'
|
|
17
18
|
import { GitPanel } from './git-panel'
|
|
@@ -107,6 +108,7 @@ interface GitViewProps {
|
|
|
107
108
|
export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
108
109
|
const t = useTokens()
|
|
109
110
|
const sidebarBg = useBg('elevated')
|
|
111
|
+
const actionBg = useBg('selected')
|
|
110
112
|
const dimensions = useTerminalDimensions()
|
|
111
113
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
112
114
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
@@ -121,6 +123,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
121
123
|
: undefined
|
|
122
124
|
const projectPath = currentSession?.projectPath
|
|
123
125
|
|
|
126
|
+
useRepoDiscovery(projectPath)
|
|
124
127
|
useGitPanelPolling({ enabled: focusMode === 'git', headOffset: gitMode.headOffset, projectPath })
|
|
125
128
|
|
|
126
129
|
const fileBarWidth = Math.max(20, Math.floor(dimensions.width * gitPane.diffModeRatio))
|
|
@@ -164,7 +167,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
164
167
|
if (!selectedDiffKey) return
|
|
165
168
|
if (diff || loading) return
|
|
166
169
|
dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
|
|
167
|
-
void fetchDiff(projectPath, selectedFile, gitMode.headOffset)
|
|
170
|
+
void fetchDiff(selectedFile.repoPath ?? projectPath, selectedFile, gitMode.headOffset)
|
|
168
171
|
.then((d) =>
|
|
169
172
|
dispatchGlobal({
|
|
170
173
|
diff: d,
|
|
@@ -206,7 +209,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
return (
|
|
209
|
-
<box flexDirection="column" flexGrow={1}>
|
|
212
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
210
213
|
<box flexDirection="row" flexGrow={1}>
|
|
211
214
|
<box
|
|
212
215
|
width={fileBarWidth}
|
|
@@ -214,25 +217,28 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
214
217
|
backgroundColor={sidebarBg}
|
|
215
218
|
padding={0}
|
|
216
219
|
gap={0}
|
|
220
|
+
overflow="hidden"
|
|
217
221
|
>
|
|
218
|
-
<
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
222
|
+
<box flexDirection="column" flexShrink={0}>
|
|
223
|
+
<text fg={t.palette.primary}>
|
|
224
|
+
<strong>aimux · git</strong>
|
|
225
|
+
</text>
|
|
226
|
+
{gitPanel.branch ? (
|
|
227
|
+
<box flexDirection="row">
|
|
228
|
+
<text fg={t.palette.primary}>{'\u{e702}'} </text>
|
|
229
|
+
<text fg={t.muted}>{gitPanel.branch}</text>
|
|
230
|
+
</box>
|
|
231
|
+
) : null}
|
|
232
|
+
{gitMode.headOffset > 0 ? (
|
|
233
|
+
<box flexDirection="row" gap={1}>
|
|
234
|
+
<text fg={t.palette.warning}>
|
|
235
|
+
<strong>HEAD~{gitMode.headOffset}</strong>
|
|
236
|
+
</text>
|
|
237
|
+
<text fg={t.muted}>[ newer · ] older</text>
|
|
238
|
+
</box>
|
|
239
|
+
) : null}
|
|
240
|
+
<text fg={t.hover}>{'·'.repeat(Math.max(0, fileBarWidth - 2))}</text>
|
|
241
|
+
</box>
|
|
236
242
|
<GitPanel
|
|
237
243
|
collapsedFolders={gitMode.collapsedFolders}
|
|
238
244
|
compact={gitPane.treeCompaction}
|
|
@@ -243,14 +249,43 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
243
249
|
selectedEntryKey={gitMode.selectedEntryKey}
|
|
244
250
|
/>
|
|
245
251
|
</box>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
253
|
+
<box
|
|
254
|
+
flexDirection="row"
|
|
255
|
+
alignItems="center"
|
|
256
|
+
paddingLeft={1}
|
|
257
|
+
paddingRight={1}
|
|
258
|
+
backgroundColor={sidebarBg}
|
|
259
|
+
>
|
|
260
|
+
<box flexGrow={1}>
|
|
261
|
+
<text fg={selectedFile ? t.palette.ink : t.muted}>
|
|
262
|
+
{selectedFile ? selectedFile.path : 'Diff'}
|
|
263
|
+
</text>
|
|
264
|
+
</box>
|
|
265
|
+
<box
|
|
266
|
+
paddingLeft={1}
|
|
267
|
+
paddingRight={1}
|
|
268
|
+
backgroundColor={actionBg}
|
|
269
|
+
onMouseDown={(event) => {
|
|
270
|
+
event.preventDefault()
|
|
271
|
+
event.stopPropagation()
|
|
272
|
+
dispatchGlobal({ type: 'exit-git-mode' })
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
<text fg={t.palette.ink}>
|
|
276
|
+
<strong>Exit diff</strong>
|
|
277
|
+
</text>
|
|
278
|
+
</box>
|
|
279
|
+
</box>
|
|
280
|
+
<DiffStage
|
|
281
|
+
diff={diff}
|
|
282
|
+
diffKey={selectedDiffKey}
|
|
283
|
+
diffRef={diffRef}
|
|
284
|
+
loading={loading}
|
|
285
|
+
themeId={themeId}
|
|
286
|
+
view={gitMode.diffView}
|
|
287
|
+
/>
|
|
288
|
+
</box>
|
|
254
289
|
</box>
|
|
255
290
|
{footerNode ? (
|
|
256
291
|
<box paddingLeft={1} paddingRight={1} backgroundColor={sidebarBg} flexDirection="column">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { GitPaneMode, GitPanePosition, GitPaneState } from '
|
|
2
|
-
import type { ContextMenuItem } from '
|
|
1
|
+
import type { GitPaneMode, GitPanePosition, GitPaneState } from '../../../../state/types'
|
|
2
|
+
import type { ContextMenuItem } from '../../../context-menu/controller'
|
|
3
3
|
|
|
4
4
|
const GIT_PANE_MENU_OPTIONS: Array<{
|
|
5
5
|
label: string
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { memo, useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
import type { GitPanelState } from '
|
|
3
|
+
import type { GitPanelState } from '../../../../state/types'
|
|
4
4
|
|
|
5
|
-
import { useGitPanelPolling } from '
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { useGitPanelPolling } from '../../../../git/git-poller'
|
|
6
|
+
import { useRepoDiscovery } from '../../../../git/use-repo-discovery'
|
|
7
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
8
|
+
import { GitPanel } from '../git-panel'
|
|
8
9
|
|
|
9
10
|
interface GitPaneWidgetProps {
|
|
10
11
|
pollingEnabled: boolean
|
|
@@ -24,6 +25,7 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
|
|
|
24
25
|
: undefined
|
|
25
26
|
const projectPath = currentSession?.projectPath
|
|
26
27
|
|
|
28
|
+
useRepoDiscovery(projectPath)
|
|
27
29
|
useGitPanelPolling({ enabled: pollingEnabled, headOffset: 0, projectPath })
|
|
28
30
|
|
|
29
31
|
const lastGoodRef = useRef<GitPanelState | null>(null)
|