From e3e3b1110e6bf5da37753c3e456caa12193d6445 Mon Sep 17 00:00:00 2001 From: cenumi Date: Thu, 2 Apr 2026 10:01:24 +0800 Subject: [PATCH 1/2] feat: support IntelliJ IDEA open-in launch Add editor-specific launch styles for direct path, --goto, and line/column arguments. Expose IntelliJ IDEA in the open-in picker and cover the new launch behavior with server tests --- apps/server/src/open.test.ts | 38 ++++++- apps/server/src/open.ts | 48 +++++++-- apps/web/src/components/Icons.tsx | 101 ++++++++++++++++++ apps/web/src/components/chat/OpenInPicker.tsx | 15 ++- packages/contracts/src/editor.ts | 20 ++-- 5 files changed, 206 insertions(+), 16 deletions(-) diff --git a/apps/server/src/open.test.ts b/apps/server/src/open.test.ts index c612922fea..76b14c8597 100644 --- a/apps/server/src/open.test.ts +++ b/apps/server/src/open.test.ts @@ -75,10 +75,19 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { command: "zed", args: ["/tmp/workspace"], }); + + const ideaLaunch = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace", editor: "idea" }, + "darwin", + ); + assert.deepEqual(ideaLaunch, { + command: "idea", + args: ["/tmp/workspace"], + }); }), ); - it.effect("uses --goto when editor supports line/column suffixes", () => + it.effect("applies launch-style-specific navigation arguments", () => Effect.gen(function* () { const lineOnly = yield* resolveEditorLaunch( { cwd: "/tmp/workspace/AGENTS.md:48", editor: "cursor" }, @@ -142,6 +151,33 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => { command: "zed", args: ["/tmp/workspace/src/open.ts:71:5"], }); + + const zedLineOnly = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace/AGENTS.md:48", editor: "zed" }, + "darwin", + ); + assert.deepEqual(zedLineOnly, { + command: "zed", + args: ["/tmp/workspace/AGENTS.md:48"], + }); + + const ideaLineOnly = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace/AGENTS.md:48", editor: "idea" }, + "darwin", + ); + assert.deepEqual(ideaLineOnly, { + command: "idea", + args: ["--line", "48", "/tmp/workspace/AGENTS.md"], + }); + + const ideaLineAndColumn = yield* resolveEditorLaunch( + { cwd: "/tmp/workspace/src/open.ts:71:5", editor: "idea" }, + "darwin", + ); + assert.deepEqual(ideaLineAndColumn, { + command: "idea", + args: ["--line", "71", "--column", "5", "/tmp/workspace/src/open.ts"], + }); }), ); diff --git a/apps/server/src/open.ts b/apps/server/src/open.ts index 212db36c01..cdacda7c09 100644 --- a/apps/server/src/open.ts +++ b/apps/server/src/open.ts @@ -34,10 +34,45 @@ interface CommandAvailabilityOptions { readonly env?: NodeJS.ProcessEnv; } -const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; +const TARGET_WITH_POSITION_PATTERN = /^(.*?):(\d+)(?::(\d+))?$/; + +function parseTargetPathAndPosition(target: string): { + path: string; + line: string | undefined; + column: string | undefined; +} | null { + const match = TARGET_WITH_POSITION_PATTERN.exec(target); + if (!match?.[1] || !match[2]) { + return null; + } -function shouldUseGotoFlag(editor: (typeof EDITORS)[number], target: string): boolean { - return editor.supportsGoto && LINE_COLUMN_SUFFIX_PATTERN.test(target); + return { + path: match[1], + line: match[2], + column: match[3], + }; +} + +function resolveCommandEditorArgs( + editor: (typeof EDITORS)[number], + target: string, +): ReadonlyArray { + const parsedTarget = parseTargetPathAndPosition(target); + + switch (editor.launchStyle) { + case "direct-path": + return [target]; + case "goto": + return parsedTarget ? ["--goto", target] : [target]; + case "line-column": { + if (!parsedTarget) { + return [target]; + } + + const { path, line, column } = parsedTarget; + return [...(line ? ["--line", line] : []), ...(column ? ["--column", column] : []), path]; + } + } } function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { @@ -208,9 +243,10 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( } if (editorDef.command) { - return shouldUseGotoFlag(editorDef, input.cwd) - ? { command: editorDef.command, args: ["--goto", input.cwd] } - : { command: editorDef.command, args: [input.cwd] }; + return { + command: editorDef.command, + args: resolveCommandEditorArgs(editorDef, input.cwd), + }; } if (editorDef.id !== "file-manager") { diff --git a/apps/web/src/components/Icons.tsx b/apps/web/src/components/Icons.tsx index 3f4844af80..2e95b54e25 100644 --- a/apps/web/src/components/Icons.tsx +++ b/apps/web/src/components/Icons.tsx @@ -310,6 +310,107 @@ export const AntigravityIcon: Icon = (props) => ( ); +export const IntelliJIdeaIcon: Icon = (props) => { + const id = useId(); + const gradientAId = `${id}-idea-a`; + const gradientBId = `${id}-idea-b`; + const gradientCId = `${id}-idea-c`; + const gradientDId = `${id}-idea-d`; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + export const OpenCodeIcon: Icon = (props) => ( diff --git a/apps/web/src/components/chat/OpenInPicker.tsx b/apps/web/src/components/chat/OpenInPicker.tsx index bb5362439e..703bfadaa3 100644 --- a/apps/web/src/components/chat/OpenInPicker.tsx +++ b/apps/web/src/components/chat/OpenInPicker.tsx @@ -6,7 +6,15 @@ 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, TraeIcon, VisualStudioCode, Zed } from "../Icons"; +import { + AntigravityIcon, + CursorIcon, + Icon, + TraeIcon, + IntelliJIdeaIcon, + VisualStudioCode, + Zed, +} from "../Icons"; import { isMacPlatform, isWindowsPlatform } from "~/lib/utils"; import { readNativeApi } from "~/nativeApi"; @@ -47,6 +55,11 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray e.id)); From 648e20452cb3501e63ff7b927d39ff057d5097ae Mon Sep 17 00:00:00 2001 From: cenumi Date: Thu, 2 Apr 2026 10:39:24 +0800 Subject: [PATCH 2/2] fix: add EditorDefinition type and apply it to the EDITORS constant for type safety --- packages/contracts/src/editor.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/contracts/src/editor.ts b/packages/contracts/src/editor.ts index 57713dc912..4ecaa72c49 100644 --- a/packages/contracts/src/editor.ts +++ b/packages/contracts/src/editor.ts @@ -4,6 +4,13 @@ import { TrimmedNonEmptyString } from "./baseSchemas"; export const EditorLaunchStyle = Schema.Literals(["direct-path", "goto", "line-column"]); export type EditorLaunchStyle = typeof EditorLaunchStyle.Type; +type EditorDefinition = { + readonly id: string; + readonly label: string; + readonly command: string | null; + readonly launchStyle: EditorLaunchStyle; +}; + export const EDITORS = [ { id: "cursor", label: "Cursor", command: "cursor", launchStyle: "goto" }, { id: "trae", label: "Trae", command: "trae", launchStyle: "goto" }, @@ -19,7 +26,7 @@ export const EDITORS = [ { id: "antigravity", label: "Antigravity", command: "agy", launchStyle: "goto" }, { id: "idea", label: "IntelliJ IDEA", command: "idea", launchStyle: "line-column" }, { id: "file-manager", label: "File Manager", command: null, launchStyle: "direct-path" }, -] as const; +] as const satisfies ReadonlyArray; export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); export type EditorId = typeof EditorId.Type;