+
+ The repairs are verified in{" "}
+
+ .
+
+ ,
+ );
+ const link = screen.getByRole("link", { name: "Thread, Code store foundation" });
+ await expect.element(link).toHaveAttribute("href", `/local/${encodeURIComponent(threadId)}`);
+ await screen.unmount();
+ expect(appAtomRegistry.get(familyLoaded)).toBe(false);
+});
diff --git a/apps/web/src/components/chat/MarkdownThreadLink.tsx b/apps/web/src/components/chat/MarkdownThreadLink.tsx
index b5d3bbb5ac782..7a797ee7932a9 100644
--- a/apps/web/src/components/chat/MarkdownThreadLink.tsx
+++ b/apps/web/src/components/chat/MarkdownThreadLink.tsx
@@ -23,23 +23,26 @@ export function MarkdownThreadLink(props: {
readonly label: string | null;
}) {
const environmentId = props.link.environmentId ?? props.environmentId;
- const decodedId = percentDecodedThreadLinkId(props.link.threadId);
- const written = useThreadShell(scopeThreadRef(environmentId, props.link.threadId));
+ const { threadId } = props.link;
+ const decodedId = percentDecodedThreadLinkId(threadId);
+ const written = useThreadShell(scopeThreadRef(environmentId, threadId));
const decoded = useThreadShell(
written === null && decodedId !== null ? scopeThreadRef(environmentId, decodedId) : null,
);
const local = written ?? decoded;
- // Shells leave out idle subagent children and older settled threads, so a cited thread the
- // shell lacks is read with its family until it lands; only a settled empty read proves it gone.
- const missingThreadId = local === null ? (decodedId ?? props.link.threadId) : null;
- // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- mounts the family read for a cited thread the shell omits while this link shows it
- useEffect(
- () =>
- missingThreadId === null
+ // An idle child's shell row can come from this very read. Releasing on title arrival removes it.
+ // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- retains the cited families while the link is displayed
+ useEffect(() => {
+ const releaseWritten = threadFamilies.mount(appAtomRegistry, environmentId, threadId);
+ const releaseDecoded =
+ decodedId === null
? undefined
- : threadFamilies.mount(appAtomRegistry, environmentId, missingThreadId),
- [environmentId, missingThreadId],
- );
+ : threadFamilies.mount(appAtomRegistry, environmentId, decodedId);
+ return () => {
+ releaseWritten();
+ releaseDecoded?.();
+ };
+ }, [environmentId, threadId, decodedId]);
const guess = local === null && props.link.environmentId === null;
const refs = useThreadRefs(guess);
const remote = useThreadShell(guess ? guessThreadLinkRef(props.link, refs) : null);
From 7d5eec35fc04aac0a8a77cabdb9070036d5f6fa8 Mon Sep 17 00:00:00 2001
From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com>
Date: Sun, 8 Nov 2026 00:05:02 -0800
Subject: [PATCH 2/2] fix(chat): fetch cited families only when absent from
shell
---
apps/mobile/src/state/entities.ts | 16 ++--
.../components/chat/MarkdownThreadLink.tsx | 21 ++---
.../src/state/threadFamilies.test.ts | 91 ++++++++++++++++++-
.../src/state/threadFamilies.ts | 57 +++++++++++-
4 files changed, 158 insertions(+), 27 deletions(-)
diff --git a/apps/mobile/src/state/entities.ts b/apps/mobile/src/state/entities.ts
index 799d92cdf70cc..e01611b5c596f 100644
--- a/apps/mobile/src/state/entities.ts
+++ b/apps/mobile/src/state/entities.ts
@@ -210,19 +210,17 @@ export function useSideChats(ref: ScopedThreadRef) {
export function useLiveThreadLinkLabels(markdown: string, environmentId: EnvironmentId): string {
const refs = hasThreadLinks(markdown) ? threadLinkRefs(markdown, environmentId) : [];
+ const refsKey = JSON.stringify(refs.map(threadKey));
const titles = useAtomValue(
- refs.length === 0
- ? EMPTY_THREAD_LINK_TITLES_ATOM
- : threadLinkTitlesAtom(JSON.stringify(refs.map(threadKey))),
+ refs.length === 0 ? EMPTY_THREAD_LINK_TITLES_ATOM : threadLinkTitlesAtom(refsKey),
);
- // Shells leave out idle subagent children and older settled threads, so a cited thread the
- // shell lacks is read with its family until it lands; only a settled empty read proves it gone.
- const refsKey = JSON.stringify(refs.map(threadKey));
- // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- keeps cited families mounted until the links change; releasing on title arrival removes that title again
+ // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- keeps cited rows mounted for the message's lifetime, independently of the family data it loads
useEffect(() => {
- const releases = (JSON.parse(refsKey) as ReadonlyArray