@brimveyn/aimux 1.20.4 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) 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 +305 -1589
  17. package/src/app-runtime/snippet-actions.ts +3 -2
  18. package/src/app-runtime/split-drag-controller.ts +4 -8
  19. package/src/app-runtime/tab-actions.ts +218 -0
  20. package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
  21. package/src/app-runtime/use-auto-commit-driver.ts +14 -13
  22. package/src/app-runtime/use-backend-runtime.ts +12 -11
  23. package/src/app-runtime/use-directory-search.ts +5 -4
  24. package/src/app-runtime/use-mouse-handlers.ts +28 -42
  25. package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
  26. package/src/app-runtime/use-renderer-bindings.ts +2 -1
  27. package/src/app-runtime/use-setup-runner.ts +96 -0
  28. package/src/app-runtime/use-terminal-resize.ts +28 -33
  29. package/src/app-runtime/workspace-actions.ts +366 -0
  30. package/src/app-runtime/workspace-activity.ts +138 -0
  31. package/src/app-runtime/workspace-naming.ts +90 -0
  32. package/src/app.tsx +102 -85
  33. package/src/assets/sounds/bell.wav +0 -0
  34. package/src/assets/sounds/ding.wav +0 -0
  35. package/src/assets/sounds/plane.wav +0 -0
  36. package/src/cli/client/daemon-client.ts +2 -2
  37. package/src/cli/client/project-resolver.ts +112 -0
  38. package/src/cli/commands/project/close.ts +32 -0
  39. package/src/cli/commands/project/create.ts +93 -0
  40. package/src/cli/commands/project/list.ts +25 -0
  41. package/src/cli/commands/project/show.ts +32 -0
  42. package/src/cli/commands/{workspace → project}/switch.ts +19 -19
  43. package/src/cli/commands/tab/await.ts +2 -2
  44. package/src/cli/commands/tab/close.ts +3 -3
  45. package/src/cli/commands/tab/create.ts +74 -73
  46. package/src/cli/commands/tab/focus.ts +3 -3
  47. package/src/cli/commands/tab/list.ts +6 -6
  48. package/src/cli/commands/tab/run.ts +2 -2
  49. package/src/cli/commands/tab/send.ts +2 -2
  50. package/src/cli/commands/tab/snapshot.ts +2 -2
  51. package/src/cli/commands/tab/tail.ts +2 -2
  52. package/src/cli/commands/tab/wait.ts +2 -2
  53. package/src/cli/commands/worker/await.ts +3 -3
  54. package/src/cli/commands/worker/doctor.ts +30 -30
  55. package/src/cli/commands/worker/list.ts +15 -15
  56. package/src/cli/commands/worker/prompt.ts +3 -3
  57. package/src/cli/commands/worker/run.ts +23 -23
  58. package/src/cli/commands/worker/shared.ts +53 -59
  59. package/src/cli/commands/worker/stop.ts +29 -29
  60. package/src/cli/commands/worker/submit.ts +3 -3
  61. package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
  62. package/src/cli/commands/workspace/create.ts +30 -68
  63. package/src/cli/commands/workspace/list.ts +52 -9
  64. package/src/cli/commands/workspace/remove.ts +81 -0
  65. package/src/cli/completion/plan.ts +2 -2
  66. package/src/cli/completion/sources.ts +18 -18
  67. package/src/cli/context.ts +13 -13
  68. package/src/cli/flags.ts +30 -10
  69. package/src/cli/index.ts +26 -16
  70. package/src/cli/output.ts +1 -1
  71. package/src/cli/registry.ts +12 -12
  72. package/src/config/loader.ts +16 -5
  73. package/src/config.ts +149 -101
  74. package/src/daemon/catalog-writer.ts +67 -67
  75. package/src/daemon/daemon.ts +205 -201
  76. package/src/daemon/session-manager.ts +39 -39
  77. package/src/daemon/session-registry.ts +14 -14
  78. package/src/git/divergence.ts +33 -2
  79. package/src/git/git-poller.ts +1 -1
  80. package/src/git/git-status.ts +2 -2
  81. package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
  82. package/src/git/pr-merge.ts +64 -0
  83. package/src/git/pr-status-poller.ts +57 -0
  84. package/src/git/pr-status.ts +227 -0
  85. package/src/git/repo-discovery.ts +1 -1
  86. package/src/git/use-repo-discovery.ts +1 -1
  87. package/src/git/workspace-branch-poller.ts +54 -0
  88. package/src/git/workspace-divergence-poller.ts +65 -0
  89. package/src/git/worktree.ts +29 -0
  90. package/src/index.tsx +11 -6
  91. package/src/input/keymap/help-entries.ts +7 -5
  92. package/src/input/modes/bridge.ts +13 -12
  93. package/src/input/modes/handlers/shared.ts +1 -1
  94. package/src/input/modes/transitions.ts +34 -21
  95. package/src/input/modes/types.ts +48 -31
  96. package/src/ipc/manager-protocol.ts +44 -44
  97. package/src/ipc/protocol.ts +192 -166
  98. package/src/platform/open-url.ts +39 -0
  99. package/src/platform/play-sound.ts +184 -0
  100. package/src/platform/project-search.ts +8 -8
  101. package/src/platform/worktree-paths.ts +8 -6
  102. package/src/pty/assistant-question-extractor.ts +1 -1
  103. package/src/pty/assistant-status-detection-loop.ts +79 -55
  104. package/src/pty/assistant-status-detector.ts +40 -15
  105. package/src/pty/command-registry.ts +81 -1
  106. package/src/restart-terminal-manager.ts +1 -1
  107. package/src/services/ai-usage/provider.ts +9 -2
  108. package/src/services/ai-usage/spawn.ts +3 -1
  109. package/src/session-backend/bootstrap.ts +4 -4
  110. package/src/session-backend/local-session-backend.ts +78 -75
  111. package/src/session-backend/remote-session-backend.ts +64 -52
  112. package/src/session-backend/types.ts +43 -32
  113. package/src/settings/live.ts +90 -0
  114. package/src/settings/search.ts +43 -0
  115. package/src/settings/sections/about.ts +49 -0
  116. package/src/settings/sections/appearance.ts +54 -0
  117. package/src/settings/sections/automation.ts +105 -0
  118. package/src/settings/sections/commands.ts +72 -0
  119. package/src/settings/sections/editor.ts +56 -0
  120. package/src/settings/sections/experimental.ts +57 -0
  121. package/src/settings/sections/git.ts +112 -0
  122. package/src/settings/sections/index.ts +95 -0
  123. package/src/settings/sections/integrations.ts +23 -0
  124. package/src/settings/sections/layout.ts +72 -0
  125. package/src/settings/sections/notifications.ts +99 -0
  126. package/src/settings/sections/setup.ts +54 -0
  127. package/src/settings/sections/status-bar.ts +63 -0
  128. package/src/settings/settings-store.ts +207 -0
  129. package/src/settings/types.ts +108 -0
  130. package/src/snippets/run-shell-var.ts +48 -22
  131. package/src/state/actions.ts +342 -0
  132. package/src/state/app-store.ts +2 -1
  133. package/src/state/bars.ts +75 -0
  134. package/src/state/dispatch-ref.ts +1 -1
  135. package/src/state/git-pane-sizing.ts +0 -9
  136. package/src/state/layout-resize.ts +2 -2
  137. package/src/state/pr-status-store.ts +39 -0
  138. package/src/state/project-catalog.ts +275 -0
  139. package/src/state/project-data.ts +143 -0
  140. package/src/state/{session-persistence.ts → project-persistence.ts} +77 -73
  141. package/src/state/project-save.ts +46 -0
  142. package/src/state/project-workspaces.ts +392 -0
  143. package/src/state/reducers/auto-commit-state.ts +11 -10
  144. package/src/state/reducers/git-commit-modal-state.ts +122 -0
  145. package/src/state/reducers/git-mode-state.ts +1 -1
  146. package/src/state/reducers/git-panel-state.ts +10 -53
  147. package/src/state/reducers/modal-state.ts +290 -681
  148. package/src/state/reducers/multi-repo-state.ts +3 -6
  149. package/src/state/reducers/project-state.ts +312 -0
  150. package/src/state/reducers/settings-state.ts +80 -0
  151. package/src/state/reducers/tab-state.ts +107 -60
  152. package/src/state/reducers/ui-state.ts +86 -15
  153. package/src/state/selectors.ts +49 -37
  154. package/src/state/store.ts +83 -57
  155. package/src/state/types.ts +261 -417
  156. package/src/state/validation.ts +14 -12
  157. package/src/terminal-manager/manager-client.ts +32 -32
  158. package/src/terminal-manager/terminal-manager.ts +24 -24
  159. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
  160. package/src/ui/components/git/git-panel.tsx +2 -2
  161. package/src/ui/components/git/git-view.tsx +12 -12
  162. package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
  163. package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
  164. package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
  165. package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
  166. package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
  167. package/src/ui/components/layout/bar.tsx +191 -0
  168. package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
  169. package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
  170. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
  171. package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
  172. package/src/ui/components/layout/status-bar.tsx +6 -2
  173. package/src/ui/components/layout/terminal-pane.tsx +34 -29
  174. package/src/ui/components/layout/top-tab-bar.tsx +48 -37
  175. package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
  176. package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
  177. package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
  178. package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
  179. package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
  180. package/src/ui/components/modals/shared/form.tsx +34 -22
  181. package/src/ui/components/modals/shared/picker.tsx +10 -10
  182. package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
  183. package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
  184. package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
  185. package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
  186. package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
  187. package/src/ui/components/primitives/input-field.tsx +7 -3
  188. package/src/ui/components/primitives/surface.tsx +3 -0
  189. package/src/ui/components/settings/row-value.tsx +74 -0
  190. package/src/ui/components/settings/settings-row.tsx +78 -0
  191. package/src/ui/components/settings/settings-view.tsx +169 -0
  192. package/src/ui/components/setup/setup-widget.tsx +152 -0
  193. package/src/ui/flash/build-labels.ts +27 -27
  194. package/src/ui/hooks/use-activity-sprite.ts +73 -0
  195. package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
  196. package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
  197. package/src/ui/root.tsx +164 -230
  198. package/src/ui/status-bar-model.ts +48 -34
  199. package/src/ui/terminal-graphics/kitty.ts +28 -2
  200. package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
  201. package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
  202. package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
  203. package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
  204. package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
  205. package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
  206. package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
  207. package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
  208. package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
  209. package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
  210. package/src/ui/terminal-graphics/sprites.ts +246 -0
  211. package/src/ui/truncate.ts +7 -0
  212. package/src/ui/widgets/registry.tsx +21 -0
  213. package/src/ui/widgets/widget-context-menu.ts +85 -0
  214. package/src/update.ts +2 -2
  215. package/src/app-runtime/session-actions.ts +0 -187
  216. package/src/cli/client/workspace-resolver.ts +0 -110
  217. package/src/cli/commands/workspace/close.ts +0 -32
  218. package/src/cli/commands/workspace/show.ts +0 -32
  219. package/src/cli/commands/worktree/create.ts +0 -55
  220. package/src/cli/commands/worktree/list.ts +0 -68
  221. package/src/cli/commands/worktree/remove.ts +0 -81
  222. package/src/git/worktree-branch-poller.ts +0 -54
  223. package/src/git/worktree-divergence-poller.ts +0 -59
  224. package/src/state/reducers/session-state.ts +0 -262
  225. package/src/state/session-catalog.ts +0 -143
  226. package/src/state/session-worktrees.ts +0 -260
  227. package/src/state/workspace-save.ts +0 -45
  228. package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
  229. package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
  230. package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
  231. package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
  232. package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
  233. /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
package/src/cli/index.ts CHANGED
@@ -2,7 +2,7 @@ import type { DaemonClient } from './client/daemon-client'
2
2
  import type { CliContext } from './context'
3
3
 
4
4
  import { connectToDaemon, DaemonUnreachableError } from './client/bootstrap'
5
- import { listWorkspaces, resolveWorkspaceWithOrigin } from './client/workspace-resolver'
5
+ import { listProjects, resolveProjectWithOrigin } from './client/project-resolver'
6
6
  import { type ArgSpec, CliUsageError, type FlagSpec, parseArgs, SHARED_FLAGS } from './flags'
7
7
  import {
8
8
  EXIT_DAEMON_UNREACHABLE,
@@ -22,7 +22,7 @@ const EXIT_CODES_BLOCK = [
22
22
  ' 4 daemon unreachable (socket missing and autostart failed)',
23
23
  ' 10 question (tab run / tab await: worker is blocked on a question/permission)',
24
24
  ' 11 pending submit (worker holds an unsubmitted prompt — see `worker submit`)',
25
- ' 124 timeout (tab run, tab await, tab wait, tab tail --timeout, workspace switch --wait)',
25
+ ' 124 timeout (tab run, tab await, tab wait, tab tail --timeout, project switch --wait)',
26
26
  ].join('\n')
27
27
 
28
28
  const OUTPUT_CONTRACT_BLOCK = [
@@ -100,12 +100,12 @@ function printHelp(): void {
100
100
  '',
101
101
  'Env:',
102
102
  ' AIMUX_PROFILE Runtime profile (state dir, socket paths); --profile overrides.',
103
- ' AIMUX_WORKSPACE Pin the target workspace (id or name); --workspace overrides.',
103
+ ' AIMUX_PROJECT Pin the target project (id or name); --project overrides.',
104
104
  '',
105
105
  'Agent recipe:',
106
106
  ' # create an isolated named worker, dispatch, and await one structured outcome',
107
- ' # --workspace pins the target repo so a UI workspace switch cannot redirect it',
108
- ' aimux worker run --workspace myrepo --name fixup --assistant claude "explain this repo"',
107
+ ' # --project pins the target repo so a UI project switch cannot redirect it',
108
+ ' aimux worker run --project myrepo --name fixup --assistant claude "explain this repo"',
109
109
  '',
110
110
  'Maintenance:',
111
111
  ' aimux doctor | update | restart-daemon | restart-terminal-manager | version',
@@ -172,6 +172,17 @@ export async function runCli(argv: readonly string[]): Promise<number> {
172
172
  const group = argv[0] ?? ''
173
173
  const verb = argv[1] ?? ''
174
174
 
175
+ // `aimux worktree` was renamed to `aimux workspace`, and the old
176
+ // `aimux workspace` became `aimux project`. Aliasing the groups would
177
+ // silently change what `aimux workspace create` does, so refuse loudly for
178
+ // a release instead.
179
+ if (group === 'worktree') {
180
+ writeError(
181
+ '`aimux worktree` was renamed to `aimux workspace`, and the old `aimux workspace` is now `aimux project`. See docs/reference/cli.md'
182
+ )
183
+ return EXIT_USAGE
184
+ }
185
+
175
186
  // `aimux <group> --help` (or bare `aimux <group>`) — group-scoped help. Print
176
187
  // the verb list without erroring on the missing verb, so agents can drill
177
188
  // down step by step.
@@ -225,17 +236,16 @@ export async function runCli(argv: readonly string[]): Promise<number> {
225
236
 
226
237
  const state: {
227
238
  daemon: DaemonClient | null
228
- workspace: ReturnType<typeof resolveWorkspaceWithOrigin> | null
239
+ project: ReturnType<typeof resolveProjectWithOrigin> | null
229
240
  } = {
230
241
  daemon: null,
231
- workspace: null,
242
+ project: null,
232
243
  }
233
- const resolveOnce = (): ReturnType<typeof resolveWorkspaceWithOrigin> => {
234
- if (state.workspace) return state.workspace
235
- const workspaceFlag =
236
- typeof parsed.flags.workspace === 'string' ? parsed.flags.workspace : undefined
237
- state.workspace = resolveWorkspaceWithOrigin(workspaceFlag)
238
- return state.workspace
244
+ const resolveOnce = (): ReturnType<typeof resolveProjectWithOrigin> => {
245
+ if (state.project) return state.project
246
+ const projectFlag = typeof parsed.flags.project === 'string' ? parsed.flags.project : undefined
247
+ state.project = resolveProjectWithOrigin(projectFlag)
248
+ return state.project
239
249
  }
240
250
  const ctx: CliContext = {
241
251
  args: parsed,
@@ -244,9 +254,9 @@ export async function runCli(argv: readonly string[]): Promise<number> {
244
254
  state.daemon = await connectToDaemon()
245
255
  return state.daemon
246
256
  },
247
- getWorkspace: () => resolveOnce().record,
248
- getWorkspaceOrigin: () => resolveOnce().origin,
249
- getWorkspaces: () => listWorkspaces(),
257
+ getProject: () => resolveOnce().record,
258
+ getProjectOrigin: () => resolveOnce().origin,
259
+ getProjects: () => listProjects(),
250
260
  }
251
261
 
252
262
  try {
package/src/cli/output.ts CHANGED
@@ -25,7 +25,7 @@ export function writeError(message: string): void {
25
25
  // 10 question (`tab run`: worker is blocked on a question/permission)
26
26
  // 11 pending submit (`worker run --detach`: prompt is in the composer but no
27
27
  // turn started — recoverable with `worker submit`, unlike a real error)
28
- // 124 timeout (`tab wait`, `tab tail --timeout`, `workspace switch --wait`)
28
+ // 124 timeout (`tab wait`, `tab tail --timeout`, `project switch --wait`)
29
29
  export const EXIT_OK = 0
30
30
  export const EXIT_USAGE = 2
31
31
  export const EXIT_RUNTIME = 3
@@ -1,6 +1,11 @@
1
1
  import type { CliContext } from './context'
2
2
  import type { ArgSpec, FlagSpec } from './flags'
3
3
 
4
+ import { projectClose } from './commands/project/close'
5
+ import { projectCreate } from './commands/project/create'
6
+ import { projectList } from './commands/project/list'
7
+ import { projectShow } from './commands/project/show'
8
+ import { projectSwitch } from './commands/project/switch'
4
9
  import { tabAwait } from './commands/tab/await'
5
10
  import { tabClose } from './commands/tab/close'
6
11
  import { tabCreate } from './commands/tab/create'
@@ -18,14 +23,9 @@ import { workerPrompt } from './commands/worker/prompt'
18
23
  import { workerRun } from './commands/worker/run'
19
24
  import { workerStop } from './commands/worker/stop'
20
25
  import { workerSubmit } from './commands/worker/submit'
21
- import { workspaceClose } from './commands/workspace/close'
22
26
  import { workspaceCreate } from './commands/workspace/create'
23
27
  import { workspaceList } from './commands/workspace/list'
24
- import { workspaceShow } from './commands/workspace/show'
25
- import { workspaceSwitch } from './commands/workspace/switch'
26
- import { worktreeCreate } from './commands/worktree/create'
27
- import { worktreeList } from './commands/worktree/list'
28
- import { worktreeRemove } from './commands/worktree/remove'
28
+ import { workspaceRemove } from './commands/workspace/remove'
29
29
 
30
30
  export interface CliCommand {
31
31
  group: string
@@ -47,14 +47,14 @@ export const COMMANDS: readonly CliCommand[] = [
47
47
  tabSnapshot,
48
48
  tabTail,
49
49
  tabWait,
50
+ projectList,
51
+ projectShow,
52
+ projectCreate,
53
+ projectSwitch,
54
+ projectClose,
50
55
  workspaceList,
51
- workspaceShow,
52
56
  workspaceCreate,
53
- workspaceSwitch,
54
- workspaceClose,
55
- worktreeList,
56
- worktreeCreate,
57
- worktreeRemove,
57
+ workspaceRemove,
58
58
  workerRun,
59
59
  workerPrompt,
60
60
  workerSubmit,
@@ -6,11 +6,22 @@ import { getProfileConfigDir } from '../profile-paths'
6
6
 
7
7
  const CONFIG_FILENAMES = ['aimux.config.ts', 'aimux.config.js']
8
8
 
9
+ export interface LoadedUserConfig {
10
+ resolved: ResolvedConfig
11
+ /**
12
+ * The file as written, before defaults are merged in. The settings screen needs
13
+ * this one: `resolved` has a value for every key, so it cannot tell a setting
14
+ * the user declared from one that merely defaulted — and only a declared
15
+ * setting outranks what the screen has written.
16
+ */
17
+ user: AimuxUserConfig
18
+ }
19
+
9
20
  /**
10
- * Load the user's aimux.config.ts (or .js), merge with defaults, and return
11
- * a resolved config. Falls back to pure defaults if no config file exists.
21
+ * Load the user's aimux.config.ts (or .js) and merge it with defaults. Falls back
22
+ * to pure defaults if no config file exists.
12
23
  */
13
- export async function loadUserConfig(): Promise<ResolvedConfig> {
24
+ export async function loadUserConfig(): Promise<LoadedUserConfig> {
14
25
  const configDir = getProfileConfigDir()
15
26
 
16
27
  for (const filename of CONFIG_FILENAMES) {
@@ -24,7 +35,7 @@ export async function loadUserConfig(): Promise<ResolvedConfig> {
24
35
  const userConfig: AimuxUserConfig = mod.default ?? mod
25
36
 
26
37
  logDebug('config.user.loaded', { path: configPath })
27
- return resolveConfig(userConfig)
38
+ return { resolved: resolveConfig(userConfig), user: userConfig }
28
39
  } catch (error) {
29
40
  const message = error instanceof Error ? error.message : String(error)
30
41
  logDebug('config.user.error', { error: message, path: configPath })
@@ -32,5 +43,5 @@ export async function loadUserConfig(): Promise<ResolvedConfig> {
32
43
  }
33
44
  }
34
45
 
35
- return resolveConfig({})
46
+ return { resolved: resolveConfig({}), user: {} }
36
47
  }
package/src/config.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
 
3
- import type { SplitDirection } from './state/layout-tree'
4
- import type { GitFileListMode, WorkspaceSnapshotV1 } from './state/types'
3
+ import type { StoredSettings } from './settings/types'
4
+ import type { GitFileListMode, ProjectSnapshotV1 } from './state/types'
5
5
 
6
6
  import { logDebug } from './debug/input-log'
7
7
  import { getProfileConfigDir } from './profile-paths'
8
- import { isWorkspaceSnapshotV1 } from './state/validation'
8
+ import { isProjectSnapshotV1 } from './state/validation'
9
9
  import { migrateThemeId as resolveLegacyThemeId, type ThemeId, type ThemeMode } from './ui/themes'
10
10
 
11
11
  function migrateThemeId(value: unknown): ThemeId | undefined {
@@ -22,42 +22,46 @@ export function getConfigPath(): string {
22
22
  return `${getProfileConfigDir()}/aimux.json`
23
23
  }
24
24
 
25
+ /**
26
+ * `visible` / `mode` / `position` / `*Ratio` are legacy placement fields, read
27
+ * once by `deriveBarsFromLegacy` and no longer written. They must stay
28
+ * optional: rejecting the whole block would silently drop the git *content*
29
+ * preferences (`fileListMode`, `diffModeRatio`, `treeCompaction`).
30
+ */
25
31
  export interface PersistedGitPane {
26
32
  diffModeRatio?: number
27
33
  fileListMode?: GitFileListMode
28
34
  treeCompaction?: boolean
29
35
  prefetchRadius?: number
30
- visible: boolean
31
- mode: 'embedded' | 'pane'
32
- position: 'top' | 'bottom' | 'left' | 'right'
36
+ visible?: boolean
37
+ mode?: 'embedded' | 'pane'
38
+ position?: 'top' | 'bottom' | 'left' | 'right'
33
39
  paneRatio?: number
34
40
  embeddedRatio?: number
35
41
  ratio?: number
36
42
  }
37
43
 
44
+ /** Legacy; still written for downgrade safety, read only as a bars fallback. */
38
45
  export interface PersistedSidebar {
39
46
  visible: boolean
40
47
  width: number
41
48
  }
42
49
 
43
- export interface WorktreeTemplatePane {
50
+ export interface PersistedBarWidget {
44
51
  id: string
45
- assistant: string
46
- splitFrom?: string
47
- direction?: SplitDirection
48
- ratio?: number
49
- send?: string
52
+ grow: number
53
+ visible: boolean
50
54
  }
51
55
 
52
- export interface WorktreeTemplateTab {
53
- panes: WorktreeTemplatePane[]
56
+ export interface PersistedBar {
57
+ visible: boolean
58
+ width: number
59
+ widgets: PersistedBarWidget[]
54
60
  }
55
61
 
56
- export interface WorktreeTemplate {
57
- id: string
58
- name: string
59
- description?: string
60
- tabs: WorktreeTemplateTab[]
62
+ export interface PersistedBars {
63
+ left: PersistedBar
64
+ right: PersistedBar
61
65
  }
62
66
 
63
67
  export interface AimuxConfig {
@@ -67,20 +71,28 @@ export interface AimuxConfig {
67
71
  themeTransparent?: boolean
68
72
  themeMode?: ThemeMode
69
73
  gitPane?: PersistedGitPane
74
+ bars?: PersistedBars
70
75
  sidebar?: PersistedSidebar
71
- sessionBarVisible?: boolean
72
- workspaceSnapshot?: WorkspaceSnapshotV1
76
+ projectBarVisible?: boolean
77
+ projectSnapshot?: ProjectSnapshotV1
78
+ /**
79
+ * What the settings screen has written, keyed by row id. Sparse: an absent key
80
+ * means the user never touched that row, which is not the same as `false`.
81
+ * Keys this build doesn't know are kept as-is so a downgrade doesn't erase
82
+ * settings a newer one wrote.
83
+ */
84
+ settings?: StoredSettings
73
85
  skippedUpdateVersion?: string
74
86
  /** One-shot guard: orphan `aimux/` branches were pruned from existing repos. */
75
87
  prunedOrphanAimuxBranches?: boolean
76
- worktreeTemplates?: WorktreeTemplate[]
77
88
  }
78
89
 
79
90
  function isPersistedGitPane(value: unknown): value is PersistedGitPane {
80
91
  if (typeof value !== 'object' || value === null) return false
81
92
  const v = value as Record<string, unknown>
82
- const modeOk = v.mode === 'embedded' || v.mode === 'pane'
93
+ const modeOk = v.mode === undefined || v.mode === 'embedded' || v.mode === 'pane'
83
94
  const positionOk =
95
+ v.position === undefined ||
84
96
  v.position === 'top' ||
85
97
  v.position === 'bottom' ||
86
98
  v.position === 'left' ||
@@ -106,7 +118,7 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
106
118
  Number.isFinite(v.diffModeRatio) &&
107
119
  v.diffModeRatio > 0 &&
108
120
  v.diffModeRatio < 1)
109
- const visibleOk = typeof v.visible === 'boolean'
121
+ const visibleOk = v.visible === undefined || typeof v.visible === 'boolean'
110
122
  const fileListModeOk =
111
123
  v.fileListMode === undefined || v.fileListMode === 'tree' || v.fileListMode === 'flat'
112
124
  const treeCompactionOk = v.treeCompaction === undefined || typeof v.treeCompaction === 'boolean'
@@ -130,81 +142,89 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
130
142
  ) {
131
143
  return false
132
144
  }
133
- // cross-field coherence: embedded => top|bottom; pane => left|right
134
- if (v.mode === 'embedded' && v.position !== 'top' && v.position !== 'bottom') return false
135
- if (v.mode === 'pane' && v.position !== 'left' && v.position !== 'right') return false
136
145
  return true
137
146
  }
138
147
 
139
- function isRatioValid(value: unknown): boolean {
140
- return typeof value === 'number' && Number.isFinite(value) && value > 0.15 && value < 0.85
141
- }
142
-
143
- function isWorktreeTemplateTab(value: unknown): value is WorktreeTemplateTab {
148
+ function isPersistedBar(value: unknown): value is PersistedBar {
144
149
  if (typeof value !== 'object' || value === null) return false
145
150
  const v = value as Record<string, unknown>
146
- if (!Array.isArray(v.panes) || v.panes.length === 0) return false
147
- const seenIds = new Set<string>()
148
- for (let i = 0; i < v.panes.length; i++) {
149
- const rawPane = v.panes[i]
150
- if (typeof rawPane !== 'object' || rawPane === null) return false
151
- const pane = rawPane as Record<string, unknown>
152
- if (typeof pane.id !== 'string' || pane.id.length === 0) return false
153
- if (seenIds.has(pane.id)) return false
154
- seenIds.add(pane.id)
155
- if (typeof pane.assistant !== 'string' || pane.assistant.length === 0) return false
156
- if (i === 0) {
157
- if (pane.splitFrom !== undefined) return false
158
- if (pane.direction !== undefined) return false
159
- if (pane.ratio !== undefined) return false
160
- } else {
161
- if (typeof pane.splitFrom !== 'string' || !seenIds.has(pane.splitFrom)) return false
162
- if (pane.splitFrom === pane.id) return false
163
- if (pane.direction !== 'horizontal' && pane.direction !== 'vertical') return false
164
- if (pane.ratio !== undefined && !isRatioValid(pane.ratio)) return false
165
- }
166
- if (pane.send !== undefined && typeof pane.send !== 'string') return false
167
- }
168
- return true
151
+ if (typeof v.visible !== 'boolean') return false
152
+ if (typeof v.width !== 'number' || !Number.isFinite(v.width) || v.width <= 0) return false
153
+ if (!Array.isArray(v.widgets)) return false
154
+ return v.widgets.every((raw) => {
155
+ if (typeof raw !== 'object' || raw === null) return false
156
+ const widget = raw as Record<string, unknown>
157
+ return (
158
+ typeof widget.id === 'string' &&
159
+ widget.id.length > 0 &&
160
+ typeof widget.grow === 'number' &&
161
+ Number.isFinite(widget.grow) &&
162
+ widget.grow > 0 &&
163
+ typeof widget.visible === 'boolean'
164
+ )
165
+ })
169
166
  }
170
167
 
171
- export function isWorktreeTemplate(value: unknown): value is WorktreeTemplate {
168
+ export function isPersistedBars(value: unknown): value is PersistedBars {
172
169
  if (typeof value !== 'object' || value === null) return false
173
170
  const v = value as Record<string, unknown>
174
- if (typeof v.id !== 'string' || v.id.length === 0) return false
175
- if (typeof v.name !== 'string' || v.name.length === 0) return false
176
- if (v.description !== undefined && typeof v.description !== 'string') return false
177
- if (!Array.isArray(v.tabs) || v.tabs.length === 0) return false
178
- for (const tab of v.tabs) {
179
- if (!isWorktreeTemplateTab(tab)) return false
171
+ return isPersistedBar(v.left) && isPersistedBar(v.right)
172
+ }
173
+
174
+ /**
175
+ * One-time upgrade from the pre-bars layout (a left sidebar plus a git pane
176
+ * with its own mode/position). Runs whenever `bars` is absent so an existing
177
+ * install keeps the layout it had.
178
+ */
179
+ export function deriveBarsFromLegacy(
180
+ sidebar: PersistedSidebar | undefined,
181
+ gitPane: PersistedGitPane | undefined
182
+ ): PersistedBars {
183
+ const gitVisible = gitPane?.visible ?? true
184
+ const embeddedRatio = gitPane?.embeddedRatio ?? gitPane?.ratio ?? 0.5
185
+ const gitGrow = Math.max(1, Math.round(embeddedRatio * 100))
186
+ const git = { grow: gitGrow, id: 'git', visible: gitVisible }
187
+ const projects = { grow: Math.max(1, 100 - gitGrow), id: 'projects', visible: true }
188
+
189
+ const left: PersistedBar = {
190
+ visible: sidebar?.visible ?? true,
191
+ widgets: gitPane?.position === 'top' ? [git, projects] : [projects, git],
192
+ width: sidebar?.width ?? 28,
180
193
  }
181
- return true
194
+ const right: PersistedBar = { visible: false, widgets: [], width: 40 }
195
+
196
+ // A legacy `pane` git panel on the right was already its own right-hand
197
+ // column — it maps straight onto the right bar. On the left it shared the
198
+ // edge with the sidebar, which the single-bar model expresses as a stack.
199
+ if (gitPane?.mode === 'pane' && gitPane.position === 'right') {
200
+ left.widgets = [{ ...projects, grow: 100 }]
201
+ right.visible = true
202
+ right.widgets = [{ ...git, grow: 100 }]
203
+ right.width = Math.round((gitPane.paneRatio ?? gitPane.ratio ?? 0.5) * 80)
204
+ }
205
+ return { left, right }
182
206
  }
183
207
 
184
- export function parseWorktreeTemplates(
185
- value: unknown,
186
- issues: string[]
187
- ): WorktreeTemplate[] | undefined {
188
- if (value === undefined) return undefined
189
- if (!Array.isArray(value)) {
190
- issues.push('ignored invalid worktreeTemplates (not an array)')
191
- return undefined
208
+ /**
209
+ * Keeps the entries it understands and drops the rest, rather than rejecting the
210
+ * whole block: this record grows one independent key per setting, so one bad
211
+ * value has no business resetting every other one.
212
+ */
213
+ function parseStoredSettings(value: unknown): { settings: StoredSettings; dropped: number } {
214
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
215
+ return { dropped: 0, settings: {} }
192
216
  }
193
- const seen = new Set<string>()
194
- const valid: WorktreeTemplate[] = []
195
- for (const entry of value) {
196
- if (!isWorktreeTemplate(entry)) {
197
- issues.push('ignored invalid worktreeTemplate entry')
198
- continue
199
- }
200
- if (seen.has(entry.id)) {
201
- issues.push(`ignored duplicate worktreeTemplate id "${entry.id}"`)
202
- continue
203
- }
204
- seen.add(entry.id)
205
- valid.push(entry)
217
+ const settings: StoredSettings = {}
218
+ let dropped = 0
219
+ for (const [key, entry] of Object.entries(value)) {
220
+ const ok =
221
+ typeof entry === 'boolean' ||
222
+ typeof entry === 'string' ||
223
+ (typeof entry === 'number' && Number.isFinite(entry))
224
+ if (ok) settings[key] = entry
225
+ else dropped++
206
226
  }
207
- return valid.length > 0 ? valid : undefined
227
+ return { dropped, settings }
208
228
  }
209
229
 
210
230
  function isPersistedSidebar(value: unknown): value is PersistedSidebar {
@@ -259,13 +279,19 @@ export function loadConfigResult(): ConfigLoadResult {
259
279
  themeTransparent?: unknown
260
280
  themeMode?: unknown
261
281
  gitPane?: unknown
282
+ bars?: unknown
262
283
  sidebar?: unknown
263
284
  gitPanelVisible?: unknown
264
285
  gitPanelRatio?: unknown
286
+ projectBarVisible?: unknown
287
+ /** ponytail: pre-rename spelling, honoured for one release. */
265
288
  sessionBarVisible?: unknown
266
- workspaceSnapshot?: unknown
289
+ projectSnapshot?: unknown
290
+ settings?: unknown
267
291
  skippedUpdateVersion?: unknown
268
292
  prunedOrphanAimuxBranches?: unknown
293
+ /** Removed; still detected so the doctor can say so. */
294
+ workspaceTemplates?: unknown
269
295
  worktreeTemplates?: unknown
270
296
  }
271
297
 
@@ -305,6 +331,11 @@ export function loadConfigResult(): ConfigLoadResult {
305
331
  issues.push('ignored invalid sidebar')
306
332
  }
307
333
 
334
+ const validBars = isPersistedBars(parsed.bars) ? parsed.bars : undefined
335
+ if (parsed.bars !== undefined && validBars === undefined) {
336
+ issues.push('ignored invalid bars')
337
+ }
338
+
308
339
  // Legacy migration: previous schema stored gitPanelVisible/gitPanelRatio at
309
340
  // top level. If the new `gitPane` field is absent, synthesize it from legacy
310
341
  // keys so users don't lose their toggle/ratio on upgrade.
@@ -330,17 +361,28 @@ export function loadConfigResult(): ConfigLoadResult {
330
361
  }
331
362
  }
332
363
 
333
- const validSessionBarVisible =
334
- typeof parsed.sessionBarVisible === 'boolean' ? parsed.sessionBarVisible : undefined
335
- if (parsed.sessionBarVisible !== undefined && validSessionBarVisible === undefined) {
336
- issues.push('ignored invalid sessionBarVisible')
364
+ // `sessionBarVisible` was the pre-rename key; without the fallback the bar
365
+ // would silently reset to its default on the first launch after upgrading.
366
+ const rawProjectBarVisible = parsed.projectBarVisible ?? parsed.sessionBarVisible
367
+ const validProjectBarVisible =
368
+ typeof rawProjectBarVisible === 'boolean' ? rawProjectBarVisible : undefined
369
+ if (rawProjectBarVisible !== undefined && validProjectBarVisible === undefined) {
370
+ issues.push('ignored invalid projectBarVisible')
371
+ }
372
+
373
+ if (parsed.projectSnapshot !== undefined && !isProjectSnapshotV1(parsed.projectSnapshot)) {
374
+ issues.push('ignored invalid projectSnapshot')
337
375
  }
338
376
 
339
- if (
340
- parsed.workspaceSnapshot !== undefined &&
341
- !isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
342
- ) {
343
- issues.push('ignored invalid workspaceSnapshot')
377
+ const storedSettings = parseStoredSettings(parsed.settings)
378
+ const settingsIsRecord =
379
+ typeof parsed.settings === 'object' &&
380
+ parsed.settings !== null &&
381
+ !Array.isArray(parsed.settings)
382
+ if (parsed.settings !== undefined && !settingsIsRecord) {
383
+ issues.push('ignored invalid settings')
384
+ } else if (storedSettings.dropped > 0) {
385
+ issues.push(`ignored ${String(storedSettings.dropped)} invalid setting(s)`)
344
386
  }
345
387
 
346
388
  const validSkippedUpdateVersion =
@@ -351,7 +393,12 @@ export function loadConfigResult(): ConfigLoadResult {
351
393
  issues.push('ignored invalid skippedUpdateVersion')
352
394
  }
353
395
 
354
- const validWorktreeTemplates = parseWorktreeTemplates(parsed.worktreeTemplates, issues)
396
+ // Templates are gone, and an unknown key parses fine — so a config that
397
+ // still declares them would silently stop doing anything. The doctor is the
398
+ // only place that can say so.
399
+ if (parsed.workspaceTemplates !== undefined || parsed.worktreeTemplates !== undefined) {
400
+ issues.push('workspaceTemplates was removed — use a per-project setup script instead')
401
+ }
355
402
 
356
403
  if (issues.length > 0) {
357
404
  logDebug('config.load.validationIssue', { issues, path: configPath })
@@ -359,20 +406,21 @@ export function loadConfigResult(): ConfigLoadResult {
359
406
 
360
407
  return {
361
408
  config: {
409
+ bars: validBars ?? deriveBarsFromLegacy(validSidebar, validGitPane),
362
410
  customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
363
411
  gitPane: validGitPane,
412
+ projectBarVisible: validProjectBarVisible,
413
+ projectSnapshot: isProjectSnapshotV1(parsed.projectSnapshot)
414
+ ? parsed.projectSnapshot
415
+ : undefined,
364
416
  prunedOrphanAimuxBranches: parsed.prunedOrphanAimuxBranches === true ? true : undefined,
365
- sessionBarVisible: validSessionBarVisible,
417
+ settings: storedSettings.settings,
366
418
  sidebar: validSidebar,
367
419
  skippedUpdateVersion: validSkippedUpdateVersion,
368
420
  themeId: migrateThemeId(parsed.themeId),
369
421
  themeMode: validThemeMode,
370
422
  themeTransparent: validThemeTransparent,
371
423
  version: 2,
372
- workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
373
- ? parsed.workspaceSnapshot
374
- : undefined,
375
- worktreeTemplates: validWorktreeTemplates,
376
424
  },
377
425
  issues,
378
426
  source: 'file',