@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,342 @@
|
|
|
1
|
+
// Every action the reducer accepts, grouped by the slice of state it touches.
|
|
2
|
+
//
|
|
3
|
+
// Split from `types.ts` so the state shapes and the messages that change them
|
|
4
|
+
// are separate files: the shapes are read by the whole app, this union only by
|
|
5
|
+
// the reducer and the things that dispatch into it.
|
|
6
|
+
|
|
7
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
8
|
+
|
|
9
|
+
import type { LayoutNode, SplitDirection } from './layout-tree'
|
|
10
|
+
import type {
|
|
11
|
+
AssistantId,
|
|
12
|
+
BarSide,
|
|
13
|
+
BranchDivergence,
|
|
14
|
+
DiffData,
|
|
15
|
+
DirectoryResult,
|
|
16
|
+
DiscoveredRepo,
|
|
17
|
+
FocusMode,
|
|
18
|
+
GitFileSection,
|
|
19
|
+
GitPanelError,
|
|
20
|
+
GitPaneState,
|
|
21
|
+
GitRefreshPayload,
|
|
22
|
+
PendingWorkspaceLaunch,
|
|
23
|
+
ProjectRecord,
|
|
24
|
+
ProjectSnapshotV1,
|
|
25
|
+
ProjectStatus,
|
|
26
|
+
SnippetRecord,
|
|
27
|
+
TabActivity,
|
|
28
|
+
TabSession,
|
|
29
|
+
TerminalModeState,
|
|
30
|
+
TerminalSnapshot,
|
|
31
|
+
WorkspaceRecord,
|
|
32
|
+
} from './types'
|
|
33
|
+
|
|
34
|
+
export type ModalAction =
|
|
35
|
+
| { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
|
|
36
|
+
| {
|
|
37
|
+
type: 'open-new-tab-modal'
|
|
38
|
+
pendingWorkspace?: PendingWorkspaceLaunch
|
|
39
|
+
pendingPrompt?: string
|
|
40
|
+
}
|
|
41
|
+
| { type: 'open-edit-custom-command'; assistantId: AssistantId }
|
|
42
|
+
| { type: 'open-help-modal'; scope?: ModeId }
|
|
43
|
+
| { type: 'open-split-picker'; direction: SplitDirection }
|
|
44
|
+
| { type: 'open-project-picker' }
|
|
45
|
+
| {
|
|
46
|
+
type: 'open-project-name-modal'
|
|
47
|
+
projectTargetId?: string
|
|
48
|
+
initialName?: string
|
|
49
|
+
returnToProjectPicker?: boolean
|
|
50
|
+
}
|
|
51
|
+
| { type: 'close-modal' }
|
|
52
|
+
| { type: 'move-modal-selection'; delta: number }
|
|
53
|
+
| { type: 'update-command-edit'; char: string }
|
|
54
|
+
| { type: 'cancel-command-edit' }
|
|
55
|
+
| { type: 'open-create-project-modal'; returnToProjectPicker: boolean }
|
|
56
|
+
| { type: 'open-create-workspace-modal' }
|
|
57
|
+
| { type: 'set-create-workspace-base-branches'; branches: string[]; defaultBranch?: string }
|
|
58
|
+
| { type: 'set-create-workspace-branch-error'; message: string | null }
|
|
59
|
+
| { type: 'switch-create-workspace-field' }
|
|
60
|
+
| { type: 'set-directory-results'; results: DirectoryResult[] }
|
|
61
|
+
| { type: 'switch-create-project-field' }
|
|
62
|
+
| { type: 'select-directory' }
|
|
63
|
+
| { type: 'open-rename-tab-modal' }
|
|
64
|
+
| {
|
|
65
|
+
type: 'open-rename-workspace-modal'
|
|
66
|
+
projectId: string
|
|
67
|
+
workspaceId: string
|
|
68
|
+
initialName: string
|
|
69
|
+
}
|
|
70
|
+
| { type: 'open-snippet-picker'; returnTo?: FocusMode }
|
|
71
|
+
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
72
|
+
| { type: 'set-help-entry-count'; count: number }
|
|
73
|
+
| { type: 'set-theme-entry-count'; count: number }
|
|
74
|
+
| { type: 'open-theme-picker'; returnTo?: FocusMode }
|
|
75
|
+
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
76
|
+
| { type: 'set-modal-selection-index'; index: number }
|
|
77
|
+
| { type: 'open-ai-usage-modal' }
|
|
78
|
+
| { type: 'open-workspace-move-modal'; sourceWorkspaceId: string }
|
|
79
|
+
| { type: 'toggle-workspace-move-delete' }
|
|
80
|
+
| { type: 'set-workspace-move-stats'; dirtyFiles: Record<string, number> }
|
|
81
|
+
| {
|
|
82
|
+
type: 'open-workspace-move-confirm'
|
|
83
|
+
variant: 'stash-target' | 'keep-conflicts'
|
|
84
|
+
files: string[]
|
|
85
|
+
projectId: string
|
|
86
|
+
sourceWorkspaceId: string
|
|
87
|
+
targetWorkspaceId: string
|
|
88
|
+
deleteSource: boolean
|
|
89
|
+
sourceLabel: string
|
|
90
|
+
targetLabel: string
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
type: 'open-workspace-delete-confirm'
|
|
94
|
+
projectId: string
|
|
95
|
+
workspaceId: string
|
|
96
|
+
workspaceLabel: string
|
|
97
|
+
reason: string
|
|
98
|
+
closeTabs: boolean
|
|
99
|
+
force: boolean
|
|
100
|
+
}
|
|
101
|
+
| { type: 'open-flash-jump-modal' }
|
|
102
|
+
| { type: 'clear-flash-jump-pending' }
|
|
103
|
+
|
|
104
|
+
export type ProjectAction =
|
|
105
|
+
| {
|
|
106
|
+
type: 'load-project'
|
|
107
|
+
projectId: string
|
|
108
|
+
projectSnapshot?: ProjectSnapshotV1
|
|
109
|
+
forceDisconnected?: boolean
|
|
110
|
+
}
|
|
111
|
+
| { type: 'set-projects'; projects: ProjectRecord[] }
|
|
112
|
+
| { type: 'create-project-record'; project: ProjectRecord }
|
|
113
|
+
| { type: 'rename-project-record'; projectId: string; name: string }
|
|
114
|
+
| { type: 'delete-project-record'; projectId: string; openProjectPicker?: boolean }
|
|
115
|
+
| { type: 'reorder-projects'; orderedIds: string[] }
|
|
116
|
+
| { type: 'reorder-active-project'; delta: number }
|
|
117
|
+
| { type: 'set-project-status'; projectId: string; status: ProjectStatus }
|
|
118
|
+
/** What a workspace's assistants are doing. `done` is owned by the reducer,
|
|
119
|
+
* so this carries only the two flags derived from tab statuses. */
|
|
120
|
+
| { type: 'set-workspace-activity'; workspaceId: string; working: boolean; waiting: boolean }
|
|
121
|
+
/** An assistant in this workspace finished a turn and nobody has looked yet. */
|
|
122
|
+
| { type: 'mark-workspace-done'; workspaceId: string }
|
|
123
|
+
/** The tab that rang: it finished a turn while the user was elsewhere. */
|
|
124
|
+
| { type: 'mark-tab-unseen'; tabId: string }
|
|
125
|
+
| {
|
|
126
|
+
type: 'add-workspace-record'
|
|
127
|
+
projectId: string
|
|
128
|
+
workspace: WorkspaceRecord
|
|
129
|
+
activate?: boolean
|
|
130
|
+
}
|
|
131
|
+
| { type: 'set-active-workspace'; projectId: string; workspaceId: string }
|
|
132
|
+
| {
|
|
133
|
+
type: 'update-workspace-record'
|
|
134
|
+
projectId: string
|
|
135
|
+
workspaceId: string
|
|
136
|
+
patch: Partial<WorkspaceRecord>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type TabAction =
|
|
140
|
+
| { type: 'add-tab'; tab: TabSession }
|
|
141
|
+
| {
|
|
142
|
+
type: 'hydrate-project'
|
|
143
|
+
tabs: TabSession[]
|
|
144
|
+
activeTabId: string | null
|
|
145
|
+
layoutTree?: LayoutNode | null
|
|
146
|
+
layoutTrees?: Record<string, LayoutNode>
|
|
147
|
+
tabGroupMap?: Record<string, string>
|
|
148
|
+
}
|
|
149
|
+
| { type: 'close-tab'; tabId: string }
|
|
150
|
+
| { type: 'close-active-tab' }
|
|
151
|
+
| { type: 'set-active-tab'; tabId: string }
|
|
152
|
+
| { type: 'move-active-tab'; delta: number }
|
|
153
|
+
| { type: 'reorder-active-tab'; delta: number }
|
|
154
|
+
| { type: 'reorder-tabs'; orderedTabIds: string[] }
|
|
155
|
+
| { type: 'reset-tab-project'; tabId: string }
|
|
156
|
+
| {
|
|
157
|
+
type: 'rename-tab'
|
|
158
|
+
tabId: string
|
|
159
|
+
title: string
|
|
160
|
+
autoRenameStatus?: 'eligible' | 'attempted'
|
|
161
|
+
}
|
|
162
|
+
| {
|
|
163
|
+
type: 'update-tab-metadata'
|
|
164
|
+
tabId: string
|
|
165
|
+
title?: string
|
|
166
|
+
autoRenameStatus?: 'eligible' | 'attempted'
|
|
167
|
+
/** Set to false to promote a hidden tab into the normal tab strip. */
|
|
168
|
+
hidden?: boolean
|
|
169
|
+
}
|
|
170
|
+
| { type: 'append-tab-buffer'; tabId: string; chunk: string }
|
|
171
|
+
| {
|
|
172
|
+
type: 'replace-tab-viewport'
|
|
173
|
+
tabId: string
|
|
174
|
+
viewport: TerminalSnapshot
|
|
175
|
+
terminalModes: TerminalModeState
|
|
176
|
+
source?: 'resize' | 'scroll' | 'data' | 'switch'
|
|
177
|
+
}
|
|
178
|
+
| { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
|
|
179
|
+
| { type: 'set-tab-error'; tabId: string; message: string }
|
|
180
|
+
|
|
181
|
+
export type LayoutAction =
|
|
182
|
+
| {
|
|
183
|
+
type: 'split-pane'
|
|
184
|
+
direction: SplitDirection
|
|
185
|
+
newTab: TabSession
|
|
186
|
+
}
|
|
187
|
+
| { type: 'close-pane'; tabId: string }
|
|
188
|
+
| {
|
|
189
|
+
type: 'focus-pane-direction'
|
|
190
|
+
direction: 'left' | 'right' | 'up' | 'down'
|
|
191
|
+
}
|
|
192
|
+
| {
|
|
193
|
+
type: 'resize-pane'
|
|
194
|
+
tabId: string
|
|
195
|
+
delta: number
|
|
196
|
+
axis?: SplitDirection
|
|
197
|
+
}
|
|
198
|
+
| {
|
|
199
|
+
type: 'set-split-ratio'
|
|
200
|
+
tabId: string
|
|
201
|
+
ratio: number
|
|
202
|
+
axis?: SplitDirection
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type UIAction =
|
|
206
|
+
| { type: 'toggle-bar'; side: BarSide }
|
|
207
|
+
| { type: 'resize-bar'; side: BarSide; delta: number }
|
|
208
|
+
| { type: 'set-bar-width'; side: BarSide; width: number }
|
|
209
|
+
| { type: 'toggle-widget'; widgetId: string }
|
|
210
|
+
| { type: 'move-widget'; widgetId: string; side: BarSide; index: number }
|
|
211
|
+
| { type: 'set-bar-boundary'; side: BarSide; index: number; ratio: number }
|
|
212
|
+
| { type: 'resize-widget'; widgetId: string; delta: number }
|
|
213
|
+
| { type: 'set-focus-mode'; focusMode: FocusMode }
|
|
214
|
+
| { type: 'set-terminal-size'; cols: number; rows: number }
|
|
215
|
+
| { type: 'resize-git-diff-pane'; delta: number }
|
|
216
|
+
| { type: 'set-pending-chords'; chords: string[] | null }
|
|
217
|
+
| { type: 'toggle-project-bar' }
|
|
218
|
+
|
|
219
|
+
export type SettingsAction =
|
|
220
|
+
| { type: 'enter-settings' }
|
|
221
|
+
| { type: 'exit-settings' }
|
|
222
|
+
| { type: 'settings-focus-pane'; pane: 'nav' | 'rows' }
|
|
223
|
+
| { type: 'settings-move-selection'; delta: -1 | 1 }
|
|
224
|
+
| { type: 'settings-select-section'; sectionId: string }
|
|
225
|
+
| { type: 'settings-select-row'; rowIndex: number }
|
|
226
|
+
| { type: 'open-settings-search' }
|
|
227
|
+
| { type: 'open-setting-text-modal'; settingId: string; label: string; value: string }
|
|
228
|
+
|
|
229
|
+
export type GitPanelAction =
|
|
230
|
+
| { type: 'git-refresh-success'; payload: GitRefreshPayload }
|
|
231
|
+
| { type: 'git-refresh-error'; kind: GitPanelError }
|
|
232
|
+
| { type: 'git-panel-reset' }
|
|
233
|
+
| { type: 'set-workspace-divergence'; divergence: Record<string, BranchDivergence> }
|
|
234
|
+
| { type: 'set-git-pane'; patch: Partial<GitPaneState> }
|
|
235
|
+
|
|
236
|
+
export type AutoCommitAction =
|
|
237
|
+
| {
|
|
238
|
+
type: 'auto-commit-generation-started'
|
|
239
|
+
projectId: string
|
|
240
|
+
tabId: string
|
|
241
|
+
workingTreeHash: string
|
|
242
|
+
abortController: AbortController
|
|
243
|
+
startedAt: number
|
|
244
|
+
}
|
|
245
|
+
| {
|
|
246
|
+
type: 'auto-commit-generation-ready'
|
|
247
|
+
projectId: string
|
|
248
|
+
workingTreeHash: string
|
|
249
|
+
title: string
|
|
250
|
+
body: string
|
|
251
|
+
generatedAt: number
|
|
252
|
+
}
|
|
253
|
+
| { type: 'auto-commit-clear'; projectId: string }
|
|
254
|
+
|
|
255
|
+
export type GitModeAction =
|
|
256
|
+
| { type: 'enter-git-mode' }
|
|
257
|
+
| { type: 'exit-git-mode' }
|
|
258
|
+
| { type: 'git-mode-move-selection'; delta: -1 | 1 }
|
|
259
|
+
| { type: 'git-mode-move-file-selection'; delta: -1 | 1 }
|
|
260
|
+
| { type: 'git-mode-select-entry-by-key'; key: string }
|
|
261
|
+
| { type: 'git-mode-toggle-folder'; key: string }
|
|
262
|
+
| { type: 'git-mode-toggle-selected-folder' }
|
|
263
|
+
| { type: 'git-mode-collapse-selection' }
|
|
264
|
+
| { type: 'git-mode-expand-selection' }
|
|
265
|
+
| { type: 'git-mode-toggle-file-list-mode' }
|
|
266
|
+
| { type: 'git-mode-toggle-tree-compaction' }
|
|
267
|
+
| { type: 'git-mode-set-diff'; key: string; diff: DiffData; hash: string }
|
|
268
|
+
| {
|
|
269
|
+
type: 'git-mode-set-parsed'
|
|
270
|
+
key: string
|
|
271
|
+
hash: string
|
|
272
|
+
file: unknown
|
|
273
|
+
}
|
|
274
|
+
| {
|
|
275
|
+
type: 'git-mode-set-highlights'
|
|
276
|
+
key: string
|
|
277
|
+
hash: string
|
|
278
|
+
themeId: string
|
|
279
|
+
add: unknown
|
|
280
|
+
del: unknown
|
|
281
|
+
}
|
|
282
|
+
| {
|
|
283
|
+
type: 'git-mode-merge-highlights'
|
|
284
|
+
key: string
|
|
285
|
+
hash: string
|
|
286
|
+
themeId: string
|
|
287
|
+
add: { start: number; tokens: unknown }[]
|
|
288
|
+
del: { start: number; tokens: unknown }[]
|
|
289
|
+
}
|
|
290
|
+
| { type: 'git-mode-invalidate-diffs'; paths: string[] }
|
|
291
|
+
| { type: 'git-mode-set-loading'; key: string; loading: boolean }
|
|
292
|
+
| { type: 'git-mode-set-pending-delete'; path: string | null }
|
|
293
|
+
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
294
|
+
| { type: 'git-mode-set-message'; message: string | null }
|
|
295
|
+
| { type: 'snippet-picker-set-message'; message: string | null }
|
|
296
|
+
| { type: 'git-mode-toggle-diff-view' }
|
|
297
|
+
| { type: 'git-mode-toggle-review-base' }
|
|
298
|
+
| { type: 'git-mode-shift-head-offset'; delta: number }
|
|
299
|
+
| { type: 'git-mode-set-head-offset'; offset: number }
|
|
300
|
+
| {
|
|
301
|
+
type: 'git-mode-fold-adjust'
|
|
302
|
+
key: string
|
|
303
|
+
foldId: string
|
|
304
|
+
side: 'top' | 'bottom'
|
|
305
|
+
delta: number
|
|
306
|
+
}
|
|
307
|
+
| { type: 'git-mode-fold-set'; key: string; foldId: string; top: number; bottom: number }
|
|
308
|
+
| { type: 'git-mode-fold-toggle-all'; key: string }
|
|
309
|
+
| {
|
|
310
|
+
type: 'git-mode-optimistic-move'
|
|
311
|
+
path: string
|
|
312
|
+
fromSection: GitFileSection
|
|
313
|
+
toSection: GitFileSection | null
|
|
314
|
+
}
|
|
315
|
+
| { type: 'open-git-commit-modal'; projectId?: string }
|
|
316
|
+
| { type: 'git-commit-enter-confirm' }
|
|
317
|
+
| { type: 'git-commit-leave-confirm' }
|
|
318
|
+
| { type: 'git-commit-enter-generating'; projectId: string }
|
|
319
|
+
| { type: 'git-commit-leave-generating' }
|
|
320
|
+
| { type: 'git-commit-use-background-suggestion'; projectId: string }
|
|
321
|
+
|
|
322
|
+
export type DataAction =
|
|
323
|
+
| { type: 'set-snippets'; snippets: SnippetRecord[] }
|
|
324
|
+
| { type: 'delete-snippet'; snippetId: string }
|
|
325
|
+
| { type: 'set-custom-commands'; customCommands: Record<AssistantId, string> }
|
|
326
|
+
|
|
327
|
+
export type MultiRepoAction =
|
|
328
|
+
| { type: 'multi-repo-set-repos'; repos: DiscoveredRepo[] }
|
|
329
|
+
| { type: 'multi-repo-clear' }
|
|
330
|
+
|
|
331
|
+
export type AppAction =
|
|
332
|
+
| ModalAction
|
|
333
|
+
| ProjectAction
|
|
334
|
+
| TabAction
|
|
335
|
+
| LayoutAction
|
|
336
|
+
| UIAction
|
|
337
|
+
| SettingsAction
|
|
338
|
+
| DataAction
|
|
339
|
+
| GitPanelAction
|
|
340
|
+
| GitModeAction
|
|
341
|
+
| AutoCommitAction
|
|
342
|
+
| MultiRepoAction
|
package/src/state/app-store.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useStore } from 'zustand'
|
|
2
2
|
import { createStore } from 'zustand/vanilla'
|
|
3
3
|
|
|
4
|
-
import type { AppAction
|
|
4
|
+
import type { AppAction } from './actions'
|
|
5
|
+
import type { AppState } from './types'
|
|
5
6
|
|
|
6
7
|
import { appReducer, createInitialState } from './store'
|
|
7
8
|
|
package/src/state/bars.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const BAR_MIN_WIDTH = 18
|
|
|
4
4
|
export const BAR_MAX_WIDTH = 80
|
|
5
5
|
|
|
6
6
|
/** Widget ids the app knows how to render. Unknown ids are pruned on load. */
|
|
7
|
-
export const KNOWN_WIDGET_IDS = ['
|
|
7
|
+
export const KNOWN_WIDGET_IDS = ['projects', 'git', 'setup'] as const
|
|
8
8
|
|
|
9
9
|
/** Smallest share of a bar a single widget may shrink to, as a fraction. */
|
|
10
10
|
const MIN_WIDGET_SHARE = 0.1
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProjectSnapshotV1 } from './types'
|
|
2
2
|
|
|
3
3
|
import { computePaneRects, type LayoutNode, PANE_BORDER, type PaneRect } from './layout-tree'
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ export interface TerminalBounds {
|
|
|
9
9
|
rows: number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function getSnapshotTrees(snapshot:
|
|
12
|
+
export function getSnapshotTrees(snapshot: ProjectSnapshotV1 | undefined): LayoutNode[] {
|
|
13
13
|
if (snapshot?.layoutTrees) {
|
|
14
14
|
return Object.values(snapshot.layoutTrees)
|
|
15
15
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { ProjectRecord } from './types'
|
|
5
|
+
|
|
6
|
+
import { loadConfig, saveConfig } from '../config'
|
|
7
|
+
import { logDebug } from '../debug/input-log'
|
|
8
|
+
import { getProfileConfigDir } from '../profile-paths'
|
|
9
|
+
import { ensureProjectWorkspaces } from './project-workspaces'
|
|
10
|
+
import { toast } from './toast-store'
|
|
11
|
+
import { isProjectRecord } from './validation'
|
|
12
|
+
|
|
13
|
+
const CATALOG_VERSION = 2
|
|
14
|
+
|
|
15
|
+
interface ProjectCatalogFile {
|
|
16
|
+
version: typeof CATALOG_VERSION
|
|
17
|
+
projects: ProjectRecord[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Resolved per call, never module-cached: $HOME is read at call time so tests
|
|
21
|
+
// that redirect it cannot clobber the real catalog.
|
|
22
|
+
function projectsPath(): string {
|
|
23
|
+
return join(getProfileConfigDir(), 'aimux-projects.json')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ponytail: the v1 file is left on disk so a downgrade still works. Drop this
|
|
27
|
+
// helper, migrateV1 and the fallback branch a release or two out.
|
|
28
|
+
function legacySessionsPath(): string {
|
|
29
|
+
return join(getProfileConfigDir(), 'aimux-sessions.json')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The v1 shape, before project/workspace naming. Only what migrateV1 reads. */
|
|
33
|
+
interface LegacyProjectRecordV1 {
|
|
34
|
+
projectPath?: string
|
|
35
|
+
workspaceSnapshot?: {
|
|
36
|
+
tabs?: { worktreeId?: string }[]
|
|
37
|
+
lastActiveTabByWorktree?: Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
worktrees?: { source?: string; repoRoot?: string }[]
|
|
40
|
+
activeWorktreeId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* v1 -> v2: `worktrees` became `workspaces`, `workspaceSnapshot` became
|
|
45
|
+
* `projectSnapshot`, and each tab's `worktreeId` became `workspaceId`. Record
|
|
46
|
+
* ids are deliberately left alone — they are opaque and may be scripted.
|
|
47
|
+
*
|
|
48
|
+
* `projectPath` is also healed here: v1 rewrote it with the *active worktree's*
|
|
49
|
+
* path, so it usually pointed inside the worktree root rather than at the repo.
|
|
50
|
+
*/
|
|
51
|
+
function migrateV1(records: LegacyProjectRecordV1[]): unknown[] {
|
|
52
|
+
return records.map((record) => {
|
|
53
|
+
const { activeWorktreeId, workspaceSnapshot, worktrees, ...rest } = record
|
|
54
|
+
const primaryRepoRoot = worktrees?.find((w) => w.source === 'primary')?.repoRoot
|
|
55
|
+
return {
|
|
56
|
+
...rest,
|
|
57
|
+
...(primaryRepoRoot != null && primaryRepoRoot !== ''
|
|
58
|
+
? { projectPath: primaryRepoRoot }
|
|
59
|
+
: null),
|
|
60
|
+
...(worktrees ? { workspaces: worktrees } : null),
|
|
61
|
+
...(activeWorktreeId != null ? { activeWorkspaceId: activeWorktreeId } : null),
|
|
62
|
+
...(workspaceSnapshot
|
|
63
|
+
? {
|
|
64
|
+
projectSnapshot: (() => {
|
|
65
|
+
const { lastActiveTabByWorktree, tabs, ...snapshot } = workspaceSnapshot
|
|
66
|
+
return {
|
|
67
|
+
...snapshot,
|
|
68
|
+
...(lastActiveTabByWorktree
|
|
69
|
+
? { lastActiveTabByWorkspace: lastActiveTabByWorktree }
|
|
70
|
+
: null),
|
|
71
|
+
...(tabs
|
|
72
|
+
? {
|
|
73
|
+
tabs: tabs.map(({ worktreeId, ...tab }) => ({
|
|
74
|
+
...tab,
|
|
75
|
+
...(worktreeId != null ? { workspaceId: worktreeId } : null),
|
|
76
|
+
})),
|
|
77
|
+
}
|
|
78
|
+
: null),
|
|
79
|
+
}
|
|
80
|
+
})(),
|
|
81
|
+
}
|
|
82
|
+
: null),
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Copy the catalog aside before the load path writes over it. Called whenever
|
|
89
|
+
* we could not read the file whole: the app is about to persist its own
|
|
90
|
+
* reduced view, and without this that write is the point of no return.
|
|
91
|
+
*/
|
|
92
|
+
function backupUnreadableCatalog(): void {
|
|
93
|
+
const source = projectsPath()
|
|
94
|
+
if (!existsSync(source)) return
|
|
95
|
+
const target = `${source}.unreadable`
|
|
96
|
+
try {
|
|
97
|
+
copyFileSync(source, target)
|
|
98
|
+
logDebug('projects.catalog.backup', { target })
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logDebug('projects.catalog.backupFailed', {
|
|
101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseCatalog(path: string, version: number): { projects: unknown[] } | { issue: string } {
|
|
107
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8')) as {
|
|
108
|
+
version?: unknown
|
|
109
|
+
projects?: unknown
|
|
110
|
+
sessions?: unknown
|
|
111
|
+
}
|
|
112
|
+
if (parsed.version !== version) {
|
|
113
|
+
return { issue: `invalid project catalog header in ${path}` }
|
|
114
|
+
}
|
|
115
|
+
// v1 called the array `sessions`.
|
|
116
|
+
const records = parsed.projects ?? parsed.sessions
|
|
117
|
+
if (!Array.isArray(records)) {
|
|
118
|
+
return { issue: `invalid project catalog header in ${path}` }
|
|
119
|
+
}
|
|
120
|
+
return { projects: records }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readCatalogFile(): { file: ProjectCatalogFile | null; issue?: string } {
|
|
124
|
+
try {
|
|
125
|
+
let records: unknown[]
|
|
126
|
+
if (existsSync(projectsPath())) {
|
|
127
|
+
const parsed = parseCatalog(projectsPath(), CATALOG_VERSION)
|
|
128
|
+
if ('issue' in parsed) {
|
|
129
|
+
backupUnreadableCatalog()
|
|
130
|
+
return { file: null, issue: parsed.issue }
|
|
131
|
+
}
|
|
132
|
+
records = parsed.projects
|
|
133
|
+
} else if (existsSync(legacySessionsPath())) {
|
|
134
|
+
const parsed = parseCatalog(legacySessionsPath(), 1)
|
|
135
|
+
if ('issue' in parsed) return { file: null, issue: parsed.issue }
|
|
136
|
+
records = migrateV1(parsed.projects as LegacyProjectRecordV1[])
|
|
137
|
+
logDebug('projects.catalog.migrateV1', { projectCount: records.length })
|
|
138
|
+
} else {
|
|
139
|
+
return { file: null }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Keep the records that survive validation instead of failing the whole
|
|
143
|
+
// file: one malformed entry used to cost the user every project, and since
|
|
144
|
+
// the load path writes back what it loaded, that loss became permanent on
|
|
145
|
+
// the next save. Whatever we could not read is preserved by the backup
|
|
146
|
+
// below, so nothing is destroyed either way.
|
|
147
|
+
const valid = records.filter(isProjectRecord)
|
|
148
|
+
if (valid.length !== records.length) {
|
|
149
|
+
backupUnreadableCatalog()
|
|
150
|
+
return {
|
|
151
|
+
file: { projects: valid, version: CATALOG_VERSION },
|
|
152
|
+
issue: `${records.length - valid.length} unreadable project record(s) skipped`,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { file: { projects: valid, version: CATALOG_VERSION } }
|
|
157
|
+
} catch (error) {
|
|
158
|
+
backupUnreadableCatalog()
|
|
159
|
+
return {
|
|
160
|
+
file: null,
|
|
161
|
+
issue: `failed to load project catalog: ${error instanceof Error ? error.message : String(error)}`,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function loadProjectCatalog(): ProjectRecord[] {
|
|
167
|
+
const { file, issue } = readCatalogFile()
|
|
168
|
+
if (file) {
|
|
169
|
+
logDebug('projects.catalog.load', { projectCount: file.projects.length })
|
|
170
|
+
if (issue != null && issue !== '') {
|
|
171
|
+
// Same reasoning as the save path below: losing projects is not something
|
|
172
|
+
// the user should have to read a debug log to discover.
|
|
173
|
+
toast.error(`${issue} — original kept at ${projectsPath()}.unreadable`)
|
|
174
|
+
}
|
|
175
|
+
const normalized = normalizeProjects(file.projects)
|
|
176
|
+
// A fresh migration has no v2 file yet, so write even when normalizing
|
|
177
|
+
// changed nothing.
|
|
178
|
+
if (
|
|
179
|
+
!existsSync(projectsPath()) ||
|
|
180
|
+
JSON.stringify(normalized) !== JSON.stringify(file.projects)
|
|
181
|
+
) {
|
|
182
|
+
saveProjectCatalog(normalized)
|
|
183
|
+
}
|
|
184
|
+
return normalized
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (issue != null && issue !== '') {
|
|
188
|
+
logDebug('projects.catalog.loadIssue', { issue, path: projectsPath() })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const config = loadConfig()
|
|
192
|
+
if (!config.projectSnapshot) {
|
|
193
|
+
logDebug('projects.catalog.load', { projectCount: 0 })
|
|
194
|
+
return []
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const now = new Date().toISOString()
|
|
198
|
+
const migrated: ProjectRecord = {
|
|
199
|
+
createdAt: now,
|
|
200
|
+
id: `project-${Date.now()}`,
|
|
201
|
+
lastOpenedAt: now,
|
|
202
|
+
name: 'Last project',
|
|
203
|
+
projectSnapshot: config.projectSnapshot,
|
|
204
|
+
updatedAt: now,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const normalized = normalizeProjects([migrated])
|
|
208
|
+
saveProjectCatalog(normalized)
|
|
209
|
+
saveConfig({ ...config, projectSnapshot: undefined })
|
|
210
|
+
logDebug('projects.catalog.migrateLegacyProject', {
|
|
211
|
+
migratedProjectId: migrated.id,
|
|
212
|
+
tabCount: migrated.projectSnapshot?.tabs.length ?? 0,
|
|
213
|
+
})
|
|
214
|
+
return normalized
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function saveProjectCatalog(projects: ProjectRecord[]): void {
|
|
218
|
+
try {
|
|
219
|
+
mkdirSync(getProfileConfigDir(), { recursive: true })
|
|
220
|
+
writeFileSync(
|
|
221
|
+
projectsPath(),
|
|
222
|
+
`${JSON.stringify({ projects, version: CATALOG_VERSION }, null, 2)}\n`
|
|
223
|
+
)
|
|
224
|
+
logDebug('projects.catalog.save', { projectCount: projects.length })
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
227
|
+
logDebug('projects.catalog.saveError', {
|
|
228
|
+
error: message,
|
|
229
|
+
path: projectsPath(),
|
|
230
|
+
projectCount: projects.length,
|
|
231
|
+
})
|
|
232
|
+
// Persisting the catalog underpins project/workspace state — a silent
|
|
233
|
+
// failure means the user loses projects on restart. Surface it.
|
|
234
|
+
toast.error(`Failed to save projects: ${message}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function getProjectCatalogPath(): string {
|
|
239
|
+
return projectsPath()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function findMostRecentProject(projects: ProjectRecord[]): ProjectRecord | undefined {
|
|
243
|
+
let best: ProjectRecord | undefined
|
|
244
|
+
for (const candidate of projects) {
|
|
245
|
+
if (!best || candidate.lastOpenedAt.localeCompare(best.lastOpenedAt) > 0) {
|
|
246
|
+
best = candidate
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return best
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Assign a stable `order` to every project. Records with an existing numeric
|
|
254
|
+
* `order` keep their slot (sorted ascending); the rest are appended by
|
|
255
|
+
* createdAt ascending so older projects come first.
|
|
256
|
+
*/
|
|
257
|
+
function normalizeOrder(projects: ProjectRecord[]): ProjectRecord[] {
|
|
258
|
+
const withOrder: ProjectRecord[] = []
|
|
259
|
+
const withoutOrder: ProjectRecord[] = []
|
|
260
|
+
for (const s of projects) {
|
|
261
|
+
if (typeof s.order === 'number' && Number.isFinite(s.order)) {
|
|
262
|
+
withOrder.push(s)
|
|
263
|
+
} else {
|
|
264
|
+
withoutOrder.push(s)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
withOrder.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
268
|
+
withoutOrder.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
|
269
|
+
const merged = [...withOrder, ...withoutOrder]
|
|
270
|
+
return merged.map((s, i) => (s.order === i ? s : { ...s, order: i }))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeProjects(projects: ProjectRecord[]): ProjectRecord[] {
|
|
274
|
+
return normalizeOrder(projects).map((project) => ensureProjectWorkspaces(project))
|
|
275
|
+
}
|