@brimveyn/aimux 1.20.4 → 1.22.0

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 (233) 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 +305 -1589
  17. package/src/app-runtime/snippet-actions.ts +3 -2
  18. package/src/app-runtime/split-drag-controller.ts +4 -8
  19. package/src/app-runtime/tab-actions.ts +218 -0
  20. package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
  21. package/src/app-runtime/use-auto-commit-driver.ts +14 -13
  22. package/src/app-runtime/use-backend-runtime.ts +12 -11
  23. package/src/app-runtime/use-directory-search.ts +5 -4
  24. package/src/app-runtime/use-mouse-handlers.ts +28 -42
  25. package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
  26. package/src/app-runtime/use-renderer-bindings.ts +2 -1
  27. package/src/app-runtime/use-setup-runner.ts +96 -0
  28. package/src/app-runtime/use-terminal-resize.ts +28 -33
  29. package/src/app-runtime/workspace-actions.ts +366 -0
  30. package/src/app-runtime/workspace-activity.ts +138 -0
  31. package/src/app-runtime/workspace-naming.ts +90 -0
  32. package/src/app.tsx +102 -85
  33. package/src/assets/sounds/bell.wav +0 -0
  34. package/src/assets/sounds/ding.wav +0 -0
  35. package/src/assets/sounds/plane.wav +0 -0
  36. package/src/cli/client/daemon-client.ts +2 -2
  37. package/src/cli/client/project-resolver.ts +112 -0
  38. package/src/cli/commands/project/close.ts +32 -0
  39. package/src/cli/commands/project/create.ts +93 -0
  40. package/src/cli/commands/project/list.ts +25 -0
  41. package/src/cli/commands/project/show.ts +32 -0
  42. package/src/cli/commands/{workspace → project}/switch.ts +19 -19
  43. package/src/cli/commands/tab/await.ts +2 -2
  44. package/src/cli/commands/tab/close.ts +3 -3
  45. package/src/cli/commands/tab/create.ts +74 -73
  46. package/src/cli/commands/tab/focus.ts +3 -3
  47. package/src/cli/commands/tab/list.ts +6 -6
  48. package/src/cli/commands/tab/run.ts +2 -2
  49. package/src/cli/commands/tab/send.ts +2 -2
  50. package/src/cli/commands/tab/snapshot.ts +2 -2
  51. package/src/cli/commands/tab/tail.ts +2 -2
  52. package/src/cli/commands/tab/wait.ts +2 -2
  53. package/src/cli/commands/worker/await.ts +3 -3
  54. package/src/cli/commands/worker/doctor.ts +30 -30
  55. package/src/cli/commands/worker/list.ts +15 -15
  56. package/src/cli/commands/worker/prompt.ts +3 -3
  57. package/src/cli/commands/worker/run.ts +23 -23
  58. package/src/cli/commands/worker/shared.ts +53 -59
  59. package/src/cli/commands/worker/stop.ts +29 -29
  60. package/src/cli/commands/worker/submit.ts +3 -3
  61. package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
  62. package/src/cli/commands/workspace/create.ts +30 -68
  63. package/src/cli/commands/workspace/list.ts +52 -9
  64. package/src/cli/commands/workspace/remove.ts +81 -0
  65. package/src/cli/completion/plan.ts +2 -2
  66. package/src/cli/completion/sources.ts +18 -18
  67. package/src/cli/context.ts +13 -13
  68. package/src/cli/flags.ts +30 -10
  69. package/src/cli/index.ts +26 -16
  70. package/src/cli/output.ts +1 -1
  71. package/src/cli/registry.ts +12 -12
  72. package/src/config/loader.ts +16 -5
  73. package/src/config.ts +149 -101
  74. package/src/daemon/catalog-writer.ts +67 -67
  75. package/src/daemon/daemon.ts +205 -201
  76. package/src/daemon/session-manager.ts +39 -39
  77. package/src/daemon/session-registry.ts +14 -14
  78. package/src/git/divergence.ts +33 -2
  79. package/src/git/git-poller.ts +1 -1
  80. package/src/git/git-status.ts +2 -2
  81. package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
  82. package/src/git/pr-merge.ts +64 -0
  83. package/src/git/pr-status-poller.ts +57 -0
  84. package/src/git/pr-status.ts +227 -0
  85. package/src/git/repo-discovery.ts +1 -1
  86. package/src/git/use-repo-discovery.ts +1 -1
  87. package/src/git/workspace-branch-poller.ts +54 -0
  88. package/src/git/workspace-divergence-poller.ts +65 -0
  89. package/src/git/worktree.ts +29 -0
  90. package/src/index.tsx +11 -6
  91. package/src/input/keymap/help-entries.ts +7 -5
  92. package/src/input/modes/bridge.ts +13 -12
  93. package/src/input/modes/handlers/shared.ts +1 -1
  94. package/src/input/modes/transitions.ts +34 -21
  95. package/src/input/modes/types.ts +48 -31
  96. package/src/ipc/manager-protocol.ts +44 -44
  97. package/src/ipc/protocol.ts +192 -166
  98. package/src/platform/open-url.ts +39 -0
  99. package/src/platform/play-sound.ts +184 -0
  100. package/src/platform/project-search.ts +8 -8
  101. package/src/platform/worktree-paths.ts +8 -6
  102. package/src/pty/assistant-question-extractor.ts +1 -1
  103. package/src/pty/assistant-status-detection-loop.ts +79 -55
  104. package/src/pty/assistant-status-detector.ts +40 -15
  105. package/src/pty/command-registry.ts +81 -1
  106. package/src/restart-terminal-manager.ts +1 -1
  107. package/src/services/ai-usage/provider.ts +9 -2
  108. package/src/services/ai-usage/spawn.ts +3 -1
  109. package/src/session-backend/bootstrap.ts +4 -4
  110. package/src/session-backend/local-session-backend.ts +78 -75
  111. package/src/session-backend/remote-session-backend.ts +64 -52
  112. package/src/session-backend/types.ts +43 -32
  113. package/src/settings/live.ts +90 -0
  114. package/src/settings/search.ts +43 -0
  115. package/src/settings/sections/about.ts +49 -0
  116. package/src/settings/sections/appearance.ts +54 -0
  117. package/src/settings/sections/automation.ts +105 -0
  118. package/src/settings/sections/commands.ts +72 -0
  119. package/src/settings/sections/editor.ts +56 -0
  120. package/src/settings/sections/experimental.ts +57 -0
  121. package/src/settings/sections/git.ts +112 -0
  122. package/src/settings/sections/index.ts +95 -0
  123. package/src/settings/sections/integrations.ts +23 -0
  124. package/src/settings/sections/layout.ts +72 -0
  125. package/src/settings/sections/notifications.ts +99 -0
  126. package/src/settings/sections/setup.ts +54 -0
  127. package/src/settings/sections/status-bar.ts +63 -0
  128. package/src/settings/settings-store.ts +207 -0
  129. package/src/settings/types.ts +108 -0
  130. package/src/snippets/run-shell-var.ts +48 -22
  131. package/src/state/actions.ts +342 -0
  132. package/src/state/app-store.ts +2 -1
  133. package/src/state/bars.ts +75 -0
  134. package/src/state/dispatch-ref.ts +1 -1
  135. package/src/state/git-pane-sizing.ts +0 -9
  136. package/src/state/layout-resize.ts +2 -2
  137. package/src/state/pr-status-store.ts +39 -0
  138. package/src/state/project-catalog.ts +275 -0
  139. package/src/state/project-data.ts +143 -0
  140. package/src/state/{session-persistence.ts → project-persistence.ts} +77 -73
  141. package/src/state/project-save.ts +46 -0
  142. package/src/state/project-workspaces.ts +392 -0
  143. package/src/state/reducers/auto-commit-state.ts +11 -10
  144. package/src/state/reducers/git-commit-modal-state.ts +122 -0
  145. package/src/state/reducers/git-mode-state.ts +1 -1
  146. package/src/state/reducers/git-panel-state.ts +10 -53
  147. package/src/state/reducers/modal-state.ts +290 -681
  148. package/src/state/reducers/multi-repo-state.ts +3 -6
  149. package/src/state/reducers/project-state.ts +312 -0
  150. package/src/state/reducers/settings-state.ts +80 -0
  151. package/src/state/reducers/tab-state.ts +107 -60
  152. package/src/state/reducers/ui-state.ts +86 -15
  153. package/src/state/selectors.ts +49 -37
  154. package/src/state/store.ts +83 -57
  155. package/src/state/types.ts +261 -417
  156. package/src/state/validation.ts +14 -12
  157. package/src/terminal-manager/manager-client.ts +32 -32
  158. package/src/terminal-manager/terminal-manager.ts +24 -24
  159. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
  160. package/src/ui/components/git/git-panel.tsx +2 -2
  161. package/src/ui/components/git/git-view.tsx +12 -12
  162. package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
  163. package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
  164. package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
  165. package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
  166. package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
  167. package/src/ui/components/layout/bar.tsx +191 -0
  168. package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
  169. package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
  170. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
  171. package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
  172. package/src/ui/components/layout/status-bar.tsx +6 -2
  173. package/src/ui/components/layout/terminal-pane.tsx +34 -29
  174. package/src/ui/components/layout/top-tab-bar.tsx +48 -37
  175. package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
  176. package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
  177. package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
  178. package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
  179. package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
  180. package/src/ui/components/modals/shared/form.tsx +34 -22
  181. package/src/ui/components/modals/shared/picker.tsx +10 -10
  182. package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
  183. package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
  184. package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
  185. package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
  186. package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
  187. package/src/ui/components/primitives/input-field.tsx +7 -3
  188. package/src/ui/components/primitives/surface.tsx +3 -0
  189. package/src/ui/components/settings/row-value.tsx +74 -0
  190. package/src/ui/components/settings/settings-row.tsx +78 -0
  191. package/src/ui/components/settings/settings-view.tsx +169 -0
  192. package/src/ui/components/setup/setup-widget.tsx +152 -0
  193. package/src/ui/flash/build-labels.ts +27 -27
  194. package/src/ui/hooks/use-activity-sprite.ts +73 -0
  195. package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
  196. package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
  197. package/src/ui/root.tsx +164 -230
  198. package/src/ui/status-bar-model.ts +48 -34
  199. package/src/ui/terminal-graphics/kitty.ts +28 -2
  200. package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
  201. package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
  202. package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
  203. package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
  204. package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
  205. package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
  206. package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
  207. package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
  208. package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
  209. package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
  210. package/src/ui/terminal-graphics/sprites.ts +246 -0
  211. package/src/ui/truncate.ts +7 -0
  212. package/src/ui/widgets/registry.tsx +21 -0
  213. package/src/ui/widgets/widget-context-menu.ts +85 -0
  214. package/src/update.ts +2 -2
  215. package/src/app-runtime/session-actions.ts +0 -187
  216. package/src/cli/client/workspace-resolver.ts +0 -110
  217. package/src/cli/commands/workspace/close.ts +0 -32
  218. package/src/cli/commands/workspace/show.ts +0 -32
  219. package/src/cli/commands/worktree/create.ts +0 -55
  220. package/src/cli/commands/worktree/list.ts +0 -68
  221. package/src/cli/commands/worktree/remove.ts +0 -81
  222. package/src/git/worktree-branch-poller.ts +0 -54
  223. package/src/git/worktree-divergence-poller.ts +0 -59
  224. package/src/state/reducers/session-state.ts +0 -262
  225. package/src/state/session-catalog.ts +0 -143
  226. package/src/state/session-worktrees.ts +0 -260
  227. package/src/state/workspace-save.ts +0 -45
  228. package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
  229. package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
  230. package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
  231. package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
  232. package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
  233. /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
@@ -1,10 +1,10 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
 
3
3
  import type {
4
+ ProjectSnapshotV1,
4
5
  TabSession,
5
6
  TerminalModeState,
6
7
  TerminalSnapshot,
7
- WorkspaceSnapshotV1,
8
8
  } from '../state/types'
9
9
 
10
10
  import { logDebug } from '../debug/input-log'
@@ -12,110 +12,110 @@ import { SessionRegistry } from './session-registry'
12
12
 
13
13
  interface SessionManagerEvents {
14
14
  render: [
15
- sessionId: string,
15
+ projectId: string,
16
16
  tabId: string,
17
17
  viewport: TerminalSnapshot,
18
18
  terminalModes: TerminalModeState,
19
19
  ]
20
- exit: [sessionId: string, tabId: string, exitCode: number]
21
- error: [sessionId: string, tabId: string, message: string]
20
+ exit: [projectId: string, tabId: string, exitCode: number]
21
+ error: [projectId: string, tabId: string, message: string]
22
22
  }
23
23
 
24
24
  export class SessionManager extends EventEmitter<SessionManagerEvents> {
25
25
  private registries = new Map<string, SessionRegistry>()
26
26
 
27
- private getOrCreateRegistry(sessionId: string): SessionRegistry {
28
- const existing = this.registries.get(sessionId)
27
+ private getOrCreateRegistry(projectId: string): SessionRegistry {
28
+ const existing = this.registries.get(projectId)
29
29
  if (existing) {
30
30
  return existing
31
31
  }
32
32
 
33
33
  const registry = new SessionRegistry()
34
34
  registry.on('render', (tabId, viewport, terminalModes) => {
35
- this.emit('render', sessionId, tabId, viewport, terminalModes)
35
+ this.emit('render', projectId, tabId, viewport, terminalModes)
36
36
  })
37
37
  registry.on('exit', (tabId, exitCode) => {
38
- this.emit('exit', sessionId, tabId, exitCode)
38
+ this.emit('exit', projectId, tabId, exitCode)
39
39
  })
40
40
  registry.on('error', (tabId, message) => {
41
- this.emit('error', sessionId, tabId, message)
41
+ this.emit('error', projectId, tabId, message)
42
42
  })
43
- this.registries.set(sessionId, registry)
43
+ this.registries.set(projectId, registry)
44
44
  return registry
45
45
  }
46
46
 
47
- attachSession(sessionId: string, snapshot?: WorkspaceSnapshotV1) {
47
+ attachSession(projectId: string, snapshot?: ProjectSnapshotV1) {
48
48
  logDebug('daemon.sessionManager.attachSession', {
49
49
  hasSnapshot: !!snapshot,
50
- sessionId,
50
+ projectId,
51
51
  snapshotTabs: snapshot?.tabs.length ?? 0,
52
52
  })
53
- return this.getOrCreateRegistry(sessionId).attachFromSnapshot(snapshot)
53
+ return this.getOrCreateRegistry(projectId).attachFromSnapshot(snapshot)
54
54
  }
55
55
 
56
- createTab(sessionId: string, options: Parameters<SessionRegistry['createSession']>[0]): void {
56
+ createTab(projectId: string, options: Parameters<SessionRegistry['createSession']>[0]): void {
57
57
  logDebug('daemon.sessionManager.createTab', {
58
58
  command: options.command,
59
- sessionId,
59
+ projectId,
60
60
  tabId: options.tabId,
61
61
  title: options.title,
62
62
  })
63
- this.getOrCreateRegistry(sessionId).createSession(options)
63
+ this.getOrCreateRegistry(projectId).createSession(options)
64
64
  }
65
65
 
66
- write(sessionId: string, tabId: string, data: string): void {
67
- this.getOrCreateRegistry(sessionId).write(tabId, data)
66
+ write(projectId: string, tabId: string, data: string): void {
67
+ this.getOrCreateRegistry(projectId).write(tabId, data)
68
68
  }
69
69
 
70
70
  updateTabMetadata(
71
- sessionId: string,
71
+ projectId: string,
72
72
  tabId: string,
73
73
  patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
74
74
  ): void {
75
- this.getOrCreateRegistry(sessionId).updateTabMetadata(tabId, patch)
75
+ this.getOrCreateRegistry(projectId).updateTabMetadata(tabId, patch)
76
76
  }
77
77
 
78
- resize(sessionId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
79
- this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, options)
78
+ resize(projectId: string, cols: number, rows: number, options?: { sync?: boolean }): void {
79
+ this.getOrCreateRegistry(projectId).resizeAll(cols, rows, options)
80
80
  }
81
81
 
82
82
  resizeTab(
83
- sessionId: string,
83
+ projectId: string,
84
84
  tabId: string,
85
85
  cols: number,
86
86
  rows: number,
87
87
  options?: { sync?: boolean }
88
88
  ): void {
89
- this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, options)
89
+ this.getOrCreateRegistry(projectId).resizeTab(tabId, cols, rows, options)
90
90
  }
91
91
 
92
- scroll(sessionId: string, tabId: string, deltaLines: number): void {
93
- this.getOrCreateRegistry(sessionId).scrollViewport(tabId, deltaLines)
92
+ scroll(projectId: string, tabId: string, deltaLines: number): void {
93
+ this.getOrCreateRegistry(projectId).scrollViewport(tabId, deltaLines)
94
94
  }
95
95
 
96
- scrollToBottom(sessionId: string, tabId: string): void {
97
- this.getOrCreateRegistry(sessionId).scrollViewportToBottom(tabId)
96
+ scrollToBottom(projectId: string, tabId: string): void {
97
+ this.getOrCreateRegistry(projectId).scrollViewportToBottom(tabId)
98
98
  }
99
99
 
100
- setActiveTab(sessionId: string, tabId: string | null): void {
101
- this.getOrCreateRegistry(sessionId).setActiveTab(tabId)
100
+ setActiveTab(projectId: string, tabId: string | null): void {
101
+ this.getOrCreateRegistry(projectId).setActiveTab(tabId)
102
102
  }
103
103
 
104
- closeTab(sessionId: string, tabId: string): void {
105
- this.getOrCreateRegistry(sessionId).closeTab(tabId)
104
+ closeTab(projectId: string, tabId: string): void {
105
+ this.getOrCreateRegistry(projectId).closeTab(tabId)
106
106
  }
107
107
 
108
- disposeSession(sessionId: string): void {
108
+ disposeSession(projectId: string): void {
109
109
  logDebug('daemon.sessionManager.disposeSession', {
110
- hadRegistry: this.registries.has(sessionId),
111
- sessionId,
110
+ hadRegistry: this.registries.has(projectId),
111
+ projectId,
112
112
  })
113
- const registry = this.registries.get(sessionId)
113
+ const registry = this.registries.get(projectId)
114
114
  if (!registry) {
115
115
  return
116
116
  }
117
117
  registry.disposeAll()
118
- this.registries.delete(sessionId)
118
+ this.registries.delete(projectId)
119
119
  }
120
120
 
121
121
  disposeAll(): void {
@@ -143,7 +143,7 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
143
143
  return [...this.registries.keys()]
144
144
  }
145
145
 
146
- listTabs(sessionId: string): TabSession[] {
147
- return this.registries.get(sessionId)?.listTabs() ?? []
146
+ listTabs(projectId: string): TabSession[] {
147
+ return this.registries.get(projectId)?.listTabs() ?? []
148
148
  }
149
149
  }
@@ -1,10 +1,10 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
 
3
3
  import type {
4
+ ProjectSnapshotV1,
4
5
  TabSession,
5
6
  TerminalModeState,
6
7
  TerminalSnapshot,
7
- WorkspaceSnapshotV1,
8
8
  } from '../state/types'
9
9
 
10
10
  import { logDebug } from '../debug/input-log'
@@ -12,8 +12,8 @@ import { PtyManager } from '../pty/pty-manager'
12
12
  import {
13
13
  normalizeGroupedTabOrder,
14
14
  restoreLayoutTrees,
15
- restoreTabsFromWorkspace,
16
- } from '../state/session-persistence'
15
+ restoreTabsFromProject,
16
+ } from '../state/project-persistence'
17
17
  import { createDefaultTerminalModes } from '../state/terminal-modes'
18
18
 
19
19
  interface SessionRegistryEvents {
@@ -65,7 +65,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
65
65
  })
66
66
  }
67
67
 
68
- attachFromSnapshot(snapshot: WorkspaceSnapshotV1 | undefined): {
68
+ attachFromSnapshot(snapshot: ProjectSnapshotV1 | undefined): {
69
69
  tabs: TabSession[]
70
70
  activeTabId: string | null
71
71
  } {
@@ -75,7 +75,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
75
75
  snapshotTabs: snapshot?.tabs.length ?? 0,
76
76
  })
77
77
  if (this.tabs.size === 0 && snapshot) {
78
- const restoredTabs = restoreTabsFromWorkspace(snapshot)
78
+ const restoredTabs = restoreTabsFromProject(snapshot)
79
79
  for (const tab of restoredTabs) {
80
80
  this.tabs.set(tab.id, tab)
81
81
  }
@@ -93,7 +93,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
93
93
  existing.title = persisted.title
94
94
  existing.autoRenameStatus = persisted.autoRenameStatus
95
95
  }
96
- existing.worktreeId = persisted.worktreeId
96
+ existing.workspaceId = persisted.workspaceId
97
97
  existing.workerName = persisted.workerName
98
98
  }
99
99
  }
@@ -116,10 +116,10 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
116
116
  .map((persistedTab) => this.tabs.get(persistedTab.id))
117
117
  .filter((tab): tab is TabSession => tab !== undefined)
118
118
  // The registry — not the snapshot — is the source of truth for *which*
119
- // tabs exist. A tab spawned by a sibling CLI after this workspace's
119
+ // tabs exist. A tab spawned by a sibling CLI after this project's
120
120
  // snapshot was last persisted lives in `this.tabs` but is absent from
121
121
  // `snapshot.tabs`; ordering by the snapshot alone would silently drop it
122
- // from the attach result, so the UI would render an empty/stale workspace
122
+ // from the attach result, so the UI would render an empty/stale project
123
123
  // on switch. Append any live tab the snapshot doesn't mention (in
124
124
  // registry order) so membership stays authoritative while the snapshot
125
125
  // still supplies ordering + layout for the tabs it captured.
@@ -151,12 +151,12 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
151
151
  env?: Record<string, string>
152
152
  autoRenameStatus?: 'eligible' | 'attempted'
153
153
  /**
154
- * Worktree this tab belongs to (for UI grouping). Stored on the
154
+ * Workspace this tab belongs to (for UI grouping). Stored on the
155
155
  * TabSession so an attach-time replay surfaces it inside the right
156
- * worktree column. Optional — tabs not bound to a worktree are valid.
156
+ * workspace column. Optional — tabs not bound to a workspace are valid.
157
157
  */
158
- worktreeId?: string
159
- /** Workspace-scoped orchestration handle; does not affect PTY behavior. */
158
+ workspaceId?: string
159
+ /** Project-scoped orchestration handle; does not affect PTY behavior. */
160
160
  workerName?: string
161
161
  }): void {
162
162
  logDebug('daemon.registry.createSession', {
@@ -179,7 +179,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
179
179
  terminalModes: createDefaultTerminalModes(),
180
180
  title: options.title,
181
181
  workerName: options.workerName,
182
- worktreeId: options.worktreeId,
182
+ workspaceId: options.workspaceId,
183
183
  })
184
184
  } else {
185
185
  existing.status = 'starting'
@@ -192,7 +192,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
192
192
  existing.title = options.title
193
193
  existing.command = [options.command, ...(options.args ?? [])].join(' ')
194
194
  existing.autoRenameStatus = options.autoRenameStatus
195
- if (options.worktreeId !== undefined) existing.worktreeId = options.worktreeId
195
+ if (options.workspaceId !== undefined) existing.workspaceId = options.workspaceId
196
196
  if (options.workerName !== undefined) existing.workerName = options.workerName
197
197
  }
198
198
 
@@ -16,7 +16,7 @@ export function parseDivergenceCount(output: string): BranchDivergence | undefin
16
16
  }
17
17
  }
18
18
 
19
- // Commits a worktree branch is ahead/behind the ref it forked from. Returns
19
+ // Commits a workspace branch is ahead/behind the ref it forked from. Returns
20
20
  // undefined when the refs can't be compared (missing ref, not a repo, …) so
21
21
  // callers can simply render nothing.
22
22
  export async function getBranchDivergence(
@@ -32,8 +32,39 @@ export async function getBranchDivergence(
32
32
  return parseDivergenceCount(result.text())
33
33
  }
34
34
 
35
+ // Parses `git diff --shortstat`, e.g.
36
+ // " 7 files changed, 149 insertions(+), 629 deletions(-)". Either side may be
37
+ // absent (a pure addition reports no deletions line at all).
38
+ export function parseShortstat(output: string): { added: number; removed: number } | undefined {
39
+ const trimmed = output.trim()
40
+ if (trimmed === '') return undefined
41
+ const added = /(\d+) insertions?\(\+\)/.exec(trimmed)
42
+ const removed = /(\d+) deletions?\(-\)/.exec(trimmed)
43
+ if (!added && !removed) return undefined
44
+ return {
45
+ added: Number.parseInt(added?.[1] ?? '0', 10),
46
+ removed: Number.parseInt(removed?.[1] ?? '0', 10),
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Lines a workspace has changed since it forked, measured from the merge base
52
+ * to the *working tree* — so commits, staged and unstaged work all count. That
53
+ * is the number the sidebar shows, and it is what a squash-move would land.
54
+ */
55
+ export async function getWorkspaceDiffStat(
56
+ workspacePath: string,
57
+ baseRef: string
58
+ ): Promise<{ added: number; removed: number } | undefined> {
59
+ const mergeBase = await getMergeBase(workspacePath, baseRef)
60
+ if (mergeBase == null) return undefined
61
+ const result = await $`git -C ${workspacePath} diff --shortstat ${mergeBase}`.quiet().nothrow()
62
+ if (result.exitCode !== 0) return undefined
63
+ return parseShortstat(result.text())
64
+ }
65
+
35
66
  // Fork point: the most recent commit shared by baseRef and ref. Diffing the
36
- // working tree against this shows everything the worktree changed since it
67
+ // working tree against this shows everything the workspace changed since it
37
68
  // branched off (commits + staged + unstaged) — i.e. what a squash-move lands.
38
69
  export async function getMergeBase(
39
70
  repoPath: string,
@@ -13,7 +13,7 @@ interface Options {
13
13
  enabled: boolean
14
14
  projectPath: string | undefined
15
15
  headOffset: number
16
- // When set, diff the working tree against this ref (worktree fork point for
16
+ // When set, diff the working tree against this ref (workspace fork point for
17
17
  // review-vs-base) instead of HEAD/HEAD~N.
18
18
  compareRef?: string
19
19
  }
@@ -199,7 +199,7 @@ async function annotateUntrackedCounts(cwd: string, files: GitFileEntry[]): Prom
199
199
 
200
200
  interface CollectOptions {
201
201
  headOffset?: number
202
- // Explicit ref to diff the working tree against (e.g. a worktree's fork
202
+ // Explicit ref to diff the working tree against (e.g. a workspace's fork
203
203
  // point). Takes precedence over headOffset when set.
204
204
  compareRef?: string
205
205
  }
@@ -266,7 +266,7 @@ async function collectAgainstHistorical(
266
266
  }
267
267
 
268
268
  // Diffs the working tree against an arbitrary ref (a HEAD~N commit, or a
269
- // worktree's fork point for review-vs-base), surfacing changed files as a
269
+ // workspace's fork point for review-vs-base), surfacing changed files as a
270
270
  // single `historical` section plus any untracked files.
271
271
  async function collectAgainstRef(cwd: string, ref: string): Promise<GitCollectResult> {
272
272
  const [statusResult, nameStatus, numstat, untrackedStatus] = await Promise.all([
@@ -1,6 +1,6 @@
1
1
  import { $ } from 'bun'
2
2
 
3
- export interface MoveWorktreeOptions {
3
+ export interface MoveWorkspaceOptions {
4
4
  sourcePath: string
5
5
  sourceBranch: string
6
6
  targetPath: string
@@ -10,7 +10,7 @@ export interface MoveWorktreeOptions {
10
10
  keepConflicts?: boolean
11
11
  }
12
12
 
13
- export type MoveWorktreeResult =
13
+ export type MoveWorkspaceResult =
14
14
  | { kind: 'ok'; filesChanged: number; stashedTarget: boolean }
15
15
  /** Target dirty/untracked files would be overwritten by the move; nothing was touched. */
16
16
  | { kind: 'needs-stash'; files: string[] }
@@ -76,8 +76,8 @@ function parseOverwrittenFiles(output: string): string[] {
76
76
  return files
77
77
  }
78
78
 
79
- // Squashes everything a source worktree changed since its fork point into the
80
- // target worktree's working tree (staged, uncommitted) — committed work plus
79
+ // Squashes everything a source workspace changed since its fork point into the
80
+ // target workspace's working tree (staged, uncommitted) — committed work plus
81
81
  // any uncommitted/untracked changes. The source is left exactly as it was; the
82
82
  // caller decides whether to delete it. The target may be dirty as long as its
83
83
  // local changes don't overlap the incoming ones: on overlap the move stops
@@ -85,7 +85,7 @@ function parseOverwrittenFiles(output: string): string[] {
85
85
  // conflict nothing is kept — target reset, source restored — unless
86
86
  // keepConflicts is set, in which case the conflicted squash state is left in
87
87
  // the target for manual resolution.
88
- export async function moveWorktree(opts: MoveWorktreeOptions): Promise<MoveWorktreeResult> {
88
+ export async function moveWorkspace(opts: MoveWorkspaceOptions): Promise<MoveWorkspaceResult> {
89
89
  const { sourceBranch, sourcePath, targetPath } = opts
90
90
  try {
91
91
  const headResult = await $`git -C ${sourcePath} rev-parse HEAD`.quiet().nothrow()
@@ -0,0 +1,64 @@
1
+ import { logDebug } from '../debug/input-log'
2
+ import { runCli } from '../services/ai-usage/spawn'
3
+
4
+ export interface RepoMergeMethods {
5
+ mergeCommitAllowed: boolean
6
+ squashMergeAllowed: boolean
7
+ rebaseMergeAllowed: boolean
8
+ }
9
+
10
+ /**
11
+ * Mirrors the method GitHub pre-selects on the merge button: a merge commit
12
+ * when the repo allows one, then squash, then rebase. `gh pr merge` refuses to
13
+ * guess when several are enabled, so we have to name one explicitly.
14
+ */
15
+ export function pickMergeFlag(methods: RepoMergeMethods): string | null {
16
+ if (methods.mergeCommitAllowed) return '--merge'
17
+ if (methods.squashMergeAllowed) return '--squash'
18
+ if (methods.rebaseMergeAllowed) return '--rebase'
19
+ return null
20
+ }
21
+
22
+ export type MergeResult = { ok: true } | { ok: false; message: string }
23
+
24
+ async function repoMergeMethods(gh: string, cwd: string): Promise<RepoMergeMethods | null> {
25
+ const result = await runCli(
26
+ gh,
27
+ ['repo', 'view', '--json', 'mergeCommitAllowed,squashMergeAllowed,rebaseMergeAllowed'],
28
+ 15_000,
29
+ cwd
30
+ )
31
+ if (!result.ok) return null
32
+ try {
33
+ const raw = JSON.parse(result.stdout) as Record<string, unknown>
34
+ return {
35
+ mergeCommitAllowed: raw.mergeCommitAllowed === true,
36
+ rebaseMergeAllowed: raw.rebaseMergeAllowed === true,
37
+ squashMergeAllowed: raw.squashMergeAllowed === true,
38
+ }
39
+ } catch {
40
+ return null
41
+ }
42
+ }
43
+
44
+ export async function approveAndMergePr(cwd: string): Promise<MergeResult> {
45
+ const gh = Bun.which('gh')
46
+ if (gh === null) return { message: 'gh CLI not found', ok: false }
47
+
48
+ const methods = await repoMergeMethods(gh, cwd)
49
+ if (methods === null) return { message: 'could not read repo merge settings', ok: false }
50
+ const flag = pickMergeFlag(methods)
51
+ if (flag === null) return { message: 'repo allows no merge method', ok: false }
52
+
53
+ // GitHub rejects approving your own PR, and a missing approval is not a
54
+ // reason to skip the merge — best effort, log it, move on.
55
+ const approve = await runCli(gh, ['pr', 'review', '--approve'], 15_000, cwd)
56
+ if (!approve.ok) logDebug('git.pr.approveSkipped', { error: approve.error })
57
+
58
+ const merge = await runCli(gh, ['pr', 'merge', flag], 60_000, cwd)
59
+ if (!merge.ok) {
60
+ const detail = merge.stderr.trim().split('\n')[0] ?? 'merge failed'
61
+ return { message: detail.slice(0, 160), ok: false }
62
+ }
63
+ return { ok: true }
64
+ }
@@ -0,0 +1,57 @@
1
+ import { useEffect } from 'react'
2
+
3
+ import { prStatusStore } from '../state/pr-status-store'
4
+ import { collectPrStatus } from './pr-status'
5
+
6
+ /** A run in flight is worth watching closely; a settled one barely changes. */
7
+ const ACTIVE_INTERVAL_MS = 15_000
8
+ const IDLE_INTERVAL_MS = 60_000
9
+ const MAX_INTERVAL_MS = 120_000
10
+
11
+ interface Options {
12
+ enabled: boolean
13
+ projectPath: string | undefined
14
+ }
15
+
16
+ /** One-shot refetch, for when an action we took just invalidated the state. */
17
+ export async function refreshPrStatus(projectPath: string): Promise<void> {
18
+ prStatusStore.getState().setResult(await collectPrStatus(projectPath))
19
+ }
20
+
21
+ export function usePrStatusPolling({ enabled, projectPath }: Options): void {
22
+ useEffect(() => {
23
+ if (!enabled || !(projectPath != null && projectPath !== '')) return
24
+
25
+ prStatusStore.getState().reset()
26
+
27
+ let cancelled = false
28
+ let timer: ReturnType<typeof setTimeout> | null = null
29
+ let delay = ACTIVE_INTERVAL_MS
30
+
31
+ const schedule = () => {
32
+ if (cancelled) return
33
+ timer = setTimeout(() => void tick(), delay)
34
+ }
35
+
36
+ const tick = async () => {
37
+ const result = await collectPrStatus(projectPath)
38
+ if (cancelled) return
39
+ prStatusStore.getState().setResult(result)
40
+ if (result.kind === 'error') {
41
+ delay = Math.min(delay * 2, MAX_INTERVAL_MS)
42
+ } else if (result.kind === 'ok' && result.checks.some((c) => c.state === 'pending')) {
43
+ delay = ACTIVE_INTERVAL_MS
44
+ } else {
45
+ delay = IDLE_INTERVAL_MS
46
+ }
47
+ schedule()
48
+ }
49
+
50
+ void tick()
51
+
52
+ return () => {
53
+ cancelled = true
54
+ if (timer) clearTimeout(timer)
55
+ }
56
+ }, [enabled, projectPath])
57
+ }