Load (canned) proposal from IPC

This commit is contained in:
Will Chen
2025-04-18 11:11:58 -07:00
parent 7aec3ef455
commit 4e0f93d21c
8 changed files with 191 additions and 45 deletions

View File

@@ -23,6 +23,15 @@ import { useLoadApp } from "@/hooks/useLoadApp";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { useProposal } from "@/hooks/useProposal";
import { Proposal } from "@/lib/schemas";
interface ChatInputActionsProps {
proposal: Proposal;
onApprove: () => void;
onReject: () => void;
isApprovable: boolean; // Can be used to enable/disable buttons
}
interface ChatInputProps { interface ChatInputProps {
chatId?: number; chatId?: number;
@@ -38,6 +47,13 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
const [selectedAppId] = useAtom(selectedAppIdAtom); const [selectedAppId] = useAtom(selectedAppIdAtom);
const [showError, setShowError] = useState(true); const [showError, setShowError] = useState(true);
// Use the hook to fetch the proposal
const {
proposal,
isLoading: isProposalLoading,
error: proposalError,
} = useProposal(chatId);
const adjustHeight = () => { const adjustHeight = () => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
if (textarea) { if (textarea) {
@@ -86,6 +102,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
setShowError(false); setShowError(false);
}; };
const handleApprove = () => {
console.log("Approve clicked");
// Add approve logic here
};
const handleReject = () => {
console.log("Reject clicked");
// Add reject logic here
};
if (!settings) { if (!settings) {
return null; // Or loading state return null; // Or loading state
} }
@@ -105,9 +131,28 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
</div> </div>
</div> </div>
)} )}
{/* Display loading or error state for proposal */}
{isProposalLoading && (
<div className="p-4 text-sm text-muted-foreground">
Loading proposal...
</div>
)}
{proposalError && (
<div className="p-4 text-sm text-red-600">
Error loading proposal: {proposalError}
</div>
)}
<div className="p-4"> <div className="p-4">
<div className="flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm"> <div className="flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm">
<ChatInputActions /> {/* Only render ChatInputActions if proposal is loaded */}
{proposal && (
<ChatInputActions
proposal={proposal}
onApprove={handleApprove}
onReject={handleReject}
isApprovable={!isProposalLoading && !!proposal}
/>
)}
<div className="flex items-start space-x-2 "> <div className="flex items-start space-x-2 ">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
@@ -151,47 +196,16 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
); );
} }
function ChatInputActions() { // Update ChatInputActions to accept props
function ChatInputActions({
proposal,
onApprove,
onReject,
isApprovable,
}: ChatInputActionsProps) {
const [autoApprove, setAutoApprove] = useState(false); const [autoApprove, setAutoApprove] = useState(false);
const [isDetailsVisible, setIsDetailsVisible] = useState(false); const [isDetailsVisible, setIsDetailsVisible] = useState(false);
const handleApprove = () => {
console.log("Approve clicked");
// Add approve logic here
};
const handleReject = () => {
console.log("Reject clicked");
// Add reject logic here
};
// Placeholder data
const 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.",
},
];
const 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.",
},
];
return ( return (
<div className="border-b border-border"> <div className="border-b border-border">
<div className="p-2"> <div className="p-2">
@@ -206,9 +220,9 @@ function ChatInputActions() {
) : ( ) : (
<ChevronDown size={16} className="mr-1" /> <ChevronDown size={16} className="mr-1" />
)} )}
Review: foo bar changes Review: {proposal.title}
</button> </button>
{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">
Security risks found Security risks found
</span> </span>
@@ -221,7 +235,8 @@ function ChatInputActions() {
className="px-8" className="px-8"
size="sm" size="sm"
variant="outline" variant="outline"
onClick={handleApprove} onClick={onApprove}
disabled={!isApprovable}
> >
<Check size={16} className="mr-1" /> <Check size={16} className="mr-1" />
Approve Approve
@@ -230,7 +245,8 @@ function ChatInputActions() {
className="px-8" className="px-8"
size="sm" size="sm"
variant="outline" variant="outline"
onClick={handleReject} onClick={onReject}
disabled={!isApprovable}
> >
<X size={16} className="mr-1" /> <X size={16} className="mr-1" />
Reject Reject
@@ -256,7 +272,7 @@ function ChatInputActions() {
<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">
{securityRisks.map((risk, index) => ( {proposal.securityRisks.map((risk, index) => (
<li key={index} className="flex items-start space-x-2"> <li key={index} className="flex items-start space-x-2">
{risk.type === "warning" ? ( {risk.type === "warning" ? (
<AlertTriangle <AlertTriangle
@@ -281,7 +297,7 @@ function ChatInputActions() {
<div> <div>
<h4 className="font-semibold mb-1">Files Changed</h4> <h4 className="font-semibold mb-1">Files Changed</h4>
<ul className="space-y-1"> <ul className="space-y-1">
{filesChanged.map((file, index) => ( {proposal.filesChanged.map((file, index) => (
<li key={index} className="flex items-center space-x-2"> <li key={index} className="flex items-center space-x-2">
<FileText <FileText
size={16} size={16}

View File

@@ -34,6 +34,9 @@ export const messages = sqliteTable("messages", {
.references(() => chats.id, { onDelete: "cascade" }), .references(() => chats.id, { onDelete: "cascade" }),
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", {
enum: ["approved", "rejected", "pending"],
}),
createdAt: integer("created_at", { mode: "timestamp" }) createdAt: integer("created_at", { mode: "timestamp" })
.notNull() .notNull()
.default(sql`(unixepoch())`), .default(sql`(unixepoch())`),

44
src/hooks/useProposal.ts Normal file
View File

@@ -0,0 +1,44 @@
import { useState, useEffect } 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<Proposal | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (chatId === undefined) {
setProposal(null);
setIsLoading(false);
setError(null);
return;
}
const fetchProposal = async () => {
setIsLoading(true);
setError(null);
try {
const fetchedProposal = await IpcClient.getInstance().getProposal(
chatId
);
setProposal(fetchedProposal);
} catch (err: any) {
console.error("Error fetching proposal:", err);
setError(err.message || "Failed to fetch proposal");
setProposal(null); // Clear proposal on error
} finally {
setIsLoading(false);
}
};
fetchProposal();
// Cleanup function if needed (e.g., for aborting requests)
// return () => {
// // Abort logic here
// };
}, [chatId]); // Re-run effect if chatId changes
return { proposal, isLoading, error };
}

View File

@@ -0,0 +1,47 @@
import { ipcMain, type IpcMainInvokeEvent } from "electron";
import type { Proposal } from "@/lib/schemas";
// 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.",
},
],
};
const getProposalHandler = async (
_event: IpcMainInvokeEvent,
{ chatId }: { chatId: number }
): Promise<Proposal> => {
console.log(`IPC: get-proposal called for chatId: ${chatId}`);
// Simulate async operation
await new Promise((resolve) => setTimeout(resolve, 500)); // 500ms delay
return placeholderProposal;
};
// Function to register proposal-related handlers
export function registerProposalHandlers() {
ipcMain.handle("get-proposal", getProposalHandler);
console.log("Registered proposal IPC handlers");
}

View File

@@ -17,6 +17,7 @@ import type {
NodeSystemInfo, NodeSystemInfo,
Version, Version,
} from "./ipc_types"; } from "./ipc_types";
import type { Proposal } from "@/lib/schemas";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
export interface ChatStreamCallbacks { export interface ChatStreamCallbacks {
@@ -614,6 +615,18 @@ export class IpcClient {
} }
} }
// Get proposal details
public async getProposal(chatId: number): Promise<Proposal> {
try {
const data = await this.ipcRenderer.invoke("get-proposal", { chatId });
// Assuming the main process returns data matching the Proposal interface
return data as Proposal;
} catch (error) {
showError(error);
throw error;
}
}
// Example methods for listening to events (if needed) // Example methods for listening to events (if needed)
// public on(channel: string, func: (...args: any[]) => void): void { // public on(channel: string, func: (...args: any[]) => void): void {
} }

View File

@@ -6,6 +6,7 @@ import { registerShellHandlers } from "./handlers/shell_handler";
import { registerDependencyHandlers } from "./handlers/dependency_handlers"; import { registerDependencyHandlers } from "./handlers/dependency_handlers";
import { registerGithubHandlers } from "./handlers/github_handlers"; import { registerGithubHandlers } from "./handlers/github_handlers";
import { registerNodeHandlers } from "./handlers/node_handlers"; import { registerNodeHandlers } from "./handlers/node_handlers";
import { registerProposalHandlers } from "./handlers/proposal_handlers";
export function registerIpcHandlers() { export function registerIpcHandlers() {
// Register all IPC handlers by category // Register all IPC handlers by category
@@ -17,4 +18,5 @@ export function registerIpcHandlers() {
registerDependencyHandlers(); registerDependencyHandlers();
registerGithubHandlers(); registerGithubHandlers();
registerNodeHandlers(); registerNodeHandlers();
registerProposalHandlers();
} }

View File

@@ -97,3 +97,23 @@ export const UserSettingsSchema = z.object({
* Type derived from the UserSettingsSchema * Type derived from the UserSettingsSchema
*/ */
export type UserSettings = z.infer<typeof UserSettingsSchema>; export type UserSettings = z.infer<typeof UserSettingsSchema>;
// Define interfaces for the props
export interface SecurityRisk {
type: "warning" | "danger";
title: string;
description: string;
}
export interface FileChange {
name: string;
path: string;
summary: string;
}
// New Proposal interface
export interface Proposal {
title: string;
securityRisks: SecurityRisk[];
filesChanged: FileChange[];
}

View File

@@ -38,6 +38,7 @@ const validInvokeChannels = [
"github:push", "github:push",
"get-app-version", "get-app-version",
"reload-env-path", "reload-env-path",
"get-proposal",
] as const; ] as const;
// Add valid receive channels // Add valid receive channels