tasks/prd-compaction-ux-improvement.md
改进 session 自动压缩(Compaction)的用户体验。当前压缩在助手回复后静默执行,过程对用户不可见,且压缩点只能查看不能删除。本次改进将压缩触发时机调整为用户发送消息时,压缩过程在对话界面可见,并支持删除最新的摘要消息。
Phase 2 补充:解决 Phase 1 实现中发现的以下问题:
Phase 3 Bugfix:修复自动压缩永远不会触发的 bug(待确认根因)
Description: As a developer, I need to move the compaction check from after AI response to before message sending, so that compaction happens at the right time.
Acceptance Criteria:
scheduleCompactionCheck call from generate function end (sessionActions.ts:892)Description: As a developer, I need to track compaction status in UI state, so that components can react to compaction progress.
Acceptance Criteria:
compactionUIState atom per session: { status: 'idle' | 'running' | 'failed', error: string | null }Description: As a user, I want to see when compaction is happening in the chat, so that I understand why sending is delayed.
Acceptance Criteria:
CompactionProgressIndicator component renders at message list bottom when status === 'running'CompressionModalDescription: As a user, I want to see error information when compaction fails, so that I can retry.
Acceptance Criteria:
status === 'failed', show error message and "Retry" button in indicatorDescription: As a user, I need the input to be disabled during compaction, so that I don't accidentally modify my message.
Acceptance Criteria:
compactionStatus === 'running'compactionStatus === 'running'Description: As a developer, I need to change when the draft is cleared, so that failed compaction doesn't lose user's message.
Acceptance Criteria:
Description: As a user, I can switch sessions while compaction is running without losing the compaction progress.
Acceptance Criteria:
Description: As a user, I want to delete summary messages, so that I can undo unwanted compaction.
Acceptance Criteria:
isSummary: true) show "Delete" option in context menuDescription: As a developer, I need to handle summary message deletion with cascade delete of compaction point.
Acceptance Criteria:
session.compactionPointsDescription: As a user, I want to confirm before deleting a summary, so that I understand the consequences.
Acceptance Criteria:
Description: As a user, I want my draft to be cleared at the right time (after compaction, before user message is sent), so that my input doesn't remain in the box after sending.
Acceptance Criteria:
submitNewUserMessage to accept onUserMessageReady callback parameterInputBox.handleSubmit passes callback that clears draft and resets stateDescription: As a user, I want the delete menu to appear below the summary content on hover, consistent with regular message behavior.
Acceptance Criteria:
group/summary + group-hover/summary:opacity-100 opacity-0 for hover visibilityDescription: As a developer, I need a unified component to display compaction status that works for both auto and manual compaction.
Acceptance Criteria:
CompactionStatus.tsx componentCompressionModal approach)compactionUIState atom for status and streaming textDescription: As a developer, I need to track streaming output text during compaction for display in the UI.
Acceptance Criteria:
streamingText: string field to compactionUIState atomrunCompactionWithUIState to use streamText instead of generateTextstreamingText in atom on each chunk receivedstreamingText when compaction completes or failsDescription: As a user, I want to see compaction status at the bottom of the message list, so it scrolls with messages.
Acceptance Criteria:
CompactionStatus after the last message in Virtuoso liststatus !== 'idle'CompactionProgressIndicator from session/$sessionId.tsxCompactionProgressIndicator.tsx fileDescription: As a user, I want manual compression to use the same display as auto compression after I confirm.
Acceptance Criteria:
CompressionModal keeps confirmation step (show warning, Cancel/Confirm buttons)runCompactionWithUIState with force: true to trigger compactionCompactionStatus in MessageList takes over the displayCompressionModal (only keep confirmation UI)Description: As a user, I expect auto-compaction to trigger correctly when my conversation exceeds the token threshold. Currently it never triggers.
Root Cause Analysis:
compaction-detector.ts 使用 getModelContextWindowSync(modelId) 从 builtin-data 获取 contextWindow,但 UI 显示的是从 provider settings(ChatboxAI API 返回)获取的 modelInfo.contextWindow。
例如 DeepSeek V3.2:
deepseek-v3 前缀匹配)导致:
Solution:
修改 checkOverflow() 接受可选 contextWindow 参数,优先使用 provider settings 中的值,fallback 到 builtin-data。
Acceptance Criteria:
OverflowCheckOptions 添加可选 contextWindow 字段checkOverflow() 优先使用传入的 contextWindow,未提供时 fallback 到 getModelContextWindowSync()getCompactionThresholdTokens() 同样支持可选 contextWindow 参数getModelContextWindowFromSettings() 辅助函数从 provider settings 获取 contextWindowneedsCompaction() 使用 getModelContextWindowFromSettings() 获取并传入 contextWindowrunCompaction() 同样传入正确的 contextWindowgenerate function end to before message sendingcompactionUIState atom with status and error fields (per session, not persisted)CompactionProgressIndicator at message list bottom during compactionCompressionModal for progress displaysubmitNewUserMessage accepts onUserMessageReady callback, invoked after compaction before message insertCompactionStatus component replacing CompactionProgressIndicatorstreamingText field to compactionUIState for real-time output displaygenerateText to streamText for streaming outputCompactionStatus renders inside MessageList (scrolls with messages), not above InputBoxCompressionModal) triggers unified compaction flow after confirmationCompressionModal only shows confirmation UI, streaming display delegated to CompactionStatuscheckOverflow() 支持可选 contextWindow 参数,优先使用传入值needsCompaction() 和 runCompaction() 从 provider settings 获取 contextWindow 并传入 checkOverflow()getModelContextWindowFromSettings() 辅助函数从 settings.providers[providerId].models 获取 contextWindowsummarizeConversation remains unchanged)src/renderer/stores/sessionActions.ts - Move compaction trigger, adjust send flowsrc/renderer/packages/context-management/compaction.ts - Ensure runCompaction returns Promise for awaitsrc/renderer/stores/atoms/compactionAtoms.ts - Add compactionUIState atomsrc/renderer/components/CompactionProgressIndicator.tsx - New progress indicator componentsrc/renderer/components/InputBox/InputBox.tsx - Disable state based on compaction statussrc/renderer/components/SummaryMessage.tsx - Add delete menu to summary messagessrc/renderer/components/MessageList.tsx - Handle summary message renderingsrc/renderer/stores/sessionActions.ts - Add onUserMessageReady callback to submitNewUserMessagesrc/renderer/stores/atoms/compactionAtoms.ts - Add streamingText fieldsrc/renderer/packages/context-management/compaction.ts - Change to streamText, add streaming callbacksrc/renderer/components/InputBox/InputBox.tsx - Update onSubmit type, pass callback for draft clearingsrc/renderer/components/SummaryMessage.tsx - Relocate delete menu to content area with hoversrc/renderer/components/CompactionStatus.tsx - New: Unified compaction status componentsrc/renderer/components/MessageList.tsx - Integrate CompactionStatus at list bottomsrc/renderer/components/CompressionModal.tsx - Simplify to confirmation-only, delegate displaysrc/renderer/routes/session/$sessionId.tsx - Remove CompactionProgressIndicator, update onSubmitsrc/renderer/components/CompactionProgressIndicator.tsx - Delete: Replaced by CompactionStatussrc/renderer/packages/context-management/compaction-detector.ts - 添加 contextWindow 参数支持src/renderer/packages/context-management/compaction.ts - 添加 getModelContextWindowFromSettings() 辅助函数,修改 needsCompaction() 和 runCompaction()src/renderer/packages/context-management/compaction-detector.test.ts - 新增 contextWindow override 测试用例CompressionModal (last 3 lines, fixed height 60px)MessageActionIcon component for hover action buttonscompactionUIState should be a Jotai atom keyed by sessionId{ status: 'idle' | 'running' | 'failed', error: string | null, streamingText: string }User clicks Send
↓
InputBox.handleSubmit
↓
onSubmit({ constructedMessage, needGenerating, onUserMessageReady })
↓
submitNewUserMessage(sessionId, { newUserMsg, ... })
↓
Get model's contextWindow from settings (modelInfo.contextWindow) ← Phase 3 fix
↓
runCompactionWithUIState(sessionId, { contextWindow }) ← Phase 3 fix: 传入正确的 contextWindow
├── needsCompaction(sessionId, { contextWindow })
│ └── checkOverflow({ tokens, modelId, contextWindow }) ← Phase 3 fix: 使用传入的 contextWindow
├── If (tokens > threshold): execute compaction
│ ├── Updates compactionUIState.status = 'running'
│ ├── streamText with onChunk callback
│ │ └── Updates compactionUIState.streamingText
│ ├── On success: status = 'idle', streamingText = ''
│ └── On failure: status = 'failed', error = message
└── If (tokens <= threshold): skip compaction
↓
onUserMessageReady callback → InputBox clears draft
↓
insertMessage (user message)
↓
generate (if needGenerating)
compaction-detector 使用 builtin-data 的 contextWindow(如 DeepSeek V3.2 匹配到 128K),而 UI 使用 provider settings 的 contextWindow(64K),导致阈值计算不一致checkOverflow() 添加可选 contextWindow 参数,needsCompaction() 和 runCompaction() 从 provider settings 获取并传入compaction-detector 只使用 getModelContextWindowSync(modelId) 获取 contextWindow,没有使用用户配置的 modelInfo.contextWindowgetModelContextWindowSync 返回 null,导致 checkOverflow 直接返回 isOverflow: false