"use client"; import { useState, useCallback, useEffect, useRef } from "react"; import { IconListCheck, IconFile, IconFolder, IconSearch, IconAt } from "@tabler/icons-react"; import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover"; import { Checkbox } from "@kandev/ui/checkbox"; import { Input } from "@kandev/ui/input"; import { getWebSocketClient } from "@/lib/ws/connection"; import { searchWorkspaceFiles } from "@/lib/ws/workspace-files"; import { useCustomPrompts } from "@/hooks/domains/settings/use-custom-prompts"; import { isDirectory, getFileName } from "@/lib/utils/file-path"; import type { ContextFile } from "@/lib/state/context-files-store"; type ContextPopoverProps = { open: boolean; onOpenChange: (open: boolean) => void; trigger: React.ReactNode; sessionId: string | null; /** Whether plan is included as context (from context files store, not plan panel) */ planContextEnabled: boolean; contextFiles: ContextFile[]; onToggleFile: (file: ContextFile) => void; }; const FILE_SEARCH_DEBOUNCE = 300; const PLAN_CONTEXT_FILE: ContextFile = { path: "plan:context", name: "Plan", pinned: true }; function FileResultsList({ isLoading, fileResults, query, filteredPromptsEmpty, isFileSelected, onToggle, }: { isLoading: boolean; fileResults: string[]; query: string; filteredPromptsEmpty: boolean; isFileSelected: (path: string) => boolean; onToggle: (filePath: string) => void; }) { if (isLoading) { return
Loading...
; } if (fileResults.length === 0 && query && filteredPromptsEmpty) { return (
No results found
); } return ( <> {fileResults.map((filePath) => { const isDir = isDirectory(filePath); const name = getFileName(filePath); const parent = filePath.slice(0, filePath.length - name.length); return (
onToggle(filePath)} > onToggle(filePath)} className="h-3.5 w-3.5" /> {isDir ? ( ) : ( )}
{name} {parent && ( {parent} )}
); })} ); } type PromptsSectionProps = { query: string; prompts: { id: string; name: string; content?: string }[]; contextFiles: ContextFile[]; onToggleFile: (file: ContextFile) => void; }; function PromptsSection({ query, prompts, contextFiles, onToggleFile }: PromptsSectionProps) { if (prompts.length === 0) return null; return ( <> {!query && (

Prompts

)} {prompts.map((prompt) => (
onToggleFile({ path: `prompt:${prompt.id}`, name: prompt.name, pinned: true }) } > f.path === `prompt:${prompt.id}`)} onCheckedChange={() => onToggleFile({ path: `prompt:${prompt.id}`, name: prompt.name, pinned: true }) } className="h-3.5 w-3.5" />
{prompt.name} {prompt.content && ( {prompt.content.length > 60 ? prompt.content.slice(0, 60) + "..." : prompt.content} )}
))} ); } function useContextPopoverState(open: boolean, sessionId: string | null) { const [query, setQuery] = useState(""); const [fileResults, setFileResults] = useState([]); const [isLoading, setIsLoading] = useState(false); const searchTimeoutRef = useRef | null>(null); const inputRef = useRef(null); /* eslint-disable react-hooks/set-state-in-effect -- resetting state on open/close is intentional */ useEffect(() => { if (open) { requestAnimationFrame(() => inputRef.current?.focus()); } else { setQuery(""); setFileResults([]); } }, [open]); /* eslint-enable react-hooks/set-state-in-effect */ /* eslint-disable react-hooks/set-state-in-effect -- loading state sync is intentional for UX */ useEffect(() => { if (!open || !sessionId) return; if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); const delay = query === "" ? 0 : FILE_SEARCH_DEBOUNCE; setIsLoading(true); let cancelled = false; searchTimeoutRef.current = setTimeout(async () => { try { const client = getWebSocketClient(); if (!client) { if (!cancelled) { setFileResults([]); setIsLoading(false); } return; } const response = await searchWorkspaceFiles(client, sessionId, query || "", 20); if (!cancelled) setFileResults(response.files || []); } catch { if (!cancelled) setFileResults([]); } if (!cancelled) setIsLoading(false); }, delay); return () => { cancelled = true; if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); }; }, [open, sessionId, query]); /* eslint-enable react-hooks/set-state-in-effect */ return { query, setQuery, fileResults, isLoading, inputRef }; } function ContextPopoverList({ query, fileResults, isLoading, planContextEnabled, filteredPrompts, contextFiles, onToggleFile, isFileSelected, handleToggleFile, }: { query: string; fileResults: string[]; isLoading: boolean; planContextEnabled: boolean; filteredPrompts: { id: string; name: string; content?: string }[]; contextFiles: ContextFile[]; onToggleFile: (file: ContextFile) => void; isFileSelected: (path: string) => boolean; handleToggleFile: (filePath: string) => void; }) { return (
{!query && (
onToggleFile(PLAN_CONTEXT_FILE)} > onToggleFile(PLAN_CONTEXT_FILE)} className="h-3.5 w-3.5" /> Plan {planContextEnabled && ( ACTIVE )}
)} {!query && fileResults.length > 0 && (

Files

)}
); } export function ContextPopover({ open, onOpenChange, trigger, sessionId, planContextEnabled, contextFiles, onToggleFile, }: ContextPopoverProps) { const { query, setQuery, fileResults, isLoading, inputRef } = useContextPopoverState( open, sessionId, ); const { prompts } = useCustomPrompts(); const handleToggleFile = useCallback( (filePath: string) => { const name = getFileName(filePath); onToggleFile({ path: filePath, name, pinned: true }); }, [onToggleFile], ); const isFileSelected = useCallback( (path: string) => contextFiles.some((f) => f.path === path), [contextFiles], ); const filteredPrompts = query ? prompts.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())) : prompts; return ( {trigger} e.preventDefault()} >

Context

ยท Select files and prompts to include

setQuery(e.target.value)} placeholder="Search files and prompts..." className="h-7 pl-7 text-xs" />
); }