diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts
index f600a05028..ac356c6a3d 100644
--- a/apps/server/src/open.test.ts
+++ b/apps/server/src/open.test.ts
@@ -36,6 +36,16 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
args: ["/tmp/workspace"],
});
+ const windsurfLaunch = yield* resolveEditorLaunch(
+ { cwd: "/tmp/workspace", editor: "windsurf" },
+ "darwin",
+ { PATH: "" },
+ );
+ assert.deepEqual(windsurfLaunch, {
+ command: "windsurf",
+ args: ["/tmp/workspace"],
+ });
+
const traeLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "trae" },
"darwin",
@@ -225,6 +235,16 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
});
+ const windsurfLineAndColumn = yield* resolveEditorLaunch(
+ { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "windsurf" },
+ "darwin",
+ { PATH: "" },
+ );
+ assert.deepEqual(windsurfLineAndColumn, {
+ command: "windsurf",
+ args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
+ });
+
const traeLineAndColumn = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "trae" },
"darwin",
@@ -411,6 +431,29 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
}),
);
+ it.effect("falls back to surf when windsurf is not installed", () =>
+ Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ const path = yield* Path.Path;
+ const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-open-test-" });
+ yield* fs.writeFileString(path.join(dir, "surf"), "#!/bin/sh\nexit 0\n");
+ yield* fs.chmod(path.join(dir, "surf"), 0o755);
+
+ const result = yield* resolveEditorLaunch(
+ { cwd: "/tmp/workspace", editor: "windsurf" },
+ "linux",
+ {
+ PATH: dir,
+ },
+ );
+
+ assert.deepEqual(result, {
+ command: "surf",
+ args: ["/tmp/workspace"],
+ });
+ }),
+ );
+
it.effect("falls back to zeditor when zed is not installed", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
@@ -576,6 +619,7 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
yield* fs.writeFileString(path.join(dir, "trae.CMD"), "@echo off\r\n");
yield* fs.writeFileString(path.join(dir, "kiro.CMD"), "@echo off\r\n");
+ yield* fs.writeFileString(path.join(dir, "windsurf.CMD"), "@echo off\r\n");
yield* fs.writeFileString(path.join(dir, "code-insiders.CMD"), "@echo off\r\n");
yield* fs.writeFileString(path.join(dir, "codium.CMD"), "@echo off\r\n");
yield* fs.writeFileString(path.join(dir, "aqua.CMD"), "@echo off\r\n");
@@ -595,6 +639,7 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
PATHEXT: ".COM;.EXE;.BAT;.CMD",
});
assert.deepEqual(editors, [
+ "windsurf",
"trae",
"kiro",
"vscode-insiders",
diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx
index bb62d8edbc..b600006eb3 100644
--- a/apps/web/src/components/ChatView.browser.tsx
+++ b/apps/web/src/components/ChatView.browser.tsx
@@ -2102,6 +2102,55 @@ describe("ChatView timeline estimator parity (full app)", () => {
}
});
+ it("shows Windsurf in the open picker menu and opens the project cwd with it", async () => {
+ setDraftThreadWithoutWorktree();
+
+ const mounted = await mountChatView({
+ viewport: DEFAULT_VIEWPORT,
+ snapshot: createDraftOnlySnapshot(),
+ configureFixture: (nextFixture) => {
+ nextFixture.serverConfig = {
+ ...nextFixture.serverConfig,
+ availableEditors: ["windsurf"],
+ };
+ },
+ });
+
+ try {
+ await waitForServerConfigToApply();
+ const menuButton = await waitForElement(
+ () => document.querySelector('button[aria-label="Copy options"]'),
+ "Unable to find Open picker button.",
+ );
+ (menuButton as HTMLButtonElement).click();
+
+ const windsurfItem = await waitForElement(
+ () =>
+ Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) =>
+ item.textContent?.includes("Windsurf"),
+ ) ?? null,
+ "Unable to find Windsurf menu item.",
+ );
+ (windsurfItem as HTMLElement).click();
+
+ await vi.waitFor(
+ () => {
+ const openRequest = wsRequests.find(
+ (request) => request._tag === WS_METHODS.shellOpenInEditor,
+ );
+ expect(openRequest).toMatchObject({
+ _tag: WS_METHODS.shellOpenInEditor,
+ cwd: "/repo/project",
+ editor: "windsurf",
+ });
+ },
+ { timeout: 8_000, interval: 16 },
+ );
+ } finally {
+ await mounted.cleanup();
+ }
+ });
+
it("filters the open picker menu and opens VSCodium from the menu", async () => {
setDraftThreadWithoutWorktree();
diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx
index b3211e1775..06b2a9b48f 100644
--- a/apps/web/src/components/Icons.tsx
+++ b/apps/web/src/components/Icons.tsx
@@ -199,6 +199,15 @@ export const CursorIcon: Icon = ({ className, ...props }) => (
);
+export const WindsurfIcon: Icon = (props) => (
+
+);
+
export const TraeIcon: Icon = (props) => (