mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-26 06:04:08 +00:00
fix(raycast): preserve typed input over deferred selection (#1520)
This commit is contained in:
parent
8b24b3aaf6
commit
6ff5b63a43
3 changed files with 120 additions and 37 deletions
|
|
@ -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<typeof createDeferredPrefillOwner> | 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}
|
||||
/>
|
||||
<Form.Separator />
|
||||
<Form.Dropdown
|
||||
|
|
|
|||
86
apps/raycast-extension/src/deferred-prefill.ts
Normal file
86
apps/raycast-extension/src/deferred-prefill.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
export function createDeferredPrefillOwner(
|
||||
applyValue: (value: string) => void,
|
||||
) {
|
||||
let prefillOwnsValue = true
|
||||
|
||||
return {
|
||||
updateFromUser(value: string) {
|
||||
prefillOwnsValue = false
|
||||
applyValue(value)
|
||||
},
|
||||
start(readValue: () => Promise<string>) {
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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<typeof createSearchInputAdapter> | 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 (
|
||||
<List
|
||||
isLoading={isLoading}
|
||||
onSearchTextChange={setSearchText}
|
||||
searchText={searchText}
|
||||
{...searchInput.getListProps(searchText)}
|
||||
isLoading={isSearching}
|
||||
searchBarPlaceholder="Search your memories..."
|
||||
throttle
|
||||
>
|
||||
{hasSearched && !searchText.trim() ? (
|
||||
<List.EmptyView
|
||||
|
|
@ -137,7 +139,7 @@ function Command() {
|
|||
title="No Memories Found"
|
||||
description={`No memories found for "${searchText}"`}
|
||||
/>
|
||||
) : isLoading && searchText.trim() ? (
|
||||
) : isSearching && searchText.trim() ? (
|
||||
<List.EmptyView
|
||||
icon={Icon.MagnifyingGlass}
|
||||
title="Searching Your Memories"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue