Parse proposal from latest chat message (w/o security parts)
This commit is contained in:
1
drizzle/0001_hesitant_roland_deschain.sql
Normal file
1
drizzle/0001_hesitant_roland_deschain.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `messages` ADD `approval_state` text;
|
||||||
199
drizzle/meta/0001_snapshot.json
Normal file
199
drizzle/meta/0001_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,13 @@
|
|||||||
"when": 1744692127560,
|
"when": 1744692127560,
|
||||||
"tag": "0000_nebulous_proemial_gods",
|
"tag": "0000_nebulous_proemial_gods",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1744999922420,
|
||||||
|
"tag": "0001_hesitant_roland_deschain",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ function ChatInputActions({
|
|||||||
) : (
|
) : (
|
||||||
<ChevronDown size={16} className="mr-1" />
|
<ChevronDown size={16} className="mr-1" />
|
||||||
)}
|
)}
|
||||||
Review: {proposal.title}
|
{proposal.title}
|
||||||
</button>
|
</button>
|
||||||
{proposal.securityRisks.length > 0 && (
|
{proposal.securityRisks.length > 0 && (
|
||||||
<span className="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">
|
<span className="bg-red-100 text-red-700 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||||
@@ -269,6 +269,7 @@ function ChatInputActions({
|
|||||||
|
|
||||||
{isDetailsVisible && (
|
{isDetailsVisible && (
|
||||||
<div className="p-3 border-t border-border bg-muted/50 text-sm">
|
<div className="p-3 border-t border-border bg-muted/50 text-sm">
|
||||||
|
{!!proposal.securityRisks.length && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h4 className="font-semibold mb-1">Security Risks</h4>
|
<h4 className="font-semibold mb-1">Security Risks</h4>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
@@ -293,6 +294,7 @@ function ChatInputActions({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="font-semibold mb-1">Files Changed</h4>
|
<h4 className="font-semibold mb-1">Files Changed</h4>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const messages = sqliteTable("messages", {
|
|||||||
role: text("role", { enum: ["user", "assistant"] }).notNull(),
|
role: text("role", { enum: ["user", "assistant"] }).notNull(),
|
||||||
content: text("content").notNull(),
|
content: text("content").notNull(),
|
||||||
approvalState: text("approval_state", {
|
approvalState: text("approval_state", {
|
||||||
enum: ["approved", "rejected", "pending"],
|
enum: ["approved", "rejected"],
|
||||||
}),
|
}),
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
|
|||||||
@@ -1,43 +1,86 @@
|
|||||||
import { ipcMain, type IpcMainInvokeEvent } from "electron";
|
import { ipcMain, type IpcMainInvokeEvent } from "electron";
|
||||||
import type { Proposal } from "@/lib/schemas";
|
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
|
// Placeholder Proposal data (can be removed or kept for reference)
|
||||||
const placeholderProposal: Proposal = {
|
// const placeholderProposal: Proposal = { ... };
|
||||||
title: "Review: Example Refactoring (from IPC)",
|
|
||||||
securityRisks: [
|
// Type guard for the parsed proposal structure
|
||||||
{
|
interface ParsedProposal {
|
||||||
type: "warning",
|
title: string;
|
||||||
title: "Potential XSS Vulnerability",
|
files: string[];
|
||||||
description: "User input is directly rendered without sanitization.",
|
}
|
||||||
},
|
|
||||||
{
|
function isParsedProposal(obj: any): obj is ParsedProposal {
|
||||||
type: "danger",
|
return (
|
||||||
title: "Hardcoded API Key",
|
obj &&
|
||||||
description: "API key found in plain text in configuration file.",
|
typeof obj === "object" &&
|
||||||
},
|
typeof obj.title === "string" &&
|
||||||
],
|
Array.isArray(obj.files) &&
|
||||||
filesChanged: [
|
obj.files.every((file: any) => typeof file === "string")
|
||||||
{
|
);
|
||||||
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.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProposalHandler = async (
|
const getProposalHandler = async (
|
||||||
_event: IpcMainInvokeEvent,
|
_event: IpcMainInvokeEvent,
|
||||||
{ chatId }: { chatId: number }
|
{ chatId }: { chatId: number }
|
||||||
): Promise<Proposal> => {
|
): Promise<Proposal | null> => {
|
||||||
console.log(`IPC: get-proposal called for chatId: ${chatId}`);
|
console.log(`IPC: get-proposal called for chatId: ${chatId}`);
|
||||||
// Simulate async operation
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500)); // 500ms delay
|
try {
|
||||||
return placeholderProposal;
|
// 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
|
// Function to register proposal-related handlers
|
||||||
|
|||||||
@@ -11,20 +11,42 @@ import { getGitAuthor } from "../utils/git_author";
|
|||||||
export function getDyadWriteTags(fullResponse: string): {
|
export function getDyadWriteTags(fullResponse: string): {
|
||||||
path: string;
|
path: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
description?: string;
|
||||||
}[] {
|
}[] {
|
||||||
const dyadWriteRegex =
|
const dyadWriteRegex = /<dyad-write([^>]*)>([\s\S]*?)<\/dyad-write>/gi;
|
||||||
/<dyad-write path="([^"]+)"[^>]*>([\s\S]*?)<\/dyad-write>/g;
|
const pathRegex = /path="([^"]+)"/;
|
||||||
|
const descriptionRegex = /description="([^"]+)"/;
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
const tags: { path: string; content: string }[] = [];
|
const tags: { path: string; content: string; description?: string }[] = [];
|
||||||
|
|
||||||
while ((match = dyadWriteRegex.exec(fullResponse)) !== null) {
|
while ((match = dyadWriteRegex.exec(fullResponse)) !== null) {
|
||||||
const content = match[2].trim().split("\n");
|
const attributesString = match[1];
|
||||||
if (content[0].startsWith("```")) {
|
let content = match[2].trim();
|
||||||
content.shift();
|
|
||||||
|
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 (content[content.length - 1].startsWith("```")) {
|
if (contentLines[contentLines.length - 1]?.startsWith("```")) {
|
||||||
content.pop();
|
contentLines.pop();
|
||||||
|
}
|
||||||
|
content = contentLines.join("\n");
|
||||||
|
|
||||||
|
tags.push({ path, content, description });
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"Found <dyad-write> tag without a valid 'path' attribute:",
|
||||||
|
match[0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
tags.push({ path: match[1], content: content.join("\n") });
|
|
||||||
}
|
}
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
@@ -65,6 +87,16 @@ export function getDyadAddDependencyTags(fullResponse: string): string[] {
|
|||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDyadChatSummaryTag(fullResponse: string): string | null {
|
||||||
|
const dyadChatSummaryRegex =
|
||||||
|
/<dyad-chat-summary>([\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(
|
export async function processFullResponseActions(
|
||||||
fullResponse: string,
|
fullResponse: string,
|
||||||
chatId: number,
|
chatId: number,
|
||||||
@@ -206,6 +238,7 @@ export async function processFullResponseActions(
|
|||||||
if (deletedFiles.length > 0)
|
if (deletedFiles.length > 0)
|
||||||
changes.push(`deleted ${deletedFiles.length} file(s)`);
|
changes.push(`deleted ${deletedFiles.length} file(s)`);
|
||||||
|
|
||||||
|
// Use chat summary, if provided, or default for commit message
|
||||||
await git.commit({
|
await git.commit({
|
||||||
fs,
|
fs,
|
||||||
dir: appPath,
|
dir: appPath,
|
||||||
|
|||||||
Reference in New Issue
Block a user