@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
@@ -0,0 +1,73 @@
1
+ import { useRenderer } from '@opentui/react'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { useActivitySprites } from '../../settings/live'
5
+ import { detectGraphicsProtocol, isInsideTmux } from '../terminal-graphics/capabilities'
6
+ import {
7
+ loadSprites,
8
+ SPRITE_PLACEHOLDERS,
9
+ type SpriteSet,
10
+ type SpriteState,
11
+ } from '../terminal-graphics/sprites'
12
+
13
+ export interface ActivitySprite {
14
+ /** The colour is the frame's image id — see `Sprite.colors`. */
15
+ fg: string
16
+ /** The cells to draw, one entry per line of the row, top line first. */
17
+ glyphs: readonly string[]
18
+ }
19
+
20
+ const NO_SPRITES: SpriteSet = {}
21
+
22
+ function useFrame(intervalMs: number): number {
23
+ const [frame, setFrame] = useState(0)
24
+
25
+ useEffect(() => {
26
+ // A tick count, not an index: consumers cycle over lists of different
27
+ // lengths and take their own modulo, so wrapping here would make whichever
28
+ // length does not divide the wrap jump backwards once per cycle.
29
+ if (intervalMs <= 0) return
30
+ const interval = setInterval(() => setFrame((prev) => prev + 1), intervalMs)
31
+ return () => clearInterval(interval)
32
+ }, [intervalMs])
33
+
34
+ return frame
35
+ }
36
+
37
+ /**
38
+ * The sprite standing in for a row's status marker, or null when the caller
39
+ * should draw its own glyph — the toggle is off, the terminal cannot composite
40
+ * images, or nothing in the sprite folder covers this state.
41
+ *
42
+ * tmux owns the grid and does not understand placeholder cells, so sprites are
43
+ * off there even when the terminal underneath could draw them.
44
+ */
45
+ export function useActivitySprite(state: SpriteState | null): ActivitySprite | null {
46
+ const [sprites, setSprites] = useState<SpriteSet>(NO_SPRITES)
47
+ const renderer = useRenderer()
48
+ const enabled =
49
+ useActivitySprites() && detectGraphicsProtocol(renderer) === 'kitty' && !isInsideTmux()
50
+
51
+ useEffect(() => {
52
+ if (!enabled) return
53
+ let cancelled = false
54
+ void (async () => {
55
+ const loaded = await loadSprites()
56
+ if (!cancelled) setSprites(loaded)
57
+ })()
58
+ return () => {
59
+ cancelled = true
60
+ }
61
+ }, [enabled])
62
+
63
+ // Gated on `enabled` here, not only at load time: the loaded set stays in
64
+ // state when the toggle is switched off, and the toggle is a live setting.
65
+ const sprite = !enabled || state === null ? undefined : sprites[state]
66
+ const colors = sprite?.colors
67
+ // A still sprite needs no timer, and neither does a row whose state has none.
68
+ const frame = useFrame(colors === undefined || colors.length < 2 ? 0 : (sprite?.frameMs ?? 0))
69
+
70
+ const fg = colors?.[frame % colors.length]
71
+ if (fg === undefined) return null
72
+ return { fg, glyphs: SPRITE_PLACEHOLDERS }
73
+ }
@@ -0,0 +1,26 @@
1
+ import type { ScrollBoxRenderable } from '@opentui/core'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ interface Options {
6
+ scrollRef: React.RefObject<ScrollBoxRenderable | null>
7
+ visible: boolean
8
+ /** Id of the active child, without the prefix. Null when there is none. */
9
+ activeId: string | null
10
+ idPrefix: string
11
+ }
12
+
13
+ /**
14
+ * Scroll the active child into view whenever it changes (and on first reveal).
15
+ * A thin wrapper around `scrollChildIntoView` — no wrap-around/edge heuristics,
16
+ * which is all a single-axis list of equal rows needs.
17
+ */
18
+ export function useScrollActiveIntoView({ activeId, idPrefix, scrollRef, visible }: Options): void {
19
+ useEffect(() => {
20
+ if (!visible) return
21
+ if (activeId == null || activeId === '') return
22
+ const scrollbox = scrollRef.current
23
+ if (!scrollbox) return
24
+ scrollbox.scrollChildIntoView(`${idPrefix}${activeId}`)
25
+ }, [activeId, idPrefix, scrollRef, visible])
26
+ }
@@ -1,11 +1,11 @@
1
- import type { SessionRecord } from '../state/types'
1
+ import type { ProjectRecord } from '../state/types'
2
2
 
3
3
  /**
4
- * Return sessions in user-facing display order: persisted `order` ascending,
4
+ * Return projects in user-facing display order: persisted `order` ascending,
5
5
  * with any missing `order` falling back to `createdAt` ascending.
6
6
  */
7
- export function orderSessionsForDisplay(sessions: SessionRecord[]): SessionRecord[] {
8
- return [...sessions].sort((a, b) => {
7
+ export function orderProjectsForDisplay(projects: ProjectRecord[]): ProjectRecord[] {
8
+ return [...projects].sort((a, b) => {
9
9
  const ao = a.order ?? Number.MAX_SAFE_INTEGER
10
10
  const bo = b.order ?? Number.MAX_SAFE_INTEGER
11
11
  if (ao !== bo) return ao - bo
package/src/ui/root.tsx CHANGED
@@ -1,21 +1,21 @@
1
1
  import type { MouseEvent } from '@opentui/core'
2
2
 
3
- import { useCallback, useMemo } from 'react'
3
+ import { type ReactNode, useCallback, useMemo } from 'react'
4
4
 
5
5
  import type { MeasuredPaneRect } from '../app-runtime/use-pane-size-report'
6
- import type { WorktreeTemplate } from '../config'
7
6
  import type { TerminalContentOrigin } from '../input/raw-input-handler'
8
7
  import type {
9
8
  BarSide,
10
9
  FocusMode,
11
10
  ModalState,
12
- SessionRecord,
11
+ ProjectRecord,
13
12
  SnippetRecord,
14
- WorktreeRecord,
13
+ WorkspaceRecord,
15
14
  } from '../state/types'
16
15
  import type { ThemeId } from './themes'
17
16
 
18
- import { useWorktreeBranchPolling } from '../git/worktree-branch-poller'
17
+ import { useWorkspaceBranchPolling } from '../git/workspace-branch-poller'
18
+ import { useAutoCommitModel } from '../settings/live'
19
19
  import { useAppStore } from '../state/app-store'
20
20
  import { getBarWidth } from '../state/bars'
21
21
  import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout-tree'
@@ -29,39 +29,41 @@ import { AIUsageModal } from './components/modals/app/ai-usage-modal'
29
29
  import { HelpModal } from './components/modals/app/help-modal'
30
30
  import { UpdateAvailableModal } from './components/modals/app/update-available-modal'
31
31
  import { GitCommitModal } from './components/modals/git/git-commit-modal'
32
- import { CreateSessionModal } from './components/modals/sessions/create-session-modal'
33
- import { SessionNameModal } from './components/modals/sessions/session-name-modal'
34
- import { SessionPickerModal } from './components/modals/sessions/session-picker-modal'
35
- import { WorktreeDeleteConfirm } from './components/modals/shared/worktree-delete-confirm'
32
+ import { CreateProjectModal } from './components/modals/projects/create-project-modal'
33
+ import { ProjectNameModal } from './components/modals/projects/project-name-modal'
34
+ import { ProjectPickerModal } from './components/modals/projects/project-picker-modal'
35
+ import { SettingsSearchModal } from './components/modals/settings/settings-search-modal'
36
+ import { WorkspaceDeleteConfirm } from './components/modals/shared/workspace-delete-confirm'
36
37
  import { SnippetEditorModal } from './components/modals/snippets/snippet-editor-modal'
37
38
  import { SnippetPickerModal } from './components/modals/snippets/snippet-picker-modal'
38
39
  import { NewTabModal } from './components/modals/tabs/new-tab-modal'
39
40
  import { ThemePickerModal } from './components/modals/themes/theme-picker-modal'
40
- import { WorktreeMoveConfirmModal } from './components/modals/worktree/worktree-move-confirm-modal'
41
- import { WorktreeMoveModal } from './components/modals/worktree/worktree-move-modal'
41
+ import { CreateWorkspaceModal } from './components/modals/workspace/create-workspace-modal'
42
+ import { WorkspaceMoveConfirmModal } from './components/modals/workspace/workspace-move-confirm-modal'
43
+ import { WorkspaceMoveModal } from './components/modals/workspace/workspace-move-modal'
42
44
  import { ContextMenuOverlay } from './components/overlays/context-menu/context-menu-overlay'
43
45
  import { PendingChordOverlay } from './components/overlays/pending-chord-overlay'
44
46
  import { ToastViewport } from './components/overlays/toast/toast-viewport'
47
+ import { SettingsView } from './components/settings/settings-view'
45
48
  import { useTheme } from './theme'
46
49
 
47
- const EMPTY_WORKTREES: WorktreeRecord[] = []
48
- const EMPTY_BASE_BRANCHES: string[] = []
50
+ const EMPTY_WORKSPACES: WorkspaceRecord[] = []
49
51
 
50
- function getCreateSessionFields(modal: ModalState) {
51
- if (modal.type !== 'create-session') {
52
- return { directoryQuery: '', sessionName: '' }
52
+ function getCreateProjectFields(modal: ModalState) {
53
+ if (modal.type !== 'create-project') {
54
+ return { directoryQuery: '', projectName: '' }
53
55
  }
54
56
 
55
57
  if (modal.activeField === 'directory') {
56
58
  return {
57
59
  directoryQuery: modal.editBuffer ?? '',
58
- sessionName: modal.nameBuffer,
60
+ projectName: modal.nameBuffer,
59
61
  }
60
62
  }
61
63
 
62
64
  return {
63
65
  directoryQuery: modal.nameBuffer,
64
- sessionName: modal.editBuffer ?? '',
66
+ projectName: modal.editBuffer ?? '',
65
67
  }
66
68
  }
67
69
 
@@ -82,18 +84,17 @@ function renderModal(
82
84
  modal: ModalState,
83
85
  options: {
84
86
  customCommands: Record<string, string>
85
- sessions: SessionRecord[]
86
- currentSessionId: string | null
87
+ projects: ProjectRecord[]
88
+ currentProjectId: string | null
87
89
  currentTabCount: number
88
90
  snippets: SnippetRecord[]
89
91
  themeId: ThemeId
90
- createSessionFields: { directoryQuery: string; sessionName: string }
92
+ createProjectFields: { directoryQuery: string; projectName: string }
91
93
  snippetEditorFields: { snippetName: string; snippetTrigger: string; snippetContent: string }
92
94
  focusMode: FocusMode
93
95
  activeAssistant?: string
94
96
  autoCommitModel?: string
95
- worktreeDivergence: Record<string, { ahead: number; behind: number }>
96
- worktreeTemplates: WorktreeTemplate[]
97
+ workspaceDivergence: Record<string, { ahead: number; behind: number }>
97
98
  }
98
99
  ) {
99
100
  switch (modal.type) {
@@ -105,62 +106,72 @@ function renderModal(
105
106
  customCommands={options.customCommands}
106
107
  filter={modal.editBuffer}
107
108
  cursorPos={modal.cursorPos}
108
- currentSessionId={options.currentSessionId}
109
109
  editingCommand={modal.type === 'new-tab' ? modal.editingCommand : null}
110
110
  editBuffer={modal.editBuffer ?? ''}
111
- activeField={modal.type === 'new-tab' ? modal.activeField : 'assistant'}
112
- branchError={modal.type === 'new-tab' ? modal.branchError : null}
113
- branchName={modal.type === 'new-tab' ? modal.branchName : ''}
114
- createWorktree={modal.type === 'new-tab' ? modal.createWorktree : false}
115
- selectedAssistantId={modal.type === 'new-tab' ? modal.selectedAssistantId : null}
116
- step={modal.type === 'new-tab' ? modal.step : 'assistant'}
117
- baseQuery={modal.type === 'new-tab' ? modal.baseQuery : ''}
118
- baseRef={modal.type === 'new-tab' ? modal.baseRef : ''}
119
- baseBranches={modal.type === 'new-tab' ? modal.baseBranches : EMPTY_BASE_BRANCHES}
120
- worktreeDeletePrompt={modal.type === 'new-tab' ? modal.worktreeDeletePrompt : null}
121
- worktrees={
122
- options.currentSessionId != null && options.currentSessionId !== ''
123
- ? (options.sessions.find((session) => session.id === options.currentSessionId)
124
- ?.worktrees ?? EMPTY_WORKTREES)
125
- : EMPTY_WORKTREES
111
+ excludeTerminal={modal.type === 'new-tab' && modal.pendingWorkspace != null}
112
+ />
113
+ )
114
+ case 'create-workspace':
115
+ return (
116
+ <CreateWorkspaceModal
117
+ activeField={modal.activeField}
118
+ prompt={modal.prompt}
119
+ branchError={modal.branchError}
120
+ baseQuery={modal.baseQuery}
121
+ baseRef={modal.baseRef}
122
+ baseBranches={modal.baseBranches}
123
+ cursorPos={modal.cursorPos}
124
+ selectedIndex={modal.selectedIndex}
125
+ workspaces={
126
+ options.currentProjectId != null && options.currentProjectId !== ''
127
+ ? (options.projects.find((project) => project.id === options.currentProjectId)
128
+ ?.workspaces ?? EMPTY_WORKSPACES)
129
+ : EMPTY_WORKSPACES
126
130
  }
127
- worktreeName={modal.type === 'new-tab' ? modal.worktreeName : ''}
128
- worktreeTemplates={options.worktreeTemplates}
129
- allowTemplateShortcut={modal.type === 'new-tab'}
130
131
  />
131
132
  )
132
- case 'session-picker':
133
+ case 'project-picker':
133
134
  return (
134
- <SessionPickerModal
135
- sessions={options.sessions}
135
+ <ProjectPickerModal
136
+ projects={options.projects}
136
137
  selectedIndex={modal.selectedIndex}
137
- currentSessionId={options.currentSessionId}
138
+ currentProjectId={options.currentProjectId}
138
139
  currentTabCount={options.currentTabCount}
139
140
  filter={modal.editBuffer}
140
141
  cursorPos={modal.cursorPos}
141
142
  />
142
143
  )
143
- case 'session-name':
144
+ case 'project-name':
144
145
  return (
145
- <SessionNameModal
146
+ <ProjectNameModal
146
147
  title={
147
- modal.sessionTargetId != null && modal.sessionTargetId !== ''
148
- ? 'Rename workspace'
149
- : 'Create workspace'
148
+ modal.projectTargetId != null && modal.projectTargetId !== ''
149
+ ? 'Rename project'
150
+ : 'Create project'
150
151
  }
151
152
  value={modal.editBuffer ?? ''}
152
153
  />
153
154
  )
154
155
  case 'rename-tab':
155
- return <SessionNameModal title="Rename tab" value={modal.editBuffer ?? ''} />
156
- case 'rename-worktree':
157
- return <SessionNameModal title="Rename worktree" value={modal.editBuffer ?? ''} />
158
- case 'create-session':
156
+ return <ProjectNameModal title="Rename tab" value={modal.editBuffer ?? ''} />
157
+ case 'setting-text':
158
+ return <ProjectNameModal title={modal.settingLabel} value={modal.editBuffer ?? ''} />
159
+ case 'settings-search':
160
+ return (
161
+ <SettingsSearchModal
162
+ filter={modal.editBuffer}
163
+ selectedIndex={modal.selectedIndex}
164
+ cursorPos={modal.cursorPos}
165
+ />
166
+ )
167
+ case 'rename-workspace':
168
+ return <ProjectNameModal title="Rename workspace" value={modal.editBuffer ?? ''} />
169
+ case 'create-project':
159
170
  return (
160
- <CreateSessionModal
171
+ <CreateProjectModal
161
172
  activeField={modal.activeField}
162
- directoryQuery={options.createSessionFields.directoryQuery}
163
- sessionName={options.createSessionFields.sessionName}
173
+ directoryQuery={options.createProjectFields.directoryQuery}
174
+ projectName={options.createProjectFields.projectName}
164
175
  results={modal.directoryResults}
165
176
  selectedIndex={modal.selectedIndex}
166
177
  pendingProjectPath={modal.pendingProjectPath}
@@ -183,7 +194,7 @@ function renderModal(
183
194
  snippetName={options.snippetEditorFields.snippetName}
184
195
  snippetTrigger={options.snippetEditorFields.snippetTrigger}
185
196
  snippetContent={options.snippetEditorFields.snippetContent}
186
- isEditing={modal.sessionTargetId !== null}
197
+ isEditing={modal.projectTargetId !== null}
187
198
  />
188
199
  )
189
200
  case 'theme-picker':
@@ -203,25 +214,25 @@ function renderModal(
203
214
  latestVersion={modal.latestVersion}
204
215
  />
205
216
  )
206
- case 'worktree-move': {
207
- const session =
208
- options.currentSessionId != null && options.currentSessionId !== ''
209
- ? options.sessions.find((entry) => entry.id === options.currentSessionId)
217
+ case 'workspace-move': {
218
+ const project =
219
+ options.currentProjectId != null && options.currentProjectId !== ''
220
+ ? options.projects.find((entry) => entry.id === options.currentProjectId)
210
221
  : undefined
211
222
  return (
212
- <WorktreeMoveModal
213
- deleteSource={modal.type === 'worktree-move' ? modal.deleteSource : false}
214
- divergence={options.worktreeDivergence}
223
+ <WorkspaceMoveModal
224
+ deleteSource={modal.type === 'workspace-move' ? modal.deleteSource : false}
225
+ divergence={options.workspaceDivergence}
215
226
  selectedIndex={modal.selectedIndex}
216
- sourceWorktreeId={modal.sourceWorktreeId}
227
+ sourceWorkspaceId={modal.sourceWorkspaceId}
217
228
  stats={modal.stats}
218
- worktrees={session?.worktrees ?? EMPTY_WORKTREES}
229
+ workspaces={project?.workspaces ?? EMPTY_WORKSPACES}
219
230
  />
220
231
  )
221
232
  }
222
- case 'worktree-move-confirm':
233
+ case 'workspace-move-confirm':
223
234
  return (
224
- <WorktreeMoveConfirmModal
235
+ <WorkspaceMoveConfirmModal
225
236
  variant={modal.variant}
226
237
  files={modal.files}
227
238
  sourceLabel={modal.sourceLabel}
@@ -239,12 +250,12 @@ function renderModal(
239
250
  )
240
251
  case 'ai-usage':
241
252
  return <AIUsageModal />
242
- case 'worktree-delete-confirm':
253
+ case 'workspace-delete-confirm':
243
254
  return (
244
- <WorktreeDeleteConfirm
245
- keybindsModeId="modal.worktree-delete-confirm"
255
+ <WorkspaceDeleteConfirm
256
+ keybindsModeId="modal.workspace-delete-confirm"
246
257
  reason={modal.reason}
247
- worktreeLabel={modal.worktreeLabel}
258
+ workspaceLabel={modal.workspaceLabel}
248
259
  />
249
260
  )
250
261
  case 'git-commit': {
@@ -332,17 +343,16 @@ export function RootView({
332
343
  const modal = useAppStore((s) => s.modal)
333
344
  const snippets = useAppStore((s) => s.snippets)
334
345
  const customCommands = useAppStore((s) => s.customCommands)
335
- const sessions = useAppStore((s) => s.sessions)
336
- const currentSessionId = useAppStore((s) => s.currentSessionId)
337
- const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
338
- const worktreeTemplates = useAppStore((s) => s.worktreeTemplates)
346
+ const projects = useAppStore((s) => s.projects)
347
+ const currentProjectId = useAppStore((s) => s.currentProjectId)
348
+ const workspaceDivergence = useAppStore((s) => s.workspaceDivergence)
339
349
  const leftBarWidth = useAppStore((s) => getBarWidth(s.bars.left))
340
350
 
341
351
  const splitChrome = PANE_BORDER * 2
342
352
 
343
- // Keep every worktree's `branch` in state synchronized with the on-disk
353
+ // Keep every workspace's `branch` in state synchronized with the on-disk
344
354
  // HEAD so the sidebar (and divergence calc) never reads a stale value.
345
- useWorktreeBranchPolling(true)
355
+ useWorkspaceBranchPolling(true)
346
356
 
347
357
  // The terminal's own left edge stays a drag target for the left bar, so the
348
358
  // resize gesture works from either side of the seam.
@@ -396,50 +406,42 @@ export function RootView({
396
406
  )
397
407
 
398
408
  const activeTab = tabs.find((tab) => tab.id === activeTabId)
409
+ // The generating overlay has always been able to name the model, and nothing
410
+ // ever passed it one.
411
+ const autoCommitModel = useAutoCommitModel(activeTab?.assistant)
399
412
  const activeTree =
400
413
  activeTabId != null && activeTabId !== ''
401
414
  ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId)
402
415
  : null
403
- const createSessionFields = getCreateSessionFields(modal)
416
+ const createProjectFields = getCreateProjectFields(modal)
404
417
  const snippetEditorFields = getSnippetEditorFields(modal)
405
418
 
419
+ // Git mode and settings each swap the pane tree for one full-width view.
420
+ // Everything else on screen — status bar, overlays, modal, toasts — is the
421
+ // same either way, so only the center is a branch: a return per view would
422
+ // mean maintaining the chrome three times.
406
423
  const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
407
- if (inGitMode) {
408
- return (
409
- <box flexDirection="column" width="100%" height="100%" backgroundColor={editorBg}>
410
- <TopTabBar forceVisible />
411
- <GitView themeId={themeId} />
412
- <StatusBar />
413
- <PendingChordOverlay />
414
- <ContextMenuOverlay />
415
- {renderModal(modal, {
416
- activeAssistant: activeTab?.assistant,
417
- createSessionFields,
418
- currentSessionId,
419
- currentTabCount: tabs.length,
420
- customCommands,
421
- focusMode,
422
- sessions,
423
- snippetEditorFields,
424
- snippets,
425
- themeId,
426
- worktreeDivergence,
427
- worktreeTemplates,
428
- })}
429
- <ToastViewport />
430
- </box>
431
- )
432
- }
424
+ // A settings row's text field flips focusMode to command-edit, the same way the
425
+ // commit modal does in git mode, so the screen behind it has to stay mounted:
426
+ // those two modals belong to the screen and are read against it.
427
+ //
428
+ // Not `returnTo === 'settings'`, which is where focus goes and not what is
429
+ // behind: the theme picker also returns here, and it previews each theme as you
430
+ // move. Previewing it on the settings screen is not the answer to "how does this
431
+ // theme look" — so it gets the panes, and closing still comes back here.
432
+ const inSettings =
433
+ focusMode === 'settings' || modal.type === 'setting-text' || modal.type === 'settings-search'
434
+ let replacesPanes: ReactNode = null
435
+ if (inGitMode) replacesPanes = <GitView themeId={themeId} />
436
+ else if (inSettings) replacesPanes = <SettingsView />
433
437
 
434
- return (
435
- <box
436
- flexDirection="column"
437
- width="100%"
438
- height="100%"
439
- backgroundColor={editorBg}
440
- onMouseDrag={handleRootMouseDrag}
441
- onMouseUp={handleRootMouseUp}
442
- >
438
+ const center =
439
+ replacesPanes !== null ? (
440
+ <>
441
+ <TopTabBar panesReplaced />
442
+ {replacesPanes}
443
+ </>
444
+ ) : (
443
445
  <box flexDirection="row" gap={0} padding={0} flexGrow={1}>
444
446
  <Bar
445
447
  side="left"
@@ -503,21 +505,37 @@ export function RootView({
503
505
  onResizeDragEnd={onSeparatorDragEnd}
504
506
  />
505
507
  </box>
508
+ )
509
+
510
+ return (
511
+ <box
512
+ flexDirection="column"
513
+ width="100%"
514
+ height="100%"
515
+ backgroundColor={editorBg}
516
+ // Both drag handlers no-op unless a gesture is in flight, so the full-width
517
+ // views inherit them harmlessly — and a drag that was in flight when one
518
+ // opened gets finalised instead of stranded.
519
+ onMouseDrag={handleRootMouseDrag}
520
+ onMouseUp={handleRootMouseUp}
521
+ >
522
+ {center}
506
523
  <StatusBar />
507
524
  <PendingChordOverlay />
508
525
  <ContextMenuOverlay />
509
526
  {renderModal(modal, {
510
- createSessionFields,
511
- currentSessionId,
527
+ activeAssistant: activeTab?.assistant,
528
+ autoCommitModel,
529
+ createProjectFields,
530
+ currentProjectId,
512
531
  currentTabCount: tabs.length,
513
532
  customCommands,
514
533
  focusMode,
515
- sessions,
534
+ projects,
516
535
  snippetEditorFields,
517
536
  snippets,
518
537
  themeId,
519
- worktreeDivergence,
520
- worktreeTemplates,
538
+ workspaceDivergence,
521
539
  })}
522
540
  <ToastViewport />
523
541
  </box>