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) => ( + + {/* Back rectangle: left strip + bottom strip drawn separately — empty bottom-left corner is the gap between them */} + + + {/* Front frame: top bar + right bar only — left and bottom are replaced by the back strips above */} + + + {/* Two diamonds, offset slightly to the right within the open area */} + + + +); + 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