feat: add AI diff preview workflow#469
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
👋 Thanks for opening a PR, @Bhagy-Yelleti!Your PR has entered the 🚦 PR Review Pipeline.
What happens next
A pipeline status comment will appear below and update automatically as your PR progresses. While you wait
This comment is posted only once. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughThis PR implements an AI Diff Preview workflow: AI-generated ChangesAI Diff Preview Feature
sequenceDiagram
participant AIModel
participant ChatPanel as ai-chat-panel
participant DiffPreview as AIDiffPreview
participant Storage as TemplateStorage
participant User
AIModel->>ChatPanel: emits tool part (edit/read/delete)
ChatPanel->>ChatPanel: create pendingChanges & set isReviewPending (for edits)
ChatPanel->>DiffPreview: show diffs (isOpen)
User->>DiffPreview: Accept / Reject
DiffPreview->>ChatPanel: onAccept / onReject
ChatPanel->>Storage: saveTemplateData (apply) / discard (reject)
🎯 3 (Moderate) | ⏱️ ~22 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@modules/playground/components/ai-chat-panel.tsx`:
- Around line 90-93: The current pendingChanges state (PendingChange | null) is
overwritten on every edit_file/edit_multiple_files result, losing earlier staged
diffs; change pendingChanges to hold a collection (e.g., PendingChange[]) or
implement a locking/queue so new tool results are appended instead of replacing
existing ones, and prevent new appends while a review is being resolved if you
want a lock. Update all usages and setters (pendingChanges, setPendingChanges)
and the handlers that process edit_file/edit_multiple_files (and places around
isReviewPending) to push/merge incoming changes into the list (or enqueue them)
and adjust the UI/consumer logic to iterate over the array rather than assuming
a single PendingChange. Ensure any code that clears or accepts changes drains
the queue appropriately.
- Around line 246-257: The client code in AIChatPanel that constructs toolParts
from rawParts and logs JSON.stringify(toolParts, null, 2) leaks full tool
payloads (including file contents); remove that detailed logging and instead log
only safe metadata (e.g., tool type, name, and size) or nothing at all, and wrap
any dev-only metadata logging behind an environment/dev-mode guard (use existing
dev flag or process.env.NODE_ENV === "development"); also find the duplicate raw
tool-payload console.log occurrences elsewhere in the same component (the other
console.log calls that output toolParts) and apply the same removal/replacement
with metadata-only, dev-guarded logging.
- Around line 210-214: The open-buffer reconciliation incorrectly matches
buffers using change.path.endsWith(fullName), causing different files with the
same filename to be updated; change the comparison to match the exact file path
used by the rest of the system (e.g., use change.path === f.path or the
equivalent fullPath property on the open file object) inside the
currentOpenFiles map/update logic (the block that builds fullName and returns {
...f, content: change.newContent, hasUnsavedChanges: true }) and make the same
exact-path change in the other accept/delete handler (the similar code around
the second occurrence). Ensure this uses the same path identifier as
findFileByPath / addOrUpdateFile / deleteFileByPath so open buffers are
reconciled by exact path.
- Around line 130-144: The unresolved-tool detection in hasUnresolvedTools
currently only checks p.toolInvocation.state === "call", which misses AI SDK v3
static tool parts that lack toolInvocation; update the predicate in the
parts.some(...) (used with hasUnresolvedTools / lastMessage / parts /
MessagePart) to also treat v3 tool parts as unresolved when p.type
startsWith("tool-") and either p.toolInvocation?.state === "call" OR
(p.toolCallId is present and p.state === "call"); ensure you check p.toolCallId
and p.state safely (type guard for object) so v3 parts with toolCallId/state are
considered unresolved.
- Around line 220-227: The code applies UI updates and clears the pending review
before persistence completes; change the flow so
saveTemplateData(updatedTemplate) is awaited and only on success call
setTemplateData(updatedTemplate), setOpenFiles(currentOpenFiles),
toast.success(...), setPendingChanges(null), and setIsReviewPending(false); on
failure do not clear pendingChanges or close the review—catch the error, surface
an error toast (or processLogger/error handler) and leave pendingChanges intact
so the user can retry. Use the existing saveTemplateData, setTemplateData,
setOpenFiles, setPendingChanges, setIsReviewPending, pendingChanges,
currentItems and currentOpenFiles identifiers to locate and update the logic.
In `@modules/playground/components/ai-diff-preview.tsx`:
- Around line 24-29: The component currently only supports global accept/reject
via AIDiffPreviewProps (onAccept, onReject) and renders all-or-nothing footer
buttons; update it to support file-by-file actions by extending the props and
UI: change AIDiffPreviewProps to accept either a PendingChange[] or a
PendingChange that contains a files array and add per-file handlers (e.g.,
onAcceptFile(fileId: string) and onRejectFile(fileId: string)) alongside the
existing global onAccept/onReject, then update the AIDiffPreview component
render logic (where it maps pending changes / files — see the file list
rendering around lines 136-173) to show Accept/Reject buttons for each file
entry that call the new handlers and keep the footer global buttons for “accept
all/reject all” wired to onAccept/onReject; ensure state updates or callback
payloads include the file identifier so the parent can process per-file
decisions.
- Around line 55-88: renderDiffPreview currently truncates the top-of-file
output from diffLines and can omit the first changed hunk; change truncation to
be hunk-aware by ensuring at least one part with added/removed === true is
included. Iterate diffParts (diffLines) accumulating lines into truncatedDiff up
to maxLines as you do now, but if you reach maxLines without encountering any
changed part, locate the first part where part.added||part.removed and
replace/truncate the accumulated context to include that hunk plus a small
configurable context (e.g., N lines before/after) while still respecting
maxLines; update uses of truncatedDiff, DiffPart, renderDiffPreview, maxLines
and lineCount to implement this logic so previews always include the first edit
hunk.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a726c39a-017c-4632-8263-50370edb94a4
📒 Files selected for processing (2)
modules/playground/components/ai-chat-panel.tsxmodules/playground/components/ai-diff-preview.tsx
| const [pendingChanges, setPendingChanges] = useState<PendingChange | null>( | ||
| null, | ||
| ); | ||
| const [isReviewPending, setIsReviewPending] = useState(false); |
There was a problem hiding this comment.
Queue or lock staged edits instead of overwriting them.
pendingChanges only holds one tool result, and every edit_file / edit_multiple_files call replaces it. If the model emits multiple edit tool parts in one assistant message, or the user sends another prompt before resolving the review, earlier staged diffs are silently discarded.
Also applies to: 147-149, 323-328, 362-367
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modules/playground/components/ai-chat-panel.tsx` around lines 90 - 93, The
current pendingChanges state (PendingChange | null) is overwritten on every
edit_file/edit_multiple_files result, losing earlier staged diffs; change
pendingChanges to hold a collection (e.g., PendingChange[]) or implement a
locking/queue so new tool results are appended instead of replacing existing
ones, and prevent new appends while a review is being resolved if you want a
lock. Update all usages and setters (pendingChanges, setPendingChanges) and the
handlers that process edit_file/edit_multiple_files (and places around
isReviewPending) to push/merge incoming changes into the list (or enqueue them)
and adjust the UI/consumer logic to iterate over the array rather than assuming
a single PendingChange. Ensure any code that clears or accepts changes drains
the queue appropriately.
| currentOpenFiles = currentOpenFiles.map((f) => { | ||
| const ext = f.fileExtension ? `.${f.fileExtension}` : ""; | ||
| const fullName = `${f.filename}${ext}`; | ||
| if (change.path.endsWith(fullName)) { | ||
| return { ...f, content: change.newContent, hasUnsavedChanges: true }; |
There was a problem hiding this comment.
Match open buffers by exact path, not filename suffix.
Both accept/delete paths reconcile openFiles with change.path.endsWith(fullName). That will update or close every open index.tsx when only one of src/index.tsx or tests/index.tsx changed. The underlying file mutations are already path-driven via findFileByPath / addOrUpdateFile / deleteFileByPath in modules/playground/hooks/useAI.ts:168-291, so the open-buffer contract needs the same identifier.
Also applies to: 381-384
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modules/playground/components/ai-chat-panel.tsx` around lines 210 - 214, The
open-buffer reconciliation incorrectly matches buffers using
change.path.endsWith(fullName), causing different files with the same filename
to be updated; change the comparison to match the exact file path used by the
rest of the system (e.g., use change.path === f.path or the equivalent fullPath
property on the open file object) inside the currentOpenFiles map/update logic
(the block that builds fullName and returns { ...f, content: change.newContent,
hasUnsavedChanges: true }) and make the same exact-path change in the other
accept/delete handler (the similar code around the second occurrence). Ensure
this uses the same path identifier as findFileByPath / addOrUpdateFile /
deleteFileByPath so open buffers are reconciled by exact path.
| interface AIDiffPreviewProps { | ||
| pendingChanges: PendingChange | null; | ||
| isOpen: boolean; | ||
| onAccept: () => void; | ||
| onReject: () => void; | ||
| } |
There was a problem hiding this comment.
The file-by-file review requirement still isn't implemented.
The linked objective calls for accepting/rejecting pending changes globally or file-by-file, but this component only exposes global onAccept / onReject handlers and renders all-or-nothing footer buttons. In a multi-file edit, users still can't keep one file and reject another.
Also applies to: 136-173
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modules/playground/components/ai-diff-preview.tsx` around lines 24 - 29, The
component currently only supports global accept/reject via AIDiffPreviewProps
(onAccept, onReject) and renders all-or-nothing footer buttons; update it to
support file-by-file actions by extending the props and UI: change
AIDiffPreviewProps to accept either a PendingChange[] or a PendingChange that
contains a files array and add per-file handlers (e.g., onAcceptFile(fileId:
string) and onRejectFile(fileId: string)) alongside the existing global
onAccept/onReject, then update the AIDiffPreview component render logic (where
it maps pending changes / files — see the file list rendering around lines
136-173) to show Accept/Reject buttons for each file entry that call the new
handlers and keep the footer global buttons for “accept all/reject all” wired to
onAccept/onReject; ensure state updates or callback payloads include the file
identifier so the parent can process per-file decisions.
| const renderDiffPreview = (oldContent: string, newContent: string) => { | ||
| const diff = diffLines(oldContent, newContent); | ||
| const maxLines = 100; | ||
| let lineCount = 0; | ||
| const truncatedDiff: DiffPart[] = []; | ||
|
|
||
| for (const part of diff) { | ||
| const lines = part.value | ||
| .split("\n") | ||
| .filter((line: string) => line !== ""); | ||
| if (lineCount + lines.length > maxLines) { | ||
| const remaining = maxLines - lineCount; | ||
| if (remaining > 0) { | ||
| const truncatedValue = part.value | ||
| .split("\n") | ||
| .slice(0, remaining) | ||
| .join("\n"); | ||
| truncatedDiff.push({ | ||
| value: | ||
| truncatedValue + | ||
| (remaining < lines.length ? "\n... (truncated)" : ""), | ||
| added: part.added, | ||
| removed: part.removed, | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| truncatedDiff.push({ | ||
| value: part.value, | ||
| added: part.added, | ||
| removed: part.removed, | ||
| }); | ||
| lineCount += lines.length; | ||
| } |
There was a problem hiding this comment.
This truncation can hide the actual edit hunk.
The preview clips the first 100 lines of the raw diffLines output. For a large file whose first change is after line 100, the card shows only unchanged context and never renders the added/removed lines the user is supposed to approve. Truncation needs to be hunk-aware, not top-of-file aware.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@modules/playground/components/ai-diff-preview.tsx` around lines 55 - 88,
renderDiffPreview currently truncates the top-of-file output from diffLines and
can omit the first changed hunk; change truncation to be hunk-aware by ensuring
at least one part with added/removed === true is included. Iterate diffParts
(diffLines) accumulating lines into truncatedDiff up to maxLines as you do now,
but if you reach maxLines without encountering any changed part, locate the
first part where part.added||part.removed and replace/truncate the accumulated
context to include that hunk plus a small configurable context (e.g., N lines
before/after) while still respecting maxLines; update uses of truncatedDiff,
DiffPart, renderDiffPreview, maxLines and lineCount to implement this logic so
previews always include the first edit hunk.
|
Hi @piyushdotcomm I've made the requested changes and pushed the updates. |
Summary
This PR implements an AI Diff Preview workflow before AI-generated code changes are applied to the editor.
Previously, AI-generated edits could be applied directly, making it difficult for users to understand what was changed or verify modifications before they affected their files. This update introduces a review step that allows users to inspect proposed changes and decide whether to apply or discard them.
What changed
AIDiffPreviewcomponent to display pending AI-generated file changes.Why it changed
The goal of this feature is to improve transparency and user control when working with AI-generated code.
Instead of immediately applying modifications, users can now review the proposed changes first, making it easier to:
Type of change
Related issue
Closes #433
Validation
npm run lintnpm testnpm run buildList any additional manual verification you performed:
Checklist
Summary by CodeRabbit
New Features
Improvements