From 766a7f6593b6778488eaaacf6eda41f1758db2b6 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Wed, 13 May 2026 16:01:53 +0000 Subject: [PATCH 01/11] feat: add chat history panel with searchable conversation list --- .changeset/chat-history-panel.md | 5 + packages/core/src/client/AgentPanel.tsx | 184 +++++++++++++++++- .../core/src/client/MultiTabAssistantChat.tsx | 14 ++ 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 .changeset/chat-history-panel.md diff --git a/.changeset/chat-history-panel.md b/.changeset/chat-history-panel.md new file mode 100644 index 000000000..b052bbeca --- /dev/null +++ b/.changeset/chat-history-panel.md @@ -0,0 +1,5 @@ +--- +"@agent-native/core": minor +--- + +Add chat history panel: a dedicated History tab in the agent panel header that shows a searchable list of all previous conversations. Clicking a conversation switches back to chat mode with that thread active. diff --git a/packages/core/src/client/AgentPanel.tsx b/packages/core/src/client/AgentPanel.tsx index 872f93ba2..b8895e89a 100644 --- a/packages/core/src/client/AgentPanel.tsx +++ b/packages/core/src/client/AgentPanel.tsx @@ -64,6 +64,7 @@ import { IconArrowsMaximize, IconArrowsMinimize, IconExternalLink, + IconSearch, } from "@tabler/icons-react"; import { FeedbackButton } from "./FeedbackButton.js"; import { @@ -78,6 +79,11 @@ import { useLocation, useNavigate } from "react-router"; import { cn } from "./utils.js"; import { agentNativePath } from "./api-path.js"; import { getFrameOrigin, isInFrame, isTrustedFrameMessage } from "./frame.js"; +import { + useChatThreads, + type ChatThreadScope, + type ChatThreadSummary, +} from "./use-chat-threads.js"; import { getInitialAgentSidebarOpen, SIDEBAR_OPEN_KEY, @@ -135,7 +141,7 @@ const CLI_STORAGE_KEY = "agent-native-cli-command"; const CLI_DEFAULT = "claude"; const EXEC_MODE_KEY = "agent-native-exec-mode"; type ExecMode = "build" | "plan"; -type PanelMode = "chat" | "cli" | "resources" | "settings"; +type PanelMode = "chat" | "cli" | "resources" | "settings" | "history"; const AGENT_PANEL_FONT_FAMILY = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; const AGENT_PANEL_ROOT_STYLE = { @@ -513,6 +519,7 @@ function AgentPanelInner({ return defaultMode; }); useEffect(() => { + if (mode === "history") return; try { localStorage.setItem(panelModeKey, mode); } catch {} @@ -717,6 +724,25 @@ function AgentPanelInner({ Chat mode + + + + + Chat history + {showCliMode && ( @@ -1415,6 +1441,25 @@ function AgentPanelInner({ )} + {/* History view */} + {mode === "history" && ( +
+ { + window.dispatchEvent( + new CustomEvent("agent-panel:open-thread", { + detail: { threadId }, + }), + ); + switchMode("chat"); + }} + /> +
+ )} + {/* Settings / Setup view */} {mode === "settings" && (
@@ -1726,6 +1771,143 @@ function ScreenRefreshBoundary({ children }: { children: React.ReactNode }) { return {children}; } +// ─── HistoryPanel ──────────────────────────────────────────────────────────── + +function formatHistoryTime(ts: number): string { + const d = new Date(ts); + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const diffDays = Math.floor(diffMs / 86400000); + if (diffDays === 0) + return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + if (diffDays === 1) return "Yesterday"; + if (diffDays < 7) return d.toLocaleDateString([], { weekday: "short" }); + return d.toLocaleDateString([], { month: "short", day: "numeric" }); +} + +function HistoryPanel({ + apiUrl, + storageKey, + scope, + onSelectThread, +}: { + apiUrl?: string; + storageKey?: string; + scope?: ChatThreadScope | null; + onSelectThread: (threadId: string) => void; +}) { + const { threads, activeThreadId, searchThreads } = useChatThreads( + apiUrl ?? agentNativePath("/_agent-native/agent-chat"), + storageKey, + scope, + ); + const [search, setSearch] = useState(""); + const [searchResults, setSearchResults] = useState< + ChatThreadSummary[] | null + >(null); + const [isSearching, setIsSearching] = useState(false); + const debounceRef = useRef>(undefined); + const searchIdRef = useRef(0); + + useEffect(() => { + if (debounceRef.current) clearTimeout(debounceRef.current); + const q = search.trim(); + if (!q) { + searchIdRef.current++; + setSearchResults(null); + setIsSearching(false); + return; + } + setIsSearching(true); + const id = ++searchIdRef.current; + debounceRef.current = setTimeout(async () => { + if (searchThreads) { + const results = await searchThreads(q); + if (id !== searchIdRef.current) return; + setSearchResults(results); + } else { + setSearchResults(null); + } + setIsSearching(false); + }, 250); + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, [search, searchThreads]); + + const visibleThreads = threads.filter( + (t) => t.messageCount > 0 || t.id === activeThreadId, + ); + + const filtered = search.trim() + ? (searchResults ?? visibleThreads).filter( + (t) => t.messageCount > 0 || t.id === activeThreadId, + ) + : visibleThreads; + + return ( +
+
+ + setSearch(e.target.value)} + placeholder="Search conversations..." + autoFocus + className="flex-1 bg-transparent text-xs text-foreground placeholder:text-muted-foreground outline-none" + /> +
+
+ {isSearching ? ( +
+ Searching... +
+ ) : filtered.length === 0 ? ( +
+ {search ? "No matching conversations" : "No conversations yet"} +
+ ) : ( +
+ {filtered.map((thread) => ( + + ))} +
+ )} +
+
+ ); +} + class AgentPanelErrorBoundary extends React.Component< { children: React.ReactNode; onReset: () => void }, { error: Error | null } diff --git a/packages/core/src/client/MultiTabAssistantChat.tsx b/packages/core/src/client/MultiTabAssistantChat.tsx index 97521ca11..6a6feb7bf 100644 --- a/packages/core/src/client/MultiTabAssistantChat.tsx +++ b/packages/core/src/client/MultiTabAssistantChat.tsx @@ -1480,6 +1480,20 @@ export function MultiTabAssistantChat({ [openTabIds, switchThread], ); + // Listen for history panel thread selection (from AgentPanel history mode) + const openFromHistoryRef = useRef(openFromHistory); + openFromHistoryRef.current = openFromHistory; + useEffect(() => { + function handleOpenThread(e: Event) { + const threadId = (e as CustomEvent<{ threadId: string }>).detail + ?.threadId; + if (threadId) openFromHistoryRef.current(threadId); + } + window.addEventListener("agent-panel:open-thread", handleOpenThread); + return () => + window.removeEventListener("agent-panel:open-thread", handleOpenThread); + }, []); + // Listen for agent-task-open events (from AgentTaskCard "Open" button) useEffect(() => { function handleOpenTask(e: Event) { From 8a26946b1b60de4a03be30d296d3d04742f3a88b Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Wed, 13 May 2026 16:05:33 +0000 Subject: [PATCH 02/11] feat: add ability to delete chats in history --- packages/core/src/client/AgentPanel.tsx | 81 ++++++++++++++++--------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/packages/core/src/client/AgentPanel.tsx b/packages/core/src/client/AgentPanel.tsx index b8895e89a..a15d87cb7 100644 --- a/packages/core/src/client/AgentPanel.tsx +++ b/packages/core/src/client/AgentPanel.tsx @@ -1796,11 +1796,12 @@ function HistoryPanel({ scope?: ChatThreadScope | null; onSelectThread: (threadId: string) => void; }) { - const { threads, activeThreadId, searchThreads } = useChatThreads( - apiUrl ?? agentNativePath("/_agent-native/agent-chat"), - storageKey, - scope, - ); + const { threads, activeThreadId, searchThreads, deleteThread } = + useChatThreads( + apiUrl ?? agentNativePath("/_agent-native/agent-chat"), + storageKey, + scope, + ); const [search, setSearch] = useState(""); const [searchResults, setSearchResults] = useState< ChatThreadSummary[] | null @@ -1847,6 +1848,13 @@ function HistoryPanel({ return (
+ {/* Hover-reveal delete button — Tailwind group-hover doesn't work in core package */} +