From 03ff2779b0bca72fc3698edbf1f3aa00f66a63e8 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Sat, 8 Nov 2025 22:24:19 +0000 Subject: [PATCH] persist prompt drafts per session --- src/components/prompt-input.tsx | 18 ++++---- src/stores/instances.ts | 11 ++++- src/stores/prompt-state.ts | 34 --------------- src/stores/sessions.ts | 76 +++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 46 deletions(-) delete mode 100644 src/stores/prompt-state.ts diff --git a/src/components/prompt-input.tsx b/src/components/prompt-input.tsx index 192648dd..198d42fc 100644 --- a/src/components/prompt-input.tsx +++ b/src/components/prompt-input.tsx @@ -2,13 +2,12 @@ import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack import UnifiedPicker from "./unified-picker" import { addToHistory, getHistory } from "../stores/message-history" import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments" -import { getPromptValue, setPromptValue, clearPromptValue } from "../stores/prompt-state" import { createFileAttachment, createTextAttachment, createAgentAttachment } from "../types/attachment" import type { Attachment } from "../types/attachment" import Kbd from "./kbd" import HintRow from "./hint-row" import { getActiveInstance } from "../stores/instances" -import { agents } from "../stores/sessions" +import { agents, getSessionDraftPrompt, setSessionDraftPrompt, clearSessionDraftPrompt } from "../stores/sessions" interface PromptInputProps { instanceId: string @@ -57,11 +56,11 @@ export default function PromptInput(props: PromptInputProps) { const setPrompt = (value: string) => { setPromptInternal(value) - setPromptValue(props.instanceId, props.sessionId, value) + setSessionDraftPrompt(props.instanceId, props.sessionId, value) } const clearPrompt = () => { - clearPromptValue(props.instanceId, props.sessionId) + clearSessionDraftPrompt(props.instanceId, props.sessionId) setPromptInternal("") } @@ -116,14 +115,14 @@ export default function PromptInput(props: PromptInputProps) { const sessionId = props.sessionId onCleanup(() => { - setPromptValue(instanceId, sessionId, prompt()) + setSessionDraftPrompt(instanceId, sessionId, prompt()) }) - const storedPrompt = getPromptValue(instanceId, sessionId) + const storedPrompt = getSessionDraftPrompt(instanceId, sessionId) const currentAttachments = untrack(() => getAttachments(instanceId, sessionId)) setPromptInternal(storedPrompt) - setPromptValue(instanceId, sessionId, storedPrompt) + setSessionDraftPrompt(instanceId, sessionId, storedPrompt) setHistoryIndex(-1) setIgnoredAtPositions(new Set()) setShowPicker(false) @@ -134,9 +133,8 @@ export default function PromptInput(props: PromptInputProps) { queueMicrotask(() => { adjustTextareaHeight(textareaRef) }) - }, - { defer: true }, - ), + } + ) ) function handleRemoveAttachment(attachmentId: string) { diff --git a/src/stores/instances.ts b/src/stores/instances.ts index 3ac732b8..7b9f47d6 100644 --- a/src/stores/instances.ts +++ b/src/stores/instances.ts @@ -2,7 +2,13 @@ import { createSignal } from "solid-js" import type { Instance, LogEntry } from "../types/instance" import { sdkManager } from "../lib/sdk-manager" import { sseManager } from "../lib/sse-manager" -import { fetchSessions, fetchAgents, fetchProviders, removeSessionIndexes } from "./sessions" +import { + fetchSessions, + fetchAgents, + fetchProviders, + removeSessionIndexes, + clearInstanceDraftPrompts, +} from "./sessions" import { preferences, updateLastUsedBinary } from "./preferences" const [instances, setInstances] = createSignal>(new Map()) @@ -69,8 +75,9 @@ function removeInstance(id: string) { setActiveInstanceId(null) } - // Clean up session indexes for removed instance + // Clean up session indexes and drafts for removed instance removeSessionIndexes(id) + clearInstanceDraftPrompts(id) } async function createInstance(folder: string, binaryPath?: string): Promise { diff --git a/src/stores/prompt-state.ts b/src/stores/prompt-state.ts deleted file mode 100644 index f3260101..00000000 --- a/src/stores/prompt-state.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createSignal } from "solid-js" - -function getSessionKey(instanceId: string, sessionId: string): string { - return `${instanceId}:${sessionId}` -} - -const [prompts, setPrompts] = createSignal>(new Map()) - -export function getPromptValue(instanceId: string, sessionId: string): string { - const key = getSessionKey(instanceId, sessionId) - return prompts().get(key) || "" -} - -export function setPromptValue(instanceId: string, sessionId: string, value: string) { - const key = getSessionKey(instanceId, sessionId) - setPrompts((prev) => { - const next = new Map(prev) - if (value.length === 0) { - next.delete(key) - } else { - next.set(key, value) - } - return next - }) -} - -export function clearPromptValue(instanceId: string, sessionId: string) { - const key = getSessionKey(instanceId, sessionId) - setPrompts((prev) => { - const next = new Map(prev) - next.delete(key) - return next - }) -} diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index a78ec34b..03fc9963 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -25,6 +25,74 @@ const [activeSessionId, setActiveSessionId] = createSignal>( const [activeParentSessionId, setActiveParentSessionId] = createSignal>(new Map()) const [agents, setAgents] = createSignal>(new Map()) const [providers, setProviders] = createSignal>(new Map()) +const [sessionDraftPrompts, setSessionDraftPrompts] = createSignal>(new Map()) + +function getDraftKey(instanceId: string, sessionId: string): string { + return `${instanceId}:${sessionId}` +} + +function getSessionDraftPrompt(instanceId: string, sessionId: string): string { + if (!instanceId || !sessionId) return "" + const key = getDraftKey(instanceId, sessionId) + return sessionDraftPrompts().get(key) ?? "" +} + +function setSessionDraftPrompt(instanceId: string, sessionId: string, value: string) { + const key = getDraftKey(instanceId, sessionId) + setSessionDraftPrompts((prev) => { + const next = new Map(prev) + if (!value) { + next.delete(key) + } else { + next.set(key, value) + } + return next + }) +} + +function clearSessionDraftPrompt(instanceId: string, sessionId: string) { + const key = getDraftKey(instanceId, sessionId) + setSessionDraftPrompts((prev) => { + if (!prev.has(key)) return prev + const next = new Map(prev) + next.delete(key) + return next + }) +} + +function clearInstanceDraftPrompts(instanceId: string) { + if (!instanceId) return + setSessionDraftPrompts((prev) => { + let changed = false + const next = new Map(prev) + const prefix = `${instanceId}:` + for (const key of Array.from(next.keys())) { + if (key.startsWith(prefix)) { + next.delete(key) + changed = true + } + } + return changed ? next : prev + }) +} + +function pruneDraftPrompts(instanceId: string, validSessionIds: Set) { + setSessionDraftPrompts((prev) => { + let changed = false + const next = new Map(prev) + const prefix = `${instanceId}:` + for (const key of Array.from(next.keys())) { + if (key.startsWith(prefix)) { + const sessionId = key.slice(prefix.length) + if (!validSessionIds.has(sessionId)) { + next.delete(key) + changed = true + } + } + } + return changed ? next : prev + }) +} const [loading, setLoading] = createSignal({ fetchingSessions: new Map(), @@ -323,6 +391,8 @@ async function fetchSessions(instanceId: string): Promise { next.set(instanceId, sessionMap) return next }) + + pruneDraftPrompts(instanceId, new Set(sessionMap.keys())) } catch (error) { console.error("Failed to fetch sessions:", error) throw error @@ -736,6 +806,8 @@ async function deleteSession(instanceId: string, sessionId: string): Promise { const next = new Map(prev) @@ -1788,4 +1860,8 @@ export { updateSessionModel, getDefaultModel, removeSessionIndexes, + getSessionDraftPrompt, + setSessionDraftPrompt, + clearSessionDraftPrompt, + clearInstanceDraftPrompts, }