Skip to content

Commit 9c6429d

Browse files
fix(web): threads without a project start on the primary environment (#26301)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 28da8c8 commit 9c6429d

6 files changed

Lines changed: 69 additions & 15 deletions

File tree

‎apps/web/src/components/CommandPalette.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,9 +622,7 @@ function OpenCommandPaletteDialog(props: {
622622
const currentProjectEnvironmentId =
623623
activeThread?.environmentId ?? activeDraftThread?.environmentId ?? null;
624624
const currentProjectId = activeThread?.projectId ?? activeDraftThread?.projectId ?? null;
625-
const scratchTargetEnvironmentId = scratchEnvironmentId(
626-
currentProjectEnvironmentId ?? primaryEnvironmentId,
627-
);
625+
const scratchTargetEnvironmentId = scratchEnvironmentId(currentProjectEnvironmentId);
628626
const supportsTriage = (environmentId: EnvironmentId | null) =>
629627
environments.find((environment) => environment.environmentId === environmentId)?.serverConfig
630628
?.environment.capabilities.triage === true;

‎apps/web/src/components/chat/DraftHeroHeadline.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,11 @@ export function DraftHeroHeadline({
143143
project.environmentId === activeProjectRef.environmentId &&
144144
project.id === activeProjectRef.projectId,
145145
) ?? null);
146-
const scratchTargetEnvironmentId = scratchEnvironmentId(
147-
activeProjectRef?.environmentId ?? primaryEnvironmentId,
148-
);
146+
const scratchTargetEnvironmentId = scratchEnvironmentId(activeProjectRef?.environmentId ?? null);
149147
const scratchWorkspaceRoot = scratchWorkspaceRootFor(scratchTargetEnvironmentId);
150148
const isScratchDraft =
151-
activeProject !== null && isScratchProject(activeProject, scratchWorkspaceRoot);
149+
activeProject !== null &&
150+
isScratchProject(activeProject, scratchWorkspaceRootFor(activeProject.environmentId));
152151

153152
const pickerItems: readonly PickerItem[] = [
154153
...menuEntries.map(({ group }) => ({ value: group.projectKey, label: group.displayName })),

‎apps/web/src/components/promptLinks/NewThreadLinkView.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,7 @@ export function NewThreadLinkView({ link }: { readonly link: NewThreadLink }) {
175175
};
176176

177177
const openScratchDraft = async () => {
178-
const environmentId =
179-
scratchEnvironmentId(lastActiveEnvironmentId()) ?? scratchEnvironmentId(null);
178+
const environmentId = scratchEnvironmentId(lastActiveEnvironmentId());
180179
const project = environmentId === null ? null : await openScratchProject(environmentId);
181180
if (project === null) {
182181
setState({
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { EnvironmentId } from "@t3tools/contracts";
2+
import { AtomRegistry } from "effect/reactivity";
3+
import { describe, expect, it } from "vite-plus/test";
4+
5+
import { createScratchProjectActions } from "./useScratchProject";
6+
7+
const local = "local" as EnvironmentId;
8+
const remote = "remote" as EnvironmentId;
9+
const other = "other" as EnvironmentId;
10+
11+
function scratchEnvironment(environmentId: EnvironmentId, scratchWorkspaceRoot: string | null) {
12+
return {
13+
environmentId,
14+
connection: { phase: "connected" },
15+
serverConfig: { scratchWorkspaceRoot },
16+
} as never;
17+
}
18+
19+
function actions(
20+
primary: EnvironmentId | null,
21+
localRoot: string | null,
22+
remoteRoot: string | null = "/remote/scratch",
23+
) {
24+
const environments = [
25+
scratchEnvironment(local, localRoot),
26+
scratchEnvironment(remote, remoteRoot),
27+
scratchEnvironment(other, "/other/scratch"),
28+
];
29+
return createScratchProjectActions({
30+
environments: () => environments,
31+
primaryEnvironmentId: () => primary,
32+
registry: AtomRegistry.make(),
33+
handleNewThread: async () => null,
34+
});
35+
}
36+
37+
describe("scratchEnvironmentId", () => {
38+
it("prefers the primary environment over the one on screen", () => {
39+
expect(actions(local, "/local/scratch").scratchEnvironmentId(remote)).toBe(local);
40+
expect(actions(local, "/local/scratch").scratchEnvironmentId(null)).toBe(local);
41+
});
42+
43+
it("falls back when the primary environment offers no scratch root", () => {
44+
expect(actions(local, null).scratchEnvironmentId(remote)).toBe(remote);
45+
expect(actions(null, null).scratchEnvironmentId(remote)).toBe(remote);
46+
expect(actions(null, null, null).scratchEnvironmentId(remote)).toBe(other);
47+
expect(actions(local, null).scratchEnvironmentId(null)).toBeNull();
48+
});
49+
});

‎apps/web/src/hooks/useScratchProject.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import type { AtomRegistry } from "effect/reactivity";
1212
import { useContext } from "react";
1313

1414
import { stackedThreadToast, toastManager } from "~/components/ui/toast";
15-
import { type EnvironmentPresentation, useEnvironments } from "~/state/environments";
15+
import {
16+
type EnvironmentPresentation,
17+
useEnvironments,
18+
usePrimaryEnvironmentId,
19+
} from "~/state/environments";
1620
import { projectEnvironment } from "~/state/projects";
1721
import { type NewThreadHandler, useNewThreadHandler } from "./useHandleNewThread";
1822

@@ -32,6 +36,7 @@ type ScratchEnvironment = Pick<EnvironmentPresentation, "connection" | "serverCo
3236

3337
export function createScratchProjectActions(input: {
3438
readonly environments: () => Iterable<ScratchEnvironment>;
39+
readonly primaryEnvironmentId: () => EnvironmentId | null;
3540
readonly registry: AtomRegistry.AtomRegistry;
3641
readonly handleNewThread: NewThreadHandler;
3742
}) {
@@ -43,8 +48,11 @@ export function createScratchProjectActions(input: {
4348
return availableScratchWorkspaceRoot(environment?.connection.phase, environment?.serverConfig);
4449
};
4550

46-
const scratchEnvironmentId = (current: EnvironmentId | null): EnvironmentId | null => {
47-
if (current !== null) return scratchWorkspaceRootFor(current) !== null ? current : null;
51+
// The primary (local) environment wins; `fallback` only applies when it offers no scratch root.
52+
const scratchEnvironmentId = (fallback: EnvironmentId | null): EnvironmentId | null => {
53+
const primary = input.primaryEnvironmentId();
54+
if (primary !== null && scratchWorkspaceRootFor(primary) !== null) return primary;
55+
if (fallback !== null && scratchWorkspaceRootFor(fallback) !== null) return fallback;
4856
const offering = [...input.environments()].filter(
4957
(entry) => scratchWorkspaceRootFor(entry.environmentId) !== null,
5058
);
@@ -85,10 +93,12 @@ export function createScratchProjectActions(input: {
8593

8694
export function useScratchProject() {
8795
const { environments } = useEnvironments();
96+
const primaryEnvironmentId = usePrimaryEnvironmentId();
8897
const registry = useContext(RegistryContext);
8998
const handleNewThread = useNewThreadHandler();
9099
return createScratchProjectActions({
91100
environments: () => environments,
101+
primaryEnvironmentId: () => primaryEnvironmentId,
92102
registry,
93103
handleNewThread,
94104
});

‎apps/web/src/shortcuts/chatRouteCommands.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function installChatRouteCommands(router: AnyRouter): () => void {
3939
[...appAtomRegistry.get(environmentPresentations.presentationsAtom)].map(
4040
([environmentId, presentation]) => ({ ...presentation, environmentId }),
4141
),
42+
primaryEnvironmentId: () => appAtomRegistry.get(primaryEnvironmentIdAtom),
4243
registry: appAtomRegistry,
4344
handleNewThread,
4445
});
@@ -109,9 +110,7 @@ export function installChatRouteCommands(router: AnyRouter): () => void {
109110
"chat.newWithoutProject": (event) => {
110111
const { activeThread, activeDraftThread } = readThreadContext();
111112
const environmentId = scratch.scratchEnvironmentId(
112-
activeThread?.environmentId ??
113-
activeDraftThread?.environmentId ??
114-
appAtomRegistry.get(primaryEnvironmentIdAtom),
113+
activeThread?.environmentId ?? activeDraftThread?.environmentId ?? null,
115114
);
116115
if (environmentId === null) return;
117116
claim(event);

0 commit comments

Comments
 (0)