@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,5 +1,6 @@
1
1
  import type { SessionBackend } from '../session-backend/types'
2
- import type { AppAction, AppState, SnippetRecord, TabSession } from '../state/types'
2
+ import type { AppAction } from '../state/actions'
3
+ import type { AppState, SnippetRecord, TabSession } from '../state/types'
3
4
 
4
5
  import { createPrefixedId } from '../platform/id'
5
6
  import { isConfigSnippetId, saveSnippetCatalog } from '../state/snippet-catalog'
@@ -40,7 +41,7 @@ export function saveSnippetEditorState(state: AppState): SnippetRecord[] | null
40
41
  return null
41
42
  }
42
43
 
43
- const snippetId = state.modal.sessionTargetId
44
+ const snippetId = state.modal.projectTargetId
44
45
  // Config-pinned snippets are sticky and read-only in the UI.
45
46
  if (snippetId != null && snippetId !== '' && isConfigSnippetId(snippetId)) {
46
47
  return null
@@ -16,8 +16,7 @@ export interface AxisDragState {
16
16
  screenStart: number
17
17
  }
18
18
 
19
- export interface AnchoredRatioDragState {
20
- anchor: 'start' | 'end'
19
+ export interface RatioDragState {
21
20
  axis: ScreenAxis
22
21
  screenStart: number
23
22
  totalSize: number
@@ -38,10 +37,7 @@ export function getAxisDeltaFromDrag(event: OtuiMouseEvent, drag: AxisDragState)
38
37
  return getScreenPosition(event, drag.axis) - drag.screenStart
39
38
  }
40
39
 
41
- export function getAnchoredRatioFromDrag(
42
- event: OtuiMouseEvent,
43
- drag: AnchoredRatioDragState
44
- ): number {
45
- const offset = getScreenPosition(event, drag.axis) - drag.screenStart
46
- return drag.anchor === 'start' ? offset / drag.totalSize : 1 - offset / drag.totalSize
40
+ /** Cursor position as a 0..1 fraction of the dragged container. */
41
+ export function getRatioFromDrag(event: OtuiMouseEvent, drag: RatioDragState): number {
42
+ return (getScreenPosition(event, drag.axis) - drag.screenStart) / drag.totalSize
47
43
  }
@@ -0,0 +1,218 @@
1
+ import type { AssistantId, TabSession } from '../state/types'
2
+ import type { SideEffectContext } from './side-effect-context'
3
+
4
+ import { logInputDebug } from '../debug/input-log'
5
+ import { createPrefixedId } from '../platform/id'
6
+ import {
7
+ getAllAssistantOptions,
8
+ getAssistantOption,
9
+ isCommandAvailable,
10
+ parseCommand,
11
+ } from '../pty/command-registry'
12
+ import { createTerminalBounds } from '../state/layout-resize'
13
+ import {
14
+ computePaneRects,
15
+ createLeaf,
16
+ getTreeForTab,
17
+ PANE_BORDER,
18
+ type SplitDirection,
19
+ splitNode,
20
+ } from '../state/layout-tree'
21
+ import { getActiveWorkspace, getCurrentProject } from '../state/project-workspaces'
22
+ import { createDefaultTerminalModes } from '../state/terminal-modes'
23
+ import { getSelectedAssistantOption } from './selection'
24
+
25
+ /**
26
+ * How long a freshly spawned assistant is given to draw something before the
27
+ * tab stops being treated as "still starting up".
28
+ */
29
+ const STARTUP_GRACE_MS = 5_000
30
+
31
+ export function confirmSplitSelection(ctx: SideEffectContext): void {
32
+ const { dispatch, state } = ctx
33
+ const option = getSelectedAssistantOption(state)
34
+ const direction = state.modal.type === 'split-picker' ? state.modal.splitDirection : 'vertical'
35
+ const customCommand = state.customCommands[option.id]
36
+ const tab = createTabSession(
37
+ option.id,
38
+ customCommand,
39
+ state.customCommands,
40
+ getActiveWorkspace(getCurrentProject(state))?.id
41
+ )
42
+ dispatch({ type: 'close-modal' })
43
+ executeSplitPane(ctx, direction, tab)
44
+ dispatch({ focusMode: 'terminal-input', type: 'set-focus-mode' })
45
+ }
46
+
47
+ function createTabId(): string {
48
+ return createPrefixedId('tab')
49
+ }
50
+
51
+ export function createTabSession(
52
+ assistant: AssistantId,
53
+ customCommand?: string,
54
+ customCommands?: Record<string, string>,
55
+ workspaceId?: string
56
+ ): TabSession {
57
+ const allOptions = getAllAssistantOptions(customCommands ?? {})
58
+ const option = allOptions.find((o) => o.id === assistant) ?? getAssistantOption(0)
59
+
60
+ return {
61
+ activity: 'idle',
62
+ assistant,
63
+ buffer: '',
64
+ command: customCommand ?? option.command,
65
+ id: createTabId(),
66
+ status: 'starting',
67
+ terminalModes: createDefaultTerminalModes(),
68
+ title: option.label,
69
+ workspaceId,
70
+ }
71
+ }
72
+
73
+ /** Everything `startTabSession` needs from the side-effect context. */
74
+ type StartTabSessionContext = Pick<
75
+ SideEffectContext,
76
+ 'backend' | 'clearStartupGrace' | 'dispatch' | 'startStartupGrace'
77
+ >
78
+
79
+ export interface StartTabSessionOptions {
80
+ cols: number
81
+ rows: number
82
+ cwd?: string
83
+ /** Default true: only a tab the user did not just ask for opts out. */
84
+ autoRenameCandidate?: boolean
85
+ /**
86
+ * Appended after the command's own args. Kept out of `tab.command` on purpose:
87
+ * an initial prompt is up to a few thousand characters, and `command` is what
88
+ * the UI shows, what the snapshot persists, and what the custom-command editor
89
+ * round-trips.
90
+ */
91
+ extraArgs?: string[]
92
+ }
93
+
94
+ export function startTabSession(
95
+ ctx: StartTabSessionContext,
96
+ tab: Pick<TabSession, 'id' | 'assistant' | 'title' | 'command' | 'workspaceId'>,
97
+ { autoRenameCandidate = true, cols, cwd, extraArgs, rows }: StartTabSessionOptions
98
+ ): void {
99
+ const { backend, clearStartupGrace, dispatch } = ctx
100
+ logInputDebug('app.tab.start.request', {
101
+ cols,
102
+ command: tab.command,
103
+ cwd: cwd ?? null,
104
+ rows,
105
+ tabId: tab.id,
106
+ title: tab.title,
107
+ workspaceId: tab.workspaceId ?? null,
108
+ })
109
+ ctx.startStartupGrace(tab.id, STARTUP_GRACE_MS)
110
+
111
+ const { args, executable } = parseCommand(tab.command)
112
+
113
+ if (!isCommandAvailable(executable)) {
114
+ clearStartupGrace(tab.id)
115
+ dispatch({
116
+ message: `[command not found] ${executable} is not available in PATH.`,
117
+ tabId: tab.id,
118
+ type: 'set-tab-error',
119
+ })
120
+ return
121
+ }
122
+
123
+ backend.createSession({
124
+ args: extraArgs ? [...args, ...extraArgs] : args,
125
+ assistant: tab.assistant,
126
+ autoRenameCandidate,
127
+ cols,
128
+ command: executable,
129
+ cwd,
130
+ rows,
131
+ tabId: tab.id,
132
+ title: tab.title,
133
+ workspaceId: tab.workspaceId,
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Returns the id of the tab it created, so a caller can write into it.
139
+ *
140
+ * `initialPromptArgs` hands the prompt to the CLI at spawn — see
141
+ * `assistantAcceptsPromptArg`. Callers pass it only for assistants that support
142
+ * it, and fall back to `injectPromptWhenReady` otherwise.
143
+ */
144
+ export function launchAssistant(
145
+ ctx: SideEffectContext,
146
+ assistant: AssistantId,
147
+ workspaceId?: string,
148
+ initialPromptArgs?: string[]
149
+ ): string {
150
+ const { dispatch, state } = ctx
151
+ const customCommand = state.customCommands[assistant]
152
+ const tab = createTabSession(
153
+ assistant,
154
+ customCommand,
155
+ state.customCommands,
156
+ workspaceId ?? getActiveWorkspace(getCurrentProject(state))?.id
157
+ )
158
+ logInputDebug('app.launchAssistant', {
159
+ assistant,
160
+ command: tab.command,
161
+ tabId: tab.id,
162
+ })
163
+ dispatch({ tab, type: 'add-tab' })
164
+ dispatch({ focusMode: 'terminal-input', type: 'set-focus-mode' })
165
+ startTabSession(ctx, tab, {
166
+ cols: state.layout.terminalCols,
167
+ cwd: getTabProjectPath(ctx, tab),
168
+ extraArgs: initialPromptArgs,
169
+ rows: state.layout.terminalRows,
170
+ })
171
+ return tab.id
172
+ }
173
+
174
+ export function getTabProjectPath(
175
+ ctx: SideEffectContext,
176
+ tab: Pick<TabSession, 'workspaceId'>
177
+ ): string | undefined {
178
+ // Scoped to the current project on purpose: a tab pinned to a workspace of
179
+ // some other project gets the current project's path, not that project's.
180
+ const project = getCurrentProject(ctx.state)
181
+ const workspace = project?.workspaces?.find((entry) => entry.id === tab.workspaceId)
182
+ if (workspace) return workspace.path
183
+ return ctx.getCurrentProjectProjectPath()
184
+ }
185
+
186
+ export function startExistingTab(ctx: SideEffectContext, tab: TabSession): void {
187
+ startTabSession(ctx, tab, {
188
+ autoRenameCandidate: false,
189
+ cols: ctx.state.layout.terminalCols,
190
+ cwd: getTabProjectPath(ctx, tab),
191
+ rows: ctx.state.layout.terminalRows,
192
+ })
193
+ }
194
+
195
+ export function executeSplitPane(
196
+ ctx: SideEffectContext,
197
+ direction: SplitDirection,
198
+ tab: TabSession
199
+ ): void {
200
+ const { dispatch, state } = ctx
201
+ const activeTabId = state.activeTabId
202
+ if (!(activeTabId != null && activeTabId !== '')) {
203
+ return
204
+ }
205
+
206
+ const existingTree = getTreeForTab(state.layoutTrees, state.tabGroupMap, activeTabId)
207
+ const baseTree = existingTree ?? createLeaf(activeTabId)
208
+ const newTree = splitNode(baseTree, activeTabId, direction, tab.id)
209
+ const bounds = createTerminalBounds(state.layout.terminalCols, state.layout.terminalRows)
210
+ const paneRect = computePaneRects(newTree, bounds).get(tab.id)
211
+
212
+ dispatch({ direction, newTab: tab, type: 'split-pane' })
213
+ startTabSession(ctx, tab, {
214
+ cols: Math.max(1, (paneRect?.cols ?? state.layout.terminalCols) - PANE_BORDER * 2),
215
+ cwd: getTabProjectPath(ctx, tab),
216
+ rows: Math.max(1, (paneRect?.rows ?? state.layout.terminalRows) - PANE_BORDER * 2),
217
+ })
218
+ }
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef } from 'react'
2
2
 
3
- import type { AppAction } from '../state/types'
3
+ import type { AppAction } from '../state/actions'
4
4
 
5
5
  export interface TabRuntimeTimeouts {
6
6
  clearIdleTimer: (tabId: string) => void
@@ -1,6 +1,7 @@
1
1
  import { type MutableRefObject, useEffect, useRef } from 'react'
2
2
 
3
- import type { AppAction, AppState, GitRefreshPayload, TabActivity } from '../state/types'
3
+ import type { AppAction } from '../state/actions'
4
+ import type { AppState, GitRefreshPayload, TabActivity } from '../state/types'
4
5
 
5
6
  import {
6
7
  type AutoCommitConfigSnapshot,
@@ -61,7 +62,7 @@ export function useAutoCommitDriver({
61
62
  const prevActivityRef = useRef<Map<string, TabActivity | undefined>>(new Map())
62
63
 
63
64
  useEffect(() => {
64
- // Always refresh the activity map so re-enabling mid-session doesn't fire
65
+ // Always refresh the activity map so re-enabling mid-project doesn't fire
65
66
  // a stale "became idle" transition. Skip only the dispatch work.
66
67
  const prev = prevActivityRef.current
67
68
  const next = new Map<string, TabActivity | undefined>()
@@ -73,15 +74,15 @@ export function useAutoCommitDriver({
73
74
  const becameIdle =
74
75
  (before === 'working' || before === 'waiting-input') && tab.activity === 'idle'
75
76
  if (becameIdle) {
76
- const sessionId = state.currentSessionId
77
- if (!(sessionId != null && sessionId !== '')) continue
78
- const session = state.sessions.find((s) => s.id === sessionId)
77
+ const projectId = state.currentProjectId
78
+ if (!(projectId != null && projectId !== '')) continue
79
+ const project = state.projects.find((s) => s.id === projectId)
79
80
  const git = gitPayloadFromState(state)
80
81
  void onActivityTransition(depsRef.current, {
81
82
  assistant: tab.assistant,
82
83
  git,
83
- projectPath: session?.projectPath,
84
- sessionId,
84
+ projectId,
85
+ projectPath: project?.projectPath,
85
86
  tabId: tab.id,
86
87
  })
87
88
  }
@@ -94,14 +95,14 @@ export function useAutoCommitDriver({
94
95
 
95
96
  useEffect(() => {
96
97
  if (!configRef.current.enabled) return
97
- const sessionId = state.currentSessionId
98
- if (!(sessionId != null && sessionId !== '')) return
98
+ const projectId = state.currentProjectId
99
+ if (!(projectId != null && projectId !== '')) return
99
100
  const payload = gitPayloadFromState(state)
100
101
  if (!payload) return
101
102
  const cacheKey = JSON.stringify(payload)
102
103
  if (lastGitHashRef.current === cacheKey) return
103
104
  lastGitHashRef.current = cacheKey
104
- onGitRefresh(depsRef.current, sessionId, payload)
105
+ onGitRefresh(depsRef.current, projectId, payload)
105
106
 
106
107
  // Debounced trigger: when the working tree stays stable for
107
108
  // GIT_STABILIZATION_DEBOUNCE_MS, try to start generation. Covers cases
@@ -114,14 +115,14 @@ export function useAutoCommitDriver({
114
115
  ? state.tabs.find((tab) => tab.id === activeTabId)
115
116
  : undefined
116
117
  if (!activeTab) return
117
- const session = state.sessions.find((s) => s.id === sessionId)
118
+ const project = state.projects.find((s) => s.id === projectId)
118
119
  gitStabilizeTimerRef.current = setTimeout(() => {
119
120
  gitStabilizeTimerRef.current = null
120
121
  void onActivityTransition(depsRef.current, {
121
122
  assistant: activeTab.assistant,
122
123
  git: payload,
123
- projectPath: session?.projectPath,
124
- sessionId,
124
+ projectId,
125
+ projectPath: project?.projectPath,
125
126
  tabId: activeTab.id,
126
127
  })
127
128
  }, GIT_STABILIZATION_DEBOUNCE_MS)
@@ -1,7 +1,8 @@
1
1
  import { type MutableRefObject, useEffect, useRef } from 'react'
2
2
 
3
3
  import type { SessionBackend } from '../session-backend/types'
4
- import type { AppAction, LayoutState } from '../state/types'
4
+ import type { AppAction } from '../state/actions'
5
+ import type { LayoutState } from '../state/types'
5
6
 
6
7
  import { attachCurrentSession } from './backend-attach-runtime'
7
8
  import { bindBackendRuntimeEvents } from './backend-runtime-events'
@@ -11,10 +12,10 @@ interface BackendRuntimeOptions {
11
12
  backend: SessionBackend
12
13
  dispatch: (action: AppAction) => void
13
14
  activeTabId: string | null
14
- currentSessionId: string | null
15
+ currentProjectId: string | null
15
16
  layoutRef: MutableRefObject<LayoutState>
16
17
  resizingRef: MutableRefObject<boolean>
17
- currentSessionWorkspaceSnapshot: Parameters<SessionBackend['attach']>[0]['workspaceSnapshot']
18
+ currentProjectProjectSnapshot: Parameters<SessionBackend['attach']>[0]['projectSnapshot']
18
19
  syntaxOverlayEnabled: () => boolean
19
20
  }
20
21
 
@@ -27,8 +28,8 @@ export interface TabRuntimeControls {
27
28
  export function useBackendRuntime({
28
29
  activeTabId,
29
30
  backend,
30
- currentSessionId,
31
- currentSessionWorkspaceSnapshot,
31
+ currentProjectId,
32
+ currentProjectProjectSnapshot,
32
33
  dispatch,
33
34
  layoutRef,
34
35
  resizingRef,
@@ -39,7 +40,7 @@ export function useBackendRuntime({
39
40
  const { clearAllTimers, clearIdleTimer, clearStartupGrace, startStartupGrace } = timeouts
40
41
 
41
42
  useEffect(() => {
42
- if (!(currentSessionId != null && currentSessionId !== '')) {
43
+ if (!(currentProjectId != null && currentProjectId !== '')) {
43
44
  attachRequestIdRef.current += 1
44
45
  return
45
46
  }
@@ -47,22 +48,22 @@ export function useBackendRuntime({
47
48
  return attachCurrentSession({
48
49
  attachRequestIdRef,
49
50
  backend,
50
- currentSessionId,
51
- currentSessionWorkspaceSnapshot,
51
+ currentProjectId,
52
+ currentProjectProjectSnapshot,
52
53
  dispatch,
53
54
  layoutRef,
54
55
  })
55
- }, [backend, currentSessionId, currentSessionWorkspaceSnapshot, dispatch, layoutRef])
56
+ }, [backend, currentProjectId, currentProjectProjectSnapshot, dispatch, layoutRef])
56
57
 
57
58
  useEffect(() => {
58
- if (!(currentSessionId != null && currentSessionId !== '')) {
59
+ if (!(currentProjectId != null && currentProjectId !== '')) {
59
60
  return
60
61
  }
61
62
 
62
63
  // The backend owns each tab's scroll position; a backgrounded tab keeps it
63
64
  // in its own emulator, so switching back needs no re-anchor from here.
64
65
  backend.setActiveTab(activeTabId)
65
- }, [activeTabId, backend, currentSessionId])
66
+ }, [activeTabId, backend, currentProjectId])
66
67
 
67
68
  useEffect(() => {
68
69
  return bindBackendRuntimeEvents({
@@ -1,13 +1,14 @@
1
1
  import { useEffect } from 'react'
2
2
 
3
- import type { AppAction, ModalState } from '../state/types'
3
+ import type { AppAction } from '../state/actions'
4
+ import type { ModalState } from '../state/types'
4
5
 
5
6
  import { searchProjectDirectories, warmDirectoryCache } from '../platform/project-search'
6
7
 
7
8
  const DEFAULT_DIRECTORY_SEARCH_DEBOUNCE_MS = 200
8
9
 
9
10
  function getDirectoryQuery(modal: ModalState): string {
10
- if (modal.type !== 'create-session') {
11
+ if (modal.type !== 'create-project') {
11
12
  return ''
12
13
  }
13
14
 
@@ -26,12 +27,12 @@ export function useDirectorySearch(
26
27
  const directoryQuery = getDirectoryQuery(modal)
27
28
 
28
29
  useEffect(() => {
29
- if (modal.type !== 'create-session') return
30
+ if (modal.type !== 'create-project') return
30
31
  void warmDirectoryCache()
31
32
  }, [modal.type])
32
33
 
33
34
  useEffect(() => {
34
- if (modal.type !== 'create-session') {
35
+ if (modal.type !== 'create-project') {
35
36
  return
36
37
  }
37
38
 
@@ -4,8 +4,9 @@ import { useEffect, useRef } from 'react'
4
4
 
5
5
  import type { TerminalContentOrigin } from '../input/raw-input-handler'
6
6
  import type { SessionBackend } from '../session-backend/types'
7
+ import type { AppAction } from '../state/actions'
7
8
  import type { SplitDirection } from '../state/layout-tree'
8
- import type { AppAction, AppState, TabSession, TerminalLine } from '../state/types'
9
+ import type { AppState, BarSide, TabSession, TerminalLine } from '../state/types'
9
10
 
10
11
  import { logInputDebug } from '../debug/input-log'
11
12
  import { MultiClickDetector } from '../input/multi-click-detector'
@@ -23,20 +24,19 @@ import { recordMultiClickClipboardWrite } from './multi-click-clipboard-guard'
23
24
  import { requestRenderUpTree } from './render-invalidation'
24
25
  import { shouldWriteSelectionToClipboard } from './selection-clipboard-dedup'
25
26
  import {
26
- type AnchoredRatioDragState,
27
27
  type AxisDragState,
28
- getAnchoredRatioFromDrag,
29
28
  getAxisDeltaFromDrag,
29
+ getRatioFromDrag,
30
30
  getSplitRatioFromDrag,
31
+ type RatioDragState,
31
32
  type SplitDragState,
32
33
  } from './split-drag-controller'
33
34
  import { getForwardedMouseSequence, getScrollViewportDelta } from './terminal-mouse-adapter'
34
35
 
35
36
  type ResizeDragState =
36
37
  | ({ kind: 'split' } & SplitDragState)
37
- | ({ initialWidth: number; kind: 'sidebar' } & AxisDragState)
38
- | ({ initialWidth: number; kind: 'git-pane'; side: 'left' | 'right' } & AxisDragState)
39
- | ({ kind: 'embedded-git'; position: 'top' | 'bottom' } & AnchoredRatioDragState)
38
+ | ({ initialWidth: number; kind: 'bar'; side: BarSide } & AxisDragState)
39
+ | ({ index: number; kind: 'bar-boundary'; side: BarSide } & RatioDragState)
40
40
 
41
41
  interface MultiClickDragState {
42
42
  mode: MultiClickMode
@@ -322,40 +322,32 @@ export function useMouseHandlers({
322
322
  separatorDragRef.current = { kind: 'split', ...info }
323
323
  }
324
324
 
325
- const handleSidebarResizeStart = (info: { initialWidth: number; screenStart: number }) => {
326
- separatorDragRef.current = {
327
- axis: 'x',
328
- initialWidth: info.initialWidth,
329
- kind: 'sidebar',
330
- screenStart: info.screenStart,
331
- }
332
- }
333
-
334
- const handleGitPaneResizeStart = (info: {
325
+ const handleBarResizeStart = (info: {
335
326
  initialWidth: number
336
327
  screenStart: number
337
- side: 'left' | 'right'
328
+ side: BarSide
338
329
  }) => {
339
330
  separatorDragRef.current = {
340
331
  axis: 'x',
341
332
  initialWidth: info.initialWidth,
342
- kind: 'git-pane',
333
+ kind: 'bar',
343
334
  screenStart: info.screenStart,
344
335
  side: info.side,
345
336
  }
346
337
  }
347
338
 
348
- const handleEmbeddedGitResizeStart = (info: {
339
+ const handleBarBoundaryResizeStart = (info: {
349
340
  containerStart: number
350
- position: 'top' | 'bottom'
341
+ index: number
342
+ side: BarSide
351
343
  totalSize: number
352
344
  }) => {
353
345
  separatorDragRef.current = {
354
- anchor: info.position === 'top' ? 'start' : 'end',
355
346
  axis: 'y',
356
- kind: 'embedded-git',
357
- position: info.position,
347
+ index: info.index,
348
+ kind: 'bar-boundary',
358
349
  screenStart: info.containerStart,
350
+ side: info.side,
359
351
  totalSize: info.totalSize,
360
352
  }
361
353
  }
@@ -377,27 +369,22 @@ export function useMouseHandlers({
377
369
  })
378
370
  break
379
371
  }
380
- case 'sidebar': {
381
- const nextWidth = Math.round(drag.initialWidth + getAxisDeltaFromDrag(event, drag))
382
- dispatch({ type: 'set-sidebar-width', width: nextWidth })
383
- break
384
- }
385
- case 'git-pane': {
386
- const delta = getAxisDeltaFromDrag(event, drag)
387
- const direction = drag.side === 'left' ? 1 : -1
388
- const nextWidth = drag.initialWidth + delta * direction
372
+ case 'bar': {
373
+ // The right bar grows as the cursor moves left, hence the flipped sign.
374
+ const delta = getAxisDeltaFromDrag(event, drag) * (drag.side === 'left' ? 1 : -1)
389
375
  dispatch({
390
- ratio: nextWidth / 80,
391
- target: 'pane',
392
- type: 'set-git-pane-ratio',
376
+ side: drag.side,
377
+ type: 'set-bar-width',
378
+ width: Math.round(drag.initialWidth + delta),
393
379
  })
394
380
  break
395
381
  }
396
- case 'embedded-git': {
382
+ case 'bar-boundary': {
397
383
  dispatch({
398
- ratio: getAnchoredRatioFromDrag(event, drag),
399
- target: 'embedded',
400
- type: 'set-git-pane-ratio',
384
+ index: drag.index,
385
+ ratio: getRatioFromDrag(event, drag),
386
+ side: drag.side,
387
+ type: 'set-bar-boundary',
401
388
  })
402
389
  break
403
390
  }
@@ -586,13 +573,12 @@ export function useMouseHandlers({
586
573
  }
587
574
 
588
575
  return {
589
- handleEmbeddedGitResizeStart,
590
- handleGitPaneResizeStart,
576
+ handleBarBoundaryResizeStart,
577
+ handleBarResizeStart,
591
578
  handlePaneActivate,
592
579
  handleSeparatorDrag,
593
580
  handleSeparatorDragEnd,
594
581
  handleSeparatorDragStart,
595
- handleSidebarResizeStart,
596
582
  handleSplitResize,
597
583
  handleTerminalClick,
598
584
  handleTerminalDrag,
@@ -2,9 +2,9 @@ import { useEffect, useRef } from 'react'
2
2
 
3
3
  import type { AppState } from '../state/types'
4
4
 
5
- import { saveCurrentWorkspace } from '../state/workspace-save'
5
+ import { saveCurrentProject } from '../state/project-save'
6
6
 
7
- export function useWorkspaceAutosave(state: AppState, debounceMs: number): void {
7
+ export function useProjectAutosave(state: AppState, debounceMs: number): void {
8
8
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
9
9
  const latestStateRef = useRef(state)
10
10
  latestStateRef.current = state
@@ -15,7 +15,7 @@ export function useWorkspaceAutosave(state: AppState, debounceMs: number): void
15
15
  }
16
16
 
17
17
  timeoutRef.current = setTimeout(() => {
18
- saveCurrentWorkspace(latestStateRef.current)
18
+ saveCurrentProject(latestStateRef.current)
19
19
  timeoutRef.current = null
20
20
  }, debounceMs)
21
21
 
@@ -33,7 +33,7 @@ export function useWorkspaceAutosave(state: AppState, debounceMs: number): void
33
33
  clearTimeout(timeoutRef.current)
34
34
  timeoutRef.current = null
35
35
  }
36
- saveCurrentWorkspace(latestStateRef.current)
36
+ saveCurrentProject(latestStateRef.current)
37
37
  }
38
38
  }, [])
39
39
  }
@@ -4,7 +4,8 @@ import { type MutableRefObject, useEffect, useRef } from 'react'
4
4
 
5
5
  import type { KeyChord } from '../input/keymap/key-chord'
6
6
  import type { SessionBackend } from '../session-backend/types'
7
- import type { AppAction, FocusMode, SnippetRecord, TabSession } from '../state/types'
7
+ import type { AppAction } from '../state/actions'
8
+ import type { FocusMode, SnippetRecord, TabSession } from '../state/types'
8
9
 
9
10
  import { INPUT_DEBUG_LOG_PATH, logInputDebug } from '../debug/input-log'
10
11
  import { createRawInputHandler } from '../input/raw-input-handler'