@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
@@ -1,8 +1,8 @@
1
- import type { SessionRecord, WorktreeRecord } from '../../../state/types'
1
+ import type { ProjectRecord, WorkspaceRecord } from '../../../state/types'
2
2
  import type { DaemonClient } from '../../client/daemon-client'
3
3
 
4
4
  import { createGitWorktree, removeGitWorktree, resolveGitRef } from '../../../git/worktree'
5
- import { IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS } from '../../../ipc/protocol'
5
+ import { IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS } from '../../../ipc/protocol'
6
6
  import { createPrefixedId } from '../../../platform/id'
7
7
  import {
8
8
  assertSafeAimuxWorktreePath,
@@ -11,59 +11,59 @@ import {
11
11
  pruneEmptyWorktreeParent,
12
12
  } from '../../../platform/worktree-paths'
13
13
 
14
- export interface CreateWorktreeParams {
14
+ export interface CreateWorkspaceParams {
15
15
  /** Base ref for the branch (callers default to 'HEAD'). */
16
16
  base: string
17
17
  /** Branch name (callers default to `aimux/<name>`). */
18
18
  branch: string
19
19
  daemon: DaemonClient
20
20
  name: string
21
- workspace: SessionRecord
21
+ project: ProjectRecord
22
22
  }
23
23
 
24
24
  /**
25
- * Create a git worktree + its catalog record for a workspace. Shared by
26
- * `worktree create` and `tab create --new-worktree`. Checks the daemon
27
- * capability BEFORE touching disk, and rolls back the on-disk worktree if
25
+ * Create a git worktree + its catalog record for a project. Shared by
26
+ * `workspace create` and `tab create --new-workspace`. Checks the daemon
27
+ * capability BEFORE touching disk, and rolls back the on-disk workspace if
28
28
  * catalog registration fails (so `worktree list` never surfaces an orphan).
29
29
  * Throws on any failure; returns the registered record on success.
30
30
  */
31
- export async function createWorkspaceWorktree(
32
- params: CreateWorktreeParams
33
- ): Promise<WorktreeRecord> {
34
- const { base, branch, daemon, name, workspace } = params
31
+ export async function createProjectWorkspace(
32
+ params: CreateWorkspaceParams
33
+ ): Promise<WorkspaceRecord> {
34
+ const { base, branch, daemon, name, project } = params
35
35
 
36
- const primary = workspace.worktrees?.find((w) => w.source === 'primary')
36
+ const primary = project.workspaces?.find((w) => w.source === 'primary')
37
37
  if (!primary) {
38
38
  throw new Error(
39
- `workspace "${workspace.name}" has no primary worktree — set --project when creating it`
39
+ `project "${project.name}" has no primary workspace — set --project when creating it`
40
40
  )
41
41
  }
42
42
 
43
43
  // Check the daemon's capability BEFORE mutating disk — otherwise a capability
44
44
  // mismatch would leave a git worktree on disk with no catalog record.
45
- if (!daemon.hasCapability(IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS)) {
45
+ if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS)) {
46
46
  throw new Error(
47
- 'daemon predates worktreeLifecycleEvents capability — restart aimux to pick up the new daemon'
47
+ 'daemon predates workspaceLifecycleEvents capability — restart aimux to pick up the new daemon'
48
48
  )
49
49
  }
50
50
 
51
51
  // Verify the base ref in the REPO WE ARE ABOUT TO USE, before touching disk.
52
- // The workspace decides the repo, so a caller who believes it is orchestrating
53
- // project A while the resolved workspace points at project B would otherwise
52
+ // The project decides the repo, so a caller who believes it is orchestrating
53
+ // project A while the resolved project points at project B would otherwise
54
54
  // get either a confusing bare git error or — when the ref exists in both repos
55
55
  // — a silent success in the wrong project.
56
56
  if ((await resolveGitRef(primary.repoRoot, base)) === undefined) {
57
57
  throw new Error(
58
- `base ref "${base}" does not exist in ${primary.repoRoot} (workspace "${workspace.name}") — check that this is the repository you meant`
58
+ `base ref "${base}" does not exist in ${primary.repoRoot} (project "${project.name}") — check that this is the repository you meant`
59
59
  )
60
60
  }
61
61
 
62
- const worktreeId = createPrefixedId('worktree')
62
+ const workspaceId = createPrefixedId('workspace')
63
63
  const targetPath = makeWorktreePath({
64
64
  repoRoot: primary.repoRoot,
65
- worktreeId,
66
- worktreeName: name,
65
+ workspaceId,
66
+ workspaceName: name,
67
67
  })
68
68
  await ensureAimuxWorktreeRoot()
69
69
  await assertSafeAimuxWorktreePath(targetPath)
@@ -83,12 +83,12 @@ export async function createWorkspaceWorktree(
83
83
  }
84
84
 
85
85
  const now = new Date().toISOString()
86
- const record: WorktreeRecord = {
86
+ const record: WorkspaceRecord = {
87
87
  baseRef: base,
88
88
  branch,
89
89
  createdAt: now,
90
90
  createdByAimux: true,
91
- id: worktreeId,
91
+ id: workspaceId,
92
92
  name,
93
93
  path: targetPath,
94
94
  repoRoot: primary.repoRoot,
@@ -97,16 +97,16 @@ export async function createWorkspaceWorktree(
97
97
  }
98
98
 
99
99
  try {
100
- await daemon.expectOk('addWorktreeRecord', { sessionId: workspace.id, worktree: record })
100
+ await daemon.expectOk('addWorkspaceRecord', { projectId: project.id, workspace: record })
101
101
  } catch (error) {
102
- // Catalog registration failed — roll back the on-disk worktree so
102
+ // Catalog registration failed — roll back the on-disk workspace so
103
103
  // `worktree list` doesn't perpetually surface an orphan. Swallow rollback
104
104
  // errors: report the original failure, the real problem to surface.
105
105
  try {
106
106
  await removeGitWorktree({ force: true, repoPath: primary.repoRoot, targetPath })
107
107
  await pruneEmptyWorktreeParent(targetPath)
108
108
  } catch {
109
- // Best-effort rollback; leave the git-side worktree if it can't be removed
109
+ // Best-effort rollback; leave the git-side workspace if it can't be removed
110
110
  // cleanly. `worktree list` will flag it as gitTracked with no catalog.
111
111
  }
112
112
  throw error
@@ -1,93 +1,55 @@
1
- import { resolve as resolvePath } from 'node:path'
2
-
3
1
  import type { CliCommand } from '../../registry'
4
2
 
5
- import { IPC_CAPABILITY_WORKSPACE_LIFECYCLE } from '../../../ipc/protocol'
6
3
  import { SHARED_FLAGS } from '../../flags'
7
- import { EXIT_OK, EXIT_TIMEOUT, writeJson } from '../../output'
8
-
9
- const DEFAULT_WAIT_TIMEOUT_MS = 30_000
4
+ import { EXIT_OK, writeJson } from '../../output'
5
+ import { createProjectWorkspace } from './create-core'
10
6
 
11
7
  export const workspaceCreate: CliCommand = {
12
- args: [{ complete: { kind: 'none' }, name: 'name', required: true }],
8
+ args: [],
13
9
  flags: [
14
10
  ...SHARED_FLAGS,
15
11
  {
16
- complete: { kind: 'file' },
17
- description: 'project path to associate with the workspace',
12
+ complete: { kind: 'none' },
13
+ description: 'display name for the new workspace',
18
14
  kind: 'string',
19
- name: 'project',
20
- },
21
- {
22
- description: 'immediately switch the running UI to the new workspace',
23
- kind: 'boolean',
24
- name: 'switch',
15
+ name: 'name',
25
16
  },
26
17
  {
27
- description: 'with --switch, wait until the UI confirms the switch completed',
28
- kind: 'boolean',
29
- name: 'wait',
18
+ complete: { kind: 'none' },
19
+ description: 'branch name (defaults to aimux/<name>)',
20
+ kind: 'string',
21
+ name: 'branch',
30
22
  },
31
23
  {
32
- description: 'timeout for --wait, in milliseconds (default 30000)',
33
- kind: 'number',
34
- name: 'timeout',
24
+ complete: { kind: 'dynamic', source: 'git-ref' },
25
+ description: 'base ref for the branch (defaults to HEAD)',
26
+ kind: 'string',
27
+ name: 'base',
35
28
  },
36
29
  ],
37
30
  group: 'workspace',
38
31
  run: async (ctx) => {
39
- const name = ctx.args.positionals[0]
32
+ const name = ctx.args.flags.name
40
33
  if (typeof name !== 'string' || name.length === 0) {
41
- throw new Error('workspace name is required')
42
- }
43
- const projectRaw =
44
- typeof ctx.args.flags.project === 'string' ? ctx.args.flags.project : undefined
45
- const projectPath = projectRaw === undefined ? undefined : resolvePath(projectRaw)
46
- const doSwitch = ctx.args.flags.switch === true
47
- const wait = ctx.args.flags.wait === true
48
- const timeoutMs =
49
- typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_WAIT_TIMEOUT_MS
50
-
51
- if (wait && !doSwitch) {
52
- // Without --switch, the UI never emits an ack event, so there's
53
- // nothing meaningful for --wait to wait on.
54
- throw new Error('--wait requires --switch (nothing to wait for otherwise)')
34
+ throw new Error('--name is required')
55
35
  }
36
+ const branch =
37
+ typeof ctx.args.flags.branch === 'string' ? ctx.args.flags.branch : `aimux/${name}`
38
+ const base = typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : 'HEAD'
56
39
 
40
+ const project = ctx.getProject()
57
41
  const daemon = await ctx.getDaemon()
58
- if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE)) {
59
- throw new Error(
60
- 'daemon predates workspaceLifecycle capability — restart aimux to pick up the new daemon'
61
- )
62
- }
63
-
64
- if (!wait) {
65
- await daemon.expectOk('createWorkspace', { name, projectPath, switch: doSwitch })
66
- writeJson({ name, projectPath, switch: doSwitch })
67
- return EXIT_OK
68
- }
69
-
70
- // Subscribe BEFORE sending the request. The UI's create+switch path
71
- // relays as `workspaceSwitched` once handleCreateSessionEffect has
72
- // dispatched load-session. Match on name+projectPath since the CLI
73
- // doesn't know the id the UI will assign to the new session.
74
- const settled = new Promise<number>((resolve) => {
75
- const off = daemon.on('workspaceSwitched', (payload) => {
76
- off()
77
- clearTimeout(timer)
78
- writeJson({ name, projectPath, sessionId: payload.sessionId, switch: doSwitch })
79
- resolve(EXIT_OK)
80
- })
81
- const timer = setTimeout(() => {
82
- off()
83
- writeJson({ error: 'timed out waiting for workspaceSwitched', name, projectPath })
84
- resolve(EXIT_TIMEOUT)
85
- }, timeoutMs)
42
+ const record = await createProjectWorkspace({ base, branch, daemon, name, project })
43
+
44
+ writeJson({
45
+ branch: record.branch,
46
+ id: record.id,
47
+ name: record.name,
48
+ path: record.path,
49
+ repoRoot: record.repoRoot,
86
50
  })
87
-
88
- await daemon.expectOk('createWorkspace', { name, projectPath, switch: doSwitch })
89
- return settled
51
+ return EXIT_OK
90
52
  },
91
- summary: 'Create a new workspace (via the UI when attached, otherwise the catalog)',
53
+ summary: 'Create a new workspace in the active project',
92
54
  verb: 'create',
93
55
  }
@@ -1,6 +1,7 @@
1
1
  import type { CliCommand } from '../../registry'
2
2
 
3
- import { listWorkspaces } from '../../client/workspace-resolver'
3
+ import { listGitWorktrees } from '../../../git/worktree'
4
+ import { IPC_CAPABILITY_LIST_TABS } from '../../../ipc/protocol'
4
5
  import { SHARED_FLAGS } from '../../flags'
5
6
  import { EXIT_OK, writeJson } from '../../output'
6
7
 
@@ -8,18 +9,60 @@ export const workspaceList: CliCommand = {
8
9
  args: [],
9
10
  flags: SHARED_FLAGS,
10
11
  group: 'workspace',
11
- run: async () => {
12
- const sessions = listWorkspaces()
12
+ run: async (ctx) => {
13
+ const project = ctx.getProject()
14
+ const records = project.workspaces ?? []
15
+
16
+ // Count live tabs per workspace so an orchestrator can see co-location
17
+ // (several workers sharing one workspace) at a glance. Best-effort: `null`
18
+ // when the daemon is unreachable or predates the listTabs capability —
19
+ // `worktree list` otherwise reads purely from the catalog + git, so we don't
20
+ // want a down daemon to fail it.
21
+ const tabCounts = new Map<string, number>()
22
+ let tabCountsAvailable = false
23
+ try {
24
+ const daemon = await ctx.getDaemon()
25
+ if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
26
+ const { tabs } = await daemon.listTabs(project.id)
27
+ for (const tab of tabs) {
28
+ if (tab.workspaceId != null) {
29
+ tabCounts.set(tab.workspaceId, (tabCounts.get(tab.workspaceId) ?? 0) + 1)
30
+ }
31
+ }
32
+ tabCountsAvailable = true
33
+ }
34
+ } catch {
35
+ // daemon unreachable — leave tabCount null rather than failing the list.
36
+ }
37
+
38
+ // Cross-check against git so we can flag catalog entries that git no
39
+ // longer knows about (prunable / vanished) — otherwise the CLI would
40
+ // happily report workspaces that don't exist on disk.
41
+ const primary = records.find((w) => w.source === 'primary')
42
+ const gitPaths = new Set<string>()
43
+ if (primary) {
44
+ for (const w of await listGitWorktrees(primary.repoRoot)) {
45
+ if (w.prunable !== true) gitPaths.add(w.path)
46
+ }
47
+ }
48
+
13
49
  writeJson({
14
- workspaces: sessions.map((session) => ({
15
- id: session.id,
16
- lastOpenedAt: session.lastOpenedAt,
17
- name: session.name,
18
- projectPath: session.projectPath,
50
+ activeWorkspaceId: project.activeWorkspaceId ?? null,
51
+ projectId: project.id,
52
+ workspaces: records.map((w) => ({
53
+ branch: w.branch,
54
+ createdByAimux: w.createdByAimux,
55
+ gitTracked: primary ? gitPaths.has(w.path) : null,
56
+ id: w.id,
57
+ name: w.name,
58
+ path: w.path,
59
+ repoRoot: w.repoRoot,
60
+ source: w.source,
61
+ tabCount: tabCountsAvailable ? (tabCounts.get(w.id) ?? 0) : null,
19
62
  })),
20
63
  })
21
64
  return EXIT_OK
22
65
  },
23
- summary: 'List known workspaces (sessions) in the profile catalog',
66
+ summary: 'List workspaces for a project',
24
67
  verb: 'list',
25
68
  }
@@ -0,0 +1,81 @@
1
+ import type { CliCommand } from '../../registry'
2
+
3
+ import { removeGitWorktree } from '../../../git/worktree'
4
+ import {
5
+ IPC_CAPABILITY_LIST_TABS,
6
+ IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS,
7
+ } from '../../../ipc/protocol'
8
+ import { pruneEmptyWorktreeParent } from '../../../platform/worktree-paths'
9
+ import { SHARED_FLAGS } from '../../flags'
10
+ import { EXIT_OK, writeJson } from '../../output'
11
+
12
+ export const workspaceRemove: CliCommand = {
13
+ args: [{ complete: { kind: 'dynamic', source: 'workspace' }, name: 'workspace', required: true }],
14
+ flags: [
15
+ ...SHARED_FLAGS,
16
+ { description: 'pass --force to git worktree remove', kind: 'boolean', name: 'force' },
17
+ ],
18
+ group: 'workspace',
19
+ run: async (ctx) => {
20
+ const target = ctx.args.positionals[0]
21
+ if (typeof target !== 'string' || target.length === 0) {
22
+ throw new Error('workspace id or path is required')
23
+ }
24
+ const force = ctx.args.flags.force === true
25
+
26
+ const project = ctx.getProject()
27
+ const workspace =
28
+ project.workspaces?.find((w) => w.id === target) ??
29
+ project.workspaces?.find((w) => w.path === target)
30
+ if (!workspace) {
31
+ const known = project.workspaces?.map((w) => w.id).join(', ') ?? '(none)'
32
+ throw new Error(`unknown workspace: ${target} (known: ${known})`)
33
+ }
34
+ if (workspace.source === 'primary') {
35
+ throw new Error('refusing to remove the primary workspace')
36
+ }
37
+
38
+ const primary = project.workspaces?.find((w) => w.source === 'primary')
39
+ if (!primary) {
40
+ throw new Error('project has no primary workspace — cannot resolve repoRoot for git remove')
41
+ }
42
+
43
+ const daemon = await ctx.getDaemon()
44
+ if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS)) {
45
+ throw new Error(
46
+ 'daemon predates workspaceLifecycleEvents capability — restart aimux to pick up the new daemon'
47
+ )
48
+ }
49
+ if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
50
+ const live = (await daemon.listTabs(project.id)).tabs.filter(
51
+ (tab) => tab.workspaceId === workspace.id
52
+ )
53
+ if (live.length > 0) {
54
+ throw new Error(
55
+ `refusing to remove workspace with live tabs: ${live.map((tab) => tab.id).join(', ')}`
56
+ )
57
+ }
58
+ }
59
+
60
+ // All capability and liveness checks happen before touching git. If git
61
+ // refuses a dirty workspace the catalog remains unchanged.
62
+ await removeGitWorktree({ force, repoPath: primary.repoRoot, targetPath: workspace.path })
63
+ await pruneEmptyWorktreeParent(workspace.path)
64
+ try {
65
+ await daemon.expectOk('removeWorkspaceRecord', {
66
+ projectId: project.id,
67
+ workspaceId: workspace.id,
68
+ })
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error)
71
+ throw new Error(
72
+ `worktree removed from git but catalog reconciliation failed for ${workspace.id}: ${message}`
73
+ )
74
+ }
75
+
76
+ writeJson({ id: workspace.id, name: workspace.name, path: workspace.path })
77
+ return EXIT_OK
78
+ },
79
+ summary: 'Remove a workspace from the active project',
80
+ verb: 'remove',
81
+ }
@@ -51,10 +51,10 @@ const COMPLETION_SUBCOMMANDS: readonly CompletionCandidate[] = [
51
51
  ]
52
52
 
53
53
  const GROUP_DESCRIPTIONS: Record<string, string> = {
54
+ project: 'Projects (projects) in the profile catalog',
54
55
  tab: 'Drive individual tabs (create, send, snapshot, await)',
55
56
  worker: 'Named agent workers — the preferred orchestration surface',
56
- workspace: 'Workspaces (sessions) in the profile catalog',
57
- worktree: 'Git worktrees attached to the active workspace',
57
+ workspace: 'Git workspaces attached to the active project',
58
58
  }
59
59
 
60
60
  function groupCandidates(): CompletionCandidate[] {
@@ -8,8 +8,8 @@
8
8
  * shell must never see a stack trace where a completion list belongs.
9
9
  *
10
10
  * Phase 1 implements only the sources that read local state (built-in
11
- * assistants, the workspace catalog). Daemon-backed sources — tabs, workers,
12
- * worktrees — and git refs return nothing until phase 2 wires them up.
11
+ * assistants, the project catalog). Daemon-backed sources — tabs, workers,
12
+ * workspaces — and git refs return nothing until phase 2 wires them up.
13
13
  */
14
14
 
15
15
  import type { DynamicCompletionSource } from '../flags'
@@ -24,20 +24,20 @@ function assistantCandidates(): CompletionCandidate[] {
24
24
  }))
25
25
  }
26
26
 
27
- async function workspaceCandidates(): Promise<CompletionCandidate[]> {
28
- // Imported lazily: the session catalog pulls in config + state modules that
27
+ async function projectCandidates(): Promise<CompletionCandidate[]> {
28
+ // Imported lazily: the project catalog pulls in config + state modules that
29
29
  // the static-source paths (groups, verbs, flags) have no reason to load.
30
- const { listWorkspaces } = await import('../client/workspace-resolver')
31
- const sessions = listWorkspaces()
30
+ const { listProjects } = await import('../client/project-resolver')
31
+ const projects = listProjects()
32
32
  const nameCounts = new Map<string, number>()
33
- for (const session of sessions) {
34
- nameCounts.set(session.name, (nameCounts.get(session.name) ?? 0) + 1)
33
+ for (const project of projects) {
34
+ nameCounts.set(project.name, (nameCounts.get(project.name) ?? 0) + 1)
35
35
  }
36
- return sessions.map((session) =>
37
- // Ambiguous names can't be resolved by `--workspace`, so offer the id.
38
- (nameCounts.get(session.name) ?? 0) > 1
39
- ? { description: session.name, value: session.id }
40
- : { description: session.id, value: session.name }
36
+ return projects.map((project) =>
37
+ // Ambiguous names can't be resolved by `--project`, so offer the id.
38
+ (nameCounts.get(project.name) ?? 0) > 1
39
+ ? { description: project.name, value: project.id }
40
+ : { description: project.id, value: project.name }
41
41
  )
42
42
  }
43
43
 
@@ -45,10 +45,10 @@ async function resolveSource(source: DynamicCompletionSource): Promise<Completio
45
45
  switch (source) {
46
46
  case 'assistant':
47
47
  return assistantCandidates()
48
- case 'workspace':
49
- return await workspaceCandidates()
50
- // Phase 2: 'tab' | 'worker' | 'worktree' need a daemon round-trip under a
51
- // deadline; 'git-ref' needs a `git for-each-ref` in the workspace repo.
48
+ case 'project':
49
+ return await projectCandidates()
50
+ // Phase 2: 'tab' | 'worker' | 'workspace' need a daemon round-trip under a
51
+ // deadline; 'git-ref' needs a `git for-each-ref` in the project repo.
52
52
  default:
53
53
  return []
54
54
  }
@@ -56,7 +56,7 @@ async function resolveSource(source: DynamicCompletionSource): Promise<Completio
56
56
 
57
57
  /**
58
58
  * Resolve a dynamic source, filter by the partial word, and re-apply the
59
- * prefix the planner stripped (e.g. `--workspace=`). Always resolves.
59
+ * prefix the planner stripped (e.g. `--project=`). Always resolves.
60
60
  */
61
61
  export async function resolveDynamicCandidates(
62
62
  source: DynamicCompletionSource,
@@ -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' },