| author | |
| committer | |
| log | b3de2bd926bf9e5f381d02cdcc32b85c39f7c4ed |
| tree | 3d2aabe84c198c7d6ae3d7a9523b16bb3a1adb74 |
| parent | b81ec02984c92c9506c7859ab5c967f3b8afae1d |
- Add server-side command detection and return `availableEditors` in server config
- Update Chat header Open In picker to show only installed editors and handle empty state
- Extend server/web tests and contracts for `availableEditors`; remove unused branch-sync logic9 files changed, 363 insertions(+), 232 deletions(-)
apps/server/src/open.test.ts+57-1| ... | ... | @@ -1,6 +1,15 @@ |
| 1 | import fs from "node:fs"; | |
| 2 | import os from "node:os"; | |
| 3 | import path from "node:path"; | |
| 4 | ||
| 1 | 5 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | 6 | |
| 3 | import { launchDetached, resolveEditorLaunch } from "./open"; | |
| 7 | import { | |
| 8 | isCommandAvailable, | |
| 9 | launchDetached, | |
| 10 | resolveAvailableEditors, | |
| 11 | resolveEditorLaunch, | |
| 12 | } from "./open"; | |
| 4 | 13 | import { Effect } from "effect"; |
| 5 | 14 | import { assertSuccess } from "@effect/vitest/utils"; |
| 6 | 15 | |
| ... | ... | @@ -111,3 +120,50 @@ describe("launchDetached", () => { |
| 111 | 120 | }), |
| 112 | 121 | ); |
| 113 | 122 | }); |
| 123 | ||
| 124 | describe("isCommandAvailable", () => { | |
| 125 | function withTempDir(run: (dir: string) => void): void { | |
| 126 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-open-")); | |
| 127 | try { | |
| 128 | run(dir); | |
| 129 | } finally { | |
| 130 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | it("resolves win32 commands with PATHEXT", () => { | |
| 135 | withTempDir((dir) => { | |
| 136 | fs.writeFileSync(path.join(dir, "code.CMD"), "@echo off\r\n", "utf8"); | |
| 137 | const env = { | |
| 138 | PATH: dir, | |
| 139 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | |
| 140 | } satisfies NodeJS.ProcessEnv; | |
| 141 | assert.equal(isCommandAvailable("code", { platform: "win32", env }), true); | |
| 142 | }); | |
| 143 | }); | |
| 144 | ||
| 145 | it("returns false when a command is not on PATH", () => { | |
| 146 | const env = { | |
| 147 | PATH: "", | |
| 148 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | |
| 149 | } satisfies NodeJS.ProcessEnv; | |
| 150 | assert.equal(isCommandAvailable("definitely-not-installed", { platform: "win32", env }), false); | |
| 151 | }); | |
| 152 | }); | |
| 153 | ||
| 154 | describe("resolveAvailableEditors", () => { | |
| 155 | it("returns only editors whose launch commands are available", () => { | |
| 156 | const dir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-editors-")); | |
| 157 | try { | |
| 158 | fs.writeFileSync(path.join(dir, "cursor.CMD"), "@echo off\r\n", "utf8"); | |
| 159 | fs.writeFileSync(path.join(dir, "explorer.EXE"), "MZ", "utf8"); | |
| 160 | const editors = resolveAvailableEditors("win32", { | |
| 161 | PATH: dir, | |
| 162 | PATHEXT: ".COM;.EXE;.BAT;.CMD", | |
| 163 | }); | |
| 164 | assert.deepEqual(editors, ["cursor", "file-manager"]); | |
| 165 | } finally { | |
| 166 | fs.rmSync(dir, { recursive: true, force: true }); | |
| 167 | } | |
| 168 | }); | |
| 169 | }); |
apps/server/src/open.ts+113-8| ... | ... | @@ -7,6 +7,8 @@ |
| 7 | 7 | * @module Open |
| 8 | 8 | */ |
| 9 | 9 | import { spawn } from "node:child_process"; |
| 10 | import { accessSync, constants, statSync } from "node:fs"; | |
| 11 | import { delimiter, extname, join } from "node:path"; | |
| 10 | 12 | |
| 11 | 13 | import { EDITORS, type EditorId } from "@t3tools/contracts"; |
| 12 | 14 | import { ServiceMap, Schema, Effect, Layer } from "effect"; |
| ... | ... | @@ -30,12 +32,122 @@ interface EditorLaunch { |
| 30 | 32 | readonly args: ReadonlyArray<string>; |
| 31 | 33 | } |
| 32 | 34 | |
| 35 | interface CommandAvailabilityOptions { | |
| 36 | readonly platform?: NodeJS.Platform; | |
| 37 | readonly env?: NodeJS.ProcessEnv; | |
| 38 | } | |
| 39 | ||
| 33 | 40 | const LINE_COLUMN_SUFFIX_PATTERN = /:\d+(?::\d+)?$/; |
| 34 | 41 | |
| 35 | 42 | function shouldUseGotoFlag(editorId: EditorId, target: string): boolean { |
| 36 | 43 | return (editorId === "cursor" || editorId === "vscode") && LINE_COLUMN_SUFFIX_PATTERN.test(target); |
| 37 | 44 | } |
| 38 | 45 | |
| 46 | function fileManagerCommandForPlatform(platform: NodeJS.Platform): string { | |
| 47 | switch (platform) { | |
| 48 | case "darwin": | |
| 49 | return "open"; | |
| 50 | case "win32": | |
| 51 | return "explorer"; | |
| 52 | default: | |
| 53 | return "xdg-open"; | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | function stripWrappingQuotes(value: string): string { | |
| 58 | return value.replace(/^"+|"+$/g, ""); | |
| 59 | } | |
| 60 | ||
| 61 | function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { | |
| 62 | return env.PATH ?? env.Path ?? env.path ?? ""; | |
| 63 | } | |
| 64 | ||
| 65 | function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<string> { | |
| 66 | const rawValue = env.PATHEXT; | |
| 67 | const fallback = [".COM", ".EXE", ".BAT", ".CMD"]; | |
| 68 | if (!rawValue) return fallback; | |
| 69 | ||
| 70 | const parsed = rawValue | |
| 71 | .split(";") | |
| 72 | .map((entry) => entry.trim()) | |
| 73 | .filter((entry) => entry.length > 0) | |
| 74 | .map((entry) => (entry.startsWith(".") ? entry.toUpperCase() : `.${entry.toUpperCase()}`)); | |
| 75 | return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback; | |
| 76 | } | |
| 77 | ||
| 78 | function resolveCommandCandidates( | |
| 79 | command: string, | |
| 80 | platform: NodeJS.Platform, | |
| 81 | env: NodeJS.ProcessEnv, | |
| 82 | ): ReadonlyArray<string> { | |
| 83 | if (platform !== "win32") return [command]; | |
| 84 | if (extname(command).length > 0) return [command]; | |
| 85 | ||
| 86 | const candidates = [command]; | |
| 87 | for (const extension of resolveWindowsPathExtensions(env)) { | |
| 88 | candidates.push(`${command}${extension}`); | |
| 89 | candidates.push(`${command}${extension.toLowerCase()}`); | |
| 90 | } | |
| 91 | return Array.from(new Set(candidates)); | |
| 92 | } | |
| 93 | ||
| 94 | function isExecutableFile(filePath: string, platform: NodeJS.Platform): boolean { | |
| 95 | try { | |
| 96 | const stat = statSync(filePath); | |
| 97 | if (!stat.isFile()) return false; | |
| 98 | if (platform === "win32") return true; | |
| 99 | accessSync(filePath, constants.X_OK); | |
| 100 | return true; | |
| 101 | } catch { | |
| 102 | return false; | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | export function isCommandAvailable( | |
| 107 | command: string, | |
| 108 | options: CommandAvailabilityOptions = {}, | |
| 109 | ): boolean { | |
| 110 | const platform = options.platform ?? process.platform; | |
| 111 | const env = options.env ?? process.env; | |
| 112 | const commandCandidates = resolveCommandCandidates(command, platform, env); | |
| 113 | ||
| 114 | if (command.includes("/") || command.includes("\\")) { | |
| 115 | return commandCandidates.some((candidate) => isExecutableFile(candidate, platform)); | |
| 116 | } | |
| 117 | ||
| 118 | const pathValue = resolvePathEnvironmentVariable(env); | |
| 119 | if (pathValue.length === 0) return false; | |
| 120 | const pathEntries = pathValue | |
| 121 | .split(delimiter) | |
| 122 | .map((entry) => stripWrappingQuotes(entry.trim())) | |
| 123 | .filter((entry) => entry.length > 0); | |
| 124 | ||
| 125 | for (const pathEntry of pathEntries) { | |
| 126 | for (const candidate of commandCandidates) { | |
| 127 | if (isExecutableFile(join(pathEntry, candidate), platform)) { | |
| 128 | return true; | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | return false; | |
| 133 | } | |
| 134 | ||
| 135 | export function resolveAvailableEditors( | |
| 136 | platform: NodeJS.Platform = process.platform, | |
| 137 | env: NodeJS.ProcessEnv = process.env, | |
| 138 | ): ReadonlyArray<EditorId> { | |
| 139 | const available: EditorId[] = []; | |
| 140 | ||
| 141 | for (const editor of EDITORS) { | |
| 142 | const command = editor.command ?? fileManagerCommandForPlatform(platform); | |
| 143 | if (isCommandAvailable(command, { platform, env })) { | |
| 144 | available.push(editor.id); | |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | return available; | |
| 149 | } | |
| 150 | ||
| 39 | 151 | /** |
| 40 | 152 | * OpenShape - Service API for browser and editor launch actions. |
| 41 | 153 | */ |
| ... | ... | @@ -81,14 +193,7 @@ export const resolveEditorLaunch = Effect.fnUntraced(function* ( |
| 81 | 193 | return yield* new OpenError({ message: `Unsupported editor: ${input.editor}` }); |
| 82 | 194 | } |
| 83 | 195 | |
| 84 | switch (platform) { | |
| 85 | case "darwin": | |
| 86 | return { command: "open", args: [input.cwd] }; | |
| 87 | case "win32": | |
| 88 | return { command: "explorer", args: [input.cwd] }; | |
| 89 | default: | |
| 90 | return { command: "xdg-open", args: [input.cwd] }; | |
| 91 | } | |
| 196 | return { command: fileManagerCommandForPlatform(platform), args: [input.cwd] }; | |
| 92 | 197 | }); |
| 93 | 198 | |
| 94 | 199 | export const launchDetached = (launch: EditorLaunch) => |
apps/server/src/wsServer.test.ts+24| ... | ... | @@ -13,6 +13,7 @@ import { makeServerProviderLayer, makeServerRuntimeServicesLayer } from "./serve |
| 13 | 13 | |
| 14 | 14 | import { |
| 15 | 15 | DEFAULT_TERMINAL_ID, |
| 16 | EDITORS, | |
| 16 | 17 | EventId, |
| 17 | 18 | ORCHESTRATION_WS_CHANNELS, |
| 18 | 19 | ORCHESTRATION_WS_METHODS, |
| ... | ... | @@ -340,6 +341,15 @@ function compileKeybindings(bindings: KeybindingsConfig): ResolvedKeybindingsCon |
| 340 | 341 | } |
| 341 | 342 | |
| 342 | 343 | const DEFAULT_RESOLVED_KEYBINDINGS = compileKeybindings([...DEFAULT_KEYBINDINGS]); |
| 344 | const VALID_EDITOR_IDS = new Set(EDITORS.map((editor) => editor.id)); | |
| 345 | ||
| 346 | function expectAvailableEditors(value: unknown): void { | |
| 347 | expect(Array.isArray(value)).toBe(true); | |
| 348 | for (const editorId of value as unknown[]) { | |
| 349 | expect(typeof editorId).toBe("string"); | |
| 350 | expect(VALID_EDITOR_IDS.has(editorId as (typeof EDITORS)[number]["id"])).toBe(true); | |
| 351 | } | |
| 352 | } | |
| 343 | 353 | |
| 344 | 354 | describe("WebSocket Server", () => { |
| 345 | 355 | let server: Http.Server | null = null; |
| ... | ... | @@ -726,7 +736,9 @@ describe("WebSocket Server", () => { |
| 726 | 736 | keybindingsConfigPath: keybindingsPath, |
| 727 | 737 | keybindings: DEFAULT_RESOLVED_KEYBINDINGS, |
| 728 | 738 | issues: [], |
| 739 | availableEditors: expect.any(Array), | |
| 729 | 740 | }); |
| 741 | expectAvailableEditors((response.result as { availableEditors: unknown }).availableEditors); | |
| 730 | 742 | }); |
| 731 | 743 | |
| 732 | 744 | it("bootstraps default keybindings file when missing", async () => { |
| ... | ... | @@ -749,7 +761,9 @@ describe("WebSocket Server", () => { |
| 749 | 761 | keybindingsConfigPath: keybindingsPath, |
| 750 | 762 | keybindings: DEFAULT_RESOLVED_KEYBINDINGS, |
| 751 | 763 | issues: [], |
| 764 | availableEditors: expect.any(Array), | |
| 752 | 765 | }); |
| 766 | expectAvailableEditors((response.result as { availableEditors: unknown }).availableEditors); | |
| 753 | 767 | |
| 754 | 768 | const persistedConfig = JSON.parse( |
| 755 | 769 | fs.readFileSync(keybindingsPath, "utf8"), |
| ... | ... | @@ -782,7 +796,9 @@ describe("WebSocket Server", () => { |
| 782 | 796 | message: expect.stringContaining("expected JSON array"), |
| 783 | 797 | }, |
| 784 | 798 | ], |
| 799 | availableEditors: expect.any(Array), | |
| 785 | 800 | }); |
| 801 | expectAvailableEditors((response.result as { availableEditors: unknown }).availableEditors); | |
| 786 | 802 | expect(fs.readFileSync(keybindingsPath, "utf8")).toBe("{ not-json"); |
| 787 | 803 | }); |
| 788 | 804 | |
| ... | ... | @@ -814,6 +830,7 @@ describe("WebSocket Server", () => { |
| 814 | 830 | keybindingsConfigPath: string; |
| 815 | 831 | keybindings: ResolvedKeybindingsConfig; |
| 816 | 832 | issues: Array<{ kind: string; index?: number; message: string }>; |
| 833 | availableEditors: unknown; | |
| 817 | 834 | }; |
| 818 | 835 | expect(result.cwd).toBe("/my/workspace"); |
| 819 | 836 | expect(result.keybindingsConfigPath).toBe(keybindingsPath); |
| ... | ... | @@ -832,6 +849,7 @@ describe("WebSocket Server", () => { |
| 832 | 849 | expect(result.keybindings).toHaveLength(DEFAULT_RESOLVED_KEYBINDINGS.length); |
| 833 | 850 | expect(result.keybindings.some((entry) => entry.command === "terminal.toggle")).toBe(true); |
| 834 | 851 | expect(result.keybindings.some((entry) => entry.command === "terminal.new")).toBe(true); |
| 852 | expectAvailableEditors(result.availableEditors); | |
| 835 | 853 | }); |
| 836 | 854 | |
| 837 | 855 | it("pushes server.configUpdated issues when keybindings file changes", async () => { |
| ... | ... | @@ -929,7 +947,9 @@ describe("WebSocket Server", () => { |
| 929 | 947 | keybindingsConfigPath: keybindingsPath, |
| 930 | 948 | keybindings: compileKeybindings(persistedConfig), |
| 931 | 949 | issues: [], |
| 950 | availableEditors: expect.any(Array), | |
| 932 | 951 | }); |
| 952 | expectAvailableEditors((response.result as { availableEditors: unknown }).availableEditors); | |
| 933 | 953 | }); |
| 934 | 954 | |
| 935 | 955 | it("upserts keybinding rules and updates cached server config", async () => { |
| ... | ... | @@ -974,7 +994,11 @@ describe("WebSocket Server", () => { |
| 974 | 994 | keybindingsConfigPath: keybindingsPath, |
| 975 | 995 | keybindings: compileKeybindings(persistedConfig), |
| 976 | 996 | issues: [], |
| 997 | availableEditors: expect.any(Array), | |
| 977 | 998 | }); |
| 999 | expectAvailableEditors( | |
| 1000 | (configResponse.result as { availableEditors: unknown }).availableEditors, | |
| 1001 | ); | |
| 978 | 1002 | }); |
| 979 | 1003 | |
| 980 | 1004 | it("returns error for unknown methods", async () => { |
apps/server/src/wsServer.ts+3-1| ... | ... | @@ -54,7 +54,7 @@ import { OrchestrationReactor } from "./orchestration/Services/OrchestrationReac |
| 54 | 54 | import { ProviderService } from "./provider/Services/ProviderService"; |
| 55 | 55 | import { CheckpointDiffQuery } from "./checkpointing/Services/CheckpointDiffQuery"; |
| 56 | 56 | import { clamp } from "effect/Number"; |
| 57 | import { Open } from "./open"; | |
| 57 | import { Open, resolveAvailableEditors } from "./open"; | |
| 58 | 58 | import { ServerConfig } from "./config"; |
| 59 | 59 | import { GitCore } from "./git/Services/GitCore.ts"; |
| 60 | 60 | import { tryHandleProjectFaviconRequest } from "./projectFaviconRoute"; |
| ... | ... | @@ -193,6 +193,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 193 | 193 | logWebSocketEvents, |
| 194 | 194 | autoBootstrapProjectFromCwd, |
| 195 | 195 | } = serverConfig; |
| 196 | const availableEditors = resolveAvailableEditors(); | |
| 196 | 197 | |
| 197 | 198 | const gitManager = yield* GitManager; |
| 198 | 199 | const terminalManager = yield* TerminalManager; |
| ... | ... | @@ -734,6 +735,7 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return< |
| 734 | 735 | keybindingsConfigPath, |
| 735 | 736 | keybindings: keybindingsConfig.keybindings, |
| 736 | 737 | issues: keybindingsConfig.issues, |
| 738 | availableEditors, | |
| 737 | 739 | }; |
| 738 | 740 | |
| 739 | 741 | case WS_METHODS.serverUpsertKeybinding: { |
apps/web/src/components/BranchToolbar.logic.test.ts+1-58| ... | ... | @@ -1,67 +1,10 @@ |
| 1 | import { ThreadId, type GitBranch } from "@t3tools/contracts"; | |
| 1 | import type { GitBranch } from "@t3tools/contracts"; | |
| 2 | 2 | import { describe, expect, it } from "vitest"; |
| 3 | 3 | import { |
| 4 | 4 | dedupeRemoteBranchesWithLocalMatches, |
| 5 | 5 | deriveLocalBranchNameFromRemoteRef, |
| 6 | deriveSyncedLocalBranch, | |
| 7 | 6 | } from "./BranchToolbar.logic"; |
| 8 | 7 | |
| 9 | const branches: GitBranch[] = [ | |
| 10 | { | |
| 11 | name: "main", | |
| 12 | current: true, | |
| 13 | isDefault: true, | |
| 14 | worktreePath: null, | |
| 15 | }, | |
| 16 | { | |
| 17 | name: "feature/demo", | |
| 18 | current: false, | |
| 19 | isDefault: false, | |
| 20 | worktreePath: null, | |
| 21 | }, | |
| 22 | ]; | |
| 23 | ||
| 24 | describe("deriveSyncedLocalBranch", () => { | |
| 25 | it("syncs to git current branch in local mode", () => { | |
| 26 | const threadId = ThreadId.makeUnsafe("thread-1"); | |
| 27 | const result = deriveSyncedLocalBranch({ | |
| 28 | activeThreadId: threadId, | |
| 29 | activeWorktreePath: null, | |
| 30 | envMode: "local", | |
| 31 | activeThreadBranch: "feature/demo", | |
| 32 | queryBranches: branches, | |
| 33 | }); | |
| 34 | ||
| 35 | expect(result).toBe("main"); | |
| 36 | }); | |
| 37 | ||
| 38 | it("does not sync when creating a new worktree", () => { | |
| 39 | const threadId = ThreadId.makeUnsafe("thread-1"); | |
| 40 | const result = deriveSyncedLocalBranch({ | |
| 41 | activeThreadId: threadId, | |
| 42 | activeWorktreePath: null, | |
| 43 | envMode: "worktree", | |
| 44 | activeThreadBranch: "feature/demo", | |
| 45 | queryBranches: branches, | |
| 46 | }); | |
| 47 | ||
| 48 | expect(result).toBeNull(); | |
| 49 | }); | |
| 50 | ||
| 51 | it("does not sync when thread already targets a worktree path", () => { | |
| 52 | const threadId = ThreadId.makeUnsafe("thread-1"); | |
| 53 | const result = deriveSyncedLocalBranch({ | |
| 54 | activeThreadId: threadId, | |
| 55 | activeWorktreePath: "/tmp/repo/worktrees/feature-demo", | |
| 56 | envMode: "local", | |
| 57 | activeThreadBranch: "feature/demo", | |
| 58 | queryBranches: branches, | |
| 59 | }); | |
| 60 | ||
| 61 | expect(result).toBeNull(); | |
| 62 | }); | |
| 63 | }); | |
| 64 | ||
| 65 | 8 | describe("deriveLocalBranchNameFromRemoteRef", () => { |
| 66 | 9 | it("strips the remote prefix from a remote ref", () => { |
| 67 | 10 | expect(deriveLocalBranchNameFromRemoteRef("origin/feature/demo")).toBe("feature/demo"); |
apps/web/src/components/BranchToolbar.logic.ts+1-28| ... | ... | @@ -1,12 +1,4 @@ |
| 1 | import type { GitBranch, ThreadId } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | interface DeriveSyncedLocalBranchInput { | |
| 4 | activeThreadId: ThreadId | undefined; | |
| 5 | activeWorktreePath: string | null; | |
| 6 | envMode: "local" | "worktree"; | |
| 7 | activeThreadBranch: string | null; | |
| 8 | queryBranches: ReadonlyArray<GitBranch> | undefined; | |
| 9 | } | |
| 1 | import type { GitBranch } from "@t3tools/contracts"; | |
| 10 | 2 | |
| 11 | 3 | export function deriveLocalBranchNameFromRemoteRef(branchName: string): string { |
| 12 | 4 | const firstSeparatorIndex = branchName.indexOf("/"); |
| ... | ... | @@ -55,22 +47,3 @@ export function dedupeRemoteBranchesWithLocalMatches( |
| 55 | 47 | return !localBranchCandidates.some((candidate) => localBranchNames.has(candidate)); |
| 56 | 48 | }); |
| 57 | 49 | } |
| 58 | ||
| 59 | export function deriveSyncedLocalBranch({ | |
| 60 | activeThreadId, | |
| 61 | activeWorktreePath, | |
| 62 | envMode, | |
| 63 | activeThreadBranch, | |
| 64 | queryBranches, | |
| 65 | }: DeriveSyncedLocalBranchInput): string | null { | |
| 66 | if (!activeThreadId || activeWorktreePath || envMode !== "local") { | |
| 67 | return null; | |
| 68 | } | |
| 69 | ||
| 70 | const currentBranch = queryBranches?.find((branch) => branch.current); | |
| 71 | if (!currentBranch || currentBranch.name === activeThreadBranch) { | |
| 72 | return null; | |
| 73 | } | |
| 74 | ||
| 75 | return currentBranch.name; | |
| 76 | } |
apps/web/src/components/ChatView.tsx+67-34| ... | ... | @@ -147,6 +147,7 @@ const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 147 | 147 | const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; |
| 148 | 148 | const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; |
| 149 | 149 | const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = []; |
| 150 | const EMPTY_AVAILABLE_EDITORS: EditorId[] = []; | |
| 150 | 151 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; |
| 151 | 152 | const SCRIPT_TERMINAL_COLS = 120; |
| 152 | 153 | const SCRIPT_TERMINAL_ROWS = 30; |
| ... | ... | @@ -869,10 +870,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 869 | 870 | ); |
| 870 | 871 | const effectivePathQuery = pathTriggerQuery.length > 0 ? debouncedPathQuery : ""; |
| 871 | 872 | const branchesQuery = useQuery(gitBranchesQueryOptions(gitCwd)); |
| 872 | const keybindingsQuery = useQuery({ | |
| 873 | ...serverConfigQueryOptions(), | |
| 874 | select: (config) => config.keybindings, | |
| 875 | }); | |
| 873 | const serverConfigQuery = useQuery(serverConfigQueryOptions()); | |
| 876 | 874 | const workspaceEntriesQuery = useQuery( |
| 877 | 875 | projectSearchEntriesQueryOptions({ |
| 878 | 876 | cwd: gitCwd, |
| ... | ... | @@ -944,7 +942,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 944 | 942 | () => new Set(nonPersistedComposerImageIds), |
| 945 | 943 | [nonPersistedComposerImageIds], |
| 946 | 944 | ); |
| 947 | const keybindings = keybindingsQuery.data ?? EMPTY_KEYBINDINGS; | |
| 945 | const keybindings = serverConfigQuery.data?.keybindings ?? EMPTY_KEYBINDINGS; | |
| 946 | const availableEditors = serverConfigQuery.data?.availableEditors ?? EMPTY_AVAILABLE_EDITORS; | |
| 948 | 947 | const activeProjectCwd = activeProject?.cwd ?? null; |
| 949 | 948 | const activeThreadWorktreePath = activeThread?.worktreePath ?? null; |
| 950 | 949 | const threadTerminalRuntimeEnv = useMemo(() => { |
| ... | ... | @@ -2486,6 +2485,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2486 | 2485 | activeProject ? (lastInvokedScriptByProjectId[activeProject.id] ?? null) : null |
| 2487 | 2486 | } |
| 2488 | 2487 | keybindings={keybindings} |
| 2488 | availableEditors={availableEditors} | |
| 2489 | 2489 | diffToggleShortcutLabel={diffPanelShortcutLabel} |
| 2490 | 2490 | gitCwd={gitCwd} |
| 2491 | 2491 | diffOpen={diffOpen} |
| ... | ... | @@ -2899,6 +2899,7 @@ interface ChatHeaderProps { |
| 2899 | 2899 | activeProjectScripts: ProjectScript[] | undefined; |
| 2900 | 2900 | preferredScriptId: string | null; |
| 2901 | 2901 | keybindings: ResolvedKeybindingsConfig; |
| 2902 | availableEditors: ReadonlyArray<EditorId>; | |
| 2902 | 2903 | diffToggleShortcutLabel: string | null; |
| 2903 | 2904 | gitCwd: string | null; |
| 2904 | 2905 | diffOpen: boolean; |
| ... | ... | @@ -2915,6 +2916,7 @@ const ChatHeader = memo(function ChatHeader({ |
| 2915 | 2916 | activeProjectScripts, |
| 2916 | 2917 | preferredScriptId, |
| 2917 | 2918 | keybindings, |
| 2919 | availableEditors, | |
| 2918 | 2920 | diffToggleShortcutLabel, |
| 2919 | 2921 | gitCwd, |
| 2920 | 2922 | diffOpen, |
| ... | ... | @@ -2951,7 +2953,11 @@ const ChatHeader = memo(function ChatHeader({ |
| 2951 | 2953 | /> |
| 2952 | 2954 | )} |
| 2953 | 2955 | {activeProjectName && ( |
| 2954 | <OpenInPicker keybindings={keybindings} activeThreadId={activeThreadId} /> | |
| 2956 | <OpenInPicker | |
| 2957 | keybindings={keybindings} | |
| 2958 | availableEditors={availableEditors} | |
| 2959 | activeThreadId={activeThreadId} | |
| 2960 | /> | |
| 2955 | 2961 | )} |
| 2956 | 2962 | {activeProjectName && <GitActionsControl gitCwd={gitCwd} activeThreadId={activeThreadId} />} |
| 2957 | 2963 | <Tooltip> |
| ... | ... | @@ -3663,9 +3669,11 @@ const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: { |
| 3663 | 3669 | |
| 3664 | 3670 | const OpenInPicker = memo(function OpenInPicker({ |
| 3665 | 3671 | keybindings, |
| 3672 | availableEditors, | |
| 3666 | 3673 | activeThreadId, |
| 3667 | 3674 | }: { |
| 3668 | 3675 | keybindings: ResolvedKeybindingsConfig; |
| 3676 | availableEditors: ReadonlyArray<EditorId>; | |
| 3669 | 3677 | activeThreadId: ThreadId | null; |
| 3670 | 3678 | }) { |
| 3671 | 3679 | const [lastEditor, setLastEditor] = useState<EditorId>(() => { |
| ... | ... | @@ -3673,28 +3681,45 @@ const OpenInPicker = memo(function OpenInPicker({ |
| 3673 | 3681 | return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id; |
| 3674 | 3682 | }); |
| 3675 | 3683 | |
| 3676 | const options = [ | |
| 3677 | { | |
| 3678 | label: "Cursor", | |
| 3679 | Icon: CursorIcon, | |
| 3680 | value: "cursor", | |
| 3681 | }, | |
| 3682 | { | |
| 3683 | label: "VS Code", | |
| 3684 | Icon: VisualStudioCode, | |
| 3685 | value: "vscode", | |
| 3686 | }, | |
| 3687 | { | |
| 3688 | label: isMacPlatform(navigator.platform) | |
| 3689 | ? "Finder" | |
| 3690 | : isWindowsPlatform(navigator.platform) | |
| 3691 | ? "Explorer" | |
| 3692 | : "Files", | |
| 3693 | Icon: FolderClosedIcon, | |
| 3694 | value: "file-manager", | |
| 3695 | }, | |
| 3696 | ] satisfies { label: string; Icon: Icon; value: EditorId }[]; | |
| 3697 | const primaryOption = options.find(({ value }) => value === lastEditor); | |
| 3684 | const allOptions = useMemo<Array<{ label: string; Icon: Icon; value: EditorId }>>( | |
| 3685 | () => [ | |
| 3686 | { | |
| 3687 | label: "Cursor", | |
| 3688 | Icon: CursorIcon, | |
| 3689 | value: "cursor", | |
| 3690 | }, | |
| 3691 | { | |
| 3692 | label: "VS Code", | |
| 3693 | Icon: VisualStudioCode, | |
| 3694 | value: "vscode", | |
| 3695 | }, | |
| 3696 | { | |
| 3697 | label: isMacPlatform(navigator.platform) | |
| 3698 | ? "Finder" | |
| 3699 | : isWindowsPlatform(navigator.platform) | |
| 3700 | ? "Explorer" | |
| 3701 | : "Files", | |
| 3702 | Icon: FolderClosedIcon, | |
| 3703 | value: "file-manager", | |
| 3704 | }, | |
| 3705 | ], | |
| 3706 | [], | |
| 3707 | ); | |
| 3708 | const options = useMemo( | |
| 3709 | () => allOptions.filter((option) => availableEditors.includes(option.value)), | |
| 3710 | [allOptions, availableEditors], | |
| 3711 | ); | |
| 3712 | ||
| 3713 | const effectiveEditor = options.some((option) => option.value === lastEditor) | |
| 3714 | ? lastEditor | |
| 3715 | : (options[0]?.value ?? null); | |
| 3716 | const primaryOption = options.find(({ value }) => value === effectiveEditor) ?? null; | |
| 3717 | ||
| 3718 | useEffect(() => { | |
| 3719 | if (!effectiveEditor || effectiveEditor === lastEditor) return; | |
| 3720 | localStorage.setItem(LAST_EDITOR_KEY, effectiveEditor); | |
| 3721 | setLastEditor(effectiveEditor); | |
| 3722 | }, [effectiveEditor, lastEditor]); | |
| 3698 | 3723 | |
| 3699 | 3724 | const activeThread = useStore((store) => |
| 3700 | 3725 | activeThreadId ? store.threads.find((thread) => thread.id === activeThreadId) : undefined, |
| ... | ... | @@ -3708,13 +3733,14 @@ const OpenInPicker = memo(function OpenInPicker({ |
| 3708 | 3733 | (editorId: EditorId | null) => { |
| 3709 | 3734 | const api = readNativeApi(); |
| 3710 | 3735 | if (!api || !activeProject) return; |
| 3711 | const editor = editorId ?? lastEditor; | |
| 3736 | const editor = editorId ?? effectiveEditor; | |
| 3737 | if (!editor) return; | |
| 3712 | 3738 | const cwd = activeThread?.worktreePath ?? activeProject.cwd; |
| 3713 | 3739 | void api.shell.openInEditor(cwd, editor); |
| 3714 | 3740 | localStorage.setItem(LAST_EDITOR_KEY, editor); |
| 3715 | 3741 | setLastEditor(editor); |
| 3716 | 3742 | }, |
| 3717 | [activeProject, activeThread, lastEditor, setLastEditor], | |
| 3743 | [activeProject, activeThread, effectiveEditor, setLastEditor], | |
| 3718 | 3744 | ); |
| 3719 | 3745 | |
| 3720 | 3746 | const openFavoriteEditorShortcutLabel = useMemo( |
| ... | ... | @@ -3727,18 +3753,24 @@ const OpenInPicker = memo(function OpenInPicker({ |
| 3727 | 3753 | const api = readNativeApi(); |
| 3728 | 3754 | if (!isOpenFavoriteEditorShortcut(e, keybindings)) return; |
| 3729 | 3755 | if (!api || !activeProject) return; |
| 3756 | if (!effectiveEditor) return; | |
| 3730 | 3757 | |
| 3731 | 3758 | e.preventDefault(); |
| 3732 | 3759 | const cwd = activeThread?.worktreePath ?? activeProject.cwd; |
| 3733 | void api.shell.openInEditor(cwd, lastEditor); | |
| 3760 | void api.shell.openInEditor(cwd, effectiveEditor); | |
| 3734 | 3761 | }; |
| 3735 | 3762 | window.addEventListener("keydown", handler); |
| 3736 | 3763 | return () => window.removeEventListener("keydown", handler); |
| 3737 | }, [activeProject, activeThread, keybindings, lastEditor]); | |
| 3764 | }, [activeProject, activeThread, keybindings, effectiveEditor]); | |
| 3738 | 3765 | |
| 3739 | 3766 | return ( |
| 3740 | 3767 | <Group aria-label="Subscription actions"> |
| 3741 | <Button size="xs" variant="outline" onClick={() => openInEditor(lastEditor)}> | |
| 3768 | <Button | |
| 3769 | size="xs" | |
| 3770 | variant="outline" | |
| 3771 | disabled={!effectiveEditor || !activeProject} | |
| 3772 | onClick={() => openInEditor(effectiveEditor)} | |
| 3773 | > | |
| 3742 | 3774 | {primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3.5" />} |
| 3743 | 3775 | <span className="sr-only @sm/header-actions:not-sr-only @sm/header-actions:ml-0.5"> |
| 3744 | 3776 | Open |
| ... | ... | @@ -3750,11 +3782,12 @@ const OpenInPicker = memo(function OpenInPicker({ |
| 3750 | 3782 | <ChevronDownIcon aria-hidden="true" className="size-4" /> |
| 3751 | 3783 | </MenuTrigger> |
| 3752 | 3784 | <MenuPopup align="end"> |
| 3785 | {options.length === 0 && <MenuItem disabled>No installed editors found</MenuItem>} | |
| 3753 | 3786 | {options.map(({ label, Icon, value }) => ( |
| 3754 | 3787 | <MenuItem key={value} onClick={() => openInEditor(value)}> |
| 3755 | 3788 | <Icon aria-hidden="true" className="text-muted-foreground" /> |
| 3756 | 3789 | {label} |
| 3757 | {value === lastEditor && openFavoriteEditorShortcutLabel && ( | |
| 3790 | {value === effectiveEditor && openFavoriteEditorShortcutLabel && ( | |
| 3758 | 3791 | <MenuShortcut>{openFavoriteEditorShortcutLabel}</MenuShortcut> |
| 3759 | 3792 | )} |
| 3760 | 3793 | </MenuItem> |
apps/web/src/components/Icons.tsx+95-102| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { SVGProps } from "react"; | |
| 1 | import { type SVGProps, useId } from "react"; | |
| 2 | 2 | |
| 3 | 3 | export type Icon = React.FC<SVGProps<SVGSVGElement>>; |
| 4 | 4 | |
| ... | ... | @@ -20,109 +20,102 @@ export const CursorIcon: Icon = (props) => ( |
| 20 | 20 | </svg> |
| 21 | 21 | ); |
| 22 | 22 | |
| 23 | export const VisualStudioCode: Icon = (props) => ( | |
| 24 | <svg {...props} fill="none" viewBox="0 0 100 100"> | |
| 25 | <mask | |
| 26 | id="vscode__a" | |
| 27 | width="100" | |
| 28 | height="100" | |
| 29 | x="0" | |
| 30 | y="0" | |
| 31 | maskUnits="userSpaceOnUse" | |
| 32 | > | |
| 33 | <path | |
| 34 | fill="#fff" | |
| 35 | fillRule="evenodd" | |
| 36 | d="M70.912 99.317a6.223 6.223 0 0 0 4.96-.19l20.589-9.907A6.25 6.25 0 0 0 100 83.587V16.413a6.25 6.25 0 0 0-3.54-5.632L75.874.874a6.226 6.226 0 0 0-7.104 1.21L29.355 38.04 12.187 25.01a4.162 4.162 0 0 0-5.318.236l-5.506 5.009a4.168 4.168 0 0 0-.004 6.162L16.247 50 1.36 63.583a4.168 4.168 0 0 0 .004 6.162l5.506 5.01a4.162 4.162 0 0 0 5.318.236l17.168-13.032L68.77 97.917a6.217 6.217 0 0 0 2.143 1.4ZM75.015 27.3 45.11 50l29.906 22.701V27.3Z" | |
| 37 | clipRule="evenodd" | |
| 38 | /> | |
| 39 | </mask> | |
| 40 | <g mask="url(#vscode__a)"> | |
| 41 | <path | |
| 42 | fill="#0065A9" | |
| 43 | d="M96.461 10.796 75.857.876a6.23 6.23 0 0 0-7.107 1.207l-67.451 61.5a4.167 4.167 0 0 0 .004 6.162l5.51 5.009a4.167 4.167 0 0 0 5.32.236l81.228-61.62c2.725-2.067 6.639-.124 6.639 3.297v-.24a6.25 6.25 0 0 0-3.539-5.63Z" | |
| 44 | /> | |
| 45 | <g filter="url(#vscode__b)"> | |
| 23 | export const VisualStudioCode: Icon = (props) => { | |
| 24 | const id = useId(); | |
| 25 | const maskId = `${id}-vscode-a`; | |
| 26 | const topShadowFilterId = `${id}-vscode-b`; | |
| 27 | const sideShadowFilterId = `${id}-vscode-c`; | |
| 28 | const overlayGradientId = `${id}-vscode-d`; | |
| 29 | ||
| 30 | return ( | |
| 31 | <svg {...props} fill="none" viewBox="0 0 100 100"> | |
| 32 | <mask id={maskId} width="100" height="100" x="0" y="0" maskUnits="userSpaceOnUse"> | |
| 46 | 33 | <path |
| 47 | fill="#007ACC" | |
| 48 | d="m96.461 89.204-20.604 9.92a6.229 6.229 0 0 1-7.107-1.207l-67.451-61.5a4.167 4.167 0 0 1 .004-6.162l5.51-5.009a4.167 4.167 0 0 1 5.32-.236l81.228 61.62c2.725 2.067 6.639.124 6.639-3.297v.24a6.25 6.25 0 0 1-3.539 5.63Z" | |
| 34 | fill="#fff" | |
| 35 | fillRule="evenodd" | |
| 36 | d="M70.912 99.317a6.223 6.223 0 0 0 4.96-.19l20.589-9.907A6.25 6.25 0 0 0 100 83.587V16.413a6.25 6.25 0 0 0-3.54-5.632L75.874.874a6.226 6.226 0 0 0-7.104 1.21L29.355 38.04 12.187 25.01a4.162 4.162 0 0 0-5.318.236l-5.506 5.009a4.168 4.168 0 0 0-.004 6.162L16.247 50 1.36 63.583a4.168 4.168 0 0 0 .004 6.162l5.506 5.01a4.162 4.162 0 0 0 5.318.236l17.168-13.032L68.77 97.917a6.217 6.217 0 0 0 2.143 1.4ZM75.015 27.3 45.11 50l29.906 22.701V27.3Z" | |
| 37 | clipRule="evenodd" | |
| 49 | 38 | /> |
| 50 | </g> | |
| 51 | <g filter="url(#vscode__c)"> | |
| 39 | </mask> | |
| 40 | <g mask={`url(#${maskId})`}> | |
| 52 | 41 | <path |
| 53 | fill="#1F9CF0" | |
| 54 | d="M75.858 99.126a6.232 6.232 0 0 1-7.108-1.21c2.306 2.307 6.25.674 6.25-2.588V4.672c0-3.262-3.944-4.895-6.25-2.589a6.232 6.232 0 0 1 7.108-1.21l20.6 9.908A6.25 6.25 0 0 1 100 16.413v67.174a6.25 6.25 0 0 1-3.541 5.633l-20.601 9.906Z" | |
| 55 | /> | |
| 56 | </g> | |
| 57 | <path | |
| 58 | fill="url(#vscode__d)" | |
| 59 | fillRule="evenodd" | |
| 60 | d="M70.851 99.317a6.224 6.224 0 0 0 4.96-.19L96.4 89.22a6.25 6.25 0 0 0 3.54-5.633V16.413a6.25 6.25 0 0 0-3.54-5.632L75.812.874a6.226 6.226 0 0 0-7.104 1.21L29.294 38.04 12.126 25.01a4.162 4.162 0 0 0-5.317.236l-5.507 5.009a4.168 4.168 0 0 0-.004 6.162L16.186 50 1.298 63.583a4.168 4.168 0 0 0 .004 6.162l5.507 5.009a4.162 4.162 0 0 0 5.317.236L29.294 61.96l39.414 35.958a6.218 6.218 0 0 0 2.143 1.4ZM74.954 27.3 45.048 50l29.906 22.701V27.3Z" | |
| 61 | clipRule="evenodd" | |
| 62 | opacity=".25" | |
| 63 | style={{ mixBlendMode: "overlay" }} | |
| 64 | /> | |
| 65 | </g> | |
| 66 | <defs> | |
| 67 | <filter | |
| 68 | id="vscode__b" | |
| 69 | width="116.727" | |
| 70 | height="92.246" | |
| 71 | x="-8.394" | |
| 72 | y="15.829" | |
| 73 | colorInterpolationFilters="sRGB" | |
| 74 | filterUnits="userSpaceOnUse" | |
| 75 | > | |
| 76 | <feFlood floodOpacity="0" result="BackgroundImageFix" /> | |
| 77 | <feColorMatrix | |
| 78 | in="SourceAlpha" | |
| 79 | values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" | |
| 80 | /> | |
| 81 | <feOffset /> | |
| 82 | <feGaussianBlur stdDeviation="4.167" /> | |
| 83 | <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" /> | |
| 84 | <feBlend | |
| 85 | in2="BackgroundImageFix" | |
| 86 | mode="overlay" | |
| 87 | result="effect1_dropShadow" | |
| 42 | fill="#0065A9" | |
| 43 | d="M96.461 10.796 75.857.876a6.23 6.23 0 0 0-7.107 1.207l-67.451 61.5a4.167 4.167 0 0 0 .004 6.162l5.51 5.009a4.167 4.167 0 0 0 5.32.236l81.228-61.62c2.725-2.067 6.639-.124 6.639 3.297v-.24a6.25 6.25 0 0 0-3.539-5.63Z" | |
| 88 | 44 | /> |
| 89 | <feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" /> | |
| 90 | </filter> | |
| 91 | <filter | |
| 92 | id="vscode__c" | |
| 93 | width="47.917" | |
| 94 | height="116.151" | |
| 95 | x="60.417" | |
| 96 | y="-8.076" | |
| 97 | colorInterpolationFilters="sRGB" | |
| 98 | filterUnits="userSpaceOnUse" | |
| 99 | > | |
| 100 | <feFlood floodOpacity="0" result="BackgroundImageFix" /> | |
| 101 | <feColorMatrix | |
| 102 | in="SourceAlpha" | |
| 103 | values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" | |
| 104 | /> | |
| 105 | <feOffset /> | |
| 106 | <feGaussianBlur stdDeviation="4.167" /> | |
| 107 | <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" /> | |
| 108 | <feBlend | |
| 109 | in2="BackgroundImageFix" | |
| 110 | mode="overlay" | |
| 111 | result="effect1_dropShadow" | |
| 45 | <g filter={`url(#${topShadowFilterId})`}> | |
| 46 | <path | |
| 47 | fill="#007ACC" | |
| 48 | d="m96.461 89.204-20.604 9.92a6.229 6.229 0 0 1-7.107-1.207l-67.451-61.5a4.167 4.167 0 0 1 .004-6.162l5.51-5.009a4.167 4.167 0 0 1 5.32-.236l81.228 61.62c2.725 2.067 6.639.124 6.639-3.297v.24a6.25 6.25 0 0 1-3.539 5.63Z" | |
| 49 | /> | |
| 50 | </g> | |
| 51 | <g filter={`url(#${sideShadowFilterId})`}> | |
| 52 | <path | |
| 53 | fill="#1F9CF0" | |
| 54 | d="M75.858 99.126a6.232 6.232 0 0 1-7.108-1.21c2.306 2.307 6.25.674 6.25-2.588V4.672c0-3.262-3.944-4.895-6.25-2.589a6.232 6.232 0 0 1 7.108-1.21l20.6 9.908A6.25 6.25 0 0 1 100 16.413v67.174a6.25 6.25 0 0 1-3.541 5.633l-20.601 9.906Z" | |
| 55 | /> | |
| 56 | </g> | |
| 57 | <path | |
| 58 | fill={`url(#${overlayGradientId})`} | |
| 59 | fillRule="evenodd" | |
| 60 | d="M70.851 99.317a6.224 6.224 0 0 0 4.96-.19L96.4 89.22a6.25 6.25 0 0 0 3.54-5.633V16.413a6.25 6.25 0 0 0-3.54-5.632L75.812.874a6.226 6.226 0 0 0-7.104 1.21L29.294 38.04 12.126 25.01a4.162 4.162 0 0 0-5.317.236l-5.507 5.009a4.168 4.168 0 0 0-.004 6.162L16.186 50 1.298 63.583a4.168 4.168 0 0 0 .004 6.162l5.507 5.009a4.162 4.162 0 0 0 5.317.236L29.294 61.96l39.414 35.958a6.218 6.218 0 0 0 2.143 1.4ZM74.954 27.3 45.048 50l29.906 22.701V27.3Z" | |
| 61 | clipRule="evenodd" | |
| 62 | opacity=".25" | |
| 63 | style={{ mixBlendMode: "overlay" }} | |
| 112 | 64 | /> |
| 113 | <feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" /> | |
| 114 | </filter> | |
| 115 | <linearGradient | |
| 116 | id="vscode__d" | |
| 117 | x1="49.939" | |
| 118 | x2="49.939" | |
| 119 | y1=".258" | |
| 120 | y2="99.742" | |
| 121 | gradientUnits="userSpaceOnUse" | |
| 122 | > | |
| 123 | <stop stopColor="#fff" /> | |
| 124 | <stop offset="1" stopColor="#fff" stopOpacity="0" /> | |
| 125 | </linearGradient> | |
| 126 | </defs> | |
| 127 | </svg> | |
| 128 | ); | |
| 65 | </g> | |
| 66 | <defs> | |
| 67 | <filter | |
| 68 | id={topShadowFilterId} | |
| 69 | width="116.727" | |
| 70 | height="92.246" | |
| 71 | x="-8.394" | |
| 72 | y="15.829" | |
| 73 | colorInterpolationFilters="sRGB" | |
| 74 | filterUnits="userSpaceOnUse" | |
| 75 | > | |
| 76 | <feFlood floodOpacity="0" result="BackgroundImageFix" /> | |
| 77 | <feColorMatrix | |
| 78 | in="SourceAlpha" | |
| 79 | values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" | |
| 80 | /> | |
| 81 | <feOffset /> | |
| 82 | <feGaussianBlur stdDeviation="4.167" /> | |
| 83 | <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" /> | |
| 84 | <feBlend in2="BackgroundImageFix" mode="overlay" result="effect1_dropShadow" /> | |
| 85 | <feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" /> | |
| 86 | </filter> | |
| 87 | <filter | |
| 88 | id={sideShadowFilterId} | |
| 89 | width="47.917" | |
| 90 | height="116.151" | |
| 91 | x="60.417" | |
| 92 | y="-8.076" | |
| 93 | colorInterpolationFilters="sRGB" | |
| 94 | filterUnits="userSpaceOnUse" | |
| 95 | > | |
| 96 | <feFlood floodOpacity="0" result="BackgroundImageFix" /> | |
| 97 | <feColorMatrix | |
| 98 | in="SourceAlpha" | |
| 99 | values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" | |
| 100 | /> | |
| 101 | <feOffset /> | |
| 102 | <feGaussianBlur stdDeviation="4.167" /> | |
| 103 | <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0" /> | |
| 104 | <feBlend in2="BackgroundImageFix" mode="overlay" result="effect1_dropShadow" /> | |
| 105 | <feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" /> | |
| 106 | </filter> | |
| 107 | <linearGradient | |
| 108 | id={overlayGradientId} | |
| 109 | x1="49.939" | |
| 110 | x2="49.939" | |
| 111 | y1=".258" | |
| 112 | y2="99.742" | |
| 113 | gradientUnits="userSpaceOnUse" | |
| 114 | > | |
| 115 | <stop stopColor="#fff" /> | |
| 116 | <stop offset="1" stopColor="#fff" stopOpacity="0" /> | |
| 117 | </linearGradient> | |
| 118 | </defs> | |
| 119 | </svg> | |
| 120 | ); | |
| 121 | }; |
packages/contracts/src/server.ts+2| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | import { Schema } from "effect"; |
| 2 | 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; |
| 3 | 3 | import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings"; |
| 4 | import { EditorId } from "./editor"; | |
| 4 | 5 | |
| 5 | 6 | export const KeybindingsMalformedConfigIssue = Schema.Struct({ |
| 6 | 7 | kind: Schema.Literal("keybindings.malformed-config"), |
| ... | ... | @@ -29,6 +30,7 @@ export const ServerConfig = Schema.Struct({ |
| 29 | 30 | keybindingsConfigPath: TrimmedNonEmptyString, |
| 30 | 31 | keybindings: ResolvedKeybindingsConfig, |
| 31 | 32 | issues: ServerConfigIssues, |
| 33 | availableEditors: Schema.Array(EditorId), | |
| 32 | 34 | }); |
| 33 | 35 | export type ServerConfig = typeof ServerConfig.Type; |
| 34 | 36 |