diff --git a/apps/app/src/hooks/useLocalOpenTargets.ts b/apps/app/src/hooks/useLocalOpenTargets.ts index ba573bd1c..1481e97c0 100644 --- a/apps/app/src/hooks/useLocalOpenTargets.ts +++ b/apps/app/src/hooks/useLocalOpenTargets.ts @@ -211,9 +211,29 @@ function useOpenTargetResolution( export function useLocalOpenTargets( args: UseLocalOpenTargetsArgs, ): UseLocalOpenTargetsResult { + // FIX (#1304 part B): memoize structurally. Callers (ThreadDetailView) + // rebuild an equal context object on every render; keying on identity made + // every callback below — and the timeline-wide context value built from + // them — change identity per render. + const openContextKind = args.openContext?.kind ?? "local"; + const openContextHostId = + args.openContext?.kind === "remote-ssh" ? args.openContext.hostId : null; + const openContextServerOrigin = + args.openContext?.kind === "remote-ssh" + ? args.openContext.serverOrigin + : null; const openContext = useMemo( - () => args.openContext ?? { kind: "local" }, - [args.openContext], + () => + openContextKind === "remote-ssh" && + openContextHostId !== null && + openContextServerOrigin !== null + ? { + kind: "remote-ssh", + hostId: openContextHostId, + serverOrigin: openContextServerOrigin, + } + : { kind: "local" }, + [openContextHostId, openContextKind, openContextServerOrigin], ); const contextKind = openContext.kind; const { hasDaemon } = useHostDaemon(); diff --git a/apps/app/src/hooks/usePromptDraftStorage.ts b/apps/app/src/hooks/usePromptDraftStorage.ts index a040f7da2..33073d107 100644 --- a/apps/app/src/hooks/usePromptDraftStorage.ts +++ b/apps/app/src/hooks/usePromptDraftStorage.ts @@ -251,13 +251,30 @@ function getPromptDraftStorageKey(scope: PromptDraftScope): string { * renders the draft. */ export function getPromptDraftAccessor(scope: PromptDraftScope): { + storageKey: string; getCurrent: () => PromptDraftState; setDraft: (draft: PromptDraftState) => void; + addQuote: ( + text: string, + attachments?: readonly PromptDraftAttachment[], + ) => void; } { const storageKey = getPromptDraftStorageKey(scope); return { + storageKey, getCurrent: () => readPromptDraft(storageKey), setDraft: (draft) => writePromptDraft(storageKey, draft), + addQuote: (text, attachments = []) => { + const currentDraft = readPromptDraft(storageKey); + const nextDraft = appendQuoteAndAttachmentsToDraft( + currentDraft, + text, + attachments, + ); + // Whitespace-only text with no new attachments is a no-op. + if (nextDraft === currentDraft) return; + writePromptDraft(storageKey, nextDraft); + }, }; } diff --git a/apps/app/src/views/thread-detail/ThreadDetailView.tsx b/apps/app/src/views/thread-detail/ThreadDetailView.tsx index a2a4724bd..8893b677f 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailView.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailView.tsx @@ -74,7 +74,7 @@ import { type ProjectThreadSubsetFilters, } from "../../hooks/queries/thread-queries"; import { isTransientReadError } from "@/hooks/queries/query-helpers"; -import { usePromptDraftStorage } from "@/hooks/usePromptDraftStorage"; +import { getPromptDraftAccessor } from "@/hooks/usePromptDraftStorage"; import { subscribeComposerFocusRequests } from "@/lib/composer-focus-requests"; import { ThreadGitActionDialog } from "@/components/dialogs/ThreadGitActionDialog"; import { PageShell } from "@/components/ui/page-shell.js"; @@ -962,11 +962,20 @@ function ThreadDetailViewInternal(props: ThreadDetailViewInternalProps) { // localStorage-backed draft — the quoted text is appended to the draft as a // `> ` blockquote block and renders inline in the composer immediately, with // no duplicated draft state. - const selectionPromptDraft = usePromptDraftStorage({ - kind: "thread", - projectId: thread?.projectId ?? projectId ?? "", - threadId: thread?.id ?? "", - }); + // FIX (#1304 part A): this view never renders the draft; it only needs + // event-time access (addQuote) and the storage key. Subscribing via + // usePromptDraftStorage re-rendered the whole thread view on every keystroke. + const selectionPromptDraftProjectId = thread?.projectId ?? projectId ?? ""; + const selectionPromptDraftThreadId = thread?.id ?? ""; + const selectionPromptDraft = useMemo( + () => + getPromptDraftAccessor({ + kind: "thread", + projectId: selectionPromptDraftProjectId, + threadId: selectionPromptDraftThreadId, + }), + [selectionPromptDraftProjectId, selectionPromptDraftThreadId], + ); const addQuoteToComposer = selectionPromptDraft.addQuote; // Desktop quote actions keep their existing focus handoff. Mobile web does // not focus inputs programmatically; see PromptBoxInternal.