feat(24-03): wire search, branch selector, export, scroll-to-message into ChatPanel
- Add scrollToMessageId/setScrollToMessageId to ChatPanelContext - Add "Search chat messages" item to CommandPalette (dispatches nexus:open-chat-search) - Integrate ChatSearchDialog, ChatBranchSelector, ChatBookmarkList into ChatPanel - Add export buttons (Markdown) and bookmarks panel toggle in header - Wire branch-on-edit: branchConversation called when editing message with subsequent replies - Add scroll-to-message support in ChatMessageList via virtualizer.scrollToIndex - Show GitBranch icon for branch conversations in ChatConversationList
This commit is contained in:
parent
d20dce57ba
commit
77132b4351
5 changed files with 202 additions and 20 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Plus, Search, X } from "lucide-react";
|
import { GitBranch, Plus, Search, X } from "lucide-react";
|
||||||
import { useChatConversations } from "../hooks/useChatConversations";
|
import { useChatConversations } from "../hooks/useChatConversations";
|
||||||
import { useChatPanel } from "../context/ChatPanelContext";
|
import { useChatPanel } from "../context/ChatPanelContext";
|
||||||
import { ChatConversationItem } from "./ChatConversationItem";
|
import { ChatConversationItem } from "./ChatConversationItem";
|
||||||
|
|
@ -169,16 +169,22 @@ export function ChatConversationList({ companyId }: ChatConversationListProps) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
sorted.map((conversation) => (
|
sorted.map((conversation) => (
|
||||||
<ChatConversationItem
|
<div key={conversation.id} className="relative">
|
||||||
key={conversation.id}
|
{conversation.parentConversationId && (
|
||||||
conversation={conversation}
|
<GitBranch className="absolute left-1 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground pointer-events-none z-10" />
|
||||||
isActive={activeConversationId === conversation.id}
|
)}
|
||||||
onSelect={setActiveConversationId}
|
<div className={conversation.parentConversationId ? "pl-4" : undefined}>
|
||||||
onRename={handleRename}
|
<ChatConversationItem
|
||||||
onPin={handlePin}
|
conversation={conversation}
|
||||||
onArchive={handleArchive}
|
isActive={activeConversationId === conversation.id}
|
||||||
onDelete={handleDeleteRequest}
|
onSelect={setActiveConversationId}
|
||||||
/>
|
onRename={handleRename}
|
||||||
|
onPin={handlePin}
|
||||||
|
onArchive={handleArchive}
|
||||||
|
onDelete={handleDeleteRequest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useRef, useEffect, useCallback, useState } from "react";
|
import { useRef, useEffect, useCallback, useState } from "react";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { useChatMessages } from "../hooks/useChatMessages";
|
import { useChatMessages } from "../hooks/useChatMessages";
|
||||||
|
import { useChatPanel } from "../context/ChatPanelContext";
|
||||||
import { ChatMessage } from "./ChatMessage";
|
import { ChatMessage } from "./ChatMessage";
|
||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -18,6 +19,8 @@ interface ChatMessageListProps {
|
||||||
onRetry?: (messageId: string) => void;
|
onRetry?: (messageId: string) => void;
|
||||||
onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void;
|
onHandoff?: (spec: { what: string; why: string; constraints: string; success: string }) => void;
|
||||||
agentMap?: Map<string, { name: string; icon: string | null; role: AgentRole | null }>;
|
agentMap?: Map<string, { name: string; icon: string | null; role: AgentRole | null }>;
|
||||||
|
onBookmark?: (messageId: string) => void;
|
||||||
|
bookmarkedMessageIds?: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessageList({
|
export function ChatMessageList({
|
||||||
|
|
@ -31,8 +34,11 @@ export function ChatMessageList({
|
||||||
onRetry,
|
onRetry,
|
||||||
onHandoff,
|
onHandoff,
|
||||||
agentMap,
|
agentMap,
|
||||||
|
onBookmark,
|
||||||
|
bookmarkedMessageIds,
|
||||||
}: ChatMessageListProps) {
|
}: ChatMessageListProps) {
|
||||||
const { messages, isLoading } = useChatMessages(conversationId);
|
const { messages, isLoading } = useChatMessages(conversationId);
|
||||||
|
const { scrollToMessageId, setScrollToMessageId } = useChatPanel();
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const [showJumpToBottom, setShowJumpToBottom] = useState(false);
|
const [showJumpToBottom, setShowJumpToBottom] = useState(false);
|
||||||
|
|
||||||
|
|
@ -78,6 +84,19 @@ export function ChatMessageList({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [streamingContent, isStreaming]);
|
}, [streamingContent, isStreaming]);
|
||||||
|
|
||||||
|
// Scroll to a specific message when scrollToMessageId is set
|
||||||
|
useEffect(() => {
|
||||||
|
if (!scrollToMessageId) return;
|
||||||
|
const index = displayMessages.findIndex((m) => m.id === scrollToMessageId);
|
||||||
|
if (index >= 0) {
|
||||||
|
virtualizer.scrollToIndex(index, { align: "center" });
|
||||||
|
setScrollToMessageId(null);
|
||||||
|
}
|
||||||
|
// TODO: if message not in current page (infinite scroll), scroll is best-effort only.
|
||||||
|
// Future iteration: load pages until message found, then scroll.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [scrollToMessageId]);
|
||||||
|
|
||||||
// Track scroll position for "jump to bottom" button
|
// Track scroll position for "jump to bottom" button
|
||||||
const handleScroll = useCallback(() => {
|
const handleScroll = useCallback(() => {
|
||||||
const el = parentRef.current;
|
const el = parentRef.current;
|
||||||
|
|
@ -156,6 +175,8 @@ export function ChatMessageList({
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
onRetry={onRetry}
|
onRetry={onRetry}
|
||||||
onHandoff={onHandoff}
|
onHandoff={onHandoff}
|
||||||
|
onBookmark={onBookmark}
|
||||||
|
isBookmarked={msg.id ? (bookmarkedMessageIds?.has(msg.id) ?? false) : false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||||
import { X } from "lucide-react";
|
import { Bookmark, Download, X } from "lucide-react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useChatPanel } from "../context/ChatPanelContext";
|
import { useChatPanel } from "../context/ChatPanelContext";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
|
|
@ -9,28 +9,41 @@ import { ChatConversationList } from "./ChatConversationList";
|
||||||
import { ChatMessageList } from "./ChatMessageList";
|
import { ChatMessageList } from "./ChatMessageList";
|
||||||
import { ChatAgentSelector } from "./ChatAgentSelector";
|
import { ChatAgentSelector } from "./ChatAgentSelector";
|
||||||
import { ChatStopButton } from "./ChatStopButton";
|
import { ChatStopButton } from "./ChatStopButton";
|
||||||
|
import { ChatSearchDialog } from "./ChatSearchDialog";
|
||||||
|
import { ChatBranchSelector } from "./ChatBranchSelector";
|
||||||
|
import { ChatBookmarkList } from "./ChatBookmarkList";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { chatApi } from "../api/chat";
|
import { chatApi } from "../api/chat";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
import { useChatMessages } from "../hooks/useChatMessages";
|
import { useChatMessages } from "../hooks/useChatMessages";
|
||||||
import { useStreamingChat } from "../hooks/useStreamingChat";
|
import { useStreamingChat } from "../hooks/useStreamingChat";
|
||||||
import { useBrainstormerDefault } from "../hooks/useBrainstormerDefault";
|
import { useBrainstormerDefault } from "../hooks/useBrainstormerDefault";
|
||||||
|
import { useChatBookmarks, useToggleBookmark } from "../hooks/useChatBookmarks";
|
||||||
import { resolveAgentFromContent } from "../lib/slash-commands";
|
import { resolveAgentFromContent } from "../lib/slash-commands";
|
||||||
import type { AgentRole } from "@paperclipai/shared";
|
import type { AgentRole } from "@paperclipai/shared";
|
||||||
|
|
||||||
export function ChatPanel() {
|
export function ChatPanel() {
|
||||||
const { chatOpen, setChatOpen, activeConversationId, setActiveConversationId } = useChatPanel();
|
const { chatOpen, setChatOpen, activeConversationId, setActiveConversationId, setScrollToMessageId } = useChatPanel();
|
||||||
const { selectedCompanyId } = useCompany();
|
const { selectedCompanyId } = useCompany();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { pushToast } = useToast();
|
const { pushToast } = useToast();
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||||
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
|
const [bookmarksOpen, setBookmarksOpen] = useState(false);
|
||||||
|
|
||||||
const { messages } = useChatMessages(activeConversationId);
|
const { messages } = useChatMessages(activeConversationId);
|
||||||
const { streamingContent, isStreaming, startStream, stop } = useStreamingChat(activeConversationId);
|
const { streamingContent, isStreaming, startStream, stop } = useStreamingChat(activeConversationId);
|
||||||
|
|
||||||
const brainstormerDefaultId = useBrainstormerDefault();
|
const brainstormerDefaultId = useBrainstormerDefault();
|
||||||
|
|
||||||
|
// Listen for nexus:open-chat-search custom event (dispatched by CommandPalette)
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => setSearchOpen(true);
|
||||||
|
window.addEventListener("nexus:open-chat-search", handler);
|
||||||
|
return () => window.removeEventListener("nexus:open-chat-search", handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Auto-select brainstormer (general agent) for new conversations
|
// Auto-select brainstormer (general agent) for new conversations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeAgentId === null && brainstormerDefaultId !== null && !activeConversationId) {
|
if (activeAgentId === null && brainstormerDefaultId !== null && !activeConversationId) {
|
||||||
|
|
@ -68,6 +81,51 @@ export function ChatPanel() {
|
||||||
// Resolve streaming agent identity
|
// Resolve streaming agent identity
|
||||||
const streamingAgent = activeAgentId ? agentMap.get(activeAgentId) : undefined;
|
const streamingAgent = activeAgentId ? agentMap.get(activeAgentId) : undefined;
|
||||||
|
|
||||||
|
// Fetch branches for active conversation
|
||||||
|
const { data: branchesData } = useQuery({
|
||||||
|
queryKey: ["chat", "branches", activeConversationId],
|
||||||
|
queryFn: () => chatApi.listBranches(activeConversationId!),
|
||||||
|
enabled: !!activeConversationId,
|
||||||
|
});
|
||||||
|
const branches = branchesData?.items ?? [];
|
||||||
|
|
||||||
|
// Bookmark state for active conversation
|
||||||
|
const { data: bookmarkData } = useChatBookmarks(selectedCompanyId, activeConversationId ?? undefined);
|
||||||
|
const { toggleBookmark } = useToggleBookmark();
|
||||||
|
const bookmarkedMessageIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const b of bookmarkData?.items ?? []) {
|
||||||
|
ids.add(b.message.id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [bookmarkData]);
|
||||||
|
|
||||||
|
// Search navigation handler
|
||||||
|
const handleSearchNavigate = useCallback((conversationId: string, messageId: string) => {
|
||||||
|
setActiveConversationId(conversationId);
|
||||||
|
setScrollToMessageId(messageId);
|
||||||
|
setSearchOpen(false);
|
||||||
|
}, [setActiveConversationId, setScrollToMessageId]);
|
||||||
|
|
||||||
|
// Bookmark navigation handler
|
||||||
|
const handleBookmarkNavigate = useCallback((conversationId: string, messageId: string) => {
|
||||||
|
setActiveConversationId(conversationId);
|
||||||
|
setScrollToMessageId(messageId);
|
||||||
|
setBookmarksOpen(false);
|
||||||
|
}, [setActiveConversationId, setScrollToMessageId]);
|
||||||
|
|
||||||
|
// Bookmark toggle handler
|
||||||
|
const handleBookmark = useCallback((messageId: string) => {
|
||||||
|
if (!activeConversationId) return;
|
||||||
|
toggleBookmark({ conversationId: activeConversationId, messageId });
|
||||||
|
}, [activeConversationId, toggleBookmark]);
|
||||||
|
|
||||||
|
// Export handler
|
||||||
|
const handleExport = useCallback((format: "markdown" | "json") => {
|
||||||
|
if (!activeConversationId) return;
|
||||||
|
window.location.href = chatApi.exportConversation(activeConversationId, format);
|
||||||
|
}, [activeConversationId]);
|
||||||
|
|
||||||
const handleSend = async (content: string) => {
|
const handleSend = async (content: string) => {
|
||||||
if (!selectedCompanyId) return;
|
if (!selectedCompanyId) return;
|
||||||
|
|
||||||
|
|
@ -97,13 +155,39 @@ export function ChatPanel() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Edit handler: update message, truncate after it, re-stream
|
// Edit handler: if editing a message that has subsequent responses, branch first
|
||||||
const handleEdit = async (messageId: string, newContent: string) => {
|
const handleEdit = async (messageId: string, newContent: string) => {
|
||||||
if (!activeConversationId) return;
|
if (!activeConversationId) return;
|
||||||
await chatApi.editMessage(activeConversationId, messageId, newContent);
|
|
||||||
await chatApi.truncateMessagesAfter(activeConversationId, messageId);
|
// Check if there are any messages after the edited message
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", activeConversationId] });
|
const editedIdx = messages.findIndex((m) => m.id === messageId);
|
||||||
startStream(newContent, activeAgentId ?? undefined);
|
const hasSubsequentMessages = editedIdx >= 0 && editedIdx < messages.length - 1;
|
||||||
|
|
||||||
|
if (hasSubsequentMessages) {
|
||||||
|
// Branch the conversation from this message first (CHAT-14)
|
||||||
|
try {
|
||||||
|
const newConv = await chatApi.branchConversation(activeConversationId, messageId);
|
||||||
|
setActiveConversationId(newConv.id);
|
||||||
|
// Invalidate branches list so branch selector updates
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "branches"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat"] });
|
||||||
|
// Edit the message on the new branch
|
||||||
|
await chatApi.editMessage(newConv.id, messageId, newContent);
|
||||||
|
await chatApi.truncateMessagesAfter(newConv.id, messageId);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", newConv.id] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "search"] });
|
||||||
|
startStream(newContent, activeAgentId ?? undefined);
|
||||||
|
} catch {
|
||||||
|
pushToast({ title: "Could not create branch. Try again.", tone: "error" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No subsequent messages — edit in place
|
||||||
|
await chatApi.editMessage(activeConversationId, messageId, newContent);
|
||||||
|
await chatApi.truncateMessagesAfter(activeConversationId, messageId);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", activeConversationId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "search"] });
|
||||||
|
startStream(newContent, activeAgentId ?? undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Retry handler: find the last user message before the assistant message,
|
// Retry handler: find the last user message before the assistant message,
|
||||||
|
|
@ -140,6 +224,7 @@ export function ChatPanel() {
|
||||||
// Delete everything after the user message (includes the assistant message itself)
|
// Delete everything after the user message (includes the assistant message itself)
|
||||||
await chatApi.truncateMessagesAfter(activeConversationId, userMessageId);
|
await chatApi.truncateMessagesAfter(activeConversationId, userMessageId);
|
||||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", activeConversationId] });
|
queryClient.invalidateQueries({ queryKey: ["chat", "messages", activeConversationId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["chat", "search"] });
|
||||||
|
|
||||||
// Re-stream using the actual user message content
|
// Re-stream using the actual user message content
|
||||||
startStream(lastUserContent, activeAgentId ?? undefined);
|
startStream(lastUserContent, activeAgentId ?? undefined);
|
||||||
|
|
@ -154,7 +239,33 @@ export function ChatPanel() {
|
||||||
{/* Header with agent selector */}
|
{/* Header with agent selector */}
|
||||||
<div className="flex items-center justify-between border-b border-border px-4 py-2 min-w-[380px]">
|
<div className="flex items-center justify-between border-b border-border px-4 py-2 min-w-[380px]">
|
||||||
<span className="text-sm font-medium">Chat</span>
|
<span className="text-sm font-medium">Chat</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1">
|
||||||
|
{selectedCompanyId && activeConversationId && (
|
||||||
|
<>
|
||||||
|
{/* Bookmarks toggle */}
|
||||||
|
<Button
|
||||||
|
variant={bookmarksOpen ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => setBookmarksOpen((v) => !v)}
|
||||||
|
aria-label="Bookmarks"
|
||||||
|
title="Bookmarks"
|
||||||
|
>
|
||||||
|
<Bookmark className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{/* Export buttons */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => handleExport("markdown")}
|
||||||
|
aria-label="Export as Markdown"
|
||||||
|
title="Export as Markdown"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{selectedCompanyId && (
|
{selectedCompanyId && (
|
||||||
<ChatAgentSelector
|
<ChatAgentSelector
|
||||||
companyId={selectedCompanyId}
|
companyId={selectedCompanyId}
|
||||||
|
|
@ -190,6 +301,26 @@ export function ChatPanel() {
|
||||||
|
|
||||||
{/* Right column: message thread + stop button + input */}
|
{/* Right column: message thread + stop button + input */}
|
||||||
<div className="flex flex-1 flex-col min-w-0">
|
<div className="flex flex-1 flex-col min-w-0">
|
||||||
|
{/* Bookmarks panel (shown when toggled) */}
|
||||||
|
{bookmarksOpen && selectedCompanyId && (
|
||||||
|
<div className="border-b border-border" style={{ maxHeight: 200, overflow: "hidden" }}>
|
||||||
|
<ChatBookmarkList
|
||||||
|
companyId={selectedCompanyId}
|
||||||
|
onNavigate={handleBookmarkNavigate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Branch selector */}
|
||||||
|
{activeConversationId && branches.length > 0 && (
|
||||||
|
<ChatBranchSelector
|
||||||
|
conversationId={activeConversationId}
|
||||||
|
branches={branches}
|
||||||
|
activeBranchId={activeConversationId}
|
||||||
|
onSelectBranch={setActiveConversationId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Message area */}
|
{/* Message area */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{activeConversationId ? (
|
{activeConversationId ? (
|
||||||
|
|
@ -204,6 +335,8 @@ export function ChatPanel() {
|
||||||
onRetry={handleRetry}
|
onRetry={handleRetry}
|
||||||
onHandoff={handleHandoff}
|
onHandoff={handleHandoff}
|
||||||
agentMap={agentMap}
|
agentMap={agentMap}
|
||||||
|
onBookmark={handleBookmark}
|
||||||
|
bookmarkedMessageIds={bookmarkedMessageIds}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full p-3">
|
<div className="flex items-center justify-center h-full p-3">
|
||||||
|
|
@ -230,6 +363,14 @@ export function ChatPanel() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Search dialog */}
|
||||||
|
<ChatSearchDialog
|
||||||
|
open={searchOpen}
|
||||||
|
onOpenChange={setSearchOpen}
|
||||||
|
companyId={selectedCompanyId}
|
||||||
|
onNavigate={handleSearchNavigate}
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
History,
|
History,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Plus,
|
Plus,
|
||||||
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Identity } from "./Identity";
|
import { Identity } from "./Identity";
|
||||||
import { agentUrl, projectUrl } from "../lib/utils";
|
import { agentUrl, projectUrl } from "../lib/utils";
|
||||||
|
|
@ -114,6 +115,16 @@ export function CommandPalette() {
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
|
||||||
<CommandGroup heading="Actions">
|
<CommandGroup heading="Actions">
|
||||||
|
<CommandItem
|
||||||
|
value="search-chat"
|
||||||
|
onSelect={() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("nexus:open-chat-search"));
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search className="mr-2 h-4 w-4" />
|
||||||
|
Search chat messages
|
||||||
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ const STORAGE_KEY = "nexus:chat-panel-open";
|
||||||
interface ChatPanelContextValue {
|
interface ChatPanelContextValue {
|
||||||
chatOpen: boolean;
|
chatOpen: boolean;
|
||||||
activeConversationId: string | null;
|
activeConversationId: string | null;
|
||||||
|
scrollToMessageId: string | null;
|
||||||
setChatOpen: (open: boolean) => void;
|
setChatOpen: (open: boolean) => void;
|
||||||
toggleChat: () => void;
|
toggleChat: () => void;
|
||||||
setActiveConversationId: (id: string | null) => void;
|
setActiveConversationId: (id: string | null) => void;
|
||||||
|
setScrollToMessageId: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatPanelContext = createContext<ChatPanelContextValue | null>(null);
|
const ChatPanelContext = createContext<ChatPanelContextValue | null>(null);
|
||||||
|
|
@ -32,6 +34,7 @@ function writePreference(open: boolean) {
|
||||||
export function ChatPanelProvider({ children }: { children: ReactNode }) {
|
export function ChatPanelProvider({ children }: { children: ReactNode }) {
|
||||||
const [chatOpen, setChatOpenState] = useState(readPreference);
|
const [chatOpen, setChatOpenState] = useState(readPreference);
|
||||||
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
|
||||||
|
const [scrollToMessageId, setScrollToMessageId] = useState<string | null>(null);
|
||||||
|
|
||||||
const setChatOpen = useCallback((open: boolean) => {
|
const setChatOpen = useCallback((open: boolean) => {
|
||||||
setChatOpenState(open);
|
setChatOpenState(open);
|
||||||
|
|
@ -48,7 +51,7 @@ export function ChatPanelProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatPanelContext.Provider
|
<ChatPanelContext.Provider
|
||||||
value={{ chatOpen, activeConversationId, setChatOpen, toggleChat, setActiveConversationId }}
|
value={{ chatOpen, activeConversationId, scrollToMessageId, setChatOpen, toggleChat, setActiveConversationId, setScrollToMessageId }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ChatPanelContext.Provider>
|
</ChatPanelContext.Provider>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue