From b9cbe71adbec9e9f92fabbc54bd0bea09b08f185 Mon Sep 17 00:00:00 2001 From: Hooman <69511785+hoomanaskari@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:10:38 -0400 Subject: [PATCH] Fix project icon syncing and fallbacks Ensure project icon updates propagate across the app when a custom icon is uploaded or removed from project settings. This change refreshes selected project state from the latest project records, replaces GitHub avatar fallback with a repo-initial fallback avatar, and loads custom icons through the binary file reader so icons stored under the app userData path render reliably. It also updates sidebar icon sizing and fallback avatar styling for a clearer, more consistent workspace list. --- src/renderer/App.tsx | 9 +- .../agents-project-worktree-tab.tsx | 58 ++++--- src/renderer/components/ui/project-icon.tsx | 46 +++-- src/renderer/features/agents/atoms/index.ts | 2 + .../agents/components/agent-chat-card.tsx | 103 +++-------- .../components/agents-quick-switch-dialog.tsx | 14 +- .../agents/components/project-selector.tsx | 55 +----- src/renderer/features/agents/lib/drafts.ts | 3 +- .../features/agents/lib/selected-project.ts | 58 +++++++ .../features/agents/main/new-chat-form.tsx | 29 +--- .../features/agents/ui/archive-popover.tsx | 83 ++++----- .../features/layout/agents-layout.tsx | 11 +- .../features/onboarding/select-repo-page.tsx | 29 +--- .../features/sidebar/agents-sidebar.tsx | 160 +++++++++--------- src/renderer/lib/hooks/use-project-icon.ts | 86 +++++----- 15 files changed, 349 insertions(+), 397 deletions(-) create mode 100644 src/renderer/features/agents/lib/selected-project.ts 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 (