diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx
index da415c4..06eae15 100644
--- a/src/components/chat/ChatInput.tsx
+++ b/src/components/chat/ChatInput.tsx
@@ -330,12 +330,20 @@ function ChatInputActions({
}: ChatInputActionsProps) {
const [autoApprove, setAutoApprove] = useState(false);
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
+
if (proposal.type === "tip-proposal") {
return
Tip proposal
;
}
if (proposal.type === "action-proposal") {
return ;
}
+
+ // Split files into server functions and other files - only for CodeProposal
+ const serverFunctions =
+ proposal.filesChanged?.filter((f: FileChange) => f.isServerFunction) ?? [];
+ const otherFilesChanged =
+ proposal.filesChanged?.filter((f: FileChange) => !f.isServerFunction) ?? [];
+
return (
@@ -462,11 +470,30 @@ function ChatInputActions({
)}
- {proposal.filesChanged?.length > 0 && (
+ {serverFunctions.length > 0 && (
+
+
Server Functions Changed
+
+ {serverFunctions.map((file: FileChange, index: number) => (
+ -
+ {getIconForFileChange(file)}
+
+ {file.name}
+
+
+ - {file.summary}
+
+
+ ))}
+
+
+ )}
+
+ {otherFilesChanged.length > 0 && (
Files Changed
- {proposal.filesChanged.map((file, index) => (
+ {otherFilesChanged.map((file: FileChange, index: number) => (
-
{getIconForFileChange(file)}
diff --git a/src/components/chat/DyadMarkdownParser.tsx b/src/components/chat/DyadMarkdownParser.tsx
index e7b515e..598ac10 100644
--- a/src/components/chat/DyadMarkdownParser.tsx
+++ b/src/components/chat/DyadMarkdownParser.tsx
@@ -11,6 +11,7 @@ import { CodeHighlight } from "./CodeHighlight";
import { useAtomValue } from "jotai";
import { isStreamingAtom } from "@/atoms/chatAtoms";
import { CustomTagState } from "./stateTypes";
+import { DyadOutput } from "./DyadOutput";
interface DyadMarkdownParserProps {
content: string;
@@ -77,6 +78,7 @@ function preprocessUnclosedTags(content: string): {
"dyad-add-dependency",
"dyad-execute-sql",
"dyad-add-integration",
+ "dyad-output",
];
let processedContent = content;
@@ -137,6 +139,7 @@ function parseCustomTags(content: string): ContentPiece[] {
"dyad-add-dependency",
"dyad-execute-sql",
"dyad-add-integration",
+ "dyad-output",
];
const tagPattern = new RegExp(
@@ -303,6 +306,16 @@ function renderCustomTag(
);
+ case "dyad-output":
+ return (
+
+ {content}
+
+ );
+
default:
return null;
}
diff --git a/src/components/chat/DyadOutput.tsx b/src/components/chat/DyadOutput.tsx
new file mode 100644
index 0000000..8a78a2c
--- /dev/null
+++ b/src/components/chat/DyadOutput.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from "react";
+import {
+ ChevronsDownUp,
+ ChevronsUpDown,
+ AlertTriangle,
+ XCircle,
+} from "lucide-react";
+
+interface DyadOutputProps {
+ type: "error" | "warning";
+ message?: string;
+ children?: React.ReactNode;
+}
+
+export const DyadOutput: React.FC = ({
+ type,
+ message,
+ children,
+}) => {
+ const [isContentVisible, setIsContentVisible] = useState(false);
+
+ // If the type is not warning, it is an error (in case LLM gives a weird "type")
+ const isError = type !== "warning";
+ const borderColor = isError ? "border-red-500" : "border-amber-500";
+ const iconColor = isError ? "text-red-500" : "text-amber-500";
+ const icon = isError ? (
+
+ ) : (
+
+ );
+ const label = isError ? "Error" : "Warning";
+
+ return (
+
setIsContentVisible(!isContentVisible)}
+ >
+ {/* Top-left label badge */}
+
+ {icon}
+ {label}
+
+ {/* Main content, padded to avoid label */}
+
+
+ {message && (
+
+ {message.slice(0, isContentVisible ? undefined : 80) +
+ (!isContentVisible ? "..." : "")}
+
+ )}
+
+
+ {isContentVisible ? (
+
+ ) : (
+
+ )}
+
+
+ {isContentVisible && children && (
+
+ {children}
+
+ )}
+
+ );
+};
diff --git a/src/ipc/handlers/supabase_handlers.ts b/src/ipc/handlers/supabase_handlers.ts
index cc7eea0..2a934aa 100644
--- a/src/ipc/handlers/supabase_handlers.ts
+++ b/src/ipc/handlers/supabase_handlers.ts
@@ -1,6 +1,4 @@
import { ipcMain } from "electron";
-import { SupabaseManagementAPI } from "supabase-management-js";
-import { readSettings, writeSettings } from "../../main/settings";
import log from "electron-log";
import { db } from "../../db";
import { eq } from "drizzle-orm";
diff --git a/src/ipc/processors/executeAddDependency.ts b/src/ipc/processors/executeAddDependency.ts
index f7c079c..6580936 100644
--- a/src/ipc/processors/executeAddDependency.ts
+++ b/src/ipc/processors/executeAddDependency.ts
@@ -17,55 +17,31 @@ export async function executeAddDependency({
appPath: string;
}) {
const packageStr = packages.join(" ");
- try {
- const { stdout, stderr } = await execPromise(
- `(pnpm add ${packageStr}) || (npm install ${packageStr})`,
- {
- cwd: appPath,
- }
- );
- const installResults = stdout + (stderr ? `\n${stderr}` : "");
- // Update the message content with the installation results
- const updatedContent = message.content.replace(
- new RegExp(
- `[^<]*`,
- "g"
- ),
+ const { stdout, stderr } = await execPromise(
+ `(pnpm add ${packageStr}) || (npm install ${packageStr})`,
+ {
+ cwd: appPath,
+ }
+ );
+ const installResults = stdout + (stderr ? `\n${stderr}` : "");
+
+ // Update the message content with the installation results
+ const updatedContent = message.content.replace(
+ new RegExp(
`${installResults}`
- );
+ )}">[^<]*`,
+ "g"
+ ),
+ `${installResults}`
+ );
- // Save the updated message back to the database
- await db
- .update(messages)
- .set({ content: updatedContent })
- .where(eq(messages.id, message.id));
-
- // Return undefined implicitly
- } catch (error: any) {
- // Update the message with the error
- const updatedContent = message.content.replace(
- new RegExp(
- `[^<]*`,
- "g"
- ),
- `${
- error.message
- }`
- );
-
- // Save the updated message back to the database
- await db
- .update(messages)
- .set({ content: updatedContent })
- .where(eq(messages.id, message.id));
-
- throw error;
- }
+ // Save the updated message back to the database
+ await db
+ .update(messages)
+ .set({ content: updatedContent })
+ .where(eq(messages.id, message.id));
}
diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts
index d925bd0..9256b2c 100644
--- a/src/ipc/processors/response_processor.ts
+++ b/src/ipc/processors/response_processor.ts
@@ -10,11 +10,13 @@ import { getGitAuthor } from "../utils/git_author";
import log from "electron-log";
import { executeAddDependency } from "./executeAddDependency";
import {
+ deleteSupabaseFunction,
deploySupabaseFunctions,
executeSupabaseSql,
} from "../../supabase_admin/supabase_management_client";
import { isServerFunction } from "../../supabase_admin/supabase_utils";
+const readFile = fs.promises.readFile;
const logger = log.scope("response_processor");
export function getDyadWriteTags(fullResponse: string): {
@@ -131,6 +133,23 @@ export function getDyadExecuteSqlTags(fullResponse: string): string[] {
return queries;
}
+interface Output {
+ message: string;
+ error: unknown;
+}
+
+function getFunctionNameFromPath(input: string): string {
+ return path.basename(path.extname(input) ? path.dirname(input) : input);
+}
+
+async function readFileFromFunctionPath(input: string): Promise {
+ // Sometimes, the path given is a directory, sometimes it's the file itself.
+ if (path.extname(input) === "") {
+ return readFile(path.join(input, "index.ts"), "utf8");
+ }
+ return readFile(input, "utf8");
+}
+
export async function processFullResponseActions(
fullResponse: string,
chatId: number,
@@ -158,6 +177,9 @@ export async function processFullResponseActions(
const deletedFiles: string[] = [];
let hasChanges = false;
+ const warnings: Output[] = [];
+ const errors: Output[] = [];
+
try {
// Extract all tags
const dyadWriteTags = getDyadWriteTags(fullResponse);
@@ -184,21 +206,37 @@ export async function processFullResponseActions(
// Handle SQL execution tags
if (dyadExecuteSqlQueries.length > 0) {
for (const query of dyadExecuteSqlQueries) {
- const result = await executeSupabaseSql({
- supabaseProjectId: chatWithApp.app.supabaseProjectId!,
- query,
- });
+ try {
+ const result = await executeSupabaseSql({
+ supabaseProjectId: chatWithApp.app.supabaseProjectId!,
+ query,
+ });
+ } catch (error) {
+ errors.push({
+ message: `Failed to execute SQL query: ${query}`,
+ error: error,
+ });
+ }
}
logger.log(`Executed ${dyadExecuteSqlQueries.length} SQL queries`);
}
// TODO: Handle add dependency tags
if (dyadAddDependencyPackages.length > 0) {
- await executeAddDependency({
- packages: dyadAddDependencyPackages,
- message: message,
- appPath,
- });
+ try {
+ await executeAddDependency({
+ packages: dyadAddDependencyPackages,
+ message: message,
+ appPath,
+ });
+ } catch (error) {
+ errors.push({
+ message: `Failed to add dependencies: ${dyadAddDependencyPackages.join(
+ ", "
+ )}`,
+ error: error,
+ });
+ }
writtenFiles.push("package.json");
const pnpmFilename = "pnpm-lock.yaml";
if (fs.existsSync(path.join(appPath, pnpmFilename))) {
@@ -225,11 +263,18 @@ export async function processFullResponseActions(
logger.log(`Successfully wrote file: ${fullFilePath}`);
writtenFiles.push(filePath);
if (isServerFunction(filePath)) {
- await deploySupabaseFunctions({
- supabaseProjectId: chatWithApp.app.supabaseProjectId!,
- functionName: path.basename(path.dirname(filePath)),
- content: content,
- });
+ try {
+ await deploySupabaseFunctions({
+ supabaseProjectId: chatWithApp.app.supabaseProjectId!,
+ functionName: path.basename(path.dirname(filePath)),
+ content: content,
+ });
+ } catch (error) {
+ errors.push({
+ message: `Failed to deploy Supabase function: ${filePath}`,
+ error: error,
+ });
+ }
}
}
@@ -267,6 +312,33 @@ export async function processFullResponseActions(
} else {
logger.warn(`Source file for rename does not exist: ${fromPath}`);
}
+ if (isServerFunction(tag.from)) {
+ try {
+ await deleteSupabaseFunction({
+ supabaseProjectId: chatWithApp.app.supabaseProjectId!,
+ functionName: getFunctionNameFromPath(tag.from),
+ });
+ } catch (error) {
+ warnings.push({
+ message: `Failed to delete Supabase function: ${tag.from} as part of renaming ${tag.from} to ${tag.to}`,
+ error: error,
+ });
+ }
+ }
+ if (isServerFunction(tag.to)) {
+ try {
+ await deploySupabaseFunctions({
+ supabaseProjectId: chatWithApp.app.supabaseProjectId!,
+ functionName: getFunctionNameFromPath(tag.to),
+ content: await readFileFromFunctionPath(toPath),
+ });
+ } catch (error) {
+ errors.push({
+ message: `Failed to deploy Supabase function: ${tag.to} as part of renaming ${tag.from} to ${tag.to}`,
+ error: error,
+ });
+ }
+ }
}
// Process all file deletions
@@ -275,7 +347,11 @@ export async function processFullResponseActions(
// Delete the file if it exists
if (fs.existsSync(fullFilePath)) {
- fs.unlinkSync(fullFilePath);
+ if (fs.lstatSync(fullFilePath).isDirectory()) {
+ fs.rmdirSync(fullFilePath, { recursive: true });
+ } else {
+ fs.unlinkSync(fullFilePath);
+ }
logger.log(`Successfully deleted file: ${fullFilePath}`);
deletedFiles.push(filePath);
@@ -293,6 +369,19 @@ export async function processFullResponseActions(
} else {
logger.warn(`File to delete does not exist: ${fullFilePath}`);
}
+ if (isServerFunction(filePath)) {
+ try {
+ await deleteSupabaseFunction({
+ supabaseProjectId: chatWithApp.app.supabaseProjectId!,
+ functionName: getFunctionNameFromPath(filePath),
+ });
+ } catch (error) {
+ errors.push({
+ message: `Failed to delete Supabase function: ${filePath}`,
+ error: error,
+ });
+ }
+ }
}
// If we have any file changes, commit them all at once
@@ -346,10 +435,32 @@ export async function processFullResponseActions(
approvalState: "approved",
})
.where(eq(messages.id, messageId));
-
return { updatedFiles: hasChanges };
} catch (error: unknown) {
logger.error("Error processing files:", error);
return { error: (error as any).toString() };
+ } finally {
+ const appendedContent = `
+ ${warnings
+ .map(
+ (warning) =>
+ `${warning.error}`
+ )
+ .join("\n")}
+ ${errors
+ .map(
+ (error) =>
+ `${error.error}`
+ )
+ .join("\n")}
+ `;
+ if (appendedContent.length > 0) {
+ await db
+ .update(messages)
+ .set({
+ content: fullResponse + "\n\n" + appendedContent,
+ })
+ .where(eq(messages.id, messageId));
+ }
}
}
diff --git a/src/supabase_admin/supabase_management_client.ts b/src/supabase_admin/supabase_management_client.ts
index 81a79b1..78f01b8 100644
--- a/src/supabase_admin/supabase_management_client.ts
+++ b/src/supabase_admin/supabase_management_client.ts
@@ -4,6 +4,9 @@ import {
SupabaseManagementAPI,
SupabaseManagementAPIError,
} from "@dyad-sh/supabase-management-js";
+import log from "electron-log";
+
+const logger = log.scope("supabase_management_client");
/**
* Checks if the Supabase access token is expired or about to expire
@@ -137,6 +140,23 @@ export async function executeSupabaseSql({
return JSON.stringify(result);
}
+export async function deleteSupabaseFunction({
+ supabaseProjectId,
+ functionName,
+}: {
+ supabaseProjectId: string;
+ functionName: string;
+}): Promise {
+ logger.info(
+ `Deleting Supabase function: ${functionName} from project: ${supabaseProjectId}`
+ );
+ const supabase = await getSupabaseClient();
+ await supabase.deleteFunction(supabaseProjectId, functionName);
+ logger.info(
+ `Deleted Supabase function: ${functionName} from project: ${supabaseProjectId}`
+ );
+}
+
export async function deploySupabaseFunctions({
supabaseProjectId,
functionName,
@@ -146,6 +166,9 @@ export async function deploySupabaseFunctions({
functionName: string;
content: string;
}): Promise {
+ logger.info(
+ `Deploying Supabase function: ${functionName} to project: ${supabaseProjectId}`
+ );
const supabase = await getSupabaseClient();
const formData = new FormData();
formData.append(
@@ -172,6 +195,9 @@ export async function deploySupabaseFunctions({
throw await createResponseError(response, "create function");
}
+ logger.info(
+ `Deployed Supabase function: ${functionName} to project: ${supabaseProjectId}`
+ );
return response.json();
}