@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,8 +1,8 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
  import { connect, type Socket } from 'node:net'
3
3
 
4
- import type { AssistantId, WorkspaceSnapshotV1 } from '../state/types'
5
- import type { ResizeOptions, SessionBackend, SessionBackendEvents } from './types'
4
+ import type { AssistantId, ProjectSnapshotV1 } from '../state/types'
5
+ import type { ProjectBackendEvents, ResizeOptions, SessionBackend } from './types'
6
6
 
7
7
  import { getIpcDaemonSocketPath } from '../daemon/runtime-paths'
8
8
  import { logDebug } from '../debug/input-log'
@@ -24,7 +24,7 @@ const IPC_REQUEST_TIMEOUT_MS = 10_000
24
24
  const RECONNECT_DELAY_MS = 250
25
25
 
26
26
  export class RemoteSessionBackend
27
- extends EventEmitter<SessionBackendEvents>
27
+ extends EventEmitter<ProjectBackendEvents>
28
28
  implements SessionBackend
29
29
  {
30
30
  private socket: Socket | null = null
@@ -38,12 +38,12 @@ export class RemoteSessionBackend
38
38
  >()
39
39
  private decoder = new MessageDecoder<ServerResponse | ServerEvent>(parseServerMessage)
40
40
  private attached = false
41
- private currentSessionId: string | null = null
41
+ private currentProjectId: string | null = null
42
42
  private attachOptions: {
43
- sessionId: string
43
+ projectId: string
44
44
  cols: number
45
45
  rows: number
46
- workspaceSnapshot?: WorkspaceSnapshotV1
46
+ projectSnapshot?: ProjectSnapshotV1
47
47
  } | null = null
48
48
  private selectedProtocolVersion: number | null = null
49
49
  private daemonCapabilities: ReadonlySet<string> = new Set()
@@ -68,7 +68,7 @@ export class RemoteSessionBackend
68
68
  }
69
69
  }
70
70
 
71
- private closeSocket(reason: string, preserveSession = false): void {
71
+ private closeSocket(reason: string, preserveProject = false): void {
72
72
  const socket = this.socket
73
73
  this.socket = null
74
74
  this.attached = false
@@ -77,8 +77,8 @@ export class RemoteSessionBackend
77
77
  this.decoder.reset()
78
78
  this.rejectPendingRequests(new Error(reason))
79
79
 
80
- if (!preserveSession) {
81
- this.currentSessionId = null
80
+ if (!preserveProject) {
81
+ this.currentProjectId = null
82
82
  this.attachOptions = null
83
83
  }
84
84
 
@@ -94,7 +94,7 @@ export class RemoteSessionBackend
94
94
  }
95
95
 
96
96
  private handleConnectionLoss(reason: string): void {
97
- logDebug('backend.remote.connectionLoss', { reason, sessionId: this.currentSessionId })
97
+ logDebug('backend.remote.connectionLoss', { projectId: this.currentProjectId, reason })
98
98
  this.closeSocket(reason, true)
99
99
  if (this.shouldReconnect && this.attachOptions) {
100
100
  void this.scheduleReconnect()
@@ -187,59 +187,71 @@ export class RemoteSessionBackend
187
187
  break
188
188
  case 'tabStatus':
189
189
  logDebug('backend.remote.tabStatus', {
190
- sessionId: message.payload.sessionId,
190
+ projectId: message.payload.projectId,
191
191
  status: message.payload.status,
192
192
  tabId: message.payload.tabId,
193
193
  })
194
- this.emit('tabActivity', message.payload.tabId, message.payload.status)
194
+ this.emit(
195
+ 'tabActivity',
196
+ message.payload.tabId,
197
+ message.payload.status,
198
+ message.payload.workspaceId
199
+ )
195
200
  break
196
201
  case 'tabTurnComplete':
202
+ this.emit(
203
+ 'tabTurnComplete',
204
+ message.payload.tabId,
205
+ message.payload.projectId,
206
+ message.payload.workspaceId
207
+ )
208
+ break
197
209
  case 'tabQuestion':
198
- // v13 orchestration signals — consumed by the headless CLI, not the UI
199
- // backend. Enumerated so the switch stays exhaustive; no UI wiring yet.
210
+ // v13 orchestration signal — consumed by the headless CLI, not the UI
211
+ // backend. Enumerated so the switch stays exhaustive.
200
212
  break
201
- case 'sessionStatus':
202
- logDebug('backend.remote.sessionStatus', {
203
- sessionId: message.payload.sessionId,
213
+ case 'projectStatus':
214
+ logDebug('backend.remote.projectStatus', {
215
+ projectId: message.payload.projectId,
204
216
  status: message.payload.status,
205
217
  })
206
- this.emit('sessionActivity', message.payload.sessionId, message.payload.status)
218
+ this.emit('projectActivity', message.payload.projectId, message.payload.status)
207
219
  break
208
220
  case 'tabAdded':
209
221
  logDebug('backend.remote.tabAdded', {
210
- sessionId: message.payload.sessionId,
222
+ projectId: message.payload.projectId,
211
223
  tabId: message.payload.tab.id,
212
224
  })
213
- this.emit('tabAdded', message.payload.sessionId, message.payload.tab)
225
+ this.emit('tabAdded', message.payload.projectId, message.payload.tab)
214
226
  break
215
227
  case 'tabMetadataUpdated':
216
- this.emit('tabMetadataUpdated', message.payload.sessionId, message.payload.tabId, {
228
+ this.emit('tabMetadataUpdated', message.payload.projectId, message.payload.tabId, {
217
229
  autoRenameStatus: message.payload.autoRenameStatus,
218
230
  title: message.payload.title,
219
231
  })
220
232
  break
221
- case 'workspaceCreateRequested':
233
+ case 'projectCreateRequested':
222
234
  this.emit(
223
- 'workspaceCreateRequested',
235
+ 'projectCreateRequested',
224
236
  message.payload.name,
225
237
  message.payload.projectPath,
226
238
  message.payload.switch === true
227
239
  )
228
240
  break
229
- case 'workspaceSwitchRequested':
230
- this.emit('workspaceSwitchRequested', message.payload.targetSessionId)
241
+ case 'projectSwitchRequested':
242
+ this.emit('projectSwitchRequested', message.payload.targetProjectId)
231
243
  break
232
- case 'workspaceCloseRequested':
233
- this.emit('workspaceCloseRequested', message.payload.targetSessionId)
244
+ case 'projectCloseRequested':
245
+ this.emit('projectCloseRequested', message.payload.targetProjectId)
234
246
  break
235
- case 'workspaceSwitched':
236
- this.emit('workspaceSwitched', message.payload.sessionId)
247
+ case 'projectSwitched':
248
+ this.emit('projectSwitched', message.payload.projectId)
237
249
  break
238
- case 'worktreeAdded':
239
- this.emit('worktreeAdded', message.payload.sessionId, message.payload.worktree)
250
+ case 'workspaceAdded':
251
+ this.emit('workspaceAdded', message.payload.projectId, message.payload.workspace)
240
252
  break
241
- case 'worktreeRemoved':
242
- this.emit('worktreeRemoved', message.payload.sessionId, message.payload.worktreeId)
253
+ case 'workspaceRemoved':
254
+ this.emit('workspaceRemoved', message.payload.projectId, message.payload.workspaceId)
243
255
  break
244
256
  }
245
257
  }
@@ -314,10 +326,10 @@ export class RemoteSessionBackend
314
326
  }
315
327
 
316
328
  private async performAttach(options: {
317
- sessionId: string
329
+ projectId: string
318
330
  cols: number
319
331
  rows: number
320
- workspaceSnapshot?: WorkspaceSnapshotV1
332
+ projectSnapshot?: ProjectSnapshotV1
321
333
  }): Promise<AttachResult> {
322
334
  await this.connectAndHandshake()
323
335
 
@@ -360,13 +372,13 @@ export class RemoteSessionBackend
360
372
  try {
361
373
  await this.performAttach(reconnectOptions)
362
374
  logDebug('backend.remote.reconnect.success', {
363
- sessionId: reconnectOptions.sessionId,
375
+ projectId: reconnectOptions.projectId,
364
376
  })
365
377
  return
366
378
  } catch (error) {
367
379
  logDebug('backend.remote.reconnect.retry', {
368
380
  error: error instanceof Error ? error.message : String(error),
369
- sessionId: reconnectOptions.sessionId,
381
+ projectId: reconnectOptions.projectId,
370
382
  })
371
383
  await Bun.sleep(RECONNECT_DELAY_MS)
372
384
  }
@@ -381,27 +393,27 @@ export class RemoteSessionBackend
381
393
  }
382
394
 
383
395
  async attach(options: {
384
- sessionId: string
396
+ projectId: string
385
397
  cols: number
386
398
  rows: number
387
- workspaceSnapshot?: WorkspaceSnapshotV1
399
+ projectSnapshot?: ProjectSnapshotV1
388
400
  }): Promise<AttachResult> {
389
401
  this.shouldReconnect = true
390
- this.currentSessionId = options.sessionId
402
+ this.currentProjectId = options.projectId
391
403
  this.attachOptions = options
392
404
 
393
405
  logDebug('backend.remote.attach.start', {
394
406
  cols: options.cols,
407
+ projectId: options.projectId,
395
408
  rows: options.rows,
396
- sessionId: options.sessionId,
397
- snapshotTabs: options.workspaceSnapshot?.tabs.length ?? 0,
409
+ snapshotTabs: options.projectSnapshot?.tabs.length ?? 0,
398
410
  socketPath: getIpcDaemonSocketPath(),
399
411
  })
400
412
 
401
413
  const result = await this.performAttach(options)
402
414
  logDebug('backend.remote.attach.success', {
403
415
  activeTabId: result.activeTabId,
404
- sessionId: options.sessionId,
416
+ projectId: options.projectId,
405
417
  tabs: result.tabs.length,
406
418
  })
407
419
 
@@ -417,7 +429,7 @@ export class RemoteSessionBackend
417
429
  cols: number
418
430
  rows: number
419
431
  cwd?: string
420
- worktreeId?: string
432
+ workspaceId?: string
421
433
  autoRenameCandidate?: boolean
422
434
  }): void {
423
435
  if (!this.attached) {
@@ -490,7 +502,7 @@ export class RemoteSessionBackend
490
502
  logDebug('backend.remote.skipResizeBeforeAttach', { cols, rows })
491
503
  return
492
504
  }
493
- logDebug('backend.remote.resize', { cols, rows, sessionId: this.currentSessionId })
505
+ logDebug('backend.remote.resize', { cols, projectId: this.currentProjectId, rows })
494
506
  this.dispatchCommand(
495
507
  { id: crypto.randomUUID(), payload: { cols, rows }, type: 'resizeClient' },
496
508
  'resizeClient'
@@ -533,17 +545,17 @@ export class RemoteSessionBackend
533
545
  this.dispatchCommand({ id: crypto.randomUUID(), payload: {}, type: 'disposeAll' }, 'disposeAll')
534
546
  }
535
547
 
536
- announceWorkspaceSwitched(sessionId: string): void {
537
- // Fire-and-forget so `handleSwitchSessionEffect` doesn't block waiting on
538
- // the daemon roundtrip. The daemon relays this as a `workspaceSwitched`
539
- // broadcast that unblocks any `aimux workspace switch --wait` CLI.
548
+ announceProjectSwitched(projectId: string): void {
549
+ // Fire-and-forget so `handleSwitchProjectEffect` doesn't block waiting on
550
+ // the daemon roundtrip. The daemon relays this as a `projectSwitched`
551
+ // broadcast that unblocks any `aimux project switch --wait` CLI.
540
552
  this.dispatchCommand(
541
553
  {
542
554
  id: crypto.randomUUID(),
543
- payload: { sessionId },
544
- type: 'announceWorkspaceSwitched',
555
+ payload: { projectId },
556
+ type: 'announceProjectSwitched',
545
557
  },
546
- 'announceWorkspaceSwitched'
558
+ 'announceProjectSwitched'
547
559
  )
548
560
  }
549
561
 
@@ -1,60 +1,71 @@
1
1
  import type { EventEmitter } from 'node:events'
2
2
 
3
3
  import type {
4
- SessionStatus,
4
+ ProjectSnapshotV1,
5
+ ProjectStatus,
5
6
  TabActivity,
6
7
  TabSession,
7
8
  TerminalModeState,
8
9
  TerminalSnapshot,
9
- WorkspaceSnapshotV1,
10
- WorktreeRecord,
10
+ WorkspaceRecord,
11
11
  } from '../state/types'
12
12
 
13
- export interface SessionBackendEvents {
13
+ export interface ProjectBackendEvents {
14
14
  render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
15
15
  exit: [tabId: string, exitCode: number]
16
16
  error: [tabId: string, message: string]
17
- sessionActivity: [sessionId: string, status: SessionStatus]
18
- tabActivity: [tabId: string, activity: TabActivity]
17
+ projectActivity: [projectId: string, status: ProjectStatus]
18
+ /**
19
+ * `workspaceId` is present when the backend knows which workspace the tab
20
+ * belongs to. The UI holds tabs for the attached project only, so this is
21
+ * what lets a *foreign* project's workspace row show a status glyph.
22
+ */
23
+ tabActivity: [tabId: string, activity: TabActivity, workspaceId?: string]
24
+ /**
25
+ * A tab's turn ended: its `idle` held for the daemon's settle window. Edge
26
+ * triggered, once per turn — the honest "the agent has finished" signal,
27
+ * unlike a raw `idle` which flickers between tool calls.
28
+ */
29
+ tabTurnComplete: [tabId: string, projectId: string, workspaceId?: string]
19
30
  /**
20
31
  * Fired when a tab was added by a sibling client (e.g. the CLI control
21
- * plane creating a tab in the same session). The UI subscribes and
32
+ * plane creating a tab in the same project). The UI subscribes and
22
33
  * dispatches `add-tab` so its store learns about the new tab before any
23
34
  * `tabRender` event lands.
24
35
  */
25
- tabAdded: [sessionId: string, tab: TabSession]
36
+ tabAdded: [projectId: string, tab: TabSession]
26
37
  tabMetadataUpdated: [
27
- sessionId: string,
38
+ projectId: string,
28
39
  tabId: string,
29
40
  patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' },
30
41
  ]
31
42
  /**
32
- * v12 workspace-lifecycle events. Fired when a CLI issued
33
- * `createWorkspace` / `switchWorkspace` / `closeWorkspace` and the daemon
43
+ * v12 project-lifecycle events. Fired when a CLI issued
44
+ * `createProject` / `switchProject` / `closeProject` and the daemon
34
45
  * relays as an event because a UI is attached. The UI reducer owns the
35
46
  * catalog write; see `backend-runtime-events.ts` for the wiring.
36
47
  */
37
- workspaceCreateRequested: [name: string, projectPath: string | undefined, doSwitch: boolean]
38
- workspaceSwitchRequested: [targetSessionId: string]
39
- workspaceCloseRequested: [targetSessionId: string]
40
- workspaceSwitched: [sessionId: string]
48
+ projectCreateRequested: [name: string, projectPath: string | undefined, doSwitch: boolean]
49
+ projectSwitchRequested: [targetProjectId: string]
50
+ projectCloseRequested: [targetProjectId: string]
51
+ projectSwitched: [projectId: string]
41
52
  /**
42
- * v12 worktree-lifecycle events. Fired when a CLI issued
43
- * `addWorktreeRecord` / `removeWorktreeRecord` and the daemon relays.
53
+ * v12 workspace-lifecycle events. Fired when a CLI issued
54
+ * `addWorkspaceRecord` / `removeWorkspaceRecord` and the daemon relays.
44
55
  */
45
- worktreeAdded: [sessionId: string, worktree: WorktreeRecord]
46
- worktreeRemoved: [sessionId: string, worktreeId: string]
56
+ workspaceAdded: [projectId: string, workspace: WorkspaceRecord]
57
+ workspaceRemoved: [projectId: string, workspaceId: string]
47
58
  }
48
59
 
49
60
  export interface ResizeOptions {
50
61
  /** When set, opentui's flush is invoked synchronously so the resize lands
51
62
  * in the same commit. Used by the chrome cascade to keep the cols/rows
52
- * state and the backend buffer in lockstep on sidebar/session-bar toggles. */
63
+ * state and the backend buffer in lockstep on sidebar/project-bar toggles. */
53
64
  sync?: boolean
54
65
  /** When true, this resize is the closed-loop measurement of the rendered
55
66
  * content box — the authoritative size for the visible viewport. The
56
67
  * `LocalSessionBackend` uses this to lift the per-tab snapshot gate that
57
- * suppresses renders between session/attach time and the first measurement,
68
+ * suppresses renders between project/attach time and the first measurement,
58
69
  * so the frontend never paints a snapshot whose row count disagrees with
59
70
  * the pane's actual height. */
60
71
  confirmedFromMeasurement?: boolean
@@ -64,19 +75,19 @@ export interface BackendAttachResult {
64
75
  tabs: TabSession[]
65
76
  activeTabId: string | null
66
77
  /**
67
- * Per-session status snapshot taken at attach time and applied
78
+ * Per-project status snapshot taken at attach time and applied
68
79
  * atomically with tab hydration to prevent an unknown-tab race on
69
- * separate `sessionActivity` events.
80
+ * separate `projectActivity` events.
70
81
  */
71
- initialSessionStatuses: { sessionId: string; status: SessionStatus }[]
82
+ initialProjectStatuses: { projectId: string; status: ProjectStatus }[]
72
83
  }
73
84
 
74
- export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
85
+ export interface SessionBackend extends EventEmitter<ProjectBackendEvents> {
75
86
  attach(options: {
76
- sessionId: string
87
+ projectId: string
77
88
  cols: number
78
89
  rows: number
79
- workspaceSnapshot?: WorkspaceSnapshotV1
90
+ projectSnapshot?: ProjectSnapshotV1
80
91
  }): Promise<BackendAttachResult | null>
81
92
  createSession(options: {
82
93
  tabId: string
@@ -87,10 +98,10 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
87
98
  cols: number
88
99
  rows: number
89
100
  cwd?: string
90
- /** Worktree the tab belongs to. Passed through to the daemon so its
101
+ /** Workspace the tab belongs to. Passed through to the daemon so its
91
102
  * registry surfaces the right grouping in `listTabs` for headless
92
103
  * consumers (CLI control plane). */
93
- worktreeId?: string
104
+ workspaceId?: string
94
105
  autoRenameCandidate?: boolean
95
106
  }): void
96
107
  write(tabId: string, input: string): void
@@ -104,9 +115,9 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
104
115
  disposeAll(): void
105
116
  destroy(keepSessions?: boolean): Promise<void> | void
106
117
  /**
107
- * v12 — the UI calls this after `handleSwitchSessionEffect` finishes so the
108
- * daemon can broadcast `workspaceSwitched` and any `aimux workspace switch
118
+ * v12 — the UI calls this after `handleSwitchProjectEffect` finishes so the
119
+ * daemon can broadcast `projectSwitched` and any `aimux project switch
109
120
  * --wait` CLI can exit. No-op on local backends (no daemon).
110
121
  */
111
- announceWorkspaceSwitched(sessionId: string): void
122
+ announceProjectSwitched(projectId: string): void
112
123
  }
@@ -0,0 +1,90 @@
1
+ import type { AIUsageToolConfig, AutoCommitConfig } from '@brimveyn/aimux-config'
2
+
3
+ import { useMemo } from 'react'
4
+
5
+ import { AUTO_COMMIT_ENABLED, AUTO_COMMIT_TIMEOUT } from './sections/automation'
6
+ import { AUTO_COMMIT_MODEL_PREFIX, SNIPPET_TRIGGER_CHAR } from './sections/commands'
7
+ import { ACTIVITY_SPRITES, HARMONIZE_CLAUDE_THEME } from './sections/experimental'
8
+ import { AI_USAGE_ENABLED, AI_USAGE_POLL_SECONDS } from './sections/status-bar'
9
+ import { useSettingsStore } from './settings-store'
10
+
11
+ /**
12
+ * The read side of the settings screen: where a consumer used to take a value
13
+ * straight off the resolved config, it takes it from here instead, and the change
14
+ * lands without a restart.
15
+ *
16
+ * One hook per consumer, all in this file, so the row ids are read in exactly one
17
+ * place — and the ids themselves come from the section that defines them.
18
+ */
19
+ export function useAutoCommitConfig(fromConfigFile: AutoCommitConfig): AutoCommitConfig {
20
+ const values = useSettingsStore((s) => s.values)
21
+ const timeoutMs = values[AUTO_COMMIT_TIMEOUT]
22
+ const models = { ...fromConfigFile.models }
23
+ for (const [id, value] of Object.entries(values)) {
24
+ if (!id.startsWith(AUTO_COMMIT_MODEL_PREFIX)) continue
25
+ // An empty row means "leave the default alone", not "use no model".
26
+ if (typeof value === 'string' && value !== '') {
27
+ models[id.slice(AUTO_COMMIT_MODEL_PREFIX.length)] = value
28
+ }
29
+ }
30
+ return {
31
+ enabled: values[AUTO_COMMIT_ENABLED] === true,
32
+ models,
33
+ timeoutMs: typeof timeoutMs === 'number' ? timeoutMs : fromConfigFile.timeoutMs,
34
+ }
35
+ }
36
+
37
+ /**
38
+ * The character that opens an inline snippet trigger. Clamped to one character,
39
+ * the same rule the config resolver applies — a two-character value would simply
40
+ * never match.
41
+ */
42
+ export function useSnippetTriggerChar(fromConfigFile: string): string {
43
+ const stored = useSettingsStore((s) => s.values[SNIPPET_TRIGGER_CHAR])
44
+ if (typeof stored === 'string' && stored.length === 1) return stored
45
+ return fromConfigFile
46
+ }
47
+
48
+ /**
49
+ * Memoized on purpose: its consumer restarts the polling service whenever this
50
+ * object changes, so a fresh identity per render would restart it per render.
51
+ */
52
+ export function useAIUsageConfig(fromConfigFile: AIUsageToolConfig | undefined): AIUsageToolConfig {
53
+ const values = useSettingsStore((s) => s.values)
54
+ const enabled = values[AI_USAGE_ENABLED] === true
55
+ const poll = values[AI_USAGE_POLL_SECONDS]
56
+ const pollSeconds = typeof poll === 'number' ? poll : fromConfigFile?.pollSeconds
57
+ return useMemo(
58
+ () => ({ ...fromConfigFile, enabled, pollSeconds }),
59
+ [enabled, fromConfigFile, pollSeconds]
60
+ )
61
+ }
62
+
63
+ /**
64
+ * The model that would write the commit message for this assistant, or undefined
65
+ * when nothing overrides the built-in default. The generating overlay names it, so
66
+ * an override set on the settings screen is visible where it applies.
67
+ */
68
+ export function useAutoCommitModel(assistant: string | undefined): string | undefined {
69
+ const stored = useSettingsStore((s) =>
70
+ assistant == null || assistant === ''
71
+ ? undefined
72
+ : s.values[`${AUTO_COMMIT_MODEL_PREFIX}${assistant}`]
73
+ )
74
+ return typeof stored === 'string' && stored !== '' ? stored : undefined
75
+ }
76
+
77
+ /**
78
+ * Whether agent state is drawn as a sprite. Read straight off the store rather
79
+ * than through the resolved config: hydration already folds in whatever
80
+ * `aimux.config.ts` declares, and this way the toggle lands without a restart.
81
+ */
82
+ export function useActivitySprites(): boolean {
83
+ return useSettingsStore((s) => s.values[ACTIVITY_SPRITES] === true)
84
+ }
85
+
86
+ /** Whether to keep Claude Code's theme in step with the active aimux theme. */
87
+ export function useHarmonizeClaudeTheme(fromConfigFile: boolean | undefined): boolean {
88
+ const stored = useSettingsStore((s) => s.values[HARMONIZE_CLAUDE_THEME])
89
+ return typeof stored === 'boolean' ? stored : fromConfigFile === true
90
+ }
@@ -0,0 +1,43 @@
1
+ import type { ProjectRecord } from '../state/types'
2
+ import type { SettingRow } from './types'
3
+
4
+ import { sectionRows, SETTING_SECTIONS } from './sections'
5
+
6
+ export interface SettingSearchHit {
7
+ row: SettingRow
8
+ sectionId: string
9
+ sectionLabel: string
10
+ /** Index within its own section, which is what the cursor is expressed in. */
11
+ rowIndex: number
12
+ }
13
+
14
+ /**
15
+ * Every setting the query matches, across every section. Ten sections is more
16
+ * than anyone will scan, and this is the only list in the app you could not type
17
+ * into to find something.
18
+ *
19
+ * Shared with `getModalOptionCount` so the picker and the reducer that moves its
20
+ * cursor count the same list — two filters would drift the moment one changed.
21
+ */
22
+ export function filterSettingRows(
23
+ projects: readonly ProjectRecord[],
24
+ query: string | null
25
+ ): SettingSearchHit[] {
26
+ const needle = (query ?? '').trim().toLowerCase()
27
+ const hits: SettingSearchHit[] = []
28
+
29
+ for (const section of SETTING_SECTIONS) {
30
+ const rows = sectionRows(section, projects)
31
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
32
+ const row = rows[rowIndex]
33
+ if (!row) continue
34
+ // The id is in the haystack deliberately: it is the key in `aimux.json`, so
35
+ // someone who saw it in that file can search for it.
36
+ const haystack = `${row.label} ${row.description ?? ''} ${row.id} ${section.label}`
37
+ if (needle !== '' && !haystack.toLowerCase().includes(needle)) continue
38
+ hits.push({ row, rowIndex, sectionId: section.id, sectionLabel: section.label })
39
+ }
40
+ }
41
+
42
+ return hits
43
+ }
@@ -0,0 +1,49 @@
1
+ import type { SettingSection } from '../types'
2
+
3
+ import { version as APP_VERSION } from '../../../package.json'
4
+ import { getConfigPath } from '../../config'
5
+ import { getProfileConfigDir, getProfileName } from '../../profile-paths'
6
+ import { settingsStore } from '../settings-store'
7
+
8
+ /**
9
+ * Read-only. Everything here answers a question you would otherwise answer by
10
+ * digging through `~/.config`: which build is running, which profile, where its
11
+ * files are, and how many settings your own config file is holding onto.
12
+ */
13
+ export const ABOUT_SECTION: SettingSection = {
14
+ id: 'about',
15
+ label: 'About',
16
+ rows: [
17
+ { id: 'about.version', kind: 'info', label: 'Version', value: () => APP_VERSION },
18
+ {
19
+ description: 'Set with --profile, or AIMUX_PROFILE in the environment.',
20
+ id: 'about.profile',
21
+ kind: 'info',
22
+ label: 'Profile',
23
+ value: () => getProfileName(),
24
+ },
25
+ {
26
+ id: 'about.configDir',
27
+ kind: 'info',
28
+ label: 'Config directory',
29
+ value: () => getProfileConfigDir(),
30
+ },
31
+ {
32
+ description: 'Settings written here land in this file.',
33
+ id: 'about.configFile',
34
+ kind: 'info',
35
+ label: 'App config',
36
+ value: () => getConfigPath(),
37
+ },
38
+ {
39
+ description: 'Those come back from your config file every launch.',
40
+ id: 'about.fromConfigFile',
41
+ kind: 'info',
42
+ label: 'Set in aimux.config.ts',
43
+ value: () => {
44
+ const count = settingsStore.getState().fromConfigFile.size
45
+ return count === 0 ? 'nothing' : `${String(count)} setting(s)`
46
+ },
47
+ },
48
+ ],
49
+ }
@@ -0,0 +1,54 @@
1
+ import type { SettingSection } from '../types'
2
+
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
+ import { getCurrentMode, getCurrentThemeId, getTransparent } from '../../ui/theme'
5
+
6
+ /**
7
+ * The theme lives in its own store, not in `AppState`. Rows over it read it
8
+ * directly and write through the side effects that already apply it live and
9
+ * persist it — every row on this screen calls `useTheme`, so a theme change
10
+ * re-renders them all and these reads never go stale.
11
+ */
12
+ export const APPEARANCE_SECTION: SettingSection = {
13
+ id: 'appearance',
14
+ label: 'Appearance',
15
+ rows: [
16
+ {
17
+ // Hands over to the picker rather than being a list of 30-odd options on
18
+ // this screen: it filters as you type and previews each theme as you move.
19
+ // It comes back here when it closes, which is what `returnTo` is for.
20
+ description: 'Filters as you type, and previews as you move.',
21
+ id: 'appearance.theme',
22
+ kind: 'action',
23
+ label: 'Theme',
24
+ run: () => {
25
+ dispatchGlobal({ returnTo: 'settings', type: 'open-theme-picker' })
26
+ runSideEffectGlobal({ action: 'open', type: 'apply-theme' })
27
+ },
28
+ value: () => `${getCurrentThemeId()} ›`,
29
+ },
30
+ {
31
+ id: 'appearance.mode',
32
+ kind: 'select',
33
+ label: 'Mode',
34
+ options: [
35
+ { label: 'dark', value: 'dark' },
36
+ { label: 'light', value: 'light' },
37
+ ],
38
+ read: () => getCurrentMode(),
39
+ storage: 'app',
40
+ // Flips rather than sets, which is safe because `writeRow` never hands a
41
+ // row the value it already has.
42
+ write: () => runSideEffectGlobal({ type: 'toggle-mode' }),
43
+ },
44
+ {
45
+ description: "Let the host terminal's own background show through.",
46
+ id: 'appearance.transparent',
47
+ kind: 'toggle',
48
+ label: 'Transparent background',
49
+ read: () => getTransparent(),
50
+ storage: 'app',
51
+ write: () => runSideEffectGlobal({ type: 'toggle-transparent' }),
52
+ },
53
+ ],
54
+ }