@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,3 +1,5 @@
1
+ import { useCallback, useMemo } from 'react'
2
+
1
3
  import type { SessionRecord } from '../../../../state/types'
2
4
 
3
5
  import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
@@ -44,40 +46,46 @@ export function SessionPickerModal({
44
46
  sessions,
45
47
  }: SessionPickerModalProps) {
46
48
  const t = useTheme()
47
- const ordered = orderSessionsForDisplay(sessions)
48
- const baselineOrder = ordered.map((s) => s.id)
49
- const filtered = filterSessions(ordered, filter)
50
- const hasFilter = !!filter
49
+ const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
50
+ const filtered = useMemo(() => filterSessions(ordered, filter), [filter, ordered])
51
+ const hasFilter = !!(filter != null && filter !== '')
51
52
 
52
- const sessionItems: PickerItem[] = filtered.map((session, index) => {
53
- const active = index === selectedIndex
54
- const displayIndex = baselineOrder.indexOf(session.id) + 1
55
- return {
56
- key: session.id,
53
+ const items = useMemo<PickerItem[]>(() => {
54
+ const baselineOrder = ordered.map((s) => s.id)
55
+ const sessionItems: PickerItem[] = filtered.map((session, index) => {
56
+ const active = index === selectedIndex
57
+ const displayIndex = baselineOrder.indexOf(session.id) + 1
58
+ return {
59
+ key: session.id,
60
+ onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
61
+ onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
62
+ subtitle:
63
+ session.projectPath != null && session.projectPath !== '' ? (
64
+ <text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
65
+ ) : undefined,
66
+ title: (
67
+ <text fg={active ? t.text : t.textMuted}>
68
+ {formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
69
+ </text>
70
+ ),
71
+ }
72
+ })
73
+ const createNewItem: PickerItem = {
74
+ key: '__create-new__',
57
75
  onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
58
- onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
59
- subtitle: session.projectPath ? (
60
- <text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
61
- ) : undefined,
62
76
  title: (
63
- <text fg={active ? t.text : t.textMuted}>
64
- {formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
77
+ <text fg={selectedIndex === filtered.length ? t.text : t.textMuted}>
78
+ Create new workspace
65
79
  </text>
66
80
  ),
67
81
  }
68
- })
82
+ return [...sessionItems, createNewItem]
83
+ }, [currentSessionId, currentTabCount, filtered, ordered, selectedIndex, t])
69
84
 
70
- const createNewItem: PickerItem = {
71
- key: '__create-new__',
72
- onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
73
- title: (
74
- <text fg={selectedIndex === filtered.length ? t.text : t.textMuted}>
75
- Create new workspace
76
- </text>
77
- ),
78
- }
79
-
80
- const items = [...sessionItems, createNewItem]
85
+ const handleHover = useCallback(
86
+ (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
87
+ []
88
+ )
81
89
 
82
90
  return (
83
91
  <Picker
@@ -94,7 +102,7 @@ export function SessionPickerModal({
94
102
  <text fg={t.textMuted}>{getEmptyStateMessage(hasFilter)}</text>
95
103
  ) : undefined
96
104
  }
97
- onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
105
+ onHover={handleHover}
98
106
  />
99
107
  )
100
108
  }
@@ -1,6 +1,5 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
-
3
- import { type ReactNode } from 'react'
2
+ import type { ReactNode } from 'react'
4
3
 
5
4
  import { useTheme } from '../../../theme'
6
5
  import { InputField } from '../../primitives/input-field'
@@ -84,8 +83,8 @@ export function FieldLabel({ active = false, children, description }: FieldLabel
84
83
  const t = useTheme()
85
84
  return (
86
85
  <box flexDirection="column">
87
- {children ? <text fg={active ? t.text : t.textMuted}>{children}</text> : null}
88
- {description ? <text fg={t.textMuted}>{description}</text> : null}
86
+ {children != null ? <text fg={active ? t.text : t.textMuted}>{children}</text> : null}
87
+ {description != null ? <text fg={t.textMuted}>{description}</text> : null}
89
88
  </box>
90
89
  )
91
90
  }
@@ -100,7 +99,7 @@ export function TextField({
100
99
  }: TextFieldProps) {
101
100
  return (
102
101
  <box flexDirection="column">
103
- {label || description ? (
102
+ {label != null || description != null ? (
104
103
  <FieldLabel active={active} description={description}>
105
104
  {label}
106
105
  </FieldLabel>
@@ -148,12 +147,13 @@ export function AutoComplete({
148
147
  return (
149
148
  <ListItem
150
149
  key={item.key}
150
+ index={optionIndex}
151
151
  active={optionActive}
152
152
  leading={resolveItemContent(item.leading, optionActive)}
153
153
  title={resolveItemContent(item.title, optionActive)}
154
154
  subtitle={resolveItemContent(item.subtitle, optionActive)}
155
155
  trailing={resolveItemContent(item.trailing, optionActive)}
156
- onHover={onHover ? () => onHover(optionIndex) : undefined}
156
+ onHoverIndex={onHover}
157
157
  onClick={item.onClick}
158
158
  />
159
159
  )
@@ -1,7 +1,7 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
+ import type { ReactNode } from 'react'
2
3
 
3
4
  import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
4
- import { type ReactNode } from 'react'
5
5
 
6
6
  import { useTheme, useTransparent } from '../../../theme'
7
7
  import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
@@ -81,10 +81,10 @@ export function ModalShell({
81
81
  <box width="100%" flexDirection="column" gap={listGap}>
82
82
  <box flexDirection="column">
83
83
  <text fg={t.text}>{title}</text>
84
- {subtitle ? <text fg={t.textMuted}>{subtitle}</text> : null}
84
+ {subtitle != null && subtitle !== '' ? <text fg={t.textMuted}>{subtitle}</text> : null}
85
85
  </box>
86
86
  {children}
87
- {footer ? <box>{footer}</box> : null}
87
+ {footer != null ? <box>{footer}</box> : null}
88
88
  </box>
89
89
  </box>
90
90
  {keybindsModeId ? <ModalKeybindsOverlay modeId={keybindsModeId} /> : null}
@@ -1,8 +1,8 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
- import type { ScrollBoxRenderable } from '@opentui/core'
2
+ import type { MouseEvent as OtuiMouseEvent, ScrollBoxRenderable } from '@opentui/core'
3
3
 
4
4
  import { useTerminalDimensions } from '@opentui/react'
5
- import { type ReactNode, useLayoutEffect, useRef } from 'react'
5
+ import { type ReactNode, useCallback, useLayoutEffect, useMemo, useRef } from 'react'
6
6
 
7
7
  import { useTheme } from '../../../theme'
8
8
  import { BareInput } from '../../primitives/bare-input'
@@ -18,6 +18,7 @@ export interface PickerItem {
18
18
  onClick?: () => void
19
19
  onEdit?: () => void
20
20
  onDelete?: () => void
21
+ onWorktree?: () => void
21
22
  }
22
23
 
23
24
  interface PickerProps {
@@ -38,27 +39,51 @@ interface PickerProps {
38
39
  const VIEWPORT_HEIGHT_RATIO = 0.6
39
40
  const MODAL_CHROME_ROWS = 6
40
41
 
41
- function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?: () => void }) {
42
+ function PickerItemCtas({
43
+ onDelete,
44
+ onEdit,
45
+ onWorktree,
46
+ }: {
47
+ onEdit?: () => void
48
+ onDelete?: () => void
49
+ onWorktree?: () => void
50
+ }) {
42
51
  const t = useTheme()
52
+ const handleEdit = useCallback(
53
+ (event: OtuiMouseEvent) => {
54
+ event.stopPropagation()
55
+ onEdit?.()
56
+ },
57
+ [onEdit]
58
+ )
59
+ const handleWorktree = useCallback(
60
+ (event: OtuiMouseEvent) => {
61
+ event.stopPropagation()
62
+ onWorktree?.()
63
+ },
64
+ [onWorktree]
65
+ )
66
+ const handleDelete = useCallback(
67
+ (event: OtuiMouseEvent) => {
68
+ event.stopPropagation()
69
+ onDelete?.()
70
+ },
71
+ [onDelete]
72
+ )
43
73
  return (
44
74
  <box flexDirection="row" gap={1}>
45
75
  {onEdit ? (
46
- <box
47
- onMouseDown={(event) => {
48
- event.stopPropagation()
49
- onEdit()
50
- }}
51
- >
76
+ <box onMouseDown={handleEdit}>
52
77
  <text fg={t.primary}>[edit]</text>
53
78
  </box>
54
79
  ) : null}
80
+ {onWorktree ? (
81
+ <box onMouseDown={handleWorktree}>
82
+ <text fg={t.warning}>[WT]</text>
83
+ </box>
84
+ ) : null}
55
85
  {onDelete ? (
56
- <box
57
- onMouseDown={(event) => {
58
- event.stopPropagation()
59
- onDelete()
60
- }}
61
- >
86
+ <box onMouseDown={handleDelete}>
62
87
  <text fg={t.error}>[del]</text>
63
88
  </box>
64
89
  ) : null}
@@ -90,13 +115,29 @@ export function Picker({
90
115
  const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
91
116
  const skipNextScrollRef = useRef(false)
92
117
 
93
- const resetScrollTimer = () => {
118
+ const resetScrollTimer = useCallback(() => {
94
119
  if (scrollTimerRef.current) clearTimeout(scrollTimerRef.current)
95
120
  scrollTimerRef.current = setTimeout(() => {
96
121
  isScrollingRef.current = false
97
122
  scrollTimerRef.current = null
98
123
  }, 10)
99
- }
124
+ }, [])
125
+
126
+ const handleScrollboxScroll = useCallback(() => {
127
+ isScrollingRef.current = true
128
+ resetScrollTimer()
129
+ }, [resetScrollTimer])
130
+
131
+ const handleItemHover = useCallback(
132
+ (index: number) => {
133
+ if (isScrollingRef.current) return
134
+ skipNextScrollRef.current = true
135
+ onHover(index)
136
+ },
137
+ [onHover]
138
+ )
139
+
140
+ const listContentOptions = useMemo(() => ({ flexDirection: 'column' as const, gap }), [gap])
100
141
 
101
142
  useLayoutEffect(() => {
102
143
  if (!scrollboxRef.current) return
@@ -105,11 +146,11 @@ export function Picker({
105
146
  return
106
147
  }
107
148
  const target = items[selectedIndex]?.key
108
- if (!target) return
149
+ if (!(target != null && target !== '')) return
109
150
  isScrollingRef.current = true
110
151
  scrollboxRef.current.scrollChildIntoView(target)
111
152
  resetScrollTimer()
112
- }, [selectedIndex, items])
153
+ }, [selectedIndex, items, resetScrollTimer])
113
154
 
114
155
  return (
115
156
  <ModalShell
@@ -125,11 +166,8 @@ export function Picker({
125
166
  ref={scrollboxRef}
126
167
  scrollY
127
168
  height={items.length === 0 ? 0 : listHeight}
128
- contentOptions={{ flexDirection: 'column', gap }}
129
- onMouseScroll={() => {
130
- isScrollingRef.current = true
131
- resetScrollTimer()
132
- }}
169
+ contentOptions={listContentOptions}
170
+ onMouseScroll={handleScrollboxScroll}
133
171
  >
134
172
  {(() => {
135
173
  let prevGroup: string | undefined
@@ -137,7 +175,7 @@ export function Picker({
137
175
  for (let index = 0; index < items.length; index++) {
138
176
  const item = items[index]
139
177
  if (!item) continue
140
- if (item.group && item.group !== prevGroup) {
178
+ if (item.group != null && item.group !== '' && item.group !== prevGroup) {
141
179
  nodes.push(
142
180
  <box key={`group::${item.group}`} paddingLeft={1} paddingTop={index === 0 ? 0 : 1}>
143
181
  <text fg={t.textMuted} wrapMode="none">
@@ -151,22 +189,23 @@ export function Picker({
151
189
  const capturedIndex = index
152
190
  const trailing =
153
191
  item.trailing ??
154
- (active && (item.onEdit || item.onDelete) ? (
155
- <PickerItemCtas onEdit={item.onEdit} onDelete={item.onDelete} />
192
+ (active && (item.onEdit || item.onDelete || item.onWorktree) ? (
193
+ <PickerItemCtas
194
+ onEdit={item.onEdit}
195
+ onDelete={item.onDelete}
196
+ onWorktree={item.onWorktree}
197
+ />
156
198
  ) : null)
157
199
  nodes.push(
158
200
  <ListItem
159
201
  key={item.key}
160
202
  id={item.key}
203
+ index={capturedIndex}
161
204
  active={active}
162
205
  title={item.title}
163
206
  subtitle={item.subtitle}
164
207
  trailing={trailing}
165
- onHover={() => {
166
- if (isScrollingRef.current) return
167
- skipNextScrollRef.current = true
168
- onHover(capturedIndex)
169
- }}
208
+ onHoverIndex={handleItemHover}
170
209
  onClick={item.onClick}
171
210
  />
172
211
  )
@@ -1,3 +1,5 @@
1
+ import { useCallback, useMemo } from 'react'
2
+
1
3
  import type { SnippetRecord } from '../../../../state/types'
2
4
 
3
5
  import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
@@ -31,33 +33,46 @@ export function SnippetPickerModal({
31
33
  snippets,
32
34
  }: SnippetPickerModalProps) {
33
35
  const t = useTheme()
34
- const filtered = filterSnippets(snippets, filter)
36
+ const filtered = useMemo(() => filterSnippets(snippets, filter), [filter, snippets])
35
37
 
36
- const items: PickerItem[] = filtered.map((snippet, index) => {
37
- const active = index === selectedIndex
38
- const fromConfig = isConfigSnippetId(snippet.id)
39
- return {
40
- key: snippet.id,
41
- onClick: () => {
42
- dispatchGlobal({ type: 'close-modal' })
43
- runSideEffectGlobal({ type: 'paste-selected-snippet' })
44
- },
45
- onDelete: fromConfig
46
- ? undefined
47
- : () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
48
- onEdit: fromConfig ? undefined : () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
49
- subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
50
- title: (
51
- <box flexDirection="row">
52
- <text fg={active ? t.text : t.textMuted}>
53
- <strong>{snippet.name}</strong>
54
- </text>
55
- {snippet.trigger ? <text fg={t.textMuted}>{` :${snippet.trigger}`}</text> : null}
56
- {fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
57
- </box>
58
- ),
59
- }
60
- })
38
+ const items = useMemo<PickerItem[]>(
39
+ () =>
40
+ filtered.map((snippet, index) => {
41
+ const active = index === selectedIndex
42
+ const fromConfig = isConfigSnippetId(snippet.id)
43
+ return {
44
+ key: snippet.id,
45
+ onClick: () => {
46
+ dispatchGlobal({ type: 'close-modal' })
47
+ runSideEffectGlobal({ type: 'paste-selected-snippet' })
48
+ },
49
+ onDelete: fromConfig
50
+ ? undefined
51
+ : () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
52
+ onEdit: fromConfig
53
+ ? undefined
54
+ : () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
55
+ subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
56
+ title: (
57
+ <box flexDirection="row">
58
+ <text fg={active ? t.text : t.textMuted}>
59
+ <strong>{snippet.name}</strong>
60
+ </text>
61
+ {snippet.trigger != null && snippet.trigger !== '' ? (
62
+ <text fg={t.textMuted}>{` :${snippet.trigger}`}</text>
63
+ ) : null}
64
+ {fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
65
+ </box>
66
+ ),
67
+ }
68
+ }),
69
+ [filtered, selectedIndex, t]
70
+ )
71
+
72
+ const handleHover = useCallback(
73
+ (index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
74
+ []
75
+ )
61
76
 
62
77
  return (
63
78
  <Picker
@@ -71,11 +86,17 @@ export function SnippetPickerModal({
71
86
  selectedIndex={selectedIndex}
72
87
  emptyState={
73
88
  <text fg={t.textMuted}>
74
- {filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
89
+ {filter != null && filter !== ''
90
+ ? 'No matching snippets.'
91
+ : 'No snippets yet. Press n to create one.'}
75
92
  </text>
76
93
  }
77
- footer={actionMessage ? <text fg={t.error}>{actionMessage}</text> : undefined}
78
- onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
94
+ footer={
95
+ actionMessage != null && actionMessage !== '' ? (
96
+ <text fg={t.error}>{actionMessage}</text>
97
+ ) : undefined
98
+ }
99
+ onHover={handleHover}
79
100
  />
80
101
  )
81
102
  }