Skip to content

feat(tui): add /undo and /revert commands - #43

Merged
enowdev merged 1 commit into
enowdev:mainfrom
hshinosa:feat/tui-undo-revert
Sep 18, 2026
Merged

enowdev merged 1 commit into
enowdev:mainfrom
hshinosa:feat/tui-undo-revert

Conversation

@hshinosa

Copy link
Copy Markdown
Collaborator

What

Two new TUI slash commands that reuse the dashboard's edit-message primitives:

  • /undo — rollback the most recent user turn (files + messages).
  • /revert — picker over the session's user messages; rollback to any earlier point. Also accepts /revert <message-id> for scripts and copy/paste from the dashboard.

Why

The dashboard's "edit message" flow already lets an operator drop a turn and revert every file the agent touched in it. The TUI had no way to reach that machinery — operators had to leave the terminal, open the dashboard, find the message, and click a button, or hand-edit ~/.antares/checkpoints/ and prune the message log by hand.

UX

Inline confirm block, no modal. y on an empty composer commits, Esc discards, everything else discards and falls through so the keystroke lands normally.

Revert to the last turn?
  3 file(s) to restore (1 created files will be removed).
  1 file(s) edited outside this session will be skipped.
  [revert] internal/agent/agent.go
  [delete] internal/agent/foo_new.go
  [skip  ] README.md
Press y to confirm, any other key to cancel.

/revert uses the same modal picker /model and /theme already use — one keyboard idiom for every selection. Preview label carries the message text (single-line elided at 60 chars); right column shows latest / N turns ago.

Commit path

Files first, then messages, then drop the persisted context summary:

  1. agent.RollbackSince(sessionID, marker, skipExternal=true) — restore snapshots, remove agent-created files, honour "externally changed" (leave user-edited files alone).
  2. store.DeleteMessagesFrom(sessionID, marker) — drop the message and everything after it.
  3. Drop meta.context_compact — its through_seq now points past deleted rows; leaving it would either replay a stale summary or hide live messages.
  4. Reload the transcript from storage — the message log is authoritative, so the composer no longer shows the reply we just deleted server-side.

If file restore fails, the message log stays intact — a half-done state where messages are gone but files are still at their post-turn contents is worse than "nothing happened, try again".

Every primitive is the same one server/handlers_chat.go's handleEditMessage uses, so the dashboard and TUI stay in sync — a fix in the checkpoint layer lands in both surfaces at once.

/redo — intentionally not included

OpenCode and omp implement /redo by recording reverse-ops per tool call into a session journal. Antares has append-only messages plus per-turn checkpoint tarballs; no branch semantics. Adding /redo would need a schema change (session_undo_log table + reverse-op recording per tool call). Left as future work — same conclusion as the "known bugs" gap OpenCode issue #15391 documents.

Files

File LOC Purpose
internal/tui/undo.go 327 (new) Both commands, pending state, commit, transcript reload, previewText
internal/tui/undo_test.go 128 (new) previewText, confirm message shape, cancel state, commands registered
internal/tui/tui.go +5 pending *pendingRevert field
internal/tui/tui.go +22 Key handler intercept for y/Esc after a stage
internal/tui/commands.go +2 Register /undo + /revert in palette

Not touched

  • Dashboard, HTTP handlers, agent, store, checkpoint — all reused as-is.
  • No schema change.
  • No config field.
  • No new dependency.

Verify

  • GOTOOLCHAIN=go1.26.3 go build ./... clean
  • GOTOOLCHAIN=go1.26.3 go vet ./... clean
  • go test ./internal/tui/... ./internal/agent/... ./internal/server/... ./internal/checkpoint/... ./internal/store/... -count=1 — 5 packages green
  • 4 new subtests
  • Live smoke: go run ./cmd/antares tui/undo, /revert, /revert <id> all work; empty-session guards fire the expected notice; cancel path leaves state untouched.

The dashboard's "edit message" flow already lets an operator drop a turn
and revert every file the agent touched in it. The TUI had no way to
reach that machinery — operators had to leave the terminal, open the
dashboard, find the message, and click a button, or hand-edit
~/.antares/checkpoints/ and prune the message log by hand.

/undo stages a rollback of the most recent user turn:

  Revert to the last turn?
    3 file(s) to restore (1 created files will be removed).
    1 file(s) edited outside this session will be skipped.
    [revert] internal/agent/agent.go
    [delete] internal/agent/foo_new.go
    [skip  ] README.md
  Press y to confirm, any other key to cancel.

'y' on an empty composer runs the same three-step commit the dashboard
uses: agent.RollbackSince (files), store.DeleteMessagesFrom (message
log), and drop the persisted context_compact summary whose through_seq
now points past deleted rows. Esc or any other key discards the stage
and falls through so the keystroke lands normally — no modal to
dismiss, no separate confirm widget.

/revert opens the existing modal picker over the session's user
messages, newest first, with a preview label and "N turns ago" hint.
Selecting an entry runs through the same stage/commit path. Also
accepts /revert <message-id> for scripts and copy/paste from the
dashboard.

The dashboard, /undo, and /revert share every primitive
(agent.PreviewChangesSince / RollbackSince, store.DeleteMessagesFrom)
so their behaviour stays in sync — a fix to the checkpoint layer lands
in both surfaces at once. TUI-side additions total ~330 lines behind
the existing Model + block/picker infrastructure.

/redo is intentionally out of scope: OpenCode and omp implement it by
recording reverse-ops per tool call into a session journal, which
Antares does not have. Adding it would need a schema change; left as
future work.

Tests cover the picker-label renderer (previewText), the confirm
message shape (counts, tags, always-present cancel hint), the cancel
path (state cleared, idempotent), and that both commands stay in the
palette registry.
@enowdev
enowdev merged commit e1189cc into enowdev:main Sep 18, 2026
3 checks passed
@enowdev

enowdev commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Merged into main via 4dd05ab, with two fixes applied on top for defects found during review. Both were verified against the real checkpoint store before and after.

1. /revert <message-id> with an unknown id could destroy the session log. The id went straight to PreviewChangesSince with no validation. touchedSince matches nothing for an unrecognised marker, so PreviewSince returns (empty, nil) — indistinguishable from "this turn changed no files". The operator saw "No file changes to revert — only the chat history will be trimmed", pressed y, RollbackSince silently no-opped, and DeleteMessagesFrom returned ErrNotFound, surfacing "files restored, but trimming messages failed" when neither had happened. (DeleteMessagesFrom is correctly session-scoped, so no rows were actually touched — the damage was the false report, not data loss.) Now the id is resolved against the message log first and rejected before anything is staged; assistant ids are rejected too, since truncating from one would cut the log at the wrong point.

2. The "N skipped" count was always zero. commitPending read len(res.Failed), but RestoreSince drops externally-changed files with a bare continue and never records them in Failed. Confirmed by probe: agent writes a file, user edits it outside the session, revert → Restored=[] Deleted=[] Failed=map[]. The confirm block correctly warned "1 file(s) edited outside this session will be skipped", then the summary silently dropped it. The count now comes from the staged preview, and genuine restore failures are reported separately rather than conflated with skips.

Three regression tests added in internal/tui/undo_guard_test.go, backed by a real in-memory store rather than a fake.

The design itself is good — reusing handleEditMessage's exact primitives, files-before-messages ordering, and dropping context_compact all match the dashboard, so the two surfaces stay in sync. Agreed on leaving /redo out.

Verified: go build, go vet, full go test ./..., and go test -race over the affected packages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants