Skip to content

Add restore-to-message arrow on user messages#3596

Open
azizmejri1 wants to merge 18 commits into
dyad-sh:mainfrom
azizmejri1:copy-restore-to-message
Open

Add restore-to-message arrow on user messages#3596
azizmejri1 wants to merge 18 commits into
dyad-sh:mainfrom
azizmejri1:copy-restore-to-message

Conversation

@azizmejri1

Copy link
Copy Markdown
Collaborator

No description provided.

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a feature to restore the app's codebase and database to the state prior to a specific user message, creating a new chat with the preceding messages. Key changes include the implementation of the restoreToMessageVersion IPC handler, extraction of the core revert logic into revertCodebaseToVersion, UI updates in ChatMessage to trigger the restore, and a new E2E test. Feedback highlights three main areas for improvement: resolving an unhandled promise rejection in revertCodebaseToVersion due to an unawaited promise, performing the codebase revert before database insertion to avoid orphaned chats on failure, and disabling all restore buttons globally during a restore operation to prevent concurrent requests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/ipc/handlers/version_handlers.ts Outdated
Comment thread src/ipc/handlers/version_handlers.ts Outdated
Comment thread src/components/chat/ChatMessage.tsx
@github-actions github-actions Bot added the needs-human:review-issue ai agent flagged an issue that requires human review label Jun 5, 2026

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude review: 2 inline finding(s).

Comment thread src/ipc/handlers/version_handlers.ts
Comment thread package-lock.json Outdated
@dyad-assistant

dyad-assistant Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge
Recommendation: ready

Well-structured PR that adds a "restore to message" feature with good refactoring of the existing revertCodebaseToVersion logic into a shared helper. The IPC contract uses typed Zod schemas, the renderer uses TanStack Query mutations, the confirmation dialog protects against accidental clicks, and all message columns are correctly copied. E2E test coverage is solid for the happy path.

Issues Summary

Severity File Issue
🟡 MEDIUM src/ipc/handlers/version_handlers.ts:426 No chat-app ownership check in restoreToMessageVersion
🟡 MEDIUM package-lock.json:3 Stale branch causes version field conflict with main

🟡 MEDIUM — No chat-app ownership check in restoreToMessageVersion
(src/ipc/handlers/version_handlers.ts:426)

The restoreToMessageVersion handler fetches the chat by chatId alone (where: eq(chats.id, chatId)) without verifying chat.appId === appId. If a renderer bug sends a mismatched appId and chatId, the handler would create a new chat under the wrong app with messages from a different app's history, and attempt to revert the codebase to a commit hash that may not exist in the target app's git repo. Adding if (chat.appId !== appId) throw new DyadError(...) after the chat fetch would close this gap. Note: the existing revertVersion handler has the same pattern, so this is a pre-existing gap — not introduced by this PR, just carried forward.

🟡 MEDIUM — Stale branch causes version field conflict with main
(package-lock.json:3)

The PR changes package-lock.json version from 1.2.0-beta.1 to 1.3.0, but main is currently at 1.3.0-beta.2. This is an artifact of the branch being based on an older commit. GitHub's 3-way merge will likely flag a conflict on these lines, but if it auto-resolves, the version would be set to 1.3.0 (stable), which may be premature. A rebase onto main would resolve this.

🟢 Low Priority Notes (2 items)
  • First message restore creates an empty chat - Clicking restore on the very first user message produces a new chat with zero messages, reverting to initialCommitHash. This works correctly but may surprise users. Consider hiding the button on the first message or adding a note in the dialog. (src/components/chat/ChatMessage.tsx)

  • Restore button invisible on touch devices - The button uses opacity-0 group-hover:opacity-100, which means it is not discoverable on touch/mobile. This matches the existing hover-reveal pattern in the component, so it's consistent, but worth noting for accessibility. (src/components/chat/ChatMessage.tsx)


Generated by Dyadbot persona-based code review

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@azizmejri1

Copy link
Copy Markdown
Collaborator Author

🤖 Claude Code Review Summary

PR Confidence: 4/5

All five reviewer-raised concerns were addressed with scoped code changes; unit tests (1723), lint, and typecheck all pass. Not 5/5 only because the new restore-to-message flow is exercised by E2E (not unit) tests, which were not run in this pass.

Unresolved Threads

Thread Rationale Link

No unresolved threads

Resolved Threads

Issue Rationale Link
Un-awaited retryOnLocked for preserve-branch cleanup (HIGH) The surrounding try/catch could not catch a rejection from a non-awaited promise. Replaced it with a .catch() attached directly to the promise, so the cleanup error is logged instead of surfacing as an unhandled rejection — keeping the call fire-and-forget. View
Orphaned chat created before revertCodebaseToVersion (MEDIUM) Reordered so the revert runs before inserting the new chat; if the revert throws (Git/Neon), no partial chat is left behind. A warningMessage still means the codebase was reverted, so the new chat is still created in that case. View
Per-message restore loading state (MEDIUM) Added a shared mutation key + React Query useIsMutating so all restore buttons disable while any restore is in flight, preventing concurrent restores. Per Principle #6: Delightful, the spinner stays scoped to the clicked button rather than lighting up every button. View
No chat-app ownership check in restoreToMessageVersion (MEDIUM) Added a chat.appId === appId defense-in-depth check that throws DyadError(..., DyadErrorKind.Validation) on mismatch (DyadErrorKind has no Forbidden variant; Validation is the right bad-input semantic and is telemetry-filtered). View
Stale package-lock.json version field (MEDIUM) The branch is already on the latest origin/main, so a rebase wouldn't help. Instead reverted the unintended 1.2.0-beta.1 -> 1.3.0 bump so this feature PR no longer touches the release version at all. View
Product Principle Suggestions

No suggestions — the principles were clear enough for every decision this run (Principle #6 cleanly covered the only UX judgment call).


🤖 Generated by Claude Code

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude review: 1 inline finding(s).

Comment thread src/hooks/useVersions.ts Outdated
@dyad-assistant

dyad-assistant Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge
Recommendation: ready

Issues Summary

Severity File Issue
🟡 MEDIUM src/hooks/useVersions.ts:136 Missing chat list cache invalidation after creating new chat

MEDIUM: Missing chat list cache invalidation -- The onSuccess handler for restoreToMessageMutation invalidates version, branch, and problem queries, but does not invalidate queryKeys.chats.all. The backend handler creates a new chat row, and every other chat-creation path in the codebase (e.g. ChatInput.tsx:548, useChatMode.ts:124, useChats.ts:19) invalidates queryKeys.chats.all afterward. Without it, the sidebar chat list won't show the newly created chat until the next stale-time refresh. The user is navigated to the new chat and can interact with it, so this is not a blocker, but the sidebar UX will be inconsistent. Fix: add await queryClient.invalidateQueries({ queryKey: queryKeys.chats.all }); in the onSuccess handler.

🟢 Low Priority Notes (5 items)
  • revertCodebaseToVersion return type is imprecise - The function returns { successMessage: string; warningMessage: string } where warningMessage defaults to "". A more precise type like { successMessage: string; warningMessage?: string } would better communicate intent. (src/ipc/handlers/version_handlers.ts)

  • useVersions called per ChatMessage creates N mutation instances - Each ChatMessage mounts its own useVersions(appId) hook, creating independent mutation instances. For a 50-message chat, that's 50 mutation instances. The useIsMutating pattern correctly handles cross-instance coordination, and query fetches are deduplicated, so this is a performance nit rather than a bug. Pre-existing pattern. (src/components/chat/ChatMessage.tsx)

  • No cross-disable between revert and restore mutations - If a revertVersion operation is in-flight (from the version timeline), restore-to-message buttons remain enabled, and vice versa. The backend withLock(appId) ensures safety, but the UI doesn't visually prevent both from being triggered. Unlikely to occur in practice. (src/hooks/useVersions.ts)

  • E2E test does not verify sidebar reflects new chat - The test verifies URL change, message content, and file reversion, but doesn't check that the sidebar/chat-list shows the new chat. Given the missing cache invalidation above, this gap means the sidebar bug wouldn't be caught by CI. (e2e-tests/restore_to_message.spec.ts)

  • Neon branch deletion error handling improved (positive note) - The refactoring correctly replaces a nested try/catch around an un-awaited retryOnLocked promise (which could never catch the rejection) with a .catch() handler. This is a genuine bug fix for potential unhandled promise rejections. (src/ipc/handlers/version_handlers.ts)

What looks good

  • Security model is solid: Zod input validation on the IPC contract, app-existence check, chat-existence check, chat-belongs-to-app ownership validation (defense in depth), message-existence check, and DyadError with appropriate DyadErrorKind throughout.
  • Concurrency safety: withLock(appId) prevents concurrent codebase mutations.
  • Commit hash resolution logic: Three-tier fallback (following assistant's sourceCommitHash -> preceding assistant's commitHash -> chat.initialCommitHash) handles edge cases gracefully, with a safe degradation path when no hash can be determined.
  • Message copy is complete: All schema fields are copied to the new chat, correctly excluding auto-generated id and setting chatId to the new chat.
  • UX patterns are well-applied: AlertDialog uses Base UI wrappers, useIsMutating correctly disables all restore buttons globally while one is in-flight, the button is hidden by default and appears on hover via the existing group class hierarchy, and the confirmation dialog prevents accidental clicks.
  • Clean refactoring: Extracting revertCodebaseToVersion reduces duplication between revertVersion and restoreToMessageVersion handlers while preserving identical behavior.
  • E2E test is thorough for the happy path: covers chat forking, message filtering, and file content reversion with appropriate retry-based assertions.

Generated by Dyadbot persona-based code review

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@azizmejri1

Copy link
Copy Markdown
Collaborator Author

🤖 Claude Code Review Summary

PR Confidence: 5/5

All review threads from trusted authors are resolved; the only outstanding comment was a missing chat-list cache invalidation, now fixed, and lint/type-check pass.

Unresolved Threads

No unresolved threads

Resolved Threads

Issue Rationale Link
Missing chat list cache invalidation after restoreToMessageVersion creates a new chat Added invalidateQueries({ queryKey: queryKeys.chats.all }) to the onSuccess handler in useVersions.ts, matching every other chat-creation path so the sidebar refreshes immediately. Cited Principle #4: Transparent Over Magical — the new chat should be visible right away, not a hidden side effect. View
Pre-existing threads from earlier review (unawaited retryOnLocked, orphaned-chat ordering, global restore-disable state, chat-app ownership check, package-lock version bump) Already addressed and resolved in commit 45b385d before this run. View
Product Principle Suggestions

No suggestions — the principles were clear enough for every decision in this run.


🤖 Generated by Claude Code

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude review: 2 inline finding(s).

Comment thread src/ipc/handlers/version_handlers.ts
Comment thread src/ipc/handlers/version_handlers.ts
@dyad-assistant

dyad-assistant Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge
Recommendation: ready

This PR adds a well-implemented "restore to message" feature that lets users fork their chat and revert their app's codebase to the state before a specific user message. The implementation is solid across all layers:

  • IPC/Security: The new restoreToMessageVersion handler properly validates inputs (app exists, chat exists, message exists), includes defense-in-depth checks (chat belongs to app), uses DyadError with appropriate DyadErrorKind values, and holds withLock(appId) for concurrency safety.
  • Refactoring: The revertCodebaseToVersion extraction is behavior-preserving, with one minor improvement (the un-awaited deleteProjectBranch now uses .catch() instead of an ineffective try/catch).
  • Renderer: Uses TanStack Query useMutation with a shared mutationKey and useIsMutating to correctly disable all restore buttons while one is in flight. AlertDialog uses the Base UI wrapper, not Radix.
  • Message copy completeness: All non-auto-generated messages schema columns are explicitly copied to the new chat.
  • E2E test: Covers the golden path with proper assertions on chat navigation, message content, and file state.

Issues Summary

Severity File Issue
🟡 MEDIUM src/ipc/handlers/version_handlers.ts:485 Soft-failure returns original chatId as newChatId, causing misleading navigation
🟡 MEDIUM src/ipc/handlers/version_handlers.ts:508 Forked chat is created without a title, making it hard to identify in the sidebar
🟢 Low Priority Notes (3 items)
  • Unrelated version bump in lockfile - package-lock.json bumps from 1.2.0-beta.1 to 1.3.0. This is the lockfile catching up to package.json on main but clutters the diff; confirm this is expected or rebase. (package-lock.json)

  • E2E test clicks opacity-0 button without hover - The restore button has opacity-0 group-hover:opacity-100. The test clicks via getByTestId without hovering first. This works because Playwright doesn't enforce opacity checks, but adding a hover() before click would better mirror real usage and be resilient to future pointer-events-none additions. (e2e-tests/restore_to_message.spec.ts)

  • Behavior improvement in refactor is worth noting - The original revertVersion handler wrapped the un-awaited retryOnLocked(deleteProjectBranch(...)) in a try/catch that could never catch async rejections. The refactored revertCodebaseToVersion correctly uses .catch() on the promise chain. This is an improvement, not a regression. (src/ipc/handlers/version_handlers.ts)


Generated by Dyadbot persona-based code review

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@azizmejri1

Copy link
Copy Markdown
Collaborator Author

🤖 Claude Code Review Summary

PR Confidence: 4/5

All review comments are addressed; changes are minimal, lint-clean, and type-check passes — only knocked from 5 because the e2e suite (restore_to_message.spec.ts) wasn't run locally this round.

Unresolved Threads

Thread Rationale Link
No unresolved threads

Resolved Threads

Issue Rationale Link
Soft-failure returned the original chatId as newChatId, causing misleading navigation to the same chat Added a distinct { warningMessage } response member (no newChatId) for the no-version case; ChatMessage.tsx now only navigates when "newChatId" in result. Per Principle #4: Transparent Over Magical, navigation now reflects what actually happened. View
Forked chat created without a title, indistinguishable in the sidebar Forked chat now copies the original title with a (restored) suffix (null when the original is untitled). Per Principle #4: Transparent Over Magical and Principle #6: Delightful, the new chat is identifiable. View
Product Principle Suggestions

No suggestions — the existing principles (#4 and #6) gave clear guidance for both decisions.


🤖 Generated by Claude Code

@dyad-assistant

dyad-assistant Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge
Recommendation: ready

Well-structured PR that adds a "restore to message" feature with an undo arrow on user messages. The feature forks the chat (and optionally reverts the codebase) to the state before a given message. The implementation is solid: proper withLock serialization, defense-in-depth validation (chat-belongs-to-app check), transactional chat + messages insert, thoughtful handling of dirty working trees from cancelled streams, and correct DyadError/DyadErrorKind usage throughout.

The refactoring of revertCodebaseToVersion into a shared function is clean and fixes a pre-existing bug (useless try/catch around a non-awaited deleteProjectBranch promise, now correctly using .catch()). The stream completion promise pattern in chat_stream_handlers.ts correctly ensures cancelled streams fully unwind before the restore touches the working tree. E2E tests cover the three key scenarios (fork+revert, fork-only, first-message edge case) and verify the original chat remains intact.

Issues Summary

Severity File Issue
🟡 MEDIUM src/ipc/handlers/chat_stream_handlers.ts:1991 Completion promise deleted from map before being resolved
🟡 MEDIUM src/components/chat/ChatMessage.tsx:278 AlertDialog rendered per user message creates many hidden DOM trees
🟡 MEDIUM src/ipc/types/version.ts:109 chatId and messageId accept negative, zero, and float values
🟢 Low Priority Notes (5 items)
  • & vs & inconsistency in dialog text - The AlertDialogDescription uses & while the AlertDialogAction button uses & for the ampersand in "Restore code & fork chat". Both render identically in JSX, but normalizing to & everywhere would be more consistent. (src/components/chat/ChatMessage.tsx)

  • setShowRestoreConfirm(false) is redundant - handleRestoreToMessage starts by setting setShowRestoreConfirm(false), but both action buttons are AlertDialogAction which auto-close the dialog on click. The explicit reset is harmless but unnecessary. (src/components/chat/ChatMessage.tsx)

  • warningMessage concatenation without separator - In revertCodebaseToVersion, if both the Neon DB restore and Supabase function deployment fail, warningMessage uses += without inserting a space or newline, producing run-on text. This is pre-existing behavior preserved by the refactor. (src/ipc/handlers/version_handlers.ts)

  • Loading spinner is per-instance while disabled state is global - isRestoringToMessage drives the spinner but comes from the per-component mutation instance, so only the clicked message shows a spinner while other messages' buttons are disabled with a static icon. Acceptable but could be clearer with a shared "which message is restoring" state. (src/components/chat/ChatMessage.tsx)

  • Message field list must be manually synced with schema - The messagesBefore.map() in restoreToMessageVersion explicitly lists message columns to copy. The code includes a comment warning about this, which is good, but any future schema change that adds a column could silently drop data in forked chats. (src/ipc/handlers/version_handlers.ts)


Generated by Dyadbot persona-based code review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 353 9 1 7

Summary: 353 passed, 9 failed, 1 flaky, 7 skipped

Failed Tests

🍎 macOS

  • cloud_sandbox.spec.ts > cloud sandbox runtime mode runs previews
    • Error: expect(locator).toBeVisible() failed
  • engine.spec.ts > smart auto should send message to engine
    • Error: expect(string).toMatchSnapshot(expected) failed
  • local_agent_advanced.spec.ts > local-agent - get mcp tool schema
    • Error: expect(received).toBe(expected) // Object.is equality
  • local_agent_advanced.spec.ts > local-agent - enable nitro
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth connects and calls a tool
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth manual (non-DCR) connects and calls a tool
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth disable-and-retry when server doesn't support OAuth
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp.spec.ts > mcp - call calculator
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp.spec.ts > mcp - call calculator via http
    • Error: expect(received).toBe(expected) // Object.is equality

📋 Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/cloud_sandbox.spec.ts \
  e2e-tests/engine.spec.ts \
  e2e-tests/local_agent_advanced.spec.ts \
  e2e-tests/mcp_oauth.spec.ts \
  e2e-tests/mcp.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • visual_editing.spec.ts > discard changes (passed after 1 retry)

📊 View full report

@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@azizmejri1

Copy link
Copy Markdown
Collaborator Author

🤖 Claude Code Review Summary

PR Confidence: 5/5

All six review threads were valid, addressable feedback (or a factually-inapplicable concern); each was fixed or explained, the branch typechecks, and lint/format pass.

Unresolved Threads

No unresolved threads

Resolved Threads

Issue Rationale Link
streamCompletions map not cleared in before-quit handler Added streamCompletions.clear() alongside the existing activeStreams/partialResponses cleanup to match the pattern. View
Completion promise deleted before being resolved Swapped the finally-block order so resolveCompletion() runs before streamCompletions.delete(), keeping a post-abort reader safe. View
No validation that targetCommitHash exists before reverting Added assertVersionExists (mirroring the revertVersion flow) before restore; a stale/GC'd hash now returns a user-friendly warning instead of an opaque git error. View
Message-copy field list must be manually kept in sync with schema Added a compile-time exhaustiveness guard (_assertAllMessageColumnsHandled) so a new messages column is a type error until classified as copied or excluded — per Principle #2: Productionizable (maintainability). View
chatId/messageId accept negative, zero, and float values Tightened the Zod schema to z.number().int().positive() for both fields. View
AlertDialog per user message creates many hidden DOM trees Not applicable: @base-ui/react does not mount the Portal/Popup while closed, so no hidden DOM trees exist per message. Per Principle #4: Transparent Over Magical, keeping restore state local to each message beats a lifted controller. Resolved with explanation. View
Product Principle Suggestions

No suggestions


🤖 Generated by Claude Code

@dyad-assistant

dyad-assistant Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge
Recommendation: ready

✅ No issues found by persona-based review.

Review Notes

Scope: 78 files changed (1049+/147-). ~65 are aria snapshot updates adding the new "Restore to this point" button. The core changes span 7 source files introducing a new IPC handler, React hook mutation, UI component, and 3 E2E tests. All diffs were reviewed in full.

Architecture & Correctness:

  • The streamCompletions promise pattern in chat_stream_handlers.ts correctly ensures cancelStream awaits in-flight tool/file writes before returning, preventing a race between stream cancellation and the subsequent gitStageToRevert in the restore flow.
  • The withLock(appId) serialization in the backend handler prevents concurrent version mutations on the same app.
  • The version resolution logic (forward-walk for sourceCommitHash, backward-walk for commitHash, then initialCommitHash) covers the key edge cases: last user message, first user message, messages predating sourceCommitHash tracking, and chats with no assistant responses.
  • The revertCodebaseToVersion refactor correctly preserves all behavior from the original revertVersion handler. The new dirty-tree handling (commit uncommitted changes before reverting) is a genuine improvement that handles cancelled streams leaving partial file writes.
  • The fire-and-forget deleteProjectBranch now correctly uses .catch() instead of try/catch for the unawaited promise rejection.

Security / IPC Boundary:

  • Input validation via RestoreToMessageParamsSchema constrains chatId/messageId to positive integers at the IPC boundary.
  • The handler validates chat ownership (chat.appId !== appId), preventing cross-app access.
  • The handler uses DyadError with appropriate DyadErrorKind for all expected error paths.

Database:

  • The transaction wrapping chat + messages insertion ensures atomicity (no orphaned empty chats on failure).
  • The compile-time _assertAllMessageColumnsHandled guard will produce a type error when new columns are added to the messages schema without being classified, preventing silent data loss in forked chats.
  • The usingFreeAgentModeQuota reset to false on copied messages is correct — without it, copied messages would double-count against the free-agent quota.

UX:

  • Two-step flow (icon click -> confirmation dialog) prevents accidental restores.
  • isAnyVersionMutationPending (via useIsMutating) globally disables all restore/revert buttons across components during any version mutation, preventing conflicting operations.
  • Toast messaging distinguishes full success, partial success (code restored but DB restore failed), and warning-only (no version determinable).
  • The "(restored)" title suffix with dedup logic prevents sidebar clutter from repeated restores.

Testing:

  • Three E2E tests cover the primary flows: restore+fork, fork-only, and first-message edge case. All use testSkipIfWindows consistent with existing test patterns.
🟢 Low Priority Notes (4 items)
  • Completion promise ordering in finally block - resolveCompletion() runs after clearPendingMcpConsentsForChat() in the stream handler's finally block. If clearPendingMcpConsentsForChat threw synchronously, the completion promise would never resolve and cancelStream would hang. In practice this function is a simple Map operation that won't throw, but placing resolveCompletion() first would be more defensive. (src/ipc/handlers/chat_stream_handlers.ts:1996)

  • Silent overwrite of streamCompletions entry - streamCompletions.set(req.chatId, ...) silently overwrites if two streams run concurrently for the same chat. The UI prevents this and activeStreams provides a guard, but a logger.warn on overwrite would aid debugging. (src/ipc/handlers/chat_stream_handlers.ts:296)

  • Redundant setShowRestoreConfirm(false) - AlertDialogAction renders as AlertDialogPrimitive.Close which automatically calls onOpenChange(false). The explicit setShowRestoreConfirm(false) in handleRestoreToMessage is therefore redundant (React deduplicates the update). Harmless but could confuse future readers. (src/components/chat/ChatMessage.tsx:224)

  • Forward-walk version resolution - The forward-walk for sourceCommitHash correctly breaks on the first assistant message, even if its sourceCommitHash is null. This is semantically correct: the immediately next assistant message is the one that responded to the target user message, so checking later assistant messages would give the wrong version. The backward-walk fallback correctly compensates when the field is missing. (src/ipc/handlers/version_handlers.ts:730)


Generated by Dyadbot persona-based code review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 354 8 0 7

Summary: 354 passed, 8 failed, 7 skipped

Failed Tests

🍎 macOS

  • engine.spec.ts > smart auto should send message to engine
    • Error: expect(string).toMatchSnapshot(expected) failed
  • local_agent_advanced.spec.ts > local-agent - get mcp tool schema
    • Error: expect(received).toBe(expected) // Object.is equality
  • local_agent_advanced.spec.ts > local-agent - enable nitro
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth connects and calls a tool
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth manual (non-DCR) connects and calls a tool
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth disable-and-retry when server doesn't support OAuth
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp.spec.ts > mcp - call calculator
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp.spec.ts > mcp - call calculator via http
    • Error: expect(received).toBe(expected) // Object.is equality

📋 Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/engine.spec.ts \
  e2e-tests/local_agent_advanced.spec.ts \
  e2e-tests/mcp_oauth.spec.ts \
  e2e-tests/mcp.spec.ts

📊 View full report

azizmejri1 and others added 17 commits July 9, 2026 01:54
- version_handlers: attach .catch() to the un-awaited preserve-branch
  deletion so its async rejection is handled instead of becoming an
  unhandled promise rejection (the surrounding try/catch could not catch it)
- restoreToMessageVersion: revert the codebase before creating the new chat
  so a failed revert no longer leaves an orphaned, partially-created chat
- restoreToMessageVersion: add a chat.appId === appId ownership check
  (defense in depth) so a mismatched (appId, chatId) can't act on the wrong app
- ChatMessage/useVersions: disable all restore buttons while any
  restore-to-message is pending (via useIsMutating) to prevent concurrent
  restores; only the initiating button shows the spinner
- package-lock.json: drop the unintended 1.2.0-beta.1 -> 1.3.0 version bump
  so this feature PR no longer touches the release version

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Invalidate chats query cache after restoreToMessageVersion creates a new
  chat, so the sidebar chat list refreshes immediately instead of the new
  chat appearing as a ghost entry until the next refresh

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Return a distinct response shape (no newChatId) when no version can be
  determined for restore-to-message, so the renderer stays on the current
  chat instead of "navigating" to the same chat (misleading no-op)
- Carry over the original chat's title (with "(restored)" suffix) to the
  forked chat so it's identifiable in the sidebar instead of untitled

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the new "Restore to this point" button to user-message snapshots
across 70 specs. Also update restore_to_message.spec.ts to expect 3
restore buttons instead of 2, since importing the "minimal" fixture
auto-generates an AI_RULES.md user message before the test's prompts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove duplicate error toast on restore failure: drop meta.showErrorToast
  from restoreToMessageMutation since handleRestoreToMessage already shows the
  error (and also covers the cancelStream call)
- Wrap forked chat + messages inserts in a transaction so a failed messages
  insert can't leave an orphaned "(restored)" chat
- Walk forward past intervening user messages when resolving the target commit
  hash so consecutive user messages don't skip the responding assistant

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Disable all version-modifying buttons (message restore arrows and the
  version-pane revert button) while any restore-to-message or revert-version
  operation is in flight, via a unified isAnyVersionMutationPending flag
- Show a compound 'Code restored, but: ...' toast when a restore-to-message
  reverts the code but a secondary step fails, so the partial success is clear

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- version_handlers.ts: move successMessage "(including database)" inside the
  try block so it only reports success when the Neon restore actually succeeded
- version_handlers.ts: fall back to a bare "(restored)" title for forks of
  untitled chats so they remain identifiable in the sidebar

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add focus-visible:opacity-100 to the restore-to-message button so
  keyboard-only users can see it when focused
- Make the pre-restore auto-commit message more descriptive so the
  preserved uncommitted changes are easy to identify in version history

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Don't carry over usingFreeAgentModeQuota when forking a chat on restore, to avoid double-counting free-agent quota
- Bail out with a warning instead of creating an empty '(restored)' chat when restoring to the first message
- Strip existing '(restored)' suffix before appending to prevent title pile-up on repeated restores
- Run restore onSuccess query invalidations concurrently so navigation isn't delayed
- Hide restore button on cancelled user prompts
- E2E: verify the original chat is left intact after a restore

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Restoring to the first message now forks an empty chat at version 1
  instead of erroring with "Cannot restore before the first message".
- The per-message restore button is always visible (no longer hover-only).
- The restore dialog now offers two vertical options: "Restore code & fork
  chat" (previous behavior) and "Fork chat only" (forks the chat without
  reverting the codebase/database). Fork-only anchors the new chat's
  initialCommitHash to the current HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restore the libc: ["glibc"] block that was inadvertently removed on this
branch; it's unrelated to this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The revertVersion handler no longer inlines the Neon restore and
Supabase re-deploy logic — upstream extracted it into
revertCodebaseToVersion(), which the handler already calls. The rebase
had re-introduced the old inline copy, referencing now-const
successMessage/warningMessage and an undefined currentCommitHash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Use getDatabaseRestoreWarning in revertCodebaseToVersion's Neon catch so
  the retention-window explanation is preserved for both the revert and
  restore-to-message flows; fix the copy-pasted log message
- Add AlertDialogDescription to the restore confirmation dialog explaining
  the consequences of each action (fixes a11y warning)
- Make cancelStream await the in-flight stream handler's completion so a
  cancelled turn's partial file writes settle before a subsequent revert
- Document that the copied-message field list must stay in sync with the
  messages table schema

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Clear streamCompletions map in the before-quit handler to match the
  established stream-cleanup pattern
- Resolve the stream completion promise before deleting it from the map
  so a post-abort reader still observes a settled promise
- Validate the resolved targetCommitHash exists (via assertVersionExists)
  before restoring the codebase, returning a user-friendly warning instead
  of an opaque git error for stale/GC'd hashes
- Add a compile-time exhaustiveness guard so new messages columns can't be
  silently dropped when forking a chat
- Constrain restoreToMessage chatId/messageId to positive integers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

@dyad-assistant dyad-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude review: 1 inline finding(s).

| "role"
| "content"
| "approvalState"
| "sourceCommitHash"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM

Fork-only mode uses hardcoded "main" ref instead of current branch HEAD

When restoreCodebase is false (fork-only mode), forkInitialCommitHash is computed via getCurrentCommitHash({ path: appPath, ref: "main" }). If the user's app is on a non-main branch, this captures main's commit hash rather than the current branch HEAD. Since initialCommitHash tracks the code state when a chat started, the forked chat would have an incorrect baseline, making "changes since chat started" diffs wrong. The regular createChat handler (chat_handlers.ts:41) correctly uses getCurrentCommitHash without a ref (defaulting to HEAD), and the existing revertCodebaseToVersion helper also resolves the current branch dynamically.

💡 Suggestion: Replace ref: "main" with ref: "HEAD" (or omit the ref argument entirely to use the default) so the fork-only chat records the current branch HEAD: await getCurrentCommitHash({ path: appPath }).catch(() => targetCommitHash).

@dyad-assistant

dyad-assistant Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge
Recommendation: ready

This PR adds a well-implemented "restore to message" feature with two modes: revert code + fork chat, or fork chat only. The implementation is solid across the stack: proper Zod-validated IPC contract, defense-in-depth validation (chat-app ownership check), compile-time schema drift guard for message copying, DB transaction for atomic chat+messages creation, and stream completion tracking to prevent races with cancelled streams.

Issues Summary

Severity File Issue
🟡 MEDIUM src/ipc/handlers/version_handlers.ts:843 Fork-only mode uses hardcoded "main" ref instead of current branch HEAD

MEDIUM detail: When restoreCodebase is false, forkInitialCommitHash calls getCurrentCommitHash({ ref: "main" }). If the app is on a non-main branch, this records the wrong baseline for the forked chat. The regular createChat handler correctly defaults to HEAD. Fix: drop the ref: "main" argument so the default "HEAD" is used.

🟢 Low Priority Notes (3 items)
  • Restore button always visible - The undo arrow has no invisible/group-hover class, so it appears on every user message at all times. With many messages this could be visually noisy. Consider showing on hover only (with a group/group-hover pattern) once discoverability is established. (src/components/chat/ChatMessage.tsx)

  • No E2E test for mid-stream restore - The three E2E tests cover the main flows well, but there is no test verifying that clicking restore while a response is actively streaming correctly cancels the stream before forking. The code handles this path, but it is untested. (e2e-tests/restore_to_message.spec.ts)

  • Dirty working tree commit is new behavior for existing revert flow - revertCodebaseToVersion now commits uncommitted changes (via gitAddAll + gitCommit) before reverting. This is a behavior change for the existing revertVersion handler too (not just the new restore-to-message path). It handles a real edge case (cancelled streams leaving partial writes) and is an improvement, but worth noting as it silently creates a version commit the user didn't explicitly request. (src/ipc/handlers/version_handlers.ts)

Positive observations:

  • The _assertAllMessageColumnsHandled compile-time guard is an excellent pattern that prevents silent data loss when the messages schema evolves.
  • Stream completion tracking (streamCompletions map) correctly prevents race conditions between cancel and restore without adding complexity to the normal streaming path.
  • The useIsMutating approach for cross-component mutation state is well-motivated and prevents confusing concurrent operations.
  • Input validation via z.number().int().positive() on IDs catches bad input at the IPC boundary.
  • The chat-app ownership check (chat.appId !== appId) is good defense in depth against renderer bugs.

Generated by Dyadbot persona-based code review

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 185 4 1 6

Summary: 185 passed, 4 failed, 1 flaky, 6 skipped

Failed Tests

🍎 macOS

  • local_agent_advanced.spec.ts > local-agent - enable nitro
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth connects and calls a tool
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth manual (non-DCR) connects and calls a tool
    • Error: expect(received).toBe(expected) // Object.is equality
  • mcp_oauth.spec.ts > mcp - oauth disable-and-retry when server doesn't support OAuth
    • Error: expect(received).toBe(expected) // Object.is equality

📋 Re-run Failing Tests (macOS)

Copy and paste to re-run all failing spec files locally:

npm run e2e \
  e2e-tests/local_agent_advanced.spec.ts \
  e2e-tests/mcp_oauth.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • mention_app.spec.ts > mention app (with pro) (passed after 1 retry)

📊 View full report

In fork-only mode (restoreCodebase=false), forkInitialCommitHash was
computed with ref: "main", capturing main's HEAD rather than the current
branch HEAD. This gave forked chats an incorrect baseline, making
"changes since chat started" diffs wrong on non-main branches. Omit the
ref so getCurrentCommitHash defaults to HEAD, matching the createChat
handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

1 similar comment
@wwwillchen

Copy link
Copy Markdown
Collaborator

@BugBot run

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

Labels

needs-human:review-issue ai agent flagged an issue that requires human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants