@brimveyn/aimux 1.24.0 → 1.24.1
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 +19 -1
- package/src/state/actions.ts +1 -1
- package/src/state/types.ts +1 -1
- package/src/state/workspace-delete-store.ts +32 -0
- package/src/ui/components/layout/sidebar/workspace-row.tsx +16 -6
- package/src/ui/components/modals/shared/workspace-delete-confirm.tsx +8 -7
- package/src/ui/components/overlays/workspace-deleting-overlay.tsx +29 -0
- package/src/ui/root.tsx +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode, Kimi side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"bump:terminal_manager": "bun run scripts/bump-protocol.ts terminal-manager"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@brimveyn/aimux-config": "0.10.
|
|
69
|
+
"@brimveyn/aimux-config": "0.10.9",
|
|
70
70
|
"@opentui/core": "^0.1.90",
|
|
71
71
|
"@opentui/react": "^0.1.90",
|
|
72
72
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -13,6 +13,7 @@ import { allLeafIds, getGroupIdForTab } from '../state/layout-tree'
|
|
|
13
13
|
import { saveCurrentProject } from '../state/project-save'
|
|
14
14
|
import { getActiveWorkspace, getActiveWorkspacePath } from '../state/project-workspaces'
|
|
15
15
|
import { toast } from '../state/toast-store'
|
|
16
|
+
import { beginWorkspaceDelete, endWorkspaceDelete } from '../state/workspace-delete-store'
|
|
16
17
|
import { filterThemeIds } from '../ui/filter-themes'
|
|
17
18
|
import { scrollGitDiff } from '../ui/git-view-controls'
|
|
18
19
|
import { applyTheme, getCurrentMode, getTransparent, setMode, setTransparent } from '../ui/theme'
|
|
@@ -306,6 +307,16 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
306
307
|
return
|
|
307
308
|
}
|
|
308
309
|
case 'delete-workspace': {
|
|
310
|
+
const deleting = state.projects
|
|
311
|
+
.find((entry) => entry.id === effect.projectId)
|
|
312
|
+
?.workspaces?.find((entry) => entry.id === effect.workspaceId)
|
|
313
|
+
// Marked before the enqueue, not inside the git op: the queue is why a
|
|
314
|
+
// delete can sit there doing nothing visible, which is the whole thing
|
|
315
|
+
// this indicator exists to answer.
|
|
316
|
+
beginWorkspaceDelete(
|
|
317
|
+
effect.workspaceId,
|
|
318
|
+
deleting?.branch ?? deleting?.name ?? 'this workspace'
|
|
319
|
+
)
|
|
309
320
|
void (async () => {
|
|
310
321
|
try {
|
|
311
322
|
await enqueueGitOp(async () =>
|
|
@@ -333,11 +344,18 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
333
344
|
closeTabs: effect.closeTabs === true,
|
|
334
345
|
force: true,
|
|
335
346
|
projectId: effect.projectId,
|
|
336
|
-
|
|
347
|
+
// Git's refusal, condensed to the one line that changes the answer.
|
|
348
|
+
// The raw message is a paragraph of plumbing nobody reads before
|
|
349
|
+
// pressing y, and the dialog is not where it gets debugged.
|
|
350
|
+
reason: /active assistant tabs/i.test(message)
|
|
351
|
+
? 'Its assistant tabs will be closed.'
|
|
352
|
+
: 'Deleting anyway discards uncommitted work.',
|
|
337
353
|
type: 'open-workspace-delete-confirm',
|
|
338
354
|
workspaceId: effect.workspaceId,
|
|
339
355
|
workspaceLabel: workspace?.branch ?? workspace?.name ?? 'this workspace',
|
|
340
356
|
})
|
|
357
|
+
} finally {
|
|
358
|
+
endWorkspaceDelete(effect.workspaceId)
|
|
341
359
|
}
|
|
342
360
|
})()
|
|
343
361
|
return
|
package/src/state/actions.ts
CHANGED
package/src/state/types.ts
CHANGED
|
@@ -648,7 +648,7 @@ export interface ModalWorkspaceDeleteConfirm extends ModalBase {
|
|
|
648
648
|
projectId: string
|
|
649
649
|
workspaceId: string
|
|
650
650
|
workspaceLabel: string
|
|
651
|
-
reason
|
|
651
|
+
reason?: string
|
|
652
652
|
closeTabs: boolean
|
|
653
653
|
/** Whether confirming force-deletes — true only after a recoverable failure. */
|
|
654
654
|
force: boolean
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useStore } from 'zustand'
|
|
2
|
+
import { createStore } from 'zustand/vanilla'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workspace deletes in flight, keyed by workspace id, valued by the label to
|
|
6
|
+
* show for them. Its own store rather than a slice of `AppState`: nothing here
|
|
7
|
+
* survives the operation, no reducer decides anything about it, and the side
|
|
8
|
+
* effect that owns the git work can set it without a dispatch round-trip.
|
|
9
|
+
*/
|
|
10
|
+
interface WorkspaceDeleteState {
|
|
11
|
+
deleting: Record<string, string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const workspaceDeleteStore = createStore<WorkspaceDeleteState>(() => ({ deleting: {} }))
|
|
15
|
+
|
|
16
|
+
export function beginWorkspaceDelete(workspaceId: string, label: string): void {
|
|
17
|
+
workspaceDeleteStore.setState((state) => ({
|
|
18
|
+
deleting: { ...state.deleting, [workspaceId]: label },
|
|
19
|
+
}))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function endWorkspaceDelete(workspaceId: string): void {
|
|
23
|
+
workspaceDeleteStore.setState((state) => {
|
|
24
|
+
const deleting = { ...state.deleting }
|
|
25
|
+
delete deleting[workspaceId]
|
|
26
|
+
return { deleting }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useWorkspaceDeleteStore<T>(selector: (state: WorkspaceDeleteState) => T): T {
|
|
31
|
+
return useStore(workspaceDeleteStore, selector)
|
|
32
|
+
}
|
|
@@ -9,6 +9,7 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-
|
|
|
9
9
|
import { formatDiffStat } from '../../../../state/project-workspaces'
|
|
10
10
|
// eslint-disable-next-line no-duplicate-imports
|
|
11
11
|
import { IDLE_WORKSPACE_ACTIVITY } from '../../../../state/types'
|
|
12
|
+
import { useWorkspaceDeleteStore } from '../../../../state/workspace-delete-store'
|
|
12
13
|
import { useActivitySprite } from '../../../hooks/use-activity-sprite'
|
|
13
14
|
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
14
15
|
// eslint-disable-next-line no-duplicate-imports
|
|
@@ -56,9 +57,13 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
56
57
|
// own: one shape is what keeps a cell from resolving to the wrong image.
|
|
57
58
|
const hasBranch = workspace.branch != null && workspace.branch !== ''
|
|
58
59
|
const sprite = useActivitySprite(hasBranch ? spriteStateFor(activity) : null)
|
|
60
|
+
// A delete takes seconds of git work with the row still on screen. The branch
|
|
61
|
+
// line is where it says so — the name above it is what you are looking for,
|
|
62
|
+
// and the branch under it is the one fact the delete is about to take away.
|
|
63
|
+
const isDeleting = useWorkspaceDeleteStore((s) => s.deleting[workspace.id] !== undefined)
|
|
59
64
|
// Only the working case animates, so the timer is off for every other row —
|
|
60
65
|
// and off entirely when a sprite is drawing this row instead.
|
|
61
|
-
const spinner = useBusySpinner(activity.working && sprite === null)
|
|
66
|
+
const spinner = useBusySpinner(isDeleting || (activity.working && sprite === null))
|
|
62
67
|
// A primitive, so the selector stays referentially stable across renders.
|
|
63
68
|
const hasSleepingTabs = useAppStore((s) =>
|
|
64
69
|
s.tabs.some((tab) => tab.workspaceId === workspace.id && tab.hibernated === true)
|
|
@@ -121,7 +126,6 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
121
126
|
closeTabs: true,
|
|
122
127
|
force: false,
|
|
123
128
|
projectId: project.id,
|
|
124
|
-
reason: 'Its assistant tabs will be closed and the worktree removed.',
|
|
125
129
|
type: 'open-workspace-delete-confirm',
|
|
126
130
|
workspaceId: workspace.id,
|
|
127
131
|
workspaceLabel: workspace.branch ?? workspace.name,
|
|
@@ -243,7 +247,7 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
243
247
|
</text>
|
|
244
248
|
) : null}
|
|
245
249
|
</box>
|
|
246
|
-
{branchLabel == null ? null : (
|
|
250
|
+
{branchLabel == null && !isDeleting ? null : (
|
|
247
251
|
<box flexDirection="row" alignItems="center">
|
|
248
252
|
<text fg={t.primary} selectable={false} wrapMode="none">
|
|
249
253
|
{cursorGlyph}
|
|
@@ -254,9 +258,15 @@ export const WorkspaceRow = memo(function WorkspaceRow({
|
|
|
254
258
|
<text fg={statusColor} selectable={false} wrapMode="none">
|
|
255
259
|
{sprite?.glyphs[1] ?? ' '}
|
|
256
260
|
</text>
|
|
257
|
-
|
|
258
|
-
{
|
|
259
|
-
|
|
261
|
+
{isDeleting ? (
|
|
262
|
+
<text fg={t.error} selectable={false} wrapMode="none">
|
|
263
|
+
{spinner} Deleting…
|
|
264
|
+
</text>
|
|
265
|
+
) : (
|
|
266
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
267
|
+
{'\u{e702}'} {branchLabel}
|
|
268
|
+
</text>
|
|
269
|
+
)}
|
|
260
270
|
</box>
|
|
261
271
|
)}
|
|
262
272
|
</ContextMenuBox>
|
|
@@ -6,7 +6,11 @@ import { Form } from './form'
|
|
|
6
6
|
|
|
7
7
|
interface WorkspaceDeleteConfirmProps {
|
|
8
8
|
keybindsModeId: ModeId
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* One short line naming what confirming costs beyond the delete itself.
|
|
11
|
+
* Omitted for the ordinary case, where the title already said everything.
|
|
12
|
+
*/
|
|
13
|
+
reason?: string
|
|
10
14
|
workspaceLabel: string
|
|
11
15
|
}
|
|
12
16
|
|
|
@@ -24,16 +28,13 @@ export function WorkspaceDeleteConfirm({
|
|
|
24
28
|
return (
|
|
25
29
|
<Form
|
|
26
30
|
title="Delete workspace?"
|
|
31
|
+
subtitle={workspaceLabel}
|
|
27
32
|
keybindsModeId={keybindsModeId}
|
|
28
33
|
width={uiTokens.modalWidth.md}
|
|
34
|
+
// Destructive confirmation — the one place §7 keeps an inline key hint.
|
|
29
35
|
footer={<text fg={t.textMuted}>Enter / y to delete · Esc / n to cancel</text>}
|
|
30
36
|
>
|
|
31
|
-
<
|
|
32
|
-
<text fg={t.text}>
|
|
33
|
-
Delete <strong>{workspaceLabel}</strong>?
|
|
34
|
-
</text>
|
|
35
|
-
<text fg={t.warning}>{reason}</text>
|
|
36
|
-
</box>
|
|
37
|
+
{reason == null || reason === '' ? null : <text fg={t.textMuted}>{reason}</text>}
|
|
37
38
|
</Form>
|
|
38
39
|
)
|
|
39
40
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useWorkspaceDeleteStore } from '../../../state/workspace-delete-store'
|
|
2
|
+
import { useBusySpinner } from '../../hooks/use-busy-spinner'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
import { uiTokens } from '../../ui-tokens'
|
|
5
|
+
import { ModalShell } from '../modals/shared/modal-shell'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The card that stands in for the confirmation while the git work runs. Same
|
|
9
|
+
* shell, same width, same place on screen as the dialog that was just there, so
|
|
10
|
+
* confirming reads as the dialog changing what it says rather than the screen
|
|
11
|
+
* going quiet for a few seconds.
|
|
12
|
+
*/
|
|
13
|
+
function DeletingCard({ label }: { label: string }) {
|
|
14
|
+
const t = useTheme()
|
|
15
|
+
const spinner = useBusySpinner()
|
|
16
|
+
return (
|
|
17
|
+
<ModalShell title={`${spinner} Deleting workspace`} width={uiTokens.modalWidth.md}>
|
|
18
|
+
<text fg={t.textMuted}>{label}</text>
|
|
19
|
+
</ModalShell>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function WorkspaceDeletingOverlay() {
|
|
24
|
+
// Joined in the selector so the subscription compares a string: a fresh object
|
|
25
|
+
// every render would re-render the whole root on every unrelated store write.
|
|
26
|
+
const label = useWorkspaceDeleteStore((state) => Object.values(state.deleting).join(' · '))
|
|
27
|
+
if (label === '') return null
|
|
28
|
+
return <DeletingCard label={label} />
|
|
29
|
+
}
|
package/src/ui/root.tsx
CHANGED
|
@@ -44,6 +44,7 @@ import { WorkspaceMoveModal } from './components/modals/workspace/workspace-move
|
|
|
44
44
|
import { ContextMenuOverlay } from './components/overlays/context-menu/context-menu-overlay'
|
|
45
45
|
import { PendingChordOverlay } from './components/overlays/pending-chord-overlay'
|
|
46
46
|
import { ToastViewport } from './components/overlays/toast/toast-viewport'
|
|
47
|
+
import { WorkspaceDeletingOverlay } from './components/overlays/workspace-deleting-overlay'
|
|
47
48
|
import { SettingsView } from './components/settings/settings-view'
|
|
48
49
|
import { StatsView } from './components/stats/stats-view'
|
|
49
50
|
import { useTheme } from './theme'
|
|
@@ -541,6 +542,7 @@ export function RootView({
|
|
|
541
542
|
themeId,
|
|
542
543
|
workspaceDivergence,
|
|
543
544
|
})}
|
|
545
|
+
<WorkspaceDeletingOverlay />
|
|
544
546
|
<ToastViewport />
|
|
545
547
|
</box>
|
|
546
548
|
)
|