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<OpenInTargetContext>(
-    () => 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.
