@brimveyn/aimux 1.9.0 → 1.9.1
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 +6 -1
- package/src/app.tsx +20 -16
- 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/types.ts +11 -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 +8 -17
- 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} +9 -5
- package/src/ui/components/{git-view.tsx → git/git-view.tsx} +70 -37
- 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} +4 -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,15 @@ import type {
|
|
|
9
9
|
GitPaneDiffCountConfig,
|
|
10
10
|
GitPanelState,
|
|
11
11
|
GitPanePathConfig,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../../../state/types'
|
|
13
13
|
|
|
14
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
15
|
-
import {
|
|
16
|
-
|
|
14
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
15
|
+
import {
|
|
16
|
+
buildGitTreeRows,
|
|
17
|
+
type GitTreeFileRow,
|
|
18
|
+
type GitTreeFolderRow,
|
|
19
|
+
} from '../../../state/git-tree'
|
|
20
|
+
import { getCurrentTokens, getTransparent, useTokens, useTransparent } from '../../theme'
|
|
17
21
|
|
|
18
22
|
interface GitPanelProps {
|
|
19
23
|
collapsedFolders?: Record<string, true>
|
|
@@ -344,7 +348,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
344
348
|
}, [selectedEntryKey, tree.visibleRows])
|
|
345
349
|
|
|
346
350
|
return (
|
|
347
|
-
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
351
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden" gap={0}>
|
|
348
352
|
{hasRemoteTracking ? (
|
|
349
353
|
<text fg={t.muted}>
|
|
350
354
|
↑{gitPanel.ahead} ↓{gitPanel.behind}
|
|
@@ -1,17 +1,17 @@
|
|
|
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 { useAppStore } from '
|
|
11
|
-
import { dispatchGlobal } from '
|
|
12
|
-
import { getSelectedGitFile, gitFileKey } from '
|
|
13
|
-
import { setGitDiffScroller } from '
|
|
14
|
-
import { useBg, useTokens } from '
|
|
7
|
+
import { diffHash } from '../../../git/diff-hash'
|
|
8
|
+
import { fetchDiff } from '../../../git/git-diff'
|
|
9
|
+
import { useGitPanelPolling } from '../../../git/git-poller'
|
|
10
|
+
import { useAppStore } from '../../../state/app-store'
|
|
11
|
+
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
12
|
+
import { getSelectedGitFile, gitFileKey } from '../../../state/git-tree'
|
|
13
|
+
import { setGitDiffScroller } from '../../git-view-controls'
|
|
14
|
+
import { useBg, useTokens } from '../../theme'
|
|
15
15
|
import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
16
16
|
import { useDiffPrefetch } from './diff-renderer/use-diff-prefetch'
|
|
17
17
|
import { GitPanel } from './git-panel'
|
|
@@ -107,6 +107,7 @@ interface GitViewProps {
|
|
|
107
107
|
export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
108
108
|
const t = useTokens()
|
|
109
109
|
const sidebarBg = useBg('elevated')
|
|
110
|
+
const actionBg = useBg('selected')
|
|
110
111
|
const dimensions = useTerminalDimensions()
|
|
111
112
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
112
113
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
@@ -206,7 +207,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
return (
|
|
209
|
-
<box flexDirection="column" flexGrow={1}>
|
|
210
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
210
211
|
<box flexDirection="row" flexGrow={1}>
|
|
211
212
|
<box
|
|
212
213
|
width={fileBarWidth}
|
|
@@ -214,25 +215,28 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
214
215
|
backgroundColor={sidebarBg}
|
|
215
216
|
padding={0}
|
|
216
217
|
gap={0}
|
|
218
|
+
overflow="hidden"
|
|
217
219
|
>
|
|
218
|
-
<
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
<
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
220
|
+
<box flexDirection="column" flexShrink={0}>
|
|
221
|
+
<text fg={t.palette.primary}>
|
|
222
|
+
<strong>aimux · git</strong>
|
|
223
|
+
</text>
|
|
224
|
+
{gitPanel.branch ? (
|
|
225
|
+
<box flexDirection="row">
|
|
226
|
+
<text fg={t.palette.primary}>{'\u{e702}'} </text>
|
|
227
|
+
<text fg={t.muted}>{gitPanel.branch}</text>
|
|
228
|
+
</box>
|
|
229
|
+
) : null}
|
|
230
|
+
{gitMode.headOffset > 0 ? (
|
|
231
|
+
<box flexDirection="row" gap={1}>
|
|
232
|
+
<text fg={t.palette.warning}>
|
|
233
|
+
<strong>HEAD~{gitMode.headOffset}</strong>
|
|
234
|
+
</text>
|
|
235
|
+
<text fg={t.muted}>[ newer · ] older</text>
|
|
236
|
+
</box>
|
|
237
|
+
) : null}
|
|
238
|
+
<text fg={t.hover}>{'·'.repeat(Math.max(0, fileBarWidth - 2))}</text>
|
|
239
|
+
</box>
|
|
236
240
|
<GitPanel
|
|
237
241
|
collapsedFolders={gitMode.collapsedFolders}
|
|
238
242
|
compact={gitPane.treeCompaction}
|
|
@@ -243,14 +247,43 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
243
247
|
selectedEntryKey={gitMode.selectedEntryKey}
|
|
244
248
|
/>
|
|
245
249
|
</box>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
251
|
+
<box
|
|
252
|
+
flexDirection="row"
|
|
253
|
+
alignItems="center"
|
|
254
|
+
paddingLeft={1}
|
|
255
|
+
paddingRight={1}
|
|
256
|
+
backgroundColor={sidebarBg}
|
|
257
|
+
>
|
|
258
|
+
<box flexGrow={1}>
|
|
259
|
+
<text fg={selectedFile ? t.palette.ink : t.muted}>
|
|
260
|
+
{selectedFile ? selectedFile.path : 'Diff'}
|
|
261
|
+
</text>
|
|
262
|
+
</box>
|
|
263
|
+
<box
|
|
264
|
+
paddingLeft={1}
|
|
265
|
+
paddingRight={1}
|
|
266
|
+
backgroundColor={actionBg}
|
|
267
|
+
onMouseDown={(event) => {
|
|
268
|
+
event.preventDefault()
|
|
269
|
+
event.stopPropagation()
|
|
270
|
+
dispatchGlobal({ type: 'exit-git-mode' })
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
<text fg={t.palette.ink}>
|
|
274
|
+
<strong>Exit diff</strong>
|
|
275
|
+
</text>
|
|
276
|
+
</box>
|
|
277
|
+
</box>
|
|
278
|
+
<DiffStage
|
|
279
|
+
diff={diff}
|
|
280
|
+
diffKey={selectedDiffKey}
|
|
281
|
+
diffRef={diffRef}
|
|
282
|
+
loading={loading}
|
|
283
|
+
themeId={themeId}
|
|
284
|
+
view={gitMode.diffView}
|
|
285
|
+
/>
|
|
286
|
+
</box>
|
|
254
287
|
</box>
|
|
255
288
|
{footerNode ? (
|
|
256
289
|
<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,10 @@
|
|
|
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 { useAppStore } from '
|
|
7
|
-
import { GitPanel } from '
|
|
5
|
+
import { useGitPanelPolling } from '../../../../git/git-poller'
|
|
6
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
7
|
+
import { GitPanel } from '../git-panel'
|
|
8
8
|
|
|
9
9
|
interface GitPaneWidgetProps {
|
|
10
10
|
pollingEnabled: boolean
|
|
@@ -2,18 +2,22 @@ import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useRef, useState } from 'react'
|
|
4
4
|
|
|
5
|
-
import type { SessionRecord, SessionStatus } from '
|
|
5
|
+
import type { SessionRecord, SessionStatus } from '../../../state/types'
|
|
6
6
|
|
|
7
|
-
import { useAppStore } from '
|
|
8
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
7
|
+
import { useAppStore } from '../../../state/app-store'
|
|
8
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
9
9
|
// eslint-disable-next-line no-duplicate-imports
|
|
10
|
-
import { IDLE_SESSION_STATUS } from '
|
|
11
|
-
import { useBusySpinner } from '
|
|
12
|
-
import { moveIdToIdPosition, orderSessionsForDisplay } from '
|
|
13
|
-
import { useBg, useTokens } from '
|
|
14
|
-
import { ContextMenuBox } from '
|
|
10
|
+
import { IDLE_SESSION_STATUS } from '../../../state/types'
|
|
11
|
+
import { useBusySpinner } from '../../hooks/use-busy-spinner'
|
|
12
|
+
import { moveIdToIdPosition, orderSessionsForDisplay } from '../../session-ordering'
|
|
13
|
+
import { useBg, useTokens } from '../../theme'
|
|
14
|
+
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface SessionBarProps {
|
|
17
|
+
forceVisible?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SessionBar({ forceVisible = false }: SessionBarProps) {
|
|
17
21
|
const t = useTokens()
|
|
18
22
|
const headerBg = useBg('elevated')
|
|
19
23
|
const sessions = useAppStore((s) => s.sessions)
|
|
@@ -27,7 +31,7 @@ export function SessionBar() {
|
|
|
27
31
|
const chipRefs = useRef(new Map<string, BoxRenderable>())
|
|
28
32
|
|
|
29
33
|
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
30
|
-
if (!bar.visible || ordered.length === 0) return null
|
|
34
|
+
if ((!bar.visible && !forceVisible) || ordered.length === 0) return null
|
|
31
35
|
|
|
32
36
|
const visibleSessions =
|
|
33
37
|
dragOrder !== null
|
|
@@ -103,6 +107,7 @@ export function SessionBar() {
|
|
|
103
107
|
<box
|
|
104
108
|
width="100%"
|
|
105
109
|
flexDirection="row"
|
|
110
|
+
flexShrink={0}
|
|
106
111
|
paddingLeft={1}
|
|
107
112
|
paddingRight={1}
|
|
108
113
|
backgroundColor={headerBg}
|
|
@@ -205,6 +210,7 @@ function SessionChip({
|
|
|
205
210
|
const idleColor = t.palette.success
|
|
206
211
|
const workingColor = t.palette.primary
|
|
207
212
|
const waitingColor = t.palette.warning
|
|
213
|
+
const [hovered, setHovered] = useState(false)
|
|
208
214
|
|
|
209
215
|
return (
|
|
210
216
|
<ContextMenuBox
|
|
@@ -214,6 +220,8 @@ function SessionChip({
|
|
|
214
220
|
paddingRight={1}
|
|
215
221
|
backgroundColor={bgColor}
|
|
216
222
|
rightClickMenu={rightClickMenu}
|
|
223
|
+
onMouseOver={() => setHovered(true)}
|
|
224
|
+
onMouseOut={() => setHovered(false)}
|
|
217
225
|
onMouseDown={(e) => {
|
|
218
226
|
e.preventDefault()
|
|
219
227
|
onMouseDown(e)
|
|
@@ -247,9 +255,24 @@ function SessionChip({
|
|
|
247
255
|
<text fg={labelColor} selectable={false}>
|
|
248
256
|
[{index}] {session.name}
|
|
249
257
|
</text>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
{hovered ? (
|
|
259
|
+
<box
|
|
260
|
+
paddingLeft={1}
|
|
261
|
+
onMouseDown={(event) => {
|
|
262
|
+
event.preventDefault()
|
|
263
|
+
event.stopPropagation()
|
|
264
|
+
runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' })
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<text fg={t.muted} selectable={false}>
|
|
268
|
+
×
|
|
269
|
+
</text>
|
|
270
|
+
</box>
|
|
271
|
+
) : (
|
|
272
|
+
<text fg={t.hover} selectable={false}>
|
|
273
|
+
{' '}
|
|
274
|
+
</text>
|
|
275
|
+
)}
|
|
253
276
|
</ContextMenuBox>
|
|
254
277
|
)
|
|
255
278
|
}
|
package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { TabSession } from '
|
|
1
|
+
import type { TabSession } from '../../../../state/types'
|
|
2
2
|
|
|
3
|
-
import { allLeafIds, type LayoutNode } from '
|
|
3
|
+
import { allLeafIds, type LayoutNode } from '../../../../state/layout-tree'
|
|
4
4
|
|
|
5
5
|
export interface TabGroupInfo {
|
|
6
6
|
inLayout: boolean
|
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
} from '@opentui/core'
|
|
6
6
|
import { memo, useMemo, useRef } from 'react'
|
|
7
7
|
|
|
8
|
-
import { useAppStore } from '
|
|
9
|
-
import { dispatchGlobal } from '
|
|
10
|
-
import { getCurrentTokens, type ThemeTokens, useBg, useTokens } from '
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
8
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
9
|
+
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
10
|
+
import { getCurrentTokens, type ThemeTokens, useBg, useTokens } from '../../../theme'
|
|
11
|
+
import { buildGitPaneContextMenu } from '../../git/pane/git-pane-context-menu'
|
|
12
|
+
import { GitPaneWidget } from '../../git/pane/git-pane-widget'
|
|
13
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
14
14
|
import { buildTabGroupInfo } from './sidebar-group-metadata'
|
|
15
15
|
import { TabItem } from './tab-item'
|
|
16
16
|
import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import type { TabSession } from '
|
|
3
|
+
import type { TabSession } from '../../../../state/types'
|
|
4
4
|
|
|
5
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
6
|
-
import { useBusySpinner } from '
|
|
7
|
-
import { getCurrentTokens, useTokens } from '
|
|
8
|
-
import { ContextMenuBox } from '
|
|
5
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
|
+
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
7
|
+
import { getCurrentTokens, useTokens } from '../../../theme'
|
|
8
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
9
9
|
|
|
10
10
|
interface TabItemProps {
|
|
11
11
|
id?: string
|