diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 145074c7717d..bf7c387d3e77 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState, useEffect, useMemo } from 'react'; -import { FolderKey } from 'lucide-react'; +import { FolderKey, ScrollText } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip'; import { Button } from './ui/button'; import type { View } from '../App'; @@ -12,7 +12,6 @@ import { Message } from '../types/message'; import { DirSwitcher } from './bottom_menu/DirSwitcher'; import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar'; import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection'; -import { ManualCompactButton } from './context_management/ManualCompactButton'; import { AlertType, useAlerts } from './alerts'; import { useToolCount } from './alerts/useToolCount'; import { useConfig } from './ConfigContext'; @@ -110,7 +109,7 @@ export default function ChatInput({ const { alerts, addAlert, clearAlerts } = useAlerts(); const dropdownRef = useRef(null); const toolCount = useToolCount(); - const { isLoadingCompaction } = useChatContextManager(); + const { isLoadingCompaction, handleManualCompaction } = useChatContextManager(); const { getProviders, read } = useConfig(); const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); @@ -420,7 +419,7 @@ export default function ChatInput({ autoShow: true, // Auto-show token limit warnings }); } else { - // Show info alert only when not in warning/error state + // Show info alert with summarize button addAlert({ type: AlertType.Info, message: 'Context window', @@ -428,6 +427,11 @@ export default function ChatInput({ current: numTokens, total: tokenLimit, }, + showSummarizeButton: true, + onSummarize: () => { + handleManualCompaction(messages, setMessages); + }, + summarizeIcon: , }); } } else if (isTokenLimitLoaded && tokenLimit) { @@ -439,6 +443,14 @@ export default function ChatInput({ current: 0, total: tokenLimit, }, + showSummarizeButton: messages.length > 0, + onSummarize: + messages.length > 0 + ? () => { + handleManualCompaction(messages, setMessages); + } + : undefined, + summarizeIcon: messages.length > 0 ? : undefined, }); } @@ -1286,13 +1298,6 @@ export default function ChatInput({
- {messages.length > 0 && ( - - )}
diff --git a/ui/desktop/src/components/alerts/AlertBox.tsx b/ui/desktop/src/components/alerts/AlertBox.tsx index c93f249f2ff5..0fafdd3bb531 100644 --- a/ui/desktop/src/components/alerts/AlertBox.tsx +++ b/ui/desktop/src/components/alerts/AlertBox.tsx @@ -60,6 +60,19 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => { : alert.progress!.total}
+ {alert.showSummarizeButton && alert.onSummarize && ( + + )}
) : ( <> diff --git a/ui/desktop/src/components/alerts/types.ts b/ui/desktop/src/components/alerts/types.ts index 661191e50b7f..44eeafbd19a0 100644 --- a/ui/desktop/src/components/alerts/types.ts +++ b/ui/desktop/src/components/alerts/types.ts @@ -16,4 +16,7 @@ export interface Alert { current: number; total: number; }; + showSummarizeButton?: boolean; + onSummarize?: () => void; + summarizeIcon?: React.ReactNode; } diff --git a/ui/desktop/src/components/context_management/ChatContextManager.tsx b/ui/desktop/src/components/context_management/ChatContextManager.tsx index 2e18e02bf354..4e3493d7d35f 100644 --- a/ui/desktop/src/components/context_management/ChatContextManager.tsx +++ b/ui/desktop/src/components/context_management/ChatContextManager.tsx @@ -1,10 +1,20 @@ import React, { createContext, useContext, useState } from 'react'; +import { ScrollText } from 'lucide-react'; import { Message } from '../../types/message'; import { manageContextFromBackend, convertApiMessageToFrontendMessage, createSummarizationRequestMessage, } from './index'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '../ui/dialog'; +import { Button } from '../ui/button'; // Define the context management interface interface ChatContextManagerState { @@ -14,6 +24,8 @@ interface ChatContextManagerState { isLoadingCompaction: boolean; errorLoadingSummary: boolean; preparingManualSummary: boolean; + isConfirmationOpen: boolean; + pendingCompactionData: { messages: Message[]; setMessages: (messages: Message[]) => void } | null; } interface ChatContextManagerActions { @@ -50,6 +62,11 @@ export const ChatContextManagerProvider: React.FC<{ children: React.ReactNode }> const [isLoadingCompaction, setIsLoadingCompaction] = useState(false); const [errorLoadingSummary, setErrorLoadingSummary] = useState(false); const [preparingManualSummary, setPreparingManualSummary] = useState(false); + const [isConfirmationOpen, setIsConfirmationOpen] = useState(false); + const [pendingCompactionData, setPendingCompactionData] = useState<{ + messages: Message[]; + setMessages: (messages: Message[]) => void; + } | null>(null); const handleContextLengthExceeded = async (messages: Message[]): Promise => { setIsLoadingCompaction(true); @@ -90,6 +107,16 @@ export const ChatContextManagerProvider: React.FC<{ children: React.ReactNode }> messages: Message[], setMessages: (messages: Message[]) => void ): void => { + // Store the pending compaction data and open confirmation dialog + setPendingCompactionData({ messages, setMessages }); + setIsConfirmationOpen(true); + }; + + const handleCompactionConfirm = () => { + if (!pendingCompactionData) return; + + const { messages, setMessages } = pendingCompactionData; + // add some messages to the message thread // these messages will be filtered out in chat view // but they will also be what allows us to render some text in the chatview itself, similar to CLE events @@ -100,6 +127,14 @@ export const ChatContextManagerProvider: React.FC<{ children: React.ReactNode }> // add the message to the message thread setMessages([...messages, summarizationRequest]); + + setIsConfirmationOpen(false); + setPendingCompactionData(null); + }; + + const handleCompactionCancel = () => { + setIsConfirmationOpen(false); + setPendingCompactionData(null); }; const updateSummary = (newSummaryContent: string) => { @@ -242,6 +277,8 @@ export const ChatContextManagerProvider: React.FC<{ children: React.ReactNode }> isLoadingCompaction, errorLoadingSummary, preparingManualSummary, + isConfirmationOpen, + pendingCompactionData, // Actions updateSummary, @@ -259,6 +296,39 @@ export const ChatContextManagerProvider: React.FC<{ children: React.ReactNode }> return ( {children} + + {/* Confirmation Modal */} + + + + + + Compact Conversation + + + This will compact your conversation by summarizing the context into a single message + and will help you save context space for future interactions. + + + +
+

+ Previous messages will remain visible but only the summary will be included in the + active context for Goose. This is useful for long conversations that are approaching + the context limit. +

+
+ + + + + +
+
); }; diff --git a/ui/desktop/src/components/context_management/ManualCompactButton.tsx b/ui/desktop/src/components/context_management/ManualCompactButton.tsx index daf1b52d3c15..bfc965cb2110 100644 --- a/ui/desktop/src/components/context_management/ManualCompactButton.tsx +++ b/ui/desktop/src/components/context_management/ManualCompactButton.tsx @@ -72,7 +72,7 @@ export const ManualCompactButton: React.FC = ({
- {/* Confirmation Modal */} + {/* Summarization Confirmation Modal */}