@brimveyn/aimux 1.12.2 → 1.13.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 +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +18 -2
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/pty-write.ts +52 -0
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +598 -102
- package/src/app-runtime/snippet-actions.ts +25 -11
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +143 -8
- package/src/app.tsx +25 -9
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +3 -3
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +1 -1
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +13 -1
- package/src/input/raw-input-handler.ts +34 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +18 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +8 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +3 -3
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +385 -24
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/snippet-catalog.ts +65 -4
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +108 -16
- package/src/state/validation.ts +47 -3
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
- package/src/ui/components/git/git-panel.tsx +17 -10
- package/src/ui/components/git/git-view.tsx +79 -19
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +3 -3
- package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
- package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +1 -1
- package/src/ui/components/layout/terminal-pane.tsx +43 -40
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +4 -2
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
- package/src/ui/components/modals/shared/form.tsx +4 -5
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +28 -5
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +3 -3
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +65 -14
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { SnippetRecord } from '../state/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inline trigger detector for snippet/macro expansion.
|
|
5
|
+
*
|
|
6
|
+
* State machine (per-tab):
|
|
7
|
+
* Idle ──(trigger char)──▶ Capturing
|
|
8
|
+
* Capturing ──(separator)──▶ try match → return or reset → Idle
|
|
9
|
+
* Capturing ──(printable char)──▶ append (max 32 chars)
|
|
10
|
+
* Capturing ──(non-printable / control / escape)──▶ reset → Idle
|
|
11
|
+
* Capturing ──(trigger char again)──▶ restart Capturing
|
|
12
|
+
*
|
|
13
|
+
* Paste heuristic: if two `feed` calls land within 5ms, treat as paste
|
|
14
|
+
* (sometimes terminals don't enable bracketed paste) and reset.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MAX_TRIGGER_BUFFER = 32
|
|
18
|
+
const SEPARATOR_RE = /[\s.,;:!?)\]}]/u
|
|
19
|
+
const PRINTABLE_RE = /^[\x20-\x7e]$/
|
|
20
|
+
const PASTE_WINDOW_MS = 5
|
|
21
|
+
|
|
22
|
+
export interface TriggerMatch {
|
|
23
|
+
snippet: SnippetRecord
|
|
24
|
+
/** Characters typed since trigger char (inclusive), including the closing separator. */
|
|
25
|
+
triggerText: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TriggerDetector {
|
|
29
|
+
feed(char: string): TriggerMatch | null
|
|
30
|
+
reset(): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TriggerDetectorOptions {
|
|
34
|
+
getSnippets: () => readonly SnippetRecord[]
|
|
35
|
+
getTriggerChar: () => string
|
|
36
|
+
now?: () => number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createTriggerDetector(opts: TriggerDetectorOptions): TriggerDetector {
|
|
40
|
+
const now = opts.now ?? (() => Date.now())
|
|
41
|
+
let capturing = false
|
|
42
|
+
let buffer = ''
|
|
43
|
+
let lastFeedAt = 0
|
|
44
|
+
|
|
45
|
+
function reset(): void {
|
|
46
|
+
capturing = false
|
|
47
|
+
buffer = ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function tryMatch(separator: string): TriggerMatch | null {
|
|
51
|
+
const triggerChar = opts.getTriggerChar()
|
|
52
|
+
for (const snippet of opts.getSnippets()) {
|
|
53
|
+
if (snippet.trigger != null && snippet.trigger !== '' && snippet.trigger === buffer) {
|
|
54
|
+
const triggerText = `${triggerChar}${buffer}${separator}`
|
|
55
|
+
reset()
|
|
56
|
+
return { snippet, triggerText }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
reset()
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
feed(char: string): TriggerMatch | null {
|
|
65
|
+
const t = now()
|
|
66
|
+
const isPaste = capturing && t - lastFeedAt < PASTE_WINDOW_MS && buffer.length > 0
|
|
67
|
+
lastFeedAt = t
|
|
68
|
+
|
|
69
|
+
if (isPaste) {
|
|
70
|
+
reset()
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const triggerChar = opts.getTriggerChar()
|
|
75
|
+
|
|
76
|
+
if (char === triggerChar) {
|
|
77
|
+
capturing = true
|
|
78
|
+
buffer = ''
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!capturing) return null
|
|
83
|
+
|
|
84
|
+
if (SEPARATOR_RE.test(char)) {
|
|
85
|
+
if (buffer.length === 0) {
|
|
86
|
+
reset()
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
return tryMatch(char)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!PRINTABLE_RE.test(char)) {
|
|
93
|
+
reset()
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
buffer += char
|
|
98
|
+
if (buffer.length > MAX_TRIGGER_BUFFER) {
|
|
99
|
+
reset()
|
|
100
|
+
}
|
|
101
|
+
return null
|
|
102
|
+
},
|
|
103
|
+
reset,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -23,7 +23,7 @@ export const aiUsageStore = createStore<AIUsageState>((set) => ({
|
|
|
23
23
|
const isFailure = Boolean(snap.error)
|
|
24
24
|
const hasPriorValue = prev && prev.percent !== null
|
|
25
25
|
const merged: UsageSnapshot =
|
|
26
|
-
isFailure && hasPriorValue && prev
|
|
26
|
+
isFailure && hasPriorValue === true && prev != null
|
|
27
27
|
? { ...prev, error: snap.error, lastUpdated: snap.lastUpdated, stale: true }
|
|
28
28
|
: snap
|
|
29
29
|
return { snapshots: { ...state.snapshots, [snap.tool]: merged } }
|
package/src/state/git-tree.ts
CHANGED
|
@@ -42,8 +42,12 @@ export interface GitTreeRows {
|
|
|
42
42
|
visibleRows: GitTreeRow[]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function gitFileKey(
|
|
46
|
-
|
|
45
|
+
export function gitFileKey(
|
|
46
|
+
file: Pick<GitFileEntry, 'path' | 'section'> & { repoPath?: string }
|
|
47
|
+
): string {
|
|
48
|
+
return file.repoPath != null && file.repoPath !== ''
|
|
49
|
+
? `${file.section}:${file.repoPath}:${file.path}`
|
|
50
|
+
: `${file.section}:${file.path}`
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
export function gitFolderKey(section: GitFileSection, folderPath: string): string {
|
|
@@ -85,7 +89,7 @@ export function getSelectedGitRow(
|
|
|
85
89
|
compact?: boolean
|
|
86
90
|
}
|
|
87
91
|
): GitTreeRow | null {
|
|
88
|
-
if (!options.selectedEntryKey) return null
|
|
92
|
+
if (!(options.selectedEntryKey != null && options.selectedEntryKey !== '')) return null
|
|
89
93
|
const { visibleRows } = buildGitTreeRows(
|
|
90
94
|
files,
|
|
91
95
|
options.collapsedFolders,
|
|
@@ -181,7 +185,7 @@ function buildSectionTree(files: GitFileEntry[], section: GitFileSection): GitTr
|
|
|
181
185
|
let cursor = root
|
|
182
186
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
183
187
|
const name = parts[i]
|
|
184
|
-
if (!name) continue
|
|
188
|
+
if (!(name != null && name !== '')) continue
|
|
185
189
|
const nextPath = cursor.path ? `${cursor.path}/${name}` : name
|
|
186
190
|
let child = cursor.folders.get(name)
|
|
187
191
|
if (!child) {
|
package/src/state/layout-tree.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export type SplitDirection = 'horizontal' | 'vertical'
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export interface LayoutLeaf {
|
|
4
|
+
type: 'leaf'
|
|
5
|
+
tabId: string
|
|
6
|
+
}
|
|
7
|
+
export interface LayoutSplit {
|
|
5
8
|
type: 'split'
|
|
6
9
|
direction: SplitDirection
|
|
7
10
|
ratio: number
|
|
@@ -42,7 +45,7 @@ export function getTreeForTab(
|
|
|
42
45
|
tabId: string
|
|
43
46
|
): LayoutNode | null {
|
|
44
47
|
const groupId = tabGroupMap[tabId]
|
|
45
|
-
if (!groupId) return null
|
|
48
|
+
if (!(groupId != null && groupId !== '')) return null
|
|
46
49
|
return layoutTrees[groupId] ?? null
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -303,14 +306,14 @@ export function getAdjacentLeaf(
|
|
|
303
306
|
if (inFirst && goingToSecond) {
|
|
304
307
|
// Try to recurse in first child first
|
|
305
308
|
const deeper = getAdjacentLeaf(tree.first, fromTabId, direction)
|
|
306
|
-
if (deeper) return deeper
|
|
309
|
+
if (deeper != null && deeper !== '') return deeper
|
|
307
310
|
// Cross boundary: go to first leaf of second child
|
|
308
311
|
return firstLeafId(tree.second)
|
|
309
312
|
}
|
|
310
313
|
|
|
311
314
|
if (inSecond && !goingToSecond) {
|
|
312
315
|
const deeper = getAdjacentLeaf(tree.second, fromTabId, direction)
|
|
313
|
-
if (deeper) return deeper
|
|
316
|
+
if (deeper != null && deeper !== '') return deeper
|
|
314
317
|
return lastLeafId(tree.first)
|
|
315
318
|
}
|
|
316
319
|
}
|
|
@@ -50,6 +50,7 @@ export function emptyGitMode(): GitModeState {
|
|
|
50
50
|
loading: {},
|
|
51
51
|
parsedFiles: {},
|
|
52
52
|
pendingDeletePath: null,
|
|
53
|
+
reviewBase: false,
|
|
53
54
|
selectedEntryKey: null,
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -90,6 +91,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
90
91
|
highlights: {},
|
|
91
92
|
loading: {},
|
|
92
93
|
parsedFiles: {},
|
|
94
|
+
reviewBase: false,
|
|
93
95
|
},
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -102,7 +104,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
102
104
|
action.delta,
|
|
103
105
|
state.gitPane.treeCompaction
|
|
104
106
|
)
|
|
105
|
-
if (
|
|
107
|
+
if (next == null || next === '' || next === state.gitMode.selectedEntryKey) return state
|
|
106
108
|
return {
|
|
107
109
|
...state,
|
|
108
110
|
gitMode: {
|
|
@@ -121,7 +123,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
121
123
|
action.delta,
|
|
122
124
|
state.gitPane.treeCompaction
|
|
123
125
|
)
|
|
124
|
-
if (
|
|
126
|
+
if (next == null || next === '' || next === state.gitMode.selectedEntryKey) return state
|
|
125
127
|
return {
|
|
126
128
|
...state,
|
|
127
129
|
gitMode: {
|
|
@@ -140,7 +142,13 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
140
142
|
[action.key],
|
|
141
143
|
state.gitPane.treeCompaction
|
|
142
144
|
)
|
|
143
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
next == null ||
|
|
147
|
+
next === '' ||
|
|
148
|
+
next !== action.key ||
|
|
149
|
+
next === state.gitMode.selectedEntryKey
|
|
150
|
+
)
|
|
151
|
+
return state
|
|
144
152
|
return {
|
|
145
153
|
...state,
|
|
146
154
|
gitMode: {
|
|
@@ -199,7 +207,12 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
199
207
|
},
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
|
-
if (
|
|
210
|
+
if (
|
|
211
|
+
row.parentKey == null ||
|
|
212
|
+
row.parentKey === '' ||
|
|
213
|
+
row.parentKey === state.gitMode.selectedEntryKey
|
|
214
|
+
)
|
|
215
|
+
return state
|
|
203
216
|
return {
|
|
204
217
|
...state,
|
|
205
218
|
gitMode: {
|
|
@@ -318,7 +331,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
318
331
|
const k = `${action.key}|${action.themeId}`
|
|
319
332
|
const existing = state.gitMode.highlights[k]
|
|
320
333
|
const sameContent = existing && existing.hash === action.hash
|
|
321
|
-
if (sameContent) {
|
|
334
|
+
if (sameContent === true) {
|
|
322
335
|
const existingAdd = existing.add as unknown[]
|
|
323
336
|
const existingDel = existing.del as unknown[]
|
|
324
337
|
let changesAnything = false
|
|
@@ -344,8 +357,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
344
357
|
}
|
|
345
358
|
if (!changesAnything) return state
|
|
346
359
|
}
|
|
347
|
-
const nextAdd = sameContent ? ([...(existing.add as unknown[])] as unknown[]) : []
|
|
348
|
-
const nextDel = sameContent ? ([...(existing.del as unknown[])] as unknown[]) : []
|
|
360
|
+
const nextAdd = sameContent === true ? ([...(existing.add as unknown[])] as unknown[]) : []
|
|
361
|
+
const nextDel = sameContent === true ? ([...(existing.del as unknown[])] as unknown[]) : []
|
|
349
362
|
for (const patch of action.add) {
|
|
350
363
|
const tokens = patch.tokens as unknown[]
|
|
351
364
|
for (let i = 0; i < tokens.length; i++) nextAdd[patch.start + i] = tokens[i]
|
|
@@ -398,6 +411,26 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
398
411
|
const next = state.gitMode.diffView === 'split' ? 'stacked' : 'split'
|
|
399
412
|
return { ...state, gitMode: { ...state.gitMode, diffView: next } }
|
|
400
413
|
}
|
|
414
|
+
case 'git-mode-toggle-review-base': {
|
|
415
|
+
// Base review and history walking are mutually exclusive views; entering
|
|
416
|
+
// either resets headOffset and drops cached diffs (computed vs old ref).
|
|
417
|
+
const next = !state.gitMode.reviewBase
|
|
418
|
+
return {
|
|
419
|
+
...state,
|
|
420
|
+
gitMode: {
|
|
421
|
+
...state.gitMode,
|
|
422
|
+
actionMessage: null,
|
|
423
|
+
diffs: {},
|
|
424
|
+
folds: {},
|
|
425
|
+
headOffset: 0,
|
|
426
|
+
highlights: {},
|
|
427
|
+
loading: {},
|
|
428
|
+
parsedFiles: {},
|
|
429
|
+
pendingDeletePath: null,
|
|
430
|
+
reviewBase: next,
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
}
|
|
401
434
|
case 'git-mode-shift-head-offset': {
|
|
402
435
|
const next = Math.max(0, state.gitMode.headOffset + action.delta)
|
|
403
436
|
if (next === state.gitMode.headOffset) return state
|
|
@@ -485,13 +518,10 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
485
518
|
const duplicateIdx = files.findIndex(
|
|
486
519
|
(f, i) => i !== idx && f.path === action.path && f.section === toSection
|
|
487
520
|
)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
i === idx ? ({ ...f, section: toSection } as GitFileEntry) : f
|
|
493
|
-
)
|
|
494
|
-
}
|
|
521
|
+
nextFiles =
|
|
522
|
+
duplicateIdx >= 0
|
|
523
|
+
? files.filter((_, i) => i !== idx)
|
|
524
|
+
: files.map((f, i) => (i === idx ? ({ ...f, section: toSection } as GitFileEntry) : f))
|
|
495
525
|
}
|
|
496
526
|
nextFiles = sortFilesBySection(nextFiles)
|
|
497
527
|
const movedCurrentKey = gitFileKey({ path: action.path, section: action.fromSection })
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AppAction,
|
|
3
|
+
AppState,
|
|
4
|
+
BranchDivergence,
|
|
5
|
+
GitFileEntry,
|
|
6
|
+
GitFileSection,
|
|
7
|
+
GitPanelState,
|
|
8
|
+
} from '../types'
|
|
2
9
|
|
|
3
10
|
import { clampGitPaneRatio } from '../git-pane-sizing'
|
|
4
11
|
import { reconcileSelectedGitEntryKey } from '../git-tree'
|
|
@@ -27,6 +34,20 @@ function clampRatio(value: number): number {
|
|
|
27
34
|
return clampGitPaneRatio(value)
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
function sameDivergence(
|
|
38
|
+
a: Record<string, BranchDivergence>,
|
|
39
|
+
b: Record<string, BranchDivergence>
|
|
40
|
+
): boolean {
|
|
41
|
+
const aKeys = Object.keys(a)
|
|
42
|
+
if (aKeys.length !== Object.keys(b).length) return false
|
|
43
|
+
for (const key of aKeys) {
|
|
44
|
+
const x = a[key]
|
|
45
|
+
const y = b[key]
|
|
46
|
+
if (x == null || y == null || x.ahead !== y.ahead || x.behind !== y.behind) return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
export function emptyGitPanel(): GitPanelState {
|
|
31
52
|
return {
|
|
32
53
|
ahead: 0,
|
|
@@ -188,6 +209,10 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
188
209
|
gitPanel: { ...prev, error: action.kind, files: [] },
|
|
189
210
|
}
|
|
190
211
|
}
|
|
212
|
+
case 'set-worktree-divergence': {
|
|
213
|
+
if (sameDivergence(state.worktreeDivergence, action.divergence)) return state
|
|
214
|
+
return { ...state, worktreeDivergence: action.divergence }
|
|
215
|
+
}
|
|
191
216
|
case 'git-panel-reset': {
|
|
192
217
|
const prev = state.gitPanel
|
|
193
218
|
if (
|
|
@@ -201,7 +226,18 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
201
226
|
}
|
|
202
227
|
return {
|
|
203
228
|
...state,
|
|
204
|
-
|
|
229
|
+
// A reset means the underlying worktree/ref changed, so cached diffs are
|
|
230
|
+
// stale — drop them (keyed by section:path, they'd otherwise serve a
|
|
231
|
+
// previous worktree's diff for a same-named file after a move/switch).
|
|
232
|
+
gitMode: {
|
|
233
|
+
...state.gitMode,
|
|
234
|
+
diffs: {},
|
|
235
|
+
highlights: {},
|
|
236
|
+
loading: {},
|
|
237
|
+
parsedFiles: {},
|
|
238
|
+
pendingDeletePath: null,
|
|
239
|
+
selectedEntryKey: null,
|
|
240
|
+
},
|
|
205
241
|
gitPanel: emptyGitPanel(),
|
|
206
242
|
}
|
|
207
243
|
}
|