authorgravatar for najjarnassim1995@gmail.comNassim Najjar <najjarnassim1995@gmail.com> 2026-03-27 05:46:43+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-27 04:46:43+00:00
log648f0676bde993ae64e57d2e2ccd2e797803de32
treee9299cf7ec4518b16bf5cc1c25d4c11b7b123b08
parentadd5f34619b65c3283317d8c77e1feef6fc63267
signature Signed by PGP key B5690EEEBB952194

Add VS Code Insiders and VSCodium to Open In editor picker (#1392)


5 files changed, 229 insertions(+), 26 deletions(-)

apps/server/src/open.test.ts+39-2
......@@ -40,6 +40,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
4040 args: ["/tmp/workspace"],
4141 });
4242
43 const vscodeInsidersLaunch = yield* resolveEditorLaunch(
44 { cwd: "/tmp/workspace", editor: "vscode-insiders" },
45 "darwin",
46 );
47 assert.deepEqual(vscodeInsidersLaunch, {
48 command: "code-insiders",
49 args: ["/tmp/workspace"],
50 });
51
52 const vscodiumLaunch = yield* resolveEditorLaunch(
53 { cwd: "/tmp/workspace", editor: "vscodium" },
54 "darwin",
55 );
56 assert.deepEqual(vscodiumLaunch, {
57 command: "codium",
58 args: ["/tmp/workspace"],
59 });
60
4361 const zedLaunch = yield* resolveEditorLaunch(
4462 { cwd: "/tmp/workspace", editor: "zed" },
4563 "darwin",
......@@ -80,6 +98,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
8098 args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
8199 });
82100
101 const vscodeInsidersLineAndColumn = yield* resolveEditorLaunch(
102 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode-insiders" },
103 "darwin",
104 );
105 assert.deepEqual(vscodeInsidersLineAndColumn, {
106 command: "code-insiders",
107 args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
108 });
109
110 const vscodiumLineAndColumn = yield* resolveEditorLaunch(
111 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscodium" },
112 "darwin",
113 );
114 assert.deepEqual(vscodiumLineAndColumn, {
115 command: "codium",
116 args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
117 });
118
83119 const zedLineAndColumn = yield* resolveEditorLaunch(
84120 { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" },
85121 "darwin",
......@@ -220,13 +256,14 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
220256 const path = yield* Path.Path;
221257 const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });
222258
223 yield* fs.writeFileString(path.join(dir, "cursor.CMD"), "@echo off\r\n");
259 yield* fs.writeFileString(path.join(dir, "code-insiders.CMD"), "@echo off\r\n");
260 yield* fs.writeFileString(path.join(dir, "codium.CMD"), "@echo off\r\n");
224261 yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ");
225262 const editors = resolveAvailableEditors("win32", {
226263 PATH: dir,
227264 PATHEXT: ".COM;.EXE;.BAT;.CMD",
228265 });
229 assert.deepEqual(editors, ["cursor", "file-manager"]);
266 assert.deepEqual(editors, ["vscode-insiders", "vscodium", "file-manager"]);
230267 }),
231268 );
232269});
apps/server/src/open.ts+3-5
......@@ -39,10 +39,8 @@ interface CommandAvailabilityOptions {
3939
4040const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/;
4141
42function shouldUseGotoFlag(editorId: EditorId, target: string): boolean {
43 return (
44 (editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target)
45 );
42function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean {
43 return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target);
4644}
4745
4846function fileManagerCommandForPlatform(platform: NodeJS.Platform): string {
......@@ -213,7 +211,7 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* (
213211 }
214212
215213 if (editorDef.command) {
216 return shouldUseGotoFlag(editorDef.id, input.cwd)
214 return shouldUseGotoFlag(editorDef, input.cwd)
217215 ? { command: editorDef.command, args: ["--goto", input.cwd] }
218216 : { command: editorDef.command, args: [input.cwd] };
219217 }
apps/web/src/components/ChatView.browser.tsx+165-14
......@@ -356,6 +356,25 @@ function withProjectScripts(
356356 };
357357}
358358
359function setDraftThreadWithoutWorktree(): void {
360 useComposerDraftStore.setState({
361 draftThreadsByThreadId: {
362 [THREAD_ID]: {
363 projectId: PROJECT_ID,
364 createdAt: NOW_ISO,
365 runtimeMode: "full-access",
366 interactionMode: "default",
367 branch: null,
368 worktreePath: null,
369 envMode: "local",
370 },
371 },
372 projectDraftThreadIdByProjectId: {
373 [PROJECT_ID]: THREAD_ID,
374 },
375 });
376}
377
359378function createSnapshotWithLongProposedPlan(): OrchestrationReadModel {
360379 const snapshot = createSnapshotForTargetUser({
361380 targetMessageId: "msg-user-plan-target" as MessageId,
......@@ -1011,30 +1030,162 @@ describe("ChatView timeline estimator parity (full app)", () => {
10111030 );
10121031
10131032 it("opens the project cwd for draft threads without a worktree path", async () => {
1014 useComposerDraftStore.setState({
1015 draftThreadsByThreadId: {
1016 [THREAD_ID]: {
1017 projectId: PROJECT_ID,
1018 createdAt: NOW_ISO,
1019 runtimeMode: "full-access",
1020 interactionMode: "default",
1021 branch: null,
1022 worktreePath: null,
1023 envMode: "local",
1033 setDraftThreadWithoutWorktree();
1034
1035 const mounted = await mountChatView({
1036 viewport: DEFAULT_VIEWPORT,
1037 snapshot: createDraftOnlySnapshot(),
1038 configureFixture: (nextFixture) => {
1039 nextFixture.serverConfig = {
1040 ...nextFixture.serverConfig,
1041 availableEditors: ["vscode"],
1042 };
1043 },
1044 });
1045
1046 try {
1047 const openButton = await waitForElement(
1048 () =>
1049 Array.from(document.querySelectorAll("button")).find(
1050 (button) => button.textContent?.trim() === "Open",
1051 ) as HTMLButtonElement | null,
1052 "Unable to find Open button.",
1053 );
1054 openButton.click();
1055
1056 await vi.waitFor(
1057 () => {
1058 const openRequest = wsRequests.find(
1059 (request) => request._tag === WS_METHODS.shellOpenInEditor,
1060 );
1061 expect(openRequest).toMatchObject({
1062 _tag: WS_METHODS.shellOpenInEditor,
1063 cwd: "/repo/project",
1064 editor: "vscode",
1065 });
10241066 },
1067 { timeout: 8_000, interval: 16 },
1068 );
1069 } finally {
1070 await mounted.cleanup();
1071 }
1072 });
1073
1074 it("opens the project cwd with VS Code Insiders when it is the only available editor", async () => {
1075 setDraftThreadWithoutWorktree();
1076
1077 const mounted = await mountChatView({
1078 viewport: DEFAULT_VIEWPORT,
1079 snapshot: createDraftOnlySnapshot(),
1080 configureFixture: (nextFixture) => {
1081 nextFixture.serverConfig = {
1082 ...nextFixture.serverConfig,
1083 availableEditors: ["vscode-insiders"],
1084 };
10251085 },
1026 projectDraftThreadIdByProjectId: {
1027 [PROJECT_ID]: THREAD_ID,
1086 });
1087
1088 try {
1089 const openButton = await waitForElement(
1090 () =>
1091 Array.from(document.querySelectorAll("button")).find(
1092 (button) => button.textContent?.trim() === "Open",
1093 ) as HTMLButtonElement | null,
1094 "Unable to find Open button.",
1095 );
1096 openButton.click();
1097
1098 await vi.waitFor(
1099 () => {
1100 const openRequest = wsRequests.find(
1101 (request) => request._tag === WS_METHODS.shellOpenInEditor,
1102 );
1103 expect(openRequest).toMatchObject({
1104 _tag: WS_METHODS.shellOpenInEditor,
1105 cwd: "/repo/project",
1106 editor: "vscode-insiders",
1107 });
1108 },
1109 { timeout: 8_000, interval: 16 },
1110 );
1111 } finally {
1112 await mounted.cleanup();
1113 }
1114 });
1115
1116 it("filters the open picker menu and opens VSCodium from the menu", async () => {
1117 setDraftThreadWithoutWorktree();
1118
1119 const mounted = await mountChatView({
1120 viewport: DEFAULT_VIEWPORT,
1121 snapshot: createDraftOnlySnapshot(),
1122 configureFixture: (nextFixture) => {
1123 nextFixture.serverConfig = {
1124 ...nextFixture.serverConfig,
1125 availableEditors: ["vscode-insiders", "vscodium"],
1126 };
10281127 },
10291128 });
10301129
1130 try {
1131 const menuButton = await waitForElement(
1132 () => document.querySelector('button[aria-label="Copy options"]'),
1133 "Unable to find Open picker button.",
1134 );
1135 (menuButton as HTMLButtonElement).click();
1136
1137 await waitForElement(
1138 () =>
1139 Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) =>
1140 item.textContent?.includes("VS Code Insiders"),
1141 ) ?? null,
1142 "Unable to find VS Code Insiders menu item.",
1143 );
1144
1145 expect(
1146 Array.from(document.querySelectorAll('[data-slot="menu-item"]')).some((item) =>
1147 item.textContent?.includes("Zed"),
1148 ),
1149 ).toBe(false);
1150
1151 const vscodiumItem = await waitForElement(
1152 () =>
1153 Array.from(document.querySelectorAll('[data-slot="menu-item"]')).find((item) =>
1154 item.textContent?.includes("VSCodium"),
1155 ) ?? null,
1156 "Unable to find VSCodium menu item.",
1157 );
1158 (vscodiumItem as HTMLElement).click();
1159
1160 await vi.waitFor(
1161 () => {
1162 const openRequest = wsRequests.find(
1163 (request) => request._tag === WS_METHODS.shellOpenInEditor,
1164 );
1165 expect(openRequest).toMatchObject({
1166 _tag: WS_METHODS.shellOpenInEditor,
1167 cwd: "/repo/project",
1168 editor: "vscodium",
1169 });
1170 },
1171 { timeout: 8_000, interval: 16 },
1172 );
1173 } finally {
1174 await mounted.cleanup();
1175 }
1176 });
1177
1178 it("falls back to the first installed editor when the stored favorite is unavailable", async () => {
1179 localStorage.setItem("t3code:last-editor", "vscodium");
1180 setDraftThreadWithoutWorktree();
1181
10311182 const mounted = await mountChatView({
10321183 viewport: DEFAULT_VIEWPORT,
10331184 snapshot: createDraftOnlySnapshot(),
10341185 configureFixture: (nextFixture) => {
10351186 nextFixture.serverConfig = {
10361187 ...nextFixture.serverConfig,
1037 availableEditors: ["vscode"],
1188 availableEditors: ["vscode-insiders"],
10381189 };
10391190 },
10401191 });
......@@ -1057,7 +1208,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
10571208 expect(openRequest).toMatchObject({
10581209 _tag: WS_METHODS.shellOpenInEditor,
10591210 cwd: "/repo/project",
1060 editor: "vscode",
1211 editor: "vscode-insiders",
10611212 });
10621213 },
10631214 { timeout: 8_000, interval: 16 },
apps/web/src/components/chat/OpenInPicker.tsx+10
......@@ -22,6 +22,16 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor
2222 Icon: VisualStudioCode,
2323 value: "vscode",
2424 },
25 {
26 label: "VS Code Insiders",
27 Icon: VisualStudioCode,
28 value: "vscode-insiders",
29 },
30 {
31 label: "VSCodium",
32 Icon: VisualStudioCode,
33 value: "vscodium",
34 },
2535 {
2636 label: "Zed",
2737 Icon: Zed,
packages/contracts/src/editor.ts+12-5
......@@ -2,11 +2,18 @@ import { Schema } from "effect";
22import { TrimmedNonEmptyString } from "./baseSchemas";
33
44export const EDITORS = [
5 { id: "cursor", label: "Cursor", command: "cursor" },
6 { id: "vscode", label: "VS Code", command: "code" },
7 { id: "zed", label: "Zed", command: "zed" },
8 { id: "antigravity", label: "Antigravity", command: "agy" },
9 { id: "file-manager", label: "File Manager", command: null },
5 { id: "cursor", label: "Cursor", command: "cursor", supportsGoto: true },
6 { id: "vscode", label: "VS Code", command: "code", supportsGoto: true },
7 {
8 id: "vscode-insiders",
9 label: "VS Code Insiders",
10 command: "code-insiders",
11 supportsGoto: true,
12 },
13 { id: "vscodium", label: "VSCodium", command: "codium", supportsGoto: true },
14 { id: "zed", label: "Zed", command: "zed", supportsGoto: false },
15 { id: "antigravity", label: "Antigravity", command: "agy", supportsGoto: false },
16 { id: "file-manager", label: "File Manager", command: null, supportsGoto: false },
1017] as const;
1118
1219export const EditorId = Schema.Literals(EDITORS.map((e) => e.id));