|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' |
| 3 | +import { |
| 4 | + startTransition, |
| 5 | + useCallback, |
| 6 | + useDeferredValue, |
| 7 | + useEffect, |
| 8 | + useMemo, |
| 9 | + useRef, |
| 10 | + useState, |
| 11 | +} from 'react' |
4 | 12 | import { Button, cn } from '@sim/emcn' |
5 | 13 | import { X } from '@sim/emcn/icons' |
6 | 14 | import { WorkflowBlockBorder, type WorkflowBorderPort } from '@sim/workflow-renderer' |
@@ -47,6 +55,7 @@ const SELECTOR_ACTION_MENU_RIGHT_INSET = 24 |
47 | 55 | const SELECTOR_ACTION_MENU_AMPLITUDE = 7 |
48 | 56 | const RECENT_SELECTION_LIMIT = 3 |
49 | 57 | const RECENT_SELECTION_STORAGE_PREFIX = 'sim:connection-block-selector:recent' |
| 58 | +const BROWSE_PREFETCH_MARGIN_PX = 640 |
50 | 59 | const POPULAR_BLOCK_TYPES = [ |
51 | 60 | 'agent', |
52 | 61 | 'function', |
@@ -138,6 +147,7 @@ export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockS |
138 | 147 | const posthog = usePostHog() |
139 | 148 | const inputRef = useRef<HTMLInputElement>(null) |
140 | 149 | const listRef = useRef<HTMLDivElement>(null) |
| 150 | + const browseSentinelRef = useRef<HTMLDivElement>(null) |
141 | 151 | const [search, setSearch] = useState('') |
142 | 152 | const [selectedValue, setSelectedValue] = useState('') |
143 | 153 | const [recentSelections, setRecentSelections] = useState<RecentSelection[]>([]) |
@@ -269,6 +279,36 @@ export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockS |
269 | 279 | ) |
270 | 280 | const hasMoreBrowseResults = browseLimit < browseBlocks.length + browseTools.length |
271 | 281 |
|
| 282 | + /** |
| 283 | + * Keep the initial commit bounded, then extend the catalog before the user |
| 284 | + * reaches its end. Rendering every cmdk item up front caused the original |
| 285 | + * frame spike, while a manual pagination control exposed that constraint in |
| 286 | + * the UI. Prefetching near the viewport preserves continuous scrolling and |
| 287 | + * cmdk's native keyboard navigation without mounting the whole catalog. |
| 288 | + */ |
| 289 | + useEffect(() => { |
| 290 | + const list = listRef.current |
| 291 | + const sentinel = browseSentinelRef.current |
| 292 | + if (isSearching || !hasMoreBrowseResults || !list || !sentinel) return |
| 293 | + |
| 294 | + const browseResultCount = browseBlocks.length + browseTools.length |
| 295 | + const observer = new IntersectionObserver( |
| 296 | + ([entry]) => { |
| 297 | + if (!entry.isIntersecting) return |
| 298 | + startTransition(() => { |
| 299 | + setBrowseLimit((current) => Math.min(current + MAX_RESULTS_PER_GROUP, browseResultCount)) |
| 300 | + }) |
| 301 | + }, |
| 302 | + { |
| 303 | + root: list, |
| 304 | + rootMargin: `0px 0px ${BROWSE_PREFETCH_MARGIN_PX}px 0px`, |
| 305 | + } |
| 306 | + ) |
| 307 | + |
| 308 | + observer.observe(sentinel) |
| 309 | + return () => observer.disconnect() |
| 310 | + }, [browseBlocks.length, browseTools.length, hasMoreBrowseResults, isSearching]) |
| 311 | + |
272 | 312 | const dispatchSelection = useCallback( |
273 | 313 | (type: string, resultType: 'block' | 'tool' | 'tool_operation', presetOperation?: string) => { |
274 | 314 | window.dispatchEvent( |
@@ -494,15 +534,7 @@ export function ConnectionBlockSelector({ id, data }: NodeProps<ConnectionBlockS |
494 | 534 | /> |
495 | 535 | <ToolsGroup items={visibleBrowseTools} onSelect={handleToolSelect} /> |
496 | 536 | {hasMoreBrowseResults && ( |
497 | | - <div className='px-2 py-1.5'> |
498 | | - <Button |
499 | | - variant='ghost' |
500 | | - className='nodrag nopan h-8 w-full text-[var(--text-secondary)]' |
501 | | - onClick={() => setBrowseLimit((current) => current + MAX_RESULTS_PER_GROUP)} |
502 | | - > |
503 | | - Show more |
504 | | - </Button> |
505 | | - </div> |
| 537 | + <div ref={browseSentinelRef} aria-hidden='true' className='h-px' /> |
506 | 538 | )} |
507 | 539 | </> |
508 | 540 | )} |
|
0 commit comments