diff --git a/src/components/conversations/sidebar-conversation-card.tsx b/src/components/conversations/sidebar-conversation-card.tsx index 97ce7f6..a338a26 100644 --- a/src/components/conversations/sidebar-conversation-card.tsx +++ b/src/components/conversations/sidebar-conversation-card.tsx @@ -145,7 +145,7 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({ "transition-colors duration-[120ms]", "pr-[0.5rem] pl-7", isSelected - ? "bg-sidebar-primary/15" + ? "bg-sidebar-primary/8" : "hover:bg-[color-mix(in_oklab,var(--sidebar-accent),var(--sidebar-foreground)_2%)]" )} > diff --git a/src/components/conversations/sidebar-conversation-list.tsx b/src/components/conversations/sidebar-conversation-list.tsx index 22aa83c..ea92202 100644 --- a/src/components/conversations/sidebar-conversation-list.tsx +++ b/src/components/conversations/sidebar-conversation-list.tsx @@ -31,7 +31,6 @@ import { saveFolderExpanded, } from "@/lib/sidebar-view-mode-storage" import { SidebarConversationCard } from "./sidebar-conversation-card" -import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { ScrollArea } from "@/components/ui/scroll-area" @@ -96,7 +95,6 @@ type FlatItem = type: "folder_header" folderId: number folderName: string - branch: string | null count: number expanded: boolean } @@ -107,70 +105,102 @@ const CARD_HEIGHT_REM = 2 const FolderHeader = memo(function FolderHeader({ folderId, folderName, - branch, count, expanded, onToggle, onFocus, onCloseFolderTabs, onRemoveFromWorkspace, + onNewConversation, t, }: { folderId: number folderName: string - branch: string | null count: number expanded: boolean onToggle: (folderId: number) => void onFocus: (folderId: number) => void onCloseFolderTabs: (folderId: number) => void onRemoveFromWorkspace: (folderId: number) => void + onNewConversation: (folderId: number) => void t: ReturnType }) { return (
- +
- - {count} - - + > + + +
@@ -227,7 +257,6 @@ export function SidebarConversationList({ conversationsError: error, refreshConversations, updateConversationLocal, - branches, removeFolderFromWorkspace, } = useAppWorkspace() const refreshing = loading @@ -319,13 +348,11 @@ export function SidebarConversationList({ for (const folderId of orderedFolderIds) { const list = byFolder.get(folderId) ?? [] const folderName = folderIndex.get(folderId)?.name ?? String(folderId) - const branch = branches.get(folderId) ?? null const expanded = folderExpanded[folderId] ?? true items.push({ type: "folder_header", folderId, folderName, - branch, count: list.length, expanded, }) @@ -335,7 +362,7 @@ export function SidebarConversationList({ } } return items - }, [orderedFolderIds, byFolder, folderIndex, branches, folderExpanded]) + }, [orderedFolderIds, byFolder, folderIndex, folderExpanded]) const stickyState = useMemo<{ folder: Extract | null @@ -559,6 +586,15 @@ export function SidebarConversationList({ openNewConversationTab(activeFolder.id, activeFolder.path) }, [activeFolder, openNewConversationTab]) + const handleNewConversationForFolder = useCallback( + (folderId: number) => { + const folder = folderIndex.get(folderId) + if (!folder) return + openNewConversationTab(folderId, folder.path) + }, + [folderIndex, openNewConversationTab] + ) + const handleImport = useCallback(async () => { if (importing) return if (!activeFolder) return @@ -678,13 +714,13 @@ export function SidebarConversationList({ key={`sticky-${stickyFolderItem.folderId}`} folderId={stickyFolderItem.folderId} folderName={stickyFolderItem.folderName} - branch={stickyFolderItem.branch} count={stickyFolderItem.count} expanded={stickyFolderItem.expanded} onToggle={toggleFolder} onFocus={focusFolder} onCloseFolderTabs={handleCloseFolderTabs} onRemoveFromWorkspace={handleRemoveFolder} + onNewConversation={handleNewConversationForFolder} t={t} /> @@ -708,13 +744,13 @@ export function SidebarConversationList({ key={`folder-${item.folderId}`} folderId={item.folderId} folderName={item.folderName} - branch={item.branch} count={item.count} expanded={item.expanded} onToggle={toggleFolder} onFocus={focusFolder} onCloseFolderTabs={handleCloseFolderTabs} onRemoveFromWorkspace={handleRemoveFolder} + onNewConversation={handleNewConversationForFolder} t={t} /> ) diff --git a/src/contexts/tab-context.tsx b/src/contexts/tab-context.tsx index ef5336f..3d31ab6 100644 --- a/src/contexts/tab-context.tsx +++ b/src/contexts/tab-context.tsx @@ -12,6 +12,7 @@ import { } from "react" import { useTranslations } from "next-intl" import { useAppWorkspace } from "@/contexts/app-workspace-context" +import { useAcpActions } from "@/contexts/acp-connections-context" import { useWorkspaceContext } from "@/contexts/workspace-context" import { listOpenedTabs, saveOpenedTabs } from "@/lib/api" import type { AgentType, ConversationStatus, OpenedTab } from "@/lib/types" @@ -125,6 +126,7 @@ export function TabProvider({ children }: TabProviderProps) { const t = useTranslations("Folder.tabContext") const { activateConversationPane } = useWorkspaceContext() const { conversations, folders, setActiveFolderId } = useAppWorkspace() + const { disconnect: acpDisconnect } = useAcpActions() const [rawTabs, setTabs] = useState([]) const [activeTabId, setActiveTabId] = useState(null) @@ -530,21 +532,44 @@ export function TabProvider({ children }: TabProviderProps) { const openNewConversationTab = useCallback( (folderId: number, workingDir: string) => { - // Reuse existing draft tab for the same folder if present + // Singleton: reuse any existing draft tab regardless of folder, + // so only one new-conversation tab can exist at a time. const existingTab = rawTabsRef.current.find( - (t) => t.conversationId == null && t.folderId === folderId + (t) => t.conversationId == null ) if (existingTab) { - if (existingTab.workingDir !== workingDir) { + const folderChanged = existingTab.folderId !== folderId + const workingDirChanged = existingTab.workingDir !== workingDir + + setActiveTabId(existingTab.id) + activateConversationPane() + + if (folderChanged) { + // Tear down the old ACP connection (bound to the old + // workingDir) before patching the tab's folderId/workingDir. + // The connection-lifecycle effect watches workingDir; once + // status has settled to disconnected and workingDir flips, + // it auto-reconnects against the new folder. + void (async () => { + try { + await acpDisconnect(existingTab.id) + } catch (err) { + console.error("[TabProvider] disconnect draft tab:", err) + } + setTabs((prev) => + prev.map((t) => + t.id === existingTab.id ? { ...t, folderId, workingDir } : t + ) + ) + })() + } else if (workingDirChanged) { setTabs((prev) => prev.map((t) => t.id === existingTab.id ? { ...t, workingDir } : t ) ) } - setActiveTabId(existingTab.id) - activateConversationPane() return } @@ -565,7 +590,7 @@ export function TabProvider({ children }: TabProviderProps) { setActiveTabId(tabId) activateConversationPane() }, - [activateConversationPane, t] + [acpDisconnect, activateConversationPane, t] ) const bindConversationTab = useCallback(