Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e-tests/fixtures/engine/local-agent/generate-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";

export const fixture: LocalAgentFixture = {
description: "Generate an image using the generate_image tool",
turns: [
{
text: "I'll generate a hero image for your landing page.",
toolCalls: [
{
name: "generate_image",
args: {
prompt:
"A modern, minimal hero illustration of a rocket launching from a laptop screen, flat design style, blue and purple gradient background, clean lines",
},
},
],
},
{
text: "I've generated the hero image and saved it to your project. You can find it in the .dyad/media directory.",
},
],
};
16 changes: 16 additions & 0 deletions e2e-tests/local_agent_generate_image.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { testSkipIfWindows } from "./helpers/test_helper";

/**
* E2E tests for the generate_image agent tool
* Tests image generation in local-agent mode
*/

testSkipIfWindows("local-agent - generate image", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectLocalAgentMode();

await po.sendPrompt("tc=local-agent/generate-image");

await po.snapshotMessages();
});

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,27 @@
}
}
},
{
"type": "function",
"function": {
"name": "generate_image",
"description": "Generate an image using AI based on a text prompt. The generated image is saved to the project's .dyad/media directory.\n\n### When to Use\n- User requests a custom image, illustration, icon, or graphic for their app\n- User wants a hero image, background, banner, or visual asset\n- Creating images that are more visually relevant than placeholder rectangles\n\n### Prompt Guidelines\nWrite detailed, descriptive prompts. Be specific about:\n- **Subject**: What is in the image (objects, people, scenes)\n- **Style**: Photography, illustration, flat design, 3D render, watercolor, etc.\n- **Composition**: Layout, perspective, framing\n- **Colors**: Specific color palette or mood\n- **Mood**: Cheerful, professional, dramatic, minimal, etc.\n\n### Examples\n- \"A modern flat illustration of a team collaborating around a laptop, using a blue and purple color palette, clean minimal style with subtle gradients, white background\"\n- \"Professional product photography of a sleek smartphone on a marble surface, soft studio lighting, shallow depth of field, warm neutral tones\"\n\n### After Generation\nThe tool returns the file path in .dyad/media. Use the copy_file tool to copy it to the appropriate location in the project (e.g., public/assets/) and reference that path in your code.\n",
"parameters": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "A detailed, descriptive prompt for the image to generate. Be specific about colors, composition, style, mood, and subject matter. Avoid generic or vague descriptions."
}
},
"required": [
"prompt"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
- paragraph: /Generate an AI_RULES\.md file for this app\. Describe the tech stack in 5-\d+ bullet points and describe clear rules about what libraries to use for what\./
- button "file1.txt file1.txt Edit":
- img
- text: ""
- button "Edit":
- img
- text: ""
- img
- paragraph: More EOM
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- paragraph: tc=local-agent/generate-image
- paragraph: I'll generate a hero image for your landing page.
- button "Image Generation A modern, minimal hero illustration of a rocket launching from a laptop screen, flat design style, blue and purple gradient background, clean lines":
- img
- text: ""
- img
- paragraph: I've generated the hero image and saved it to your project. You can find it in the .dyad/media directory.
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- button "Undo":
- img
- text: ""
- button "Retry":
- img
- text: ""
9 changes: 9 additions & 0 deletions src/__tests__/__snapshots__/local_agent_prompt.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ After every edit, read the file to verify changes applied correctly. If somethin
6. **Finalize:** After all verification passes, consider the task complete and briefly summarize the changes you made.
</development_workflow>

<image_generation_guidelines>
When a user explicitly requests custom images, illustrations, or visual media for their app:
- Use the \`generate_image\` tool instead of using placeholder images or broken external URLs
- Do NOT generate images when an existing asset, SVG, or icon library (e.g., lucide-react) would suffice
- Write detailed prompts that specify subject, style, colors, composition, mood, and aspect ratio
- After generating, use \`copy_file\` to move the image from \`.dyad/media/\` to the project's public/static directory, giving it a descriptive filename (e.g., \`public/assets/hero-banner.png\`)
- Reference the copied path in code (e.g., \`<img src="/assets/hero-banner.png" />\`)
</image_generation_guidelines>

# Tech Stack
- You are building a React application.
- Use TypeScript.
Expand Down
87 changes: 87 additions & 0 deletions src/components/chat/DyadImageGeneration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type React from "react";
import { useState, type ReactNode } from "react";
import { ImageIcon } from "lucide-react";
import { CustomTagState } from "./stateTypes";
import {
DyadCard,
DyadCardHeader,
DyadBadge,
DyadExpandIcon,
DyadStateIndicator,
DyadCardContent,
} from "./DyadCardPrimitives";

interface DyadImageGenerationNode {
properties: {
prompt: string;
path: string;
state: CustomTagState;
};
}

interface DyadImageGenerationProps {
children?: ReactNode;
node?: DyadImageGenerationNode;
}

export const DyadImageGeneration: React.FC<DyadImageGenerationProps> = ({
children,
node,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const prompt = node?.properties?.prompt ?? "";
const imagePath = node?.properties?.path ?? "";
const state = node?.properties?.state;
const inProgress = state === "pending";
const aborted = state === "aborted";

return (
<DyadCard
state={state}
accentColor="violet"
isExpanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
>
<DyadCardHeader icon={<ImageIcon size={15} />} accentColor="violet">
<DyadBadge color="violet">Image Generation</DyadBadge>
{!isExpanded && prompt && (
<span className="text-sm text-muted-foreground italic truncate">
{prompt}
</span>
)}
{inProgress && (
<DyadStateIndicator state="pending" pendingLabel="Generating..." />
)}
{aborted && (
<DyadStateIndicator state="aborted" abortedLabel="Did not finish" />
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | consistency

No success state indicator after image generation completes

The component shows Generating... (pending) and Did not finish (aborted), but has no visual confirmation when generation succeeds. Other card components like DyadCopy show a green checkmark via DyadStateIndicator with state='finished' and a finishedLabel. After waiting through what could be a long generation, the user gets no visual feedback that it worked.

💡 Suggestion: Add a finished state indicator:

{!inProgress && !aborted && state === 'finished' && (
  <DyadStateIndicator state="finished" finishedLabel="Generated" />
)}

<div className="ml-auto">
<DyadExpandIcon isExpanded={isExpanded} />
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | missing-state-feedback

No success indicator when image generation completes

When image generation finishes successfully, the card shows no visual confirmation. The spinner disappears with no replacement feedback, making it unclear whether the operation succeeded. This is especially noticeable because image generation is a slow operation where the user is actively watching for completion.

Other similar tools in the codebase (e.g., DyadCopy) display a green checkmark with a completion label via DyadStateIndicator.

💡 Suggestion: Add a finished state indicator:

Suggested change
<DyadExpandIcon isExpanded={isExpanded} />
{aborted && (
<DyadStateIndicator state="aborted" abortedLabel="Did not finish" />
)}
{state === "finished" && (
<DyadStateIndicator state="finished" finishedLabel="Generated" />
)}

</div>
</DyadCardHeader>
<DyadCardContent isExpanded={isExpanded}>
<div className="text-sm text-muted-foreground space-y-2">
{prompt && (
<div>
<span className="text-xs font-medium text-muted-foreground">
Prompt:
</span>
<div className="italic mt-0.5 text-foreground">{prompt}</div>
</div>
)}
{imagePath && (
<div>
<span className="text-xs font-medium text-muted-foreground">
Saved to:
</span>
<div className="mt-0.5 font-mono text-xs text-foreground">
{imagePath}
</div>
</div>
)}
{children && <div className="mt-0.5 text-foreground">{children}</div>}
</div>
</DyadCardContent>
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM | missing-image-preview

Expanded card shows only a file path, not the actual generated image

When a user expands the card after image generation completes, they see the prompt text and a raw file path like .dyad/media/generated-1234-abc.png, but never the actual image. For an image generation feature, not showing the generated image is a significant UX miss — the user has to navigate to the file manually to verify the output.

💡 Suggestion: Render an inline image preview in the expanded content when the image path is available and generation is complete. For example:

{imagePath && !inProgress && (
  <img src={resolvedImagePath} alt={prompt} className="rounded-md max-h-48 mt-2" />
)}

</DyadCard>
);
};
17 changes: 17 additions & 0 deletions src/components/chat/DyadMarkdownParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { DyadMcpToolResult } from "./DyadMcpToolResult";
import { DyadWebSearchResult } from "./DyadWebSearchResult";
import { DyadWebSearch } from "./DyadWebSearch";
import { DyadWebCrawl } from "./DyadWebCrawl";
import { DyadImageGeneration } from "./DyadImageGeneration";
import { DyadCodeSearchResult } from "./DyadCodeSearchResult";
import { DyadCodeSearch } from "./DyadCodeSearch";
import { DyadRead } from "./DyadRead";
Expand Down Expand Up @@ -76,6 +77,7 @@ const DYAD_CUSTOM_TAGS = [
"dyad-status",
"dyad-compaction",
"dyad-copy",
"dyad-image-generation",
// Plan mode tags
"dyad-write-plan",
"dyad-exit-plan",
Expand Down Expand Up @@ -727,6 +729,21 @@ function renderCustomTag(
</DyadSupabaseProjectInfo>
);

case "dyad-image-generation":
return (
<DyadImageGeneration
node={{
properties: {
prompt: attributes.prompt || "",
path: attributes.path || "",
state: getState({ isStreaming, inProgress }),
},
}}
>
{content}
</DyadImageGeneration>
);

case "dyad-status":
return (
<DyadStatus
Expand Down
2 changes: 2 additions & 0 deletions src/pro/main/ipc/handlers/local_agent/tool_definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { editFileTool } from "./tools/edit_file";
import { searchReplaceTool } from "./tools/search_replace";
import { webSearchTool } from "./tools/web_search";
import { webCrawlTool } from "./tools/web_crawl";
import { generateImageTool } from "./tools/generate_image";
import { updateTodosTool } from "./tools/update_todos";
import { runTypeChecksTool } from "./tools/run_type_checks";
import { grepTool } from "./tools/grep";
Expand Down Expand Up @@ -64,6 +65,7 @@ export const TOOL_DEFINITIONS: readonly ToolDefinition[] = [
readLogsTool,
webSearchTool,
webCrawlTool,
generateImageTool,
updateTodosTool,
runTypeChecksTool,
// Plan mode tools
Expand Down
Loading