@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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { GitFileEntry, GitFileListMode, GitFileSection } from './types'
|
|
2
|
+
|
|
3
|
+
const SECTION_ORDER: GitFileSection[] = ['staged', 'unstaged', 'untracked']
|
|
4
|
+
|
|
5
|
+
interface GitTreeNode {
|
|
6
|
+
files: GitFileEntry[]
|
|
7
|
+
folders: Map<string, GitTreeNode>
|
|
8
|
+
path: string
|
|
9
|
+
section: GitFileSection
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GitTreeFolderRow {
|
|
13
|
+
kind: 'folder'
|
|
14
|
+
key: string
|
|
15
|
+
section: GitFileSection
|
|
16
|
+
depth: number
|
|
17
|
+
name: string
|
|
18
|
+
folderPath: string
|
|
19
|
+
parentKey?: string
|
|
20
|
+
isCollapsed: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface GitTreeFileRow {
|
|
24
|
+
kind: 'file'
|
|
25
|
+
key: string
|
|
26
|
+
section: GitFileSection
|
|
27
|
+
depth: number
|
|
28
|
+
file: GitFileEntry
|
|
29
|
+
parentKey?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type GitTreeRow = GitTreeFolderRow | GitTreeFileRow
|
|
33
|
+
|
|
34
|
+
export interface GitTreeSectionRows {
|
|
35
|
+
section: GitFileSection
|
|
36
|
+
files: GitFileEntry[]
|
|
37
|
+
rows: GitTreeRow[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GitTreeRows {
|
|
41
|
+
sections: GitTreeSectionRows[]
|
|
42
|
+
visibleRows: GitTreeRow[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function gitFileKey(file: Pick<GitFileEntry, 'path' | 'section'>): string {
|
|
46
|
+
return `${file.section}:${file.path}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function gitFolderKey(section: GitFileSection, folderPath: string): string {
|
|
50
|
+
return `${section}:dir:${folderPath}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildGitTreeRows(
|
|
54
|
+
files: GitFileEntry[],
|
|
55
|
+
collapsedFolders: Record<string, true>,
|
|
56
|
+
fileListMode: GitFileListMode = 'tree'
|
|
57
|
+
): GitTreeRows {
|
|
58
|
+
const sections = SECTION_ORDER.map((section) => {
|
|
59
|
+
const sectionFiles = files.filter((file) => file.section === section)
|
|
60
|
+
return {
|
|
61
|
+
files: sectionFiles,
|
|
62
|
+
rows:
|
|
63
|
+
fileListMode === 'flat'
|
|
64
|
+
? sectionFiles.map((file) => ({
|
|
65
|
+
depth: 0,
|
|
66
|
+
file,
|
|
67
|
+
key: gitFileKey(file),
|
|
68
|
+
kind: 'file' as const,
|
|
69
|
+
section,
|
|
70
|
+
}))
|
|
71
|
+
: flattenSectionRows(sectionFiles, collapsedFolders),
|
|
72
|
+
section,
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
return { sections, visibleRows: sections.flatMap((section) => section.rows) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getSelectedGitRow(
|
|
79
|
+
files: GitFileEntry[],
|
|
80
|
+
options: {
|
|
81
|
+
collapsedFolders: Record<string, true>
|
|
82
|
+
fileListMode: GitFileListMode
|
|
83
|
+
selectedEntryKey: string | null
|
|
84
|
+
}
|
|
85
|
+
): GitTreeRow | null {
|
|
86
|
+
if (!options.selectedEntryKey) return null
|
|
87
|
+
const { visibleRows } = buildGitTreeRows(files, options.collapsedFolders, options.fileListMode)
|
|
88
|
+
return visibleRows.find((row) => row.key === options.selectedEntryKey) ?? null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getSelectedGitFile(
|
|
92
|
+
files: GitFileEntry[],
|
|
93
|
+
options: {
|
|
94
|
+
collapsedFolders: Record<string, true>
|
|
95
|
+
fileListMode: GitFileListMode
|
|
96
|
+
selectedEntryKey: string | null
|
|
97
|
+
}
|
|
98
|
+
): GitFileEntry | null {
|
|
99
|
+
const row = getSelectedGitRow(files, options)
|
|
100
|
+
return row?.kind === 'file' ? row.file : null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function reconcileSelectedGitEntryKey(
|
|
104
|
+
files: GitFileEntry[],
|
|
105
|
+
collapsedFolders: Record<string, true>,
|
|
106
|
+
fileListMode: GitFileListMode,
|
|
107
|
+
selectedEntryKey: string | null | undefined,
|
|
108
|
+
preferredKeys: string[] = []
|
|
109
|
+
): string | null {
|
|
110
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
111
|
+
if (visibleRows.length === 0) return null
|
|
112
|
+
const candidates = [...preferredKeys, selectedEntryKey ?? '']
|
|
113
|
+
for (const key of candidates) {
|
|
114
|
+
if (!key) continue
|
|
115
|
+
if (visibleRows.some((row) => row.key === key)) return key
|
|
116
|
+
}
|
|
117
|
+
return visibleRows[0]?.key ?? null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function moveGitSelection(
|
|
121
|
+
files: GitFileEntry[],
|
|
122
|
+
collapsedFolders: Record<string, true>,
|
|
123
|
+
fileListMode: GitFileListMode,
|
|
124
|
+
selectedEntryKey: string | null,
|
|
125
|
+
delta: -1 | 1
|
|
126
|
+
): string | null {
|
|
127
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
128
|
+
const total = visibleRows.length
|
|
129
|
+
if (total === 0) return null
|
|
130
|
+
const current = visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
131
|
+
const base = current >= 0 ? current : 0
|
|
132
|
+
return visibleRows[(base + delta + total) % total]?.key ?? null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function moveGitFileSelection(
|
|
136
|
+
files: GitFileEntry[],
|
|
137
|
+
collapsedFolders: Record<string, true>,
|
|
138
|
+
fileListMode: GitFileListMode,
|
|
139
|
+
selectedEntryKey: string | null,
|
|
140
|
+
delta: -1 | 1
|
|
141
|
+
): string | null {
|
|
142
|
+
const { visibleRows } = buildGitTreeRows(files, collapsedFolders, fileListMode)
|
|
143
|
+
const fileRows = visibleRows.filter((row) => row.kind === 'file')
|
|
144
|
+
const total = fileRows.length
|
|
145
|
+
if (total === 0) return null
|
|
146
|
+
const current = fileRows.findIndex((row) => row.key === selectedEntryKey)
|
|
147
|
+
let base = current
|
|
148
|
+
if (base < 0) {
|
|
149
|
+
base = delta > 0 ? -1 : 0
|
|
150
|
+
}
|
|
151
|
+
return fileRows[(base + delta + total) % total]?.key ?? null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function flattenSectionRows(
|
|
155
|
+
files: GitFileEntry[],
|
|
156
|
+
collapsedFolders: Record<string, true>
|
|
157
|
+
): GitTreeRow[] {
|
|
158
|
+
if (files.length === 0) return []
|
|
159
|
+
const section = files[0]?.section
|
|
160
|
+
if (!section) return []
|
|
161
|
+
const root = buildSectionTree(files, section)
|
|
162
|
+
return flattenTreeNode(root, collapsedFolders, 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildSectionTree(files: GitFileEntry[], section: GitFileSection): GitTreeNode {
|
|
166
|
+
const root: GitTreeNode = { files: [], folders: new Map(), path: '', section }
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
const parts = file.path.split('/')
|
|
169
|
+
let cursor = root
|
|
170
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
171
|
+
const name = parts[i]
|
|
172
|
+
if (!name) continue
|
|
173
|
+
const nextPath = cursor.path ? `${cursor.path}/${name}` : name
|
|
174
|
+
let child = cursor.folders.get(name)
|
|
175
|
+
if (!child) {
|
|
176
|
+
child = { files: [], folders: new Map(), path: nextPath, section }
|
|
177
|
+
cursor.folders.set(name, child)
|
|
178
|
+
}
|
|
179
|
+
cursor = child
|
|
180
|
+
}
|
|
181
|
+
cursor.files.push(file)
|
|
182
|
+
}
|
|
183
|
+
return root
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function flattenTreeNode(
|
|
187
|
+
node: GitTreeNode,
|
|
188
|
+
collapsedFolders: Record<string, true>,
|
|
189
|
+
depth: number,
|
|
190
|
+
parentKey?: string
|
|
191
|
+
): GitTreeRow[] {
|
|
192
|
+
const rows: GitTreeRow[] = []
|
|
193
|
+
for (const child of node.folders.values()) {
|
|
194
|
+
const name = child.path.split('/').pop() ?? child.path
|
|
195
|
+
const key = gitFolderKey(node.section, child.path)
|
|
196
|
+
const isCollapsed = key in collapsedFolders
|
|
197
|
+
rows.push({
|
|
198
|
+
depth,
|
|
199
|
+
folderPath: child.path,
|
|
200
|
+
isCollapsed,
|
|
201
|
+
key,
|
|
202
|
+
kind: 'folder',
|
|
203
|
+
name,
|
|
204
|
+
parentKey,
|
|
205
|
+
section: node.section,
|
|
206
|
+
})
|
|
207
|
+
if (!isCollapsed) rows.push(...flattenTreeNode(child, collapsedFolders, depth + 1, key))
|
|
208
|
+
}
|
|
209
|
+
for (const file of node.files) {
|
|
210
|
+
rows.push({
|
|
211
|
+
depth,
|
|
212
|
+
file,
|
|
213
|
+
key: gitFileKey(file),
|
|
214
|
+
kind: 'file',
|
|
215
|
+
parentKey,
|
|
216
|
+
section: file.section,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
return rows
|
|
220
|
+
}
|
|
@@ -1,14 +1,70 @@
|
|
|
1
|
-
import type { AppAction, AppState, GitFileEntry, GitModeState } from '../types'
|
|
1
|
+
import type { AppAction, AppState, FoldState, GitFileEntry, GitModeState } from '../types'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
getSelectedGitRow,
|
|
5
|
+
gitFileKey,
|
|
6
|
+
moveGitFileSelection,
|
|
7
|
+
moveGitSelection,
|
|
8
|
+
reconcileSelectedGitEntryKey,
|
|
9
|
+
} from '../git-tree'
|
|
3
10
|
import { sortFilesBySection } from './git-panel-state'
|
|
4
11
|
|
|
12
|
+
function applyFold(state: AppState, key: string, foldId: string, next: FoldState): AppState {
|
|
13
|
+
const perPath = state.gitMode.folds[key] ?? {}
|
|
14
|
+
const prev = perPath[foldId] ?? { bottom: 0, top: 0 }
|
|
15
|
+
if (prev.top === next.top && prev.bottom === next.bottom) return state
|
|
16
|
+
const nextPerPath = { ...perPath }
|
|
17
|
+
if (next.top === 0 && next.bottom === 0) {
|
|
18
|
+
delete nextPerPath[foldId]
|
|
19
|
+
} else {
|
|
20
|
+
nextPerPath[foldId] = next
|
|
21
|
+
}
|
|
22
|
+
const nextFolds = { ...state.gitMode.folds }
|
|
23
|
+
if (Object.keys(nextPerPath).length === 0) {
|
|
24
|
+
delete nextFolds[key]
|
|
25
|
+
} else {
|
|
26
|
+
nextFolds[key] = nextPerPath
|
|
27
|
+
}
|
|
28
|
+
return { ...state, gitMode: { ...state.gitMode, folds: nextFolds } }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clearDiffCacheForPath(state: AppState, path: string): AppState {
|
|
32
|
+
const nextDiffs = { ...state.gitMode.diffs }
|
|
33
|
+
const nextFolds = { ...state.gitMode.folds }
|
|
34
|
+
const nextLoading = { ...state.gitMode.loading }
|
|
35
|
+
let changed = false
|
|
36
|
+
for (const key of Object.keys(nextDiffs)) {
|
|
37
|
+
if (nextDiffs[key]?.path !== path) continue
|
|
38
|
+
delete nextDiffs[key]
|
|
39
|
+
changed = true
|
|
40
|
+
}
|
|
41
|
+
for (const key of Object.keys(nextFolds)) {
|
|
42
|
+
if (!key.endsWith(`:${path}`)) continue
|
|
43
|
+
delete nextFolds[key]
|
|
44
|
+
changed = true
|
|
45
|
+
}
|
|
46
|
+
for (const key of Object.keys(nextLoading)) {
|
|
47
|
+
if (!key.endsWith(`:${path}`)) continue
|
|
48
|
+
delete nextLoading[key]
|
|
49
|
+
changed = true
|
|
50
|
+
}
|
|
51
|
+
if (!changed) return state
|
|
52
|
+
return {
|
|
53
|
+
...state,
|
|
54
|
+
gitMode: { ...state.gitMode, diffs: nextDiffs, folds: nextFolds, loading: nextLoading },
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
5
58
|
export function emptyGitMode(): GitModeState {
|
|
6
59
|
return {
|
|
7
60
|
actionMessage: null,
|
|
61
|
+
collapsedFolders: {},
|
|
8
62
|
diffs: {},
|
|
63
|
+
diffView: 'split',
|
|
64
|
+
folds: {},
|
|
9
65
|
loading: {},
|
|
10
66
|
pendingDeletePath: null,
|
|
11
|
-
|
|
67
|
+
selectedEntryKey: null,
|
|
12
68
|
}
|
|
13
69
|
}
|
|
14
70
|
|
|
@@ -23,7 +79,12 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
23
79
|
...state.gitMode,
|
|
24
80
|
actionMessage: null,
|
|
25
81
|
pendingDeletePath: null,
|
|
26
|
-
|
|
82
|
+
selectedEntryKey: reconcileSelectedGitEntryKey(
|
|
83
|
+
state.gitPanel.files,
|
|
84
|
+
state.gitMode.collapsedFolders,
|
|
85
|
+
state.gitPane.fileListMode,
|
|
86
|
+
null
|
|
87
|
+
),
|
|
27
88
|
},
|
|
28
89
|
}
|
|
29
90
|
}
|
|
@@ -31,28 +92,152 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
31
92
|
if (state.focusMode !== 'git') return state
|
|
32
93
|
return { ...state, focusMode: 'navigation' }
|
|
33
94
|
}
|
|
34
|
-
case 'git-mode-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
95
|
+
case 'git-mode-move-selection': {
|
|
96
|
+
const next = moveGitSelection(
|
|
97
|
+
state.gitPanel.files,
|
|
98
|
+
state.gitMode.collapsedFolders,
|
|
99
|
+
state.gitPane.fileListMode,
|
|
100
|
+
state.gitMode.selectedEntryKey,
|
|
101
|
+
action.delta
|
|
102
|
+
)
|
|
103
|
+
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
gitMode: {
|
|
107
|
+
...state.gitMode,
|
|
108
|
+
pendingDeletePath: null,
|
|
109
|
+
selectedEntryKey: next,
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
case 'git-mode-move-file-selection': {
|
|
114
|
+
const next = moveGitFileSelection(
|
|
115
|
+
state.gitPanel.files,
|
|
116
|
+
state.gitMode.collapsedFolders,
|
|
117
|
+
state.gitPane.fileListMode,
|
|
118
|
+
state.gitMode.selectedEntryKey,
|
|
119
|
+
action.delta
|
|
120
|
+
)
|
|
121
|
+
if (!next || next === state.gitMode.selectedEntryKey) return state
|
|
122
|
+
return {
|
|
123
|
+
...state,
|
|
124
|
+
gitMode: {
|
|
125
|
+
...state.gitMode,
|
|
126
|
+
pendingDeletePath: null,
|
|
127
|
+
selectedEntryKey: next,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
case 'git-mode-select-entry-by-key': {
|
|
132
|
+
const next = reconcileSelectedGitEntryKey(
|
|
133
|
+
state.gitPanel.files,
|
|
134
|
+
state.gitMode.collapsedFolders,
|
|
135
|
+
state.gitPane.fileListMode,
|
|
136
|
+
state.gitMode.selectedEntryKey,
|
|
137
|
+
[action.key]
|
|
138
|
+
)
|
|
139
|
+
if (!next || next !== action.key || next === state.gitMode.selectedEntryKey) return state
|
|
140
|
+
return {
|
|
141
|
+
...state,
|
|
142
|
+
gitMode: {
|
|
143
|
+
...state.gitMode,
|
|
144
|
+
pendingDeletePath: null,
|
|
145
|
+
selectedEntryKey: next,
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
case 'git-mode-toggle-folder': {
|
|
150
|
+
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
151
|
+
collapsedFolders: state.gitMode.collapsedFolders,
|
|
152
|
+
fileListMode: state.gitPane.fileListMode,
|
|
153
|
+
selectedEntryKey: action.key,
|
|
154
|
+
})
|
|
155
|
+
if (!row || row.kind !== 'folder') return state
|
|
156
|
+
const nextCollapsed = { ...state.gitMode.collapsedFolders }
|
|
157
|
+
if (action.key in nextCollapsed) {
|
|
158
|
+
delete nextCollapsed[action.key]
|
|
159
|
+
} else {
|
|
160
|
+
nextCollapsed[action.key] = true
|
|
161
|
+
}
|
|
162
|
+
return { ...state, gitMode: { ...state.gitMode, collapsedFolders: nextCollapsed } }
|
|
163
|
+
}
|
|
164
|
+
case 'git-mode-toggle-selected-folder': {
|
|
165
|
+
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
166
|
+
collapsedFolders: state.gitMode.collapsedFolders,
|
|
167
|
+
fileListMode: state.gitPane.fileListMode,
|
|
168
|
+
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
169
|
+
})
|
|
170
|
+
if (!row || row.kind !== 'folder') return state
|
|
171
|
+
const nextCollapsed = { ...state.gitMode.collapsedFolders }
|
|
172
|
+
if (row.key in nextCollapsed) {
|
|
173
|
+
delete nextCollapsed[row.key]
|
|
174
|
+
} else {
|
|
175
|
+
nextCollapsed[row.key] = true
|
|
176
|
+
}
|
|
177
|
+
return { ...state, gitMode: { ...state.gitMode, collapsedFolders: nextCollapsed } }
|
|
178
|
+
}
|
|
179
|
+
case 'git-mode-collapse-selection': {
|
|
180
|
+
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
181
|
+
collapsedFolders: state.gitMode.collapsedFolders,
|
|
182
|
+
fileListMode: state.gitPane.fileListMode,
|
|
183
|
+
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
184
|
+
})
|
|
185
|
+
if (!row) return state
|
|
186
|
+
if (row.kind === 'folder' && !row.isCollapsed) {
|
|
187
|
+
return {
|
|
188
|
+
...state,
|
|
189
|
+
gitMode: {
|
|
190
|
+
...state.gitMode,
|
|
191
|
+
collapsedFolders: { ...state.gitMode.collapsedFolders, [row.key]: true },
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!row.parentKey || row.parentKey === state.gitMode.selectedEntryKey) return state
|
|
196
|
+
return {
|
|
197
|
+
...state,
|
|
198
|
+
gitMode: {
|
|
199
|
+
...state.gitMode,
|
|
200
|
+
pendingDeletePath: null,
|
|
201
|
+
selectedEntryKey: row.parentKey,
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
case 'git-mode-expand-selection': {
|
|
206
|
+
const row = getSelectedGitRow(state.gitPanel.files, {
|
|
207
|
+
collapsedFolders: state.gitMode.collapsedFolders,
|
|
208
|
+
fileListMode: state.gitPane.fileListMode,
|
|
209
|
+
selectedEntryKey: state.gitMode.selectedEntryKey,
|
|
210
|
+
})
|
|
211
|
+
if (!row || row.kind !== 'folder' || !row.isCollapsed) return state
|
|
212
|
+
const nextCollapsed = { ...state.gitMode.collapsedFolders }
|
|
213
|
+
delete nextCollapsed[row.key]
|
|
214
|
+
return { ...state, gitMode: { ...state.gitMode, collapsedFolders: nextCollapsed } }
|
|
215
|
+
}
|
|
216
|
+
case 'git-mode-toggle-file-list-mode': {
|
|
217
|
+
const fileListMode = state.gitPane.fileListMode === 'tree' ? 'flat' : 'tree'
|
|
39
218
|
return {
|
|
40
219
|
...state,
|
|
41
220
|
gitMode: {
|
|
42
221
|
...state.gitMode,
|
|
43
222
|
pendingDeletePath: null,
|
|
44
|
-
|
|
223
|
+
selectedEntryKey: reconcileSelectedGitEntryKey(
|
|
224
|
+
state.gitPanel.files,
|
|
225
|
+
state.gitMode.collapsedFolders,
|
|
226
|
+
fileListMode,
|
|
227
|
+
state.gitMode.selectedEntryKey
|
|
228
|
+
),
|
|
45
229
|
},
|
|
230
|
+
gitPane: { ...state.gitPane, fileListMode },
|
|
46
231
|
}
|
|
47
232
|
}
|
|
48
233
|
case 'git-mode-set-diff': {
|
|
49
234
|
const nextLoading = { ...state.gitMode.loading }
|
|
50
|
-
delete nextLoading[action.
|
|
235
|
+
delete nextLoading[action.key]
|
|
51
236
|
return {
|
|
52
237
|
...state,
|
|
53
238
|
gitMode: {
|
|
54
239
|
...state.gitMode,
|
|
55
|
-
diffs: { ...state.gitMode.diffs, [action.
|
|
240
|
+
diffs: { ...state.gitMode.diffs, [action.key]: action.diff },
|
|
56
241
|
loading: nextLoading,
|
|
57
242
|
},
|
|
58
243
|
}
|
|
@@ -60,9 +245,9 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
60
245
|
case 'git-mode-set-loading': {
|
|
61
246
|
const nextLoading = { ...state.gitMode.loading }
|
|
62
247
|
if (action.loading) {
|
|
63
|
-
nextLoading[action.
|
|
248
|
+
nextLoading[action.key] = true
|
|
64
249
|
} else {
|
|
65
|
-
delete nextLoading[action.
|
|
250
|
+
delete nextLoading[action.key]
|
|
66
251
|
}
|
|
67
252
|
return { ...state, gitMode: { ...state.gitMode, loading: nextLoading } }
|
|
68
253
|
}
|
|
@@ -71,17 +256,33 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
71
256
|
return { ...state, gitMode: { ...state.gitMode, pendingDeletePath: action.path } }
|
|
72
257
|
}
|
|
73
258
|
case 'git-mode-clear-diff-cache': {
|
|
74
|
-
|
|
75
|
-
const nextDiffs = { ...state.gitMode.diffs }
|
|
76
|
-
delete nextDiffs[action.path]
|
|
77
|
-
return { ...state, gitMode: { ...state.gitMode, diffs: nextDiffs } }
|
|
259
|
+
return clearDiffCacheForPath(state, action.path)
|
|
78
260
|
}
|
|
79
261
|
case 'git-mode-set-message': {
|
|
80
262
|
if (state.gitMode.actionMessage === action.message) return state
|
|
81
263
|
return { ...state, gitMode: { ...state.gitMode, actionMessage: action.message } }
|
|
82
264
|
}
|
|
265
|
+
case 'git-mode-toggle-diff-view': {
|
|
266
|
+
const next = state.gitMode.diffView === 'split' ? 'stacked' : 'split'
|
|
267
|
+
return { ...state, gitMode: { ...state.gitMode, diffView: next } }
|
|
268
|
+
}
|
|
269
|
+
case 'git-mode-fold-adjust': {
|
|
270
|
+
const perPath = state.gitMode.folds[action.key] ?? {}
|
|
271
|
+
const prev = perPath[action.foldId] ?? { bottom: 0, top: 0 }
|
|
272
|
+
const nextSide = Math.max(0, prev[action.side] + action.delta)
|
|
273
|
+
const nextFold =
|
|
274
|
+
action.side === 'top'
|
|
275
|
+
? { bottom: prev.bottom, top: nextSide }
|
|
276
|
+
: { bottom: nextSide, top: prev.top }
|
|
277
|
+
return applyFold(state, action.key, action.foldId, nextFold)
|
|
278
|
+
}
|
|
279
|
+
case 'git-mode-fold-set': {
|
|
280
|
+
const top = Math.max(0, action.top)
|
|
281
|
+
const bottom = Math.max(0, action.bottom)
|
|
282
|
+
return applyFold(state, action.key, action.foldId, { bottom, top })
|
|
283
|
+
}
|
|
83
284
|
case 'git-mode-optimistic-move': {
|
|
84
|
-
const
|
|
285
|
+
const currentKey = state.gitMode.selectedEntryKey
|
|
85
286
|
const files = state.gitPanel.files
|
|
86
287
|
const idx = files.findIndex((f) => f.path === action.path && f.section === action.fromSection)
|
|
87
288
|
if (idx < 0) return state
|
|
@@ -103,29 +304,25 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
103
304
|
}
|
|
104
305
|
}
|
|
105
306
|
nextFiles = sortFilesBySection(nextFiles)
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} else {
|
|
119
|
-
nextIdx = Math.min(currentIdx, newLen - 1)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
307
|
+
const movedCurrentKey = gitFileKey({ path: action.path, section: action.fromSection })
|
|
308
|
+
const preferredSelection =
|
|
309
|
+
currentKey === movedCurrentKey && toSection !== null
|
|
310
|
+
? [gitFileKey({ path: action.path, section: toSection })]
|
|
311
|
+
: []
|
|
312
|
+
const nextSelectedEntryKey = reconcileSelectedGitEntryKey(
|
|
313
|
+
nextFiles,
|
|
314
|
+
state.gitMode.collapsedFolders,
|
|
315
|
+
state.gitPane.fileListMode,
|
|
316
|
+
currentKey,
|
|
317
|
+
preferredSelection
|
|
318
|
+
)
|
|
122
319
|
|
|
123
320
|
return {
|
|
124
321
|
...state,
|
|
125
322
|
gitMode: {
|
|
126
323
|
...state.gitMode,
|
|
127
324
|
pendingDeletePath: null,
|
|
128
|
-
|
|
325
|
+
selectedEntryKey: nextSelectedEntryKey,
|
|
129
326
|
},
|
|
130
327
|
gitPanel: { ...state.gitPanel, files: nextFiles },
|
|
131
328
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { AppAction, AppState, GitFileEntry, GitFileSection, GitPanelState } from '../types'
|
|
2
2
|
|
|
3
|
+
import { reconcileSelectedGitEntryKey } from '../git-tree'
|
|
4
|
+
|
|
3
5
|
export const GIT_PANEL_MIN_RATIO = 0.2
|
|
4
6
|
export const GIT_PANEL_MAX_RATIO = 0.8
|
|
5
7
|
|
|
@@ -63,6 +65,11 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
63
65
|
if (nextRatio === state.gitPane.ratio) return state
|
|
64
66
|
return { ...state, gitPane: { ...state.gitPane, ratio: nextRatio } }
|
|
65
67
|
}
|
|
68
|
+
case 'resize-git-diff-pane': {
|
|
69
|
+
const nextRatio = clampRatio(state.gitPane.diffModeRatio + action.delta)
|
|
70
|
+
if (nextRatio === state.gitPane.diffModeRatio) return state
|
|
71
|
+
return { ...state, gitPane: { ...state.gitPane, diffModeRatio: nextRatio } }
|
|
72
|
+
}
|
|
66
73
|
case 'set-git-pane-mode': {
|
|
67
74
|
if (state.gitPane.mode === action.mode) return state
|
|
68
75
|
const isEmbedded = action.mode === 'embedded'
|
|
@@ -93,17 +100,25 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
93
100
|
const prev = state.gitPanel
|
|
94
101
|
const next = action.payload
|
|
95
102
|
const sortedNext = sortFilesBySection(next.files)
|
|
103
|
+
const nextSelectedEntryKey = reconcileSelectedGitEntryKey(
|
|
104
|
+
sortedNext,
|
|
105
|
+
state.gitMode.collapsedFolders,
|
|
106
|
+
state.gitPane.fileListMode,
|
|
107
|
+
state.gitMode.selectedEntryKey
|
|
108
|
+
)
|
|
96
109
|
if (
|
|
97
110
|
prev.branch === next.branch &&
|
|
98
111
|
prev.ahead === next.ahead &&
|
|
99
112
|
prev.behind === next.behind &&
|
|
100
113
|
prev.error === null &&
|
|
101
|
-
sameFiles(prev.files, sortedNext)
|
|
114
|
+
sameFiles(prev.files, sortedNext) &&
|
|
115
|
+
nextSelectedEntryKey === state.gitMode.selectedEntryKey
|
|
102
116
|
) {
|
|
103
117
|
return state
|
|
104
118
|
}
|
|
105
119
|
return {
|
|
106
120
|
...state,
|
|
121
|
+
gitMode: { ...state.gitMode, selectedEntryKey: nextSelectedEntryKey },
|
|
107
122
|
gitPanel: {
|
|
108
123
|
...prev,
|
|
109
124
|
ahead: next.ahead,
|
|
@@ -116,9 +131,16 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
116
131
|
}
|
|
117
132
|
case 'git-refresh-error': {
|
|
118
133
|
const prev = state.gitPanel
|
|
119
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
prev.error === action.kind &&
|
|
136
|
+
prev.files.length === 0 &&
|
|
137
|
+
state.gitMode.selectedEntryKey === null
|
|
138
|
+
) {
|
|
139
|
+
return state
|
|
140
|
+
}
|
|
120
141
|
return {
|
|
121
142
|
...state,
|
|
143
|
+
gitMode: { ...state.gitMode, pendingDeletePath: null, selectedEntryKey: null },
|
|
122
144
|
gitPanel: { ...prev, error: action.kind, files: [] },
|
|
123
145
|
}
|
|
124
146
|
}
|
|
@@ -133,7 +155,11 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
133
155
|
) {
|
|
134
156
|
return state
|
|
135
157
|
}
|
|
136
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
...state,
|
|
160
|
+
gitMode: { ...state.gitMode, pendingDeletePath: null, selectedEntryKey: null },
|
|
161
|
+
gitPanel: emptyGitPanel(),
|
|
162
|
+
}
|
|
137
163
|
}
|
|
138
164
|
default:
|
|
139
165
|
return null
|