feat: interactive rewind command to truncate conversations and revert file changes#3344
Open
rohithmahesh3 wants to merge 19 commits into
Open
feat: interactive rewind command to truncate conversations and revert file changes#3344rohithmahesh3 wants to merge 19 commits into
rohithmahesh3 wants to merge 19 commits into
Conversation
Implements an interactive rewind command that allows users to roll back a conversation to any earlier message, discarding all subsequent messages, tool results, and usage data. Changes: - crates/forge_domain/src/context.rs: Add Context::truncate() and Context::format_messages_for_rewind() methods - crates/forge_main/src/model.rs: Add Rewind variant to AppCommand enum - crates/forge_main/src/ui.rs: Add on_slash_rewind handler with interactive conversation picker, message list display, and prompt for target index Usage: :rewind [conversation-id] - With an ID: rewinds the specified conversation - Without an ID: rewinds the active conversation, or shows a picker - Displays indexed message previews and prompts for truncation point - Saves the truncated conversation and shows confirmation
Changes rewind UX to only display user messages instead of all message types (system, assistant, tool results, images). This makes it clearer for the user to decide where to rewind to. - format_messages_for_rewind() now returns Vec<(usize, String)> with (full_index, display_string) tuples, filtered to only User role messages, numbered 1..N (1-indexed) - Added truncate_to_user_message(nth_user) which finds the Nth (0-indexed) user message and truncates everything after it - Updated handler to use 1-indexed user message selection
When rewinding a conversation to a previous user message, any file changes made during the truncated portion are now automatically reverted via the snapshot system. Implementation details: - Added modified_files: Vec<String> field to ToolResult — populated by forge_service in ToolRegistry::call() by extracting file_path from ToolCallArguments for Write/Patch/Remove tools - Added Context::modified_files_after(index) — collects all file paths that were modified by tool results after a given message index - Added undo_snapshot to the API trait + ForgeAPI impl — takes a list of file paths, calls SnapshotRepository::undo_snapshot for each, falling back to fs::remove_file if no snapshot exists (new file) - Updated on_slash_rewind to call the new API method before truncating - Updated all test fixtures with the new modified_files field Files changed: 15 files, +139 lines
Replaces the text-based numeric input prompt with a full TUI selector (arrow keys + Enter) for choosing the rewind target message. Same interactive pattern as the conversation picker, provider/model selectors. - Displays user messages as selectable rows in the nucleo-backed fuzzy search TUI (ForgeWidget::select_rows) - Cursor starts on the last message by default (initial_raw) so the common no-op case is just Enter - Up/down arrow keys, PageUp/Down, fuzzy search filtering all work - Enter selects, Esc/Ctrl+C cancels - Removed 32 lines of text-display + input-prompt boilerplate
The user message list in :rewind now shows just the plain text content (e.g. 'write a python script...') instead of '[1] User: write a python script...'. The header row (# Message) is removed since there's only one data column.
User messages are often wrapped in <task>...</task> or <feedback>...</feedback> tags from the event context template. Strip these before showing in the rewind TUI selector so the previews show clean text. - Added strip_xml_tags() to forge_domain::xml which removes <tag>...</tag> pairs for known event tags - Applied in Context::format_messages_for_rewind()
The rewind selector was showing auto-injected messages (externally modified files notification, piped input, resume todos) alongside actual user-typed messages because they all use Role::User. Fix: use the 'droppable' field as the discriminator. User-typed messages have droppable=false while system-generated user messages have droppable=true. Also exclude non-Text variants (Tool, Image). - format_messages_for_rewind: filter by Text+User+!droppable - user_message_count: same filter for consistency
Bug tailcallhq#1 - truncate_to_user_message used has_role(Role::User) which counts auto-generated droppable messages (externally modified files notification, piped input, resume todos). This caused truncation to the wrong index, losing user-typed messages and skipping file reversion. Bug tailcallhq#2 - cut_index in on_slash_rewind (ui.rs) used has_role(Role::User) for the same reason, making modified_files_after look at the wrong slice of messages. Files that should have been reverted were missed. Both now use the same filter as format_messages_for_rewind(): ContextMessage::Text(msg) with msg.role == Role::User && !msg.droppable
…on order in rewind - Add to the list of modifying tools in - Remove BTreeSet deduplication in rewind to preserve operation order - Iterate modifications in reverse order for correct undo sequence - Use 'file change(s)' terminology to reflect per-operation tracking Co-Authored-By: ForgeCode <noreply@forgecode.dev>
- Truncation semantics: Changed truncate_to_user_message to exclude the target user message (was inclusive), giving cleaner rewinds. - File creation undo: Snapshot non-existent files with a .none marker so undo can delete files that were created after the rewind point. - Path detection: Extract modified file paths from tool output XML tags for accurate absolute paths; fall back to argument-based extraction. - Message preview cleanup: Added clean_user_prompt to extract feedback tag content or strip meta tags for cleaner display. - Shell integration: Added :rewind command to the zsh plugin, writing the rewound message into the user's buffer for editing/resubmission. - Snapshot coordination: Always snapshot before fs_write (not just when file exists) and before plan_create to support full undo coverage. - CLI command: Added conversation rewind subcommand with optional conversation ID. Co-Authored-By: ForgeCode <noreply@forgecode.dev>
…ed_files_from - Formatting cleanup across forge_api, tool_registry, context, info, ui, and plan_create (line wrapping, closure simplification, whitespace removal). - Add fallback dynamic path extraction in modified_files_from by parsing XML tags from tool output, for backward compatibility with older conversations where modified_files may be empty. Co-Authored-By: ForgeCode <noreply@forgecode.dev>
- Extract `resolve_conversation_id` in UI to eliminate duplicated
conversation resolution logic between clone and rewind commands
- Add `MessageEntry::is_user_message()` helper and use it across
truncate, format, and count operations for DRYer context code
- Introduce `extract_modified_files_from_output()` in xml module to
centralize file path extraction from XML tags, and use it in both
tool_registry and context
- Use let-chains for more idiomatic Rust conditionals
- Introduce `REWIND_PREVIEW_MAX_LEN` constant instead of magic number
- Fix typo in test fixture ('receipe' -> 'recipe')
- Add tests for `extract_tag_content` with duplicate closing tags and
`extract_modified_files_from_output`
Co-Authored-By: ForgeCode <noreply@forgecode.dev>
- Add support for <task> tag extraction in prompt cleaning (xml.rs) - Strip terminal_context tags from prompts to prevent clutter - Skip conversation picker when --conversation-id is provided (ui.rs) - Make :rewind and :clone fall back to current conversation ID when no target is given (conversation.zsh)
- Call zle reset-prompt directly in rewind action instead of relying on caller to handle it (conversation.zsh) - Add OSC133 status emission for rewind action in the dispatcher to ensure proper terminal state management (dispatcher.zsh)
- Move modified_files extraction and fallback logic inside the Ok(output) block to prevent false positives from error results (tool_registry.rs) - Fix dedup check to use msg_modified instead of files to prevent cross-tool de-duplication of the same file (context.rs) - Add tests for duplicate modified files across tools and dedup within a single tool result (context.rs)
…slicing - Use safe char-based truncation for rewind preview (context.rs:746) - Use .iter().skip() instead of range slicing in modified_files_from (context.rs:769) - Use .get() instead of direct indexing in rewind handler (ui.rs:2731)
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
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.
Summary
Adds an interactive
:rewindcommand that lets users truncate a conversation to an earlier message via a TUI message selector and revert file changes made by tools in the discarded messages.Changes
cli.rs— AddedConversationCommand::Rewindvariantui.rs— Interactive TUI message picker, file revert orchestration, refactoredresolve_conversation_idshared helpercontext.rs— Truncation helpers (truncate,truncate_to_user_message), modified file tracking (modified_files_from,modified_files_after), dedup fix,format_messages_for_rewindxml.rs— XML tag extraction utilities (extract_tag,extract_attribute,extract_modified_files_from_output,clean_user_prompt,strip_xml_tags)tool_registry.rs+result.rs— Track files modified by each tool call (modified_filesfield onToolResult)service.rs— Handle snapshots for newly created files (.nonemarker), revert file creation on undofs_write.rs,plan_create.rs— Always snapshot before write (capture non-existent state):rewindaction handler with TUI integration,:clonefallback to current conversation ID, OSC133 status emission for dispatchermodified_filesstored/restored in conversation SQL recordsTesting
cargo clippy— 0 warnings