Skip to content

Commit bb73fef

Browse files
committed
feat: Enhance auto-compression functionality with retry logic and improved error handling
- Added `isCompressing` state to track compression status in chat logic. - Implemented `performAutoCompression` function with retry mechanism for context compression. - Updated UI components to reflect compression status and errors, including retry notifications. - Introduced new messages for compression retry attempts in sub-agent handling. - Enhanced file creation logic to support overwriting with backup. - Added translation keys for model count and scroll hints in language files. - Refactored model selection logic to ensure unique model options are displayed.
1 parent 08dcaaa commit bb73fef

23 files changed

Lines changed: 425 additions & 164 deletions

source/hooks/conversation/chatLogic/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface UseChatLogicProps {
3636
images?: Array<{type: 'image'; data: string; mimeType: string}>;
3737
} | null>
3838
>;
39+
isCompressing: boolean;
3940
setIsCompressing: React.Dispatch<React.SetStateAction<boolean>>;
4041
setCompressionError: React.Dispatch<React.SetStateAction<string | null>>;
4142
currentContextPercentageRef: React.MutableRefObject<number>;

source/hooks/conversation/chatLogic/useMessageProcessing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export function useMessageProcessing(props: UseChatLogicProps) {
161161

162162
const errorMessage: Message = {
163163
role: 'assistant',
164-
content: `**Auto-compression Failed**\n\n${errorMsg}`,
164+
content: `**Auto-compression Failed**`,
165165
streaming: false,
166166
};
167167
setMessages(prev => [...prev, errorMessage]);

source/hooks/conversation/chatLogic/useRemoteEvents.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {UseChatLogicProps} from './types.js';
33
import type {RollbackMode} from '../../../ui/components/tools/FileRollbackConfirmation.js';
44
import {connectionManager} from '../../../utils/connection/ConnectionManager.js';
55
import {sessionManager} from '../../../utils/session/sessionManager.js';
6-
import {executeContextCompression} from '../useCommandHandler.js';
6+
import {performAutoCompression} from '../../../utils/core/autoCompress.js';
77

88
interface UseRemoteEventsHandlers {
99
handleMessageSubmit: (
@@ -318,19 +318,29 @@ export function useRemoteEvents(
318318
await connectionManager.notifyCompactStarted();
319319

320320
const currentSession = sessionManager.getCurrentSession();
321-
if (!currentSession) {
322-
throw new Error('No active session to compress');
323-
}
324321

325-
const compressionResult = await executeContextCompression(
326-
currentSession.id,
327-
status => {
322+
const compressionResult = await performAutoCompression(
323+
currentSession?.id,
324+
(status) => {
328325
props.onCompressionStatus?.(status);
329326
},
330327
);
331328

329+
if (compressionResult && (compressionResult as any).hookFailed) {
330+
setCompressionError('Blocked by beforeCompress hook');
331+
await connectionManager.notifyCompactCompleted({
332+
success: false,
333+
error: 'Blocked by beforeCompress hook',
334+
});
335+
return;
336+
}
337+
332338
if (!compressionResult) {
333-
throw new Error('Compression failed');
339+
await connectionManager.notifyCompactCompleted({
340+
success: false,
341+
error: 'Compression failed after retries',
342+
});
343+
return;
334344
}
335345

336346
props.onCompressionStatus?.(null);
@@ -353,6 +363,9 @@ export function useRemoteEvents(
353363
message: errorMsg,
354364
});
355365
setCompressionError(errorMsg);
366+
setTimeout(() => {
367+
props.onCompressionStatus?.(null);
368+
}, 5000);
356369

357370
await connectionManager.notifyCompactCompleted({
358371
success: false,

source/hooks/conversation/core/autoCompressHandler.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function handleAutoCompression(
7272
const session = sessionManager.getCurrentSession();
7373

7474
// Set up status callback for UI display
75-
const onStatusUpdate = (status: CompressionStatus) => {
75+
const onStatusUpdate = (status: CompressionStatus | null) => {
7676
options.onCompressionStatus?.(status);
7777
};
7878

@@ -81,8 +81,11 @@ export async function handleAutoCompression(
8181
onStatusUpdate,
8282
);
8383

84-
// Clear status after completion
85-
options.onCompressionStatus?.(null);
84+
// Only clear status on success/hookFailed;
85+
// failed status will auto-dismiss after 5s (handled by performAutoCompression)
86+
if (compressionResult) {
87+
options.onCompressionStatus?.(null);
88+
}
8689

8790
// Check if beforeCompress hook failed
8891
if (compressionResult && (compressionResult as any).hookFailed) {
@@ -131,6 +134,9 @@ export async function handleAutoCompression(
131134
step: 'failed',
132135
message: error instanceof Error ? error.message : 'Unknown error',
133136
});
137+
setTimeout(() => {
138+
options.onCompressionStatus?.(null);
139+
}, 5000);
134140
} finally {
135141
compressionCoordinator.releaseLock('main');
136142
}

source/hooks/conversation/core/subAgentMessageHandler.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ export class SubAgentUIHandler {
150150
return this.handleContextUsage(prev, subAgentMessage);
151151
case 'context_compressing':
152152
return this.handleContextCompressing(prev, subAgentMessage);
153+
case 'context_compress_retrying':
154+
return this.handleContextCompressRetrying(prev, subAgentMessage);
153155
case 'context_compressed':
154156
return this.handleContextCompressed(prev, subAgentMessage);
155157
case 'inter_agent_sent':
@@ -731,6 +733,27 @@ export class SubAgentUIHandler {
731733
];
732734
}
733735

736+
private handleContextCompressRetrying(
737+
prev: Message[],
738+
subAgentMessage: SubAgentMessage,
739+
): Message[] {
740+
const msg = subAgentMessage.message as any;
741+
return [
742+
...prev,
743+
{
744+
role: 'subagent' as const,
745+
content: `\x1b[36m⚇ ${subAgentMessage.agentName}\x1b[0m \x1b[33m⟳ Compression retry (${msg.attempt}/${msg.maxRetries})...\x1b[0m${msg.error ? ` \x1b[90m${msg.error}\x1b[0m` : ''}`,
746+
streaming: false,
747+
subAgent: {
748+
agentId: subAgentMessage.agentId,
749+
agentName: subAgentMessage.agentName,
750+
isComplete: false,
751+
},
752+
subAgentInternal: true,
753+
},
754+
];
755+
}
756+
734757
private handleContextCompressed(
735758
prev: Message[],
736759
subAgentMessage: SubAgentMessage,

source/hooks/conversation/useChatLogic.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function useChatLogic(props: UseChatLogicProps) {
2020
setPendingMessages,
2121
setRestoreInputContent,
2222
userInterruptedRef,
23+
isCompressing,
2324
vscodeState,
2425
commandsLoaded,
2526
terminalExecutionState,
@@ -211,6 +212,14 @@ export function useChatLogic(props: UseChatLogicProps) {
211212
return true;
212213
}
213214

215+
// Block ESC during /compact command compression
216+
if (isCompressing) {
217+
streamingState.setCompressBlockToast(
218+
t.chatScreen.compressionBlockToast,
219+
);
220+
return true;
221+
}
222+
214223
// Handle scheduler task interruption
215224
if (schedulerExecutionState?.state.isRunning) {
216225
schedulerExecutionState.resetTask();
@@ -273,6 +282,7 @@ export function useChatLogic(props: UseChatLogicProps) {
273282
backgroundProcesses,
274283
terminalExecutionState,
275284
streamingState,
285+
isCompressing,
276286
hasFocus,
277287
pendingMessages,
278288
handleInterrupt,

source/hooks/conversation/useCommandHandler.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -478,36 +478,36 @@ export function useCommandHandler(options: CommandHandlerOptions) {
478478
options.setCompressionError(null);
479479

480480
try {
481-
// 获取当前会话ID
482-
const currentSession = sessionManager.getCurrentSession();
483-
if (!currentSession) {
484-
throw new Error('No active session to compress');
485-
}
481+
const {performAutoCompression} = await import(
482+
'../../utils/core/autoCompress.js'
483+
);
486484

487-
// 使用提取的压缩函数,传入当前会话ID和状态回调
488-
const compressionResult = await executeContextCompression(
489-
currentSession.id,
490-
status => {
485+
const currentSession = sessionManager.getCurrentSession();
486+
const compressionResult = await performAutoCompression(
487+
currentSession?.id,
488+
(status: CompressionStatus | null) => {
491489
options.onCompressionStatus?.(status);
492490
},
493491
);
494492

493+
if (compressionResult && (compressionResult as any).hookFailed) {
494+
const errorMsg = 'Blocked by beforeCompress hook';
495+
options.setCompressionError(errorMsg);
496+
return;
497+
}
498+
495499
if (!compressionResult) {
496-
throw new Error('Compression failed');
500+
return;
497501
}
498502

499-
// Clear compression status after completion
500503
options.onCompressionStatus?.(null);
501504

502-
// 更新UI
503505
options.clearSavedMessages();
504506
options.setMessages(compressionResult.uiMessages);
505507
options.setRemountKey(prev => prev + 1);
506508

507-
// Update token usage with compression result
508509
options.setContextUsage(compressionResult.usage);
509510
} catch (error) {
510-
// Show error message
511511
const errorMsg =
512512
error instanceof Error
513513
? error.message
@@ -517,13 +517,9 @@ export function useCommandHandler(options: CommandHandlerOptions) {
517517
message: errorMsg,
518518
});
519519
options.setCompressionError(errorMsg);
520-
521-
const errorMessage: Message = {
522-
role: 'assistant',
523-
content: `**Compression Failed**\\n\\n${errorMsg}`,
524-
streaming: false,
525-
};
526-
options.setMessages(prev => [...prev, errorMessage]);
520+
setTimeout(() => {
521+
options.onCompressionStatus?.(null);
522+
}, 5000);
527523
} finally {
528524
options.setIsCompressing(false);
529525
}

source/i18n/lang/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,8 @@ export const en: TranslationKeys = {
708708
saveFailed: 'Save failed',
709709
modelSaveFailed: 'Model save failed',
710710
tipLabel: 'Tip:',
711+
modelCount: '{count} models',
712+
scrollHint: '↑↓ scroll for more',
711713
},
712714
profilePanel: {
713715
title: 'Select Profile',

source/i18n/lang/zh-TW.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,8 @@ export const zhTW: TranslationKeys = {
666666
saveFailed: '儲存失敗',
667667
modelSaveFailed: '模型儲存失敗',
668668
tipLabel: '提示:',
669+
modelCount: '共 {count} 個模型',
670+
scrollHint: '↑↓ 捲動瀏覽更多模型',
669671
},
670672
profilePanel: {
671673
title: '選擇設定檔',

source/i18n/lang/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@ export const zh: TranslationKeys = {
665665
saveFailed: '保存失败',
666666
modelSaveFailed: '模型保存失败',
667667
tipLabel: '提示:',
668+
modelCount: '共 {count} 个模型',
669+
scrollHint: '↑↓ 滚动浏览更多模型',
668670
},
669671
profilePanel: {
670672
title: '选择配置',

0 commit comments

Comments
 (0)