@brimveyn/aimux 1.5.0 → 1.6.0
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-runtime/use-renderer-bindings.ts +1 -1
- 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/index.tsx +2 -2
- 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 +4 -4
- package/src/state/git-tree.ts +220 -0
- package/src/state/reducers/git-mode-state.ts +232 -35
- package/src/state/reducers/git-panel-state.ts +29 -3
- package/src/state/reducers/modal-state.ts +43 -10
- package/src/state/store.ts +5 -1
- package/src/state/types.ts +41 -5
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/create-session-modal.tsx +23 -7
- 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 +84 -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 +170 -0
- package/src/ui/components/diff-renderer/split-view.tsx +207 -0
- package/src/ui/components/diff-renderer/stacked-view.tsx +166 -0
- package/src/ui/components/git-commit-modal.tsx +14 -2
- package/src/ui/components/git-pane-widget.tsx +5 -0
- package/src/ui/components/git-panel.tsx +176 -79
- package/src/ui/components/git-view.tsx +89 -87
- package/src/ui/components/help-modal.tsx +43 -11
- package/src/ui/components/input-field.tsx +2 -2
- package/src/ui/components/list-item.tsx +9 -1
- package/src/ui/components/modal-filter-bar.tsx +1 -1
- package/src/ui/components/modal-keybinds-overlay.tsx +2 -2
- package/src/ui/components/modal-shell.tsx +3 -3
- package/src/ui/components/new-tab-modal.tsx +15 -3
- package/src/ui/components/pending-chord-overlay.tsx +3 -3
- package/src/ui/components/session-bar.tsx +10 -5
- package/src/ui/components/session-picker-modal.tsx +26 -9
- package/src/ui/components/sidebar.tsx +22 -10
- package/src/ui/components/snippet-editor-modal.tsx +16 -2
- package/src/ui/components/snippet-picker-modal.tsx +11 -3
- package/src/ui/components/split-layout.tsx +1 -1
- package/src/ui/components/status-bar.tsx +12 -9
- package/src/ui/components/surface.tsx +5 -5
- package/src/ui/components/tab-item.tsx +22 -16
- package/src/ui/components/terminal-pane.tsx +25 -15
- package/src/ui/components/theme-picker-modal.tsx +111 -16
- package/src/ui/components/update-available-modal.tsx +11 -1
- package/src/ui/filter-themes.ts +10 -0
- package/src/ui/house-themes.ts +313 -0
- package/src/ui/keymap-context.ts +10 -2
- package/src/ui/root.tsx +26 -6
- package/src/ui/shiki.ts +49 -0
- package/src/ui/status-bar-model.ts +19 -1
- package/src/ui/theme.ts +22 -10
- package/src/ui/themes.generated.ts +59794 -0
- package/src/ui/themes.ts +68 -274
- package/src/ui/syntax.ts +0 -102
|
@@ -17,7 +17,13 @@ export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommi
|
|
|
17
17
|
return (
|
|
18
18
|
<ModalShell title="Commit" keybindsModeId="modal.git-commit" width={uiTokens.modalWidth.xl}>
|
|
19
19
|
<box flexDirection="column">
|
|
20
|
-
<text
|
|
20
|
+
<text
|
|
21
|
+
fg={
|
|
22
|
+
titleActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
23
|
+
}
|
|
24
|
+
>
|
|
25
|
+
Title
|
|
26
|
+
</text>
|
|
21
27
|
<InputField
|
|
22
28
|
active={titleActive}
|
|
23
29
|
cursorPos={titleActive ? cursorPos : undefined}
|
|
@@ -26,7 +32,13 @@ export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommi
|
|
|
26
32
|
</box>
|
|
27
33
|
|
|
28
34
|
<box flexDirection="column">
|
|
29
|
-
<text
|
|
35
|
+
<text
|
|
36
|
+
fg={
|
|
37
|
+
bodyActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
Body (optional)
|
|
41
|
+
</text>
|
|
30
42
|
<InputField
|
|
31
43
|
active={bodyActive}
|
|
32
44
|
cursorPos={bodyActive ? cursorPos : undefined}
|
|
@@ -12,6 +12,8 @@ interface GitPaneWidgetProps {
|
|
|
12
12
|
|
|
13
13
|
export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: GitPaneWidgetProps) {
|
|
14
14
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
15
|
+
const gitMode = useAppStore((s) => s.gitMode)
|
|
16
|
+
const gitFileListMode = useAppStore((s) => s.gitPane.fileListMode)
|
|
15
17
|
const pathConfig = useAppStore((s) => s.gitPane.path)
|
|
16
18
|
const diffCountConfig = useAppStore((s) => s.gitPane.diffCount)
|
|
17
19
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
@@ -37,10 +39,13 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
|
|
|
37
39
|
|
|
38
40
|
return (
|
|
39
41
|
<GitPanel
|
|
42
|
+
collapsedFolders={gitMode.collapsedFolders}
|
|
40
43
|
diffCountConfig={diffCountConfig}
|
|
44
|
+
fileListMode={gitFileListMode}
|
|
41
45
|
gitPanel={display}
|
|
42
46
|
pathConfig={pathConfig}
|
|
43
47
|
projectPath={projectPath}
|
|
48
|
+
selectedEntryKey={gitMode.selectedEntryKey}
|
|
44
49
|
/>
|
|
45
50
|
)
|
|
46
51
|
})
|
|
@@ -1,27 +1,30 @@
|
|
|
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
|
|
|
14
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
15
|
+
import { buildGitTreeRows, type GitTreeFileRow, type GitTreeFolderRow } from '../../state/git-tree'
|
|
11
16
|
import { theme } 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
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
export function fileKey(file: Pick<GitFileEntry, 'path' | 'section'>): string {
|
|
22
|
-
return `${file.section}:${file.path}`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
28
|
const SECTION_ORDER: { key: GitFileSection; title: string }[] = [
|
|
26
29
|
{ key: 'staged', title: 'Staged Changes' },
|
|
27
30
|
{ key: 'unstaged', title: 'Changes' },
|
|
@@ -29,13 +32,13 @@ const SECTION_ORDER: { key: GitFileSection; title: string }[] = [
|
|
|
29
32
|
]
|
|
30
33
|
|
|
31
34
|
const STATUS_COLORS: Record<GitFileEntry['status'], string> = {
|
|
32
|
-
'?': theme.
|
|
33
|
-
'A': theme.
|
|
34
|
-
'C': theme.
|
|
35
|
-
'D': theme.
|
|
36
|
-
'M': theme.
|
|
37
|
-
'R': theme.
|
|
38
|
-
'U': theme.
|
|
35
|
+
'?': theme.colors['gitDecoration.addedResourceForeground'],
|
|
36
|
+
'A': theme.colors['gitDecoration.addedResourceForeground'],
|
|
37
|
+
'C': theme.colors['textLink.foreground'],
|
|
38
|
+
'D': theme.colors['editorError.foreground'],
|
|
39
|
+
'M': theme.colors['editorWarning.foreground'],
|
|
40
|
+
'R': theme.colors['textLink.foreground'],
|
|
41
|
+
'U': theme.colors['editorError.foreground'],
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
function displayStatus(file: GitFileEntry): string {
|
|
@@ -43,16 +46,6 @@ function displayStatus(file: GitFileEntry): string {
|
|
|
43
46
|
return file.status
|
|
44
47
|
}
|
|
45
48
|
|
|
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
49
|
function maxDigitWidth(files: GitFileEntry[]): { added: number; removed: number } {
|
|
57
50
|
let addedMax = 1
|
|
58
51
|
let removedMax = 1
|
|
@@ -67,7 +60,7 @@ function padRight(value: number | null, width: number): string {
|
|
|
67
60
|
return String(value ?? 0).padStart(width, ' ')
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
function splitPath(path: string): { prefix: string; basename: string } {
|
|
71
64
|
const slash = path.lastIndexOf('/')
|
|
72
65
|
if (slash < 0) return { basename: path, prefix: '' }
|
|
73
66
|
return { basename: path.slice(slash + 1), prefix: path.slice(0, slash + 1) }
|
|
@@ -77,30 +70,36 @@ function stripTrailingSlash(prefix: string): string {
|
|
|
77
70
|
return prefix.endsWith('/') ? prefix.slice(0, -1) : prefix
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
function
|
|
81
|
-
|
|
73
|
+
function renderFileLabel(
|
|
74
|
+
file: GitFileEntry,
|
|
75
|
+
pathConfig: GitPanePathConfig,
|
|
76
|
+
fileListMode: GitFileListMode
|
|
77
|
+
): ReactNode {
|
|
82
78
|
const transform = pathConfig.enabled ? pathConfig.pathFn : undefined
|
|
83
79
|
const displayPath = transform ? transform(file.path) : file.path
|
|
84
80
|
const { basename, prefix } = splitPath(displayPath)
|
|
85
81
|
const dir = stripTrailingSlash(prefix)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const renamed = splitPath(renamedDisplay)
|
|
89
|
-
const renamedDir = stripTrailingSlash(renamed.prefix)
|
|
82
|
+
const showDir = fileListMode === 'flat' && pathConfig.enabled && dir
|
|
83
|
+
if (!file.renamedFrom) {
|
|
90
84
|
return (
|
|
91
85
|
<text wrapMode="none">
|
|
92
|
-
<span fg={theme.
|
|
93
|
-
{showDir
|
|
94
|
-
<span fg={theme.textMuted}> → </span>
|
|
95
|
-
<span fg={theme.text}>{basename}</span>
|
|
96
|
-
{showDir && dir ? <span fg={theme.textMuted}> {dir}</span> : null}
|
|
86
|
+
<span fg={theme.colors['editor.foreground']}>{basename}</span>
|
|
87
|
+
{showDir ? <span fg={theme.colors['descriptionForeground']}> {dir}</span> : null}
|
|
97
88
|
</text>
|
|
98
89
|
)
|
|
99
90
|
}
|
|
91
|
+
const renamedDisplay = transform ? transform(file.renamedFrom) : file.renamedFrom
|
|
92
|
+
const renamed = splitPath(renamedDisplay)
|
|
93
|
+
const renamedDir = stripTrailingSlash(renamed.prefix)
|
|
100
94
|
return (
|
|
101
95
|
<text wrapMode="none">
|
|
102
|
-
<span fg={theme.
|
|
103
|
-
{showDir &&
|
|
96
|
+
<span fg={theme.colors['descriptionForeground']}>{renamed.basename}</span>
|
|
97
|
+
{showDir && renamedDir ? (
|
|
98
|
+
<span fg={theme.colors['descriptionForeground']}> {renamedDir}</span>
|
|
99
|
+
) : null}
|
|
100
|
+
<span fg={theme.colors['descriptionForeground']}> → </span>
|
|
101
|
+
<span fg={theme.colors['editor.foreground']}>{basename}</span>
|
|
102
|
+
{showDir ? <span fg={theme.colors['descriptionForeground']}> {dir}</span> : null}
|
|
104
103
|
</text>
|
|
105
104
|
)
|
|
106
105
|
}
|
|
@@ -116,74 +115,145 @@ function renderDiffCount(
|
|
|
116
115
|
if (!diffCountConfig.enabled) return null
|
|
117
116
|
if (!hasNumstat) {
|
|
118
117
|
return (
|
|
119
|
-
<text fg={theme.
|
|
118
|
+
<text fg={theme.colors['descriptionForeground']} bg={bg} flexShrink={0}>
|
|
120
119
|
—
|
|
121
120
|
</text>
|
|
122
121
|
)
|
|
123
122
|
}
|
|
124
123
|
return (
|
|
125
124
|
<box flexDirection="row" flexShrink={0}>
|
|
126
|
-
<text
|
|
127
|
-
|
|
125
|
+
<text
|
|
126
|
+
fg={theme.colors['gitDecoration.addedResourceForeground']}
|
|
127
|
+
bg={bg}
|
|
128
|
+
>{`+${padRight(file.added, addedW)}`}</text>
|
|
129
|
+
<text fg={theme.colors['editor.lineHighlightBackground']} bg={bg}>
|
|
128
130
|
{' '}
|
|
129
131
|
</text>
|
|
130
|
-
<text
|
|
132
|
+
<text
|
|
133
|
+
fg={theme.colors['editorError.foreground']}
|
|
134
|
+
bg={bg}
|
|
135
|
+
>{`−${padRight(file.removed, removedW)}`}</text>
|
|
136
|
+
</box>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode {
|
|
141
|
+
const bg = isSelected ? theme.colors['list.activeSelectionBackground'] : undefined
|
|
142
|
+
const onSelect = (): void => {
|
|
143
|
+
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
144
|
+
}
|
|
145
|
+
const onToggle = (): void => {
|
|
146
|
+
dispatchGlobal({ key: row.key, type: 'git-mode-toggle-folder' })
|
|
147
|
+
}
|
|
148
|
+
return (
|
|
149
|
+
<box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
150
|
+
<box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
|
|
151
|
+
<box flexDirection="row" gap={1} onMouseDown={onToggle}>
|
|
152
|
+
<text fg={theme.colors['descriptionForeground']} bg={bg}>
|
|
153
|
+
{row.isCollapsed ? '▸' : '▾'}
|
|
154
|
+
</text>
|
|
155
|
+
<text fg={theme.colors['terminal.ansiMagenta']} bg={bg} wrapMode="none">
|
|
156
|
+
{row.name}
|
|
157
|
+
</text>
|
|
158
|
+
</box>
|
|
159
|
+
</box>
|
|
131
160
|
</box>
|
|
132
161
|
)
|
|
133
162
|
}
|
|
134
163
|
|
|
135
164
|
function renderFileRow(
|
|
136
|
-
|
|
137
|
-
key: string,
|
|
165
|
+
row: GitTreeFileRow,
|
|
138
166
|
addedW: number,
|
|
139
167
|
removedW: number,
|
|
140
168
|
isSelected: boolean,
|
|
169
|
+
fileListMode: GitFileListMode,
|
|
141
170
|
pathConfig: GitPanePathConfig,
|
|
142
171
|
diffCountConfig: GitPaneDiffCountConfig
|
|
143
172
|
): ReactNode {
|
|
173
|
+
const file = row.file
|
|
144
174
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
145
|
-
const bg = isSelected ? theme.
|
|
175
|
+
const bg = isSelected ? theme.colors['list.activeSelectionBackground'] : undefined
|
|
176
|
+
const onSelect = (): void => {
|
|
177
|
+
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
178
|
+
}
|
|
146
179
|
return (
|
|
147
|
-
<box key={key} flexDirection="row" gap={1}
|
|
148
|
-
<
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
180
|
+
<box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
181
|
+
<box width={2} flexShrink={0} justifyContent="center">
|
|
182
|
+
<text fg={STATUS_COLORS[file.status]} bg={bg}>
|
|
183
|
+
<strong>{displayStatus(file)}</strong>
|
|
184
|
+
</text>
|
|
185
|
+
</box>
|
|
186
|
+
<box flexGrow={1} overflow="hidden" paddingLeft={fileListMode === 'tree' ? row.depth * 2 : 0}>
|
|
187
|
+
{renderFileLabel(file, pathConfig, fileListMode)}
|
|
153
188
|
</box>
|
|
154
189
|
{renderDiffCount(file, addedW, removedW, bg, diffCountConfig, hasNumstat)}
|
|
155
190
|
</box>
|
|
156
191
|
)
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
function
|
|
194
|
+
function renderTreeSection(
|
|
160
195
|
section: GitFileSection,
|
|
161
196
|
title: string,
|
|
162
197
|
files: GitFileEntry[],
|
|
198
|
+
rows: Array<GitTreeFolderRow | GitTreeFileRow>,
|
|
163
199
|
addedW: number,
|
|
164
200
|
removedW: number,
|
|
165
|
-
|
|
201
|
+
fileListMode: GitFileListMode,
|
|
202
|
+
selectedEntryKey: string | null | undefined,
|
|
203
|
+
showListModeToggle: boolean,
|
|
166
204
|
pathConfig: GitPanePathConfig,
|
|
167
205
|
diffCountConfig: GitPaneDiffCountConfig
|
|
168
206
|
): ReactNode {
|
|
169
207
|
if (files.length === 0) return null
|
|
208
|
+
const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
|
|
209
|
+
const toggleListMode = (): void => {
|
|
210
|
+
dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
|
|
211
|
+
runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
|
|
212
|
+
}
|
|
170
213
|
return (
|
|
171
214
|
<box key={section} flexDirection="column">
|
|
172
|
-
<
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
215
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
216
|
+
<text fg={theme.colors['terminal.ansiMagenta']}>
|
|
217
|
+
<strong>
|
|
218
|
+
{title} ({files.length})
|
|
219
|
+
</strong>
|
|
220
|
+
</text>
|
|
221
|
+
{showListModeToggle ? (
|
|
222
|
+
<box flexDirection="row" gap={1} onMouseDown={toggleListMode}>
|
|
223
|
+
<text
|
|
224
|
+
fg={
|
|
225
|
+
fileListMode === 'tree'
|
|
226
|
+
? theme.colors['textLink.foreground']
|
|
227
|
+
: theme.colors['descriptionForeground']
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
tree
|
|
231
|
+
</text>
|
|
232
|
+
<text fg={theme.colors['descriptionForeground']}>|</text>
|
|
233
|
+
<text
|
|
234
|
+
fg={
|
|
235
|
+
fileListMode === 'flat'
|
|
236
|
+
? theme.colors['textLink.foreground']
|
|
237
|
+
: theme.colors['descriptionForeground']
|
|
238
|
+
}
|
|
239
|
+
>
|
|
240
|
+
flat
|
|
241
|
+
</text>
|
|
242
|
+
</box>
|
|
243
|
+
) : null}
|
|
244
|
+
</box>
|
|
245
|
+
{rows.map((row) =>
|
|
246
|
+
row.kind === 'folder'
|
|
247
|
+
? renderFolderRow(row, row.key === selectedEntryKey)
|
|
248
|
+
: renderFileRow(
|
|
249
|
+
row,
|
|
250
|
+
addedW,
|
|
251
|
+
removedW,
|
|
252
|
+
row.key === selectedEntryKey,
|
|
253
|
+
fileListMode,
|
|
254
|
+
pathConfig,
|
|
255
|
+
diffCountConfig
|
|
256
|
+
)
|
|
187
257
|
)}
|
|
188
258
|
</box>
|
|
189
259
|
)
|
|
@@ -209,16 +279,16 @@ function computeStatusPlaceholder(
|
|
|
209
279
|
hasProjectPath: boolean
|
|
210
280
|
): StatusPlaceholder | null {
|
|
211
281
|
if (!hasProjectPath) {
|
|
212
|
-
return { label: 'No active session', labelColor: theme.
|
|
282
|
+
return { label: 'No active session', labelColor: theme.colors['descriptionForeground'] }
|
|
213
283
|
}
|
|
214
284
|
if (gitPanel.error === 'not-a-repo') {
|
|
215
|
-
return { label: 'Not a git repository', labelColor: theme.
|
|
285
|
+
return { label: 'Not a git repository', labelColor: theme.colors['descriptionForeground'] }
|
|
216
286
|
}
|
|
217
287
|
if (gitPanel.error === 'unknown') {
|
|
218
|
-
return { label: 'Git error', labelColor: theme.
|
|
288
|
+
return { label: 'Git error', labelColor: theme.colors['editorError.foreground'] }
|
|
219
289
|
}
|
|
220
290
|
if (gitPanel.files.length === 0) {
|
|
221
|
-
return { label: 'Working tree clean', labelColor: theme.
|
|
291
|
+
return { label: 'Working tree clean', labelColor: theme.colors['descriptionForeground'] }
|
|
222
292
|
}
|
|
223
293
|
return null
|
|
224
294
|
}
|
|
@@ -227,13 +297,19 @@ const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
|
|
|
227
297
|
const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
|
|
228
298
|
|
|
229
299
|
export const GitPanel = memo(function GitPanel({
|
|
300
|
+
collapsedFolders = {},
|
|
230
301
|
diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
|
|
302
|
+
fileListMode = 'tree',
|
|
231
303
|
gitPanel,
|
|
232
304
|
pathConfig = DEFAULT_PATH_CONFIG,
|
|
233
305
|
projectPath,
|
|
234
|
-
|
|
306
|
+
selectedEntryKey,
|
|
235
307
|
}: GitPanelProps) {
|
|
236
|
-
const
|
|
308
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
309
|
+
const tree = useMemo(
|
|
310
|
+
() => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode),
|
|
311
|
+
[collapsedFolders, fileListMode, gitPanel.files]
|
|
312
|
+
)
|
|
237
313
|
const { added: addedW, removed: removedW } = useMemo(
|
|
238
314
|
() => maxDigitWidth(gitPanel.files),
|
|
239
315
|
[gitPanel.files]
|
|
@@ -242,33 +318,54 @@ export const GitPanel = memo(function GitPanel({
|
|
|
242
318
|
const statusNode = renderStatus(gitPanel, !!projectPath)
|
|
243
319
|
|
|
244
320
|
const hasRemoteTracking = gitPanel.ahead > 0 || gitPanel.behind > 0
|
|
321
|
+
const toggleSection =
|
|
322
|
+
tree.sections.find((section) => section.section === 'unstaged' && section.files.length > 0)
|
|
323
|
+
?.section ??
|
|
324
|
+
tree.sections.find((section) => section.files.length > 0)?.section ??
|
|
325
|
+
null
|
|
326
|
+
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
const scrollbox = scrollRef.current
|
|
329
|
+
if (!scrollbox || !selectedEntryKey) return
|
|
330
|
+
const selectedIndex = tree.visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
331
|
+
if (selectedIndex < 0) return
|
|
332
|
+
const viewportHeight = Math.max(1, scrollbox.viewport.height)
|
|
333
|
+
const target = Math.max(0, selectedIndex - Math.floor(viewportHeight / 2))
|
|
334
|
+
scrollbox.scrollTo({ x: 0, y: target })
|
|
335
|
+
}, [selectedEntryKey, tree.visibleRows])
|
|
245
336
|
|
|
246
337
|
return (
|
|
247
338
|
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
248
339
|
{hasRemoteTracking ? (
|
|
249
|
-
<text fg={theme.
|
|
340
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
250
341
|
↑{gitPanel.ahead} ↓{gitPanel.behind}
|
|
251
342
|
</text>
|
|
252
343
|
) : null}
|
|
253
344
|
{statusNode ?? (
|
|
254
345
|
<scrollbox
|
|
255
346
|
flexGrow={1}
|
|
347
|
+
ref={scrollRef}
|
|
256
348
|
scrollY
|
|
349
|
+
scrollbarOptions={{ visible: false }}
|
|
257
350
|
viewportCulling
|
|
258
351
|
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
259
352
|
>
|
|
260
|
-
{SECTION_ORDER.map((s) =>
|
|
261
|
-
|
|
353
|
+
{SECTION_ORDER.map((s) => {
|
|
354
|
+
const section = tree.sections.find((entry) => entry.section === s.key)
|
|
355
|
+
return renderTreeSection(
|
|
262
356
|
s.key,
|
|
263
357
|
s.title,
|
|
264
|
-
|
|
358
|
+
section?.files ?? [],
|
|
359
|
+
section?.rows ?? [],
|
|
265
360
|
addedW,
|
|
266
361
|
removedW,
|
|
267
|
-
|
|
362
|
+
fileListMode,
|
|
363
|
+
selectedEntryKey,
|
|
364
|
+
s.key === toggleSection,
|
|
268
365
|
pathConfig,
|
|
269
366
|
diffCountConfig
|
|
270
367
|
)
|
|
271
|
-
)}
|
|
368
|
+
})}
|
|
272
369
|
</scrollbox>
|
|
273
370
|
)}
|
|
274
371
|
</box>
|