@brimveyn/aimux 1.12.6 → 1.13.2

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 +29 -13
  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 +563 -85
  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 +21 -11
  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 +19 -3
  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 +60 -45
  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 +32 -24
  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 +39 -11
  88. package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
  89. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
  90. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
  91. package/src/ui/components/git/git-panel.tsx +129 -83
  92. package/src/ui/components/git/git-view.tsx +96 -28
  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 +14 -13
  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 +135 -117
  98. package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
  99. package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
  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 +27 -20
  103. package/src/ui/components/layout/terminal-pane.tsx +163 -120
  104. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  105. package/src/ui/components/modals/app/help-modal.tsx +29 -20
  106. package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
  107. package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
  108. package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
  109. package/src/ui/components/modals/shared/form.tsx +6 -6
  110. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  111. package/src/ui/components/modals/shared/picker.tsx +71 -32
  112. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
  113. package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
  114. package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
  115. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
  116. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
  117. package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
  118. package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
  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 +23 -7
  126. package/src/ui/filter-themes.ts +1 -1
  127. package/src/ui/root.tsx +121 -38
  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
@@ -1,9 +1,9 @@
1
1
  import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
2
 
3
- import { memo, type ReactNode } from 'react'
3
+ import { memo, type ReactNode, useCallback, useMemo } from 'react'
4
4
 
5
5
  import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
6
- import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
6
+ import type { FocusMode, TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
7
7
 
8
8
  import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
9
9
  import { logInputDebug } from '../../../debug/input-log'
@@ -15,7 +15,7 @@ import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
15
15
  interface TerminalPaneProps {
16
16
  tab?: TabSession
17
17
  tabId?: string
18
- focusMode: import('../../../state/types').FocusMode
18
+ focusMode: FocusMode
19
19
  isActive?: boolean
20
20
  contentOrigin: TerminalContentOrigin
21
21
  mouseForwardingEnabled: boolean
@@ -61,15 +61,15 @@ function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMo
61
61
  function renderSpan(span: TerminalSpan, key: string): ReactNode {
62
62
  let node: ReactNode = span.text
63
63
 
64
- if (span.underline) {
64
+ if (span.underline === true) {
65
65
  node = <u>{node}</u>
66
66
  }
67
67
 
68
- if (span.italic) {
68
+ if (span.italic === true) {
69
69
  node = <em>{node}</em>
70
70
  }
71
71
 
72
- if (span.bold) {
72
+ if (span.bold === true) {
73
73
  node = <strong>{node}</strong>
74
74
  }
75
75
 
@@ -95,6 +95,8 @@ const TerminalViewport = memo(function TerminalViewport({
95
95
  return (
96
96
  <text fg={t.text}>
97
97
  {lines.map((line, lineIndex) => (
98
+ // Terminal rows are a fixed positional grid; the row index is the identity.
99
+ // eslint-disable-next-line react/no-array-index-key
98
100
  <span key={`line-${lineIndex}`}>
99
101
  {line.spans.map((span, spanIndex) => renderSpan(span, `s-${spanIndex}`))}
100
102
  {lineIndex < lines.length - 1 ? '\n' : ''}
@@ -132,124 +134,169 @@ export function TerminalPane({
132
134
  const paneIsActive = isActive ?? true
133
135
  const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
134
136
  const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
135
- const rightClickMenu: ContextMenuItem[] | undefined = tabId
136
- ? [
137
- [
138
- 'Split vertically',
139
- () =>
140
- runSideEffectGlobal({
141
- direction: 'vertical',
142
- sourceTabId: tabId,
143
- type: 'split-pane',
144
- }),
145
- ],
146
- [
147
- 'Split horizontally',
148
- () =>
149
- runSideEffectGlobal({
150
- direction: 'horizontal',
151
- sourceTabId: tabId,
152
- type: 'split-pane',
153
- }),
154
- ],
155
- [
156
- 'Close pane',
157
- () => {
158
- dispatchGlobal({ tabId, type: 'close-pane' })
159
- runSideEffectGlobal({ tabId, type: 'close-tab' })
160
- },
161
- ],
162
- ]
163
- : undefined
164
- const isOnPaneBorder = (event: OtuiMouseEvent) =>
165
- event.x === contentOrigin.x - 1 ||
166
- event.x === contentOrigin.x + contentOrigin.cols ||
167
- event.y === contentOrigin.y - 1 ||
168
- event.y === contentOrigin.y + contentOrigin.rows
169
- const forwardMouseEvent = (event: OtuiMouseEvent) => {
170
- if (event.type === 'down' && event.button === 2 && rightClickMenu) {
171
- event.preventDefault()
172
- event.stopPropagation()
173
- openContextMenu(event.x, event.y, rightClickMenu)
174
- return
175
- }
176
- if (
177
- event.type === 'down' &&
178
- event.button === 0 &&
179
- onLeftEdgeMouseDown &&
180
- event.x === contentOrigin.x - 1
181
- ) {
182
- if (onLeftEdgeMouseDown(event)) {
137
+ const rightClickMenu = useMemo<ContextMenuItem[] | undefined>(
138
+ () =>
139
+ tabId != null && tabId !== ''
140
+ ? [
141
+ [
142
+ 'Split vertically',
143
+ () =>
144
+ runSideEffectGlobal({
145
+ direction: 'vertical',
146
+ sourceTabId: tabId,
147
+ type: 'split-pane',
148
+ }),
149
+ ],
150
+ [
151
+ 'Split horizontally',
152
+ () =>
153
+ runSideEffectGlobal({
154
+ direction: 'horizontal',
155
+ sourceTabId: tabId,
156
+ type: 'split-pane',
157
+ }),
158
+ ],
159
+ [
160
+ 'Close pane',
161
+ () => {
162
+ dispatchGlobal({ tabId, type: 'close-pane' })
163
+ runSideEffectGlobal({ tabId, type: 'close-tab' })
164
+ },
165
+ ],
166
+ ]
167
+ : undefined,
168
+ [tabId]
169
+ )
170
+ const isOnPaneBorder = useCallback(
171
+ (event: OtuiMouseEvent) =>
172
+ event.x === contentOrigin.x - 1 ||
173
+ event.x === contentOrigin.x + contentOrigin.cols ||
174
+ event.y === contentOrigin.y - 1 ||
175
+ event.y === contentOrigin.y + contentOrigin.rows,
176
+ [contentOrigin]
177
+ )
178
+ const forwardMouseEvent = useCallback(
179
+ (event: OtuiMouseEvent) => {
180
+ if (event.type === 'down' && event.button === 2 && rightClickMenu) {
183
181
  event.preventDefault()
184
182
  event.stopPropagation()
183
+ openContextMenu(event.x, event.y, rightClickMenu)
185
184
  return
186
185
  }
187
- }
188
- // Absorb left-button clicks on pane borders — they should not focus the
189
- // pane or be forwarded to the terminal. This keeps borders available for
190
- // resize actions (split separators, sidebar edge) without conflicting
191
- // with focus-on-click behavior in the content area.
192
- if (event.type === 'down' && event.button === 0 && isOnPaneBorder(event)) {
193
- event.preventDefault()
194
- event.stopPropagation()
195
- return
196
- }
197
- if (event.type === 'down') {
198
- logInputDebug('pane.mouseDown', {
199
- button: event.button,
200
- canForward: canForwardMouse,
201
- eventType: event.type,
202
- tabId,
203
- willClick: !canForwardMouse && !!tab && event.button === 0 && !!onTerminalClick,
204
- x: event.x,
205
- y: event.y,
206
- })
207
- }
208
- if (event.type === 'drag') {
209
- if (onTerminalDrag?.(event, contentOrigin, tabId)) {
210
- event.preventDefault()
211
- return
186
+ if (
187
+ event.type === 'down' &&
188
+ event.button === 0 &&
189
+ onLeftEdgeMouseDown &&
190
+ event.x === contentOrigin.x - 1
191
+ ) {
192
+ if (onLeftEdgeMouseDown(event)) {
193
+ event.preventDefault()
194
+ event.stopPropagation()
195
+ return
196
+ }
212
197
  }
213
- if (onSeparatorDrag?.(event)) {
198
+ // Absorb left-button clicks on pane borders — they should not focus the
199
+ // pane or be forwarded to the terminal. This keeps borders available for
200
+ // resize actions (split separators, sidebar edge) without conflicting
201
+ // with focus-on-click behavior in the content area.
202
+ if (event.type === 'down' && event.button === 0 && isOnPaneBorder(event)) {
214
203
  event.preventDefault()
204
+ event.stopPropagation()
215
205
  return
216
206
  }
217
- }
218
- if (event.type === 'up') {
219
- if (onTerminalMouseUp?.(event)) {
220
- event.preventDefault()
207
+ if (event.type === 'down') {
208
+ logInputDebug('pane.mouseDown', {
209
+ button: event.button,
210
+ canForward: canForwardMouse,
211
+ eventType: event.type,
212
+ tabId,
213
+ willClick: !canForwardMouse && !!tab && event.button === 0 && !!onTerminalClick,
214
+ x: event.x,
215
+ y: event.y,
216
+ })
217
+ }
218
+ if (event.type === 'drag') {
219
+ if (onTerminalDrag?.(event, contentOrigin, tabId) === true) {
220
+ event.preventDefault()
221
+ return
222
+ }
223
+ if (onSeparatorDrag?.(event) === true) {
224
+ event.preventDefault()
225
+ return
226
+ }
227
+ }
228
+ if (event.type === 'up') {
229
+ if (onTerminalMouseUp?.(event) === true) {
230
+ event.preventDefault()
231
+ }
232
+ onSeparatorDragEnd?.()
233
+ }
234
+ if (tabId != null && tabId !== '' && onPaneActivate && event.type === 'down') {
235
+ onPaneActivate(tabId)
221
236
  }
222
- onSeparatorDragEnd?.()
223
- }
224
- if (tabId && onPaneActivate && event.type === 'down') {
225
- onPaneActivate(tabId)
226
- }
227
- if (!canForwardMouse) {
228
- if (tab && event.type === 'down' && event.button === 0 && onTerminalClick) {
229
- onTerminalClick(event, contentOrigin, tabId)
237
+ if (!canForwardMouse) {
238
+ if (tab && event.type === 'down' && event.button === 0 && onTerminalClick) {
239
+ onTerminalClick(event, contentOrigin, tabId)
240
+ }
241
+ return
230
242
  }
231
- return
232
- }
233
243
 
234
- event.preventDefault()
235
- event.stopPropagation()
236
- onTerminalMouseEvent(event, contentOrigin)
237
- }
238
- const forwardScrollEvent = (event: OtuiMouseEvent) => {
239
- if (!canForwardMouse && !canUseLocalScrollback) {
240
- return
241
- }
244
+ event.preventDefault()
245
+ event.stopPropagation()
246
+ onTerminalMouseEvent(event, contentOrigin)
247
+ },
248
+ [
249
+ canForwardMouse,
250
+ contentOrigin,
251
+ isOnPaneBorder,
252
+ onLeftEdgeMouseDown,
253
+ onPaneActivate,
254
+ onSeparatorDrag,
255
+ onSeparatorDragEnd,
256
+ onTerminalClick,
257
+ onTerminalDrag,
258
+ onTerminalMouseEvent,
259
+ onTerminalMouseUp,
260
+ rightClickMenu,
261
+ tab,
262
+ tabId,
263
+ ]
264
+ )
265
+ const forwardScrollEvent = useCallback(
266
+ (event: OtuiMouseEvent) => {
267
+ if (!canForwardMouse && !canUseLocalScrollback) {
268
+ return
269
+ }
242
270
 
243
- event.preventDefault()
244
- event.stopPropagation()
271
+ event.preventDefault()
272
+ event.stopPropagation()
245
273
 
246
- if (canForwardMouse) {
247
- onTerminalMouseEvent(event, contentOrigin)
248
- return
249
- }
274
+ if (canForwardMouse) {
275
+ onTerminalMouseEvent(event, contentOrigin)
276
+ return
277
+ }
250
278
 
251
- onTerminalScrollEvent(event)
252
- }
279
+ onTerminalScrollEvent(event)
280
+ },
281
+ [
282
+ canForwardMouse,
283
+ canUseLocalScrollback,
284
+ contentOrigin,
285
+ onTerminalMouseEvent,
286
+ onTerminalScrollEvent,
287
+ ]
288
+ )
289
+ const handleNoTabMouseDown = useCallback((event: OtuiMouseEvent) => {
290
+ event.stopPropagation()
291
+ dispatchGlobal({ type: 'open-new-tab-modal' })
292
+ }, [])
293
+ const handleContentMouseDown = useCallback(
294
+ (e: OtuiMouseEvent) => {
295
+ e.stopPropagation()
296
+ forwardMouseEvent(e)
297
+ },
298
+ [forwardMouseEvent]
299
+ )
253
300
  return (
254
301
  <box flexDirection="column" flexGrow={1} gap={0}>
255
302
  <ContextMenuBox
@@ -281,10 +328,7 @@ export function TerminalPane({
281
328
  marginTop={1}
282
329
  paddingX={2}
283
330
  backgroundColor={t.backgroundPanel}
284
- onMouseDown={(event) => {
285
- event.stopPropagation()
286
- dispatchGlobal({ type: 'open-new-tab-modal' })
287
- }}
331
+ onMouseDown={handleNoTabMouseDown}
288
332
  >
289
333
  <text fg={t.text}>New assistant</text>
290
334
  </box>
@@ -296,10 +340,7 @@ export function TerminalPane({
296
340
  flexGrow={1}
297
341
  width="100%"
298
342
  overflow="hidden"
299
- onMouseDown={(e) => {
300
- e.stopPropagation()
301
- forwardMouseEvent(e)
302
- }}
343
+ onMouseDown={handleContentMouseDown}
303
344
  onMouseUp={forwardMouseEvent}
304
345
  onMouseDrag={forwardMouseEvent}
305
346
  onMouseScroll={forwardScrollEvent}
@@ -308,7 +349,7 @@ export function TerminalPane({
308
349
  </box>
309
350
  )}
310
351
  </ContextMenuBox>
311
- {tab?.status === 'disconnected' || tab?.errorMessage ? (
352
+ {tab?.status === 'disconnected' || (tab?.errorMessage != null && tab.errorMessage !== '') ? (
312
353
  // Absolutely positioned so it overlays the bordered box instead of
313
354
  // consuming a flex row. If it took a row, the rendered terminal area
314
355
  // would be one line shorter than the size sent to the PTY/xterm,
@@ -317,7 +358,9 @@ export function TerminalPane({
317
358
  {tab?.status === 'disconnected' ? (
318
359
  <text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this workspace.</text>
319
360
  ) : null}
320
- {tab?.errorMessage ? <text fg={t.error}>{tab.errorMessage}</text> : null}
361
+ {tab?.errorMessage != null && tab?.errorMessage !== '' ? (
362
+ <text fg={t.error}>{tab.errorMessage}</text>
363
+ ) : null}
321
364
  </box>
322
365
  ) : null}
323
366
  </box>
@@ -93,7 +93,7 @@ interface ToolSectionProps {
93
93
 
94
94
  function ToolSection({ snap, tool }: ToolSectionProps) {
95
95
  const t = useTheme()
96
- const isHardError = Boolean(snap.error) && !snap.stale
96
+ const isHardError = Boolean(snap.error) && !(snap.stale === true)
97
97
  const relative = formatRelative(snap.lastUpdated)
98
98
 
99
99
  let body: ReactNode
@@ -119,7 +119,7 @@ function ToolSection({ snap, tool }: ToolSectionProps) {
119
119
  <text fg={t.text} selectable={false}>
120
120
  {TOOL_TITLE[tool]}
121
121
  </text>
122
- {snap.planTier ? (
122
+ {snap.planTier != null && snap.planTier !== '' ? (
123
123
  <text fg={t.textMuted} selectable={false}>
124
124
  {snap.planTier}
125
125
  </text>
@@ -145,7 +145,10 @@ function WindowRow({ window }: { window: UsageWindow }) {
145
145
  }
146
146
 
147
147
  const pctText = percent === null ? '—' : `${Math.round(percent)}% used`
148
- const resetText = window.timeRemaining ? `Resets in ${window.timeRemaining}` : null
148
+ const resetText =
149
+ window.timeRemaining != null && window.timeRemaining !== ''
150
+ ? `Resets in ${window.timeRemaining}`
151
+ : null
149
152
 
150
153
  return (
151
154
  <box flexDirection="column" paddingTop={1}>
@@ -164,7 +167,7 @@ function WindowRow({ window }: { window: UsageWindow }) {
164
167
  <text fg={t.textMuted} selectable={false}>
165
168
  {pctText}
166
169
  </text>
167
- {resetText ? (
170
+ {resetText != null && resetText !== '' ? (
168
171
  <text fg={t.textMuted} selectable={false}>
169
172
  {resetText}
170
173
  </text>
@@ -181,7 +184,7 @@ function PaceLine({ pace }: { pace: NonNullable<UsageWindow['pace']> }) {
181
184
  if (paceStageIsBehind(pace.stage)) color = t.warning
182
185
  else if (paceStageIsAhead(pace.stage)) color = t.success
183
186
 
184
- const suffix = pace.rightText ? ` · ${pace.rightText}` : ''
187
+ const suffix = pace.rightText != null && pace.rightText !== '' ? ` · ${pace.rightText}` : ''
185
188
  return (
186
189
  <text fg={color} selectable={false}>
187
190
  {`Pace: ${pace.label}${suffix}`}
@@ -1,6 +1,6 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
2
 
3
- import { useLayoutEffect, useMemo } from 'react'
3
+ import { useCallback, useLayoutEffect, useMemo } from 'react'
4
4
 
5
5
  import { collectHelpEntries, HELP_MODE_LABELS } from '../../../../input/keymap/help-entries'
6
6
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
@@ -19,7 +19,7 @@ interface HelpModalProps {
19
19
  function matchesFilter(needle: string, ...fields: (string | undefined)[]): boolean {
20
20
  if (!needle) return true
21
21
  const lower = needle.toLowerCase()
22
- return fields.some((f) => f?.toLowerCase().includes(lower))
22
+ return fields.some((f) => f?.toLowerCase().includes(lower) === true)
23
23
  }
24
24
 
25
25
  export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModalProps) {
@@ -48,22 +48,29 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
48
48
  dispatchGlobal({ count: filtered.length, type: 'set-help-entry-count' })
49
49
  }, [filtered.length])
50
50
 
51
- const items: PickerItem[] = filtered.map((entry, index) => {
52
- return {
53
- group: entry.modeLabel,
54
- key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
55
- title: (
56
- <text fg={t.textMuted} wrapMode="none">
57
- {entry.description ?? ''}
58
- </text>
59
- ),
60
- trailing: (
61
- <text fg={t.textMuted} wrapMode="none">
62
- {entry.keysDisplay}
63
- </text>
64
- ),
65
- }
66
- })
51
+ const items = useMemo<PickerItem[]>(
52
+ () =>
53
+ filtered.map((entry, index) => ({
54
+ group: entry.modeLabel,
55
+ key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
56
+ title: (
57
+ <text fg={t.textMuted} wrapMode="none">
58
+ {entry.description ?? ''}
59
+ </text>
60
+ ),
61
+ trailing: (
62
+ <text fg={t.textMuted} wrapMode="none">
63
+ {entry.keysDisplay}
64
+ </text>
65
+ ),
66
+ })),
67
+ [filtered, t]
68
+ )
69
+
70
+ const handleHover = useCallback(
71
+ (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
72
+ []
73
+ )
67
74
 
68
75
  return (
69
76
  <Picker
@@ -75,9 +82,11 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
75
82
  items={items}
76
83
  selectedIndex={selectedIndex}
77
84
  emptyState={
78
- <text fg={t.textMuted}>{filter ? 'No matching bindings.' : 'No bindings registered.'}</text>
85
+ <text fg={t.textMuted}>
86
+ {filter != null && filter !== '' ? 'No matching bindings.' : 'No bindings registered.'}
87
+ </text>
79
88
  }
80
- onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
89
+ onHover={handleHover}
81
90
  />
82
91
  )
83
92
  }
@@ -1,5 +1,7 @@
1
+ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
+
1
3
  import { isAutoCommitEnabled } from '@brimveyn/aimux-config'
2
- import { useEffect, useState } from 'react'
4
+ import { useCallback, useEffect, useState } from 'react'
3
5
 
4
6
  import type { ModeId } from '../../../../input/modes/types'
5
7
 
@@ -90,7 +92,7 @@ export function GitCommitModal({
90
92
  const currentSessionId = useAppStore((s) => s.currentSessionId)
91
93
  const bgKind = useAppStore((s) => {
92
94
  const id = s.currentSessionId
93
- return id ? s.autoCommit.bySession[id]?.kind : undefined
95
+ return id != null && id !== '' ? s.autoCommit.bySession[id]?.kind : undefined
94
96
  })
95
97
  const isBgGenerating = bgKind === 'generating'
96
98
  const showBgSpinner = isBgGenerating && !isConfirm && !isGenerating
@@ -99,9 +101,9 @@ export function GitCommitModal({
99
101
  (s) => s.gitPanel.files.filter((f) => f.section === 'staged').length
100
102
  )
101
103
 
102
- const onAutoCommitClick = (): void => {
104
+ const onAutoCommitClick = useCallback((): void => {
103
105
  const hasTitle = title.trim().length > 0
104
- if (hasTitle || !currentSessionId) {
106
+ if (hasTitle || !(currentSessionId != null && currentSessionId !== '')) {
105
107
  dispatchGlobal({ type: 'git-commit-enter-confirm' })
106
108
  return
107
109
  }
@@ -113,7 +115,16 @@ export function GitCommitModal({
113
115
  if (bgKind !== 'generating') {
114
116
  runSideEffectGlobal({ sessionId: currentSessionId, type: 'generate-auto-commit-now' })
115
117
  }
116
- }
118
+ }, [bgKind, currentSessionId, title])
119
+
120
+ const handleAutoCommitMouseDown = useCallback(
121
+ (event: OtuiMouseEvent) => {
122
+ event.preventDefault()
123
+ event.stopPropagation()
124
+ onAutoCommitClick()
125
+ },
126
+ [onAutoCommitClick]
127
+ )
117
128
 
118
129
  const modeId = pickModeId(stage)
119
130
  const shellTitle = pickShellTitle(stage)
@@ -169,11 +180,7 @@ export function GitCommitModal({
169
180
  flexDirection="row"
170
181
  gap={1}
171
182
  alignItems="center"
172
- onMouseDown={(event) => {
173
- event.preventDefault()
174
- event.stopPropagation()
175
- onAutoCommitClick()
176
- }}
183
+ onMouseDown={handleAutoCommitMouseDown}
177
184
  >
178
185
  {showBgSpinner ? <text fg={t.primary}>{bgSpinnerFrame}</text> : null}
179
186
  <text fg={t.text}>
@@ -1,3 +1,5 @@
1
+ import { useMemo } from 'react'
2
+
1
3
  import type { DirectoryResult } from '../../../../state/types'
2
4
 
3
5
  import { abbreviatePath } from '../../../path-format'
@@ -41,15 +43,17 @@ export function CreateSessionModal({
41
43
  const dirActive = activeField === 'directory'
42
44
  const nameActive = activeField === 'name'
43
45
 
44
- const items: FormOptionItem[] = results.map((result) => {
45
- return {
46
- key: result.path,
47
- leading: <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>,
48
- title: (active) => (
49
- <text fg={active ? t.text : t.textMuted}>{abbreviatePath(result.path)}</text>
50
- ),
51
- }
52
- })
46
+ const items = useMemo<FormOptionItem[]>(
47
+ () =>
48
+ results.map((result) => ({
49
+ key: result.path,
50
+ leading: <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>,
51
+ title: (active) => (
52
+ <text fg={active ? t.text : t.textMuted}>{abbreviatePath(result.path)}</text>
53
+ ),
54
+ })),
55
+ [results, t]
56
+ )
53
57
 
54
58
  return (
55
59
  <Form
@@ -62,7 +66,11 @@ export function CreateSessionModal({
62
66
  label="Search projects"
63
67
  placeholder="Type a project name..."
64
68
  value={directoryQuery}
65
- displayValue={pendingProjectPath ? abbreviatePath(pendingProjectPath) : directoryQuery}
69
+ displayValue={
70
+ pendingProjectPath != null && pendingProjectPath !== ''
71
+ ? abbreviatePath(pendingProjectPath)
72
+ : directoryQuery
73
+ }
66
74
  items={items}
67
75
  selectedIndex={selectedIndex}
68
76
  maxVisibleRows={VISIBLE_ROWS}