Formal verification cases for the timeline-of-restore-points UX in AIChatPanel.
- Initial PUC (snapshot 0): appears once per session before the first edit tool indicator. Always shows "Restore to this point".
- Summary card (latest): shows "Undo" (when
_undoAppliedis false) or "Restore to this point" (when_undoAppliedis true). - Summary card (not latest): always shows "Restore to this point".
- After any restore/undo is clicked,
_undoApplied = trueand ALL buttons become "Restore to this point" until the next AI response creates new edits. - The clicked restore point shows "Restored" text with green highlight styling. Clicking a different restore point moves the "Restored" indicator to that one.
- All restore/undo buttons are disabled during AI streaming and re-enabled when the response completes.
_snapshots[0] = initial state (original files, before any AI edits)
_snapshots[1] = after R1 edits
_snapshots[2] = after R2 edits
...
_snapshots[]: flat array of{ filePath: hash|null }snapshots.getSnapshotCount() > 0replaces the old_initialSnapshotCreatedflag._pendingBeforeSnap: per-file pre-edit tracking during current response (dedup guard for first-edit-per-file + file list forfinalizeResponse)
_undoApplied: whether undo/restore has been clicked on any card (UI control for button labels)
[User: "fix bugs"]
[── Restore to this point ──] <- initial PUC (snapshot 0), session-first only
[Claude: "I'll fix..."]
[Edit file1.js]
[Edit file2.js]
[Summary: 2 files changed | Undo] <- snapshot 1
[User: "also refactor"]
[Claude: "Refactoring..."]
[Edit file1.js]
[Summary: 1 file changed | Undo] <- snapshot 2 (snapshot 1 becomes "Restore to this point")
recordFileBeforeEdit(filePath, previousContent, isNewFile): tracks pre-edit state, back-fills all existing snapshotscreateInitialSnapshot(): pushes empty{}as snapshot 0, returns index 0. Must be called beforerecordFileBeforeEditso the back-fill populates it.getSnapshotCount(): returns_snapshots.length(replacesisInitialSnapshotCreated())finalizeResponse(): builds after-snapshot from_snapshots[last]+ current doc content, pushes it, returns index (or -1)restoreToSnapshot(index, callback): applies_snapshots[index]to files, callscallback(errorCount)reset(): clears all state for new session
_$msgs(): live DOM query helper — returns$(".ai-chat-messages")to avoid stale cached$messagesreference (see Implementation Notes)_undoApplied: local module state — reset tofalsein_appendEditSummary()(afterfinalizeResponse()) and_newSession(); set totruein_onRestoreClick()and_onUndoClick()_onToolEdit(): on first edit per response, creates initial snapshot (if none) then records pre-edit state. Inserts initial PUC. Diff toggle only (no per-edit undo)._appendEditSummary(): callsfinalizeResponse(), resets_undoApplied, creates summary card with "Undo" or "Restore to this point" button_onUndoClick(afterIndex): sets_undoApplied, resets all buttons to "Restore to this point", restores toafterIndex - 1, highlights target element as "Restored", scrolls to it_onRestoreClick(snapshotIndex): sets_undoApplied, resets all buttons to "Restore to this point", restores to the given snapshot, marks clicked element as "Restored"_setStreaming(streaming): disables/enables all restore buttons during AI streaming
- R1 edits A: "v0" -> "v1", edits B: "b0" -> "b1"
- Snapshots: [0: {A:v0, B:b0}], [1: {A:v1, B:b1}]
- Initial PUC appears (snapshot 0), summary card shows "Undo" (snapshot 1)
- Click "Undo" on summary -> files revert to snapshot 0 (A=v0, B=b0)
- Scroll to initial PUC, highlighted green, button says "Restored". Summary says "Restore to this point"
- Click "Restore to this point" on summary (snapshot 1) -> files forward to A=v1, B=b1
- Summary now says "Restored", initial PUC says "Restore to this point"
- R1: A "v0"->"v1", R2: A "v1"->"v2"
- Snapshots: [0: {A:v0}], [1: {A:v1}], [2: {A:v2}]
- Card 1 shows "Restore to this point", card 2 shows "Undo"
- Click "Undo" on card 2 -> A="v1" (snapshot 1), card 1 highlighted with "Restored"
- All other buttons become "Restore to this point"
- Same setup as Case 2
- Click "Restore to this point" on initial PUC (snapshot 0) -> A="v0"
- Initial PUC shows "Restored", all others show "Restore to this point"
- R1: A "v0"->"v1", R2: A "v1"->"v2", R3: A "v2"->"v3"
- Snapshots: [0: {A:v0}], [1: {A:v1}], [2: {A:v2}], [3: {A:v3}]
- Click "Restore to this point" on card 1 (snapshot 1) -> A="v1", card 1 shows "Restored"
- Click "Restore to this point" on card 2 (snapshot 2) -> A="v2", card 2 shows "Restored", card 1 back to "Restore to this point"
- Click "Restore to this point" on initial PUC (snapshot 0) -> A="v0"
- R1: A "a0"->"a1", R2: B "b0"->"b1"
- Snapshots: [0: {A:a0}], [1: {A:a1}], [2: {A:a1, B:b1}]
- Back-fill: when B is first seen in R2, snapshot 0 and 1 get B:b0 added
- Click initial PUC (snapshot 0) -> A=a0, B=b0
- Click card 1 (snapshot 1) -> A=a1, B=b0 (B not yet edited)
- Click card 2 (snapshot 2) -> A=a1, B=b1
- R1 creates A (null -> "new"), R2 edits A: "new"->"edited"
- Snapshots: [0: {A:null}], [1: {A:new}], [2: {A:edited}]
- Click initial PUC (snapshot 0) -> A deleted (hash=null)
- Click card 1 (snapshot 1) -> A="new" (re-created)
- Click card 2 (snapshot 2) -> A="edited"
- R1 edits A, R2 creates B
- Snapshots: [0: {A:a0}], [1: {A:a1}], [2: {A:a1, B:new}]
- Back-fill: snapshot 0 and 1 get B:null
- Click initial PUC (snapshot 0) -> A=a0, B deleted
- Click card 1 (snapshot 1) -> A=a1, B deleted (null)
- Click card 2 (snapshot 2) -> A=a1, B=new
- R1 edits A. Click "Undo" ->
_undoApplied = true, all buttons "Restore to this point" - User sends new message, R2 edits B
_undoAppliedresets to false viafinalizeResponse()- New summary card shows "Undo", previous cards show "Restore to this point"
- R1 only reads files, no edits
- No initial PUC inserted, no summary card, no restore buttons
_onCompletefires,_appendEditSummary()callsfinalizeResponse()with partial edits. Works identically to a complete response.
- User sends message, AI starts streaming with edits
- Initial PUC and all summary card buttons have
disabledattribute set - Clicking them does nothing (
_isStreamingguard in handlers) - When streaming completes,
_setStreaming(false)re-enables all buttons
The cached $messages jQuery variable (set in _renderChatUI()) can become stale after SidebarTabs.addToTab() reparents the panel. DOM queries via the stale reference silently fail — mutations apply to a detached node instead of the visible DOM.
Fix: _$msgs() helper returns $(".ai-chat-messages") (live DOM query). Used in all deferred operations: _onRestoreClick, _onUndoClick, _setStreaming (button disable/enable), _sendMessage (highlight removal), _appendEditSummary (previous button update), and PUC insertion in _onToolEdit.
The cached $messages is still used for synchronous operations during rendering (appending messages, streaming updates) where it remains valid.
- Reload Phoenix, open AI tab
- Ask Claude to edit a file (two changes)
- Verify initial PUC appears before the first Edit tool indicator
- Verify summary card with "Undo" button appears after response completes
- Verify all restore buttons are disabled during streaming, enabled after
- Click "Undo" -> verify file reverts, scroll to initial PUC, highlighted green with "Restored" text
- Verify all other buttons now show "Restore to this point"
- Click "Restore to this point" on summary card -> verify file returns to edited state, summary shows "Restored", PUC shows "Restore to this point"
- Ask Claude to make another edit (second response)
- Verify first summary card says "Restore to this point", second says "Undo"
- Click "Undo" on second -> verify files revert to state after first response, first card highlighted with "Restored"
- Click "Restore to this point" on any card -> verify files match that snapshot, clicked card shows "Restored"
- Ask Claude a question (no edits) -> verify no PUC or restore buttons appear
- Start new session -> verify all state cleared