@brimveyn/aimux 1.14.15 → 1.14.16
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/package.json +2 -2
- package/src/app-runtime/side-effects.ts +71 -33
- package/src/app.tsx +26 -1
- package/src/config.ts +4 -0
- package/src/git/worktree.ts +45 -0
- package/src/input/modes/bridge.ts +4 -0
- package/src/input/modes/transitions.ts +8 -1
- package/src/input/modes/types.ts +15 -1
- package/src/state/reducers/modal-state.ts +138 -19
- package/src/state/reducers/session-state.ts +11 -19
- package/src/state/reducers/tab-state.ts +17 -0
- package/src/state/selectors.ts +45 -1
- package/src/state/session-persistence.ts +23 -1
- package/src/state/types.ts +39 -7
- package/src/ui/components/layout/sidebar/worktree-row.tsx +12 -2
- package/src/ui/components/layout/top-tab-bar.tsx +146 -12
- package/src/ui/components/modals/shared/worktree-delete-confirm.tsx +39 -0
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +70 -23
- package/src/ui/root.tsx +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.16",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"bump": "bun run scripts/bump.ts"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@brimveyn/aimux-config": "0.6.
|
|
63
|
+
"@brimveyn/aimux-config": "0.6.10",
|
|
64
64
|
"@opentui/core": "^0.1.90",
|
|
65
65
|
"@opentui/react": "^0.1.90",
|
|
66
66
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -22,10 +22,13 @@ import { enqueueGitOp } from '../git/command-queue'
|
|
|
22
22
|
import { moveWorktree } from '../git/move-worktree'
|
|
23
23
|
import {
|
|
24
24
|
createGitWorktree,
|
|
25
|
+
deleteGitBranch,
|
|
25
26
|
getCurrentBranch,
|
|
26
27
|
getHeadSha,
|
|
27
28
|
getMainWorktreeRoot,
|
|
28
29
|
listGitWorktrees,
|
|
30
|
+
listLocalBranches,
|
|
31
|
+
pruneGitWorktrees,
|
|
29
32
|
removeGitWorktree,
|
|
30
33
|
} from '../git/worktree'
|
|
31
34
|
import { createPrefixedId } from '../platform/id'
|
|
@@ -419,6 +422,7 @@ async function launchAssistantInNewWorktree(
|
|
|
419
422
|
worktreeName: string,
|
|
420
423
|
branchName?: string,
|
|
421
424
|
sourceWorktreeId?: string,
|
|
425
|
+
baseRef?: string,
|
|
422
426
|
templateId?: string
|
|
423
427
|
): Promise<void> {
|
|
424
428
|
const sessionId = ctx.state.currentSessionId
|
|
@@ -428,7 +432,7 @@ async function launchAssistantInNewWorktree(
|
|
|
428
432
|
sessionId,
|
|
429
433
|
worktreeName,
|
|
430
434
|
branchName,
|
|
431
|
-
|
|
435
|
+
baseRef,
|
|
432
436
|
sourceWorktreeId
|
|
433
437
|
)
|
|
434
438
|
if (!worktree) return
|
|
@@ -665,6 +669,7 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
665
669
|
if (state.modal.type === 'new-tab' && state.modal.createWorktree) {
|
|
666
670
|
const worktreeName = state.modal.worktreeName
|
|
667
671
|
const branchName = state.modal.branchName
|
|
672
|
+
const baseRef = state.modal.baseRef
|
|
668
673
|
const sourceWorktreeId = getNewTabTargetWorktreeId(state)
|
|
669
674
|
let templateId: string | undefined
|
|
670
675
|
if (state.modal.step === 'template') {
|
|
@@ -683,6 +688,7 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
683
688
|
worktreeName,
|
|
684
689
|
branchName,
|
|
685
690
|
sourceWorktreeId,
|
|
691
|
+
baseRef !== '' ? baseRef : undefined,
|
|
686
692
|
templateId
|
|
687
693
|
)
|
|
688
694
|
)
|
|
@@ -700,6 +706,17 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
700
706
|
dispatch({ assistantId: option.id, type: 'open-edit-custom-command' })
|
|
701
707
|
return
|
|
702
708
|
}
|
|
709
|
+
case 'load-new-tab-base-branches': {
|
|
710
|
+
void (async () => {
|
|
711
|
+
const session = state.sessions.find((entry) => entry.id === state.currentSessionId)
|
|
712
|
+
const sourcePath = getActiveWorktree(session)?.path ?? getSessionProjectPath(session)
|
|
713
|
+
if (!(sourcePath != null && sourcePath !== '')) return
|
|
714
|
+
const branches = await listLocalBranches(sourcePath)
|
|
715
|
+
if (ctx.getState().modal.type !== 'new-tab') return
|
|
716
|
+
ctx.dispatch({ branches, type: 'set-new-tab-base-branches' })
|
|
717
|
+
})()
|
|
718
|
+
return
|
|
719
|
+
}
|
|
703
720
|
case 'confirm-selected-session': {
|
|
704
721
|
handleSessionSelection(ctx)
|
|
705
722
|
return
|
|
@@ -720,27 +737,40 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
720
737
|
{ ...ctx, state: ctx.getState() },
|
|
721
738
|
effect.sessionId,
|
|
722
739
|
effect.worktreeId,
|
|
723
|
-
!!(effect.force === true)
|
|
740
|
+
!!(effect.force === true),
|
|
741
|
+
!!(effect.closeTabs === true)
|
|
724
742
|
)
|
|
725
743
|
)
|
|
726
744
|
} catch (error) {
|
|
727
745
|
const message = error instanceof Error ? error.message : String(error)
|
|
728
|
-
|
|
746
|
+
// Real errors surface as a toast. Recoverable failures (dirty tree,
|
|
747
|
+
// active tabs, …) open a confirmation so the user can opt into a
|
|
748
|
+
// force-delete: in-place inside the new-tab worktree picker (preserving
|
|
749
|
+
// it), or as a standalone modal elsewhere (e.g. the sidebar's "Remove
|
|
750
|
+
// worktree").
|
|
751
|
+
if (!isForceableWorktreeDeleteError(message)) {
|
|
752
|
+
toast.error(`Could not delete worktree: ${message}`)
|
|
753
|
+
return
|
|
754
|
+
}
|
|
729
755
|
const latest = ctx.getState()
|
|
730
756
|
if (latest.modal.type === 'new-tab' && latest.modal.step === 'worktree') {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
757
|
+
ctx.dispatch({
|
|
758
|
+
prompt: { reason: message, worktreeId: effect.worktreeId },
|
|
759
|
+
type: 'set-new-tab-worktree-delete-prompt',
|
|
760
|
+
})
|
|
761
|
+
return
|
|
737
762
|
}
|
|
763
|
+
const session = latest.sessions.find((entry) => entry.id === effect.sessionId)
|
|
764
|
+
const worktree = session?.worktrees?.find((entry) => entry.id === effect.worktreeId)
|
|
738
765
|
ctx.dispatch({
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
766
|
+
closeTabs: effect.closeTabs === true,
|
|
767
|
+
force: true,
|
|
768
|
+
reason: message,
|
|
769
|
+
sessionId: effect.sessionId,
|
|
770
|
+
type: 'open-worktree-delete-confirm',
|
|
771
|
+
worktreeId: effect.worktreeId,
|
|
772
|
+
worktreeLabel: worktree?.branch ?? worktree?.name ?? 'this worktree',
|
|
742
773
|
})
|
|
743
|
-
ctx.dispatch({ message, type: 'git-mode-set-message' })
|
|
744
774
|
}
|
|
745
775
|
})()
|
|
746
776
|
return
|
|
@@ -1544,7 +1574,8 @@ async function runDeleteWorktree(
|
|
|
1544
1574
|
ctx: SideEffectContext,
|
|
1545
1575
|
sessionId: string,
|
|
1546
1576
|
worktreeId: string,
|
|
1547
|
-
force: boolean
|
|
1577
|
+
force: boolean,
|
|
1578
|
+
closeTabs = false
|
|
1548
1579
|
): Promise<void> {
|
|
1549
1580
|
const session = ctx.state.sessions.find((entry) => entry.id === sessionId)
|
|
1550
1581
|
const worktree = session?.worktrees?.find((entry) => entry.id === worktreeId)
|
|
@@ -1554,36 +1585,43 @@ async function runDeleteWorktree(
|
|
|
1554
1585
|
if (worktree.source === 'primary') throw new Error('root worktree cannot be deleted')
|
|
1555
1586
|
|
|
1556
1587
|
const tabsInWorktree = ctx.state.tabs.filter((tab) => tab.worktreeId === worktreeId)
|
|
1557
|
-
|
|
1588
|
+
// The active-tabs guard asks the modal user to confirm before closing tabs.
|
|
1589
|
+
// `closeTabs` (the sidebar's "Remove worktree") opts into closing them
|
|
1590
|
+
// directly without forcing the git removal, so dirty temp worktrees are still
|
|
1591
|
+
// protected by the non-force `git worktree remove`.
|
|
1592
|
+
if (tabsInWorktree.length > 0 && !force && !closeTabs) {
|
|
1558
1593
|
throw new ActiveWorktreeTabsError(tabsInWorktree.length)
|
|
1559
1594
|
}
|
|
1560
1595
|
disposeWorktreeTabs(ctx, worktreeId)
|
|
1561
1596
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
) {
|
|
1597
|
+
const repoPath = resolveWorktreeGitDir(session, worktree)
|
|
1598
|
+
const isAimuxTemp = worktree.source === 'aimux-temp' && worktree.createdByAimux
|
|
1599
|
+
// Drop the throwaway aimux branch alongside the worktree so deleted temp
|
|
1600
|
+
// worktrees don't accumulate in the repo or haunt the base picker. Scoped to
|
|
1601
|
+
// the `aimux/` namespace (matches the picker filter); best-effort.
|
|
1602
|
+
const cleanupAimuxBranch = async (): Promise<void> => {
|
|
1603
|
+
const branch = worktree.branch
|
|
1604
|
+
if (isAimuxTemp && branch != null && branch !== '' && branch.startsWith('aimux/')) {
|
|
1605
|
+
await deleteGitBranch(repoPath, branch)
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
if (isAimuxTemp && isInsideAimuxWorktreeRoot(worktree.path) && !existsSync(worktree.path)) {
|
|
1610
|
+
// The dir vanished but git may still pin the branch to a stale worktree entry.
|
|
1611
|
+
await pruneGitWorktrees(repoPath)
|
|
1612
|
+
await cleanupAimuxBranch()
|
|
1568
1613
|
removeWorktreeRecordFromSession(ctx, sessionId, session, worktreeId)
|
|
1569
1614
|
return
|
|
1570
1615
|
}
|
|
1571
1616
|
|
|
1572
|
-
if (
|
|
1573
|
-
worktree.source === 'aimux-temp' &&
|
|
1574
|
-
worktree.createdByAimux &&
|
|
1575
|
-
isInsideAimuxWorktreeRoot(worktree.path)
|
|
1576
|
-
) {
|
|
1617
|
+
if (isAimuxTemp && isInsideAimuxWorktreeRoot(worktree.path)) {
|
|
1577
1618
|
await assertSafeAimuxWorktreePath(worktree.path)
|
|
1578
|
-
await removeGitWorktree({
|
|
1579
|
-
force,
|
|
1580
|
-
repoPath: resolveWorktreeGitDir(session, worktree),
|
|
1581
|
-
targetPath: worktree.path,
|
|
1582
|
-
})
|
|
1619
|
+
await removeGitWorktree({ force, repoPath, targetPath: worktree.path })
|
|
1583
1620
|
} else if (worktree.source === 'aimux-temp' || worktree.createdByAimux) {
|
|
1584
1621
|
throw new Error(`refusing unsafe worktree delete: ${worktree.path}`)
|
|
1585
1622
|
}
|
|
1586
1623
|
|
|
1624
|
+
await cleanupAimuxBranch()
|
|
1587
1625
|
removeWorktreeRecordFromSession(ctx, sessionId, session, worktreeId)
|
|
1588
1626
|
}
|
|
1589
1627
|
|
|
@@ -1694,7 +1732,7 @@ function removeWorktreeRecordFromSession(
|
|
|
1694
1732
|
type: 'set-modal-selection-index',
|
|
1695
1733
|
})
|
|
1696
1734
|
}
|
|
1697
|
-
ctx.dispatch({
|
|
1735
|
+
ctx.dispatch({ prompt: null, type: 'set-new-tab-worktree-delete-prompt' })
|
|
1698
1736
|
}
|
|
1699
1737
|
|
|
1700
1738
|
function isForceableWorktreeDeleteError(message: string): boolean {
|
|
@@ -1706,7 +1744,7 @@ function isForceableWorktreeDeleteError(message: string): boolean {
|
|
|
1706
1744
|
class ActiveWorktreeTabsError extends Error {
|
|
1707
1745
|
constructor(tabCount: number) {
|
|
1708
1746
|
super(
|
|
1709
|
-
`active assistant tabs are using this worktree (${tabCount})
|
|
1747
|
+
`active assistant tabs are using this worktree (${tabCount}) — they will be closed if you confirm.`
|
|
1710
1748
|
)
|
|
1711
1749
|
}
|
|
1712
1750
|
}
|
package/src/app.tsx
CHANGED
|
@@ -22,7 +22,9 @@ import { useMouseHandlers } from './app-runtime/use-mouse-handlers'
|
|
|
22
22
|
import { useRendererBindings } from './app-runtime/use-renderer-bindings'
|
|
23
23
|
import { useTerminalResize } from './app-runtime/use-terminal-resize'
|
|
24
24
|
import { useWorkspaceAutosave } from './app-runtime/use-workspace-autosave'
|
|
25
|
-
import { loadConfig } from './config'
|
|
25
|
+
import { loadConfig, saveConfig } from './config'
|
|
26
|
+
import { enqueueGitOp } from './git/command-queue'
|
|
27
|
+
import { pruneOrphanAimuxBranches } from './git/worktree'
|
|
26
28
|
import { setActiveKeymap } from './input/keymap/keymap-ref'
|
|
27
29
|
import { deriveModeId } from './input/modes/bridge'
|
|
28
30
|
import { registerAllModes } from './input/modes/handlers'
|
|
@@ -39,6 +41,7 @@ import { findMostRecentSession, loadSessionCatalog } from './state/session-catal
|
|
|
39
41
|
import { getSessionProjectPath } from './state/session-worktrees'
|
|
40
42
|
import { loadSnippetCatalog, mergeConfigSnippets } from './state/snippet-catalog'
|
|
41
43
|
import { createInitialState } from './state/store'
|
|
44
|
+
import { toast } from './state/toast-store'
|
|
42
45
|
import { KeymapContext } from './ui/keymap-context'
|
|
43
46
|
import { RootView } from './ui/root'
|
|
44
47
|
import {
|
|
@@ -250,6 +253,28 @@ export function App({
|
|
|
250
253
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
251
254
|
}, [])
|
|
252
255
|
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
// One-shot cleanup: prune `aimux/` branches orphaned by temp worktrees that
|
|
258
|
+
// were deleted before delete-time branch cleanup existed. Gated by a config
|
|
259
|
+
// flag so it runs once per machine. All git calls are best-effort (nothrow)
|
|
260
|
+
// and git protects branches still checked out in a live worktree.
|
|
261
|
+
if (loadConfig().prunedOrphanAimuxBranches === true) return
|
|
262
|
+
void (async () => {
|
|
263
|
+
const repoRoots = new Set<string>()
|
|
264
|
+
for (const session of loadSessionCatalog()) {
|
|
265
|
+
for (const worktree of session.worktrees ?? []) {
|
|
266
|
+
if (worktree.repoRoot !== '') repoRoots.add(worktree.repoRoot)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
let removed = 0
|
|
270
|
+
for (const repoRoot of repoRoots) {
|
|
271
|
+
removed += await enqueueGitOp(() => pruneOrphanAimuxBranches(repoRoot))
|
|
272
|
+
}
|
|
273
|
+
saveConfig({ ...loadConfig(), prunedOrphanAimuxBranches: true })
|
|
274
|
+
if (removed > 0) toast.success(`Cleaned ${removed} orphaned aimux branch(es)`)
|
|
275
|
+
})()
|
|
276
|
+
}, [])
|
|
277
|
+
|
|
253
278
|
const resizingRef = useRef(false)
|
|
254
279
|
// Seeded with state.layout's DEFAULT 80x24; reassigned below once
|
|
255
280
|
// useTerminalResize has produced the open-loop estimate from real dimensions.
|
package/src/config.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface AimuxConfig {
|
|
|
64
64
|
sessionBarVisible?: boolean
|
|
65
65
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
66
66
|
skippedUpdateVersion?: string
|
|
67
|
+
/** One-shot guard: orphan `aimux/` branches were pruned from existing repos. */
|
|
68
|
+
prunedOrphanAimuxBranches?: boolean
|
|
67
69
|
worktreeTemplates?: WorktreeTemplate[]
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -255,6 +257,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
255
257
|
sessionBarVisible?: unknown
|
|
256
258
|
workspaceSnapshot?: unknown
|
|
257
259
|
skippedUpdateVersion?: unknown
|
|
260
|
+
prunedOrphanAimuxBranches?: unknown
|
|
258
261
|
worktreeTemplates?: unknown
|
|
259
262
|
}
|
|
260
263
|
|
|
@@ -350,6 +353,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
350
353
|
config: {
|
|
351
354
|
customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
|
|
352
355
|
gitPane: validGitPane,
|
|
356
|
+
prunedOrphanAimuxBranches: parsed.prunedOrphanAimuxBranches === true ? true : undefined,
|
|
353
357
|
sessionBarVisible: validSessionBarVisible,
|
|
354
358
|
sidebar: validSidebar,
|
|
355
359
|
skippedUpdateVersion: validSkippedUpdateVersion,
|
package/src/git/worktree.ts
CHANGED
|
@@ -33,6 +33,24 @@ export async function getHeadSha(cwd: string): Promise<string | undefined> {
|
|
|
33
33
|
return result.text().trim() || undefined
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// Local branch names, ordered most-recently-committed first so the likely base
|
|
37
|
+
// surfaces near the top of the picker.
|
|
38
|
+
export async function listLocalBranches(cwd: string): Promise<string[]> {
|
|
39
|
+
// The format must be interpolated, not inlined: Bun's shell parses a bare
|
|
40
|
+
// `%(refname:short)` and chokes on the parentheses.
|
|
41
|
+
const format = '%(refname:short)'
|
|
42
|
+
const result =
|
|
43
|
+
await $`git -C ${cwd} for-each-ref --sort=-committerdate refs/heads --format=${format}`
|
|
44
|
+
.quiet()
|
|
45
|
+
.nothrow()
|
|
46
|
+
if (result.exitCode !== 0) return []
|
|
47
|
+
return result
|
|
48
|
+
.text()
|
|
49
|
+
.split('\n')
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter((line) => line !== '')
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
export async function createGitWorktree({
|
|
37
55
|
baseRef,
|
|
38
56
|
branchName,
|
|
@@ -52,6 +70,33 @@ export async function createGitWorktree({
|
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
|
|
73
|
+
// Force-delete a local branch. Returns false (without throwing) when git
|
|
74
|
+
// refuses — notably when the branch is still checked out in a live worktree,
|
|
75
|
+
// which is exactly how orphan-pruning stays safe.
|
|
76
|
+
export async function deleteGitBranch(repoPath: string, branch: string): Promise<boolean> {
|
|
77
|
+
const result = await $`git -C ${repoPath} branch -D ${branch}`.quiet().nothrow()
|
|
78
|
+
return result.exitCode === 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function pruneGitWorktrees(repoPath: string): Promise<void> {
|
|
82
|
+
await $`git -C ${repoPath} worktree prune`.quiet().nothrow()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Drop every `aimux/` branch left behind by deleted temp worktrees. git refuses
|
|
86
|
+
// to delete branches still checked out in a live worktree, so this only removes
|
|
87
|
+
// true orphans. Returns the number of branches removed.
|
|
88
|
+
export async function pruneOrphanAimuxBranches(repoPath: string): Promise<number> {
|
|
89
|
+
await pruneGitWorktrees(repoPath)
|
|
90
|
+
const branches = (await listLocalBranches(repoPath)).filter((branch) =>
|
|
91
|
+
branch.startsWith('aimux/')
|
|
92
|
+
)
|
|
93
|
+
let removed = 0
|
|
94
|
+
for (const branch of branches) {
|
|
95
|
+
if (await deleteGitBranch(repoPath, branch)) removed++
|
|
96
|
+
}
|
|
97
|
+
return removed
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
export async function removeGitWorktree({
|
|
56
101
|
force,
|
|
57
102
|
repoPath,
|
|
@@ -26,6 +26,7 @@ const COMMAND_EDIT_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
|
|
|
26
26
|
const MODAL_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
|
|
27
27
|
'ai-usage': 'modal.ai-usage',
|
|
28
28
|
'update-available': 'modal.update-available',
|
|
29
|
+
'worktree-delete-confirm': 'modal.worktree-delete-confirm',
|
|
29
30
|
'worktree-move': 'modal.worktree-move',
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -51,6 +52,9 @@ export function deriveModeId(state: AppState): ModeId {
|
|
|
51
52
|
if (state.modal.type === 'new-tab' && state.modal.editingCommand !== null) {
|
|
52
53
|
return 'modal.new-tab.editing-command'
|
|
53
54
|
}
|
|
55
|
+
if (state.modal.type === 'new-tab' && state.modal.worktreeDeletePrompt !== null) {
|
|
56
|
+
return 'modal.new-tab.worktree-delete-confirm'
|
|
57
|
+
}
|
|
54
58
|
if (state.modal.type === 'git-commit' && state.modal.stage === 'confirm') {
|
|
55
59
|
return 'modal.git-commit.confirm'
|
|
56
60
|
}
|
|
@@ -8,8 +8,13 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
|
|
|
8
8
|
'modal.git-commit.confirm': ['modal.git-commit', 'git-mode'],
|
|
9
9
|
'modal.git-commit.generating': ['modal.git-commit', 'modal.git-commit.confirm', 'git-mode'],
|
|
10
10
|
'modal.help.filtering': ['navigation'],
|
|
11
|
-
'modal.new-tab.command-edit': [
|
|
11
|
+
'modal.new-tab.command-edit': [
|
|
12
|
+
'navigation',
|
|
13
|
+
'modal.new-tab.editing-command',
|
|
14
|
+
'modal.new-tab.worktree-delete-confirm',
|
|
15
|
+
],
|
|
12
16
|
'modal.new-tab.editing-command': ['navigation', 'modal.new-tab.command-edit'],
|
|
17
|
+
'modal.new-tab.worktree-delete-confirm': ['navigation', 'modal.new-tab.command-edit'],
|
|
13
18
|
'modal.rename-tab': ['navigation'],
|
|
14
19
|
'modal.session-name': ['modal.session-picker.filtering', 'navigation'],
|
|
15
20
|
'modal.session-picker.filtering': ['navigation', 'modal.session-name', 'modal.create-session'],
|
|
@@ -18,6 +23,7 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
|
|
|
18
23
|
'modal.split-picker': ['navigation', 'terminal-input'],
|
|
19
24
|
'modal.theme-picker.filtering': ['navigation'],
|
|
20
25
|
'modal.update-available': ['navigation'],
|
|
26
|
+
'modal.worktree-delete-confirm': ['navigation'],
|
|
21
27
|
'modal.worktree-move': ['git-mode', 'navigation'],
|
|
22
28
|
'navigation': [
|
|
23
29
|
'terminal-input',
|
|
@@ -30,6 +36,7 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
|
|
|
30
36
|
'modal.rename-tab',
|
|
31
37
|
'modal.update-available',
|
|
32
38
|
'modal.ai-usage',
|
|
39
|
+
'modal.worktree-delete-confirm',
|
|
33
40
|
'git-mode',
|
|
34
41
|
],
|
|
35
42
|
'terminal-input': ['navigation', 'modal.split-picker', 'modal.ai-usage'],
|
package/src/input/modes/types.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type ModeId =
|
|
|
9
9
|
| 'git-mode'
|
|
10
10
|
| 'modal.new-tab.command-edit'
|
|
11
11
|
| 'modal.new-tab.editing-command'
|
|
12
|
+
| 'modal.new-tab.worktree-delete-confirm'
|
|
13
|
+
| 'modal.worktree-delete-confirm'
|
|
12
14
|
| 'modal.session-picker.filtering'
|
|
13
15
|
| 'modal.session-name'
|
|
14
16
|
| 'modal.create-session'
|
|
@@ -28,6 +30,7 @@ export type ModeId =
|
|
|
28
30
|
export type SideEffect =
|
|
29
31
|
| { type: 'quit'; state: AppState }
|
|
30
32
|
| { type: 'launch-selected-assistant' }
|
|
33
|
+
| { type: 'load-new-tab-base-branches' }
|
|
31
34
|
| { type: 'edit-selected-assistant' }
|
|
32
35
|
| { type: 'confirm-selected-session' }
|
|
33
36
|
| { type: 'delete-selected-session' }
|
|
@@ -71,7 +74,18 @@ export type SideEffect =
|
|
|
71
74
|
| { type: 'cycle-sidebar-item'; direction: 1 | -1 }
|
|
72
75
|
| { type: 'switch-tab-by-index'; index: number }
|
|
73
76
|
| { type: 'delete-session'; sessionId: string }
|
|
74
|
-
| {
|
|
77
|
+
| {
|
|
78
|
+
type: 'delete-worktree'
|
|
79
|
+
sessionId: string
|
|
80
|
+
worktreeId: string
|
|
81
|
+
// Force the git worktree removal (discards uncommitted changes in the
|
|
82
|
+
// worktree). Also implies closing the worktree's tabs.
|
|
83
|
+
force?: boolean
|
|
84
|
+
// Close the worktree's tabs without forcing the git removal. Lets the
|
|
85
|
+
// sidebar "Remove worktree" clean up tabs (avoiding orphans) while still
|
|
86
|
+
// refusing to discard uncommitted work in a temp worktree.
|
|
87
|
+
closeTabs?: boolean
|
|
88
|
+
}
|
|
75
89
|
| {
|
|
76
90
|
type: 'move-worktree'
|
|
77
91
|
sessionId: string
|