@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,187 +0,0 @@
1
- import type { SessionBackend } from '../session-backend/types'
2
- import type { AppAction, AppState, SessionRecord, TabSession } from '../state/types'
3
-
4
- import { logInputDebug } from '../debug/input-log'
5
- import { createPrefixedId } from '../platform/id'
6
- import { saveSessionCatalog } from '../state/session-catalog'
7
- import { createEmptyWorkspaceSnapshot, serializeWorkspace } from '../state/session-persistence'
8
- import { createPrimaryWorktree, ensureSessionWorktrees } from '../state/session-worktrees'
9
- import { toast } from '../state/toast-store'
10
- import { buildSessionsWithCurrentSnapshot } from '../state/workspace-save'
11
-
12
- export function createSessionFromCurrentState(
13
- state: AppState,
14
- name: string,
15
- projectPath?: string
16
- ): { session: SessionRecord; sessions: SessionRecord[] } {
17
- const now = new Date().toISOString()
18
- const workspaceSnapshot =
19
- (state.currentSessionId != null && state.currentSessionId !== '') || state.tabs.length === 0
20
- ? createEmptyWorkspaceSnapshot()
21
- : serializeWorkspace(state)
22
- const session: SessionRecord = {
23
- activeWorktreeId: undefined,
24
- createdAt: now,
25
- id: createPrefixedId('session'),
26
- lastOpenedAt: now,
27
- name,
28
- projectPath,
29
- updatedAt: now,
30
- workspaceSnapshot,
31
- worktrees: undefined,
32
- }
33
- if (projectPath != null && projectPath !== '') {
34
- const worktree = createPrimaryWorktree(projectPath, now)
35
- session.activeWorktreeId = worktree.id
36
- session.worktrees = [worktree]
37
- }
38
-
39
- let updatedSessions = state.sessions
40
- if (state.currentSessionId != null && state.currentSessionId !== '') {
41
- const currentSnapshot = serializeWorkspace(state)
42
- updatedSessions = state.sessions.map((entry) =>
43
- entry.id === state.currentSessionId
44
- ? { ...entry, updatedAt: now, workspaceSnapshot: currentSnapshot }
45
- : entry
46
- )
47
- }
48
-
49
- return {
50
- session,
51
- sessions: [...updatedSessions, ensureSessionWorktrees(session)],
52
- }
53
- }
54
-
55
- export function renameSessionRecords(
56
- sessions: SessionRecord[],
57
- sessionId: string,
58
- name: string
59
- ): SessionRecord[] {
60
- return sessions.map((session) =>
61
- session.id === sessionId ? { ...session, name, updatedAt: new Date().toISOString() } : session
62
- )
63
- }
64
-
65
- export function switchSessionRecords(state: AppState, session: SessionRecord): SessionRecord[] {
66
- const sessionsWithSnapshot = buildSessionsWithCurrentSnapshot(
67
- state.sessions,
68
- state.currentSessionId,
69
- state
70
- )
71
- return sessionsWithSnapshot.map((entry) =>
72
- entry.id === session.id ? { ...entry, lastOpenedAt: new Date().toISOString() } : entry
73
- )
74
- }
75
-
76
- export function deleteSessionRecords(
77
- sessions: SessionRecord[],
78
- sessionId: string
79
- ): SessionRecord[] {
80
- return sessions.filter((session) => session.id !== sessionId)
81
- }
82
-
83
- export function handleCreateSessionEffect(
84
- state: AppState,
85
- dispatch: (action: AppAction) => void,
86
- name: string,
87
- projectPath?: string
88
- ): void {
89
- const { session, sessions } = createSessionFromCurrentState(state, name, projectPath)
90
- logInputDebug('app.session.create', {
91
- fromCurrentWorkspace:
92
- (state.currentSessionId == null || state.currentSessionId === '') && state.tabs.length > 0,
93
- name,
94
- sessionId: session.id,
95
- tabCount: session.workspaceSnapshot?.tabs.length ?? 0,
96
- })
97
- saveSessionCatalog(sessions)
98
- dispatch({ sessions, type: 'set-sessions' })
99
- dispatch({
100
- sessionId: session.id,
101
- type: 'load-session',
102
- workspaceSnapshot: session.workspaceSnapshot,
103
- })
104
- }
105
-
106
- export function handleRenameSessionEffect(
107
- sessions: SessionRecord[],
108
- dispatch: (action: AppAction) => void,
109
- sessionId: string,
110
- name: string
111
- ): void {
112
- logInputDebug('app.session.rename', { name, sessionId })
113
- const renamed = renameSessionRecords(sessions, sessionId, name)
114
- saveSessionCatalog(renamed)
115
- dispatch({ name, sessionId, type: 'rename-session-record' })
116
- }
117
-
118
- export function handleSwitchSessionEffect(
119
- state: AppState,
120
- backend: SessionBackend,
121
- dispatch: (action: AppAction) => void,
122
- session: SessionRecord
123
- ): void {
124
- logInputDebug('app.session.switch.start', {
125
- currentTabCount: state.tabs.length,
126
- fromSessionId: state.currentSessionId,
127
- restoredTabCount: session.workspaceSnapshot?.tabs.length ?? 0,
128
- toName: session.name,
129
- toSessionId: session.id,
130
- })
131
- const sessions = switchSessionRecords(state, session)
132
- saveSessionCatalog(sessions)
133
- void backend.destroy(true)
134
- dispatch({ sessions, type: 'set-sessions' })
135
- dispatch({
136
- sessionId: session.id,
137
- type: 'load-session',
138
- workspaceSnapshot: session.workspaceSnapshot,
139
- })
140
- logInputDebug('app.session.switch.dispatched', { toSessionId: session.id })
141
- }
142
-
143
- export function handleDeleteSessionEffect(
144
- state: AppState,
145
- backend: SessionBackend,
146
- dispatch: (action: AppAction) => void,
147
- sessionId: string,
148
- options?: { openSessionPicker?: boolean }
149
- ): void {
150
- const name = state.sessions.find((entry) => entry.id === sessionId)?.name
151
- const remaining = deleteSessionRecords(state.sessions, sessionId)
152
- logInputDebug('app.session.delete', {
153
- remainingCount: remaining.length,
154
- sessionId,
155
- wasCurrent: sessionId === state.currentSessionId,
156
- })
157
- saveSessionCatalog(remaining)
158
- if (sessionId === state.currentSessionId) {
159
- void backend.destroy(true)
160
- }
161
- dispatch({
162
- openSessionPicker: options?.openSessionPicker,
163
- sessionId,
164
- type: 'delete-session-record',
165
- })
166
- toast.success(name != null && name !== '' ? `Deleted workspace "${name}"` : 'Workspace deleted')
167
- }
168
-
169
- export function restartTabSession(
170
- backend: SessionBackend,
171
- dispatch: (action: AppAction) => void,
172
- clearIdleTimer: (tabId: string) => void,
173
- clearStartupGrace: (tabId: string) => void,
174
- startTabSession: (tab: TabSession) => void,
175
- tab: TabSession
176
- ): void {
177
- logInputDebug('app.restartTab', {
178
- command: tab.command,
179
- status: tab.status,
180
- tabId: tab.id,
181
- })
182
- clearIdleTimer(tab.id)
183
- clearStartupGrace(tab.id)
184
- backend.disposeSession(tab.id)
185
- dispatch({ tabId: tab.id, type: 'reset-tab-session' })
186
- startTabSession(tab)
187
- }
@@ -1,110 +0,0 @@
1
- import type { SessionRecord, WorktreeRecord } from '../../state/types'
2
-
3
- import { findMostRecentSession, loadSessionCatalog } from '../../state/session-catalog'
4
-
5
- /**
6
- * Where a command's target workspace came from. An orchestrator needs this to
7
- * tell "I asked for pragma-once" from "aimux guessed, and the UI had moved on":
8
- * `active` is the only origin that can silently follow the UI to another repo.
9
- */
10
- export type WorkspaceOrigin = 'flag' | 'env' | 'active'
11
-
12
- export interface ResolvedWorkspace {
13
- origin: WorkspaceOrigin
14
- record: SessionRecord
15
- }
16
-
17
- /** Env pin for headless orchestrators — `--workspace` still wins over it. */
18
- export const WORKSPACE_ENV_VAR = 'AIMUX_WORKSPACE'
19
-
20
- /** The workspace's primary (root) worktree, i.e. the repository it is about. */
21
- export function findPrimaryWorktree(session: SessionRecord): WorktreeRecord | undefined {
22
- return session.worktrees?.find((worktree) => worktree.source === 'primary')
23
- }
24
-
25
- /**
26
- * The repo every fresh worktree for this workspace is cut from. Surfaced in
27
- * every worker envelope so an agent can see *which project* it just acted on
28
- * instead of inferring it from a worktree path hash.
29
- */
30
- export function workspaceRepoRoot(session: SessionRecord): string | null {
31
- return findPrimaryWorktree(session)?.repoRoot ?? session.projectPath ?? null
32
- }
33
-
34
- /** Stable workspace identity block embedded in command output. */
35
- export function workspaceIdentity(session: SessionRecord): {
36
- id: string
37
- name: string
38
- repoRoot: string | null
39
- } {
40
- return { id: session.id, name: session.name, repoRoot: workspaceRepoRoot(session) }
41
- }
42
-
43
- /**
44
- * Resolve `--workspace W` to a session record from the catalog, reporting where
45
- * the choice came from. Precedence: the explicit flag, then `AIMUX_WORKSPACE`,
46
- * then the most recently opened session. Throws when the catalog is empty (no
47
- * session has ever been created) or when the explicit name/id doesn't match.
48
- *
49
- * Matching: exact id wins; otherwise exact name (case-sensitive); otherwise
50
- * unique case-insensitive name match.
51
- */
52
- export function resolveWorkspaceWithOrigin(name: string | undefined): ResolvedWorkspace {
53
- const sessions = loadSessionCatalog()
54
- if (sessions.length === 0) {
55
- throw new Error(
56
- 'no sessions found — create one from the aimux UI first (or pass --workspace once created)'
57
- )
58
- }
59
-
60
- const flag = name !== undefined && name !== '' ? name : undefined
61
- const env = process.env[WORKSPACE_ENV_VAR]
62
- const fromEnv = env != null && env !== '' ? env : undefined
63
- const selector = flag ?? fromEnv
64
- if (selector === undefined) {
65
- const active = findMostRecentSession(sessions)
66
- if (!active) {
67
- throw new Error('no active workspace and the catalog is empty')
68
- }
69
- return { origin: 'active', record: active }
70
- }
71
-
72
- return {
73
- origin: flag !== undefined ? 'flag' : 'env',
74
- record: matchWorkspace(sessions, selector),
75
- }
76
- }
77
-
78
- export function resolveWorkspace(name: string | undefined): SessionRecord {
79
- return resolveWorkspaceWithOrigin(name).record
80
- }
81
-
82
- function matchWorkspace(sessions: SessionRecord[], name: string): SessionRecord {
83
- const byId = sessions.find((session) => session.id === name)
84
- if (byId) return byId
85
-
86
- const exactNameMatches = sessions.filter((session) => session.name === name)
87
- if (exactNameMatches.length > 1) {
88
- throw new Error(
89
- `workspace "${name}" matches multiple sessions: ${exactNameMatches.map((s) => s.id).join(', ')}`
90
- )
91
- }
92
- const exactOnly = exactNameMatches[0]
93
- if (exactOnly) return exactOnly
94
-
95
- const lower = name.toLowerCase()
96
- const ciMatches = sessions.filter((session) => session.name.toLowerCase() === lower)
97
- if (ciMatches.length > 1) {
98
- throw new Error(
99
- `workspace "${name}" matches multiple sessions: ${ciMatches.map((s) => s.id).join(', ')}`
100
- )
101
- }
102
- const ciOnly = ciMatches[0]
103
- if (ciOnly) return ciOnly
104
-
105
- throw new Error(`workspace not found: ${name}`)
106
- }
107
-
108
- export function listWorkspaces(): SessionRecord[] {
109
- return loadSessionCatalog()
110
- }
@@ -1,32 +0,0 @@
1
- import type { CliCommand } from '../../registry'
2
-
3
- import { IPC_CAPABILITY_WORKSPACE_LIFECYCLE } from '../../../ipc/protocol'
4
- import { resolveWorkspace } from '../../client/workspace-resolver'
5
- import { SHARED_FLAGS } from '../../flags'
6
- import { EXIT_OK, writeJson } from '../../output'
7
-
8
- export const workspaceClose: CliCommand = {
9
- args: [{ complete: { kind: 'dynamic', source: 'workspace' }, name: 'workspace', required: true }],
10
- flags: SHARED_FLAGS,
11
- group: 'workspace',
12
- run: async (ctx) => {
13
- const target = ctx.args.positionals[0]
14
- if (typeof target !== 'string' || target.length === 0) {
15
- throw new Error('target workspace is required (id or name)')
16
- }
17
- const session = resolveWorkspace(target)
18
-
19
- const daemon = await ctx.getDaemon()
20
- if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE)) {
21
- throw new Error(
22
- 'daemon predates workspaceLifecycle capability — restart aimux to pick up the new daemon'
23
- )
24
- }
25
-
26
- await daemon.expectOk('closeWorkspace', { targetSessionId: session.id })
27
- writeJson({ closedSessionId: session.id, name: session.name })
28
- return EXIT_OK
29
- },
30
- summary: 'Close a workspace (via the UI when attached, otherwise the catalog)',
31
- verb: 'close',
32
- }
@@ -1,32 +0,0 @@
1
- import type { CliCommand } from '../../registry'
2
-
3
- import { SHARED_FLAGS } from '../../flags'
4
- import { EXIT_OK, writeJson } from '../../output'
5
-
6
- export const workspaceShow: CliCommand = {
7
- args: [],
8
- flags: SHARED_FLAGS,
9
- group: 'workspace',
10
- run: async (ctx) => {
11
- const workspace = ctx.getWorkspace()
12
- writeJson({
13
- activeWorktreeId: workspace.activeWorktreeId,
14
- createdAt: workspace.createdAt,
15
- id: workspace.id,
16
- lastOpenedAt: workspace.lastOpenedAt,
17
- name: workspace.name,
18
- projectPath: workspace.projectPath,
19
- worktrees:
20
- workspace.worktrees?.map((w) => ({
21
- branch: w.branch,
22
- id: w.id,
23
- name: w.name,
24
- path: w.path,
25
- source: w.source,
26
- })) ?? [],
27
- })
28
- return EXIT_OK
29
- },
30
- summary: 'Show the active workspace (or the one named via --workspace)',
31
- verb: 'show',
32
- }
@@ -1,55 +0,0 @@
1
- import type { CliCommand } from '../../registry'
2
-
3
- import { SHARED_FLAGS } from '../../flags'
4
- import { EXIT_OK, writeJson } from '../../output'
5
- import { createWorkspaceWorktree } from './create-core'
6
-
7
- export const worktreeCreate: CliCommand = {
8
- args: [],
9
- flags: [
10
- ...SHARED_FLAGS,
11
- {
12
- complete: { kind: 'none' },
13
- description: 'display name for the new worktree',
14
- kind: 'string',
15
- name: 'name',
16
- },
17
- {
18
- complete: { kind: 'none' },
19
- description: 'branch name (defaults to aimux/<name>)',
20
- kind: 'string',
21
- name: 'branch',
22
- },
23
- {
24
- complete: { kind: 'dynamic', source: 'git-ref' },
25
- description: 'base ref for the branch (defaults to HEAD)',
26
- kind: 'string',
27
- name: 'base',
28
- },
29
- ],
30
- group: 'worktree',
31
- run: async (ctx) => {
32
- const name = ctx.args.flags.name
33
- if (typeof name !== 'string' || name.length === 0) {
34
- throw new Error('--name is required')
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'
39
-
40
- const workspace = ctx.getWorkspace()
41
- const daemon = await ctx.getDaemon()
42
- const record = await createWorkspaceWorktree({ base, branch, daemon, name, workspace })
43
-
44
- writeJson({
45
- branch: record.branch,
46
- id: record.id,
47
- name: record.name,
48
- path: record.path,
49
- repoRoot: record.repoRoot,
50
- })
51
- return EXIT_OK
52
- },
53
- summary: 'Create a new worktree in the active workspace',
54
- verb: 'create',
55
- }
@@ -1,68 +0,0 @@
1
- import type { CliCommand } from '../../registry'
2
-
3
- import { listGitWorktrees } from '../../../git/worktree'
4
- import { IPC_CAPABILITY_LIST_TABS } from '../../../ipc/protocol'
5
- import { SHARED_FLAGS } from '../../flags'
6
- import { EXIT_OK, writeJson } from '../../output'
7
-
8
- export const worktreeList: CliCommand = {
9
- args: [],
10
- flags: SHARED_FLAGS,
11
- group: 'worktree',
12
- run: async (ctx) => {
13
- const workspace = ctx.getWorkspace()
14
- const records = workspace.worktrees ?? []
15
-
16
- // Count live tabs per worktree so an orchestrator can see co-location
17
- // (several workers sharing one worktree) 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(workspace.id)
27
- for (const tab of tabs) {
28
- if (tab.worktreeId != null) {
29
- tabCounts.set(tab.worktreeId, (tabCounts.get(tab.worktreeId) ?? 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 worktrees 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
-
49
- writeJson({
50
- activeWorktreeId: workspace.activeWorktreeId ?? null,
51
- workspaceId: workspace.id,
52
- worktrees: 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,
62
- })),
63
- })
64
- return EXIT_OK
65
- },
66
- summary: 'List worktrees for a workspace',
67
- verb: 'list',
68
- }
@@ -1,81 +0,0 @@
1
- import type { CliCommand } from '../../registry'
2
-
3
- import { removeGitWorktree } from '../../../git/worktree'
4
- import {
5
- IPC_CAPABILITY_LIST_TABS,
6
- IPC_CAPABILITY_WORKTREE_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 worktreeRemove: CliCommand = {
13
- args: [{ complete: { kind: 'dynamic', source: 'worktree' }, name: 'worktree', required: true }],
14
- flags: [
15
- ...SHARED_FLAGS,
16
- { description: 'pass --force to git worktree remove', kind: 'boolean', name: 'force' },
17
- ],
18
- group: 'worktree',
19
- run: async (ctx) => {
20
- const target = ctx.args.positionals[0]
21
- if (typeof target !== 'string' || target.length === 0) {
22
- throw new Error('worktree id or path is required')
23
- }
24
- const force = ctx.args.flags.force === true
25
-
26
- const workspace = ctx.getWorkspace()
27
- const worktree =
28
- workspace.worktrees?.find((w) => w.id === target) ??
29
- workspace.worktrees?.find((w) => w.path === target)
30
- if (!worktree) {
31
- const known = workspace.worktrees?.map((w) => w.id).join(', ') ?? '(none)'
32
- throw new Error(`unknown worktree: ${target} (known: ${known})`)
33
- }
34
- if (worktree.source === 'primary') {
35
- throw new Error('refusing to remove the primary worktree')
36
- }
37
-
38
- const primary = workspace.worktrees?.find((w) => w.source === 'primary')
39
- if (!primary) {
40
- throw new Error('workspace has no primary worktree — cannot resolve repoRoot for git remove')
41
- }
42
-
43
- const daemon = await ctx.getDaemon()
44
- if (!daemon.hasCapability(IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS)) {
45
- throw new Error(
46
- 'daemon predates worktreeLifecycleEvents 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(workspace.id)).tabs.filter(
51
- (tab) => tab.worktreeId === worktree.id
52
- )
53
- if (live.length > 0) {
54
- throw new Error(
55
- `refusing to remove worktree 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 worktree the catalog remains unchanged.
62
- await removeGitWorktree({ force, repoPath: primary.repoRoot, targetPath: worktree.path })
63
- await pruneEmptyWorktreeParent(worktree.path)
64
- try {
65
- await daemon.expectOk('removeWorktreeRecord', {
66
- sessionId: workspace.id,
67
- worktreeId: worktree.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 ${worktree.id}: ${message}`
73
- )
74
- }
75
-
76
- writeJson({ id: worktree.id, name: worktree.name, path: worktree.path })
77
- return EXIT_OK
78
- },
79
- summary: 'Remove a worktree from the active workspace',
80
- verb: 'remove',
81
- }
@@ -1,54 +0,0 @@
1
- import { useEffect } from 'react'
2
-
3
- import { appStore } from '../state/app-store'
4
- import { dispatchGlobal } from '../state/dispatch-ref'
5
- import { getCurrentBranch } from '../ui/git-branch'
6
-
7
- const INTERVAL_MS = 4000
8
-
9
- // Single source of truth for "what git branch is each worktree on right now".
10
- // Polls every known worktree's path and dispatches update-worktree-record
11
- // when the live branch differs from the stored one. Components read
12
- // `worktree.branch` from state — no per-component polling, no flickers.
13
- //
14
- // Runs once when enabled; reads sessions from the store on each tick so
15
- // session updates do NOT re-create the effect (which would otherwise feedback
16
- // off our own dispatches).
17
- export function useWorktreeBranchPolling(enabled: boolean): void {
18
- useEffect(() => {
19
- if (!enabled) return
20
-
21
- let cancelled = false
22
- let timer: ReturnType<typeof setTimeout> | null = null
23
-
24
- const tick = async () => {
25
- const sessions = appStore.getState().sessions
26
- await Promise.all(
27
- sessions.flatMap((session) =>
28
- (session.worktrees ?? []).map(async (worktree) => {
29
- if (worktree.path == null || worktree.path === '') return
30
- const branch = await getCurrentBranch(worktree.path)
31
- if (cancelled) return
32
- if (branch != null && branch !== worktree.branch) {
33
- dispatchGlobal({
34
- patch: { branch },
35
- sessionId: session.id,
36
- type: 'update-worktree-record',
37
- worktreeId: worktree.id,
38
- })
39
- }
40
- })
41
- )
42
- )
43
- if (cancelled) return
44
- timer = setTimeout(() => void tick(), INTERVAL_MS)
45
- }
46
-
47
- void tick()
48
-
49
- return () => {
50
- cancelled = true
51
- if (timer != null) clearTimeout(timer)
52
- }
53
- }, [enabled])
54
- }
@@ -1,59 +0,0 @@
1
- import { useEffect } from 'react'
2
-
3
- import type { BranchDivergence } from '../state/types'
4
-
5
- import { useAppStore } from '../state/app-store'
6
- import { dispatchGlobal } from '../state/dispatch-ref'
7
- import { getBranchDivergence } from './divergence'
8
-
9
- const INTERVAL_MS = 4000
10
-
11
- // Polls per-worktree base divergence for the current session while enabled and
12
- // dispatches it into worktreeDivergence. Only worktrees that record a baseRef
13
- // (the aimux-created ones) are measured; the primary and externally-discovered
14
- // worktrees have no recorded base and are left out.
15
- export function useWorktreeDivergencePolling(enabled: boolean): void {
16
- const currentSessionId = useAppStore((s) => s.currentSessionId)
17
- const sessions = useAppStore((s) => s.sessions)
18
-
19
- useEffect(() => {
20
- if (!enabled) return
21
- const session =
22
- currentSessionId != null && currentSessionId !== ''
23
- ? sessions.find((s) => s.id === currentSessionId)
24
- : undefined
25
- const targets = (session?.worktrees ?? []).filter(
26
- (w) => w.baseRef != null && w.baseRef !== '' && w.branch != null && w.branch !== ''
27
- )
28
- if (targets.length === 0) return
29
-
30
- let cancelled = false
31
- let timer: ReturnType<typeof setTimeout> | null = null
32
-
33
- const tick = async () => {
34
- const entries = await Promise.all(
35
- targets.map(async (worktree) => {
36
- const base = worktree.baseRef
37
- const branch = worktree.branch
38
- if (base == null || branch == null) return null
39
- const divergence = await getBranchDivergence(worktree.repoRoot, base, branch)
40
- return divergence != null ? ([worktree.id, divergence] as const) : null
41
- })
42
- )
43
- if (cancelled) return
44
- const next: Record<string, BranchDivergence> = {}
45
- for (const entry of entries) {
46
- if (entry != null) next[entry[0]] = entry[1]
47
- }
48
- dispatchGlobal({ divergence: next, type: 'set-worktree-divergence' })
49
- timer = setTimeout(() => void tick(), INTERVAL_MS)
50
- }
51
-
52
- void tick()
53
-
54
- return () => {
55
- cancelled = true
56
- if (timer != null) clearTimeout(timer)
57
- }
58
- }, [enabled, currentSessionId, sessions])
59
- }