@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.
- package/README.md +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +305 -1589
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/split-drag-controller.ts +4 -8
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +28 -42
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +28 -33
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +102 -85
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +149 -101
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-merge.ts +64 -0
- package/src/git/pr-status-poller.ts +57 -0
- package/src/git/pr-status.ts +227 -0
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +11 -6
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/open-url.ts +39 -0
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +3 -1
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +75 -0
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/git-pane-sizing.ts +0 -9
- package/src/state/layout-resize.ts +2 -2
- package/src/state/pr-status-store.ts +39 -0
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +77 -73
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -53
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +86 -15
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +83 -57
- package/src/state/types.ts +261 -417
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
- package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
- package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
- package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
- package/src/ui/components/layout/bar.tsx +191 -0
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +48 -37
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +164 -230
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +21 -0
- package/src/ui/widgets/widget-context-menu.ts +85 -0
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -45
- package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
- package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AppAction
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
import type { AppState, TabSession } from '../types'
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
allLeafIds,
|
|
@@ -14,12 +15,13 @@ import {
|
|
|
14
15
|
setSplitRatio,
|
|
15
16
|
splitNode,
|
|
16
17
|
} from '../layout-tree'
|
|
17
|
-
import { normalizeGroupedTabOrder } from '../
|
|
18
|
+
import { normalizeGroupedTabOrder } from '../project-persistence'
|
|
18
19
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
clearWorkspaceDone,
|
|
21
|
+
filterTabsForActiveWorkspace,
|
|
22
|
+
orderTabsByWorkspace,
|
|
23
|
+
withActiveWorkspace,
|
|
24
|
+
} from '../project-workspaces'
|
|
23
25
|
import { createDefaultTerminalModes } from '../terminal-modes'
|
|
24
26
|
|
|
25
27
|
const MAX_BUFFER_LENGTH = 50_000
|
|
@@ -197,47 +199,61 @@ function getActiveIndex(state: AppState): number {
|
|
|
197
199
|
return state.tabs.findIndex((tab) => tab.id === state.activeTabId)
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
function
|
|
201
|
-
return state.
|
|
202
|
-
? state.
|
|
202
|
+
function getCurrentProject(state: AppState) {
|
|
203
|
+
return state.currentProjectId != null && state.currentProjectId !== ''
|
|
204
|
+
? state.projects.find((project) => project.id === state.currentProjectId)
|
|
203
205
|
: undefined
|
|
204
206
|
}
|
|
205
207
|
|
|
206
|
-
function
|
|
208
|
+
function withActiveTabWorkspace(
|
|
207
209
|
state: AppState,
|
|
208
210
|
tabId: string | null,
|
|
209
211
|
opts?: { onlyIfMissing?: boolean }
|
|
210
212
|
): AppState {
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
const tab =
|
|
218
|
-
if (!(tab?.
|
|
219
|
-
const
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// a
|
|
223
|
-
|
|
213
|
+
if (tabId == null || tabId === '') return state
|
|
214
|
+
// Opening the tab is reading its notification, whatever else this helper
|
|
215
|
+
// decides about the workspace. Every path that makes a tab the active one
|
|
216
|
+
// goes through here.
|
|
217
|
+
const seen = { ...state, tabs: clearTabUnseen(state.tabs, tabId) }
|
|
218
|
+
if (!(seen.currentProjectId != null && seen.currentProjectId !== '')) return seen
|
|
219
|
+
const tab = seen.tabs.find((entry) => entry.id === tabId)
|
|
220
|
+
if (!(tab?.workspaceId != null && tab?.workspaceId !== '')) return seen
|
|
221
|
+
const workspaceId = tab.workspaceId
|
|
222
|
+
// Opening one of a workspace's tabs is seeing that workspace, so it drops the
|
|
223
|
+
// finished-a-turn tick — otherwise a background tab finishing in the workspace
|
|
224
|
+
// you are already in leaves a ✓ that only a workspace switch could clear.
|
|
225
|
+
const workspaceActivity = clearWorkspaceDone(seen.workspaceActivity, workspaceId)
|
|
226
|
+
// When `onlyIfMissing` is set, we leave the project's workspace alone if it
|
|
227
|
+
// already points at a known workspace. Used by `hydrate-project` so that
|
|
228
|
+
// a backend re-attach after a user-initiated workspace switch (j/k cycling)
|
|
229
|
+
// doesn't clobber the freshly chosen workspace by re-syncing from the
|
|
224
230
|
// restored active tab.
|
|
225
231
|
if (opts?.onlyIfMissing === true) {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
(
|
|
231
|
-
if (
|
|
232
|
+
const project = seen.projects.find((entry) => entry.id === seen.currentProjectId)
|
|
233
|
+
const hasValidWorkspace =
|
|
234
|
+
project?.activeWorkspaceId != null &&
|
|
235
|
+
project.activeWorkspaceId !== '' &&
|
|
236
|
+
(project.workspaces?.some((w) => w.id === project.activeWorkspaceId) ?? false)
|
|
237
|
+
if (hasValidWorkspace) return { ...seen, workspaceActivity }
|
|
232
238
|
}
|
|
233
239
|
return {
|
|
234
|
-
...
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
...seen,
|
|
241
|
+
projects: seen.projects.map((project) =>
|
|
242
|
+
project.id === seen.currentProjectId ? withActiveWorkspace(project, workspaceId) : project
|
|
237
243
|
),
|
|
244
|
+
workspaceActivity,
|
|
238
245
|
}
|
|
239
246
|
}
|
|
240
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Drop one tab's unseen mark. Returns the same array when it holds none, so a
|
|
250
|
+
* caller can run it on every activation without re-rendering the tab list.
|
|
251
|
+
*/
|
|
252
|
+
function clearTabUnseen(tabs: TabSession[], tabId: string): TabSession[] {
|
|
253
|
+
if (!tabs.some((tab) => tab.id === tabId && tab.unseen === true)) return tabs
|
|
254
|
+
return tabs.map((tab) => (tab.id === tabId ? { ...tab, unseen: undefined } : tab))
|
|
255
|
+
}
|
|
256
|
+
|
|
241
257
|
function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
242
258
|
if (indexToClose < 0 || indexToClose >= state.tabs.length) {
|
|
243
259
|
return state
|
|
@@ -262,7 +278,12 @@ function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
|
262
278
|
getAdjacentLeaf(groupTree, closingTabId, 'down') ??
|
|
263
279
|
getAdjacentLeaf(groupTree, closingTabId, 'up'))
|
|
264
280
|
: null
|
|
265
|
-
|
|
281
|
+
// Positional fallbacks must skip hidden tabs: landing `activeTabId` on one
|
|
282
|
+
// would render a pane the user cannot see in any tab bar.
|
|
283
|
+
const positional = [tabs[indexToClose], tabs[indexToClose - 1]].find(
|
|
284
|
+
(tab) => tab !== undefined && tab.hidden !== true
|
|
285
|
+
)
|
|
286
|
+
nextActiveTabId = layoutNeighbor ?? positional?.id ?? null
|
|
266
287
|
} else {
|
|
267
288
|
nextActiveTabId = tabs.find((tab) => tab.id === state.activeTabId)?.id ?? null
|
|
268
289
|
}
|
|
@@ -292,7 +313,7 @@ function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
|
292
313
|
}
|
|
293
314
|
}
|
|
294
315
|
|
|
295
|
-
return
|
|
316
|
+
return withActiveTabWorkspace(
|
|
296
317
|
{
|
|
297
318
|
...state,
|
|
298
319
|
activeTabId: nextActiveTabId,
|
|
@@ -309,35 +330,41 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
309
330
|
switch (action.type) {
|
|
310
331
|
case 'add-tab': {
|
|
311
332
|
const newTab = { ...action.tab, activity: action.tab.activity ?? 'idle' }
|
|
312
|
-
|
|
333
|
+
// A hidden tab is chrome-invisible, so adding it must be invisible too:
|
|
334
|
+
// no focus steal, no modal dismissal, and — critically — no workspace
|
|
335
|
+
// re-sync, which would teleport a user sitting on another workspace.
|
|
336
|
+
if (newTab.hidden === true) {
|
|
337
|
+
return { ...state, tabs: [...state.tabs, newTab] }
|
|
338
|
+
}
|
|
339
|
+
return withActiveTabWorkspace(
|
|
313
340
|
{
|
|
314
341
|
...state,
|
|
315
342
|
activeTabId: newTab.id,
|
|
316
343
|
focusMode: 'navigation',
|
|
317
|
-
modal: { editBuffer: null,
|
|
344
|
+
modal: { editBuffer: null, projectTargetId: null, selectedIndex: 0, type: null },
|
|
318
345
|
tabs: [...state.tabs, newTab],
|
|
319
346
|
},
|
|
320
347
|
newTab.id
|
|
321
348
|
)
|
|
322
349
|
}
|
|
323
|
-
case 'hydrate-
|
|
324
|
-
// The
|
|
350
|
+
case 'hydrate-project': {
|
|
351
|
+
// The project's activeWorkspaceId may have just been switched by the
|
|
325
352
|
// user (j/k cycle, sidebar click) before the backend attached. Honor
|
|
326
|
-
// that choice by only considering tabs visible in that
|
|
353
|
+
// that choice by only considering tabs visible in that workspace —
|
|
327
354
|
// otherwise we'd land the active tab on whatever the backend picked
|
|
328
|
-
// (typically the
|
|
329
|
-
const
|
|
330
|
-
state.
|
|
331
|
-
? state.
|
|
355
|
+
// (typically the workspace we *last* persisted, not the current one).
|
|
356
|
+
const currentProject =
|
|
357
|
+
state.currentProjectId != null && state.currentProjectId !== ''
|
|
358
|
+
? state.projects.find((s) => s.id === state.currentProjectId)
|
|
332
359
|
: undefined
|
|
333
|
-
const
|
|
334
|
-
const visibleIds = new Set(
|
|
360
|
+
const visibleForWorkspace = filterTabsForActiveWorkspace(action.tabs, currentProject)
|
|
361
|
+
const visibleIds = new Set(visibleForWorkspace.map((t) => t.id))
|
|
335
362
|
const hydratedActiveTabId =
|
|
336
363
|
action.activeTabId != null &&
|
|
337
364
|
action.activeTabId !== '' &&
|
|
338
365
|
visibleIds.has(action.activeTabId)
|
|
339
366
|
? action.activeTabId
|
|
340
|
-
: (
|
|
367
|
+
: (visibleForWorkspace[0]?.id ?? null)
|
|
341
368
|
const tabIds = new Set(action.tabs.map((t) => t.id))
|
|
342
369
|
|
|
343
370
|
// Restore from new multi-tree format or migrate from legacy single tree
|
|
@@ -368,7 +395,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
368
395
|
}
|
|
369
396
|
}
|
|
370
397
|
|
|
371
|
-
return
|
|
398
|
+
return withActiveTabWorkspace(
|
|
372
399
|
{
|
|
373
400
|
...state,
|
|
374
401
|
activeTabId: hydratedActiveTabId,
|
|
@@ -389,24 +416,24 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
389
416
|
case 'close-active-tab':
|
|
390
417
|
return closeTabAtIndex(state, getActiveIndex(state))
|
|
391
418
|
case 'set-active-tab':
|
|
392
|
-
return
|
|
419
|
+
return withActiveTabWorkspace({ ...state, activeTabId: action.tabId }, action.tabId)
|
|
393
420
|
case 'move-active-tab': {
|
|
394
421
|
if (state.tabs.length === 0) {
|
|
395
422
|
return state
|
|
396
423
|
}
|
|
397
|
-
const
|
|
398
|
-
const visibleTabs =
|
|
424
|
+
const project = getCurrentProject(state)
|
|
425
|
+
const visibleTabs = filterTabsForActiveWorkspace(state.tabs, project)
|
|
399
426
|
if (visibleTabs.length === 0) {
|
|
400
427
|
return state
|
|
401
428
|
}
|
|
402
|
-
const orderedTabs =
|
|
429
|
+
const orderedTabs = orderTabsByWorkspace(visibleTabs, project)
|
|
403
430
|
const currentIndex = orderedTabs.findIndex((tab) => tab.id === state.activeTabId)
|
|
404
431
|
const safeIndex = currentIndex === -1 ? 0 : currentIndex
|
|
405
432
|
const nextIndex = (safeIndex + action.delta + orderedTabs.length) % orderedTabs.length
|
|
406
433
|
const nextTabId = orderedTabs[nextIndex]?.id
|
|
407
434
|
return nextTabId == null || nextTabId === '' || nextTabId === state.activeTabId
|
|
408
435
|
? state
|
|
409
|
-
:
|
|
436
|
+
: withActiveTabWorkspace({ ...state, activeTabId: nextTabId }, nextTabId)
|
|
410
437
|
}
|
|
411
438
|
case 'reorder-active-tab': {
|
|
412
439
|
const activeIndex = getActiveIndex(state)
|
|
@@ -436,8 +463,14 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
436
463
|
return { ...state, tabs }
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
// Standalone tab reorder
|
|
440
|
-
|
|
466
|
+
// Standalone tab reorder. Step over hidden tabs rather than swapping with
|
|
467
|
+
// one: the swap would look like a dead keypress and shuffle the hidden
|
|
468
|
+
// tab out of its append-order slot.
|
|
469
|
+
let nextIndex = activeIndex + action.delta
|
|
470
|
+
const step = action.delta < 0 ? -1 : 1
|
|
471
|
+
while (state.tabs[nextIndex]?.hidden === true) {
|
|
472
|
+
nextIndex += step
|
|
473
|
+
}
|
|
441
474
|
if (nextIndex < 0 || nextIndex >= state.tabs.length) {
|
|
442
475
|
return state
|
|
443
476
|
}
|
|
@@ -473,7 +506,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
473
506
|
// Drag-and-drop reorder of the visible tab strip. `orderedTabIds` is the
|
|
474
507
|
// flattened new order of the currently-visible tabs (group members already
|
|
475
508
|
// expanded into contiguous runs). We only rewrite the slots those tabs
|
|
476
|
-
// occupy in `state.tabs`, leaving tabs from other
|
|
509
|
+
// occupy in `state.tabs`, leaving tabs from other workspaces anchored in
|
|
477
510
|
// place. Because group members arrive contiguous, splits stay intact.
|
|
478
511
|
const visibleSet = new Set(action.orderedTabIds)
|
|
479
512
|
const byId = new Map(state.tabs.map((tab) => [tab.id, tab]))
|
|
@@ -486,8 +519,8 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
486
519
|
})
|
|
487
520
|
return { ...state, tabs }
|
|
488
521
|
}
|
|
489
|
-
case 'reset-tab-
|
|
490
|
-
return
|
|
522
|
+
case 'reset-tab-project':
|
|
523
|
+
return withActiveTabWorkspace(
|
|
491
524
|
{
|
|
492
525
|
...state,
|
|
493
526
|
activeTabId: action.tabId,
|
|
@@ -527,7 +560,20 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
527
560
|
case 'set-tab-activity':
|
|
528
561
|
return {
|
|
529
562
|
...state,
|
|
530
|
-
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
563
|
+
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
564
|
+
...tab,
|
|
565
|
+
activity: action.activity,
|
|
566
|
+
// Back to work: whatever it wanted to tell you is out of date.
|
|
567
|
+
unseen:
|
|
568
|
+
action.activity === 'working' || action.activity === 'waiting-input'
|
|
569
|
+
? undefined
|
|
570
|
+
: tab.unseen,
|
|
571
|
+
})),
|
|
572
|
+
}
|
|
573
|
+
case 'mark-tab-unseen':
|
|
574
|
+
return {
|
|
575
|
+
...state,
|
|
576
|
+
tabs: updateTab(state.tabs, action.tabId, (tab) => ({ ...tab, unseen: true })),
|
|
531
577
|
}
|
|
532
578
|
case 'set-tab-error':
|
|
533
579
|
return {
|
|
@@ -555,6 +601,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
555
601
|
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
556
602
|
...tab,
|
|
557
603
|
autoRenameStatus: action.autoRenameStatus ?? tab.autoRenameStatus,
|
|
604
|
+
hidden: action.hidden ?? tab.hidden,
|
|
558
605
|
title: action.title ?? tab.title,
|
|
559
606
|
})),
|
|
560
607
|
}
|
|
@@ -590,7 +637,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
590
637
|
}
|
|
591
638
|
const tabs = [...state.tabs.slice(0, insertIndex), newTab, ...state.tabs.slice(insertIndex)]
|
|
592
639
|
|
|
593
|
-
return
|
|
640
|
+
return withActiveTabWorkspace(
|
|
594
641
|
{
|
|
595
642
|
...state,
|
|
596
643
|
activeTabId: newTab.id,
|
|
@@ -621,7 +668,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
621
668
|
if (!(neighbor != null && neighbor !== '')) {
|
|
622
669
|
return state
|
|
623
670
|
}
|
|
624
|
-
return
|
|
671
|
+
return withActiveTabWorkspace({ ...state, activeTabId: neighbor }, neighbor)
|
|
625
672
|
}
|
|
626
673
|
case 'resize-pane': {
|
|
627
674
|
const resizeGroupId = getGroupIdForTab(state.tabGroupMap, action.tabId)
|
|
@@ -1,21 +1,92 @@
|
|
|
1
|
-
import type { AppAction
|
|
1
|
+
import type { AppAction } from '../actions'
|
|
2
|
+
import type { AppState, BarSide, BarState, BarWidget } from '../types'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
boundaryDeltaFromRatio,
|
|
6
|
+
clampBarWidth,
|
|
7
|
+
findWidgetBar,
|
|
8
|
+
shiftBoundary,
|
|
9
|
+
visibleWidgets,
|
|
10
|
+
} from '../bars'
|
|
11
|
+
|
|
12
|
+
function withBar(state: AppState, side: BarSide, next: BarState): AppState {
|
|
13
|
+
if (next === state.bars[side]) return state
|
|
14
|
+
return { ...state, bars: { ...state.bars, [side]: next } }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function setBarWidth(state: AppState, side: BarSide, width: number): AppState {
|
|
18
|
+
const bar = state.bars[side]
|
|
19
|
+
const next = clampBarWidth(width)
|
|
20
|
+
if (next === bar.width) return state
|
|
21
|
+
return withBar(state, side, { ...bar, width: next })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Move `widgetId` to `side` at `index`. Covers both cross-bar moves and
|
|
26
|
+
* in-bar reordering — removing then re-inserting is the same operation.
|
|
27
|
+
*/
|
|
28
|
+
function moveWidget(state: AppState, widgetId: string, side: BarSide, index: number): AppState {
|
|
29
|
+
const from = findWidgetBar(state.bars, widgetId)
|
|
30
|
+
if (from === null) return state
|
|
31
|
+
const widget = state.bars[from].widgets.find((w) => w.id === widgetId)
|
|
32
|
+
if (!widget) return state
|
|
33
|
+
|
|
34
|
+
const source = state.bars[from].widgets.filter((w) => w.id !== widgetId)
|
|
35
|
+
const target: BarWidget[] = from === side ? source : [...state.bars[side].widgets]
|
|
36
|
+
const at = Math.max(0, Math.min(target.length, index))
|
|
37
|
+
target.splice(at, 0, widget)
|
|
38
|
+
|
|
39
|
+
const bars = { ...state.bars }
|
|
40
|
+
bars[from] = { ...state.bars[from], widgets: from === side ? target : source }
|
|
41
|
+
// Showing a widget in a hidden bar would silently swallow it.
|
|
42
|
+
bars[side] = { ...bars[side], visible: bars[side].visible || widget.visible, widgets: target }
|
|
43
|
+
return { ...state, bars }
|
|
44
|
+
}
|
|
2
45
|
|
|
3
46
|
export function reduceUIState(state: AppState, action: AppAction): AppState | null {
|
|
4
47
|
switch (action.type) {
|
|
5
|
-
case 'toggle-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
48
|
+
case 'toggle-bar': {
|
|
49
|
+
const bar = state.bars[action.side]
|
|
50
|
+
return withBar(state, action.side, { ...bar, visible: !bar.visible })
|
|
51
|
+
}
|
|
52
|
+
case 'resize-bar':
|
|
53
|
+
return setBarWidth(state, action.side, state.bars[action.side].width + action.delta)
|
|
54
|
+
case 'set-bar-width':
|
|
55
|
+
return setBarWidth(state, action.side, action.width)
|
|
56
|
+
case 'toggle-widget': {
|
|
57
|
+
const side = findWidgetBar(state.bars, action.widgetId)
|
|
58
|
+
if (side === null) return state
|
|
59
|
+
const bar = state.bars[side]
|
|
60
|
+
const widgets = bar.widgets.map((w) =>
|
|
61
|
+
w.id === action.widgetId ? { ...w, visible: !w.visible } : w
|
|
11
62
|
)
|
|
12
|
-
|
|
13
|
-
return { ...
|
|
63
|
+
const revealed = widgets.some((w) => w.id === action.widgetId && w.visible)
|
|
64
|
+
return withBar(state, side, { ...bar, visible: bar.visible || revealed, widgets })
|
|
65
|
+
}
|
|
66
|
+
case 'move-widget':
|
|
67
|
+
return moveWidget(state, action.widgetId, action.side, action.index)
|
|
68
|
+
case 'set-bar-boundary': {
|
|
69
|
+
const bar = state.bars[action.side]
|
|
70
|
+
const delta = boundaryDeltaFromRatio(bar, action.index, action.ratio)
|
|
71
|
+
const widgets = shiftBoundary(bar, action.index, delta)
|
|
72
|
+
if (widgets === bar.widgets) return state
|
|
73
|
+
return withBar(state, action.side, { ...bar, widgets })
|
|
14
74
|
}
|
|
15
|
-
case '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
75
|
+
case 'resize-widget': {
|
|
76
|
+
// Keyboard resize: grow/shrink this widget against its neighbour. The
|
|
77
|
+
// widget below owns the boundary above it, so the sign flips there.
|
|
78
|
+
const side = findWidgetBar(state.bars, action.widgetId)
|
|
79
|
+
if (side === null) return state
|
|
80
|
+
const bar = state.bars[side]
|
|
81
|
+
const visible = visibleWidgets(bar)
|
|
82
|
+
const at = visible.findIndex((w) => w.id === action.widgetId)
|
|
83
|
+
if (at === -1) return state
|
|
84
|
+
const total = visible.reduce((sum, w) => sum + w.grow, 0)
|
|
85
|
+
const index = at > 0 ? at - 1 : at
|
|
86
|
+
const deltaGrow = action.delta * total * (at > 0 ? -1 : 1)
|
|
87
|
+
const widgets = shiftBoundary(bar, index, deltaGrow)
|
|
88
|
+
if (widgets === bar.widgets) return state
|
|
89
|
+
return withBar(state, side, { ...bar, widgets })
|
|
19
90
|
}
|
|
20
91
|
case 'set-focus-mode':
|
|
21
92
|
return { ...state, focusMode: action.focusMode }
|
|
@@ -29,10 +100,10 @@ export function reduceUIState(state: AppState, action: AppAction): AppState | nu
|
|
|
29
100
|
return state
|
|
30
101
|
}
|
|
31
102
|
return { ...state, pendingChords: action.chords }
|
|
32
|
-
case 'toggle-
|
|
103
|
+
case 'toggle-project-bar':
|
|
33
104
|
return {
|
|
34
105
|
...state,
|
|
35
|
-
|
|
106
|
+
projectBar: { ...state.projectBar, visible: !state.projectBar.visible },
|
|
36
107
|
}
|
|
37
108
|
default:
|
|
38
109
|
return null
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,38 +1,39 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { ProjectRecord, SnippetRecord, WorkspaceRecord } from './types'
|
|
2
|
+
|
|
3
|
+
import { type AssistantOption, getAllAssistantOptions } from '../pty/command-registry'
|
|
3
4
|
|
|
4
5
|
export interface BaseRefOption {
|
|
5
|
-
/** Git ref the new
|
|
6
|
+
/** Git ref the new workspace is forked from. */
|
|
6
7
|
ref: string
|
|
7
8
|
label: string
|
|
8
|
-
kind: '
|
|
9
|
-
/**
|
|
9
|
+
kind: 'workspace' | 'branch'
|
|
10
|
+
/** Workspace name, for the 'workspace' kind. */
|
|
10
11
|
detail?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Ordered, filtered base-ref candidates for the
|
|
15
|
-
* branches checked out in the
|
|
16
|
-
*
|
|
17
|
-
* via a
|
|
18
|
-
* a live
|
|
19
|
-
* so deleted temp
|
|
15
|
+
* Ordered, filtered base-ref candidates for the workspace-create "Base" picker:
|
|
16
|
+
* branches checked out in the project's workspaces first (labelled with the
|
|
17
|
+
* workspace name), then the remaining local branches. A branch already surfaced
|
|
18
|
+
* via a workspace is not repeated. Throwaway `aimux/` branches are skipped unless
|
|
19
|
+
* a live workspace is on them — `git worktree remove` leaves the branch behind,
|
|
20
|
+
* so deleted temp workspaces would otherwise haunt the list as orphan branches.
|
|
20
21
|
*/
|
|
21
22
|
export function buildBaseRefOptions(
|
|
22
|
-
|
|
23
|
+
workspaces: WorkspaceRecord[],
|
|
23
24
|
localBranches: string[],
|
|
24
25
|
query: string
|
|
25
26
|
): BaseRefOption[] {
|
|
26
27
|
const seen = new Set<string>()
|
|
27
28
|
const options: BaseRefOption[] = []
|
|
28
|
-
for (const
|
|
29
|
-
if (
|
|
30
|
-
seen.add(
|
|
29
|
+
for (const workspace of workspaces) {
|
|
30
|
+
if (workspace.branch == null || workspace.branch === '' || seen.has(workspace.branch)) continue
|
|
31
|
+
seen.add(workspace.branch)
|
|
31
32
|
options.push({
|
|
32
|
-
detail:
|
|
33
|
-
kind: '
|
|
34
|
-
label:
|
|
35
|
-
ref:
|
|
33
|
+
detail: workspace.name,
|
|
34
|
+
kind: 'workspace',
|
|
35
|
+
label: workspace.branch,
|
|
36
|
+
ref: workspace.branch,
|
|
36
37
|
})
|
|
37
38
|
}
|
|
38
39
|
for (const branch of localBranches) {
|
|
@@ -45,16 +46,6 @@ export function buildBaseRefOptions(
|
|
|
45
46
|
return options.filter((option) => option.label.toLowerCase().includes(trimmed))
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
* 0 when the template picker should NOT show the "None" fallback (no assistant
|
|
50
|
-
* was picked — typical for the template shortcut path), 1 otherwise. Shared
|
|
51
|
-
* by the modal reducer, the side-effect that resolves templateId, and the
|
|
52
|
-
* picker UI so the three stay in sync.
|
|
53
|
-
*/
|
|
54
|
-
export function getTemplateNoneOffset(selectedAssistantId: AssistantId | null): 0 | 1 {
|
|
55
|
-
return selectedAssistantId == null ? 0 : 1
|
|
56
|
-
}
|
|
57
|
-
|
|
58
49
|
export function filterAssistants(
|
|
59
50
|
options: AssistantOption[],
|
|
60
51
|
filter: string | null
|
|
@@ -66,18 +57,39 @@ export function filterAssistants(
|
|
|
66
57
|
)
|
|
67
58
|
}
|
|
68
59
|
|
|
69
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Assistants offered by the new-tab picker. One source of truth on purpose: the
|
|
62
|
+
* modal renders this list while `launch-selected-assistant` indexes into it, so
|
|
63
|
+
* a divergence launches the row above or below the one the user highlighted.
|
|
64
|
+
*
|
|
65
|
+
* Chained from `<C-p>`, the prompt is pasted into the assistant and drives the
|
|
66
|
+
* workspace's name — neither of which a plain shell can do — so Terminal is not
|
|
67
|
+
* offered there. A bare `<C-n>` still lists it.
|
|
68
|
+
*/
|
|
69
|
+
export function getNewTabAssistantOptions(
|
|
70
|
+
customCommands: Record<string, string>,
|
|
71
|
+
filter: string | null,
|
|
72
|
+
excludeTerminal: boolean
|
|
73
|
+
): AssistantOption[] {
|
|
74
|
+
const all = getAllAssistantOptions(customCommands)
|
|
75
|
+
return filterAssistants(
|
|
76
|
+
excludeTerminal ? all.filter((option) => option.id !== 'terminal') : all,
|
|
77
|
+
filter
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function filterProjects(projects: ProjectRecord[], filter: string | null): ProjectRecord[] {
|
|
70
82
|
if (!(filter != null && filter !== '')) {
|
|
71
|
-
return
|
|
83
|
+
return projects
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
const lower = filter.toLowerCase()
|
|
75
|
-
return
|
|
76
|
-
(
|
|
77
|
-
|
|
78
|
-
(
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
return projects.filter(
|
|
88
|
+
(project) =>
|
|
89
|
+
project.name.toLowerCase().includes(lower) ||
|
|
90
|
+
(project.projectPath != null &&
|
|
91
|
+
project.projectPath !== '' &&
|
|
92
|
+
project.projectPath.toLowerCase().includes(lower))
|
|
81
93
|
)
|
|
82
94
|
}
|
|
83
95
|
|