authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-27 21:52:11+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-27 21:52:11+02:00
loge77f42c111dc40e0b208393957ebb23863330e2e
treed18baf32dbbfcf7e2cfe59054980a825764aea0b
parent6e3b73884bf40eeb2859cb16f1200c8e2f9cf7c3
signature Signed by PGP key B5690EEEBB952194

Prevent draft thread detail polling before shell registration (#4670)

Co-authored-by: codex <codex@users.noreply.github.com>

6 files changed, 79 insertions(+), 16 deletions(-)

apps/web/src/components/BranchToolbar.tsx+1-1
......@@ -234,10 +234,10 @@ export const BranchToolbar = memo(function BranchToolbar({
234234 () => scopeThreadRef(environmentId, threadId),
235235 [environmentId, threadId],
236236 );
237 const serverThread = useThread(threadRef);
238237 const draftThread = useComposerDraftStore((store) =>
239238 draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef),
240239 );
240 const serverThread = useThread(threadRef, { waitForShell: draftThread !== null });
241241 const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
242242 const activeProjectRef = serverThread
243243 ? scopeProjectRef(serverThread.environmentId, serverThread.projectId)
apps/web/src/components/BranchToolbarBranchSelector.tsx+2-2
......@@ -129,11 +129,11 @@ export function BranchToolbarBranchSelector({
129129 () => scopeThreadRef(environmentId, threadId),
130130 [environmentId, threadId],
131131 );
132 const serverThread = useThread(threadRef);
133 const serverSession = serverThread?.session ?? null;
134132 const draftThread = useComposerDraftStore((store) =>
135133 draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef),
136134 );
135 const serverThread = useThread(threadRef, { waitForShell: draftThread !== null });
136 const serverSession = serverThread?.session ?? null;
137137 const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
138138
139139 const activeProjectRef = serverThread
apps/web/src/components/ChatView.tsx+10-10
......@@ -620,8 +620,8 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
620620 const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open");
621621 const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write");
622622 const closeTerminalMutation = useAtomCommand(terminalEnvironment.close, "terminal close");
623 const serverThread = useThread(threadRef);
624623 const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef));
624 const serverThread = useThread(threadRef, { waitForShell: draftThread !== null });
625625 const projectRef = serverThread
626626 ? scopeProjectRef(serverThread.environmentId, serverThread.projectId)
627627 : draftThread
......@@ -976,8 +976,8 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane
976976 newShortcutLabel,
977977 closeShortcutLabel,
978978}: PersistentThreadTerminalPanelProps) {
979 const serverThread = useThread(threadRef);
980979 const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef));
980 const serverThread = useThread(threadRef, { waitForShell: draftThread !== null });
981981 const projectRef = serverThread
982982 ? scopeProjectRef(serverThread.environmentId, serverThread.projectId)
983983 : draftThread
......@@ -1181,7 +1181,14 @@ function ChatViewContent(props: ChatViewProps) {
11811181 );
11821182 const composerDraftTarget: ScopedThreadRef | DraftId =
11831183 routeKind === "server" ? routeThreadRef : props.draftId;
1184 const serverThread = useThread(routeThreadRef);
1184 const draftThread = useComposerDraftStore((store) =>
1185 routeKind === "server"
1186 ? store.getDraftSessionByRef(routeThreadRef)
1187 : draftId
1188 ? store.getDraftSession(draftId)
1189 : null,
1190 );
1191 const serverThread = useThread(routeThreadRef, { waitForShell: draftThread !== null });
11851192 const markThreadVisited = useUiStateStore((store) => store.markThreadVisited);
11861193 const activeThreadLastVisitedAt = useUiStateStore(
11871194 (store) => store.threadLastVisitedAtById[routeThreadKey],
......@@ -1234,13 +1241,6 @@ function ChatViewContent(props: ChatViewProps) {
12341241 const setLogicalProjectDraftThreadId = useComposerDraftStore(
12351242 (store) => store.setLogicalProjectDraftThreadId,
12361243 );
1237 const draftThread = useComposerDraftStore((store) =>
1238 routeKind === "server"
1239 ? store.getDraftSessionByRef(routeThreadRef)
1240 : draftId
1241 ? store.getDraftSession(draftId)
1242 : null,
1243 );
12441244 const promptRef = useRef("");
12451245 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
12461246 const composerTerminalContextsRef = useRef<TerminalContextDraft[]>([]);
apps/web/src/components/GitActionsControl.tsx+3-1
......@@ -986,7 +986,6 @@ export default function GitActionsControl({
986986 () => (activeThreadRef ? { threadRef: activeThreadRef } : undefined),
987987 [activeThreadRef],
988988 );
989 const activeServerThread = useThread(activeThreadRef);
990989 const activeDraftThread = useComposerDraftStore((store) =>
991990 draftId
992991 ? store.getDraftSession(draftId)
......@@ -994,6 +993,9 @@ export default function GitActionsControl({
994993 ? store.getDraftThreadByRef(activeThreadRef)
995994 : null,
996995 );
996 const activeServerThread = useThread(activeThreadRef, {
997 waitForShell: activeDraftThread !== null,
998 });
997999 const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
9981000 const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false);
9991001 const [dialogCommitMessage, setDialogCommitMessage] = useState("");
apps/web/src/state/entities.test.ts created+36
......@@ -0,0 +1,36 @@
1import { scopeThreadRef } from "@t3tools/client-runtime/environment";
2import { EnvironmentId, ThreadId } from "@t3tools/contracts";
3import { describe, expect, it } from "vite-plus/test";
4
5import { resolveThreadDetailRef } from "./entities";
6
7const threadRef = scopeThreadRef(EnvironmentId.make("environment-1"), ThreadId.make("thread-1"));
8
9describe("resolveThreadDetailRef", () => {
10 it("does not subscribe to a reserved draft thread before it enters the shell index", () => {
11 expect(
12 resolveThreadDetailRef(threadRef, {
13 shellExists: false,
14 waitForShell: true,
15 }),
16 ).toBeNull();
17 });
18
19 it("subscribes once the reserved draft thread enters the shell index", () => {
20 expect(
21 resolveThreadDetailRef(threadRef, {
22 shellExists: true,
23 waitForShell: true,
24 }),
25 ).toBe(threadRef);
26 });
27
28 it("keeps direct server-thread lookups enabled when the shell has not loaded it", () => {
29 expect(
30 resolveThreadDetailRef(threadRef, {
31 shellExists: false,
32 waitForShell: false,
33 }),
34 ).toBe(threadRef);
35 });
36});
apps/web/src/state/entities.ts+27-2
......@@ -152,10 +152,35 @@ export function useThreadStatus(ref: ScopedThreadRef | null): EnvironmentThreadS
152152 );
153153}
154154
155export function resolveThreadDetailRef(
156 ref: ScopedThreadRef | null,
157 options: {
158 shellExists: boolean;
159 waitForShell: boolean;
160 },
161): ScopedThreadRef | null {
162 return ref !== null && (!options.waitForShell || options.shellExists) ? ref : null;
163}
164
155165/** Detail collections composed with shell-authoritative thread/workspace metadata. */
156export function useThread(ref: ScopedThreadRef | null): EnvironmentThread | null {
166export function useThread(
167 ref: ScopedThreadRef | null,
168 options?: {
169 /**
170 * Client-reserved draft thread ids do not exist on the server until the
171 * first send. Waiting for the shell index avoids polling the detail
172 * endpoint for an intentionally missing thread during that window.
173 */
174 waitForShell?: boolean;
175 },
176): EnvironmentThread | null {
157177 const shell = useThreadShell(ref);
158 const detail = useThreadDetail(ref);
178 const detail = useThreadDetail(
179 resolveThreadDetailRef(ref, {
180 shellExists: shell !== null,
181 waitForShell: options?.waitForShell === true,
182 }),
183 );
159184 return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]);
160185}
161186