Skip to content

fix(desktop): optimistic rewind + checkpoint boundary corrections#4272

Merged
esengine merged 5 commits into
main-v2from
fix/4269-rewind-clean
Jun 13, 2026
Merged

fix(desktop): optimistic rewind + checkpoint boundary corrections#4272
esengine merged 5 commits into
main-v2from
fix/4269-rewind-clean

Conversation

@esengine

Copy link
Copy Markdown
Owner

Lands #4269 (by @CVEngineer66) and tightens a few rough edges in the optimistic-rewind flow. Closes #4269.

What #4269 fixes (commits 1–4, @CVEngineer66)

  • CheckpointHasBoundary: after compaction the cpBound[turn] key can survive while its boundary points past the truncated log. Now validates boundary <= len(Messages) — exactly the guard Rewind itself applies — so the UI button state matches what a rewind will accept.
  • checkpoint.List(): the in-progress turn's files no longer participate in CanCode propagation (its Paths are nilled), while the turn still shows as a rewind point.
  • RestoreCode notice: only emitted when files actually changed (no more "0 restored, 0 removed").
  • CheckpointsForTab: propagates the cumulative unique file set backward so the UI shows how many files a code rewind would really affect.
  • Optimistic rewind: clicking rewind truncates the UI, refills the composer, and shows an undo banner immediately; the real Go Rewind is deferred until the next send. Undo restores the full items with no Go call.

Follow-on tightening (commit 5)

  • Failure path: if the deferred rewind throws, leave the optimistic state cleared (full transcript shown, matching the intact Go conversation) instead of re-truncating to a view the Go side no longer matches.
  • scope=both: defer the dock/project refresh until the rewind actually commits, so the file dock no longer flashes the un-rewound state.
  • Undo clears the refilled composer.
  • Deterministic Files: sort CheckpointsForTab's cumulative list (Go map order is unordered).
  • Drop the unused onDismiss prop from UndoRewindBanner.

Validation

  • go build ./... (desktop + core), go vet, go test ./internal/checkpoint/ ./internal/control/ pass.
  • tsc --noEmit (after wails generate module) + check:css pass.

wufengfan and others added 5 commits June 13, 2026 00:44
- UndoRewindBanner: shows above composer after non-fork rewind, displaying
  turns rolled back and files changed, with two-click confirm undo button
- Silent fork before rewind: creates a backup tab that undo switches to,
  fully restoring the pre-rewind session
- rewindSignal prop: Transcript scrolls to the last user message after
  rewind so the user sees exactly where they rewound to
- CSS: .undo-rewind styles matching the composer area
- Locales: undoRewind keys in en/zh/zh-TW
CheckpointHasBoundary previously only checked whether the cpBound[turn]
key existed.  After compaction the key remains but its value is stale
(boundary > len(Messages)), so the UI showed the rewind button as
enabled despite the backend refusing the operation with 'conversation
rewind unavailable for turn N: the conversation was compacted past
this point'.

Now CheckpointHasBoundary also validates boundary <= len(Messages),
so compacted turns get CanConversation=false and the frontend button
is correctly disabled.
Frontend:
- Optimistic rewind: immediate UI truncation, undo via restore,
  deferred Go Rewind on actual send.  Composer refill with
  rewound-to prompt.  Code-only rewind skips truncation.
- displayItems memo: switches between full and truncated items
  based on rewindState.
- send wrapper: commits any pending rewind before sending.

Go backend:
- CheckpointHasBoundary: also validate boundary <= len(Messages),
  so compacted turns correctly show CanConversation=false.
- List(): nil cur.Paths so in-progress file snapshots don't
  pollute CanCode backward-propagation.
- RestoreCode: only emit notice when files actually changed
  (skip confusing '0 file(s) restored, 0 removed').
actionMeta('code') showed checkpoint.files.length (the current turn's
own files), but RestoreCode(fromTurn) restores all files from turn
fromTurn onward.  Now CheckpointsForTab propagates the cumulative
unique file set backwards so the UI shows the true number of files
that would be affected.
Follow-on cleanup to the optimistic rewind in this PR:
- On a deferred-rewind failure, leave the optimistic state cleared (full
  transcript restored) instead of re-truncating to a view the Go side no
  longer matches; still skip the send.
- Defer the dock/project refresh for scope=both until the rewind actually
  commits, so the file dock no longer flashes the un-rewound state.
- Clear the refilled composer when the user undoes the rewind.
- Sort CheckpointsForTab's cumulative Files (Go map order is nondeterministic).
- Drop the unused onDismiss prop from UndoRewindBanner.
@esengine esengine requested a review from SivanCola as a code owner June 13, 2026 07:51
@github-actions github-actions Bot added desktop Wails desktop app (desktop/**) agent Core agent loop (internal/agent, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development labels Jun 13, 2026
@esengine esengine merged commit f6aba59 into main-v2 Jun 13, 2026
14 checks passed
@esengine esengine deleted the fix/4269-rewind-clean branch June 13, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant