Skip to content

Add image selection and swapping in the visual editor#2717

Open
azizmejri1 wants to merge 11 commits intodyad-sh:mainfrom
azizmejri1:feat/image-visual-editor
Open

Add image selection and swapping in the visual editor#2717
azizmejri1 wants to merge 11 commits intodyad-sh:mainfrom
azizmejri1:feat/image-visual-editor

Conversation

@azizmejri1
Copy link
Collaborator

@azizmejri1 azizmejri1 commented Feb 14, 2026

closes #2634


Open with Devin

@wwwillchen
Copy link
Collaborator

@BugBot run

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @azizmejri1, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the visual editing experience by introducing comprehensive image management capabilities. It allows users to intuitively change image sources within their components, supporting both URL-based and file upload methods. The changes span across the frontend for UI interaction, the backend for file persistence and Git integration, and the core AST analysis and IPC layers to ensure seamless real-time updates and accurate code transformation.

Highlights

  • Image Swap Popover Component: A new React component, ImageSwapPopover.tsx, has been added to provide a user interface for changing image sources. Users can now swap images by providing a URL or by uploading a local file.
  • Image Upload Handling: The backend now supports processing image uploads. Uploaded images are saved to the public/images directory with unique filenames, and these new files are automatically added to Git if the repository is initialized.
  • Visual Editor Integration: The visual editing toolbar (VisualEditingToolbar.tsx) has been updated to include the new image swap functionality. When an image element is selected, the popover appears, allowing users to modify its source. Changes are reflected in the iframe preview and stored as pending changes.
  • AST Analysis for Images: The Abstract Syntax Tree (AST) analysis utility (visual_editing_utils.ts) has been enhanced to detect <img> elements within components and extract their src attributes. This enables the visual editor to identify and manage images effectively.
  • IPC Communication for Images: New Inter-Process Communication (IPC) messages (modify-dyad-image-src and get-dyad-image-src) have been introduced. These messages facilitate real-time communication between the main application process and the iframe for dynamic image manipulation and retrieval.
Changelog
  • src/components/preview_panel/ImageSwapPopover.tsx
    • Added a new React component for image source selection and upload.
  • src/components/preview_panel/PreviewIframe.tsx
    • Updated to manage image-related state (hasImage, currentImageSrc) and pass it to the visual editing toolbar.
  • src/components/preview_panel/VisualEditingToolbar.tsx
    • Modified to import and render the ImageSwapPopover when an image element is selected.
    • Updated to handle image swap events by sending messages to the iframe and updating pending changes.
  • src/ipc/types/visual-editing.ts
    • Extended VisualEditingChangeSchema to include imageSrc and imageUpload properties.
    • Extended AnalyseComponentResultSchema to include hasImage and imageSrc properties.
  • src/pro/main/ipc/handlers/visual_editing_handlers.ts
    • Enhanced to process image uploads, save them to the public/images directory, and add them to Git.
    • Updated the fileChanges map to include imageSrc for component modifications.
    • Modified analyzeComponent return type to include image-related analysis results.
  • src/pro/main/utils/visual_editing_utils.test.ts
    • Added new test cases for image detection within components.
    • Added new test cases for transformContent functionality related to updating image src attributes.
  • src/pro/main/utils/visual_editing_utils.ts
    • Modified transformContent to update or add src attributes for <img> elements.
    • Modified analyzeComponent to detect <img> elements and extract their src attributes.
  • worker/dyad-visual-editor-client.js
    • Added new functions handleModifyImageSrc and handleGetImageSrc to update image sources in the iframe and retrieve them.
    • Integrated new image-related message types into the message listener for IPC communication.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

@github-actions
Copy link
Contributor

🔍 Dyadbot Code Review Summary

Found 4 new issue(s) flagged by 3 independent reviewers (consensus voting: only issues identified by 2+ reviewers are reported).

Summary

Severity Count
🔴 HIGH 1
🟡 MEDIUM 3

Issues to Address

Severity File Issue
🔴 HIGH src/components/preview_panel/ImageSwapPopover.tsx:31 URL input shows stale value when switching between image components
🟡 MEDIUM src/components/preview_panel/ImageSwapPopover.tsx:45 Invalid file type silently rejected with no user feedback
🟡 MEDIUM src/components/preview_panel/ImageSwapPopover.tsx:55 Duplicate filename generation between client and server produces dead values
🟡 MEDIUM src/pro/main/ipc/handlers/visual_editing_handlers.ts:44 No server-side validation of uploaded file content type or size

See inline comments for details.

Generated by Dyadbot multi-agent code review (3 independent reviewers with consensus voting)

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

@github-actions github-actions bot added the needs-human:review-issue ai agent flagged an issue that requires human review label Feb 14, 2026
@wwwillchen
Copy link
Collaborator

@BugBot run

@github-actions
Copy link
Contributor

github-actions bot commented Feb 14, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@azizmejri1 azizmejri1 removed the needs-human:review-issue ai agent flagged an issue that requires human review label Feb 14, 2026
@github-actions
Copy link
Contributor

🔍 Dyadbot Code Review Summary (Round 2)

Found 7 new issue(s) flagged by 3 independent reviewers (consensus voting: only issues identified by 2+ reviewers are reported).
(5 issue(s) from previous review already addressed or acknowledged)

Summary

Severity Count
🔴 HIGH 1
🟡 MEDIUM 5
⚪ LOW 1

Issues to Address

Severity File Issue
🔴 HIGH src/components/preview_panel/ImageSwapPopover.tsx:43 Apply button accepts empty/invalid URLs with no validation
🟡 MEDIUM worker/dyad-visual-editor-client.js:260 Coordinates sent before new image loads, causing stale overlay
🟡 MEDIUM worker/dyad-visual-editor-client.js:280 handleGetImageSrc is dead code / silently fails when element missing
🟡 MEDIUM src/components/preview_panel/ImageSwapPopover.tsx:61 FileReader error not handled — user sees no feedback on failure
🟡 MEDIUM src/pro/main/ipc/handlers/visual_editing_handlers.ts:53 Single invalid image upload aborts ALL pending changes
🟡 MEDIUM src/components/preview_panel/VisualEditingToolbar.tsx:530 38-line inline onSwap handler with no user feedback on success
⚪ Low Priority Issues (1 item)
  • Hardcoded hex color #7f22fe instead of theme tokensrc/components/preview_panel/ImageSwapPopover.tsx:92

See inline comments for details on HIGH/MEDIUM issues.

Generated by Dyadbot multi-agent code review (3 independent reviewers with consensus voting)

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

@github-actions github-actions bot added the needs-human:review-issue ai agent flagged an issue that requires human review label Feb 14, 2026
@wwwillchen
Copy link
Collaborator

@BugBot run

@azizmejri1 azizmejri1 removed the needs-human:review-issue ai agent flagged an issue that requires human review label Feb 18, 2026
@wwwillchen
Copy link
Collaborator

@BugBot run

azizmejri1 and others added 2 commits February 28, 2026 17:30
- Fix stale event listeners on rapid image swaps using AbortController
- Add dynamic image source detection with warning in ImageSwapPopover
- Fix handleTextUpdated dropping imageSrc/imageUpload from pending changes
- Extract mergePendingChange helper to prevent data loss across edit types
- Add orphaned image file cleanup on transform failure
- Fix client/server image size limit mismatch (derive server limit from client)
- Update "Current source" display to reflect applied swaps
- Add dark mode variants to ImageSwapPopover
- Remove broken image URL from pending changes on load failure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wwwillchen
Copy link
Collaborator

@BugBot run

@azizmejri1
Copy link
Collaborator Author

🤖 Claude Code Review Summary

PR Confidence: 4/5

All 12 unresolved review threads have been addressed with code changes and resolved. The core data-integrity and race-condition issues are fixed, but manual testing of the full image swap flow would be valuable before merge.

Unresolved Threads

Thread Rationale Link

No unresolved threads

Resolved Threads

Issue Rationale Link
Client-side file size limit before reading into memory Already addressed in prior commit (line 69 of ImageSwapPopover.tsx) View
Stale event listeners on rapid image swaps Fixed with AbortController to cancel previous load/error listeners View
VALID_IMAGE_TYPES duplicated between client and server Already addressed — extracted to shared visual-editing.ts View
Error messages not announced to screen readers Already addressed — role="alert", aria-invalid, aria-describedby added View
"Current source" display not updating after swap Fixed with local appliedSrc state that updates on swap View
Dynamic image sources silently overwritten Fixed with isDynamicImage detection and warning in popover View
handleTextUpdated drops imageSrc/imageUpload Fixed with shared mergePendingChange helper View
Orphaned image files on transform failure Fixed with cleanup in catch block View
Pending change construction duplicated across 3 locations Extracted mergePendingChange helper used by all 3 locations View
No dark mode support in ImageSwapPopover Added dark: variants for text and border View
Failed image URL saved as pending change with no undo Broken image URL removed from pending changes on load error View
Client/server image size limits mismatched Server limit now derived from client limit accounting for base64 overhead View
Product Principle Suggestions

No suggestions — product principles were clear enough for all decisions made in this run.


🤖 Generated by Claude Code

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Multi-agent review: 2 new issue(s) found

findImg(child);
if (hasImage) return;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | logic (2/3 reviewers)

findImg does not traverse JSXExpressionContainer children

The recursive findImg function only walks node.children, which contains direct JSX children. However, <img> elements inside JSXExpressionContainer nodes (e.g., {condition && <img src="..." />} or {items.map(i => <img />)}) are nested under node.expression, not node.children. These images will not be detected by analyzeComponent, so the Swap Image button will not appear for components that contain images inside conditional expressions.

Meanwhile, transformContent uses Babel's path.traverse which does handle these cases — so there's a detection/transformation mismatch.

💡 Suggestion: When findImg encounters a node with type === 'JSXExpressionContainer', recursively traverse its expression property. Also handle ConditionalExpression (traverse consequent/alternate) and LogicalExpression (traverse right).

...existing,
imageSrc: undefined,
imageUpload: undefined,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | duplication (2/3 reviewers)

Redundant identical branches in image-load-error handler

The first branch (line 568: if styles has keys) and the third branch (line 578: else) execute the exact same code — both spread existing and set imageSrc: undefined, imageUpload: undefined. Only the middle branch (else if !textContent → delete) is distinct.

💡 Suggestion: Simplify to two branches:

const hasOtherChanges =
  (existing.styles && Object.keys(existing.styles).length > 0) ||
  existing.textContent;
if (hasOtherChanges) {
  updated.set(elementId, { ...existing, imageSrc: undefined, imageUpload: undefined });
} else {
  updated.delete(elementId);
}

@github-actions
Copy link
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

All previously flagged HIGH issues from prior review rounds have been addressed. The remaining issues are minor and non-blocking.

Issues Summary

Severity File Issue
🟡 MEDIUM src/pro/main/utils/visual_editing_utils.ts:481 findImg doesn't traverse JSXExpressionContainer children
🟡 MEDIUM src/components/preview_panel/PreviewIframe.tsx:583 Redundant identical branches in image-load-error handler
🟢 Low Priority Notes (6 items)
  • Image swap hidden for dynamically-styled componentsVisualEditingToolbar.tsx:574 — The ImageSwapPopover is inside the !isDynamic branch, so users can't swap images on components with dynamic CSS. Image swapping is orthogonal to CSS class editing. Consider moving it outside the isDynamic ternary.
  • Image error reverts pending change but not previewPreviewIframe.tsx:559 — When an image fails to load, the pending change is cleaned up but the broken image remains in the iframe preview. Consider sending a message to revert the <img> src in the preview.
  • No loading indicator during file readImageSwapPopover.tsx:82 — Large images (up to 7.5 MB) read via FileReader have no visual feedback during the async read.
  • Tab switching doesn't clear error stateImageSwapPopover.tsx:127 — URL/file errors persist when switching tabs, showing stale error messages.
  • URL input not auto-focusedImageSwapPopover.tsx:148 — When the popover opens, focus stays on the trigger button rather than the URL input.
  • change.imageSrc mutated in-placevisual_editing_handlers.ts:113 — The handler mutates the input parameter object directly. Consider using a separate lookup map.
🚫 Dropped False Positives (3 items)
  • Image detection logic duplicated — Dropped: Only 2 occurrences (~8 lines each), below the 3x threshold for warranting an abstraction per code health guidelines.
  • ComponentAnalysis interface duplicates Zod schema — Dropped: Standard pattern in projects with separate backend type definitions. Minor.
  • Filename sanitization allows leading dots — Dropped: The timestamp prefix (${timestamp}-) prevents the final filename from starting with a dot.

Generated by Dyadbot multi-agent code review

@wwwillchen
Copy link
Collaborator

@BugBot run

@github-actions
Copy link
Contributor

🔍 Dyadbot Code Review Summary

Verdict: ✅ YES - Ready to merge

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

All previously flagged HIGH issues from prior review rounds have been addressed. The remaining issues are minor and non-blocking.

Issues Summary

Severity File Issue
🟡 MEDIUM src/components/preview_panel/PreviewIframe.tsx:556 Broken image remains visible in preview after load error
🟢 Low Priority Notes (5 items)
  • gitAdd not reverted on failuresrc/pro/main/ipc/handlers/visual_editing_handlers.ts:115 — If image write succeeds but transform fails, files are deleted but remain staged in git. Minor edge case.
  • skipOverlayElement may skip elements with text contentworker/dyad-component-selector-client.js:225 — An absolutely-positioned div with meaningful text that covers its parent would be incorrectly classified as an overlay. Heuristic is reasonable but has edge cases.
  • ImageUploadData duplicates zod schema shapesrc/components/preview_panel/ImageSwapPopover.tsx:10 — Interface could be derived from VisualEditingChange['imageUpload'] to avoid drift.
  • Image detection logic duplicated in analyzeComponentsrc/pro/main/utils/visual_editing_utils.ts:434 — Self-check and descendant-check branches duplicate the pattern of calling extractStaticSrc + checking hasSrcAttr + setting isDynamicImage.
  • No loading indicator during file readsrc/components/preview_panel/ImageSwapPopover.tsx:84 — Large file uploads (up to 7.5 MB) show no spinner while FileReader.readAsDataURL is running.
🚫 Dropped False Positives (11 items)
  • Sanitized filename could be empty — Dropped: Timestamp prefix ensures uniqueness regardless; extension loss is unlikely to cause serving issues in practice.
  • mimeType schema accepts any string — Dropped: Server-side validation already catches invalid MIME types; schema tightening is nice-to-have but not blocking.
  • hasStyles false positive for empty nested objects — Dropped: mergePendingChange defaults styles to {} and style values are only set with real data; empty nested objects are not produced in practice.
  • Protocol-relative URLs allowed — Dropped: Intentional and harmless in the Electron context.
  • Multiple uploads identical timestamps — Dropped: Requires same filename + same millisecond timing, which is extremely unlikely.
  • Filename sanitization regex duplicated — Dropped: Client generates a placeholder path; server generates the authoritative name. They don't need to be in sync.
  • Coordinate-sending postMessage duplicated — Dropped: Only 2 occurrences, below the threshold for abstraction.
  • MAX_IMAGE_SIZE naming misleading — Dropped: The inline comment explains the derivation clearly.
  • Apply button no visual feedback — Dropped: The live preview update provides sufficient feedback.
  • Image swap button appears for containers with descendant images — Dropped: This is intentional — the feature is designed to work with containers containing images.
  • Overlay skip may surprise users — Dropped: Well-documented heuristic with appropriate guardrails for content-bearing elements.

Generated by Dyadbot multi-agent code review

@github-actions github-actions bot added needs-human:final-check ai agent thinks everything looks good - needs final review from human and removed needs-human:review-issue ai agent flagged an issue that requires human review labels Feb 28, 2026
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Multi-agent review: 1 MEDIUM issue found

}

if (event.data?.type === "dyad-image-load-error") {
showError("Image failed to load. Please check the URL and try again.");
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | error-recovery (2/3 reviewers)

Broken image remains visible in preview after load error

When a URL is entered and the image fails to load, the dyad-image-load-error handler correctly removes the broken entry from pending changes and shows a toast error. However, the iframe's <img> element still has the broken src set — the user sees a broken image icon in the preview with no way to revert it (short of re-selecting the component). The preview and the pending changes state become out of sync.

💡 Suggestion: When a dyad-image-load-error is received, send a modify-dyad-image-src message back to the iframe with the original/previous src to restore the image in the preview. The original src could be passed in the error event data or tracked in currentImageSrc state.

@github-actions
Copy link
Contributor

🎭 Playwright Test Results

✅ All tests passed!

OS Passed Flaky Skipped
🍎 macOS 237 9 6

Total: 237 tests passed (9 flaky) (6 skipped)

⚠️ Flaky Tests

🍎 macOS

  • context_manage.spec.ts > manage context - exclude paths with smart context (passed after 1 retry)
  • debugging_logs.spec.ts > clear logs button clears all logs (passed after 1 retry)
  • fix_error.spec.ts > fix error with AI (passed after 1 retry)
  • partial_response.spec.ts > partial message is resumed (passed after 1 retry)
  • refresh.spec.ts > refresh preserves current route (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • setup.spec.ts > setup ai provider (passed after 1 retry)
  • undo.spec.ts > undo (passed after 1 retry)
  • visual_editing.spec.ts > swap image via URL (passed after 1 retry)

📊 View full report

@azizmejri1 azizmejri1 marked this pull request as ready for review February 28, 2026 19:56
@azizmejri1 azizmejri1 requested a review from a team February 28, 2026 19:56
@wwwillchen
Copy link
Collaborator

@BugBot run

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +347 to +351
imageSrc: newSrc,
...(uploadData && {
imageUpload: uploadData,
}),
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Stale imageUpload persists when switching from upload to URL mode, causing URL to be overwritten

When a user first uploads an image and then switches to URL mode to enter a URL instead, the stale imageUpload data from the first action is never cleared from pending changes. This causes the saved changes to write the old uploaded file and overwrite the user's URL.

Root Cause

In handleImageSwap (src/components/preview_panel/VisualEditingToolbar.tsx:348-350), when uploadData is undefined (URL mode), the conditional spread ...(uploadData && { imageUpload: uploadData }) evaluates to ...false, which is a no-op. This means imageUpload is not a key in the partial object passed to mergePendingChange.

In mergePendingChange (src/ipc/types/visual-editing.ts:124-125), the check "imageUpload" in partial returns false, so it falls back to existing?.imageUpload — preserving the stale upload data from the previous action.

When changes are saved, the handler at src/pro/main/ipc/handlers/visual_editing_handlers.ts:84 sees change.imageUpload is truthy, writes the old uploaded file to disk, and at line 113 overwrites change.imageSrc with the uploaded file path (/images/${finalFileName}), discarding the URL the user actually intended.

Impact: The user's intended URL is silently replaced with the previously uploaded image. An unnecessary file is written to disk.

Suggested change
imageSrc: newSrc,
...(uploadData && {
imageUpload: uploadData,
}),
}),
imageSrc: newSrc,
imageUpload: uploadData,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 12 files

Confidence score: 3/5

  • The broken image src remains in src/components/preview_panel/PreviewIframe.tsx, so failed loads can still render a bad image even after error handling—this is user-visible behavior.
  • Orphaned staging files are possible in src/pro/main/ipc/handlers/visual_editing_handlers.ts if a write fails before tracking, which could leak temp media on disk.
  • Pay close attention to src/components/preview_panel/PreviewIframe.tsx, src/pro/main/ipc/handlers/visual_editing_handlers.ts, src/components/preview_panel/ImageSwapPopover.tsx - image error handling, cleanup tracking, and file re-selection UX.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/components/preview_panel/PreviewIframe.tsx">

<violation number="1" location="src/components/preview_panel/PreviewIframe.tsx:559">
P2: When an image fails to load, the `dyad-image-load-error` handler correctly removes the broken entry from pending changes and shows an error toast. However, the iframe's `<img>` element still has the broken `src` set — the user sees a broken image icon in the preview with no way to revert it. The preview and pending changes state become out of sync. After removing the pending change, send a `modify-dyad-image-src` message back to the iframe with the original `src` (available via `currentImageSrc` state) to restore the image in the preview.</violation>
</file>

<file name="src/components/preview_panel/ImageSwapPopover.tsx">

<violation number="1" location="src/components/preview_panel/ImageSwapPopover.tsx:48">
P2: Clear the file input in the validation-error branches so users can reselect the same file after an error. Right now the early returns skip `e.target.value = ""`, preventing re-selection of the same invalid file.</violation>
</file>

<file name="src/pro/main/ipc/handlers/visual_editing_handlers.ts">

<violation number="1" location="src/pro/main/ipc/handlers/visual_editing_handlers.ts:99">
P2: The staging copy in .dyad/media isn’t tracked for cleanup until after the public write succeeds, so failures between the media write and the later push can leave orphaned files. Track the media path immediately after writing it so the catch cleanup can remove it.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

return;
}

if (event.data?.type === "dyad-image-load-error") {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

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

P2: When an image fails to load, the dyad-image-load-error handler correctly removes the broken entry from pending changes and shows an error toast. However, the iframe's <img> element still has the broken src set — the user sees a broken image icon in the preview with no way to revert it. The preview and pending changes state become out of sync. After removing the pending change, send a modify-dyad-image-src message back to the iframe with the original src (available via currentImageSrc state) to restore the image in the preview.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/PreviewIframe.tsx, line 559:

<comment>When an image fails to load, the `dyad-image-load-error` handler correctly removes the broken entry from pending changes and shows an error toast. However, the iframe's `<img>` element still has the broken `src` set — the user sees a broken image icon in the preview with no way to revert it. The preview and pending changes state become out of sync. After removing the pending change, send a `modify-dyad-image-src` message back to the iframe with the original `src` (available via `currentImageSrc` state) to restore the image in the preview.</comment>

<file context>
@@ -542,6 +556,35 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
         return;
       }
 
+      if (event.data?.type === "dyad-image-load-error") {
+        showError("Image failed to load. Please check the URL and try again.");
+        // Remove the broken image from pending changes
</file context>
Fix with Cubic

const trimmed = urlValue.trim();
if (!trimmed) {
setUrlError("Please enter a URL.");
return;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

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

P2: Clear the file input in the validation-error branches so users can reselect the same file after an error. Right now the early returns skip e.target.value = "", preventing re-selection of the same invalid file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/preview_panel/ImageSwapPopover.tsx, line 48:

<comment>Clear the file input in the validation-error branches so users can reselect the same file after an error. Right now the early returns skip `e.target.value = ""`, preventing re-selection of the same invalid file.</comment>

<file context>
@@ -0,0 +1,222 @@
+    const trimmed = urlValue.trim();
+    if (!trimmed) {
+      setUrlError("Please enter a URL.");
+      return;
+    }
+    // Accept absolute URLs (http/https/protocol-relative) and root-relative paths
</file context>
Fix with Cubic

Comment on lines +99 to +103
await fsPromises.writeFile(
path.join(mediaDir, finalFileName),
buffer,
);
await ensureDyadGitignored(appPath);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

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

P2: The staging copy in .dyad/media isn’t tracked for cleanup until after the public write succeeds, so failures between the media write and the later push can leave orphaned files. Track the media path immediately after writing it so the catch cleanup can remove it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pro/main/ipc/handlers/visual_editing_handlers.ts, line 99:

<comment>The staging copy in .dyad/media isn’t tracked for cleanup until after the public write succeeds, so failures between the media write and the later push can leave orphaned files. Track the media path immediately after writing it so the catch cleanup can remove it.</comment>

<file context>
@@ -40,11 +48,91 @@ export function registerVisualEditingHandlers() {
+            // Save to .dyad/media as a staging copy
+            const mediaDir = path.join(appPath, DYAD_MEDIA_DIR_NAME);
+            await fsPromises.mkdir(mediaDir, { recursive: true });
+            await fsPromises.writeFile(
+              path.join(mediaDir, finalFileName),
+              buffer,
</file context>
Suggested change
await fsPromises.writeFile(
path.join(mediaDir, finalFileName),
buffer,
);
await ensureDyadGitignored(appPath);
await fsPromises.writeFile(
path.join(mediaDir, finalFileName),
buffer,
);
writtenImagePaths.push(path.join(mediaDir, finalFileName));
await ensureDyadGitignored(appPath);
Fix with Cubic

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab35747a4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +347 to +350
imageSrc: newSrc,
...(uploadData && {
imageUpload: uploadData,
}),

Choose a reason for hiding this comment

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

P1 Badge Clear stale imageUpload when switching to URL swaps

handleImageSwap only sets imageUpload when uploadData is provided, so mergePendingChange keeps any prior upload metadata on URL-only updates. If a user uploads an image and then switches to a URL before saving, the pending change still contains imageUpload; the main apply-visual-editing-changes handler then treats that upload as authoritative and rewrites imageSrc to /images/<generated>, so the saved source no longer matches the URL the user just applied.

Useful? React with 👍 / 👎.

Comment on lines +181 to +184
for (const filePath of writtenImagePaths) {
try {
await fsPromises.unlink(filePath);
} catch {

Choose a reason for hiding this comment

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

P2 Badge Reset git index when cleaning up failed image uploads

The failure cleanup only unlinks files from disk, but uploaded images were already staged earlier with gitAdd. If any later step in this handler throws (for example, while transforming or writing component files), the repo can be left with staged additions pointing to deleted files (AD state), which pollutes subsequent commits and can break later git operations; cleanup needs to unstage/reset those paths as well.

Useful? React with 👍 / 👎.

Copy link
Collaborator

Choose a reason for hiding this comment

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

lets do this too

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR adds image selection and swapping to the visual editor, allowing users to replace <img> sources either via URL or file upload directly from the component toolbar. It also fixes click-through on absolute-positioned overlay elements in the component selector.

Key changes:

  • New ImageSwapPopover component (ImageSwapPopover.tsx) provides a URL input and a file-upload picker with client-side MIME type and size validation (7.5 MB cap).
  • VisualEditingToolbar adds handleImageSwap, which previews the change live in the iframe and stores it as a pending change via the shared mergePendingChange utility.
  • PreviewIframe tracks hasImage/isDynamicImage/currentImageSrc from analyzeComponent and handles dyad-image-load-error messages to revert broken URL changes automatically.
  • visual_editing_handlers.ts writes uploaded image buffers to public/images/ (git-staged) and .dyad/media/ (gitignored staging copy), with upfront validation and cleanup-on-failure rollback.
  • visual_editing_utils.ts adds AST-based image detection (self or first <img> descendant) and transformContent support for replacing src attributes.
  • dyad-component-selector-client.js adds skipOverlayElement to prevent absolute/fixed overlay divs from blocking selection of the underlying component.
  • One logic bug identified: in VisualEditingToolbar.handleImageSwap, when called without uploadData (URL mode), the spread ...(uploadData && { imageUpload: uploadData }) emits no key at all, so mergePendingChange preserves the previous imageUpload from an earlier file upload. On save, the server detects imageUpload, writes the file, and silently overrides imageSrc with the uploaded path — discarding the URL the user entered. Fix: always include imageUpload: uploadData as an explicit key in the partial.

Confidence Score: 3/5

  • Not safe to merge as-is — the stale imageUpload bug can silently apply the wrong image source when a user switches from upload to URL mode before saving.
  • The overall architecture is clean and well-tested (good unit + e2e coverage, server-side validation, rollback on failure). However, there is a clear logic bug in VisualEditingToolbar.handleImageSwap where an upload → URL mode switch leaves stale imageUpload data in the pending change, causing the server to override the user's intended URL with the previously-uploaded file path. This can silently produce incorrect source code. The fix is a one-line change but the impact is user-visible data corruption.
  • Pay close attention to src/components/preview_panel/VisualEditingToolbar.tsx (handleImageSwap, lines 340–355) and the interaction with mergePendingChange in src/ipc/types/visual-editing.ts.

Important Files Changed

Filename Overview
src/components/preview_panel/VisualEditingToolbar.tsx Adds handleImageSwap and wires up ImageSwapPopover; contains a logic bug where switching from upload mode back to URL mode leaves stale imageUpload data in the pending change, causing the server to override the user's URL with the previously-uploaded file on save.
src/components/preview_panel/ImageSwapPopover.tsx New component providing URL and file-upload tabs for swapping image sources; well-structured with validation and error states, but error messages from one tab are not cleared when the user switches to the other tab.
src/pro/main/ipc/handlers/visual_editing_handlers.ts Extends the IPC handler to write uploaded image files to both public/images/ and .dyad/media, with upfront validation, cleanup on failure, and git staging; logic is sound, but relies on the client correctly clearing imageUpload before passing URL-only changes.
src/ipc/types/visual-editing.ts Adds imageSrc/imageUpload fields to the change schema and introduces the mergePendingChange helper; the "key" in partial pattern for conditional preservation is correct but fragile — callers must always include the key to clear a field.
src/pro/main/utils/visual_editing_utils.ts Adds analyzeComponent image detection (self + descendant <img>) and transformContent image src replacement; well-tested and handles both static string literals and JSXExpressionContainer-wrapped string literals.
worker/dyad-visual-editor-client.js Adds handleModifyImageSrc for live image previewing in the iframe; correctly uses AbortController to cancel stale load/error listeners on rapid swaps and fires dyad-image-load-error on failures.
worker/dyad-component-selector-client.js Adds skipOverlayElement to improve click-selection accuracy by walking up from absolute/fixed overlay elements to the underlying dyad-tagged parent; heuristic (≥98% width+height coverage) is reasonable and excludes media elements and scrollable containers.
src/components/preview_panel/PreviewIframe.tsx Adds hasImage, isDynamicImage, and currentImageSrc state from the component analysis result; handles dyad-image-load-error messages from the iframe to revert broken image pending changes cleanly.
e2e-tests/visual_editing.spec.ts Adds a new e2e test covering the URL-based image swap flow end-to-end including applying the change, saving, and snapshot verification; no issues found.
src/pro/main/utils/visual_editing_utils.test.ts Comprehensive new unit tests for image detection and image-src transformation; covers static src, expression-wrapped src, child img detection, missing src, and combined image+class changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant ImageSwapPopover
    participant VisualEditingToolbar
    participant PreviewIframe
    participant IframeWorker as dyad-visual-editor-client.js
    participant IPC as visual_editing_handlers.ts

    User->>PreviewIframe: Click image element
    PreviewIframe->>IPC: analyze-component (appId, componentId)
    IPC-->>PreviewIframe: {hasImage, imageSrc, isDynamicImage}
    PreviewIframe->>VisualEditingToolbar: hasImage=true, currentImageSrc, isDynamicImage

    User->>ImageSwapPopover: Open "Swap Image" popover
    Note over ImageSwapPopover: URL tab or Upload tab

    alt URL mode
        User->>ImageSwapPopover: Enter URL, click Apply
        ImageSwapPopover->>VisualEditingToolbar: onSwap(newSrc, undefined)
        VisualEditingToolbar->>IframeWorker: postMessage(modify-dyad-image-src, src=URL)
        IframeWorker->>IframeWorker: Set imgEl.src = URL
        alt Image loads
            IframeWorker-->>PreviewIframe: dyad-component-coordinates-updated
        else Image fails
            IframeWorker-->>PreviewIframe: dyad-image-load-error
            PreviewIframe->>PreviewIframe: Remove imageSrc from pendingChanges
        end
        VisualEditingToolbar->>PreviewIframe: setPendingChanges (imageSrc=URL)
    else Upload mode
        User->>ImageSwapPopover: Choose file
        ImageSwapPopover->>ImageSwapPopover: FileReader.readAsDataURL
        ImageSwapPopover->>VisualEditingToolbar: onSwap(/images/name, uploadData{base64})
        VisualEditingToolbar->>IframeWorker: postMessage(modify-dyad-image-src, src=base64DataURL)
        IframeWorker->>IframeWorker: Set imgEl.src = base64DataURL
        VisualEditingToolbar->>PreviewIframe: setPendingChanges (imageSrc+imageUpload)
    end

    User->>PreviewIframe: Click "Save Changes"
    PreviewIframe->>IPC: apply-visual-editing-changes(appId, changes)
    opt change.imageUpload present
        IPC->>IPC: Validate MIME type + size
        IPC->>IPC: Write buffer to public/images/ and .dyad/media/
        IPC->>IPC: git add public/images/finalFileName
        IPC->>IPC: change.imageSrc = /images/timestamp-name
    end
    IPC->>IPC: transformContent (update <img src>)
    IPC->>IPC: git add + git commit source file
    IPC-->>PreviewIframe: success
    PreviewIframe-->>User: Toast "Visual changes saved to source files"
Loading

Last reviewed commit: ab35747

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

12 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +340 to +356
updated.set(
selectedComponent.id,
mergePendingChange(existing, {
componentId: selectedComponent.id,
componentName: selectedComponent.name,
relativePath: selectedComponent.relativePath,
lineNumber: selectedComponent.lineNumber,
imageSrc: newSrc,
...(uploadData && {
imageUpload: uploadData,
}),
}),
);
return updated;
});
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Stale imageUpload when switching from upload → URL mode

When a user uploads a file first and then switches to the URL tab and clicks Apply, the old imageUpload data is silently preserved in the pending change. This is because ...(uploadData && { imageUpload: uploadData }) spreads nothing when uploadData is undefined, so "imageUpload" in partial is false in mergePendingChange — and it falls back to existing?.imageUpload.

When the user saves, the server handler detects change.imageUpload, writes the previously-uploaded file, and overwrites change.imageSrc with that file's path — discarding the URL the user just entered.

Fix: always include the imageUpload key in the partial (even as undefined) so mergePendingChange clears the old value:

Suggested change
updated.set(
selectedComponent.id,
mergePendingChange(existing, {
componentId: selectedComponent.id,
componentName: selectedComponent.name,
relativePath: selectedComponent.relativePath,
lineNumber: selectedComponent.lineNumber,
imageSrc: newSrc,
...(uploadData && {
imageUpload: uploadData,
}),
}),
);
return updated;
});
}
};
updated.set(
selectedComponent.id,
mergePendingChange(existing, {
componentId: selectedComponent.id,
componentName: selectedComponent.name,
relativePath: selectedComponent.relativePath,
lineNumber: selectedComponent.lineNumber,
imageSrc: newSrc,
imageUpload: uploadData,
}),
);

With imageUpload: uploadData always present as a key in the partial, "imageUpload" in partial is always true, so mergePendingChange uses partial.imageUpload (either the new upload data or undefined).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/preview_panel/VisualEditingToolbar.tsx
Line: 340-356

Comment:
**Stale `imageUpload` when switching from upload → URL mode**

When a user uploads a file first and then switches to the URL tab and clicks Apply, the old `imageUpload` data is silently preserved in the pending change. This is because `...(uploadData && { imageUpload: uploadData })` spreads **nothing** when `uploadData` is `undefined`, so `"imageUpload" in partial` is `false` in `mergePendingChange` — and it falls back to `existing?.imageUpload`.

When the user saves, the server handler detects `change.imageUpload`, writes the previously-uploaded file, and **overwrites** `change.imageSrc` with that file's path — discarding the URL the user just entered.

Fix: always include the `imageUpload` key in the partial (even as `undefined`) so `mergePendingChange` clears the old value:

```suggestion
        updated.set(
          selectedComponent.id,
          mergePendingChange(existing, {
            componentId: selectedComponent.id,
            componentName: selectedComponent.name,
            relativePath: selectedComponent.relativePath,
            lineNumber: selectedComponent.lineNumber,
            imageSrc: newSrc,
            imageUpload: uploadData,
          }),
        );
```

With `imageUpload: uploadData` always present as a key in the partial, `"imageUpload" in partial` is always `true`, so `mergePendingChange` uses `partial.imageUpload` (either the new upload data or `undefined`).

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Contributor

🔍 Dyadbot Code Review Summary

Verdict: 🤔 NOT SURE - Potential issues

Reviewed by 3 independent agents: Correctness Expert, Code Health Expert, UX Wizard.

Issues Summary

Severity File Issue
🔴 HIGH src/components/preview_panel/VisualEditingToolbar.tsx:342 Stale imageUpload persists when switching from upload to URL
🟡 MEDIUM src/pro/main/utils/visual_editing_utils.ts:441 Image detection logic duplicated for self vs. descendant img
🟡 MEDIUM src/components/preview_panel/ImageSwapPopover.tsx:10 ImageUploadData interface duplicates Zod schema shape
🟢 Low Priority Notes (5 items)
  • Redundant null checks on selectedComponent - src/components/preview_panel/VisualEditingToolbar.tsx:319 — both branches guard against the same variable; a single early return would be cleaner
  • Magic number for file size limit - src/components/preview_panel/ImageSwapPopover.tsx:747.5 * 1024 * 1024 should be a shared constant alongside VALID_IMAGE_MIME_TYPES
  • Client-side sanitized filename is misleading - src/components/preview_panel/ImageSwapPopover.tsx:90 — the placeholder path constructed on the client is never used; a clearly placeholder value would reduce confusion
  • Two file write locations without explanation - src/pro/main/ipc/handlers/visual_editing_handlers.ts:91 — the purpose of the .dyad/media staging copy vs. public/images is not documented
  • Empty/extension-only filenames produce poor final filenames - src/pro/main/ipc/handlers/visual_editing_handlers.ts:87 — if filename is empty after sanitization, the resulting path has no extension
🚫 Dropped False Positives (8 items)
  • findImg does not traverse JSXExpressionContainer children — Already commented on in existing review (line 481)
  • transformContent traversal inconsistency — Corollary of the JSXExpressionContainer issue already flagged
  • Style emptiness check doesn't account for nested empty objects — Harmless; empty styles won't modify source files during save
  • Default tab mode should be contextual — Enhancement/preference, not a real issue; URL tab is a reasonable default
  • No visual confirmation after file upload — The filename appears on the button and the preview updates; sufficient feedback
  • Mode not reset when switching components — Staying on the same tab is arguably reasonable for workflow continuity
  • Upload button screen reader state — The button text already updates to the filename; additional announcement is optional polish
  • skipOverlayElement doesn't handle all patterns — The current implementation handles the primary case (inset-0 overlays); handling sticky/partial overlays would add false positive risk

Generated by Dyadbot multi-agent code review

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Multi-agent review: 3 issue(s) found (1 HIGH, 2 MEDIUM)

lineNumber: selectedComponent.lineNumber,
imageSrc: newSrc,
...(uploadData && {
imageUpload: uploadData,
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 HIGH | data-integrity

Stale imageUpload persists when switching from file upload to URL mode

When handleImageSwap is called without uploadData (URL mode), the spread ...(uploadData && { imageUpload: uploadData }) produces no keys, so imageUpload is never set in the partial object. In mergePendingChange, since "imageUpload" in partial is false, it falls back to existing?.imageUpload, preserving stale upload data from a previous file upload.

If a user uploads a file, then switches to URL mode and enters a URL, the old imageUpload data remains. When saved, the server sees imageUpload, writes the old file to disk, and overwrites change.imageSrc with the file path, silently discarding the URL the user entered.

💡 Suggestion: Always set imageUpload explicitly:

Suggested change
imageUpload: uploadData,
imageSrc: newSrc,
imageUpload: uploadData,

Copy link
Collaborator

Choose a reason for hiding this comment

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

@azizmejri1 let's fix this

Comment on lines +441 to +472
// Check if the element itself is an <img>
if (tagName.type === "JSXIdentifier" && tagName.name === "img") {
hasImage = true;
imageSrc = extractStaticSrc(foundElement.openingElement);
// If there's a src attribute but extractStaticSrc returned undefined, it's dynamic
const hasSrcAttr = foundElement.openingElement.attributes.some(
(attr: any) => attr.type === "JSXAttribute" && attr.name?.name === "src",
);
if (hasSrcAttr && !imageSrc) {
isDynamicImage = true;
}
}

// Recursively check descendants for <img> elements
if (!hasImage && foundElement) {
const findImg = (node: any): void => {
if (!node || hasImage) return;

if (
node.type === "JSXElement" &&
node.openingElement.name.type === "JSXIdentifier" &&
node.openingElement.name.name === "img"
) {
hasImage = true;
imageSrc = extractStaticSrc(node.openingElement);
const hasSrcAttr = node.openingElement.attributes.some(
(attr: any) =>
attr.type === "JSXAttribute" && attr.name?.name === "src",
);
if (hasSrcAttr && !imageSrc) {
isDynamicImage = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | duplication

Image detection logic duplicated for self vs. descendant <img>

The block at lines 441-451 (checking if the element itself is an <img>) and the block inside findImg (lines 459-472) contain identical logic: call extractStaticSrc, check for hasSrcAttr, set isDynamicImage. This is a ~10-line copy-paste.

💡 Suggestion: Extract into a helper, e.g.:

function analyzeImgElement(openingElement: any): { imageSrc?: string; isDynamicImage: boolean } {
  const imageSrc = extractStaticSrc(openingElement);
  const hasSrcAttr = openingElement.attributes.some(
    (attr: any) => attr.type === "JSXAttribute" && attr.name?.name === "src",
  );
  return { imageSrc, isDynamicImage: hasSrcAttr && !imageSrc };
}

Comment on lines +10 to +14
export interface ImageUploadData {
fileName: string;
base64Data: string;
mimeType: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | duplication

ImageUploadData interface duplicates the Zod schema shape

This manually declares { fileName, base64Data, mimeType } which is the exact same shape as the imageUpload field in VisualEditingChangeSchema. If the schema changes, this interface won't automatically stay in sync.

💡 Suggestion: Derive from the Zod schema:

Suggested change
export interface ImageUploadData {
fileName: string;
base64Data: string;
mimeType: string;
}
export type ImageUploadData = NonNullable<VisualEditingChange['imageUpload']>;

Copy link
Collaborator

@wwwillchen wwwillchen left a comment

Choose a reason for hiding this comment

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

@azizmejri1 overall LGTM (good feature!)
let's first resolve the review comments (I commented on a couple specific issues) before merging

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

Labels

needs-human:final-check ai agent thinks everything looks good - needs final review from human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add image selection and swapping in the visual editor

3 participants