Open History to see the last commit
1import { useAtomValue } from "@effect/atom-react";
2import { useState, type ReactNode } from "react";
3
4import { useActiveProjectTarget, type ActiveProjectTarget } from "~/hooks/useActiveProjectTarget";
5import { useTheme } from "~/hooks/useTheme";
6import { useRightPanelStore } from "~/rightPanelStore";
7import { primaryServerKeybindingsAtom } from "~/state/server";
8
9import { PierreEntryIcon } from "../chat/PierreEntryIcon";
10import { CommandPaletteContent } from "../CommandPaletteContent";
11import { type CommandPaletteActionItem } from "../CommandPalette.logic";
12import { CommandPaletteResults } from "../CommandPaletteResults";
13import {
14 getProjectFilePickerMatches,
15 PROJECT_FILE_PICKER_RESULT_LIMIT,
16} from "./ProjectFilePicker.logic";
17import { useProjectFilePickerQuery } from "./projectFilesQueryState";
18
19interface ProjectFilePickerProps {
20 readonly setOpen: (open: boolean) => void;
21}
22
23function HighlightedFuzzyText(props: {
24 readonly active: boolean;
25 readonly indices: ReadonlyArray<number>;
26 readonly value: string;
27}) {
28 if (!props.active) return props.value;
29
30 const parts: ReactNode[] = [];
31 let start = 0;
32 for (const index of props.indices) {
33 if (start < index) parts.push(props.value.slice(start, index));
34 parts.push(
35 <strong className="font-semibold text-foreground" key={index}>
36 {props.value[index]}
37 </strong>,
38 );
39 start = index + 1;
40 }
41 if (start < props.value.length) parts.push(props.value.slice(start));
42
43 return <span className="text-muted-foreground">{parts}</span>;
44}
45
46function getEmptyStateMessage(query: string, error: string | null, isPending: boolean): string {
47 if (error) return error;
48 const isSearching = query.trim().length > 0;
49 if (isPending) return isSearching ? "Searching workspace files…" : "Indexing workspace files…";
50 return isSearching ? "No matching files." : "No files found.";
51}
52
53function EmptyProjectFilePicker() {
54 return (
55 <CommandPaletteContent
56 aria-label="File picker"
57 escapeLabel="Back"
58 footerActionLabel="Open file"
59 inputProps={{ disabled: true, placeholder: "Search files…" }}
60 mode="none"
61 testId="project-file-picker"
62 value=""
63 >
64 <div className="py-10 text-center text-sm text-muted-foreground">
65 Open a project to search its files.
66 </div>
67 </CommandPaletteContent>
68 );
69}
70
71function OpenProjectFilePicker(props: ProjectFilePickerProps & { target: ActiveProjectTarget }) {
72 const { target } = props;
73 const [query, setQuery] = useState("");
74 const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(null);
75 const result = useProjectFilePickerQuery(
76 target.environmentId,
77 target.cwd,
78 query,
79 PROJECT_FILE_PICKER_RESULT_LIMIT,
80 );
81 const { resolvedTheme } = useTheme();
82 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
83 const matches = getProjectFilePickerMatches(result.entries, result.matchedQuery);
84 const hasMatchedQuery = /\S/.test(result.matchedQuery);
85 const items: CommandPaletteActionItem[] = matches.map((match) => ({
86 kind: "action",
87 value: `file:${match.path}`,
88 searchTerms: [match.name, match.path],
89 title: (
90 <HighlightedFuzzyText
91 active={hasMatchedQuery}
92 value={match.name}
93 indices={match.nameMatchIndices}
94 />
95 ),
96 description: (
97 <HighlightedFuzzyText
98 active={hasMatchedQuery}
99 value={match.path}
100 indices={match.pathMatchIndices}
101 />
102 ),
103 icon: <PierreEntryIcon pathValue={match.path} kind="file" theme={resolvedTheme} />,
104 run: async () => {
105 useRightPanelStore.getState().openFile(target.threadRef, match.path);
106 },
107 }));
108
109 const emptyStateMessage = getEmptyStateMessage(query, result.error, result.isPending);
110
111 return (
112 <CommandPaletteContent
113 aria-label="File picker"
114 autoHighlight="always"
115 escapeLabel="Back"
116 footerActionLabel="Open file"
117 inputProps={{ placeholder: "Search files…" }}
118 mode="none"
119 onItemHighlighted={(value) => {
120 setHighlightedItemValue(typeof value === "string" ? value : null);
121 }}
122 onValueChange={(value) => {
123 setHighlightedItemValue(null);
124 setQuery(value);
125 }}
126 panelSize="tall-list"
127 testId="project-file-picker"
128 value={query}
129 >
130 <CommandPaletteResults
131 groups={
132 items.length > 0 ? [{ value: "project-files", label: target.projectName, items }] : []
133 }
134 highlightedItemValue={highlightedItemValue}
135 isActionsOnly={false}
136 keybindings={keybindings}
137 onExecuteItem={(item) => {
138 if (item.kind !== "action") return;
139 props.setOpen(false);
140 void item.run();
141 }}
142 emptyStateMessage={emptyStateMessage}
143 />
144 </CommandPaletteContent>
145 );
146}
147
148export function ProjectFilePicker(props: ProjectFilePickerProps) {
149 const target = useActiveProjectTarget();
150
151 if (!target) {
152 return <EmptyProjectFilePicker />;
153 }
154
155 return <OpenProjectFilePicker setOpen={props.setOpen} target={target} />;
156}