diff --git a/apps/raycast-extension/src/add-memory.tsx b/apps/raycast-extension/src/add-memory.tsx index 812c1c4a..54204d35 100644 --- a/apps/raycast-extension/src/add-memory.tsx +++ b/apps/raycast-extension/src/add-memory.tsx @@ -8,8 +8,9 @@ import { Icon, getSelectedText, } from "@raycast/api" -import { useState, useEffect } from "react" +import { useEffect, useRef, useState } from "react" import { addMemory, fetchProjects } from "./api" +import { createDeferredPrefillOwner } from "./deferred-prefill" import { usePromise } from "@raycast/utils" import { withSupermemory } from "./withSupermemory" @@ -23,24 +24,18 @@ function Command() { const [isSubmitting, setIsSubmitting] = useState(false) const [initialContent, setInitialContent] = useState("") const { pop } = useNavigation() + const prefillOwnerRef = useRef< + ReturnType | undefined + >(undefined) + prefillOwnerRef.current ??= createDeferredPrefillOwner(setInitialContent) + const prefillOwner = prefillOwnerRef.current const { isLoading, data: projects = [] } = usePromise(fetchProjects) useEffect(() => { - async function loadSelectedText() { - try { - const selectedText = await getSelectedText() - if (selectedText) { - setInitialContent(selectedText) - } - } catch { - // No text selected or error getting selected text - silently fail - // User can still manually enter content - } - } - - loadSelectedText() - }, []) + const request = prefillOwner.start(getSelectedText) + return request.cancel + }, [prefillOwner]) async function handleSubmit(values: FormValues) { if (!values.content.trim()) { @@ -91,7 +86,7 @@ function Command() { value={initialContent} placeholder="Enter the memory content..." info="The main content of your memory. This is required." - onChange={(value) => setInitialContent(value)} + onChange={prefillOwner.updateFromUser} /> void, +) { + let prefillOwnsValue = true + + return { + updateFromUser(value: string) { + prefillOwnsValue = false + applyValue(value) + }, + start(readValue: () => Promise) { + let active = true + const completion = (async () => { + try { + const value = await readValue() + if (active && prefillOwnsValue && value) { + applyValue(value) + } + } catch { + // Selection is unavailable; keep the current value unchanged. + } + })() + + return { + completion, + cancel() { + active = false + }, + } + }, + } +} + +export const SEARCH_QUERY_DEBOUNCE_MS = 300 + +type CancelScheduledQuery = () => void +type ScheduleQuery = ( + callback: () => void, + delayMs: number, +) => CancelScheduledQuery + +const scheduleQuery: ScheduleQuery = (callback, delayMs) => { + const timeout = setTimeout(callback, delayMs) + return () => clearTimeout(timeout) +} + +export function createSearchInputAdapter( + applySearchText: (value: string) => void, + applySearchQuery: (value: string) => void, + schedule: ScheduleQuery = scheduleQuery, +) { + let cancelScheduledQuery: CancelScheduledQuery | undefined + + const applyValue = (value: string) => { + applySearchText(value) + cancelScheduledQuery?.() + cancelScheduledQuery = undefined + + if (!value.trim()) { + applySearchQuery(value) + return + } + + cancelScheduledQuery = schedule(() => { + cancelScheduledQuery = undefined + applySearchQuery(value) + }, SEARCH_QUERY_DEBOUNCE_MS) + } + + const prefillOwner = createDeferredPrefillOwner(applyValue) + + return { + getListProps(searchText: string) { + return { + onSearchTextChange: prefillOwner.updateFromUser, + searchText, + throttle: false as const, + } + }, + startPrefill: prefillOwner.start, + cancelPendingQuery() { + cancelScheduledQuery?.() + cancelScheduledQuery = undefined + }, + } +} diff --git a/apps/raycast-extension/src/search-memories.tsx b/apps/raycast-extension/src/search-memories.tsx index 11635e68..3d728ab1 100644 --- a/apps/raycast-extension/src/search-memories.tsx +++ b/apps/raycast-extension/src/search-memories.tsx @@ -9,8 +9,9 @@ import { getSelectedText, } from "@raycast/api" import { usePromise } from "@raycast/utils" -import { useState, useEffect } from "react" +import { useEffect, useRef, useState } from "react" import { searchMemories, type SearchResult } from "./api" +import { createSearchInputAdapter } from "./deferred-prefill" import { formatRelevanceScore } from "./relevance-score" import { withSupermemory } from "./withSupermemory" @@ -63,23 +64,25 @@ const truncateContent = (content: string, maxLength = 100) => { } export default withSupermemory(Command) -function Command() { +export function Command() { const [searchText, setSearchText] = useState("") + const [searchQuery, setSearchQuery] = useState("") + const searchInputRef = useRef< + ReturnType | undefined + >(undefined) + searchInputRef.current ??= createSearchInputAdapter( + setSearchText, + setSearchQuery, + ) + const searchInput = searchInputRef.current useEffect(() => { - async function loadSelectedText() { - try { - const selectedText = await getSelectedText() - if (selectedText) { - setSearchText(selectedText) - } - } catch { - // No text selected or error getting selected text - silently fail - } + const request = searchInput.startPrefill(getSelectedText) + return () => { + request.cancel() + searchInput.cancelPendingQuery() } - - loadSelectedText() - }, []) + }, [searchInput]) const { isLoading, data: searchResults = [] } = usePromise( async (query: string) => { @@ -99,7 +102,7 @@ function Command() { } return results }, - [searchText], + [searchQuery], ) const formatDate = (dateString: string) => { @@ -116,14 +119,13 @@ function Command() { } } - const hasSearched = !isLoading && !searchResults.length + const isSearching = isLoading || searchText !== searchQuery + const hasSearched = !isSearching && !searchResults.length return ( {hasSearched && !searchText.trim() ? ( - ) : isLoading && searchText.trim() ? ( + ) : isSearching && searchText.trim() ? (