feat: add manual/auto conversation compaction and preserve full export history
This commit is contained in:
@@ -313,6 +313,28 @@
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.message-divider-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.message-divider-banner-line {
|
||||
flex: 1 1 auto;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(102, 102, 102, 0.2) 0%, rgba(112, 112, 112, 0.55) 50%, rgba(102, 102, 102, 0.2) 100%);
|
||||
}
|
||||
|
||||
.message-divider-banner-label {
|
||||
flex: 0 0 auto;
|
||||
color: #8c8c8c;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Abort link styling */
|
||||
.abort-link {
|
||||
background: none !important;
|
||||
@@ -326,7 +348,7 @@
|
||||
}
|
||||
|
||||
.abort-link:hover {
|
||||
color: #9b88ff !important;
|
||||
color: #5a9fd4 !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import React from 'react';
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import ChatBox from './ChatBox';
|
||||
import { I18nContext } from '../i18n/I18nProvider';
|
||||
import type { ResolvedLocaleCode } from '../i18n/types';
|
||||
import { translate } from '../i18n/translate';
|
||||
|
||||
const {
|
||||
agentCoreMock,
|
||||
processUserMessageMock,
|
||||
processStreamMock,
|
||||
} = vi.hoisted(() => ({
|
||||
agentCoreMock: {
|
||||
setLLMProvider: vi.fn(),
|
||||
getLLMProvider: vi.fn(() => ({ getPreferredSystemPromptPath: vi.fn() })),
|
||||
abortCurrentRequest: vi.fn(),
|
||||
getAgentState: vi.fn(() => ({ getMessages: vi.fn(() => []) })),
|
||||
compactConversation: vi.fn(async () => ({ changed: true, compactedConversation: 'summary' })),
|
||||
shouldCompactBeforeNextTurn: vi.fn(async () => false),
|
||||
},
|
||||
processUserMessageMock: vi.fn(),
|
||||
processStreamMock: vi.fn(async () => ''),
|
||||
}));
|
||||
|
||||
vi.mock('./chat', () => ({
|
||||
UserMessage: ({ content }: { content: string }) => <div>{content}</div>,
|
||||
AssistantMessage: ({ content }: { content: string }) => <div>{content}</div>,
|
||||
@@ -13,12 +30,7 @@ vi.mock('./chat', () => ({
|
||||
|
||||
vi.mock('../agent/core/AgentCore', () => ({
|
||||
AgentCore: {
|
||||
instance: () => ({
|
||||
setLLMProvider: vi.fn(),
|
||||
getLLMProvider: vi.fn(),
|
||||
abortCurrentRequest: vi.fn(),
|
||||
getAgentState: vi.fn(() => ({ getMessages: vi.fn(() => []) })),
|
||||
}),
|
||||
instance: () => agentCoreMock,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -66,18 +78,29 @@ vi.mock('../util/chatUtil', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../util/messageFilter/UserMessageFilter', () => ({
|
||||
processUserMessage: vi.fn(),
|
||||
processUserMessage: processUserMessageMock,
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useStreamProcessor', () => ({
|
||||
useStreamProcessor: () => ({
|
||||
abortController: null,
|
||||
processStream: vi.fn(),
|
||||
processStream: processStreamMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../utils/chatMessageUtils', () => ({
|
||||
createMessage: vi.fn(),
|
||||
createMessage: vi.fn((role: 'user' | 'assistant', content: string) => ({
|
||||
id: `${role}-${content}`,
|
||||
role,
|
||||
content,
|
||||
})),
|
||||
createStreamingMessage: vi.fn(() => ({
|
||||
id: 'streaming-message',
|
||||
role: 'assistant',
|
||||
content: '<span class="processing-wave">Thinking...</span> click here to abort.',
|
||||
isStreaming: true,
|
||||
tokenCount: 0,
|
||||
})),
|
||||
addWelcomeMessage: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
@@ -134,6 +157,13 @@ describe('ChatBox', () => {
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
processUserMessageMock.mockReset();
|
||||
processStreamMock.mockClear();
|
||||
agentCoreMock.compactConversation.mockClear();
|
||||
agentCoreMock.shouldCompactBeforeNextTurn.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it('renders the English assistant title under en_us', () => {
|
||||
renderWithLocale('en_us');
|
||||
|
||||
@@ -151,4 +181,28 @@ describe('ChatBox', () => {
|
||||
|
||||
expect(screen.getByRole('heading', { level: 3, name: 'Assistant musical K.G.Studio' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows compacting status and completion for /compact', async () => {
|
||||
processUserMessageMock.mockResolvedValue({
|
||||
displayUserMessage: false,
|
||||
sendToLLM: false,
|
||||
finalMessageForLLM: null,
|
||||
pseudoAssistantResponse: null,
|
||||
metadata: {
|
||||
command: 'compact',
|
||||
focus: 'keep the latest work',
|
||||
},
|
||||
});
|
||||
|
||||
renderWithLocale('en_us');
|
||||
|
||||
const input = screen.getByPlaceholderText('Press Enter to send message, Shift + Enter for new line');
|
||||
fireEvent.change(input, { target: { value: '/compact keep the latest work' } });
|
||||
fireEvent.keyDown(input, { key: 'Enter', shiftKey: false });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(agentCoreMock.compactConversation).toHaveBeenCalled();
|
||||
expect(screen.getByText('Conversation Compacted')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SystemPrompts } from '../agent/core/SystemPrompts';
|
||||
import { clearChatHistoryAndUI, registerClearChatUICallback } from '../util/chatUtil';
|
||||
import { processUserMessage } from '../util/messageFilter/UserMessageFilter';
|
||||
import { useStreamProcessor } from '../hooks/useStreamProcessor';
|
||||
import { createMessage, addWelcomeMessage } from '../utils/chatMessageUtils';
|
||||
import { createMessage, createStreamingMessage, addWelcomeMessage } from '../utils/chatMessageUtils';
|
||||
import { formatLocalDateTime } from '../util/timeUtil';
|
||||
import { downloadBlob, buildTimestampSuffix } from '../util/miscUtil';
|
||||
import { LocalLLMModelManager, type LocalLLMModelState } from '../util/localLLMModelManager';
|
||||
@@ -70,6 +70,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isCompacting, setIsCompacting] = useState(false);
|
||||
const [lastUserMessage, setLastUserMessage] = useState<string>('');
|
||||
const [localModelState, setLocalModelState] = useState<LocalLLMModelState>(LocalLLMModelManager.getState());
|
||||
const [activeProvider, setActiveProvider] = useState<string>('openai');
|
||||
@@ -87,7 +88,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
const handleExportOptionSelect = (option: string) => {
|
||||
if (option === 'Export conversation as JSON') {
|
||||
try {
|
||||
const agentMessages = AgentCore.instance().getAgentState().getMessages();
|
||||
const agentMessages = AgentCore.instance().getAgentState().getFullMessages();
|
||||
const exportMessages = agentMessages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
@@ -105,7 +106,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
} else if (option === 'Export conversation as Markdown') {
|
||||
(async () => {
|
||||
try {
|
||||
const agentMessages = AgentCore.instance().getAgentState().getMessages();
|
||||
const agentMessages = AgentCore.instance().getAgentState().getFullMessages();
|
||||
const templateUrl = `${import.meta.env.BASE_URL}chat/export_conversation_template.md`;
|
||||
const res = await fetch(templateUrl);
|
||||
const template = await res.text();
|
||||
@@ -204,6 +205,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
changedKeys.includes('general.llm_provider') ||
|
||||
changedKeys.includes('general.local_browser.context_length') ||
|
||||
changedKeys.some(k => k.startsWith('general.openai.')) ||
|
||||
changedKeys.some(k => k.startsWith('general.claude_openrouter.')) ||
|
||||
changedKeys.some(k => k.startsWith('general.openai_compatible.'))
|
||||
) {
|
||||
applyProviderFromConfig();
|
||||
@@ -255,8 +257,78 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
clearChatHistoryAndUI(setStatus);
|
||||
};
|
||||
|
||||
const runCompactionWithStatus = useCallback(async (
|
||||
trigger: 'manual' | 'auto',
|
||||
focus?: string,
|
||||
): Promise<boolean> => {
|
||||
const agentCore = AgentCore.instance();
|
||||
const statusMessage = createMessage('assistant', 'Compacting Conversation');
|
||||
const progressMessage = createStreamingMessage();
|
||||
let progressTokenCount = 0;
|
||||
|
||||
handleMessageAdd(statusMessage);
|
||||
handleMessageAdd(progressMessage);
|
||||
setIsCompacting(true);
|
||||
|
||||
try {
|
||||
const result = await agentCore.compactConversation({
|
||||
trigger,
|
||||
focus,
|
||||
onProgress: () => {
|
||||
progressTokenCount += 1;
|
||||
handleMessageUpdate(progressMessage.id, (msg) => ({
|
||||
...msg,
|
||||
content: `<span class="processing-wave">Processing...</span>${progressTokenCount > 0 ? ` ${progressTokenCount} tokens received.` : ''} click here to abort.`,
|
||||
tokenCount: progressTokenCount,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
handleMessageUpdate(statusMessage.id, (msg) => ({
|
||||
...msg,
|
||||
content: result.changed ? 'Conversation Compacted' : 'Nothing to Compact Yet',
|
||||
}));
|
||||
handleMessageRemove(progressMessage.id);
|
||||
return result.changed;
|
||||
} catch (error) {
|
||||
console.error('Conversation compaction failed:', error);
|
||||
handleMessageUpdate(statusMessage.id, (msg) => ({
|
||||
...msg,
|
||||
content: `Compaction failed: ${error instanceof Error ? error.message : 'Unable to compact the conversation.'}`,
|
||||
}));
|
||||
handleMessageRemove(progressMessage.id);
|
||||
return false;
|
||||
} finally {
|
||||
setIsCompacting(false);
|
||||
}
|
||||
}, [handleMessageAdd, handleMessageRemove, handleMessageUpdate]);
|
||||
|
||||
const sendWithCompactionRecovery = useCallback(async (
|
||||
llmInput: string,
|
||||
originalUserMessage: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await streamProcessor.processStream(llmInput, 'USER');
|
||||
} catch (error) {
|
||||
const provider = AgentCore.instance().getLLMProvider();
|
||||
if (provider?.isContextTooLongError?.(error)) {
|
||||
const compacted = await runCompactionWithStatus('auto');
|
||||
if (compacted) {
|
||||
try {
|
||||
await streamProcessor.processStream(llmInput, 'USER');
|
||||
} catch (retryError) {
|
||||
console.error('Retry after compaction failed:', retryError, originalUserMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Failed to process user message:', error, originalUserMessage);
|
||||
}
|
||||
}, [runCompactionWithStatus, streamProcessor]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (inputValue.trim() && !isProcessing) {
|
||||
if (inputValue.trim() && !isProcessing && !isCompacting) {
|
||||
const userMessage = inputValue.trim();
|
||||
setLastUserMessage(userMessage);
|
||||
setInputValue('');
|
||||
@@ -273,6 +345,14 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
handleMessageAdd(pseudoMessage);
|
||||
}
|
||||
|
||||
if (filterResult.metadata?.command === 'compact') {
|
||||
await runCompactionWithStatus(
|
||||
'manual',
|
||||
typeof filterResult.metadata.focus === 'string' ? filterResult.metadata.focus : undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filterResult.sendToLLM || !filterResult.finalMessageForLLM) {
|
||||
return;
|
||||
}
|
||||
@@ -294,7 +374,12 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
}
|
||||
|
||||
// Process through stream processor — AgentCore handles the full agentic loop internally
|
||||
await streamProcessor.processStream(filterResult.finalMessageForLLM, 'USER');
|
||||
const agentCore = AgentCore.instance();
|
||||
if (await agentCore.shouldCompactBeforeNextTurn(filterResult.finalMessageForLLM)) {
|
||||
await runCompactionWithStatus('auto');
|
||||
}
|
||||
|
||||
await sendWithCompactionRecovery(filterResult.finalMessageForLLM, userMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -333,13 +418,13 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
|
||||
// Auto-focus input when processing completes
|
||||
useEffect(() => {
|
||||
if (!isProcessing && textareaRef.current) {
|
||||
if (!isProcessing && !isCompacting && textareaRef.current) {
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 0);
|
||||
}
|
||||
}, [isProcessing]);
|
||||
}, [isCompacting, isProcessing]);
|
||||
|
||||
const localRuntimeMessage = localModelState.runtimeSupport.reason;
|
||||
const hasLocalRuntimeHardFailure = !localModelState.runtimeSupport.supported;
|
||||
@@ -448,7 +533,7 @@ const ChatBox: React.FC<ChatBoxProps> = ({ isVisible }) => {
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{!isProcessing && (
|
||||
{!isProcessing && !isCompacting && (
|
||||
<div className="chatbox-input-area">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
|
||||
@@ -93,4 +93,21 @@ describe('AssistantMessage', () => {
|
||||
expect(codeElement).toBeInTheDocument();
|
||||
expect(codeElement).toHaveTextContent('const value = 1;');
|
||||
});
|
||||
|
||||
it('renders compacting and compacted messages as divider banners', () => {
|
||||
const { rerender, container } = render(
|
||||
<AssistantMessage content="Compacting Conversation" />
|
||||
);
|
||||
|
||||
expect(container.querySelector('.message-divider-banner')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Compacting Conversation')).toBeInTheDocument();
|
||||
|
||||
rerender(<AssistantMessage content="Conversation Compacted" />);
|
||||
|
||||
expect(screen.getByLabelText('Conversation Compacted')).toBeInTheDocument();
|
||||
|
||||
rerender(<AssistantMessage content="Nothing to Compact Yet" />);
|
||||
|
||||
expect(screen.getByLabelText('Nothing to Compact Yet')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,6 +44,9 @@ const formatTps = (value?: number): string | null => {
|
||||
|
||||
const THINKING_LABEL = 'Thinking...';
|
||||
const PROCESSING_LABEL = 'Processing...';
|
||||
const COMPACTION_IN_PROGRESS_LABEL = 'Compacting Conversation';
|
||||
const COMPACTION_DONE_LABEL = 'Conversation Compacted';
|
||||
const COMPACTION_EMPTY_LABEL = 'Nothing to Compact Yet';
|
||||
|
||||
const formatThinkingDuration = (elapsedSeconds: number): string => {
|
||||
if (elapsedSeconds < 60) {
|
||||
@@ -62,6 +65,9 @@ const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreamin
|
||||
const [thinkingElapsedSeconds, setThinkingElapsedSeconds] = useState(0);
|
||||
const processingWaveLabels = [THINKING_LABEL, PROCESSING_LABEL];
|
||||
const isThinking = isStreaming && content.includes(`<span class="processing-wave">${THINKING_LABEL}</span>`);
|
||||
const isCompactionBanner = content === COMPACTION_IN_PROGRESS_LABEL
|
||||
|| content === COMPACTION_DONE_LABEL
|
||||
|| content === COMPACTION_EMPTY_LABEL;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isThinking) {
|
||||
@@ -83,6 +89,16 @@ const AssistantMessage: React.FC<AssistantMessageProps> = ({ content, isStreamin
|
||||
}, [isThinking]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isCompactionBanner) {
|
||||
return (
|
||||
<div className="message-divider-banner" aria-label={content}>
|
||||
<span className="message-divider-banner-line" aria-hidden="true" />
|
||||
<span className="message-divider-banner-label">{content}</span>
|
||||
<span className="message-divider-banner-line" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle special abort link for streaming messages
|
||||
if (isStreaming && onAbort && content.includes('click here to abort')) {
|
||||
const processingWaveMarkup = processingWaveLabels
|
||||
|
||||
@@ -16,6 +16,7 @@ const configState = new Map<string, unknown>([
|
||||
['general.language', 'auto'],
|
||||
['general.llm_provider', 'local_browser'],
|
||||
['general.persist_api_keys_non_localhost', false],
|
||||
['general.auto_compact_threshold_percent', 90],
|
||||
['general.openai.api_key', ''],
|
||||
['general.openai.model', 'gpt-5.4-mini'],
|
||||
['general.openai.flex', false],
|
||||
@@ -107,6 +108,7 @@ describe('GeneralSettings', () => {
|
||||
beforeEach(() => {
|
||||
configState.set('general.language', 'auto');
|
||||
configState.set('general.local_browser.context_length', 65536);
|
||||
configState.set('general.auto_compact_threshold_percent', 90);
|
||||
configManagerMock.get.mockClear();
|
||||
configManagerMock.set.mockClear();
|
||||
localModelState.isCached = false;
|
||||
@@ -173,6 +175,19 @@ describe('GeneralSettings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders and persists the auto-compact threshold', async () => {
|
||||
renderSettings();
|
||||
|
||||
const select = await screen.findByLabelText('Auto-Compact Threshold');
|
||||
expect((select as HTMLSelectElement).value).toBe('90');
|
||||
|
||||
fireEvent.change(select, { target: { value: '80' } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(configManagerMock.set).toHaveBeenCalledWith('general.auto_compact_threshold_percent', 80);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders and persists local runtime download URLs', async () => {
|
||||
renderSettings();
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ const GeneralSettings: React.FC = () => {
|
||||
const [claudeOpenRouterModel, setClaudeOpenRouterModel] = useState<string>('');
|
||||
const [openaiFlex, setOpenaiFlex] = useState<boolean>(false);
|
||||
const [persistApiKeysNonLocalhost, setPersistApiKeysNonLocalhost] = useState<boolean>(false);
|
||||
const [autoCompactThresholdPercent, setAutoCompactThresholdPercent] = useState<80 | 90 | 95>(90);
|
||||
const [compatibleKey, setCompatibleKey] = useState<string>('');
|
||||
const [compatibleBaseUrl, setCompatibleBaseUrl] = useState<string>('');
|
||||
const [compatibleModel, setCompatibleModel] = useState<string>('');
|
||||
@@ -107,6 +108,9 @@ const GeneralSettings: React.FC = () => {
|
||||
setOpenaiModel((configManager.get('general.openai.model') as string) || '');
|
||||
setOpenaiFlex((configManager.get('general.openai.flex') as boolean) ?? false);
|
||||
setPersistApiKeysNonLocalhost((configManager.get('general.persist_api_keys_non_localhost') as boolean) ?? false);
|
||||
setAutoCompactThresholdPercent(
|
||||
((configManager.get('general.auto_compact_threshold_percent') as 80 | 90 | 95 | undefined) ?? 90),
|
||||
);
|
||||
setGeminiKey((configManager.get('general.gemini.api_key') as string) || '');
|
||||
setGeminiModel((configManager.get('general.gemini.model') as string) || '');
|
||||
setClaudeKey((configManager.get('general.claude.api_key') as string) || '');
|
||||
@@ -206,6 +210,18 @@ const GeneralSettings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoCompactThresholdChange = async (value: string) => {
|
||||
const parsed = Number(value);
|
||||
const normalized: 80 | 90 | 95 = parsed === 80 || parsed === 95 ? parsed : 90;
|
||||
setAutoCompactThresholdPercent(normalized);
|
||||
try {
|
||||
await configManager.set('general.auto_compact_threshold_percent', normalized);
|
||||
console.log('Auto-compact threshold changed to:', normalized);
|
||||
} catch (error) {
|
||||
console.error('Failed to save auto-compact threshold:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGeminiKeyChange = (value: string) => {
|
||||
setGeminiKey(value);
|
||||
debouncedSave('general.gemini.api_key', value);
|
||||
@@ -407,6 +423,25 @@ const GeneralSettings: React.FC = () => {
|
||||
{t('settings.general.persistKeys.help')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-item">
|
||||
<label className="settings-label" htmlFor="general-auto-compact-threshold">
|
||||
Auto-Compact Threshold
|
||||
</label>
|
||||
<select
|
||||
id="general-auto-compact-threshold"
|
||||
className="settings-select"
|
||||
value={autoCompactThresholdPercent}
|
||||
onChange={(e) => void handleAutoCompactThresholdChange(e.target.value)}
|
||||
>
|
||||
<option value="95">Conservative (95%)</option>
|
||||
<option value="90">Standard (90%)</option>
|
||||
<option value="80">Early (80%)</option>
|
||||
</select>
|
||||
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
|
||||
Compact the conversation before the next request when estimated context usage reaches this level.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-group">
|
||||
|
||||
Reference in New Issue
Block a user