-
Notifications
You must be signed in to change notification settings - Fork 32
🤖 Stateless /compact UX with structured metadata #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7ed9245 to
0d5ca84
Compare
a4198f6 to
3876d0f
Compare
When compaction is running with a user message queued, display that pending message under the compacting barrier and allow the user to edit it. Changes: - Add PendingUserDraft component that displays queued input under compacting barrier - Add CompactionBarrier component for visual distinction from normal streaming - Keep ChatInput enabled during compaction (users can edit draft) - Block sending during compaction with friendly toast - Persist draft with listener mode for live cross-component sync - Expose ChatInput focus API to parent for Edit button UX Flow: 1. User types a message but doesn't send it 2. User triggers compaction (e.g., /compact) 3. While compacting: - CompactionBarrier appears - Below it: PENDING USER [not sent] block shows current draft - User can continue editing; changes reflect live - Edit button focuses input; Discard clears draft - Pressing Enter shows toast: message is queued 4. When compaction completes, user presses Enter to send Generated with `cmux`
Replace localStorage-based compaction state tracking with structured metadata embedded in messages. This eliminates string parsing, event systems, and localStorage complexity. Key changes: - Add CmuxFrontendMetadata to IPC layer for frontend-defined metadata - Backend passes through cmuxMetadata as black-box (no interpretation) - ChatInput sends structured metadata with compaction requests - useAutoCompactContinue queries message metadata instead of localStorage - Remove PendingUserDraft component (no longer needed) - Remove onCompactStart callback chain Benefits: - Type-safe structured metadata with discriminated unions - No string parsing - direct access to parsed command options - Single source of truth (message metadata) - Queryable - easy to find compaction messages - Handles edited commands (re-parses on edit) - Simpler: ~60 lines net reduction Deleted: - src/components/Messages/PendingUserDraft.tsx (84 lines) Modified: - src/types/message.ts - CmuxFrontendMetadata already defined - src/types/ipc.ts - Add cmuxMetadata to SendMessageOptions - src/services/agentSession.ts - Pass through metadata - src/components/ChatInput.tsx - Send structured metadata, remove callback - src/utils/messages/StreamingMessageAggregator.ts - Handle compaction-request - src/components/AIView.tsx - Remove PendingUserDraft and callback - src/App.tsx - Remove callback chain - src/hooks/useAutoCompactContinue.ts - Rewrite to use metadata _Generated with `cmux`_
Verify that arbitrary frontend metadata (cmuxMetadata) can be: 1. Sent from frontend via IPC (SendMessageOptions) 2. Stored by backend as black-box (no interpretation) 3. Read back by frontend via chat events Test specifically validates: - Structured compaction-request metadata storage - All fields preserved exactly as sent - Backend treats metadata as opaque data - Round-trip through IPC + history maintains integrity This confirms the structured metadata approach works correctly through the full IPC layer. _Generated with `cmux`_
Remove obsolete "Message Queued" toast that was paired with the deleted PendingUserDraft component. Now input is disabled during compaction (consistent with streaming behavior) with a clear placeholder message. Changes: - Disable textarea during compaction - Update placeholder: "Compacting... (Esc to cancel)" - Remove spammy "Message Queued" toast on every Enter press - Simplify handleSend early return logic This makes compaction UX consistent with streaming - the barrier shows status and the input is disabled until the operation completes. _Generated with `cmux`_
…tadata Fix two editing-related bugs: 1. **Editing state persists across workspace switches** - Add useEffect to clear editingMessage when workspaceId changes - Prevents stuck editing state when switching workspaces 2. **Duplicate/missing compaction messages after edit + reload** - Parse commands when editing to preserve metadata - Re-create cmuxMetadata for /compact commands during edit - Ensures edited compaction messages maintain their metadata - Prevents duplicate messages or missing metadata after reload Changes: - src/components/AIView.tsx: Clear editing state on workspace change - src/components/ChatInput.tsx: Parse commands when editing, include metadata This ensures: - Clean state when switching workspaces - Consistent display of compaction messages after editing - Metadata preserved through edit operations _Generated with `cmux`_
When editing a compaction message, properly regenerate the actual summarization request instead of sending the command text directly. Problem: - Original: sends "Summarize this conversation..." - Edit to "/compact -c new": was sending "/compact -c new" as text ❌ - Result: broken message, duplicate UI, locked editing state Solution: - Detect when editing a /compact command - Regenerate: "Summarize this conversation... Use approximately N words" - Include proper metadata (command + parsed options) - Include compaction options (toolPolicy, mode, thinkingLevel) This allows natural editing of compaction messages: - Edit "/compact -c old" to "/compact -c new" - New compaction runs with new settings - Hook picks up new continue message - No duplicate messages, clean state Changes: - Regenerate actualMessageText from parsed command - Include compactionOptions when editing /compact - Hook already handles "most recent" correctly _Generated with `cmux`_
Extract duplicated compaction message preparation logic into a single helper function used by both slash command handler and edit handler. Changes: - Add prepareCompactionMessage() helper function - Returns: messageText, metadata, options - Slash command handler: use helper (removed 18 lines) - Edit handler: use helper (removed 19 lines) - Net: +40 lines helper, -37 lines duplication = +3 lines Benefits: - DRY: Single source of truth for compaction logic - Type-safe: Return type ensures consistency - Maintainable: Change once, applies everywhere - Testable: Can unit test independently The helper encapsulates: - Parsing command - Generating summarization request text - Creating metadata structure - Setting compaction options (toolPolicy, mode, thinkingLevel) _Generated with `cmux`_
3876d0f to
28415b6
Compare
Changes: - Renamed 'command' to 'rawCommand' in CmuxFrontendMetadata - Clarifies this is the user's exact input for faithful display - Removed CompactionBarrier component - StreamingBarrier now handles both streaming and compaction - Reduces code duplication (37 lines removed) - Updated all references to use rawCommand: - prepareCompactionMessage() in ChatInput - StreamingMessageAggregator display logic - useAutoCompactContinue edit detection - Integration test Benefits: - More semantic field name (rawCommand vs command) - Single barrier component for both streaming states - No functional changes - all tests pass
6f6bb49 to
93c1ef3
Compare
The edit detection logic was fundamentally broken:
- Compared message parts ("Summarize...") to rawCommand ("/compact -c msg")
- These are ALWAYS different by design
- Condition always triggered, tried parsing "Summarize...", failed
- Set continueMessage = undefined, breaking auto-continue for ALL compactions
Root cause: Unnecessary complexity. ChatInput already regenerates metadata
on every edit, so the metadata always reflects the current state.
Solution: Remove the broken edit detection entirely. Just use the metadata.
Changes:
- Remove edit detection block (lines 69-80)
- Remove unused imports (parseCommand, CmuxTextPart)
- Simplify: continueMessage = cmuxMeta.parsed.continueMessage
Benefits:
- Fixes auto-continue for all compaction messages
- Reduces duplicate message race conditions
- Simplifies code (-14 lines)
- Works correctly for all edit scenarios
Net: -14 lines
Fix stuck editing state and duplicate messages during compaction edits. Problem: - editingMessage state persists even after message is replaced - User sees editing controls but message doesn't exist - Causes duplicate display and stuck UI state Root cause: - Message replaced during edit (new historySequence) - editCutoffHistoryId becomes undefined (can't find old historyId) - But editingMessage still set, causing UI confusion Solution: - Add useEffect to detect when editingMessage is set but editCutoffHistoryId is undefined - Automatically clear editingMessage when this happens - Handles: edits, deletions, replacements Benefits: - Editing state clears automatically when message replaced - No stuck editing mode - No duplicate messages - Works for all edit scenarios (compact, regular, delete) Net: +8 lines
- Pass editMessageId in compaction path to replace instead of duplicate - Clear editing state on successful compaction submission - Use cmuxMetadata.type check instead of toolPolicy regex matching - Ensures consistent 'compacting...' status during stream replay Fixes three issues: 1. Editing compaction messages no longer creates duplicates 2. Continue message preserved correctly (no duplicate confusion) 3. After reload, barrier shows 'compacting...' not 'streaming...'
Store continueMessage in summary metadata so it survives history replacement. **Problem:** - User sends /compact -c "continue working" - Compaction completes, history replaced with summary - Original compaction-request message deleted - useAutoCompactContinue can't find continueMessage **Solution:** - Extract continueMessage from compaction-request BEFORE replacement - Store in summary message as compaction-result metadata - useAutoCompactContinue reads from summary instead of deleted request **Changes:** - Add compaction-result type to CmuxFrontendMetadata - WorkspaceStore extracts and preserves continueMessage during replacement - useAutoCompactContinue reads from summary's compaction-result metadata
**Fix 1: Enable input when editing during compaction**
- Editing mode now overrides isCompacting/isSending disable logic
- Users can edit messages during active streams as designed
- Change: disabled={!editingMessage && (disabled || isSending || isCompacting)}
**Fix 2: Buffer stream events until history is loaded**
- Stream events (stream-start, deltas, etc.) now buffer until caught-up
- Ensures aggregator has full historical context when processing events
- Fixes isCompacting detection during page reload/replay
**Why this fixes "streaming" vs "compacting" after reload:**
- Before: stream-start processed with empty message cache → no user messages → isCompacting=false
- After: stream-start buffered → history loaded → THEN stream-start processed → finds compaction-request → isCompacting=true
**Benefits:**
- ✅ Eliminates entire class of timing bugs during replay
- ✅ All stream events processed with complete context
- ✅ isCompacting detection works correctly after reload
- ✅ Cleaner architecture - don't process events until ready
**Changes:**
- Add pendingStreamEvents buffer
- Add isStreamEvent() helper to identify stream events
- Buffer stream events when !isCaughtUp
- Process buffered events after loading history
- Extract processStreamEvent() method for event handling
- Edit button now disabled when isCompacting=true - Tooltip shows: "Cannot edit while compacting (press Esc to cancel)" - Uses formatKeybind(KEYBINDS.INTERRUPT_STREAM) for platform-appropriate key display - Prevents broken state from editing during compaction **Why disable vs enable input:** - Simpler UX - clear visual feedback that editing is unavailable - Prevents confusion - user sees grayed button with explanation - Consistent with "cancel to edit" pattern - No risk of broken state from edit during compaction
Stream event buffering requires workspaces to be caught-up before processing stream events. Updated tests to send caught-up message first to match real IPC replay behavior.
Member
|
Manual merging due to flake |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
Replace localStorage-based compaction state tracking with structured metadata embedded in messages. Four critical bugs discovered after initial implementation were fixed through systematic improvements to the replay and metadata flow architecture.
Key insight: Embed structured metadata directly in the user's compaction request message. The backend stores it as a black-box, and the frontend queries it when needed—no localStorage, no callback chains, no event systems.
What Changed
Architecture
Before (localStorage-based):
After (metadata-based):
Key Improvements
compaction-request→ backend black-box →compaction-resultin summarycaught-upto ensure aggregator has full historical contextcmuxMetadatadrives all compaction detection, display, and continuation logicBugs Fixed
Issue #1: Continue Message Lost After Edit ✅
Problem: Editing
/compact -c "msg"lost the continue message.Root Cause: When compaction completed, history was replaced with just the summary. The original
compaction-requestmessage (containingcontinueMessage) was deleted.useAutoCompactContinuelooked for it and found nothing.Solution: Extract
continueMessagefrom request before history replacement and store it in summary metadata ascompaction-result. Hook now reads from summary, not request.Issue #2: "streaming..." Instead of "compacting..." After Reload ✅
Problem: After page reload during active compaction, StreamingBarrier showed "streaming..." instead of "compacting...".
Root Cause: Replay timing issue. When
stream-startarrived during replay, aggregator's message cache was still empty (messages buffered separately), soisCompactingcheck failed.Solution: Buffer all stream events (start/delta/end/abort/tool calls) until
caught-upmessage arrives. Process them only after historical messages are loaded. This ensures aggregator always has full context when detecting compaction state.Benefits:
Issue #3: Edit Button Active During Compaction ✅
Problem: Clicking "Edit" during compaction created a stuck state—input populated but disabled, user couldn't type or cancel.
Solution: Disable Edit button during compaction with helpful tooltip explaining how to cancel first. Prevents user from entering broken state.
Issue #4: Duplicate Messages When Editing Compaction ✅
Problem: Editing a compaction message created duplicates instead of replacing.
Root Cause: Compaction command path didn't pass
editMessageId.Solution: Pass
editMessageIdthrough compaction path and clear editing state on success.Files Changed
Deleted
src/components/Messages/PendingUserDraft.tsx(84 lines)src/components/Messages/ChatBarrier/CompactionBarrier.tsx(37 lines)Modified
compaction-resultfrom summary instead of requestcmuxMetadatafor isCompacting detectionCmuxFrontendMetadatadiscriminated unioncmuxMetadatato SendMessageOptionsAdded
Benefits
.find()Testing
Manual testing recommended:
/compactwithout continue message/compact -c "message"with auto-continueGenerated with
cmux