@brimveyn/aimux 1.12.2 → 1.13.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.
Files changed (133) hide show
  1. package/README.md +3 -0
  2. package/package.json +4 -3
  3. package/src/app-runtime/auto-commit-driver.ts +8 -4
  4. package/src/app-runtime/auto-commit-ref.ts +1 -1
  5. package/src/app-runtime/backend-attach-runtime.ts +18 -2
  6. package/src/app-runtime/backend-runtime-events.ts +1 -1
  7. package/src/app-runtime/pty-write.ts +52 -0
  8. package/src/app-runtime/selection-scroll.ts +2 -2
  9. package/src/app-runtime/session-actions.ts +16 -4
  10. package/src/app-runtime/side-effects.ts +598 -102
  11. package/src/app-runtime/snippet-actions.ts +25 -11
  12. package/src/app-runtime/use-auto-commit-driver.ts +7 -4
  13. package/src/app-runtime/use-backend-runtime.ts +3 -3
  14. package/src/app-runtime/use-directory-search.ts +7 -5
  15. package/src/app-runtime/use-mouse-handlers.ts +11 -6
  16. package/src/app-runtime/use-pane-size-report.ts +1 -1
  17. package/src/app-runtime/use-renderer-bindings.ts +143 -8
  18. package/src/app.tsx +25 -9
  19. package/src/auto-commit/headless-commands.ts +4 -6
  20. package/src/daemon/daemon.ts +3 -3
  21. package/src/daemon/runtime-paths.ts +1 -1
  22. package/src/daemon/session-manager.ts +1 -1
  23. package/src/daemon/session-registry.ts +10 -3
  24. package/src/diff-parser/parse-patch-files.ts +18 -18
  25. package/src/git/command-queue.ts +1 -1
  26. package/src/git/divergence.ts +46 -0
  27. package/src/git/git-diff.ts +12 -5
  28. package/src/git/git-poller.ts +33 -11
  29. package/src/git/git-status.ts +14 -3
  30. package/src/git/move-worktree.ts +96 -0
  31. package/src/git/use-repo-discovery.ts +2 -1
  32. package/src/git/worktree-divergence-poller.ts +59 -0
  33. package/src/git/worktree.ts +99 -0
  34. package/src/index.tsx +1 -1
  35. package/src/input/keymap/describe-bindings.ts +27 -9
  36. package/src/input/keymap/key-chord.ts +4 -4
  37. package/src/input/keymap/key-format.ts +2 -2
  38. package/src/input/keymap/sequence-resolver.ts +1 -1
  39. package/src/input/keymap/trie.ts +1 -1
  40. package/src/input/modes/bridge.ts +7 -0
  41. package/src/input/modes/transitions.ts +2 -1
  42. package/src/input/modes/types.ts +13 -1
  43. package/src/input/raw-input-handler.ts +34 -1
  44. package/src/input/terminal-text-extraction.ts +9 -5
  45. package/src/integrations/claude-syntax-overlay.ts +8 -8
  46. package/src/integrations/claude-theme-sync.ts +12 -12
  47. package/src/ipc/protocol.ts +3 -3
  48. package/src/platform/clipboard.ts +18 -2
  49. package/src/platform/worktree-deps.ts +22 -0
  50. package/src/platform/worktree-paths.ts +67 -0
  51. package/src/profile-paths.ts +3 -3
  52. package/src/pty/assistant-status-detection-loop.ts +2 -2
  53. package/src/pty/assistant-status-detector.ts +15 -6
  54. package/src/pty/command-registry.ts +8 -1
  55. package/src/pty/pty-manager.ts +1 -1
  56. package/src/services/ai-usage/adapters/claude.ts +5 -5
  57. package/src/services/ai-usage/adapters/codex.ts +8 -8
  58. package/src/services/ai-usage/cache.ts +2 -2
  59. package/src/services/ai-usage/pace.ts +3 -6
  60. package/src/services/ai-usage/provider.ts +1 -1
  61. package/src/session-backend/bootstrap.ts +2 -2
  62. package/src/session-backend/local-session-backend.ts +11 -11
  63. package/src/session-backend/remote-session-backend.ts +3 -3
  64. package/src/session-backend/types.ts +2 -2
  65. package/src/snippets/expand-variables.ts +112 -0
  66. package/src/snippets/run-shell-var.ts +93 -0
  67. package/src/snippets/trigger-detector.ts +105 -0
  68. package/src/state/ai-usage-store.ts +1 -1
  69. package/src/state/git-tree.ts +8 -4
  70. package/src/state/layout-tree.ts +8 -5
  71. package/src/state/reducers/git-mode-state.ts +44 -14
  72. package/src/state/reducers/git-panel-state.ts +38 -2
  73. package/src/state/reducers/modal-state.ts +385 -24
  74. package/src/state/reducers/session-state.ts +60 -1
  75. package/src/state/reducers/tab-state.ts +151 -85
  76. package/src/state/selectors.ts +6 -4
  77. package/src/state/session-catalog.ts +20 -5
  78. package/src/state/session-persistence.ts +8 -4
  79. package/src/state/session-worktrees.ts +227 -0
  80. package/src/state/snippet-catalog.ts +65 -4
  81. package/src/state/store.ts +1 -0
  82. package/src/state/toast-store.ts +95 -0
  83. package/src/state/types.ts +108 -16
  84. package/src/state/validation.ts +47 -3
  85. package/src/terminal-manager/manager-client.ts +15 -13
  86. package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
  87. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
  88. package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
  89. package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
  90. package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
  91. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
  92. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
  93. package/src/ui/components/git/git-panel.tsx +17 -10
  94. package/src/ui/components/git/git-view.tsx +79 -19
  95. package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
  96. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
  97. package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
  98. package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
  99. package/src/ui/components/layout/session-bar.tsx +3 -3
  100. package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
  101. package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
  102. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
  103. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
  104. package/src/ui/components/layout/split-layout.tsx +1 -1
  105. package/src/ui/components/layout/terminal-pane.tsx +43 -40
  106. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  107. package/src/ui/components/modals/app/help-modal.tsx +4 -2
  108. package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
  109. package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
  110. package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
  111. package/src/ui/components/modals/shared/form.tsx +4 -5
  112. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  113. package/src/ui/components/modals/shared/picker.tsx +28 -5
  114. package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
  115. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
  116. package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
  117. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
  118. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
  119. package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
  120. package/src/ui/components/overlays/toast/toast-position.ts +45 -0
  121. package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
  122. package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
  123. package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
  124. package/src/ui/components/primitives/input-field.tsx +1 -1
  125. package/src/ui/components/primitives/list-item.tsx +3 -3
  126. package/src/ui/filter-themes.ts +1 -1
  127. package/src/ui/root.tsx +65 -14
  128. package/src/ui/session-ordering.ts +2 -2
  129. package/src/ui/status-bar-model.ts +14 -7
  130. package/src/ui/terminal-graphics/capabilities.ts +1 -1
  131. package/src/ui/terminal-graphics/format-fallback.ts +7 -9
  132. package/src/ui/terminal-graphics/kitty.ts +2 -7
  133. package/src/update/version-check.ts +1 -1
@@ -0,0 +1,45 @@
1
+ import type { ToastPosition } from '../../../../state/toast-store'
2
+
3
+ export interface ToastAnchor {
4
+ vertical: 'top' | 'bottom'
5
+ horizontal: 'left' | 'center' | 'right'
6
+ }
7
+
8
+ export function parseToastPosition(position: ToastPosition): ToastAnchor {
9
+ const [vertical, horizontal] = position.split('-') as [
10
+ ToastAnchor['vertical'],
11
+ ToastAnchor['horizontal'],
12
+ ]
13
+ return { horizontal, vertical }
14
+ }
15
+
16
+ // Absolute box props that pin a stack to its corner. Center spans the width and
17
+ // centers its children; left/right hug their edge. Margin keeps it off the edge.
18
+ export function stackContainerProps(anchor: ToastAnchor, margin: number) {
19
+ const vertical = anchor.vertical === 'top' ? { top: margin } : { bottom: margin }
20
+ if (anchor.horizontal === 'left') {
21
+ return { ...vertical, alignItems: 'flex-start' as const, left: margin }
22
+ }
23
+ if (anchor.horizontal === 'right') {
24
+ return { ...vertical, alignItems: 'flex-end' as const, right: margin }
25
+ }
26
+ return { ...vertical, alignItems: 'center' as const, left: margin, right: margin }
27
+ }
28
+
29
+ interface SlideMargins {
30
+ marginLeft?: number
31
+ marginRight?: number
32
+ marginTop?: number
33
+ marginBottom?: number
34
+ }
35
+
36
+ // A negative margin on the entering edge pushes the toast that many cells off the
37
+ // nearest screen edge (the overflow is clipped by the screen buffer); animating it
38
+ // back to 0 slides the toast in. `offset` is cells off-screen (0 = resting). Used
39
+ // instead of opacity/transform, which terminals don't have.
40
+ export function slideOffsetStyle(anchor: ToastAnchor, offset: number): SlideMargins {
41
+ const off = -offset
42
+ if (anchor.horizontal === 'right') return { marginRight: off }
43
+ if (anchor.horizontal === 'left') return { marginLeft: off }
44
+ return anchor.vertical === 'top' ? { marginTop: off } : { marginBottom: off }
45
+ }
@@ -0,0 +1,30 @@
1
+ import { type Toast, TOAST_CONFIG, type ToastPosition } from '../../../../state/toast-store'
2
+ import { ToastItem } from './toast-item'
3
+ import { parseToastPosition, stackContainerProps } from './toast-position'
4
+
5
+ const EDGE_MARGIN = 1
6
+
7
+ interface ToastStackProps {
8
+ position: ToastPosition
9
+ toasts: Toast[]
10
+ }
11
+
12
+ // One corner's column of toasts. Newest sits nearest the anchored edge; the stack
13
+ // is capped at maxVisible (older toasts drop off, though they usually auto-dismiss).
14
+ export function ToastStack({ position, toasts }: ToastStackProps) {
15
+ const anchor = parseToastPosition(position)
16
+ const recent = toasts.slice(-TOAST_CONFIG.maxVisible)
17
+ const ordered = anchor.vertical === 'top' ? [...recent].reverse() : recent
18
+ return (
19
+ <box
20
+ position="absolute"
21
+ flexDirection="column"
22
+ gap={TOAST_CONFIG.gap}
23
+ {...stackContainerProps(anchor, EDGE_MARGIN)}
24
+ >
25
+ {ordered.map((toast) => (
26
+ <ToastItem key={toast.id} toast={toast} anchor={anchor} />
27
+ ))}
28
+ </box>
29
+ )
30
+ }
@@ -0,0 +1,24 @@
1
+ import { type Toast, type ToastPosition, useToastStore } from '../../../../state/toast-store'
2
+ import { ToastStack } from './toast-stack'
3
+
4
+ // Mounted once at the root. Groups active toasts by position and renders one
5
+ // absolutely-positioned stack per occupied corner, on top of everything else.
6
+ export function ToastViewport() {
7
+ const toasts = useToastStore((state) => state.toasts)
8
+ if (toasts.length === 0) return null
9
+
10
+ const byPosition = new Map<ToastPosition, Toast[]>()
11
+ for (const entry of toasts) {
12
+ const group = byPosition.get(entry.position) ?? []
13
+ group.push(entry)
14
+ byPosition.set(entry.position, group)
15
+ }
16
+
17
+ return (
18
+ <>
19
+ {[...byPosition].map(([position, group]) => (
20
+ <ToastStack key={position} position={position} toasts={group} />
21
+ ))}
22
+ </>
23
+ )
24
+ }
@@ -0,0 +1,64 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+ const STEP_MS = 28
4
+ // Normalized distance off-screen: 1 = fully hidden, 0 = resting. Eased so the
5
+ // toast decelerates in and accelerates out.
6
+ const ENTER_FRAMES = [1, 0.55, 0.3, 0.15, 0.05, 0]
7
+ const EXIT_FRAMES = [0.2, 0.45, 0.72, 1]
8
+
9
+ interface ToastAnimationOptions {
10
+ reduceMotion: boolean
11
+ onExited: () => void
12
+ }
13
+
14
+ /**
15
+ * Drives a toast's slide via a frame loop (terminals have no opacity/transform,
16
+ * so motion is a stepped integer offset). Returns `away` in [0,1] — multiply by a
17
+ * pixel distance to get the cell offset — and `requestExit` to slide back out.
18
+ */
19
+ export function useToastAnimation({ onExited, reduceMotion }: ToastAnimationOptions): {
20
+ away: number
21
+ requestExit: () => void
22
+ } {
23
+ const [away, setAway] = useState(reduceMotion ? 0 : 1)
24
+ const exitingRef = useRef(false)
25
+ const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
26
+ const onExitedRef = useRef(onExited)
27
+ onExitedRef.current = onExited
28
+
29
+ const runFrames = useCallback((frames: number[], done?: () => void) => {
30
+ if (timerRef.current != null) clearInterval(timerRef.current)
31
+ let index = 0
32
+ setAway(frames[0] ?? 0)
33
+ timerRef.current = setInterval(() => {
34
+ index += 1
35
+ if (index >= frames.length) {
36
+ if (timerRef.current != null) clearInterval(timerRef.current)
37
+ timerRef.current = null
38
+ done?.()
39
+ return
40
+ }
41
+ setAway(frames[index] ?? 0)
42
+ }, STEP_MS)
43
+ }, [])
44
+
45
+ useEffect(() => {
46
+ if (reduceMotion) return
47
+ runFrames(ENTER_FRAMES)
48
+ return () => {
49
+ if (timerRef.current != null) clearInterval(timerRef.current)
50
+ }
51
+ }, [reduceMotion, runFrames])
52
+
53
+ const requestExit = useCallback(() => {
54
+ if (exitingRef.current) return
55
+ exitingRef.current = true
56
+ if (reduceMotion) {
57
+ onExitedRef.current()
58
+ return
59
+ }
60
+ runFrames(EXIT_FRAMES, () => onExitedRef.current())
61
+ }, [reduceMotion, runFrames])
62
+
63
+ return { away, requestExit }
64
+ }
@@ -12,7 +12,7 @@ export function InputField({ active, cursorPos, placeholder, value }: InputField
12
12
  const t = useTheme()
13
13
  const fg = active ? t.text : t.textMuted
14
14
  if (!active) {
15
- const showPlaceholder = !value && !!placeholder
15
+ const showPlaceholder = !value && !!(placeholder != null && placeholder !== '')
16
16
  return (
17
17
  <Surface tone="input" padding={1}>
18
18
  <text fg={showPlaceholder ? t.textMuted : fg}>{showPlaceholder ? placeholder : value}</text>
@@ -40,11 +40,11 @@ export function ListItem({
40
40
  </>
41
41
  )}
42
42
  {leading}
43
- {leading ? <text> </text> : null}
43
+ {leading != null ? <text> </text> : null}
44
44
  <box flexGrow={isRow ? 0 : 1}>{title}</box>
45
- {trailing ? <box>{trailing}</box> : null}
45
+ {trailing != null ? <box>{trailing}</box> : null}
46
46
  </box>
47
- {subtitle ? <box paddingLeft={2}>{subtitle}</box> : null}
47
+ {subtitle != null ? <box paddingLeft={2}>{subtitle}</box> : null}
48
48
  </box>
49
49
  )
50
50
  return (
@@ -8,7 +8,7 @@ export function themeDisplayName(id: ThemeId): string {
8
8
  }
9
9
 
10
10
  export function filterThemeIds(filter: string | null): ThemeId[] {
11
- if (!filter) return THEME_IDS
11
+ if (!(filter != null && filter !== '')) return THEME_IDS
12
12
  const needle = filter.toLowerCase()
13
13
  return THEME_IDS.filter((id) => {
14
14
  return id.toLowerCase().includes(needle) || themeDisplayName(id).toLowerCase().includes(needle)
package/src/ui/root.tsx CHANGED
@@ -28,9 +28,11 @@ import { SnippetEditorModal } from './components/modals/snippets/snippet-editor-
28
28
  import { SnippetPickerModal } from './components/modals/snippets/snippet-picker-modal'
29
29
  import { NewTabModal } from './components/modals/tabs/new-tab-modal'
30
30
  import { ThemePickerModal } from './components/modals/themes/theme-picker-modal'
31
+ import { WorktreeMoveModal } from './components/modals/worktree/worktree-move-modal'
31
32
  import { ContextMenuBox } from './components/overlays/context-menu/context-menu-box'
32
33
  import { ContextMenuOverlay } from './components/overlays/context-menu/context-menu-overlay'
33
34
  import { PendingChordOverlay } from './components/overlays/pending-chord-overlay'
35
+ import { ToastViewport } from './components/overlays/toast/toast-viewport'
34
36
  import { useTheme } from './theme'
35
37
 
36
38
  function getCreateSessionFields(modal: ModalState) {
@@ -53,19 +55,14 @@ function getCreateSessionFields(modal: ModalState) {
53
55
 
54
56
  function getSnippetEditorFields(modal: ModalState) {
55
57
  if (modal.type !== 'snippet-editor') {
56
- return { snippetContent: '', snippetName: '' }
57
- }
58
-
59
- if (modal.activeField === 'name') {
60
- return {
61
- snippetContent: modal.contentBuffer,
62
- snippetName: modal.editBuffer ?? '',
63
- }
58
+ return { snippetContent: '', snippetName: '', snippetTrigger: '' }
64
59
  }
65
60
 
61
+ const editValue = modal.editBuffer ?? ''
66
62
  return {
67
- snippetContent: modal.editBuffer ?? '',
68
- snippetName: modal.contentBuffer,
63
+ snippetContent: modal.activeField === 'content' ? editValue : modal.contentBuffer,
64
+ snippetName: modal.activeField === 'name' ? editValue : modal.nameBuffer,
65
+ snippetTrigger: modal.activeField === 'trigger' ? editValue : modal.triggerBuffer,
69
66
  }
70
67
  }
71
68
 
@@ -79,10 +76,11 @@ function renderModal(
79
76
  snippets: SnippetRecord[]
80
77
  themeId: ThemeId
81
78
  createSessionFields: { directoryQuery: string; sessionName: string }
82
- snippetEditorFields: { snippetName: string; snippetContent: string }
79
+ snippetEditorFields: { snippetName: string; snippetTrigger: string; snippetContent: string }
83
80
  focusMode: FocusMode
84
81
  activeAssistant?: string
85
82
  autoCommitModel?: string
83
+ worktreeDivergence: Record<string, { ahead: number; behind: number }>
86
84
  }
87
85
  ) {
88
86
  switch (modal.type) {
@@ -94,8 +92,24 @@ function renderModal(
94
92
  customCommands={options.customCommands}
95
93
  filter={modal.editBuffer}
96
94
  cursorPos={modal.cursorPos}
95
+ currentSessionId={options.currentSessionId}
97
96
  editingCommand={modal.type === 'new-tab' ? modal.editingCommand : null}
98
97
  editBuffer={modal.editBuffer ?? ''}
98
+ activeField={modal.type === 'new-tab' ? modal.activeField : 'assistant'}
99
+ branchError={modal.type === 'new-tab' ? modal.branchError : null}
100
+ branchName={modal.type === 'new-tab' ? modal.branchName : ''}
101
+ createWorktree={modal.type === 'new-tab' ? modal.createWorktree : false}
102
+ selectedAssistantId={modal.type === 'new-tab' ? modal.selectedAssistantId : null}
103
+ step={modal.type === 'new-tab' ? modal.step : 'assistant'}
104
+ worktreeDeleteConfirmId={modal.type === 'new-tab' ? modal.worktreeDeleteConfirmId : null}
105
+ worktreeDeleteMessage={modal.type === 'new-tab' ? modal.worktreeDeleteMessage : null}
106
+ worktrees={
107
+ options.currentSessionId != null && options.currentSessionId !== ''
108
+ ? (options.sessions.find((session) => session.id === options.currentSessionId)
109
+ ?.worktrees ?? [])
110
+ : []
111
+ }
112
+ worktreeName={modal.type === 'new-tab' ? modal.worktreeName : ''}
99
113
  />
100
114
  )
101
115
  case 'session-picker':
@@ -112,7 +126,11 @@ function renderModal(
112
126
  case 'session-name':
113
127
  return (
114
128
  <SessionNameModal
115
- title={modal.sessionTargetId ? 'Rename workspace' : 'Create workspace'}
129
+ title={
130
+ modal.sessionTargetId != null && modal.sessionTargetId !== ''
131
+ ? 'Rename workspace'
132
+ : 'Create workspace'
133
+ }
116
134
  value={modal.editBuffer ?? ''}
117
135
  />
118
136
  )
@@ -136,6 +154,7 @@ function renderModal(
136
154
  selectedIndex={modal.selectedIndex}
137
155
  filter={modal.editBuffer}
138
156
  cursorPos={modal.cursorPos}
157
+ actionMessage={modal.actionMessage}
139
158
  />
140
159
  )
141
160
  case 'snippet-editor':
@@ -143,6 +162,7 @@ function renderModal(
143
162
  <SnippetEditorModal
144
163
  activeField={modal.activeField}
145
164
  snippetName={options.snippetEditorFields.snippetName}
165
+ snippetTrigger={options.snippetEditorFields.snippetTrigger}
146
166
  snippetContent={options.snippetEditorFields.snippetContent}
147
167
  isEditing={modal.sessionTargetId !== null}
148
168
  />
@@ -164,6 +184,29 @@ function renderModal(
164
184
  latestVersion={modal.latestVersion}
165
185
  />
166
186
  )
187
+ case 'worktree-move': {
188
+ const session =
189
+ options.currentSessionId != null && options.currentSessionId !== ''
190
+ ? options.sessions.find((entry) => entry.id === options.currentSessionId)
191
+ : undefined
192
+ const worktrees = session?.worktrees ?? []
193
+ const sourceId = modal.sourceWorktreeId
194
+ const source = worktrees.find((w) => w.id === sourceId)
195
+ const targets = worktrees.filter((w) => w.id !== sourceId)
196
+ const sourceLabel =
197
+ source?.branch != null && source.branch !== ''
198
+ ? source.branch
199
+ : (source?.name ?? 'worktree')
200
+ return (
201
+ <WorktreeMoveModal
202
+ deleteSource={modal.type === 'worktree-move' ? modal.deleteSource : false}
203
+ divergence={options.worktreeDivergence}
204
+ selectedIndex={modal.selectedIndex}
205
+ sourceLabel={sourceLabel}
206
+ targets={targets}
207
+ />
208
+ )
209
+ }
167
210
  case 'help':
168
211
  return (
169
212
  <HelpModal
@@ -269,6 +312,7 @@ export function RootView({
269
312
  const customCommands = useAppStore((s) => s.customCommands)
270
313
  const sessions = useAppStore((s) => s.sessions)
271
314
  const currentSessionId = useAppStore((s) => s.currentSessionId)
315
+ const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
272
316
  const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
273
317
  const gitPaneMode = useAppStore((s) => s.gitPane.mode)
274
318
  const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
@@ -287,7 +331,10 @@ export function RootView({
287
331
  : undefined
288
332
 
289
333
  const activeTab = tabs.find((tab) => tab.id === activeTabId)
290
- const activeTree = activeTabId ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId) : null
334
+ const activeTree =
335
+ activeTabId != null && activeTabId !== ''
336
+ ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId)
337
+ : null
291
338
  const createSessionFields = getCreateSessionFields(modal)
292
339
  const snippetEditorFields = getSnippetEditorFields(modal)
293
340
  const splitChrome = PANE_BORDER * 2
@@ -313,7 +360,9 @@ export function RootView({
313
360
  snippetEditorFields,
314
361
  snippets,
315
362
  themeId,
363
+ worktreeDivergence,
316
364
  })}
365
+ <ToastViewport />
317
366
  </box>
318
367
  )
319
368
  }
@@ -325,7 +374,7 @@ export function RootView({
325
374
  height="100%"
326
375
  backgroundColor={editorBg}
327
376
  onMouseDrag={(event) => {
328
- if (onSeparatorDrag?.(event)) {
377
+ if (onSeparatorDrag?.(event) === true) {
329
378
  event.preventDefault()
330
379
  event.stopPropagation()
331
380
  }
@@ -427,7 +476,9 @@ export function RootView({
427
476
  snippetEditorFields,
428
477
  snippets,
429
478
  themeId,
479
+ worktreeDivergence,
430
480
  })}
481
+ <ToastViewport />
431
482
  </box>
432
483
  )
433
484
  }
@@ -5,7 +5,7 @@ import type { SessionRecord } from '../state/types'
5
5
  * with any missing `order` falling back to `createdAt` ascending.
6
6
  */
7
7
  export function orderSessionsForDisplay(sessions: SessionRecord[]): SessionRecord[] {
8
- return sessions.slice().sort((a, b) => {
8
+ return [...sessions].sort((a, b) => {
9
9
  const ao = a.order ?? Number.MAX_SAFE_INTEGER
10
10
  const bo = b.order ?? Number.MAX_SAFE_INTEGER
11
11
  if (ao !== bo) return ao - bo
@@ -27,7 +27,7 @@ export function moveIdToIdPosition(
27
27
  const from = ids.indexOf(moveId)
28
28
  const to = ids.indexOf(intoPositionOfId)
29
29
  if (from < 0 || to < 0) return ids
30
- const next = ids.slice()
30
+ const next = [...ids]
31
31
  next.splice(from, 1)
32
32
  next.splice(to, 0, moveId)
33
33
  return next
@@ -3,6 +3,7 @@ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
3
3
  import type { AppState, TabSession } from '../state/types'
4
4
 
5
5
  import { describeBindings } from '../input/keymap/describe-bindings'
6
+ import { getSessionProjectPath } from '../state/session-worktrees'
6
7
  import { buildHintText } from './keymap-context'
7
8
  import { abbreviatePath } from './path-format'
8
9
 
@@ -58,13 +59,16 @@ export function getStatusBarModel(
58
59
  activeTab: TabSession | undefined,
59
60
  config: ResolvedKeymapConfig
60
61
  ): StatusBarModel {
61
- const currentSession = state.currentSessionId
62
- ? state.sessions.find((session) => session.id === state.currentSessionId)
63
- : undefined
62
+ const currentSession =
63
+ state.currentSessionId != null && state.currentSessionId !== ''
64
+ ? state.sessions.find((session) => session.id === state.currentSessionId)
65
+ : undefined
64
66
  const sessionName = currentSession?.name ?? 'no workspace'
65
- const sessionLabel = currentSession?.projectPath
66
- ? `${sessionName} (${abbreviatePath(currentSession.projectPath)})`
67
- : sessionName
67
+ const sessionPath = getSessionProjectPath(currentSession)
68
+ const sessionLabel =
69
+ sessionPath != null && sessionPath !== ''
70
+ ? `${sessionName} (${abbreviatePath(sessionPath)})`
71
+ : sessionName
68
72
 
69
73
  // \u{f0b1} = nf-fa-briefcase (session icon)
70
74
  const sessionIcon = '\u{f0b1}'
@@ -87,9 +91,10 @@ export function getStatusBarModel(
87
91
  case 'git': {
88
92
  const headOffset = state.gitMode.headOffset
89
93
  const offsetTag = headOffset > 0 ? ` HEAD~${headOffset}` : ''
94
+ const reviewTag = state.gitMode.reviewBase ? ' vs base' : ''
90
95
  return {
91
96
  help: helpHintForMode(config, 'git-mode'),
92
- left: `${sessionIcon} ${sessionLabel}${offsetTag}`,
97
+ left: `${sessionIcon} ${sessionLabel}${offsetTag}${reviewTag}`,
93
98
  right: hintForGitMode(config, headOffset),
94
99
  }
95
100
  }
@@ -127,6 +132,8 @@ function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null
127
132
  return 'modal.theme-picker.filtering'
128
133
  case 'update-available':
129
134
  return 'modal.update-available'
135
+ case 'worktree-move':
136
+ return 'modal.worktree-move'
130
137
  default:
131
138
  return null
132
139
  }
@@ -11,7 +11,7 @@ let cached: GraphicsProtocol | null = null
11
11
  export function detectGraphicsProtocol(renderer: CliRenderer): GraphicsProtocol {
12
12
  if (cached !== null) return cached
13
13
  const caps = (renderer as RendererWithCapabilities).capabilities
14
- if (caps?.kitty_graphics) {
14
+ if (caps?.kitty_graphics === true) {
15
15
  cached = 'kitty'
16
16
  return cached
17
17
  }
@@ -24,10 +24,8 @@ async function tryConverter(
24
24
  stdout: 'pipe',
25
25
  })
26
26
  const writer = proc.stdin
27
- if (writer) {
28
- writer.write(bytes)
29
- await writer.end()
30
- }
27
+ void writer.write(bytes)
28
+ await writer.end()
31
29
  const timer = setTimeout(() => proc.kill(), timeoutMs)
32
30
  const [out, code] = await Promise.all([new Response(proc.stdout).bytes(), proc.exited])
33
31
  clearTimeout(timer)
@@ -85,11 +83,11 @@ export async function convertToPng(bytes: Uint8Array, mime: string): Promise<Con
85
83
  const png = await renderSvgToPng(bytes)
86
84
  result = png ? { kind: 'ok', png } : { kind: 'error', reason: 'failed to render SVG' }
87
85
  } else {
88
- const attempts: Array<() => Promise<Uint8Array | null>> = [
89
- () => tryConverter(['magick', '-', 'png:-'], bytes, 1500),
90
- () => tryConverter(['convert', '-', 'png:-'], bytes, 1500),
91
- () => trySips(bytes),
92
- () =>
86
+ const attempts: (() => Promise<Uint8Array | null>)[] = [
87
+ async () => tryConverter(['magick', '-', 'png:-'], bytes, 1500),
88
+ async () => tryConverter(['convert', '-', 'png:-'], bytes, 1500),
89
+ async () => trySips(bytes),
90
+ async () =>
93
91
  tryConverter(
94
92
  [
95
93
  'ffmpeg',
@@ -53,13 +53,8 @@ export function uploadPngEscape(pngBytes: Uint8Array, id: number): string {
53
53
  for (let i = 0; i < chunks.length; i++) {
54
54
  const isLast = i === chunks.length - 1
55
55
  const m = isLast ? 0 : 1
56
- let header: string
57
- if (i === 0) {
58
- // a=t (transmit), t=d (direct), f=100 (PNG), q=2 (quiet).
59
- header = `q=2,a=t,t=d,f=100,i=${id},m=${m}`
60
- } else {
61
- header = `m=${m},q=2`
62
- }
56
+ // a=t (transmit), t=d (direct), f=100 (PNG), q=2 (quiet).
57
+ const header = i === 0 ? `q=2,a=t,t=d,f=100,i=${id},m=${m}` : `m=${m},q=2`
63
58
  parts.push(`${ESC}_G${header};${chunks[i]}${ST}`)
64
59
  }
65
60
  return wrapForTmux(parts.join(''))
@@ -9,7 +9,7 @@ export async function getCurrentPackageVersion(): Promise<string> {
9
9
 
10
10
  export async function fetchLatestNpmVersion(packageName: string): Promise<string | null> {
11
11
  const debugOverride = process.env.AIMUX_DEBUG_UPDATE_LATEST
12
- if (debugOverride) {
12
+ if (debugOverride != null && debugOverride !== '') {
13
13
  return debugOverride
14
14
  }
15
15