@brimveyn/aimux 1.21.0 → 1.22.2
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 +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { basename } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
BranchDivergence,
|
|
8
|
+
ProjectRecord,
|
|
9
|
+
TabSession,
|
|
10
|
+
WorkspaceActivity,
|
|
11
|
+
WorkspaceRecord,
|
|
12
|
+
} from './types'
|
|
13
|
+
|
|
14
|
+
import { createPrefixedId } from '../platform/id'
|
|
15
|
+
import { isInsideAimuxWorktreeRoot } from '../platform/worktree-paths'
|
|
16
|
+
|
|
17
|
+
export function formatDivergence(divergence: BranchDivergence | undefined): string {
|
|
18
|
+
if (divergence == null) return ''
|
|
19
|
+
const parts: string[] = []
|
|
20
|
+
if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
|
|
21
|
+
if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
|
|
22
|
+
return parts.join(' ')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The workspace standing for the repo checkout itself.
|
|
27
|
+
*
|
|
28
|
+
* Falls back to the first record because a project loaded from an older
|
|
29
|
+
* catalog can have workspaces with no `primary` among them, and every caller
|
|
30
|
+
* wants *something* to anchor the project row on. Callers that need the real
|
|
31
|
+
* primary or nothing — the CLI, catalog repair — should keep asking for
|
|
32
|
+
* `source === 'primary'` directly and get `undefined` when it is absent.
|
|
33
|
+
*/
|
|
34
|
+
export function getPrimaryWorkspace(
|
|
35
|
+
workspaces: WorkspaceRecord[] | undefined
|
|
36
|
+
): WorkspaceRecord | undefined {
|
|
37
|
+
if (!workspaces) return undefined
|
|
38
|
+
return workspaces.find((workspace) => workspace.source === 'primary') ?? workspaces[0]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `4823` -> `4.8k`, so a churn number never widens the sidebar row. */
|
|
42
|
+
export function formatDiffCount(value: number): string {
|
|
43
|
+
if (value < 1_000) return String(value)
|
|
44
|
+
const thousands = value / 1_000
|
|
45
|
+
if (thousands < 10) return `${thousands.toFixed(1).replace(/\.0$/, '')}k`
|
|
46
|
+
return `${Math.round(thousands)}k`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `+149 -629` — lines the workspace changed since it forked. Empty when clean. */
|
|
50
|
+
export function formatDiffStat(divergence: BranchDivergence | undefined): {
|
|
51
|
+
added: string
|
|
52
|
+
removed: string
|
|
53
|
+
} {
|
|
54
|
+
const added = divergence?.added ?? 0
|
|
55
|
+
const removed = divergence?.removed ?? 0
|
|
56
|
+
return {
|
|
57
|
+
added: added > 0 ? `+${formatDiffCount(added)}` : '',
|
|
58
|
+
removed: removed > 0 ? `-${formatDiffCount(removed)}` : '',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What the repo checkout is called in the sidebar.
|
|
64
|
+
*
|
|
65
|
+
* Not the directory name: the project heading directly above it already says
|
|
66
|
+
* that, and a row repeating its own parent is what made the two look like the
|
|
67
|
+
* same thing. "root" names its role instead — the checkout the worktrees hang
|
|
68
|
+
* off — and the branch under it says which one it is.
|
|
69
|
+
*/
|
|
70
|
+
export const PRIMARY_WORKSPACE_NAME = 'root'
|
|
71
|
+
|
|
72
|
+
export function createPrimaryWorkspace(projectPath: string, now: string): WorkspaceRecord {
|
|
73
|
+
const id = createPrefixedId('workspace')
|
|
74
|
+
return {
|
|
75
|
+
createdAt: now,
|
|
76
|
+
createdByAimux: false,
|
|
77
|
+
id,
|
|
78
|
+
name: PRIMARY_WORKSPACE_NAME,
|
|
79
|
+
path: projectPath,
|
|
80
|
+
repoRoot: projectPath,
|
|
81
|
+
source: 'primary',
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Retire the old primary name — the directory's own basename — for `root`.
|
|
88
|
+
*
|
|
89
|
+
* Catalogs written before the checkout had a row of its own carry it, and there
|
|
90
|
+
* it duplicates the project heading it now sits under. Only that exact name is
|
|
91
|
+
* replaced: anything else is a name the user chose, and it stays.
|
|
92
|
+
*/
|
|
93
|
+
function renamePrimaryFromDirectoryName(workspaces: WorkspaceRecord[]): WorkspaceRecord[] {
|
|
94
|
+
return workspaces.map((workspace) =>
|
|
95
|
+
workspace.source === 'primary' && workspace.name === basename(workspace.path)
|
|
96
|
+
? { ...workspace, name: PRIMARY_WORKSPACE_NAME }
|
|
97
|
+
: workspace
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ensureProjectWorkspaces(
|
|
102
|
+
project: ProjectRecord,
|
|
103
|
+
now = new Date().toISOString()
|
|
104
|
+
): ProjectRecord {
|
|
105
|
+
if (project.workspaces?.length != null && project.workspaces?.length !== 0) {
|
|
106
|
+
const workspaces = renamePrimaryFromDirectoryName(
|
|
107
|
+
mergeExistingGitWorktrees(pruneMissingAimuxTempWorkspaces(project.workspaces), now)
|
|
108
|
+
)
|
|
109
|
+
if (workspaces.length === 0 && project.projectPath != null && project.projectPath !== '') {
|
|
110
|
+
const workspace = createPrimaryWorkspace(project.projectPath, now)
|
|
111
|
+
return { ...project, activeWorkspaceId: workspace.id, workspaces: [workspace] }
|
|
112
|
+
}
|
|
113
|
+
// Prefer a worktree when the stored selection is gone: coming back to a
|
|
114
|
+
// project, the branch you were on is a better guess than the trunk. Not a
|
|
115
|
+
// restriction — the checkout hosts tabs like any other workspace — just
|
|
116
|
+
// which one to open on. An explicit selection always wins.
|
|
117
|
+
const activeWorkspaceId = workspaces.some((w) => w.id === project.activeWorkspaceId)
|
|
118
|
+
? project.activeWorkspaceId
|
|
119
|
+
: (workspaces.find((w) => w.source !== 'primary') ?? workspaces[0])?.id
|
|
120
|
+
return {
|
|
121
|
+
...project,
|
|
122
|
+
activeWorkspaceId,
|
|
123
|
+
// projectPath stays pinned to the repo. The active workspace's path is a
|
|
124
|
+
// separate thing — read it with getActiveWorkspacePath.
|
|
125
|
+
projectPath: workspaces.find((w) => w.source === 'primary')?.repoRoot ?? project.projectPath,
|
|
126
|
+
workspaces,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!(project.projectPath != null && project.projectPath !== '')) {
|
|
131
|
+
return project
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const workspace = createPrimaryWorkspace(project.projectPath, now)
|
|
135
|
+
const workspaces = mergeExistingGitWorktrees([workspace], now)
|
|
136
|
+
// Same rule as above: an adopted worktree wins over the repo checkout. A
|
|
137
|
+
// project with nothing but its primary lands there and stays inert until
|
|
138
|
+
// `<C-p>`, which is the intended funnel.
|
|
139
|
+
return {
|
|
140
|
+
...project,
|
|
141
|
+
activeWorkspaceId: (workspaces.find((entry) => entry.source !== 'primary') ?? workspace).id,
|
|
142
|
+
workspaces,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stableWorkspaceId(path: string): string {
|
|
147
|
+
// Keeps the `worktree-` prefix on purpose: this id is re-derived from the
|
|
148
|
+
// path on every load, so changing it would re-key every adopted workspace
|
|
149
|
+
// and orphan the tabs bound to them.
|
|
150
|
+
return `worktree-${createHash('sha1').update(path).digest('hex').slice(0, 10)}`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mergeExistingGitWorktrees(workspaces: WorkspaceRecord[], now: string): WorkspaceRecord[] {
|
|
154
|
+
const anchor = workspaces.find((workspace) => existsSync(workspace.path)) ?? workspaces[0]
|
|
155
|
+
if (!anchor) return workspaces
|
|
156
|
+
const discovered = listGitWorktreesSync(anchor.path)
|
|
157
|
+
if (discovered.length === 0) return workspaces
|
|
158
|
+
|
|
159
|
+
// Git keeps admin entries for workspaces whose directory is gone (e.g. a temp
|
|
160
|
+
// dir cleared on reboot). Drop that stale state so the entries stop
|
|
161
|
+
// resurfacing in `git worktree list`, and ignore them while reconciling.
|
|
162
|
+
if (discovered.some((entry) => entry.prunable === true)) {
|
|
163
|
+
pruneGitWorktreesSync(anchor.path)
|
|
164
|
+
}
|
|
165
|
+
const live = discovered.filter((entry) => entry.prunable !== true)
|
|
166
|
+
|
|
167
|
+
const byPath = new Map(workspaces.map((workspace) => [workspace.path, workspace]))
|
|
168
|
+
// `git worktree list` reports the main workspace first; prefer the primary
|
|
169
|
+
// record's repoRoot when it still exists, then git's main workspace.
|
|
170
|
+
const primaryRepoRoot = workspaces.find((workspace) => workspace.source === 'primary')?.repoRoot
|
|
171
|
+
const repoRoot =
|
|
172
|
+
primaryRepoRoot !== undefined && existsSync(primaryRepoRoot)
|
|
173
|
+
? primaryRepoRoot
|
|
174
|
+
: (live[0]?.path ?? anchor.repoRoot)
|
|
175
|
+
for (const entry of live) {
|
|
176
|
+
const existing = byPath.get(entry.path)
|
|
177
|
+
if (existing) {
|
|
178
|
+
// Patch branch / commitSha from git for entries we already track —
|
|
179
|
+
// notably the primary workspace, whose `branch` is never set at creation
|
|
180
|
+
// time (createPrimaryWorkspace doesn't run `git`). Without this, the UI
|
|
181
|
+
// and the divergence poller have no branch for primary workspaces.
|
|
182
|
+
const patchedBranch = entry.branch ?? existing.branch
|
|
183
|
+
const patchedSha = entry.head ?? existing.commitSha
|
|
184
|
+
if (patchedBranch !== existing.branch || patchedSha !== existing.commitSha) {
|
|
185
|
+
byPath.set(entry.path, {
|
|
186
|
+
...existing,
|
|
187
|
+
branch: patchedBranch,
|
|
188
|
+
commitSha: patchedSha,
|
|
189
|
+
updatedAt: now,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (isInsideAimuxWorktreeRoot(entry.path)) continue
|
|
195
|
+
const workspace: WorkspaceRecord = {
|
|
196
|
+
branch: entry.branch,
|
|
197
|
+
commitSha: entry.head,
|
|
198
|
+
createdAt: now,
|
|
199
|
+
createdByAimux: false,
|
|
200
|
+
id: stableWorkspaceId(entry.path),
|
|
201
|
+
name: basename(entry.path) || entry.path,
|
|
202
|
+
path: entry.path,
|
|
203
|
+
repoRoot,
|
|
204
|
+
source: 'external',
|
|
205
|
+
updatedAt: now,
|
|
206
|
+
}
|
|
207
|
+
byPath.set(entry.path, workspace)
|
|
208
|
+
}
|
|
209
|
+
return (
|
|
210
|
+
[...byPath.values()]
|
|
211
|
+
// Drop records whose directory no longer exists (a deleted external
|
|
212
|
+
// workspace that git already pruned) so they don't reappear on restart.
|
|
213
|
+
.filter((workspace) => workspace.source === 'primary' || existsSync(workspace.path))
|
|
214
|
+
// Heal records whose repoRoot points at a since-deleted sibling workspace
|
|
215
|
+
// so later git operations (e.g. `git -C repoRoot`) don't fail.
|
|
216
|
+
.map((workspace) =>
|
|
217
|
+
existsSync(workspace.repoRoot) ? workspace : { ...workspace, repoRoot, updatedAt: now }
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface DiscoveredWorkspace {
|
|
223
|
+
path: string
|
|
224
|
+
head?: string
|
|
225
|
+
branch?: string
|
|
226
|
+
prunable?: boolean
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function listGitWorktreesSync(cwd: string): DiscoveredWorkspace[] {
|
|
230
|
+
const result = spawnSync('git', ['-C', cwd, 'worktree', 'list', '--porcelain'], {
|
|
231
|
+
encoding: 'utf8',
|
|
232
|
+
})
|
|
233
|
+
if (result.status !== 0 || !result.stdout) return []
|
|
234
|
+
const workspaces: DiscoveredWorkspace[] = []
|
|
235
|
+
let current: DiscoveredWorkspace | null = null
|
|
236
|
+
for (const line of result.stdout.split('\n')) {
|
|
237
|
+
if (line.startsWith('worktree ')) {
|
|
238
|
+
if (current) workspaces.push(current)
|
|
239
|
+
current = { path: line.slice('worktree '.length) }
|
|
240
|
+
} else if (current && line.startsWith('HEAD ')) {
|
|
241
|
+
current.head = line.slice('HEAD '.length)
|
|
242
|
+
} else if (current && line.startsWith('branch ')) {
|
|
243
|
+
current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '')
|
|
244
|
+
} else if (current && line.startsWith('prunable')) {
|
|
245
|
+
current.prunable = true
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (current) workspaces.push(current)
|
|
249
|
+
return workspaces
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function pruneGitWorktreesSync(cwd: string): void {
|
|
253
|
+
spawnSync('git', ['-C', cwd, 'worktree', 'prune'], { encoding: 'utf8' })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pruneMissingAimuxTempWorkspaces(workspaces: WorkspaceRecord[]): WorkspaceRecord[] {
|
|
257
|
+
return workspaces.filter((workspace) => {
|
|
258
|
+
if (
|
|
259
|
+
workspace.source !== 'aimux-temp' ||
|
|
260
|
+
!workspace.createdByAimux ||
|
|
261
|
+
!isInsideAimuxWorktreeRoot(workspace.path)
|
|
262
|
+
) {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
return existsSync(workspace.path)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The project the user is in. `currentProjectId` is a nullable string that is
|
|
271
|
+
* also meaningfully empty, so open-coding this lookup means repeating both
|
|
272
|
+
* checks; there is one home for it instead.
|
|
273
|
+
*/
|
|
274
|
+
export function getCurrentProject(state: {
|
|
275
|
+
currentProjectId?: string | null
|
|
276
|
+
projects: ProjectRecord[]
|
|
277
|
+
}): ProjectRecord | undefined {
|
|
278
|
+
const id = state.currentProjectId
|
|
279
|
+
if (id == null || id === '') return undefined
|
|
280
|
+
return state.projects.find((project) => project.id === id)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The project that owns a workspace, and the workspace itself. */
|
|
284
|
+
export function findWorkspace(
|
|
285
|
+
projects: ProjectRecord[],
|
|
286
|
+
workspaceId: string | undefined
|
|
287
|
+
): { project: ProjectRecord; workspace: WorkspaceRecord } | undefined {
|
|
288
|
+
if (workspaceId == null || workspaceId === '') return undefined
|
|
289
|
+
for (const project of projects) {
|
|
290
|
+
const workspace = project.workspaces?.find((entry) => entry.id === workspaceId)
|
|
291
|
+
if (workspace) return { project, workspace }
|
|
292
|
+
}
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function getActiveWorkspace(
|
|
297
|
+
project: ProjectRecord | undefined
|
|
298
|
+
): WorkspaceRecord | undefined {
|
|
299
|
+
if (!(project?.workspaces?.length != null && project?.workspaces?.length !== 0)) return undefined
|
|
300
|
+
return (
|
|
301
|
+
project.workspaces.find((workspace) => workspace.id === project.activeWorkspaceId) ??
|
|
302
|
+
project.workspaces[0]
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The cwd to work in: the active workspace's path, falling back to the project
|
|
308
|
+
* root. Not the same as `project.projectPath`, which names the repo itself.
|
|
309
|
+
*/
|
|
310
|
+
export function getActiveWorkspacePath(project: ProjectRecord | undefined): string | undefined {
|
|
311
|
+
return getActiveWorkspace(project)?.path ?? project?.projectPath
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function withActiveWorkspace(project: ProjectRecord, workspaceId: string): ProjectRecord {
|
|
315
|
+
const workspace = project.workspaces?.find((entry) => entry.id === workspaceId)
|
|
316
|
+
if (!workspace) return project
|
|
317
|
+
return {
|
|
318
|
+
...project,
|
|
319
|
+
activeWorkspaceId: workspaceId,
|
|
320
|
+
updatedAt: new Date().toISOString(),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Landing on a workspace — entering it, or opening one of its tabs — is what
|
|
326
|
+
* "you have seen it" means, so it drops the finished-a-turn tick. Returns the
|
|
327
|
+
* same map when there is nothing to clear, so every caller can spread it
|
|
328
|
+
* unconditionally without churning the sidebar's subscribers.
|
|
329
|
+
*/
|
|
330
|
+
export function clearWorkspaceDone(
|
|
331
|
+
workspaceActivity: Record<string, WorkspaceActivity>,
|
|
332
|
+
workspaceId: string | undefined
|
|
333
|
+
): Record<string, WorkspaceActivity> {
|
|
334
|
+
if (workspaceId == null || workspaceId === '') return workspaceActivity
|
|
335
|
+
const entry = workspaceActivity[workspaceId]
|
|
336
|
+
if (entry === undefined || !entry.done) return workspaceActivity
|
|
337
|
+
return { ...workspaceActivity, [workspaceId]: { ...entry, done: false } }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function getRenderedTabWorkspaceId(
|
|
341
|
+
tab: { workspaceId?: string },
|
|
342
|
+
workspaces: { id: string }[]
|
|
343
|
+
): string {
|
|
344
|
+
return tab.workspaceId ?? workspaces[0]?.id ?? '__main__'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* The one gate every "which tabs does the user see" path shares: the top tab
|
|
349
|
+
* bar, Leader+1..9, Ctrl+Tab, the three `activeTabId` re-picks, and flash-jump
|
|
350
|
+
* labels. Hidden tabs are dropped here rather than at each caller — including
|
|
351
|
+
* on the no-project / no-active-workspace paths, which must not become a hole.
|
|
352
|
+
*/
|
|
353
|
+
export function filterTabsForActiveWorkspace(
|
|
354
|
+
tabs: TabSession[],
|
|
355
|
+
project: ProjectRecord | undefined
|
|
356
|
+
): TabSession[] {
|
|
357
|
+
const visible = tabs.filter((tab) => tab.hidden !== true)
|
|
358
|
+
if (!project) return visible
|
|
359
|
+
const activeWorkspaceId = project.activeWorkspaceId
|
|
360
|
+
if (activeWorkspaceId == null || activeWorkspaceId === '') return visible
|
|
361
|
+
const workspaces = project.workspaces ?? []
|
|
362
|
+
const primaryId = workspaces[0]?.id
|
|
363
|
+
const activeIsPrimary = primaryId != null && primaryId === activeWorkspaceId
|
|
364
|
+
return visible.filter((tab) => {
|
|
365
|
+
const owned = tab.workspaceId != null && tab.workspaceId !== ''
|
|
366
|
+
if (owned) return tab.workspaceId === activeWorkspaceId
|
|
367
|
+
// Unbound (legacy) tabs surface only under the primary workspace.
|
|
368
|
+
return activeIsPrimary
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function orderTabsByWorkspace(
|
|
373
|
+
tabs: TabSession[],
|
|
374
|
+
project: ProjectRecord | undefined
|
|
375
|
+
): TabSession[] {
|
|
376
|
+
const workspaces = project?.workspaces ?? []
|
|
377
|
+
const order = new Map<string, number>()
|
|
378
|
+
for (const [index, workspace] of workspaces.entries()) {
|
|
379
|
+
order.set(workspace.id, index)
|
|
380
|
+
}
|
|
381
|
+
order.set('__main__', order.size)
|
|
382
|
+
|
|
383
|
+
return tabs
|
|
384
|
+
.map((tab, index) => ({ index, tab }))
|
|
385
|
+
.sort((a, b) => {
|
|
386
|
+
const aw = order.get(getRenderedTabWorkspaceId(a.tab, workspaces)) ?? Number.MAX_SAFE_INTEGER
|
|
387
|
+
const bw = order.get(getRenderedTabWorkspaceId(b.tab, workspaces)) ?? Number.MAX_SAFE_INTEGER
|
|
388
|
+
if (aw !== bw) return aw - bw
|
|
389
|
+
return a.index - b.index
|
|
390
|
+
})
|
|
391
|
+
.map((entry) => entry.tab)
|
|
392
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { AppAction
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
import type { AppState, AutoCommitState } from '../types'
|
|
2
3
|
|
|
3
|
-
function
|
|
4
|
+
function setByProject(
|
|
4
5
|
state: AutoCommitState,
|
|
5
|
-
|
|
6
|
-
next: AutoCommitState['
|
|
6
|
+
projectId: string,
|
|
7
|
+
next: AutoCommitState['byProject'][string]
|
|
7
8
|
): AutoCommitState {
|
|
8
|
-
return {
|
|
9
|
+
return { byProject: { ...state.byProject, [projectId]: next } }
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function reduceAutoCommitState(
|
|
@@ -14,7 +15,7 @@ export function reduceAutoCommitState(
|
|
|
14
15
|
): AutoCommitState | null {
|
|
15
16
|
switch (action.type) {
|
|
16
17
|
case 'auto-commit-generation-started': {
|
|
17
|
-
return
|
|
18
|
+
return setByProject(state, action.projectId, {
|
|
18
19
|
abortController: action.abortController,
|
|
19
20
|
kind: 'generating',
|
|
20
21
|
startedAt: action.startedAt,
|
|
@@ -23,10 +24,10 @@ export function reduceAutoCommitState(
|
|
|
23
24
|
})
|
|
24
25
|
}
|
|
25
26
|
case 'auto-commit-generation-ready': {
|
|
26
|
-
const current = state.
|
|
27
|
+
const current = state.byProject[action.projectId]
|
|
27
28
|
if (!current || current.kind !== 'generating') return null
|
|
28
29
|
if (current.workingTreeHash !== action.workingTreeHash) return null
|
|
29
|
-
return
|
|
30
|
+
return setByProject(state, action.projectId, {
|
|
30
31
|
body: action.body,
|
|
31
32
|
generatedAt: action.generatedAt,
|
|
32
33
|
kind: 'ready',
|
|
@@ -36,7 +37,7 @@ export function reduceAutoCommitState(
|
|
|
36
37
|
})
|
|
37
38
|
}
|
|
38
39
|
case 'auto-commit-clear': {
|
|
39
|
-
const current = state.
|
|
40
|
+
const current = state.byProject[action.projectId]
|
|
40
41
|
if (!current || current.kind === 'idle') return null
|
|
41
42
|
if (current.kind === 'generating') {
|
|
42
43
|
try {
|
|
@@ -45,7 +46,7 @@ export function reduceAutoCommitState(
|
|
|
45
46
|
// ignore
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
return
|
|
49
|
+
return setByProject(state, action.projectId, { kind: 'idle' })
|
|
49
50
|
}
|
|
50
51
|
default:
|
|
51
52
|
return null
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
import type { AppState } from '../types'
|
|
3
|
+
|
|
4
|
+
import { reduceAutoCommitState } from './auto-commit-state'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The commit modal: its four stages, and the two ways a background auto-commit
|
|
8
|
+
* suggestion lands in it.
|
|
9
|
+
*
|
|
10
|
+
* Its own file because it is the one modal with a stage machine of its own — the
|
|
11
|
+
* others are a buffer and a selection — and because `modal-state.ts` was within a
|
|
12
|
+
* few dozen lines of the 1000-line ceiling.
|
|
13
|
+
*
|
|
14
|
+
* Returns `null` on an action it does not handle, and also when a `git-commit`
|
|
15
|
+
* action arrives with no commit modal open, or with a stale project: those are for
|
|
16
|
+
* `reduceAutoCommit` further down the chain, which owns the slice without caring
|
|
17
|
+
* what is on screen.
|
|
18
|
+
*/
|
|
19
|
+
export function reduceGitCommitModalState(state: AppState, action: AppAction): AppState | null {
|
|
20
|
+
switch (action.type) {
|
|
21
|
+
case 'open-git-commit-modal':
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
focusMode: 'command-edit',
|
|
25
|
+
modal: {
|
|
26
|
+
activeField: 'title',
|
|
27
|
+
contentBuffer: '',
|
|
28
|
+
cursorPos: 0,
|
|
29
|
+
editBuffer: '',
|
|
30
|
+
projectTargetId: action.projectId ?? null,
|
|
31
|
+
// Git mode is what is drawn behind it, and closing has always gone back
|
|
32
|
+
// there. Said here rather than in `close-modal`, so that reducer has one
|
|
33
|
+
// rule instead of a rule and this exception.
|
|
34
|
+
returnTo: 'git',
|
|
35
|
+
selectedIndex: 0,
|
|
36
|
+
stage: 'edit',
|
|
37
|
+
type: 'git-commit',
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
case 'git-commit-enter-confirm':
|
|
41
|
+
if (state.modal.type !== 'git-commit') return state
|
|
42
|
+
return { ...state, focusMode: 'command-edit', modal: { ...state.modal, stage: 'confirm' } }
|
|
43
|
+
case 'git-commit-leave-confirm':
|
|
44
|
+
if (state.modal.type !== 'git-commit') return state
|
|
45
|
+
return { ...state, focusMode: 'command-edit', modal: { ...state.modal, stage: 'edit' } }
|
|
46
|
+
case 'git-commit-enter-generating':
|
|
47
|
+
if (state.modal.type !== 'git-commit') return state
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
focusMode: 'modal',
|
|
51
|
+
modal: { ...state.modal, projectTargetId: action.projectId, stage: 'generating' },
|
|
52
|
+
}
|
|
53
|
+
case 'git-commit-leave-generating':
|
|
54
|
+
if (state.modal.type !== 'git-commit') return state
|
|
55
|
+
return { ...state, focusMode: 'command-edit', modal: { ...state.modal, stage: 'edit' } }
|
|
56
|
+
case 'auto-commit-generation-ready': {
|
|
57
|
+
if (
|
|
58
|
+
state.modal.type !== 'git-commit' ||
|
|
59
|
+
state.modal.stage !== 'generating' ||
|
|
60
|
+
state.modal.projectTargetId !== action.projectId
|
|
61
|
+
) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
const nextAutoCommit = reduceAutoCommitState(state.autoCommit, action)
|
|
65
|
+
if (!nextAutoCommit) {
|
|
66
|
+
// Stale result (hash mismatch or slice was cleared mid-flight): don't
|
|
67
|
+
// strand the modal in `generating` — flip back to edit so the user isn't
|
|
68
|
+
// stuck staring at a spinner that will never resolve.
|
|
69
|
+
return { ...state, focusMode: 'command-edit', modal: { ...state.modal, stage: 'edit' } }
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...state,
|
|
73
|
+
autoCommit: nextAutoCommit,
|
|
74
|
+
focusMode: 'command-edit',
|
|
75
|
+
modal: {
|
|
76
|
+
...state.modal,
|
|
77
|
+
activeField: 'title',
|
|
78
|
+
contentBuffer: action.body,
|
|
79
|
+
cursorPos: action.title.length,
|
|
80
|
+
editBuffer: action.title,
|
|
81
|
+
stage: 'confirm',
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
case 'git-commit-use-background-suggestion': {
|
|
86
|
+
if (state.modal.type !== 'git-commit' || state.modal.projectTargetId !== action.projectId) {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
const suggestion = state.autoCommit.byProject[action.projectId]
|
|
90
|
+
if (!suggestion || suggestion.kind !== 'ready') return null
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
focusMode: 'command-edit',
|
|
94
|
+
modal: {
|
|
95
|
+
...state.modal,
|
|
96
|
+
activeField: 'title',
|
|
97
|
+
contentBuffer: suggestion.body,
|
|
98
|
+
cursorPos: suggestion.title.length,
|
|
99
|
+
editBuffer: suggestion.title,
|
|
100
|
+
stage: 'confirm',
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
case 'auto-commit-clear': {
|
|
105
|
+
if (
|
|
106
|
+
state.modal.type !== 'git-commit' ||
|
|
107
|
+
state.modal.stage !== 'generating' ||
|
|
108
|
+
state.modal.projectTargetId !== action.projectId
|
|
109
|
+
) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
...state,
|
|
114
|
+
autoCommit: reduceAutoCommitState(state.autoCommit, action) ?? state.autoCommit,
|
|
115
|
+
focusMode: 'command-edit',
|
|
116
|
+
modal: { ...state.modal, stage: 'edit' },
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PreparedParsedDiff } from '../../ui/components/git/diff-renderer/prepare-diff'
|
|
2
|
+
import type { AppAction } from '../actions'
|
|
2
3
|
|
|
3
4
|
import { collectFoldables } from '../../ui/components/git/diff-renderer/build-rows'
|
|
4
5
|
import {
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
reconcileSelectedGitEntryKey,
|
|
10
11
|
} from '../git-tree'
|
|
11
12
|
import {
|
|
12
|
-
type AppAction,
|
|
13
13
|
type AppState,
|
|
14
14
|
type FoldState,
|
|
15
15
|
getParsedPayload,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
1
2
|
import type {
|
|
2
|
-
AppAction,
|
|
3
3
|
AppState,
|
|
4
4
|
BranchDivergence,
|
|
5
5
|
GitFileEntry,
|
|
@@ -110,6 +110,10 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
110
110
|
if (nextRatio === state.gitPane.diffModeRatio) return state
|
|
111
111
|
return { ...state, gitPane: { ...state.gitPane, diffModeRatio: nextRatio } }
|
|
112
112
|
}
|
|
113
|
+
// The preferences with no toggle of their own: the settings screen sets them
|
|
114
|
+
// outright rather than each one growing its own action.
|
|
115
|
+
case 'set-git-pane':
|
|
116
|
+
return { ...state, gitPane: { ...state.gitPane, ...action.patch } }
|
|
113
117
|
case 'git-refresh-success': {
|
|
114
118
|
const prev = state.gitPanel
|
|
115
119
|
const next = action.payload
|
|
@@ -162,9 +166,9 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
162
166
|
gitPanel: { ...prev, error: action.kind, files: [] },
|
|
163
167
|
}
|
|
164
168
|
}
|
|
165
|
-
case 'set-
|
|
166
|
-
if (sameDivergence(state.
|
|
167
|
-
return { ...state,
|
|
169
|
+
case 'set-workspace-divergence': {
|
|
170
|
+
if (sameDivergence(state.workspaceDivergence, action.divergence)) return state
|
|
171
|
+
return { ...state, workspaceDivergence: action.divergence }
|
|
168
172
|
}
|
|
169
173
|
case 'git-panel-reset': {
|
|
170
174
|
const prev = state.gitPanel
|
|
@@ -179,9 +183,9 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
179
183
|
}
|
|
180
184
|
return {
|
|
181
185
|
...state,
|
|
182
|
-
// A reset means the underlying
|
|
186
|
+
// A reset means the underlying workspace/ref changed, so cached diffs are
|
|
183
187
|
// stale — drop them (keyed by section:path, they'd otherwise serve a
|
|
184
|
-
// previous
|
|
188
|
+
// previous workspace's diff for a same-named file after a move/switch).
|
|
185
189
|
gitMode: {
|
|
186
190
|
...state.gitMode,
|
|
187
191
|
diffs: {},
|