Open History to see the last commit
1import { useAtomValue } from "@effect/atom-react";
2import type { EnvironmentId } from "@t3tools/contracts";
3import { isWindowsAbsolutePath } from "@t3tools/shared/path";
4import { useState } from "react";
5
6import { primaryServerKeybindingsAtom } from "~/state/server";
7import { useTheme } from "~/hooks/useTheme";
8import { getLocalFileManagerName, isWindowsPlatform } from "~/lib/utils";
9import { CommandPaletteContent } from "../CommandPaletteContent";
10import type { CommandPaletteActionItem } from "../CommandPalette.logic";
11import { CommandPaletteResults } from "../CommandPaletteResults";
12import { PierreEntryIcon } from "../chat/PierreEntryIcon";
13import {
14 getProjectFilePickerMatches,
15 PROJECT_FILE_PICKER_RESULT_LIMIT,
16} from "../files/ProjectFilePicker.logic";
17import { useProjectFilePickerQuery } from "../files/projectFilesQueryState";
18import { CommandDialog, CommandDialogPopup, CommandFooterAction } from "../ui/command";
19import { toastManager } from "../ui/toast";
20import { ProjectFaviconThumbnail } from "./ProjectFaviconThumbnail";
21
22function emptyMessage(query: string, error: string | null, isPending: boolean): string {
23 if (error) return error;
24 if (isPending) return query.trim() ? "Searching project files…" : "Indexing project files…";
25 return query.trim() ? "No matching image files." : "No image files found.";
26}
27export function canPickExternalProjectFavicon(cwd: string, platform: string): boolean {
28 return !isWindowsPlatform(platform) || isWindowsAbsolutePath(cwd);
29}
30
31export function ProjectFaviconPickerDialog(props: {
32 readonly cwd: string;
33 readonly environmentId: EnvironmentId;
34 readonly onOpenChange: (open: boolean) => void;
35 readonly onPickExternal?: () => Promise<string | null>;
36 readonly onSelect: (path: string) => void;
37 readonly open: boolean;
38 readonly projectName: string;
39}) {
40 const [query, setQuery] = useState("");
41 const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(null);
42 const [isPickingExternal, setIsPickingExternal] = useState(false);
43 const result = useProjectFilePickerQuery(
44 props.environmentId,
45 props.cwd,
46 query,
47 PROJECT_FILE_PICKER_RESULT_LIMIT,
48 { imageOnly: true },
49 );
50 const { resolvedTheme } = useTheme();
51 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
52 const { onPickExternal: pickExternal, onSelect } = props;
53 const fileManagerName = getLocalFileManagerName(
54 typeof navigator === "undefined" ? "" : navigator.platform,
55 );
56 const items: CommandPaletteActionItem[] = getProjectFilePickerMatches(
57 result.entries,
58 result.matchedQuery,
59 ).map((match) => ({
60 kind: "action",
61 value: `project-favicon:${match.path}`,
62 searchTerms: [match.name, match.path],
63 title: match.name,
64 description: match.path,
65 icon: (
66 <ProjectFaviconThumbnail
67 cwd={props.cwd}
68 environmentId={props.environmentId}
69 fallback={<PierreEntryIcon pathValue={match.path} kind="file" theme={resolvedTheme} />}
70 path={match.path}
71 />
72 ),
73 run: async () => onSelect(match.path),
74 }));
75
76 return (
77 <CommandDialog open={props.open} onOpenChange={props.onOpenChange}>
78 {props.open ? (
79 <CommandDialogPopup
80 aria-label="Choose project icon"
81 className="overflow-hidden"
82 onBackdropPointerDown={() => props.onOpenChange(false)}
83 >
84 <CommandPaletteContent
85 aria-label="Choose project icon"
86 autoHighlight="always"
87 escapeLabel="Close"
88 footerActionLabel="Select icon"
89 footerTrailing={
90 pickExternal ? (
91 <CommandFooterAction
92 disabled={isPickingExternal}
93 onClick={() => {
94 setIsPickingExternal(true);
95 void pickExternal()
96 .then((path) => {
97 if (!path) return;
98 props.onOpenChange(false);
99 onSelect(path);
100 })
101 .catch((error: unknown) => {
102 toastManager.add({
103 type: "error",
104 title: "Could not open image picker",
105 description:
106 error instanceof Error ? error.message : "An error occurred.",
107 });
108 })
109 .finally(() => setIsPickingExternal(false));
110 }}
111 >
112 {`Open in ${fileManagerName}`}
113 </CommandFooterAction>
114 ) : null
115 }
116 inputProps={{ placeholder: "Search image files…" }}
117 mode="none"
118 onItemHighlighted={(value) => {
119 setHighlightedItemValue(typeof value === "string" ? value : null);
120 }}
121 onValueChange={(value) => {
122 setHighlightedItemValue(null);
123 setQuery(value);
124 }}
125 panelSize="tall-list"
126 testId="project-favicon-picker"
127 value={query}
128 >
129 <CommandPaletteResults
130 groups={
131 items.length > 0
132 ? [{ value: "project-favicon-files", label: props.projectName, items }]
133 : []
134 }
135 highlightedItemValue={highlightedItemValue}
136 isActionsOnly={false}
137 keybindings={keybindings}
138 onExecuteItem={(item) => {
139 if (item.kind !== "action") return;
140 props.onOpenChange(false);
141 void item.run();
142 }}
143 emptyStateMessage={emptyMessage(query, result.error, result.isPending)}
144 />
145 </CommandPaletteContent>
146 </CommandDialogPopup>
147 ) : null}
148 </CommandDialog>
149 );
150}