@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { AppState, ProjectRecord, TabSession, WorkspaceRecord } from '../state/types'
|
|
4
|
+
import type { SideEffectContext } from './side-effect-context'
|
|
5
|
+
|
|
6
|
+
import { hasSetupScript } from '../state/project-data'
|
|
7
|
+
import { findSetupTab, runSetup } from './setup-actions'
|
|
8
|
+
|
|
9
|
+
export interface SetupCandidate {
|
|
10
|
+
projectId: string
|
|
11
|
+
workspace: WorkspaceRecord
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Which workspaces should have their setup script run right now.
|
|
16
|
+
*
|
|
17
|
+
* The trigger is "a workspace id this session has not seen before", not "a
|
|
18
|
+
* workspace became active". Activation is the wrong signal: `j`/`k` cycles
|
|
19
|
+
* workspaces, so an activation-based rule would fire the script in every
|
|
20
|
+
* workspace the user scrolls past. Watching the id set also means one rule
|
|
21
|
+
* covers both creation paths — the TUI's `<C-p>` and `aimux workspace create`
|
|
22
|
+
* arriving over the daemon — instead of a hook per call site, which is how the
|
|
23
|
+
* CLI path ends up silently skipping setup.
|
|
24
|
+
*
|
|
25
|
+
* Mutates `seenByProject`: every id examined is marked seen, so a workspace is
|
|
26
|
+
* only ever a candidate once.
|
|
27
|
+
*/
|
|
28
|
+
export function collectSetupCandidates(
|
|
29
|
+
projects: ProjectRecord[],
|
|
30
|
+
tabs: TabSession[],
|
|
31
|
+
seenByProject: Map<string, Set<string>>,
|
|
32
|
+
hasScript: (projectId: string) => boolean
|
|
33
|
+
): SetupCandidate[] {
|
|
34
|
+
const candidates: SetupCandidate[] = []
|
|
35
|
+
|
|
36
|
+
for (const project of projects) {
|
|
37
|
+
const seen = seenByProject.get(project.id)
|
|
38
|
+
const workspaces = project.workspaces ?? []
|
|
39
|
+
|
|
40
|
+
// First sight of a project: record what it already has and run nothing.
|
|
41
|
+
// Without this, switching to a project surfaces N pre-existing workspaces
|
|
42
|
+
// at once and would fire N setups.
|
|
43
|
+
if (!seen) {
|
|
44
|
+
seenByProject.set(project.id, new Set(workspaces.map((w) => w.id)))
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const workspace of workspaces) {
|
|
49
|
+
if (seen.has(workspace.id)) continue
|
|
50
|
+
seen.add(workspace.id)
|
|
51
|
+
|
|
52
|
+
// The primary workspace *is* the user's real repo checkout. Running a
|
|
53
|
+
// script that does `cp .env.example .env` over it would clobber their
|
|
54
|
+
// actual .env.
|
|
55
|
+
if (workspace.source === 'primary') continue
|
|
56
|
+
if (!hasScript(project.id)) continue
|
|
57
|
+
if (findSetupTab(tabs, workspace.id)) continue
|
|
58
|
+
|
|
59
|
+
candidates.push({ projectId: project.id, workspace })
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return candidates
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runs a project's setup script once per newly created workspace.
|
|
68
|
+
*
|
|
69
|
+
* ponytail: a workspace created by the CLI while the app is closed is seeded as
|
|
70
|
+
* pre-existing on the next boot and never auto-runs. The widget's Run button
|
|
71
|
+
* covers it; compare createdAt against setupRanAt if that turns out to matter.
|
|
72
|
+
*
|
|
73
|
+
* The outcome is recorded by `recordSetupExitIfHidden`, called from the
|
|
74
|
+
* backend's exit event, which already carries the exit code.
|
|
75
|
+
*/
|
|
76
|
+
export function useSetupRunner(state: AppState, ctx: SideEffectContext): void {
|
|
77
|
+
const seenByProject = useRef(new Map<string, Set<string>>())
|
|
78
|
+
const ctxRef = useRef(ctx)
|
|
79
|
+
ctxRef.current = ctx
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
// Tabs are read fresh rather than taken as a dependency: their array
|
|
83
|
+
// identity changes on every viewport frame, which would re-run this at the
|
|
84
|
+
// rate the PTYs print. The store is also the more correct source — the
|
|
85
|
+
// render snapshot can lag a setup tab that was just added.
|
|
86
|
+
const candidates = collectSetupCandidates(
|
|
87
|
+
state.projects,
|
|
88
|
+
ctxRef.current.getState().tabs,
|
|
89
|
+
seenByProject.current,
|
|
90
|
+
hasSetupScript
|
|
91
|
+
)
|
|
92
|
+
for (const { projectId, workspace } of candidates) {
|
|
93
|
+
runSetup(ctxRef.current, projectId, workspace)
|
|
94
|
+
}
|
|
95
|
+
}, [state.projects])
|
|
96
|
+
}
|
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
|
|
11
11
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
12
12
|
import type { SessionBackend } from '../session-backend/types'
|
|
13
|
-
import type { AppAction
|
|
13
|
+
import type { AppAction } from '../state/actions'
|
|
14
|
+
import type { AppState } from '../state/types'
|
|
14
15
|
import type { MeasuredPaneRect } from './use-pane-size-report'
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
+
import { getBarWidth } from '../state/bars'
|
|
17
18
|
import {
|
|
18
19
|
createTerminalBounds,
|
|
19
20
|
forEachSplitPaneRect,
|
|
@@ -151,7 +152,10 @@ export function useTerminalResize({
|
|
|
151
152
|
const handledBySyncRef = useRef(false)
|
|
152
153
|
const sidebarMountedRef = useRef(false)
|
|
153
154
|
|
|
154
|
-
|
|
155
|
+
// Hidden tabs are excluded from the open-loop cascade: their host (the setup
|
|
156
|
+
// widget) measures its own box, and the cascade would overwrite that with the
|
|
157
|
+
// main terminal area's size on every terminal or bar resize.
|
|
158
|
+
const currentTabIds = state.tabs.filter((t) => t.hidden !== true).map((t) => t.id)
|
|
155
159
|
const tabIdsChanged =
|
|
156
160
|
currentTabIds.length !== tabIdsRef.current.length ||
|
|
157
161
|
currentTabIds.some((id, i) => id !== tabIdsRef.current[i])
|
|
@@ -179,15 +183,15 @@ export function useTerminalResize({
|
|
|
179
183
|
// loop so an unchanged box never re-triggers a resize.
|
|
180
184
|
const measuredRef = useRef(new Map<string, { cols: number; rows: number }>())
|
|
181
185
|
|
|
182
|
-
// Drop the dedupe cache when the
|
|
186
|
+
// Drop the dedupe cache when the project changes. attach() re-gates every
|
|
183
187
|
// restored tab's paneReady to false; if a tabId carries the same dimensions
|
|
184
|
-
// across
|
|
188
|
+
// across projects, handleMeasure's prev-match would short-circuit and
|
|
185
189
|
// never call resizeTab({confirmedFromMeasurement:true}), leaving the gate
|
|
186
190
|
// closed forever — the new pane then paints a stale buffered snapshot from
|
|
187
|
-
// the old
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
+
// the old project, which is the 2–3 row offset users see.
|
|
192
|
+
const lastProjectIdRef = useRef(state.currentProjectId)
|
|
193
|
+
if (lastProjectIdRef.current !== state.currentProjectId) {
|
|
194
|
+
lastProjectIdRef.current = state.currentProjectId
|
|
191
195
|
measuredRef.current.clear()
|
|
192
196
|
}
|
|
193
197
|
|
|
@@ -223,29 +227,30 @@ export function useTerminalResize({
|
|
|
223
227
|
[backend, contentOriginRef]
|
|
224
228
|
)
|
|
225
229
|
|
|
226
|
-
|
|
230
|
+
// `getBarWidth` is the single authority on bar width — the Bar component
|
|
231
|
+
// renders from the same function. A mismatch silently corrupts PTY columns
|
|
232
|
+
// and mouse hit-testing.
|
|
233
|
+
const leftBarWidth = getBarWidth(state.bars.left)
|
|
234
|
+
const rightBarWidth = getBarWidth(state.bars.right)
|
|
227
235
|
const terminalSize = useMemo(() => {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const sessionBarTopOffset = sessionBarRows
|
|
236
|
+
const projectBarRows = state.projectBar.visible ? 1 : 0
|
|
237
|
+
const projectBarTopOffset = projectBarRows
|
|
231
238
|
const reservedRows =
|
|
232
239
|
MAIN_AREA_VERTICAL_PADDING +
|
|
233
240
|
STATUS_BAR_HEIGHT +
|
|
234
241
|
TERMINAL_PANE_VERTICAL_CHROME +
|
|
235
|
-
|
|
236
|
-
const gitPaneWidth = gitPaneInPaneMode ? getGitPaneWidthFromRatio(state.gitPane.paneRatio) : 0
|
|
237
|
-
const gitOnLeft = gitPaneInPaneMode && state.gitPane.position === 'left'
|
|
242
|
+
projectBarRows
|
|
238
243
|
const cols = Math.max(
|
|
239
244
|
MIN_TERMINAL_COLS,
|
|
240
|
-
Math.floor(dimensions.width -
|
|
245
|
+
Math.floor(dimensions.width - leftBarWidth - rightBarWidth - MAIN_AREA_HORIZONTAL_CHROME)
|
|
241
246
|
)
|
|
242
247
|
const rows = Math.max(MIN_TERMINAL_ROWS, Math.floor(dimensions.height - reservedRows))
|
|
243
248
|
|
|
244
249
|
contentOriginRef.current = {
|
|
245
250
|
cols,
|
|
246
251
|
rows,
|
|
247
|
-
x:
|
|
248
|
-
y: 1 +
|
|
252
|
+
x: leftBarWidth + 1,
|
|
253
|
+
y: 1 + projectBarTopOffset,
|
|
249
254
|
}
|
|
250
255
|
|
|
251
256
|
return { cols, rows }
|
|
@@ -253,12 +258,9 @@ export function useTerminalResize({
|
|
|
253
258
|
contentOriginRef,
|
|
254
259
|
dimensions.height,
|
|
255
260
|
dimensions.width,
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
state.
|
|
259
|
-
gitPaneInPaneMode,
|
|
260
|
-
state.gitPane.position,
|
|
261
|
-
state.gitPane.paneRatio,
|
|
261
|
+
leftBarWidth,
|
|
262
|
+
rightBarWidth,
|
|
263
|
+
state.projectBar.visible,
|
|
262
264
|
])
|
|
263
265
|
|
|
264
266
|
useLayoutEffect(() => {
|
|
@@ -280,14 +282,7 @@ export function useTerminalResize({
|
|
|
280
282
|
})
|
|
281
283
|
handledBySyncRef.current = true
|
|
282
284
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
283
|
-
}, [
|
|
284
|
-
state.sidebar.visible,
|
|
285
|
-
state.sidebar.width,
|
|
286
|
-
state.sessionBar.visible,
|
|
287
|
-
gitPaneInPaneMode,
|
|
288
|
-
state.gitPane.position,
|
|
289
|
-
state.gitPane.paneRatio,
|
|
290
|
-
])
|
|
285
|
+
}, [leftBarWidth, rightBarWidth, state.projectBar.visible])
|
|
291
286
|
|
|
292
287
|
useEffect(() => {
|
|
293
288
|
if (handledBySyncRef.current) {
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir } from 'node:fs/promises'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { AppState, WorkspaceRecord } from '../state/types'
|
|
6
|
+
import type { SideEffectContext } from './side-effect-context'
|
|
7
|
+
|
|
8
|
+
import { moveWorkspace } from '../git/move-workspace'
|
|
9
|
+
import {
|
|
10
|
+
createGitWorktree,
|
|
11
|
+
deleteGitBranch,
|
|
12
|
+
getCurrentBranch,
|
|
13
|
+
getHeadSha,
|
|
14
|
+
getMainWorktreeRoot,
|
|
15
|
+
listGitWorktrees,
|
|
16
|
+
pruneGitWorktrees,
|
|
17
|
+
removeGitWorktree,
|
|
18
|
+
} from '../git/worktree'
|
|
19
|
+
import { createPrefixedId } from '../platform/id'
|
|
20
|
+
import {
|
|
21
|
+
assertSafeAimuxWorktreePath,
|
|
22
|
+
isInsideAimuxWorktreeRoot,
|
|
23
|
+
makeWorktreePath,
|
|
24
|
+
sanitizePathSegment,
|
|
25
|
+
} from '../platform/worktree-paths'
|
|
26
|
+
import { saveProjectCatalog } from '../state/project-catalog'
|
|
27
|
+
import { pruneSnapshotOfWorkspace } from '../state/project-persistence'
|
|
28
|
+
import { getActiveWorkspace, getActiveWorkspacePath } from '../state/project-workspaces'
|
|
29
|
+
import { toast } from '../state/toast-store'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rewrite one project and hand back the whole list, ready for `set-projects`.
|
|
33
|
+
*
|
|
34
|
+
* Reads the *live* store rather than `ctx.state`: callers reach here after
|
|
35
|
+
* seconds of git work, and `ctx.state` is the render snapshot from before it.
|
|
36
|
+
* Since `set-projects` replaces the array wholesale, a stale read does not just
|
|
37
|
+
* miss an update — it reverts it. Workspace naming lands asynchronously, so
|
|
38
|
+
* that window is wide open.
|
|
39
|
+
*/
|
|
40
|
+
export function replaceProject(
|
|
41
|
+
ctx: SideEffectContext,
|
|
42
|
+
projectId: string,
|
|
43
|
+
next: (project: AppState['projects'][number]) => AppState['projects'][number]
|
|
44
|
+
): AppState['projects'] {
|
|
45
|
+
return ctx
|
|
46
|
+
.getState()
|
|
47
|
+
.projects.map((project) => (project.id === projectId ? next(project) : project))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function handleSwitchWorkspace(
|
|
51
|
+
ctx: SideEffectContext,
|
|
52
|
+
projectId: string,
|
|
53
|
+
workspaceId: string
|
|
54
|
+
): void {
|
|
55
|
+
const project = ctx.state.projects.find((entry) => entry.id === projectId)
|
|
56
|
+
const workspace = project?.workspaces?.find((entry) => entry.id === workspaceId)
|
|
57
|
+
if (!project || !workspace) return
|
|
58
|
+
const projects = replaceProject(ctx, projectId, (entry) => ({
|
|
59
|
+
...entry,
|
|
60
|
+
activeWorkspaceId: workspaceId,
|
|
61
|
+
updatedAt: new Date().toISOString(),
|
|
62
|
+
}))
|
|
63
|
+
saveProjectCatalog(projects)
|
|
64
|
+
ctx.dispatch({ projects, type: 'set-projects' })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeBranchName(branch: string | undefined): string | undefined {
|
|
68
|
+
return branch?.replace(/^refs\/heads\//, '').trim()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createAimuxTempWorkspace(
|
|
72
|
+
ctx: SideEffectContext,
|
|
73
|
+
projectId: string,
|
|
74
|
+
requestedName?: string,
|
|
75
|
+
requestedBranchName?: string,
|
|
76
|
+
requestedBaseRef?: string,
|
|
77
|
+
sourceWorkspaceId?: string
|
|
78
|
+
): Promise<WorkspaceRecord | undefined> {
|
|
79
|
+
const project = ctx.state.projects.find((entry) => entry.id === projectId)
|
|
80
|
+
const source =
|
|
81
|
+
project?.workspaces?.find((entry) => entry.id === sourceWorkspaceId) ??
|
|
82
|
+
getActiveWorkspace(project)
|
|
83
|
+
const sourcePath = source?.path ?? getActiveWorkspacePath(project)
|
|
84
|
+
if (!project || !(sourcePath != null && sourcePath !== '')) return undefined
|
|
85
|
+
|
|
86
|
+
// Resolve the *main* repo checkout, never the active linked workspace, so the
|
|
87
|
+
// record's repoRoot stays valid after sibling workspaces are deleted.
|
|
88
|
+
const repoRoot = (await getMainWorktreeRoot(sourcePath)) ?? source?.repoRoot ?? sourcePath
|
|
89
|
+
const baseBranch = (await getCurrentBranch(sourcePath)) ?? source?.branch ?? 'HEAD'
|
|
90
|
+
const baseRef = requestedBaseRef ?? baseBranch
|
|
91
|
+
const workspaceId = createPrefixedId('workspace')
|
|
92
|
+
const trimmedName = requestedName?.trim()
|
|
93
|
+
const workspaceName =
|
|
94
|
+
trimmedName != null && trimmedName !== ''
|
|
95
|
+
? trimmedName
|
|
96
|
+
: `wt-${sanitizePathSegment(project.name, 12)}`
|
|
97
|
+
const trimmedBranch = requestedBranchName?.trim()
|
|
98
|
+
const branchName =
|
|
99
|
+
trimmedBranch != null && trimmedBranch !== ''
|
|
100
|
+
? trimmedBranch
|
|
101
|
+
: `aimux/${sanitizePathSegment(workspaceName, 40)}-${Date.now().toString(36)}`
|
|
102
|
+
const targetPath = makeWorktreePath({ repoRoot, workspaceId, workspaceName })
|
|
103
|
+
|
|
104
|
+
const existingWorkspace = (await listGitWorktrees(repoRoot)).find(
|
|
105
|
+
(entry) =>
|
|
106
|
+
entry.prunable !== true &&
|
|
107
|
+
normalizeBranchName(entry.branch) === normalizeBranchName(branchName)
|
|
108
|
+
)
|
|
109
|
+
if (existingWorkspace) {
|
|
110
|
+
ctx.dispatch({
|
|
111
|
+
message: `Branch already checked out in another workspace: ${existingWorkspace.path}`,
|
|
112
|
+
type: 'set-create-workspace-branch-error',
|
|
113
|
+
})
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await mkdir(dirname(targetPath), { recursive: true })
|
|
118
|
+
await assertSafeAimuxWorktreePath(targetPath)
|
|
119
|
+
await createGitWorktree({ baseRef, branchName, repoPath: repoRoot, targetPath })
|
|
120
|
+
const now = new Date().toISOString()
|
|
121
|
+
|
|
122
|
+
const workspace: WorkspaceRecord = {
|
|
123
|
+
baseRef,
|
|
124
|
+
branch: branchName,
|
|
125
|
+
commitSha: await getHeadSha(targetPath),
|
|
126
|
+
createdAt: now,
|
|
127
|
+
createdByAimux: true,
|
|
128
|
+
id: workspaceId,
|
|
129
|
+
name: workspaceName,
|
|
130
|
+
path: targetPath,
|
|
131
|
+
repoRoot,
|
|
132
|
+
source: 'aimux-temp',
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
}
|
|
135
|
+
const projects = replaceProject(ctx, projectId, (entry) => ({
|
|
136
|
+
...entry,
|
|
137
|
+
activeWorkspaceId: workspace.id,
|
|
138
|
+
updatedAt: now,
|
|
139
|
+
workspaces: [...(entry.workspaces ?? []), workspace],
|
|
140
|
+
}))
|
|
141
|
+
saveProjectCatalog(projects)
|
|
142
|
+
ctx.dispatch({ projects, type: 'set-projects' })
|
|
143
|
+
toast.success(`Created workspace ${branchName}`)
|
|
144
|
+
return workspace
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Dispose and close every tab pinned to a workspace (timers, pty project, state).
|
|
148
|
+
function disposeWorkspaceTabs(ctx: SideEffectContext, workspaceId: string): void {
|
|
149
|
+
for (const tab of ctx.state.tabs.filter((entry) => entry.workspaceId === workspaceId)) {
|
|
150
|
+
ctx.clearIdleTimer(tab.id)
|
|
151
|
+
ctx.clearStartupGrace(tab.id)
|
|
152
|
+
ctx.backend.disposeSession(tab.id)
|
|
153
|
+
ctx.dispatch({ tabId: tab.id, type: 'close-tab' })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function runDeleteWorkspace(
|
|
158
|
+
ctx: SideEffectContext,
|
|
159
|
+
projectId: string,
|
|
160
|
+
workspaceId: string,
|
|
161
|
+
force: boolean,
|
|
162
|
+
closeTabs = false
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const project = ctx.state.projects.find((entry) => entry.id === projectId)
|
|
165
|
+
const workspace = project?.workspaces?.find((entry) => entry.id === workspaceId)
|
|
166
|
+
if (!project) throw new Error('project not found')
|
|
167
|
+
if (!workspace) throw new Error('workspace not found')
|
|
168
|
+
if ((project.workspaces?.length ?? 0) <= 1) throw new Error('at least one workspace must remain')
|
|
169
|
+
if (workspace.source === 'primary') throw new Error('root workspace cannot be deleted')
|
|
170
|
+
|
|
171
|
+
const tabsInWorkspace = ctx.state.tabs.filter((tab) => tab.workspaceId === workspaceId)
|
|
172
|
+
// The active-tabs guard asks the modal user to confirm before closing tabs.
|
|
173
|
+
// `closeTabs` (the sidebar's "Remove workspace") opts into closing them
|
|
174
|
+
// directly without forcing the git removal, so dirty temp workspaces are still
|
|
175
|
+
// protected by the non-force `git worktree remove`.
|
|
176
|
+
if (tabsInWorkspace.length > 0 && !force && !closeTabs) {
|
|
177
|
+
throw new ActiveWorkspaceTabsError(tabsInWorkspace.length)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const repoPath = resolveWorkspaceGitDir(project, workspace)
|
|
181
|
+
const isAimuxTemp = workspace.source === 'aimux-temp' && workspace.createdByAimux
|
|
182
|
+
// Drop the throwaway aimux branch alongside the workspace so deleted temp
|
|
183
|
+
// workspaces don't accumulate in the repo or haunt the base picker. Scoped to
|
|
184
|
+
// the `aimux/` namespace (matches the picker filter); best-effort.
|
|
185
|
+
const cleanupAimuxBranch = async (): Promise<void> => {
|
|
186
|
+
const branch = workspace.branch
|
|
187
|
+
if (isAimuxTemp && branch != null && branch !== '' && branch.startsWith('aimux/')) {
|
|
188
|
+
await deleteGitBranch(repoPath, branch)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Run git ops FIRST. If any throws (dirty workspace, etc.) the catch in the
|
|
193
|
+
// delete-workspace side effect handler re-prompts force or toasts the error —
|
|
194
|
+
// tabs stay open and the row stays in the sidebar, so the UI matches reality
|
|
195
|
+
// instead of leaving tabs closed against a workspace that still exists.
|
|
196
|
+
if (isAimuxTemp && isInsideAimuxWorktreeRoot(workspace.path) && !existsSync(workspace.path)) {
|
|
197
|
+
// The dir vanished but git may still pin the branch to a stale workspace entry.
|
|
198
|
+
await pruneGitWorktrees(repoPath)
|
|
199
|
+
} else if (isAimuxTemp && isInsideAimuxWorktreeRoot(workspace.path)) {
|
|
200
|
+
await assertSafeAimuxWorktreePath(workspace.path)
|
|
201
|
+
await removeGitWorktree({ force, repoPath, targetPath: workspace.path })
|
|
202
|
+
} else if (workspace.source === 'aimux-temp' || workspace.createdByAimux) {
|
|
203
|
+
throw new Error(`refusing unsafe workspace delete: ${workspace.path}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await cleanupAimuxBranch()
|
|
207
|
+
|
|
208
|
+
// Only after git success: close tabs + retire the record. Re-read state so
|
|
209
|
+
// we don't operate on the snapshot captured before the awaits above.
|
|
210
|
+
disposeWorkspaceTabs({ ...ctx, state: ctx.getState() }, workspaceId)
|
|
211
|
+
const latest = ctx.getState()
|
|
212
|
+
const latestProject = latest.projects.find((entry) => entry.id === projectId)
|
|
213
|
+
if (latestProject) {
|
|
214
|
+
removeWorkspaceRecordFromProject(
|
|
215
|
+
{ ...ctx, state: latest },
|
|
216
|
+
projectId,
|
|
217
|
+
latestProject,
|
|
218
|
+
workspaceId
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// A workspace's stored repoRoot can point at a sibling workspace that was since
|
|
224
|
+
// deleted. Git commands only need *some* existing workspace of the repo (they
|
|
225
|
+
// share the common git dir), so fall back to the main checkout or any live
|
|
226
|
+
// workspace path rather than letting `git -C` fail on a vanished directory.
|
|
227
|
+
function resolveWorkspaceGitDir(
|
|
228
|
+
project: NonNullable<SideEffectContext['state']['projects'][number]>,
|
|
229
|
+
workspace: WorkspaceRecord
|
|
230
|
+
): string {
|
|
231
|
+
const candidates = [
|
|
232
|
+
project.workspaces?.find((entry) => entry.source === 'primary')?.path,
|
|
233
|
+
workspace.repoRoot,
|
|
234
|
+
...(project.workspaces ?? [])
|
|
235
|
+
.filter((entry) => entry.id !== workspace.id)
|
|
236
|
+
.map((entry) => entry.path),
|
|
237
|
+
]
|
|
238
|
+
return candidates.find((path) => path !== undefined && existsSync(path)) ?? workspace.repoRoot
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function runMoveWorkspace(
|
|
242
|
+
ctx: SideEffectContext,
|
|
243
|
+
projectId: string,
|
|
244
|
+
sourceWorkspaceId: string,
|
|
245
|
+
targetWorkspaceId: string,
|
|
246
|
+
deleteSource: boolean,
|
|
247
|
+
stashTarget: boolean,
|
|
248
|
+
keepConflicts: boolean
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
const project = ctx.state.projects.find((entry) => entry.id === projectId)
|
|
251
|
+
const source = project?.workspaces?.find((entry) => entry.id === sourceWorkspaceId)
|
|
252
|
+
const target = project?.workspaces?.find((entry) => entry.id === targetWorkspaceId)
|
|
253
|
+
if (!project) throw new Error('project not found')
|
|
254
|
+
if (!source || !target) throw new Error('workspace not found')
|
|
255
|
+
if (source.id === target.id) throw new Error('source and target are the same workspace')
|
|
256
|
+
if (source.branch == null || source.branch === '') {
|
|
257
|
+
throw new Error('source workspace has no branch to move')
|
|
258
|
+
}
|
|
259
|
+
if (deleteSource && source.source === 'primary') {
|
|
260
|
+
throw new Error('the primary workspace cannot be deleted')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const sourceLabel = source.branch ?? source.name
|
|
264
|
+
const targetLabel = target.branch ?? target.name
|
|
265
|
+
const result = await moveWorkspace({
|
|
266
|
+
keepConflicts,
|
|
267
|
+
sourceBranch: source.branch,
|
|
268
|
+
sourcePath: source.path,
|
|
269
|
+
stashTarget,
|
|
270
|
+
targetPath: target.path,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Recoverable failures open a confirm dialog carrying retry params; both
|
|
274
|
+
// workspaces are already back in their original state, so confirming simply
|
|
275
|
+
// re-dispatches move-workspace with the matching flag.
|
|
276
|
+
if (result.kind === 'needs-stash' || result.kind === 'conflict') {
|
|
277
|
+
ctx.dispatch({
|
|
278
|
+
deleteSource,
|
|
279
|
+
files: result.files,
|
|
280
|
+
projectId,
|
|
281
|
+
sourceLabel,
|
|
282
|
+
sourceWorkspaceId,
|
|
283
|
+
targetLabel,
|
|
284
|
+
targetWorkspaceId,
|
|
285
|
+
type: 'open-workspace-move-confirm',
|
|
286
|
+
variant: result.kind === 'needs-stash' ? 'stash-target' : 'keep-conflicts',
|
|
287
|
+
})
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
if (result.kind === 'conflict-kept') {
|
|
291
|
+
// Never delete the source here — its work only landed half-resolved. The
|
|
292
|
+
// auto-commit driver is safe against this state: git refuses to commit
|
|
293
|
+
// with unmerged index entries, so it fails loudly instead of committing
|
|
294
|
+
// conflict markers.
|
|
295
|
+
handleSwitchWorkspace({ ...ctx, state: ctx.getState() }, projectId, targetWorkspaceId)
|
|
296
|
+
toast.warning(
|
|
297
|
+
`Left conflict markers in ${targetLabel} (${result.files.length} file(s)) — resolve & commit there; ${sourceLabel} kept`
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (result.kind === 'error') {
|
|
302
|
+
toast.error(`Move failed: ${result.message}`)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Land on the target. When deleting the source, close its terminals and
|
|
307
|
+
// switch to the target up front, synchronously, BEFORE the slow
|
|
308
|
+
// `git worktree remove`. Closing the source's active tab re-syncs the active
|
|
309
|
+
// workspace to a default tab (withActiveTabWorkspace); doing the close+switch in
|
|
310
|
+
// one batch lands on the target with no intermediate render, so the removal
|
|
311
|
+
// runs with the target already active instead of flashing/sticking to a
|
|
312
|
+
// default workspace. Re-read state each step so we never resurrect the source.
|
|
313
|
+
if (deleteSource) {
|
|
314
|
+
disposeWorkspaceTabs({ ...ctx, state: ctx.getState() }, sourceWorkspaceId)
|
|
315
|
+
handleSwitchWorkspace({ ...ctx, state: ctx.getState() }, projectId, targetWorkspaceId)
|
|
316
|
+
await runDeleteWorkspace({ ...ctx, state: ctx.getState() }, projectId, sourceWorkspaceId, true)
|
|
317
|
+
} else {
|
|
318
|
+
handleSwitchWorkspace({ ...ctx, state: ctx.getState() }, projectId, targetWorkspaceId)
|
|
319
|
+
}
|
|
320
|
+
const stashNote = result.stashedTarget
|
|
321
|
+
? ` · target's previous changes stashed (recover with git stash pop)`
|
|
322
|
+
: ''
|
|
323
|
+
toast.success(
|
|
324
|
+
`Moved ${sourceLabel} → ${targetLabel} · ${result.filesChanged} file(s) staged — review & commit${stashNote}`
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function removeWorkspaceRecordFromProject(
|
|
329
|
+
ctx: SideEffectContext,
|
|
330
|
+
projectId: string,
|
|
331
|
+
project: NonNullable<SideEffectContext['state']['projects'][number]>,
|
|
332
|
+
workspaceId: string
|
|
333
|
+
): void {
|
|
334
|
+
const projects = replaceProject(ctx, projectId, (entry) => {
|
|
335
|
+
// Filtered from `entry`, not from the `project` this was called with: a
|
|
336
|
+
// workspace created while the delete's git work ran is in one and not the
|
|
337
|
+
// other, and rebuilding from the stale copy would delete it too.
|
|
338
|
+
const remaining = (entry.workspaces ?? []).filter((w) => w.id !== workspaceId)
|
|
339
|
+
return {
|
|
340
|
+
...entry,
|
|
341
|
+
activeWorkspaceId: (entry.activeWorkspaceId === workspaceId
|
|
342
|
+
? remaining[0]
|
|
343
|
+
: getActiveWorkspace(entry)
|
|
344
|
+
)?.id,
|
|
345
|
+
projectSnapshot: pruneSnapshotOfWorkspace(entry.projectSnapshot, workspaceId),
|
|
346
|
+
updatedAt: new Date().toISOString(),
|
|
347
|
+
workspaces: remaining,
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
saveProjectCatalog(projects)
|
|
351
|
+
ctx.dispatch({ projects, type: 'set-projects' })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function isForceableWorkspaceDeleteError(message: string): boolean {
|
|
355
|
+
return /active assistant tabs|dirty|uncommitted|modified|untracked|not clean|contains.*changes/i.test(
|
|
356
|
+
message
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
class ActiveWorkspaceTabsError extends Error {
|
|
361
|
+
constructor(tabCount: number) {
|
|
362
|
+
super(
|
|
363
|
+
`active assistant tabs are using this workspace (${tabCount}) — they will be closed if you confirm.`
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
}
|