| author | |
| committer | |
| log | 648f0676bde993ae64e57d2e2ccd2e797803de32 |
| tree | e9299cf7ec4518b16bf5cc1c25d4c11b7b123b08 |
| parent | add5f34619b65c3283317d8c77e1feef6fc63267 |
| signature | Signed by PGP key B5690EEEBB952194 |
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) => { |
| 40 | 40 | args: ["/tmp/workspace"], |
| 41 | 41 | }); |
| 42 | 42 | |
| 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 | ||
| 43 | 61 | const zedLaunch = yield* resolveEditorLaunch( |
| 44 | 62 | { cwd: "/tmp/workspace", editor: "zed" }, |
| 45 | 63 | "darwin", |
| ... | ... | @@ -80,6 +98,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { |
| 80 | 98 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], |
| 81 | 99 | }); |
| 82 | 100 | |
| 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 | ||
| 83 | 119 | const zedLineAndColumn = yield* resolveEditorLaunch( |
| 84 | 120 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" }, |
| 85 | 121 | "darwin", |
| ... | ... | @@ -220,13 +256,14 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { |
| 220 | 256 | const path = yield* Path.Path; |
| 221 | 257 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" }); |
| 222 | 258 | |
| 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"); | |
| 224 | 261 | yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ"); |
| 225 | 262 | const editors = resolveAvailableEditors("win32", { |
| 226 | 263 | PATH: dir, |
| 227 | 264 | PATHEXT: ".COM;.EXE;.BAT;.CMD", |
| 228 | 265 | }); |
| 229 | assert.deepEqual(editors, ["cursor", "file-manager"]); | |
| 266 | assert.deepEqual(editors, ["vscode-insiders", "vscodium", "file-manager"]); | |
| 230 | 267 | }), |
| 231 | 268 | ); |
| 232 | 269 | }); |
apps/server/src/open.ts+3-5| ... | ... | @@ -39,10 +39,8 @@ interface CommandAvailabilityOptions { |
| 39 | 39 | |
| 40 | 40 | const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; |
| 41 | 41 | |
| 42 | function shouldUseGotoFlag(editorId: EditorId, target: string): boolean { | |
| 43 | return ( | |
| 44 | (editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target) | |
| 45 | ); | |
| 42 | function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { | |
| 43 | return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); | |
| 46 | 44 | } |
| 47 | 45 | |
| 48 | 46 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { |
| ... | ... | @@ -213,7 +211,7 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 213 | 211 | } |
| 214 | 212 | |
| 215 | 213 | if (editorDef.command) { |
| 216 | return shouldUseGotoFlag(editorDef.id, input.cwd) | |
| 214 | return shouldUseGotoFlag(editorDef, input.cwd) | |
| 217 | 215 | ? { command: editorDef.command, args: ["--goto", input.cwd] } |
| 218 | 216 | : { command: editorDef.command, args: [input.cwd] }; |
| 219 | 217 | } |
apps/web/src/components/ChatView.browser.tsx+165-14| ... | ... | @@ -356,6 +356,25 @@ function withProjectScripts( |
| 356 | 356 | }; |
| 357 | 357 | } |
| 358 | 358 | |
| 359 | function 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 | ||
| 359 | 378 | function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { |
| 360 | 379 | const snapshot = createSnapshotForTargetUser({ |
| 361 | 380 | targetMessageId: "msg-user-plan-target" as MessageId, |
| ... | ... | @@ -1011,30 +1030,162 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 1011 | 1030 | ); |
| 1012 | 1031 | |
| 1013 | 1032 | 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 | }); | |
| 1024 | 1066 | }, |
| 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 | }; | |
| 1025 | 1085 | }, |
| 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 | }; | |
| 1028 | 1127 | }, |
| 1029 | 1128 | }); |
| 1030 | 1129 | |
| 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 | ||
| 1031 | 1182 | const mounted = await mountChatView({ |
| 1032 | 1183 | viewport: DEFAULT_VIEWPORT, |
| 1033 | 1184 | snapshot: createDraftOnlySnapshot(), |
| 1034 | 1185 | configureFixture: (nextFixture) => { |
| 1035 | 1186 | nextFixture.serverConfig = { |
| 1036 | 1187 | ...nextFixture.serverConfig, |
| 1037 | availableEditors: ["vscode"], | |
| 1188 | availableEditors: ["vscode-insiders"], | |
| 1038 | 1189 | }; |
| 1039 | 1190 | }, |
| 1040 | 1191 | }); |
| ... | ... | @@ -1057,7 +1208,7 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 1057 | 1208 | expect(openRequest).toMatchObject({ |
| 1058 | 1209 | _tag: WS_METHODS.shellOpenInEditor, |
| 1059 | 1210 | cwd: "/repo/project", |
| 1060 | editor: "vscode", | |
| 1211 | editor: "vscode-insiders", | |
| 1061 | 1212 | }); |
| 1062 | 1213 | }, |
| 1063 | 1214 | { timeout: 8_000, interval: 16 }, |
apps/web/src/components/chat/OpenInPicker.tsx+10| ... | ... | @@ -22,6 +22,16 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor |
| 22 | 22 | Icon: VisualStudioCode, |
| 23 | 23 | value: "vscode", |
| 24 | 24 | }, |
| 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 | }, | |
| 25 | 35 | { |
| 26 | 36 | label: "Zed", |
| 27 | 37 | Icon: Zed, |
packages/contracts/src/editor.ts+12-5| ... | ... | @@ -2,11 +2,18 @@ import { Schema } from "effect"; |
| 2 | 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; |
| 3 | 3 | |
| 4 | 4 | export 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 }, | |
| 10 | 17 | ] as const; |
| 11 | 18 | |
| 12 | 19 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); |