From 639b3a320c6105ac51380035dd50d4746b53132e Mon Sep 17 00:00:00 2001 From: Will Chen Date: Fri, 18 Apr 2025 13:03:19 -0700 Subject: [PATCH] basic suggested action scaffolding --- src/atoms/appAtoms.ts | 2 + src/atoms/proposalAtoms.ts | 2 +- src/components/chat/ChatInput.tsx | 69 ++++++++++++++---- src/components/preview_panel/PreviewPanel.tsx | 15 ++-- src/hooks/useProposal.ts | 71 ++++++++----------- src/hooks/useRunApp.ts | 19 +++-- src/hooks/useStreamChat.ts | 6 +- src/ipc/handlers/proposal_handlers.ts | 33 +++++---- src/ipc/ipc_client.ts | 2 +- src/lib/schemas.ts | 16 ++++- 10 files changed, 149 insertions(+), 86 deletions(-) diff --git a/src/atoms/appAtoms.ts b/src/atoms/appAtoms.ts index 723bf59..a18c456 100644 --- a/src/atoms/appAtoms.ts +++ b/src/atoms/appAtoms.ts @@ -17,3 +17,5 @@ export const userSettingsAtom = atom(null); // Atom for storing allow-listed environment variables export const envVarsAtom = atom>({}); + +export const previewPanelKeyAtom = atom(0); diff --git a/src/atoms/proposalAtoms.ts b/src/atoms/proposalAtoms.ts index d9f19ae..203a649 100644 --- a/src/atoms/proposalAtoms.ts +++ b/src/atoms/proposalAtoms.ts @@ -1,4 +1,4 @@ import { atom } from "jotai"; -import type { Proposal, ProposalResult } from "@/lib/schemas"; +import type { CodeProposal, ProposalResult } from "@/lib/schemas"; export const proposalResultAtom = atom(null); diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index f347c71..b8c1a94 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -25,17 +25,16 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useProposal } from "@/hooks/useProposal"; -import { Proposal } from "@/lib/schemas"; +import { + CodeProposal, + ActionProposal, + Proposal, + SuggestedAction, + ProposalResult, +} from "@/lib/schemas"; import type { Message } from "@/ipc/ipc_types"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; -interface ChatInputActionsProps { - proposal: Proposal; - onApprove: () => void; - onReject: () => void; - isApprovable: boolean; // Can be used to enable/disable buttons - isApproving: boolean; // State for approving - isRejecting: boolean; // State for rejecting -} +import { useRunApp } from "@/hooks/useRunApp"; export function ChatInput({ chatId }: { chatId?: number }) { const [inputValue, setInputValue] = useAtom(chatInputValueAtom); @@ -52,12 +51,12 @@ export function ChatInput({ chatId }: { chatId?: number }) { // Use the hook to fetch the proposal const { - proposal, - messageId, + proposalResult, isLoading: isProposalLoading, error: proposalError, refreshProposal, } = useProposal(chatId); + const { proposal, chatId: proposalChatId, messageId } = proposalResult ?? {}; const adjustHeight = () => { const textarea = textareaRef.current; @@ -205,9 +204,9 @@ export function ChatInput({ chatId }: { chatId?: number }) { )}
-
+
{/* Only render ChatInputActions if proposal is loaded */} - {proposal && ( + {proposal && proposalResult?.chatId === chatId && ( + Restart App + + ); + default: + console.error(`Unsupported action: ${action.id}`); + return ( + + ); + } +} + +function ActionProposalActions({ proposal }: { proposal: ActionProposal }) { + return ( +
+ {proposal.actions.map((action) => mapActionToButton(action))} +
+ ); +} + +interface ChatInputActionsProps { + proposal: Proposal; + onApprove: () => void; + onReject: () => void; + isApprovable: boolean; // Can be used to enable/disable buttons + isApproving: boolean; // State for approving + isRejecting: boolean; // State for rejecting +} + // Update ChatInputActions to accept props function ChatInputActions({ proposal, @@ -279,6 +319,9 @@ function ChatInputActions({ const [autoApprove, setAutoApprove] = useState(false); const [isDetailsVisible, setIsDetailsVisible] = useState(false); + if (proposal.type === "action-proposal") { + return ; + } return (
diff --git a/src/components/preview_panel/PreviewPanel.tsx b/src/components/preview_panel/PreviewPanel.tsx index ce3da4f..736bfd2 100644 --- a/src/components/preview_panel/PreviewPanel.tsx +++ b/src/components/preview_panel/PreviewPanel.tsx @@ -1,5 +1,9 @@ import { useAtom, useAtomValue } from "jotai"; -import { previewModeAtom, selectedAppIdAtom } from "../../atoms/appAtoms"; +import { + previewModeAtom, + previewPanelKeyAtom, + selectedAppIdAtom, +} from "../../atoms/appAtoms"; import { useLoadApp } from "@/hooks/useLoadApp"; import { CodeView } from "./CodeView"; import { PreviewIframe } from "./PreviewIframe"; @@ -99,14 +103,11 @@ export function PreviewPanel() { const [isConsoleOpen, setIsConsoleOpen] = useState(false); const { runApp, stopApp, restartApp, error, loading, app } = useRunApp(); const runningAppIdRef = useRef(null); - const [key, setKey] = useState(0); + const key = useAtomValue(previewPanelKeyAtom); const handleRestart = useCallback(() => { - if (selectedAppId !== null) { - restartApp(selectedAppId); - setKey((prevKey) => prevKey + 1); - } - }, [selectedAppId, restartApp]); + restartApp(); + }, [restartApp]); useEffect(() => { const previousAppId = runningAppIdRef.current; diff --git a/src/hooks/useProposal.ts b/src/hooks/useProposal.ts index a10577b..36c35f7 100644 --- a/src/hooks/useProposal.ts +++ b/src/hooks/useProposal.ts @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react"; import { IpcClient } from "@/ipc/ipc_client"; -import type { Proposal, ProposalResult } from "@/lib/schemas"; // Import Proposal type +import type { CodeProposal, ProposalResult } from "@/lib/schemas"; // Import Proposal type import { proposalResultAtom } from "@/atoms/proposalAtoms"; import { useAtom } from "jotai"; export function useProposal(chatId?: number | undefined) { @@ -8,40 +8,35 @@ export function useProposal(chatId?: number | undefined) { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchProposal = useCallback( - async (innerChatId?: number) => { - chatId = chatId ?? innerChatId; - console.log("fetching proposal for chatId", chatId); - if (chatId === undefined) { - setProposalResult(null); - setIsLoading(false); - setError(null); - return; - } - setIsLoading(true); + const fetchProposal = useCallback(async () => { + if (chatId === undefined) { + setProposalResult(null); + setIsLoading(false); setError(null); - setProposalResult(null); // Reset on new fetch - try { - // Type assertion might be needed depending on how IpcClient is typed - const result = (await IpcClient.getInstance().getProposal( - chatId - )) as ProposalResult | null; + return; + } + setIsLoading(true); + setError(null); + setProposalResult(null); // Reset on new fetch + try { + // Type assertion might be needed depending on how IpcClient is typed + const result = (await IpcClient.getInstance().getProposal( + chatId + )) as ProposalResult | null; - if (result) { - setProposalResult(result); - } else { - setProposalResult(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"); - setProposalResult(null); // Clear proposal data on error - } finally { - setIsLoading(false); + if (result) { + setProposalResult(result); + } else { + setProposalResult(null); // Explicitly set to null if IPC returns null } - }, - [chatId] - ); // Depend on chatId + } catch (err: any) { + console.error("Error fetching proposal:", err); + setError(err.message || "Failed to fetch proposal"); + setProposalResult(null); // Clear proposal data on error + } finally { + setIsLoading(false); + } + }, [chatId]); // Depend on chatId useEffect(() => { fetchProposal(); @@ -52,18 +47,10 @@ export function useProposal(chatId?: number | undefined) { // }; }, [fetchProposal]); // Re-run effect if fetchProposal changes (due to chatId change) - const refreshProposal = useCallback( - (chatId?: number) => { - fetchProposal(chatId); - }, - [fetchProposal] - ); - return { - proposal: proposalResult?.proposal ?? null, - messageId: proposalResult?.messageId, + proposalResult: proposalResult, isLoading, error, - refreshProposal, // Expose the refresh function + refreshProposal: fetchProposal, // Expose the refresh function }; } diff --git a/src/hooks/useRunApp.ts b/src/hooks/useRunApp.ts index 79229de..f854c4f 100644 --- a/src/hooks/useRunApp.ts +++ b/src/hooks/useRunApp.ts @@ -1,7 +1,13 @@ import { useState, useCallback } from "react"; import { IpcClient } from "@/ipc/ipc_client"; -import { appOutputAtom, appUrlAtom, currentAppAtom } from "@/atoms/appAtoms"; -import { useAtom, useSetAtom } from "jotai"; +import { + appOutputAtom, + appUrlAtom, + currentAppAtom, + previewPanelKeyAtom, + selectedAppIdAtom, +} from "@/atoms/appAtoms"; +import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { App } from "@/ipc/ipc_types"; export function useRunApp() { @@ -10,7 +16,8 @@ export function useRunApp() { const [app, setApp] = useAtom(currentAppAtom); const setAppOutput = useSetAtom(appOutputAtom); const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom); - + const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom); + const appId = useAtomValue(selectedAppIdAtom); const runApp = useCallback(async (appId: number) => { setLoading(true); try { @@ -63,7 +70,10 @@ export function useRunApp() { } }, []); - const restartApp = useCallback(async (appId: number) => { + const restartApp = useCallback(async () => { + if (appId === null) { + return; + } setLoading(true); try { const ipcClient = IpcClient.getInstance(); @@ -91,6 +101,7 @@ export function useRunApp() { console.error(`Error restarting app ${appId}:`, error); setError(error instanceof Error ? error : new Error(String(error))); } finally { + setPreviewPanelKey((prevKey) => prevKey + 1); setLoading(false); } }, []); diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index 203de74..6e5e666 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -16,6 +16,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { useLoadVersions } from "./useLoadVersions"; import { showError } from "@/lib/toast"; import { useProposal } from "./useProposal"; +import { useSearch } from "@tanstack/react-router"; export function getRandomString() { return Math.random().toString(36).substring(2, 15); @@ -31,7 +32,8 @@ export function useStreamChat() { const { refreshApp } = useLoadApp(selectedAppId); const setStreamCount = useSetAtom(chatStreamCountAtom); const { refreshVersions } = useLoadVersions(selectedAppId); - const { refreshProposal } = useProposal(); + const { id: chatId } = useSearch({ from: "/chat" }); + const { refreshProposal } = useProposal(chatId); const streamMessage = useCallback( async ({ prompt, @@ -91,7 +93,7 @@ export function useStreamChat() { if (response.updatedFiles) { setIsPreviewOpen(true); } - refreshProposal(chatId); + refreshProposal(); // Keep the same as below setIsStreaming(false); diff --git a/src/ipc/handlers/proposal_handlers.ts b/src/ipc/handlers/proposal_handlers.ts index 3219a74..ba763a9 100644 --- a/src/ipc/handlers/proposal_handlers.ts +++ b/src/ipc/handlers/proposal_handlers.ts @@ -1,5 +1,5 @@ import { ipcMain, type IpcMainInvokeEvent } from "electron"; -import type { Proposal } from "@/lib/schemas"; +import type { CodeProposal, ProposalResult } from "@/lib/schemas"; import { db } from "../../db"; import { messages } from "../../db/schema"; import { desc, eq, and, Update } from "drizzle-orm"; @@ -19,13 +19,6 @@ interface ParsedProposal { title: string; files: string[]; } - -// Define return type for getProposalHandler -interface ProposalResult { - proposal: Proposal; - messageId: number; -} - function isParsedProposal(obj: any): obj is ParsedProposal { return ( obj && @@ -54,12 +47,23 @@ const getProposalHandler = async ( }, }); - if ( - latestAssistantMessage?.approvalState === "approved" || - latestAssistantMessage?.approvalState === "rejected" - ) { + if (latestAssistantMessage?.approvalState === "rejected") { return null; } + if (latestAssistantMessage?.approvalState === "approved") { + return { + proposal: { + type: "action-proposal", + actions: [ + { + id: "restart-app", + }, + ], + }, + chatId: chatId, + messageId: latestAssistantMessage.id, + }; + } if (latestAssistantMessage?.content && latestAssistantMessage.id) { const messageId = latestAssistantMessage.id; // Get the message ID @@ -74,7 +78,8 @@ const getProposalHandler = async ( // Check if we have enough information to create a proposal if (proposalTitle || proposalFiles.length > 0) { - const proposal: Proposal = { + const proposal: CodeProposal = { + type: "code-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 @@ -85,7 +90,7 @@ const getProposalHandler = async ( })), }; console.log("Generated proposal on the fly:", proposal); - return { proposal, messageId }; // Return proposal and messageId + return { proposal, chatId, messageId }; // Return proposal and messageId } else { console.log( "No relevant tags found in the latest assistant message content." diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index fcf55a4..00cc47c 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -17,7 +17,7 @@ import type { Message, Version, } from "./ipc_types"; -import type { Proposal, ProposalResult } from "@/lib/schemas"; +import type { CodeProposal, ProposalResult } from "@/lib/schemas"; import { showError } from "@/lib/toast"; export interface ChatStreamCallbacks { diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 75cb620..dd209e6 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -111,14 +111,26 @@ export interface FileChange { summary: string; } -// New Proposal interface -export interface Proposal { +export interface CodeProposal { + type: "code-proposal"; title: string; securityRisks: SecurityRisk[]; filesChanged: FileChange[]; } +export interface SuggestedAction { + id: "restart-app"; +} + +export interface ActionProposal { + type: "action-proposal"; + actions: SuggestedAction[]; +} + +export type Proposal = CodeProposal | ActionProposal; + export interface ProposalResult { proposal: Proposal; + chatId: number; messageId: number; }