@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.24.0",
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.8",
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
- reason: message,
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
@@ -95,7 +95,7 @@ export type ModalAction =
95
95
  projectId: string
96
96
  workspaceId: string
97
97
  workspaceLabel: string
98
- reason: string
98
+ reason?: string
99
99
  closeTabs: boolean
100
100
  force: boolean
101
101
  }
@@ -648,7 +648,7 @@ export interface ModalWorkspaceDeleteConfirm extends ModalBase {
648
648
  projectId: string
649
649
  workspaceId: string
650
650
  workspaceLabel: string
651
- reason: string
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
- <text fg={t.textMuted} selectable={false} wrapMode="none">
258
- {'\u{e702}'} {branchLabel}
259
- </text>
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
- reason: string
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
- <box flexDirection="column" gap={1}>
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
  )