| author | |
| committer | |
| log | fb3d165d33b155038de717851207d25fb52fe65e |
| tree | 582ef09c058a76caab58787806d8db29dbc3c8ef |
| parent | 211618fd9fe39d3dde01171a6856ce9f633571c9 |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 71 insertions(+), 8 deletions(-)
apps/web/src/components/Icons.tsx+28| ... | ... | @@ -2,6 +2,34 @@ import React, { type SVGProps, useId } from "react"; |
| 2 | 2 | import { cn } from "~/lib/utils"; |
| 3 | 3 | export type Icon = React.FC<SVGProps<SVGSVGElement>>; |
| 4 | 4 | |
| 5 | export const FinderIcon: Icon = (props) => ( | |
| 6 | <svg viewBox="0 0 24 24" fill="none" {...props}> | |
| 7 | <rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" /> | |
| 8 | <path | |
| 9 | d="M13 2h5a4 4 0 0 1 4 4v12a4 4 0 0 1-4 4h-6c-1-4-1-7 0-10H9c0-4 2-8 4-10Z" | |
| 10 | fill="#D9F1FF" | |
| 11 | /> | |
| 12 | <path | |
| 13 | d="M7 7v2m10-2v2M6 15c3 3 9 3 12 0" | |
| 14 | stroke="#163A59" | |
| 15 | strokeWidth="1.5" | |
| 16 | strokeLinecap="round" | |
| 17 | /> | |
| 18 | </svg> | |
| 19 | ); | |
| 20 | ||
| 21 | export const FileExplorerIcon: Icon = (props) => ( | |
| 22 | <svg viewBox="0 0 24 24" fill="none" {...props}> | |
| 23 | <path | |
| 24 | d="M2 5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2Z" | |
| 25 | fill="#D99A16" | |
| 26 | /> | |
| 27 | <path d="M2 9h20v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2Z" fill="#FFCE45" /> | |
| 28 | <path d="M8 14h8v7H8Z" fill="#58B8E8" /> | |
| 29 | <path d="M10 16h4v2h-4Z" fill="#1879B9" /> | |
| 30 | </svg> | |
| 31 | ); | |
| 32 | ||
| 5 | 33 | // Apple brand mark from Simple Icons (CC0). |
| 6 | 34 | export const AppleIcon: Icon = (props) => ( |
| 7 | 35 | <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}> |
apps/web/src/components/chat/OpenInPicker.test.ts created+26| ... | ... | @@ -0,0 +1,26 @@ |
| 1 | import { FolderClosedIcon } from "lucide-react"; | |
| 2 | import { describe, expect, it } from "vite-plus/test"; | |
| 3 | ||
| 4 | import { FileExplorerIcon, FinderIcon } from "../Icons"; | |
| 5 | import { resolveOpenInOptions } from "./OpenInPicker"; | |
| 6 | ||
| 7 | describe("resolveOpenInOptions", () => { | |
| 8 | it.each([ | |
| 9 | ["MacIntel", "Finder", FinderIcon], | |
| 10 | ["Win32", "File Explorer", FileExplorerIcon], | |
| 11 | ["Linux x86_64", "Files", FolderClosedIcon], | |
| 12 | ] as const)("includes the file manager with its icon on %s", (platform, label, Icon) => { | |
| 13 | expect(resolveOpenInOptions(platform, ["cursor", "vscode", "file-manager"])).toEqual([ | |
| 14 | expect.objectContaining({ value: "cursor", label: "Cursor" }), | |
| 15 | expect.objectContaining({ value: "vscode", label: "VS Code" }), | |
| 16 | expect.objectContaining({ value: "file-manager", label, Icon }), | |
| 17 | ]); | |
| 18 | }); | |
| 19 | ||
| 20 | it("omits the file manager when unavailable or using remote editors", () => { | |
| 21 | expect(resolveOpenInOptions("MacIntel", ["vscode"])).toEqual([ | |
| 22 | expect.objectContaining({ value: "vscode" }), | |
| 23 | ]); | |
| 24 | expect(resolveOpenInOptions("MacIntel", [])).toEqual([]); | |
| 25 | }); | |
| 26 | }); |
apps/web/src/components/chat/OpenInPicker.tsx+14-5| ... | ... | @@ -22,6 +22,8 @@ import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu |
| 22 | 22 | import { |
| 23 | 23 | AntigravityIcon, |
| 24 | 24 | CursorIcon, |
| 25 | FileExplorerIcon, | |
| 26 | FinderIcon, | |
| 25 | 27 | Icon, |
| 26 | 28 | KiroIcon, |
| 27 | 29 | TraeIcon, |
| ... | ... | @@ -44,7 +46,7 @@ import { |
| 44 | 46 | RustRoverIcon, |
| 45 | 47 | WebStormIcon, |
| 46 | 48 | } from "../JetBrainsIcons"; |
| 47 | import { cn } from "~/lib/utils"; | |
| 49 | import { cn, isMacPlatform, isWindowsPlatform } from "~/lib/utils"; | |
| 48 | 50 | import { shellEnvironment } from "~/state/shell"; |
| 49 | 51 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 50 | 52 | |
| ... | ... | @@ -55,7 +57,10 @@ type OpenInOption = { |
| 55 | 57 | kind: "brand" | "generic"; |
| 56 | 58 | }; |
| 57 | 59 | |
| 58 | const resolveOptions = (platform: string, availableEditors: ReadonlyArray<EditorId>) => { | |
| 60 | export const resolveOpenInOptions = ( | |
| 61 | platform: string, | |
| 62 | availableEditors: ReadonlyArray<EditorId>, | |
| 63 | ) => { | |
| 59 | 64 | const baseOptions: ReadonlyArray<Omit<OpenInOption, "label">> = [ |
| 60 | 65 | { |
| 61 | 66 | Icon: CursorIcon, |
| ... | ... | @@ -158,9 +163,13 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor |
| 158 | 163 | kind: "brand", |
| 159 | 164 | }, |
| 160 | 165 | { |
| 161 | Icon: FolderClosedIcon, | |
| 166 | Icon: isMacPlatform(platform) | |
| 167 | ? FinderIcon | |
| 168 | : isWindowsPlatform(platform) | |
| 169 | ? FileExplorerIcon | |
| 170 | : FolderClosedIcon, | |
| 162 | 171 | value: "file-manager", |
| 163 | kind: "generic", | |
| 172 | kind: isMacPlatform(platform) || isWindowsPlatform(platform) ? "brand" : "generic", | |
| 164 | 173 | }, |
| 165 | 174 | ]; |
| 166 | 175 | const availableEditorSet = new Set(availableEditors); |
| ... | ... | @@ -198,7 +207,7 @@ export const OpenInPicker = memo(function OpenInPicker({ |
| 198 | 207 | const effectiveEditors = remote.mode === "local-exec" ? availableEditors : remoteCapableEditors; |
| 199 | 208 | const [preferredEditor, setPreferredEditor] = usePreferredEditor(effectiveEditors); |
| 200 | 209 | const options = useMemo( |
| 201 | () => resolveOptions(navigator.platform, effectiveEditors), | |
| 210 | () => resolveOpenInOptions(navigator.platform, effectiveEditors), | |
| 202 | 211 | [effectiveEditors], |
| 203 | 212 | ); |
| 204 | 213 | const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null; |
apps/web/src/editorLabels.test.ts+1-1| ... | ... | @@ -10,7 +10,7 @@ describe("editorLabelForPlatform", () => { |
| 10 | 10 | |
| 11 | 11 | it.each([ |
| 12 | 12 | ["MacIntel", "Finder"], |
| 13 | ["Win32", "Explorer"], | |
| 13 | ["Win32", "File Explorer"], | |
| 14 | 14 | ["Linux x86_64", "Files"], |
| 15 | 15 | ])("uses the platform file-manager name on %s", (platform, label) => { |
| 16 | 16 | expect(editorLabelForPlatform("file-manager", platform)).toBe(label); |
apps/web/src/lib/utils.test.ts+1-1| ... | ... | @@ -4,7 +4,7 @@ import { getLocalFileManagerName, isWindowsPlatform } from "./utils"; |
| 4 | 4 | describe("getLocalFileManagerName", () => { |
| 5 | 5 | it.each([ |
| 6 | 6 | ["MacIntel", "Finder"], |
| 7 | ["Win32", "Explorer"], | |
| 7 | ["Win32", "File Explorer"], | |
| 8 | 8 | ["Linux", "Files"], |
| 9 | 9 | ])("uses the %s file manager name", (platform, expected) => { |
| 10 | 10 | assert.strictEqual(getLocalFileManagerName(platform), expected); |
apps/web/src/lib/utils.ts+1-1| ... | ... | @@ -25,7 +25,7 @@ export function getLocalFileManagerName(platform: string): string { |
| 25 | 25 | return "Finder"; |
| 26 | 26 | } |
| 27 | 27 | if (isWindowsPlatform(platform)) { |
| 28 | return "Explorer"; | |
| 28 | return "File Explorer"; | |
| 29 | 29 | } |
| 30 | 30 | return "Files"; |
| 31 | 31 | } |