Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Difference so far-
- [WIP] Async input

Planned -
- [o] Undo/ Redo controll
- [X] Undo/ Redo controll
- [o] Background tasks
- [o] Always visible prompt and enque pending tasks
- [e] Parallel tasks
Expand Down
100 changes: 100 additions & 0 deletions UNDO_FEATURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Undo System

The undo system allows you to revert the last interaction and restore the conversation to its previous state.

## How It Works

1. **Automatic Snapshots**: Before each user interaction, Paws automatically creates a snapshot of the conversation context.
2. **Snapshot History**: Up to 50 snapshots are stored per conversation to allow multiple undo operations.
3. **Snapshot Cleanup**: When the limit is reached, the oldest snapshots are automatically removed to maintain performance.

## Usage

### Undo Command

```
/undo
```

Use the `/undo` command to revert the last interaction. This will:
- Restore the conversation context to the state before the last interaction
- Remove all messages, tool calls, and results from that interaction
- Display a confirmation message showing what was undone

### Examples

```
> Can you add a new feature to the codebase?
< [AI response with code changes]

> /undo
Undone: Can you add a new feature to the codebase?
[Conversation reverted to state before the feature request]
```

### Multiple Undo Operations

You can call `/undo` multiple times to step back through your conversation history:

```
> First question
< Answer 1

> Second question
< Answer 2

> Third question
< Answer 3

> /undo
Undone: Third question
[Back to after Answer 2]

> /undo
Undone: Second question
[Back to after Answer 1]
```

### When Undo is Not Available

If there are no snapshots to undo (i.e., at the start of a conversation), you'll see:

```
> /undo
No more actions to undo
```

## Technical Details

- **Snapshot Limit**: 50 snapshots per conversation
- **Storage**: Snapshots are persisted with the conversation in the database
- **Performance**: Snapshots are lightweight and contain only the conversation context state
- **Summary**: Each snapshot includes a brief summary (first 100 characters of the user message) for display purposes

## Implementation

The undo system is implemented through:

1. **Domain Model** (`Conversation` struct):
- `context_history: Vec<ContextSnapshot>` - stores snapshot history
- `save_snapshot(summary)` - creates and saves a snapshot
- `undo()` - reverts to the previous snapshot
- `can_undo()` - checks if undo is available

2. **Orchestrator** (in `paws_app`):
- Automatically creates snapshots before processing each user interaction

3. **Services** (`ConversationService`):
- `undo_conversation(&conversation_id)` - performs the undo operation

4. **UI** (`SlashCommand::Undo`):
- `/undo` command to trigger undo from the interactive prompt

## Future Enhancements

Potential future improvements:
- Redo functionality to reverse an undo
- Named snapshots for bookmarking important conversation states
- Snapshot browsing to see and select from history
- Configurable snapshot limit
- Differential snapshots to reduce memory usage for large conversations
12 changes: 12 additions & 0 deletions crates/paws_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ pub trait API: Sync + Send {
/// Returns an error if the operation fails
async fn delete_conversation(&self, conversation_id: &ConversationId) -> Result<()>;

/// Undoes the last interaction in a conversation by reverting to the previous snapshot.
///
/// # Arguments
/// * `conversation_id` - The ID of the conversation to undo
///
/// # Returns
/// `true` if the undo was successful, `false` if there were no snapshots to undo
///
/// # Errors
/// Returns an error if the conversation doesn't exist or the operation fails
async fn undo_conversation(&self, conversation_id: &ConversationId) -> Result<bool>;

/// Compacts the context of the main agent for the given conversation and
/// persists it. Returns metrics about the compaction (original vs.
/// compacted tokens and messages).
Expand Down
4 changes: 4 additions & 0 deletions crates/paws_api/src/paws_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ impl<A: Services, F: CommandInfra + EnvironmentInfra + SkillRepository + AppConf
self.services.delete_conversation(conversation_id).await
}

async fn undo_conversation(&self, conversation_id: &ConversationId) -> anyhow::Result<bool> {
self.services.undo_conversation(conversation_id).await
}

async fn execute_shell_command(
&self,
command: &str,
Expand Down
28 changes: 28 additions & 0 deletions crates/paws_app/src/orch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ impl<S: AgentService> Orchestrator<S> {
// TODO: Move into app.rs
let title = self.generate_title(model_id.clone());

// Save initial snapshot before starting the interaction
// This allows undoing the entire interaction
let snapshot_summary = extract_user_message_summary(&event);
self.conversation.save_snapshot(snapshot_summary);

while !should_yield {
// Set context for the current loop iteration
self.conversation.context = Some(context.clone());
Expand Down Expand Up @@ -418,3 +423,26 @@ impl<S: AgentService> Orchestrator<S> {
}
}
}

/// Extracts a brief summary from an event's user message for snapshot purposes.
///
/// # Arguments
/// * `event` - The event containing the user message
///
/// # Returns
/// A string summary, truncated to 100 characters
fn extract_user_message_summary(event: &Event) -> String {
event
.value
.as_ref()
.and_then(|v| v.as_user_prompt())
.map(|prompt| {
let summary = prompt.as_str().trim();
if summary.len() > 100 {
format!("{}...", &summary[..97])
} else {
summary.to_string()
}
})
.unwrap_or_else(|| "User interaction".to_string())
}
12 changes: 12 additions & 0 deletions crates/paws_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ pub trait ConversationService: Send + Sync {

/// Permanently deletes a conversation
async fn delete_conversation(&self, conversation_id: &ConversationId) -> anyhow::Result<()>;

/// Undoes the last interaction by reverting to a previous snapshot
///
/// # Returns
/// `true` if undo was successful, `false` if there were no snapshots to undo
async fn undo_conversation(&self, conversation_id: &ConversationId) -> anyhow::Result<bool>;
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -573,6 +579,12 @@ impl<I: Services> ConversationService for I {
.delete_conversation(conversation_id)
.await
}

async fn undo_conversation(&self, conversation_id: &ConversationId) -> anyhow::Result<bool> {
self.conversation_service()
.undo_conversation(conversation_id)
.await
}
}
#[async_trait::async_trait]
impl<I: Services> ProviderService for I {
Expand Down
Loading