@brimveyn/aimux 1.21.0 → 1.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- 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 +2 -1
- 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 +18 -14
- 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 +93 -48
- 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 +70 -112
- 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-status.ts +1 -1
- 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 +4 -4
- 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/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 +19 -6
- 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 +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- 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} +74 -68
- 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 -6
- 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 +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- 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/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-widget.tsx +7 -7
- 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 +46 -36
- 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 +139 -121
- 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 +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- 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 -43
- 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,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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-workspace activity, and the notification sound that goes with it.
|
|
3
|
+
*
|
|
4
|
+
* The store only holds tabs for the project the client is attached to, but the
|
|
5
|
+
* daemon broadcasts every tab's status with its `workspaceId` (v18). So the
|
|
6
|
+
* table of "which tab is doing what" lives here, module-owned, and only the
|
|
7
|
+
* aggregate per workspace is dispatched into `AppState.workspaceActivity` —
|
|
8
|
+
* which is what the sidebar draws for every project, current or not.
|
|
9
|
+
*
|
|
10
|
+
* Both notification sounds are triggered from here so the glyph and the sound
|
|
11
|
+
* can never disagree about what happened:
|
|
12
|
+
* - a tab entering `waiting-input` (the agent is asking something),
|
|
13
|
+
* - a tab completing a turn (the daemon's settled end-of-turn signal).
|
|
14
|
+
* Neither fires for the tab you are looking at.
|
|
15
|
+
*/
|
|
16
|
+
import type { TabActivity } from '../state/types'
|
|
17
|
+
|
|
18
|
+
import { playNotificationSound } from '../settings/sections/notifications'
|
|
19
|
+
import { appStore } from '../state/app-store'
|
|
20
|
+
import { dispatchGlobal } from '../state/dispatch-ref'
|
|
21
|
+
|
|
22
|
+
interface Entry {
|
|
23
|
+
status: TabActivity
|
|
24
|
+
workspaceId: string | undefined
|
|
25
|
+
/**
|
|
26
|
+
* Whether this client has seen the tab do something since the last turn it
|
|
27
|
+
* announced. The daemon's end-of-turn timer is per-process: restart it (an
|
|
28
|
+
* update, `restart-daemon`) and every surviving idle PTY produces a
|
|
29
|
+
* turn-complete a settle window later. Requiring an observed `working` /
|
|
30
|
+
* `waiting-input` first is what keeps that restart from ticking every
|
|
31
|
+
* workspace and ringing.
|
|
32
|
+
*/
|
|
33
|
+
armed: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A tab in another project never sends this client its `tabExit`, so an entry
|
|
37
|
+
// here can outlive its PTY. The daemon emits a final `idle` for a tab it stops
|
|
38
|
+
// seeing (the tick cleanup in `assistant-status-detection-loop.ts`), which is
|
|
39
|
+
// what keeps a tab that died mid-turn from pinning a spinner to a workspace
|
|
40
|
+
// row forever; the entry itself is left behind, contributing nothing.
|
|
41
|
+
const entries = new Map<string, Entry>()
|
|
42
|
+
|
|
43
|
+
/** The two flags a set of tabs adds up to. Pure, and the reason it is exported. */
|
|
44
|
+
export function aggregate(statuses: readonly TabActivity[]): {
|
|
45
|
+
working: boolean
|
|
46
|
+
waiting: boolean
|
|
47
|
+
} {
|
|
48
|
+
return {
|
|
49
|
+
waiting: statuses.includes('waiting-input'),
|
|
50
|
+
working: statuses.includes('working'),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function publish(workspaceId: string | undefined): void {
|
|
55
|
+
if (workspaceId == null || workspaceId === '') return
|
|
56
|
+
const statuses: TabActivity[] = []
|
|
57
|
+
for (const entry of entries.values()) {
|
|
58
|
+
if (entry.workspaceId === workspaceId) statuses.push(entry.status)
|
|
59
|
+
}
|
|
60
|
+
const { waiting, working } = aggregate(statuses)
|
|
61
|
+
dispatchGlobal({ type: 'set-workspace-activity', waiting, working, workspaceId })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** True when this tab is the one on screen — no sound, no unseen-tick, for it. */
|
|
65
|
+
function isBeingWatched(tabId: string): boolean {
|
|
66
|
+
return appStore.getState().activeTabId === tabId
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function recordTabStatus(
|
|
70
|
+
tabId: string,
|
|
71
|
+
status: TabActivity,
|
|
72
|
+
workspaceId: string | undefined,
|
|
73
|
+
options?: { silent?: boolean }
|
|
74
|
+
): void {
|
|
75
|
+
const previous = entries.get(tabId)
|
|
76
|
+
const armed = status === 'working' || status === 'waiting-input' || (previous?.armed ?? false)
|
|
77
|
+
// An event that carries no workspace does not mean the tab lost one. A tab
|
|
78
|
+
// created before the daemon recorded workspaces has none in its registry,
|
|
79
|
+
// but the client's own snapshot does — and the attach seeded us with it, so
|
|
80
|
+
// keep it rather than blanking the row on the next status change.
|
|
81
|
+
const known = workspaceId ?? previous?.workspaceId
|
|
82
|
+
entries.set(tabId, { armed, status, workspaceId: known })
|
|
83
|
+
// A tab can be reassigned to another workspace (workspace move); the one it
|
|
84
|
+
// left has one tab fewer and has to be recomputed too.
|
|
85
|
+
if (previous && previous.workspaceId !== known) publish(previous.workspaceId)
|
|
86
|
+
publish(known)
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
options?.silent !== true &&
|
|
90
|
+
status === 'waiting-input' &&
|
|
91
|
+
previous?.status !== 'waiting-input' &&
|
|
92
|
+
!isBeingWatched(tabId)
|
|
93
|
+
) {
|
|
94
|
+
playNotificationSound()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Seed from the tabs an attach handed us, so a workspace whose agent has been
|
|
100
|
+
* waiting since before this client started says so on the first paint instead
|
|
101
|
+
* of waiting for a transition that already happened. Silent by definition:
|
|
102
|
+
* nothing here is news.
|
|
103
|
+
*/
|
|
104
|
+
export function seedTabActivity(
|
|
105
|
+
tabs: readonly { id: string; activity?: TabActivity; workspaceId?: string }[]
|
|
106
|
+
): void {
|
|
107
|
+
for (const tab of tabs) {
|
|
108
|
+
recordTabStatus(tab.id, tab.activity ?? 'idle', tab.workspaceId, { silent: true })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function recordTurnComplete(tabId: string, workspaceId: string | undefined): void {
|
|
113
|
+
const entry = entries.get(tabId)
|
|
114
|
+
// Nothing observed since the last turn — this is a restarted daemon
|
|
115
|
+
// re-announcing the end of a turn that ended before we were listening.
|
|
116
|
+
if (entry?.armed !== true) return
|
|
117
|
+
entry.armed = false
|
|
118
|
+
if (isBeingWatched(tabId)) return
|
|
119
|
+
if (workspaceId != null && workspaceId !== '') {
|
|
120
|
+
dispatchGlobal({ type: 'mark-workspace-done', workspaceId })
|
|
121
|
+
}
|
|
122
|
+
// The workspace tick says where to look; this says which tab rang. A no-op
|
|
123
|
+
// for a tab this client doesn't hold — its workspace row is the answer there.
|
|
124
|
+
dispatchGlobal({ tabId, type: 'mark-tab-unseen' })
|
|
125
|
+
playNotificationSound()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function forgetTabActivity(tabId: string): void {
|
|
129
|
+
const entry = entries.get(tabId)
|
|
130
|
+
if (!entry) return
|
|
131
|
+
entries.delete(tabId)
|
|
132
|
+
publish(entry.workspaceId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Test seam: drop every remembered tab. */
|
|
136
|
+
export function resetWorkspaceActivity(): void {
|
|
137
|
+
entries.clear()
|
|
138
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Names a workspace after the prompt that created it.
|
|
2
|
+
//
|
|
3
|
+
// Two stages, because a model call takes seconds and workspace creation must
|
|
4
|
+
// not wait on the network: a local heuristic name is applied at creation, then
|
|
5
|
+
// the generated one replaces it in place when it arrives. The heuristic result
|
|
6
|
+
// is not a stopgap to be embarrassed about — when no headless CLI is installed
|
|
7
|
+
// it is the final name, and it is still derived from what the user asked for.
|
|
8
|
+
|
|
9
|
+
import type { AssistantId, WorkspaceRecord } from '../state/types'
|
|
10
|
+
|
|
11
|
+
import { heuristicTitle } from '../auto-rename/heuristic-title'
|
|
12
|
+
import { generateTabTitle, type TitleSpawnFn } from '../auto-rename/title-runner'
|
|
13
|
+
import { renameGitBranch } from '../git/worktree'
|
|
14
|
+
import { sanitizePathSegment } from '../platform/worktree-paths'
|
|
15
|
+
|
|
16
|
+
/** Model naming is best-effort background work; it must never outlive the app. */
|
|
17
|
+
const NAMING_TIMEOUT_MS = 30_000
|
|
18
|
+
|
|
19
|
+
export const BRANCH_PREFIX = 'aimux/'
|
|
20
|
+
const BRANCH_SLUG_MAX = 40
|
|
21
|
+
|
|
22
|
+
export function branchNameFor(name: string): string {
|
|
23
|
+
return `${BRANCH_PREFIX}${sanitizePathSegment(name, BRANCH_SLUG_MAX).toLowerCase()}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The name a workspace carries until the model answers. Undefined when the
|
|
28
|
+
* prompt yields nothing usable, which lets `createAimuxTempWorkspace` fall back
|
|
29
|
+
* to its own `wt-<project>` default.
|
|
30
|
+
*/
|
|
31
|
+
export function placeholderWorkspaceName(prompt: string): string | undefined {
|
|
32
|
+
return heuristicTitle(prompt) ?? undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WorkspaceNamingTarget {
|
|
36
|
+
projectId: string
|
|
37
|
+
workspace: WorkspaceRecord
|
|
38
|
+
prompt: string
|
|
39
|
+
/** Provider used for the naming call — the assistant the user just picked. */
|
|
40
|
+
provider: AssistantId
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WorkspaceNamingDeps {
|
|
44
|
+
applyName: (projectId: string, workspaceId: string, patch: Partial<WorkspaceRecord>) => void
|
|
45
|
+
renameBranch?: (repoRoot: string, from: string, to: string) => Promise<boolean>
|
|
46
|
+
spawn?: TitleSpawnFn
|
|
47
|
+
signal?: AbortSignal
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Replace a workspace's placeholder name and branch with ones generated from
|
|
52
|
+
* the prompt. Silent on every failure: a missing CLI, a timeout or a branch
|
|
53
|
+
* collision all leave the placeholder standing, which is a perfectly usable
|
|
54
|
+
* outcome and not worth a toast.
|
|
55
|
+
*
|
|
56
|
+
* ponytail: fixed timeout, no model override. Read `autoRename.models` here if
|
|
57
|
+
* anyone wants naming on a cheaper model than the tab's assistant.
|
|
58
|
+
*/
|
|
59
|
+
export async function renameWorkspaceFromPrompt(
|
|
60
|
+
target: WorkspaceNamingTarget,
|
|
61
|
+
deps: WorkspaceNamingDeps
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const result = await generateTabTitle({
|
|
64
|
+
firstPrompt: target.prompt,
|
|
65
|
+
provider: target.provider,
|
|
66
|
+
signal: deps.signal ?? new AbortController().signal,
|
|
67
|
+
spawn: deps.spawn,
|
|
68
|
+
timeoutMs: NAMING_TIMEOUT_MS,
|
|
69
|
+
})
|
|
70
|
+
if (result.status !== 'ok') return
|
|
71
|
+
|
|
72
|
+
const { workspace } = target
|
|
73
|
+
if (result.title === workspace.name) return
|
|
74
|
+
|
|
75
|
+
const branch = branchNameFor(result.title)
|
|
76
|
+
const rename = deps.renameBranch ?? renameGitBranch
|
|
77
|
+
// Renamed from inside the workspace, not the main checkout: the branch is
|
|
78
|
+
// that worktree's current branch, which is the form git always accepts.
|
|
79
|
+
// Renaming first means a refusal (the name is taken) leaves the record
|
|
80
|
+
// pointing at the branch that actually exists.
|
|
81
|
+
const branchRenamed =
|
|
82
|
+
workspace.branch != null && workspace.branch !== '' && branch !== workspace.branch
|
|
83
|
+
? await rename(workspace.path, workspace.branch, branch)
|
|
84
|
+
: false
|
|
85
|
+
|
|
86
|
+
deps.applyName(target.projectId, workspace.id, {
|
|
87
|
+
name: result.title,
|
|
88
|
+
...(branchRenamed ? { branch } : null),
|
|
89
|
+
})
|
|
90
|
+
}
|