import { appPath } from "@agent-native/core/client/api-path"; import { useT } from "@agent-native/core/client/i18n"; import { navigateWithAgentChatViewTransition, useAgentChatRunningThreads, useChatThreads, } from "@agent-native/toolkit/app/chat/agentkit-chat/rail"; import { ownDraftRows, OwnershipChatHistoryRail, useOwnershipChatHistory, } from "@agent-native/toolkit/app/chat/ownership-history"; import { OrgSwitcher } from "@agent-native/toolkit/app/org"; import { openCommandMenu } from "@agent-native/toolkit/app/shared"; import { AgentNativeIcon } from "@agent-native/toolkit/app/shared/AgentNativeIcon"; import { IconEdit, IconHome, IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftExpand, IconLoader2, IconSearch, } from "@tabler/icons-react"; import { useEffect, useRef, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { APP_TITLE } from "@/lib/app-config"; import { chatThreadPath, HOME_PATH, threadIdFromPath } from "@/lib/chat-paths"; import { cn } from "@/lib/utils"; const CHAT_STORAGE_KEY = "chat"; const CHAT_ACTIVE_THREAD_KEY = `agent-chat-active-thread:${CHAT_STORAGE_KEY}`; interface SidebarProps { collapsed?: boolean; collapsible?: boolean; onCollapsedChange?: (collapsed: boolean) => void; } function ThreadHistoryTitle({ title, running, }: { title: string; running: boolean; }) { const t = useT(); const viewportRef = useRef(null); const contentRef = useRef(null); const [isOverflowing, setIsOverflowing] = useState(false); useEffect(() => { const viewport = viewportRef.current; const content = contentRef.current; if (!viewport || !content) return; const measure = () => { const overflow = Math.max(0, content.scrollWidth - viewport.clientWidth); const overflowing = overflow > 1; setIsOverflowing(overflowing); content.style.setProperty("--chat-sidebar-title-shift", `${overflow}px`); content.style.setProperty( "--chat-sidebar-title-duration", `${Math.max(1800, overflow * 24)}ms`, ); }; const observer = new ResizeObserver(measure); observer.observe(viewport); observer.observe(content); measure(); return () => observer.disconnect(); }, [title]); return ( {title} {running ? ( ) : null} ); } function persistedActiveThreadId() { try { return localStorage.getItem(CHAT_ACTIVE_THREAD_KEY); } catch { return null; } } function persistActiveThreadId(threadId: string) { try { localStorage.setItem(CHAT_ACTIVE_THREAD_KEY, threadId); } catch {} } function HomeNavLink({ collapsed }: { collapsed: boolean }) { const t = useT(); const location = useLocation(); const active = location.pathname === HOME_PATH; const link = ( {t("navigation.home")} ); if (collapsed) { return ( {link} {t("navigation.home")} ); } return
{link}
; } function ChatThreadsSection({ collapsed }: { collapsed: boolean }) { const navigate = useNavigate(); const location = useLocation(); const t = useT(); const { threads, activeThreadId, createThread, switchThread, pinThread, archiveThread, renameThread, isNewThread, } = useChatThreads(undefined, CHAT_STORAGE_KEY, undefined, { autoCreate: false, restoreActiveThread: false, }); const { workingThreadIds, observedThreadStarts } = useAgentChatRunningThreads(); const localMineRows = ownDraftRows({ threads, isNewThread, observedThreadStarts, }); const history = useOwnershipChatHistory({ getThreadUrl: (threadId) => appPath(chatThreadPath(threadId)), activeId: threadIdFromPath(location.pathname) ?? activeThreadId ?? persistedActiveThreadId(), localMineRows, pinned: { label: t("chat.pinned") }, mutations: { pin: pinThread, rename: renameThread, archive: archiveThread, }, onMutationFailed: (kind) => { if (kind === "archive") toast.error(t("chat.archiveFailed")); if (kind === "rename") toast.error(t("chat.renameFailed")); }, decorateItem: (item, row) => { const title = row.title || row.preview || t("chat.untitledChat"); return { ...item, title: ( ), }; }, }); const displayedActiveThreadId = threadIdFromPath(location.pathname) ?? activeThreadId ?? persistedActiveThreadId(); function openThread(threadId: string, options?: { isNew?: boolean }) { switchThread(threadId); persistActiveThreadId(threadId); if (options?.isNew) { window.dispatchEvent( new CustomEvent("agent-chat:open-thread", { detail: { threadId, newThread: true }, }), ); } navigateWithAgentChatViewTransition(navigate, chatThreadPath(threadId)); if (options?.isNew) return; window.requestAnimationFrame(() => { window.dispatchEvent( new CustomEvent("agent-chat:open-thread", { detail: { threadId, newThread: false }, }), ); }); } async function handleNewChat() { const threadId = await createThread(); if (threadId) openThread(threadId, { isNew: true }); } const newChatButton = ( ); if (collapsed) { return ( {newChatButton} {t("chat.newChat")} ); } return (
{newChatButton}
void handleNewChat()} showNewChat={false} activeId={displayedActiveThreadId} onSelect={openThread} onDelete={async (threadId) => { const wasActive = threadId === activeThreadId || threadId === persistedActiveThreadId(); const archived = await history.listProps.onDelete?.(threadId); if (archived && wasActive) await handleNewChat(); }} renameMaxLength={160} labels={{ options: (item) => t("chat.optionsFor", { title: item.titleText ?? "" }), renameInput: (item) => t("chat.renameThread", { title: item.titleText ?? "" }), rename: t("chat.renameChat"), pin: t("chat.pinChat"), unpin: t("chat.unpinChat"), delete: t("chat.archiveChat"), }} railLabels={{ newChat: t("chat.newChat"), }} className="chat-sidebar-history min-h-0 flex-1" listClassName="pb-4" />
); } export function Sidebar({ collapsed = false, collapsible = true, onCollapsedChange, }: SidebarProps) { const t = useT(); const ToggleIcon = collapsed ? IconLayoutSidebarLeftExpand : IconLayoutSidebarLeftCollapse; const collapseButton = collapsible ? ( {collapsed ? t("navigation.expandSidebar") : t("navigation.collapseSidebar")} ) : null; const searchButton = ( {t("root.commandSearch")} ); return ( ); }