authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-11 17:17:59+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-11 11:17:59-03:00
logfb3d165d33b155038de717851207d25fb52fe65e
tree582ef09c058a76caab58787806d8db29dbc3c8ef
parent211618fd9fe39d3dde01171a6856ce9f633571c9
signature Signed by PGP key B5690EEEBB952194

fix(web): show platform file manager icons in Open menu (#11228)


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,6 +2,34 @@ import React, { type SVGProps, useId } from "react";
2import { cn } from "~/lib/utils";2import { cn } from "~/lib/utils";
3export type Icon = React.FC<SVGProps<SVGSVGElement>>;3export type Icon = React.FC<SVGProps<SVGSVGElement>>;
44
5export 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
21export 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// Apple brand mark from Simple Icons (CC0).33// Apple brand mark from Simple Icons (CC0).
6export const AppleIcon: Icon = (props) => (34export const AppleIcon: Icon = (props) => (
7 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>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 @@
1import { FolderClosedIcon } from "lucide-react";
2import { describe, expect, it } from "vite-plus/test";
3
4import { FileExplorerIcon, FinderIcon } from "../Icons";
5import { resolveOpenInOptions } from "./OpenInPicker";
6
7describe("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,6 +22,8 @@ import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "../ui/menu
22import {22import {
23 AntigravityIcon,23 AntigravityIcon,
24 CursorIcon,24 CursorIcon,
25 FileExplorerIcon,
26 FinderIcon,
25 Icon,27 Icon,
26 KiroIcon,28 KiroIcon,
27 TraeIcon,29 TraeIcon,
...@@ -44,7 +46,7 @@ import {...@@ -44,7 +46,7 @@ import {
44 RustRoverIcon,46 RustRoverIcon,
45 WebStormIcon,47 WebStormIcon,
46} from "../JetBrainsIcons";48} from "../JetBrainsIcons";
47import { cn } from "~/lib/utils";49import { cn, isMacPlatform, isWindowsPlatform } from "~/lib/utils";
48import { shellEnvironment } from "~/state/shell";50import { shellEnvironment } from "~/state/shell";
49import { useAtomCommand } from "~/state/use-atom-command";51import { useAtomCommand } from "~/state/use-atom-command";
5052
...@@ -55,7 +57,10 @@ type OpenInOption = {...@@ -55,7 +57,10 @@ type OpenInOption = {
55 kind: "brand" | "generic";57 kind: "brand" | "generic";
56};58};
5759
58const resolveOptions = (platform: string, availableEditors: ReadonlyArray<EditorId>) => {60export const resolveOpenInOptions = (
61 platform: string,
62 availableEditors: ReadonlyArray<EditorId>,
63) => {
59 const baseOptions: ReadonlyArray<Omit<OpenInOption, "label">> = [64 const baseOptions: ReadonlyArray<Omit<OpenInOption, "label">> = [
60 {65 {
61 Icon: CursorIcon,66 Icon: CursorIcon,
...@@ -158,9 +163,13 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor...@@ -158,9 +163,13 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor
158 kind: "brand",163 kind: "brand",
159 },164 },
160 {165 {
161 Icon: FolderClosedIcon,166 Icon: isMacPlatform(platform)
167 ? FinderIcon
168 : isWindowsPlatform(platform)
169 ? FileExplorerIcon
170 : FolderClosedIcon,
162 value: "file-manager",171 value: "file-manager",
163 kind: "generic",172 kind: isMacPlatform(platform) || isWindowsPlatform(platform) ? "brand" : "generic",
164 },173 },
165 ];174 ];
166 const availableEditorSet = new Set(availableEditors);175 const availableEditorSet = new Set(availableEditors);
...@@ -198,7 +207,7 @@ export const OpenInPicker = memo(function OpenInPicker({...@@ -198,7 +207,7 @@ export const OpenInPicker = memo(function OpenInPicker({
198 const effectiveEditors = remote.mode === "local-exec" ? availableEditors : remoteCapableEditors;207 const effectiveEditors = remote.mode === "local-exec" ? availableEditors : remoteCapableEditors;
199 const [preferredEditor, setPreferredEditor] = usePreferredEditor(effectiveEditors);208 const [preferredEditor, setPreferredEditor] = usePreferredEditor(effectiveEditors);
200 const options = useMemo(209 const options = useMemo(
201 () => resolveOptions(navigator.platform, effectiveEditors),210 () => resolveOpenInOptions(navigator.platform, effectiveEditors),
202 [effectiveEditors],211 [effectiveEditors],
203 );212 );
204 const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null;213 const primaryOption = options.find(({ value }) => value === preferredEditor) ?? null;
apps/web/src/editorLabels.test.ts+1-1
...@@ -10,7 +10,7 @@ describe("editorLabelForPlatform", () => {...@@ -10,7 +10,7 @@ describe("editorLabelForPlatform", () => {
1010
11 it.each([11 it.each([
12 ["MacIntel", "Finder"],12 ["MacIntel", "Finder"],
13 ["Win32", "Explorer"],13 ["Win32", "File Explorer"],
14 ["Linux x86_64", "Files"],14 ["Linux x86_64", "Files"],
15 ])("uses the platform file-manager name on %s", (platform, label) => {15 ])("uses the platform file-manager name on %s", (platform, label) => {
16 expect(editorLabelForPlatform("file-manager", platform)).toBe(label);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,7 +4,7 @@ import { getLocalFileManagerName, isWindowsPlatform } from "./utils";
4describe("getLocalFileManagerName", () => {4describe("getLocalFileManagerName", () => {
5 it.each([5 it.each([
6 ["MacIntel", "Finder"],6 ["MacIntel", "Finder"],
7 ["Win32", "Explorer"],7 ["Win32", "File Explorer"],
8 ["Linux", "Files"],8 ["Linux", "Files"],
9 ])("uses the %s file manager name", (platform, expected) => {9 ])("uses the %s file manager name", (platform, expected) => {
10 assert.strictEqual(getLocalFileManagerName(platform), expected);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,7 +25,7 @@ export function getLocalFileManagerName(platform: string): string {
25 return "Finder";25 return "Finder";
26 }26 }
27 if (isWindowsPlatform(platform)) {27 if (isWindowsPlatform(platform)) {
28 return "Explorer";28 return "File Explorer";
29 }29 }
30 return "Files";30 return "Files";
31}31}