import { useCallback, useEffect, useState, type ReactNode } from "react"; import { AlertTriangle, Check, ChevronDown, Folder, Hand } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import type { WorkspaceAccessMode, WorkspaceScopePayload, WorkspacesPayload, } from "@/lib/types"; import { getHostApi } from "@/lib/runtime"; import { cn } from "@/lib/utils"; import { isAbsoluteWorkspacePath, projectNameFromPath, scopeWithAccessMode, selectedProjectScope, shortWorkspacePath, } from "@/lib/workspace"; export function WorkspaceProjectPicker({ isHero, disabled, scope, defaultScope, controls, error, onChange, }: { isHero: boolean; disabled?: boolean; scope: WorkspaceScopePayload | null; defaultScope: WorkspaceScopePayload | null; controls: WorkspacesPayload["controls"] | null; error?: string | null; onChange?: (scope: WorkspaceScopePayload) => void; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [pathDraft, setPathDraft] = useState(""); const [pathError, setPathError] = useState(null); const [pickingFolder, setPickingFolder] = useState(false); const currentProjectScope = selectedProjectScope(scope, defaultScope); const projectLabel = currentProjectScope ? currentProjectScope.project_name || projectNameFromPath(currentProjectScope.project_path) : t("thread.composer.workspace.projectPlaceholder"); const visible = isHero && !!defaultScope && !!onChange && controls?.can_change_project !== false; const hostApi = getHostApi(); const nativeProjectPicker = !!hostApi; useEffect(() => { if (!open) return; setPathDraft(currentProjectScope?.project_path ?? ""); setPathError(null); }, [currentProjectScope?.project_path, open]); useEffect(() => { if (error && visible) setOpen(true); }, [error, visible]); const applyProjectPath = useCallback( (projectPath: string, projectName?: string) => { const base = scope ?? defaultScope; const trimmed = projectPath.trim(); if (!base || !onChange) return; if (!trimmed || !isAbsoluteWorkspacePath(trimmed)) { setPathError(t("workspace.dialog.absolutePathRequired")); return; } onChange({ ...base, project_path: trimmed, project_name: projectName || projectNameFromPath(trimmed), restrict_to_workspace: base.access_mode === "restricted", }); setPathError(null); setOpen(false); }, [defaultScope, onChange, scope, t], ); const pickNativeFolder = useCallback(async () => { if (!hostApi || disabled) return; setPickingFolder(true); try { const picked = await hostApi.pickFolder(); if (picked) applyProjectPath(picked); } catch (err) { setPathError((err as Error).message); } finally { setPickingFolder(false); } }, [applyProjectPath, disabled, hostApi]); if (!visible || !defaultScope || !onChange) return null; if (nativeProjectPicker) { return (
{pathError || error ? ( {pathError ?? error} ) : null}
); } return (
applyProjectPath(defaultScope.project_path, defaultScope.project_name)} className="flex min-h-[48px] cursor-default gap-3 rounded-[16px] px-3 py-2.5 focus:bg-muted/55" > {t("workspace.dialog.defaultProject")} {shortWorkspacePath(defaultScope.project_path)} {!currentProjectScope ? : null}
{ if (event.key !== "Escape") event.stopPropagation(); }} >
{ event.preventDefault(); applyProjectPath(pathDraft); }} > { setPathDraft(event.target.value); setPathError(null); }} placeholder={t("workspace.dialog.manualPlaceholder")} aria-label={t("workspace.dialog.manual")} className={cn( "h-9 rounded-full border-border/55 bg-background/80 px-3 text-[12.5px]", "focus-visible:ring-1 focus-visible:ring-foreground/10 focus-visible:ring-offset-0", )} />
{pathError || error ? (

{pathError ?? error}

) : null}
); } export function WorkspaceAccessMenu({ scope, disabled, canUseFullAccess, isHero, onChange, }: { scope: WorkspaceScopePayload; disabled?: boolean; canUseFullAccess: boolean; isHero: boolean; onChange?: (scope: WorkspaceScopePayload) => void; }) { const { t } = useTranslation(); const mode = scope.access_mode; const isFull = mode === "full"; const setMode = (value: WorkspaceAccessMode) => { if (value === "full" && !canUseFullAccess) return; if (value === mode) return; onChange?.(scopeWithAccessMode(scope, value)); }; return ( } label={t("thread.composer.workspace.default")} selected={mode === "restricted"} onSelect={() => setMode("restricted")} /> } label={t("thread.composer.workspace.full")} selected={mode === "full"} disabled={!canUseFullAccess} warning onSelect={() => setMode("full")} /> ); } function AccessMenuItem({ icon, label, selected, disabled, warning, onSelect, }: { icon: ReactNode; label: string; selected: boolean; disabled?: boolean; warning?: boolean; onSelect: () => void; }) { return ( {icon} {label} {selected ? : null} ); }