@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
@@ -3,10 +3,10 @@ import type { CliCommand } from '../../registry'
3
3
  import { isGitWorktreeDirty, removeGitWorktree } from '../../../git/worktree'
4
4
  import {
5
5
  IPC_CAPABILITY_THIN_ATTACH,
6
- IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS,
6
+ IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS,
7
7
  } from '../../../ipc/protocol'
8
8
  import { pruneEmptyWorktreeParent } from '../../../platform/worktree-paths'
9
- import { workspaceIdentity } from '../../client/workspace-resolver'
9
+ import { projectIdentity } from '../../client/project-resolver'
10
10
  import { SHARED_FLAGS } from '../../flags'
11
11
  import { EXIT_OK, EXIT_RUNTIME, writeJson } from '../../output'
12
12
  import { resolveWorkerTarget, WORKER_SCHEMA_VERSION, workerView } from './shared'
@@ -16,45 +16,45 @@ export const workerStop: CliCommand = {
16
16
  flags: [
17
17
  ...SHARED_FLAGS,
18
18
  {
19
- description: 'remove its aimux-created worktree after closing the tab',
19
+ description: 'remove its aimux-created workspace after closing the tab',
20
20
  kind: 'boolean',
21
- name: 'cleanup-worktree',
21
+ name: 'cleanup-workspace',
22
22
  },
23
- { description: 'force removal of a dirty worktree', kind: 'boolean', name: 'force' },
23
+ { description: 'force removal of a dirty workspace', kind: 'boolean', name: 'force' },
24
24
  ],
25
25
  group: 'worker',
26
26
  run: async (ctx) => {
27
- const { tab, workspace } = await resolveWorkerTarget(ctx, ctx.args.positionals[0] ?? '')
28
- const worker = workerView(workspace, tab)
27
+ const { project, tab } = await resolveWorkerTarget(ctx, ctx.args.positionals[0] ?? '')
28
+ const worker = workerView(project, tab)
29
29
  const daemon = await ctx.getDaemon()
30
- const cleanup = ctx.args.flags['cleanup-worktree'] === true
30
+ const cleanup = ctx.args.flags['cleanup-workspace'] === true
31
31
  const record =
32
- tab.worktreeId === undefined
32
+ tab.workspaceId === undefined
33
33
  ? undefined
34
- : workspace.worktrees?.find((worktree) => worktree.id === tab.worktreeId)
34
+ : project.workspaces?.find((workspace) => workspace.id === tab.workspaceId)
35
35
 
36
36
  if (cleanup) {
37
- if (record === undefined) throw new Error('worker has no registered worktree to clean up')
37
+ if (record === undefined) throw new Error('worker has no registered workspace to clean up')
38
38
  if (record.source !== 'aimux-temp' || !record.createdByAimux) {
39
- throw new Error('refusing to clean up a primary or externally managed worktree')
39
+ throw new Error('refusing to clean up a primary or externally managed workspace')
40
40
  }
41
- const siblings = (await daemon.listTabs(workspace.id)).tabs.filter(
42
- (entry) => entry.id !== tab.id && entry.worktreeId === record.id
41
+ const siblings = (await daemon.listTabs(project.id)).tabs.filter(
42
+ (entry) => entry.id !== tab.id && entry.workspaceId === record.id
43
43
  )
44
44
  if (siblings.length > 0) {
45
45
  throw new Error(
46
- `refusing to clean up shared worktree; live tabs: ${siblings.map((entry) => entry.id).join(', ')}`
46
+ `refusing to clean up shared workspace; live tabs: ${siblings.map((entry) => entry.id).join(', ')}`
47
47
  )
48
48
  }
49
- if (!daemon.hasCapability(IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS)) {
50
- throw new Error('daemon predates safe worktree cleanup — restart aimux')
49
+ if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE_EVENTS)) {
50
+ throw new Error('daemon predates safe workspace cleanup — restart aimux')
51
51
  }
52
52
  if (ctx.args.flags.force !== true && (await isGitWorktreeDirty(record.path))) {
53
- throw new Error('refusing to clean up a dirty worktree without --force')
53
+ throw new Error('refusing to clean up a dirty workspace without --force')
54
54
  }
55
55
  }
56
56
 
57
- // `closeTab` is session-scoped on the daemon, so it fails with "No session
57
+ // `closeTab` is project-scoped on the daemon, so it fails with "No project
58
58
  // attached" unless this connection has attached first. Every other worker
59
59
  // verb thin-attaches; this one did not, which made teardown the one step of
60
60
  // the documented lifecycle a headless orchestrator could not complete.
@@ -63,10 +63,10 @@ export const workerStop: CliCommand = {
63
63
  'daemon predates thinAttach capability — restart aimux to pick up the new daemon'
64
64
  )
65
65
  }
66
- await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
66
+ await daemon.attach({ cols: 0, projectId: project.id, rows: 0, thin: true })
67
67
 
68
68
  // Teardown is two independent effects. Report them independently: a failed
69
- // tab close must not strand a merged worktree on disk with no way to remove
69
+ // tab close must not strand a merged workspace on disk with no way to remove
70
70
  // it through aimux (the alternative — a bare `git worktree remove` — desyncs
71
71
  // the catalog from disk).
72
72
  let closeError: string | undefined
@@ -77,7 +77,7 @@ export const workerStop: CliCommand = {
77
77
  if (!cleanup) throw error
78
78
  }
79
79
 
80
- let worktreeRemoved = false
80
+ let workspaceRemoved = false
81
81
  if (cleanup && record !== undefined) {
82
82
  await removeGitWorktree({
83
83
  force: ctx.args.flags.force === true,
@@ -86,9 +86,9 @@ export const workerStop: CliCommand = {
86
86
  })
87
87
  await pruneEmptyWorktreeParent(record.path)
88
88
  try {
89
- await daemon.expectOk('removeWorktreeRecord', {
90
- sessionId: workspace.id,
91
- worktreeId: record.id,
89
+ await daemon.expectOk('removeWorkspaceRecord', {
90
+ projectId: project.id,
91
+ workspaceId: record.id,
92
92
  })
93
93
  } catch (error) {
94
94
  const message = error instanceof Error ? error.message : String(error)
@@ -96,18 +96,18 @@ export const workerStop: CliCommand = {
96
96
  `worker stopped and git worktree removed, but catalog reconciliation failed: ${message}`
97
97
  )
98
98
  }
99
- worktreeRemoved = true
99
+ workspaceRemoved = true
100
100
  }
101
101
  writeJson({
102
102
  closed: closeError === undefined,
103
103
  ...(closeError === undefined ? {} : { closeError }),
104
+ project: projectIdentity(project),
104
105
  schemaVersion: WORKER_SCHEMA_VERSION,
105
106
  worker,
106
- workspace: workspaceIdentity(workspace),
107
- worktreeRemoved,
107
+ workspaceRemoved,
108
108
  })
109
109
  return closeError === undefined ? EXIT_OK : EXIT_RUNTIME
110
110
  },
111
- summary: 'Stop a named worker and optionally clean up its worktree',
111
+ summary: 'Stop a named worker and optionally clean up its workspace',
112
112
  verb: 'stop',
113
113
  }
@@ -23,16 +23,16 @@ export const workerSubmit: CliCommand = {
23
23
  ],
24
24
  group: 'worker',
25
25
  run: async (ctx) => {
26
- const { tab, workspace } = await resolveWorkerTarget(ctx, ctx.args.positionals[0] ?? '')
26
+ const { project, tab } = await resolveWorkerTarget(ctx, ctx.args.positionals[0] ?? '')
27
27
  const outcome = await submitWorkerPrompt(
28
28
  ctx,
29
- workspace,
29
+ project,
30
30
  tab.id,
31
31
  typeof ctx.args.flags['uptake-timeout'] === 'number'
32
32
  ? ctx.args.flags['uptake-timeout']
33
33
  : DETACH_UPTAKE_TIMEOUT_MS
34
34
  )
35
- writeJson(workerEnvelope(workspace, workerView(workspace, tab), outcome))
35
+ writeJson(workerEnvelope(project, workerView(project, tab), outcome))
36
36
  return workerOutcomeExitCode(outcome)
37
37
  },
38
38
  summary: 'Submit a prompt already sitting in a worker composer and confirm uptake',
@@ -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,