@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
@@ -1,248 +1,72 @@
1
1
  import { useCallback, useMemo } from 'react'
2
2
 
3
- import type { WorktreeTemplate } from '../../../../config'
4
- import type { AssistantId, WorktreeRecord } from '../../../../state/types'
3
+ import type { AssistantId } from '../../../../state/types'
5
4
 
6
5
  import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
7
6
  import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
8
- import {
9
- buildBaseRefOptions,
10
- filterAssistants,
11
- getTemplateNoneOffset,
12
- } from '../../../../state/selectors'
7
+ import { getNewTabAssistantOptions } from '../../../../state/selectors'
13
8
  import { useTheme } from '../../../theme'
14
9
  import { uiTokens } from '../../../ui-tokens'
15
- import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
10
+ import { Form, TextField } from '../shared/form'
16
11
  import { Picker, type PickerItem } from '../shared/picker'
17
- import { WorktreeDeleteConfirm } from '../shared/worktree-delete-confirm'
18
12
 
19
13
  interface NewTabModalProps {
20
14
  selectedIndex: number
21
15
  customCommands: Record<string, string>
22
16
  filter: string | null
23
17
  cursorPos?: number
24
- currentSessionId: string | null
25
18
  editingCommand: AssistantId | null
26
19
  editBuffer: string
27
- activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name' | 'base'
28
- branchError: string | null
29
- branchName: string
30
- createWorktree: boolean
31
- selectedAssistantId: AssistantId | null
32
- step: 'assistant' | 'worktree' | 'worktree-create' | 'template'
33
- worktreeDeletePrompt: { worktreeId: string; reason: string } | null
34
- worktrees: WorktreeRecord[]
35
- worktreeName: string
36
- baseQuery: string
37
- baseRef: string
38
- baseBranches: string[]
39
- worktreeTemplates: WorktreeTemplate[]
40
- /**
41
- * True when this modal is the worktree-aware new-tab flow. False when the
42
- * component is reused for `split-picker`, which can only choose an
43
- * assistant — the template shortcut entry must be hidden there.
44
- */
45
- allowTemplateShortcut: boolean
46
- }
47
-
48
- const TEMPLATE_SHORTCUT_KEY = '__template-shortcut__'
49
-
50
- function getDeleteBlockedReason({
51
- currentSessionId,
52
- worktree,
53
- worktrees,
54
- }: {
55
- currentSessionId: string | null
56
- worktree: WorktreeRecord | undefined
57
- worktrees: WorktreeRecord[]
58
- }): string | null {
59
- if (!worktree) return null
60
- if (!(currentSessionId != null && currentSessionId !== ''))
61
- return 'Cannot delete: no active session.'
62
- if (worktrees.length <= 1) return 'Cannot delete: at least one worktree must remain.'
63
- if (worktree.source === 'primary') return 'Cannot delete: root worktree is required.'
64
- if (worktree.source === 'aimux-temp' && !worktree.createdByAimux) {
65
- return 'Cannot delete: this temp worktree was not created by Aimux.'
66
- }
67
- return null
20
+ /** Chained from `<C-p>`: a shell cannot take the prompt, so Terminal is hidden. */
21
+ excludeTerminal: boolean
68
22
  }
69
23
 
70
24
  export function NewTabModal({
71
- activeField,
72
- allowTemplateShortcut,
73
- baseBranches,
74
- baseQuery,
75
- baseRef,
76
- branchError,
77
- branchName,
78
- createWorktree,
79
- currentSessionId,
80
25
  cursorPos,
81
26
  customCommands,
82
27
  editBuffer,
83
28
  editingCommand,
29
+ excludeTerminal,
84
30
  filter,
85
- selectedAssistantId,
86
31
  selectedIndex,
87
- step,
88
- worktreeDeletePrompt,
89
- worktreeName,
90
- worktrees,
91
- worktreeTemplates,
92
32
  }: NewTabModalProps) {
93
33
  const t = useTheme()
94
34
  const options = useMemo(() => getAllAssistantOptions(customCommands), [customCommands])
95
- const filtered = useMemo(() => filterAssistants(options, filter), [filter, options])
35
+ const filtered = useMemo(
36
+ () => getNewTabAssistantOptions(customCommands, filter, excludeTerminal),
37
+ [customCommands, excludeTerminal, filter]
38
+ )
96
39
 
97
40
  const handleHover = useCallback(
98
41
  (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
99
42
  []
100
43
  )
101
44
 
102
- const worktreeItems = useMemo<PickerItem[]>(
103
- () => [
104
- ...worktrees.map((worktree, index) => {
45
+ const items = useMemo<PickerItem[]>(
46
+ () =>
47
+ filtered.map((option, index) => {
105
48
  const active = index === selectedIndex
106
- const label = worktree.branch ?? worktree.name
107
- const blockedReason = getDeleteBlockedReason({ currentSessionId, worktree, worktrees })
108
- const canDelete = blockedReason == null || blockedReason === ''
49
+ const customCmd = customCommands[option.id]
109
50
  return {
110
- key: worktree.id,
51
+ key: option.id,
111
52
  onClick: () => {
112
53
  dispatchGlobal({ index, type: 'set-modal-selection-index' })
113
54
  runSideEffectGlobal({ type: 'launch-selected-assistant' })
114
55
  },
115
- onDelete:
116
- active && canDelete && currentSessionId != null && currentSessionId !== ''
117
- ? () => {
118
- dispatchGlobal({ index, type: 'set-modal-selection-index' })
119
- runSideEffectGlobal({
120
- force: false,
121
- sessionId: currentSessionId,
122
- type: 'delete-worktree',
123
- worktreeId: worktree.id,
124
- })
125
- }
126
- : undefined,
56
+ onEdit: () =>
57
+ dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
127
58
  subtitle: (
128
59
  <box flexDirection="column">
129
- <text fg={t.textMuted}>{worktree.path}</text>
130
- <text fg={t.textMuted}>{worktree.source}</text>
60
+ <text fg={t.textMuted}>{option.description}</text>
61
+ {customCmd != null && customCmd !== '' ? (
62
+ <text fg={t.primary}>{customCmd}</text>
63
+ ) : null}
131
64
  </box>
132
65
  ),
133
- title: <text fg={active ? t.text : t.textMuted}>{label}</text>,
66
+ title: <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
134
67
  }
135
68
  }),
136
- {
137
- key: '__create-worktree__',
138
- onClick: () => {
139
- dispatchGlobal({ type: 'enter-new-tab-worktree-create' })
140
- runSideEffectGlobal({ type: 'load-new-tab-base-branches' })
141
- },
142
- subtitle: <text fg={t.textMuted}>Create an Aimux temp worktree</text>,
143
- title: <text fg={createWorktree ? t.text : t.textMuted}>Create new worktree</text>,
144
- },
145
- ],
146
- [createWorktree, currentSessionId, selectedIndex, t, worktrees]
147
- )
148
-
149
- const noneOffset = getTemplateNoneOffset(selectedAssistantId)
150
- const showNoneOption = noneOffset === 1
151
- const templateItems = useMemo<PickerItem[]>(() => {
152
- const noneItem: PickerItem[] = showNoneOption
153
- ? [
154
- {
155
- key: '__none__',
156
- onClick: () => {
157
- dispatchGlobal({ index: 0, type: 'set-modal-selection-index' })
158
- runSideEffectGlobal({ type: 'launch-selected-assistant' })
159
- },
160
- subtitle: <text fg={t.textMuted}>Single pane — current assistant only</text>,
161
- title: <text fg={selectedIndex === 0 ? t.text : t.textMuted}>None</text>,
162
- },
163
- ]
164
- : []
165
- return [
166
- ...noneItem,
167
- ...worktreeTemplates.map((template, idx) => {
168
- const itemIndex = idx + noneOffset
169
- const active = itemIndex === selectedIndex
170
- const tabCount = template.tabs.length
171
- const paneCount = template.tabs.reduce((sum, tab) => sum + tab.panes.length, 0)
172
- return {
173
- key: template.id,
174
- onClick: () => {
175
- dispatchGlobal({ index: itemIndex, type: 'set-modal-selection-index' })
176
- runSideEffectGlobal({ type: 'launch-selected-assistant' })
177
- },
178
- subtitle:
179
- template.description != null && template.description !== '' ? (
180
- <text fg={t.textMuted}>{template.description}</text>
181
- ) : (
182
- <text fg={t.textMuted}>
183
- {tabCount} tab{tabCount === 1 ? '' : 's'}, {paneCount} pane
184
- {paneCount === 1 ? '' : 's'}
185
- </text>
186
- ),
187
- title: <text fg={active ? t.text : t.textMuted}>{template.name}</text>,
188
- }
189
- }),
190
- ]
191
- }, [noneOffset, selectedIndex, showNoneOption, t, worktreeTemplates])
192
-
193
- const showShortcutEntry = allowTemplateShortcut && worktreeTemplates.length > 0
194
- const items = useMemo<PickerItem[]>(() => {
195
- const assistantItems: PickerItem[] = filtered.map((option, index) => {
196
- const active = index === selectedIndex
197
- const customCmd = customCommands[option.id]
198
- return {
199
- key: option.id,
200
- onClick: () => dispatchGlobal({ assistantId: option.id, type: 'select-new-tab-assistant' }),
201
- onEdit: () => dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
202
- subtitle: (
203
- <box flexDirection="column">
204
- <text fg={t.textMuted}>{option.description}</text>
205
- {customCmd != null && customCmd !== '' ? <text fg={t.primary}>{customCmd}</text> : null}
206
- </box>
207
- ),
208
- title: <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
209
- }
210
- })
211
- if (!showShortcutEntry) return assistantItems
212
- const shortcutIndex = filtered.length
213
- const shortcutActive = shortcutIndex === selectedIndex
214
- return [
215
- ...assistantItems,
216
- {
217
- key: TEMPLATE_SHORTCUT_KEY,
218
- onClick: () => {
219
- dispatchGlobal({ index: shortcutIndex, type: 'set-modal-selection-index' })
220
- dispatchGlobal({ type: 'enter-new-tab-template-shortcut' })
221
- },
222
- subtitle: (
223
- <text fg={t.textMuted}>Create a worktree and pick a template — no assistant step</text>
224
- ),
225
- title: <text fg={shortcutActive ? t.text : t.textMuted}>Worktree from template…</text>,
226
- },
227
- ]
228
- }, [customCommands, filtered, showShortcutEntry, selectedIndex, t])
229
-
230
- const baseItems = useMemo<FormOptionItem[]>(
231
- () =>
232
- buildBaseRefOptions(worktrees, baseBranches, baseQuery).map((option) => ({
233
- key: option.ref,
234
- leading: (
235
- <text fg={option.kind === 'worktree' ? t.warning : t.textMuted}>
236
- {option.kind === 'worktree' ? '\u{e728}' : '\u{e702}'}
237
- </text>
238
- ),
239
- subtitle:
240
- option.kind === 'worktree' ? (
241
- <text fg={t.textMuted}>worktree: {option.detail}</text>
242
- ) : null,
243
- title: (active) => <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
244
- })),
245
- [baseBranches, baseQuery, t, worktrees]
69
+ [customCommands, filtered, selectedIndex, t]
246
70
  )
247
71
 
248
72
  if (editingCommand !== null) {
@@ -264,142 +88,24 @@ export function NewTabModal({
264
88
  )
265
89
  }
266
90
 
267
- if (step === 'template') {
268
- return (
269
- <Picker
270
- title="Pick worktree template"
271
- keybindsModeId="modal.new-tab.command-edit"
272
- width={uiTokens.modalWidth.md}
273
- gap={1}
274
- filter={null}
275
- items={templateItems}
276
- selectedIndex={selectedIndex}
277
- emptyState={<text fg={t.textMuted}>No templates configured.</text>}
278
- onHover={handleHover}
279
- footer={
280
- <box flexDirection="column">
281
- <text fg={t.textMuted}>Step 4/4: choose template</text>
282
- <text fg={t.textMuted}>Enter launches, Esc returns to worktree settings</text>
283
- </box>
284
- }
285
- />
286
- )
287
- }
288
-
289
- if (step === 'worktree-create') {
290
- const selectedAssistant =
291
- options.find((option) => option.id === selectedAssistantId) ?? options[0]
292
- const baseActive = activeField === 'base'
293
- return (
294
- <Form
295
- title={`New worktree: ${selectedAssistant?.label ?? 'assistant'}`}
296
- keybindsModeId="modal.new-tab.command-edit"
297
- width={uiTokens.modalWidth.xl}
298
- >
299
- <box flexDirection="column" gap={1}>
300
- <TextField
301
- active={activeField === 'worktree-name'}
302
- label="Worktree name"
303
- value={worktreeName}
304
- cursorPos={activeField === 'worktree-name' ? cursorPos : undefined}
305
- placeholder="my-feature"
306
- />
307
- <box flexDirection="column">
308
- <TextField
309
- active={activeField === 'branch-name'}
310
- label="Branch name"
311
- value={branchName}
312
- cursorPos={activeField === 'branch-name' ? cursorPos : undefined}
313
- placeholder="aimux/my-feature"
314
- />
315
- {branchError != null && branchError !== '' ? (
316
- <text fg={t.error}>{branchError}</text>
317
- ) : null}
318
- </box>
319
- <AutoComplete
320
- active={baseActive}
321
- label="Base (fork from)"
322
- placeholder="branch or worktree to fork from..."
323
- value={baseQuery}
324
- displayValue={baseRef !== '' ? baseRef : 'current branch'}
325
- items={baseItems}
326
- selectedIndex={selectedIndex}
327
- cursorPos={baseActive ? cursorPos : undefined}
328
- maxVisibleRows={6}
329
- onHover={handleHover}
330
- emptyState={<text fg={t.textMuted}>No branches found</text>}
331
- />
332
- <text fg={t.textMuted}>Step 3/3: configure new worktree · Tab switches fields</text>
333
- </box>
334
- </Form>
335
- )
336
- }
337
-
338
- if (step === 'worktree') {
339
- const selectedAssistant =
340
- options.find((option) => option.id === selectedAssistantId) ??
341
- filtered[selectedIndex] ??
342
- options[0]
343
-
344
- if (worktreeDeletePrompt != null) {
345
- const target = worktrees.find((worktree) => worktree.id === worktreeDeletePrompt.worktreeId)
346
- const label = target?.branch ?? target?.name ?? 'this worktree'
347
- return (
348
- <WorktreeDeleteConfirm
349
- keybindsModeId="modal.new-tab.worktree-delete-confirm"
350
- reason={worktreeDeletePrompt.reason}
351
- worktreeLabel={label}
352
- />
353
- )
354
- }
355
-
356
- const selectedWorktree = worktrees[selectedIndex]
357
- const deleteBlockedReason = getDeleteBlockedReason({
358
- currentSessionId,
359
- worktree: selectedWorktree,
360
- worktrees,
361
- })
362
- return (
363
- <Picker
364
- title={`New assistant: ${selectedAssistant?.label ?? 'assistant'}`}
365
- keybindsModeId="modal.new-tab.command-edit"
366
- width={uiTokens.modalWidth.md}
367
- gap={1}
368
- filter={null}
369
- items={worktreeItems}
370
- selectedIndex={selectedIndex}
371
- emptyState={<text fg={t.textMuted}>No worktrees.</text>}
372
- onHover={handleHover}
373
- footer={
374
- <box flexDirection="column">
375
- {deleteBlockedReason != null && deleteBlockedReason !== '' ? (
376
- <text fg={t.textMuted}>{deleteBlockedReason}</text>
377
- ) : null}
378
- <text fg={t.textMuted}>Step 2/2: choose worktree</text>
379
- <text fg={t.textMuted}>Enter launches, Ctrl+d deletes selected worktree</text>
380
- </box>
381
- }
382
- />
383
- )
384
- }
385
-
386
91
  return (
387
92
  <Picker
388
- title="New assistant: choose assistant"
93
+ title="New tab: choose assistant"
389
94
  keybindsModeId="modal.new-tab.command-edit"
390
95
  width={uiTokens.modalWidth.md}
391
96
  gap={1}
392
97
  filter={filter}
393
- cursorPos={activeField === 'assistant' ? cursorPos : undefined}
98
+ cursorPos={cursorPos}
394
99
  items={items}
395
100
  selectedIndex={selectedIndex}
396
101
  emptyState={<text fg={t.textMuted}>No matching assistants.</text>}
397
102
  onHover={handleHover}
398
103
  footer={
399
- <box flexDirection="column">
400
- <text fg={t.textMuted}>Step 1/2: choose assistant</text>
401
- <text fg={t.textMuted}>Enter continues to worktree selection</text>
402
- </box>
104
+ <text fg={t.textMuted}>
105
+ {excludeTerminal
106
+ ? 'Enter launches in the new workspace and sends your prompt'
107
+ : 'Enter launches in the active workspace'}
108
+ </text>
403
109
  }
404
110
  />
405
111
  )
@@ -0,0 +1,101 @@
1
+ import { useCallback, useMemo } from 'react'
2
+
3
+ import type { WorkspaceRecord } from '../../../../state/types'
4
+
5
+ import { dispatchGlobal } from '../../../../state/dispatch-ref'
6
+ import { buildBaseRefOptions } from '../../../../state/selectors'
7
+ import { useTheme } from '../../../theme'
8
+ import { uiTokens } from '../../../ui-tokens'
9
+ import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
10
+
11
+ /** The box is this tall from the start: a one-line slot asks for a one-line task. */
12
+ const PROMPT_LINES = 5
13
+
14
+ interface CreateWorkspaceModalProps {
15
+ activeField: 'prompt' | 'base'
16
+ prompt: string
17
+ branchError: string | null
18
+ baseQuery: string
19
+ baseRef: string
20
+ baseBranches: string[]
21
+ cursorPos?: number
22
+ selectedIndex: number
23
+ workspaces: WorkspaceRecord[]
24
+ }
25
+
26
+ export function CreateWorkspaceModal({
27
+ activeField,
28
+ baseBranches,
29
+ baseQuery,
30
+ baseRef,
31
+ branchError,
32
+ cursorPos,
33
+ prompt,
34
+ selectedIndex,
35
+ workspaces,
36
+ }: CreateWorkspaceModalProps) {
37
+ const t = useTheme()
38
+
39
+ const handleHover = useCallback(
40
+ (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
41
+ []
42
+ )
43
+
44
+ const baseItems = useMemo<FormOptionItem[]>(
45
+ () =>
46
+ buildBaseRefOptions(workspaces, baseBranches, baseQuery).map((option) => ({
47
+ key: option.ref,
48
+ leading: (
49
+ <text fg={option.kind === 'workspace' ? t.warning : t.textMuted}>
50
+ {option.kind === 'workspace' ? '\u{e728}' : '\u{e702}'}
51
+ </text>
52
+ ),
53
+ subtitle:
54
+ option.kind === 'workspace' ? (
55
+ <text fg={t.textMuted}>workspace: {option.detail}</text>
56
+ ) : null,
57
+ title: (active) => <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
58
+ })),
59
+ [baseBranches, baseQuery, t, workspaces]
60
+ )
61
+
62
+ const baseActive = activeField === 'base'
63
+ return (
64
+ <Form
65
+ title="New workspace"
66
+ keybindsModeId="modal.create-workspace"
67
+ width={uiTokens.modalWidth.xl}
68
+ >
69
+ <box flexDirection="column" gap={1}>
70
+ <box flexDirection="column">
71
+ <TextField
72
+ active={activeField === 'prompt'}
73
+ label="What do you want to work on?"
74
+ description="Sent to the assistant, and names the workspace and its branch."
75
+ value={prompt}
76
+ cursorPos={activeField === 'prompt' ? cursorPos : undefined}
77
+ placeholder="Describe the task..."
78
+ minLines={PROMPT_LINES}
79
+ />
80
+ {branchError != null && branchError !== '' ? (
81
+ <text fg={t.error}>{branchError}</text>
82
+ ) : null}
83
+ </box>
84
+ <AutoComplete
85
+ active={baseActive}
86
+ label="Base (fork from)"
87
+ placeholder="branch or workspace to fork from..."
88
+ value={baseQuery}
89
+ displayValue={baseRef !== '' ? baseRef : 'current branch'}
90
+ items={baseItems}
91
+ selectedIndex={selectedIndex}
92
+ cursorPos={baseActive ? cursorPos : undefined}
93
+ maxVisibleRows={6}
94
+ onHover={handleHover}
95
+ emptyState={<text fg={t.textMuted}>No branches found</text>}
96
+ />
97
+ <text fg={t.textMuted}>Ctrl+Enter newline · Tab picks a base · Enter creates</text>
98
+ </box>
99
+ </Form>
100
+ )
101
+ }
@@ -2,7 +2,7 @@ import { useTheme } from '../../../theme'
2
2
  import { uiTokens } from '../../../ui-tokens'
3
3
  import { Form } from '../shared/form'
4
4
 
5
- interface WorktreeMoveConfirmModalProps {
5
+ interface WorkspaceMoveConfirmModalProps {
6
6
  variant: 'stash-target' | 'keep-conflicts'
7
7
  files: string[]
8
8
  sourceLabel: string
@@ -12,17 +12,17 @@ interface WorktreeMoveConfirmModalProps {
12
12
  const MAX_LISTED_FILES = 8
13
13
 
14
14
  /**
15
- * Confirmation dialog after a recoverable move failure. Both worktrees are
15
+ * Confirmation dialog after a recoverable move failure. Both workspaces are
16
16
  * already back in their original state; confirming re-runs the move with the
17
17
  * flag matching the variant (stash the target's changes / keep the conflict
18
18
  * markers in the target).
19
19
  */
20
- export function WorktreeMoveConfirmModal({
20
+ export function WorkspaceMoveConfirmModal({
21
21
  files,
22
22
  sourceLabel,
23
23
  targetLabel,
24
24
  variant,
25
- }: WorktreeMoveConfirmModalProps) {
25
+ }: WorkspaceMoveConfirmModalProps) {
26
26
  const t = useTheme()
27
27
  const listed = files.slice(0, MAX_LISTED_FILES)
28
28
  const remaining = files.length - listed.length
@@ -30,7 +30,7 @@ export function WorktreeMoveConfirmModal({
30
30
  return (
31
31
  <Form
32
32
  title={isStash ? 'Target has conflicting changes' : 'Move hit conflicts'}
33
- keybindsModeId="modal.worktree-move-confirm"
33
+ keybindsModeId="modal.workspace-move-confirm"
34
34
  width={uiTokens.modalWidth.md}
35
35
  footer={
36
36
  <text fg={t.textMuted}>
@@ -1,21 +1,21 @@
1
1
  import { useCallback, useMemo } from 'react'
2
2
 
3
- import type { ModalWorktreeMove, WorktreeRecord } from '../../../../state/types'
3
+ import type { ModalWorkspaceMove, WorkspaceRecord } from '../../../../state/types'
4
4
 
5
5
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
6
- import { formatDivergence } from '../../../../state/session-worktrees'
6
+ import { formatDivergence } from '../../../../state/project-workspaces'
7
7
  import { useTheme } from '../../../theme'
8
8
  import { uiTokens } from '../../../ui-tokens'
9
9
  import { ListItem } from '../../primitives/list-item'
10
10
  import { ModalShell } from '../shared/modal-shell'
11
11
 
12
- interface WorktreeMoveModalProps {
12
+ interface WorkspaceMoveModalProps {
13
13
  deleteSource: boolean
14
14
  divergence: Record<string, { ahead: number; behind: number }>
15
15
  selectedIndex: number
16
- sourceWorktreeId: string
17
- stats: ModalWorktreeMove['stats']
18
- worktrees: WorktreeRecord[]
16
+ sourceWorkspaceId: string
17
+ stats: ModalWorkspaceMove['stats']
18
+ workspaces: WorkspaceRecord[]
19
19
  }
20
20
 
21
21
  const MOVE_HINTS: [key: string, label: string][] = [
@@ -25,37 +25,37 @@ const MOVE_HINTS: [key: string, label: string][] = [
25
25
  ['esc', 'cancel'],
26
26
  ]
27
27
 
28
- export function WorktreeMoveModal({
28
+ export function WorkspaceMoveModal({
29
29
  deleteSource,
30
30
  divergence,
31
31
  selectedIndex,
32
- sourceWorktreeId,
32
+ sourceWorkspaceId,
33
33
  stats,
34
- worktrees,
35
- }: WorktreeMoveModalProps) {
34
+ workspaces,
35
+ }: WorkspaceMoveModalProps) {
36
36
  const t = useTheme()
37
37
  const source = useMemo(
38
- () => worktrees.find((w) => w.id === sourceWorktreeId),
39
- [sourceWorktreeId, worktrees]
38
+ () => workspaces.find((w) => w.id === sourceWorkspaceId),
39
+ [sourceWorkspaceId, workspaces]
40
40
  )
41
41
  const targets = useMemo(
42
- () => worktrees.filter((w) => w.id !== sourceWorktreeId),
43
- [sourceWorktreeId, worktrees]
42
+ () => workspaces.filter((w) => w.id !== sourceWorkspaceId),
43
+ [sourceWorkspaceId, workspaces]
44
44
  )
45
45
  const sourceLabel =
46
- source?.branch != null && source.branch !== '' ? source.branch : (source?.name ?? 'worktree')
46
+ source?.branch != null && source.branch !== '' ? source.branch : (source?.name ?? 'workspace')
47
47
  // What the move would carry: commits ahead of the fork point (when known) plus
48
48
  // uncommitted files, loaded async after the modal opens.
49
49
  const sourcePreview = useMemo(() => {
50
50
  const parts: string[] = []
51
- const ahead = divergence[sourceWorktreeId]?.ahead ?? 0
51
+ const ahead = divergence[sourceWorkspaceId]?.ahead ?? 0
52
52
  if (ahead > 0) parts.push(`${ahead} commit(s)`)
53
53
  if (stats.kind === 'ready') {
54
- const dirty = stats.dirtyFiles[sourceWorktreeId] ?? 0
54
+ const dirty = stats.dirtyFiles[sourceWorkspaceId] ?? 0
55
55
  if (dirty > 0) parts.push(`${dirty} uncommitted file(s)`)
56
56
  }
57
57
  return parts.join(' · ')
58
- }, [divergence, sourceWorktreeId, stats])
58
+ }, [divergence, sourceWorkspaceId, stats])
59
59
  const handleSelectIndex = useCallback(
60
60
  (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
61
61
  []
@@ -63,7 +63,7 @@ export function WorktreeMoveModal({
63
63
  return (
64
64
  <ModalShell
65
65
  title={`Move ${sourceLabel} →`}
66
- keybindsModeId="modal.worktree-move"
66
+ keybindsModeId="modal.workspace-move"
67
67
  width={uiTokens.modalWidth.md}
68
68
  footer={
69
69
  <box flexDirection="column" gap={1} marginTop={1}>
@@ -87,18 +87,20 @@ export function WorktreeMoveModal({
87
87
  <box flexDirection="column" marginTop={1}>
88
88
  {sourcePreview !== '' ? <text fg={t.textMuted}>will move: {sourcePreview}</text> : null}
89
89
  {targets.length === 0 ? (
90
- <text fg={t.textMuted}>No other worktree to move into.</text>
90
+ <text fg={t.textMuted}>No other workspace to move into.</text>
91
91
  ) : (
92
- targets.map((worktree, index) => {
92
+ targets.map((workspace, index) => {
93
93
  const active = index === selectedIndex
94
94
  const label =
95
- worktree.branch != null && worktree.branch !== '' ? worktree.branch : worktree.name
96
- const ahead = formatDivergence(divergence[worktree.id])
97
- const dirty = stats.kind === 'ready' && (stats.dirtyFiles[worktree.id] ?? 0) > 0
95
+ workspace.branch != null && workspace.branch !== ''
96
+ ? workspace.branch
97
+ : workspace.name
98
+ const ahead = formatDivergence(divergence[workspace.id])
99
+ const dirty = stats.kind === 'ready' && (stats.dirtyFiles[workspace.id] ?? 0) > 0
98
100
  return (
99
101
  <ListItem
100
- key={worktree.id}
101
- id={worktree.id}
102
+ key={workspace.id}
103
+ id={workspace.id}
102
104
  index={index}
103
105
  active={active}
104
106
  onHoverIndex={handleSelectIndex}
@@ -106,7 +108,7 @@ export function WorktreeMoveModal({
106
108
  title={
107
109
  <text fg={active ? t.text : t.textMuted} wrapMode="none">
108
110
  {label}
109
- {worktree.source === 'primary' ? ' (primary)' : ''}
111
+ {workspace.source === 'primary' ? ' (primary)' : ''}
110
112
  {ahead !== '' ? ` ${ahead}` : ''}
111
113
  {dirty ? <span fg={t.warning}> ●</span> : null}
112
114
  </text>