| 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,6 +40,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { |
| 40 | args: ["/tmp/workspace"], | 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 | const zedLaunch = yield* resolveEditorLaunch( | 61 | const zedLaunch = yield* resolveEditorLaunch( |
| 44 | { cwd: "/tmp/workspace", editor: "zed" }, | 62 | { cwd: "/tmp/workspace", editor: "zed" }, |
| 45 | "darwin", | 63 | "darwin", |
| ... | @@ -80,6 +98,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { | ... | @@ -80,6 +98,24 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { |
| 80 | args: ["--goto", "/tmp/workspace/src/open.ts:71:5"], | 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 | const zedLineAndColumn = yield* resolveEditorLaunch( | 119 | const zedLineAndColumn = yield* resolveEditorLaunch( |
| 84 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" }, | 120 | { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "zed" }, |
| 85 | "darwin", | 121 | "darwin", |
| ... | @@ -220,13 +256,14 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { | ... | @@ -220,13 +256,14 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => { |
| 220 | const path = yield* Path.Path; | 256 | const path = yield* Path.Path; |
| 221 | const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" }); | 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 | yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ"); | 261 | yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ"); |
| 225 | const editors = resolveAvailableEditors("win32", { | 262 | const editors = resolveAvailableEditors("win32", { |
| 226 | PATH: dir, | 263 | PATH: dir, |
| 227 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | 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,10 +39,8 @@ interface CommandAvailabilityOptions { |
| 39 | 39 | ||
| 40 | const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; | 40 | const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; |
| 41 | 41 | ||
| 42 | function shouldUseGotoFlag(editorId: EditorId, target: string): boolean { | 42 | function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { |
| 43 | return ( | 43 | return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); |
| 44 | (editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target) | ||
| 45 | ); | ||
| 46 | } | 44 | } |
| 47 | 45 | ||
| 48 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { | 46 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { |
| ... | @@ -213,7 +211,7 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( | ... | @@ -213,7 +211,7 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 213 | } | 211 | } |
| 214 | 212 | ||
| 215 | if (editorDef.command) { | 213 | if (editorDef.command) { |
| 216 | return shouldUseGotoFlag(editorDef.id, input.cwd) | 214 | return shouldUseGotoFlag(editorDef, input.cwd) |
| 217 | ? { command: editorDef.command, args: ["--goto", input.cwd] } | 215 | ? { command: editorDef.command, args: ["--goto", input.cwd] } |
| 218 | : { command: editorDef.command, args: [input.cwd] }; | 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,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 | function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { | 378 | function createSnapshotWithLongProposedPlan(): OrchestrationReadModel { |
| 360 | const snapshot = createSnapshotForTargetUser({ | 379 | const snapshot = createSnapshotForTargetUser({ |
| 361 | targetMessageId: "msg-user-plan-target" as MessageId, | 380 | targetMessageId: "msg-user-plan-target" as MessageId, |
| ... | @@ -1011,30 +1030,162 @@ describe("ChatView timeline estimator parity (full app)", () => { | ... | @@ -1011,30 +1030,162 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 1011 | ); | 1030 | ); |
| 1012 | 1031 | ||
| 1013 | it("opens the project cwd for draft threads without a worktree path", async () => { | 1032 | it("opens the project cwd for draft threads without a worktree path", async () => { |
| 1014 | useComposerDraftStore.setState({ | 1033 | setDraftThreadWithoutWorktree(); |
| 1015 | draftThreadsByThreadId: { | 1034 | |
| 1016 | [THREAD_ID]: { | 1035 | const mounted = await mountChatView({ |
| 1017 | projectId: PROJECT_ID, | 1036 | viewport: DEFAULT_VIEWPORT, |
| 1018 | createdAt: NOW_ISO, | 1037 | snapshot: createDraftOnlySnapshot(), |
| 1019 | runtimeMode: "full-access", | 1038 | configureFixture: (nextFixture) => { |
| 1020 | interactionMode: "default", | 1039 | nextFixture.serverConfig = { |
| 1021 | branch: null, | 1040 | ...nextFixture.serverConfig, |
| 1022 | worktreePath: null, | 1041 | availableEditors: ["vscode"], |
| 1023 | envMode: "local", | 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: { | 1086 | }); |
| 1027 | [PROJECT_ID]: THREAD_ID, | 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 | const mounted = await mountChatView({ | 1182 | const mounted = await mountChatView({ |
| 1032 | viewport: DEFAULT_VIEWPORT, | 1183 | viewport: DEFAULT_VIEWPORT, |
| 1033 | snapshot: createDraftOnlySnapshot(), | 1184 | snapshot: createDraftOnlySnapshot(), |
| 1034 | configureFixture: (nextFixture) => { | 1185 | configureFixture: (nextFixture) => { |
| 1035 | nextFixture.serverConfig = { | 1186 | nextFixture.serverConfig = { |
| 1036 | ...nextFixture.serverConfig, | 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,7 +1208,7 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 1057 | expect(openRequest).toMatchObject({ | 1208 | expect(openRequest).toMatchObject({ |
| 1058 | _tag: WS_METHODS.shellOpenInEditor, | 1209 | _tag: WS_METHODS.shellOpenInEditor, |
| 1059 | cwd: "/repo/project", | 1210 | cwd: "/repo/project", |
| 1060 | editor: "vscode", | 1211 | editor: "vscode-insiders", |
| 1061 | }); | 1212 | }); |
| 1062 | }, | 1213 | }, |
| 1063 | { timeout: 8_000, interval: 16 }, | 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,6 +22,16 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor |
| 22 | Icon: VisualStudioCode, | 22 | Icon: VisualStudioCode, |
| 23 | value: "vscode", | 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 | label: "Zed", | 36 | label: "Zed", |
| 27 | Icon: Zed, | 37 | Icon: Zed, |
packages/contracts/src/editor.ts+12-5| ... | @@ -2,11 +2,18 @@ import { Schema } from "effect"; | ... | @@ -2,11 +2,18 @@ import { Schema } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; |
| 3 | 3 | ||
| 4 | export const EDITORS = [ | 4 | export const EDITORS = [ |
| 5 | { id: "cursor", label: "Cursor", command: "cursor" }, | 5 | { id: "cursor", label: "Cursor", command: "cursor", supportsGoto: true }, |
| 6 | { id: "vscode", label: "VS Code", command: "code" }, | 6 | { id: "vscode", label: "VS Code", command: "code", supportsGoto: true }, |
| 7 | { id: "zed", label: "Zed", command: "zed" }, | 7 | { |
| 8 | { id: "antigravity", label: "Antigravity", command: "agy" }, | 8 | id: "vscode-insiders", |
| 9 | { id: "file-manager", label: "File Manager", command: null }, | 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 | ] as const; | 17 | ] as const; |
| 11 | 18 | ||
| 12 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); | 19 | export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); |