Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/goose-cli/src/session/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ pub fn message_to_markdown(message: &Message, export_all_content: bool) -> Strin
md.push_str("**Thinking:**\n");
md.push_str("> *Thinking was redacted*\n\n");
}
MessageContent::SummarizationRequested(summarization) => {
md.push_str(&format!("*{}*\n\n", summarization.msg));
}
_ => {
md.push_str(
"`WARNING: Message content type could not be rendered to Markdown`\n\n",
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-cli/src/session/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ pub fn render_message(message: &Message, debug: bool) {
println!("\n{}", style("Thinking:").dim().italic());
print_markdown("Thinking was redacted", theme);
}
MessageContent::SummarizationRequested(summarization) => {
println!("\n{}", style(&summarization.msg).yellow());
}
_ => {
println!("WARNING: Message content type could not be rendered");
}
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -946,7 +946,7 @@ impl Agent {
// If we compacted, yield the compaction message and history replacement event
if let Some(compaction_msg) = compaction_msg {
return Ok(Box::pin(async_stream::try_stream! {
yield AgentEvent::Message(Message::assistant().with_text(compaction_msg));
yield AgentEvent::Message(Message::assistant().with_summarization_requested(compaction_msg));
yield AgentEvent::HistoryReplaced(messages.messages().clone());

// Continue with normal reply processing using compacted messages
Expand Down
19 changes: 15 additions & 4 deletions crates/goose/src/agents/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,21 @@ impl Agent {

// Add an assistant message to the summarized messages to ensure the assistant's response is included in the context.
if new_messages.len() == 1 {
let assistant_message = Message::assistant().with_text(
"I ran into a context length exceeded error so I summarized our conversation.",
);
let assistant_message_tokens: usize = 14;
let compaction_marker = Message::assistant()
.with_summarization_requested("Conversation compacted and summarized");
let compaction_marker_tokens: usize = 8;

// Insert the marker before the summary message
new_messages.insert(0, compaction_marker);
new_token_counts.insert(0, compaction_marker_tokens);

// Add an assistant message to continue the conversation
let assistant_message = Message::assistant().with_text("
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a second message here now? Is the idea that this will become agent visible only, and the other one will be user visible only?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the idea that this will become agent visible only, and the other one will be user visible only

yes! to show the user a brief inline notification that compaction occurred, and give the model the deal of what happened and how it should behave when the session continues

The previous message contains a summary that was prepared because a context limit was reached.
Do not mention that you read a summary or that conversation summarization occurred
Just continue the conversation naturally based on the summarized context
");
let assistant_message_tokens: usize = 41;
new_messages.push(assistant_message);
new_token_counts.push(assistant_message_tokens);
}
Expand Down
3 changes: 3 additions & 0 deletions crates/goose/src/context_mgmt/auto_compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@ mod tests {
create_test_message("First message"),
create_test_message("Second message"),
create_test_message("Third message"),
create_test_message("Fourth message"),
create_test_message("Fifth message"),
];

// Create session metadata with high token count to trigger compaction
Expand All @@ -617,6 +619,7 @@ mod tests {

// Verify the compacted messages are returned
assert!(!result.messages.is_empty());

// Should have fewer messages after compaction
assert!(result.messages.len() <= messages.len());
}
Expand Down
178 changes: 52 additions & 126 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* - Integrates with multiple custom hooks for separation of concerns:
* - useChatEngine: Core chat functionality and API integration
* - useRecipeManager: Recipe/agent configuration management
* - useSessionContinuation: Session persistence and resumption
* - useFileDrop: Drag-and-drop file handling with previews
* - useCostTracking: Token usage and cost calculation
*
Expand Down Expand Up @@ -51,20 +50,15 @@ import LoadingGoose from './LoadingGoose';
import RecipeActivities from './RecipeActivities';
import PopularChatTopics from './PopularChatTopics';
import ProgressiveMessageList from './ProgressiveMessageList';
import { SessionSummaryModal } from './context_management/SessionSummaryModal';
import {
ChatContextManagerProvider,
useChatContextManager,
} from './context_management/ChatContextManager';
import { View, ViewOptions } from '../utils/navigationUtils';
import { ContextManagerProvider, useContextManager } from './context_management/ContextManager';
import { MainPanelLayout } from './Layout/MainPanelLayout';
import ChatInput from './ChatInput';
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
import { RecipeWarningModal } from './ui/RecipeWarningModal';
import ParameterInputModal from './ParameterInputModal';
import { useChatEngine } from '../hooks/useChatEngine';
import { useRecipeManager } from '../hooks/useRecipeManager';
import { useSessionContinuation } from '../hooks/useSessionContinuation';
import { useFileDrop } from '../hooks/useFileDrop';
import { useCostTracking } from '../hooks/useCostTracking';
import { Message } from '../types/message';
Expand Down Expand Up @@ -125,21 +119,12 @@ function BaseChatContent({
const [hasStartedUsingRecipe, setHasStartedUsingRecipe] = React.useState(false);
const [currentRecipeTitle, setCurrentRecipeTitle] = React.useState<string | null>(null);

const {
summaryContent,
summarizedThread,
isSummaryModalOpen,
isLoadingCompaction,
resetMessagesWithSummary,
closeSummaryModal,
updateSummary,
} = useChatContextManager();
const { isCompacting, handleManualCompaction } = useContextManager();

// Use shared chat engine
const {
messages,
filteredMessages,
ancestorMessages,
setAncestorMessages,
append,
chatState,
Expand All @@ -156,7 +141,6 @@ function BaseChatContent({
localOutputTokens,
commandHistory,
toolCallNotifications,
updateMessageStreamBody,
sessionMetadata,
isUserMessage,
clearError,
Expand All @@ -173,9 +157,6 @@ function BaseChatContent({
if (recipeConfig) {
setHasStartedUsingRecipe(true);
}

// Create new session after message is sent if needed
createNewSessionIfNeeded();
},
enableLocalStorage,
});
Expand Down Expand Up @@ -233,14 +214,6 @@ function BaseChatContent({
});
}, [handleAutoExecution, append, chatState]);

// Use shared session continuation
const { createNewSessionIfNeeded } = useSessionContinuation({
chat,
setChat,
summarizedThread,
updateMessageStreamBody,
});

// Use shared file drop
const { droppedFiles, setDroppedFiles, handleDrop, handleDragOver } = useFileDrop();

Expand All @@ -260,7 +233,7 @@ function BaseChatContent({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Empty dependency array means this runs once on mount

// Handle submit with summary reset support
// Handle submit
const handleSubmit = (e: React.FormEvent) => {
const customEvent = e as unknown as CustomEvent;
const combinedTextFromInput = customEvent.detail?.value || '';
Expand All @@ -270,35 +243,12 @@ function BaseChatContent({
setHasStartedUsingRecipe(true);
}

const onSummaryReset =
summarizedThread.length > 0
? () => {
resetMessagesWithSummary(
messages,
setMessages,
ancestorMessages,
setAncestorMessages,
summaryContent
);
}
: undefined;

// Call the callback if provided (for Hub to handle navigation)
if (onMessageSubmit && combinedTextFromInput.trim()) {
onMessageSubmit(combinedTextFromInput);
}

engineHandleSubmit(combinedTextFromInput, onSummaryReset);

// Auto-scroll to bottom after submitting
if (onSummaryReset) {
// If we're resetting with summary, delay the scroll a bit more
setTimeout(() => {
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}, 200);
}
engineHandleSubmit(combinedTextFromInput);
};

// Wrapper for append that tracks recipe usage
Expand Down Expand Up @@ -433,67 +383,49 @@ function BaseChatContent({
</SearchView>
)}

{error &&
!(error as Error & { isTokenLimitError?: boolean }).isTokenLimitError && (
<>
<div className="flex flex-col items-center justify-center p-4">
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
{error.message || 'Honk! Goose experienced an error while responding'}
</div>
{error && (
<>
<div className="flex flex-col items-center justify-center p-4">
<div className="text-red-700 dark:text-red-300 bg-red-400/50 p-3 rounded-lg mb-2">
{error.message || 'Honk! Goose experienced an error while responding'}
</div>

{/* Action buttons for non-token-limit errors */}
<div className="flex gap-2 mt-2">
<div
className="px-3 py-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
onClick={async () => {
// Create a contextLengthExceeded message similar to token limit errors
const contextMessage: Message = {
id: `context-${Date.now()}`,
role: 'assistant',
created: Math.floor(Date.now() / 1000),
content: [
{
type: 'contextLengthExceeded',
msg: 'Summarization requested due to error. Creating summary to help resolve the issue.',
},
],
display: true,
sendToLLM: false,
};

// Add the context message to trigger ContextHandler
const updatedMessages = [...messages, contextMessage];
setMessages(updatedMessages);

// Clear the error state since we're handling it with summarization
clearError();
}}
>
Summarize Conversation
</div>
<div
className="px-3 py-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
onClick={async () => {
// Find the last user message
const lastUserMessage = messages.reduceRight(
(found, m) => found || (m.role === 'user' ? m : null),
null as Message | null
);
if (lastUserMessage) {
append(lastUserMessage);
}
}}
>
Retry Last Message
</div>
{/* Action buttons for all errors including token limit errors */}
<div className="flex gap-2 mt-2">
<div
className="px-3 py-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
onClick={async () => {
clearError();

await handleManualCompaction(
messages,
setMessages,
append,
setAncestorMessages
);
}}
>
Summarize Conversation
</div>
<div
className="px-3 py-2 text-center whitespace-nowrap cursor-pointer text-textStandard border border-borderSubtle hover:bg-bgSubtle rounded-full inline-block transition-all duration-150"
onClick={async () => {
// Find the last user message
const lastUserMessage = messages.reduceRight(
(found, m) => found || (m.role === 'user' ? m : null),
null as Message | null
);
if (lastUserMessage) {
append(lastUserMessage);
}
}}
>
Retry Last Message
</div>
</div>
</>
)}

{/* Token limit errors should be handled by ContextHandler, not shown here */}
{error &&
(error as Error & { isTokenLimitError?: boolean }).isTokenLimitError && <></>}
</div>
</>
)}
<div className="block h-8" />
</>
) : showPopularTopics ? (
Expand All @@ -507,10 +439,10 @@ function BaseChatContent({
</ScrollArea>

{/* Fixed loading indicator at bottom left of chat container */}
{chatState !== ChatState.Idle && (
{(chatState !== ChatState.Idle || isCompacting) && (
<div className="absolute bottom-1 left-4 z-20 pointer-events-none">
<LoadingGoose
message={isLoadingCompaction ? 'summarizing conversation' : undefined}
message={isCompacting ? 'goose is compacting the conversation...' : undefined}
chatState={chatState}
/>
</div>
Expand Down Expand Up @@ -541,21 +473,13 @@ function BaseChatContent({
recipeAccepted={recipeAccepted}
initialPrompt={initialPrompt}
autoSubmit={autoSubmit}
setAncestorMessages={setAncestorMessages}
append={append}
{...customChatInputProps}
/>
</div>
</MainPanelLayout>

<SessionSummaryModal
isOpen={isSummaryModalOpen}
onClose={closeSummaryModal}
onSave={(editedContent) => {
updateSummary(editedContent);
closeSummaryModal();
}}
summaryContent={summaryContent}
/>

{/* Recipe Warning Modal */}
<RecipeWarningModal
isOpen={isRecipeWarningModalOpen}
Expand Down Expand Up @@ -595,14 +519,16 @@ function BaseChatContent({
</div>
</div>
)}

{/* No modals needed for the new simplified context manager */}
</div>
);
}

export default function BaseChat(props: BaseChatProps) {
return (
<ChatContextManagerProvider>
<ContextManagerProvider>
<BaseChatContent {...props} />
</ChatContextManagerProvider>
</ContextManagerProvider>
);
}
Loading
Loading