Skip to content

Media library#2840

Closed
azizmejri1 wants to merge 19 commits intodyad-sh:mainfrom
azizmejri1:media-library
Closed

Media library#2840
azizmejri1 wants to merge 19 commits intodyad-sh:mainfrom
azizmejri1:media-library

Conversation

@azizmejri1
Copy link
Copy Markdown
Collaborator

@azizmejri1 azizmejri1 commented Mar 1, 2026

I am gonna add the following follow-up PRs :

  • Support image generation inside the chat
  • Support motion graphics generation using Remotion
  • Support selecting multiple images (useful for referencing/moving images)

@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 introduces a significant new 'Media Library' feature, enhancing user interaction with application assets. It integrates AI-powered image generation directly into the workflow and refines the overall library experience with improved navigation, search, and display for all content types. The changes provide a more cohesive and powerful environment for managing and utilizing creative assets within the application.

Highlights

  • Comprehensive Media Library: Introduced a new Media Library feature allowing users to manage images within their applications. This includes functionalities like renaming, moving between apps, deleting, and previewing media files directly within the UI.
  • AI-Powered Image Generation: Added the capability to generate images using AI, with options for different visual styles (plain, 3D clay, photography, isometric illustration). Generated images are automatically saved to a specified application's media folder.
  • Enhanced Chat Input with Media Mentions: The chat input now supports @media: mentions, allowing users to reference images from their media library directly in conversations. A preview of mentioned images is displayed, and these mentions are resolved to image attachments when sending messages.
  • Refactored Library UI: The main Library page has been redesigned to unify the display of prompts, themes, and media files. It now features a search bar, filter tabs (All, Themes, Prompts, Media), and a consistent card-based layout for all library items, including a new landing animation.
  • New IPC Handlers and Hooks: Implemented new Inter-Process Communication (IPC) handlers and React hooks for seamless interaction with media files and image generation services, ensuring robust and secure file operations and external API calls.
Changelog
  • e2e-tests/media_library.spec.ts
    • Added end-to-end tests for media library functionalities including rename, move, delete, and chat integration.
    • Included tests for media folder height normalization in the library grid.
  • package-lock.json
    • Updated package version from 0.37.0-beta.2 to 0.37.0.
  • src/components/DyadAppMediaFolder.tsx
    • Added a new component to display media files within an application folder.
    • Implemented actions for media files: rename, move, delete, and start chat with image reference.
    • Included image preview functionality and dynamic height adjustment for collapsed folders.
  • src/components/ImageGeneratorDialog.tsx
    • Added a new dialog component for AI-powered image generation.
    • Provided options for various image theme modes and target application selection for saving generated images.
  • src/components/LibraryCard.tsx
    • Added a new generic card component to display various library items (themes, prompts).
    • Integrated action buttons for themes and prompts within the card.
  • src/components/LibraryFilterTabs.tsx
    • Added a new component for filtering library items by type (All, Themes, Prompts, Media).
  • src/components/LibraryList.tsx
    • Updated library navigation to include 'All' and 'Media' sections.
    • Adjusted routing logic for active navigation links to support new library paths.
  • src/components/LibrarySearchBar.tsx
    • Added a new reusable search bar component for filtering library content.
  • src/components/NewLibraryItemMenu.tsx
    • Added a new dropdown menu for creating new library items, including a 'Generate Image' option.
  • src/components/app-sidebar.tsx
    • Modified the 'Library' sidebar item to point to the new unified library route.
    • Expanded the logic for determining the active library route to include new prompts and media paths.
  • src/components/chat/ChatInput.tsx
    • Integrated the new MediaMentionPreview component to display image previews in the chat input.
    • Included the useAppMediaFiles hook to fetch media data for chat functionality.
  • src/components/chat/LexicalChatInput.tsx
    • Modified the Lexical editor to support and parse @media: mentions.
    • Updated mention item types to include 'media' for proper display and handling.
    • Added logic to convert display names like @AppName/filename to internal @media:AppName/filename references.
  • src/components/chat/MediaMentionPreview.tsx
    • Added a new component to extract and display thumbnail previews of @media: mentions in the chat input.
  • src/hooks/useAppMediaFiles.ts
    • Added a new React hook for listing, renaming, deleting, and moving media files across applications.
    • Implemented mutation functions with success/error handling and query invalidation.
  • src/hooks/useGenerateImage.ts
    • Added a new React hook to trigger image generation via IPC and manage its state.
  • src/hooks/useMediaDataUri.ts
    • Added a new React hook to fetch media file content as a data URI for direct image display in the UI.
  • src/hooks/useSelectChat.ts
    • Extended the selectChat function to accept a prefillInput parameter, allowing the chat input to be pre-populated when navigating to a new chat.
  • src/ipc/handlers/chat_stream_handlers.ts
    • Modified chat stream handlers to parse and resolve @media: mentions in user prompts.
    • Added resolved media files as attachments to the chat message and stripped @media: tags from the prompt text.
  • src/ipc/handlers/image_generation_handlers.ts
    • Added new IPC handlers for generating images using an external service.
    • Implemented logic to save generated images to the specified application's media folder.
  • src/ipc/handlers/media_handlers.ts
    • Added new IPC handlers for listing, reading, renaming, deleting, and moving media files.
    • Included validation for file names and extensions, and implemented file system operations.
  • src/ipc/ipc_host.ts
    • Registered new IPC handlers for media management and image generation.
  • src/ipc/preload/channels.ts
    • Added new IPC channels for media and image generation contracts.
  • src/ipc/types/image_generation.ts
    • Defined Zod schemas and TypeScript types for image generation parameters and responses.
    • Created IPC contracts and client for image generation.
  • src/ipc/types/index.ts
    • Exported new media and image generation types and IPC clients.
  • src/ipc/types/media.ts
    • Defined Zod schemas and TypeScript types for media file management operations.
    • Created IPC contracts and client for media operations.
  • src/ipc/utils/resolve_media_mentions.ts
    • Added a new utility function to resolve @media: mentions to their corresponding file paths and metadata.
  • src/lib/queryKeys.ts
    • Added new query keys for media-related data fetching and caching.
  • src/pages/library-home.tsx
    • Added a new main library homepage component with a landing animation.
    • Integrated prompts, themes, and media folders with search and filtering capabilities.
    • Implemented dynamic card height normalization for consistent layout.
  • src/pages/library.tsx
    • Refactored the existing library page to use the new LibraryCard component.
    • Adjusted styling and removed the now-redundant PromptCard component.
  • src/pages/media.tsx
    • Added a new dedicated page for managing media files, including search and image generation.
  • src/pages/themes.tsx
    • Refactored the themes page to use the new LibraryCard component.
    • Adjusted styling and removed the now-redundant ThemeCard component.
  • src/router.ts
    • Updated the main library route to point to LibraryHomePage.
    • Added new routes for /prompts and /media.
  • src/routes/library.ts
    • Modified the library route to render LibraryHomePage.
  • src/routes/media.ts
    • Added a new route definition for the /media page.
  • src/routes/prompts.ts
    • Added a new route definition for the /prompts page.
  • src/shared/parse_media_mentions.ts
    • Added a new utility function to parse @media: mentions from a given string.
Activity
  • The pull request was created by azizmejri1, introducing a new media library feature.
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. ↩

@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive Media Library feature, including UI components for browsing, managing (rename, move, delete), and generating images, as well as backend handlers and e2e tests. The implementation is robust, with thoughtful additions like file operation locking to prevent race conditions and a unified library view. I've identified a couple of high-severity bugs in the new UI components related to incorrect CSS class definitions that will cause styling issues. Once these are addressed, this will be an excellent addition to the application.

Copy link
Copy Markdown
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: 10 issue(s) found (1 HIGH, 9 MEDIUM)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 1, 2026

πŸ” Dyadbot Code Review Summary

Verdict: β›” NO - Do NOT merge

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

Issues Summary

Severity File Issue
πŸ”΄ HIGH src/shared/parse_media_mentions.ts:1 Global regex /g flag causes stateful lastIndex bug β€” @media: mentions will intermittently fail to resolve
πŸ”΄ HIGH src/components/chat/MediaMentionPreview.tsx:4 Same global regex bug + duplicated logic from shared module
🟑 MEDIUM src/ipc/utils/resolve_media_mentions.ts:16 getMimeType duplicated across 3 files
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:118 Unvalidated URL fetch from external API (SSRF risk)
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:103 Hardcoded .dyad/media path instead of DYAD_MEDIA_DIR_NAME constant
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:106 Filename sanitization produces empty/ugly names for non-ASCII prompts
🟑 MEDIUM src/hooks/useMediaDataUri.ts:9 Manual useState/useEffect instead of useQuery β€” no caching, stale data after mutations
🟑 MEDIUM src/components/DyadAppMediaFolder.tsx:343 Collapsed media folder not keyboard-accessible + missing aria-labels
🟑 MEDIUM src/components/LibraryFilterTabs.tsx:25 Filter tabs lack ARIA tab/tablist semantics
🟑 MEDIUM src/components/ImageGeneratorDialog.tsx:227 Dialog cannot be dismissed during generation (up to 2 min)
🟒 Low Priority Notes (5 items)
  • Non-atomic move operation β€” src/ipc/handlers/media_handlers.ts:283 β€” copy+delete could leave orphaned copy if unlink fails
  • Complex height-normalization effect β€” src/pages/library-home.tsx:135 β€” 90-line useEffect could be extracted to a custom hook
  • Overlapping click targets on thumbnails β€” src/components/DyadAppMediaFolder.tsx:468 β€” 28px action button below 44px touch target minimum
  • Unstyled loading states β€” src/pages/library-home.tsx:395, src/pages/media.tsx:46 β€” plain "Loading..." text with no visual treatment
  • Image generation dialog keeps form visible after success β€” src/components/ImageGeneratorDialog.tsx β€” confusing dual state of result + editable form
🚫 Dropped False Positives (5 items)
  • Media mention regex matching substrings β€” Dropped: The word boundary (?![\w-]) in the regex and ordered conversion pass make collisions very unlikely
  • getFileNameWithoutExtension edge case for extensionless files β€” Dropped: Only SUPPORTED_MEDIA_EXTENSIONS files are allowed, making this unreachable
  • assertSafeFileName/assertSafeBaseName duplication β€” Dropped: These have different semantics and only share a few lines of validation
  • Module-level animation flag is fragile β€” Dropped: Standard pattern for once-per-session animations, works correctly in production
  • App selector popover not keyboard navigable β€” Dropped: Individual buttons are tab-focusable, providing basic keyboard access

Generated by Dyadbot multi-agent code review

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

@BugBot run

@azizmejri1
Copy link
Copy Markdown
Collaborator Author

πŸ€– Claude Code Review Summary

PR Confidence: 4/5

All review comments have been addressed with code changes, but full E2E testing is recommended before merge given the breadth of changes across rendering, IPC, and accessibility.

Unresolved Threads

No unresolved threads

Resolved Threads

Issue Rationale Link
Global regex /g stateful lastIndex bug (HIGH) Moved regex creation inside function body so each call starts fresh. Also removed duplicate in MediaMentionPreview.tsx, now imports shared parseMediaMentions. View
getMimeType duplicated across 3 files (MEDIUM) Extracted into shared src/ipc/utils/mime_utils.ts, all call sites now import from there. View
Unvalidated URL fetch / SSRF risk (MEDIUM) Added HTTPS protocol validation and 50MB size limit check before writing downloaded image to disk. Per Principle #4: Transparent Over Magical, users should not be exposed to hidden security risks. View
Hardcoded .dyad/media path (MEDIUM) Replaced with DYAD_MEDIA_DIR_NAME constant from media_path_utils.ts. View
useMediaDataUri manual useState/useEffect (MEDIUM) Refactored to useQuery with key ['media-data-uri', appId, fileName] for caching, deduplication, and automatic refetch. View
Collapsed media folder not keyboard-accessible (MEDIUM) Added role="button", tabIndex={0}, onKeyDown for Enter/Space. Added aria-label to close preview and back buttons. View
Filter tabs lack ARIA tab semantics (MEDIUM) Added role="tablist" to container, role="tab" and aria-selected to each button. View
Dialog cannot be dismissed during generation (MEDIUM) Removed early return in handleOpenChange when isPending, allowing users to close the dialog. Per Principle #3: Intuitive But Power-User Friendly, users should not be trapped. View
Filename sanitization for non-ASCII prompts (MEDIUM) Added .replace(/^_|_$/g, '') trim and || 'image' fallback for empty sanitized prompts. View
Tailwind CSS class typo bg-(--background-lightest) (HIGH) Fixed to bg-[--background-lightest] with square brackets for arbitrary CSS variable values. View
Product Principle Suggestions

No suggestions β€” principles were clear enough for all decisions.


πŸ€– Generated by Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 1, 2026

πŸ” Dyadbot Code Review Summary

Verdict: β›” NO - Do NOT merge

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

Issues Summary

Severity File Issue
πŸ”΄ HIGH src/shared/parse_media_mentions.ts:2 Media mention regex too restrictive for app names with spaces/special chars
🟑 MEDIUM src/ipc/handlers/media_handlers.ts:178 readMediaFile reads entire file into memory with no size limit
🟑 MEDIUM src/hooks/useSelectChat.ts:43 prefillInput relies on timing after navigation with no guarantee
🟑 MEDIUM src/ipc/handlers/media_handlers.ts:28 Hardcoded .dyad/media path (different file from existing comment on image_generation_handlers)
🟑 MEDIUM src/pages/media.tsx:23 Media filtering logic duplicated between media.tsx and library-home.tsx
🟑 MEDIUM src/components/chat/LexicalChatInput.tsx:454 O(NΓ—M) nested loop for media mention conversion on every keystroke
🟑 MEDIUM src/components/ImageGeneratorDialog.tsx:246 Generation error not displayed inline in dialog
🟑 MEDIUM src/components/ImageGeneratorDialog.tsx:119 Style selector buttons lack accessible selected state
🟑 MEDIUM src/hooks/useMediaDataUri.ts:10 No staleTime causes unnecessary refetches and flickers
🟑 MEDIUM src/components/DyadAppMediaFolder.tsx:202 Rename dialog allows submitting server-rejected names with no client validation
🟒 Low Priority Notes (8 items)
  • Move operation not atomic - src/ipc/handlers/media_handlers.ts:272 - Failed unlink after copy leaves file duplicated
  • Module-level mutable flag for animation - src/pages/library-home.tsx:122 - Resets on HMR during development
  • useMediaDataUri query key not in queryKeys - src/hooks/useMediaDataUri.ts:11 - Inconsistent with rest of codebase
  • Plain text "Loading..." - src/pages/library-home.tsx:399 / src/pages/media.tsx:54 - Unstyled loading state
  • Media thumbnails not keyboard-activatable - src/components/DyadAppMediaFolder.tsx:478 - Missing role/tabIndex/onKeyDown
  • Search input lacks accessible label - src/components/LibrarySearchBar.tsx:13 - No aria-label on search input
  • Empty state generic when search filters results - src/components/DyadAppMediaFolder.tsx:431 - Same message for empty folder vs filtered
  • handleOpenChange doesn't reset form state - src/components/ImageGeneratorDialog.tsx:67 - Stale prompt/style on reopen
🚫 Dropped False Positives (5 items)
  • response.json() error handling - Dropped: Overlaps with existing comment about unvalidated URL fetch from external API response
  • handleStartNewChatWithImage chatId validation - Dropped: IPC calls throw on failure; the defensive guard is unnecessary
  • Landing animation complexity - Dropped: Subjective code style preference; animation is isolated in its own component
  • Display text stripping collision with Lexical mentions - Dropped: Speculative concern; the replace pattern is straightforward and doesn't collide
  • Downloaded image response status not checked - Dropped: Covered by existing comment about unvalidated URL fetch

Generated by Dyadbot multi-agent code review

Copy link
Copy Markdown
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 HIGH + 9 MEDIUM issues found

@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

πŸ” Dyadbot Code Review Summary

Verdict: πŸ€” NOT SURE - Potential issues

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

Note: 21 issues were already flagged by previous reviews. This summary covers only NEW findings.

Issues Summary

Severity File Issue
🟑 MEDIUM src/components/chat/LexicalChatInput.tsx:249 Media mentions lose @media: prefix, indistinguishable from file mentions
🟑 MEDIUM src/ipc/handlers/media_handlers.ts:272 Move copy+delete can leave duplicate if unlink fails
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:42 Image generator shows confusing error for non-Pro users
🟑 MEDIUM src/components/DyadAppMediaFolder.tsx:434 Filtered results say "No media files found" instead of "No matches"
🟑 MEDIUM src/pages/library-home.tsx:292 Card height equalization uses ~90 lines of complex DOM measurement
🟒 Low Priority Notes (6 items)
  • Cross-app media URL concern - src/ipc/handlers/chat_stream_handlers.ts - mediaUrl uses chat.app.path which would be wrong if media mention references a different app
  • Module-level mutable state - src/pages/library-home.tsx:25 - hasAnimatedThisSession flag survives HMR and is hard to test
  • Plain text "Loading..." - src/pages/library-home.tsx / media.tsx / library.tsx - No spinner or skeleton, bare text is inconsistent with the polished animations
  • No Cmd+Enter to submit - src/components/ImageGeneratorDialog.tsx - Users expect keyboard shortcut for generation
  • Small thumbnails - src/components/DyadAppMediaFolder.tsx - 120px thumbnails are small for image recognition and touch targets
  • Search input missing label - src/components/LibrarySearchBar.tsx - No aria-label or <label> for screen readers
🚫 Dropped False Positives (5 items)
  • Double .catch on move operation - Dropped: The .catch(() => {}) pattern is intentional to prevent unhandled promise rejections; error toast fires from the mutation hook
  • DyadAppMediaFolder mega-component - Dropped: 624 lines is large but acceptable for a feature component with dialogs; can be split in follow-ups
  • Missing HTTP status check on image download - Dropped: Already partially covered by existing SSRF comment on same file/lines
  • Back button small click target - Dropped: Already partially covered by existing accessibility comment on same component
  • Non-unique app name lookup - Dropped: Theoretical concern; the UI only shows media from the current app's mention picker

Generated by Dyadbot multi-agent code review

@wwwillchen
Copy link
Copy Markdown
Collaborator

@BugBot run

@azizmejri1
Copy link
Copy Markdown
Collaborator Author

πŸ€– Claude Code Review Summary

PR Confidence: 3/5

Nine review comments addressed with code changes; six threads flagged for human review due to architectural decisions needed (mention format redesign, Pro user gating, performance optimization).

Unresolved Threads

Thread Rationale Link
Media mention regex too restrictive (HIGH) Needs design decision on mention format (app ID vs app name encoding). Principles #2 and #4 conflict. View
prefillInput race condition Theoretical timing concern; needs decision on restructuring the navigation/prefill flow. View
O(N*M) media mention conversion Performance optimization needed at scale; code is outdated per GitHub. View
Media mention nodes lose @media: prefix Tightly coupled with mention format redesign; should be addressed together. View
Non-Pro user error UX Requires product decision on Pro status surfacing and upgrade prompt design. View
Card height equalization complexity Works correctly; simpler implementation possible but needs testing to avoid visual regressions. View

Resolved Threads

Issue Rationale Link
readMediaFile no size limit Added 20MB size check before reading to prevent OOM View
Hardcoded .dyad/media path Replaced with DYAD_MEDIA_DIR_NAME constant in media_handlers.ts View
No staleTime on useMediaDataUri Added 5-minute staleTime to reduce unnecessary refetches View
Move operation no error recovery Added try/catch with cleanup on unlink failure to prevent silent duplicates View
Misleading empty state message Shows 'No files match your search' when search is active vs 'No media files found' View
Duplicated media filtering logic Extracted shared filterMediaAppsByQuery utility used by both media.tsx and library-home.tsx View
Style selector lacks accessible state Added aria-pressed to theme mode buttons in ImageGeneratorDialog View
Generation error not displayed inline Added inline error banner in dialog when generation fails View
Rename dialog allows invalid names Added client-side validation mirroring server-side INVALID_FILE_NAME_CHARS check View
Product Principle Suggestions

The following suggestions could improve rules/product-principles.md to help resolve ambiguous cases in the future:

  • Principle Infinite Loop: Checking Node.js setup...Β #2: Productionizable: Add guidance on internal data format decisions β€” when identifiers (like app IDs) should be preferred over display names for data serialization, and how to handle backward compatibility when changing formats.
  • Principle Query | Question about model availability via OpenRouterΒ #3: Intuitive But Power-User Friendly: Add guidance on whether features requiring paid subscriptions should be hidden from free users or shown with upgrade prompts, and the preferred UX pattern for surfacing subscription status.
  • Principle Make node.js setup have a more explicit error stateΒ #6: Delightful: Add guidance on acceptable complexity thresholds for DOM measurement code β€” when to prefer simpler CSS-only solutions vs JavaScript-based layout normalization.

πŸ€– Generated by Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 6, 2026

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 248 2 11 6

Summary: 248 passed, 2 failed, 11 flaky, 6 skipped

Failed Tests

🍎 macOS

  • media_library.spec.ts > media library - rename, move, delete, and start a new chat with image reference
    • Error: expect(locator).toBeVisible() failed
  • undo.spec.ts > undo
    • Error: expect(locator).toBeVisible() failed

πŸ“‹ Re-run Failing Tests (macOS)

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

npm run e2e \
  e2e-tests/media_library.spec.ts \
  e2e-tests/undo.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • approve.spec.ts > write to index, approve, check preview (passed after 1 retry)
  • chat_tabs.spec.ts > only shows tabs for chats opened in current session (passed after 1 retry)
  • context_limit_banner.spec.ts > context limit banner shows 'running out' when near context limit (passed after 1 retry)
  • context_manage.spec.ts > manage context - exclude paths (passed after 1 retry)
  • debugging_logs.spec.ts > console logs should appear in the console (passed after 1 retry)
  • engine.spec.ts > send message to engine - openai gpt-5 (passed after 1 retry)
  • select_component.spec.ts > select component next.js (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)
  • template-create-nextjs.spec.ts > create next.js app (passed after 1 retry)
  • ... and 1 more

πŸ“Š View full report

- Add data: URI blocking to SVG sanitizer (defense-in-depth)
- Add Content-Security-Policy header for SVG responses
- Add MAX_IMAGE_SIZE check for base64-decoded images
- Fix e2e test selector (remove non-existent data attribute)
- Move INVALID_FILE_NAME_CHARS import to top of DyadAppMediaFolder
- Remove redundant .catch() on move operation
- Convert sync fs calls to async (rename, unlink, copyFile)
- Use buildDyadMediaUrl instead of inline URL construction
- Add reduced-motion support for library landing animation

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

@BugBot run

@azizmejri1
Copy link
Copy Markdown
Collaborator Author

πŸ€– Claude Code Review Summary

PR Confidence: 4/5

All actionable review comments addressed; one thread flagged for human review on a product decision (slash command hint in LibraryCard).

Unresolved Threads

Thread Rationale Link
LibraryCard drops slash command hint Product decision: unclear if intentional simplification or oversight. Principles #3 and #6 considered but insufficient for this UX decision. View

Resolved Threads

Issue Rationale Link
SVG sanitization: data: URI bypass + CSP header (4 threads) Added data: URI blocking regex and Content-Security-Policy: script-src 'none' header for defense-in-depth View
Base64 image size limit (2 threads) Added MAX_IMAGE_SIZE check for b64_json branch, matching the URL download branch View
E2E test selector references non-existent data attribute Fixed selector to remove data-library-grid-height-item filter View
Import placement (INVALID_FILE_NAME_CHARS) Moved import to top of file with other imports View
Redundant .catch() on move operation Removed redundant outer .catch() View
Sync fs calls block main process (3 threads) Converted renameSync, unlinkSync, copyFileSync to async equivalents View
Inline dyad-media:// URL duplicates buildDyadMediaUrl Replaced with buildDyadMediaUrl import View
Landing animation not reduced-motion aware Added useReducedMotion() hook to skip animation View
Re-encoding mismatch in media refs (2 threads) Addressed by using buildDyadMediaUrl for consistent encoding View
generateImage returns raw app.path Already fixed β€” appPath uses getDyadAppPath(app.path) View
"Generate Another" doesn't clear error state Already fixed β€” handleNewGeneration calls generateImage.reset() View
Non-Pro user error in image generator Already fixed β€” dialog shows locked state with AiAccessBanner View
Various low-priority concerns (regex restrictiveness, race conditions, performance, redundant checks, SSRF, module state, media grid duplication, etc.) Resolved with explanations β€” issues are theoretical, intentional trade-offs, or already handled Multiple threads
Product Principle Suggestions

The following suggestions could improve rules/product-principles.md to help resolve ambiguous cases in the future:


πŸ€– Generated by Claude Code

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant 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 new issue(s) found (after deduplication against 67 existing comments)

chat.app.path,
chat.app.name,
);
const resolvedMediaRefs = resolvedMedia.map((media) =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | data-integrity

buildDyadMediaUrl receives chat.app.path (relative) but media URL builder expects an absolute app path

The buildDyadMediaUrl function at src/lib/dyadMediaUrl.ts encodes appPath directly into the dyad-media:// URL. Here it receives chat.app.path (the DB-stored relative path), but resolveMediaMentions above already calls getDyadAppPath(appPath) internally to resolve the absolute path. This means the display URL and the resolved file path may use different path formats, causing the thumbnail in the chat UI to fail to load.

πŸ’‘ Suggestion: Use the absolute media.filePath's parent directory or call getDyadAppPath(chat.app.path) before passing to buildDyadMediaUrl, consistent with how ImagePreview and DyadAppMediaFolder use absolute paths.


function MediaFileThumbnail({
file,
appPath,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | correctness

handleStartNewChatWithImage encodes filename but display text in chat shows encoded %20 etc.

When starting a new chat with an image, prefillInput is set to @media:${encodeURIComponent(file.fileName)}. The ExternalValueSyncPlugin in LexicalChatInput.tsx then strips @media: and decodes it for display. However, the e2e test at media_library.spec.ts:162 asserts the chat input toContainText('@chat-image.png') β€” this works for simple names but filenames with spaces (e.g., my photo.png) would show as @my photo.png in the editor while the internal value has @media:my%20photo.png. If the user edits the text, the round-trip encoding could break because the onChange handler looks for exact filename matches to re-encode, but the displayed text has already been decoded.

πŸ’‘ Suggestion: Consider adding an e2e test case with a filename containing spaces to verify the full round-trip works correctly.

// Remove <foreignObject> elements (can embed arbitrary HTML)
.replace(/<foreignObject[\s>][\s\S]*?<\/foreignObject>/gi, "")
// Remove on* event handler attributes
.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | security

SVG sanitizer regex can be bypassed with HTML entity encoding or newlines in attribute values

The on\w+ event handler removal regex uses (?:"[^"]*"|'[^']*'|[^\s>]+) to match attribute values, but this doesn't handle cases where the attribute value contains newlines or HTML entities like &#106;avascript:. Also, <script tags can be obfuscated with unusual whitespace (e.g., <script\n>) β€” while the [\s>] pattern handles this, SVG payloads could use <script/src="..."> (self-closing with attributes) which is not covered.

Note: This is distinct from the existing comments about data: URIs. The existing comments flagged data: URI bypass and general regex fragility. This specifically calls out the entity-encoding bypass vector in event handlers.

πŸ’‘ Suggestion: The CSP script-src 'none' header added in main.ts is a good defense-in-depth layer. Consider documenting that the regex sanitizer is a best-effort layer and the CSP header is the primary security control. For higher assurance, consider using a proper HTML/SVG parser like DOMPurify.

@dyad-assistant
Copy link
Copy Markdown
Contributor

dyad-assistant bot commented Mar 7, 2026

πŸ” Dyadbot Code Review Summary

Verdict: β›” NO - Do NOT merge (1 HIGH issue)

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

Note: This PR already has 67 existing inline comments from prior reviews. This review focused on finding issues not yet covered.

Issues Summary

Severity File Issue
πŸ”΄ HIGH src/ipc/utils/mime_utils.ts:18 SVG sanitization bypass via <animate>/<set> elements
🟑 MEDIUM src/ipc/handlers/chat_stream_handlers.ts:403 parseMediaMentions runs on already-transformed prompt
🟑 MEDIUM src/ipc/handlers/chat_stream_handlers.ts:411 buildDyadMediaUrl receives relative path but may need absolute
🟑 MEDIUM src/components/DyadAppMediaFolder.tsx:466 Filename encoding round-trip may break for names with spaces
🟑 MEDIUM src/ipc/utils/mime_utils.ts:28 SVG sanitizer regex bypassable via entity encoding
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:128 MAX_IMAGE_SIZE constant declared twice
🟑 MEDIUM src/pages/library-home.tsx:180 useEffect dependency on lastDeepLink?.timestamp is fragile
🟒 Low Priority Notes (2 items)
  • filterMediaAppsByQuery generic constraint broader than needed - src/lib/mediaUtils.ts
  • SPRING_EASE tuple used as framer-motion easing - src/pages/library-home.tsx:124 β€” works at runtime but type mismatch
🚫 Dropped / Already Covered (many items)

The following categories of issues were identified but already have existing comments on this PR:

  • SVG sanitization fragility (regex-based) β€” 4 existing comments
  • Sync fs calls blocking Electron main β€” 5 existing comments
  • Hardcoded .dyad/media paths β€” 3 existing comments
  • Image generation handler security (SSRF, untyped JSON, size limits) β€” 8 existing comments
  • Accessibility gaps (keyboard nav, ARIA, screen reader) β€” 8 existing comments
  • Media mention regex/encoding issues β€” 5 existing comments
  • Race condition in prefillInput β€” 3 existing comments
  • Code duplication (media grid, mime utils, validation regex) β€” 4 existing comments
  • UX interaction issues (dialog states, loading, empty states) β€” 7 existing comments

These are not re-flagged since they are already tracked.


Generated by Dyadbot multi-agent code review

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant 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 follow-up: 2 additional issues from late-arriving agents


if (imageData.b64_json) {
const buffer = Buffer.from(imageData.b64_json, "base64");
const MAX_IMAGE_SIZE = 50 * 1024 * 1024; // 50 MB
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | duplication

MAX_IMAGE_SIZE constant declared twice in same function

const MAX_IMAGE_SIZE = 50 * 1024 * 1024 appears identically on both the b64_json branch (line 128) and the URL branch (line 145). If the limit needs to change, both must be updated.

πŸ’‘ Suggestion: Hoist the constant above the if/else block so it's declared once.

setActiveFilter("prompts");
setPromptDialogOpen(true);
clearLastDeepLink();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | correctness

useEffect dependency on lastDeepLink?.timestamp is fragile

The effect body reads lastDeepLink and casts it as unknown as AddPromptDeepLinkData, but the dependency array only lists lastDeepLink?.timestamp and clearLastDeepLink. If lastDeepLink changes without its timestamp changing, the effect won't re-run. The double cast (as unknown as) also bypasses type safety.

πŸ’‘ Suggestion: Include lastDeepLink in the dependency array and use a proper type guard instead of the double cast.

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant 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 follow-up: 2 additional correctness findings

* Sanitize SVG content by stripping elements and attributes that could
* execute scripts in Electron (e.g., <script>, on* handlers, javascript: URIs).
*/
export function sanitizeSvgContent(raw: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ HIGH | security

SVG sanitization bypass via <animate> / <set> elements

The regex-based sanitizer can be bypassed using SVG animation elements like:

<a><animate attributeName="href" values="javascript:alert(1)" /></a>

This dynamically sets the href attribute to a javascript: URI without matching any of the existing regex patterns (which only check static href attributes). While the Content-Security-Policy: script-src 'none' header in main.ts is a defense-in-depth layer, CSP script-src doesn't reliably block javascript: URI navigation in all Electron contexts.

This is distinct from the existing comments about data: URI bypass and general regex fragility β€” this is a specific, exploitable vector.

πŸ’‘ Suggestion: Strip <animate> and <set> elements that target href/xlink:href/on* attributes. Better yet, use DOMPurify for proper SVG sanitization.

}

// Resolve @media: mentions to image attachments
const mediaRefs = parseMediaMentions(userPrompt);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | correctness

parseMediaMentions runs on already-transformed userPrompt

By this point, userPrompt has already been through prompt reference replacement (replacePromptReference) and slash skill expansion (replaceSlashSkillReference). If any of those expansions introduce text containing @media: tokens, they would be falsely detected as media mentions β€” potentially attaching unexpected files or stripping text from the prompt.

πŸ’‘ Suggestion: Parse media mentions from req.prompt (original input) before any transformations, or move media mention parsing to run first in the pipeline.

@azizmejri1
Copy link
Copy Markdown
Collaborator Author

πŸ€– Claude Code Review Summary

PR Confidence: 4/5

All review threads have been addressed and resolved; no outstanding code issues remain.

Unresolved Threads

No unresolved threads

Resolved Threads

Issue Rationale Link
LibraryCard slash command hint (github-actions) Already implemented at lines 80-84 of LibraryCard.tsx. Per Principle #3: Intuitive But Power-User Friendly. View
Product Principle Suggestions

No suggestions (principles were clear enough for all decisions)


πŸ€– Generated by Claude Code

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

@BugBot run

Comment on lines +22 to +23
.replace(/<script[\s>][\s\S]*?<\/script>/gi, "")
// Remove self-closing <script/> tags
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

HIGH | security

SVG sanitizer fails to remove <script> tags without a closing </script>

The regex /<script[\s>][\s\S]*?<\/script>/gi requires a matching </script> tag. An SVG file containing an unclosed <script> tag (e.g., <script>alert(document.cookie) at the end of the file, or <script>/*) will not be matched and the tag will survive sanitization.

While the Content-Security-Policy: script-src 'none' header on the protocol response provides defense-in-depth, CSP enforcement depends on the rendering context. If the sanitized SVG content is ever used in a context where CSP isn't applied (e.g., stored and later served differently), this becomes exploitable.

Additionally, the regex can be bypassed with HTML comments inside the script: <script>alert(1)</script> β€” the non-greedy *? would match <script>alert(1)</script> in the output.

Suggestion: Add a catch-all regex for unclosed script tags after the existing ones:

Suggested change
.replace(/<script[\s>][\s\S]*?<\/script>/gi, "")
// Remove self-closing <script/> tags
.replace(/<script[\s>][\s\S]*?<\/script>/gi, "")
// Remove self-closing <script/> tags
.replace(/<script\s*\/>/gi, "")
// Remove unclosed <script> tags (no matching </script>)
.replace(/<script[\s>][\s\S]*/gi, "")

Or better, use a proper DOM parser (like DOMParser or linkedom) for SVG sanitization instead of regex.

Comment on lines +443 to +458
// Convert media mentions : @filename -> @media:filename
const currentAppMediaFiles = mediaApps.find(
(app) => app.appId === selectedAppId,
);
if (currentAppMediaFiles) {
for (const file of currentAppMediaFiles.files) {
const escaped = file.fileName.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
);
const mediaRegex = new RegExp(`@(${escaped})(?![\\w-])`, "g");
textContent = textContent.replace(
mediaRegex,
`@media:${encodeURIComponent(file.fileName)}`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | correctness

Media mention conversion can be corrupted by subsequent app name conversion

Media mentions are converted first (@filename -> @media:encodedFilename), then app name conversion runs. If an app happens to be named media, the app regex @(media)(?![a-zA-Z0-9_/\\-]) would match the @media prefix in the already-converted @media:filename.png (since : is NOT in the negative lookahead character class), turning it into @app:media:filename.png and breaking the media mention.

Suggestion: After converting media mentions, the app name conversion regex should exclude matches that are followed by : (i.e., already-prefixed mentions). Change the app name regex negative lookahead to also include ::

const mentionRegex = new RegExp(
  `@(${escapedAppName})(?![a-zA-Z0-9_/:\\-])`,
  "g",
);

Or more robustly, skip app name conversion for any @app:, @media:, @prompt:, @file: prefixed tokens.

Comment on lines +93 to +97
);
throw new Error(
`Image generation failed (HTTP ${response.status}). Please try again.`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | data-integrity

Unvalidated JSON response from external API can cause runtime crash

response.json() is called without a try/catch, and then data.data?.[0] is accessed. If the response body is not valid JSON (e.g., HTML error page, truncated response), response.json() will throw an unhandled error that propagates as a generic "object is not valid" error rather than a user-friendly message.

Suggestion: Wrap in try/catch:

let data: any;
try {
  data = await response.json();
} catch {
  throw new Error("Invalid response from image generation service. Please try again.");
}

Comment on lines +119 to +142
throw new Error("Unsupported media file extension");
}

return extension;
}

function getMediaFilePath(appPath: string, fileName: string): string {
assertSafeFileName(fileName);
assertSupportedMediaExtension(fileName);
return safeJoin(appPath, DYAD_MEDIA_DIR_NAME, fileName);
}

function getMediaDirectoryPath(appPath: string): string {
return path.join(appPath, DYAD_MEDIA_DIR_NAME);
}

async function getAppOrThrow(appId: number) {
const app = await db.query.apps.findFirst({
where: eq(apps.id, appId),
});

if (!app) {
throw new Error("App not found");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | race-condition

TOCTOU race between existsSync check and rename operation

The rename handler checks fs.existsSync(sourcePath) and later fs.existsSync(destinationPath) before calling fs.promises.rename(). Between the existence check and the rename, another process (or a concurrent IPC call to a different handler that doesn't acquire the same lock) could create a file at destinationPath or delete the source file.

While withMediaLock serializes operations for the same appId, the lock is app-scoped. External processes (the user's file manager, git operations, the app's dev server) are not coordinated and could modify files concurrently.

Suggestion: Use fs.promises.rename() directly and handle the resulting ENOENT/EEXIST errors, or use fs.promises.link() + fs.promises.unlink() with exclusive flags for atomicity. At minimum, document that the lock only protects against concurrent IPC calls, not external modifications.

Comment on lines +155 to +172
if (files.length > 0) {
result.push({
appId: app.id,
appName: app.name,
appPath,
files,
});
}
}

return { apps: result };
});

createTypedHandler(mediaContracts.renameMediaFile, async (_, params) => {
await withMediaLock([params.appId], async () => {
const app = await getAppOrThrow(params.appId);
const appPath = getDyadAppPath(app.path);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | data-integrity

Cross-device move can silently leave source file behind on copyFile failure

The move operation uses copyFile + unlink for cross-device support, which is correct. However, there's a subtle issue: if copyFile partially writes the destination (e.g., disk full during copy) and then throws, the partially-written destination file is NOT cleaned up. The source file is intact, but the destination has a corrupted partial copy that could confuse the user if they look at the target app's media folder.

Suggestion: Wrap the copyFile call similarly to how the unlink failure is handled β€” add a cleanup of destinationPath in a catch block around copyFile:

try {
  await fs.promises.copyFile(sourcePath, destinationPath);
} catch (copyErr) {
  // Clean up partial destination on copy failure
  try { await fs.promises.unlink(destinationPath); } catch {}
  throw copyErr;
}

Comment on lines +402 to +438
// Resolve @media: mentions to image attachments
const mediaRefs = parseMediaMentions(userPrompt);
if (mediaRefs.length > 0) {
try {
const resolvedMedia = await resolveMediaMentions(
mediaRefs,
chat.app.path,
chat.app.name,
);
const resolvedMediaRefs = resolvedMedia.map((media) =>
encodeURIComponent(media.fileName),
);
let mediaDisplayInfo = "";
for (const media of resolvedMedia) {
attachmentPaths.push(media.filePath);
const mediaUrl = buildDyadMediaUrl(chat.app.path, media.fileName);
mediaDisplayInfo += `\n<dyad-attachment name="${escapeXmlAttr(media.fileName)}" type="${escapeXmlAttr(media.mimeType)}" url="${escapeXmlAttr(mediaUrl)}" path="${escapeXmlAttr(media.filePath)}" attachment-type="chat-context"></dyad-attachment>\n`;
}
// Strip only resolved @media: tags from the prompt text.
// This preserves adjacent user text when mentions are directly followed
// by text without a whitespace separator.
userPrompt = stripResolvedMediaMentions(
userPrompt,
resolvedMediaRefs,
);
// Build display prompt with attachment tags for inline rendering.
if (mediaDisplayInfo) {
const strippedPrompt = stripResolvedMediaMentions(
displayUserPrompt ?? req.prompt,
resolvedMediaRefs,
);
displayUserPrompt = strippedPrompt + mediaDisplayInfo;
}
} catch (e) {
logger.error("Failed to resolve media mentions:", e);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | correctness

Media resolution silently succeeds with zero resolved files, leaving raw @media: tags in prompt

When parseMediaMentions finds media refs but resolveMediaMentions fails to resolve ANY of them (e.g., files were deleted between mention creation and message send), resolvedMedia is an empty array. In this case:

  • resolvedMediaRefs is empty
  • stripResolvedMediaMentions with an empty array returns the prompt unchanged (just trimmed)
  • The raw @media:filename.png text is sent to the AI model as-is

This means the AI receives garbled @media: prefixed text in the prompt instead of either the image attachment or a user-friendly error message.

Suggestion: When resolvedMedia.length === 0 but mediaRefs.length > 0, either warn the user that referenced media files were not found, or strip the unresolved @media: tokens from the prompt to avoid confusing the AI model.

Comment on lines +17 to +31
export function stripResolvedMediaMentions(
prompt: string,
resolvedMediaRefs: string[],
): string {
if (resolvedMediaRefs.length === 0) {
return prompt.trim();
}

let stripped = prompt;
for (const mediaRef of resolvedMediaRefs) {
const token = `@media:${mediaRef}`;
stripped = stripped.split(token).join("");
}

return stripped.replace(/\s{2,}/g, " ").trim();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LOW | correctness

stripResolvedMediaMentions uses string splitting which is vulnerable to substring collisions

stripped.split(token).join("") performs a literal string replacement of ALL occurrences. If a user's message text happens to contain the literal string @media:cat.png as part of a code block, documentation, or discussion about the media mention syntax, it would be incorrectly stripped.

This is unlikely in normal usage but could occur in edge cases where users are discussing the mention system.

Suggestion: Only strip the first occurrence per resolved ref, or use a regex with word boundary/whitespace anchoring to ensure only standalone mention tokens are removed.

Comment on lines 35 to +44
if (!preserveTabOrder) {
pushRecentViewedChatId(chatId);
}
navigate({
const navigationResult = navigate({
to: "/chat",
search: { id: chatId },
});

if (prefillInput !== undefined) {
Promise.resolve(navigationResult)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | error-handling

Promise.resolve(navigationResult) wrapping obscures the actual navigation promise type

navigate() from TanStack Router may not return a promise at all (it can return void or a Promise<void> depending on the router version). Wrapping it in Promise.resolve() means that if navigate is synchronous and returns undefined, the .then() fires immediately in the microtask queue -- before the route component has mounted. This makes the prefillInput timing issue (already noted in existing reviews) even more likely to fail.

Additionally, the .catch(() => {}) swallows ALL errors, including potential bugs in setChatInputValue itself, making debugging difficult.

Suggestion: At minimum, log the caught error instead of silently discarding it:

.catch((err) => {
  console.warn("Failed to prefill chat input after navigation:", err);
});

Comment on lines +55 to +58
? `${systemPrompt}\n\n${params.prompt}`
: params.prompt;

const requestId = `image-gen-${uuidv4()}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LOW | correctness

Abort timeout clearTimeout runs in finally but the timeout callback calls controller.abort() which triggers an AbortError -- the catch block re-wraps it, losing the original error if the fetch was already in-flight

The flow is: timeout fires -> controller.abort() -> fetch rejects with AbortError -> caught -> re-thrown as "Image generation timed out". This is correct.

However, if the user's network drops and fetch throws a TypeError (e.g., DNS resolution failure), the catch block re-wraps it as "Failed to connect to image generation service." -- losing the original error details. This makes debugging connectivity issues harder.

Suggestion: Include the original error message in the re-thrown error for diagnostics:

throw new Error(`Failed to connect to image generation service: ${error instanceof Error ? error.message : "Unknown error"}`);

Comment on lines +24 to +32
) {
const mediaDir = path.join(appPath, DYAD_MEDIA_DIR_NAME);
try {
await fs.promises.access(mediaDir);
} catch {
return [];
}

const entries = await fs.promises.readdir(mediaDir, { withFileTypes: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MEDIUM | performance

Sequential stat() calls for every file in media directory

getMediaFilesForApp calls fs.promises.stat(fullPath) individually for each file in a serial for loop. For a media directory with many files, this is O(n) sequential I/O operations. Since each stat call is independent, they could be parallelized.

Suggestion: Use Promise.all with map to parallelize the stat calls:

const files = await Promise.all(
  entries
    .filter(entry => entry.isFile() && SUPPORTED_MEDIA_EXTENSIONS.includes(path.extname(entry.name).toLowerCase()))
    .map(async (entry) => {
      const ext = path.extname(entry.name).toLowerCase();
      const fullPath = path.join(mediaDir, entry.name);
      const stat = await fs.promises.stat(fullPath);
      return { fileName: entry.name, filePath: fullPath, appId, appName, sizeBytes: stat.size, mimeType: getMimeType(ext) };
    })
);

Copy link
Copy Markdown
Contributor

@dyad-assistant dyad-assistant 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: 11 issue(s) found (2 HIGH, 9 MEDIUM)

// Remove data: URIs in href and xlink:href attributes (defense-in-depth)
.replace(/((?:xlink:)?href\s*=\s*(?:"|'))data:[^"']*("|')/gi, "$1#$2")
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ HIGH | security

Regex-based SVG sanitization is bypassable

The regex patterns can be bypassed by unclosed <script> tags (e.g., <script>alert(1) at EOF without a closing tag), nested HTML comments, encoding tricks, and CDATA sections. Regex-based HTML/XML sanitization is fundamentally unreliable.

The Content-Security-Policy: script-src 'none' header set in main.ts provides defense-in-depth, but the sanitizer itself should be robust since it's the primary protection layer.

πŸ’‘ Suggestion: Use a proper DOM parser (e.g., DOMPurify on the Node side) to parse and allowlist safe SVG elements/attributes. As a quick fix, also add a catch-all for unclosed script tags: .replace(/<script[\s>][\s\S]*/gi, '').

Found by: Correctness Expert, Code Health Expert

// Skip animation entirely for users who prefer reduced motion
if (prefersReducedMotion) {
setTimeout(onComplete, 0);
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ HIGH | correctness

Calling setTimeout during render is a React anti-pattern

setTimeout(onComplete, 0) is called directly in the render body when prefersReducedMotion is true. This is a side effect during render that causes double-invocations in StrictMode and potential state-update-during-render warnings.

πŸ’‘ Suggestion:

useEffect(() => {
  if (prefersReducedMotion) onComplete();
}, [prefersReducedMotion, onComplete]);
if (prefersReducedMotion) return null;

Found by: Code Health Expert, UX Wizard

mediaRegex,
`@media:${encodeURIComponent(file.fileName)}`,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | correctness

Media mention conversion corrupted if an app is named 'media'

Media mentions are converted first (@filename -> @media:encodedFilename), then app names are converted. If an app is named media, the app regex @(media)(?![a-zA-Z0-9_/\\-]) matches the @media prefix in @media:filename.png because : is not in the negative lookahead, corrupting it to @app:media:filename.png.

πŸ’‘ Suggestion: Add : to the app name regex negative lookahead: (?![a-zA-Z0-9_/:\\-]).

Found by: Correctness Expert

} catch (e) {
logger.error("Failed to resolve media mentions:", e);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | correctness

Unresolved media mentions sent as raw @media: text to AI model

When parseMediaMentions finds refs but resolveMediaMentions resolves none (e.g., files deleted after mention), resolvedMediaRefs is empty and stripResolvedMediaMentions returns the prompt unchanged. The raw @media:filename.png text is sent to the AI, which won't understand it.

πŸ’‘ Suggestion: When resolvedMedia is empty but mediaRefs is non-empty, either warn the user about missing files or strip all unresolved @media: tokens.

Found by: Correctness Expert

});
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | data-integrity

Partial copy not cleaned up on copyFile failure during move

If copyFile fails partway (e.g., disk full), a corrupted partial destination file remains. The cleanup logic only handles unlink(source) failure, not copyFile failure.

πŸ’‘ Suggestion: Wrap copyFile in a try/catch that cleans up the destination file on failure:

try {
  await fs.promises.copyFile(sourcePath, destinationPath);
} catch (e) {
  try { await fs.promises.unlink(destinationPath); } catch {}
  throw e;
}

Found by: Correctness Expert


if (imageData.b64_json) {
const buffer = Buffer.from(imageData.b64_json, "base64");
const MAX_IMAGE_SIZE = 50 * 1024 * 1024; // 50 MB
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | duplication

MAX_IMAGE_SIZE constant declared twice in the same function

MAX_IMAGE_SIZE = 50 * 1024 * 1024 is declared identically in both the b64_json branch (line 128) and the url branch (line 145). If the limit is updated in one place but not the other, they'll drift.

πŸ’‘ Suggestion: Hoist to module level alongside IMAGE_GENERATION_TIMEOUT_MS.

Found by: Code Health Expert

"hover:border-primary/30 transition-colors",
)}
role="button"
tabIndex={0}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | interaction

Keyboard events on dropdown trigger bubble to thumbnail, causing accidental preview opens

The thumbnail area is a clickable button that opens the preview, and the DropdownMenuTrigger is absolutely positioned inside it. While stopPropagation() is on the trigger's onClick, the parent's onKeyDown handler can still fire when a keyboard user presses Enter while the dropdown trigger is focused.

πŸ’‘ Suggestion: Add onKeyDown={(e) => e.stopPropagation()} to the DropdownMenuTrigger.

Found by: UX Wizard

<p className="text-xs text-muted-foreground/60">
This may take up to a minute
</p>
</motion.div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | performance-feel

No way to cancel image generation (up to 2-minute wait)

The 120s timeout means users can be stuck waiting with no cancel option. The Cancel button in the footer is disabled during generation, and the copy says 'up to a minute' which understates the actual timeout.

πŸ’‘ Suggestion: Allow closing the dialog during generation, or add a Cancel button that aborts the request. Update the copy to match the actual timeout.

Found by: UX Wizard

appFiles,
messageHistory,
mediaApps,
]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | interaction

Media mentions prepended first in autocomplete, pushing frequent items down

Media items are added before app mentions, prompts, and files. If an app has many media files, users must scroll past all of them to find frequently-used app/prompt mentions.

πŸ’‘ Suggestion: Place media items after app and prompt mentions, or add section headers to the dropdown for easier scanning.

Found by: UX Wizard

>
<span className={selectedApp ? "" : "text-muted-foreground"}>
{selectedApp?.name ?? "Select an app..."}
</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 MEDIUM | accessibility

App selector popover lacks proper ARIA listbox patterns

The PopoverTrigger declares aria-haspopup='listbox' but the content doesn't use role='listbox'/role='option'. Arrow key navigation is not implemented, forcing keyboard users to Tab through every option.

πŸ’‘ Suggestion: Add role='listbox' to the options container and role='option' to each item, or use an existing accessible combobox component.

Found by: UX Wizard

@dyad-assistant
Copy link
Copy Markdown
Contributor

dyad-assistant bot commented Mar 7, 2026

πŸ” Dyadbot Code Review Summary

Verdict: β›” NO - Do NOT merge

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

Issues Summary

Severity File Issue
πŸ”΄ HIGH src/ipc/utils/mime_utils.ts:22-37 Regex-based SVG sanitization is bypassable (unclosed tags, encoding tricks)
πŸ”΄ HIGH src/pages/library-home.tsx:42 setTimeout called during render body (React anti-pattern)
🟑 MEDIUM src/components/chat/LexicalChatInput.tsx:458 Media mention corruption if app is named "media"
🟑 MEDIUM src/ipc/handlers/chat_stream_handlers.ts:438 Unresolved media mentions sent as raw text to AI
🟑 MEDIUM src/ipc/handlers/media_handlers.ts:164 Partial copy not cleaned up on move copyFile failure
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:99 response.json() not wrapped in try/catch
🟑 MEDIUM src/ipc/handlers/image_generation_handlers.ts:128 MAX_IMAGE_SIZE constant declared twice
🟑 MEDIUM src/components/DyadAppMediaFolder.tsx:347 Keyboard events bubble from dropdown to thumbnail
🟑 MEDIUM src/components/ImageGeneratorDialog.tsx:90 No cancel option during 2-minute image generation
🟑 MEDIUM src/components/chat/LexicalChatInput.tsx:405 Media mentions first in autocomplete, pushing frequent items down
🟑 MEDIUM src/components/ImageGeneratorDialog.tsx:152 App selector lacks proper ARIA listbox patterns
🟒 Low Priority Notes (8 items)
  • TOCTOU race in rename handler - src/ipc/handlers/media_handlers.ts:119 - existsSync check before rename is not atomic
  • Rename toast shows old filename only - src/hooks/useAppMediaFiles.ts:43
  • Silent error swallowing in prefill - src/hooks/useSelectChat.ts:41 - .catch(() => {}) hides bugs
  • Network error details lost - src/ipc/handlers/image_generation_handlers.ts:55
  • Filter tab state not synced with URL - src/pages/library-home.tsx:218
  • Preview close button lacks focus styles - src/components/DyadAppMediaFolder.tsx:432
  • Edit/Delete buttons lack visible labels - src/components/LibraryCard.tsx:117
  • Sparkles icon for "Plain" mode is contradictory - src/components/ImageGeneratorDialog.tsx:60
🚫 Dropped False Positives (4 items)
  • DyadAppMediaFolder is 642 lines - Dropped: Acceptable for a new feature's first PR; splitting into sub-components can be done in a follow-up
  • MediaPage duplicates LibraryHomePage media rendering - Dropped: Both pages serve different navigation paths (/media vs /library?filter=media); minor duplication is acceptable
  • Module-level mutable variable for animation - Dropped: Standard pattern for one-time animations; HMR concerns are minor
  • slug display may differ from slugForPrompt - Dropped: item.data.slug already contains the formatted slug value from the database

Generated by Dyadbot multi-agent code review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 7, 2026

🎭 Playwright Test Results

❌ Some tests failed

OS Passed Failed Flaky Skipped
🍎 macOS 249 1 11 6

Summary: 249 passed, 1 failed, 11 flaky, 6 skipped

Failed Tests

🍎 macOS

  • template-create-nextjs.spec.ts > create next.js app
    • Error: expect(locator).toBeVisible() failed

πŸ“‹ Re-run Failing Tests (macOS)

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

npm run e2e \
  e2e-tests/template-create-nextjs.spec.ts

⚠️ Flaky Tests

🍎 macOS

  • annotator.spec.ts > annotator - capture and submit screenshot (passed after 1 retry)
  • app_search.spec.ts > app search - empty search shows all apps (passed after 1 retry)
  • attach_image.spec.ts > attach image via drag - chat (passed after 1 retry)
  • debugging_logs.spec.ts > clear filters button works (passed after 1 retry)
  • local_agent_code_search.spec.ts > local-agent - code search (passed after 1 retry)
  • refresh.spec.ts > refresh preserves current route (passed after 1 retry)
  • select_component.spec.ts > select component (passed after 1 retry)
  • select_component.spec.ts > deselect component (passed after 1 retry)
  • select_component.spec.ts > select component next.js (passed after 1 retry)
  • setup_flow.spec.ts > Setup Flow > setup banner shows correct state when node.js is installed (passed after 1 retry)
  • ... and 1 more

πŸ“Š View full report

@azizmejri1
Copy link
Copy Markdown
Collaborator Author

I'm closing this and opening a cleaner PR

@azizmejri1 azizmejri1 closed this Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants