Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 },
Expand All @@ -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<typeof project>) => {
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) => {
Expand All @@ -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) {
Expand All @@ -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) => {
Expand All @@ -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")
},
})
Expand Down Expand Up @@ -336,15 +360,7 @@ function ProjectDetail({ projectId }: { projectId: string }) {
onClick={() => uploadIconMutation.mutate({ id: projectId })}
title="Click to change icon"
>
{iconSrc ? (
<img
src={iconSrc}
alt=""
className="h-full w-full object-cover"
/>
) : (
<FolderOpen className="h-5 w-5 text-muted-foreground" />
)}
<ProjectIcon project={project} className="h-full w-full" />
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover/icon:opacity-100 transition-opacity duration-150">
<ImageIcon className="h-4 w-4 text-white" />
</div>
Expand Down
46 changes: 36 additions & 10 deletions src/renderer/components/ui/project-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,62 @@
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
}

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 (
<FolderOpen
className={cn("text-muted-foreground flex-shrink-0", className)}
/>
)
}

if (hasError || !src || imgError) {
return (
<div
aria-hidden="true"
className={cn(
"rounded-sm bg-input-background border text-muted-foreground flex-shrink-0 flex items-center justify-center font-medium uppercase",
className,
)}
>
<span className="text-[0.65em] leading-none">{fallbackInitial}</span>
</div>
)
}

return (
<img
src={src}
alt=""
alt={project.gitRepo || project.name || "Project icon"}
className={cn("rounded-sm flex-shrink-0 object-cover", className)}
onError={handleError}
/>
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/features/agents/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export type SelectedProject = {
id: string
name: string
path: string
iconPath?: string | null
updatedAt?: string | Date | null
gitRemoteUrl?: string | null
gitProvider?: "github" | "gitlab" | "bitbucket" | null
gitOwner?: string | null
Expand Down
Loading