import { useAtomValue } from "@effect/atom-react"; import { useMemo, useState, type ReactNode } from "react"; import { useActiveProjectTarget, type ActiveProjectTarget } from "~/hooks/useActiveProjectTarget"; import { useTheme } from "~/hooks/useTheme"; import { useRightPanelStore } from "~/rightPanelStore"; import { primaryServerKeybindingsAtom } from "~/state/server"; import { PierreEntryIcon } from "../chat/PierreEntryIcon"; import { CommandPaletteContent } from "../CommandPaletteContent"; import { type CommandPaletteActionItem } from "../CommandPalette.logic"; import { CommandPaletteResults } from "../CommandPaletteResults"; import { getProjectFilePickerMatches, PROJECT_FILE_PICKER_RESULT_LIMIT, } from "./ProjectFilePicker.logic"; import { useProjectFilePickerQuery } from "./projectFilesQueryState"; interface ProjectFilePickerProps { readonly setOpen: (open: boolean) => void; } function HighlightedFuzzyText(props: { readonly active: boolean; readonly indices: ReadonlyArray; readonly value: string; }) { if (!props.active) return props.value; const parts: ReactNode[] = []; let start = 0; for (const index of props.indices) { if (start < index) parts.push(props.value.slice(start, index)); parts.push( {props.value[index]} , ); start = index + 1; } if (start < props.value.length) parts.push(props.value.slice(start)); return {parts}; } function getEmptyStateMessage(query: string, error: string | null, isPending: boolean): string { if (error) return error; const isSearching = query.trim().length > 0; if (isPending) return isSearching ? "Searching workspace files…" : "Indexing workspace files…"; return isSearching ? "No matching files." : "No files found."; } function EmptyProjectFilePicker() { return (
Open a project to search its files.
); } function OpenProjectFilePicker(props: ProjectFilePickerProps & { target: ActiveProjectTarget }) { const { target } = props; const [query, setQuery] = useState(""); const [highlightedItemValue, setHighlightedItemValue] = useState(null); const result = useProjectFilePickerQuery( target.environmentId, target.cwd, query, PROJECT_FILE_PICKER_RESULT_LIMIT, ); const { resolvedTheme } = useTheme(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const matches = useMemo( () => getProjectFilePickerMatches(result.entries, result.matchedQuery), [result.entries, result.matchedQuery], ); const hasMatchedQuery = /\S/.test(result.matchedQuery); const items = useMemo( () => matches.map((match) => ({ kind: "action", value: `file:${match.path}`, searchTerms: [match.name, match.path], title: ( ), description: ( ), icon: , run: async () => { useRightPanelStore.getState().openFile(target.threadRef, match.path); }, })), [hasMatchedQuery, matches, resolvedTheme, target.threadRef], ); const emptyStateMessage = getEmptyStateMessage(query, result.error, result.isPending); return ( { setHighlightedItemValue(typeof value === "string" ? value : null); }} onValueChange={(value) => { setHighlightedItemValue(null); setQuery(value); }} panelClassName="max-h-[min(34rem,76vh)]" testId="project-file-picker" value={query} > 0 ? [{ value: "project-files", label: target.projectName, items }] : [] } highlightedItemValue={highlightedItemValue} isActionsOnly={false} keybindings={keybindings} onExecuteItem={(item) => { if (item.kind !== "action") return; props.setOpen(false); void item.run(); }} emptyStateMessage={emptyStateMessage} /> ); } export function ProjectFilePicker(props: ProjectFilePickerProps) { const target = useActiveProjectTarget(); if (!target) { return ; } return ; }