diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index b155eff780..31f8e38785 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -14,6 +14,12 @@ import { } from "@/components/assistant-ui/model-selector/remembered-load-settings"; import { ProjectComposer, Thread } from "@/components/assistant-ui/thread"; import { CopyableErrorChip } from "@/components/ui/copyable-error-chip"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { ResizableHandle, ResizablePanel, @@ -41,8 +47,10 @@ import { toast } from "@/lib/toast"; import { cn } from "@/lib/utils"; import { BubbleChatTemporaryIcon, + Edit03Icon, Folder02Icon, LayoutAlignRightIcon, + MoreVerticalIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { useNavigate } from "@tanstack/react-router"; @@ -82,6 +90,7 @@ import type { SelectedModelInput } from "./hooks/use-chat-model-runtime"; import { useChatProjects } from "./hooks/use-chat-projects"; import { type SidebarItem, + renameChatItem, useChatSidebarItems, } from "./hooks/use-chat-sidebar-items"; import { useStagedModelPreparation } from "./hooks/use-staged-model-preparation"; @@ -900,6 +909,19 @@ function ProjectLanding({ const [previews, setPreviews] = useState< Record >({}); + // Inline rename, mirroring the sidebar recent-row UX: edit the title in place, + // commit on Enter/blur, cancel on Escape. Reuses the projectId-agnostic + // renameChatItem so behavior matches the sidebar. + const [renamingId, setRenamingId] = useState(null); + const [renameDraft, setRenameDraft] = useState(""); + // Skips the input's blur-commit when Enter/Escape already handled it. + const skipRenameBlurRef = useRef(false); + // Optimistic title shown until the debounced sidebar refresh (fired by the + // rename) catches up, so the old name does not flash back in. + const [pendingRename, setPendingRename] = useState<{ + id: string; + title: string; + } | null>(null); useEffect(() => { initialActiveThreadRef.current = @@ -908,8 +930,40 @@ function ProjectLanding({ useChatRuntimeStore.getState().setContextUsage(null); setPendingNewThreadId(null); setNewThreadNonce(crypto.randomUUID()); + setRenamingId(null); + setPendingRename(null); }, [projectId]); + useEffect(() => { + if (!pendingRename) return; + const match = items.find((item) => item.id === pendingRename.id); + if (match && match.title === pendingRename.title) setPendingRename(null); + }, [items, pendingRename]); + + const openRename = useCallback((item: SidebarItem) => { + skipRenameBlurRef.current = false; + setRenameDraft(item.title); + setRenamingId(item.id); + }, []); + + const commitRename = useCallback( + async (item: SidebarItem) => { + const trimmed = renameDraft.trim(); + setRenamingId(null); + if (!trimmed || trimmed === item.title) return; + setPendingRename({ id: item.id, title: trimmed }); + try { + await renameChatItem(item, trimmed); + } catch (err) { + setPendingRename(null); + toast.error("Failed to rename chat", { + description: err instanceof Error ? err.message : undefined, + }); + } + }, + [renameDraft], + ); + useEffect(() => { if (!activeThreadId) { setPendingNewThreadId(null); @@ -1039,35 +1093,122 @@ function ProjectLanding({
{items.map((item) => { const preview = previews[item.id]; - return ( - + ); + } + return ( +
+ + + + + + + openRename(item)}> + + Rename + + + +
); })}