Skip to content

feat: interactive rewind command to truncate conversations and revert file changes#3344

Open
rohithmahesh3 wants to merge 19 commits into
tailcallhq:mainfrom
rohithmahesh3:impl/interactive-rewind
Open

feat: interactive rewind command to truncate conversations and revert file changes#3344
rohithmahesh3 wants to merge 19 commits into
tailcallhq:mainfrom
rohithmahesh3:impl/interactive-rewind

Conversation

@rohithmahesh3
Copy link
Copy Markdown
Contributor

Summary

Adds an interactive :rewind command 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 — Added ConversationCommand::Rewind variant
  • ui.rs — Interactive TUI message picker, file revert orchestration, refactored resolve_conversation_id shared helper
  • context.rs — Truncation helpers (truncate, truncate_to_user_message), modified file tracking (modified_files_from, modified_files_after), dedup fix, format_messages_for_rewind
  • xml.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_files field on ToolResult)
  • service.rs — Handle snapshots for newly created files (.none marker), revert file creation on undo
  • fs_write.rs, plan_create.rs — Always snapshot before write (capture non-existent state)
  • Shell plugin:rewind action handler with TUI integration, :clone fallback to current conversation ID, OSC133 status emission for dispatcher
  • Persistencemodified_files stored/restored in conversation SQL records

Testing

  • All existing tests pass (700+ tests, 90 suites, 0 failures)
  • New tests for rewind flow, undo, XML parsing, snapshot edge cases, modified file dedup
  • cargo clippy — 0 warnings

rohithmahesh3 and others added 16 commits May 15, 2026 19:13
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 <[email protected]>
- 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 <[email protected]>
…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 <[email protected]>
- 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 <[email protected]>
- 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)
@github-actions github-actions Bot added the type: feature Brand new functionality, features, pages, workflows, endpoints, etc. label May 16, 2026
rohithmahesh3 and others added 3 commits May 16, 2026 13:40
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature Brand new functionality, features, pages, workflows, endpoints, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant