@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
@@ -8,20 +8,26 @@ import { memo, type ReactNode, useCallback, useMemo, useRef, useState } from 're
8
8
 
9
9
  import type { FocusMode, TabSession } from '../../../state/types'
10
10
 
11
- import { useWorktreeDivergencePolling } from '../../../git/worktree-divergence-poller'
11
+ import { useWorkspaceDivergencePolling } from '../../../git/workspace-divergence-poller'
12
12
  import { useAppStore } from '../../../state/app-store'
13
+ import { getBarWidth } from '../../../state/bars'
13
14
  import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
14
- import { filterTabsForActiveWorktree } from '../../../state/session-worktrees'
15
+ import { filterTabsForActiveWorkspace } from '../../../state/project-workspaces'
15
16
  import { buildTabEntries, type GroupEntry, type TabEntry } from '../../../state/tab-entries'
16
- import { moveIdToIdPosition } from '../../session-ordering'
17
+ import { useScrollActiveIntoView } from '../../hooks/use-scroll-active-into-view'
18
+ import { moveIdToIdPosition } from '../../project-ordering'
17
19
  import { useTheme } from '../../theme'
18
20
  import { FlashLabelBadge } from '../flash/flash-label-badge'
19
21
  import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
20
22
  import { TabItem } from './sidebar/tab-item'
21
- import { useTopTabBarAutoScroll } from './sidebar/use-top-tab-bar-auto-scroll'
22
23
 
23
24
  interface TopTabBarProps {
24
- forceVisible?: boolean
25
+ /**
26
+ * A full-screen view has replaced the pane tree (git mode, settings). The strip
27
+ * shows regardless of the user's own preference — it is the only orientation
28
+ * left — but it shows as a strip to read, not to act on.
29
+ */
30
+ panesReplaced?: boolean
25
31
  }
26
32
 
27
33
  const ROW_CONTENT_OPTIONS = {
@@ -191,34 +197,34 @@ function entryTabIds(entry: TabEntry): string[] {
191
197
  return entry.kind === 'single' ? [entry.tab.id] : entry.tabs.map((tab) => tab.id)
192
198
  }
193
199
 
194
- export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
200
+ export function TopTabBar({ panesReplaced = false }: TopTabBarProps) {
195
201
  const t = useTheme()
196
202
  const headerBg = t.backgroundPanel
197
203
  const tabs = useAppStore((s) => s.tabs)
198
204
  const activeTabId = useAppStore((s) => s.activeTabId)
199
- const bar = useAppStore((s) => s.sessionBar)
200
- const sidebar = useAppStore((s) => s.sidebar)
201
- const currentSessionId = useAppStore((s) => s.currentSessionId)
202
- const sessions = useAppStore((s) => s.sessions)
205
+ const bar = useAppStore((s) => s.projectBar)
206
+ const leftBarVisible = useAppStore((s) => getBarWidth(s.bars.left) > 0)
207
+ const currentProjectId = useAppStore((s) => s.currentProjectId)
208
+ const projects = useAppStore((s) => s.projects)
203
209
  const focusMode: FocusMode = useAppStore((s) => s.focusMode)
204
210
  const layoutTrees = useAppStore((s) => s.layoutTrees)
205
211
  const tabGroupMap = useAppStore((s) => s.tabGroupMap)
206
212
 
207
- // Sidebar now also shows worktree chips with divergence — poll whenever
213
+ // Sidebar now also shows workspace chips with divergence — poll whenever
208
214
  // either surface is visible.
209
- useWorktreeDivergencePolling(bar.visible || sidebar.visible || forceVisible)
215
+ useWorkspaceDivergencePolling(bar.visible || leftBarVisible || panesReplaced)
210
216
 
211
- const currentSession = useMemo(
217
+ const currentProject = useMemo(
212
218
  () =>
213
- currentSessionId != null && currentSessionId !== ''
214
- ? sessions.find((s) => s.id === currentSessionId)
219
+ currentProjectId != null && currentProjectId !== ''
220
+ ? projects.find((s) => s.id === currentProjectId)
215
221
  : undefined,
216
- [currentSessionId, sessions]
222
+ [currentProjectId, projects]
217
223
  )
218
224
 
219
225
  const visibleTabs = useMemo(
220
- () => filterTabsForActiveWorktree(tabs, currentSession),
221
- [tabs, currentSession]
226
+ () => filterTabsForActiveWorkspace(tabs, currentProject),
227
+ [tabs, currentProject]
222
228
  )
223
229
 
224
230
  const entries = useMemo(
@@ -238,11 +244,11 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
238
244
  }, [entries, activeTabId])
239
245
 
240
246
  const scrollRef = useRef<ScrollBoxRenderable | null>(null)
241
- useTopTabBarAutoScroll({
242
- activeTabId: activeEntryId,
247
+ useScrollActiveIntoView({
248
+ activeId: activeEntryId,
243
249
  idPrefix: 'top-tab-',
244
250
  scrollRef,
245
- visible: bar.visible || forceVisible,
251
+ visible: bar.visible || panesReplaced,
246
252
  })
247
253
 
248
254
  const handleEntryActivate = useCallback(
@@ -259,7 +265,7 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
259
265
  )
260
266
 
261
267
  // --- Drag-and-drop reorder of the tab strip ---------------------------------
262
- // Mirrors the workspace-list drag, but on the horizontal axis: entries are
268
+ // Mirrors the project-list drag, but on the horizontal axis: entries are
263
269
  // laid out left-to-right inside a scrollX box, so hit-testing is on x/width.
264
270
  const [draggingId, setDraggingId] = useState<string | null>(null)
265
271
  const [dragOrder, setDragOrder] = useState<string[] | null>(null)
@@ -343,13 +349,13 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
343
349
 
344
350
  const handleNewTab = useCallback((e: OtuiMouseEvent) => {
345
351
  e.stopPropagation()
346
- dispatchGlobal({ type: 'open-new-tab-modal' })
352
+ runSideEffectGlobal({ type: 'open-new-tab' })
347
353
  }, [])
348
354
 
349
- if (!bar.visible && !forceVisible) return null
350
- // Keep the bar visible even with zero entries — when the active worktree
355
+ if (!bar.visible && !panesReplaced) return null
356
+ // Keep the bar visible even with zero entries — when the active workspace
351
357
  // has no tabs, the lone "+" affordance is what tells the user "you're in
352
- // an empty worktree, click here to start one".
358
+ // an empty workspace, click here to start one".
353
359
 
354
360
  const isFocused = focusMode === 'terminal-input' || focusMode === 'navigation'
355
361
 
@@ -425,17 +431,22 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
425
431
  </TopTabCell>
426
432
  )
427
433
  })}
428
- <box
429
- flexDirection="row"
430
- flexShrink={0}
431
- paddingLeft={1}
432
- paddingRight={1}
433
- onMouseDown={handleNewTab}
434
- >
435
- <text fg={t.textMuted} selectable={false}>
436
- +
437
- </text>
438
- </box>
434
+ {/* Not while a full-screen view is up: a `+` that quietly starts a tab
435
+ and drops you out of the screen you were reading is noise, and on the
436
+ settings screen it is not even about anything on it. */}
437
+ {panesReplaced ? null : (
438
+ <box
439
+ flexDirection="row"
440
+ flexShrink={0}
441
+ paddingLeft={1}
442
+ paddingRight={1}
443
+ onMouseDown={handleNewTab}
444
+ >
445
+ <text fg={t.textMuted} selectable={false}>
446
+ +
447
+ </text>
448
+ </box>
449
+ )}
439
450
  </scrollbox>
440
451
  </box>
441
452
  )
@@ -89,10 +89,10 @@ export function GitCommitModal({
89
89
  const bodyActive = activeField === 'body'
90
90
  const isConfirm = stage === 'confirm'
91
91
  const isGenerating = stage === 'generating'
92
- const currentSessionId = useAppStore((s) => s.currentSessionId)
92
+ const currentProjectId = useAppStore((s) => s.currentProjectId)
93
93
  const bgKind = useAppStore((s) => {
94
- const id = s.currentSessionId
95
- return id != null && id !== '' ? s.autoCommit.bySession[id]?.kind : undefined
94
+ const id = s.currentProjectId
95
+ return id != null && id !== '' ? s.autoCommit.byProject[id]?.kind : undefined
96
96
  })
97
97
  const isBgGenerating = bgKind === 'generating'
98
98
  const showBgSpinner = isBgGenerating && !isConfirm && !isGenerating
@@ -103,19 +103,19 @@ export function GitCommitModal({
103
103
 
104
104
  const onAutoCommitClick = useCallback((): void => {
105
105
  const hasTitle = title.trim().length > 0
106
- if (hasTitle || !(currentSessionId != null && currentSessionId !== '')) {
106
+ if (hasTitle || !(currentProjectId != null && currentProjectId !== '')) {
107
107
  dispatchGlobal({ type: 'git-commit-enter-confirm' })
108
108
  return
109
109
  }
110
110
  if (bgKind === 'ready') {
111
- dispatchGlobal({ sessionId: currentSessionId, type: 'git-commit-use-background-suggestion' })
111
+ dispatchGlobal({ projectId: currentProjectId, type: 'git-commit-use-background-suggestion' })
112
112
  return
113
113
  }
114
- dispatchGlobal({ sessionId: currentSessionId, type: 'git-commit-enter-generating' })
114
+ dispatchGlobal({ projectId: currentProjectId, type: 'git-commit-enter-generating' })
115
115
  if (bgKind !== 'generating') {
116
- runSideEffectGlobal({ sessionId: currentSessionId, type: 'generate-auto-commit-now' })
116
+ runSideEffectGlobal({ projectId: currentProjectId, type: 'generate-auto-commit-now' })
117
117
  }
118
- }, [bgKind, currentSessionId, title])
118
+ }, [bgKind, currentProjectId, title])
119
119
 
120
120
  const handleAutoCommitMouseDown = useCallback(
121
121
  (event: OtuiMouseEvent) => {
@@ -10,35 +10,35 @@ import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/fo
10
10
  const VISIBLE_ROWS = 8
11
11
 
12
12
  function getDirectoryResultIcon(result: DirectoryResult): string {
13
- if (result.type === 'worktree') return '\u{e728}'
14
- if (result.type === 'workspace') return '\u{f07c}'
13
+ if (result.type === 'workspace') return '\u{e728}'
14
+ if (result.type === 'project') return '\u{f07c}'
15
15
  return '\u{e702}'
16
16
  }
17
17
 
18
18
  function getDirectoryResultColor(result: DirectoryResult): string {
19
19
  const t = getCurrentTheme()
20
- if (result.type === 'worktree') return t.warning
21
- if (result.type === 'workspace') return t.info
20
+ if (result.type === 'workspace') return t.warning
21
+ if (result.type === 'project') return t.info
22
22
  return t.text
23
23
  }
24
24
 
25
- interface CreateSessionModalProps {
25
+ interface CreateProjectModalProps {
26
26
  activeField: 'directory' | 'name'
27
27
  directoryQuery: string
28
- sessionName: string
28
+ projectName: string
29
29
  results: DirectoryResult[]
30
30
  selectedIndex: number
31
31
  pendingProjectPath: string | null
32
32
  }
33
33
 
34
- export function CreateSessionModal({
34
+ export function CreateProjectModal({
35
35
  activeField,
36
36
  directoryQuery,
37
37
  pendingProjectPath,
38
+ projectName,
38
39
  results,
39
40
  selectedIndex,
40
- sessionName,
41
- }: CreateSessionModalProps) {
41
+ }: CreateProjectModalProps) {
42
42
  const t = useTheme()
43
43
  const dirActive = activeField === 'directory'
44
44
  const nameActive = activeField === 'name'
@@ -57,8 +57,8 @@ export function CreateSessionModal({
57
57
 
58
58
  return (
59
59
  <Form
60
- title="Create workspace"
61
- keybindsModeId="modal.create-session"
60
+ title="Create project"
61
+ keybindsModeId="modal.create-project"
62
62
  width={uiTokens.modalWidth.xl}
63
63
  >
64
64
  <AutoComplete
@@ -81,7 +81,7 @@ export function CreateSessionModal({
81
81
  }
82
82
  />
83
83
 
84
- <TextField active={nameActive} label="Workspace name" value={sessionName} />
84
+ <TextField active={nameActive} label="Project name" value={projectName} />
85
85
  </Form>
86
86
  )
87
87
  }
@@ -1,9 +1,9 @@
1
1
  import { uiTokens } from '../../../ui-tokens'
2
2
  import { Form, TextField } from '../shared/form'
3
3
 
4
- export function SessionNameModal({ title, value }: { title: string; value: string }) {
4
+ export function ProjectNameModal({ title, value }: { title: string; value: string }) {
5
5
  return (
6
- <Form title={title} keybindsModeId="modal.session-name" width={uiTokens.modalWidth.md}>
6
+ <Form title={title} keybindsModeId="modal.project-name" width={uiTokens.modalWidth.md}>
7
7
  <TextField active value={value} />
8
8
  </Form>
9
9
  )
@@ -1,86 +1,84 @@
1
1
  import { useCallback, useMemo } from 'react'
2
2
 
3
- import type { SessionRecord } from '../../../../state/types'
3
+ import type { ProjectRecord } from '../../../../state/types'
4
4
 
5
5
  import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
6
- import { filterSessions } from '../../../../state/selectors'
6
+ import { filterProjects } from '../../../../state/selectors'
7
7
  import { abbreviatePath } from '../../../path-format'
8
- import { orderSessionsForDisplay } from '../../../session-ordering'
8
+ import { orderProjectsForDisplay } from '../../../project-ordering'
9
9
  import { useTheme } from '../../../theme'
10
10
  import { uiTokens } from '../../../ui-tokens'
11
11
  import { Picker, type PickerItem } from '../shared/picker'
12
12
 
13
- interface SessionPickerModalProps {
14
- sessions: SessionRecord[]
13
+ interface ProjectPickerModalProps {
14
+ projects: ProjectRecord[]
15
15
  selectedIndex: number
16
- currentSessionId: string | null
16
+ currentProjectId: string | null
17
17
  currentTabCount: number
18
18
  filter: string | null
19
19
  cursorPos?: number
20
20
  }
21
21
 
22
- function formatSessionLine(
23
- session: SessionRecord,
24
- currentSessionId: string | null,
22
+ function formatProjectLine(
23
+ project: ProjectRecord,
24
+ currentProjectId: string | null,
25
25
  currentTabCount: number,
26
26
  displayIndex: number
27
27
  ): string {
28
28
  const tabCount =
29
- session.id === currentSessionId
30
- ? currentTabCount
31
- : (session.workspaceSnapshot?.tabs.length ?? 0)
32
- return `[${displayIndex}] ${session.name} (${tabCount} tab${tabCount === 1 ? '' : 's'})`
29
+ project.id === currentProjectId ? currentTabCount : (project.projectSnapshot?.tabs.length ?? 0)
30
+ return `[${displayIndex}] ${project.name} (${tabCount} tab${tabCount === 1 ? '' : 's'})`
33
31
  }
34
32
 
35
33
  function getEmptyStateMessage(hasFilter: boolean): string {
36
- if (hasFilter) return 'No matching workspaces.'
37
- return 'No workspaces yet. Press Enter or n to create your first workspace.'
34
+ if (hasFilter) return 'No matching projects.'
35
+ return 'No projects yet. Press Enter or n to create your first project.'
38
36
  }
39
37
 
40
- export function SessionPickerModal({
41
- currentSessionId,
38
+ export function ProjectPickerModal({
39
+ currentProjectId,
42
40
  currentTabCount,
43
41
  cursorPos,
44
42
  filter,
43
+ projects,
45
44
  selectedIndex,
46
- sessions,
47
- }: SessionPickerModalProps) {
45
+ }: ProjectPickerModalProps) {
48
46
  const t = useTheme()
49
- const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
50
- const filtered = useMemo(() => filterSessions(ordered, filter), [filter, ordered])
47
+ const ordered = useMemo(() => orderProjectsForDisplay(projects), [projects])
48
+ const filtered = useMemo(() => filterProjects(ordered, filter), [filter, ordered])
51
49
  const hasFilter = !!(filter != null && filter !== '')
52
50
 
53
51
  const items = useMemo<PickerItem[]>(() => {
54
52
  const baselineOrder = ordered.map((s) => s.id)
55
- const sessionItems: PickerItem[] = filtered.map((session, index) => {
53
+ const projectItems: PickerItem[] = filtered.map((project, index) => {
56
54
  const active = index === selectedIndex
57
- const displayIndex = baselineOrder.indexOf(session.id) + 1
55
+ const displayIndex = baselineOrder.indexOf(project.id) + 1
58
56
  return {
59
- key: session.id,
60
- onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
61
- onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
57
+ key: project.id,
58
+ onClick: () => runSideEffectGlobal({ type: 'confirm-selected-project' }),
59
+ onDelete: () => runSideEffectGlobal({ type: 'delete-selected-project' }),
62
60
  subtitle:
63
- session.projectPath != null && session.projectPath !== '' ? (
64
- <text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
61
+ project.projectPath != null && project.projectPath !== '' ? (
62
+ <text fg={t.textMuted}>{abbreviatePath(project.projectPath)}</text>
65
63
  ) : undefined,
66
64
  title: (
67
65
  <text fg={active ? t.text : t.textMuted}>
68
- {formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
66
+ {formatProjectLine(project, currentProjectId, currentTabCount, displayIndex)}
69
67
  </text>
70
68
  ),
71
69
  }
72
70
  })
73
71
  const createNewItem: PickerItem = {
74
72
  key: '__create-new__',
75
- onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
73
+ onClick: () => runSideEffectGlobal({ type: 'confirm-selected-project' }),
76
74
  title: (
77
75
  <text fg={selectedIndex === filtered.length ? t.text : t.textMuted}>
78
- Create new workspace
76
+ Create new project
79
77
  </text>
80
78
  ),
81
79
  }
82
- return [...sessionItems, createNewItem]
83
- }, [currentSessionId, currentTabCount, filtered, ordered, selectedIndex, t])
80
+ return [...projectItems, createNewItem]
81
+ }, [currentProjectId, currentTabCount, filtered, ordered, selectedIndex, t])
84
82
 
85
83
  const handleHover = useCallback(
86
84
  (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
@@ -89,8 +87,8 @@ export function SessionPickerModal({
89
87
 
90
88
  return (
91
89
  <Picker
92
- title="Workspaces"
93
- keybindsModeId="modal.session-picker.filtering"
90
+ title="Projects"
91
+ keybindsModeId="modal.project-picker.filtering"
94
92
  width={uiTokens.modalWidth.lg}
95
93
  gap={1}
96
94
  filter={filter}
@@ -0,0 +1,70 @@
1
+ import { useCallback, useMemo } from 'react'
2
+
3
+ import { filterSettingRows } from '../../../../settings/search'
4
+ import { useAppStore } from '../../../../state/app-store'
5
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
6
+ import { useTheme } from '../../../theme'
7
+ import { uiTokens } from '../../../ui-tokens'
8
+ import { RowValue } from '../../settings/row-value'
9
+ import { Picker, type PickerItem } from '../shared/picker'
10
+
11
+ interface SettingsSearchModalProps {
12
+ filter: string | null
13
+ selectedIndex: number
14
+ cursorPos?: number
15
+ }
16
+
17
+ /**
18
+ * Every setting in one list, grouped by section and laid out the way the screen
19
+ * lays them out — label and value on one line, what it does underneath. A result
20
+ * you cannot read the current value of is a result you have to go and check.
21
+ *
22
+ * The filter is the one `getModalOptionCount` counts with, so the list and the
23
+ * cursor moving through it never disagree.
24
+ */
25
+ export function SettingsSearchModal({
26
+ cursorPos,
27
+ filter,
28
+ selectedIndex,
29
+ }: SettingsSearchModalProps) {
30
+ const t = useTheme()
31
+ const projects = useAppStore((s) => s.projects)
32
+ const hits = useMemo(() => filterSettingRows(projects, filter), [projects, filter])
33
+
34
+ const items = useMemo<PickerItem[]>(
35
+ () =>
36
+ hits.map((hit) => ({
37
+ group: hit.sectionLabel,
38
+ key: hit.row.id,
39
+ // Acts on the selection, which the hover just set — the same two-step
40
+ // every other picker's rows use.
41
+ onClick: () => runSideEffectGlobal({ type: 'confirm-settings-search' }),
42
+ subtitle:
43
+ hit.row.description != null && hit.row.description !== '' ? (
44
+ <text fg={t.textMuted}>{hit.row.description}</text>
45
+ ) : undefined,
46
+ title: <text fg={t.text}>{hit.row.label}</text>,
47
+ trailing: <RowValue row={hit.row} />,
48
+ })),
49
+ [hits, t]
50
+ )
51
+
52
+ const handleHover = useCallback(
53
+ (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
54
+ []
55
+ )
56
+
57
+ return (
58
+ <Picker
59
+ title="Search settings"
60
+ keybindsModeId="modal.settings-search.filtering"
61
+ width={uiTokens.modalWidth.xl}
62
+ filter={filter}
63
+ cursorPos={cursorPos}
64
+ items={items}
65
+ selectedIndex={selectedIndex}
66
+ emptyState={<text fg={t.textMuted}>No setting matches.</text>}
67
+ onHover={handleHover}
68
+ />
69
+ )
70
+ }
@@ -27,6 +27,7 @@ interface TextFieldProps {
27
27
  cursorPos?: number
28
28
  description?: ReactNode
29
29
  label?: ReactNode
30
+ minLines?: number
30
31
  placeholder?: string
31
32
  value: string
32
33
  }
@@ -94,6 +95,7 @@ export function TextField({
94
95
  cursorPos,
95
96
  description,
96
97
  label,
98
+ minLines,
97
99
  placeholder,
98
100
  value,
99
101
  }: TextFieldProps) {
@@ -104,7 +106,13 @@ export function TextField({
104
106
  {label}
105
107
  </FieldLabel>
106
108
  ) : null}
107
- <InputField active={active} value={value} cursorPos={cursorPos} placeholder={placeholder} />
109
+ <InputField
110
+ active={active}
111
+ value={value}
112
+ cursorPos={cursorPos}
113
+ placeholder={placeholder}
114
+ minLines={minLines}
115
+ />
108
116
  </box>
109
117
  )
110
118
  }
@@ -138,27 +146,31 @@ export function AutoComplete({
138
146
  cursorPos={cursorPos}
139
147
  placeholder={placeholder}
140
148
  />
141
- <box flexDirection="column" height={maxVisibleRows}>
142
- {items.length === 0
143
- ? (emptyState ?? null)
144
- : visibleItems.map((item, index) => {
145
- const optionIndex = scrollOffset + index
146
- const optionActive = active && optionIndex === selectedIndex
147
- return (
148
- <ListItem
149
- key={item.key}
150
- index={optionIndex}
151
- active={optionActive}
152
- leading={resolveItemContent(item.leading, optionActive)}
153
- title={resolveItemContent(item.title, optionActive)}
154
- subtitle={resolveItemContent(item.subtitle, optionActive)}
155
- trailing={resolveItemContent(item.trailing, optionActive)}
156
- onHoverIndex={onHover}
157
- onClick={item.onClick}
158
- />
159
- )
160
- })}
161
- </box>
149
+ {/* Collapsed unless focused: a dropdown nobody is driving is noise, and
150
+ it pushes the rest of the form off the modal. */}
151
+ {active ? (
152
+ <box flexDirection="column" height={maxVisibleRows}>
153
+ {items.length === 0
154
+ ? (emptyState ?? null)
155
+ : visibleItems.map((item, index) => {
156
+ const optionIndex = scrollOffset + index
157
+ const optionActive = active && optionIndex === selectedIndex
158
+ return (
159
+ <ListItem
160
+ key={item.key}
161
+ index={optionIndex}
162
+ active={optionActive}
163
+ leading={resolveItemContent(item.leading, optionActive)}
164
+ title={resolveItemContent(item.title, optionActive)}
165
+ subtitle={resolveItemContent(item.subtitle, optionActive)}
166
+ trailing={resolveItemContent(item.trailing, optionActive)}
167
+ onHoverIndex={onHover}
168
+ onClick={item.onClick}
169
+ />
170
+ )
171
+ })}
172
+ </box>
173
+ ) : null}
162
174
  </box>
163
175
  )
164
176
  }
@@ -18,7 +18,7 @@ export interface PickerItem {
18
18
  onClick?: () => void
19
19
  onEdit?: () => void
20
20
  onDelete?: () => void
21
- onWorktree?: () => void
21
+ onWorkspace?: () => void
22
22
  }
23
23
 
24
24
  interface PickerProps {
@@ -42,11 +42,11 @@ const MODAL_CHROME_ROWS = 6
42
42
  function PickerItemCtas({
43
43
  onDelete,
44
44
  onEdit,
45
- onWorktree,
45
+ onWorkspace,
46
46
  }: {
47
47
  onEdit?: () => void
48
48
  onDelete?: () => void
49
- onWorktree?: () => void
49
+ onWorkspace?: () => void
50
50
  }) {
51
51
  const t = useTheme()
52
52
  const handleEdit = useCallback(
@@ -56,12 +56,12 @@ function PickerItemCtas({
56
56
  },
57
57
  [onEdit]
58
58
  )
59
- const handleWorktree = useCallback(
59
+ const handleWorkspace = useCallback(
60
60
  (event: OtuiMouseEvent) => {
61
61
  event.stopPropagation()
62
- onWorktree?.()
62
+ onWorkspace?.()
63
63
  },
64
- [onWorktree]
64
+ [onWorkspace]
65
65
  )
66
66
  const handleDelete = useCallback(
67
67
  (event: OtuiMouseEvent) => {
@@ -77,8 +77,8 @@ function PickerItemCtas({
77
77
  <text fg={t.primary}>[edit]</text>
78
78
  </box>
79
79
  ) : null}
80
- {onWorktree ? (
81
- <box onMouseDown={handleWorktree}>
80
+ {onWorkspace ? (
81
+ <box onMouseDown={handleWorkspace}>
82
82
  <text fg={t.warning}>[WT]</text>
83
83
  </box>
84
84
  ) : null}
@@ -189,11 +189,11 @@ export function Picker({
189
189
  const capturedIndex = index
190
190
  const trailing =
191
191
  item.trailing ??
192
- (active && (item.onEdit || item.onDelete || item.onWorktree) ? (
192
+ (active && (item.onEdit || item.onDelete || item.onWorkspace) ? (
193
193
  <PickerItemCtas
194
194
  onEdit={item.onEdit}
195
195
  onDelete={item.onDelete}
196
- onWorktree={item.onWorktree}
196
+ onWorkspace={item.onWorkspace}
197
197
  />
198
198
  ) : null)
199
199
  nodes.push(
@@ -4,33 +4,33 @@ import { useTheme } from '../../../theme'
4
4
  import { uiTokens } from '../../../ui-tokens'
5
5
  import { Form } from './form'
6
6
 
7
- interface WorktreeDeleteConfirmProps {
7
+ interface WorkspaceDeleteConfirmProps {
8
8
  keybindsModeId: ModeId
9
9
  reason: string
10
- worktreeLabel: string
10
+ workspaceLabel: string
11
11
  }
12
12
 
13
13
  /**
14
- * Shared confirmation dialog for a recoverable worktree delete. Used both inside
15
- * the new-tab picker and as a standalone modal (sidebar "Remove worktree"); the
14
+ * Shared confirmation dialog for a recoverable workspace delete. Used both inside
15
+ * the new-tab picker and as a standalone modal (sidebar "Remove workspace"); the
16
16
  * keybinds mode wires Enter/y to confirm and Esc/n to cancel for each context.
17
17
  */
18
- export function WorktreeDeleteConfirm({
18
+ export function WorkspaceDeleteConfirm({
19
19
  keybindsModeId,
20
20
  reason,
21
- worktreeLabel,
22
- }: WorktreeDeleteConfirmProps) {
21
+ workspaceLabel,
22
+ }: WorkspaceDeleteConfirmProps) {
23
23
  const t = useTheme()
24
24
  return (
25
25
  <Form
26
- title="Delete worktree?"
26
+ title="Delete workspace?"
27
27
  keybindsModeId={keybindsModeId}
28
28
  width={uiTokens.modalWidth.md}
29
29
  footer={<text fg={t.textMuted}>Enter / y to delete · Esc / n to cancel</text>}
30
30
  >
31
31
  <box flexDirection="column" gap={1}>
32
32
  <text fg={t.text}>
33
- Delete <strong>{worktreeLabel}</strong>?
33
+ Delete <strong>{workspaceLabel}</strong>?
34
34
  </text>
35
35
  <text fg={t.warning}>{reason}</text>
36
36
  </box>