Skip to content

Conversation

@phodal
Copy link
Member

@phodal phodal commented Dec 4, 2025

Changes

1. Remove assistant message background

  • Assistant messages now have transparent background for a unified look
  • Multiple messages appear as a cohesive whole

2. Add DevInRenderer component

  • Parse <devin> blocks and render as tool call items
  • Filter <devin>...</devin> tags from markdown content
  • Support for read-file tool with open file button (📂)
  • Collapsible parameters view

3. Hide Task completed message

  • Task completion is implicit from the response

Files Changed

  • Timeline.css - Remove assistant message background
  • Timeline.tsx - Hide TaskCompleteItemRenderer
  • MarkdownRenderer.tsx - Filter devin tags
  • SketchRenderer.tsx - Use DevInRenderer for devin blocks
  • DevInRenderer.tsx - New component for tool call display
  • DevInRenderer.css - Styling for tool call items
  • index.ts - Export DevInRenderer

Summary by CodeRabbit

  • New Features

    • Render "devin" tool-call blocks as structured, expandable tool items with parameter view and file-open actions.
    • Hide raw devin blocks from markdown output by filtering them.
  • UI Updates

    • Added styling for DevIn tool items.
    • Assistant messages now use a minimal transparent style.
    • Task completion visual indicator removed.
  • Bug Fixes

    • Improved undo flow for file-change diffs with a rollback path and manual fallback.

✏️ Tip: You can customize this high-level summary in your review settings.

- Remove assistant message background for unified look
- Add DevInRenderer to parse and display devin blocks as tool calls
- Filter <devin> tags from markdown content
- Add open file button for read-file tool
- Hide Task completed message (implicit from response)
Copilot AI review requested due to automatic review settings December 4, 2025 02:24
@coderabbitai
Copy link

coderabbitai bot commented Dec 4, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a DevInRenderer to parse and display "devin" code blocks as tool-call items, strips tags from markdown, routes devin blocks from SketchRenderer to the new renderer, hides task_complete timeline items, and adjusts assistant message styling in the timeline.

Changes

Cohort / File(s) Summary
Timeline styling & rendering
mpp-vscode/webview/src/components/Timeline.css, mpp-vscode/webview/src/components/Timeline.tsx
Assistant message styling changed to transparent background with padding and no border; task_complete timeline items now render nothing (renderer returns null).
DevInRenderer component & styles
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx, mpp-vscode/webview/src/components/sketch/DevInRenderer.css
New React component parses "devin" blocks into tool-call entries, renders expandable tool items with truncated param preview and JSON detail view, supports file-open action and click handling; CSS provides VSCode-themed styling.
Markdown filtering & integration
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx, mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx
Adds filterDevinTags to remove <devin>...</devin> blocks; MarkdownRenderer renders filtered content and returns null if empty; SketchRenderer delegates devin code blocks to DevInRenderer (preserving isComplete and onAction).
Public exports
mpp-vscode/webview/src/components/sketch/index.ts
Exports DevInRenderer from the sketch module.
IDE diff / undo flow (IntelliJ)
mpp-idea/src/main/kotlin/.../IdeaFileChangeDiffDialog.kt, .../IdeaFileChangeSummary.kt
Replaced Compose-based diff dialog with IntelliJ DiffViewer/Swing implementation; added FileChangeConverter.toChange(project, fileChange); changed IdeaFileChangeDiffDialogWrapper to accept fileChange; undo now tries RollbackWorker.doRollback with a manual fallback handling CREATE/EDIT/RENAME/DELETE.

Sequence Diagram

sequenceDiagram
    participant Client as Webview Client
    participant SR as SketchRenderer
    participant MR as MarkdownRenderer
    participant DVR as DevInRenderer
    participant Parser as DevIn Parser
    participant UI as DevIn UI

    Client->>SR: render message with blocks
    SR->>MR: provide markdown content
    MR->>MR: filterDevinTags(content)
    MR-->>SR: filtered markdown (no <devin> blocks)

    SR->>DVR: devin code block (content, isComplete, onAction)
    DVR->>Parser: parse lines → tool calls + params
    Parser-->>DVR: tool call list
    DVR->>UI: render DevInToolItem(s) (collapsed previews)
    UI->>Client: onAction('openFile', {path}) when open clicked
    UI->>UI: expand/collapse to show JSON param details
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • DevInRenderer parsing edge cases (parameter parsing, quoted values, fallback to single "args").
    • filterDevinTags regex correctness for multiline and unclosed tags.
    • Interaction between SketchRenderer, MarkdownRenderer, and DevInRenderer to avoid duplicate or dropped rendering and to preserve isComplete/onAction behavior.
    • IntelliJ changes: correctness of FileChangeConverter content revisions and RollbackWorker/manual undo fallbacks (file/document APIs, error handling).

Possibly related PRs

Poem

🐇 I found the devin tags that hid from view,
I parsed their commands and showed them true;
I hid a task bar, softened the assistant's light,
Click to open files, expand to see each sight.
A tiny rabbit hops—your tools are in plain sight. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'improve renderer UI for better UX' is vague and generic—it does not clearly identify the specific changes made in the PR, such as adding DevInRenderer, filtering devin tags, or improving assistant message styling. Consider a more descriptive title such as 'Add DevInRenderer component with devin block parsing and filtering' or 'feat(vscode): add DevInRenderer and improve assistant message styling' to better convey the main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vscode-renderer-improvements

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot finished reviewing on behalf of phodal December 4, 2025 02:28
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)

17-35: Devin tag stripping matches intent; only minor nits

The filter handles closed blocks, streaming tails, and stray closing tags correctly, and wiring filteredContent through to the early return and ReactMarkdown prevents devin-only messages from rendering as empty shells.

Two small, optional tweaks:

  • You already trim() inside filterDevinTags, so the extra filteredContent.trim() in the if check is redundant.
  • This will also strip any literal <devin> markup a model might emit as part of normal markdown; if that’s a realistic case, you may eventually want a more constrained pattern (e.g., only tags starting at column 0 or preceded by a newline).

Also applies to: 127-127

mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (2)

22-60: DevIn parsing and completed‑block gating look good; you can avoid work while streaming

The line‑wise parsing and args fallback cover the expected /tool key="val" shapes well, and the !isComplete || toolCalls.length === 0 guard ensures we don’t render half‑baked blocks.

To cut unnecessary work during streaming (when isComplete === false), you can short‑circuit parseDevInContent via useMemo:

-  const toolCalls = useMemo(() => parseDevInContent(content), [content]);
+  const toolCalls = useMemo(
+    () => (isComplete ? parseDevInContent(content) : []),
+    [content, isComplete]
+  );

This keeps the behavior identical but skips parsing for partial DevIn blocks.

Also applies to: 74-83


106-175: Align DevInToolItem header behavior with pointer cursor

CSS gives .devin-tool-header a pointer cursor and hover state, but only the small toggle button actually expands/collapses. Making the whole header clickable better matches the affordance:

-  return (
-    <div className="devin-tool-item">
-      <div className="devin-tool-header">
+  return (
+    <div className="devin-tool-item">
+      <div className="devin-tool-header" onClick={handleToggle}>

Because handleToggle no‑ops when there are no params and already calls stopPropagation, this shouldn’t change other behavior, and the dedicated toggle button can stay as‑is.

mpp-vscode/webview/src/components/Timeline.tsx (1)

133-135: Hiding task_complete items via a no‑op renderer matches the UX goal

Returning null from TaskCompleteItemRenderer cleanly removes these entries from the DOM while keeping the switch exhaustive. The only leftover is the now‑unused .timeline-item.task-complete styles; you can keep them for possible future reuse or trim them in a follow‑up.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aadb10d and 4266164.

📒 Files selected for processing (7)
  • mpp-vscode/webview/src/components/Timeline.css (1 hunks)
  • mpp-vscode/webview/src/components/Timeline.tsx (1 hunks)
  • mpp-vscode/webview/src/components/sketch/DevInRenderer.css (1 hunks)
  • mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1 hunks)
  • mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (2 hunks)
  • mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx (2 hunks)
  • mpp-vscode/webview/src/components/sketch/index.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to mpp-ui/src/jsMain/typescript/**/*.{ts,tsx} : In CLI/TUI (TypeScript), use `mpp-ui/src/jsMain/typescript/design-system/` and import `semanticInk` / `semanticChalk`

Applied to files:

  • mpp-vscode/webview/src/components/sketch/index.ts
  • mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx
🧬 Code graph analysis (3)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/impl/PlanManagementTool.kt (1)
  • action (25-37)
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)
mpp-vscode/webview/src/components/sketch/index.ts (1)
  • MarkdownRenderer (17-17)
mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx (2)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1)
  • DevInRenderer (74-98)
mpp-vscode/webview/src/components/sketch/index.ts (1)
  • DevInRenderer (23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: Build (223)
  • GitHub Check: Build (241)
🔇 Additional comments (4)
mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx (1)

21-21: DevInRenderer wiring for devin blocks looks correct

The devin branch now correctly delegates to DevInRenderer, with content, isComplete, and onAction all passed through and languageId.toLowerCase() ensuring case‑insensitive matching. This fits cleanly into the existing dispatch structure.

Also applies to: 120-127

mpp-vscode/webview/src/components/Timeline.css (1)

25-30: Assistant message styling change is consistent and scoped

Making .timeline-item.message.assistant transparent with no border (and slightly reduced vertical padding) matches the goal of visually merging assistant replies, while leaving user and system message treatments intact.

mpp-vscode/webview/src/components/sketch/index.ts (1)

3-14: Barrel export and docs updated correctly for DevInRenderer

The index now documents and re‑exports DevInRenderer alongside the other sketch components, keeping the public surface aligned with the new DevIn rendering path.

Also applies to: 23-23

mpp-vscode/webview/src/components/sketch/DevInRenderer.css (1)

1-95: DevInRenderer theming is coherent and matches component behavior

The styles give DevIn tool calls a compact, readable card with clear affordances (success accent border, header hover state, compact action buttons) and rely on VSCode theme variables throughout. This pairs well with the React component structure and the header‑click behavior suggested in DevInRenderer.tsx.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the VSCode webview renderer with improved UI/UX for displaying AI assistant responses. It introduces a new DevInRenderer component to parse and display tool call blocks as interactive UI elements, removes visual clutter from assistant messages for a more unified look, and hides the redundant "Task completed" message.

Key changes:

  • Added DevInRenderer component to parse <devin> blocks and render them as collapsible tool call items with file-opening capabilities
  • Filtered <devin> tags from markdown content to prevent duplicate display
  • Made assistant messages transparent for a more cohesive conversation flow

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mpp-vscode/webview/src/components/sketch/index.ts Adds DevInRenderer export and updates JSDoc to document new component
mpp-vscode/webview/src/components/sketch/SketchRenderer.tsx Integrates DevInRenderer for handling 'devin' block types instead of generic code rendering
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx Filters out <devin> tags from markdown content to prevent duplication
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx New component that parses tool call syntax and renders interactive tool items with expand/collapse and file-opening features
mpp-vscode/webview/src/components/sketch/DevInRenderer.css Styling for the DevInRenderer component with VSCode theme integration
mpp-vscode/webview/src/components/Timeline.tsx Hides TaskCompleteItemRenderer output (returns null) as completion is implicit
mpp-vscode/webview/src/components/Timeline.css Changes assistant messages to transparent background for unified appearance

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Remove <devin>...</devin> blocks (including multiline)
let filtered = content.replace(/<devin>[\s\S]*?<\/devin>/gi, '');
// Remove unclosed <devin> tags at the end (streaming)
filtered = filtered.replace(/<devin>[\s\S]*$/gi, '');
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The regex pattern on line 25 /<devin>[\s\S]*$/gi uses the global flag g with $ anchor, which may not work as expected. The $ anchor in global mode will match at the end of each line when using the multiline flag m, but without it, it only matches at the very end of the string. Since this regex is intended to match unclosed <devin> tags during streaming (which would only occur at the end of the entire content string), the g flag is unnecessary and could cause unexpected behavior. Consider using /<devin>[\s\S]*$/i instead.

Suggested change
filtered = filtered.replace(/<devin>[\s\S]*$/gi, '');
filtered = filtered.replace(/<devin>[\s\S]*$/i, '');

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 47
// Parse key="value" or key=value patterns
const paramRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
let match;
while ((match = paramRegex.exec(paramsStr)) !== null) {
const key = match[1];
const value = match[2] ?? match[3] ?? match[4] ?? '';
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The parameter regex /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g doesn't handle escaped quotes within quoted values. For example, key="value with \"quote\"" would fail to parse correctly. Consider using a more robust parsing approach or documenting this limitation.

Suggested change
// Parse key="value" or key=value patterns
const paramRegex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|(\S+))/g;
let match;
while ((match = paramRegex.exec(paramsStr)) !== null) {
const key = match[1];
const value = match[2] ?? match[3] ?? match[4] ?? '';
// Parse key="value" or key=value patterns, handling escaped quotes
let i = 0;
const len = paramsStr.length;
while (i < len) {
// Skip whitespace
while (i < len && /\s/.test(paramsStr[i])) i++;
if (i >= len) break;
// Parse key
const keyMatch = paramsStr.slice(i).match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
if (!keyMatch) break;
const key = keyMatch[1];
i += key.length;
// Skip whitespace and '='
while (i < len && /\s/.test(paramsStr[i])) i++;
if (paramsStr[i] !== '=') break;
i++; // skip '='
while (i < len && /\s/.test(paramsStr[i])) i++;
if (i >= len) break;
let value = '';
if (paramsStr[i] === '"' || paramsStr[i] === "'") {
const quote = paramsStr[i];
i++;
let val = '';
while (i < len) {
if (paramsStr[i] === '\\' && i + 1 < len && paramsStr[i + 1] === quote) {
val += quote;
i += 2;
} else if (paramsStr[i] === '\\' && i + 1 < len) {
val += paramsStr[i + 1];
i += 2;
} else if (paramsStr[i] === quote) {
i++;
break;
} else {
val += paramsStr[i];
i++;
}
}
value = val;
} else {
// Unquoted value
const valueMatch = paramsStr.slice(i).match(/^\S+/);
if (valueMatch) {
value = valueMatch[0];
i += value.length;
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +134
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
if (hasParams) {
setIsExpanded(!isExpanded);
}
};
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The header div has cursor: pointer styling (line 20 in CSS), but the click behavior in handleToggle only triggers when hasParams is true. This could confuse users when clicking on tool items without parameters, as the cursor indicates clickability but nothing happens. Consider either removing the pointer cursor when hasParams is false, or making the entire header always clickable (even if it does nothing) for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines 139 to 163
<span className="devin-tool-icon"></span>
<span className="devin-tool-name">{toolName}</span>
{!isExpanded && details && (
<span className="devin-tool-details">
{details.length > 60 ? details.slice(0, 60) + '...' : details}
</span>
)}
<div className="devin-tool-actions">
{isFileTool && filePath && (
<button
className="devin-action-btn"
onClick={handleOpenFile}
title="Open file in editor"
>
📂
</button>
)}
{hasParams && (
<button
className="devin-action-btn toggle"
onClick={handleToggle}
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? '▼' : '▶'}
</button>
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The emoji icons (✅, 📂) and arrow symbols (▼, ▶) may not be accessible to screen readers. Consider adding aria-label attributes to provide text descriptions for assistive technologies. For example, the file open button could have aria-label="Open file in editor" (which you already have as title), but also consider using semantic HTML or ARIA roles for better accessibility.

Copilot uses AI. Check for mistakes.
// Remove unclosed <devin> tags at the end (streaming)
filtered = filtered.replace(/<devin>[\s\S]*$/gi, '');
// Remove standalone </devin> tags
filtered = filtered.replace(/<\/devin>/gi, '');
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The regex pattern /<\/devin>/gi uses the global flag to remove all standalone closing tags. However, this could unintentionally remove </devin> text that appears in code examples or other legitimate contexts within the markdown content. Consider being more conservative with this pattern, or ensure it only runs after the first two patterns have been applied to minimize false positives.

Suggested change
filtered = filtered.replace(/<\/devin>/gi, '');
// Remove standalone </devin> tags that appear on their own line
filtered = filtered.replace(/^\s*<\/devin>\s*$/gim, '');

Copilot uses AI. Check for mistakes.
@phodal
Copy link
Member Author

phodal commented Dec 4, 2025

augment review

- Remove unnecessary 'g' flag from regex for unclosed devin tags
- Make standalone </devin> removal more conservative (only on own line)
- Add aria-label attributes for accessibility
- Only show pointer cursor when header is clickable (has params)
- Document limitation: parser does not handle escaped quotes
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (3)

45-53: Escaped quotes limitation is now documented.

The previous concern about escaped quotes has been addressed with documentation notes on lines 26-27 and 46. This is acceptable for the expected use cases.


133-142: Cursor pointer issue addressed with conditional class.

The previous concern about misleading cursor on non-clickable headers has been resolved. Line 142 now conditionally applies the 'clickable' class only when hasParams is true.


143-170: Accessibility concerns addressed with proper ARIA attributes.

The previous concern about emoji accessibility has been resolved:

  • Line 143: has role="img" and aria-label="Completed"
  • Lines 155-158: File button has aria-label and the emoji has aria-hidden="true"
  • Lines 165-169: Toggle button has aria-label, aria-expanded, and the arrow has aria-hidden="true"
🧹 Nitpick comments (6)
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)

33-36: Redundant trim call.

Since filterDevinTags already returns a trimmed string (line 29), the .trim() call on line 35 is unnecessary.

Apply this diff:

  const filteredContent = filterDevinTags(content);

- if (!filteredContent.trim()) {
+ if (!filteredContent) {
    return null;
  }
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (2)

11-20: LGTM - Clean interface definitions.

The interfaces are well-structured. Consider using a more specific type for the data parameter in onAction (e.g., Record<string, unknown> or a union type) for improved type safety, but the current any is acceptable for a generic action handler.


110-111: Consider the specificity of the 'open' tool name.

The 'open' entry is quite generic and might incorrectly trigger file-open behavior for unrelated tools. Verify this is intentional, or consider using more specific names like 'open-file'.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt (1)

277-279: Consider notifying users when undo operations fail.

Errors are logged but not displayed to users. When manual undo fails, consider showing a notification so users are aware the operation didn't succeed and can take corrective action.

You can use IntelliJ's notification system:

import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType

// In the catch block:
catch (e: Exception) {
    logger.error("Failed to undo change for ${change.filePath}", e)
    NotificationGroupManager.getInstance()
        .getNotificationGroup("AutoDev")
        .createNotification(
            "Failed to undo changes to ${change.filePath}",
            e.message ?: "Unknown error",
            NotificationType.ERROR
        )
        .notify(project)
}
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt (2)

123-159: Extract repeated DiffContext implementation.

The anonymous DiffContext object is duplicated three times (lines 127-132, 138-143, 149-154) with identical implementations. Extract this to a private property or helper method to improve maintainability.

Apply this refactor:

+    private val diffContext = object : DiffContext() {
+        override fun getProject() = this@IdeaFileChangeDiffDialogWrapper.project
+        override fun isWindowFocused() = true
+        override fun isFocusedInWindow() = true
+        override fun requestFocusInWindow() = Unit
+    }
+
     private fun createDiffViewer(): JComponent {
         return when (fileChange.changeType) {
             ChangeType.CREATE -> {
                 val diffRequest = createOneSideDiffRequest()
-                val diffViewer = SimpleOnesideDiffViewer(object : DiffContext() {
-                    override fun getProject() = this@IdeaFileChangeDiffDialogWrapper.project
-                    override fun isWindowFocused() = true
-                    override fun isFocusedInWindow() = true
-                    override fun requestFocusInWindow() = Unit
-                }, diffRequest)
+                val diffViewer = SimpleOnesideDiffViewer(diffContext, diffRequest)
                 diffViewer.init()
                 diffViewer.component
             }
             ChangeType.DELETE -> {
                 val diffRequest = createOneSideDiffRequestForDelete()
-                val diffViewer = SimpleOnesideDiffViewer(object : DiffContext() {
-                    override fun getProject() = this@IdeaFileChangeDiffDialogWrapper.project
-                    override fun isWindowFocused() = true
-                    override fun isFocusedInWindow() = true
-                    override fun requestFocusInWindow() = Unit
-                }, diffRequest)
+                val diffViewer = SimpleOnesideDiffViewer(diffContext, diffRequest)
                 diffViewer.init()
                 diffViewer.component
             }
             else -> {
                 val diffRequest = createTwoSideDiffRequest()
-                val diffViewer = SimpleDiffViewer(object : DiffContext() {
-                    override fun getProject() = this@IdeaFileChangeDiffDialogWrapper.project
-                    override fun isWindowFocused() = true
-                    override fun isFocusedInWindow() = true
-                    override fun requestFocusInWindow() = Unit
-                }, diffRequest)
+                val diffViewer = SimpleDiffViewer(diffContext, diffRequest)
                 diffViewer.init()
                 diffViewer.component
             }
         }
     }

301-342: LGTM! Clean conversion utility.

The FileChangeConverter correctly maps FileChange to IntelliJ's Change API with proper handling of different change types and content revisions.

Minor note: Line 311's basePath variable is declared but never used. You can remove it:

     fun toChange(project: Project, fileChange: FileChange): Change {
-        val basePath = project.basePath ?: System.getProperty("user.dir")
         val file = File(fileChange.filePath)
         val filePath: FilePath = VcsUtil.getFilePath(file, false)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4266164 and e9addeb.

📒 Files selected for processing (5)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt (3 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt (1 hunks)
  • mpp-vscode/webview/src/components/sketch/DevInRenderer.css (1 hunks)
  • mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1 hunks)
  • mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mpp-vscode/webview/src/components/sketch/DevInRenderer.css
🧰 Additional context used
📓 Path-based instructions (2)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual for platform-specific code (e.g., file I/O on JVM/JS/Wasm) in Kotlin Multiplatform projects
Check export first if some functions are not working well with CLI (TypeScript)
In Kotlin/JS @JsExport: Avoid Flow, use Promise instead
In Kotlin/JS @JsExport: Use concrete classes as return types and parameter types; avoid interface types
For WASM platform, avoid using emoji and UTF-8 in code

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt
**/idea/**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/idea/**/*.kt: For SwingPanel z-index issues with Compose popups, enable Jewel's custom popup renderer: JewelFlags.useCustomPopupRenderer = true in IdeaAgentToolWindowFactory
For popup/dropdown menus in IntelliJ plugins, use Jewel's PopupMenu instead of androidx.compose.ui.window.Popup
For dialogs in IntelliJ plugins, use IntelliJ's DialogWrapper with org.jetbrains.jewel.bridge.compose instead of androidx.compose.ui.window.Dialog

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt
🧠 Learnings (3)
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to **/idea/**/*.kt : For dialogs in IntelliJ plugins, use IntelliJ's `DialogWrapper` with `org.jetbrains.jewel.bridge.compose` instead of `androidx.compose.ui.window.Dialog`

Applied to files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to **/idea/**/*.kt : For SwingPanel z-index issues with Compose popups, enable Jewel's custom popup renderer: `JewelFlags.useCustomPopupRenderer = true` in `IdeaAgentToolWindowFactory`

Applied to files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to **/idea/**/*.kt : For popup/dropdown menus in IntelliJ plugins, use Jewel's `PopupMenu` instead of `androidx.compose.ui.window.Popup`

Applied to files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt
🧬 Code graph analysis (4)
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)
mpp-vscode/webview/src/components/sketch/index.ts (1)
  • MarkdownRenderer (17-17)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt (1)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt (1)
  • performManualUndo (232-274)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1)
mpp-vscode/webview/src/components/sketch/index.ts (1)
  • DevInRenderer (23-23)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt (2)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt (1)
  • performManualUndo (241-281)
exts/devins-lang/src/main/kotlin/cc/unitmesh/devti/language/compiler/exec/file/WriteInsCommand.kt (1)
  • runWriteAction (66-74)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build (223)
  • GitHub Check: Build (241)
🔇 Additional comments (6)
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)

17-30: Past review comments have been addressed—implementation looks solid.

The regex patterns correctly implement the suggestions from previous reviews:

  • Line 26 now uses /<devin>[\s\S]*$/i without the global flag, as recommended
  • Line 28 now uses line-anchored pattern /^\s*<\/devin>\s*$/gim to be more conservative

The three-step filtering approach is well-ordered: complete blocks → trailing unclosed tags → standalone closing tags.

mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1)

78-102: LGTM - Well-structured component with appropriate memoization.

The use of useMemo for parsing prevents unnecessary re-computation. The early return for incomplete or empty blocks is a clean approach.

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt (4)

44-60: LGTM! Proper DialogWrapper usage.

The constructor and initialization correctly follow IntelliJ's DialogWrapper pattern, as recommended in the coding guidelines for IntelliJ plugin dialogs.


62-121: LGTM! Clean Swing UI implementation.

The header panel properly uses JBUI theming, appropriate icons, and displays relevant file change information clearly.


161-184: LGTM! Proper diff request construction.

The three diff request helpers correctly handle different change types using IntelliJ's diff API.


186-217: LGTM! Action buttons properly wired.

The south panel correctly creates action buttons with appropriate icons and callbacks.

Comment on lines +219 to +230
private fun performUndo() {
try {
// Try to use RollbackWorker for proper VCS integration
rollbackWorker.doRollback(listOf(change), false)
onUndoCallback()
close(OK_EXIT_CODE)
} catch (e: Exception) {
LOG.warn("RollbackWorker failed, falling back to manual revert", e)
// Fallback to manual revert
performManualUndo()
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add user-facing error notifications.

When RollbackWorker.doRollback fails (line 222), the code logs a warning and falls back to manual undo, but provides no visual feedback to the user. Similarly, if performManualUndo subsequently fails (line 271), the error is only logged. Users should be notified of undo failures.

Consider adding user notifications:

     private fun performUndo() {
         try {
             // Try to use RollbackWorker for proper VCS integration
             rollbackWorker.doRollback(listOf(change), false)
             onUndoCallback()
             close(OK_EXIT_CODE)
         } catch (e: Exception) {
             LOG.warn("RollbackWorker failed, falling back to manual revert", e)
+            // Notify user that VCS rollback failed
+            com.intellij.openapi.ui.Messages.showWarningDialog(
+                project,
+                "VCS rollback failed, attempting manual revert",
+                "Undo Warning"
+            )
             // Fallback to manual revert
             performManualUndo()
         }
     }

And in performManualUndo:

             } catch (e: Exception) {
                 LOG.error("Failed to undo change for ${fileChange.filePath}", e)
+                com.intellij.openapi.ui.Messages.showErrorDialog(
+                    project,
+                    "Failed to undo change: ${e.message}",
+                    "Undo Failed"
+                )
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun performUndo() {
try {
// Try to use RollbackWorker for proper VCS integration
rollbackWorker.doRollback(listOf(change), false)
onUndoCallback()
close(OK_EXIT_CODE)
} catch (e: Exception) {
LOG.warn("RollbackWorker failed, falling back to manual revert", e)
// Fallback to manual revert
performManualUndo()
}
}
private fun performUndo() {
try {
// Try to use RollbackWorker for proper VCS integration
rollbackWorker.doRollback(listOf(change), false)
onUndoCallback()
close(OK_EXIT_CODE)
} catch (e: Exception) {
LOG.warn("RollbackWorker failed, falling back to manual revert", e)
// Notify user that VCS rollback failed
com.intellij.openapi.ui.Messages.showWarningDialog(
project,
"VCS rollback failed, attempting manual revert",
"Undo Warning"
)
// Fallback to manual revert
performManualUndo()
}
}

Comment on lines +232 to 274
private fun performManualUndo() {
runWriteAction {
try {
when (fileChange.changeType) {
ChangeType.CREATE -> {
// For created files, delete or clear the content
val virtualFile = LocalFileSystem.getInstance().findFileByPath(fileChange.filePath)
virtualFile?.let { vf ->
val document = FileDocumentManager.getInstance().getDocument(vf)
document?.setText("")
}
}
ChangeType.EDIT, ChangeType.RENAME -> {
// Restore original content
fileChange.originalContent?.let { original ->
val virtualFile = LocalFileSystem.getInstance().findFileByPath(fileChange.filePath)
virtualFile?.let { vf ->
val document = FileDocumentManager.getInstance().getDocument(vf)
document?.setText(original)
}
}
}
ChangeType.DELETE -> {
// For deleted files, recreate them
fileChange.originalContent?.let { original ->
val parentPath = fileChange.filePath.substringBeforeLast('/')
val fileName = fileChange.filePath.substringAfterLast('/')
val parentDir = LocalFileSystem.getInstance().findFileByPath(parentPath)
parentDir?.let { dir ->
val newFile = dir.createChildData(project, fileName)
val document = FileDocumentManager.getInstance().getDocument(newFile)
document?.setText(original)
}
}
}
}
)
onUndoCallback()
close(OK_EXIT_CODE)
} catch (e: Exception) {
LOG.error("Failed to undo change for ${fileChange.filePath}", e)
}
}
dialogPanel.preferredSize = Dimension(800, 600)
return dialogPanel
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consolidate duplicated undo logic and fix path handling.

This method has two significant issues:

  1. Code duplication: The performManualUndo implementation is nearly identical to the one in IdeaFileChangeSummary.kt (lines 240-280). This logic should be extracted to a shared utility to avoid maintenance burden.

  2. Platform-specific path handling: Line 257 uses substringBeforeLast('/') which assumes Unix-style path separators and will fail on Windows where paths use backslashes.

Fix the path handling:

                 ChangeType.DELETE -> {
                     // For deleted files, recreate them
                     fileChange.originalContent?.let { original ->
-                        val parentPath = fileChange.filePath.substringBeforeLast('/')
-                        val fileName = fileChange.filePath.substringAfterLast('/')
+                        val file = File(fileChange.filePath)
+                        val parentPath = file.parent
+                        val fileName = file.name
                         val parentDir = LocalFileSystem.getInstance().findFileByPath(parentPath)
                         parentDir?.let { dir ->
                             val newFile = dir.createChildData(project, fileName)
                             val document = FileDocumentManager.getInstance().getDocument(newFile)
                             document?.setText(original)
                         }
                     }
                 }

Consolidate the duplication: Extract the manual undo logic to a shared utility class (e.g., FileChangeUndoUtil) that both IdeaFileChangeDiffDialogWrapper and IdeaFileChangeSummary can use.

Comment on lines 219 to +236
/**
* Undo a file change by restoring the original content
* Undo a file change using IntelliJ's RollbackWorker for proper VCS integration.
* Falls back to manual revert if RollbackWorker fails.
*/
private fun undoChange(project: Project, change: FileChange) {
private fun undoChange(project: Project, fileChange: FileChange) {
ApplicationManager.getApplication().invokeLater {
runWriteAction {
try {
when (change.changeType) {
ChangeType.CREATE -> {
// For created files, delete or clear the content
try {
// Convert FileChange to IntelliJ Change and use RollbackWorker
val change = FileChangeConverter.toChange(project, fileChange)
val rollbackWorker = com.intellij.openapi.vcs.changes.ui.RollbackWorker(project)
rollbackWorker.doRollback(listOf(change), false)
} catch (e: Exception) {
logger.warn("RollbackWorker failed, falling back to manual revert", e)
// Fallback to manual revert
performManualUndo(project, fileChange)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, check the actual imports in the file
echo "=== Checking all imports in IdeaFileChangeSummary.kt ==="
head -50 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt | grep "^import"

echo -e "\n=== Checking for FileChangeConverter import ==="
rg "import.*FileChangeConverter" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt

echo -e "\n=== Finding FileChangeConverter definition in the codebase ==="
rg "class FileChangeConverter|object FileChangeConverter" --type=kt

echo -e "\n=== Checking if FileChangeConverter is defined in this file itself ==="
rg -n "FileChangeConverter" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt | head -20

echo -e "\n=== Checking RollbackWorker.doRollback signature ==="
rg -nA 5 "fun doRollback" --type=kt | head -30

Repository: phodal/auto-dev-sketch

Length of output: 2014


🏁 Script executed:

#!/bin/bash

# Check the actual imports more thoroughly
echo "=== Full import section of IdeaFileChangeSummary.kt ==="
head -100 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt | grep "^import"

echo -e "\n=== Searching for FileChangeConverter definition anywhere ==="
rg "class FileChangeConverter|object FileChangeConverter"

echo -e "\n=== Searching for FileChangeConverter usage in other files ==="
rg "FileChangeConverter" mpp-idea/src/main/kotlin/ | head -20

echo -e "\n=== Checking for RollbackWorker import ==="
rg "import.*RollbackWorker"

echo -e "\n=== Checking if RollbackWorker is available in IntelliJ API ==="
rg "com.intellij.openapi.vcs.changes" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt

echo -e "\n=== Looking at lines around 227-228 in context ==="
sed -n '220,240p' mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt

Repository: phodal/auto-dev-sketch

Length of output: 3757


🏁 Script executed:

#!/bin/bash

# Find RollbackWorker.doRollback signature
echo "=== Searching for RollbackWorker.doRollback definition ==="
rg "fun doRollback" -A 5

echo -e "\n=== Checking IdeaFileChangeDiffDialog for comparison ==="
sed -n '1,50p' mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt | grep -E "^import|FileChangeConverter"

echo -e "\n=== Finding where FileChangeConverter is defined in IdeaFileChangeDiffDialog ==="
rg -n "object FileChangeConverter" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt -A 3

echo -e "\n=== Checking if FileChangeConverter needs to be public/exported ==="
grep -n "object FileChangeConverter\|fun toChange" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt

Repository: phodal/auto-dev-sketch

Length of output: 2039


🏁 Script executed:

#!/bin/bash

# Verify the exact package structure
echo "=== Checking package declarations ==="
head -5 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt
head -5 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt

echo -e "\n=== Verify if FileChangeConverter is public ==="
grep -B2 "object FileChangeConverter" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeDiffDialog.kt

echo -e "\n=== Check all usages of RollbackWorker.doRollback parameters ==="
rg "\.doRollback" -B 2 -A 2

Repository: phodal/auto-dev-sketch

Length of output: 3448


Add missing imports for FileChangeConverter and RollbackWorker.

Line 227 uses FileChangeConverter.toChange() but the import is missing. Add import cc.unitmesh.devins.idea.toolwindow.changes.FileChangeConverter at the top of the file. Additionally, add import com.intellij.openapi.vcs.changes.ui.RollbackWorker and replace the fully qualified name com.intellij.openapi.vcs.changes.ui.RollbackWorker(project) on line 228 with just RollbackWorker(project) for consistency with IdeaFileChangeDiffDialog.kt.

🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt
around lines 219 to 236, add the missing imports for FileChangeConverter and
RollbackWorker and simplify the instantiation: add import
cc.unitmesh.devins.idea.toolwindow.changes.FileChangeConverter and import
com.intellij.openapi.vcs.changes.ui.RollbackWorker at the top of the file, then
replace the fully-qualified
com.intellij.openapi.vcs.changes.ui.RollbackWorker(project) usage with
RollbackWorker(project) in the undoChange function for consistency.

Comment on lines +238 to 281
/**
* Manual undo fallback when RollbackWorker fails.
*/
private fun performManualUndo(project: Project, change: FileChange) {
runWriteAction {
try {
when (change.changeType) {
ChangeType.CREATE -> {
// For created files, delete or clear the content
val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
virtualFile?.let { vf ->
val document = FileDocumentManager.getInstance().getDocument(vf)
document?.setText("")
}
}
ChangeType.EDIT, ChangeType.RENAME -> {
// Restore original content
change.originalContent?.let { original ->
val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
virtualFile?.let { vf ->
val document = FileDocumentManager.getInstance().getDocument(vf)
document?.setText("")
}
}
ChangeType.EDIT, ChangeType.RENAME -> {
// Restore original content
change.originalContent?.let { original ->
val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
virtualFile?.let { vf ->
val document = FileDocumentManager.getInstance().getDocument(vf)
document?.setText(original)
}
document?.setText(original)
}
}
ChangeType.DELETE -> {
// For deleted files, we would need to recreate them
change.originalContent?.let { original ->
val parentPath = change.filePath.substringBeforeLast('/')
val fileName = change.filePath.substringAfterLast('/')
val parentDir = LocalFileSystem.getInstance().findFileByPath(parentPath)
parentDir?.let { dir ->
val newFile = dir.createChildData(project, fileName)
val document = FileDocumentManager.getInstance().getDocument(newFile)
document?.setText(original)
}
}
ChangeType.DELETE -> {
// For deleted files, we would need to recreate them
change.originalContent?.let { original ->
val parentPath = change.filePath.substringBeforeLast('/')
val fileName = change.filePath.substringAfterLast('/')
val parentDir = LocalFileSystem.getInstance().findFileByPath(parentPath)
parentDir?.let { dir ->
val newFile = dir.createChildData(project, fileName)
val document = FileDocumentManager.getInstance().getDocument(newFile)
document?.setText(original)
}
}
}
} catch (e: Exception) {
logger.error("Failed to undo change for ${change.filePath}", e)
}
} catch (e: Exception) {
logger.error("Failed to undo change for ${change.filePath}", e)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Refactor duplicated performManualUndo logic into a shared utility.

This function duplicates nearly identical logic from IdeaFileChangeDiffDialog.kt (lines 231-273). Extract the common undo logic into a shared utility function to improve maintainability and ensure consistent behavior across both usages.

Consider creating a shared utility like this:

object FileChangeUndoUtil {
    fun performManualUndo(project: Project, change: FileChange, onSuccess: (() -> Unit)? = null) {
        runWriteAction {
            try {
                when (change.changeType) {
                    ChangeType.CREATE -> {
                        val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
                        virtualFile?.delete(project)
                    }
                    ChangeType.EDIT, ChangeType.RENAME -> {
                        change.originalContent?.let { original ->
                            val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
                            virtualFile?.let { vf ->
                                val document = FileDocumentManager.getInstance().getDocument(vf)
                                document?.setText(original)
                            }
                        }
                    }
                    ChangeType.DELETE -> {
                        change.originalContent?.let { original ->
                            val parentPath = change.filePath.substringBeforeLast('/')
                            val fileName = change.filePath.substringAfterLast('/')
                            val parentDir = LocalFileSystem.getInstance().findFileByPath(parentPath)
                            parentDir?.let { dir ->
                                val newFile = dir.createChildData(project, fileName)
                                val document = FileDocumentManager.getInstance().getDocument(newFile)
                                document?.setText(original)
                            }
                        }
                    }
                }
                onSuccess?.invoke()
            } catch (e: Exception) {
                logger.error("Failed to undo change for ${change.filePath}", e)
            }
        }
    }
}
🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt
around lines 238-281, the performManualUndo implementation duplicates logic
present in IdeaFileChangeDiffDialog.kt; extract the shared undo logic into a
single utility (e.g., object FileChangeUndoUtil with fun
performManualUndo(project: Project, change: FileChange, onSuccess: (() -> Unit)?
= null)) that implements the Create/Edit/Rename/Delete handling (use
VirtualFile.delete(project) for CREATE, restore original text for EDIT/RENAME,
recreate file and set text for DELETE), call runWriteAction inside the utility,
invoke onSuccess after success, log exceptions with the existing logger, replace
the duplicated implementations in both files with calls to this utility, and
update imports/removals accordingly.

Comment on lines +245 to +252
ChangeType.CREATE -> {
// For created files, delete or clear the content
val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
virtualFile?.let { vf ->
val document = FileDocumentManager.getInstance().getDocument(vf)
document?.setText("")
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CREATE case should delete the file, not just clear content.

For ChangeType.CREATE, the current implementation clears the file content with setText(""), leaving an empty file on disk. To properly undo a file creation, the file should be deleted entirely using virtualFile.delete(project).

Apply this diff:

                 ChangeType.CREATE -> {
-                    // For created files, delete or clear the content
+                    // For created files, delete them
                     val virtualFile = LocalFileSystem.getInstance().findFileByPath(change.filePath)
-                    virtualFile?.let { vf ->
-                        val document = FileDocumentManager.getInstance().getDocument(vf)
-                        document?.setText("")
-                    }
+                    virtualFile?.delete(project)
                 }
🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/changes/IdeaFileChangeSummary.kt
around lines 245-252, the CREATE branch currently clears file content instead of
removing the created file; replace the document.setText("") logic with a call to
virtualFile.delete(project) to delete the file from disk (use the surrounding
class's project reference or pass the appropriate Project instance), wrap the
delete in a null-check and try/catch to handle IO exceptions, and refresh the
VFS if needed so the IDE reflects the removed file.

- Remove redundant trim() call in MarkdownRenderer (filterDevinTags already trims)
- Optimize useMemo in DevInRenderer to skip parsing during streaming
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (2)

66-76: Consider extracting the truncation length as a constant.

The hardcoded 50 on line 72 could be extracted as a named constant (e.g., MAX_PARAM_DISPLAY_LENGTH) for easier maintenance.

+const MAX_PARAM_DISPLAY_LENGTH = 50;
+
 function formatToolCallDetails(params: Record<string, string>): string {
   return Object.entries(params)
     .map(([key, value]) => {
-      const truncated = value.length > 50 ? value.slice(0, 50) + '...' : value;
+      const truncated = value.length > MAX_PARAM_DISPLAY_LENGTH 
+        ? value.slice(0, MAX_PARAM_DISPLAY_LENGTH) + '...' 
+        : value;
       return `${key}="${truncated}"`;
     })
     .join(' ');
 }

114-135: Consider making FILE_TOOLS configurable for extensibility.

The hardcoded FILE_TOOLS list (line 115) works for current needs, but if tool extensibility is planned, consider accepting this as a prop or configuration.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9addeb and e5b96b4.

📒 Files selected for processing (2)
  • mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (1 hunks)
  • mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)
mpp-vscode/webview/src/components/sketch/index.ts (1)
  • MarkdownRenderer (17-17)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (2)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/impl/PlanManagementTool.kt (1)
  • action (25-37)
mpp-vscode/webview/src/components/sketch/index.ts (1)
  • DevInRenderer (23-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build (241)
  • GitHub Check: Build (223)
🔇 Additional comments (4)
mpp-vscode/webview/src/components/sketch/DevInRenderer.tsx (3)

22-64: Parser limitation appropriately documented.

The documented limitation regarding escaped quotes in parameter values (lines 26-28) addresses the previous review concern. For the current use case, this simple regex-based approach is sufficient.


78-106: LGTM: Efficient rendering with useMemo optimization.

The useMemo hook appropriately skips parsing during streaming (line 84-87), and the early return for incomplete/empty blocks (line 90-92) prevents unnecessary rendering.


144-186: LGTM: Accessibility and UX concerns addressed.

The conditional clickable class (line 146) addresses the previous cursor styling concern, and the comprehensive ARIA attributes (lines 147, 159-160, 169-171, 162, 173) properly support assistive technologies. Event propagation is correctly managed with stopPropagation calls (lines 131, 138).

mpp-vscode/webview/src/components/sketch/MarkdownRenderer.tsx (1)

17-30: LGTM: Previous review concerns resolved.

The regex improvements address both prior comments:

  • Line 26 correctly removes the g flag since $ only matches end of string
  • Line 28 now uses ^\s*<\/devin>\s*$/gim to match only standalone closing tags on their own line

Note: DevIn tags within code examples or documentation will still be filtered. This is likely acceptable for the current use case.

@phodal phodal merged commit ee06e30 into master Dec 4, 2025
4 of 5 checks passed
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