diff --git a/apps/app/src/components/promptbox/PromptBoxInternal.tsx b/apps/app/src/components/promptbox/PromptBoxInternal.tsx
index 514924688..8f5dfb840 100644
--- a/apps/app/src/components/promptbox/PromptBoxInternal.tsx
+++ b/apps/app/src/components/promptbox/PromptBoxInternal.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import { atom, useAtom } from "jotai";
 import { RESET, atomWithStorage } from "jotai/utils";
 import type {
@@ -1132,6 +1133,7 @@ export function PromptBoxInternal({
   promptBoxRef,
   focusEndKey,
 }: PromptBoxInternalProps) {
+  renderProbe("PromptBoxInternal");
   const focusComposerShortcut = useAppCommandShortcut("composer.focus");
   const {
     isSubmitting = false,
diff --git a/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx b/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx
index e1b991ad2..288019ca6 100644
--- a/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx
+++ b/apps/app/src/components/thread/embedded-chat/EmbeddedThreadChat.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import {
   useCallback,
   useEffect,
@@ -270,6 +271,7 @@ function EmbeddedThreadChatHostedFooter({
   scrollOverlay,
   surface,
 }: EmbeddedThreadChatHostedFooterProps) {
+  renderProbe("EmbeddedThreadChatHostedFooter");
   return (
     <div
       data-thread-window=""
diff --git a/apps/app/src/components/thread/timeline/ThreadTimelineRows.tsx b/apps/app/src/components/thread/timeline/ThreadTimelineRows.tsx
index 5dadc1e2e..30b1c11ab 100644
--- a/apps/app/src/components/thread/timeline/ThreadTimelineRows.tsx
+++ b/apps/app/src/components/thread/timeline/ThreadTimelineRows.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import {
   createContext,
   memo,
@@ -1637,6 +1638,7 @@ function TimelineRowView({
   showAssistantMessageActions,
   spacing,
 }: TimelineRowViewProps) {
+  renderProbe("TimelineRowView");
   const horizontalPadding = timelineRowHorizontalPadding(spacing);
   const { onTitleAction, resolveSegmentLinkHref } =
     useTimelineRendererStaticContext();
@@ -1744,6 +1746,7 @@ function TimelineExpandableRowView({
   horizontalPadding,
   row,
 }: TimelineExpandableRowViewProps) {
+  renderProbe("TimelineExpandableRowView");
   const { onTitleAction, resolveSegmentLinkHref } =
     useTimelineRendererStaticContext();
   const {
@@ -1940,6 +1943,7 @@ function ThreadTimelineRowsComponent(props: ThreadTimelineRowsProps) {
 }
 
 function ThreadTimelineRowsForTimelineView(props: ThreadTimelineRowsProps) {
+  renderProbe("ThreadTimelineRows");
   const getViewRows = useTimelineViewRowsCache();
   const rows = useMemo(
     () => getViewRows(props.timelineRows),
diff --git a/apps/app/src/components/thread/timeline/ThreadTimelineSurface.tsx b/apps/app/src/components/thread/timeline/ThreadTimelineSurface.tsx
index 978011b31..6b41497a6 100644
--- a/apps/app/src/components/thread/timeline/ThreadTimelineSurface.tsx
+++ b/apps/app/src/components/thread/timeline/ThreadTimelineSurface.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import { useEffect, useMemo, useState, type ReactNode } from "react";
 import type {
   ActiveThinking,
@@ -178,6 +179,7 @@ export function ThreadTimelineSurface({
   unreadDividerPlacement,
   workspaceRootPath,
 }: ThreadTimelineSurfaceProps) {
+  renderProbe("ThreadTimelineSurface");
   const preferredTheme = usePreferredTheme();
   const showActiveThinking =
     activeThinking !== null && ongoingIndicatorLabel === undefined;
diff --git a/apps/app/src/components/ui/markdown-preview.tsx b/apps/app/src/components/ui/markdown-preview.tsx
index b25ea5392..0f47a5da6 100644
--- a/apps/app/src/components/ui/markdown-preview.tsx
+++ b/apps/app/src/components/ui/markdown-preview.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import {
   Children,
   cloneElement,
@@ -618,6 +619,7 @@ function MarkdownAnchor({
   rewriteLocalhostLinks,
   ...anchorProps
 }: MarkdownAnchorProps) {
+  renderProbe("MarkdownAnchor");
   const localFileRouting = linkRouting?.localFile;
   const onOpenLocalFileLink = localFileRouting?.onOpenLink;
   const rewrittenHref = rewriteLocalhostLinkHref({
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/ThreadDetailPromptArea.tsx b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx
index bc2815413..0832ff124 100644
--- a/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx
+++ b/apps/app/src/views/thread-detail/ThreadDetailPromptArea.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import { useCallback, useMemo, useRef, useState } from "react";
 import { createPortal } from "react-dom";
 import { NavLink, useNavigate } from "react-router-dom";
@@ -346,6 +347,7 @@ export function ThreadDetailPromptArea({
   composerFocusRequestNonce,
   thread,
 }: ThreadDetailPromptAreaProps) {
+  renderProbe("ThreadDetailPromptArea");
   const navigate = useNavigate();
   const defaultExecutionOptionsQuery = useThreadDefaultExecutionOptions(
     thread.id,
diff --git a/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx b/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx
index fe23cf36a..6fa495375 100644
--- a/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx
+++ b/apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import { useMemo, type ComponentProps, type ReactNode } from "react";
 import { Skeleton } from "@bb/shared-ui/skeleton";
 import { cn } from "@bb/shared-ui/lib/utils";
@@ -78,6 +79,7 @@ function ThreadDetailSecondaryContentBody({
   secondaryPanel,
   timeline,
 }: ThreadDetailSecondaryContentProps) {
+  renderProbe("ThreadDetailSecondaryContentBody");
   const composerHost = usePluginComposerHost();
   const { renderBrowserDeck, ...threadSecondaryPanelProps } = secondaryPanel;
 
diff --git a/apps/app/src/views/thread-detail/ThreadDetailView.tsx b/apps/app/src/views/thread-detail/ThreadDetailView.tsx
index a2a4724bd..6ba829694 100644
--- a/apps/app/src/views/thread-detail/ThreadDetailView.tsx
+++ b/apps/app/src/views/thread-detail/ThreadDetailView.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import {
   useCallback,
   useEffect,
@@ -74,7 +75,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";
@@ -503,6 +504,7 @@ export function ThreadDetailView(props: ThreadDetailViewProps) {
 }
 
 function ThreadDetailViewInternal(props: ThreadDetailViewInternalProps) {
+  renderProbe("ThreadDetailViewInternal");
   const { projectId, threadId } = props;
   const { isFocused, navigateInPane, onRequestClose, isBoundedPane } =
     usePaneContext();
@@ -962,11 +964,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.
@@ -2368,6 +2379,14 @@ function ThreadDetailViewInternal(props: ThreadDetailViewInternalProps) {
       pluginFileOpeners,
     ],
   );
+  // #1304 investigation probe: which deps of getLocalFileContextMenuItems change identity per render
+  {
+    const w = window as unknown as { __bbDepPrev?: Record<string, unknown>; __bbDepChanges?: Record<string, number> };
+    const cur: Record<string, unknown> = { threadOpenContext, fileOpenTargets, openPathInFileTarget, handleOpenTimelineLocalFileLink, pluginFileOpeners, getLocalFileContextMenuItems, workspaceOpenTargetsLen: fileOpenTargets.length };
+    const prev = w.__bbDepPrev;
+    if (prev) { w.__bbDepChanges ??= {}; for (const k of Object.keys(cur)) if (!Object.is(prev[k], cur[k])) w.__bbDepChanges[k] = (w.__bbDepChanges[k] ?? 0) + 1; }
+    w.__bbDepPrev = cur;
+  }
   const workspaceMarkdownLinkRouting = useMemo(
     () =>
       buildMarkdownPreviewLinkRouting({
diff --git a/apps/app/src/views/thread-detail/ThreadTimelinePane.tsx b/apps/app/src/views/thread-detail/ThreadTimelinePane.tsx
index 7d323f95c..7c112546e 100644
--- a/apps/app/src/views/thread-detail/ThreadTimelinePane.tsx
+++ b/apps/app/src/views/thread-detail/ThreadTimelinePane.tsx
@@ -1,3 +1,4 @@
+import { renderProbe } from "@/lib/render-probe";
 import type { ReactNode } from "react";
 import type { ThreadTimelineUnreadDividerPlacement } from "@/components/thread/timeline";
 import type { PromptMentionLinkResolver } from "@/components/promptbox/editor/prompt-mention-link";
@@ -27,6 +28,7 @@ export function ThreadTimelinePane({
   footer,
   ...surface
 }: ThreadTimelinePaneProps) {
+  renderProbe("ThreadTimelinePane");
   return (
     <EmbeddedThreadChat
       variant="hosted-footer"
