From 7a0477e1e3c6dd8bc07383d4be14a87188142758 Mon Sep 17 00:00:00 2001
From: Lars Nieuwenhuis <35393046+lnieuwenhuis@users.noreply.github.com>
Date: Wed, 1 Apr 2026 15:43:22 +0200
Subject: [PATCH] Feat: Added Trae IDE support
---
apps/server/src/open.test.ts | 21 ++++++++-
apps/web/src/components/ChatView.browser.tsx | 46 +++++++++++++++++++
apps/web/src/components/Icons.tsx | 14 ++++++
apps/web/src/components/chat/OpenInPicker.tsx | 7 ++-
packages/contracts/src/editor.ts | 1 +
5 files changed, 87 insertions(+), 2 deletions(-)
diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts
index 947a60ac2a..c612922fea 100644
--- a/apps/server/src/open.test.ts
+++ b/apps/server/src/open.test.ts
@@ -31,6 +31,15 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
args: ["/tmp/workspace"],
});
+ const traeLaunch = yield* resolveEditorLaunch(
+ { cwd: "/tmp/workspace", editor: "trae" },
+ "darwin",
+ );
+ assert.deepEqual(traeLaunch, {
+ command: "trae",
+ args: ["/tmp/workspace"],
+ });
+
const vscodeLaunch = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace", editor: "vscode" },
"darwin",
@@ -89,6 +98,15 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
});
+ const traeLineAndColumn = yield* resolveEditorLaunch(
+ { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "trae" },
+ "darwin",
+ );
+ assert.deepEqual(traeLineAndColumn, {
+ command: "trae",
+ args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
+ });
+
const vscodeLineAndColumn = yield* resolveEditorLaunch(
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode" },
"darwin",
@@ -256,6 +274,7 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });
+ yield* fs.writeFileString(path.join(dir, "trae.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, "explorer.CMD"), "MZ");
@@ -263,7 +282,7 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
PATH: dir,
PATHEXT: ".COM;.EXE;.BAT;.CMD",
});
- assert.deepEqual(editors, ["vscode-insiders", "vscodium", "file-manager"]);
+ assert.deepEqual(editors, ["trae", "vscode-insiders", "vscodium", "file-manager"]);
}),
);
});
diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx
index 33b9b62859..0e5f573d54 100644
--- a/apps/web/src/components/ChatView.browser.tsx
+++ b/apps/web/src/components/ChatView.browser.tsx
@@ -1407,6 +1407,52 @@ describe("ChatView timeline estimator parity (full app)", () => {
}
});
+ it("opens the project cwd with Trae when it is the only available editor", async () => {
+ setDraftThreadWithoutWorktree();
+
+ const mounted = await mountChatView({
+ viewport: DEFAULT_VIEWPORT,
+ snapshot: createDraftOnlySnapshot(),
+ configureFixture: (nextFixture) => {
+ nextFixture.serverConfig = {
+ ...nextFixture.serverConfig,
+ availableEditors: ["trae"],
+ };
+ },
+ });
+
+ try {
+ await waitForServerConfigToApply();
+ const openButton = await waitForElement(
+ () =>
+ Array.from(document.querySelectorAll("button")).find(
+ (button) => button.textContent?.trim() === "Open",
+ ) as HTMLButtonElement | null,
+ "Unable to find Open button.",
+ );
+ await vi.waitFor(() => {
+ expect(openButton.disabled).toBe(false);
+ });
+ openButton.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: "trae",
+ });
+ },
+ { 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 7d210fa173..3f4844af80 100644
--- a/apps/web/src/components/Icons.tsx
+++ b/apps/web/src/components/Icons.tsx
@@ -20,6 +20,20 @@ export const CursorIcon: Icon = (props) => (
);
+export const TraeIcon: Icon = (props) => (
+
+);
+
export const VisualStudioCode: Icon = (props) => {
const id = useId();
const maskId = `${id}-vscode-a`;
diff --git a/apps/web/src/components/chat/OpenInPicker.tsx b/apps/web/src/components/chat/OpenInPicker.tsx
index 6a956f6f42..bb5362439e 100644
--- a/apps/web/src/components/chat/OpenInPicker.tsx
+++ b/apps/web/src/components/chat/OpenInPicker.tsx
@@ -6,7 +6,7 @@ import { ChevronDownIcon, FolderClosedIcon } from "lucide-react";
import { Button } from "../ui/button";
import { Group, GroupSeparator } from "../ui/group";
import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu";
-import { AntigravityIcon, CursorIcon, Icon, VisualStudioCode, Zed } from "../Icons";
+import { AntigravityIcon, CursorIcon, Icon, TraeIcon, VisualStudioCode, Zed } from "../Icons";
import { isMacPlatform, isWindowsPlatform } from "~/lib/utils";
import { readNativeApi } from "~/nativeApi";
@@ -17,6 +17,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray