Auto-commit extra files (#197)
Whenever Dyad does a commit from a proposal, it will automatically amend the commit with outside changes (e.g. made outside of Dyad). This helps avoid a lot of user confusion, e.g. https://github.com/dyad-sh/dyad/issues/187 https://www.reddit.com/r/dyadbuilders/comments/1kjysc0/error_pushing_images/ Edge cases: If a user adds a file outside of Dyad, and then they hit retry, it will revert these outside changes, but it's still technically in the version history, so I think it's OK. This should also be a pretty unusual situation. Fixes #164 Fixes #187
This commit is contained in:
@@ -58,7 +58,7 @@ import { useVersions } from "@/hooks/useVersions";
|
|||||||
import { useAttachments } from "@/hooks/useAttachments";
|
import { useAttachments } from "@/hooks/useAttachments";
|
||||||
import { AttachmentsList } from "./AttachmentsList";
|
import { AttachmentsList } from "./AttachmentsList";
|
||||||
import { DragDropOverlay } from "./DragDropOverlay";
|
import { DragDropOverlay } from "./DragDropOverlay";
|
||||||
import { showError, showUncommittedFilesWarning } from "@/lib/toast";
|
import { showError, showExtraFilesToast } from "@/lib/toast";
|
||||||
import { ChatInputControls } from "../ChatInputControls";
|
import { ChatInputControls } from "../ChatInputControls";
|
||||||
const showTokenBarAtom = atom(false);
|
const showTokenBarAtom = atom(false);
|
||||||
|
|
||||||
@@ -183,8 +183,11 @@ export function ChatInput({ chatId }: { chatId?: number }) {
|
|||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
});
|
});
|
||||||
if (result.uncommittedFiles) {
|
if (result.extraFiles) {
|
||||||
showUncommittedFilesWarning(result.uncommittedFiles);
|
showExtraFilesToast({
|
||||||
|
files: result.extraFiles,
|
||||||
|
error: result.extraFilesError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error approving proposal:", err);
|
console.error("Error approving proposal:", err);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useChats } from "./useChats";
|
|||||||
import { useLoadApp } from "./useLoadApp";
|
import { useLoadApp } from "./useLoadApp";
|
||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
import { useVersions } from "./useVersions";
|
import { useVersions } from "./useVersions";
|
||||||
import { showUncommittedFilesWarning } from "@/lib/toast";
|
import { showExtraFilesToast } from "@/lib/toast";
|
||||||
import { useProposal } from "./useProposal";
|
import { useProposal } from "./useProposal";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
import { useRunApp } from "./useRunApp";
|
import { useRunApp } from "./useRunApp";
|
||||||
@@ -87,8 +87,11 @@ export function useStreamChat({
|
|||||||
setIsPreviewOpen(true);
|
setIsPreviewOpen(true);
|
||||||
refreshAppIframe();
|
refreshAppIframe();
|
||||||
}
|
}
|
||||||
if (response.uncommittedFiles) {
|
if (response.extraFiles) {
|
||||||
showUncommittedFilesWarning(response.uncommittedFiles);
|
showExtraFilesToast({
|
||||||
|
files: response.extraFiles,
|
||||||
|
error: response.extraFilesError,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
refreshProposal(chatId);
|
refreshProposal(chatId);
|
||||||
|
|
||||||
|
|||||||
@@ -535,7 +535,8 @@ This conversation includes one or more image attachments. When the user uploads
|
|||||||
event.sender.send("chat:response:end", {
|
event.sender.send("chat:response:end", {
|
||||||
chatId: req.chatId,
|
chatId: req.chatId,
|
||||||
updatedFiles: status.updatedFiles ?? false,
|
updatedFiles: status.updatedFiles ?? false,
|
||||||
uncommittedFiles: status.uncommittedFiles,
|
extraFiles: status.extraFiles,
|
||||||
|
extraFilesError: status.extraFilesError,
|
||||||
} satisfies ChatResponseEnd);
|
} satisfies ChatResponseEnd);
|
||||||
} else {
|
} else {
|
||||||
event.sender.send("chat:response:end", {
|
event.sender.send("chat:response:end", {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import { extractCodebase } from "../../utils/codebase";
|
|||||||
import { getDyadAppPath } from "../../paths/paths";
|
import { getDyadAppPath } from "../../paths/paths";
|
||||||
import { withLock } from "../utils/lock_utils";
|
import { withLock } from "../utils/lock_utils";
|
||||||
import { createLoggedHandler } from "./safe_handle";
|
import { createLoggedHandler } from "./safe_handle";
|
||||||
|
import { ApproveProposalResult } from "../ipc_types";
|
||||||
|
|
||||||
const logger = log.scope("proposal_handlers");
|
const logger = log.scope("proposal_handlers");
|
||||||
const handle = createLoggedHandler(logger);
|
const handle = createLoggedHandler(logger);
|
||||||
// Cache for codebase token counts
|
// Cache for codebase token counts
|
||||||
@@ -317,9 +319,7 @@ const getProposalHandler = async (
|
|||||||
const approveProposalHandler = async (
|
const approveProposalHandler = async (
|
||||||
_event: IpcMainInvokeEvent,
|
_event: IpcMainInvokeEvent,
|
||||||
{ chatId, messageId }: { chatId: number; messageId: number },
|
{ chatId, messageId }: { chatId: number; messageId: number },
|
||||||
): Promise<{
|
): Promise<ApproveProposalResult> => {
|
||||||
uncommittedFiles?: string[];
|
|
||||||
}> => {
|
|
||||||
// 1. Fetch the specific assistant message
|
// 1. Fetch the specific assistant message
|
||||||
const messageToApprove = await db.query.messages.findFirst({
|
const messageToApprove = await db.query.messages.findFirst({
|
||||||
where: and(
|
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)
|
// Handler to reject a proposal (just update message state)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
CreateCustomLanguageModelProviderParams,
|
CreateCustomLanguageModelProviderParams,
|
||||||
CreateCustomLanguageModelParams,
|
CreateCustomLanguageModelParams,
|
||||||
DoesReleaseNoteExistParams,
|
DoesReleaseNoteExistParams,
|
||||||
|
ApproveProposalResult,
|
||||||
} from "./ipc_types";
|
} from "./ipc_types";
|
||||||
import type { ProposalResult } from "@/lib/schemas";
|
import type { ProposalResult } from "@/lib/schemas";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
@@ -601,9 +602,7 @@ export class IpcClient {
|
|||||||
}: {
|
}: {
|
||||||
chatId: number;
|
chatId: number;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
}): Promise<{
|
}): Promise<ApproveProposalResult> {
|
||||||
uncommittedFiles?: string[];
|
|
||||||
}> {
|
|
||||||
return this.ipcRenderer.invoke("approve-proposal", {
|
return this.ipcRenderer.invoke("approve-proposal", {
|
||||||
chatId,
|
chatId,
|
||||||
messageId,
|
messageId,
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export interface ChatStreamParams {
|
|||||||
export interface ChatResponseEnd {
|
export interface ChatResponseEnd {
|
||||||
chatId: number;
|
chatId: number;
|
||||||
updatedFiles: boolean;
|
updatedFiles: boolean;
|
||||||
uncommittedFiles?: string[];
|
extraFiles?: string[];
|
||||||
|
extraFilesError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateAppParams {
|
export interface CreateAppParams {
|
||||||
@@ -185,3 +186,8 @@ export interface CreateCustomLanguageModelParams {
|
|||||||
export interface DoesReleaseNoteExistParams {
|
export interface DoesReleaseNoteExistParams {
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApproveProposalResult {
|
||||||
|
extraFiles?: string[];
|
||||||
|
extraFilesError?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ export async function processFullResponseActions(
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
updatedFiles?: boolean;
|
updatedFiles?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
uncommittedFiles?: string[];
|
extraFiles?: string[];
|
||||||
|
extraFilesError?: string;
|
||||||
}> {
|
}> {
|
||||||
logger.log("processFullResponseActions for chatId", chatId);
|
logger.log("processFullResponseActions for chatId", chatId);
|
||||||
// Get the app associated with the chat
|
// Get the app associated with the chat
|
||||||
@@ -413,6 +414,10 @@ export async function processFullResponseActions(
|
|||||||
deletedFiles.length > 0 ||
|
deletedFiles.length > 0 ||
|
||||||
dyadAddDependencyPackages.length > 0 ||
|
dyadAddDependencyPackages.length > 0 ||
|
||||||
dyadExecuteSqlQueries.length > 0;
|
dyadExecuteSqlQueries.length > 0;
|
||||||
|
|
||||||
|
let uncommittedFiles: string[] = [];
|
||||||
|
let extraFilesError: string | undefined;
|
||||||
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
// Stage all written files
|
// Stage all written files
|
||||||
for (const file of writtenFiles) {
|
for (const file of writtenFiles) {
|
||||||
@@ -438,17 +443,54 @@ export async function processFullResponseActions(
|
|||||||
if (dyadExecuteSqlQueries.length > 0)
|
if (dyadExecuteSqlQueries.length > 0)
|
||||||
changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`);
|
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
|
// Use chat summary, if provided, or default for commit message
|
||||||
const commitHash = await git.commit({
|
let commitHash = await git.commit({
|
||||||
fs,
|
fs,
|
||||||
dir: appPath,
|
dir: appPath,
|
||||||
message: chatSummary
|
message,
|
||||||
? `[dyad] ${chatSummary} - ${changes.join(", ")}`
|
|
||||||
: `[dyad] ${changes.join(", ")}`,
|
|
||||||
author: await getGitAuthor(),
|
author: await getGitAuthor(),
|
||||||
});
|
});
|
||||||
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
|
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
|
// Save the commit hash to the message
|
||||||
await db
|
await db
|
||||||
.update(messages)
|
.update(messages)
|
||||||
@@ -457,13 +499,6 @@ export async function processFullResponseActions(
|
|||||||
})
|
})
|
||||||
.where(eq(messages.id, messageId));
|
.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);
|
logger.log("mark as approved: hasChanges", hasChanges);
|
||||||
// Update the message to approved
|
// Update the message to approved
|
||||||
await db
|
await db
|
||||||
@@ -475,8 +510,8 @@ export async function processFullResponseActions(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
updatedFiles: hasChanges,
|
updatedFiles: hasChanges,
|
||||||
uncommittedFiles:
|
extraFiles: uncommittedFiles.length > 0 ? uncommittedFiles : undefined,
|
||||||
uncommittedFiles.length > 0 ? uncommittedFiles : undefined,
|
extraFilesError,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error("Error processing files:", error);
|
logger.error("Error processing files:", error);
|
||||||
|
|||||||
@@ -58,11 +58,23 @@ export const showLoading = <T>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showUncommittedFilesWarning = (files: string[]) => {
|
export const showExtraFilesToast = ({
|
||||||
showWarning(
|
files,
|
||||||
`Some changed files were not committed. Please use git to manually commit them.
|
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")}`,
|
\n\n${files.join("\n")}`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-export for direct use
|
// Re-export for direct use
|
||||||
|
|||||||
Reference in New Issue
Block a user