@brimveyn/aimux 1.12.6 → 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 (130) 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/selection-scroll.ts +2 -2
  8. package/src/app-runtime/session-actions.ts +16 -4
  9. package/src/app-runtime/side-effects.ts +538 -83
  10. package/src/app-runtime/snippet-actions.ts +3 -3
  11. package/src/app-runtime/use-auto-commit-driver.ts +7 -4
  12. package/src/app-runtime/use-backend-runtime.ts +3 -3
  13. package/src/app-runtime/use-directory-search.ts +7 -5
  14. package/src/app-runtime/use-mouse-handlers.ts +11 -6
  15. package/src/app-runtime/use-pane-size-report.ts +1 -1
  16. package/src/app-runtime/use-renderer-bindings.ts +6 -5
  17. package/src/app.tsx +12 -7
  18. package/src/auto-commit/headless-commands.ts +4 -6
  19. package/src/daemon/daemon.ts +3 -3
  20. package/src/daemon/runtime-paths.ts +1 -1
  21. package/src/daemon/session-manager.ts +1 -1
  22. package/src/daemon/session-registry.ts +10 -3
  23. package/src/diff-parser/parse-patch-files.ts +18 -18
  24. package/src/git/command-queue.ts +1 -1
  25. package/src/git/divergence.ts +46 -0
  26. package/src/git/git-diff.ts +12 -5
  27. package/src/git/git-poller.ts +33 -11
  28. package/src/git/git-status.ts +14 -3
  29. package/src/git/move-worktree.ts +96 -0
  30. package/src/git/use-repo-discovery.ts +2 -1
  31. package/src/git/worktree-divergence-poller.ts +59 -0
  32. package/src/git/worktree.ts +99 -0
  33. package/src/index.tsx +1 -1
  34. package/src/input/keymap/describe-bindings.ts +27 -9
  35. package/src/input/keymap/key-chord.ts +4 -4
  36. package/src/input/keymap/key-format.ts +2 -2
  37. package/src/input/keymap/sequence-resolver.ts +1 -1
  38. package/src/input/keymap/trie.ts +1 -1
  39. package/src/input/modes/bridge.ts +7 -0
  40. package/src/input/modes/transitions.ts +2 -1
  41. package/src/input/modes/types.ts +11 -1
  42. package/src/input/raw-input-handler.ts +1 -1
  43. package/src/input/terminal-text-extraction.ts +9 -5
  44. package/src/integrations/claude-syntax-overlay.ts +8 -8
  45. package/src/integrations/claude-theme-sync.ts +12 -12
  46. package/src/ipc/protocol.ts +3 -3
  47. package/src/platform/clipboard.ts +4 -2
  48. package/src/platform/worktree-deps.ts +22 -0
  49. package/src/platform/worktree-paths.ts +67 -0
  50. package/src/profile-paths.ts +3 -3
  51. package/src/pty/assistant-status-detection-loop.ts +2 -2
  52. package/src/pty/assistant-status-detector.ts +15 -6
  53. package/src/pty/command-registry.ts +2 -1
  54. package/src/pty/pty-manager.ts +1 -1
  55. package/src/services/ai-usage/adapters/claude.ts +5 -5
  56. package/src/services/ai-usage/adapters/codex.ts +8 -8
  57. package/src/services/ai-usage/cache.ts +2 -2
  58. package/src/services/ai-usage/pace.ts +3 -6
  59. package/src/services/ai-usage/provider.ts +1 -1
  60. package/src/session-backend/bootstrap.ts +2 -2
  61. package/src/session-backend/local-session-backend.ts +11 -11
  62. package/src/session-backend/remote-session-backend.ts +3 -3
  63. package/src/session-backend/types.ts +2 -2
  64. package/src/snippets/expand-variables.ts +7 -7
  65. package/src/snippets/run-shell-var.ts +1 -1
  66. package/src/snippets/trigger-detector.ts +1 -1
  67. package/src/state/ai-usage-store.ts +1 -1
  68. package/src/state/git-tree.ts +8 -4
  69. package/src/state/layout-tree.ts +8 -5
  70. package/src/state/reducers/git-mode-state.ts +44 -14
  71. package/src/state/reducers/git-panel-state.ts +38 -2
  72. package/src/state/reducers/modal-state.ts +355 -21
  73. package/src/state/reducers/session-state.ts +60 -1
  74. package/src/state/reducers/tab-state.ts +151 -85
  75. package/src/state/selectors.ts +6 -4
  76. package/src/state/session-catalog.ts +20 -5
  77. package/src/state/session-persistence.ts +8 -4
  78. package/src/state/session-worktrees.ts +227 -0
  79. package/src/state/store.ts +1 -0
  80. package/src/state/toast-store.ts +95 -0
  81. package/src/state/types.ts +92 -13
  82. package/src/state/validation.ts +25 -2
  83. package/src/terminal-manager/manager-client.ts +15 -13
  84. package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
  85. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
  86. package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
  87. package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
  88. package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
  89. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
  90. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
  91. package/src/ui/components/git/git-panel.tsx +17 -10
  92. package/src/ui/components/git/git-view.tsx +79 -19
  93. package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
  94. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
  95. package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
  96. package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
  97. package/src/ui/components/layout/session-bar.tsx +3 -3
  98. package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
  99. package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
  100. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
  101. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
  102. package/src/ui/components/layout/split-layout.tsx +1 -1
  103. package/src/ui/components/layout/terminal-pane.tsx +43 -40
  104. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  105. package/src/ui/components/modals/app/help-modal.tsx +4 -2
  106. package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
  107. package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
  108. package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
  109. package/src/ui/components/modals/shared/form.tsx +4 -5
  110. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  111. package/src/ui/components/modals/shared/picker.tsx +28 -5
  112. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +11 -3
  113. package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
  114. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
  115. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
  116. package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
  117. package/src/ui/components/overlays/toast/toast-position.ts +45 -0
  118. package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
  119. package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
  120. package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
  121. package/src/ui/components/primitives/input-field.tsx +1 -1
  122. package/src/ui/components/primitives/list-item.tsx +3 -3
  123. package/src/ui/filter-themes.ts +1 -1
  124. package/src/ui/root.tsx +57 -3
  125. package/src/ui/session-ordering.ts +2 -2
  126. package/src/ui/status-bar-model.ts +14 -7
  127. package/src/ui/terminal-graphics/capabilities.ts +1 -1
  128. package/src/ui/terminal-graphics/format-fallback.ts +7 -9
  129. package/src/ui/terminal-graphics/kitty.ts +2 -7
  130. package/src/update/version-check.ts +1 -1
@@ -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) {
@@ -78,6 +80,7 @@ function renderModal(
78
80
  focusMode: FocusMode
79
81
  activeAssistant?: string
80
82
  autoCommitModel?: string
83
+ worktreeDivergence: Record<string, { ahead: number; behind: number }>
81
84
  }
82
85
  ) {
83
86
  switch (modal.type) {
@@ -89,8 +92,24 @@ function renderModal(
89
92
  customCommands={options.customCommands}
90
93
  filter={modal.editBuffer}
91
94
  cursorPos={modal.cursorPos}
95
+ currentSessionId={options.currentSessionId}
92
96
  editingCommand={modal.type === 'new-tab' ? modal.editingCommand : null}
93
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 : ''}
94
113
  />
95
114
  )
96
115
  case 'session-picker':
@@ -107,7 +126,11 @@ function renderModal(
107
126
  case 'session-name':
108
127
  return (
109
128
  <SessionNameModal
110
- title={modal.sessionTargetId ? 'Rename workspace' : 'Create workspace'}
129
+ title={
130
+ modal.sessionTargetId != null && modal.sessionTargetId !== ''
131
+ ? 'Rename workspace'
132
+ : 'Create workspace'
133
+ }
111
134
  value={modal.editBuffer ?? ''}
112
135
  />
113
136
  )
@@ -161,6 +184,29 @@ function renderModal(
161
184
  latestVersion={modal.latestVersion}
162
185
  />
163
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
+ }
164
210
  case 'help':
165
211
  return (
166
212
  <HelpModal
@@ -266,6 +312,7 @@ export function RootView({
266
312
  const customCommands = useAppStore((s) => s.customCommands)
267
313
  const sessions = useAppStore((s) => s.sessions)
268
314
  const currentSessionId = useAppStore((s) => s.currentSessionId)
315
+ const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
269
316
  const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
270
317
  const gitPaneMode = useAppStore((s) => s.gitPane.mode)
271
318
  const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
@@ -284,7 +331,10 @@ export function RootView({
284
331
  : undefined
285
332
 
286
333
  const activeTab = tabs.find((tab) => tab.id === activeTabId)
287
- const activeTree = activeTabId ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId) : null
334
+ const activeTree =
335
+ activeTabId != null && activeTabId !== ''
336
+ ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId)
337
+ : null
288
338
  const createSessionFields = getCreateSessionFields(modal)
289
339
  const snippetEditorFields = getSnippetEditorFields(modal)
290
340
  const splitChrome = PANE_BORDER * 2
@@ -310,7 +360,9 @@ export function RootView({
310
360
  snippetEditorFields,
311
361
  snippets,
312
362
  themeId,
363
+ worktreeDivergence,
313
364
  })}
365
+ <ToastViewport />
314
366
  </box>
315
367
  )
316
368
  }
@@ -322,7 +374,7 @@ export function RootView({
322
374
  height="100%"
323
375
  backgroundColor={editorBg}
324
376
  onMouseDrag={(event) => {
325
- if (onSeparatorDrag?.(event)) {
377
+ if (onSeparatorDrag?.(event) === true) {
326
378
  event.preventDefault()
327
379
  event.stopPropagation()
328
380
  }
@@ -424,7 +476,9 @@ export function RootView({
424
476
  snippetEditorFields,
425
477
  snippets,
426
478
  themeId,
479
+ worktreeDivergence,
427
480
  })}
481
+ <ToastViewport />
428
482
  </box>
429
483
  )
430
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