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<number>; 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( <strong className="font-semibold text-foreground" key={index}> {props.value[index]} </strong>, ); start = index + 1; } if (start < props.value.length) parts.push(props.value.slice(start)); return <span className="text-muted-foreground">{parts}</span>;} 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 ( <CommandPaletteContent aria-label="File picker" escapeLabel="Back" footerActionLabel="Open file" inputProps={{ disabled: true, placeholder: "Search files…" }} mode="none" testId="project-file-picker" value="" > <div className="py-10 text-center text-sm text-muted-foreground"> Open a project to search its files. </div> </CommandPaletteContent> );} function OpenProjectFilePicker(props: ProjectFilePickerProps & { target: ActiveProjectTarget }) { const { target } = props; const [query, setQuery] = useState(""); const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(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<CommandPaletteActionItem[]>( () => matches.map((match) => ({ kind: "action", value: `file:${match.path}`, searchTerms: [match.name, match.path], title: ( <HighlightedFuzzyText active={hasMatchedQuery} value={match.name} indices={match.nameMatchIndices} /> ), description: ( <HighlightedFuzzyText active={hasMatchedQuery} value={match.path} indices={match.pathMatchIndices} /> ), icon: <PierreEntryIcon pathValue={match.path} kind="file" theme={resolvedTheme} />, run: async () => { useRightPanelStore.getState().openFile(target.threadRef, match.path); }, })), [hasMatchedQuery, matches, resolvedTheme, target.threadRef], ); const emptyStateMessage = getEmptyStateMessage(query, result.error, result.isPending); return ( <CommandPaletteContent aria-label="File picker" autoHighlight="always" escapeLabel="Back" footerActionLabel="Open file" inputProps={{ placeholder: "Search files…" }} mode="none" onItemHighlighted={(value) => { setHighlightedItemValue(typeof value === "string" ? value : null); }} onValueChange={(value) => { setHighlightedItemValue(null); setQuery(value); }} panelClassName="max-h-[min(34rem,76vh)]" testId="project-file-picker" value={query} > <CommandPaletteResults groups={ items.length > 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} /> </CommandPaletteContent> );} export function ProjectFilePicker(props: ProjectFilePickerProps) { const target = useActiveProjectTarget(); if (!target) { return <EmptyProjectFilePicker />; } return <OpenProjectFilePicker setOpen={props.setOpen} target={target} />;}
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<number>;
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(
<strong className="font-semibold text-foreground" key={index}>
{props.value[index]}
</strong>,
);
start = index + 1;
}
if (start < props.value.length) parts.push(props.value.slice(start));
return <span className="text-muted-foreground">{parts}</span>;
}
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 (
<CommandPaletteContent
aria-label="File picker"
escapeLabel="Back"
footerActionLabel="Open file"
inputProps={{ disabled: true, placeholder: "Search files…" }}
mode="none"
testId="project-file-picker"
value=""
>
<div className="py-10 text-center text-sm text-muted-foreground">
Open a project to search its files.
</div>
</CommandPaletteContent>
);
}
function OpenProjectFilePicker(props: ProjectFilePickerProps & { target: ActiveProjectTarget }) {
const { target } = props;
const [query, setQuery] = useState("");
const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(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<CommandPaletteActionItem[]>(
() =>
matches.map((match) => ({
kind: "action",
value: `file:${match.path}`,
searchTerms: [match.name, match.path],
title: (
<HighlightedFuzzyText
active={hasMatchedQuery}
value={match.name}
indices={match.nameMatchIndices}
/>
),
description: (
<HighlightedFuzzyText
active={hasMatchedQuery}
value={match.path}
indices={match.pathMatchIndices}
/>
),
icon: <PierreEntryIcon pathValue={match.path} kind="file" theme={resolvedTheme} />,
run: async () => {
useRightPanelStore.getState().openFile(target.threadRef, match.path);
},
})),
[hasMatchedQuery, matches, resolvedTheme, target.threadRef],
);
const emptyStateMessage = getEmptyStateMessage(query, result.error, result.isPending);
return (
<CommandPaletteContent
aria-label="File picker"
autoHighlight="always"
escapeLabel="Back"
footerActionLabel="Open file"
inputProps={{ placeholder: "Search files…" }}
mode="none"
onItemHighlighted={(value) => {
setHighlightedItemValue(typeof value === "string" ? value : null);
}}
onValueChange={(value) => {
setHighlightedItemValue(null);
setQuery(value);
}}
panelClassName="max-h-[min(34rem,76vh)]"
testId="project-file-picker"
value={query}
>
<CommandPaletteResults
groups={
items.length > 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}
/>
</CommandPaletteContent>
);
}
export function ProjectFilePicker(props: ProjectFilePickerProps) {
const target = useActiveProjectTarget();
if (!target) {
return <EmptyProjectFilePicker />;
}
return <OpenProjectFilePicker setOpen={props.setOpen} target={target} />;
}