diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 117afc2..5e99400 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -58,7 +58,7 @@ import { useVersions } from "@/hooks/useVersions"; import { useAttachments } from "@/hooks/useAttachments"; import { AttachmentsList } from "./AttachmentsList"; import { DragDropOverlay } from "./DragDropOverlay"; -import { showError, showUncommittedFilesWarning } from "@/lib/toast"; +import { showError, showExtraFilesToast } from "@/lib/toast"; import { ChatInputControls } from "../ChatInputControls"; const showTokenBarAtom = atom(false); @@ -183,8 +183,11 @@ export function ChatInput({ chatId }: { chatId?: number }) { chatId, messageId, }); - if (result.uncommittedFiles) { - showUncommittedFilesWarning(result.uncommittedFiles); + if (result.extraFiles) { + showExtraFilesToast({ + files: result.extraFiles, + error: result.extraFilesError, + }); } } catch (err) { console.error("Error approving proposal:", err); diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index f3193fa..24668d8 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -14,7 +14,7 @@ import { useChats } from "./useChats"; import { useLoadApp } from "./useLoadApp"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { useVersions } from "./useVersions"; -import { showUncommittedFilesWarning } from "@/lib/toast"; +import { showExtraFilesToast } from "@/lib/toast"; import { useProposal } from "./useProposal"; import { useSearch } from "@tanstack/react-router"; import { useRunApp } from "./useRunApp"; @@ -87,8 +87,11 @@ export function useStreamChat({ setIsPreviewOpen(true); refreshAppIframe(); } - if (response.uncommittedFiles) { - showUncommittedFilesWarning(response.uncommittedFiles); + if (response.extraFiles) { + showExtraFilesToast({ + files: response.extraFiles, + error: response.extraFilesError, + }); } refreshProposal(chatId); diff --git a/src/ipc/handlers/chat_stream_handlers.ts b/src/ipc/handlers/chat_stream_handlers.ts index 45cbd54..073dc7f 100644 --- a/src/ipc/handlers/chat_stream_handlers.ts +++ b/src/ipc/handlers/chat_stream_handlers.ts @@ -535,7 +535,8 @@ This conversation includes one or more image attachments. When the user uploads event.sender.send("chat:response:end", { chatId: req.chatId, updatedFiles: status.updatedFiles ?? false, - uncommittedFiles: status.uncommittedFiles, + extraFiles: status.extraFiles, + extraFilesError: status.extraFilesError, } satisfies ChatResponseEnd); } else { event.sender.send("chat:response:end", { diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index 3b54f7f..4d9d47f 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -30,6 +30,8 @@ import { extractCodebase } from "../../utils/codebase"; import { getDyadAppPath } from "../../paths/paths"; import { withLock } from "../utils/lock_utils"; import { createLoggedHandler } from "./safe_handle"; +import { ApproveProposalResult } from "../ipc_types"; + const logger = log.scope("proposal_handlers"); const handle = createLoggedHandler(logger); // Cache for codebase token counts @@ -317,9 +319,7 @@ const getProposalHandler = async ( const approveProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId, messageId }: { chatId: number; messageId: number }, -): Promise<{ - uncommittedFiles?: string[]; -}> => { +): Promise => { // 1. Fetch the specific assistant message const messageToApprove = await db.query.messages.findFirst({ where: and( @@ -355,7 +355,10 @@ const approveProposalHandler = async ( ); } - return { uncommittedFiles: processResult.uncommittedFiles }; + return { + extraFiles: processResult.extraFiles, + extraFilesError: processResult.extraFilesError, + }; }; // Handler to reject a proposal (just update message state) diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 46ee80b..5cf5fa2 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -26,6 +26,7 @@ import type { CreateCustomLanguageModelProviderParams, CreateCustomLanguageModelParams, DoesReleaseNoteExistParams, + ApproveProposalResult, } from "./ipc_types"; import type { ProposalResult } from "@/lib/schemas"; import { showError } from "@/lib/toast"; @@ -601,9 +602,7 @@ export class IpcClient { }: { chatId: number; messageId: number; - }): Promise<{ - uncommittedFiles?: string[]; - }> { + }): Promise { return this.ipcRenderer.invoke("approve-proposal", { chatId, messageId, diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 74b50b0..0f83d17 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -24,7 +24,8 @@ export interface ChatStreamParams { export interface ChatResponseEnd { chatId: number; updatedFiles: boolean; - uncommittedFiles?: string[]; + extraFiles?: string[]; + extraFilesError?: string; } export interface CreateAppParams { @@ -185,3 +186,8 @@ export interface CreateCustomLanguageModelParams { export interface DoesReleaseNoteExistParams { version: string; } + +export interface ApproveProposalResult { + extraFiles?: string[]; + extraFilesError?: string; +} diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts index f64d52c..6d01cec 100644 --- a/src/ipc/processors/response_processor.ts +++ b/src/ipc/processors/response_processor.ts @@ -178,7 +178,8 @@ export async function processFullResponseActions( ): Promise<{ updatedFiles?: boolean; error?: string; - uncommittedFiles?: string[]; + extraFiles?: string[]; + extraFilesError?: string; }> { logger.log("processFullResponseActions for chatId", chatId); // Get the app associated with the chat @@ -413,6 +414,10 @@ export async function processFullResponseActions( deletedFiles.length > 0 || dyadAddDependencyPackages.length > 0 || dyadExecuteSqlQueries.length > 0; + + let uncommittedFiles: string[] = []; + let extraFilesError: string | undefined; + if (hasChanges) { // Stage all written files for (const file of writtenFiles) { @@ -438,17 +443,54 @@ export async function processFullResponseActions( if (dyadExecuteSqlQueries.length > 0) changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`); + let message = chatSummary + ? `[dyad] ${chatSummary} - ${changes.join(", ")}` + : `[dyad] ${changes.join(", ")}`; // Use chat summary, if provided, or default for commit message - const commitHash = await git.commit({ + let commitHash = await git.commit({ fs, dir: appPath, - message: chatSummary - ? `[dyad] ${chatSummary} - ${changes.join(", ")}` - : `[dyad] ${changes.join(", ")}`, + message, author: await getGitAuthor(), }); logger.log(`Successfully committed changes: ${changes.join(", ")}`); + // Check for any uncommitted changes after the commit + const statusMatrix = await git.statusMatrix({ fs, dir: appPath }); + uncommittedFiles = statusMatrix + .filter((row) => row[1] !== 1 || row[2] !== 1 || row[3] !== 1) + .map((row) => row[0]); // Get just the file paths + + if (uncommittedFiles.length > 0) { + // Stage all changes + await git.add({ + fs, + dir: appPath, + filepath: ".", + }); + try { + commitHash = await git.commit({ + fs, + dir: appPath, + message: message + " + extra files edited outside of Dyad", + author: await getGitAuthor(), + amend: true, + }); + logger.log( + `Amend commit with changes outside of dyad: ${uncommittedFiles.join(", ")}`, + ); + } catch (error) { + // Just log, but don't throw an error because the user can still + // commit these changes outside of Dyad if needed. + logger.error( + `Failed to commit changes outside of dyad: ${uncommittedFiles.join( + ", ", + )}`, + ); + extraFilesError = (error as any).toString(); + } + } + // Save the commit hash to the message await db .update(messages) @@ -457,13 +499,6 @@ export async function processFullResponseActions( }) .where(eq(messages.id, messageId)); } - - // Check for any uncommitted changes after the commit - const statusMatrix = await git.statusMatrix({ fs, dir: appPath }); - const uncommittedFiles = statusMatrix - .filter((row) => row[1] !== 1 || row[2] !== 1 || row[3] !== 1) - .map((row) => row[0]); // Get just the file paths - logger.log("mark as approved: hasChanges", hasChanges); // Update the message to approved await db @@ -475,8 +510,8 @@ export async function processFullResponseActions( return { updatedFiles: hasChanges, - uncommittedFiles: - uncommittedFiles.length > 0 ? uncommittedFiles : undefined, + extraFiles: uncommittedFiles.length > 0 ? uncommittedFiles : undefined, + extraFilesError, }; } catch (error: unknown) { logger.error("Error processing files:", error); diff --git a/src/lib/toast.ts b/src/lib/toast.ts index b20aba9..dc95291 100644 --- a/src/lib/toast.ts +++ b/src/lib/toast.ts @@ -58,11 +58,23 @@ export const showLoading = ( }); }; -export const showUncommittedFilesWarning = (files: string[]) => { - showWarning( - `Some changed files were not committed. Please use git to manually commit them. +export const showExtraFilesToast = ({ + files, + error, +}: { + files: string[]; + error?: string; +}) => { + if (error) { + showError( + `Error committing files ${files.join(", ")} changed outside of Dyad: ${error}`, + ); + } else { + showWarning( + `Files changed outside of Dyad have automatically been committed: \n\n${files.join("\n")}`, - ); + ); + } }; // Re-export for direct use