@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
@@ -1,31 +1,31 @@
1
- import type { SessionRecord } from '../state/types'
1
+ import type { ProjectRecord } from '../state/types'
2
2
  import type { DaemonClient } from './client/daemon-client'
3
- import type { WorkspaceOrigin } from './client/workspace-resolver'
3
+ import type { ProjectOrigin } from './client/project-resolver'
4
4
  import type { ParsedArgs } from './flags'
5
5
 
6
6
  /**
7
7
  * Per-command context handed to `CliCommand.run`. Commands lazily resolve
8
- * the workspace + daemon via the helpers on this object so a command that
9
- * only reads flags (e.g. `workspace list`) doesn't have to open a socket.
8
+ * the project + daemon via the helpers on this object so a command that
9
+ * only reads flags (e.g. `project list`) doesn't have to open a socket.
10
10
  */
11
11
  export interface CliContext {
12
12
  args: ParsedArgs
13
13
  /** Already-running daemon client, populated on first call to `daemon()`. */
14
14
  getDaemon: () => Promise<DaemonClient>
15
- /** Resolved workspace, populated on first call to `workspace()`. */
16
- getWorkspace: () => SessionRecord
15
+ /** Resolved project, populated on first call to `project()`. */
16
+ getProject: () => ProjectRecord
17
17
  /**
18
- * Every catalogued workspace. Commands that must answer "is my worker really
19
- * gone, or did the active workspace move?" need the whole catalog, not just
18
+ * Every catalogued project. Commands that must answer "is my worker really
19
+ * gone, or did the active project move?" need the whole catalog, not just
20
20
  * the resolved record. Optional so test fixtures can inject one.
21
21
  */
22
- getWorkspaces?: () => SessionRecord[]
22
+ getProjects?: () => ProjectRecord[]
23
23
  /**
24
- * Where `getWorkspace()` came from — `flag` (`--workspace`), `env`
25
- * (`AIMUX_WORKSPACE`), or `active` (most recently opened session). Only
26
- * `active` can drift under a command while the UI switches workspaces, so
24
+ * Where `getProject()` came from — `flag` (`--project`), `env`
25
+ * (`AIMUX_PROJECT`), or `active` (most recently opened project). Only
26
+ * `active` can drift under a command while the UI switches projects, so
27
27
  * commands that must not follow the UI branch on this. Optional so test
28
28
  * fixtures can build a minimal context.
29
29
  */
30
- getWorkspaceOrigin?: () => WorkspaceOrigin
30
+ getProjectOrigin?: () => ProjectOrigin
31
31
  }
package/src/cli/flags.ts CHANGED
@@ -15,8 +15,8 @@ export type DynamicCompletionSource =
15
15
  | 'git-ref'
16
16
  | 'tab'
17
17
  | 'worker'
18
+ | 'project'
18
19
  | 'workspace'
19
- | 'worktree'
20
20
 
21
21
  /**
22
22
  * Where a flag value or positional draws its shell-completion candidates.
@@ -67,6 +67,23 @@ export function parseArgs(
67
67
  ): ParsedArgs {
68
68
  const flagByName = new Map<string, FlagSpec>()
69
69
  for (const spec of flagSpecs) flagByName.set(spec.name, spec)
70
+ // Deprecated aliases from the project/workspace rename. Unlike the command
71
+ // groups these are safe to map silently: --workspace named a project and
72
+ // --worktree named what is now a workspace, so each old name still points at
73
+ // the same object.
74
+ // ponytail: drop with the aimux-sessions.json fallback.
75
+ // Resolved against the declared specs, never against another alias —
76
+ // otherwise --worktree would chain through the --workspace alias onto
77
+ // --project, which is a different object entirely.
78
+ for (const [old, current] of [
79
+ ['workspace', 'project'],
80
+ ['worktree', 'workspace'],
81
+ ['new-worktree', 'new-workspace'],
82
+ ] as const) {
83
+ if (flagByName.has(old)) continue
84
+ const spec = flagSpecs.find((entry) => entry.name === current)
85
+ if (spec) flagByName.set(old, spec)
86
+ }
70
87
 
71
88
  const flags: Record<string, string | number | boolean> = {}
72
89
  const positionals: string[] = []
@@ -85,18 +102,21 @@ export function parseArgs(
85
102
  if (!spec) {
86
103
  throw new CliUsageError(`unknown flag: --${name}`)
87
104
  }
105
+ // Deprecated aliases resolve to the current spec, so results are always
106
+ // keyed by the canonical name regardless of which spelling was typed.
107
+ const key = spec.name
88
108
  if (spec.kind === 'boolean') {
89
109
  if (eq !== -1) {
90
110
  throw new CliUsageError(`flag --${name} does not take a value`)
91
111
  }
92
- flags[name] = true
112
+ flags[key] = true
93
113
  continue
94
114
  }
95
115
  if (spec.kind === 'optional-string') {
96
116
  // Value binds ONLY in the `=` form (`--flag=value`). A bare `--flag`
97
117
  // must not swallow the next token (it may be a positional), so it
98
118
  // parses as boolean `true`.
99
- flags[name] = eq === -1 ? true : token.slice(eq + 1)
119
+ flags[key] = eq === -1 ? true : token.slice(eq + 1)
100
120
  continue
101
121
  }
102
122
  const raw = eq === -1 ? argv[++i] : token.slice(eq + 1)
@@ -108,9 +128,9 @@ export function parseArgs(
108
128
  if (!Number.isFinite(parsed)) {
109
129
  throw new CliUsageError(`flag --${name} must be a number (got: ${raw})`)
110
130
  }
111
- flags[name] = parsed
131
+ flags[key] = parsed
112
132
  } else {
113
- flags[name] = raw
133
+ flags[key] = raw
114
134
  }
115
135
  continue
116
136
  }
@@ -129,17 +149,17 @@ export function parseArgs(
129
149
  }
130
150
 
131
151
  /**
132
- * Shared flag set — every command takes these. `--workspace` selects the
133
- * target session (by id or name); `--profile` overrides `AIMUX_PROFILE`
152
+ * Shared flag set — every command takes these. `--project` selects the
153
+ * target project (by id or name); `--profile` overrides `AIMUX_PROFILE`
134
154
  * before any runtime path is resolved; `--json` is a no-op kept for
135
155
  * consistency with future formats.
136
156
  */
137
157
  export const SHARED_FLAGS: readonly FlagSpec[] = [
138
158
  {
139
- complete: { kind: 'dynamic', source: 'workspace' },
140
- description: 'workspace id or name',
159
+ complete: { kind: 'dynamic', source: 'project' },
160
+ description: 'project id or name',
141
161
  kind: 'string',
142
- name: 'workspace',
162
+ name: 'project',
143
163
  },
144
164
  {
145
165
  complete: { kind: 'none' },
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 {
@@ -64,26 +64,6 @@ export interface PersistedBars {
64
64
  right: PersistedBar
65
65
  }
66
66
 
67
- export interface WorktreeTemplatePane {
68
- id: string
69
- assistant: string
70
- splitFrom?: string
71
- direction?: SplitDirection
72
- ratio?: number
73
- send?: string
74
- }
75
-
76
- export interface WorktreeTemplateTab {
77
- panes: WorktreeTemplatePane[]
78
- }
79
-
80
- export interface WorktreeTemplate {
81
- id: string
82
- name: string
83
- description?: string
84
- tabs: WorktreeTemplateTab[]
85
- }
86
-
87
67
  export interface AimuxConfig {
88
68
  version: 2
89
69
  customCommands: Record<string, string>
@@ -93,12 +73,18 @@ export interface AimuxConfig {
93
73
  gitPane?: PersistedGitPane
94
74
  bars?: PersistedBars
95
75
  sidebar?: PersistedSidebar
96
- sessionBarVisible?: boolean
97
- 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
98
85
  skippedUpdateVersion?: string
99
86
  /** One-shot guard: orphan `aimux/` branches were pruned from existing repos. */
100
87
  prunedOrphanAimuxBranches?: boolean
101
- worktreeTemplates?: WorktreeTemplate[]
102
88
  }
103
89
 
104
90
  function isPersistedGitPane(value: unknown): value is PersistedGitPane {
@@ -198,11 +184,11 @@ export function deriveBarsFromLegacy(
198
184
  const embeddedRatio = gitPane?.embeddedRatio ?? gitPane?.ratio ?? 0.5
199
185
  const gitGrow = Math.max(1, Math.round(embeddedRatio * 100))
200
186
  const git = { grow: gitGrow, id: 'git', visible: gitVisible }
201
- const workspaces = { grow: Math.max(1, 100 - gitGrow), id: 'workspaces', visible: true }
187
+ const projects = { grow: Math.max(1, 100 - gitGrow), id: 'projects', visible: true }
202
188
 
203
189
  const left: PersistedBar = {
204
190
  visible: sidebar?.visible ?? true,
205
- widgets: gitPane?.position === 'top' ? [git, workspaces] : [workspaces, git],
191
+ widgets: gitPane?.position === 'top' ? [git, projects] : [projects, git],
206
192
  width: sidebar?.width ?? 28,
207
193
  }
208
194
  const right: PersistedBar = { visible: false, widgets: [], width: 40 }
@@ -211,7 +197,7 @@ export function deriveBarsFromLegacy(
211
197
  // column — it maps straight onto the right bar. On the left it shared the
212
198
  // edge with the sidebar, which the single-bar model expresses as a stack.
213
199
  if (gitPane?.mode === 'pane' && gitPane.position === 'right') {
214
- left.widgets = [{ ...workspaces, grow: 100 }]
200
+ left.widgets = [{ ...projects, grow: 100 }]
215
201
  right.visible = true
216
202
  right.widgets = [{ ...git, grow: 100 }]
217
203
  right.width = Math.round((gitPane.paneRatio ?? gitPane.ratio ?? 0.5) * 80)
@@ -219,75 +205,26 @@ export function deriveBarsFromLegacy(
219
205
  return { left, right }
220
206
  }
221
207
 
222
- function isRatioValid(value: unknown): boolean {
223
- return typeof value === 'number' && Number.isFinite(value) && value > 0.15 && value < 0.85
224
- }
225
-
226
- function isWorktreeTemplateTab(value: unknown): value is WorktreeTemplateTab {
227
- if (typeof value !== 'object' || value === null) return false
228
- const v = value as Record<string, unknown>
229
- if (!Array.isArray(v.panes) || v.panes.length === 0) return false
230
- const seenIds = new Set<string>()
231
- for (let i = 0; i < v.panes.length; i++) {
232
- const rawPane = v.panes[i]
233
- if (typeof rawPane !== 'object' || rawPane === null) return false
234
- const pane = rawPane as Record<string, unknown>
235
- if (typeof pane.id !== 'string' || pane.id.length === 0) return false
236
- if (seenIds.has(pane.id)) return false
237
- seenIds.add(pane.id)
238
- if (typeof pane.assistant !== 'string' || pane.assistant.length === 0) return false
239
- if (i === 0) {
240
- if (pane.splitFrom !== undefined) return false
241
- if (pane.direction !== undefined) return false
242
- if (pane.ratio !== undefined) return false
243
- } else {
244
- if (typeof pane.splitFrom !== 'string' || !seenIds.has(pane.splitFrom)) return false
245
- if (pane.splitFrom === pane.id) return false
246
- if (pane.direction !== 'horizontal' && pane.direction !== 'vertical') return false
247
- if (pane.ratio !== undefined && !isRatioValid(pane.ratio)) return false
248
- }
249
- if (pane.send !== undefined && typeof pane.send !== 'string') return false
250
- }
251
- return true
252
- }
253
-
254
- export function isWorktreeTemplate(value: unknown): value is WorktreeTemplate {
255
- if (typeof value !== 'object' || value === null) return false
256
- const v = value as Record<string, unknown>
257
- if (typeof v.id !== 'string' || v.id.length === 0) return false
258
- if (typeof v.name !== 'string' || v.name.length === 0) return false
259
- if (v.description !== undefined && typeof v.description !== 'string') return false
260
- if (!Array.isArray(v.tabs) || v.tabs.length === 0) return false
261
- for (const tab of v.tabs) {
262
- if (!isWorktreeTemplateTab(tab)) return false
263
- }
264
- return true
265
- }
266
-
267
- export function parseWorktreeTemplates(
268
- value: unknown,
269
- issues: string[]
270
- ): WorktreeTemplate[] | undefined {
271
- if (value === undefined) return undefined
272
- if (!Array.isArray(value)) {
273
- issues.push('ignored invalid worktreeTemplates (not an array)')
274
- 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: {} }
275
216
  }
276
- const seen = new Set<string>()
277
- const valid: WorktreeTemplate[] = []
278
- for (const entry of value) {
279
- if (!isWorktreeTemplate(entry)) {
280
- issues.push('ignored invalid worktreeTemplate entry')
281
- continue
282
- }
283
- if (seen.has(entry.id)) {
284
- issues.push(`ignored duplicate worktreeTemplate id "${entry.id}"`)
285
- continue
286
- }
287
- seen.add(entry.id)
288
- 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++
289
226
  }
290
- return valid.length > 0 ? valid : undefined
227
+ return { dropped, settings }
291
228
  }
292
229
 
293
230
  function isPersistedSidebar(value: unknown): value is PersistedSidebar {
@@ -346,10 +283,15 @@ export function loadConfigResult(): ConfigLoadResult {
346
283
  sidebar?: unknown
347
284
  gitPanelVisible?: unknown
348
285
  gitPanelRatio?: unknown
286
+ projectBarVisible?: unknown
287
+ /** ponytail: pre-rename spelling, honoured for one release. */
349
288
  sessionBarVisible?: unknown
350
- workspaceSnapshot?: unknown
289
+ projectSnapshot?: unknown
290
+ settings?: unknown
351
291
  skippedUpdateVersion?: unknown
352
292
  prunedOrphanAimuxBranches?: unknown
293
+ /** Removed; still detected so the doctor can say so. */
294
+ workspaceTemplates?: unknown
353
295
  worktreeTemplates?: unknown
354
296
  }
355
297
 
@@ -419,17 +361,28 @@ export function loadConfigResult(): ConfigLoadResult {
419
361
  }
420
362
  }
421
363
 
422
- const validSessionBarVisible =
423
- typeof parsed.sessionBarVisible === 'boolean' ? parsed.sessionBarVisible : undefined
424
- if (parsed.sessionBarVisible !== undefined && validSessionBarVisible === undefined) {
425
- 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')
426
375
  }
427
376
 
428
- if (
429
- parsed.workspaceSnapshot !== undefined &&
430
- !isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
431
- ) {
432
- 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)`)
433
386
  }
434
387
 
435
388
  const validSkippedUpdateVersion =
@@ -440,7 +393,12 @@ export function loadConfigResult(): ConfigLoadResult {
440
393
  issues.push('ignored invalid skippedUpdateVersion')
441
394
  }
442
395
 
443
- 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
+ }
444
402
 
445
403
  if (issues.length > 0) {
446
404
  logDebug('config.load.validationIssue', { issues, path: configPath })
@@ -451,18 +409,18 @@ export function loadConfigResult(): ConfigLoadResult {
451
409
  bars: validBars ?? deriveBarsFromLegacy(validSidebar, validGitPane),
452
410
  customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
453
411
  gitPane: validGitPane,
412
+ projectBarVisible: validProjectBarVisible,
413
+ projectSnapshot: isProjectSnapshotV1(parsed.projectSnapshot)
414
+ ? parsed.projectSnapshot
415
+ : undefined,
454
416
  prunedOrphanAimuxBranches: parsed.prunedOrphanAimuxBranches === true ? true : undefined,
455
- sessionBarVisible: validSessionBarVisible,
417
+ settings: storedSettings.settings,
456
418
  sidebar: validSidebar,
457
419
  skippedUpdateVersion: validSkippedUpdateVersion,
458
420
  themeId: migrateThemeId(parsed.themeId),
459
421
  themeMode: validThemeMode,
460
422
  themeTransparent: validThemeTransparent,
461
423
  version: 2,
462
- workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
463
- ? parsed.workspaceSnapshot
464
- : undefined,
465
- worktreeTemplates: validWorktreeTemplates,
466
424
  },
467
425
  issues,
468
426
  source: 'file',