Skip to content

feat(tools): file-change snapshot store with /undo, /rollback, and tool prompt hardening - #433

Merged
jexShain merged 2 commits into
AI-Shell-Team:mainfrom
jexShain:feat/file-snapshot-undo
Aug 6, 2026
Merged

feat(tools): file-change snapshot store with /undo, /rollback, and tool prompt hardening#433
jexShain merged 2 commits into
AI-Shell-Team:mainfrom
jexShain:feat/file-snapshot-undo

Conversation

@jexShain

@jexShain jexShain commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

User-visible Changes

  • New /undo [path] command: undo the most recent AI file change (optional path filter to undo a specific file).
  • New /rollback command: interactive panel to review and restore any prior checkpoint.
  • write_file/edit_file results carry a content tag; a non-undoable write (unreadable/oversized prior) now reports (not undoable: prior content unavailable).
  • File-edit diffs render as centered hunks with bounded context — edits near the end of a large file stay visible instead of being crowded out.

Compatibility

  • No config or storage format changes. The snapshot store is session-scoped and in-memory; it does not persist across shell restarts (both /undo and /rollback descriptions mark "this session").
  • Internal API change: commit_undo_last/commit_undo_last_for now require a snapshot_id argument (self-verifying peek→commit). All callers updated.
  • No breaking changes to existing commands; two new slash commands added.

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace ✅ — incl. new regression tests:
    • snapshot: stale-drift-rejected, fresh-allows-and-records, malformed-tag-rejected, peek-then-failed-io-keeps-history, oversized-prior-skips-rollback, unreadable-prior-skips-rollback
    • diff: tail_edit_beyond_lcs_bound_is_shown, output_never_exceeds_max_lines, distant_hunks_kept_when_changed_fit
    • i18n: slash-command descriptions resolve across all 6 locales
  • cargo build --release --target x86_64-unknown-linux-musl + aish --help smoke ✅
  • Packaging smoke tests ✅

Change Type

  • Feature: /undo, /rollback, undo_edit tool, server-side drift detection
  • Improvement: tool prompt hardening, diff rendering as bounded hunks
  • Bug fix: render_diff lost tail edits beyond the LCS bound; truncation emitted max_lines+1 and could drop later changed hunks

Scope

Implements #205 (检查点/快照/回退). Refs #432 (tool prompt hardening).

Snapshot & undo (#205)

  • /undo [path], /rollback panel, undo_edit tool — all use peek → apply → commit-on-success with snapshot_id verification (a stale peek is rejected rather than consuming the wrong snapshot).
  • read_file stamps a content tag; edit_file enforces is_fresh server-side to reject edits against stale/drifted content even without a model-supplied tag.
  • Memory bounded: MAX_HISTORY=200 FIFO; write_file skips rollback for priors larger than MAX_WRITE_BYTES (mirrors the SIZE_LIMIT gate in read_file/edit_file).

Tool prompt hardening (#432)

  • bash / secure_bash: forbid redirections and file writes.
  • python_exec: forbid open()/pathlib/shutil/os file I/O; guide the AI to print and use write_file/edit_file.
  • channel_bash (SSH remote): unchanged — the only remote write path; remote files are outside the local snapshot scope.

Design notes

  • Defense in depth: prompt soft-constraint (guidance) + is_fresh hard safety net (edit drift detection). The write-creates-new-file path has no is_fresh net (no prior tag), so it relies on prompt guidance.
  • Session-scoped: store is in-memory; descriptions set that expectation.

Closes #205
Refs #432

Summary by CodeRabbit

  • New Features

    • Added /undo to reverse the latest file change.
    • Added /rollback to review and restore earlier file changes.
    • Added snapshot tags and safeguards against editing files that changed after reading.
    • Added undo support for restoring overwritten files and removing newly created files.
    • Improved diff displays with clearer change hunks and context limits.
    • Added localized messages in English, German, Spanish, French, Japanese, and Chinese.
  • Documentation

    • Clarified that dedicated file tools should be used instead of Bash or Python for file modifications.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for the pull request. A maintainer will review it when available.

Please keep the PR focused, explain the why in the description, and make sure local checks pass before requesting review.

Contribution guide: https://github.com/AI-Shell-Team/aish/blob/main/CONTRIBUTING.md

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This pull request description looks incomplete. Please update the missing sections below before review.

Missing items:

  • Summary
  • Scope

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds session-scoped file snapshots, snapshot-aware file tools, /undo and /rollback commands, localized messages, stricter file-operation guidance, and hunk-based diff rendering.

Changes

Snapshot undo and rollback

Layer / File(s) Summary
Snapshot store and restore contracts
crates/aish-tools/src/fs/snapshot_store.rs
Adds tagged snapshots, bounded mutation history, binary-safe restore, undo, rollback, and transactional history commits with tests.
Snapshot-aware file tools
crates/aish-tools/src/{read_file,write_file,edit_file,undo_edit}/*, crates/aish-tools/src/lib.rs, crates/aish-tools/src/{bash,python}/*
File tools share snapshot state. Reads emit tags, edits validate freshness, writes record prior content, and UndoEditTool restores or removes files. Prompts direct file operations to dedicated tools.
Shell integration and localized commands
crates/aish-shell/src/app.rs, crates/aish-shell/src/readline.rs, crates/aish-shell/tests/slash_popup_commands.rs, crates/aish-i18n/locales/*
AishShell wires shared state and implements /undo and interactive /rollback. Tests and six locale files cover the commands and outcomes.

Diff rendering

Layer / File(s) Summary
Hunk-based diff rendering
crates/aish-shell/src/theme.rs
render_diff preserves changed lines, limits context, separates distant regions, and marks hidden content. Tests cover boundary and separated edits.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AishShell
  participant SnapshotStore
  participant FileSystem
  AishShell->>SnapshotStore: select rollback target
  SnapshotStore-->>AishShell: return restore actions
  AishShell->>FileSystem: restore or remove files
  FileSystem-->>AishShell: return disk result
  AishShell->>SnapshotStore: commit successful rollback
Loading

Possibly related PRs

Poem

A rabbit tags each file with care,
Then rolls old changes backward there.
Undo restores, or removes new,
While hunks keep changed lines in view.
Translations guide each command bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The diff-hunk rendering changes in theme.rs are not directly related to the checkpoint and file-restore objective in issue #205. Move the diff-hunk rendering changes to a separate pull request unless they are required for snapshot-based recovery.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements issue #205 with session-scoped snapshots and commands to undo or restore recorded file changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the snapshot store, undo and rollback commands, and prompt hardening added by the pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jexShain
jexShain force-pushed the feat/file-snapshot-undo branch from f9f0542 to 535cdd3 Compare August 6, 2026 03:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
crates/aish-tools/src/fs/snapshot_store.rs (1)

271-287: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider verifying snapshot_id when committing an undo.

UndoResult carries snapshot_id, but commit_undo_last and commit_undo_last_for ignore it. They pop or remove whatever entry is newest at commit time. If any mutation lands between the peek and the commit, the wrong snapshot is consumed and the remembered tag is rewound to the wrong content.

The module doc at lines 379-384 states that the shell drives tools sequentially, so this is currently latent. An id check makes the peek → disk IO → commit protocol self-verifying and removes the dependency on that ordering assumption.

♻️ Proposed id-checked commit variants
+    /// Commit the undo for a specific snapshot id. Returns `None` when the
+    /// id is no longer the entry that would be undone, so a stale peek is
+    /// rejected instead of consuming the wrong snapshot.
+    pub fn commit_undo(&mut self, snapshot_id: u64) -> Option<FileSnapshot> {
+        if self.history.last()?.id != snapshot_id {
+            return None;
+        }
+        self.commit_undo_last()
+    }
+
+    /// Commit the undo for a specific path and snapshot id.
+    pub fn commit_undo_for(&mut self, path: &Path, snapshot_id: u64) -> Option<FileSnapshot> {
+        let key = normalize_path(path);
+        let idx = self.history.iter().rposition(|s| s.path == key)?;
+        if self.history[idx].id != snapshot_id {
+            return None;
+        }
+        let snap = self.history.remove(idx);
+        self.rewind_tag(&snap);
+        Some(snap)
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/fs/snapshot_store.rs` around lines 271 - 287, Update
commit_undo_last and commit_undo_last_for to accept the expected snapshot_id
from UndoResult and verify the selected history entry’s snapshot_id before
removing it. Return None without mutating history or rewinding tags when the ID
does not match, preserving the existing successful-commit behavior.
crates/aish-tools/src/undo_edit/undo_edit.rs (1)

102-156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for path-scoped undo and for the failed-restore-retains-history guarantee.

The test suite covers the no-path overwrite case, the created-file deletion case, and the empty-history error case. It does not cover peek_undo_last_for/commit_undo_last_for (the path argument path), and it does not verify the documented guarantee that a failed apply_to_disk call leaves the history entry intact for a retry.

Add a test that makes the target path unwritable (or otherwise forces apply_to_disk to fail) and asserts the snapshot is still present in the store afterward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aish-tools/src/undo_edit/undo_edit.rs` around lines 102 - 156, Extend
the undo_edit tests with path-scoped coverage using the path argument and the
SnapshotStore APIs peek_undo_last_for and commit_undo_last_for, verifying the
correct snapshot is selected and committed. Add a failed-restore test that
forces apply_to_disk to fail, then assert the corresponding snapshot remains in
the store so the undo can be retried.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aish-shell/src/theme.rs`:
- Around line 510-517: Update render_diff and its line_diff input handling so
edits beyond the 1,000-line resource bound are still detected when the bounded
prefixes match, while preserving the existing limit. Ensure the fallback
includes changed tail regions and renders them correctly, then add a regression
test covering more than 1,000 unchanged leading lines followed by a tail edit.
- Around line 601-624: The sequential truncation after building lines can drop
changed hunks and produce max_lines + 1 output. Update the region-rendering flow
around choose_regions and render_diff_line to account for elision-marker budget
while selecting or emitting regions, preserve all changed lines when they fit
within max_lines, and reserve capacity for the final omission marker when
content is dropped. Add tests covering distant changed hunks and asserting the
rendered output never exceeds max_lines.

In `@crates/aish-tools/src/write_file/write_file.rs`:
- Around line 89-128: Update the successful write response around skip_rollback
in the write_file implementation to append a localized
tools.fs.write_file.not_undoable marker whenever the write is untracked, while
preserving the existing output for tracked writes. Add the corresponding
translation key to every locale file with text explaining that the change is not
undoable because the prior content could not be snapshotted.
- Around line 195-231: Guard the Unix test
write_file_unreadable_prior_skips_rollback by detecting whether the effective
user is root and returning early when it is. Keep the existing permission setup
and assertions unchanged for non-root execution, using the platform-appropriate
UID check already available in the test environment.

---

Nitpick comments:
In `@crates/aish-tools/src/fs/snapshot_store.rs`:
- Around line 271-287: Update commit_undo_last and commit_undo_last_for to
accept the expected snapshot_id from UndoResult and verify the selected history
entry’s snapshot_id before removing it. Return None without mutating history or
rewinding tags when the ID does not match, preserving the existing
successful-commit behavior.

In `@crates/aish-tools/src/undo_edit/undo_edit.rs`:
- Around line 102-156: Extend the undo_edit tests with path-scoped coverage
using the path argument and the SnapshotStore APIs peek_undo_last_for and
commit_undo_last_for, verifying the correct snapshot is selected and committed.
Add a failed-restore test that forces apply_to_disk to fail, then assert the
corresponding snapshot remains in the store so the undo can be retried.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 34094f83-8138-4fd8-b8ff-a9aadbebf1fe

📥 Commits

Reviewing files that changed from the base of the PR and between e970866 and f9f0542.

📒 Files selected for processing (22)
  • crates/aish-i18n/locales/de-DE.yaml
  • crates/aish-i18n/locales/en-US.yaml
  • crates/aish-i18n/locales/es-ES.yaml
  • crates/aish-i18n/locales/fr-FR.yaml
  • crates/aish-i18n/locales/ja-JP.yaml
  • crates/aish-i18n/locales/zh-CN.yaml
  • crates/aish-shell/src/app.rs
  • crates/aish-shell/src/readline.rs
  • crates/aish-shell/src/theme.rs
  • crates/aish-shell/tests/slash_popup_commands.rs
  • crates/aish-tools/src/bash/prompt.rs
  • crates/aish-tools/src/edit_file/edit_file.rs
  • crates/aish-tools/src/edit_file/prompt.rs
  • crates/aish-tools/src/fs/snapshot_store.rs
  • crates/aish-tools/src/lib.rs
  • crates/aish-tools/src/python/prompt.rs
  • crates/aish-tools/src/read_file/prompt.rs
  • crates/aish-tools/src/read_file/read_file.rs
  • crates/aish-tools/src/undo_edit/prompt.rs
  • crates/aish-tools/src/undo_edit/undo_edit.rs
  • crates/aish-tools/src/write_file/prompt.rs
  • crates/aish-tools/src/write_file/write_file.rs

Comment thread crates/aish-shell/src/theme.rs
Comment thread crates/aish-shell/src/theme.rs
Comment thread crates/aish-tools/src/write_file/write_file.rs
Comment thread crates/aish-tools/src/write_file/write_file.rs
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

…ol prompt hardening

Add a per-process SnapshotStore recording write_file/edit_file mutations so
users can recover AI file edits (AI-Shell-Team#205):

- /undo [path]: undo the most recent file change (optional path filter)
- /rollback: interactive panel to restore any prior checkpoint
- undo_edit tool: lets the AI undo its own last edit
- read_file stamps a content tag; edit_file enforces is_fresh server-side to
  reject edits against stale/drifted content (even without a model tag)

Harden tool prompts so the AI uses dedicated file tools instead of bypassing
the snapshot system (AI-Shell-Team#432):

- bash/secure_bash: forbid redirections and file writes
- python_exec: forbid open()/pathlib/shutil/os file I/O; print and use
  write_file instead

write_file caps rollback memory: a prior larger than MAX_WRITE_BYTES is not
tracked (mirrors the SIZE_LIMIT gate in read_file/edit_file); the write still
succeeds, it just isn't undoable.

Closes AI-Shell-Team#205, Refs AI-Shell-Team#432
…text

Split render_diff output into hunks centered on changed lines, each surrounded
by up to DIFF_CONTEXT (3) unchanged lines. Edits near the end of a large file
stay fully visible — leading context no longer crowds them out. The max_lines
cap sheds context first, dropping bare changed lines only on overflow.
@jexShain
jexShain force-pushed the feat/file-snapshot-undo branch from 535cdd3 to 4ef62ad Compare August 6, 2026 05:30
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@jexShain
jexShain merged commit 06f2347 into AI-Shell-Team:main Aug 6, 2026
9 checks passed
@jexShain
jexShain deleted the feat/file-snapshot-undo branch August 6, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: 检查点/快照/分支功能

1 participant