From 32a37a2db33624e8736686d79297f8230c00b740 Mon Sep 17 00:00:00 2001 From: Lily Delalande <119957291+lily-de@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:04:04 -0400 Subject: [PATCH] ui: context management modal (#2326) --- ui/desktop/src/components/ChatView.tsx | 99 ++++++++-- .../src/components/bottom_menu/BottomMenu.tsx | 178 +++++++++--------- .../SessionSummaryModal.tsx | 126 +++++++++++++ .../components/context_management/index.ts | 37 ++++ ui/desktop/src/hooks/useMessageStream.ts | 1 + 5 files changed, 341 insertions(+), 100 deletions(-) create mode 100644 ui/desktop/src/components/context_management/SessionSummaryModal.tsx create mode 100644 ui/desktop/src/components/context_management/index.ts diff --git a/ui/desktop/src/components/ChatView.tsx b/ui/desktop/src/components/ChatView.tsx index 33af8278..25f597f4 100644 --- a/ui/desktop/src/components/ChatView.tsx +++ b/ui/desktop/src/components/ChatView.tsx @@ -19,6 +19,7 @@ import { fetchSessionDetails } from '../sessions'; // import { configureRecipeExtensions } from '../utils/recipeExtensions'; import 'react-toastify/dist/ReactToastify.css'; import { useMessageStream } from '../hooks/useMessageStream'; +import { SessionSummaryModal } from './context_management/SessionSummaryModal'; import { Recipe } from '../recipe'; import { Message, @@ -29,6 +30,7 @@ import { ToolResponseMessageContent, ToolConfirmationRequestMessageContent, } from '../types/message'; +import { manageContext } from './context_management'; export interface ChatType { id: string; @@ -70,6 +72,16 @@ export default function ChatView({ const [sessionTokenCount, setSessionTokenCount] = useState(0); const scrollRef = useRef(null); + const [isSummaryModalOpen, setIsSummaryModalOpen] = useState(false); + const [summaryContent, setSummaryContent] = useState(''); + const [summarizedThread, setSummarizedThread] = useState([]); + + // Add this function to handle opening the summary modal with content + const handleViewSummary = (summary: string) => { + setSummaryContent(summary); + setIsSummaryModalOpen(true); + }; + // Get recipeConfig directly from appConfig const recipeConfig = window.appConfig.get('recipeConfig') as Recipe | null; @@ -288,9 +300,55 @@ export default function ChatView({ } }; + // Add this function to ChatView.tsx to detect if a message contains ContextLengthExceededContent + const hasContextLengthExceededContent = (message: Message): boolean => { + return message.content.some((content) => content.type === 'contextLengthExceeded'); + }; + + const handleContextLengthExceeded = async () => { + // If we already have a summary, use that + if (summaryContent) { + return summaryContent; + } + + // Otherwise, generate a summary + const response = await manageContext({ messages: messages, manageAction: 'summarize' }); + setSummarizedThread(response.messages); + return response.messages[0].text; + }; + + const SummarizedNotification = ({ + onViewSummary, + }: { + onViewSummary: (summaryContent: string) => void; + }) => { + const handleViewSummary = async () => { + // Await the result to get a string + const summary = summaryContent || (await handleContextLengthExceeded()); + onViewSummary(summary); // Now always passing a string + }; + + return ( +
+ Session summarized + +
+ ); + }; + // Filter out standalone tool response messages for rendering // They will be shown as part of the tool invocation in the assistant message const filteredMessages = messages.filter((message) => { + // TODO: use this summarized thread in the chat window + if (summarizedThread.length > 0) { + // we have a summarized thread + console.log('summarized thread has been created --', summarizedThread); + } // Keep all assistant messages and user messages that aren't just tool responses if (message.role === 'assistant') return true; @@ -379,17 +437,24 @@ export default function ChatView({ {isUserMessage(message) ? ( ) : ( - append(createUserMessage(text))} - appendMessage={(newMessage) => { - const updatedMessages = [...messages, newMessage]; - setMessages(updatedMessages); - }} - /> + <> + {/* Only render GooseMessage if it's not a CLE message (and we are not in alpha mode) */} + {process.env.ALPHA && hasContextLengthExceededContent(message) ? ( + // Render the summarized notification for CLE messages only in alpha mode + + ) : ( + append(createUserMessage(text))} + appendMessage={(newMessage) => { + const updatedMessages = [...messages, newMessage]; + setMessages(updatedMessages); + }} + /> + )} + )} ))} @@ -434,6 +499,18 @@ export default function ChatView({ {showGame && setShowGame(false)} />} + {process.env.ALPHA && ( + setIsSummaryModalOpen(false)} + onSave={(editedContent) => { + console.log('Saving summary...'); + setSummaryContent(editedContent); + setIsSummaryModalOpen(false); + }} + summaryContent={summaryContent} + /> + )} ); } diff --git a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx index 8537bc59..ffecc350 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx @@ -19,10 +19,10 @@ const TOKEN_WARNING_THRESHOLD = 0.8; // warning shows at 80% of the token limit const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning export default function BottomMenu({ - hasMessages, - setView, - numTokens = 0, -}: { + hasMessages, + setView, + numTokens = 0, + }: { hasMessages: boolean; setView: (view: View, viewOptions?: ViewOptions) => void; numTokens?: number; @@ -158,18 +158,18 @@ export default function BottomMenu({ }, [isDirTruncated]); return ( -
- {/* Directory Chooser - Always visible */} - { - if (hasMessages) { - window.electron.directoryChooser(); - } else { - window.electron.directoryChooser(true); - } - }} - > +
+ {/* Directory Chooser - Always visible */} + { + if (hasMessages) { + window.electron.directoryChooser(); + } else { + window.electron.directoryChooser(true); + } + }} + > @@ -191,85 +191,85 @@ export default function BottomMenu({ - {/* Goose Mode Selector Dropdown */} - + {/* Goose Mode Selector Dropdown */} + - {/* Right-side section with ToolCount and Model Selector together */} -
- {/* Tool and Token count */} - {} - {/* Model Selector Dropdown */} - {settingsV2Enabled ? ( - - ) : ( -
-
setIsModelMenuOpen(!isModelMenuOpen)} - > - {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} - {isModelMenuOpen ? ( - - ) : ( - - )} -
+ {/* Right-side section with ToolCount and Model Selector together */} +
+ {/* Tool and Token count */} + {} + {/* Model Selector Dropdown */} + {settingsV2Enabled ? ( + + ) : ( +
+
setIsModelMenuOpen(!isModelMenuOpen)} + > + {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} + {isModelMenuOpen ? ( + + ) : ( + + )} +
- {/* Dropdown Menu */} - {isModelMenuOpen && ( -
-
- ( -
- )} -
- )} + )} +
-
); -} +} \ No newline at end of file diff --git a/ui/desktop/src/components/context_management/SessionSummaryModal.tsx b/ui/desktop/src/components/context_management/SessionSummaryModal.tsx new file mode 100644 index 00000000..7a875f54 --- /dev/null +++ b/ui/desktop/src/components/context_management/SessionSummaryModal.tsx @@ -0,0 +1,126 @@ +import React, { useRef, useEffect } from 'react'; +import { Card } from '../ui/card'; +import { Geese } from '../icons/Geese'; + +interface SessionSummaryModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (editedContent: string) => void; + summaryContent: string; +} + +// This is a specialized version of BaseModal that's wider just for the SessionSummaryModal +function WiderBaseModal({ + isOpen, + title, + children, + actions, +}: { + isOpen: boolean; + title: string; + children: React.ReactNode; + actions: React.ReactNode; // Buttons for actions +}) { + if (!isOpen) return null; + + return ( +
+ +
+ {/* Header */} +
+

{title}

+
+ + {/* Content - Make it scrollable */} + {children &&
{children}
} + + {/* Actions */} +
{actions}
+
+
+
+ ); +} + +export function SessionSummaryModal({ + isOpen, + onClose, + onSave, + summaryContent, +}: SessionSummaryModalProps) { + // Use a ref for the textarea for uncontrolled component + const textareaRef = useRef(null); + + // Initialize the textarea value when the modal opens + useEffect(() => { + if (isOpen && textareaRef.current) { + textareaRef.current.value = summaryContent; + } + }, [isOpen, summaryContent]); + + // Handle Save action with the edited content from the ref + const handleSave = () => { + const currentText = textareaRef.current ? textareaRef.current.value : ''; + onSave(currentText); + }; + + // Header Component - Icon, Title, and Description + const Header = () => ( +
+ {/* Icon */} +
+ +
+ + {/* Title */} +

Session Summary

+ + {/* Description */} +

+ This summary was created to manage your context limit. Review and edit to keep your session + running smoothly with the information that matters most. +

+
+ ); + + // Uncontrolled Summary Content Component + const SummaryContent = () => ( +
+

Summarization

+ +