diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 72fa8d406..8651aa6fe 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -6,6 +6,7 @@ import { TooltipProvider } from "./components/ui/tooltip" import { TRPCProvider } from "./contexts/TRPCProvider" import { WindowProvider, getInitialWindowParams } from "./contexts/WindowContext" import { selectedProjectAtom, selectedAgentChatIdAtom } from "./features/agents/atoms" +import { getFreshSelectedProject } from "./features/agents/lib/selected-project" import { useAgentSubChatStore } from "./features/agents/stores/sub-chat-store" import { AgentsLayout } from "./features/layout/agents-layout" import { @@ -119,13 +120,7 @@ function AppContent() { // Validated project - only valid if exists in DB const validatedProject = useMemo(() => { - if (!selectedProject) return null - // While loading, trust localStorage value to prevent flicker - if (isLoadingProjects) return selectedProject - // After loading, validate against DB - if (!projects) return null - const exists = projects.some((p) => p.id === selectedProject.id) - return exists ? selectedProject : null + return getFreshSelectedProject(selectedProject, projects, isLoadingProjects) }, [selectedProject, projects, isLoadingProjects]) // Determine which page to show: diff --git a/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx index 079b05008..f8c0f6360 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-project-worktree-tab.tsx @@ -4,9 +4,9 @@ import { useAtomValue, useSetAtom } from "jotai" import { trpc } from "../../../lib/trpc" import { Button, buttonVariants } from "../../ui/button" import { Input } from "../../ui/input" -import { Plus, Trash2, FolderOpen } from "lucide-react" +import { Plus, Trash2 } from "lucide-react" import { AIPenIcon, ExternalLinkIcon, FolderFilledIcon, ImageIcon } from "../../ui/icons" -import { invalidateProjectIcon, useProjectIcon } from "../../../lib/hooks/use-project-icon" +import { invalidateProjectIcon } from "../../../lib/hooks/use-project-icon" import { ProjectIcon } from "../../ui/project-icon" import finderIcon from "../../../assets/app-icons/finder.png" import { @@ -36,11 +36,13 @@ import { import { cn } from "../../../lib/utils" import { ResizableSidebar } from "../../ui/resizable-sidebar" import { settingsProjectsSidebarWidthAtom } from "../../../features/agents/atoms" +import { toSelectedProject } from "../../../features/agents/lib/selected-project" // --- Detail Panel --- function ProjectDetail({ projectId }: { projectId: string }) { + const utils = trpc.useUtils() // Get config for selected project - const { data: configData, refetch: refetchConfig } = + const { data: configData } = trpc.worktreeConfig.get.useQuery( { projectId }, { enabled: !!projectId }, @@ -65,18 +67,32 @@ function ProjectDetail({ projectId }: { projectId: string }) { }) // Get project info - const { data: project, refetch: refetchProject } = trpc.projects.get.useQuery( + const { data: project } = trpc.projects.get.useQuery( { id: projectId }, { enabled: !!projectId }, ) - // Cached project icon - const { src: iconSrc } = useProjectIcon(project) + const syncProjectState = useCallback((updatedProject: NonNullable) => { + utils.projects.get.setData({ id: projectId }, updatedProject) + utils.projects.list.setData(undefined, (oldProjects) => { + if (!oldProjects) return [updatedProject] + const exists = oldProjects.some((candidate) => candidate.id === updatedProject.id) + if (!exists) return [updatedProject, ...oldProjects] + return oldProjects.map((candidate) => + candidate.id === updatedProject.id ? updatedProject : candidate, + ) + }) + setSelectedProject((current) => + current?.id === updatedProject.id ? toSelectedProject(updatedProject) : current, + ) + }, [projectId, setSelectedProject, utils.projects.get, utils.projects.list]) // Rename mutation const renameMutation = trpc.projects.rename.useMutation({ - onSuccess: () => { - refetchProject() + onSuccess: (updatedProject) => { + if (updatedProject) { + syncProjectState(updatedProject) + } toast.success("Project renamed") }, onError: (err) => { @@ -86,7 +102,13 @@ function ProjectDetail({ projectId }: { projectId: string }) { // Delete project mutation const deleteMutation = trpc.projects.delete.useMutation({ - onSuccess: () => { + onSuccess: (deletedProject) => { + if (deletedProject) { + utils.projects.get.setData({ id: projectId }, undefined) + utils.projects.list.setData(undefined, (oldProjects) => + oldProjects?.filter((candidate) => candidate.id !== deletedProject.id) ?? [], + ) + } toast.success("Project removed from list") setSelectedProject((current) => { if (current?.id === projectId) { @@ -105,7 +127,7 @@ function ProjectDetail({ projectId }: { projectId: string }) { onSuccess: (data) => { if (!data) return // User cancelled file picker invalidateProjectIcon(projectId) - refetchProject() + syncProjectState(data) toast.success("Icon updated") }, onError: (err) => { @@ -114,9 +136,11 @@ function ProjectDetail({ projectId }: { projectId: string }) { }) const removeIconMutation = trpc.projects.removeIcon.useMutation({ - onSuccess: () => { + onSuccess: (updatedProject) => { invalidateProjectIcon(projectId) - refetchProject() + if (updatedProject) { + syncProjectState(updatedProject) + } toast.success("Icon removed") }, }) @@ -336,15 +360,7 @@ function ProjectDetail({ projectId }: { projectId: string }) { onClick={() => uploadIconMutation.mutate({ id: projectId })} title="Click to change icon" > - {iconSrc ? ( - - ) : ( - - )} +
diff --git a/src/renderer/components/ui/project-icon.tsx b/src/renderer/components/ui/project-icon.tsx index eafcfcfc4..672533723 100644 --- a/src/renderer/components/ui/project-icon.tsx +++ b/src/renderer/components/ui/project-icon.tsx @@ -1,16 +1,19 @@ -import { useState, useCallback } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" import { FolderOpen } from "lucide-react" import { useProjectIcon } from "../../lib/hooks/use-project-icon" import { cn } from "../../lib/utils" interface ProjectIconProps { - project: { - id: string - iconPath?: string | null - updatedAt?: string | Date | null - gitOwner?: string | null - gitProvider?: string | null - } | null | undefined + project: + | { + id?: string | null + name?: string | null + gitRepo?: string | null + iconPath?: string | null + updatedAt?: string | Date | null + } + | null + | undefined className?: string } @@ -18,8 +21,17 @@ export function ProjectIcon({ project, className }: ProjectIconProps) { const { src, hasError } = useProjectIcon(project) const [imgError, setImgError] = useState(false) const handleError = useCallback(() => setImgError(true), []) + const fallbackInitial = useMemo(() => { + const label = (project?.gitRepo || project?.name || "").trim() + const initial = label.replace(/^[^a-zA-Z0-9]+/, "").charAt(0) + return initial ? initial.toUpperCase() : "?" + }, [project?.gitRepo, project?.name]) - if (!project || hasError || !src || imgError) { + useEffect(() => { + setImgError(false) + }, [src, project?.id, project?.iconPath, project?.updatedAt]) + + if (!project) { return (