@brimveyn/aimux 1.12.6 → 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.
Files changed (130) hide show
  1. package/README.md +3 -0
  2. package/package.json +4 -3
  3. package/src/app-runtime/auto-commit-driver.ts +8 -4
  4. package/src/app-runtime/auto-commit-ref.ts +1 -1
  5. package/src/app-runtime/backend-attach-runtime.ts +18 -2
  6. package/src/app-runtime/backend-runtime-events.ts +1 -1
  7. package/src/app-runtime/selection-scroll.ts +2 -2
  8. package/src/app-runtime/session-actions.ts +16 -4
  9. package/src/app-runtime/side-effects.ts +538 -83
  10. package/src/app-runtime/snippet-actions.ts +3 -3
  11. package/src/app-runtime/use-auto-commit-driver.ts +7 -4
  12. package/src/app-runtime/use-backend-runtime.ts +3 -3
  13. package/src/app-runtime/use-directory-search.ts +7 -5
  14. package/src/app-runtime/use-mouse-handlers.ts +11 -6
  15. package/src/app-runtime/use-pane-size-report.ts +1 -1
  16. package/src/app-runtime/use-renderer-bindings.ts +6 -5
  17. package/src/app.tsx +12 -7
  18. package/src/auto-commit/headless-commands.ts +4 -6
  19. package/src/daemon/daemon.ts +3 -3
  20. package/src/daemon/runtime-paths.ts +1 -1
  21. package/src/daemon/session-manager.ts +1 -1
  22. package/src/daemon/session-registry.ts +10 -3
  23. package/src/diff-parser/parse-patch-files.ts +18 -18
  24. package/src/git/command-queue.ts +1 -1
  25. package/src/git/divergence.ts +46 -0
  26. package/src/git/git-diff.ts +12 -5
  27. package/src/git/git-poller.ts +33 -11
  28. package/src/git/git-status.ts +14 -3
  29. package/src/git/move-worktree.ts +96 -0
  30. package/src/git/use-repo-discovery.ts +2 -1
  31. package/src/git/worktree-divergence-poller.ts +59 -0
  32. package/src/git/worktree.ts +99 -0
  33. package/src/index.tsx +1 -1
  34. package/src/input/keymap/describe-bindings.ts +27 -9
  35. package/src/input/keymap/key-chord.ts +4 -4
  36. package/src/input/keymap/key-format.ts +2 -2
  37. package/src/input/keymap/sequence-resolver.ts +1 -1
  38. package/src/input/keymap/trie.ts +1 -1
  39. package/src/input/modes/bridge.ts +7 -0
  40. package/src/input/modes/transitions.ts +2 -1
  41. package/src/input/modes/types.ts +11 -1
  42. package/src/input/raw-input-handler.ts +1 -1
  43. package/src/input/terminal-text-extraction.ts +9 -5
  44. package/src/integrations/claude-syntax-overlay.ts +8 -8
  45. package/src/integrations/claude-theme-sync.ts +12 -12
  46. package/src/ipc/protocol.ts +3 -3
  47. package/src/platform/clipboard.ts +4 -2
  48. package/src/platform/worktree-deps.ts +22 -0
  49. package/src/platform/worktree-paths.ts +67 -0
  50. package/src/profile-paths.ts +3 -3
  51. package/src/pty/assistant-status-detection-loop.ts +2 -2
  52. package/src/pty/assistant-status-detector.ts +15 -6
  53. package/src/pty/command-registry.ts +2 -1
  54. package/src/pty/pty-manager.ts +1 -1
  55. package/src/services/ai-usage/adapters/claude.ts +5 -5
  56. package/src/services/ai-usage/adapters/codex.ts +8 -8
  57. package/src/services/ai-usage/cache.ts +2 -2
  58. package/src/services/ai-usage/pace.ts +3 -6
  59. package/src/services/ai-usage/provider.ts +1 -1
  60. package/src/session-backend/bootstrap.ts +2 -2
  61. package/src/session-backend/local-session-backend.ts +11 -11
  62. package/src/session-backend/remote-session-backend.ts +3 -3
  63. package/src/session-backend/types.ts +2 -2
  64. package/src/snippets/expand-variables.ts +7 -7
  65. package/src/snippets/run-shell-var.ts +1 -1
  66. package/src/snippets/trigger-detector.ts +1 -1
  67. package/src/state/ai-usage-store.ts +1 -1
  68. package/src/state/git-tree.ts +8 -4
  69. package/src/state/layout-tree.ts +8 -5
  70. package/src/state/reducers/git-mode-state.ts +44 -14
  71. package/src/state/reducers/git-panel-state.ts +38 -2
  72. package/src/state/reducers/modal-state.ts +355 -21
  73. package/src/state/reducers/session-state.ts +60 -1
  74. package/src/state/reducers/tab-state.ts +151 -85
  75. package/src/state/selectors.ts +6 -4
  76. package/src/state/session-catalog.ts +20 -5
  77. package/src/state/session-persistence.ts +8 -4
  78. package/src/state/session-worktrees.ts +227 -0
  79. package/src/state/store.ts +1 -0
  80. package/src/state/toast-store.ts +95 -0
  81. package/src/state/types.ts +92 -13
  82. package/src/state/validation.ts +25 -2
  83. package/src/terminal-manager/manager-client.ts +15 -13
  84. package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
  85. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
  86. package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
  87. package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
  88. package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
  89. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
  90. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
  91. package/src/ui/components/git/git-panel.tsx +17 -10
  92. package/src/ui/components/git/git-view.tsx +79 -19
  93. package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
  94. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
  95. package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
  96. package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
  97. package/src/ui/components/layout/session-bar.tsx +3 -3
  98. package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
  99. package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
  100. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
  101. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
  102. package/src/ui/components/layout/split-layout.tsx +1 -1
  103. package/src/ui/components/layout/terminal-pane.tsx +43 -40
  104. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  105. package/src/ui/components/modals/app/help-modal.tsx +4 -2
  106. package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
  107. package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
  108. package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
  109. package/src/ui/components/modals/shared/form.tsx +4 -5
  110. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  111. package/src/ui/components/modals/shared/picker.tsx +28 -5
  112. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +11 -3
  113. package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
  114. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
  115. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
  116. package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
  117. package/src/ui/components/overlays/toast/toast-position.ts +45 -0
  118. package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
  119. package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
  120. package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
  121. package/src/ui/components/primitives/input-field.tsx +1 -1
  122. package/src/ui/components/primitives/list-item.tsx +3 -3
  123. package/src/ui/filter-themes.ts +1 -1
  124. package/src/ui/root.tsx +57 -3
  125. package/src/ui/session-ordering.ts +2 -2
  126. package/src/ui/status-bar-model.ts +14 -7
  127. package/src/ui/terminal-graphics/capabilities.ts +1 -1
  128. package/src/ui/terminal-graphics/format-fallback.ts +7 -9
  129. package/src/ui/terminal-graphics/kitty.ts +2 -7
  130. package/src/update/version-check.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from 'node:events'
2
- import { connect, Socket } from 'node:net'
2
+ import { connect, type Socket } from 'node:net'
3
3
 
4
4
  import type { AssistantId, ScrollIntent, WorkspaceSnapshotV1 } from '../state/types'
5
5
  import type { SessionBackend, SessionBackendEvents } from './types'
@@ -96,7 +96,7 @@ export class RemoteSessionBackend
96
96
  return this.socket
97
97
  }
98
98
 
99
- private send(request: ClientRequest): Promise<ServerResponse> {
99
+ private async send(request: ClientRequest): Promise<ServerResponse> {
100
100
  const socket = this.getConnectedSocket()
101
101
  logDebug('backend.remote.send', { id: request.id, type: request.type })
102
102
  return new Promise((resolve, reject) => {
@@ -139,7 +139,7 @@ export class RemoteSessionBackend
139
139
  private reportCommandError(context: string, error: unknown, tabId?: string): void {
140
140
  const message = error instanceof Error ? error.message : String(error)
141
141
  logDebug('backend.remote.commandError', { context, error: message, tabId })
142
- if (tabId) {
142
+ if (tabId != null && tabId !== '') {
143
143
  this.emit('error', tabId, message)
144
144
  }
145
145
  }
@@ -10,7 +10,7 @@ import type {
10
10
  WorkspaceSnapshotV1,
11
11
  } from '../state/types'
12
12
 
13
- export type SessionBackendEvents = {
13
+ export interface SessionBackendEvents {
14
14
  render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
15
15
  exit: [tabId: string, exitCode: number]
16
16
  error: [tabId: string, message: string]
@@ -26,7 +26,7 @@ export interface BackendAttachResult {
26
26
  * atomically with tab hydration to prevent an unknown-tab race on
27
27
  * separate `sessionActivity` events.
28
28
  */
29
- initialSessionStatuses: Array<{ sessionId: string; status: SessionStatus }>
29
+ initialSessionStatuses: { sessionId: string; status: SessionStatus }[]
30
30
  }
31
31
 
32
32
  export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
@@ -34,16 +34,16 @@ const VAR_RE = /\{\{([^{}]+)\}\}/g
34
34
  const TWO = (n: number): string => String(n).padStart(2, '0')
35
35
 
36
36
  function formatDate(date: Date, format?: string): string {
37
- if (!format) {
37
+ if (!(format != null && format !== '')) {
38
38
  return `${date.getFullYear()}-${TWO(date.getMonth() + 1)}-${TWO(date.getDate())}`
39
39
  }
40
40
  return format
41
- .replace(/YYYY/g, String(date.getFullYear()))
42
- .replace(/MM/g, TWO(date.getMonth() + 1))
43
- .replace(/DD/g, TWO(date.getDate()))
44
- .replace(/HH/g, TWO(date.getHours()))
45
- .replace(/mm/g, TWO(date.getMinutes()))
46
- .replace(/ss/g, TWO(date.getSeconds()))
41
+ .replaceAll('YYYY', String(date.getFullYear()))
42
+ .replaceAll('MM', TWO(date.getMonth() + 1))
43
+ .replaceAll('DD', TWO(date.getDate()))
44
+ .replaceAll('HH', TWO(date.getHours()))
45
+ .replaceAll('mm', TWO(date.getMinutes()))
46
+ .replaceAll('ss', TWO(date.getSeconds()))
47
47
  }
48
48
 
49
49
  function resolveSyncVariable(name: string, ctx: SyncExpansionContext): string | null {
@@ -14,7 +14,7 @@ const MAX_TIMEOUT_MS = 30_000
14
14
  */
15
15
  function buildShellArgv(cmd: string): string[] {
16
16
  const userShell = process.env.SHELL
17
- if (userShell && userShell.length > 0) {
17
+ if (userShell != null && userShell !== '' && userShell.length > 0) {
18
18
  return [userShell, '-l', '-c', cmd]
19
19
  }
20
20
  return ['/bin/sh', '-c', cmd]
@@ -50,7 +50,7 @@ export function createTriggerDetector(opts: TriggerDetectorOptions): TriggerDete
50
50
  function tryMatch(separator: string): TriggerMatch | null {
51
51
  const triggerChar = opts.getTriggerChar()
52
52
  for (const snippet of opts.getSnippets()) {
53
- if (snippet.trigger && snippet.trigger === buffer) {
53
+ if (snippet.trigger != null && snippet.trigger !== '' && snippet.trigger === buffer) {
54
54
  const triggerText = `${triggerChar}${buffer}${separator}`
55
55
  reset()
56
56
  return { snippet, triggerText }
@@ -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 } }
@@ -42,8 +42,12 @@ export interface GitTreeRows {
42
42
  visibleRows: GitTreeRow[]
43
43
  }
44
44
 
45
- export function gitFileKey(file: Pick<GitFileEntry, 'path' | 'section'>): string {
46
- return `${file.section}:${file.path}`
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) {
@@ -1,7 +1,10 @@
1
1
  export type SplitDirection = 'horizontal' | 'vertical'
2
2
 
3
- export type LayoutLeaf = { type: 'leaf'; tabId: string }
4
- export type LayoutSplit = {
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 (!next || next === state.gitMode.selectedEntryKey) return state
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 (!next || next === state.gitMode.selectedEntryKey) return state
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 (!next || next !== action.key || next === state.gitMode.selectedEntryKey) return state
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 (!row.parentKey || row.parentKey === state.gitMode.selectedEntryKey) return state
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
- if (duplicateIdx >= 0) {
489
- nextFiles = files.filter((_, i) => i !== idx)
490
- } else {
491
- nextFiles = files.map((f, i) =>
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 { AppAction, AppState, GitFileEntry, GitFileSection, GitPanelState } from '../types'
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
- gitMode: { ...state.gitMode, pendingDeletePath: null, selectedEntryKey: null },
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
  }