Replace compaction notifications with system notifications (#5218)
This commit is contained in:
+24
-103
@@ -852,50 +852,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/context/manage": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Context Management"
|
||||
],
|
||||
"operationId": "manage_context",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContextManageRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Context managed successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ContextManageResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized - Invalid or missing API key"
|
||||
},
|
||||
"412": {
|
||||
"description": "Precondition failed - Agent not available"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/diagnostics/{session_id}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2195,69 +2151,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ContextManageRequest": {
|
||||
"type": "object",
|
||||
"description": "Request payload for context management operations",
|
||||
"required": [
|
||||
"messages",
|
||||
"sessionId"
|
||||
],
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
},
|
||||
"description": "Collection of messages to be managed"
|
||||
},
|
||||
"sessionId": {
|
||||
"type": "string",
|
||||
"description": "Optional session ID for session-specific agent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ContextManageResponse": {
|
||||
"type": "object",
|
||||
"description": "Response from context management operations",
|
||||
"required": [
|
||||
"messages",
|
||||
"tokenCounts"
|
||||
],
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
},
|
||||
"description": "Processed messages after the operation"
|
||||
},
|
||||
"tokenCounts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"description": "Token counts for each processed message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Conversation": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Message"
|
||||
}
|
||||
},
|
||||
"ConversationCompacted": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -3249,7 +3148,7 @@
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ConversationCompacted"
|
||||
"$ref": "#/components/schemas/SystemNotificationContent"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
@@ -3260,7 +3159,7 @@
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"conversationCompacted"
|
||||
"systemNotification"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -4310,6 +4209,28 @@
|
||||
"propertyName": "type"
|
||||
}
|
||||
},
|
||||
"SystemNotificationContent": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"notificationType",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"notificationType": {
|
||||
"$ref": "#/components/schemas/SystemNotificationType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SystemNotificationType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"thinkingMessage",
|
||||
"inlineMessage"
|
||||
]
|
||||
},
|
||||
"TextContent": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ManageContextData, ManageContextErrors, ManageContextResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionDescriptionData, UpdateSessionDescriptionErrors, UpdateSessionDescriptionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionDescriptionData, UpdateSessionDescriptionErrors, UpdateSessionDescriptionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -245,17 +245,6 @@ export const confirmPermission = <ThrowOnError extends boolean = false>(options:
|
||||
});
|
||||
};
|
||||
|
||||
export const manageContext = <ThrowOnError extends boolean = false>(options: Options<ManageContextData, ThrowOnError>) => {
|
||||
return (options.client ?? client).post<ManageContextResponses, ManageContextErrors, ThrowOnError>({
|
||||
url: '/context/manage',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const diagnostics = <ThrowOnError extends boolean = false>(options: Options<DiagnosticsData, ThrowOnError>) => {
|
||||
return (options.client ?? client).get<DiagnosticsResponses, DiagnosticsErrors, ThrowOnError>({
|
||||
url: '/diagnostics/{session_id}',
|
||||
|
||||
@@ -67,40 +67,8 @@ export type ConfigResponse = {
|
||||
|
||||
export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | RawAudioContent | RawResource;
|
||||
|
||||
/**
|
||||
* Request payload for context management operations
|
||||
*/
|
||||
export type ContextManageRequest = {
|
||||
/**
|
||||
* Collection of messages to be managed
|
||||
*/
|
||||
messages: Array<Message>;
|
||||
/**
|
||||
* Optional session ID for session-specific agent
|
||||
*/
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Response from context management operations
|
||||
*/
|
||||
export type ContextManageResponse = {
|
||||
/**
|
||||
* Processed messages after the operation
|
||||
*/
|
||||
messages: Array<Message>;
|
||||
/**
|
||||
* Token counts for each processed message
|
||||
*/
|
||||
tokenCounts: Array<number>;
|
||||
};
|
||||
|
||||
export type Conversation = Array<Message>;
|
||||
|
||||
export type ConversationCompacted = {
|
||||
msg: string;
|
||||
};
|
||||
|
||||
export type CreateRecipeRequest = {
|
||||
author?: AuthorRequest | null;
|
||||
session_id: string;
|
||||
@@ -388,8 +356,8 @@ export type MessageContent = (TextContent & {
|
||||
type: 'thinking';
|
||||
}) | (RedactedThinkingContent & {
|
||||
type: 'redactedThinking';
|
||||
}) | (ConversationCompacted & {
|
||||
type: 'conversationCompacted';
|
||||
}) | (SystemNotificationContent & {
|
||||
type: 'systemNotification';
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -762,6 +730,13 @@ export type SuccessCheck = {
|
||||
type: 'Shell';
|
||||
};
|
||||
|
||||
export type SystemNotificationContent = {
|
||||
msg: string;
|
||||
notificationType: SystemNotificationType;
|
||||
};
|
||||
|
||||
export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage';
|
||||
|
||||
export type TextContent = {
|
||||
_meta?: {
|
||||
[key: string]: unknown;
|
||||
@@ -1563,37 +1538,6 @@ export type ConfirmPermissionResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ManageContextData = {
|
||||
body: ContextManageRequest;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/context/manage';
|
||||
};
|
||||
|
||||
export type ManageContextErrors = {
|
||||
/**
|
||||
* Unauthorized - Invalid or missing API key
|
||||
*/
|
||||
401: unknown;
|
||||
/**
|
||||
* Precondition failed - Agent not available
|
||||
*/
|
||||
412: unknown;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: unknown;
|
||||
};
|
||||
|
||||
export type ManageContextResponses = {
|
||||
/**
|
||||
* Context managed successfully
|
||||
*/
|
||||
200: ContextManageResponse;
|
||||
};
|
||||
|
||||
export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses];
|
||||
|
||||
export type DiagnosticsData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
||||
@@ -46,11 +46,11 @@ import { useLocation } from 'react-router-dom';
|
||||
import { SearchView } from './conversation/SearchView';
|
||||
import { RecipeHeader } from './RecipeHeader';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import { getThinkingMessage } from '../types/message';
|
||||
import RecipeActivities from './recipes/RecipeActivities';
|
||||
import PopularChatTopics from './PopularChatTopics';
|
||||
import ProgressiveMessageList from './ProgressiveMessageList';
|
||||
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';
|
||||
@@ -115,7 +115,6 @@ function BaseChatContent({
|
||||
const disableAnimation = location.state?.disableAnimation || false;
|
||||
const [hasStartedUsingRecipe, setHasStartedUsingRecipe] = React.useState(false);
|
||||
const [currentRecipeTitle, setCurrentRecipeTitle] = React.useState<string | null>(null);
|
||||
const { isCompacting, handleManualCompaction } = useContextManager();
|
||||
|
||||
// Use shared chat engine
|
||||
const {
|
||||
@@ -382,26 +381,12 @@ function BaseChatContent({
|
||||
{error.message || 'Honk! Goose experienced an error while responding'}
|
||||
</div>
|
||||
|
||||
{/* Action buttons for all errors including token limit errors */}
|
||||
{/* Action button to retry last message */}
|
||||
<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,
|
||||
chat.sessionId
|
||||
);
|
||||
}}
|
||||
>
|
||||
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),
|
||||
@@ -432,15 +417,13 @@ function BaseChatContent({
|
||||
</ScrollArea>
|
||||
|
||||
{/* Fixed loading indicator at bottom left of chat container */}
|
||||
{(chatState !== ChatState.Idle || loadingChat || isCompacting) && (
|
||||
{(chatState !== ChatState.Idle || loadingChat) && (
|
||||
<div className="absolute bottom-1 left-4 z-20 pointer-events-none">
|
||||
<LoadingGoose
|
||||
message={
|
||||
loadingChat
|
||||
? 'loading conversation...'
|
||||
: isCompacting
|
||||
? 'goose is compacting the conversation...'
|
||||
: undefined
|
||||
: getThinkingMessage(messages[messages.length - 1])
|
||||
}
|
||||
chatState={chatState}
|
||||
/>
|
||||
@@ -465,7 +448,6 @@ function BaseChatContent({
|
||||
droppedFiles={droppedFiles}
|
||||
onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
disableAnimation={disableAnimation}
|
||||
sessionCosts={sessionCosts}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
@@ -515,9 +497,5 @@ function BaseChatContent({
|
||||
}
|
||||
|
||||
export default function BaseChat(props: BaseChatProps) {
|
||||
return (
|
||||
<ContextManagerProvider>
|
||||
<BaseChatContent {...props} />
|
||||
</ContextManagerProvider>
|
||||
);
|
||||
return <BaseChatContent {...props} />;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SearchView } from './conversation/SearchView';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import { getThinkingMessage } from '../types/message';
|
||||
import PopularChatTopics from './PopularChatTopics';
|
||||
import ProgressiveMessageList from './ProgressiveMessageList';
|
||||
import { ContextManagerProvider } from './context_management/ContextManager';
|
||||
import { MainPanelLayout } from './Layout/MainPanelLayout';
|
||||
import ChatInput from './ChatInput';
|
||||
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
|
||||
@@ -168,8 +168,6 @@ function BaseChatContent({
|
||||
|
||||
const showPopularTopics =
|
||||
messages.length === 0 && !initialMessage && chatState === ChatState.Idle;
|
||||
// TODO(Douwe): get this from the backend
|
||||
const isCompacting = false;
|
||||
|
||||
const chat: ChatType = {
|
||||
messageHistoryIndex: 0,
|
||||
@@ -183,11 +181,10 @@ function BaseChatContent({
|
||||
|
||||
// Map chatState to LoadingGoose message
|
||||
const getLoadingMessage = (): string | undefined => {
|
||||
if (isCompacting) return 'goose is compacting the conversation...';
|
||||
if (messages.length === 0 && chatState === ChatState.Thinking) {
|
||||
return 'loading conversation...';
|
||||
}
|
||||
return undefined;
|
||||
return getThinkingMessage(messages[messages.length - 1]);
|
||||
};
|
||||
return (
|
||||
<div className="h-full flex flex-col min-h-0">
|
||||
@@ -258,7 +255,8 @@ function BaseChatContent({
|
||||
) : null}
|
||||
</ScrollArea>
|
||||
|
||||
{(chatState !== ChatState.Idle || isCompacting) && !sessionLoadError && (
|
||||
{/* Fixed loading indicator at bottom left of chat container */}
|
||||
{chatState !== ChatState.Idle && !sessionLoadError && (
|
||||
<div className="absolute bottom-1 left-4 z-20 pointer-events-none">
|
||||
<LoadingGoose message={getLoadingMessage()} chatState={chatState} />
|
||||
</div>
|
||||
@@ -282,7 +280,6 @@ function BaseChatContent({
|
||||
droppedFiles={droppedFiles}
|
||||
onFilesProcessed={() => setDroppedFiles([])} // Clear dropped files after processing
|
||||
messages={messages}
|
||||
setMessages={(_m) => {}}
|
||||
disableAnimation={disableAnimation}
|
||||
sessionCosts={sessionCosts}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
@@ -331,9 +328,5 @@ function BaseChatContent({
|
||||
}
|
||||
|
||||
export default function BaseChat(props: BaseChatProps) {
|
||||
return (
|
||||
<ContextManagerProvider>
|
||||
<BaseChatContent {...props} />
|
||||
</ContextManagerProvider>
|
||||
);
|
||||
return <BaseChatContent {...props} />;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { WaveformVisualizer } from './WaveformVisualizer';
|
||||
import { toastError } from '../toasts';
|
||||
import MentionPopover, { FileItemWithMatch } from './MentionPopover';
|
||||
import { useDictationSettings } from '../hooks/useDictationSettings';
|
||||
import { useContextManager } from './context_management/ContextManager';
|
||||
import { useChatContext } from '../contexts/ChatContext';
|
||||
import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates';
|
||||
import { CostTracker } from './bottom_menu/CostTracker';
|
||||
@@ -52,6 +51,9 @@ const MAX_IMAGE_SIZE_MB = 5;
|
||||
const TOKEN_LIMIT_DEFAULT = 128000; // fallback for custom models that the backend doesn't know about
|
||||
const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning
|
||||
|
||||
// Manual compact trigger message - must match backend constant
|
||||
const MANUAL_COMPACT_TRIGGER = 'Please compact this conversation';
|
||||
|
||||
interface ModelLimit {
|
||||
pattern: string;
|
||||
context_limit: number;
|
||||
@@ -71,7 +73,6 @@ interface ChatInputProps {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
messages?: Message[];
|
||||
setMessages: (messages: Message[]) => void;
|
||||
sessionCosts?: {
|
||||
[key: string]: {
|
||||
inputTokens: number;
|
||||
@@ -105,7 +106,6 @@ export default function ChatInput({
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
messages = [],
|
||||
setMessages,
|
||||
disableAnimation = false,
|
||||
sessionCosts,
|
||||
setIsGoosehintsModalOpen,
|
||||
@@ -115,7 +115,7 @@ export default function ChatInput({
|
||||
initialPrompt,
|
||||
toolCount,
|
||||
autoSubmit = false,
|
||||
append,
|
||||
append: _append,
|
||||
isExtensionsLoading = false,
|
||||
}: ChatInputProps) {
|
||||
const [_value, setValue] = useState(initialValue);
|
||||
@@ -137,7 +137,6 @@ export default function ChatInput({
|
||||
const dropdownRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
||||
null
|
||||
) as React.RefObject<HTMLDivElement>;
|
||||
const { isCompacting, handleManualCompaction } = useContextManager();
|
||||
const { getProviders, read } = useConfig();
|
||||
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
|
||||
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
||||
@@ -523,9 +522,6 @@ export default function ChatInput({
|
||||
|
||||
// Show alert when either there is registered token usage, or we know the limit
|
||||
if ((numTokens && numTokens > 0) || (isTokenLimitLoaded && tokenLimit)) {
|
||||
// in these conditions we want it to be present but disabled
|
||||
const compactButtonDisabled = !numTokens || isCompacting;
|
||||
|
||||
addAlert({
|
||||
type: AlertType.Info,
|
||||
message: 'Context window',
|
||||
@@ -534,12 +530,15 @@ export default function ChatInput({
|
||||
total: tokenLimit,
|
||||
},
|
||||
showCompactButton: true,
|
||||
compactButtonDisabled,
|
||||
compactButtonDisabled: !numTokens,
|
||||
onCompact: () => {
|
||||
// Hide the alert popup by dispatching a custom event that the popover can listen to
|
||||
// Importantly, this leaves the alert so the dot still shows up, but hides the popover
|
||||
window.dispatchEvent(new CustomEvent('hide-alert-popover'));
|
||||
handleManualCompaction(messages, setMessages, append, sessionId || '');
|
||||
|
||||
const customEvent = new CustomEvent('submit', {
|
||||
detail: { value: MANUAL_COMPACT_TRIGGER },
|
||||
}) as unknown as React.FormEvent;
|
||||
|
||||
handleSubmit(customEvent);
|
||||
},
|
||||
compactIcon: <ScrollText size={12} />,
|
||||
autoCompactThreshold: autoCompactThreshold,
|
||||
@@ -569,7 +568,6 @@ export default function ChatInput({
|
||||
tokenLimit,
|
||||
isTokenLimitLoaded,
|
||||
addAlert,
|
||||
isCompacting,
|
||||
clearAlerts,
|
||||
autoCompactThreshold,
|
||||
]);
|
||||
@@ -940,7 +938,6 @@ export default function ChatInput({
|
||||
|
||||
const canSubmit =
|
||||
!isLoading &&
|
||||
!isCompacting &&
|
||||
agentIsReady &&
|
||||
(displayValue.trim() ||
|
||||
pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) ||
|
||||
@@ -1097,7 +1094,6 @@ export default function ChatInput({
|
||||
e.preventDefault();
|
||||
const canSubmit =
|
||||
!isLoading &&
|
||||
!isCompacting &&
|
||||
agentIsReady &&
|
||||
(displayValue.trim() ||
|
||||
pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) ||
|
||||
@@ -1152,7 +1148,6 @@ export default function ChatInput({
|
||||
isAnyDroppedFileLoading ||
|
||||
isRecording ||
|
||||
isTranscribing ||
|
||||
isCompacting ||
|
||||
!agentIsReady ||
|
||||
isExtensionsLoading;
|
||||
|
||||
@@ -1397,17 +1392,15 @@ export default function ChatInput({
|
||||
<p>
|
||||
{isExtensionsLoading
|
||||
? 'Loading extensions...'
|
||||
: isCompacting
|
||||
? 'Compacting conversation...'
|
||||
: isAnyImageLoading
|
||||
? 'Waiting for images to save...'
|
||||
: isAnyDroppedFileLoading
|
||||
? 'Processing dropped files...'
|
||||
: isRecording
|
||||
? 'Recording...'
|
||||
: isTranscribing
|
||||
? 'Transcribing...'
|
||||
: (chatContext?.agentWaitingMessage ?? 'Send')}
|
||||
: isAnyImageLoading
|
||||
? 'Waiting for images to save...'
|
||||
: isAnyDroppedFileLoading
|
||||
? 'Processing dropped files...'
|
||||
: isRecording
|
||||
? 'Recording...'
|
||||
: isTranscribing
|
||||
? 'Transcribing...'
|
||||
: (chatContext?.agentWaitingMessage ?? 'Send')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -18,8 +18,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Message } from '../api';
|
||||
import GooseMessage from './GooseMessage';
|
||||
import UserMessage from './UserMessage';
|
||||
import { CompactionMarker } from './context_management/CompactionMarker';
|
||||
import { useContextManager } from './context_management/ContextManager';
|
||||
import { SystemNotificationInline } from './context_management/SystemNotificationInline';
|
||||
import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import { ChatType } from '../types/chat';
|
||||
@@ -68,17 +67,12 @@ export default function ProgressiveMessageList({
|
||||
const hasOnlyToolResponses = (message: Message) =>
|
||||
message.content.every((c) => c.type === 'toolResponse');
|
||||
|
||||
// Try to use context manager, but don't require it for session history
|
||||
let hasCompactionMarker: ((message: Message) => boolean) | undefined;
|
||||
|
||||
try {
|
||||
const contextManager = useContextManager();
|
||||
hasCompactionMarker = contextManager.hasCompactionMarker;
|
||||
} catch {
|
||||
// Context manager not available (e.g., in session history view)
|
||||
// This is fine, we'll just skip compaction marker functionality
|
||||
hasCompactionMarker = undefined;
|
||||
}
|
||||
const hasInlineSystemNotification = (message: Message): boolean => {
|
||||
return message.content.some(
|
||||
(content) =>
|
||||
content.type === 'systemNotification' && content.notificationType === 'inlineMessage'
|
||||
);
|
||||
};
|
||||
|
||||
// Simple progressive loading - start immediately when component mounts if needed
|
||||
useEffect(() => {
|
||||
@@ -187,6 +181,19 @@ export default function ProgressiveMessageList({
|
||||
return null;
|
||||
}
|
||||
|
||||
// System notifications are never user messages, handle them first
|
||||
if (hasInlineSystemNotification(message)) {
|
||||
return (
|
||||
<div
|
||||
key={message.id && `${message.id}-${message.content.length}`}
|
||||
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} assistant`}
|
||||
data-testid="message-container"
|
||||
>
|
||||
<SystemNotificationInline message={message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isUser = isUserMessage(message);
|
||||
|
||||
return (
|
||||
@@ -196,37 +203,25 @@ export default function ProgressiveMessageList({
|
||||
data-testid="message-container"
|
||||
>
|
||||
{isUser ? (
|
||||
<>
|
||||
{hasCompactionMarker && hasCompactionMarker(message) ? (
|
||||
<CompactionMarker message={message} />
|
||||
) : (
|
||||
!hasOnlyToolResponses(message) && (
|
||||
<UserMessage message={message} onMessageUpdate={onMessageUpdate} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
!hasOnlyToolResponses(message) && (
|
||||
<UserMessage message={message} onMessageUpdate={onMessageUpdate} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{hasCompactionMarker && hasCompactionMarker(message) ? (
|
||||
<CompactionMarker message={message} />
|
||||
) : (
|
||||
<GooseMessage
|
||||
sessionId={chat.sessionId}
|
||||
messageHistoryIndex={chat.messageHistoryIndex}
|
||||
message={message}
|
||||
messages={messages}
|
||||
append={append}
|
||||
appendMessage={appendMessage}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
isStreaming={
|
||||
isStreamingMessage &&
|
||||
!isUser &&
|
||||
index === messagesToRender.length - 1 &&
|
||||
message.role === 'assistant'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<GooseMessage
|
||||
sessionId={chat.sessionId}
|
||||
messageHistoryIndex={chat.messageHistoryIndex}
|
||||
message={message}
|
||||
messages={messages}
|
||||
append={append}
|
||||
appendMessage={appendMessage}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
isStreaming={
|
||||
isStreamingMessage &&
|
||||
!isUser &&
|
||||
index === messagesToRender.length - 1 &&
|
||||
message.role === 'assistant'
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -243,7 +238,6 @@ export default function ProgressiveMessageList({
|
||||
toolCallNotifications,
|
||||
isStreamingMessage,
|
||||
onMessageUpdate,
|
||||
hasCompactionMarker,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Message, ConversationCompacted } from '../../api';
|
||||
|
||||
interface CompactionMarkerProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export const CompactionMarker: React.FC<CompactionMarkerProps> = ({ message }) => {
|
||||
const compactionContent = message.content.find(
|
||||
(content): content is ConversationCompacted & { type: 'conversationCompacted' } =>
|
||||
content.type === 'conversationCompacted'
|
||||
);
|
||||
|
||||
const markerText = compactionContent?.msg || 'Conversation compacted';
|
||||
|
||||
return <div className="text-xs text-gray-400 py-2 text-left">{markerText}</div>;
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||
import { manageContextFromBackend } from './index';
|
||||
import { Message } from '../../api';
|
||||
|
||||
// Define the context management interface
|
||||
interface ContextManagerState {
|
||||
isCompacting: boolean;
|
||||
compactionError: string | null;
|
||||
}
|
||||
|
||||
interface ContextManagerActions {
|
||||
handleManualCompaction: (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append?: (message: Message) => void,
|
||||
sessionId?: string
|
||||
) => Promise<void>;
|
||||
hasCompactionMarker: (message: Message) => boolean;
|
||||
}
|
||||
|
||||
// Create the context
|
||||
const ContextManagerContext = createContext<
|
||||
(ContextManagerState & ContextManagerActions) | undefined
|
||||
>(undefined);
|
||||
|
||||
// Create the provider component
|
||||
export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isCompacting, setIsCompacting] = useState<boolean>(false);
|
||||
const [compactionError, setCompactionError] = useState<string | null>(null);
|
||||
|
||||
const performCompaction = useCallback(
|
||||
async (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append: (message: Message) => void,
|
||||
sessionId: string,
|
||||
isManual: boolean = false
|
||||
) => {
|
||||
setIsCompacting(true);
|
||||
setCompactionError(null);
|
||||
|
||||
try {
|
||||
// Get the summary from the backend
|
||||
const summaryResponse = await manageContextFromBackend({
|
||||
messages: messages,
|
||||
manageAction: 'summarize',
|
||||
sessionId: sessionId,
|
||||
});
|
||||
|
||||
setMessages(summaryResponse.messages);
|
||||
|
||||
// Only automatically submit the continuation message for auto-compaction (context limit reached)
|
||||
// Manual compaction should just compact without continuing the conversation
|
||||
if (!isManual) {
|
||||
// Automatically submit the continuation message to continue the conversation
|
||||
// This should be the third message (index 2) which contains the "I ran into a context length exceeded error..." text
|
||||
const continuationMessage = summaryResponse.messages[2];
|
||||
if (continuationMessage) {
|
||||
setTimeout(() => {
|
||||
append(continuationMessage);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
setIsCompacting(false);
|
||||
} catch (err) {
|
||||
// TODO(Douwe): move this to the server
|
||||
console.error('Error during compaction:', err);
|
||||
setCompactionError(err instanceof Error ? err.message : 'Unknown error during compaction');
|
||||
|
||||
// Create an error marker
|
||||
const errorMarker: Message = {
|
||||
id: `compaction-error-${Date.now()}`,
|
||||
role: 'assistant',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
content: [
|
||||
{
|
||||
type: 'conversationCompacted',
|
||||
msg: 'Compaction failed. Please try again or start a new session.',
|
||||
},
|
||||
],
|
||||
metadata: { userVisible: true, agentVisible: true },
|
||||
};
|
||||
|
||||
setMessages([...messages, errorMarker]);
|
||||
setIsCompacting(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleManualCompaction = useCallback(
|
||||
async (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append?: (message: Message) => void,
|
||||
sessionId?: string
|
||||
) => {
|
||||
await performCompaction(messages, setMessages, append || (() => {}), sessionId || '', true);
|
||||
},
|
||||
[performCompaction]
|
||||
);
|
||||
|
||||
const hasCompactionMarker = useCallback((message: Message): boolean => {
|
||||
return message.content.some((content) => content.type === 'conversationCompacted');
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
// State
|
||||
isCompacting,
|
||||
compactionError,
|
||||
|
||||
// Actions
|
||||
handleManualCompaction,
|
||||
hasCompactionMarker,
|
||||
};
|
||||
|
||||
return <ContextManagerContext.Provider value={value}>{children}</ContextManagerContext.Provider>;
|
||||
};
|
||||
|
||||
// Create a hook to use the context
|
||||
export const useContextManager = () => {
|
||||
const context = useContext(ContextManagerContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useContextManager must be used within a ContextManagerProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Message, SystemNotificationContent } from '../../api';
|
||||
|
||||
interface SystemNotificationInlineProps {
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export const SystemNotificationInline: React.FC<SystemNotificationInlineProps> = ({ message }) => {
|
||||
const systemNotification = message.content.find(
|
||||
(content): content is SystemNotificationContent & { type: 'systemNotification' } =>
|
||||
content.type === 'systemNotification' && content.notificationType === 'inlineMessage'
|
||||
);
|
||||
|
||||
if (!systemNotification?.msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="text-xs text-gray-400 py-2 text-left">{systemNotification.msg}</div>;
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { CompactionMarker } from '../CompactionMarker';
|
||||
import { Message } from '../../../api';
|
||||
|
||||
const default_message: Message = {
|
||||
metadata: {
|
||||
agentVisible: false,
|
||||
userVisible: false,
|
||||
},
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
created: 1000,content: []
|
||||
};
|
||||
|
||||
describe('CompactionMarker', () => {
|
||||
it('should render default message when no conversationCompacted content found', () => {
|
||||
const message: Message = {
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Regular message' }],
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
|
||||
expect(screen.getByText('Conversation compacted')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render custom message from conversationCompacted content', () => {
|
||||
const message: Message = {
|
||||
...default_message,
|
||||
content: [
|
||||
{ type: 'text', text: 'Some other content' },
|
||||
{ type: 'conversationCompacted', msg: 'Custom compaction message' },
|
||||
],
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
|
||||
expect(screen.getByText('Custom compaction message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle empty message content array', () => {
|
||||
const message: Message = {
|
||||
...default_message,
|
||||
content: [],
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
|
||||
expect(screen.getByText('Conversation compacted')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle summarizationRequested content with empty msg', () => {
|
||||
const message: Message = {
|
||||
...default_message,
|
||||
content: [{ type: 'conversationCompacted', msg: '' }],
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
|
||||
// Empty string falls back to default due to || operator
|
||||
expect(screen.getByText('Conversation compacted')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle summarizationRequested content with undefined msg', () => {
|
||||
const message: Message = {
|
||||
...default_message,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: [{ type: 'conversationCompacted' } as any],
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
|
||||
// Should render the default message when msg is undefined
|
||||
expect(screen.getByText('Conversation compacted')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,283 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { ContextManagerProvider, useContextManager } from '../ContextManager';
|
||||
import * as contextManagement from '../index';
|
||||
import { Message } from '../../../api';
|
||||
|
||||
const default_message: Message = {
|
||||
metadata: {
|
||||
agentVisible: false,
|
||||
userVisible: false,
|
||||
},
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
created: 1000,
|
||||
content: [],
|
||||
};
|
||||
|
||||
// Mock the context management functions
|
||||
vi.mock('../index', () => ({
|
||||
manageContextFromBackend: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockManageContextFromBackend = vi.mocked(contextManagement.manageContextFromBackend);
|
||||
|
||||
describe('ContextManager', () => {
|
||||
const mockMessages: Message[] = [
|
||||
{
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
},
|
||||
{
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Hi there!' }],
|
||||
},
|
||||
];
|
||||
|
||||
const mockSetMessages = vi.fn();
|
||||
const mockAppend = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const renderContextManager = () => {
|
||||
return renderHook(() => useContextManager(), {
|
||||
wrapper: ({ children }) => <ContextManagerProvider>{children}</ContextManagerProvider>,
|
||||
});
|
||||
};
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have correct initial state', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
expect(result.current.isCompacting).toBe(false);
|
||||
expect(result.current.compactionError).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCompactionMarker', () => {
|
||||
it('should return true for messages with summarizationRequested content', () => {
|
||||
const { result } = renderContextManager();
|
||||
const messageWithMarker: Message = {
|
||||
...default_message,
|
||||
content: [{ type: 'conversationCompacted', msg: 'Compaction marker' }],
|
||||
};
|
||||
|
||||
expect(result.current.hasCompactionMarker(messageWithMarker)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for messages without summarizationRequested content', () => {
|
||||
const { result } = renderContextManager();
|
||||
const regularMessage: Message = {
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
};
|
||||
|
||||
expect(result.current.hasCompactionMarker(regularMessage)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for messages with mixed content including conversationCompacted', () => {
|
||||
const { result } = renderContextManager();
|
||||
const mixedMessage: Message = {
|
||||
...default_message,
|
||||
content: [
|
||||
{ type: 'text', text: 'Some text' },
|
||||
{ type: 'conversationCompacted', msg: 'Compaction marker' },
|
||||
],
|
||||
};
|
||||
|
||||
expect(result.current.hasCompactionMarker(mixedMessage)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleManualCompaction', () => {
|
||||
it('should perform compaction with server-provided messages', async () => {
|
||||
mockManageContextFromBackend.mockResolvedValue({
|
||||
messages: [
|
||||
{
|
||||
...default_message,
|
||||
content: [
|
||||
{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' },
|
||||
],
|
||||
},
|
||||
{
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Manual summary content' }],
|
||||
},
|
||||
{
|
||||
...default_message,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokenCounts: [8, 100, 50],
|
||||
});
|
||||
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleManualCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
'test-session-id'
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockManageContextFromBackend).toHaveBeenCalledWith({
|
||||
messages: mockMessages,
|
||||
manageAction: 'summarize',
|
||||
sessionId: 'test-session-id',
|
||||
});
|
||||
|
||||
// Verify all three messages are set
|
||||
expect(mockSetMessages).toHaveBeenCalledTimes(1);
|
||||
const setMessagesCall = mockSetMessages.mock.calls[0][0];
|
||||
expect(setMessagesCall).toHaveLength(3);
|
||||
expect(setMessagesCall[0]).toMatchObject({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' }],
|
||||
});
|
||||
expect(setMessagesCall[1]).toMatchObject({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Manual summary content' }],
|
||||
});
|
||||
expect(setMessagesCall[2]).toMatchObject({
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Fast-forward timers to check if append would be called
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(150);
|
||||
});
|
||||
|
||||
// Should NOT append the continuation message for manual compaction
|
||||
expect(mockAppend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work without append function', async () => {
|
||||
mockManageContextFromBackend.mockResolvedValue({
|
||||
messages: [
|
||||
{
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Manual summary content' }],
|
||||
},
|
||||
],
|
||||
tokenCounts: [100, 50],
|
||||
});
|
||||
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleManualCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
undefined // No append function
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockManageContextFromBackend).toHaveBeenCalled();
|
||||
// Should not throw error when append is undefined
|
||||
|
||||
// Fast-forward timers to check if append would be called
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(150);
|
||||
});
|
||||
|
||||
// No append function provided, so no calls should be made
|
||||
expect(mockAppend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not auto-continue conversation for manual compaction even with append function', async () => {
|
||||
mockManageContextFromBackend.mockResolvedValue({
|
||||
messages: [
|
||||
{
|
||||
...default_message,
|
||||
content: [
|
||||
{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' },
|
||||
],
|
||||
},
|
||||
{
|
||||
...default_message,
|
||||
content: [{ type: 'text', text: 'Manual summary content' }],
|
||||
},
|
||||
{
|
||||
...default_message,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tokenCounts: [8, 100, 50],
|
||||
});
|
||||
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleManualCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
'test-session-id'
|
||||
);
|
||||
});
|
||||
|
||||
// Verify all three messages are set
|
||||
expect(mockSetMessages).toHaveBeenCalledTimes(1);
|
||||
const setMessagesCall = mockSetMessages.mock.calls[0][0];
|
||||
expect(setMessagesCall).toHaveLength(3);
|
||||
expect(setMessagesCall[0]).toMatchObject({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' }],
|
||||
});
|
||||
expect(setMessagesCall[1]).toMatchObject({
|
||||
role: 'assistant',
|
||||
content: [{ type: 'text', text: 'Manual summary content' }],
|
||||
});
|
||||
expect(setMessagesCall[2]).toMatchObject({
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: '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',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Fast-forward timers to check if append would be called
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(150);
|
||||
});
|
||||
|
||||
// Should NOT auto-continue for manual compaction, even with append function
|
||||
expect(mockAppend).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Context Provider Error', () => {
|
||||
it('should throw error when useContextManager is used outside provider', () => {
|
||||
expect(() => {
|
||||
renderHook(() => useContextManager());
|
||||
}).toThrow('useContextManager must be used within a ContextManagerProvider');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ContextManageRequest, ContextManageResponse, manageContext, Message } from '../../api';
|
||||
|
||||
export async function manageContextFromBackend({
|
||||
messages,
|
||||
manageAction,
|
||||
sessionId,
|
||||
}: {
|
||||
messages: Message[];
|
||||
manageAction: 'truncation' | 'summarize';
|
||||
sessionId: string;
|
||||
}): Promise<ContextManageResponse> {
|
||||
const contextManagementRequest = { manageAction, messages, sessionId };
|
||||
|
||||
// Cast to the API-expected type
|
||||
const result = await manageContext({
|
||||
body: contextManagementRequest as unknown as ContextManageRequest,
|
||||
});
|
||||
|
||||
// Check for errors in the result
|
||||
if (result.error) {
|
||||
throw new Error(`Context management failed: ${result.error}`);
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
throw new Error('Context management returned no data');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
import { SessionInsights } from './sessions/SessionsInsights';
|
||||
import ChatInput from './ChatInput';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import { ContextManagerProvider } from './context_management/ContextManager';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
import { startAgent } from '../api';
|
||||
@@ -65,35 +64,32 @@ export default function Hub({
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextManagerProvider>
|
||||
<div className="flex flex-col h-full bg-background-muted">
|
||||
<div className="flex-1 flex flex-col mb-0.5">
|
||||
<SessionInsights />
|
||||
</div>
|
||||
|
||||
<ChatInput
|
||||
sessionId={null}
|
||||
handleSubmit={handleSubmit}
|
||||
autoSubmit={false}
|
||||
chatState={ChatState.Idle}
|
||||
onStop={() => {}}
|
||||
commandHistory={[]}
|
||||
initialValue=""
|
||||
setView={setView}
|
||||
numTokens={0}
|
||||
inputTokens={0}
|
||||
outputTokens={0}
|
||||
droppedFiles={[]}
|
||||
onFilesProcessed={() => {}}
|
||||
messages={[]}
|
||||
setMessages={() => {}}
|
||||
disableAnimation={false}
|
||||
sessionCosts={undefined}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
isExtensionsLoading={isExtensionsLoading}
|
||||
toolCount={0}
|
||||
/>
|
||||
<div className="flex flex-col h-full bg-background-muted">
|
||||
<div className="flex-1 flex flex-col mb-0.5">
|
||||
<SessionInsights />
|
||||
</div>
|
||||
</ContextManagerProvider>
|
||||
|
||||
<ChatInput
|
||||
sessionId={null}
|
||||
handleSubmit={handleSubmit}
|
||||
autoSubmit={false}
|
||||
chatState={ChatState.Idle}
|
||||
onStop={() => {}}
|
||||
commandHistory={[]}
|
||||
initialValue=""
|
||||
setView={setView}
|
||||
numTokens={0}
|
||||
inputTokens={0}
|
||||
outputTokens={0}
|
||||
droppedFiles={[]}
|
||||
onFilesProcessed={() => {}}
|
||||
messages={[]}
|
||||
disableAnimation={false}
|
||||
sessionCosts={undefined}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
isExtensionsLoading={isExtensionsLoading}
|
||||
toolCount={0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
} from '../ui/dialog';
|
||||
import ProgressiveMessageList from '../ProgressiveMessageList';
|
||||
import { SearchView } from '../conversation/SearchView';
|
||||
import { ContextManagerProvider } from '../context_management/ContextManager';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
|
||||
import { Message, Session } from '../../api';
|
||||
@@ -101,29 +100,27 @@ const SessionMessages: React.FC<{
|
||||
</Button>
|
||||
</div>
|
||||
) : filteredMessages?.length > 0 ? (
|
||||
<ContextManagerProvider>
|
||||
<div className="max-w-4xl mx-auto w-full">
|
||||
<SearchView placeholder="Search history...">
|
||||
<ProgressiveMessageList
|
||||
messages={filteredMessages}
|
||||
chat={{
|
||||
sessionId: 'session-preview',
|
||||
messageHistoryIndex: filteredMessages.length,
|
||||
}}
|
||||
toolCallNotifications={new Map()}
|
||||
append={() => {}} // Read-only for session history
|
||||
appendMessage={(newMessage) => {
|
||||
// Read-only - do nothing
|
||||
console.log('appendMessage called in read-only session history:', newMessage);
|
||||
}}
|
||||
isUserMessage={isUserMessage} // Use the same function as BaseChat
|
||||
batchSize={15} // Same as BaseChat default
|
||||
batchDelay={30} // Same as BaseChat default
|
||||
showLoadingThreshold={30} // Same as BaseChat default
|
||||
/>
|
||||
</SearchView>
|
||||
</div>
|
||||
</ContextManagerProvider>
|
||||
<div className="max-w-4xl mx-auto w-full">
|
||||
<SearchView placeholder="Search history...">
|
||||
<ProgressiveMessageList
|
||||
messages={filteredMessages}
|
||||
chat={{
|
||||
sessionId: 'session-preview',
|
||||
messageHistoryIndex: filteredMessages.length,
|
||||
}}
|
||||
toolCallNotifications={new Map()}
|
||||
append={() => {}} // Read-only for session history
|
||||
appendMessage={(newMessage) => {
|
||||
// Read-only - do nothing
|
||||
console.log('appendMessage called in read-only session history:', newMessage);
|
||||
}}
|
||||
isUserMessage={isUserMessage} // Use the same function as BaseChat
|
||||
batchSize={15} // Same as BaseChat default
|
||||
batchDelay={30} // Same as BaseChat default
|
||||
showLoadingThreshold={30} // Same as BaseChat default
|
||||
/>
|
||||
</SearchView>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
|
||||
<MessageSquareText className="w-12 h-12 mb-4" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { createUserMessage, hasCompletedToolCalls } from '../types/message';
|
||||
import { createUserMessage, getThinkingMessage, hasCompletedToolCalls } from '../types/message';
|
||||
import { Conversation, Message, Role } from '../api';
|
||||
|
||||
import { getSession, Session } from '../api';
|
||||
@@ -307,6 +307,10 @@ export function useMessageStream({
|
||||
mutateChatState(ChatState.WaitingForUserInput);
|
||||
}
|
||||
|
||||
if (getThinkingMessage(newMessage)) {
|
||||
mutateChatState(ChatState.Thinking);
|
||||
}
|
||||
|
||||
mutate(currentMessages, false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -70,3 +70,17 @@ export function hasCompletedToolCalls(message: Message): boolean {
|
||||
const toolRequests = getToolRequests(message);
|
||||
return toolRequests.length > 0;
|
||||
}
|
||||
|
||||
export function getThinkingMessage(message: Message | undefined): string | undefined {
|
||||
if (!message || message.role !== 'assistant') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const content of message.content) {
|
||||
if (content.type === 'systemNotification' && content.notificationType === 'thinkingMessage') {
|
||||
return content.msg;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user