Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Undo and diff

efr takes its own snapshots of your files while the agent works. With them, efr shows what each turn changed, and efr undo takes a turn back. This works in every registered project and in $SCRATCH, with or without git. efr keeps the snapshots in its own store and never writes the project's .git to take one.

What a turn changed

While the turn runs

  • When the agent writes a file with a file tool, the call shows the diff: its first 20 lines, then a dim … 12 more lines. render.diff_lines sets the count, and 0 shows no diff.

  • When a shell command changes files in the project or in $SCRATCH, one dim line under its result names them:

    changed src/a.rs +3 -1 · deleted old.rs · new notes.md (+2 more)
    

These lines say what changed while the call ran, not who changed it. A file that your editor saved at the same time shows too.

At the end of the turn

A turn that changed files ends with a line such as 3 files changed, +24 -7. When a changed file is not code, the line names up to two groups:

9 files changed, +635 -44 (evals +503, tests +77)

The built-in groups are:

  • generated: lock files such as Cargo.lock and package-lock.json, *.snap files, and files below fixtures/;
  • tests: files below tests/, test/, __tests__/ or spec/, and names such as a_test.go or a.spec.ts;
  • docs: *.md files and files below docs/;
  • code: every other file.

You can add your own groups in config.toml. Your groups match first, in the order of the file:

[diff.groups]
evals = ["evals/", "*.eval.toml"]

A glob without / matches the name of a file in any directory. A glob that ends in / matches every file below a directory of that name. Other globs match the whole path from the project root.

efr diff

efr diff prints the whole diff of the last turn of this terminal's conversation into your scrollback.

CommandWhat it shows
efr diffThe diff of the last turn.
efr diff --statA table of the groups, then each file with its counts.
efr diff --stat --allThe totals of every turn of the conversation that was not undone.
efr diff --turn <id>The diff of another turn.
efr diff --conversation <id>The last turn of another conversation.
$ efr diff --stat
code   +1  -1
docs   +1
total  +2  -1

changed src/main.rs  +1 -1
new     notes.md     +1
2 files changed, +2 -1

In a pipe, efr diff writes the plain diff, for git apply or a pager. An ignored file such as .env shows in the list, but efr does not show its contents.

efr undo

efr undo takes back the newest turn of this terminal's conversation that is not undone. It shows what it will do, and then asks:

$ efr undo
undo turn 0193...: change a.txt and add notes.md
restore     a.txt
trash       notes.md
skip        b.txt (changed after the turn; --force overwrites it)
not covered .env.large (the snapshot left it out)
ref         main 1a2b3c4 -> 5d6e7f8
your git index is unchanged; git status can show staged changes
undo? y = yes, n = no
undone: 1 file restored, 1 file moved to ~/.local/share/efr/undo-trash/u-0193.../, 1 ref moved

The rows of the preview:

RowMeaning
restoreefr writes the file back as it was before the turn. The current file goes to the undo trash first.
trashThe turn made the file. efr moves it to the undo trash.
deleteefr removes a link or an empty entry that the turn made, after a copy in the undo trash.
commandefr runs a command that undoes a change, such as crontab for your crontab.
skipThe file changed after the turn. --force overwrites it.
not coveredThe snapshot left the file out, such as a large ignored file. Undo never touches it.
refA branch, a tag or HEAD that the turn moved goes back.

What undo does and does not do

  • Undo acts only on the files that the turn's own calls changed. A change that you made between two calls stays.
  • Every file that undo overwrites or removes goes to the undo trash first: ~/.local/share/efr/undo-trash/<undo-id>/. So you can get them back.
  • Undo moves back the branches, tags, refs/stash and HEAD that the turn moved. Your git index stays as it is.
  • Undo is a stack. You cannot undo a turn while a newer turn of the conversation is not undone, unless you add --force.
  • An undo that did not complete, such as at a stop of efrd, goes on at the next efr undo.

Options

OptionWhat it does
--listList the turns, and which are undone or stopped. Change nothing.
--turn <id>Undo this turn.
--conversation <id>Undo a turn of another conversation.
--dry-runShow the preview. Change nothing.
--yes, -yUndo with no question. Without a terminal, efr undo needs it.
--forceAlso overwrite a file that changed after the turn, and undo a turn below a newer one.
--redoTake back the newest undo.

When only --force can act on the rows of the preview, efr undo changes nothing and says so:

efr undo changed nothing: 1 path acts only with --force; efr undo --force acts on it

When undo refuses

Undo refuses while one of these runs in the conversation:

  • a turn, a compaction or another undo;
  • a command that still runs after its timeout;
  • a background job. A job of another conversation that can write in the same project blocks undo too. Stop it first: efr jobs stop j3.

A prompt that you send during an undo waits for it.

When a job ends, its changes join the turn that started it, and the undo of that turn needs --force. Background jobs has more.

Files outside the project

In auto, before the agent writes a file outside the project, such as ~/.zshrc, efr copies it into its store. In full mode, efr copies such a file when the command line names it. In manual and cautious, efr makes no copy. After a copy, the turn ends with a line such as:

changed ~/.zshrc; efr undo reverts it

When a call can write a whole directory outside the project, efr copies the directory first, up to snapshot.home_max_files (2000) files and snapshot.home_max_mib (256 MiB). A larger directory gets no copy, and the end of the turn says so.

Undo in each mode

Undo works for a turn of every mode that has a snapshot. The hint efr undo reverts this turn shows only after an auto turn.

In manual and cautious, a command runs in your own shell, and its writes outside the project are not covered. The preview of such a turn says so.

In auto, a delete in the project, such as git reset --hard or rm -r build, runs with no question when efr's snapshot holds every file that it can touch, because efr undo brings them back. The turn then says:

changed 4 files (+12 -3) in <project>; efr undo reverts this turn

shred, git clean -x and a file that the snapshot left out still go to the reviewer. Undo has the full rules.

A phone cannot undo. The agent has no undo tool, and only you can change the undo.* and snapshot.* settings.

Settings

SettingDefaultWhat it sets
undo.enabledtruefalse turns undo off. In auto, deletes then go to the reviewer or to you.
snapshot.enabledtruefalse takes no snapshot: no changed files, no efr diff, no undo.
snapshot.max_file_mib10Files above this size stay out of a snapshot.
snapshot.ignored"small"Which ignored files a snapshot takes: none, or small ones such as .env.
snapshot.max_files20000A project with more files gets no snapshot.
snapshot.keep_turns50The turns of each conversation whose snapshots stay.
snapshot.max_age_days30Days with no snapshot before efr deletes the store of a project.
snapshot.mirror_max_mib512The most MiB of git objects that efr copies, so undo can move refs back. 0: no ref undo.
render.diff_lines20The lines of a file tool's diff that a call shows.
diff.groups{}Your own groups of changed files.

config.toml reference lists every key.