@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
@@ -0,0 +1,342 @@
1
+ // Every action the reducer accepts, grouped by the slice of state it touches.
2
+ //
3
+ // Split from `types.ts` so the state shapes and the messages that change them
4
+ // are separate files: the shapes are read by the whole app, this union only by
5
+ // the reducer and the things that dispatch into it.
6
+
7
+ import type { ModeId } from '@brimveyn/aimux-config'
8
+
9
+ import type { LayoutNode, SplitDirection } from './layout-tree'
10
+ import type {
11
+ AssistantId,
12
+ BarSide,
13
+ BranchDivergence,
14
+ DiffData,
15
+ DirectoryResult,
16
+ DiscoveredRepo,
17
+ FocusMode,
18
+ GitFileSection,
19
+ GitPanelError,
20
+ GitPaneState,
21
+ GitRefreshPayload,
22
+ PendingWorkspaceLaunch,
23
+ ProjectRecord,
24
+ ProjectSnapshotV1,
25
+ ProjectStatus,
26
+ SnippetRecord,
27
+ TabActivity,
28
+ TabSession,
29
+ TerminalModeState,
30
+ TerminalSnapshot,
31
+ WorkspaceRecord,
32
+ } from './types'
33
+
34
+ export type ModalAction =
35
+ | { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
36
+ | {
37
+ type: 'open-new-tab-modal'
38
+ pendingWorkspace?: PendingWorkspaceLaunch
39
+ pendingPrompt?: string
40
+ }
41
+ | { type: 'open-edit-custom-command'; assistantId: AssistantId }
42
+ | { type: 'open-help-modal'; scope?: ModeId }
43
+ | { type: 'open-split-picker'; direction: SplitDirection }
44
+ | { type: 'open-project-picker' }
45
+ | {
46
+ type: 'open-project-name-modal'
47
+ projectTargetId?: string
48
+ initialName?: string
49
+ returnToProjectPicker?: boolean
50
+ }
51
+ | { type: 'close-modal' }
52
+ | { type: 'move-modal-selection'; delta: number }
53
+ | { type: 'update-command-edit'; char: string }
54
+ | { type: 'cancel-command-edit' }
55
+ | { type: 'open-create-project-modal'; returnToProjectPicker: boolean }
56
+ | { type: 'open-create-workspace-modal' }
57
+ | { type: 'set-create-workspace-base-branches'; branches: string[]; defaultBranch?: string }
58
+ | { type: 'set-create-workspace-branch-error'; message: string | null }
59
+ | { type: 'switch-create-workspace-field' }
60
+ | { type: 'set-directory-results'; results: DirectoryResult[] }
61
+ | { type: 'switch-create-project-field' }
62
+ | { type: 'select-directory' }
63
+ | { type: 'open-rename-tab-modal' }
64
+ | {
65
+ type: 'open-rename-workspace-modal'
66
+ projectId: string
67
+ workspaceId: string
68
+ initialName: string
69
+ }
70
+ | { type: 'open-snippet-picker'; returnTo?: FocusMode }
71
+ | { type: 'open-snippet-editor'; snippetId?: string }
72
+ | { type: 'set-help-entry-count'; count: number }
73
+ | { type: 'set-theme-entry-count'; count: number }
74
+ | { type: 'open-theme-picker'; returnTo?: FocusMode }
75
+ | { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
76
+ | { type: 'set-modal-selection-index'; index: number }
77
+ | { type: 'open-ai-usage-modal' }
78
+ | { type: 'open-workspace-move-modal'; sourceWorkspaceId: string }
79
+ | { type: 'toggle-workspace-move-delete' }
80
+ | { type: 'set-workspace-move-stats'; dirtyFiles: Record<string, number> }
81
+ | {
82
+ type: 'open-workspace-move-confirm'
83
+ variant: 'stash-target' | 'keep-conflicts'
84
+ files: string[]
85
+ projectId: string
86
+ sourceWorkspaceId: string
87
+ targetWorkspaceId: string
88
+ deleteSource: boolean
89
+ sourceLabel: string
90
+ targetLabel: string
91
+ }
92
+ | {
93
+ type: 'open-workspace-delete-confirm'
94
+ projectId: string
95
+ workspaceId: string
96
+ workspaceLabel: string
97
+ reason: string
98
+ closeTabs: boolean
99
+ force: boolean
100
+ }
101
+ | { type: 'open-flash-jump-modal' }
102
+ | { type: 'clear-flash-jump-pending' }
103
+
104
+ export type ProjectAction =
105
+ | {
106
+ type: 'load-project'
107
+ projectId: string
108
+ projectSnapshot?: ProjectSnapshotV1
109
+ forceDisconnected?: boolean
110
+ }
111
+ | { type: 'set-projects'; projects: ProjectRecord[] }
112
+ | { type: 'create-project-record'; project: ProjectRecord }
113
+ | { type: 'rename-project-record'; projectId: string; name: string }
114
+ | { type: 'delete-project-record'; projectId: string; openProjectPicker?: boolean }
115
+ | { type: 'reorder-projects'; orderedIds: string[] }
116
+ | { type: 'reorder-active-project'; delta: number }
117
+ | { type: 'set-project-status'; projectId: string; status: ProjectStatus }
118
+ /** What a workspace's assistants are doing. `done` is owned by the reducer,
119
+ * so this carries only the two flags derived from tab statuses. */
120
+ | { type: 'set-workspace-activity'; workspaceId: string; working: boolean; waiting: boolean }
121
+ /** An assistant in this workspace finished a turn and nobody has looked yet. */
122
+ | { type: 'mark-workspace-done'; workspaceId: string }
123
+ /** The tab that rang: it finished a turn while the user was elsewhere. */
124
+ | { type: 'mark-tab-unseen'; tabId: string }
125
+ | {
126
+ type: 'add-workspace-record'
127
+ projectId: string
128
+ workspace: WorkspaceRecord
129
+ activate?: boolean
130
+ }
131
+ | { type: 'set-active-workspace'; projectId: string; workspaceId: string }
132
+ | {
133
+ type: 'update-workspace-record'
134
+ projectId: string
135
+ workspaceId: string
136
+ patch: Partial<WorkspaceRecord>
137
+ }
138
+
139
+ export type TabAction =
140
+ | { type: 'add-tab'; tab: TabSession }
141
+ | {
142
+ type: 'hydrate-project'
143
+ tabs: TabSession[]
144
+ activeTabId: string | null
145
+ layoutTree?: LayoutNode | null
146
+ layoutTrees?: Record<string, LayoutNode>
147
+ tabGroupMap?: Record<string, string>
148
+ }
149
+ | { type: 'close-tab'; tabId: string }
150
+ | { type: 'close-active-tab' }
151
+ | { type: 'set-active-tab'; tabId: string }
152
+ | { type: 'move-active-tab'; delta: number }
153
+ | { type: 'reorder-active-tab'; delta: number }
154
+ | { type: 'reorder-tabs'; orderedTabIds: string[] }
155
+ | { type: 'reset-tab-project'; tabId: string }
156
+ | {
157
+ type: 'rename-tab'
158
+ tabId: string
159
+ title: string
160
+ autoRenameStatus?: 'eligible' | 'attempted'
161
+ }
162
+ | {
163
+ type: 'update-tab-metadata'
164
+ tabId: string
165
+ title?: string
166
+ autoRenameStatus?: 'eligible' | 'attempted'
167
+ /** Set to false to promote a hidden tab into the normal tab strip. */
168
+ hidden?: boolean
169
+ }
170
+ | { type: 'append-tab-buffer'; tabId: string; chunk: string }
171
+ | {
172
+ type: 'replace-tab-viewport'
173
+ tabId: string
174
+ viewport: TerminalSnapshot
175
+ terminalModes: TerminalModeState
176
+ source?: 'resize' | 'scroll' | 'data' | 'switch'
177
+ }
178
+ | { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
179
+ | { type: 'set-tab-error'; tabId: string; message: string }
180
+
181
+ export type LayoutAction =
182
+ | {
183
+ type: 'split-pane'
184
+ direction: SplitDirection
185
+ newTab: TabSession
186
+ }
187
+ | { type: 'close-pane'; tabId: string }
188
+ | {
189
+ type: 'focus-pane-direction'
190
+ direction: 'left' | 'right' | 'up' | 'down'
191
+ }
192
+ | {
193
+ type: 'resize-pane'
194
+ tabId: string
195
+ delta: number
196
+ axis?: SplitDirection
197
+ }
198
+ | {
199
+ type: 'set-split-ratio'
200
+ tabId: string
201
+ ratio: number
202
+ axis?: SplitDirection
203
+ }
204
+
205
+ export type UIAction =
206
+ | { type: 'toggle-bar'; side: BarSide }
207
+ | { type: 'resize-bar'; side: BarSide; delta: number }
208
+ | { type: 'set-bar-width'; side: BarSide; width: number }
209
+ | { type: 'toggle-widget'; widgetId: string }
210
+ | { type: 'move-widget'; widgetId: string; side: BarSide; index: number }
211
+ | { type: 'set-bar-boundary'; side: BarSide; index: number; ratio: number }
212
+ | { type: 'resize-widget'; widgetId: string; delta: number }
213
+ | { type: 'set-focus-mode'; focusMode: FocusMode }
214
+ | { type: 'set-terminal-size'; cols: number; rows: number }
215
+ | { type: 'resize-git-diff-pane'; delta: number }
216
+ | { type: 'set-pending-chords'; chords: string[] | null }
217
+ | { type: 'toggle-project-bar' }
218
+
219
+ export type SettingsAction =
220
+ | { type: 'enter-settings' }
221
+ | { type: 'exit-settings' }
222
+ | { type: 'settings-focus-pane'; pane: 'nav' | 'rows' }
223
+ | { type: 'settings-move-selection'; delta: -1 | 1 }
224
+ | { type: 'settings-select-section'; sectionId: string }
225
+ | { type: 'settings-select-row'; rowIndex: number }
226
+ | { type: 'open-settings-search' }
227
+ | { type: 'open-setting-text-modal'; settingId: string; label: string; value: string }
228
+
229
+ export type GitPanelAction =
230
+ | { type: 'git-refresh-success'; payload: GitRefreshPayload }
231
+ | { type: 'git-refresh-error'; kind: GitPanelError }
232
+ | { type: 'git-panel-reset' }
233
+ | { type: 'set-workspace-divergence'; divergence: Record<string, BranchDivergence> }
234
+ | { type: 'set-git-pane'; patch: Partial<GitPaneState> }
235
+
236
+ export type AutoCommitAction =
237
+ | {
238
+ type: 'auto-commit-generation-started'
239
+ projectId: string
240
+ tabId: string
241
+ workingTreeHash: string
242
+ abortController: AbortController
243
+ startedAt: number
244
+ }
245
+ | {
246
+ type: 'auto-commit-generation-ready'
247
+ projectId: string
248
+ workingTreeHash: string
249
+ title: string
250
+ body: string
251
+ generatedAt: number
252
+ }
253
+ | { type: 'auto-commit-clear'; projectId: string }
254
+
255
+ export type GitModeAction =
256
+ | { type: 'enter-git-mode' }
257
+ | { type: 'exit-git-mode' }
258
+ | { type: 'git-mode-move-selection'; delta: -1 | 1 }
259
+ | { type: 'git-mode-move-file-selection'; delta: -1 | 1 }
260
+ | { type: 'git-mode-select-entry-by-key'; key: string }
261
+ | { type: 'git-mode-toggle-folder'; key: string }
262
+ | { type: 'git-mode-toggle-selected-folder' }
263
+ | { type: 'git-mode-collapse-selection' }
264
+ | { type: 'git-mode-expand-selection' }
265
+ | { type: 'git-mode-toggle-file-list-mode' }
266
+ | { type: 'git-mode-toggle-tree-compaction' }
267
+ | { type: 'git-mode-set-diff'; key: string; diff: DiffData; hash: string }
268
+ | {
269
+ type: 'git-mode-set-parsed'
270
+ key: string
271
+ hash: string
272
+ file: unknown
273
+ }
274
+ | {
275
+ type: 'git-mode-set-highlights'
276
+ key: string
277
+ hash: string
278
+ themeId: string
279
+ add: unknown
280
+ del: unknown
281
+ }
282
+ | {
283
+ type: 'git-mode-merge-highlights'
284
+ key: string
285
+ hash: string
286
+ themeId: string
287
+ add: { start: number; tokens: unknown }[]
288
+ del: { start: number; tokens: unknown }[]
289
+ }
290
+ | { type: 'git-mode-invalidate-diffs'; paths: string[] }
291
+ | { type: 'git-mode-set-loading'; key: string; loading: boolean }
292
+ | { type: 'git-mode-set-pending-delete'; path: string | null }
293
+ | { type: 'git-mode-clear-diff-cache'; path: string }
294
+ | { type: 'git-mode-set-message'; message: string | null }
295
+ | { type: 'snippet-picker-set-message'; message: string | null }
296
+ | { type: 'git-mode-toggle-diff-view' }
297
+ | { type: 'git-mode-toggle-review-base' }
298
+ | { type: 'git-mode-shift-head-offset'; delta: number }
299
+ | { type: 'git-mode-set-head-offset'; offset: number }
300
+ | {
301
+ type: 'git-mode-fold-adjust'
302
+ key: string
303
+ foldId: string
304
+ side: 'top' | 'bottom'
305
+ delta: number
306
+ }
307
+ | { type: 'git-mode-fold-set'; key: string; foldId: string; top: number; bottom: number }
308
+ | { type: 'git-mode-fold-toggle-all'; key: string }
309
+ | {
310
+ type: 'git-mode-optimistic-move'
311
+ path: string
312
+ fromSection: GitFileSection
313
+ toSection: GitFileSection | null
314
+ }
315
+ | { type: 'open-git-commit-modal'; projectId?: string }
316
+ | { type: 'git-commit-enter-confirm' }
317
+ | { type: 'git-commit-leave-confirm' }
318
+ | { type: 'git-commit-enter-generating'; projectId: string }
319
+ | { type: 'git-commit-leave-generating' }
320
+ | { type: 'git-commit-use-background-suggestion'; projectId: string }
321
+
322
+ export type DataAction =
323
+ | { type: 'set-snippets'; snippets: SnippetRecord[] }
324
+ | { type: 'delete-snippet'; snippetId: string }
325
+ | { type: 'set-custom-commands'; customCommands: Record<AssistantId, string> }
326
+
327
+ export type MultiRepoAction =
328
+ | { type: 'multi-repo-set-repos'; repos: DiscoveredRepo[] }
329
+ | { type: 'multi-repo-clear' }
330
+
331
+ export type AppAction =
332
+ | ModalAction
333
+ | ProjectAction
334
+ | TabAction
335
+ | LayoutAction
336
+ | UIAction
337
+ | SettingsAction
338
+ | DataAction
339
+ | GitPanelAction
340
+ | GitModeAction
341
+ | AutoCommitAction
342
+ | MultiRepoAction
@@ -1,7 +1,8 @@
1
1
  import { useStore } from 'zustand'
2
2
  import { createStore } from 'zustand/vanilla'
3
3
 
4
- import type { AppAction, AppState } from './types'
4
+ import type { AppAction } from './actions'
5
+ import type { AppState } from './types'
5
6
 
6
7
  import { appReducer, createInitialState } from './store'
7
8
 
@@ -0,0 +1,75 @@
1
+ import type { BarSide, BarsState, BarState, BarWidget } from './types'
2
+
3
+ export const BAR_MIN_WIDTH = 18
4
+ export const BAR_MAX_WIDTH = 80
5
+
6
+ /** Widget ids the app knows how to render. Unknown ids are pruned on load. */
7
+ export const KNOWN_WIDGET_IDS = ['projects', 'git', 'setup'] as const
8
+
9
+ /** Smallest share of a bar a single widget may shrink to, as a fraction. */
10
+ const MIN_WIDGET_SHARE = 0.1
11
+
12
+ export function clampBarWidth(width: number): number {
13
+ return Math.min(BAR_MAX_WIDTH, Math.max(BAR_MIN_WIDTH, Math.round(width)))
14
+ }
15
+
16
+ export function visibleWidgets(bar: BarState): BarWidget[] {
17
+ return bar.widgets.filter((widget) => widget.visible)
18
+ }
19
+
20
+ /**
21
+ * The single authority on how many columns a bar occupies. Both the `Bar`
22
+ * component and the terminal-size computation must call this — a mismatch
23
+ * silently corrupts PTY columns and mouse hit-testing.
24
+ */
25
+ export function getBarWidth(bar: BarState): number {
26
+ if (!bar.visible || visibleWidgets(bar).length === 0) return 0
27
+ return clampBarWidth(bar.width)
28
+ }
29
+
30
+ export function findWidgetBar(bars: BarsState, widgetId: string): BarSide | null {
31
+ if (bars.left.widgets.some((widget) => widget.id === widgetId)) return 'left'
32
+ if (bars.right.widgets.some((widget) => widget.id === widgetId)) return 'right'
33
+ return null
34
+ }
35
+
36
+ function totalGrow(widgets: BarWidget[]): number {
37
+ return widgets.reduce((sum, widget) => sum + widget.grow, 0)
38
+ }
39
+
40
+ /**
41
+ * Move the boundary between visible widgets `index` and `index + 1` by
42
+ * `deltaGrow`. Only the pair changes, so the bar's total grow is preserved and
43
+ * no renormalisation is ever needed — including when a widget is added.
44
+ */
45
+ export function shiftBoundary(bar: BarState, index: number, deltaGrow: number): BarWidget[] {
46
+ const visible = visibleWidgets(bar)
47
+ const above = visible[index]
48
+ const below = visible[index + 1]
49
+ if (!above || !below) return bar.widgets
50
+
51
+ const min = Math.max(1, Math.round(totalGrow(visible) * MIN_WIDGET_SHARE))
52
+ const pair = above.grow + below.grow
53
+ if (pair < min * 2) return bar.widgets
54
+
55
+ const nextAbove = Math.min(pair - min, Math.max(min, Math.round(above.grow + deltaGrow)))
56
+ if (nextAbove === above.grow) return bar.widgets
57
+
58
+ return bar.widgets.map((widget) => {
59
+ if (widget.id === above.id) return { ...widget, grow: nextAbove }
60
+ if (widget.id === below.id) return { ...widget, grow: pair - nextAbove }
61
+ return widget
62
+ })
63
+ }
64
+
65
+ /**
66
+ * Convert an absolute drag position (a 0..1 fraction of the bar's body) into
67
+ * the grow delta `shiftBoundary` expects for that boundary.
68
+ */
69
+ export function boundaryDeltaFromRatio(bar: BarState, index: number, ratio: number): number {
70
+ const visible = visibleWidgets(bar)
71
+ const target = visible[index]
72
+ if (!target) return 0
73
+ const above = visible.slice(0, index).reduce((sum, widget) => sum + widget.grow, 0)
74
+ return ratio * totalGrow(visible) - above - target.grow
75
+ }
@@ -1,5 +1,5 @@
1
1
  import type { SideEffect } from '../input/modes/types'
2
- import type { AppAction } from './types'
2
+ import type { AppAction } from './actions'
3
3
 
4
4
  type DispatchFn = (action: AppAction) => void
5
5
  type SideEffectFn = (effect: SideEffect) => void
@@ -1,15 +1,6 @@
1
1
  export const GIT_PANE_MIN_RATIO = 0.2
2
2
  export const GIT_PANE_MAX_RATIO = 0.8
3
- export const GIT_PANE_MIN_WIDTH = 20
4
- export const GIT_PANE_MAX_WIDTH = 80
5
3
 
6
4
  export function clampGitPaneRatio(value: number): number {
7
5
  return Math.max(GIT_PANE_MIN_RATIO, Math.min(GIT_PANE_MAX_RATIO, value))
8
6
  }
9
-
10
- export function getGitPaneWidthFromRatio(ratio: number): number {
11
- return Math.max(
12
- GIT_PANE_MIN_WIDTH,
13
- Math.min(GIT_PANE_MAX_WIDTH, Math.round(clampGitPaneRatio(ratio) * GIT_PANE_MAX_WIDTH))
14
- )
15
- }
@@ -1,4 +1,4 @@
1
- import type { WorkspaceSnapshotV1 } from './types'
1
+ import type { ProjectSnapshotV1 } from './types'
2
2
 
3
3
  import { computePaneRects, type LayoutNode, PANE_BORDER, type PaneRect } from './layout-tree'
4
4
 
@@ -9,7 +9,7 @@ export interface TerminalBounds {
9
9
  rows: number
10
10
  }
11
11
 
12
- export function getSnapshotTrees(snapshot: WorkspaceSnapshotV1 | undefined): LayoutNode[] {
12
+ export function getSnapshotTrees(snapshot: ProjectSnapshotV1 | undefined): LayoutNode[] {
13
13
  if (snapshot?.layoutTrees) {
14
14
  return Object.values(snapshot.layoutTrees)
15
15
  }
@@ -0,0 +1,39 @@
1
+ import { useStore } from 'zustand'
2
+ import { createStore } from 'zustand/vanilla'
3
+
4
+ import type { PrStatusResult } from '../git/pr-status'
5
+
6
+ export interface PrStatusState {
7
+ result: PrStatusResult | null
8
+ /** True once a fetch failed but we are still showing the previous good result. */
9
+ stale: boolean
10
+ setResult: (result: PrStatusResult) => void
11
+ reset: () => void
12
+ }
13
+
14
+ export const prStatusStore = createStore<PrStatusState>((set) => ({
15
+ reset: () => set({ result: null, stale: false }),
16
+ result: null,
17
+ setResult: (result: PrStatusResult) =>
18
+ set((state) => {
19
+ // A transient `gh` failure shouldn't blank a PR we already resolved — keep
20
+ // the last good snapshot and mark it stale instead (same contract as
21
+ // ai-usage-store's setSnapshot).
22
+ if (result.kind === 'error' && state.result?.kind === 'ok') return { stale: true }
23
+ return { result, stale: false }
24
+ }),
25
+ stale: false,
26
+ }))
27
+
28
+ /**
29
+ * The PR state row occupies its band both while the first fetch is in flight
30
+ * and once it resolved to a PR — anything else (no PR, no gh, error) gives the
31
+ * row back. Shared so the header and the row itself can never disagree and
32
+ * shift the layout under the user.
33
+ */
34
+ export const selectPrRowVisible = (state: PrStatusState): boolean =>
35
+ state.result === null || state.result.kind === 'ok'
36
+
37
+ export function usePrStatusStore<T>(selector: (state: PrStatusState) => T): T {
38
+ return useStore(prStatusStore, selector)
39
+ }