diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx
index 72d9703..8c24a52 100644
--- a/src/components/chat/ChatInput.tsx
+++ b/src/components/chat/ChatInput.tsx
@@ -8,6 +8,7 @@ import {
AlertOctagon,
FileText,
Check,
+ Loader2,
} from "lucide-react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
@@ -31,6 +32,8 @@ interface ChatInputActionsProps {
onApprove: () => void;
onReject: () => void;
isApprovable: boolean; // Can be used to enable/disable buttons
+ isApproving: boolean; // State for approving
+ isRejecting: boolean; // State for rejecting
}
interface ChatInputProps {
@@ -46,12 +49,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
useStreamChat();
const [selectedAppId] = useAtom(selectedAppIdAtom);
const [showError, setShowError] = useState(true);
+ const [isApproving, setIsApproving] = useState(false); // State for approving
+ const [isRejecting, setIsRejecting] = useState(false); // State for rejecting
// Use the hook to fetch the proposal
const {
proposal,
+ messageId,
isLoading: isProposalLoading,
error: proposalError,
+ refreshProposal,
} = useProposal(chatId);
const adjustHeight = () => {
@@ -102,14 +109,60 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
setShowError(false);
};
- const handleApprove = () => {
- console.log("Approve clicked");
- // Add approve logic here
+ const handleApprove = async () => {
+ if (!chatId || !messageId || isApproving || isRejecting || isStreaming)
+ return;
+ console.log(
+ `Approving proposal for chatId: ${chatId}, messageId: ${messageId}`
+ );
+ setIsApproving(true);
+ try {
+ const result = await IpcClient.getInstance().approveProposal({
+ chatId,
+ messageId,
+ });
+ if (result.success) {
+ console.log("Proposal approved successfully");
+ // TODO: Maybe refresh proposal state or show confirmation?
+ } else {
+ console.error("Failed to approve proposal:", result.error);
+ setError(result.error || "Failed to approve proposal");
+ }
+ } catch (err) {
+ console.error("Error approving proposal:", err);
+ setError((err as Error)?.message || "An error occurred while approving");
+ } finally {
+ setIsApproving(false);
+ refreshProposal();
+ }
};
- const handleReject = () => {
- console.log("Reject clicked");
- // Add reject logic here
+ const handleReject = async () => {
+ if (!chatId || !messageId || isApproving || isRejecting || isStreaming)
+ return;
+ console.log(
+ `Rejecting proposal for chatId: ${chatId}, messageId: ${messageId}`
+ );
+ setIsRejecting(true);
+ try {
+ const result = await IpcClient.getInstance().rejectProposal({
+ chatId,
+ messageId,
+ });
+ if (result.success) {
+ console.log("Proposal rejected successfully");
+ // TODO: Maybe refresh proposal state or show confirmation?
+ } else {
+ console.error("Failed to reject proposal:", result.error);
+ setError(result.error || "Failed to reject proposal");
+ }
+ } catch (err) {
+ console.error("Error rejecting proposal:", err);
+ setError((err as Error)?.message || "An error occurred while rejecting");
+ } finally {
+ setIsRejecting(false);
+ refreshProposal();
+ }
};
if (!settings) {
@@ -150,7 +203,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
proposal={proposal}
onApprove={handleApprove}
onReject={handleReject}
- isApprovable={!isProposalLoading && !!proposal}
+ isApprovable={
+ !isProposalLoading &&
+ !!proposal &&
+ !!messageId &&
+ !isApproving &&
+ !isRejecting &&
+ !isStreaming
+ }
+ isApproving={isApproving}
+ isRejecting={isRejecting}
/>
)}
@@ -202,6 +264,8 @@ function ChatInputActions({
onApprove,
onReject,
isApprovable,
+ isApproving,
+ isRejecting,
}: ChatInputActionsProps) {
const [autoApprove, setAutoApprove] = useState(false);
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
@@ -236,9 +300,13 @@ function ChatInputActions({
size="sm"
variant="outline"
onClick={onApprove}
- disabled={!isApprovable}
+ disabled={!isApprovable || isApproving || isRejecting}
>
-
+ {isApproving ? (
+
+ ) : (
+
+ )}
Approve
diff --git a/src/hooks/useProposal.ts b/src/hooks/useProposal.ts
index 3a460a3..ec426f4 100644
--- a/src/hooks/useProposal.ts
+++ b/src/hooks/useProposal.ts
@@ -1,44 +1,73 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
import { IpcClient } from "@/ipc/ipc_client";
import type { Proposal } from "@/lib/schemas"; // Import Proposal type
-export function useProposal(chatId: number | undefined) {
- const [proposal, setProposal] = useState
(null);
+// Define the structure returned by the IPC call
+interface ProposalResult {
+ proposal: Proposal;
+ messageId: number;
+}
+
+export function useProposal(chatId?: number | undefined) {
+ const [proposalData, setProposalData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
- useEffect(() => {
- if (chatId === undefined) {
- setProposal(null);
- setIsLoading(false);
- setError(null);
- return;
- }
-
- const fetchProposal = async () => {
+ const fetchProposal = useCallback(
+ async (innerChatId?: number) => {
+ chatId = chatId ?? innerChatId;
+ if (chatId === undefined) {
+ setProposalData(null);
+ setIsLoading(false);
+ setError(null);
+ return;
+ }
setIsLoading(true);
setError(null);
+ setProposalData(null); // Reset on new fetch
try {
- const fetchedProposal = await IpcClient.getInstance().getProposal(
+ // Type assertion might be needed depending on how IpcClient is typed
+ const result = (await IpcClient.getInstance().getProposal(
chatId
- );
- setProposal(fetchedProposal);
+ )) as ProposalResult | null;
+
+ if (result) {
+ setProposalData(result);
+ } else {
+ setProposalData(null); // Explicitly set to null if IPC returns null
+ }
} catch (err: any) {
console.error("Error fetching proposal:", err);
setError(err.message || "Failed to fetch proposal");
- setProposal(null); // Clear proposal on error
+ setProposalData(null); // Clear proposal data on error
} finally {
setIsLoading(false);
}
- };
+ },
+ [chatId]
+ ); // Depend on chatId
+ useEffect(() => {
fetchProposal();
// Cleanup function if needed (e.g., for aborting requests)
// return () => {
// // Abort logic here
// };
- }, [chatId]); // Re-run effect if chatId changes
+ }, [fetchProposal]); // Re-run effect if fetchProposal changes (due to chatId change)
- return { proposal, isLoading, error };
+ const refreshProposal = useCallback(
+ (chatId?: number) => {
+ fetchProposal(chatId);
+ },
+ [fetchProposal]
+ );
+
+ return {
+ proposal: proposalData?.proposal ?? null,
+ messageId: proposalData?.messageId,
+ isLoading,
+ error,
+ refreshProposal, // Expose the refresh function
+ };
}
diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts
index df1d476..5c7fb21 100644
--- a/src/hooks/useStreamChat.ts
+++ b/src/hooks/useStreamChat.ts
@@ -15,6 +15,7 @@ import { useLoadApp } from "./useLoadApp";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadVersions } from "./useLoadVersions";
import { showError } from "@/lib/toast";
+import { useProposal } from "./useProposal";
export function getRandomString() {
return Math.random().toString(36).substring(2, 15);
@@ -30,7 +31,7 @@ export function useStreamChat() {
const { refreshApp } = useLoadApp(selectedAppId);
const setStreamCount = useSetAtom(chatStreamCountAtom);
const { refreshVersions } = useLoadVersions(selectedAppId);
-
+ const { refreshProposal } = useProposal();
const streamMessage = useCallback(
async ({
prompt,
@@ -94,6 +95,7 @@ export function useStreamChat() {
// Keep the same as below
setIsStreaming(false);
+ refreshProposal(chatId);
refreshChats();
refreshApp();
refreshVersions();
diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts
index ef3d7b2..edba535 100644
--- a/src/ipc/handlers/proposal_handlers.ts
+++ b/src/ipc/handlers/proposal_handlers.ts
@@ -2,12 +2,13 @@ import { ipcMain, type IpcMainInvokeEvent } from "electron";
import type { Proposal } from "@/lib/schemas";
import { db } from "../../db";
import { messages } from "../../db/schema";
-import { desc, eq, and } from "drizzle-orm";
+import { desc, eq, and, Update } from "drizzle-orm";
import path from "node:path"; // Import path for basename
// Import tag parsers
import {
getDyadChatSummaryTag,
getDyadWriteTags,
+ processFullResponseActions,
} from "../processors/response_processor";
// Placeholder Proposal data (can be removed or kept for reference)
@@ -19,6 +20,12 @@ interface ParsedProposal {
files: string[];
}
+// Define return type for getProposalHandler
+interface ProposalResult {
+ proposal: Proposal;
+ messageId: number;
+}
+
function isParsedProposal(obj: any): obj is ParsedProposal {
return (
obj &&
@@ -32,7 +39,7 @@ function isParsedProposal(obj: any): obj is ParsedProposal {
const getProposalHandler = async (
_event: IpcMainInvokeEvent,
{ chatId }: { chatId: number }
-): Promise => {
+): Promise => {
console.log(`IPC: get-proposal called for chatId: ${chatId}`);
try {
@@ -41,12 +48,16 @@ const getProposalHandler = async (
where: and(eq(messages.chatId, chatId), eq(messages.role, "assistant")),
orderBy: [desc(messages.createdAt)],
columns: {
+ id: true, // Fetch the ID
content: true, // Fetch the content to parse
},
});
- if (latestAssistantMessage?.content) {
- console.log("Found latest assistant message, parsing content...");
+ if (latestAssistantMessage?.content && latestAssistantMessage.id) {
+ const messageId = latestAssistantMessage.id; // Get the message ID
+ console.log(
+ `Found latest assistant message (ID: ${messageId}), parsing content...`
+ );
const messageContent = latestAssistantMessage.content;
// Parse tags directly from message content
@@ -66,7 +77,7 @@ const getProposalHandler = async (
})),
};
console.log("Generated proposal on the fly:", proposal);
- return proposal;
+ return { proposal, messageId }; // Return proposal and messageId
} else {
console.log(
"No relevant tags found in the latest assistant message content."
@@ -83,8 +94,127 @@ const getProposalHandler = async (
}
};
+// Handler to approve a proposal (process actions and update message)
+const approveProposalHandler = async (
+ _event: IpcMainInvokeEvent,
+ { chatId, messageId }: { chatId: number; messageId: number }
+): Promise<{ success: boolean; error?: string }> => {
+ console.log(
+ `IPC: approve-proposal called for chatId: ${chatId}, messageId: ${messageId}`
+ );
+
+ try {
+ // 1. Fetch the specific assistant message
+ const messageToApprove = await db.query.messages.findFirst({
+ where: and(
+ eq(messages.id, messageId),
+ eq(messages.chatId, chatId),
+ eq(messages.role, "assistant")
+ ),
+ columns: {
+ content: true,
+ },
+ });
+
+ if (!messageToApprove?.content) {
+ console.error(
+ `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`
+ );
+ return { success: false, error: "Assistant message not found." };
+ }
+
+ // 2. Process the actions defined in the message content
+ const chatSummary = getDyadChatSummaryTag(messageToApprove.content);
+ const processResult = await processFullResponseActions(
+ messageToApprove.content,
+ chatId,
+ { chatSummary: chatSummary ?? undefined } // Pass summary if found
+ );
+
+ if (processResult.error) {
+ console.error(
+ `Error processing actions for message ${messageId}:`,
+ processResult.error
+ );
+ // Optionally: Update message state to 'error' or similar?
+ // For now, just return error to frontend
+ return {
+ success: false,
+ error: `Action processing failed: ${processResult.error}`,
+ };
+ }
+
+ // 3. Update the message's approval state to 'approved'
+ await db
+ .update(messages)
+ .set({ approvalState: "approved" })
+ .where(eq(messages.id, messageId));
+
+ console.log(`Message ${messageId} marked as approved.`);
+ return { success: true };
+ } catch (error) {
+ console.error(
+ `Error approving proposal for messageId ${messageId}:`,
+ error
+ );
+ return {
+ success: false,
+ error: (error as Error)?.message || "Unknown error",
+ };
+ }
+};
+
+// Handler to reject a proposal (just update message state)
+const rejectProposalHandler = async (
+ _event: IpcMainInvokeEvent,
+ { chatId, messageId }: { chatId: number; messageId: number }
+): Promise<{ success: boolean; error?: string }> => {
+ console.log(
+ `IPC: reject-proposal called for chatId: ${chatId}, messageId: ${messageId}`
+ );
+
+ try {
+ // 1. Verify the message exists and is an assistant message
+ const messageToReject = await db.query.messages.findFirst({
+ where: and(
+ eq(messages.id, messageId),
+ eq(messages.chatId, chatId),
+ eq(messages.role, "assistant")
+ ),
+ columns: { id: true }, // Only need to confirm existence
+ });
+
+ if (!messageToReject) {
+ console.error(
+ `Assistant message not found for chatId: ${chatId}, messageId: ${messageId}`
+ );
+ return { success: false, error: "Assistant message not found." };
+ }
+
+ // 2. Update the message's approval state to 'rejected'
+ await db
+ .update(messages)
+ .set({ approvalState: "rejected" })
+ .where(eq(messages.id, messageId));
+
+ console.log(`Message ${messageId} marked as rejected.`);
+ return { success: true };
+ } catch (error) {
+ console.error(
+ `Error rejecting proposal for messageId ${messageId}:`,
+ error
+ );
+ return {
+ success: false,
+ error: (error as Error)?.message || "Unknown error",
+ };
+ }
+};
+
// Function to register proposal-related handlers
export function registerProposalHandlers() {
ipcMain.handle("get-proposal", getProposalHandler);
- console.log("Registered proposal IPC handlers");
+ ipcMain.handle("approve-proposal", approveProposalHandler);
+ ipcMain.handle("reject-proposal", rejectProposalHandler);
+ console.log("Registered proposal IPC handlers (get, approve, reject)");
}
diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts
index dcc655b..27f61c4 100644
--- a/src/ipc/ipc_client.ts
+++ b/src/ipc/ipc_client.ts
@@ -20,6 +20,12 @@ import type {
import type { Proposal } from "@/lib/schemas";
import { showError } from "@/lib/toast";
+// Define the structure returned by getProposal
+interface ProposalResult {
+ proposal: Proposal;
+ messageId: number;
+}
+
export interface ChatStreamCallbacks {
onUpdate: (messages: Message[]) => void;
onEnd: (response: ChatResponseEnd) => void;
@@ -616,11 +622,12 @@ export class IpcClient {
}
// Get proposal details
- public async getProposal(chatId: number): Promise {
+ public async getProposal(chatId: number): Promise {
try {
const data = await this.ipcRenderer.invoke("get-proposal", { chatId });
- // Assuming the main process returns data matching the Proposal interface
- return data as Proposal;
+ // Assuming the main process returns data matching the ProposalResult interface
+ // Add a type check/guard if necessary for robustness
+ return data as ProposalResult | null;
} catch (error) {
showError(error);
throw error;
@@ -629,4 +636,44 @@ export class IpcClient {
// Example methods for listening to events (if needed)
// public on(channel: string, func: (...args: any[]) => void): void {
+
+ // --- Proposal Management ---
+ public async approveProposal({
+ chatId,
+ messageId,
+ }: {
+ chatId: number;
+ messageId: number;
+ }): Promise<{ success: boolean; error?: string }> {
+ try {
+ const result = await this.ipcRenderer.invoke("approve-proposal", {
+ chatId,
+ messageId,
+ });
+ return result as { success: boolean; error?: string };
+ } catch (error) {
+ showError(error);
+ return { success: false, error: (error as Error).message };
+ }
+ }
+
+ public async rejectProposal({
+ chatId,
+ messageId,
+ }: {
+ chatId: number;
+ messageId: number;
+ }): Promise<{ success: boolean; error?: string }> {
+ try {
+ const result = await this.ipcRenderer.invoke("reject-proposal", {
+ chatId,
+ messageId,
+ });
+ return result as { success: boolean; error?: string };
+ } catch (error) {
+ showError(error);
+ return { success: false, error: (error as Error).message };
+ }
+ }
+ // --- End Proposal Management ---
}
diff --git a/src/preload.ts b/src/preload.ts
index 40b5e50..09dde42 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -39,6 +39,8 @@ const validInvokeChannels = [
"get-app-version",
"reload-env-path",
"get-proposal",
+ "approve-proposal",
+ "reject-proposal",
] as const;
// Add valid receive channels