refactor(ui): split prompt input into hooks and API
Extract prompt draft/history, attachments, picker, and keydown logic into co-located hooks. Introduce PromptInputApi for quote/expand/setText and migrate SessionView off DOM poking; remove legacy registerQuoteHandler.
This commit is contained in:
193
packages/ui/src/components/prompt-input/usePromptState.ts
Normal file
193
packages/ui/src/components/prompt-input/usePromptState.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { createEffect, createSignal, on, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { addToHistory, getHistory } from "../../stores/message-history"
|
||||
import { clearSessionDraftPrompt, getSessionDraftPrompt, setSessionDraftPrompt } from "../../stores/sessions"
|
||||
import { getLogger } from "../../lib/logger"
|
||||
|
||||
const log = getLogger("actions")
|
||||
|
||||
type GetTextarea = () => HTMLTextAreaElement | undefined | null
|
||||
|
||||
type PromptStateOptions = {
|
||||
instanceId: Accessor<string>
|
||||
sessionId: Accessor<string>
|
||||
instanceFolder: Accessor<string>
|
||||
onSessionDraftLoaded?: (draft: string) => void
|
||||
}
|
||||
|
||||
type HistorySelectOptions = {
|
||||
force?: boolean
|
||||
isPickerOpen: boolean
|
||||
getTextarea: GetTextarea
|
||||
}
|
||||
|
||||
type PromptState = {
|
||||
prompt: Accessor<string>
|
||||
setPrompt: (value: string) => void
|
||||
clearPrompt: () => void
|
||||
|
||||
draftLoadedNonce: Accessor<number>
|
||||
|
||||
history: Accessor<string[]>
|
||||
historyIndex: Accessor<number>
|
||||
historyDraft: Accessor<string | null>
|
||||
|
||||
resetHistoryNavigation: () => void
|
||||
clearHistoryDraft: () => void
|
||||
recordHistoryEntry: (entry: string) => Promise<void>
|
||||
|
||||
selectPreviousHistory: (options: HistorySelectOptions) => boolean
|
||||
selectNextHistory: (options: HistorySelectOptions) => boolean
|
||||
}
|
||||
|
||||
const HISTORY_LIMIT = 100
|
||||
|
||||
export function usePromptState(options: PromptStateOptions): PromptState {
|
||||
const [prompt, setPromptInternal] = createSignal("")
|
||||
const [history, setHistory] = createSignal<string[]>([])
|
||||
const [historyIndex, setHistoryIndex] = createSignal(-1)
|
||||
const [historyDraft, setHistoryDraft] = createSignal<string | null>(null)
|
||||
const [draftLoadedNonce, setDraftLoadedNonce] = createSignal(0)
|
||||
|
||||
const setPrompt = (value: string) => {
|
||||
setPromptInternal(value)
|
||||
setSessionDraftPrompt(options.instanceId(), options.sessionId(), value)
|
||||
}
|
||||
|
||||
const clearPrompt = () => {
|
||||
clearSessionDraftPrompt(options.instanceId(), options.sessionId())
|
||||
setPromptInternal("")
|
||||
}
|
||||
|
||||
const resetHistoryNavigation = () => {
|
||||
setHistoryIndex(-1)
|
||||
setHistoryDraft(null)
|
||||
}
|
||||
|
||||
const clearHistoryDraft = () => {
|
||||
setHistoryDraft(null)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => `${options.instanceId()}:${options.sessionId()}`,
|
||||
() => {
|
||||
const instanceId = options.instanceId()
|
||||
const sessionId = options.sessionId()
|
||||
|
||||
onCleanup(() => {
|
||||
// Persist the previous session's draft when switching sessions.
|
||||
setSessionDraftPrompt(instanceId, sessionId, prompt())
|
||||
})
|
||||
|
||||
const storedPrompt = getSessionDraftPrompt(instanceId, sessionId)
|
||||
|
||||
setPromptInternal(storedPrompt)
|
||||
setSessionDraftPrompt(instanceId, sessionId, storedPrompt)
|
||||
|
||||
resetHistoryNavigation()
|
||||
|
||||
setDraftLoadedNonce((prev) => prev + 1)
|
||||
options.onSessionDraftLoaded?.(storedPrompt)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
onMount(() => {
|
||||
void (async () => {
|
||||
const loaded = await getHistory(options.instanceFolder())
|
||||
setHistory(loaded)
|
||||
})()
|
||||
})
|
||||
|
||||
const recordHistoryEntry = async (entry: string) => {
|
||||
try {
|
||||
await addToHistory(options.instanceFolder(), entry)
|
||||
setHistory((prev) => {
|
||||
const next = [entry, ...prev]
|
||||
if (next.length > HISTORY_LIMIT) {
|
||||
next.length = HISTORY_LIMIT
|
||||
}
|
||||
return next
|
||||
})
|
||||
setHistoryIndex(-1)
|
||||
} catch (historyError) {
|
||||
log.error("Failed to update prompt history:", historyError)
|
||||
}
|
||||
}
|
||||
|
||||
const canUseHistory = (selectOptions: HistorySelectOptions) => {
|
||||
if (selectOptions.force) return true
|
||||
if (selectOptions.isPickerOpen) return false
|
||||
|
||||
const textarea = selectOptions.getTextarea()
|
||||
if (!textarea) return false
|
||||
return textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
||||
}
|
||||
|
||||
const focusTextareaEnd = (getTextarea: GetTextarea) => {
|
||||
const textarea = getTextarea()
|
||||
if (!textarea) return
|
||||
setTimeout(() => {
|
||||
const next = getTextarea()
|
||||
if (!next) return
|
||||
const pos = next.value.length
|
||||
next.setSelectionRange(pos, pos)
|
||||
next.focus()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const selectPreviousHistory = (selectOptions: HistorySelectOptions) => {
|
||||
const entries = history()
|
||||
if (entries.length === 0) return false
|
||||
if (!canUseHistory(selectOptions)) return false
|
||||
|
||||
if (historyIndex() === -1) {
|
||||
setHistoryDraft(prompt())
|
||||
}
|
||||
|
||||
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, entries.length - 1)
|
||||
setHistoryIndex(newIndex)
|
||||
setPrompt(entries[newIndex])
|
||||
focusTextareaEnd(selectOptions.getTextarea)
|
||||
return true
|
||||
}
|
||||
|
||||
const selectNextHistory = (selectOptions: HistorySelectOptions) => {
|
||||
const entries = history()
|
||||
if (entries.length === 0) return false
|
||||
if (!canUseHistory(selectOptions)) return false
|
||||
if (historyIndex() === -1) return false
|
||||
|
||||
const newIndex = historyIndex() - 1
|
||||
if (newIndex >= 0) {
|
||||
setHistoryIndex(newIndex)
|
||||
setPrompt(entries[newIndex])
|
||||
} else {
|
||||
setHistoryIndex(-1)
|
||||
const draft = historyDraft()
|
||||
setPrompt(draft ?? "")
|
||||
setHistoryDraft(null)
|
||||
}
|
||||
focusTextareaEnd(selectOptions.getTextarea)
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
prompt,
|
||||
setPrompt,
|
||||
clearPrompt,
|
||||
|
||||
draftLoadedNonce,
|
||||
|
||||
history,
|
||||
historyIndex,
|
||||
historyDraft,
|
||||
|
||||
resetHistoryNavigation,
|
||||
clearHistoryDraft,
|
||||
recordHistoryEntry,
|
||||
|
||||
selectPreviousHistory,
|
||||
selectNextHistory,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user