Skip to content

Commit d8bf088

Browse files
committed
fix: reopen a workspace on the host it already used
Opening from the Coder panel matched a recent folder with isRemoteAuthorityCompatible, which accepts the legacy coder-vscode authority, but then kept only its path and opened it under a freshly minted per-editor authority. In a fork that moved the folder to a URI the editor had never seen, orphaning the window state stored against the old one. Reuse the matched folder's own authority, so only workspaces with no history get this editor's prefix.
1 parent 1ee92a3 commit d8bf088

3 files changed

Lines changed: 128 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
changed the remote authority, and editors key per-workspace state to it —
1414
Cursor's agent chats among them — so that state looked lost. Workspaces
1515
opened before v1.16.1 keep the host they already had.
16+
- Reopen a workspace on the host it already used when you open it from the
17+
Coder panel, rather than moving it to this editor's own host and starting its
18+
window state over.
1619
- Serve the historical `coder-vscode` host from a single generated file rather
1720
than one per editor, so a connection over it always uses the CLI and
1821
credentials of the editor that is connecting.

src/commands.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1521,7 +1521,7 @@ export class Commands {
15211521
...options,
15221522
};
15231523
let { folderPath } = options;
1524-
const remoteAuthority = toRemoteAuthority(
1524+
let remoteAuthority = toRemoteAuthority(
15251525
baseUrl,
15261526
workspace.owner_name,
15271527
workspace.name,
@@ -1563,6 +1563,11 @@ export class Commands {
15631563
return { status: "cancelled", stage: "recent_folder_picker" };
15641564
}
15651565
}
1566+
// A compatible folder can still be on the legacy coder-vscode host.
1567+
// Reopen it there, since the editor keys window state by the whole URI.
1568+
remoteAuthority =
1569+
opened.find((f) => f.folderUri.path === folderPath)?.folderUri
1570+
.authority ?? remoteAuthority;
15661571
}
15671572

15681573
// Only set the memento when opening a new folder/window
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import * as vscode from "vscode";
3+
4+
import { Commands } from "@/commands";
5+
6+
import { workspace as createWorkspace } from "@repo/mocks";
7+
8+
import { createTestTelemetryService } from "../mocks/telemetry";
9+
import {
10+
createMockLogger,
11+
MockConfigurationProvider,
12+
useEditor,
13+
} from "../mocks/testHelpers";
14+
15+
import type { WorkspaceAgent } from "coder/site/src/api/typesGenerated";
16+
17+
import type { CoderApi } from "@/api/coderApi";
18+
import type { ServiceContainer } from "@/core/container";
19+
import type { DeploymentManager } from "@/deployment/deploymentManager";
20+
21+
vi.mock("@/workspace/workspacesProvider", () => ({
22+
AgentTreeItem: class {
23+
constructor(
24+
public agent: unknown,
25+
public workspace: unknown,
26+
) {}
27+
},
28+
WorkspaceTreeItem: class {},
29+
}));
30+
31+
const AGENT = { name: "main" } as WorkspaceAgent;
32+
const FOLDER = "/home/foo/project";
33+
const CURSOR = "ssh-remote+coder-cursor.dev.coder.com--foo--bar.main";
34+
const LEGACY = "ssh-remote+coder-vscode.dev.coder.com--foo--bar.main";
35+
const DEVIN = "ssh-remote+coder-devin.dev.coder.com--foo--bar.main";
36+
37+
/**
38+
* Open the workspace from the sidebar, with the given authorities standing in
39+
* for recently opened folders, and report the authority the window was handed.
40+
*/
41+
async function openFromSidebar(recents: string[]): Promise<string | undefined> {
42+
new MockConfigurationProvider();
43+
const workspaces = recents.map((authority) => ({
44+
folderUri: vscode.Uri.from({
45+
scheme: "vscode-remote",
46+
authority,
47+
path: FOLDER,
48+
}),
49+
}));
50+
const executeCommand = vi
51+
.mocked(vscode.commands.executeCommand)
52+
.mockImplementation((command: string) =>
53+
Promise.resolve(
54+
command === "_workbench.getRecentlyOpened" ? { workspaces } : undefined,
55+
),
56+
);
57+
58+
// The constructor reads every service, so name only the ones in play.
59+
const services: Record<string, unknown> = {
60+
getTelemetryService: createTestTelemetryService(),
61+
getLogger: createMockLogger(),
62+
getMementoManager: { setStartupMode: vi.fn() },
63+
getDuplicateWorkspaceIpc: {
64+
sendPing: vi.fn().mockResolvedValue(undefined),
65+
},
66+
};
67+
const commands = new Commands(
68+
new Proxy({} as ServiceContainer, {
69+
get: (_, name: string) => () => services[name] ?? {},
70+
}),
71+
{
72+
getAxiosInstance: () => ({
73+
defaults: { baseURL: "https://dev.coder.com" },
74+
}),
75+
} as unknown as CoderApi,
76+
{} as DeploymentManager,
77+
);
78+
const { AgentTreeItem } = await import("@/workspace/workspacesProvider");
79+
await commands.openFromSidebar(
80+
new AgentTreeItem(
81+
AGENT,
82+
createWorkspace({ owner_name: "foo", name: "bar" }),
83+
),
84+
);
85+
86+
// A folder is handed off by URI, an empty window by option.
87+
const [, handoff] =
88+
executeCommand.mock.calls.find(([command]) =>
89+
["vscode.openFolder", "vscode.newWindow"].includes(command),
90+
) ?? [];
91+
return handoff instanceof vscode.Uri
92+
? handoff.authority
93+
: (handoff as { remoteAuthority?: string } | undefined)?.remoteAuthority;
94+
}
95+
96+
describe("openWorkspace", () => {
97+
beforeEach(() => {
98+
vi.clearAllMocks();
99+
useEditor("cursor");
100+
});
101+
102+
interface RecentCase {
103+
label: string;
104+
recents: string[];
105+
expected: string;
106+
}
107+
it.each<RecentCase>([
108+
{ label: "the legacy host it used", recents: [LEGACY], expected: LEGACY },
109+
{ label: "its own host", recents: [CURSOR], expected: CURSOR },
110+
{ label: "its own host with no history", recents: [], expected: CURSOR },
111+
{
112+
label: "its own host, ignoring another editor's",
113+
recents: [DEVIN],
114+
expected: CURSOR,
115+
},
116+
])("reopens the workspace on $label", async ({ recents, expected }) => {
117+
expect(await openFromSidebar(recents)).toBe(expected);
118+
});
119+
});

0 commit comments

Comments
 (0)