diff --git a/ui/src/components/ChatBookmarkList.tsx b/ui/src/components/ChatBookmarkList.tsx
new file mode 100644
index 00000000..9c18b459
--- /dev/null
+++ b/ui/src/components/ChatBookmarkList.tsx
@@ -0,0 +1,64 @@
+import { Bookmark } from "lucide-react";
+import { useChatBookmarks } from "../hooks/useChatBookmarks";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Skeleton } from "@/components/ui/skeleton";
+
+interface ChatBookmarkListProps {
+ companyId: string;
+ onNavigate: (conversationId: string, messageId: string) => void;
+}
+
+function formatRelativeTime(dateStr: string): string {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMin = Math.floor(diffMs / 60000);
+ if (diffMin < 1) return "just now";
+ if (diffMin < 60) return `${diffMin}m ago`;
+ const diffHr = Math.floor(diffMin / 60);
+ if (diffHr < 24) return `${diffHr}h ago`;
+ const diffDays = Math.floor(diffHr / 24);
+ if (diffDays < 30) return `${diffDays}d ago`;
+ return date.toLocaleDateString();
+}
+
+export function ChatBookmarkList({ companyId, onNavigate }: ChatBookmarkListProps) {
+ const { data, isLoading } = useChatBookmarks(companyId);
+ const bookmarks = data?.items ?? [];
+
+ return (
+
+
+ {isLoading ? (
+ Array.from({ length: 4 }).map((_, i) => (
+
+ ))
+ ) : bookmarks.length === 0 ? (
+
+ ) : (
+ bookmarks.map((bookmark) => (
+
+ ))
+ )}
+
+
+ );
+}
diff --git a/ui/src/components/ChatBranchSelector.tsx b/ui/src/components/ChatBranchSelector.tsx
new file mode 100644
index 00000000..e34212b4
--- /dev/null
+++ b/ui/src/components/ChatBranchSelector.tsx
@@ -0,0 +1,62 @@
+import { GitBranch } from "lucide-react";
+import { cn } from "@/lib/utils";
+import type { ChatConversation } from "@paperclipai/shared";
+
+interface ChatBranchSelectorProps {
+ conversationId: string;
+ branches: ChatConversation[];
+ activeBranchId: string | null;
+ onSelectBranch: (id: string) => void;
+}
+
+function formatDate(dateStr: string): string {
+ return new Date(dateStr).toLocaleDateString(undefined, { month: "short", day: "numeric" });
+}
+
+export function ChatBranchSelector({
+ conversationId,
+ branches,
+ activeBranchId,
+ onSelectBranch,
+}: ChatBranchSelectorProps) {
+ if (branches.length === 0) return null;
+
+ return (
+
+
+ Branch:
+
+ {/* Original conversation */}
+
+
+ {branches.map((branch, index) => (
+
+ ))}
+
+ );
+}
diff --git a/ui/src/components/ChatMessageBookmark.tsx b/ui/src/components/ChatMessageBookmark.tsx
new file mode 100644
index 00000000..53d367e3
--- /dev/null
+++ b/ui/src/components/ChatMessageBookmark.tsx
@@ -0,0 +1,35 @@
+import { Bookmark } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+
+interface ChatMessageBookmarkProps {
+ messageId: string;
+ conversationId: string;
+ isBookmarked: boolean;
+ onToggle: () => void;
+}
+
+export function ChatMessageBookmark({ isBookmarked, onToggle }: ChatMessageBookmarkProps) {
+ return (
+
+
+
+
+ {isBookmarked ? "Remove bookmark" : "Bookmark message"}
+
+ );
+}
diff --git a/ui/src/components/ChatSearchDialog.tsx b/ui/src/components/ChatSearchDialog.tsx
new file mode 100644
index 00000000..af3d9e48
--- /dev/null
+++ b/ui/src/components/ChatSearchDialog.tsx
@@ -0,0 +1,138 @@
+import { useState } from "react";
+import { Search } from "lucide-react";
+import { useChatSearch } from "../hooks/useChatSearch";
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { Command } from "cmdk";
+
+interface ChatSearchDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ companyId: string | null;
+ onNavigate: (conversationId: string, messageId: string) => void;
+}
+
+function stripMarkdown(text: string): string {
+ return text
+ .replace(/```[\s\S]*?```/g, "")
+ .replace(/`[^`]+`/g, "")
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
+ .replace(/\*([^*]+)\*/g, "$1")
+ .replace(/#{1,6}\s/g, "")
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
+ .replace(/>\s/g, "")
+ .trim();
+}
+
+/** Split text into segments, marking portions that match the query terms */
+function splitWithHighlight(text: string, query: string): Array<{ text: string; highlight: boolean }> {
+ if (!query.trim()) return [{ text, highlight: false }];
+ const terms = query.trim().split(/\s+/).filter(Boolean);
+ const pattern = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
+ const re = new RegExp(`(${pattern})`, "gi");
+ const parts = text.split(re);
+ return parts.map((part) => ({
+ text: part,
+ highlight: re.test(part),
+ }));
+}
+
+function HighlightedText({ text, query }: { text: string; query: string }) {
+ const segments = splitWithHighlight(text, query);
+ return (
+ <>
+ {segments.map((seg, i) =>
+ seg.highlight ? (
+
+ {seg.text}
+
+ ) : (
+ {seg.text}
+ ),
+ )}
+ >
+ );
+}
+
+function formatRelativeTime(dateStr: string): string {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMin = Math.floor(diffMs / 60000);
+ if (diffMin < 1) return "just now";
+ if (diffMin < 60) return `${diffMin}m ago`;
+ const diffHr = Math.floor(diffMin / 60);
+ if (diffHr < 24) return `${diffHr}h ago`;
+ const diffDays = Math.floor(diffHr / 24);
+ if (diffDays < 30) return `${diffDays}d ago`;
+ return date.toLocaleDateString();
+}
+
+export function ChatSearchDialog({ open, onOpenChange, companyId, onNavigate }: ChatSearchDialogProps) {
+ const [query, setQuery] = useState("");
+ const { data } = useChatSearch(companyId, query);
+ const results = data?.items ?? [];
+
+ function handleSelect(conversationId: string, messageId: string) {
+ onNavigate(conversationId, messageId);
+ onOpenChange(false);
+ setQuery("");
+ }
+
+ return (
+ {
+ onOpenChange(v);
+ if (!v) setQuery("");
+ }}
+ title="Search messages"
+ description="Search all messages across conversations"
+ >
+
+
+
+ {query.trim().length >= 2 && results.length === 0 && (
+ No results found.
+ )}
+ {results.map((result) => {
+ const snippet = stripMarkdown(result.content).slice(0, 120);
+ return (
+ handleSelect(result.conversationId, result.messageId)}
+ className="flex flex-col items-start gap-0.5 py-2"
+ >
+
+
+
+ {result.conversationTitle ?? "Untitled conversation"}
+
+
+ {result.role}
+
+
+ {formatRelativeTime(result.createdAt)}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}