authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-01 15:03:47-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 18:03:47-04:00
logc0995d2eaf8ec787b3318ed1169ae266ed1529f8
tree4d4ec0e589d0c5f34da675bebc7bec5be2fc18fd
parentd0b4acbd13b2b602710e4a7d60c42f4799a409be
signature Signed by PGP key B5690EEEBB952194

fix(web): keep the selected environment when changing projects (#9102)


3 files changed, 81 insertions(+), 7 deletions(-)

apps/web/src/environmentGrouping.test.ts+68
......@@ -323,6 +323,74 @@ describe("environment grouping", () => {
323323 expect(entries[1]?.group.displayName).toBe("separate");
324324 });
325325
326 it("keeps the current environment when available and falls back otherwise", () => {
327 const currentPrimary = makeProject({ repositoryIdentity });
328 const currentRemote = makeProject({
329 id: ProjectId.make("current-remote"),
330 environmentId: remoteEnvironmentId,
331 repositoryIdentity,
332 });
333 const destinationRepositoryIdentity = {
334 canonicalKey: "github.com/example/destination",
335 locator: {
336 source: "git-remote" as const,
337 remoteName: "origin",
338 remoteUrl: "https://github.com/example/destination.git",
339 },
340 };
341 const destinationPrimary = makeProject({
342 id: ProjectId.make("destination-primary"),
343 title: "destination",
344 workspaceRoot: "/tmp/destination",
345 repositoryIdentity: destinationRepositoryIdentity,
346 });
347 const destinationRemote = makeProject({
348 id: ProjectId.make("destination-remote"),
349 environmentId: remoteEnvironmentId,
350 title: "destination",
351 workspaceRoot: "/remote/destination",
352 repositoryIdentity: destinationRepositoryIdentity,
353 });
354 const fallbackPrimary = makeProject({
355 id: ProjectId.make("fallback-primary"),
356 title: "fallback",
357 workspaceRoot: "/tmp/fallback",
358 });
359 const groups = buildSidebarProjectSnapshots({
360 projects: [
361 currentPrimary,
362 currentRemote,
363 destinationPrimary,
364 destinationRemote,
365 fallbackPrimary,
366 ],
367 settings: defaultGroupingSettings,
368 primaryEnvironmentId,
369 resolveEnvironmentLabel: () => null,
370 });
371
372 const entries = buildSidebarProjectPickerEntries({
373 groups,
374 preferredProjectRef: {
375 environmentId: remoteEnvironmentId,
376 projectId: currentRemote.id,
377 },
378 });
379 const destination = entries.find(
380 (entry) => entry.group.projectKey === destinationRepositoryIdentity.canonicalKey,
381 );
382 const fallback = entries.find((entry) => entry.group.displayName === "fallback");
383
384 expect(destination?.targetProject).toMatchObject({
385 environmentId: remoteEnvironmentId,
386 id: destinationRemote.id,
387 });
388 expect(fallback?.targetProject).toMatchObject({
389 environmentId: primaryEnvironmentId,
390 id: fallbackPrimary.id,
391 });
392 });
393
326394 it("keeps manual project order when building grouped sidebar entries", () => {
327395 const primary = makeProject({ repositoryIdentity });
328396 const remote = makeProject({
apps/web/src/sidebarProjectGrouping.ts+8-7
......@@ -118,22 +118,23 @@ export function buildSidebarProjectPickerEntries(input: {
118118 groups: ReadonlyArray<SidebarProjectSnapshot>;
119119 preferredProjectRef: ScopedProjectRef | null;
120120}) {
121 const preferredProjectRef = input.preferredProjectRef;
121122 const entries = input.groups.flatMap((group): SidebarProjectPickerEntry[] => {
122 const isPreferred = input.preferredProjectRef
123 const isPreferred = preferredProjectRef
123124 ? group.memberProjectRefs.some(
124125 (projectRef) =>
125 projectRef.environmentId === input.preferredProjectRef?.environmentId &&
126 projectRef.projectId === input.preferredProjectRef.projectId,
126 projectRef.environmentId === preferredProjectRef.environmentId &&
127 projectRef.projectId === preferredProjectRef.projectId,
127128 )
128129 : false;
129 const preferredProject = isPreferred
130 const preferredProject = preferredProjectRef
130131 ? (group.memberProjects.find(
131132 (project) =>
132 project.environmentId === input.preferredProjectRef?.environmentId &&
133 project.id === input.preferredProjectRef?.projectId,
133 project.environmentId === preferredProjectRef.environmentId &&
134 project.id === preferredProjectRef.projectId,
134135 ) ??
135136 group.memberProjects.find(
136 (project) => project.environmentId === input.preferredProjectRef?.environmentId,
137 (project) => project.environmentId === preferredProjectRef.environmentId,
137138 ))
138139 : null;
139140 const targetProject =
docs/user/composer.md+5
......@@ -52,6 +52,11 @@ On mobile, the model picker shows each OpenCode model's upstream provider, such
5252GitHub Copilot, or OpenCode Zen, beneath its name. Search by that provider name to narrow the list
5353when starting a thread or changing an existing thread's model.
5454
55## Changing projects
56
57On web and desktop, changing the project from a new thread keeps the current environment when that
58project exists there. If it does not, T3 Code selects another environment that has the project.
59
5560## Notices above the composer
5661
5762On web and desktop, loading and syncing statuses fill the available banner width beside the