@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.
Files changed (220) hide show
  1. package/README.md +27 -25
  2. package/package.json +2 -2
  3. package/src/app-runtime/auto-commit-driver.ts +22 -27
  4. package/src/app-runtime/backend-attach-runtime.ts +43 -36
  5. package/src/app-runtime/backend-runtime-events.ts +107 -88
  6. package/src/app-runtime/editor-actions.ts +201 -0
  7. package/src/app-runtime/git-actions.ts +197 -0
  8. package/src/app-runtime/measure-ref.ts +19 -0
  9. package/src/app-runtime/navigation-actions.ts +213 -0
  10. package/src/app-runtime/project-actions.ts +188 -0
  11. package/src/app-runtime/prompt-injection.ts +123 -0
  12. package/src/app-runtime/selection.ts +29 -0
  13. package/src/app-runtime/settings-actions.ts +156 -0
  14. package/src/app-runtime/setup-actions.ts +237 -0
  15. package/src/app-runtime/side-effect-context.ts +33 -0
  16. package/src/app-runtime/side-effects.ts +302 -1541
  17. package/src/app-runtime/snippet-actions.ts +3 -2
  18. package/src/app-runtime/tab-actions.ts +218 -0
  19. package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
  20. package/src/app-runtime/use-auto-commit-driver.ts +14 -13
  21. package/src/app-runtime/use-backend-runtime.ts +12 -11
  22. package/src/app-runtime/use-directory-search.ts +5 -4
  23. package/src/app-runtime/use-mouse-handlers.ts +2 -1
  24. package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
  25. package/src/app-runtime/use-renderer-bindings.ts +2 -1
  26. package/src/app-runtime/use-setup-runner.ts +96 -0
  27. package/src/app-runtime/use-terminal-resize.ts +18 -14
  28. package/src/app-runtime/workspace-actions.ts +366 -0
  29. package/src/app-runtime/workspace-activity.ts +138 -0
  30. package/src/app-runtime/workspace-naming.ts +90 -0
  31. package/src/app.tsx +93 -48
  32. package/src/assets/sounds/bell.wav +0 -0
  33. package/src/assets/sounds/ding.wav +0 -0
  34. package/src/assets/sounds/plane.wav +0 -0
  35. package/src/cli/client/daemon-client.ts +2 -2
  36. package/src/cli/client/project-resolver.ts +112 -0
  37. package/src/cli/commands/project/close.ts +32 -0
  38. package/src/cli/commands/project/create.ts +93 -0
  39. package/src/cli/commands/project/list.ts +25 -0
  40. package/src/cli/commands/project/show.ts +32 -0
  41. package/src/cli/commands/{workspace → project}/switch.ts +19 -19
  42. package/src/cli/commands/tab/await.ts +2 -2
  43. package/src/cli/commands/tab/close.ts +3 -3
  44. package/src/cli/commands/tab/create.ts +74 -73
  45. package/src/cli/commands/tab/focus.ts +3 -3
  46. package/src/cli/commands/tab/list.ts +6 -6
  47. package/src/cli/commands/tab/run.ts +2 -2
  48. package/src/cli/commands/tab/send.ts +2 -2
  49. package/src/cli/commands/tab/snapshot.ts +2 -2
  50. package/src/cli/commands/tab/tail.ts +2 -2
  51. package/src/cli/commands/tab/wait.ts +2 -2
  52. package/src/cli/commands/worker/await.ts +3 -3
  53. package/src/cli/commands/worker/doctor.ts +30 -30
  54. package/src/cli/commands/worker/list.ts +15 -15
  55. package/src/cli/commands/worker/prompt.ts +3 -3
  56. package/src/cli/commands/worker/run.ts +23 -23
  57. package/src/cli/commands/worker/shared.ts +53 -59
  58. package/src/cli/commands/worker/stop.ts +29 -29
  59. package/src/cli/commands/worker/submit.ts +3 -3
  60. package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
  61. package/src/cli/commands/workspace/create.ts +30 -68
  62. package/src/cli/commands/workspace/list.ts +52 -9
  63. package/src/cli/commands/workspace/remove.ts +81 -0
  64. package/src/cli/completion/plan.ts +2 -2
  65. package/src/cli/completion/sources.ts +18 -18
  66. package/src/cli/context.ts +13 -13
  67. package/src/cli/flags.ts +30 -10
  68. package/src/cli/index.ts +26 -16
  69. package/src/cli/output.ts +1 -1
  70. package/src/cli/registry.ts +12 -12
  71. package/src/config/loader.ts +16 -5
  72. package/src/config.ts +70 -112
  73. package/src/daemon/catalog-writer.ts +67 -67
  74. package/src/daemon/daemon.ts +205 -201
  75. package/src/daemon/session-manager.ts +39 -39
  76. package/src/daemon/session-registry.ts +14 -14
  77. package/src/git/divergence.ts +33 -2
  78. package/src/git/git-poller.ts +1 -1
  79. package/src/git/git-status.ts +2 -2
  80. package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
  81. package/src/git/pr-status.ts +1 -1
  82. package/src/git/repo-discovery.ts +1 -1
  83. package/src/git/use-repo-discovery.ts +1 -1
  84. package/src/git/workspace-branch-poller.ts +54 -0
  85. package/src/git/workspace-divergence-poller.ts +65 -0
  86. package/src/git/worktree.ts +29 -0
  87. package/src/index.tsx +4 -4
  88. package/src/input/keymap/help-entries.ts +7 -5
  89. package/src/input/modes/bridge.ts +13 -12
  90. package/src/input/modes/handlers/shared.ts +1 -1
  91. package/src/input/modes/transitions.ts +34 -21
  92. package/src/input/modes/types.ts +48 -31
  93. package/src/ipc/manager-protocol.ts +44 -44
  94. package/src/ipc/protocol.ts +192 -166
  95. package/src/platform/play-sound.ts +184 -0
  96. package/src/platform/project-search.ts +8 -8
  97. package/src/platform/worktree-paths.ts +8 -6
  98. package/src/pty/assistant-question-extractor.ts +1 -1
  99. package/src/pty/assistant-status-detection-loop.ts +79 -55
  100. package/src/pty/assistant-status-detector.ts +40 -15
  101. package/src/pty/command-registry.ts +81 -1
  102. package/src/restart-terminal-manager.ts +1 -1
  103. package/src/services/ai-usage/provider.ts +9 -2
  104. package/src/services/ai-usage/spawn.ts +19 -6
  105. package/src/session-backend/bootstrap.ts +4 -4
  106. package/src/session-backend/local-session-backend.ts +78 -75
  107. package/src/session-backend/remote-session-backend.ts +64 -52
  108. package/src/session-backend/types.ts +43 -32
  109. package/src/settings/live.ts +90 -0
  110. package/src/settings/search.ts +43 -0
  111. package/src/settings/sections/about.ts +49 -0
  112. package/src/settings/sections/appearance.ts +54 -0
  113. package/src/settings/sections/automation.ts +105 -0
  114. package/src/settings/sections/commands.ts +72 -0
  115. package/src/settings/sections/editor.ts +56 -0
  116. package/src/settings/sections/experimental.ts +57 -0
  117. package/src/settings/sections/git.ts +112 -0
  118. package/src/settings/sections/index.ts +95 -0
  119. package/src/settings/sections/integrations.ts +23 -0
  120. package/src/settings/sections/layout.ts +72 -0
  121. package/src/settings/sections/notifications.ts +99 -0
  122. package/src/settings/sections/setup.ts +54 -0
  123. package/src/settings/sections/status-bar.ts +63 -0
  124. package/src/settings/settings-store.ts +207 -0
  125. package/src/settings/types.ts +108 -0
  126. package/src/snippets/run-shell-var.ts +48 -22
  127. package/src/state/actions.ts +342 -0
  128. package/src/state/app-store.ts +2 -1
  129. package/src/state/bars.ts +1 -1
  130. package/src/state/dispatch-ref.ts +1 -1
  131. package/src/state/layout-resize.ts +2 -2
  132. package/src/state/project-catalog.ts +275 -0
  133. package/src/state/project-data.ts +143 -0
  134. package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
  135. package/src/state/project-save.ts +46 -0
  136. package/src/state/project-workspaces.ts +392 -0
  137. package/src/state/reducers/auto-commit-state.ts +11 -10
  138. package/src/state/reducers/git-commit-modal-state.ts +122 -0
  139. package/src/state/reducers/git-mode-state.ts +1 -1
  140. package/src/state/reducers/git-panel-state.ts +10 -6
  141. package/src/state/reducers/modal-state.ts +290 -681
  142. package/src/state/reducers/multi-repo-state.ts +3 -6
  143. package/src/state/reducers/project-state.ts +312 -0
  144. package/src/state/reducers/settings-state.ts +80 -0
  145. package/src/state/reducers/tab-state.ts +107 -60
  146. package/src/state/reducers/ui-state.ts +4 -3
  147. package/src/state/selectors.ts +49 -37
  148. package/src/state/store.ts +57 -24
  149. package/src/state/types.ts +244 -405
  150. package/src/state/validation.ts +14 -12
  151. package/src/terminal-manager/manager-client.ts +32 -32
  152. package/src/terminal-manager/terminal-manager.ts +24 -24
  153. package/src/ui/components/git/git-panel.tsx +2 -2
  154. package/src/ui/components/git/git-view.tsx +12 -12
  155. package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
  156. package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
  157. package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
  158. package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
  159. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
  160. package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
  161. package/src/ui/components/layout/status-bar.tsx +6 -2
  162. package/src/ui/components/layout/terminal-pane.tsx +34 -29
  163. package/src/ui/components/layout/top-tab-bar.tsx +46 -36
  164. package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
  165. package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
  166. package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
  167. package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
  168. package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
  169. package/src/ui/components/modals/shared/form.tsx +34 -22
  170. package/src/ui/components/modals/shared/picker.tsx +10 -10
  171. package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
  172. package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
  173. package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
  174. package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
  175. package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
  176. package/src/ui/components/primitives/input-field.tsx +7 -3
  177. package/src/ui/components/primitives/surface.tsx +3 -0
  178. package/src/ui/components/settings/row-value.tsx +74 -0
  179. package/src/ui/components/settings/settings-row.tsx +78 -0
  180. package/src/ui/components/settings/settings-view.tsx +169 -0
  181. package/src/ui/components/setup/setup-widget.tsx +152 -0
  182. package/src/ui/flash/build-labels.ts +27 -27
  183. package/src/ui/hooks/use-activity-sprite.ts +73 -0
  184. package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
  185. package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
  186. package/src/ui/root.tsx +139 -121
  187. package/src/ui/status-bar-model.ts +48 -34
  188. package/src/ui/terminal-graphics/kitty.ts +28 -2
  189. package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
  190. package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
  191. package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
  192. package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
  193. package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
  194. package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
  195. package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
  196. package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
  197. package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
  198. package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
  199. package/src/ui/terminal-graphics/sprites.ts +246 -0
  200. package/src/ui/truncate.ts +7 -0
  201. package/src/ui/widgets/registry.tsx +6 -3
  202. package/src/ui/widgets/widget-context-menu.ts +26 -10
  203. package/src/update.ts +2 -2
  204. package/src/app-runtime/session-actions.ts +0 -187
  205. package/src/cli/client/workspace-resolver.ts +0 -110
  206. package/src/cli/commands/workspace/close.ts +0 -32
  207. package/src/cli/commands/workspace/show.ts +0 -32
  208. package/src/cli/commands/worktree/create.ts +0 -55
  209. package/src/cli/commands/worktree/list.ts +0 -68
  210. package/src/cli/commands/worktree/remove.ts +0 -81
  211. package/src/git/worktree-branch-poller.ts +0 -54
  212. package/src/git/worktree-divergence-poller.ts +0 -59
  213. package/src/state/reducers/session-state.ts +0 -262
  214. package/src/state/session-catalog.ts +0 -143
  215. package/src/state/session-worktrees.ts +0 -260
  216. package/src/state/workspace-save.ts +0 -43
  217. package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
  218. package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
  219. package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
  220. /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
@@ -1,7 +1,8 @@
1
1
  import { useStore } from 'zustand'
2
2
  import { createStore } from 'zustand/vanilla'
3
3
 
4
- import type { AppAction, AppState } from './types'
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 = ['workspaces', 'git'] as const
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,5 +1,5 @@
1
1
  import type { SideEffect } from '../input/modes/types'
2
- import type { AppAction } from './types'
2
+ import type { AppAction } from './actions'
3
3
 
4
4
  type DispatchFn = (action: AppAction) => void
5
5
  type SideEffectFn = (effect: SideEffect) => void
@@ -1,4 +1,4 @@
1
- import type { WorkspaceSnapshotV1 } from './types'
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: WorkspaceSnapshotV1 | undefined): LayoutNode[] {
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
+ }