@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
@@ -29,12 +29,12 @@ import { lastNonBlankLine } from '../pty/last-line'
29
29
  import { createDefaultTerminalModes } from '../state/terminal-modes'
30
30
  import { TerminalManagerClient } from '../terminal-manager/manager-client'
31
31
  import {
32
- addWorktreeToCatalog,
33
- assertSessionInCatalog,
32
+ addWorkspaceToCatalog,
33
+ assertProjectInCatalog,
34
34
  bumpLastOpenedInCatalog,
35
- createWorkspaceInCatalog,
35
+ createProjectInCatalog,
36
36
  deleteFromCatalog,
37
- removeWorktreeFromCatalog,
37
+ removeWorkspaceFromCatalog,
38
38
  } from './catalog-writer'
39
39
  import {
40
40
  consumeDaemonHandoff,
@@ -54,7 +54,7 @@ import {
54
54
  } from './runtime-paths'
55
55
 
56
56
  export interface DaemonTabEntry {
57
- sessionId: string
57
+ projectId: string
58
58
  assistant: AssistantId
59
59
  command: string
60
60
  viewport: TerminalSnapshot | undefined
@@ -73,7 +73,7 @@ export interface DaemonTabEntry {
73
73
  */
74
74
  title?: string
75
75
  status?: TabStatus
76
- worktreeId?: string
76
+ workspaceId?: string
77
77
  workerName?: string
78
78
  autoRenameStatus?: 'eligible' | 'attempted'
79
79
  }
@@ -91,7 +91,7 @@ export interface DaemonTabEntry {
91
91
  */
92
92
  export function mergeTabRegistryEntry(
93
93
  registry: Map<string, DaemonTabEntry>,
94
- sessionId: string,
94
+ projectId: string,
95
95
  tabId: string,
96
96
  assistant: AssistantId,
97
97
  command: string,
@@ -100,7 +100,7 @@ export function mergeTabRegistryEntry(
100
100
  metadata?: {
101
101
  title?: string
102
102
  status?: TabStatus
103
- worktreeId?: string
103
+ workspaceId?: string
104
104
  workerName?: string
105
105
  autoRenameStatus?: 'eligible' | 'attempted'
106
106
  }
@@ -114,13 +114,13 @@ export function mergeTabRegistryEntry(
114
114
  ? 'attempted'
115
115
  : (metadata?.autoRenameStatus ?? existing?.autoRenameStatus),
116
116
  command,
117
- sessionId,
117
+ projectId,
118
118
  status: metadata?.status ?? existing?.status,
119
119
  title: preserveAttemptedMetadata ? existing.title : (metadata?.title ?? existing?.title),
120
120
  viewport,
121
121
  viewportSeq: existing?.viewportSeq ?? (viewport ? allocateSeq() : 0),
122
122
  workerName: metadata?.workerName ?? existing?.workerName,
123
- worktreeId: metadata?.worktreeId ?? existing?.worktreeId,
123
+ workspaceId: metadata?.workspaceId ?? existing?.workspaceId,
124
124
  }
125
125
  registry.set(tabId, entry)
126
126
  return entry
@@ -128,11 +128,11 @@ export function mergeTabRegistryEntry(
128
128
 
129
129
  export function findWorkerNameConflict(
130
130
  registry: ReadonlyMap<string, DaemonTabEntry>,
131
- sessionId: string,
131
+ projectId: string,
132
132
  workerName: string
133
133
  ): string | undefined {
134
134
  for (const [tabId, entry] of registry) {
135
- if (entry.sessionId === sessionId && entry.workerName === workerName) return tabId
135
+ if (entry.projectId === projectId && entry.workerName === workerName) return tabId
136
136
  }
137
137
  return undefined
138
138
  }
@@ -157,12 +157,12 @@ function sendOk(socket: Socket, id: string): void {
157
157
  send(socket, { id, payload: {}, type: 'ok' })
158
158
  }
159
159
 
160
- function requireSession(socket: Socket, attachedSessions: Map<Socket, string>): string {
161
- const sessionId = attachedSessions.get(socket)
162
- if (!(sessionId != null && sessionId !== '')) {
163
- throw new Error('No session attached')
160
+ function requireProject(socket: Socket, attachedProjects: Map<Socket, string>): string {
161
+ const projectId = attachedProjects.get(socket)
162
+ if (!(projectId != null && projectId !== '')) {
163
+ throw new Error('No project attached')
164
164
  }
165
- return sessionId
165
+ return projectId
166
166
  }
167
167
 
168
168
  function requireNegotiatedVersion(socket: Socket, versions: Map<Socket, number>): number {
@@ -221,7 +221,7 @@ async function ensureTerminalManagerReady(manager: TerminalManagerClient): Promi
221
221
  }
222
222
 
223
223
  export async function runDaemon(): Promise<void> {
224
- const resolvedConfig = await loadUserConfig()
224
+ const { resolved: resolvedConfig } = await loadUserConfig()
225
225
  const socketPath = getIpcDaemonSocketPath()
226
226
  // A handoff file means we were spawned to take over from a predecessor
227
227
  // daemon that already drained and renamed its socket away. Consume the
@@ -248,30 +248,30 @@ export async function runDaemon(): Promise<void> {
248
248
  await ensureTerminalManagerReady(manager)
249
249
 
250
250
  const sockets = new Set<Socket>()
251
- const attachedSessions = new Map<Socket, string>()
251
+ const attachedProjects = new Map<Socket, string>()
252
252
  const negotiatedVersions = new Map<Socket, number>()
253
253
  // Sockets that attached with `thin: true` (headless CLIs). Used to
254
254
  // distinguish "a UI is attached" from "only CLIs are talking to me" when
255
- // deciding whether workspace/worktree mutations go via broadcast (UI does
255
+ // deciding whether project/workspace mutations go via broadcast (UI does
256
256
  // the write) or via catalog-writer (daemon does the write).
257
257
  const thinAttachers = new Set<Socket>()
258
258
 
259
259
  // Per-tab registry so the status-detection loop can poll every terminal
260
260
  // continuously, not just the one the UI is currently attached to.
261
261
  const tabRegistry = new Map<string, DaemonTabEntry>()
262
- // Active tab id per session, populated from attachResult and updated by
262
+ // Active tab id per project, populated from attachResult and updated by
263
263
  // setActiveTab. Surfaced by `listTabs` so a headless CLI doesn't need to
264
264
  // attach just to know which tab the UI is focused on.
265
- const sessionActiveTabIds = new Map<string, string | null>()
266
- // Last (cols, rows) the session was attached with. `createTab` with
265
+ const projectActiveTabIds = new Map<string, string | null>()
266
+ // Last (cols, rows) the project was attached with. `createTab` with
267
267
  // cols/rows = 0 falls back to this when the `createTabSizeFallback`
268
268
  // capability is in play.
269
- const sessionDimensions = new Map<string, { cols: number; rows: number }>()
269
+ const projectDimensions = new Map<string, { cols: number; rows: number }>()
270
270
  let nextViewportSeq = 1
271
271
 
272
272
  const allocateSeq = (): number => nextViewportSeq++
273
273
  const rememberTab = (
274
- sessionId: string,
274
+ projectId: string,
275
275
  tabId: string,
276
276
  assistant: AssistantId,
277
277
  command: string,
@@ -279,7 +279,7 @@ export async function runDaemon(): Promise<void> {
279
279
  metadata?: {
280
280
  title?: string
281
281
  status?: TabStatus
282
- worktreeId?: string
282
+ workspaceId?: string
283
283
  workerName?: string
284
284
  autoRenameStatus?: 'eligible' | 'attempted'
285
285
  }
@@ -287,7 +287,7 @@ export async function runDaemon(): Promise<void> {
287
287
  const before = tabRegistry.get(tabId)
288
288
  const entry = mergeTabRegistryEntry(
289
289
  tabRegistry,
290
- sessionId,
290
+ projectId,
291
291
  tabId,
292
292
  assistant,
293
293
  command,
@@ -301,8 +301,8 @@ export async function runDaemon(): Promise<void> {
301
301
  initialViewportProvided: initialViewport !== undefined,
302
302
  preservedExistingViewport:
303
303
  before?.viewport !== undefined && entry.viewport === before.viewport,
304
+ projectId,
304
305
  resultSeq: entry.viewportSeq,
305
- sessionId,
306
306
  tabId,
307
307
  })
308
308
  return entry
@@ -314,9 +314,9 @@ export async function runDaemon(): Promise<void> {
314
314
  }
315
315
  }
316
316
 
317
- const broadcastForSession = (sessionId: string, event: ServerEvent): void => {
317
+ const broadcastForProject = (projectId: string, event: ServerEvent): void => {
318
318
  for (const socket of sockets) {
319
- if (attachedSessions.get(socket) === sessionId) {
319
+ if (attachedProjects.get(socket) === projectId) {
320
320
  send(socket, event)
321
321
  }
322
322
  }
@@ -326,23 +326,23 @@ export async function runDaemon(): Promise<void> {
326
326
  // it. Older peers whose `parseServerMessage` doesn't recognise the type
327
327
  // would throw on receipt and drop the connection — MIN_VERSION stays at 10
328
328
  // for compat, so we can't rely on every attached socket being v11.
329
- const broadcastForSessionVersioned = (
330
- sessionId: string,
329
+ const broadcastForProjectVersioned = (
330
+ projectId: string,
331
331
  minVersion: number,
332
332
  event: ServerEvent
333
333
  ): void => {
334
334
  for (const socket of sockets) {
335
- if (attachedSessions.get(socket) !== sessionId) continue
335
+ if (attachedProjects.get(socket) !== projectId) continue
336
336
  const version = negotiatedVersions.get(socket)
337
337
  if (version === undefined || version < minVersion) continue
338
338
  send(socket, event)
339
339
  }
340
340
  }
341
341
 
342
- // Workspace-scope broadcast: reaches every socket that negotiated at least
343
- // `minVersion` regardless of which session it's attached to. Workspace
344
- // lifecycle events (create/switch/close) target a session that the UI may
345
- // not currently be attached to, so `broadcastForSessionVersioned` won't do.
342
+ // Project-scope broadcast: reaches every socket that negotiated at least
343
+ // `minVersion` regardless of which project it's attached to. Project
344
+ // lifecycle events (create/switch/close) target a project that the UI may
345
+ // not currently be attached to, so `broadcastForProjectVersioned` won't do.
346
346
  const broadcastAllVersioned = (minVersion: number, event: ServerEvent): void => {
347
347
  for (const socket of sockets) {
348
348
  const version = negotiatedVersions.get(socket)
@@ -361,7 +361,7 @@ export async function runDaemon(): Promise<void> {
361
361
  if (patch.autoRenameStatus !== undefined) entry.autoRenameStatus = patch.autoRenameStatus
362
362
  void (async () => {
363
363
  try {
364
- await manager.updateTabMetadata(entry.sessionId, tabId, patch)
364
+ await manager.updateTabMetadata(entry.projectId, tabId, patch)
365
365
  } catch (error) {
366
366
  logDebug('daemon.tabMetadata.managerFailed', {
367
367
  error: error instanceof Error ? error.message : String(error),
@@ -369,8 +369,8 @@ export async function runDaemon(): Promise<void> {
369
369
  })
370
370
  }
371
371
  })()
372
- broadcastForSessionVersioned(entry.sessionId, 14, {
373
- payload: { ...patch, sessionId: entry.sessionId, tabId },
372
+ broadcastForProjectVersioned(entry.projectId, 14, {
373
+ payload: { ...patch, projectId: entry.projectId, tabId },
374
374
  type: 'tabMetadataUpdated',
375
375
  })
376
376
  }
@@ -390,110 +390,114 @@ export async function runDaemon(): Promise<void> {
390
390
  updateTab: applyTabMetadata,
391
391
  })
392
392
 
393
- // Count UI attachers so workspace/worktree handlers can decide whether to
393
+ // Count UI attachers so project/workspace handlers can decide whether to
394
394
  // relay via broadcast (UI attached → its reducer owns the write) or mutate
395
395
  // the catalog directly. A "UI attacher" here is any non-thin attach — thin
396
396
  // attachers are CLIs which don't run reducers.
397
397
  const countUiAttachers = (): number => {
398
398
  let count = 0
399
399
  for (const socket of sockets) {
400
- if (attachedSessions.get(socket) === undefined) continue
400
+ if (attachedProjects.get(socket) === undefined) continue
401
401
  if (thinAttachers.has(socket)) continue
402
402
  count++
403
403
  }
404
404
  return count
405
405
  }
406
406
 
407
- manager.on('render', (sessionId, tabId, viewport, terminalModes) => {
407
+ manager.on('render', (projectId, tabId, viewport, terminalModes) => {
408
408
  const existing = tabRegistry.get(tabId)
409
409
  let newSeq: number | null = null
410
410
  if (existing) {
411
411
  existing.viewport = viewport
412
412
  existing.viewportSeq = nextViewportSeq++
413
- existing.sessionId = sessionId
413
+ existing.projectId = projectId
414
414
  newSeq = existing.viewportSeq
415
415
  }
416
416
  logDebug('daemon.manager.render', {
417
417
  attachedSocketCount: sockets.size,
418
418
  hadRegistryEntry: existing !== undefined,
419
419
  newSeq,
420
- sessionId,
420
+ projectId,
421
421
  tabId,
422
422
  viewportLines: viewport.lines.length,
423
423
  })
424
424
  const event: ServerEvent = { payload: { tabId, terminalModes, viewport }, type: 'tabRender' }
425
- broadcastForSession(sessionId, event)
425
+ broadcastForProject(projectId, event)
426
426
  })
427
- manager.on('exit', (sessionId, tabId, exitCode) => {
428
- logDebug('daemon.manager.exit', { exitCode, sessionId, tabId })
427
+ manager.on('exit', (projectId, tabId, exitCode) => {
428
+ logDebug('daemon.manager.exit', { exitCode, projectId, tabId })
429
429
  tabRegistry.delete(tabId)
430
430
  autoRename.unregister(tabId)
431
- if (sessionActiveTabIds.get(sessionId) === tabId) {
432
- sessionActiveTabIds.set(sessionId, null)
431
+ if (projectActiveTabIds.get(projectId) === tabId) {
432
+ projectActiveTabIds.set(projectId, null)
433
433
  }
434
434
  const event: ServerEvent = { payload: { exitCode, tabId }, type: 'tabExit' }
435
- broadcastForSession(sessionId, event)
435
+ broadcastForProject(projectId, event)
436
436
  })
437
- manager.on('error', (sessionId, tabId, message) => {
438
- logDebug('daemon.manager.error', { message, sessionId, tabId })
437
+ manager.on('error', (projectId, tabId, message) => {
438
+ logDebug('daemon.manager.error', { message, projectId, tabId })
439
439
  const event: ServerEvent = { payload: { message, tabId }, type: 'tabError' }
440
- broadcastForSession(sessionId, event)
440
+ broadcastForProject(projectId, event)
441
441
  })
442
442
 
443
- const listTabsForSession = (sessionId: string): LoopTabView[] => {
443
+ const listTabsForProject = (projectId: string): LoopTabView[] => {
444
444
  const result: LoopTabView[] = []
445
445
  for (const [tabId, entry] of tabRegistry) {
446
- if (entry.sessionId !== sessionId) continue
446
+ if (entry.projectId !== projectId) continue
447
447
  result.push({
448
448
  assistant: entry.assistant,
449
449
  command: entry.command,
450
450
  id: tabId,
451
451
  viewport: entry.viewport,
452
+ workspaceId: entry.workspaceId,
452
453
  })
453
454
  }
454
455
  return result
455
456
  }
456
457
 
457
458
  const statusLoop = runStatusDetectionLoop({
458
- listSessions: () => {
459
+ listProjects: () => {
459
460
  const seen = new Set<string>()
460
- for (const entry of tabRegistry.values()) seen.add(entry.sessionId)
461
+ for (const entry of tabRegistry.values()) seen.add(entry.projectId)
461
462
  return [...seen]
462
463
  },
463
- listTabs: listTabsForSession,
464
- onSessionStatus: (sessionId, status) => {
465
- logDebug('daemon.status.session', { sessionId, status })
466
- broadcastAll({ payload: { sessionId, status }, type: 'sessionStatus' })
464
+ listTabs: listTabsForProject,
465
+ onProjectStatus: (projectId, status) => {
466
+ logDebug('daemon.status.project', { projectId, status })
467
+ broadcastAll({ payload: { projectId, status }, type: 'projectStatus' })
467
468
  },
468
- onTabQuestion: (tabId, sessionId, detail) => {
469
- logDebug('daemon.status.question', { kind: detail.kind, sessionId, tabId })
469
+ onTabQuestion: (tabId, projectId, detail) => {
470
+ logDebug('daemon.status.question', { kind: detail.kind, projectId, tabId })
470
471
  // v13 / capability `questionEvents`. Same v13 send-time gate as below.
471
472
  broadcastAllVersioned(13, {
472
473
  payload: {
473
474
  kind: detail.kind,
474
475
  options: detail.options,
476
+ projectId,
475
477
  prompt: detail.prompt,
476
- sessionId,
477
478
  tabId,
478
479
  },
479
480
  type: 'tabQuestion',
480
481
  })
481
482
  },
482
- onTabStatus: (tabId, status, sessionId) => {
483
- logDebug('daemon.status.tab', { sessionId, status, tabId })
483
+ onTabStatus: (tabId, status, projectId, workspaceId) => {
484
+ logDebug('daemon.status.tab', { projectId, status, tabId })
484
485
  // Broadcast to every client. Clients silently ignore events for tabIds
485
486
  // they don't know about, so there's no UI impact — this costs one
486
487
  // extra socket write per tab per change, which is trivial, and
487
488
  // removes a race where in-flight tab events could be dropped when a
488
- // client tears down its socket to switch sessions.
489
- broadcastAll({ payload: { sessionId, status, tabId }, type: 'tabStatus' })
489
+ // client tears down its socket to switch projects.
490
+ broadcastAll({ payload: { projectId, status, tabId, workspaceId }, type: 'tabStatus' })
490
491
  },
491
- onTurnComplete: (tabId, sessionId, idleMs) => {
492
- logDebug('daemon.status.turnComplete', { idleMs, sessionId, tabId })
492
+ onTurnComplete: (tabId, projectId, idleMs, workspaceId) => {
493
+ logDebug('daemon.status.turnComplete', { idleMs, projectId, tabId })
493
494
  // v13 / capability `turnLifecycle`. Gate at send time — pre-v13 parsers
494
495
  // throw on unknown event types and would drop the connection. MIN stays
495
496
  // at 10, so we fan this only to peers that negotiated at least v13.
496
- broadcastAllVersioned(13, { payload: { idleMs, sessionId, tabId }, type: 'tabTurnComplete' })
497
+ broadcastAllVersioned(13, {
498
+ payload: { idleMs, projectId, tabId, workspaceId },
499
+ type: 'tabTurnComplete',
500
+ })
497
501
  },
498
502
  turnSettleMs: turnCompleteSettleMs(),
499
503
  })
@@ -546,7 +550,7 @@ export async function runDaemon(): Promise<void> {
546
550
  * Tell the TM whether to bother snapshotting + broadcasting. Toggled on
547
551
  * 0↔1 transitions of the client socket count: when no UI is watching, the
548
552
  * TM can skip per-chunk viewport diff/projection work entirely. The TM
549
- * flushes a fresh snapshot per session on re-enable, so reattaching gives
553
+ * flushes a fresh snapshot per project on re-enable, so reattaching gives
550
554
  * the client a current viewport.
551
555
  *
552
556
  * Fire-and-forget: failure to send isn't fatal (TM will just keep its
@@ -633,9 +637,9 @@ export async function runDaemon(): Promise<void> {
633
637
  case 'attach': {
634
638
  logDebug('daemon.request.attach.start', {
635
639
  cols: message.payload.cols,
640
+ projectId: message.payload.projectId,
636
641
  rows: message.payload.rows,
637
- sessionId: message.payload.sessionId,
638
- snapshotTabs: message.payload.workspaceSnapshot?.tabs.length ?? 0,
642
+ snapshotTabs: message.payload.projectSnapshot?.tabs.length ?? 0,
639
643
  thin: message.payload.thin === true,
640
644
  })
641
645
  const negotiatedVersion = requireNegotiatedVersion(socket, negotiatedVersions)
@@ -645,27 +649,27 @@ export async function runDaemon(): Promise<void> {
645
649
  )
646
650
  }
647
651
 
648
- attachedSessions.set(socket, message.payload.sessionId)
652
+ attachedProjects.set(socket, message.payload.projectId)
649
653
  if (message.payload.thin === true) {
650
654
  thinAttachers.add(socket)
651
655
  } else {
652
656
  thinAttachers.delete(socket)
653
657
  }
654
658
  // A thin attacher (headless CLI) does not own a viewport, so
655
- // it must not resize PTYs on a session a UI is driving. TM's
659
+ // it must not resize PTYs on a project a UI is driving. TM's
656
660
  // attachSession always calls `sessionManager.resize` on the
657
661
  // incoming dimensions, so we substitute prior dims (or a
658
662
  // safe default when none exist) before calling it. That
659
663
  // makes the resize a no-op against the current PTY size
660
- // instead of clobbering it. We also seed sessionDimensions
664
+ // instead of clobbering it. We also seed projectDimensions
661
665
  // on first-ever thin attach so `createTab`'s 0×0 fallback
662
666
  // works for headless bootstrap flows (no UI has ever
663
- // attached to this session).
667
+ // attached to this project).
664
668
  let attachCols = message.payload.cols
665
669
  let attachRows = message.payload.rows
666
670
  const isThin = message.payload.thin === true
667
671
  if (isThin) {
668
- const prior = sessionDimensions.get(message.payload.sessionId)
672
+ const prior = projectDimensions.get(message.payload.projectId)
669
673
  if (prior) {
670
674
  attachCols = prior.cols
671
675
  attachRows = prior.rows
@@ -676,32 +680,32 @@ export async function runDaemon(): Promise<void> {
676
680
  // real UI attach afterwards overwrites this.
677
681
  attachCols = 80
678
682
  attachRows = 24
679
- sessionDimensions.set(message.payload.sessionId, {
683
+ projectDimensions.set(message.payload.projectId, {
680
684
  cols: attachCols,
681
685
  rows: attachRows,
682
686
  })
683
687
  logDebug('daemon.attach.thin.seedDefaultDimensions', {
684
688
  cols: attachCols,
689
+ projectId: message.payload.projectId,
685
690
  rows: attachRows,
686
- sessionId: message.payload.sessionId,
687
691
  })
688
692
  }
689
693
  } else {
690
- sessionDimensions.set(message.payload.sessionId, {
694
+ projectDimensions.set(message.payload.projectId, {
691
695
  cols: message.payload.cols,
692
696
  rows: message.payload.rows,
693
697
  })
694
698
  }
695
699
  const attachResult = await manager.attachSession({
696
700
  cols: attachCols,
701
+ projectId: message.payload.projectId,
702
+ projectSnapshot: message.payload.projectSnapshot,
697
703
  rows: attachRows,
698
- sessionId: message.payload.sessionId,
699
- workspaceSnapshot: message.payload.workspaceSnapshot,
700
704
  })
701
- sessionActiveTabIds.set(message.payload.sessionId, attachResult.activeTabId)
705
+ projectActiveTabIds.set(message.payload.projectId, attachResult.activeTabId)
702
706
  for (const tab of attachResult.tabs) {
703
707
  const remembered = rememberTab(
704
- message.payload.sessionId,
708
+ message.payload.projectId,
705
709
  tab.id,
706
710
  tab.assistant,
707
711
  tab.command,
@@ -711,7 +715,7 @@ export async function runDaemon(): Promise<void> {
711
715
  status: tab.status,
712
716
  title: tab.title,
713
717
  workerName: tab.workerName,
714
- worktreeId: tab.worktreeId,
718
+ workspaceId: tab.workspaceId,
715
719
  }
716
720
  )
717
721
  autoRename.register({
@@ -722,14 +726,14 @@ export async function runDaemon(): Promise<void> {
722
726
  })
723
727
  }
724
728
  // Classify synchronously BEFORE sending attachResult so
725
- // each tab's activity and the full session-status snapshot
729
+ // each tab's activity and the full project-status snapshot
726
730
  // are available to embed in the reply. This makes the
727
- // client apply them atomically with `hydrate-workspace`
731
+ // client apply them atomically with `hydrate-project`
728
732
  // and closes the race where separate `tabStatus` events
729
733
  // landed before the tab existed in client state.
730
- const tabsForLoop = listTabsForSession(message.payload.sessionId)
734
+ const tabsForLoop = listTabsForProject(message.payload.projectId)
731
735
  logDebug('daemon.attach.preClassify', {
732
- sessionId: message.payload.sessionId,
736
+ projectId: message.payload.projectId,
733
737
  tabs: tabsForLoop.map((t) => ({
734
738
  assistant: t.assistant,
735
739
  hasViewport: t.viewport !== undefined,
@@ -737,7 +741,7 @@ export async function runDaemon(): Promise<void> {
737
741
  seq: tabRegistry.get(t.id)?.viewportSeq ?? null,
738
742
  })),
739
743
  })
740
- statusLoop.classifyNow(message.payload.sessionId, tabsForLoop)
744
+ statusLoop.classifyNow(message.payload.projectId, tabsForLoop)
741
745
  const tabsWithActivity = attachResult.tabs.map((tab) => {
742
746
  const metadata = tabRegistry.get(tab.id)
743
747
  return {
@@ -748,17 +752,17 @@ export async function runDaemon(): Promise<void> {
748
752
  workerName: metadata?.workerName ?? tab.workerName,
749
753
  }
750
754
  })
751
- const initialSessionStatuses = statusLoop.snapshotSessions()
755
+ const initialProjectStatuses = statusLoop.snapshotProjects()
752
756
  logDebug('daemon.attach.replay', {
753
- sessionId: message.payload.sessionId,
754
- sessions: initialSessionStatuses,
757
+ projectId: message.payload.projectId,
758
+ projects: initialProjectStatuses,
755
759
  tabs: tabsWithActivity.map((t) => ({ activity: t.activity, id: t.id })),
756
760
  })
757
761
  send(socket, {
758
762
  id: message.id,
759
763
  payload: {
760
764
  activeTabId: attachResult.activeTabId,
761
- initialSessionStatuses,
765
+ initialProjectStatuses,
762
766
  protocolVersion: negotiatedVersion,
763
767
  tabs: tabsWithActivity,
764
768
  },
@@ -766,13 +770,13 @@ export async function runDaemon(): Promise<void> {
766
770
  })
767
771
  logDebug('daemon.request.attach.success', {
768
772
  activeTabId: attachResult.activeTabId,
769
- sessionId: message.payload.sessionId,
773
+ projectId: message.payload.projectId,
770
774
  tabs: attachResult.tabs.length,
771
775
  })
772
776
  break
773
777
  }
774
778
  case 'createTab': {
775
- const sessionId = requireSession(socket, attachedSessions)
779
+ const projectId = requireProject(socket, attachedProjects)
776
780
  requireNegotiatedVersion(socket, negotiatedVersions)
777
781
  if (message.payload.workerName !== undefined) {
778
782
  if (!manager.hasCapability(MANAGER_CAPABILITY_WORKER_METADATA)) {
@@ -782,29 +786,29 @@ export async function runDaemon(): Promise<void> {
782
786
  }
783
787
  const conflictTabId = findWorkerNameConflict(
784
788
  tabRegistry,
785
- sessionId,
789
+ projectId,
786
790
  message.payload.workerName
787
791
  )
788
792
  if (conflictTabId !== undefined) {
789
793
  throw new Error(
790
- `worker name already exists in this workspace: ${message.payload.workerName} (${conflictTabId})`
794
+ `worker name already exists in this project: ${message.payload.workerName} (${conflictTabId})`
791
795
  )
792
796
  }
793
797
  }
794
798
  // Capability `createTabSizeFallback`: cols/rows = 0 means
795
- // "use the session's last attached dimensions". Headless
799
+ // "use the project's last attached dimensions". Headless
796
800
  // CLIs don't have a viewport of their own, so this lets
797
801
  // them spawn a PTY that lines up with the UI on the same
798
- // session. If we have no remembered size yet, the TM
802
+ // project. If we have no remembered size yet, the TM
799
803
  // would receive 0 and spawn a zero-sized PTY — surface a
800
804
  // clear error instead.
801
805
  let cols = message.payload.cols
802
806
  let rows = message.payload.rows
803
807
  if (cols === 0 || rows === 0) {
804
- const prior = sessionDimensions.get(sessionId)
808
+ const prior = projectDimensions.get(projectId)
805
809
  if (!prior) {
806
810
  throw new Error(
807
- 'createTab requested size fallback (cols=0 or rows=0) but no UI has attached to this session yet'
811
+ 'createTab requested size fallback (cols=0 or rows=0) but no UI has attached to this project yet'
808
812
  )
809
813
  }
810
814
  if (cols === 0) cols = prior.cols
@@ -813,8 +817,8 @@ export async function runDaemon(): Promise<void> {
813
817
  logDebug('daemon.request.createTab.start', {
814
818
  cols,
815
819
  command: message.payload.command,
820
+ projectId,
816
821
  rows,
817
- sessionId,
818
822
  sizeFallback: message.payload.cols === 0 || message.payload.rows === 0,
819
823
  tabId: message.payload.tabId,
820
824
  title: message.payload.title,
@@ -829,7 +833,7 @@ export async function runDaemon(): Promise<void> {
829
833
  message.payload.autoRenameCandidate === true
830
834
  )
831
835
  rememberTab(
832
- sessionId,
836
+ projectId,
833
837
  message.payload.tabId,
834
838
  message.payload.assistant,
835
839
  fullCommand,
@@ -839,7 +843,7 @@ export async function runDaemon(): Promise<void> {
839
843
  status: 'starting',
840
844
  title: message.payload.title,
841
845
  workerName: message.payload.workerName,
842
- worktreeId: message.payload.worktreeId,
846
+ workspaceId: message.payload.workspaceId,
843
847
  }
844
848
  )
845
849
  // Inject the hook bridge env so Claude Code's hooks can
@@ -858,8 +862,8 @@ export async function runDaemon(): Promise<void> {
858
862
  autoRenameStatus,
859
863
  cols,
860
864
  env,
865
+ projectId,
861
866
  rows,
862
- sessionId,
863
867
  })
864
868
  } catch (error) {
865
869
  // `rememberTab` must happen before the manager call so
@@ -885,32 +889,32 @@ export async function runDaemon(): Promise<void> {
885
889
  terminalModes: createDefaultTerminalModes(),
886
890
  title: message.payload.title,
887
891
  workerName: message.payload.workerName,
888
- worktreeId: message.payload.worktreeId,
892
+ workspaceId: message.payload.workspaceId,
889
893
  }
890
- broadcastForSessionVersioned(sessionId, 11, {
891
- payload: { sessionId, tab: synthesizedTab },
894
+ broadcastForProjectVersioned(projectId, 11, {
895
+ payload: { projectId, tab: synthesizedTab },
892
896
  type: 'tabAdded',
893
897
  })
894
898
  autoRename.register(synthesizedTab)
895
899
  logDebug('daemon.request.createTab.success', {
896
- sessionId,
900
+ projectId,
897
901
  tabId: message.payload.tabId,
898
902
  })
899
903
  break
900
904
  }
901
905
  case 'write': {
902
- const sessionId = requireSession(socket, attachedSessions)
906
+ const projectId = requireProject(socket, attachedProjects)
903
907
  requireNegotiatedVersion(socket, negotiatedVersions)
904
- await manager.write(sessionId, message.payload.tabId, message.payload.data)
908
+ await manager.write(projectId, message.payload.tabId, message.payload.data)
905
909
  autoRename.observeWrite(message.payload.tabId, message.payload.data)
906
910
  sendOk(socket, message.id)
907
911
  break
908
912
  }
909
913
  case 'renameTab': {
910
- const sessionId = requireSession(socket, attachedSessions)
914
+ const projectId = requireProject(socket, attachedProjects)
911
915
  requireNegotiatedVersion(socket, negotiatedVersions)
912
- if (tabRegistry.get(message.payload.tabId)?.sessionId !== sessionId) {
913
- throw new Error(`Tab not found in attached session: ${message.payload.tabId}`)
916
+ if (tabRegistry.get(message.payload.tabId)?.projectId !== projectId) {
917
+ throw new Error(`Tab not found in attached project: ${message.payload.tabId}`)
914
918
  }
915
919
  autoRename.manualRename(message.payload.tabId)
916
920
  applyTabMetadata(message.payload.tabId, {
@@ -921,21 +925,21 @@ export async function runDaemon(): Promise<void> {
921
925
  break
922
926
  }
923
927
  case 'resizeClient': {
924
- const sessionId = requireSession(socket, attachedSessions)
928
+ const projectId = requireProject(socket, attachedProjects)
925
929
  requireNegotiatedVersion(socket, negotiatedVersions)
926
- sessionDimensions.set(sessionId, {
930
+ projectDimensions.set(projectId, {
927
931
  cols: message.payload.cols,
928
932
  rows: message.payload.rows,
929
933
  })
930
- await manager.resize(sessionId, message.payload.cols, message.payload.rows)
934
+ await manager.resize(projectId, message.payload.cols, message.payload.rows)
931
935
  sendOk(socket, message.id)
932
936
  break
933
937
  }
934
938
  case 'resizeTab': {
935
- const sessionId = requireSession(socket, attachedSessions)
939
+ const projectId = requireProject(socket, attachedProjects)
936
940
  requireNegotiatedVersion(socket, negotiatedVersions)
937
941
  await manager.resizeTab(
938
- sessionId,
942
+ projectId,
939
943
  message.payload.tabId,
940
944
  message.payload.cols,
941
945
  message.payload.rows
@@ -944,49 +948,49 @@ export async function runDaemon(): Promise<void> {
944
948
  break
945
949
  }
946
950
  case 'scroll': {
947
- const sessionId = requireSession(socket, attachedSessions)
951
+ const projectId = requireProject(socket, attachedProjects)
948
952
  requireNegotiatedVersion(socket, negotiatedVersions)
949
- await manager.scroll(sessionId, message.payload.tabId, message.payload.deltaLines)
953
+ await manager.scroll(projectId, message.payload.tabId, message.payload.deltaLines)
950
954
  sendOk(socket, message.id)
951
955
  break
952
956
  }
953
957
  case 'scrollToBottom': {
954
- const sessionId = requireSession(socket, attachedSessions)
958
+ const projectId = requireProject(socket, attachedProjects)
955
959
  requireNegotiatedVersion(socket, negotiatedVersions)
956
- await manager.scrollToBottom(sessionId, message.payload.tabId)
960
+ await manager.scrollToBottom(projectId, message.payload.tabId)
957
961
  sendOk(socket, message.id)
958
962
  break
959
963
  }
960
964
  case 'setActiveTab': {
961
- const sessionId = requireSession(socket, attachedSessions)
965
+ const projectId = requireProject(socket, attachedProjects)
962
966
  requireNegotiatedVersion(socket, negotiatedVersions)
963
967
  // Update the cache AFTER the TM confirms the change — if
964
968
  // manager.setActiveTab throws (tab doesn't exist, TM
965
969
  // rejects), the cache would otherwise retain the bogus tabId
966
970
  // and a subsequent listTabs would return a stale activeTabId.
967
- await manager.setActiveTab(sessionId, message.payload.tabId)
968
- sessionActiveTabIds.set(sessionId, message.payload.tabId)
971
+ await manager.setActiveTab(projectId, message.payload.tabId)
972
+ projectActiveTabIds.set(projectId, message.payload.tabId)
969
973
  sendOk(socket, message.id)
970
974
  break
971
975
  }
972
976
  case 'closeTab': {
973
- const sessionId = requireSession(socket, attachedSessions)
977
+ const projectId = requireProject(socket, attachedProjects)
974
978
  requireNegotiatedVersion(socket, negotiatedVersions)
975
979
  tabRegistry.delete(message.payload.tabId)
976
980
  autoRename.unregister(message.payload.tabId)
977
- if (sessionActiveTabIds.get(sessionId) === message.payload.tabId) {
978
- sessionActiveTabIds.set(sessionId, null)
981
+ if (projectActiveTabIds.get(projectId) === message.payload.tabId) {
982
+ projectActiveTabIds.set(projectId, null)
979
983
  }
980
- await manager.closeTab(sessionId, message.payload.tabId)
984
+ await manager.closeTab(projectId, message.payload.tabId)
981
985
  sendOk(socket, message.id)
982
986
  break
983
987
  }
984
988
  case 'listTabs': {
985
989
  requireNegotiatedVersion(socket, negotiatedVersions)
986
- const sessionId = message.payload.sessionId
990
+ const projectId = message.payload.projectId
987
991
  const tabs: TabSessionSummary[] = []
988
992
  for (const [tabId, entry] of tabRegistry) {
989
- if (entry.sessionId !== sessionId) continue
993
+ if (entry.projectId !== projectId) continue
990
994
  tabs.push({
991
995
  activity: statusLoop.getTabStatus(tabId) ?? undefined,
992
996
  assistant: entry.assistant,
@@ -1001,33 +1005,33 @@ export async function runDaemon(): Promise<void> {
1001
1005
  status: entry.status ?? 'running',
1002
1006
  title: entry.title ?? '',
1003
1007
  workerName: entry.workerName,
1004
- worktreeId: entry.worktreeId,
1008
+ workspaceId: entry.workspaceId,
1005
1009
  })
1006
1010
  }
1007
1011
  send(socket, {
1008
1012
  id: message.id,
1009
1013
  payload: {
1010
- activeTabId: sessionActiveTabIds.get(sessionId) ?? null,
1014
+ activeTabId: projectActiveTabIds.get(projectId) ?? null,
1011
1015
  tabs,
1012
1016
  },
1013
1017
  type: 'listTabsResult',
1014
1018
  })
1015
- logDebug('daemon.request.listTabs', { sessionId, tabs: tabs.length })
1019
+ logDebug('daemon.request.listTabs', { projectId, tabs: tabs.length })
1016
1020
  break
1017
1021
  }
1018
1022
  case 'disposeAll': {
1019
- const sessionId = attachedSessions.get(socket)
1023
+ const projectId = attachedProjects.get(socket)
1020
1024
  requireNegotiatedVersion(socket, negotiatedVersions)
1021
- if (sessionId != null && sessionId !== '') {
1025
+ if (projectId != null && projectId !== '') {
1022
1026
  for (const [tabId, entry] of tabRegistry) {
1023
- if (entry.sessionId === sessionId) {
1027
+ if (entry.projectId === projectId) {
1024
1028
  autoRename.unregister(tabId)
1025
1029
  tabRegistry.delete(tabId)
1026
1030
  }
1027
1031
  }
1028
- sessionActiveTabIds.delete(sessionId)
1029
- sessionDimensions.delete(sessionId)
1030
- await manager.disposeSession(sessionId)
1032
+ projectActiveTabIds.delete(projectId)
1033
+ projectDimensions.delete(projectId)
1034
+ await manager.disposeSession(projectId)
1031
1035
  }
1032
1036
  sendOk(socket, message.id)
1033
1037
  break
@@ -1043,133 +1047,133 @@ export async function runDaemon(): Promise<void> {
1043
1047
  await handleReexecRequest(socket, message.id, message.payload.reason)
1044
1048
  break
1045
1049
  }
1046
- case 'createWorkspace': {
1050
+ case 'createProject': {
1047
1051
  requireNegotiatedVersion(socket, negotiatedVersions)
1048
1052
  const { name, projectPath, switch: doSwitch } = message.payload
1049
1053
  if (countUiAttachers() > 0) {
1050
1054
  // Relay to the UI so it can preserve the live snapshot
1051
- // of the currently-open workspace before appending the
1055
+ // of the currently-open project before appending the
1052
1056
  // new one. Fire-and-forget from the daemon's side.
1053
1057
  broadcastAllVersioned(12, {
1054
1058
  payload: { name, projectPath, switch: doSwitch },
1055
- type: 'workspaceCreateRequested',
1059
+ type: 'projectCreateRequested',
1056
1060
  })
1057
- logDebug('daemon.request.createWorkspace.relay', { name, projectPath })
1061
+ logDebug('daemon.request.createProject.relay', { name, projectPath })
1058
1062
  } else {
1059
- const created = createWorkspaceInCatalog(name, projectPath)
1063
+ const created = createProjectInCatalog(name, projectPath)
1060
1064
  if (doSwitch === true) {
1061
1065
  bumpLastOpenedInCatalog(created.id)
1062
1066
  // Mirror the UI-attached path: an ack event so a
1063
1067
  // `--wait` CLI can exit even in the headless flow.
1064
1068
  broadcastAllVersioned(12, {
1065
- payload: { sessionId: created.id },
1066
- type: 'workspaceSwitched',
1069
+ payload: { projectId: created.id },
1070
+ type: 'projectSwitched',
1067
1071
  })
1068
1072
  }
1069
- logDebug('daemon.request.createWorkspace.direct', {
1073
+ logDebug('daemon.request.createProject.direct', {
1070
1074
  name,
1071
- sessionId: created.id,
1075
+ projectId: created.id,
1072
1076
  })
1073
1077
  }
1074
1078
  sendOk(socket, message.id)
1075
1079
  break
1076
1080
  }
1077
- case 'switchWorkspace': {
1081
+ case 'switchProject': {
1078
1082
  requireNegotiatedVersion(socket, negotiatedVersions)
1079
- const { targetSessionId } = message.payload
1083
+ const { targetProjectId } = message.payload
1080
1084
  // Fail fast when the target is unknown — otherwise a UI-
1081
1085
  // attached `--wait` CLI would sit until timeout while the
1082
1086
  // UI silently drops the broadcast.
1083
- assertSessionInCatalog(targetSessionId)
1087
+ assertProjectInCatalog(targetProjectId)
1084
1088
  if (countUiAttachers() > 0) {
1085
1089
  broadcastAllVersioned(12, {
1086
- payload: { targetSessionId },
1087
- type: 'workspaceSwitchRequested',
1090
+ payload: { targetProjectId },
1091
+ type: 'projectSwitchRequested',
1088
1092
  })
1089
- logDebug('daemon.request.switchWorkspace.relay', { targetSessionId })
1093
+ logDebug('daemon.request.switchProject.relay', { targetProjectId })
1090
1094
  } else {
1091
- bumpLastOpenedInCatalog(targetSessionId)
1095
+ bumpLastOpenedInCatalog(targetProjectId)
1092
1096
  // No UI to run the switch handler, so the switch is
1093
1097
  // effectively complete once the catalog reflects it.
1094
1098
  // Emit the switched event so a --wait CLI can exit.
1095
1099
  broadcastAllVersioned(12, {
1096
- payload: { sessionId: targetSessionId },
1097
- type: 'workspaceSwitched',
1100
+ payload: { projectId: targetProjectId },
1101
+ type: 'projectSwitched',
1098
1102
  })
1099
- logDebug('daemon.request.switchWorkspace.direct', { targetSessionId })
1103
+ logDebug('daemon.request.switchProject.direct', { targetProjectId })
1100
1104
  }
1101
1105
  sendOk(socket, message.id)
1102
1106
  break
1103
1107
  }
1104
- case 'closeWorkspace': {
1108
+ case 'closeProject': {
1105
1109
  requireNegotiatedVersion(socket, negotiatedVersions)
1106
- const { targetSessionId } = message.payload
1107
- assertSessionInCatalog(targetSessionId)
1110
+ const { targetProjectId } = message.payload
1111
+ assertProjectInCatalog(targetProjectId)
1108
1112
  if (countUiAttachers() > 0) {
1109
1113
  broadcastAllVersioned(12, {
1110
- payload: { targetSessionId },
1111
- type: 'workspaceCloseRequested',
1114
+ payload: { targetProjectId },
1115
+ type: 'projectCloseRequested',
1112
1116
  })
1113
- logDebug('daemon.request.closeWorkspace.relay', { targetSessionId })
1117
+ logDebug('daemon.request.closeProject.relay', { targetProjectId })
1114
1118
  } else {
1115
- deleteFromCatalog(targetSessionId)
1116
- logDebug('daemon.request.closeWorkspace.direct', { targetSessionId })
1119
+ deleteFromCatalog(targetProjectId)
1120
+ logDebug('daemon.request.closeProject.direct', { targetProjectId })
1117
1121
  }
1118
1122
  sendOk(socket, message.id)
1119
1123
  break
1120
1124
  }
1121
- case 'announceWorkspaceSwitched': {
1125
+ case 'announceProjectSwitched': {
1122
1126
  requireNegotiatedVersion(socket, negotiatedVersions)
1123
1127
  // UI-emitted acknowledgement. Relay so any --wait CLI can
1124
1128
  // exit. We don't validate that the announcement matches
1125
1129
  // an outstanding request — a UI can announce a switch that
1126
1130
  // happened via any means (menu, keybind, CLI).
1127
1131
  broadcastAllVersioned(12, {
1128
- payload: { sessionId: message.payload.sessionId },
1129
- type: 'workspaceSwitched',
1132
+ payload: { projectId: message.payload.projectId },
1133
+ type: 'projectSwitched',
1130
1134
  })
1131
1135
  sendOk(socket, message.id)
1132
1136
  break
1133
1137
  }
1134
- case 'addWorktreeRecord': {
1138
+ case 'addWorkspaceRecord': {
1135
1139
  requireNegotiatedVersion(socket, negotiatedVersions)
1136
- const { sessionId: targetSessionId, worktree } = message.payload
1140
+ const { projectId: targetProjectId, workspace } = message.payload
1137
1141
  if (countUiAttachers() > 0) {
1138
1142
  broadcastAllVersioned(12, {
1139
- payload: { sessionId: targetSessionId, worktree },
1140
- type: 'worktreeAdded',
1143
+ payload: { projectId: targetProjectId, workspace },
1144
+ type: 'workspaceAdded',
1141
1145
  })
1142
- logDebug('daemon.request.addWorktreeRecord.relay', {
1143
- sessionId: targetSessionId,
1144
- worktreeId: worktree.id,
1146
+ logDebug('daemon.request.addWorkspaceRecord.relay', {
1147
+ projectId: targetProjectId,
1148
+ workspaceId: workspace.id,
1145
1149
  })
1146
1150
  } else {
1147
- addWorktreeToCatalog(targetSessionId, worktree)
1148
- logDebug('daemon.request.addWorktreeRecord.direct', {
1149
- sessionId: targetSessionId,
1150
- worktreeId: worktree.id,
1151
+ addWorkspaceToCatalog(targetProjectId, workspace)
1152
+ logDebug('daemon.request.addWorkspaceRecord.direct', {
1153
+ projectId: targetProjectId,
1154
+ workspaceId: workspace.id,
1151
1155
  })
1152
1156
  }
1153
1157
  sendOk(socket, message.id)
1154
1158
  break
1155
1159
  }
1156
- case 'removeWorktreeRecord': {
1160
+ case 'removeWorkspaceRecord': {
1157
1161
  requireNegotiatedVersion(socket, negotiatedVersions)
1158
- const { sessionId: targetSessionId, worktreeId } = message.payload
1162
+ const { projectId: targetProjectId, workspaceId } = message.payload
1159
1163
  if (countUiAttachers() > 0) {
1160
1164
  broadcastAllVersioned(12, {
1161
- payload: { sessionId: targetSessionId, worktreeId },
1162
- type: 'worktreeRemoved',
1165
+ payload: { projectId: targetProjectId, workspaceId },
1166
+ type: 'workspaceRemoved',
1163
1167
  })
1164
- logDebug('daemon.request.removeWorktreeRecord.relay', {
1165
- sessionId: targetSessionId,
1166
- worktreeId,
1168
+ logDebug('daemon.request.removeWorkspaceRecord.relay', {
1169
+ projectId: targetProjectId,
1170
+ workspaceId,
1167
1171
  })
1168
1172
  } else {
1169
- removeWorktreeFromCatalog(targetSessionId, worktreeId)
1170
- logDebug('daemon.request.removeWorktreeRecord.direct', {
1171
- sessionId: targetSessionId,
1172
- worktreeId,
1173
+ removeWorkspaceFromCatalog(targetProjectId, workspaceId)
1174
+ logDebug('daemon.request.removeWorkspaceRecord.direct', {
1175
+ projectId: targetProjectId,
1176
+ workspaceId,
1173
1177
  })
1174
1178
  }
1175
1179
  sendOk(socket, message.id)
@@ -1196,17 +1200,17 @@ export async function runDaemon(): Promise<void> {
1196
1200
  })
1197
1201
 
1198
1202
  socket.on('close', () => {
1199
- logDebug('daemon.client.close', { sessionId: attachedSessions.get(socket) ?? null })
1203
+ logDebug('daemon.client.close', { projectId: attachedProjects.get(socket) ?? null })
1200
1204
  sockets.delete(socket)
1201
- attachedSessions.delete(socket)
1205
+ attachedProjects.delete(socket)
1202
1206
  negotiatedVersions.delete(socket)
1203
1207
  thinAttachers.delete(socket)
1204
1208
  if (sockets.size === 0) updateTmBroadcastForClientCount(0)
1205
1209
  })
1206
1210
  socket.on('error', () => {
1207
- logDebug('daemon.client.error', { sessionId: attachedSessions.get(socket) ?? null })
1211
+ logDebug('daemon.client.error', { projectId: attachedProjects.get(socket) ?? null })
1208
1212
  sockets.delete(socket)
1209
- attachedSessions.delete(socket)
1213
+ attachedProjects.delete(socket)
1210
1214
  negotiatedVersions.delete(socket)
1211
1215
  thinAttachers.delete(socket)
1212
1216
  if (sockets.size === 0) updateTmBroadcastForClientCount(0)