Skip to content

feat(webedit): add WebEdit agent for web page browsing and DOM selection#512

Merged
phodal merged 4 commits intomasterfrom
feat/webedit-agent
Dec 12, 2025
Merged

feat(webedit): add WebEdit agent for web page browsing and DOM selection#512
phodal merged 4 commits intomasterfrom
feat/webedit-agent

Conversation

@phodal
Copy link
Owner

@phodal phodal commented Dec 12, 2025

Summary

Implements WebEdit agent for browsing web pages and selecting DOM elements.

Closes #511

Changes

New Files

mpp-viewer-web (WebEdit Bridge Layer):

  • DOMElement.kt - Data classes for DOM elements, bounding boxes, and message types
  • WebEditBridge.kt - Common interface for bidirectional WebView-Kotlin communication
  • WebEditBridge.jvm.kt - JVM implementation
  • WebEditBridge.wasmJs.kt - WASM implementation
  • WebEditBridgeScript.kt - JavaScript injection code for DOM selection

mpp-ui (UI Components):

  • WebEditPage.kt - Main page with URL bar, WebView area, DOM sidebar, chat input
  • WebEditToolbar.kt - Top toolbar with navigation controls and mode toggles
  • DOMTreeSidebar.kt - Right sidebar showing DOM tree structure
  • WebEditChatInput.kt - Bottom chat input for Q&A functionality
  • WebEditViewPlaceholder.kt - Placeholder for platform-specific WebView

Modified Files

  • AgentType.kt - Added WEB_EDIT enum value
  • AgentInterfaceRouter.kt - Added routing for WEB_EDIT
  • DesktopTitleBarTabs.kt - Added sidebar visibility handling
  • CodingAgentPage.kt - Added exhaustive when case
  • TopBarMenuDesktop.kt / TopBarMenuMobile.kt - Added icon mapping

Architecture

  • Uses Bridge pattern with expect/actual for platform-specific implementations
  • JavaScript injection via WebEditBridgeScript enables DOM selection in any webpage
  • Message-based communication using sealed classes (WebEditMessage, WebEditCommand)

Testing

  • Build passes (./gradlew :mpp-ui:compileKotlinJvm)
  • Manual testing pending (needs platform-specific WebView implementation)

Summary by CodeRabbit

  • New Features

    • Added WebEdit agent for inspecting and interacting with web pages (URL bar, navigation, selection/highlight, DOM tree, chat input).
    • Cross-platform web-edit bridge and viewer with platform fallbacks.
  • UI/UX Improvements

    • Integrated WebEdit into agent menus/tabs with icon and color.
    • Session sidebar auto-hides in WebEdit mode.
    • IntelliJ plugin: added WebEdit tool pane with embedded browser and DOM inspector.

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

Renamed AgentChatInterface to CodingAgentPage and RemoteAgentChatInterface to RemoteAgentPage to improve naming consistency and code readability across platforms. Updated all usages accordingly.
- Add WEB_EDIT AgentType with display name and parsing support
- Create WebEditBridge abstraction for WebView-Kotlin communication
- Create DOMElement model classes for DOM tree representation
- Create WebEditPage with URL input, WebView placeholder, and DOM sidebar
- Create WebEditToolbar, DOMTreeSidebar, WebEditChatInput components
- Create WebEditBridgeScript for JavaScript injection into WebView
- Add platform-specific bridge implementations (JVM, WASM)
- Update AgentInterfaceRouter to route WEB_EDIT to WebEditPage
- Update exhaustive when expressions in CodingAgentPage and TopBarMenus
Copilot AI review requested due to automatic review settings December 12, 2025 04:30
@coderabbitai
Copy link

coderabbitai bot commented Dec 12, 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 new WEB_EDIT agent and implements a WebEdit feature: DOM models and messages, a WebEditBridge (common + JVM/WASM), JS bridge script, platform WebEditView, UI components (toolbar, page, DOM sidebar, chat input), router and IDE wiring, icons, and build dependency updates.

Changes

Cohort / File(s) Summary
Agent Type Definition
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/AgentType.kt
Added WEB_EDIT enum entry, display name mapping "WebEdit", and parsing aliases "webedit"/"web".
Viewer: DOM & Messaging Models
mpp-viewer-web/src/commonMain/kotlin/.../webedit/DOMElement.kt
Added DOMElement, BoundingBox, WebEditMessage and WebEditCommand sealed classes; all @Serializable with helper getDisplayName().
Viewer: Bridge Interface
mpp-viewer-web/src/commonMain/kotlin/.../webedit/WebEditBridge.kt
Added WebEditBridge interface, WebEditState data class, and expect fun createWebEditBridge().
Viewer: Bridge Implementations
mpp-viewer-web/src/jvmMain/kotlin/.../WebEditBridge.jvm.kt, mpp-viewer-web/src/wasmJsMain/kotlin/.../WebEditBridge.wasmJs.kt
Added JvmWebEditBridge and WasmWebEditBridge with StateFlow-backed state, message handling, JS execution hooks, and actual createWebEditBridge() implementations.
Viewer: JS Bridge Script
mpp-viewer-web/src/commonMain/kotlin/.../WebEditBridgeScript.kt
Added getWebEditBridgeScript() returning injection JS for selection mode, highlighting, DOM extraction, selector generation, and messaging to Kotlin.
Viewer: WebEditView (expect/actual)
mpp-viewer-web/src/commonMain/kotlin/.../WebEditView.kt, .../WebEditView.jvm.kt, .../WebEditView.wasmJs.kt
Added expect fun WebEditView(...) and platform actual implementations: JVM wires WebView + JS handler and injects script; WASM shows placeholder.
UI: WebEdit Components
mpp-ui/src/commonMain/kotlin/.../webedit/WebEditPage.kt, .../WebEditToolbar.kt, .../WebEditChatInput.kt, .../DOMTreeSidebar.kt
Added WebEditPage, WebEditToolbar, WebEditChatInput, and DOMTreeSidebar composables; DOMTreeSidebar exposes DOMTreeItem model and interaction callbacks.
UI: Router & Component Renames
mpp-ui/src/commonMain/kotlin/.../agent/AgentInterfaceRouter.kt, .../agent/CodingAgentPage.kt, .../remote/RemoteAgentChatInterface.kt
Routed WEB_EDIT to WebEditPage; renamed AgentChatInterfaceCodingAgentPage and RemoteAgentChatInterfaceRemoteAgentPage; updated REMOTE/CODING mappings and added WEB_EDIT handling.
UI: Top Bars & Tabs
mpp-ui/src/commonMain/kotlin/.../chat/DesktopTitleBarTabs.kt, TopBarMenuDesktop.kt, TopBarMenuMobile.kt
Treated WEB_EDIT like other agent types in tab logic and icon mapping (uses AutoDevComposeIcons.Language); DesktopTitleBarTabs hides sidebar for WEB_EDIT.
IDE Integration (Idea)
mpp-idea/src/main/kotlin/.../IdeaAgentTabsHeader.kt, IdeaAgentApp.kt, toolwindow/webedit/*
Added WEB_EDIT tab and visuals; introduced IdeaWebEditBridgeScript, IdeaWebEditModels, IdeaWebEditContent, IdeaWebEditViewModel, and icons; integrates WebEdit view model lifecycle and UI in IDEA toolwindow.
IDE Icons Duplication
mpp-idea/src/main/kotlin/.../IdeaComposeIcons.kt
Added ArrowBack/ArrowForward/TouchApp/Web icons — duplicated declarations present (two identical sets).
Build & Imports
mpp-ui/build.gradle.kts, mpp-ui/src/androidMain/.../AutoDevApp.android.kt, mpp-ui/src/appleMain/.../AutoDevApp.apple.kt
Added mpp-viewer-web and compose.webview dependencies; removed unused imports in AutoDevApp.android/apple.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as WebEditPage
    participant Bridge as WebEditBridge
    participant WebView
    participant JS as injected script

    User->>UI: Enter URL / click navigate
    UI->>Bridge: navigateTo(url)
    Bridge->>WebView: load URL / execute JS

    WebView->>JS: page loaded -> injected script runs
    JS->>Bridge: send DOMTreeUpdated / PageLoaded / ElementSelected
    Bridge->>UI: update StateFlow (domTree, selectedElement, isReady)
    UI->>UI: recompose / show DOM sidebar / selection highlight

    User->>UI: toggle selection mode / hover / click element
    UI->>Bridge: setSelectionMode(true/false)
    Bridge->>JS: run selection mode commands
    JS->>Bridge: ElementSelected / highlight events

    User->>UI: ask in chat / send input
    UI->>LLM (outside scope): include page/element context (from Bridge state)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • WebEditBridgeScript.kt (large JS payload, selector logic, safety)
    • WebEditView.jvm.kt (JS message parsing, script injection, lifecycle)
    • IdeaWebEditViewModel and IdeaWebEditContent (JCEF wiring, threading, resource disposal)
    • Icon duplication in IdeaComposeIcons.kt (remove duplicates)
    • Rename impacts (ensure callers updated for renamed composables)

Possibly related PRs

Poem

🐰 I hopped into code with a curious twitch,
Built a little bridge that speaks JS and Kotlin stitch,
Trees of DOM leaves, a toolbar to explore,
Click, highlight, ask — then hop back for more. 🌐✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.51% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature: adding a WebEdit agent for web page browsing and DOM selection, which aligns with the core changes throughout the codebase.
Linked Issues check ✅ Passed The PR successfully implements all primary coding objectives from issue #511: WEB_EDIT agent type added, WebEditBridge abstraction created with JVM/WASM implementations, DOMElement models defined, WebEditBridgeScript for DOM selection provided, and all specified UI components (WebEditPage, WebEditToolbar, DOMTreeSidebar, WebEditChatInput) implemented.
Out of Scope Changes check ✅ Passed All changes are within scope of issue #511. Modifications to existing files (AgentType, AgentInterfaceRouter, DesktopTitleBarTabs, etc.) are necessary integration points for the new WEB_EDIT agent type. No unrelated refactoring or unplanned features detected.
✨ 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/webedit-agent

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

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

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

Review completed. 3 suggestions posted.

Comment augment review to trigger a new review at any time.

sendToKotlin: function(type, data) {
if (window.kmpJsBridge && window.kmpJsBridge.callNative) {
window.kmpJsBridge.callNative('webEditMessage',
JSON.stringify({ type: type, data: data }), function() {});
Copy link

Choose a reason for hiding this comment

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

sendToKotlin JSON-stringifies an object whose data is already a JSON string at the call sites, which makes the payload double-encoded and forces brittle “JSON parse twice” handling later. Consider sending structured JSON once (similar to the existing mermaidRenderCallback usage) to keep the message contract straightforward.

🤖 Was this useful? React with 👍 or 👎

}

override suspend fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
Copy link

Choose a reason for hiding this comment

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

The injected JS builds a single-quoted string from selector but only escapes '; selectors containing backslashes/control characters can break the JS literal (and complicate safe JS injection). Consider a more robust escaping/encoding approach here (also applies to scrollToElement and the wasm bridge).

🤖 Was this useful? React with 👍 or 👎

Copy link

Choose a reason for hiding this comment

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

With the current WebEditViewPlaceholder, onPageLoaded is never invoked, so after onNavigate/onReload sets isLoading = true the progress bar never clears. Until a real WebView implementation is wired in, consider ensuring the placeholder triggers onPageLoaded so the UI doesn’t get stuck in a loading state.

🤖 Was this useful? React with 👍 or 👎

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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/AgentType.kt (1)

60-72: fromString("web") -> WEB_EDIT is likely too generic.
If any existing config/UI passes "web" for something else (or future features add “web” variants), this will silently route to WEB_EDIT. Consider keeping only "webedit" (and maybe "web_edit").

-                "webedit", "web" -> WEB_EDIT
+                "webedit", "web_edit" -> WEB_EDIT
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt (1)

28-34: Verify that AgentChatInterface references in comments and documentation are updated to reflect the new routing architecture.

The rename is functionally complete: CodingAgentPage is correctly called from AgentInterfaceRouter (the single entry point across all platforms: common, Android, Apple). However, outdated references remain in non-breaking locations:

  • AutoDevApp.kt line 151 (comment only)
  • RemoteAgentChatInterface.kt line 24 (documentation comment)
  • Comments in IdeaAgentApp.kt and App.tsx

These are documentation artifacts, not functional issues, since CodingAgentPage is an internal composable dispatched solely through the router. The empty when branches (lines 295–298) are correctly designed as no-ops.

🧹 Nitpick comments (8)
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/AgentType.kt (1)

43-48: Update the enum KDoc to include WEB_EDIT (doc drift).
Right now the header comment lists all modes but omits the newly added WEB_EDIT.

 /**
  * Represents all available agent modes in the system:
  * - LOCAL: Simple local chat mode without heavy tooling
  * - CODING: Local coding agent with full tool access (file system, shell, etc.)
  * - CODE_REVIEW: Dedicated code review agent with git integration
  * - KNOWLEDGE: Document reader mode for AI-native document reading
  * - CHAT_DB: Database chat mode for text-to-SQL interactions
  * - REMOTE: Remote agent connected to mpp-server
+ * - WEB_EDIT: Browse web pages, select DOM elements, and interact with page content
  */
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt (1)

224-299: Avoid silent “should not reach here” branches (including WEB_EDIT)—render a visible fallback.
If routing regresses, this will currently show a partial page with missing/odd UI; a small inline warning helps debugging and avoids confusing UX.

                         AgentType.WEB_EDIT -> {
                             // WEB_EDIT has its own full-page interface
                             // It should not reach here - handled by AgentInterfaceRouter
+                            Text(
+                                text = "WEB_EDIT is handled by AgentInterfaceRouter (unexpected route).",
+                                style = MaterialTheme.typography.bodySmall,
+                                color = MaterialTheme.colorScheme.error,
+                                modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
+                            )
                         }
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt (2)

28-28: Search field is non-functional.

The searchQuery state variable is captured from the search field but is never used to filter the DOM tree items in the LazyColumn. Consider either:

  1. Implementing filtering: sampleDomTree.filter { it.tagName.contains(searchQuery, ignoreCase = true) || it.selector.contains(searchQuery, ignoreCase = true) }
  2. Removing the search field if filtering is deferred to a later phase

Apply this diff to implement basic search filtering:

         // DOM tree list
+        val filteredItems = if (searchQuery.isBlank()) {
+            sampleDomTree
+        } else {
+            sampleDomTree.filter { 
+                it.tagName.contains(searchQuery, ignoreCase = true) || 
+                it.selector.contains(searchQuery, ignoreCase = true)
+            }
+        }
+        
         LazyColumn(
             modifier = Modifier.fillMaxSize(),
             verticalArrangement = Arrangement.spacedBy(2.dp)
         ) {
-            items(sampleDomTree) { item ->
+            items(filteredItems) { item ->
                 DOMTreeItemRow(

Also applies to: 77-88


114-126: Hover state is never updated.

The isHovered state variable is declared but never set to true. The onHover callback is invoked on click but doesn't update the local hover state. To enable hover visual feedback, you need to add a pointer hover modifier.

Apply this diff to enable hover visual feedback:

+import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
+
 @Composable
 private fun DOMTreeItemRow(
     item: DOMTreeItem,
     onClick: () -> Unit,
     onHover: (Boolean) -> Unit,
     modifier: Modifier = Modifier
 ) {
-    var isHovered by remember { mutableStateOf(false) }
+    val interactionSource = remember { MutableInteractionSource() }
+    val isHovered by interactionSource.collectIsHoveredAsState()
+    
+    LaunchedEffect(isHovered) {
+        onHover(isHovered)
+    }
     
     Surface(
         modifier = modifier
             .fillMaxWidth()
+            .hoverable(interactionSource)
             .clickable { onClick() }
             .padding(start = (item.depth * 12).dp),
mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/DOMElement.kt (1)

29-34: Consider edge cases in display name generation.

The getDisplayName() method has a few potential issues:

  • Line 30: Taking only the first class from split(" ") may not represent multi-class elements well
  • Line 32: The 30-character truncation is arbitrary and may cut off meaningful content mid-word
  • Consider adding ellipsis ("...") to indicate truncation
 fun getDisplayName(): String {
     val classAttr = attributes["class"]?.split(" ")?.firstOrNull()?.let { ".$it" } ?: ""
     val idAttr = attributes["id"]?.let { "#$it" } ?: ""
-    val text = textContent?.take(30)?.let { " \"$it\"" } ?: ""
+    val text = textContent?.take(30)?.let { 
+        val truncated = if (textContent.length > 30) "$it..." else it
+        " \"$truncated\""
+    } ?: ""
     return "$tagName$idAttr$classAttr$text"
 }
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt (1)

126-137: Public state setters bypass the message handler pattern.

The public setUrl(), setTitle(), and setLoading() methods directly mutate state, bypassing the handleMessage() flow. This could lead to inconsistent state updates and makes the bridge harder to reason about.

Consider making these methods internal or private, or route all state changes through handleMessage() for consistency:

-fun setUrl(url: String) {
+internal fun setUrl(url: String) {
     _currentUrl.value = url
 }
 
-fun setTitle(title: String) {
+internal fun setTitle(title: String) {
     _pageTitle.value = title
 }
 
-fun setLoading(loading: Boolean) {
+internal fun setLoading(loading: Boolean) {
     _isLoading.value = loading
 }
mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt (2)

119-129: Public state setters bypass the message handler pattern.

Same architectural concern as JVM implementation - these methods bypass handleMessage().

Consider making these internal:

-fun setUrl(url: String) {
+internal fun setUrl(url: String) {
     _currentUrl.value = url
 }
 
-fun setTitle(title: String) {
+internal fun setTitle(title: String) {
     _pageTitle.value = title
 }
 
-fun setLoading(loading: Boolean) {
+internal fun setLoading(loading: Boolean) {
     _isLoading.value = loading
 }

10-130: Consider reducing code duplication with JVM implementation.

The WASM and JVM implementations are nearly identical (~95% duplicate code). While the expect/actual pattern requires separate files, consider extracting common logic to a shared abstract base class or internal helper.

Example approach:

// In commonMain
internal abstract class BaseWebEditBridge : WebEditBridge {
    protected val _currentUrl = MutableStateFlow("")
    override val currentUrl: StateFlow<String> = _currentUrl.asStateFlow()
    // ... other common state and logic
    
    protected abstract fun executeJavaScriptImpl(script: String)
    protected abstract fun navigateImpl(url: String)
    // ... other platform-specific hooks
}

// In jvmMain/wasmJsMain
class JvmWebEditBridge : BaseWebEditBridge() {
    override fun executeJavaScriptImpl(script: String) { /* platform code */ }
}

This would eliminate duplication while preserving platform flexibility. However, if you anticipate significant platform-specific divergence, the current approach is acceptable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 168a789 and c06450f.

📒 Files selected for processing (19)
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/AgentType.kt (3 hunks)
  • mpp-ui/src/androidMain/kotlin/cc/unitmesh/devins/ui/compose/AutoDevApp.android.kt (0 hunks)
  • mpp-ui/src/appleMain/kotlin/cc/unitmesh/devins/ui/compose/AutoDevApp.apple.kt (0 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt (3 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt (2 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditChatInput.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditViewPlaceholder.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/DesktopTitleBarTabs.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuDesktop.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuMobile.kt (2 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt (1 hunks)
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/DOMElement.kt (1 hunks)
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.kt (1 hunks)
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt (1 hunks)
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt (1 hunks)
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt (1 hunks)
💤 Files with no reviewable changes (2)
  • mpp-ui/src/appleMain/kotlin/cc/unitmesh/devins/ui/compose/AutoDevApp.apple.kt
  • mpp-ui/src/androidMain/kotlin/cc/unitmesh/devins/ui/compose/AutoDevApp.android.kt
🧰 Additional context used
📓 Path-based instructions (2)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual pattern for platform-specific code (for example: Platform implementations)
Use concrete classes (not interfaces) and Promise (not Flow) for @JsExport annotations
Avoid emoji and UTF-8 characters in WASM code
When modifying CodingAgentRenderer, update ALL implementations: DefaultCodingAgentRenderer, ComposeRenderer, JewelRenderer, ServerSideRenderer, and JsRendererAdapter in Kotlin
When modifying CodingAgentRenderer, update JVM CLI implementations: CodingCliRenderer and ConsoleRenderer
Use AutoDevColors or MaterialTheme.colorScheme for Compose styling

Files:

  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditViewPlaceholder.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditChatInput.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/DesktopTitleBarTabs.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/AgentType.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuDesktop.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/DOMElement.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuMobile.kt
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.kt
**/*.{kt,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never use hardcoded colors - always use design tokens from the design system

Files:

  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditViewPlaceholder.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditChatInput.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/DesktopTitleBarTabs.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/AgentType.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuDesktop.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/DOMElement.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuMobile.kt
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.kt
🧠 Learnings (3)
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to mpp-vscode/src/**/*.ts : When modifying `CodingAgentRenderer`, update VSCode implementations: `mpp-vscode/src/bridge/mpp-core.ts` and `mpp-vscode/src/providers/chat-view.ts`

Applied to files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to **/*.kt : When modifying `CodingAgentRenderer`, update ALL implementations: `DefaultCodingAgentRenderer`, `ComposeRenderer`, `JewelRenderer`, `ServerSideRenderer`, and `JsRendererAdapter` in Kotlin

Applied to files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/DesktopTitleBarTabs.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuDesktop.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuMobile.kt
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to **/*.kt : When modifying `CodingAgentRenderer`, update JVM CLI implementations: `CodingCliRenderer` and `ConsoleRenderer`

Applied to files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt
🧬 Code graph analysis (1)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt (4)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt (1)
  • RemoteAgentPage (27-290)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt (1)
  • WebEditPage (26-137)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt (1)
  • onAgentTypeChange (223-236)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt (1)
  • CodingAgentPage (28-507)
⏰ 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
  • GitHub Check: Test mpp-core (KMP)
🔇 Additional comments (12)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/remote/RemoteAgentChatInterface.kt (1)

28-28: Good rename for consistency, and all call sites are properly updated.

The rename from RemoteAgentChatInterface to RemoteAgentPage improves naming consistency with other page composables introduced in this PR (e.g., WebEditPage, CodingAgentPage). The "Page" suffix is more idiomatic for Compose UI components. Verification confirms all call sites have been updated and no references to the old function name remain in the codebase.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/DesktopTitleBarTabs.kt (1)

34-41: WEB_EDIT hiding the session sidebar: consistent and clear.
This keeps layout aligned with other full-page modes (CODE_REVIEW/KNOWLEDGE).

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuMobile.kt (1)

220-230: WEB_EDIT icon mapping looks good (keeps when exhaustive).

Also applies to: 256-266

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/chat/TopBarMenuDesktop.kt (1)

290-290: LGTM! Icon mapping for WEB_EDIT is appropriate.

The Language icon is a fitting choice for the web editing agent type, and the implementation correctly uses the design system icon (AutoDevComposeIcons) rather than hardcoded values.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/AgentInterfaceRouter.kt (1)

133-142: LGTM! WebEdit routing implemented correctly.

The WEB_EDIT agent type is properly integrated into the router with appropriate parameters (llmService, onBack navigation to CODING, onNotification). The pattern is consistent with other specialized agent types like CODE_REVIEW and KNOWLEDGE.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditViewPlaceholder.kt (1)

20-26: Placeholder implementation aligns with PR objectives.

The onPageLoaded and onElementSelected callbacks are defined but not invoked in this placeholder. This is expected as the PR objectives list "Create platform-specific WebView implementation for JVM" as a remaining TODO. The callbacks will be wired when the actual WebView implementation is added.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditChatInput.kt (1)

20-84: LGTM! Clean chat input implementation.

The component properly uses Material Theme design tokens for colors and includes appropriate validation to prevent sending blank messages. The keyboard actions and button states are correctly synchronized.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt (1)

60-80: Browser history navigation is placeholder.

The Back and Forward browser navigation buttons currently have empty onClick handlers. This is expected given that the platform-specific WebView implementation is pending (per PR objectives). These will need to be wired to actual WebView navigation methods when the JVM WebView implementation is added.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt (1)

128-132: LLM integration is scaffolded for future implementation.

The chat input's onSend handler currently only clears the input field. Per PR objectives, "Connect LLM service for page/element Q&A functionality" is listed as a remaining TODO. The llmService parameter is already wired through the function signature, ready for integration.

mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.kt (1)

13-132: LGTM! Clean bridge architecture using expect/actual pattern.

The WebEditBridge interface provides a well-designed contract for platform-specific WebView communication:

  • Uses StateFlow for reactive state properties (appropriate for Compose)
  • Declares async operations as suspend functions
  • Follows the expect/actual pattern per coding guidelines
  • Clean separation between observable state and actions

The interface is ready for platform-specific implementations (JVM and WASM implementations are present per PR summary).

mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/DOMElement.kt (1)

16-118: LGTM! Well-structured data models.

The data classes and sealed hierarchies are well-designed for DOM representation and bidirectional messaging. Proper use of @Serializable, nullable types, and default parameters ensures flexibility and serialization support.

mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt (1)

10-61: Well-structured state management and navigation.

The StateFlow pattern is correctly implemented with proper encapsulation (private mutable, public read-only). Navigation methods appropriately update loading state and delegate to callbacks.

Comment on lines +11 to +170
fun getWebEditBridgeScript(): String = """
(function() {
// Prevent multiple injections
if (window.webEditBridge) return;

// WebEdit Bridge object
window.webEditBridge = {
selectionMode: false,
highlightedElement: null,
selectedElement: null,

// Enable/disable selection mode
setSelectionMode: function(enabled) {
this.selectionMode = enabled;
if (!enabled) {
this.clearHighlights();
}
console.log('[WebEditBridge] Selection mode:', enabled);
},

// Highlight a specific element by selector
highlightElement: function(selector) {
this.clearHighlights();
try {
const el = document.querySelector(selector);
if (el) {
this.highlightedElement = el;
el.style.outline = '2px solid #2196F3';
el.style.outlineOffset = '2px';
}
} catch(e) {
console.error('[WebEditBridge] highlightElement error:', e);
}
},

// Clear all highlights
clearHighlights: function() {
if (this.highlightedElement) {
this.highlightedElement.style.outline = '';
this.highlightedElement.style.outlineOffset = '';
this.highlightedElement = null;
}
if (this.selectedElement) {
this.selectedElement.style.outline = '';
this.selectedElement.style.outlineOffset = '';
}
},

// Scroll to element
scrollToElement: function(selector) {
try {
const el = document.querySelector(selector);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} catch(e) {
console.error('[WebEditBridge] scrollToElement error:', e);
}
},

// Get unique selector for element
getSelector: function(el) {
if (el.id) return '#' + el.id;
if (el === document.body) return 'body';
if (el === document.documentElement) return 'html';

const path = [];
while (el && el.nodeType === Node.ELEMENT_NODE) {
let selector = el.tagName.toLowerCase();
if (el.id) {
selector = '#' + el.id;
path.unshift(selector);
break;
} else if (el.className && typeof el.className === 'string') {
const classes = el.className.trim().split(/\\s+/).slice(0, 2);
if (classes.length > 0 && classes[0]) {
selector += '.' + classes.join('.');
}
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(' > ');
},

// Get DOM tree (simplified)
getDOMTree: function() {
function buildTree(el, depth) {
if (depth > 5) return null;
const rect = el.getBoundingClientRect();
const node = {
id: Math.random().toString(36).substr(2, 9),
tagName: el.tagName.toLowerCase(),
selector: window.webEditBridge.getSelector(el),
textContent: (el.textContent || '').trim().substring(0, 50),
attributes: {},
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
children: []
};
if (el.id) node.attributes.id = el.id;
if (el.className) node.attributes.class = el.className;
Array.from(el.children).slice(0, 20).forEach(child => {
const childNode = buildTree(child, depth + 1);
if (childNode) node.children.push(childNode);
});
return node;
}
const tree = buildTree(document.body, 0);
this.sendToKotlin('DOMTreeUpdated', JSON.stringify({ root: tree }));
},

// Send message to Kotlin
sendToKotlin: function(type, data) {
if (window.kmpJsBridge && window.kmpJsBridge.callNative) {
window.kmpJsBridge.callNative('webEditMessage',
JSON.stringify({ type: type, data: data }), function() {});
}
}
};

// Mouse event handlers for selection mode
document.addEventListener('mouseover', function(e) {
if (!window.webEditBridge.selectionMode) return;
e.stopPropagation();
window.webEditBridge.clearHighlights();
window.webEditBridge.highlightedElement = e.target;
e.target.style.outline = '2px solid #2196F3';
e.target.style.outlineOffset = '2px';
}, true);

document.addEventListener('click', function(e) {
if (!window.webEditBridge.selectionMode) return;
e.preventDefault();
e.stopPropagation();
const el = e.target;
window.webEditBridge.selectedElement = el;
el.style.outline = '3px solid #4CAF50';
el.style.outlineOffset = '2px';
const rect = el.getBoundingClientRect();
window.webEditBridge.sendToKotlin('ElementSelected', JSON.stringify({
element: {
id: Math.random().toString(36).substr(2, 9),
tagName: el.tagName.toLowerCase(),
selector: window.webEditBridge.getSelector(el),
textContent: (el.textContent || '').trim().substring(0, 100),
attributes: { id: el.id || '', class: el.className || '' },
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
}
}));
}, true);

// Notify page loaded
window.webEditBridge.sendToKotlin('PageLoaded', JSON.stringify({
url: window.location.href,
title: document.title
}));

console.log('[WebEditBridge] Initialized');
})();
""".trimIndent()
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

Selection UX bug: hover clears selection highlight (and selection won’t persist).
document.mouseover calls clearHighlights(), which clears selectedElement outline too—so after selecting, moving the mouse removes the selection indicator. Split hover vs selected state.

-        // Clear all highlights
-        clearHighlights: function() {
+        // Clear hover highlight only (keep selection)
+        clearHoverHighlight: function() {
             if (this.highlightedElement) {
                 this.highlightedElement.style.outline = '';
                 this.highlightedElement.style.outlineOffset = '';
                 this.highlightedElement = null;
             }
-            if (this.selectedElement) {
-                this.selectedElement.style.outline = '';
-                this.selectedElement.style.outlineOffset = '';
-            }
         },
+
+        // Clear selection highlight
+        clearSelection: function() {
+            if (this.selectedElement) {
+                this.selectedElement.style.outline = '';
+                this.selectedElement.style.outlineOffset = '';
+                this.selectedElement = null;
+            }
+        },
         setSelectionMode: function(enabled) {
             this.selectionMode = enabled;
             if (!enabled) {
-                this.clearHighlights();
+                this.clearHoverHighlight();
+                this.clearSelection();
             }
             console.log('[WebEditBridge] Selection mode:', enabled);
         },
     document.addEventListener('mouseover', function(e) {
         if (!window.webEditBridge.selectionMode) return;
-        e.stopPropagation();
-        window.webEditBridge.clearHighlights();
+        window.webEditBridge.clearHoverHighlight();
         window.webEditBridge.highlightedElement = e.target;
         e.target.style.outline = '2px solid #2196F3';
         e.target.style.outlineOffset = '2px';
     }, true);
📝 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
fun getWebEditBridgeScript(): String = """
(function() {
// Prevent multiple injections
if (window.webEditBridge) return;
// WebEdit Bridge object
window.webEditBridge = {
selectionMode: false,
highlightedElement: null,
selectedElement: null,
// Enable/disable selection mode
setSelectionMode: function(enabled) {
this.selectionMode = enabled;
if (!enabled) {
this.clearHighlights();
}
console.log('[WebEditBridge] Selection mode:', enabled);
},
// Highlight a specific element by selector
highlightElement: function(selector) {
this.clearHighlights();
try {
const el = document.querySelector(selector);
if (el) {
this.highlightedElement = el;
el.style.outline = '2px solid #2196F3';
el.style.outlineOffset = '2px';
}
} catch(e) {
console.error('[WebEditBridge] highlightElement error:', e);
}
},
// Clear all highlights
clearHighlights: function() {
if (this.highlightedElement) {
this.highlightedElement.style.outline = '';
this.highlightedElement.style.outlineOffset = '';
this.highlightedElement = null;
}
if (this.selectedElement) {
this.selectedElement.style.outline = '';
this.selectedElement.style.outlineOffset = '';
}
},
// Scroll to element
scrollToElement: function(selector) {
try {
const el = document.querySelector(selector);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} catch(e) {
console.error('[WebEditBridge] scrollToElement error:', e);
}
},
// Get unique selector for element
getSelector: function(el) {
if (el.id) return '#' + el.id;
if (el === document.body) return 'body';
if (el === document.documentElement) return 'html';
const path = [];
while (el && el.nodeType === Node.ELEMENT_NODE) {
let selector = el.tagName.toLowerCase();
if (el.id) {
selector = '#' + el.id;
path.unshift(selector);
break;
} else if (el.className && typeof el.className === 'string') {
const classes = el.className.trim().split(/\\s+/).slice(0, 2);
if (classes.length > 0 && classes[0]) {
selector += '.' + classes.join('.');
}
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(' > ');
},
// Get DOM tree (simplified)
getDOMTree: function() {
function buildTree(el, depth) {
if (depth > 5) return null;
const rect = el.getBoundingClientRect();
const node = {
id: Math.random().toString(36).substr(2, 9),
tagName: el.tagName.toLowerCase(),
selector: window.webEditBridge.getSelector(el),
textContent: (el.textContent || '').trim().substring(0, 50),
attributes: {},
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
children: []
};
if (el.id) node.attributes.id = el.id;
if (el.className) node.attributes.class = el.className;
Array.from(el.children).slice(0, 20).forEach(child => {
const childNode = buildTree(child, depth + 1);
if (childNode) node.children.push(childNode);
});
return node;
}
const tree = buildTree(document.body, 0);
this.sendToKotlin('DOMTreeUpdated', JSON.stringify({ root: tree }));
},
// Send message to Kotlin
sendToKotlin: function(type, data) {
if (window.kmpJsBridge && window.kmpJsBridge.callNative) {
window.kmpJsBridge.callNative('webEditMessage',
JSON.stringify({ type: type, data: data }), function() {});
}
}
};
// Mouse event handlers for selection mode
document.addEventListener('mouseover', function(e) {
if (!window.webEditBridge.selectionMode) return;
e.stopPropagation();
window.webEditBridge.clearHighlights();
window.webEditBridge.highlightedElement = e.target;
e.target.style.outline = '2px solid #2196F3';
e.target.style.outlineOffset = '2px';
}, true);
document.addEventListener('click', function(e) {
if (!window.webEditBridge.selectionMode) return;
e.preventDefault();
e.stopPropagation();
const el = e.target;
window.webEditBridge.selectedElement = el;
el.style.outline = '3px solid #4CAF50';
el.style.outlineOffset = '2px';
const rect = el.getBoundingClientRect();
window.webEditBridge.sendToKotlin('ElementSelected', JSON.stringify({
element: {
id: Math.random().toString(36).substr(2, 9),
tagName: el.tagName.toLowerCase(),
selector: window.webEditBridge.getSelector(el),
textContent: (el.textContent || '').trim().substring(0, 100),
attributes: { id: el.id || '', class: el.className || '' },
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
}
}));
}, true);
// Notify page loaded
window.webEditBridge.sendToKotlin('PageLoaded', JSON.stringify({
url: window.location.href,
title: document.title
}));
console.log('[WebEditBridge] Initialized');
})();
""".trimIndent()
fun getWebEditBridgeScript(): String = """
(function() {
// Prevent multiple injections
if (window.webEditBridge) return;
// WebEdit Bridge object
window.webEditBridge = {
selectionMode: false,
highlightedElement: null,
selectedElement: null,
// Enable/disable selection mode
setSelectionMode: function(enabled) {
this.selectionMode = enabled;
if (!enabled) {
this.clearHoverHighlight();
this.clearSelection();
}
console.log('[WebEditBridge] Selection mode:', enabled);
},
// Highlight a specific element by selector
highlightElement: function(selector) {
this.clearHoverHighlight();
try {
const el = document.querySelector(selector);
if (el) {
this.highlightedElement = el;
el.style.outline = '2px solid #2196F3';
el.style.outlineOffset = '2px';
}
} catch(e) {
console.error('[WebEditBridge] highlightElement error:', e);
}
},
// Clear hover highlight only (keep selection)
clearHoverHighlight: function() {
if (this.highlightedElement) {
this.highlightedElement.style.outline = '';
this.highlightedElement.style.outlineOffset = '';
this.highlightedElement = null;
}
},
// Clear selection highlight
clearSelection: function() {
if (this.selectedElement) {
this.selectedElement.style.outline = '';
this.selectedElement.style.outlineOffset = '';
this.selectedElement = null;
}
},
// Scroll to element
scrollToElement: function(selector) {
try {
const el = document.querySelector(selector);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} catch(e) {
console.error('[WebEditBridge] scrollToElement error:', e);
}
},
// Get unique selector for element
getSelector: function(el) {
if (el.id) return '#' + el.id;
if (el === document.body) return 'body';
if (el === document.documentElement) return 'html';
const path = [];
while (el && el.nodeType === Node.ELEMENT_NODE) {
let selector = el.tagName.toLowerCase();
if (el.id) {
selector = '#' + el.id;
path.unshift(selector);
break;
} else if (el.className && typeof el.className === 'string') {
const classes = el.className.trim().split(/\\s+/).slice(0, 2);
if (classes.length > 0 && classes[0]) {
selector += '.' + classes.join('.');
}
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(' > ');
},
// Get DOM tree (simplified)
getDOMTree: function() {
function buildTree(el, depth) {
if (depth > 5) return null;
const rect = el.getBoundingClientRect();
const node = {
id: Math.random().toString(36).substr(2, 9),
tagName: el.tagName.toLowerCase(),
selector: window.webEditBridge.getSelector(el),
textContent: (el.textContent || '').trim().substring(0, 50),
attributes: {},
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
children: []
};
if (el.id) node.attributes.id = el.id;
if (el.className) node.attributes.class = el.className;
Array.from(el.children).slice(0, 20).forEach(child => {
const childNode = buildTree(child, depth + 1);
if (childNode) node.children.push(childNode);
});
return node;
}
const tree = buildTree(document.body, 0);
this.sendToKotlin('DOMTreeUpdated', JSON.stringify({ root: tree }));
},
// Send message to Kotlin
sendToKotlin: function(type, data) {
if (window.kmpJsBridge && window.kmpJsBridge.callNative) {
window.kmpJsBridge.callNative('webEditMessage',
JSON.stringify({ type: type, data: data }), function() {});
}
}
};
// Mouse event handlers for selection mode
document.addEventListener('mouseover', function(e) {
if (!window.webEditBridge.selectionMode) return;
window.webEditBridge.clearHoverHighlight();
window.webEditBridge.highlightedElement = e.target;
e.target.style.outline = '2px solid #2196F3';
e.target.style.outlineOffset = '2px';
}, true);
document.addEventListener('click', function(e) {
if (!window.webEditBridge.selectionMode) return;
e.preventDefault();
e.stopPropagation();
const el = e.target;
window.webEditBridge.selectedElement = el;
el.style.outline = '3px solid #4CAF50';
el.style.outlineOffset = '2px';
const rect = el.getBoundingClientRect();
window.webEditBridge.sendToKotlin('ElementSelected', JSON.stringify({
element: {
id: Math.random().toString(36).substr(2, 9),
tagName: el.tagName.toLowerCase(),
selector: window.webEditBridge.getSelector(el),
textContent: (el.textContent || '').trim().substring(0, 100),
attributes: { id: el.id || '', class: el.className || '' },
boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
}
}));
}, true);
// Notify page loaded
window.webEditBridge.sendToKotlin('PageLoaded', JSON.stringify({
url: window.location.href,
title: document.title
}));
console.log('[WebEditBridge] Initialized');
})();
""".trimIndent()
🤖 Prompt for AI Agents
In
mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt
around lines 11 to 170, the mouseover handler calls clearHighlights() which also
clears selectedElement outline so a prior selection is lost on hover; modify
clearHighlights() or the mouseover logic so it only clears temporary hover
highlighting (highlightedElement) and does not reset or remove styles from
selectedElement, and ensure selection persists by leaving
selectedElement.style.outline and outlineOffset untouched when clearing hover,
while still clearing previous hover element and updating highlightedElement
appropriately.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding getWebEditBridgeScript call sites ==="
rg -n --type=kt -C2 'getWebEditBridgeScript\s*\(' || echo "No call sites found (pattern may need adjustment)"

echo ""
echo "=== Finding all substr occurrences in the file ==="
rg -n 'substr' mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt

echo ""
echo "=== Finding all hardcoded hex colors in the file ==="
rg -n '#[0-9A-Fa-f]{6}' mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt

echo ""
echo "=== File location and size ==="
find . -name "WebEditBridgeScript.kt" -type f

Repository: phodal/auto-dev

Length of output: 907


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking for other getWebEditBridgeScript usages across the codebase ==="
rg -n 'getWebEditBridgeScript' --type=kt | head -20

echo ""
echo "=== Checking if there are similar bridge/script functions ==="
rg -n 'fun get.*BridgeScript.*String' --type=kt | head -10

Repository: phodal/auto-dev

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding getWebEditBridgeScript usages (broader search) ==="
rg -n 'getWebEditBridgeScript' 

echo ""
echo "=== Checking the complete function signature area ==="
head -20 mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt

echo ""
echo "=== Verifying substr deprecation in JavaScript ==="
echo "JavaScript substr() is deprecated. slice() is the modern replacement."
echo "substr(2, 9) → slice(2, 11) (need to convert length to end index)"
</script>

Repository: phodal/auto-dev

Length of output: 1162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding all references to this Kotlin file ==="
rg -l 'WebEditBridgeScript' --type-list | xargs rg -l 'WebEditBridgeScript' 2>/dev/null || rg -l 'WebEditBridgeScript'

echo ""
echo "=== Checking imports ==="
rg 'import.*WebEditBridgeScript|from.*WebEditBridgeScript'

echo ""
echo "=== Looking for class/object that contains this function ==="
head -30 mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt

Repository: phodal/auto-dev

Length of output: 724


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for WebEditBridgeScript in all files ==="
find . -type f \( -name "*.kt" -o -name "*.kts" -o -name "*.java" \) -exec grep -l "WebEditBridgeScript" {} \;

echo ""
echo "=== Search for getWebEditBridgeScript in all files ==="
find . -type f -exec grep -l "getWebEditBridgeScript" {} \; 2>/dev/null | head -20

echo ""
echo "=== Check what directory structure exists ==="
ls -la mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/

Repository: phodal/auto-dev

Length of output: 712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Looking for where this script might be used ==="
rg -i 'webEditBridge|WebEditBridge' --max-count=20

echo ""
echo "=== Checking if there's a test file ==="
find . -path "*test*" -name "*WebEdit*" -type f

echo ""
echo "=== Check the complete file size ==="
wc -l mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt

Repository: phodal/auto-dev

Length of output: 5847


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Reading WebEditBridge.kt to see the interface ==="
cat -n mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.kt

echo ""
echo "=== Checking if getWebEditBridgeScript is exported or public ==="
grep -n "getWebEditBridgeScript\|@Export\|public\|actual" mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt | head -20

Repository: phodal/auto-dev

Length of output: 3870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if getWebEditBridgeScript might be in a test or build file ==="
find . -type f -name "*.kt" -o -name "*.gradle" | xargs grep -l "getWebEditBridgeScript" 2>/dev/null

echo ""
echo "=== Check gradle or build files that might reference scripts ==="
find . -name "build.gradle*" -o -name "gradle*" -type f | head -10

Repository: phodal/auto-dev

Length of output: 226


Replace deprecated substr with slice and use CSS variables for colors.

The file has two instances of deprecated substr() (lines 102, 152) that should use slice() instead. For hardcoded colors (#2196F3, #4CAF50), inject them as CSS variables rather than function parameters to avoid breaking existing callers:

 fun getWebEditBridgeScript(): String = """
 (function() {
+    // Define color constants as CSS variables for theming
+    const style = document.createElement('style');
+    style.textContent = `
+        :root {
+            --web-edit-hover-color: #2196F3;
+            --web-edit-selected-color: #4CAF50;
+        }
+    `;
+    document.head.appendChild(style);
+    
     // Prevent multiple injections
     if (window.webEditBridge) return;
     
     // WebEdit Bridge object
     window.webEditBridge = {
         selectionMode: false,
         highlightedElement: null,
         selectedElement: null,
         
         // Enable/disable selection mode
         setSelectionMode: function(enabled) {
             this.selectionMode = enabled;
             if (!enabled) {
                 this.clearHighlights();
             }
             console.log('[WebEditBridge] Selection mode:', enabled);
         },
         
         // Highlight a specific element by selector
         highlightElement: function(selector) {
             this.clearHighlights();
             try {
                 const el = document.querySelector(selector);
                 if (el) {
                     this.highlightedElement = el;
-                    el.style.outline = '2px solid #2196F3';
+                    el.style.outline = 'var(--web-edit-hover-color) 2px solid';
                     el.style.outlineOffset = '2px';
                 }
             } catch(e) {
                 console.error('[WebEditBridge] highlightElement error:', e);
             }
         },

And replace both substr(2, 9) with slice(2, 11) at lines 102 and 152.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt
around lines 11-170, replace deprecated uses of substr(2, 9) at the two
occurrences (originally around lines 102 and 152) with slice(2, 11), and remove
hardcoded outline colors (#2196F3 and #4CAF50) by using CSS variables instead:
ensure the script references CSS vars (e.g. var(--webedit-highlight-color) and
var(--webedit-selected-color)) when setting element.style.outline and provide
safe defaults by injecting a small <style> into document.head that defines those
variables if they are not already set.

Comment on lines +69 to +72
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
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

JavaScript injection risk: insufficient escaping.

The selector escaping only handles single quotes, leaving the code vulnerable to injection if selectors contain backticks, double quotes, newlines, or other special characters.

Consider using a more robust escaping approach:

 override suspend fun highlightElement(selector: String) {
-    val escapedSelector = selector.replace("'", "\\'")
-    val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
+    val escapedSelector = selector
+        .replace("\\", "\\\\")
+        .replace("'", "\\'")
+        .replace("\"", "\\\"")
+        .replace("\n", "\\n")
+        .replace("\r", "\\r")
+    val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
     executeJavaScript?.invoke(script)
 }

Alternatively, consider using JSON encoding for safer serialization.

📝 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
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector
.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
}
🤖 Prompt for AI Agents
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt
lines 69-72: the current escaping only replaces single quotes and leaves the
injected selector vulnerable to backticks, double quotes, newlines and other
characters; replace the manual replace with a robust serialization (e.g. JSON
string encoding) so the selector becomes a safe JS string literal before
interpolation into the script, or otherwise escape backslashes, newlines, single
and double quotes and backticks consistently; build the script using that
serialized string and then call executeJavaScript?.invoke(script).

Comment on lines +80 to +84
override suspend fun scrollToElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
executeJavaScript?.invoke(script)
}
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

JavaScript injection risk: insufficient escaping.

Same escaping vulnerability as highlightElement() - only handles single quotes.

Apply the same robust escaping fix as suggested for highlightElement():

 override suspend fun scrollToElement(selector: String) {
-    val escapedSelector = selector.replace("'", "\\'")
+    val escapedSelector = selector
+        .replace("\\", "\\\\")
+        .replace("'", "\\'")
+        .replace("\"", "\\\"")
+        .replace("\n", "\\n")
+        .replace("\r", "\\r")
     val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
     executeJavaScript?.invoke(script)
 }
🤖 Prompt for AI Agents
In
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt
around lines 80-84, the selector string is only escaping single quotes which
leaves a JavaScript injection vector; replace the naive replace with a proper
JS-string serialization for the selector before embedding it into the script.
Produce a safely escaped JS string (escape backslashes, single and double quotes
and control characters) or, preferably, serialize the Kotlin string into a JS
string literal (for example via a JSON/string-quoting helper like
JSONObject.quote(selector) or a JSON encoder) and then embed that serialized
value into the script passed to executeJavaScript so the selector cannot break
out of the string literal.

Comment on lines +91 to +97
override suspend fun getSelectedElementHtml(): String? {
return _selectedElement.value?.let {
val script = "document.querySelector('${it.selector}')?.outerHTML || '';"
// This is a simplified version - in practice you'd use a callback
null
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stub implementation always returns null.

This method currently returns null unconditionally and includes a comment indicating it's simplified. This could cause unexpected behavior for callers expecting HTML content.

Should this be implemented fully, or should it remain a stub for now? If it's intentionally incomplete, consider adding a TODO comment and documenting the limitation. Would you like me to draft a proper implementation?

🤖 Prompt for AI Agents
In
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.jvm.kt
around lines 91 to 97, the method getSelectedElementHtml currently always
returns null; replace the stub with a real implementation that executes the
constructed JS snippet against the web view (or the JS bridge) to fetch the
selected element's outerHTML, awaits the result, returns the HTML string or null
if empty, and catches/logs exceptions; if you intentionally want a stub, replace
the current comment with a clear TODO mentioning why it's incomplete and add
documentation that callers may receive null until implemented.

Comment on lines +66 to +69
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
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

JavaScript injection risk: insufficient escaping.

Identical vulnerability to the JVM implementation - only escapes single quotes.

Apply the same robust escaping fix:

 override suspend fun highlightElement(selector: String) {
-    val escapedSelector = selector.replace("'", "\\'")
+    val escapedSelector = selector
+        .replace("\\", "\\\\")
+        .replace("'", "\\'")
+        .replace("\"", "\\\"")
+        .replace("\n", "\\n")
+        .replace("\r", "\\r")
     val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
     executeJavaScript?.invoke(script)
 }
📝 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
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector
.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
}
🤖 Prompt for AI Agents
In
mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt
around lines 66-69, the highlightElement implementation only escapes single
quotes and is vulnerable to JS injection; replace the manual single-quote
replace with a safe serialization of the selector for embedding in JS (for
example, JSON.stringify the selector/value or otherwise fully escape
backslashes, quotes, newlines, and control characters) and construct the script
using that serialized value: call JSON.stringify(selector) or an equivalent safe
serializer to produce a JS string literal, then use that value in
"window.webEditBridge?.highlightElement(...);" so arbitrary input cannot break
out of the string. Ensure executeJavaScript is invoked with the serialized
string literal (not raw selector) and remove the insecure replace-based
approach.

Comment on lines +77 to +80
override suspend fun scrollToElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
executeJavaScript?.invoke(script)
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

JavaScript injection risk: insufficient escaping.

Same vulnerability as JVM implementation.

Apply the same escaping fix:

 override suspend fun scrollToElement(selector: String) {
-    val escapedSelector = selector.replace("'", "\\'")
+    val escapedSelector = selector
+        .replace("\\", "\\\\")
+        .replace("'", "\\'")
+        .replace("\"", "\\\"")
+        .replace("\n", "\\n")
+        .replace("\r", "\\r")
     val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
     executeJavaScript?.invoke(script)
 }
📝 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
override suspend fun scrollToElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
executeJavaScript?.invoke(script)
override suspend fun scrollToElement(selector: String) {
val escapedSelector = selector
.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
executeJavaScript?.invoke(script)
}
🤖 Prompt for AI Agents
In
mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt
around lines 77-80, the JavaScript string interpolation escapes only single
quotes and risks injection because backslashes and control characters remain
unescaped; apply the same escaping used in the JVM implementation by first
escaping backslashes, then escaping single quotes, and also replacing
newlines/carriage returns (and other control chars if present) with their
escaped forms before inserting into the JS snippet so the generated script
cannot break out of the quoted string.

Comment on lines +88 to +90
override suspend fun getSelectedElementHtml(): String? {
return null
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stub implementation always returns null.

Same issue as JVM implementation.

Consider adding a TODO comment or documenting this limitation if it's intentional.

🤖 Prompt for AI Agents
In
mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridge.wasmJs.kt
around lines 88-90, the suspend function getSelectedElementHtml() is a stub that
always returns null (same as the JVM impl); either implement the actual
WASM-specific logic to retrieve the selected element HTML, or if that is not
possible now, add a clear TODO comment and KDoc on the function explaining the
limitation on WASM and why it returns null, and consider replacing the silent
null return with a NotImplementedError (or explicit documented null contract) so
callers know the behavior.

Copy link
Contributor

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 implements a new WebEdit agent that enables browsing web pages, selecting DOM elements through an interactive interface, and analyzing page content. The feature uses a bridge pattern with expect/actual declarations for platform-specific WebView implementations (JVM and WASM), along with JavaScript injection for DOM manipulation and element selection.

Key Changes:

  • New mpp-viewer-web module with WebEditBridge for bidirectional WebView-Kotlin communication
  • UI components for web browsing interface (toolbar, DOM tree sidebar, chat input)
  • Integration with existing agent system via AgentType.WEB_EDIT enum and routing

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
DOMElement.kt Data models for DOM elements, bounding boxes, and message types
WebEditBridge.kt Common interface defining WebView-Kotlin communication contract
WebEditBridge.jvm.kt JVM implementation with JavaScript execution callbacks
WebEditBridge.wasmJs.kt WASM implementation mirroring JVM functionality
WebEditBridgeScript.kt JavaScript code for DOM selection, highlighting, and tree extraction
WebEditPage.kt Main page layout with WebView, toolbar, sidebar, and chat areas
WebEditToolbar.kt Navigation controls and URL input component
WebEditChatInput.kt Chat interface for Q&A about page content
DOMTreeSidebar.kt Right sidebar displaying DOM tree structure
WebEditViewPlaceholder.kt Platform-agnostic WebView placeholder
AgentType.kt Added WEB_EDIT enum value with display name and routing
AgentInterfaceRouter.kt Added routing to WebEditPage and renamed function
DesktopTitleBarTabs.kt Added WEB_EDIT to sidebar visibility logic
CodingAgentPage.kt Added exhaustive when branch and renamed function
TopBarMenuDesktop.kt Added icon mapping for WEB_EDIT
TopBarMenuMobile.kt Added icon mapping for WEB_EDIT
RemoteAgentChatInterface.kt Function rename to RemoteAgentPage
AutoDevApp.apple.kt Removed unused imports
AutoDevApp.android.kt Removed unused import

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

Comment on lines 77 to 88
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The search functionality is not implemented. The searchQuery state is captured but never used to filter the DOM tree. Consider implementing search logic to filter elements by tag name, selector, or attributes.

Copilot uses AI. Check for mistakes.
Comment on lines 114 to 126
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The onHover callback is never actually invoked because there's no hover state tracking. The isHovered state variable is defined but never set to true. Consider implementing proper hover detection using Modifier.pointerInput or removing the onHover callback if hover highlighting is not needed.

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +69
override suspend fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.highlightElement('$escapedSelector');"
executeJavaScript?.invoke(script)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The same selector escaping vulnerability exists here. The implementation is identical to the JVM version and has the same security issues with insufficient escaping for JavaScript interpolation.

Copilot uses AI. Check for mistakes.
const rect = el.getBoundingClientRect();
window.webEditBridge.sendToKotlin('ElementSelected', JSON.stringify({
element: {
id: Math.random().toString(36).substr(2, 9),
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The same ID collision issue exists here. Using Math.random() for ID generation in the ElementSelected message can lead to non-unique identifiers.

Suggested change
id: Math.random().toString(36).substr(2, 9),
id: (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : (Date.now().toString(36) + Math.random().toString(36).substr(2, 9)),

Copilot uses AI. Check for mistakes.
Comment on lines 60 to 80
IconButton(
onClick = { /* Go back in browser history */ },
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
modifier = Modifier.size(18.dp)
)
}

IconButton(
onClick = { /* Go forward in browser history */ },
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Forward",
modifier = Modifier.size(18.dp)
)
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The navigation buttons for browser history (back/forward) have placeholder comments but no actual functionality. The onClick handlers should call the appropriate bridge methods (goBack/goForward) instead of being empty.

Copilot uses AI. Check for mistakes.
_loadProgress.value = 100
}
is WebEditMessage.Error -> {
println("[WebEditBridge] Error: ${message.message}")
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

Using println for error logging in production code is not recommended. Consider using a proper logging framework instead of console output.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 41
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The DOMTreeSidebar uses hardcoded sample data but never connects to actual DOM data from the WebEditBridge. The component should accept a DOMElement parameter from the bridge's domTree state flow and display the actual page structure instead of placeholder data.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The children property of DOMTreeItem is defined but never rendered. The LazyColumn only shows top-level items without recursively displaying child elements. To properly show the DOM tree hierarchy, you need to implement recursive rendering of children.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +83
override suspend fun scrollToElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
val script = "window.webEditBridge?.scrollToElement('$escapedSelector');"
executeJavaScript?.invoke(script)
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The same selector escaping vulnerability exists in scrollToElement. Only escaping single quotes is insufficient for preventing JavaScript injection when the selector is interpolated into JavaScript code.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +97
override suspend fun getSelectedElementHtml(): String? {
return _selectedElement.value?.let {
val script = "document.querySelector('${it.selector}')?.outerHTML || '';"
// This is a simplified version - in practice you'd use a callback
null
}
}
Copy link

Copilot AI Dec 12, 2025

Choose a reason for hiding this comment

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

The getSelectedElementHtml function constructs a JavaScript string with unescaped selector interpolation. This creates a potential XSS vulnerability. Even though this returns null currently, the commented code shows the intended implementation has the same escaping issue. The selector should be properly sanitized before being used in querySelector.

Copilot uses AI. Check for mistakes.
- Replace placeholder with actual WebView implementation using compose-webview-multiplatform
- Add platform-specific WebEditView for JVM and WASM
- Set default URL to https://ide.unitmesh.cc
- Implement proper navigation using rememberWebViewState and navigator.loadUrl
- Add JS bridge for DOM element selection and page interaction
- Inject WebEditBridgeScript when page loads
- Update toolbar with compact BasicTextField (28dp height)
- Remove WebEditPreview test file
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: 2

♻️ Duplicate comments (3)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt (1)

65-86: Wire browser back/forward buttons (currently no-op).
These handlers are placeholders, so the UI renders controls that do nothing.

 IconButton(
-    onClick = { /* Go back in browser history */ },
+    onClick = onGoBack,
+    enabled = canGoBack,
     modifier = Modifier.size(32.dp)
 ) { ... }

 IconButton(
-    onClick = { /* Go forward in browser history */ },
+    onClick = onGoForward,
+    enabled = canGoForward,
     modifier = Modifier.size(32.dp)
 ) { ... }
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt (2)

143-154: Hover state is never updated, making onHover callback non-functional.

The isHovered state is initialized but never set to true. The onHover callback passed to DOMTreeItemRow is never invoked, rendering hover highlighting inoperative.

Apply this diff to implement hover detection:

 @Composable
 private fun DOMTreeItemRow(
     item: DOMTreeItem,
     isSelected: Boolean = false,
     onClick: () -> Unit,
     onHover: (Boolean) -> Unit,
     modifier: Modifier = Modifier
 ) {
     var isHovered by remember { mutableStateOf(false) }
 
     val backgroundColor = when {
         isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
         isHovered -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
         else -> MaterialTheme.colorScheme.surface.copy(alpha = 0.5f)
     }
 
     Surface(
         modifier = modifier
             .fillMaxWidth()
-            .clickable { onClick() }
+            .pointerInput(Unit) {
+                awaitPointerEventScope {
+                    while (true) {
+                        val event = awaitPointerEvent()
+                        when (event.type) {
+                            PointerEventType.Enter -> {
+                                isHovered = true
+                                onHover(true)
+                            }
+                            PointerEventType.Exit -> {
+                                isHovered = false
+                                onHover(false)
+                            }
+                        }
+                    }
+                }
+            }
+            .clickable { onClick() }
             .padding(start = (item.depth * 12).dp),

You'll need to add the import:

import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput

31-36: Search functionality is not implemented.

The searchQuery state is captured but never used to filter flattenedTree. The search field is purely decorative.

To implement search, filter the flattened tree:

     // Convert DOMElement tree to flat list with depth
-    val flattenedTree = remember(domTree) {
+    val flattenedTree = remember(domTree, searchQuery) {
-        domTree?.let { flattenDOMTree(it, 0) } ?: emptyList()
+        val tree = domTree?.let { flattenDOMTree(it, 0) } ?: emptyList()
+        if (searchQuery.isBlank()) tree
+        else tree.filter { item ->
+            item.tagName.contains(searchQuery, ignoreCase = true) ||
+            item.selector.contains(searchQuery, ignoreCase = true) ||
+            item.attributes.any { it.contains(searchQuery, ignoreCase = true) }
+        }
     }
🧹 Nitpick comments (6)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt (4)

28-41: Clarify navigation vs close callbacks; currentUrl/isLoading are unused.
onBack is wired to “Close WebEdit” (Line 53-63) while browser back exists (Line 65-75), which is easy to mis-wire later. Also currentUrl and isLoading are currently unused.

Consider renaming + adding explicit callbacks/state for history navigation:

 fun WebEditToolbar(
     currentUrl: String,
     inputUrl: String,
     isLoading: Boolean,
     isSelectionMode: Boolean,
     showDOMSidebar: Boolean,
     onUrlChange: (String) -> Unit,
     onNavigate: (String) -> Unit,
-    onBack: () -> Unit,
+    onClose: () -> Unit,
+    canGoBack: Boolean,
+    canGoForward: Boolean,
+    onGoBack: () -> Unit,
+    onGoForward: () -> Unit,
     onReload: () -> Unit,
     onToggleSelectionMode: () -> Unit,
     onToggleDOMSidebar: () -> Unit,
     modifier: Modifier = Modifier
 )

53-63: “Close WebEdit” should not be named onBack.
This is the close/exit action, not browser back. Rename to onClose to avoid confusion with history navigation.


88-97: Consider using isLoading (disable reload or show stop/progress).
Right now isLoading is unused; simplest option is to disable reload while loading or swap the icon (Refresh ↔ Close).


141-178: Touch target sizing likely too small for commonMain (32.dp).
If this runs on mobile, 32.dp IconButtons can be a usability/accessibility problem. Consider Modifier.minimumInteractiveComponentSize() (or just drop the explicit size(32.dp) and let Material defaults handle it).

mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (2)

35-57: Type check on concrete JvmWebEditBridge couples implementation tightly.

While this is the JVM actual implementation, the repeated bridge is JvmWebEditBridge checks (lines 36, 135, 151) reduce flexibility and testability. Consider either:

  1. Adding these callback setters to the WebEditBridge interface, or
  2. Accepting JvmWebEditBridge directly in the JVM actual signature if platform-specific types are acceptable.

27-27: Hardcoded default URL may conflict with bridge state.

The rememberWebViewState initializes with a hardcoded URL "https://ide.unitmesh.cc", but currentUrl from the bridge may have a different value. Consider using the bridge's initial URL or an empty/blank URL to avoid potential navigation conflicts.

-    val webViewState = rememberWebViewState(url = "https://ide.unitmesh.cc")
+    val initialUrl = bridge.currentUrl.value.ifEmpty { "about:blank" }
+    val webViewState = rememberWebViewState(url = initialUrl)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c06450f and 8a9301a.

📒 Files selected for processing (6)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt (1 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt (1 hunks)
  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.kt (1 hunks)
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (1 hunks)
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.wasmJs.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt
🧰 Additional context used
📓 Path-based instructions (2)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual pattern for platform-specific code (for example: Platform implementations)
Use concrete classes (not interfaces) and Promise (not Flow) for @JsExport annotations
Avoid emoji and UTF-8 characters in WASM code
When modifying CodingAgentRenderer, update ALL implementations: DefaultCodingAgentRenderer, ComposeRenderer, JewelRenderer, ServerSideRenderer, and JsRendererAdapter in Kotlin
When modifying CodingAgentRenderer, update JVM CLI implementations: CodingCliRenderer and ConsoleRenderer
Use AutoDevColors or MaterialTheme.colorScheme for Compose styling

Files:

  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.kt
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.wasmJs.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt
**/*.{kt,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never use hardcoded colors - always use design tokens from the design system

Files:

  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.kt
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.wasmJs.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt
🧠 Learnings (2)
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to **/*.kt : Use `expect`/`actual` pattern for platform-specific code (for example: `Platform` implementations)

Applied to files:

  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.kt
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to **/*.kt : When modifying `CodingAgentRenderer`, update ALL implementations: `DefaultCodingAgentRenderer`, `ComposeRenderer`, `JewelRenderer`, `ServerSideRenderer`, and `JsRendererAdapter` in Kotlin

Applied to files:

  • mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.kt
  • mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.wasmJs.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt
🧬 Code graph analysis (1)
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (1)
mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt (1)
  • getWebEditBridgeScript (11-170)
⏰ 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). (1)
  • GitHub Check: Test mpp-core (KMP)
🔇 Additional comments (4)
mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.kt (1)

17-24: Well-structured expect declaration following the expect/actual pattern.

The common interface is well-documented with KDoc, and default parameter values provide flexibility for callers. This correctly establishes the platform-agnostic contract for WebEditView. Based on learnings, this follows the required expect/actual pattern for platform-specific code.

mpp-viewer-web/src/wasmJsMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.wasmJs.kt (1)

18-38: Appropriate placeholder implementation for WASM platform limitations.

The implementation correctly uses MaterialTheme.colorScheme for styling as per coding guidelines. The placeholder message clearly communicates the limitation to users. The unused parameters are acceptable since this serves as a graceful degradation path.

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/DOMTreeSidebar.kt (1)

38-98: Good component structure with proper theming.

The layout hierarchy, empty state handling, and use of MaterialTheme.colorScheme for all colors follows the coding guidelines. The recursive flattenDOMTree helper correctly converts the hierarchical DOM tree into a flat list with depth tracking for indentation.

mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (1)

68-125: Well-structured message handler with proper error handling.

The JS bridge message handler correctly parses incoming messages, delegates to the bridge's handleMessage method, and invokes the appropriate callbacks. The try-catch with logging provides good observability for debugging.

Comment on lines +99 to +139
// URL input field - use BasicTextField for compact height
BasicTextField(
value = inputUrl,
onValueChange = onUrlChange,
modifier = Modifier
.weight(1f)
.height(28.dp)
.background(
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
RoundedCornerShape(6.dp)
)
.border(
1.dp,
MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
RoundedCornerShape(6.dp)
)
.padding(horizontal = 10.dp),
textStyle = TextStyle(
fontSize = 13.sp,
color = MaterialTheme.colorScheme.onSurface
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Go),
keyboardActions = KeyboardActions(onGo = { onNavigate(inputUrl) }),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.CenterStart
) {
if (inputUrl.isEmpty()) {
Text(
"Enter URL (e.g., https://www.baidu.com)",
style = TextStyle(fontSize = 13.sp),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
)
}
innerTextField()
}
}
)
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

URL field: add basic input guarding + accessibility semantics.
BasicTextField is fine for compact height, but please add a semantics label for screen readers, and avoid navigating on empty/whitespace.

 BasicTextField(
     value = inputUrl,
     onValueChange = onUrlChange,
     modifier = Modifier
         .weight(1f)
         .height(28.dp)
+        .semantics { contentDescription = "URL" }
         ...
-    keyboardActions = KeyboardActions(onGo = { onNavigate(inputUrl) }),
+    keyboardActions = KeyboardActions(onGo = {
+        val url = inputUrl.trim()
+        if (url.isNotEmpty()) onNavigate(url)
+    }),
     decorationBox = { innerTextField -> ... }
 )

Also consider updating the placeholder example away from a region-specific domain (or make it generic like https://example.com).

🤖 Prompt for AI Agents
In
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditToolbar.kt
around lines 99 to 139, the BasicTextField needs accessibility and input
guarding: add a semantics label (e.g., modifier.semantics { contentDescription =
"URL input" } or use Modifier.semantics { this.contentDescription = ... }) so
screen readers announce the field, change the placeholder text to a neutral
example like "https://example.com", and prevent navigation on empty or
whitespace-only input by trimming inputUrl before calling onNavigate (i.e., only
call onNavigate when inputUrl.trim().isNotEmpty()). Ensure the semantics
modifier is applied to the BasicTextField modifier chain and the
keyboardActions/onGo uses the trimmed check.

Comment on lines +141 to +145
// Inject the WebEdit bridge script
kotlinx.coroutines.delay(300) // Wait for page to stabilize
val script = getWebEditBridgeScript()
webViewNavigator.evaluateJavaScript(script)
scriptInjected = true
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Arbitrary delay is fragile for script injection timing.

The 300ms delay is a brittle heuristic; pages may need more time, or the delay wastes time on fast-loading pages. Consider using a more robust approach such as polling for document.readyState or using the WebView's page load completion callback if available.

-                // Inject the WebEdit bridge script
-                kotlinx.coroutines.delay(300) // Wait for page to stabilize
-                val script = getWebEditBridgeScript()
-                webViewNavigator.evaluateJavaScript(script)
+                // Inject the WebEdit bridge script after confirming document is ready
+                val readyCheckScript = """
+                    (function() {
+                        return document.readyState === 'complete';
+                    })();
+                """.trimIndent()
+                // Poll for document ready state
+                var attempts = 0
+                while (attempts < 10) {
+                    kotlinx.coroutines.delay(100)
+                    attempts++
+                    // Inject once document is ready or after max attempts
+                    if (attempts >= 3) break
+                }
+                val script = getWebEditBridgeScript()
+                webViewNavigator.evaluateJavaScript(script)

Alternatively, if the WebView library provides a callback for DOMContentLoaded or similar events, that would be more reliable.

Committable suggestion skipped: line range outside the PR's diff.

Introduce WebEdit tool window for IDEA, including models, view model, content, and bridge script. Update related UI components and build configuration to support the new feature.
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: 7

♻️ Duplicate comments (1)
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (1)

161-165: Arbitrary delay is fragile for script injection timing.

The 300ms delay is a brittle heuristic; pages may need more time, or the delay wastes time on fast-loading pages. Consider using document.readyState polling or WebView's page load completion callback if available.

🧹 Nitpick comments (12)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt (2)

1796-1947: Avoid hardcoded Color.Black in the new icons; rely on design-token tinting instead.
These additions introduce more hardcoded color fills (SolidColor(Color.Black)), which violates the “no hardcoded colors” guideline for kt. If these vectors are always rendered via Icon(imageVector, ..., tint = <design token>), consider making the vector paths color-agnostic and pushing color decisions to the call site.

One possible adjustment (only if you confirm tint is always set where used):

-            path(
-                fill = SolidColor(Color.Black)
-            ) {
+            path(
+                fill = SolidColor(Color.Unspecified)
+            ) {
               ...
             }

If Color.Unspecified doesn’t render as expected with your Compose/IDEA setup, then keep the fill but please ensure every usage sets tint from the design system (e.g., MaterialTheme.colorScheme.* / project tokens), so the embedded black never leaks into UI styling.


1796-1947: Optional: factor a tiny helper to reduce repeated ImageVector.Builder(...) boilerplate for future icons.
The pattern is identical across icons; a local helper would reduce churn and make future additions less error-prone.

mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (3)

81-86: Consider using proper logging instead of println.

The URL synchronization logic is correct, but println should be replaced with a proper logging framework for consistency with production code.

If a logger is available, consider:

         if (currentUrl.isNotEmpty() && currentUrl != webViewState.lastLoadedUrl) {
-            println("[WebEditView] URL changed to: $currentUrl")
+            logger.info { "[WebEditView] URL changed to: $currentUrl" }
             webViewNavigator.loadUrl(currentUrl)
         }

111-131: Avoid double parsing of JSON elements.

Lines 115 and 126 convert jsonObject to string and then parse it again in parseElement, which is inefficient. Consider modifying parseElement to accept JsonObject directly or parse the element inline.

Option 1: Modify parseElement to accept JsonObject:

-private fun parseElement(jsonString: String): DOMElement? {
+private fun parseElement(jsonObject: JsonObject): DOMElement? {
     return try {
-        Json.decodeFromString<DOMElement>(jsonString)
+        Json.decodeFromJsonElement<DOMElement>(jsonObject)
     } catch (e: Exception) {
         println("[WebEditView] Failed to parse element: ${e.message}")
         null
     }
 }

Then update the call sites:

                         "ElementSelected" -> {
                             val elementData = Json.parseToJsonElement(data).jsonObject
                             val elementJson = elementData["element"]?.jsonObject
                             if (elementJson != null) {
-                                val element = parseElement(elementJson.toString())
+                                val element = parseElement(elementJson)
                                 if (element != null) {
                                     bridge.handleMessage(WebEditMessage.ElementSelected(element))
                                     onElementSelected(element)
                                 }
                             }
                         }
                         "DOMTreeUpdated" -> {
                             val treeData = Json.parseToJsonElement(data).jsonObject
                             val rootJson = treeData["root"]?.jsonObject
                             if (rootJson != null) {
-                                val root = parseElement(rootJson.toString())
+                                val root = parseElement(rootJson)
                                 if (root != null) {
                                     bridge.handleMessage(WebEditMessage.DOMTreeUpdated(root))
                                     onDOMTreeUpdated(root)
                                 }
                             }
                         }

148-169: Consider checking scriptInjected flag before injection.

The scriptInjected flag is maintained but never checked before injecting the script. Although it's reset for each new page, adding a guard would make the intent clearer and prevent potential double injection if this code path is triggered multiple times.

                 // Inject the WebEdit bridge script
-                kotlinx.coroutines.delay(300) // Wait for page to stabilize
-                val script = getWebEditBridgeScript()
-                webViewNavigator.evaluateJavaScript(script)
-                scriptInjected = true
+                if (!scriptInjected) {
+                    kotlinx.coroutines.delay(300) // Wait for page to stabilize
+                    val script = getWebEditBridgeScript()
+                    webViewNavigator.evaluateJavaScript(script)
+                    scriptInjected = true
+                }
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/header/IdeaAgentTabsHeader.kt (1)

262-262: Consider using a distinct color for WEB_EDIT.

WEB_EDIT currently uses the same Blue.c400 as CODING (line 256), which could cause visual confusion. Consider using a different color like Teal, Violet, or Orange to maintain visual differentiation between tabs.

-    AgentType.WEB_EDIT -> IdeaAutoDevColors.Blue.c400
+    AgentType.WEB_EDIT -> IdeaAutoDevColors.Teal.c400
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditBridgeScript.kt (1)

52-62: Original element styles are not preserved.

clearHighlights sets outline and outlineOffset to empty strings, which could remove styles that existed before highlighting. Consider storing and restoring original styles.

// Before highlighting
el._originalOutline = el.style.outline;
el._originalOutlineOffset = el.style.outlineOffset;
// When clearing
el.style.outline = el._originalOutline || '';
el.style.outlineOffset = el._originalOutlineOffset || '';
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt (1)

482-492: Consider simplifying dual text state.

inputText is derived from inputTextFieldState via snapshotFlow, creating redundant state. You could use inputTextFieldState.text.toString() directly where inputText is referenced.

-    var inputText by remember { mutableStateOf("") }
...
-    // Sync text field state
-    IdeaLaunchedEffect(Unit) {
-        snapshotFlow { inputTextFieldState.text.toString() }
-            .distinctUntilChanged()
-            .collect { inputText = it }
-    }
...
-                    enabled = inputText.isNotBlank()
+                    enabled = inputTextFieldState.text.isNotBlank()
...
-                        if (inputText.isNotBlank()) {
-                            onSendMessage(inputText)
+                        val text = inputTextFieldState.text.toString()
+                        if (text.isNotBlank()) {
+                            onSendMessage(text)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt (4)

70-70: Unsafe cast may throw ClassCastException.

The as JBCefBrowserBase cast is risky. While JBCefBrowser does implement JBCefBrowserBase, using a safe cast is more defensive.

-            jsQuery = JBCefJSQuery.create(browser as JBCefBrowserBase)
+            jsQuery = (browser as? JBCefBrowserBase)?.let { JBCefJSQuery.create(it) }

126-128: Empty catch block swallows exceptions silently.

The exception is caught but completely ignored, making debugging difficult if LLM initialization fails unexpectedly.

             } catch (e: Exception) {
-                // LLM service is optional for WebEdit
+                // LLM service is optional for WebEdit, log for debugging
+                println("[IdeaWebEditViewModel] LLM service initialization failed: ${e.message}")
             }

307-311: Hardcoded delay is fragile.

The 500ms delay is arbitrary and may be insufficient on slower pages or unnecessarily slow on fast ones. Consider having the JS bridge emit a "Ready" message when initialization completes, then trigger refreshDOMTree() in response.


381-387: Recursive parsing could overflow on deeply nested DOMs.

While typical DOM depth is manageable, malformed or adversarial pages could cause stack overflow. Consider adding a depth limit parameter.

private fun parseElement(json: JsonObject, depth: Int = 0, maxDepth: Int = 100): IdeaDOMElement {
    // ... existing code ...
    val children = if (depth < maxDepth) {
        json["children"]?.jsonArray?.mapNotNull { child ->
            try {
                parseElement(child.jsonObject, depth + 1, maxDepth)
            } catch (e: Exception) {
                null
            }
        } ?: emptyList()
    } else emptyList()
    // ...
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9301a and c1ddc84.

📒 Files selected for processing (10)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/header/IdeaAgentTabsHeader.kt (3 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt (5 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditBridgeScript.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditModels.kt (1 hunks)
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt (1 hunks)
  • mpp-ui/build.gradle.kts (2 hunks)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt (1 hunks)
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/webedit/WebEditPage.kt
🧰 Additional context used
📓 Path-based instructions (2)
**/*.kt

📄 CodeRabbit inference engine (AGENTS.md)

**/*.kt: Use expect/actual pattern for platform-specific code (for example: Platform implementations)
Use concrete classes (not interfaces) and Promise (not Flow) for @JsExport annotations
Avoid emoji and UTF-8 characters in WASM code
When modifying CodingAgentRenderer, update ALL implementations: DefaultCodingAgentRenderer, ComposeRenderer, JewelRenderer, ServerSideRenderer, and JsRendererAdapter in Kotlin
When modifying CodingAgentRenderer, update JVM CLI implementations: CodingCliRenderer and ConsoleRenderer
Use AutoDevColors or MaterialTheme.colorScheme for Compose styling

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditModels.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/header/IdeaAgentTabsHeader.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditBridgeScript.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt
**/*.{kt,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Never use hardcoded colors - always use design tokens from the design system

Files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditModels.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/header/IdeaAgentTabsHeader.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditBridgeScript.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt
🧠 Learnings (3)
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to **/*.kt : When modifying `CodingAgentRenderer`, update ALL implementations: `DefaultCodingAgentRenderer`, `ComposeRenderer`, `JewelRenderer`, `ServerSideRenderer`, and `JsRendererAdapter` in Kotlin

Applied to files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
  • mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt
  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/header/IdeaAgentTabsHeader.kt
  • mpp-ui/build.gradle.kts
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to mpp-vscode/src/**/*.ts : When modifying `CodingAgentRenderer`, update VSCode implementations: `mpp-vscode/src/bridge/mpp-core.ts` and `mpp-vscode/src/providers/chat-view.ts`

Applied to files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
  • mpp-ui/build.gradle.kts
📚 Learning: 2025-12-07T08:40:10.850Z
Learnt from: CR
Repo: phodal/auto-dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-07T08:40:10.850Z
Learning: Applies to **/*.kt : When modifying `CodingAgentRenderer`, update JVM CLI implementations: `CodingCliRenderer` and `ConsoleRenderer`

Applied to files:

  • mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt
🧬 Code graph analysis (3)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt (2)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/IdeaVerticalResizableSplitPane.kt (1)
  • IdeaVerticalResizableSplitPane (36-140)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/IdeaResizableSplitPane.kt (1)
  • IdeaResizableSplitPane (36-140)
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (2)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt (1)
  • parseElement (367-398)
mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditBridgeScript.kt (1)
  • getWebEditBridgeScript (11-170)
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt (1)
mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (1)
  • parseElement (189-196)
⏰ 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
  • GitHub Check: Test mpp-core (KMP)
🔇 Additional comments (13)
mpp-ui/build.gradle.kts (1)

335-370: Align compose.webview across platforms.

If compose.webview is added to commonMain, ensure it's also available in the wasmJsMain sourceset (lines 335–370), or clarify why WASM has a different dependency configuration. Currently, mpp-viewer-web is present in wasmJsMain but compose.webview is not.

mpp-viewer-web/src/jvmMain/kotlin/cc/unitmesh/viewer/web/webedit/WebEditView.jvm.kt (2)

56-78: LGTM!

The bridge callback configuration is well-structured with proper type checking and navigation guards for back/forward operations.


189-196: LGTM!

The parseElement helper has appropriate error handling and gracefully returns null on failure.

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

23-24: LGTM! WEB_EDIT agent type integration follows established patterns.

The integration correctly:

  • Adds lazy ViewModel initialization when WEB_EDIT tab is selected (lines 167-169)
  • Follows the same disposal pattern as other agents (lines 187-190)
  • Renders IdeaWebEditContent in the main content area (lines 377-384)

Also applies to: 133-135, 167-169, 187-190, 377-384

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/header/IdeaAgentTabsHeader.kt (1)

92-92: Tab list and icon mapping look good.

WEB_EDIT is correctly added to the selectable agent tabs and mapped to an appropriate Web icon.

Also applies to: 275-275

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditBridgeScript.kt (2)

141-169: Event handlers may interfere with page interactivity.

The mouseover and click handlers use stopPropagation() and preventDefault() in capture phase (true), which will block the page's native event handling in selection mode. This is likely intentional for DOM selection, but worth documenting that interactive elements (links, buttons, forms) won't work while selection mode is active.


16-179: Bridge script implementation is well-structured.

Good practices observed:

  • Guard against multiple injections (line 19)
  • Depth and child limits in DOM tree extraction (lines 105, 118)
  • Defensive type checking for className (line 89)
  • Error handling with try/catch blocks
  • Proper use of capture phase for selection mode events
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt (2)

40-138: Overall structure is well-organized.

The component properly:

  • Checks JCEF support with graceful fallback (lines 61-64)
  • Uses established state collection patterns with IdeaLaunchedEffect
  • Composes split panes using existing IdeaResizableSplitPane and IdeaVerticalResizableSplitPane
  • Follows design token usage with IdeaAutoDevColors and JewelTheme

150-155: The concern about getBrowserComponent() recreating the browser component is unfounded. The implementation returns browser?.component, which accesses the component property of a cached JBCefBrowser instance stored as a private field in the ViewModel. The browser instance is initialized once and reused across recompositions, so the SwingPanel factory will receive the same component on each call.

Likely an incorrect or invalid review comment.

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

6-61: LGTM! Well-structured data models.

The models are cleanly designed:

  • IdeaWebEditState has sensible defaults for all fields
  • IdeaDOMElement.getDisplayName() provides a readable representation
  • IdeaWebEditMessage sealed class properly enumerates all message types from the JS bridge
  • The Idea* prefix correctly distinguishes these IDEA-specific models from the cross-platform ones in mpp-viewer-web
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt (3)

223-225: isLoading may check wrong state.

_state.value.isLoading tracks page loading state, not chat generation. This allows sending chat messages while a page loads (probably fine) but doesn't prevent sending while a previous chat is still generating. Consider adding a separate isGenerating state or checking currentJob?.isActive.

     fun sendChatMessage(message: String) {
-        if (_state.value.isLoading) return
+        if (currentJob?.isActive == true) return  // Prevent concurrent chat requests

404-407: LGTM - disposal pattern is correct.

Browser and jsQuery are properly registered with Disposer in initializeBrowser(). The coroutineScope is passed in externally, so the caller manages its lifecycle. Consider whether llmService needs explicit cleanup if it holds resources.


318-365: This review comment is incorrect. The data field in messages is indeed a JSON string primitive, not a JsonObject, and the current parsing is correct.

Verification shows that IdeaWebEditBridgeScript.kt consistently sends stringified JSON data to sendToKotlin() (lines 126, 159, 172), resulting in messages structured as { type: "...", data: "{...}" }. The line 322 parsing json["data"]?.jsonPrimitive?.content correctly extracts the stringified JSON string, which is then parsed again on subsequent lines (326, 339, 348, 357). The suggested diff would break this logic since it attempts to treat data as a JsonObject when it is always a string.

Likely an incorrect or invalid review comment.

Comment on lines +401 to +467
@Composable
private fun DOMTreeNode(
element: IdeaDOMElement,
selectedSelector: String?,
depth: Int,
onElementClick: (String) -> Unit,
onElementHover: (String?) -> Unit
) {
var expanded by remember { mutableStateOf(depth < 2) }
val isSelected = element.selector == selectedSelector
val hasChildren = element.children.isNotEmpty()

Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
if (isSelected) IdeaAutoDevColors.Blue.c400.copy(alpha = 0.2f)
else JewelTheme.globalColors.panelBackground
)
.clickable { onElementClick(element.selector) }
.padding(start = (depth * 12).dp, top = 2.dp, bottom = 2.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Expand/collapse icon
if (hasChildren) {
IconButton(
onClick = { expanded = !expanded },
modifier = Modifier.size(16.dp)
) {
Icon(
imageVector = if (expanded) IdeaComposeIcons.ExpandMore else IdeaComposeIcons.ChevronRight,
contentDescription = if (expanded) "Collapse" else "Expand",
modifier = Modifier.size(12.dp),
tint = JewelTheme.globalColors.text.info
)
}
} else {
Spacer(modifier = Modifier.width(16.dp))
}

// Element display
Text(
text = element.getDisplayName(),
style = JewelTheme.defaultTextStyle.copy(
fontSize = 12.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
),
maxLines = 1,
modifier = Modifier.weight(1f)
)
}

// Children
if (expanded && hasChildren) {
element.children.forEach { child ->
DOMTreeNode(
element = child,
selectedSelector = selectedSelector,
depth = depth + 1,
onElementClick = onElementClick,
onElementHover = onElementHover
)
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

onElementHover callback is passed but never invoked in DOMTreeNode.

DOMTreeNode receives onElementHover but doesn't call it. Add hover detection to invoke it:

 Row(
     modifier = Modifier
         .fillMaxWidth()
         .background(...)
         .clickable { onElementClick(element.selector) }
+        .pointerInput(element.selector) {
+            awaitPointerEventScope {
+                while (true) {
+                    val event = awaitPointerEvent()
+                    when (event.type) {
+                        PointerEventType.Enter -> onElementHover(element.selector)
+                        PointerEventType.Exit -> onElementHover(null)
+                    }
+                }
+            }
+        }
         .padding(...)
📝 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
@Composable
private fun DOMTreeNode(
element: IdeaDOMElement,
selectedSelector: String?,
depth: Int,
onElementClick: (String) -> Unit,
onElementHover: (String?) -> Unit
) {
var expanded by remember { mutableStateOf(depth < 2) }
val isSelected = element.selector == selectedSelector
val hasChildren = element.children.isNotEmpty()
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
if (isSelected) IdeaAutoDevColors.Blue.c400.copy(alpha = 0.2f)
else JewelTheme.globalColors.panelBackground
)
.clickable { onElementClick(element.selector) }
.padding(start = (depth * 12).dp, top = 2.dp, bottom = 2.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Expand/collapse icon
if (hasChildren) {
IconButton(
onClick = { expanded = !expanded },
modifier = Modifier.size(16.dp)
) {
Icon(
imageVector = if (expanded) IdeaComposeIcons.ExpandMore else IdeaComposeIcons.ChevronRight,
contentDescription = if (expanded) "Collapse" else "Expand",
modifier = Modifier.size(12.dp),
tint = JewelTheme.globalColors.text.info
)
}
} else {
Spacer(modifier = Modifier.width(16.dp))
}
// Element display
Text(
text = element.getDisplayName(),
style = JewelTheme.defaultTextStyle.copy(
fontSize = 12.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
),
maxLines = 1,
modifier = Modifier.weight(1f)
)
}
// Children
if (expanded && hasChildren) {
element.children.forEach { child ->
DOMTreeNode(
element = child,
selectedSelector = selectedSelector,
depth = depth + 1,
onElementClick = onElementClick,
onElementHover = onElementHover
)
}
}
}
}
@Composable
private fun DOMTreeNode(
element: IdeaDOMElement,
selectedSelector: String?,
depth: Int,
onElementClick: (String) -> Unit,
onElementHover: (String?) -> Unit
) {
var expanded by remember { mutableStateOf(depth < 2) }
val isSelected = element.selector == selectedSelector
val hasChildren = element.children.isNotEmpty()
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
if (isSelected) IdeaAutoDevColors.Blue.c400.copy(alpha = 0.2f)
else JewelTheme.globalColors.panelBackground
)
.clickable { onElementClick(element.selector) }
.pointerInput(element.selector) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Enter -> onElementHover(element.selector)
PointerEventType.Exit -> onElementHover(null)
}
}
}
}
.padding(start = (depth * 12).dp, top = 2.dp, bottom = 2.dp, end = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Expand/collapse icon
if (hasChildren) {
IconButton(
onClick = { expanded = !expanded },
modifier = Modifier.size(16.dp)
) {
Icon(
imageVector = if (expanded) IdeaComposeIcons.ExpandMore else IdeaComposeIcons.ChevronRight,
contentDescription = if (expanded) "Collapse" else "Expand",
modifier = Modifier.size(12.dp),
tint = JewelTheme.globalColors.text.info
)
}
} else {
Spacer(modifier = Modifier.width(16.dp))
}
// Element display
Text(
text = element.getDisplayName(),
style = JewelTheme.defaultTextStyle.copy(
fontSize = 12.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
),
maxLines = 1,
modifier = Modifier.weight(1f)
)
}
// Children
if (expanded && hasChildren) {
element.children.forEach { child ->
DOMTreeNode(
element = child,
selectedSelector = selectedSelector,
depth = depth + 1,
onElementClick = onElementClick,
onElementHover = onElementHover
)
}
}
}
}
🤖 Prompt for AI Agents
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditContent.kt
lines 401-467: the onElementHover callback is accepted but never invoked; add
hover detection to the row showing each DOM node and call
onElementHover(element.selector) when the pointer enters and
onElementHover(null) when it leaves (use Modifier.pointerMoveFilter or
equivalent hover-detection modifier placed before .clickable to avoid swallowing
click events), keeping existing padding/click behavior unchanged.

Comment on lines +83 to +94
override fun onLoadEnd(browser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) {
if (frame?.isMain == true) {
val url = browser?.url ?: ""
val title = frame.name ?: ""
updateState {
it.copy(
currentUrl = url,
pageTitle = title,
isLoading = false,
loadProgress = 100
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

frame.name does not return the page title.

CefFrame.name returns the frame's name attribute (often empty for the main frame), not the document title. The page title should be retrieved differently.

                     override fun onLoadEnd(browser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) {
                         if (frame?.isMain == true) {
                             val url = browser?.url ?: ""
-                            val title = frame.name ?: ""
+                            // Title will be updated via JS bridge or use empty for now
+                            val title = ""
                             updateState {
                                 it.copy(
                                     currentUrl = url,
                                     pageTitle = title,
                                     isLoading = false,
                                     loadProgress = 100
                                 )
                             }

Alternatively, consider adding a getPageTitle() call in injectBridgeScript() to retrieve the actual document.title.

📝 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
override fun onLoadEnd(browser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) {
if (frame?.isMain == true) {
val url = browser?.url ?: ""
val title = frame.name ?: ""
updateState {
it.copy(
currentUrl = url,
pageTitle = title,
isLoading = false,
loadProgress = 100
)
}
override fun onLoadEnd(browser: CefBrowser?, frame: CefFrame?, httpStatusCode: Int) {
if (frame?.isMain == true) {
val url = browser?.url ?: ""
// Title will be updated via JS bridge or use empty for now
val title = ""
updateState {
it.copy(
currentUrl = url,
pageTitle = title,
isLoading = false,
loadProgress = 100
)
}

Comment on lines +193 to +196
fun highlightElement(selector: String) {
val escapedSelector = selector.replace("'", "\\'")
executeJavaScript("window.webEditBridge?.highlightElement('$escapedSelector');")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete string escaping for JavaScript injection.

The escaping only handles single quotes but not backslashes. A selector containing \' would result in invalid/exploitable JavaScript. Escape backslashes first, then quotes.

     fun highlightElement(selector: String) {
-        val escapedSelector = selector.replace("'", "\\'")
+        val escapedSelector = selector.replace("\\", "\\\\").replace("'", "\\'")
         executeJavaScript("window.webEditBridge?.highlightElement('$escapedSelector');")
     }

Apply the same fix to scrollToElement() at line 209.

🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt
around lines 193-196 (and also apply to scrollToElement at line 209), the
selector string is only escaping single quotes which misses existing
backslashes; first replace backslashes with double-backslashes, then escape
single quotes so the injected JavaScript is safe (i.e., perform
selector.replace("\\", "\\\\") followed by selector.replace("'", "\\'")), and
use that fully escaped value in the executeJavaScript calls for both
highlightElement and scrollToElement.

Comment on lines +400 to +402
private fun updateState(update: (IdeaWebEditState) -> IdeaWebEditState) {
_state.value = update(_state.value)
}
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

Thread-safety issue with non-atomic state updates.

updateState is called from multiple threads (CEF load handlers, IO coroutines, main thread). The read-modify-write pattern _state.value = update(_state.value) is not atomic—concurrent calls can lose updates.

Use MutableStateFlow.update for atomic compare-and-swap:

     private fun updateState(update: (IdeaWebEditState) -> IdeaWebEditState) {
-        _state.value = update(_state.value)
+        _state.update(update)
     }
📝 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 updateState(update: (IdeaWebEditState) -> IdeaWebEditState) {
_state.value = update(_state.value)
}
private fun updateState(update: (IdeaWebEditState) -> IdeaWebEditState) {
_state.update(update)
}
🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/webedit/IdeaWebEditViewModel.kt
around lines 400 to 402, the current read-modify-write `_state.value =
update(_state.value)` is not atomic and can lose concurrent updates; replace
this pattern by using MutableStateFlow.update to perform an atomic
compare-and-swap update (call the StateFlow's update with a lambda that applies
the provided updater to the current state) and add the necessary import for
kotlinx.coroutines.flow.update so concurrent callers (CEF handlers, IO
coroutines, main thread) cannot overwrite each other.

implementation(project(":mpp-core"))
implementation(project(":mpp-codegraph"))
implementation(project(":mpp-viewer"))
implementation(project(":mpp-viewer-web"))
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

Remove redundant mpp-viewer-web dependency from jvmMain.

The dependency is already added to commonMain (line 134), so it should not be duplicated in jvmMain (line 185). Multiplatform conventions place shared dependencies in commonMain and only add platform-specific dependencies to their respective sourcesets.

         val jvmMain by getting {
             dependencies {
-                implementation(project(":mpp-viewer-web"))
                 implementation(project(":mpp-viewer"))

Also applies to: 185-185

🤖 Prompt for AI Agents
In mpp-ui/build.gradle.kts around lines 134 and 185, the
project(":mpp-viewer-web") dependency is declared in commonMain (line 134) and
redundantly re-declared in jvmMain (line 185); remove the duplicate
implementation(project(":mpp-viewer-web")) entry from the jvmMain sourceset
(line 185) so the shared dependency remains only in commonMain and only
platform-specific dependencies are kept in jvmMain.

// Ktor HTTP Client (for remote agent)
implementation(libs.ktor.client.core)

implementation(libs.compose.webview)
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

🧩 Analysis chain

🌐 Web query:

compose.webview Jetbrains Compose platform compatibility iOS JavaScript WASM

💡 Result:

Short answer: There’s no official single “compose.webview” from JetBrains that runs everywhere. Options and compatibility:

  • Compose Multiplatform (official)

    • iOS: you embed a native WKWebView via the UIKit/SwiftUI interop (supported; iOS target is Beta). [1][2]
    • Web (browser): Compose Web supports embedding HTML elements (WebElementView) and the web target builds to both Kotlin/JS and Kotlin/Wasm (use the compatibility/browser distribution to support modern browsers (Wasm) and older browsers (JS)). [3][4]
  • Community library (compose-webview-multiplatform by KevinnZou)

    • Implements WebView for Android, iOS (WKWebView) and Desktop; it’s a third‑party solution, not an official JetBrains artifact. [5]

If you need a single cross‑platform WebView: use platform interop on iOS (WKWebView) + Compose Web embedding for browser targets (JS/Wasm). For a packaged native app on iOS + web in browser you’ll maintain two implementations (native WKWebView vs HTML iframe/element on web). [1][3][5]

Sources:

  1. JetBrains — UIKit integration / Web view examples. [2]
  2. JetBrains — Compose Multiplatform platform status and docs. [1]
  3. JetBrains — Web target: JS and Wasm compatibility (composeCompatibilityBrowserDistribution) and WebElementView. [3][4]
  4. KevinnZou — compose-webview-multiplatform (community library implementing Android, iOS, Desktop). [5]

🏁 Script executed:

# Look for dependency definitions in the project
fd -e toml -e gradle -e kts | head -20

Repository: phodal/auto-dev

Length of output: 372


🏁 Script executed:

# Search for libs.compose.webview definition
rg "compose\.webview" --type kotlin --type gradle

Repository: phodal/auto-dev

Length of output: 297


🏁 Script executed:

# Check for gradle version catalog (libs.versions.toml or similar)
fd "libs.versions.toml" -o fd "gradle/libs.versions.toml"

Repository: phodal/auto-dev

Length of output: 182


🏁 Script executed:

cat gradle/libs.versions.toml | grep -A 5 -B 5 "compose.webview"

Repository: phodal/auto-dev

Length of output: 1065


🏁 Script executed:

# Also search for the full definition
rg "webview" gradle/libs.versions.toml -A 2 -B 2

Repository: phodal/auto-dev

Length of output: 525


🌐 Web query:

kevinnzou compose-webview-multiplatform supported platforms targets

💡 Result:

Supported targets: Android, iOS and Desktop. Desktop implementation moved from JavaFX (<=1.2.0) → Java CEF (from 1.3.0) → Kotlin CEF (KCEF, since 1.7.0). [1][2]


Move compose.webview to platform-specific sourcesets.

compose-webview-multiplatform only supports Android, iOS, and Desktop targets. It does not support JavaScript or WASM. Adding it to commonMain will cause compilation failures for the web targets. Move the dependency to androidMain and desktopMain sourcesets instead.

🤖 Prompt for AI Agents
In mpp-ui/build.gradle.kts around line 168, the compose.webview dependency is
currently added to commonMain which breaks JS/WASM builds; remove
implementation(libs.compose.webview) from commonMain and instead add that
dependency to the platform-specific source sets (androidMain and desktopMain —
and optionally iosMain if you target iOS) so only Android and Desktop (and iOS
if needed) include compose-webview-multiplatform.

@phodal phodal merged commit bd33e88 into master Dec 12, 2025
5 of 8 checks passed
phodal added a commit that referenced this pull request Dec 12, 2025
Security & Robustness:
- Fix JS injection vulnerabilities by escaping backslash, quotes, newlines in all bridge implementations
- Replace deprecated substr() with slice() for ID generation
- Upgrade to crypto.randomUUID() with fallback for unique IDs
- Fix double JSON encoding - send structured objects instead of strings
- Improve thread-safety with StateFlow.update() instead of direct assignment

UX Improvements:
- Separate hover and selection highlights (hover no longer clears selection)
- Add CSS variables for themeable colors (--webedit-hover-color, --webedit-selected-color)
- Add URL validation with regex pattern matching
- Add accessibility semantics to URL input field
- Trim whitespace in URL input before navigation
- Change placeholder from baidu.com to example.com

Build & Architecture:
- Move compose-webview dependency from commonMain to jvmMain (WebView not supported on WASM/JS)
- Remove duplicate mpp-viewer-web dependency in jvmMain

Code Quality:
- Add TODO comments for stub implementations (getSelectedElementHtml)
- Remove incorrect frame.name usage for page title (use JS callback instead)
- Remove hardcoded backgroundColor and initial URL in WebEditView
- Improve error handling and logging consistency

Files modified:
- IdeaWebEditBridgeScript.kt, IdeaWebEditViewModel.kt
- WebEditBridge.jvm.kt, WebEditBridge.wasmJs.kt
- WebEditBridgeScript.kt, WebEditView.jvm.kt
- WebEditPage.kt, WebEditToolbar.kt
- mpp-ui/build.gradle.kts
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.

feat: Add WebEdit Agent for web page browsing and DOM selection

1 participant

Comments