@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.
- package/README.md +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +29 -13
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +563 -85
- package/src/app-runtime/snippet-actions.ts +3 -3
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +21 -11
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +19 -3
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +11 -1
- package/src/input/raw-input-handler.ts +1 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +4 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +2 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +60 -45
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +355 -21
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +32 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +39 -11
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
- package/src/ui/components/git/git-panel.tsx +129 -83
- package/src/ui/components/git/git-view.tsx +96 -28
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +14 -13
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +135 -117
- package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
- package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +163 -120
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +29 -20
- package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
- package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
- package/src/ui/components/modals/shared/form.tsx +6 -6
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +71 -32
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +23 -7
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +121 -38
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- 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
|
|
49
|
-
const
|
|
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
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
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={
|
|
64
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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={
|
|
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
|
-
|
|
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({
|
|
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={
|
|
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
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
89
|
+
{filter != null && filter !== ''
|
|
90
|
+
? 'No matching snippets.'
|
|
91
|
+
: 'No snippets yet. Press n to create one.'}
|
|
75
92
|
</text>
|
|
76
93
|
}
|
|
77
|
-
footer={
|
|
78
|
-
|
|
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
|
}
|