diff --git a/drizzle/0001_hesitant_roland_deschain.sql b/drizzle/0001_hesitant_roland_deschain.sql new file mode 100644 index 0000000..31a509d --- /dev/null +++ b/drizzle/0001_hesitant_roland_deschain.sql @@ -0,0 +1 @@ +ALTER TABLE `messages` ADD `approval_state` text; \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..e1dd202 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,199 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "0803dac6-46b8-4e22-8397-4840e614d6c9", + "prevId": "1a0ffcb3-606d-4b03-81b7-7c585555a548", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_app_id_apps_id_fk": { + "name": "chats_app_id_apps_id_fk", + "tableFrom": "chats", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 796a6ca..f943ad7 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1744692127560, "tag": "0000_nebulous_proemial_gods", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1744999922420, + "tag": "0001_hesitant_roland_deschain", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index f67c6ad..72d9703 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -220,7 +220,7 @@ function ChatInputActions({ ) : ( )} - Review: {proposal.title} + {proposal.title} {proposal.securityRisks.length > 0 && ( @@ -269,30 +269,32 @@ function ChatInputActions({ {isDetailsVisible && (
-
-

Security Risks

-
    - {proposal.securityRisks.map((risk, index) => ( -
  • - {risk.type === "warning" ? ( - - ) : ( - - )} -
    - {risk.title}:{" "} - {risk.description} -
    -
  • - ))} -
-
+ {!!proposal.securityRisks.length && ( +
+

Security Risks

+
    + {proposal.securityRisks.map((risk, index) => ( +
  • + {risk.type === "warning" ? ( + + ) : ( + + )} +
    + {risk.title}:{" "} + {risk.description} +
    +
  • + ))} +
+
+ )}

Files Changed

diff --git a/src/db/schema.ts b/src/db/schema.ts index d0d14a0..9084c96 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -35,7 +35,7 @@ export const messages = sqliteTable("messages", { role: text("role", { enum: ["user", "assistant"] }).notNull(), content: text("content").notNull(), approvalState: text("approval_state", { - enum: ["approved", "rejected", "pending"], + enum: ["approved", "rejected"], }), createdAt: integer("created_at", { mode: "timestamp" }) .notNull() diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index 8d665d7..ef3d7b2 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -1,43 +1,86 @@ 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 path from "node:path"; // Import path for basename +// Import tag parsers +import { + getDyadChatSummaryTag, + getDyadWriteTags, +} from "../processors/response_processor"; -// Placeholder Proposal data -const placeholderProposal: Proposal = { - title: "Review: Example Refactoring (from IPC)", - securityRisks: [ - { - type: "warning", - title: "Potential XSS Vulnerability", - description: "User input is directly rendered without sanitization.", - }, - { - type: "danger", - title: "Hardcoded API Key", - description: "API key found in plain text in configuration file.", - }, - ], - filesChanged: [ - { - name: "ChatInput.tsx", - path: "src/components/chat/ChatInput.tsx", - summary: "Added review actions and details section.", - }, - { - name: "api.ts", - path: "src/lib/api.ts", - summary: "Refactored API call structure.", - }, - ], -}; +// Placeholder Proposal data (can be removed or kept for reference) +// const placeholderProposal: Proposal = { ... }; + +// Type guard for the parsed proposal structure +interface ParsedProposal { + title: string; + files: string[]; +} + +function isParsedProposal(obj: any): obj is ParsedProposal { + return ( + obj && + typeof obj === "object" && + typeof obj.title === "string" && + Array.isArray(obj.files) && + obj.files.every((file: any) => typeof file === "string") + ); +} const getProposalHandler = async ( _event: IpcMainInvokeEvent, { chatId }: { chatId: number } -): Promise => { +): Promise => { console.log(`IPC: get-proposal called for chatId: ${chatId}`); - // Simulate async operation - await new Promise((resolve) => setTimeout(resolve, 500)); // 500ms delay - return placeholderProposal; + + try { + // Find the latest ASSISTANT message for the chat + const latestAssistantMessage = await db.query.messages.findFirst({ + where: and(eq(messages.chatId, chatId), eq(messages.role, "assistant")), + orderBy: [desc(messages.createdAt)], + columns: { + content: true, // Fetch the content to parse + }, + }); + + if (latestAssistantMessage?.content) { + console.log("Found latest assistant message, parsing content..."); + const messageContent = latestAssistantMessage.content; + + // Parse tags directly from message content + const proposalTitle = getDyadChatSummaryTag(messageContent); + const proposalFiles = getDyadWriteTags(messageContent); // Gets { path: string, content: string }[] + + // Check if we have enough information to create a proposal + if (proposalTitle || proposalFiles.length > 0) { + const proposal: Proposal = { + // Use parsed title or a default title if summary tag is missing but write tags exist + title: proposalTitle ?? "Proposed File Changes", + securityRisks: [], // Keep empty + filesChanged: proposalFiles.map((tag) => ({ + name: path.basename(tag.path), + path: tag.path, + summary: tag.description ?? "(no change summary found)", // Generic summary + })), + }; + console.log("Generated proposal on the fly:", proposal); + return proposal; + } else { + console.log( + "No relevant tags found in the latest assistant message content." + ); + return null; // No proposal could be generated + } + } else { + console.log(`No assistant message found for chatId: ${chatId}`); + return null; // No message found + } + } catch (error) { + console.error(`Error processing proposal for chatId ${chatId}:`, error); + return null; // Indicate DB or processing error + } }; // Function to register proposal-related handlers diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts index 0210273..b18cdb6 100644 --- a/src/ipc/processors/response_processor.ts +++ b/src/ipc/processors/response_processor.ts @@ -11,20 +11,42 @@ import { getGitAuthor } from "../utils/git_author"; export function getDyadWriteTags(fullResponse: string): { path: string; content: string; + description?: string; }[] { - const dyadWriteRegex = - /]*>([\s\S]*?)<\/dyad-write>/g; + const dyadWriteRegex = /]*)>([\s\S]*?)<\/dyad-write>/gi; + const pathRegex = /path="([^"]+)"/; + const descriptionRegex = /description="([^"]+)"/; + let match; - const tags: { path: string; content: string }[] = []; + const tags: { path: string; content: string; description?: string }[] = []; + while ((match = dyadWriteRegex.exec(fullResponse)) !== null) { - const content = match[2].trim().split("\n"); - if (content[0].startsWith("```")) { - content.shift(); + const attributesString = match[1]; + let content = match[2].trim(); + + const pathMatch = pathRegex.exec(attributesString); + const descriptionMatch = descriptionRegex.exec(attributesString); + + if (pathMatch && pathMatch[1]) { + const path = pathMatch[1]; + const description = descriptionMatch?.[1]; + + const contentLines = content.split("\n"); + if (contentLines[0]?.startsWith("```")) { + contentLines.shift(); + } + if (contentLines[contentLines.length - 1]?.startsWith("```")) { + contentLines.pop(); + } + content = contentLines.join("\n"); + + tags.push({ path, content, description }); + } else { + console.warn( + "Found tag without a valid 'path' attribute:", + match[0] + ); } - if (content[content.length - 1].startsWith("```")) { - content.pop(); - } - tags.push({ path: match[1], content: content.join("\n") }); } return tags; } @@ -65,6 +87,16 @@ export function getDyadAddDependencyTags(fullResponse: string): string[] { return packages; } +export function getDyadChatSummaryTag(fullResponse: string): string | null { + const dyadChatSummaryRegex = + /([\s\S]*?)<\/dyad-chat-summary>/g; + const match = dyadChatSummaryRegex.exec(fullResponse); + if (match && match[1]) { + return match[1].trim(); + } + return null; +} + export async function processFullResponseActions( fullResponse: string, chatId: number, @@ -206,6 +238,7 @@ export async function processFullResponseActions( if (deletedFiles.length > 0) changes.push(`deleted ${deletedFiles.length} file(s)`); + // Use chat summary, if provided, or default for commit message await git.commit({ fs, dir: appPath,