@brimveyn/aimux 1.5.4 → 1.6.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/README.md +14 -2
- package/package.json +5 -3
- package/src/app-runtime/side-effects.ts +41 -6
- package/src/app.tsx +17 -5
- package/src/config.ts +29 -4
- package/src/diff-parser/clean-last-newline.ts +5 -0
- package/src/diff-parser/constants.ts +13 -0
- package/src/diff-parser/index.ts +12 -0
- package/src/diff-parser/parse-line-type.ts +29 -0
- package/src/diff-parser/parse-patch-files.ts +369 -0
- package/src/diff-parser/types.ts +75 -0
- package/src/git/git-diff.ts +24 -12
- package/src/git/git-poller.ts +11 -3
- package/src/git/git-status.ts +98 -17
- package/src/input/modes/bridge.ts +8 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +4 -1
- package/src/pty/terminal-snapshot.ts +5 -5
- package/src/state/git-tree.ts +220 -0
- package/src/state/reducers/git-mode-state.ts +276 -36
- package/src/state/reducers/git-panel-state.ts +35 -4
- package/src/state/reducers/modal-state.ts +43 -10
- package/src/state/store.ts +5 -1
- package/src/state/types.ts +46 -6
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/create-session-modal.tsx +25 -8
- package/src/ui/components/diff-renderer/build-rows.ts +349 -0
- package/src/ui/components/diff-renderer/filetype.ts +29 -0
- package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
- package/src/ui/components/diff-renderer/highlight.ts +48 -0
- package/src/ui/components/diff-renderer/index.ts +1 -0
- package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
- package/src/ui/components/diff-renderer/split-view.tsx +211 -0
- package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
- package/src/ui/components/git-commit-modal.tsx +16 -3
- package/src/ui/components/git-pane-widget.tsx +6 -1
- package/src/ui/components/git-panel.tsx +215 -86
- package/src/ui/components/git-view.tsx +103 -90
- package/src/ui/components/help-modal.tsx +45 -12
- package/src/ui/components/input-field.tsx +4 -3
- package/src/ui/components/list-item.tsx +11 -2
- package/src/ui/components/modal-filter-bar.tsx +3 -2
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
- package/src/ui/components/modal-shell.tsx +5 -4
- package/src/ui/components/new-tab-modal.tsx +17 -4
- package/src/ui/components/pending-chord-overlay.tsx +5 -4
- package/src/ui/components/session-bar.tsx +13 -6
- package/src/ui/components/session-picker-modal.tsx +28 -10
- package/src/ui/components/sidebar.tsx +26 -11
- package/src/ui/components/snippet-editor-modal.tsx +18 -3
- package/src/ui/components/snippet-picker-modal.tsx +13 -4
- package/src/ui/components/split-layout.tsx +3 -2
- package/src/ui/components/status-bar.tsx +15 -10
- package/src/ui/components/surface.tsx +6 -6
- package/src/ui/components/tab-item.tsx +28 -17
- package/src/ui/components/terminal-pane.tsx +28 -16
- package/src/ui/components/theme-picker-modal.tsx +113 -17
- package/src/ui/components/update-available-modal.tsx +13 -2
- package/src/ui/filter-themes.ts +15 -0
- package/src/ui/keymap-context.ts +10 -2
- package/src/ui/root.tsx +29 -7
- package/src/ui/shiki.ts +49 -0
- package/src/ui/status-bar-model.ts +32 -4
- package/src/ui/theme-store.ts +36 -0
- package/src/ui/theme.ts +4 -17
- package/src/ui/themes.ts +23 -277
- package/src/ui/syntax.ts +0 -102
|
@@ -1,41 +1,62 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { memo, type ReactNode, useEffect, useMemo, useRef } from 'react'
|
|
2
4
|
|
|
3
5
|
import type {
|
|
4
6
|
GitFileEntry,
|
|
7
|
+
GitFileListMode,
|
|
5
8
|
GitFileSection,
|
|
6
9
|
GitPaneDiffCountConfig,
|
|
7
10
|
GitPanelState,
|
|
8
11
|
GitPanePathConfig,
|
|
9
12
|
} from '../../state/types'
|
|
10
13
|
|
|
11
|
-
import {
|
|
14
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
15
|
+
import { buildGitTreeRows, type GitTreeFileRow, type GitTreeFolderRow } from '../../state/git-tree'
|
|
16
|
+
import { getCurrentTheme, useTheme } from '../theme'
|
|
12
17
|
|
|
13
18
|
interface GitPanelProps {
|
|
19
|
+
collapsedFolders?: Record<string, true>
|
|
20
|
+
fileListMode?: GitFileListMode
|
|
14
21
|
gitPanel: GitPanelState
|
|
15
22
|
projectPath: string | undefined
|
|
16
|
-
|
|
23
|
+
selectedEntryKey?: string | null
|
|
17
24
|
pathConfig?: GitPanePathConfig
|
|
18
25
|
diffCountConfig?: GitPaneDiffCountConfig
|
|
26
|
+
headOffset?: number
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
function sectionTitle(section: GitFileSection, headOffset: number): string {
|
|
30
|
+
switch (section) {
|
|
31
|
+
case 'historical':
|
|
32
|
+
return `HEAD~${headOffset}`
|
|
33
|
+
case 'staged':
|
|
34
|
+
return 'Staged Changes'
|
|
35
|
+
case 'unstaged':
|
|
36
|
+
return 'Changes'
|
|
37
|
+
case 'untracked':
|
|
38
|
+
return 'Untracked'
|
|
39
|
+
}
|
|
23
40
|
}
|
|
24
41
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
{ key: 'unstaged', title: 'Changes' },
|
|
28
|
-
{ key: 'untracked', title: 'Untracked' },
|
|
29
|
-
]
|
|
42
|
+
const BASE_SECTION_ORDER: GitFileSection[] = ['staged', 'unstaged', 'untracked']
|
|
43
|
+
const HISTORICAL_SECTION_ORDER: GitFileSection[] = ['historical', 'untracked']
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
function statusColor(status: GitFileEntry['status']): string {
|
|
46
|
+
const t = getCurrentTheme()
|
|
47
|
+
switch (status) {
|
|
48
|
+
case '?':
|
|
49
|
+
case 'A':
|
|
50
|
+
return t.colors['gitDecoration.addedResourceForeground']
|
|
51
|
+
case 'C':
|
|
52
|
+
case 'R':
|
|
53
|
+
return t.colors['textLink.foreground']
|
|
54
|
+
case 'D':
|
|
55
|
+
case 'U':
|
|
56
|
+
return t.colors['editorError.foreground']
|
|
57
|
+
case 'M':
|
|
58
|
+
return t.colors['editorWarning.foreground']
|
|
59
|
+
}
|
|
39
60
|
}
|
|
40
61
|
|
|
41
62
|
function displayStatus(file: GitFileEntry): string {
|
|
@@ -43,16 +64,6 @@ function displayStatus(file: GitFileEntry): string {
|
|
|
43
64
|
return file.status
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
function groupBySection(files: GitFileEntry[]): Record<GitFileSection, GitFileEntry[]> {
|
|
47
|
-
const groups: Record<GitFileSection, GitFileEntry[]> = {
|
|
48
|
-
staged: [],
|
|
49
|
-
unstaged: [],
|
|
50
|
-
untracked: [],
|
|
51
|
-
}
|
|
52
|
-
for (const file of files) groups[file.section].push(file)
|
|
53
|
-
return groups
|
|
54
|
-
}
|
|
55
|
-
|
|
56
67
|
function maxDigitWidth(files: GitFileEntry[]): { added: number; removed: number } {
|
|
57
68
|
let addedMax = 1
|
|
58
69
|
let removedMax = 1
|
|
@@ -67,7 +78,7 @@ function padRight(value: number | null, width: number): string {
|
|
|
67
78
|
return String(value ?? 0).padStart(width, ' ')
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
|
|
81
|
+
function splitPath(path: string): { prefix: string; basename: string } {
|
|
71
82
|
const slash = path.lastIndexOf('/')
|
|
72
83
|
if (slash < 0) return { basename: path, prefix: '' }
|
|
73
84
|
return { basename: path.slice(slash + 1), prefix: path.slice(0, slash + 1) }
|
|
@@ -77,30 +88,38 @@ function stripTrailingSlash(prefix: string): string {
|
|
|
77
88
|
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
function
|
|
81
|
-
|
|
91
|
+
function renderFileLabel(
|
|
92
|
+
file: GitFileEntry,
|
|
93
|
+
pathConfig: GitPanePathConfig,
|
|
94
|
+
fileListMode: GitFileListMode
|
|
95
|
+
): ReactNode {
|
|
82
96
|
const transform = pathConfig.enabled ? pathConfig.pathFn : undefined
|
|
83
97
|
const displayPath = transform ? transform(file.path) : file.path
|
|
84
98
|
const { basename, prefix } = splitPath(displayPath)
|
|
85
99
|
const dir = stripTrailingSlash(prefix)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const renamed = splitPath(renamedDisplay)
|
|
89
|
-
const renamedDir = stripTrailingSlash(renamed.prefix)
|
|
100
|
+
const showDir = fileListMode === 'flat' && pathConfig.enabled && dir
|
|
101
|
+
if (!file.renamedFrom) {
|
|
90
102
|
return (
|
|
91
103
|
<text wrapMode="none">
|
|
92
|
-
<span fg={
|
|
93
|
-
{showDir
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
{showDir && dir ? <span fg={theme.textMuted}> {dir}</span> : null}
|
|
104
|
+
<span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
|
|
105
|
+
{showDir ? (
|
|
106
|
+
<span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span>
|
|
107
|
+
) : null}
|
|
97
108
|
</text>
|
|
98
109
|
)
|
|
99
110
|
}
|
|
111
|
+
const renamedDisplay = transform ? transform(file.renamedFrom) : file.renamedFrom
|
|
112
|
+
const renamed = splitPath(renamedDisplay)
|
|
113
|
+
const renamedDir = stripTrailingSlash(renamed.prefix)
|
|
100
114
|
return (
|
|
101
115
|
<text wrapMode="none">
|
|
102
|
-
<span fg={
|
|
103
|
-
{showDir &&
|
|
116
|
+
<span fg={getCurrentTheme().colors['descriptionForeground']}>{renamed.basename}</span>
|
|
117
|
+
{showDir && renamedDir ? (
|
|
118
|
+
<span fg={getCurrentTheme().colors['descriptionForeground']}> {renamedDir}</span>
|
|
119
|
+
) : null}
|
|
120
|
+
<span fg={getCurrentTheme().colors['descriptionForeground']}> → </span>
|
|
121
|
+
<span fg={getCurrentTheme().colors['editor.foreground']}>{basename}</span>
|
|
122
|
+
{showDir ? <span fg={getCurrentTheme().colors['descriptionForeground']}> {dir}</span> : null}
|
|
104
123
|
</text>
|
|
105
124
|
)
|
|
106
125
|
}
|
|
@@ -116,74 +135,145 @@ function renderDiffCount(
|
|
|
116
135
|
if (!diffCountConfig.enabled) return null
|
|
117
136
|
if (!hasNumstat) {
|
|
118
137
|
return (
|
|
119
|
-
<text fg={
|
|
138
|
+
<text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg} flexShrink={0}>
|
|
120
139
|
—
|
|
121
140
|
</text>
|
|
122
141
|
)
|
|
123
142
|
}
|
|
124
143
|
return (
|
|
125
144
|
<box flexDirection="row" flexShrink={0}>
|
|
126
|
-
<text
|
|
127
|
-
|
|
145
|
+
<text
|
|
146
|
+
fg={getCurrentTheme().colors['gitDecoration.addedResourceForeground']}
|
|
147
|
+
bg={bg}
|
|
148
|
+
>{`+${padRight(file.added, addedW)}`}</text>
|
|
149
|
+
<text fg={getCurrentTheme().colors['editor.lineHighlightBackground']} bg={bg}>
|
|
128
150
|
{' '}
|
|
129
151
|
</text>
|
|
130
|
-
<text
|
|
152
|
+
<text
|
|
153
|
+
fg={getCurrentTheme().colors['editorError.foreground']}
|
|
154
|
+
bg={bg}
|
|
155
|
+
>{`−${padRight(file.removed, removedW)}`}</text>
|
|
156
|
+
</box>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode {
|
|
161
|
+
const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
|
|
162
|
+
const onSelect = (): void => {
|
|
163
|
+
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
164
|
+
}
|
|
165
|
+
const onToggle = (): void => {
|
|
166
|
+
dispatchGlobal({ key: row.key, type: 'git-mode-toggle-folder' })
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
<box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
170
|
+
<box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
|
|
171
|
+
<box flexDirection="row" gap={1} onMouseDown={onToggle}>
|
|
172
|
+
<text fg={getCurrentTheme().colors['descriptionForeground']} bg={bg}>
|
|
173
|
+
{row.isCollapsed ? '▸' : '▾'}
|
|
174
|
+
</text>
|
|
175
|
+
<text fg={getCurrentTheme().colors['terminal.ansiMagenta']} bg={bg} wrapMode="none">
|
|
176
|
+
{row.name}
|
|
177
|
+
</text>
|
|
178
|
+
</box>
|
|
179
|
+
</box>
|
|
131
180
|
</box>
|
|
132
181
|
)
|
|
133
182
|
}
|
|
134
183
|
|
|
135
184
|
function renderFileRow(
|
|
136
|
-
|
|
137
|
-
key: string,
|
|
185
|
+
row: GitTreeFileRow,
|
|
138
186
|
addedW: number,
|
|
139
187
|
removedW: number,
|
|
140
188
|
isSelected: boolean,
|
|
189
|
+
fileListMode: GitFileListMode,
|
|
141
190
|
pathConfig: GitPanePathConfig,
|
|
142
191
|
diffCountConfig: GitPaneDiffCountConfig
|
|
143
192
|
): ReactNode {
|
|
193
|
+
const file = row.file
|
|
144
194
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
145
|
-
const bg = isSelected ?
|
|
195
|
+
const bg = isSelected ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined
|
|
196
|
+
const onSelect = (): void => {
|
|
197
|
+
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
198
|
+
}
|
|
146
199
|
return (
|
|
147
|
-
<box key={key} flexDirection="row" gap={1}
|
|
148
|
-
<
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
200
|
+
<box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
201
|
+
<box width={2} flexShrink={0} justifyContent="center">
|
|
202
|
+
<text fg={statusColor(file.status)} bg={bg}>
|
|
203
|
+
<strong>{displayStatus(file)}</strong>
|
|
204
|
+
</text>
|
|
205
|
+
</box>
|
|
206
|
+
<box flexGrow={1} overflow="hidden" paddingLeft={fileListMode === 'tree' ? row.depth * 2 : 0}>
|
|
207
|
+
{renderFileLabel(file, pathConfig, fileListMode)}
|
|
153
208
|
</box>
|
|
154
209
|
{renderDiffCount(file, addedW, removedW, bg, diffCountConfig, hasNumstat)}
|
|
155
210
|
</box>
|
|
156
211
|
)
|
|
157
212
|
}
|
|
158
213
|
|
|
159
|
-
function
|
|
214
|
+
function renderTreeSection(
|
|
160
215
|
section: GitFileSection,
|
|
161
216
|
title: string,
|
|
162
217
|
files: GitFileEntry[],
|
|
218
|
+
rows: Array<GitTreeFolderRow | GitTreeFileRow>,
|
|
163
219
|
addedW: number,
|
|
164
220
|
removedW: number,
|
|
165
|
-
|
|
221
|
+
fileListMode: GitFileListMode,
|
|
222
|
+
selectedEntryKey: string | null | undefined,
|
|
223
|
+
showListModeToggle: boolean,
|
|
166
224
|
pathConfig: GitPanePathConfig,
|
|
167
225
|
diffCountConfig: GitPaneDiffCountConfig
|
|
168
226
|
): ReactNode {
|
|
169
227
|
if (files.length === 0) return null
|
|
228
|
+
const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
|
|
229
|
+
const toggleListMode = (): void => {
|
|
230
|
+
dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
|
|
231
|
+
runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
|
|
232
|
+
}
|
|
170
233
|
return (
|
|
171
234
|
<box key={section} flexDirection="column">
|
|
172
|
-
<
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
235
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
236
|
+
<text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
|
|
237
|
+
<strong>
|
|
238
|
+
{title} ({files.length})
|
|
239
|
+
</strong>
|
|
240
|
+
</text>
|
|
241
|
+
{showListModeToggle ? (
|
|
242
|
+
<box flexDirection="row" gap={1} onMouseDown={toggleListMode}>
|
|
243
|
+
<text
|
|
244
|
+
fg={
|
|
245
|
+
fileListMode === 'tree'
|
|
246
|
+
? getCurrentTheme().colors['textLink.foreground']
|
|
247
|
+
: getCurrentTheme().colors['descriptionForeground']
|
|
248
|
+
}
|
|
249
|
+
>
|
|
250
|
+
tree
|
|
251
|
+
</text>
|
|
252
|
+
<text fg={getCurrentTheme().colors['descriptionForeground']}>|</text>
|
|
253
|
+
<text
|
|
254
|
+
fg={
|
|
255
|
+
fileListMode === 'flat'
|
|
256
|
+
? getCurrentTheme().colors['textLink.foreground']
|
|
257
|
+
: getCurrentTheme().colors['descriptionForeground']
|
|
258
|
+
}
|
|
259
|
+
>
|
|
260
|
+
flat
|
|
261
|
+
</text>
|
|
262
|
+
</box>
|
|
263
|
+
) : null}
|
|
264
|
+
</box>
|
|
265
|
+
{rows.map((row) =>
|
|
266
|
+
row.kind === 'folder'
|
|
267
|
+
? renderFolderRow(row, row.key === selectedEntryKey)
|
|
268
|
+
: renderFileRow(
|
|
269
|
+
row,
|
|
270
|
+
addedW,
|
|
271
|
+
removedW,
|
|
272
|
+
row.key === selectedEntryKey,
|
|
273
|
+
fileListMode,
|
|
274
|
+
pathConfig,
|
|
275
|
+
diffCountConfig
|
|
276
|
+
)
|
|
187
277
|
)}
|
|
188
278
|
</box>
|
|
189
279
|
)
|
|
@@ -209,16 +299,25 @@ function computeStatusPlaceholder(
|
|
|
209
299
|
hasProjectPath: boolean
|
|
210
300
|
): StatusPlaceholder | null {
|
|
211
301
|
if (!hasProjectPath) {
|
|
212
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
label: 'No active session',
|
|
304
|
+
labelColor: getCurrentTheme().colors['descriptionForeground'],
|
|
305
|
+
}
|
|
213
306
|
}
|
|
214
307
|
if (gitPanel.error === 'not-a-repo') {
|
|
215
|
-
return {
|
|
308
|
+
return {
|
|
309
|
+
label: 'Not a git repository',
|
|
310
|
+
labelColor: getCurrentTheme().colors['descriptionForeground'],
|
|
311
|
+
}
|
|
216
312
|
}
|
|
217
313
|
if (gitPanel.error === 'unknown') {
|
|
218
|
-
return { label: 'Git error', labelColor:
|
|
314
|
+
return { label: 'Git error', labelColor: getCurrentTheme().colors['editorError.foreground'] }
|
|
219
315
|
}
|
|
220
316
|
if (gitPanel.files.length === 0) {
|
|
221
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
label: 'Working tree clean',
|
|
319
|
+
labelColor: getCurrentTheme().colors['descriptionForeground'],
|
|
320
|
+
}
|
|
222
321
|
}
|
|
223
322
|
return null
|
|
224
323
|
}
|
|
@@ -227,13 +326,22 @@ const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
|
|
|
227
326
|
const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
|
|
228
327
|
|
|
229
328
|
export const GitPanel = memo(function GitPanel({
|
|
329
|
+
collapsedFolders = {},
|
|
230
330
|
diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
|
|
331
|
+
fileListMode = 'tree',
|
|
231
332
|
gitPanel,
|
|
333
|
+
headOffset = 0,
|
|
232
334
|
pathConfig = DEFAULT_PATH_CONFIG,
|
|
233
335
|
projectPath,
|
|
234
|
-
|
|
336
|
+
selectedEntryKey,
|
|
235
337
|
}: GitPanelProps) {
|
|
236
|
-
const
|
|
338
|
+
const theme = useTheme()
|
|
339
|
+
const sectionOrder = headOffset > 0 ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
|
|
340
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
341
|
+
const tree = useMemo(
|
|
342
|
+
() => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode),
|
|
343
|
+
[collapsedFolders, fileListMode, gitPanel.files]
|
|
344
|
+
)
|
|
237
345
|
const { added: addedW, removed: removedW } = useMemo(
|
|
238
346
|
() => maxDigitWidth(gitPanel.files),
|
|
239
347
|
[gitPanel.files]
|
|
@@ -242,33 +350,54 @@ export const GitPanel = memo(function GitPanel({
|
|
|
242
350
|
const statusNode = renderStatus(gitPanel, !!projectPath)
|
|
243
351
|
|
|
244
352
|
const hasRemoteTracking = gitPanel.ahead > 0 || gitPanel.behind > 0
|
|
353
|
+
const toggleSection =
|
|
354
|
+
tree.sections.find((section) => section.section === 'unstaged' && section.files.length > 0)
|
|
355
|
+
?.section ??
|
|
356
|
+
tree.sections.find((section) => section.files.length > 0)?.section ??
|
|
357
|
+
null
|
|
358
|
+
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
const scrollbox = scrollRef.current
|
|
361
|
+
if (!scrollbox || !selectedEntryKey) return
|
|
362
|
+
const selectedIndex = tree.visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
363
|
+
if (selectedIndex < 0) return
|
|
364
|
+
const viewportHeight = Math.max(1, scrollbox.viewport.height)
|
|
365
|
+
const target = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2))
|
|
366
|
+
scrollbox.scrollTo({ x: 0, y: target })
|
|
367
|
+
}, [selectedEntryKey, tree.visibleRows])
|
|
245
368
|
|
|
246
369
|
return (
|
|
247
370
|
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
248
371
|
{hasRemoteTracking ? (
|
|
249
|
-
<text fg={theme.
|
|
372
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
250
373
|
↑{gitPanel.ahead} ↓{gitPanel.behind}
|
|
251
374
|
</text>
|
|
252
375
|
) : null}
|
|
253
376
|
{statusNode ?? (
|
|
254
377
|
<scrollbox
|
|
255
378
|
flexGrow={1}
|
|
379
|
+
ref={scrollRef}
|
|
256
380
|
scrollY
|
|
381
|
+
scrollbarOptions={{ visible: false }}
|
|
257
382
|
viewportCulling
|
|
258
383
|
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
259
384
|
>
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
385
|
+
{sectionOrder.map((key) => {
|
|
386
|
+
const section = tree.sections.find((entry) => entry.section === key)
|
|
387
|
+
return renderTreeSection(
|
|
388
|
+
key,
|
|
389
|
+
sectionTitle(key, headOffset),
|
|
390
|
+
section?.files ?? [],
|
|
391
|
+
section?.rows ?? [],
|
|
265
392
|
addedW,
|
|
266
393
|
removedW,
|
|
267
|
-
|
|
394
|
+
fileListMode,
|
|
395
|
+
selectedEntryKey,
|
|
396
|
+
key === toggleSection,
|
|
268
397
|
pathConfig,
|
|
269
398
|
diffCountConfig
|
|
270
399
|
)
|
|
271
|
-
)}
|
|
400
|
+
})}
|
|
272
401
|
</scrollbox>
|
|
273
402
|
)}
|
|
274
403
|
</box>
|