From 2005d728e324a53cde48a65fbe8e80d6702c899f Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 22 Oct 2026 23:47:13 -0700 Subject: [PATCH] fix(web): new threads open when the project's machine is offline Co-Authored-By: Claude Opus 5.5 --- apps/web/src/hooks/useHandleNewThread.test.ts | 37 +++++++++++++++++-- apps/web/src/hooks/useHandleNewThread.ts | 9 ++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/apps/web/src/hooks/useHandleNewThread.test.ts b/apps/web/src/hooks/useHandleNewThread.test.ts index 5abfd0768849..678d72b038da 100644 --- a/apps/web/src/hooks/useHandleNewThread.test.ts +++ b/apps/web/src/hooks/useHandleNewThread.test.ts @@ -5,8 +5,9 @@ const testState = vi.hoisted(() => { let completeProjectFileRead: (value: null) => void = () => undefined; let projectFileRead = Promise.resolve(null); let projectRemoved = false; + let connectedEnvironmentIds = ["environment-primary", "environment-ssh"]; let targetSettings = { - defaultThreadEnvMode: "local" as "local" | "worktree", + defaultThreadEnvMode: "local" as "local" | "worktree" | null, newWorktreesStartFromOrigin: false, defaultModelSelection: null, defaultRuntimeMode: "full-access" as RuntimeMode, @@ -46,6 +47,12 @@ const testState = vi.hoisted(() => { get projectRemoved() { return projectRemoved; }, + get connectedEnvironmentIds() { + return connectedEnvironmentIds; + }, + disconnect(environmentId: string) { + connectedEnvironmentIds = connectedEnvironmentIds.filter((id) => id !== environmentId); + }, removeProject() { projectRemoved = true; }, @@ -55,12 +62,13 @@ const testState = vi.hoisted(() => { reset( nextStoredDraft: typeof storedDraft, workspaceDefaults = { - envMode: "local" as "local" | "worktree", + envMode: "local" as "local" | "worktree" | null, startFromOrigin: false, }, ) { storedDraft = nextStoredDraft; projectRemoved = false; + connectedEnvironmentIds = ["environment-primary", "environment-ssh"]; targetSettings = { defaultThreadEnvMode: workspaceDefaults.envMode, newWorktreesStartFromOrigin: workspaceDefaults.startFromOrigin, @@ -94,7 +102,12 @@ const testServerConfigs = () => ]); vi.mock("../rpc/atomRegistry", () => ({ - appAtomRegistry: { get: () => testServerConfigs() }, + appAtomRegistry: { + get: (atom: unknown) => + atom === "connected-environment-ids" + ? testState.connectedEnvironmentIds + : testServerConfigs(), + }, })); vi.mock("@effect/atom-react", () => ({ @@ -187,6 +200,9 @@ vi.mock("../state/entities", () => { useThread: () => null, }; }); +vi.mock("../state/presentation", () => ({ + environmentSummaries: { connectedEnvironmentIdsAtom: "connected-environment-ids" }, +})); vi.mock("../state/server", () => ({ environmentServerConfigsAtom: {}, primaryServerSettingsAtom: "primary-settings", @@ -244,3 +260,18 @@ describe.each([ expect(testState.draftStore.setLogicalProjectDraftThreadId).not.toHaveBeenCalled(); }); }); + +describe("useNewThreadHandler", () => { + it("opens the draft without waiting on a project file from an offline machine", async () => { + testState.reset(null, { envMode: null, startFromOrigin: false }); + testState.disconnect("environment-ssh"); + + const opened = await useNewThreadHandler()({ + environmentId: "environment-ssh", + projectId: "project-remote", + } as never); + + expect(opened).toEqual({ draftId: "draft-delayed", threadId: "thread-delayed" }); + expect(testState.router.navigate).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index cfb98fc83bd0..f079e65c8129 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -36,6 +36,7 @@ import { } from "../lib/chatThreadActions"; import { readT3ProjectFile } from "../lib/t3ProjectFileDefaults"; import { appAtomRegistry } from "../rpc/atomRegistry"; +import { environmentSummaries } from "../state/presentation"; import { environmentServerConfigsAtom } from "../state/server"; import { resolveThreadRouteTarget } from "../threadRoutes"; import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; @@ -139,8 +140,14 @@ export function useNewThreadHandler() { destinationDraftId, }); const resolveDefaultEnvMode = async (): Promise => { + // Environment queries wait for a connection, so reading the project file on an + // offline machine would never settle and the draft would never open. const consultProjectFile = - project !== undefined && projectSettings.settings.defaultThreadEnvMode === null; + project !== undefined && + projectSettings.settings.defaultThreadEnvMode === null && + appAtomRegistry + .get(environmentSummaries.connectedEnvironmentIdsAtom) + .includes(project.environmentId); const projectFile = consultProjectFile ? await readT3ProjectFile(project.environmentId, project.workspaceRoot) : null;