Add Message Metadata for Visibility Control (#4538)
This commit is contained in:
@@ -360,6 +360,7 @@ export type Message = {
|
||||
content: Array<MessageContent>;
|
||||
created?: number;
|
||||
id?: string | null;
|
||||
metadata?: MessageMetadata;
|
||||
role: Role;
|
||||
};
|
||||
|
||||
@@ -388,6 +389,20 @@ export type MessageContent = (TextContent & {
|
||||
type: 'summarizationRequested';
|
||||
});
|
||||
|
||||
/**
|
||||
* Metadata for message visibility
|
||||
*/
|
||||
export type MessageMetadata = {
|
||||
/**
|
||||
* Whether the message should be included in the agent's context window
|
||||
*/
|
||||
agentVisible?: boolean;
|
||||
/**
|
||||
* Whether the message should be visible to the user in the UI
|
||||
*/
|
||||
userVisible?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Information about a model's capabilities
|
||||
*/
|
||||
|
||||
@@ -165,7 +165,6 @@ function BaseChatContent({
|
||||
const {
|
||||
messages,
|
||||
filteredMessages,
|
||||
setAncestorMessages,
|
||||
append,
|
||||
chatState,
|
||||
error,
|
||||
@@ -238,14 +237,13 @@ function BaseChatContent({
|
||||
console.log('Switching from recipe:', previousTitle, 'to:', newTitle);
|
||||
setHasStartedUsingRecipe(false);
|
||||
setMessages([]);
|
||||
setAncestorMessages([]);
|
||||
} else if (isInitialRecipeLoad) {
|
||||
setHasStartedUsingRecipe(false);
|
||||
} else if (hasExistingConversation) {
|
||||
setHasStartedUsingRecipe(true);
|
||||
}
|
||||
}
|
||||
}, [recipeConfig?.title, currentRecipeTitle, messages.length, setMessages, setAncestorMessages]);
|
||||
}, [recipeConfig?.title, currentRecipeTitle, messages.length, setMessages]);
|
||||
|
||||
// Handle recipe auto-execution
|
||||
useEffect(() => {
|
||||
@@ -448,12 +446,7 @@ function BaseChatContent({
|
||||
onClick={async () => {
|
||||
clearError();
|
||||
|
||||
await handleManualCompaction(
|
||||
messages,
|
||||
setMessages,
|
||||
append,
|
||||
setAncestorMessages
|
||||
);
|
||||
await handleManualCompaction(messages, setMessages, append);
|
||||
}}
|
||||
>
|
||||
Summarize Conversation
|
||||
@@ -533,7 +526,6 @@ function BaseChatContent({
|
||||
initialPrompt={initialPrompt}
|
||||
toolCount={toolCount || 0}
|
||||
autoSubmit={autoSubmit}
|
||||
setAncestorMessages={setAncestorMessages}
|
||||
append={append}
|
||||
{...customChatInputProps}
|
||||
/>
|
||||
|
||||
@@ -86,7 +86,6 @@ interface ChatInputProps {
|
||||
initialPrompt?: string;
|
||||
toolCount: number;
|
||||
autoSubmit: boolean;
|
||||
setAncestorMessages?: (messages: Message[]) => void;
|
||||
append?: (message: Message) => void;
|
||||
isExtensionsLoading?: boolean;
|
||||
}
|
||||
@@ -115,7 +114,6 @@ export default function ChatInput({
|
||||
toolCount,
|
||||
autoSubmit = false,
|
||||
append,
|
||||
setAncestorMessages,
|
||||
isExtensionsLoading = false,
|
||||
}: ChatInputProps) {
|
||||
const [_value, setValue] = useState(initialValue);
|
||||
@@ -570,7 +568,7 @@ export default function ChatInput({
|
||||
// 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, setAncestorMessages);
|
||||
handleManualCompaction(messages, setMessages, append);
|
||||
},
|
||||
compactIcon: <ScrollText size={12} />,
|
||||
autoCompactThreshold: autoCompactThreshold,
|
||||
|
||||
@@ -12,14 +12,12 @@ interface ContextManagerActions {
|
||||
handleAutoCompaction: (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append: (message: Message) => void,
|
||||
setAncestorMessages?: (messages: Message[]) => void
|
||||
append: (message: Message) => void
|
||||
) => Promise<void>;
|
||||
handleManualCompaction: (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append?: (message: Message) => void,
|
||||
setAncestorMessages?: (messages: Message[]) => void
|
||||
append?: (message: Message) => void
|
||||
) => Promise<void>;
|
||||
hasCompactionMarker: (message: Message) => boolean;
|
||||
}
|
||||
@@ -39,8 +37,7 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append: (message: Message) => void,
|
||||
isManual: boolean = false,
|
||||
setAncestorMessages?: (messages: Message[]) => void
|
||||
isManual: boolean = false
|
||||
) => {
|
||||
setIsCompacting(true);
|
||||
setCompactionError(null);
|
||||
@@ -53,29 +50,10 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
||||
});
|
||||
|
||||
// Convert API messages to frontend messages
|
||||
const convertedMessages = summaryResponse.messages.map((apiMessage) => {
|
||||
const isCompactionMarker = apiMessage.content.some(
|
||||
(content) => content.type === 'summarizationRequested'
|
||||
);
|
||||
|
||||
if (isCompactionMarker) {
|
||||
// show to user but not model
|
||||
return convertApiMessageToFrontendMessage(apiMessage, true, false);
|
||||
}
|
||||
|
||||
// show to model but not user
|
||||
return convertApiMessageToFrontendMessage(apiMessage, false, true);
|
||||
});
|
||||
|
||||
// Store the original messages as ancestor messages so they can still be scrolled to
|
||||
if (setAncestorMessages) {
|
||||
const ancestorMessages = messages.map((msg) => ({
|
||||
...msg,
|
||||
display: msg.display === false ? false : true,
|
||||
sendToLLM: false,
|
||||
}));
|
||||
setAncestorMessages(ancestorMessages);
|
||||
}
|
||||
// The server now handles all visibility - we just display what we receive
|
||||
const convertedMessages = summaryResponse.messages.map((apiMessage) =>
|
||||
convertApiMessageToFrontendMessage(apiMessage)
|
||||
);
|
||||
|
||||
// Replace messages with the server-provided messages
|
||||
setMessages(convertedMessages);
|
||||
@@ -109,8 +87,6 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
||||
msg: 'Compaction failed. Please try again or start a new session.',
|
||||
},
|
||||
],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
setMessages([...messages, errorMarker]);
|
||||
@@ -124,10 +100,9 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
||||
async (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append: (message: Message) => void,
|
||||
setAncestorMessages?: (messages: Message[]) => void
|
||||
append: (message: Message) => void
|
||||
) => {
|
||||
await performCompaction(messages, setMessages, append, false, setAncestorMessages);
|
||||
await performCompaction(messages, setMessages, append, false);
|
||||
},
|
||||
[performCompaction]
|
||||
);
|
||||
@@ -136,16 +111,9 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
|
||||
async (
|
||||
messages: Message[],
|
||||
setMessages: (messages: Message[]) => void,
|
||||
append?: (message: Message) => void,
|
||||
setAncestorMessages?: (messages: Message[]) => void
|
||||
append?: (message: Message) => void
|
||||
) => {
|
||||
await performCompaction(
|
||||
messages,
|
||||
setMessages,
|
||||
append || (() => {}),
|
||||
true,
|
||||
setAncestorMessages
|
||||
);
|
||||
await performCompaction(messages, setMessages, append || (() => {}), true);
|
||||
},
|
||||
[performCompaction]
|
||||
);
|
||||
|
||||
@@ -10,8 +10,6 @@ describe('CompactionMarker', () => {
|
||||
role: 'assistant',
|
||||
created: 1000,
|
||||
content: [{ type: 'text', text: 'Regular message' }],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
@@ -28,8 +26,6 @@ describe('CompactionMarker', () => {
|
||||
{ type: 'text', text: 'Some other content' },
|
||||
{ type: 'summarizationRequested', msg: 'Custom compaction message' },
|
||||
],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
@@ -43,8 +39,6 @@ describe('CompactionMarker', () => {
|
||||
role: 'assistant',
|
||||
created: 1000,
|
||||
content: [],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
@@ -58,8 +52,6 @@ describe('CompactionMarker', () => {
|
||||
role: 'assistant',
|
||||
created: 1000,
|
||||
content: [{ type: 'summarizationRequested', msg: '' }],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
@@ -75,8 +67,6 @@ describe('CompactionMarker', () => {
|
||||
created: 1000,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: [{ type: 'summarizationRequested' } as any],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
render(<CompactionMarker message={message} />);
|
||||
|
||||
@@ -23,16 +23,12 @@ describe('ContextManager', () => {
|
||||
role: 'user',
|
||||
created: 1000,
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
display: true,
|
||||
sendToLLM: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
role: 'assistant',
|
||||
created: 2000,
|
||||
content: [{ type: 'text', text: 'Hi there!' }],
|
||||
display: true,
|
||||
sendToLLM: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -41,13 +37,10 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 3000,
|
||||
content: [{ type: 'text', text: 'This is a summary of the conversation.' }],
|
||||
display: false,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
const mockSetMessages = vi.fn();
|
||||
const mockAppend = vi.fn();
|
||||
const mockSetAncestorMessages = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -84,8 +77,6 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 1000,
|
||||
content: [{ type: 'summarizationRequested', msg: 'Compaction marker' }],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
expect(result.current.hasCompactionMarker(messageWithMarker)).toBe(true);
|
||||
@@ -98,8 +89,6 @@ describe('ContextManager', () => {
|
||||
role: 'user',
|
||||
created: 1000,
|
||||
content: [{ type: 'text', text: 'Hello' }],
|
||||
display: true,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
expect(result.current.hasCompactionMarker(regularMessage)).toBe(false);
|
||||
@@ -115,8 +104,6 @@ describe('ContextManager', () => {
|
||||
{ type: 'text', text: 'Some text' },
|
||||
{ type: 'summarizationRequested', msg: 'Compaction marker' },
|
||||
],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
expect(result.current.hasCompactionMarker(mixedMessage)).toBe(true);
|
||||
@@ -156,8 +143,6 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 3000,
|
||||
content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
const mockContinuationMessage: Message = {
|
||||
@@ -170,25 +155,18 @@ describe('ContextManager', () => {
|
||||
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',
|
||||
},
|
||||
],
|
||||
display: false,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
// Mock the conversion function to return different messages based on call order
|
||||
mockConvertApiMessageToFrontendMessage
|
||||
.mockReturnValueOnce(mockCompactionMarker) // First call - compaction marker (display: true, sendToLLM: false)
|
||||
.mockReturnValueOnce(mockSummaryMessage) // Second call - summary (display: false, sendToLLM: true)
|
||||
.mockReturnValueOnce(mockContinuationMessage); // Third call - continuation (display: false, sendToLLM: true)
|
||||
.mockReturnValueOnce(mockCompactionMarker) // First call - compaction marker
|
||||
.mockReturnValueOnce(mockSummaryMessage) // Second call - summary
|
||||
.mockReturnValueOnce(mockContinuationMessage); // Third call - continuation
|
||||
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAutoCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
expect(mockManageContextFromBackend).toHaveBeenCalledWith({
|
||||
@@ -203,17 +181,13 @@ describe('ContextManager', () => {
|
||||
content: [
|
||||
{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' },
|
||||
],
|
||||
}),
|
||||
true, // display: true
|
||||
false // sendToLLM: false
|
||||
})
|
||||
);
|
||||
expect(mockConvertApiMessageToFrontendMessage).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
content: [{ type: 'text', text: 'Summary content' }],
|
||||
}),
|
||||
false, // display: false
|
||||
true // sendToLLM: true
|
||||
})
|
||||
);
|
||||
expect(mockConvertApiMessageToFrontendMessage).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
@@ -224,24 +198,7 @@ describe('ContextManager', () => {
|
||||
text: expect.stringContaining('The previous message contains a summary'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
false, // display: false
|
||||
true // sendToLLM: true
|
||||
);
|
||||
|
||||
expect(mockSetAncestorMessages).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: '1',
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: '2',
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
}),
|
||||
])
|
||||
})
|
||||
);
|
||||
|
||||
// Expect setMessages to be called with all 3 converted messages
|
||||
@@ -268,12 +225,7 @@ describe('ContextManager', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAutoCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
expect(result.current.compactionError).toBe('Backend error');
|
||||
@@ -304,12 +256,7 @@ describe('ContextManager', () => {
|
||||
|
||||
// Start compaction
|
||||
act(() => {
|
||||
result.current.handleAutoCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
// Should be compacting
|
||||
@@ -336,8 +283,8 @@ describe('ContextManager', () => {
|
||||
// Should no longer be compacting
|
||||
expect(result.current.isCompacting).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves display: false for ancestor messages', async () => {
|
||||
// Backend returns no new messages; we're validating ancestor behavior only
|
||||
mockManageContextFromBackend.mockResolvedValue({ messages: [], tokenCounts: [] });
|
||||
|
||||
const hiddenMessage: Message = {
|
||||
@@ -345,8 +292,6 @@ describe('ContextManager', () => {
|
||||
role: 'user',
|
||||
created: 1500,
|
||||
content: [{ type: 'text', text: 'Secret' }],
|
||||
display: false,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
const visibleMessage: Message = {
|
||||
@@ -354,8 +299,6 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 1600,
|
||||
content: [{ type: 'text', text: 'Public' }],
|
||||
display: true,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
const messages: Message[] = [hiddenMessage, visibleMessage];
|
||||
@@ -363,21 +306,9 @@ describe('ContextManager', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAutoCompaction(
|
||||
messages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleAutoCompaction(messages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
expect(mockSetAncestorMessages).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'hidden-1', display: false, sendToLLM: false }),
|
||||
expect.objectContaining({ id: 'visible-1', display: true, sendToLLM: false }),
|
||||
])
|
||||
);
|
||||
|
||||
// No server messages -> setMessages called with empty list
|
||||
expect(mockSetMessages).toHaveBeenCalledWith([]);
|
||||
expect(mockAppend).not.toHaveBeenCalled();
|
||||
@@ -416,8 +347,6 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 3000,
|
||||
content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
const mockContinuationMessage: Message = {
|
||||
@@ -430,8 +359,6 @@ describe('ContextManager', () => {
|
||||
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',
|
||||
},
|
||||
],
|
||||
display: false,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
mockConvertApiMessageToFrontendMessage
|
||||
@@ -442,12 +369,7 @@ describe('ContextManager', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleManualCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
expect(mockManageContextFromBackend).toHaveBeenCalledWith({
|
||||
@@ -490,8 +412,7 @@ describe('ContextManager', () => {
|
||||
await result.current.handleManualCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
undefined, // No append function
|
||||
mockSetAncestorMessages
|
||||
undefined // No append function
|
||||
);
|
||||
});
|
||||
|
||||
@@ -538,8 +459,6 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 3000,
|
||||
content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }],
|
||||
display: true,
|
||||
sendToLLM: false,
|
||||
};
|
||||
|
||||
const mockContinuationMessage: Message = {
|
||||
@@ -552,8 +471,6 @@ describe('ContextManager', () => {
|
||||
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',
|
||||
},
|
||||
],
|
||||
display: false,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
mockConvertApiMessageToFrontendMessage
|
||||
@@ -564,12 +481,7 @@ describe('ContextManager', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleManualCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend, // Provide append function
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
// Verify all three messages are set
|
||||
@@ -596,12 +508,7 @@ describe('ContextManager', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAutoCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
expect(result.current.compactionError).toBe('Unknown error during compaction');
|
||||
@@ -625,8 +532,6 @@ describe('ContextManager', () => {
|
||||
role: 'assistant',
|
||||
created: 3000,
|
||||
content: [{ type: 'toolResponse', id: 'test', toolResult: { status: 'success' } }],
|
||||
display: false,
|
||||
sendToLLM: true,
|
||||
};
|
||||
|
||||
mockConvertApiMessageToFrontendMessage.mockReturnValue(mockMessageWithoutText);
|
||||
@@ -634,12 +539,7 @@ describe('ContextManager', () => {
|
||||
const { result } = renderContextManager();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAutoCompaction(
|
||||
mockMessages,
|
||||
mockSetMessages,
|
||||
mockAppend,
|
||||
mockSetAncestorMessages
|
||||
);
|
||||
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend);
|
||||
});
|
||||
|
||||
// Should complete without error even if content is not text
|
||||
|
||||
@@ -50,15 +50,8 @@ export async function manageContextFromBackend({
|
||||
}
|
||||
|
||||
// Function to convert API Message to frontend Message
|
||||
// TODO(Douwe): get rid of this and use the API Message format everywhere
|
||||
export function convertApiMessageToFrontendMessage(
|
||||
apiMessage: ApiMessage,
|
||||
display?: boolean,
|
||||
sendToLLM?: boolean
|
||||
): FrontendMessage {
|
||||
export function convertApiMessageToFrontendMessage(apiMessage: ApiMessage): FrontendMessage {
|
||||
return {
|
||||
display: display ?? true,
|
||||
sendToLLM: sendToLLM ?? true,
|
||||
id: generateId(),
|
||||
role: apiMessage.role as Role,
|
||||
created: apiMessage.created ?? Math.floor(Date.now() / 1000),
|
||||
@@ -150,7 +143,5 @@ export function createSummarizationRequestMessage(
|
||||
msg: requestMessage,
|
||||
},
|
||||
],
|
||||
sendToLLM: false,
|
||||
display: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const isUserMessage = (message: Message): boolean => {
|
||||
};
|
||||
|
||||
const filterMessagesForDisplay = (messages: Message[]): Message[] => {
|
||||
return messages.filter((message) => message.display ?? true);
|
||||
return messages;
|
||||
};
|
||||
|
||||
interface SessionHistoryViewProps {
|
||||
|
||||
@@ -79,7 +79,7 @@ export function useAgent(): UseAgentReturn {
|
||||
title: sessionMetadata.recipe?.title || sessionMetadata.description,
|
||||
messageHistoryIndex: 0,
|
||||
messages: agentSessionInfo.messages.map((message: ApiMessage) =>
|
||||
convertApiMessageToFrontendMessage(message, true, true)
|
||||
convertApiMessageToFrontendMessage(message)
|
||||
),
|
||||
recipeConfig: sessionMetadata.recipe,
|
||||
};
|
||||
@@ -159,7 +159,7 @@ export function useAgent(): UseAgentReturn {
|
||||
title: sessionMetadata.recipe?.title || sessionMetadata.description,
|
||||
messageHistoryIndex: 0,
|
||||
messages: agentSessionInfo.messages.map((message: ApiMessage) =>
|
||||
convertApiMessageToFrontendMessage(message, true, true)
|
||||
convertApiMessageToFrontendMessage(message)
|
||||
),
|
||||
recipeConfig: sessionMetadata.recipe,
|
||||
};
|
||||
|
||||
@@ -40,7 +40,6 @@ export const useChatEngine = ({
|
||||
}: UseChatEngineProps) => {
|
||||
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
|
||||
const [sessionTokenCount, setSessionTokenCount] = useState<number>(0);
|
||||
const [ancestorMessages, setAncestorMessages] = useState<Message[]>([]);
|
||||
const [sessionInputTokens, setSessionInputTokens] = useState<number>(0);
|
||||
const [sessionOutputTokens, setSessionOutputTokens] = useState<number>(0);
|
||||
const [localInputTokens, setLocalInputTokens] = useState<number>(0);
|
||||
@@ -353,8 +352,6 @@ export const useChatEngine = ({
|
||||
// Create tool responses for all interrupted tool requests
|
||||
|
||||
let responseMessage: Message = {
|
||||
display: true,
|
||||
sendToLLM: true,
|
||||
role: 'user',
|
||||
created: Date.now(),
|
||||
content: [],
|
||||
@@ -381,11 +378,12 @@ export const useChatEngine = ({
|
||||
}
|
||||
}, [stop, messages, _setInput, setMessages, stopPowerSaveBlocker]);
|
||||
|
||||
// Since server now handles all filtering, we just use messages directly
|
||||
const filteredMessages = useMemo(() => {
|
||||
return [...ancestorMessages, ...messages].filter((message) => message.display ?? true);
|
||||
}, [ancestorMessages, messages]);
|
||||
return messages;
|
||||
}, [messages]);
|
||||
|
||||
// Generate command history from filtered messages
|
||||
// Generate command history from messages
|
||||
const commandHistory = useMemo(() => {
|
||||
return filteredMessages
|
||||
.reduce<string[]>((history, message) => {
|
||||
@@ -445,8 +443,6 @@ export const useChatEngine = ({
|
||||
// Core message data
|
||||
messages,
|
||||
filteredMessages,
|
||||
ancestorMessages,
|
||||
setAncestorMessages,
|
||||
|
||||
// Message stream controls
|
||||
append,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { createUserMessage, hasCompletedToolCalls, Message } from '../types/message';
|
||||
import { createUserMessage, hasCompletedToolCalls, Message, Role } from '../types/message';
|
||||
import { getSessionHistory, SessionMetadata } from '../api';
|
||||
import { ChatState } from '../types/chatState';
|
||||
|
||||
@@ -273,19 +273,12 @@ export function useMessageStream({
|
||||
mutateChatState(ChatState.Streaming);
|
||||
|
||||
// Create a new message object with the properties preserved or defaulted
|
||||
const newMessage = {
|
||||
const newMessage: Message = {
|
||||
...parsedEvent.message,
|
||||
// Ensure the message has an ID - if not provided, generate one
|
||||
id: parsedEvent.message.id || generateMessageId(),
|
||||
// Only set to true if it's undefined (preserve false values)
|
||||
display:
|
||||
parsedEvent.message.display === undefined
|
||||
? true
|
||||
: parsedEvent.message.display,
|
||||
sendToLLM:
|
||||
parsedEvent.message.sendToLLM === undefined
|
||||
? true
|
||||
: parsedEvent.message.sendToLLM,
|
||||
id: parsedEvent.message.id || undefined,
|
||||
role: parsedEvent.message.role as Role,
|
||||
created: parsedEvent.message.created || Date.now(),
|
||||
content: parsedEvent.message.content || [],
|
||||
};
|
||||
|
||||
// Update messages with the new message
|
||||
@@ -408,9 +401,6 @@ export function useMessageStream({
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
// Filter out messages where sendToLLM is explicitly false
|
||||
const filteredMessages = requestMessages.filter((message) => message.sendToLLM !== false);
|
||||
|
||||
// Send request to the server
|
||||
const response = await fetch(api, {
|
||||
method: 'POST',
|
||||
@@ -420,7 +410,7 @@ export function useMessageStream({
|
||||
...extraMetadataRef.current.headers,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages: filteredMessages,
|
||||
messages: requestMessages,
|
||||
...extraMetadataRef.current.body,
|
||||
}),
|
||||
signal: abortController.signal,
|
||||
|
||||
@@ -98,14 +98,19 @@ export async function fetchSessionDetails(sessionId: string): Promise<SessionDet
|
||||
path: { session_id: sessionId },
|
||||
});
|
||||
|
||||
// Convert the SessionHistoryResponse to a SessionDetails object
|
||||
return {
|
||||
sessionId: response.data.sessionId,
|
||||
metadata: ensureWorkingDir(response.data.metadata),
|
||||
messages: response.data.messages.map((message: ApiMessage) =>
|
||||
convertApiMessageToFrontendMessage(message, true, true)
|
||||
), // slight diffs between backend and frontend Message obj
|
||||
};
|
||||
try {
|
||||
// Convert the SessionHistoryResponse to a SessionDetails object
|
||||
return {
|
||||
sessionId: response.data.sessionId,
|
||||
metadata: ensureWorkingDir(response.data.metadata),
|
||||
messages: response.data.messages.map((message: ApiMessage) =>
|
||||
convertApiMessageToFrontendMessage(message)
|
||||
), // slight diffs between backend and frontend Message obj
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching session details for ${sessionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,8 +108,6 @@ export interface Message {
|
||||
role: Role;
|
||||
created: number;
|
||||
content: MessageContent[];
|
||||
display?: boolean;
|
||||
sendToLLM?: boolean;
|
||||
}
|
||||
|
||||
// Helper functions to create messages
|
||||
|
||||
Reference in New Issue
Block a user