@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,10 +1,10 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
 
3
3
  import type {
4
+ ProjectSnapshotV1,
4
5
  TabSession,
5
6
  TerminalModeState,
6
7
  TerminalSnapshot,
7
- WorkspaceSnapshotV1,
8
8
  } from '../state/types'
9
9
 
10
10
  import { logDebug } from '../debug/input-log'
@@ -12,110 +12,110 @@ import { SessionRegistry } from './session-registry'
12
12
 
13
13
  interface SessionManagerEvents {
14
14
  render: [
15
- sessionId: string,
15
+ projectId: string,
16
16
  tabId: string,
17
17
  viewport: TerminalSnapshot,
18
18
  terminalModes: TerminalModeState,
19
19
  ]
20
- exit: [sessionId: string, tabId: string, exitCode: number]
21
- error: [sessionId: string, tabId: string, message: string]
20
+ exit: [projectId: string, tabId: string, exitCode: number]
21
+ error: [projectId: string, tabId: string, message: string]
22
22
  }
23
23
 
24
24
  export class SessionManager extends EventEmitter<SessionManagerEvents> {
25
25
  private registries = new Map<string, SessionRegistry>()
26
26
 
27
- private getOrCreateRegistry(sessionId: string): SessionRegistry {
28
- const existing = this.registries.get(sessionId)
27
+ private getOrCreateRegistry(projectId: string): SessionRegistry {
28
+ const existing = this.registries.get(projectId)
29
29
  if (existing) {
30
30
  return existing
31
31
  }
32
32
 
33
33
  const registry = new SessionRegistry()
34
34
  registry.on('render', (tabId, viewport, terminalModes) => {
35
- this.emit('render', sessionId, tabId, viewport, terminalModes)
35
+ this.emit('render', projectId, tabId, viewport, terminalModes)
36
36
  })
37
37
  registry.on('exit', (tabId, exitCode) => {
38
- this.emit('exit', sessionId, tabId, exitCode)
38
+ this.emit('exit', projectId, tabId, exitCode)
39
39
  })
40
40
  registry.on('error', (tabId, message) => {
41
- this.emit('error', sessionId, tabId, message)
41
+ this.emit('error', projectId, tabId, message)
42
42
  })
43
- this.registries.set(sessionId, registry)
43
+ this.registries.set(projectId, registry)
44
44
  return registry
45
45
  }
46
46
 
47
- attachSession(sessionId: string, snapshot?: WorkspaceSnapshotV1) {
47
+ attachSession(projectId: string, snapshot?: ProjectSnapshotV1) {
48
48
  logDebug('daemon.sessionManager.attachSession', {
49
49
  hasSnapshot: !!snapshot,
50
- sessionId,
50
+ projectId,
51
51
  snapshotTabs: snapshot?.tabs.length ?? 0,
52
52
  })
53
- return this.getOrCreateRegistry(sessionId).attachFromSnapshot(snapshot)
53
+ return this.getOrCreateRegistry(projectId).attachFromSnapshot(snapshot)
54
54
  }
55
55
 
56
- createTab(sessionId: string, options: Parameters<SessionRegistry['createSession']>[0]): void {
56
+ createTab(projectId: string, options: Parameters<SessionRegistry['createSession']>[0]): void {
57
57
  logDebug('daemon.sessionManager.createTab', {
58
58
  command: options.command,
59
- sessionId,
59
+ projectId,
60
60
  tabId: options.tabId,
61
61
  title: options.title,
62
62
  })
63
- this.getOrCreateRegistry(sessionId).createSession(options)
63
+ this.getOrCreateRegistry(projectId).createSession(options)
64
64
  }
65
65
 
66
- write(sessionId: string, tabId: string, data: string): void {
67
- this.getOrCreateRegistry(sessionId).write(tabId, data)
66
+ write(projectId: string, tabId: string, data: string): void {
67
+ this.getOrCreateRegistry(projectId).write(tabId, data)
68
68
  }
69
69
 
70
70
  updateTabMetadata(
71
- sessionId: string,
71
+ projectId: string,
72
72
  tabId: string,
73
73
  patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
74
74
  ): void {
75
- this.getOrCreateRegistry(sessionId).updateTabMetadata(tabId, patch)
75
+ this.getOrCreateRegistry(projectId).updateTabMetadata(tabId, patch)
76
76
  }
77
77
 
78
- resize(sessionId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
79
- this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, options)
78
+ resize(projectId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
79
+ this.getOrCreateRegistry(projectId).resizeAll(cols, rows, options)
80
80
  }
81
81
 
82
82
  resizeTab(
83
- sessionId: string,
83
+ projectId: string,
84
84
  tabId: string,
85
85
  cols: number,
86
86
  rows: number,
87
87
  options?: { sync?: boolean }
88
88
  ): void {
89
- this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, options)
89
+ this.getOrCreateRegistry(projectId).resizeTab(tabId, cols, rows, options)
90
90
  }
91
91
 
92
- scroll(sessionId: string, tabId: string, deltaLines: number): void {
93
- this.getOrCreateRegistry(sessionId).scrollViewport(tabId, deltaLines)
92
+ scroll(projectId: string, tabId: string, deltaLines: number): void {
93
+ this.getOrCreateRegistry(projectId).scrollViewport(tabId, deltaLines)
94
94
  }
95
95
 
96
- scrollToBottom(sessionId: string, tabId: string): void {
97
- this.getOrCreateRegistry(sessionId).scrollViewportToBottom(tabId)
96
+ scrollToBottom(projectId: string, tabId: string): void {
97
+ this.getOrCreateRegistry(projectId).scrollViewportToBottom(tabId)
98
98
  }
99
99
 
100
- setActiveTab(sessionId: string, tabId: string | null): void {
101
- this.getOrCreateRegistry(sessionId).setActiveTab(tabId)
100
+ setActiveTab(projectId: string, tabId: string | null): void {
101
+ this.getOrCreateRegistry(projectId).setActiveTab(tabId)
102
102
  }
103
103
 
104
- closeTab(sessionId: string, tabId: string): void {
105
- this.getOrCreateRegistry(sessionId).closeTab(tabId)
104
+ closeTab(projectId: string, tabId: string): void {
105
+ this.getOrCreateRegistry(projectId).closeTab(tabId)
106
106
  }
107
107
 
108
- disposeSession(sessionId: string): void {
108
+ disposeSession(projectId: string): void {
109
109
  logDebug('daemon.sessionManager.disposeSession', {
110
- hadRegistry: this.registries.has(sessionId),
111
- sessionId,
110
+ hadRegistry: this.registries.has(projectId),
111
+ projectId,
112
112
  })
113
- const registry = this.registries.get(sessionId)
113
+ const registry = this.registries.get(projectId)
114
114
  if (!registry) {
115
115
  return
116
116
  }
117
117
  registry.disposeAll()
118
- this.registries.delete(sessionId)
118
+ this.registries.delete(projectId)
119
119
  }
120
120
 
121
121
  disposeAll(): void {
@@ -143,7 +143,7 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
143
143
  return [...this.registries.keys()]
144
144
  }
145
145
 
146
- listTabs(sessionId: string): TabSession[] {
147
- return this.registries.get(sessionId)?.listTabs() ?? []
146
+ listTabs(projectId: string): TabSession[] {
147
+ return this.registries.get(projectId)?.listTabs() ?? []
148
148
  }
149
149
  }
@@ -1,10 +1,10 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
 
3
3
  import type {
4
+ ProjectSnapshotV1,
4
5
  TabSession,
5
6
  TerminalModeState,
6
7
  TerminalSnapshot,
7
- WorkspaceSnapshotV1,
8
8
  } from '../state/types'
9
9
 
10
10
  import { logDebug } from '../debug/input-log'
@@ -12,8 +12,8 @@ import { PtyManager } from '../pty/pty-manager'
12
12
  import {
13
13
  normalizeGroupedTabOrder,
14
14
  restoreLayoutTrees,
15
- restoreTabsFromWorkspace,
16
- } from '../state/session-persistence'
15
+ restoreTabsFromProject,
16
+ } from '../state/project-persistence'
17
17
  import { createDefaultTerminalModes } from '../state/terminal-modes'
18
18
 
19
19
  interface SessionRegistryEvents {
@@ -65,7 +65,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
65
65
  })
66
66
  }
67
67
 
68
- attachFromSnapshot(snapshot: WorkspaceSnapshotV1 | undefined): {
68
+ attachFromSnapshot(snapshot: ProjectSnapshotV1 | undefined): {
69
69
  tabs: TabSession[]
70
70
  activeTabId: string | null
71
71
  } {
@@ -75,7 +75,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
75
75
  snapshotTabs: snapshot?.tabs.length ?? 0,
76
76
  })
77
77
  if (this.tabs.size === 0 && snapshot) {
78
- const restoredTabs = restoreTabsFromWorkspace(snapshot)
78
+ const restoredTabs = restoreTabsFromProject(snapshot)
79
79
  for (const tab of restoredTabs) {
80
80
  this.tabs.set(tab.id, tab)
81
81
  }
@@ -93,7 +93,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
93
93
  existing.title = persisted.title
94
94
  existing.autoRenameStatus = persisted.autoRenameStatus
95
95
  }
96
- existing.worktreeId = persisted.worktreeId
96
+ existing.workspaceId = persisted.workspaceId
97
97
  existing.workerName = persisted.workerName
98
98
  }
99
99
  }
@@ -116,10 +116,10 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
116
116
  .map((persistedTab) => this.tabs.get(persistedTab.id))
117
117
  .filter((tab): tab is TabSession => tab !== undefined)
118
118
  // The registry — not the snapshot — is the source of truth for *which*
119
- // tabs exist. A tab spawned by a sibling CLI after this workspace's
119
+ // tabs exist. A tab spawned by a sibling CLI after this project's
120
120
  // snapshot was last persisted lives in `this.tabs` but is absent from
121
121
  // `snapshot.tabs`; ordering by the snapshot alone would silently drop it
122
- // from the attach result, so the UI would render an empty/stale workspace
122
+ // from the attach result, so the UI would render an empty/stale project
123
123
  // on switch. Append any live tab the snapshot doesn't mention (in
124
124
  // registry order) so membership stays authoritative while the snapshot
125
125
  // still supplies ordering + layout for the tabs it captured.
@@ -151,12 +151,12 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
151
151
  env?: Record<string, string>
152
152
  autoRenameStatus?: 'eligible' | 'attempted'
153
153
  /**
154
- * Worktree this tab belongs to (for UI grouping). Stored on the
154
+ * Workspace this tab belongs to (for UI grouping). Stored on the
155
155
  * TabSession so an attach-time replay surfaces it inside the right
156
- * worktree column. Optional — tabs not bound to a worktree are valid.
156
+ * workspace column. Optional — tabs not bound to a workspace are valid.
157
157
  */
158
- worktreeId?: string
159
- /** Workspace-scoped orchestration handle; does not affect PTY behavior. */
158
+ workspaceId?: string
159
+ /** Project-scoped orchestration handle; does not affect PTY behavior. */
160
160
  workerName?: string
161
161
  }): void {
162
162
  logDebug('daemon.registry.createSession', {
@@ -179,7 +179,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
179
179
  terminalModes: createDefaultTerminalModes(),
180
180
  title: options.title,
181
181
  workerName: options.workerName,
182
- worktreeId: options.worktreeId,
182
+ workspaceId: options.workspaceId,
183
183
  })
184
184
  } else {
185
185
  existing.status = 'starting'
@@ -192,7 +192,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
192
192
  existing.title = options.title
193
193
  existing.command = [options.command, ...(options.args ?? [])].join(' ')
194
194
  existing.autoRenameStatus = options.autoRenameStatus
195
- if (options.worktreeId !== undefined) existing.worktreeId = options.worktreeId
195
+ if (options.workspaceId !== undefined) existing.workspaceId = options.workspaceId
196
196
  if (options.workerName !== undefined) existing.workerName = options.workerName
197
197
  }
198
198
 
@@ -16,7 +16,7 @@ export function parseDivergenceCount(output: string): BranchDivergence | undefin
16
16
  }
17
17
  }
18
18
 
19
- // Commits a worktree branch is ahead/behind the ref it forked from. Returns
19
+ // Commits a workspace branch is ahead/behind the ref it forked from. Returns
20
20
  // undefined when the refs can't be compared (missing ref, not a repo, …) so
21
21
  // callers can simply render nothing.
22
22
  export async function getBranchDivergence(
@@ -32,8 +32,39 @@ export async function getBranchDivergence(
32
32
  return parseDivergenceCount(result.text())
33
33
  }
34
34
 
35
+ // Parses `git diff --shortstat`, e.g.
36
+ // " 7 files changed, 149 insertions(+), 629 deletions(-)". Either side may be
37
+ // absent (a pure addition reports no deletions line at all).
38
+ export function parseShortstat(output: string): { added: number; removed: number } | undefined {
39
+ const trimmed = output.trim()
40
+ if (trimmed === '') return undefined
41
+ const added = /(\d+) insertions?\(\+\)/.exec(trimmed)
42
+ const removed = /(\d+) deletions?\(-\)/.exec(trimmed)
43
+ if (!added && !removed) return undefined
44
+ return {
45
+ added: Number.parseInt(added?.[1] ?? '0', 10),
46
+ removed: Number.parseInt(removed?.[1] ?? '0', 10),
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Lines a workspace has changed since it forked, measured from the merge base
52
+ * to the *working tree* — so commits, staged and unstaged work all count. That
53
+ * is the number the sidebar shows, and it is what a squash-move would land.
54
+ */
55
+ export async function getWorkspaceDiffStat(
56
+ workspacePath: string,
57
+ baseRef: string
58
+ ): Promise<{ added: number; removed: number } | undefined> {
59
+ const mergeBase = await getMergeBase(workspacePath, baseRef)
60
+ if (mergeBase == null) return undefined
61
+ const result = await $`git -C ${workspacePath} diff --shortstat ${mergeBase}`.quiet().nothrow()
62
+ if (result.exitCode !== 0) return undefined
63
+ return parseShortstat(result.text())
64
+ }
65
+
35
66
  // Fork point: the most recent commit shared by baseRef and ref. Diffing the
36
- // working tree against this shows everything the worktree changed since it
67
+ // working tree against this shows everything the workspace changed since it
37
68
  // branched off (commits + staged + unstaged) — i.e. what a squash-move lands.
38
69
  export async function getMergeBase(
39
70
  repoPath: string,
@@ -13,7 +13,7 @@ interface Options {
13
13
  enabled: boolean
14
14
  projectPath: string | undefined
15
15
  headOffset: number
16
- // When set, diff the working tree against this ref (worktree fork point for
16
+ // When set, diff the working tree against this ref (workspace fork point for
17
17
  // review-vs-base) instead of HEAD/HEAD~N.
18
18
  compareRef?: string
19
19
  }
@@ -199,7 +199,7 @@ async function annotateUntrackedCounts(cwd: string, files: GitFileEntry[]): Prom
199
199
 
200
200
  interface CollectOptions {
201
201
  headOffset?: number
202
- // Explicit ref to diff the working tree against (e.g. a worktree's fork
202
+ // Explicit ref to diff the working tree against (e.g. a workspace's fork
203
203
  // point). Takes precedence over headOffset when set.
204
204
  compareRef?: string
205
205
  }
@@ -266,7 +266,7 @@ async function collectAgainstHistorical(
266
266
  }
267
267
 
268
268
  // Diffs the working tree against an arbitrary ref (a HEAD~N commit, or a
269
- // worktree's fork point for review-vs-base), surfacing changed files as a
269
+ // workspace's fork point for review-vs-base), surfacing changed files as a
270
270
  // single `historical` section plus any untracked files.
271
271
  async function collectAgainstRef(cwd: string, ref: string): Promise<GitCollectResult> {
272
272
  const [statusResult, nameStatus, numstat, untrackedStatus] = await Promise.all([
@@ -1,6 +1,6 @@
1
1
  import { $ } from 'bun'
2
2
 
3
- export interface MoveWorktreeOptions {
3
+ export interface MoveWorkspaceOptions {
4
4
  sourcePath: string
5
5
  sourceBranch: string
6
6
  targetPath: string
@@ -10,7 +10,7 @@ export interface MoveWorktreeOptions {
10
10
  keepConflicts?: boolean
11
11
  }
12
12
 
13
- export type MoveWorktreeResult =
13
+ export type MoveWorkspaceResult =
14
14
  | { kind: 'ok'; filesChanged: number; stashedTarget: boolean }
15
15
  /** Target dirty/untracked files would be overwritten by the move; nothing was touched. */
16
16
  | { kind: 'needs-stash'; files: string[] }
@@ -76,8 +76,8 @@ function parseOverwrittenFiles(output: string): string[] {
76
76
  return files
77
77
  }
78
78
 
79
- // Squashes everything a source worktree changed since its fork point into the
80
- // target worktree's working tree (staged, uncommitted) — committed work plus
79
+ // Squashes everything a source workspace changed since its fork point into the
80
+ // target workspace's working tree (staged, uncommitted) — committed work plus
81
81
  // any uncommitted/untracked changes. The source is left exactly as it was; the
82
82
  // caller decides whether to delete it. The target may be dirty as long as its
83
83
  // local changes don't overlap the incoming ones: on overlap the move stops
@@ -85,7 +85,7 @@ function parseOverwrittenFiles(output: string): string[] {
85
85
  // conflict nothing is kept — target reset, source restored — unless
86
86
  // keepConflicts is set, in which case the conflicted squash state is left in
87
87
  // the target for manual resolution.
88
- export async function moveWorktree(opts: MoveWorktreeOptions): Promise<MoveWorktreeResult> {
88
+ export async function moveWorkspace(opts: MoveWorkspaceOptions): Promise<MoveWorkspaceResult> {
89
89
  const { sourceBranch, sourcePath, targetPath } = opts
90
90
  try {
91
91
  const headResult = await $`git -C ${sourcePath} rev-parse HEAD`.quiet().nothrow()
@@ -213,7 +213,7 @@ export async function collectPrStatus(cwd: string): Promise<PrStatusResult> {
213
213
  if (!result.ok) {
214
214
  // `gh` exits non-zero for every "there is simply nothing to show" case too:
215
215
  // no PR on the branch, no GitHub remote, or a directory that isn't a repo
216
- // at all (aimux sessions can point anywhere). None of those is an error.
216
+ // at all (aimux projects can point anywhere). None of those is an error.
217
217
  const stderr = result.stderr.toLowerCase()
218
218
  if (NOT_AN_ERROR.some((needle) => stderr.includes(needle))) return { kind: 'no-pr' }
219
219
  return { kind: 'error', message: (result.error ?? 'gh failed').slice(0, 200) }
@@ -79,7 +79,7 @@ const cache = new Map<string, CacheEntry>()
79
79
  /**
80
80
  * Cached variant. The cache key is `projectPath` + `maxDepth`: a depth change
81
81
  * invalidates the entry. The cache persists until `invalidateRepoCache()` or
82
- * process exit — discovery is one-shot per session per depth setting.
82
+ * process exit — discovery is one-shot per project per depth setting.
83
83
  */
84
84
  export async function discoverRepos(projectPath: string, maxDepth = 1): Promise<DiscoveredRepo[]> {
85
85
  const existing = cache.get(projectPath)
@@ -5,7 +5,7 @@ import { dispatchGlobal } from '../state/dispatch-ref'
5
5
  import { discoverRepos } from './repo-discovery'
6
6
 
7
7
  /**
8
- * Discover nested git repos inside `projectPath` once per session and push
8
+ * Discover nested git repos inside `projectPath` once per project and push
9
9
  * the result into state. No-op when the multi-repo config flag is off.
10
10
  */
11
11
  export function useRepoDiscovery(projectPath: string | undefined): void {
@@ -0,0 +1,54 @@
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 workspace on right now".
10
+ // Polls every known workspace's path and dispatches update-workspace-record
11
+ // when the live branch differs from the stored one. Components read
12
+ // `workspace.branch` from state — no per-component polling, no flickers.
13
+ //
14
+ // Runs once when enabled; reads projects from the store on each tick so
15
+ // project updates do NOT re-create the effect (which would otherwise feedback
16
+ // off our own dispatches).
17
+ export function useWorkspaceBranchPolling(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 projects = appStore.getState().projects
26
+ await Promise.all(
27
+ projects.flatMap((project) =>
28
+ (project.workspaces ?? []).map(async (workspace) => {
29
+ if (workspace.path == null || workspace.path === '') return
30
+ const branch = await getCurrentBranch(workspace.path)
31
+ if (cancelled) return
32
+ if (branch != null && branch !== workspace.branch) {
33
+ dispatchGlobal({
34
+ patch: { branch },
35
+ projectId: project.id,
36
+ type: 'update-workspace-record',
37
+ workspaceId: workspace.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
+ }
@@ -0,0 +1,65 @@
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, getWorkspaceDiffStat } from './divergence'
8
+
9
+ const INTERVAL_MS = 4000
10
+
11
+ // Polls per-workspace base divergence for the current project while enabled and
12
+ // dispatches it into workspaceDivergence. Only workspaces that record a baseRef
13
+ // (the aimux-created ones) are measured; the primary and externally-discovered
14
+ // workspaces have no recorded base and are left out.
15
+ export function useWorkspaceDivergencePolling(enabled: boolean): void {
16
+ const currentProjectId = useAppStore((s) => s.currentProjectId)
17
+ const projects = useAppStore((s) => s.projects)
18
+
19
+ useEffect(() => {
20
+ if (!enabled) return
21
+ const project =
22
+ currentProjectId != null && currentProjectId !== ''
23
+ ? projects.find((s) => s.id === currentProjectId)
24
+ : undefined
25
+ const targets = (project?.workspaces ?? []).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 (workspace) => {
36
+ const base = workspace.baseRef
37
+ const branch = workspace.branch
38
+ if (base == null || branch == null) return null
39
+ // Commits come from the repo root (comparing two refs); lines come
40
+ // from the workspace itself, so uncommitted work is counted too.
41
+ const [divergence, stat] = await Promise.all([
42
+ getBranchDivergence(workspace.repoRoot, base, branch),
43
+ getWorkspaceDiffStat(workspace.path, base),
44
+ ])
45
+ if (divergence == null) return null
46
+ return [workspace.id, { ...divergence, ...stat }] as const
47
+ })
48
+ )
49
+ if (cancelled) return
50
+ const next: Record<string, BranchDivergence> = {}
51
+ for (const entry of entries) {
52
+ if (entry != null) next[entry[0]] = entry[1]
53
+ }
54
+ dispatchGlobal({ divergence: next, type: 'set-workspace-divergence' })
55
+ timer = setTimeout(() => void tick(), INTERVAL_MS)
56
+ }
57
+
58
+ void tick()
59
+
60
+ return () => {
61
+ cancelled = true
62
+ if (timer != null) clearTimeout(timer)
63
+ }
64
+ }, [enabled, currentProjectId, projects])
65
+ }
@@ -69,6 +69,35 @@ export async function resolveGitRef(repoPath: string, ref: string): Promise<stri
69
69
  return result.text().trim() || undefined
70
70
  }
71
71
 
72
+ // The branch a new workspace forks from unless the user picks another base.
73
+ // `origin/HEAD` is the only authoritative answer; it is missing on repos cloned
74
+ // with --no-checkout or created locally, hence the name probes behind it.
75
+ export async function getDefaultBranch(repoPath: string): Promise<string | undefined> {
76
+ const head = await $`git -C ${repoPath} symbolic-ref --short refs/remotes/origin/HEAD`
77
+ .quiet()
78
+ .nothrow()
79
+ if (head.exitCode === 0) {
80
+ const ref = head.text().trim()
81
+ const short = ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref
82
+ if (short !== '') return short
83
+ }
84
+ const locals = await listLocalBranches(repoPath)
85
+ return ['main', 'master'].find((name) => locals.includes(name))
86
+ }
87
+
88
+ // Rename a local branch. Returns false (without throwing) when git refuses —
89
+ // the target name already exists, most likely — so a failed rename leaves the
90
+ // workspace on the branch it was created with instead of losing it.
91
+ export async function renameGitBranch(
92
+ repoPath: string,
93
+ from: string,
94
+ to: string
95
+ ): Promise<boolean> {
96
+ if (from === '' || to === '' || from === to) return false
97
+ const result = await $`git -C ${repoPath} branch -m ${from} ${to}`.quiet().nothrow()
98
+ return result.exitCode === 0
99
+ }
100
+
72
101
  export async function createGitWorktree({
73
102
  baseRef,
74
103
  branchName,
package/src/index.tsx CHANGED
@@ -5,7 +5,7 @@
5
5
  const command = process.argv[2]
6
6
 
7
7
  // Shell completion. First branch and cheapest: it loads the CLI registry and
8
- // nothing else — no daemon client, no session backend, no renderer.
8
+ // nothing else — no daemon client, no project backend, no renderer.
9
9
  if (command === '__complete') {
10
10
  const { runComplete } = await import('./cli/completion/entry')
11
11
  process.exit(await runComplete(process.argv.slice(3)))
@@ -19,7 +19,7 @@ if (command === 'completion') {
19
19
  // CLI control plane (docs/reference/cli.md). Branch BEFORE the UI bootstrap so
20
20
  // `aimux tab list` from a non-TTY shell never spins up the React renderer.
21
21
  // Dynamic import keeps the CLI code out of the UI's cold-start cost.
22
- const CLI_GROUPS = new Set(['tab', 'workspace', 'worktree', 'worker'])
22
+ const CLI_GROUPS = new Set(['tab', 'project', 'workspace', 'worker'])
23
23
  if (typeof command === 'string' && CLI_GROUPS.has(command)) {
24
24
  const { runCli } = await import('./cli')
25
25
  process.exit(await runCli(process.argv.slice(2)))
@@ -96,7 +96,7 @@ const [
96
96
  import('./session-backend/bootstrap'),
97
97
  import('./cli/completion/install'),
98
98
  ])
99
- const resolvedConfig = await loadUserConfig()
99
+ const { resolved: resolvedConfig, user: userConfig } = await loadUserConfig()
100
100
 
101
101
  // First launch (and after every upgrade): drop the shell completion script in
102
102
  // the conventional location for $SHELL. Silent and best-effort — it writes one
@@ -154,4 +154,4 @@ const backend = await createSessionBackend({
154
154
  })
155
155
  logDebug('index.backendReady', { backend: backend.constructor.name, runtimeProfile })
156
156
 
157
- root.render(<App backend={backend} resolvedConfig={resolvedConfig} />)
157
+ root.render(<App backend={backend} resolvedConfig={resolvedConfig} userConfig={userConfig} />)
@@ -11,15 +11,17 @@ export const HELP_MODE_LABELS: { modeId: ModeId; label: string }[] = [
11
11
  { label: 'Navigation', modeId: 'navigation' },
12
12
  { label: 'Terminal input', modeId: 'terminal-input' },
13
13
  { label: 'Git mode', modeId: 'git-mode' },
14
+ { label: 'Settings', modeId: 'settings' },
14
15
  { label: 'Git commit', modeId: 'modal.git-commit' },
15
16
  { label: 'New tab', modeId: 'modal.new-tab.command-edit' },
16
17
  { label: 'New tab — command', modeId: 'modal.new-tab.command-edit' },
17
- { label: 'Workspace picker', modeId: 'modal.session-picker.filtering' },
18
- { label: 'Workspace picker — filter', modeId: 'modal.session-picker.filtering' },
19
- { label: 'Workspace name', modeId: 'modal.session-name' },
20
- { label: 'Create workspace', modeId: 'modal.create-session' },
18
+ { label: 'Project picker', modeId: 'modal.project-picker.filtering' },
19
+ { label: 'Project picker — filter', modeId: 'modal.project-picker.filtering' },
20
+ { label: 'Project name', modeId: 'modal.project-name' },
21
+ { label: 'Create project', modeId: 'modal.create-project' },
22
+ { label: 'Create workspace', modeId: 'modal.create-workspace' },
21
23
  { label: 'Rename tab', modeId: 'modal.rename-tab' },
22
- { label: 'Rename worktree', modeId: 'modal.rename-worktree' },
24
+ { label: 'Rename workspace', modeId: 'modal.rename-workspace' },
23
25
  { label: 'Snippet picker', modeId: 'modal.snippet-picker.filtering' },
24
26
  { label: 'Snippet picker — filter', modeId: 'modal.snippet-picker.filtering' },
25
27
  { label: 'Snippet editor', modeId: 'modal.snippet-editor' },