From e31f72bcdad6e3b05c6b5fc215c28288e7964d90 Mon Sep 17 00:00:00 2001 From: eodwyrd <31327456+eodwyrd@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:11:43 +0100 Subject: [PATCH] feat(desktop): add Fork to the Open menu Adds Fork (the Git client) as a target in the workspace Open dropdown so you can jump from a workspace straight into Fork with the repo open. Fork is a repository GUI, so it always opens the workspace root and ignores any file path. It launches through the existing openMacApplication runtime helper (open -a Fork ) - the same mechanism Cursor/Zed use when their CLI is absent - so there are no runtime changes, and it only appears when Fork.app is installed. Bundles a 64x64 Fork icon alongside the other editor-target icons. macOS only for now; Windows can follow once its launcher is verified. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../desktop/assets/editor-targets/fork.png | Bin 0 -> 5917 bytes .../features/editor-targets/registry.test.ts | 30 ++++++++++++++++++ .../src/features/editor-targets/registry.ts | 2 ++ .../features/editor-targets/targets/fork.ts | 27 ++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 packages/desktop/assets/editor-targets/fork.png create mode 100644 packages/desktop/src/features/editor-targets/targets/fork.ts diff --git a/packages/desktop/assets/editor-targets/fork.png b/packages/desktop/assets/editor-targets/fork.png new file mode 100644 index 0000000000000000000000000000000000000000..9c650d089fc872de4f415e84f7ba282d63945761 GIT binary patch literal 5917 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 literal 0 HcmV?d00001 diff --git a/packages/desktop/src/features/editor-targets/registry.test.ts b/packages/desktop/src/features/editor-targets/registry.test.ts index d0d93e0ba5..9fef70a2af 100644 --- a/packages/desktop/src/features/editor-targets/registry.test.ts +++ b/packages/desktop/src/features/editor-targets/registry.test.ts @@ -4,6 +4,7 @@ import { listAvailableEditorTargets, openEditorTarget } from "./registry.js"; import type { EditorTargetIcon, EditorTargetRuntime } from "./target.js"; import { cursorTarget } from "./targets/cursor.js"; import { explorerTarget, fileManagerTarget, finderTarget } from "./targets/file-manager.js"; +import { forkTarget } from "./targets/fork.js"; import { intellijIdeaTarget } from "./targets/intellij-idea.js"; import { pycharmTarget } from "./targets/pycharm.js"; import { vscodeTarget } from "./targets/vscode.js"; @@ -255,6 +256,35 @@ describe("editor target registry", () => { ]); }); + it("opens the repository in Fork through the macOS application", async () => { + const runtime = new FakeEditorTargets("darwin"); + runtime.installMacApplication("Fork"); + + expect(await forkTarget.isInstalled(runtime)).toBe(true); + expect(await forkTarget.describe(runtime)).toEqual({ + id: "fork", + label: "Fork", + kind: "editor", + icon: { kind: "image", dataUrl: "data:image/png;base64,fork.png" }, + }); + + // Fork is a repository GUI: it always opens the workspace root, never a file. + await forkTarget.launch( + { workspacePath: "/repo", filePath: "/repo/src/app.ts", line: 12, column: 4 }, + runtime, + ); + + expect(runtime.openedMacApplications).toEqual([{ applicationName: "Fork", paths: ["/repo"] }]); + }); + + it("does not offer Fork when its application is absent or off macOS", async () => { + const withoutApp = new FakeEditorTargets("darwin"); + const onLinux = new FakeEditorTargets("linux"); + + expect(await forkTarget.isInstalled(withoutApp)).toBe(false); + expect(await forkTarget.isInstalled(onLinux)).toBe(false); + }); + it("delegates folder opening and file reveal to the system file manager", async () => { const runtime = new FakeEditorTargets("win32"); diff --git a/packages/desktop/src/features/editor-targets/registry.ts b/packages/desktop/src/features/editor-targets/registry.ts index 8de2a55a8c..b23c165878 100644 --- a/packages/desktop/src/features/editor-targets/registry.ts +++ b/packages/desktop/src/features/editor-targets/registry.ts @@ -11,6 +11,7 @@ import { cursorTarget } from "./targets/cursor.js"; import { datagripTarget } from "./targets/datagrip.js"; import { dataspellTarget } from "./targets/dataspell.js"; import { explorerTarget, fileManagerTarget, finderTarget } from "./targets/file-manager.js"; +import { forkTarget } from "./targets/fork.js"; import { golandTarget } from "./targets/goland.js"; import { intellijIdeaTarget } from "./targets/intellij-idea.js"; import { kiroTarget } from "./targets/kiro.js"; @@ -50,6 +51,7 @@ export const EDITOR_TARGETS: readonly EditorTarget[] = [ finderTarget, explorerTarget, fileManagerTarget, + forkTarget, ]; export async function listAvailableEditorTargets( diff --git a/packages/desktop/src/features/editor-targets/targets/fork.ts b/packages/desktop/src/features/editor-targets/targets/fork.ts new file mode 100644 index 0000000000..63b4787ffd --- /dev/null +++ b/packages/desktop/src/features/editor-targets/targets/fork.ts @@ -0,0 +1,27 @@ +import type { EditorTarget } from "../target.js"; + +// Fork (https://git-fork.com) is a macOS/Windows Git client. It opens a +// *repository*, not a file, so we always hand it the workspace root and ignore +// any filePath. Launch goes through the macOS application (`open -a Fork `) +// rather than a CLI: Fork's `fork` command-line helper is opt-in (the user has +// to install it) and is cwd-based, whereas the app bundle is always present once +// Fork is installed and reliably opens the repo as a tab. Windows support can +// follow once its launcher can be verified. +export const forkTarget: EditorTarget = { + id: "fork", + async describe(runtime) { + return { + id: this.id, + label: "Fork", + kind: "editor", + icon: await runtime.loadIcon("fork.png"), + }; + }, + async isInstalled(runtime) { + return runtime.hasMacApplication("Fork"); + }, + async launch(input, runtime) { + if (!runtime.hasMacApplication("Fork")) throw new Error("Fork is not installed"); + await runtime.openMacApplication({ applicationName: "Fork", paths: [input.workspacePath] }); + }, +};