basic suggested action scaffolding

This commit is contained in:
Will Chen
2025-04-18 13:03:19 -07:00
parent b2e3631a29
commit 639b3a320c
10 changed files with 149 additions and 86 deletions

View File

@@ -17,3 +17,5 @@ export const userSettingsAtom = atom<UserSettings | null>(null);
// Atom for storing allow-listed environment variables // Atom for storing allow-listed environment variables
export const envVarsAtom = atom<Record<string, string | undefined>>({}); export const envVarsAtom = atom<Record<string, string | undefined>>({});
export const previewPanelKeyAtom = atom<number>(0);

View File

@@ -1,4 +1,4 @@
import { atom } from "jotai"; import { atom } from "jotai";
import type { Proposal, ProposalResult } from "@/lib/schemas"; import type { CodeProposal, ProposalResult } from "@/lib/schemas";
export const proposalResultAtom = atom<ProposalResult | null>(null); export const proposalResultAtom = atom<ProposalResult | null>(null);

View File

@@ -25,17 +25,16 @@ 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 { 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 type { Message } from "@/ipc/ipc_types";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
interface ChatInputActionsProps { import { useRunApp } from "@/hooks/useRunApp";
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
}
export function ChatInput({ chatId }: { chatId?: number }) { export function ChatInput({ chatId }: { chatId?: number }) {
const [inputValue, setInputValue] = useAtom(chatInputValueAtom); const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
@@ -52,12 +51,12 @@ export function ChatInput({ chatId }: { chatId?: number }) {
// Use the hook to fetch the proposal // Use the hook to fetch the proposal
const { const {
proposal, proposalResult,
messageId,
isLoading: isProposalLoading, isLoading: isProposalLoading,
error: proposalError, error: proposalError,
refreshProposal, refreshProposal,
} = useProposal(chatId); } = useProposal(chatId);
const { proposal, chatId: proposalChatId, messageId } = proposalResult ?? {};
const adjustHeight = () => { const adjustHeight = () => {
const textarea = textareaRef.current; const textarea = textareaRef.current;
@@ -205,9 +204,9 @@ export function ChatInput({ chatId }: { chatId?: number }) {
</div> </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 border border-border rounded-lg bg-(--background-lighter) shadow-sm">
{/* Only render ChatInputActions if proposal is loaded */} {/* Only render ChatInputActions if proposal is loaded */}
{proposal && ( {proposal && proposalResult?.chatId === chatId && (
<ChatInputActions <ChatInputActions
proposal={proposal} proposal={proposal}
onApprove={handleApprove} onApprove={handleApprove}
@@ -267,6 +266,47 @@ export function ChatInput({ chatId }: { chatId?: number }) {
); );
} }
function mapActionToButton(action: SuggestedAction) {
const { restartApp } = useRunApp();
switch (action.id) {
case "restart-app":
return (
<Button
variant="outline"
size="sm"
key={action.id}
onClick={restartApp}
>
Restart App
</Button>
);
default:
console.error(`Unsupported action: ${action.id}`);
return (
<Button variant="outline" size="sm" disabled key={action.id}>
Unsupported: {action.id}
</Button>
);
}
}
function ActionProposalActions({ proposal }: { proposal: ActionProposal }) {
return (
<div className="p-2 pb-0">
{proposal.actions.map((action) => mapActionToButton(action))}
</div>
);
}
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 // Update ChatInputActions to accept props
function ChatInputActions({ function ChatInputActions({
proposal, proposal,
@@ -279,6 +319,9 @@ function ChatInputActions({
const [autoApprove, setAutoApprove] = useState(false); const [autoApprove, setAutoApprove] = useState(false);
const [isDetailsVisible, setIsDetailsVisible] = useState(false); const [isDetailsVisible, setIsDetailsVisible] = useState(false);
if (proposal.type === "action-proposal") {
return <ActionProposalActions proposal={proposal}></ActionProposalActions>;
}
return ( return (
<div className="border-b border-border"> <div className="border-b border-border">
<div className="p-2"> <div className="p-2">

View File

@@ -1,5 +1,9 @@
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { previewModeAtom, selectedAppIdAtom } from "../../atoms/appAtoms"; import {
previewModeAtom,
previewPanelKeyAtom,
selectedAppIdAtom,
} from "../../atoms/appAtoms";
import { useLoadApp } from "@/hooks/useLoadApp"; import { useLoadApp } from "@/hooks/useLoadApp";
import { CodeView } from "./CodeView"; import { CodeView } from "./CodeView";
import { PreviewIframe } from "./PreviewIframe"; import { PreviewIframe } from "./PreviewIframe";
@@ -99,14 +103,11 @@ export function PreviewPanel() {
const [isConsoleOpen, setIsConsoleOpen] = useState(false); const [isConsoleOpen, setIsConsoleOpen] = useState(false);
const { runApp, stopApp, restartApp, error, loading, app } = useRunApp(); const { runApp, stopApp, restartApp, error, loading, app } = useRunApp();
const runningAppIdRef = useRef<number | null>(null); const runningAppIdRef = useRef<number | null>(null);
const [key, setKey] = useState(0); const key = useAtomValue(previewPanelKeyAtom);
const handleRestart = useCallback(() => { const handleRestart = useCallback(() => {
if (selectedAppId !== null) { restartApp();
restartApp(selectedAppId); }, [restartApp]);
setKey((prevKey) => prevKey + 1);
}
}, [selectedAppId, restartApp]);
useEffect(() => { useEffect(() => {
const previousAppId = runningAppIdRef.current; const previousAppId = runningAppIdRef.current;

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { IpcClient } from "@/ipc/ipc_client"; 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 { proposalResultAtom } from "@/atoms/proposalAtoms";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
export function useProposal(chatId?: number | undefined) { export function useProposal(chatId?: number | undefined) {
@@ -8,10 +8,7 @@ export function useProposal(chatId?: number | undefined) {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const fetchProposal = useCallback( const fetchProposal = useCallback(async () => {
async (innerChatId?: number) => {
chatId = chatId ?? innerChatId;
console.log("fetching proposal for chatId", chatId);
if (chatId === undefined) { if (chatId === undefined) {
setProposalResult(null); setProposalResult(null);
setIsLoading(false); setIsLoading(false);
@@ -39,9 +36,7 @@ export function useProposal(chatId?: number | undefined) {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, }, [chatId]); // Depend on chatId
[chatId]
); // Depend on chatId
useEffect(() => { useEffect(() => {
fetchProposal(); fetchProposal();
@@ -52,18 +47,10 @@ export function useProposal(chatId?: number | undefined) {
// }; // };
}, [fetchProposal]); // Re-run effect if fetchProposal changes (due to chatId change) }, [fetchProposal]); // Re-run effect if fetchProposal changes (due to chatId change)
const refreshProposal = useCallback(
(chatId?: number) => {
fetchProposal(chatId);
},
[fetchProposal]
);
return { return {
proposal: proposalResult?.proposal ?? null, proposalResult: proposalResult,
messageId: proposalResult?.messageId,
isLoading, isLoading,
error, error,
refreshProposal, // Expose the refresh function refreshProposal: fetchProposal, // Expose the refresh function
}; };
} }

View File

@@ -1,7 +1,13 @@
import { useState, useCallback } from "react"; import { useState, useCallback } from "react";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { appOutputAtom, appUrlAtom, currentAppAtom } from "@/atoms/appAtoms"; import {
import { useAtom, useSetAtom } from "jotai"; appOutputAtom,
appUrlAtom,
currentAppAtom,
previewPanelKeyAtom,
selectedAppIdAtom,
} from "@/atoms/appAtoms";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { App } from "@/ipc/ipc_types"; import { App } from "@/ipc/ipc_types";
export function useRunApp() { export function useRunApp() {
@@ -10,7 +16,8 @@ export function useRunApp() {
const [app, setApp] = useAtom(currentAppAtom); const [app, setApp] = useAtom(currentAppAtom);
const setAppOutput = useSetAtom(appOutputAtom); const setAppOutput = useSetAtom(appOutputAtom);
const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom); const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
const appId = useAtomValue(selectedAppIdAtom);
const runApp = useCallback(async (appId: number) => { const runApp = useCallback(async (appId: number) => {
setLoading(true); setLoading(true);
try { try {
@@ -63,7 +70,10 @@ export function useRunApp() {
} }
}, []); }, []);
const restartApp = useCallback(async (appId: number) => { const restartApp = useCallback(async () => {
if (appId === null) {
return;
}
setLoading(true); setLoading(true);
try { try {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
@@ -91,6 +101,7 @@ export function useRunApp() {
console.error(`Error restarting app ${appId}:`, error); console.error(`Error restarting app ${appId}:`, error);
setError(error instanceof Error ? error : new Error(String(error))); setError(error instanceof Error ? error : new Error(String(error)));
} finally { } finally {
setPreviewPanelKey((prevKey) => prevKey + 1);
setLoading(false); setLoading(false);
} }
}, []); }, []);

View File

@@ -16,6 +16,7 @@ import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadVersions } from "./useLoadVersions"; import { useLoadVersions } from "./useLoadVersions";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { useProposal } from "./useProposal"; import { useProposal } from "./useProposal";
import { useSearch } from "@tanstack/react-router";
export function getRandomString() { export function getRandomString() {
return Math.random().toString(36).substring(2, 15); return Math.random().toString(36).substring(2, 15);
@@ -31,7 +32,8 @@ export function useStreamChat() {
const { refreshApp } = useLoadApp(selectedAppId); const { refreshApp } = useLoadApp(selectedAppId);
const setStreamCount = useSetAtom(chatStreamCountAtom); const setStreamCount = useSetAtom(chatStreamCountAtom);
const { refreshVersions } = useLoadVersions(selectedAppId); const { refreshVersions } = useLoadVersions(selectedAppId);
const { refreshProposal } = useProposal(); const { id: chatId } = useSearch({ from: "/chat" });
const { refreshProposal } = useProposal(chatId);
const streamMessage = useCallback( const streamMessage = useCallback(
async ({ async ({
prompt, prompt,
@@ -91,7 +93,7 @@ export function useStreamChat() {
if (response.updatedFiles) { if (response.updatedFiles) {
setIsPreviewOpen(true); setIsPreviewOpen(true);
} }
refreshProposal(chatId); refreshProposal();
// Keep the same as below // Keep the same as below
setIsStreaming(false); setIsStreaming(false);

View File

@@ -1,5 +1,5 @@
import { ipcMain, type IpcMainInvokeEvent } from "electron"; import { ipcMain, type IpcMainInvokeEvent } from "electron";
import type { Proposal } from "@/lib/schemas"; import type { CodeProposal, ProposalResult } from "@/lib/schemas";
import { db } from "../../db"; import { db } from "../../db";
import { messages } from "../../db/schema"; import { messages } from "../../db/schema";
import { desc, eq, and, Update } from "drizzle-orm"; import { desc, eq, and, Update } from "drizzle-orm";
@@ -19,13 +19,6 @@ interface ParsedProposal {
title: string; title: string;
files: string[]; files: string[];
} }
// Define return type for getProposalHandler
interface ProposalResult {
proposal: Proposal;
messageId: number;
}
function isParsedProposal(obj: any): obj is ParsedProposal { function isParsedProposal(obj: any): obj is ParsedProposal {
return ( return (
obj && obj &&
@@ -54,12 +47,23 @@ const getProposalHandler = async (
}, },
}); });
if ( if (latestAssistantMessage?.approvalState === "rejected") {
latestAssistantMessage?.approvalState === "approved" ||
latestAssistantMessage?.approvalState === "rejected"
) {
return null; 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) { if (latestAssistantMessage?.content && latestAssistantMessage.id) {
const messageId = latestAssistantMessage.id; // Get the message 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 // Check if we have enough information to create a proposal
if (proposalTitle || proposalFiles.length > 0) { 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 // Use parsed title or a default title if summary tag is missing but write tags exist
title: proposalTitle ?? "Proposed File Changes", title: proposalTitle ?? "Proposed File Changes",
securityRisks: [], // Keep empty securityRisks: [], // Keep empty
@@ -85,7 +90,7 @@ const getProposalHandler = async (
})), })),
}; };
console.log("Generated proposal on the fly:", proposal); console.log("Generated proposal on the fly:", proposal);
return { proposal, messageId }; // Return proposal and messageId return { proposal, chatId, messageId }; // Return proposal and messageId
} else { } else {
console.log( console.log(
"No relevant tags found in the latest assistant message content." "No relevant tags found in the latest assistant message content."

View File

@@ -17,7 +17,7 @@ import type {
Message, Message,
Version, Version,
} from "./ipc_types"; } from "./ipc_types";
import type { Proposal, ProposalResult } from "@/lib/schemas"; import type { CodeProposal, ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
export interface ChatStreamCallbacks { export interface ChatStreamCallbacks {

View File

@@ -111,14 +111,26 @@ export interface FileChange {
summary: string; summary: string;
} }
// New Proposal interface export interface CodeProposal {
export interface Proposal { type: "code-proposal";
title: string; title: string;
securityRisks: SecurityRisk[]; securityRisks: SecurityRisk[];
filesChanged: FileChange[]; filesChanged: FileChange[];
} }
export interface SuggestedAction {
id: "restart-app";
}
export interface ActionProposal {
type: "action-proposal";
actions: SuggestedAction[];
}
export type Proposal = CodeProposal | ActionProposal;
export interface ProposalResult { export interface ProposalResult {
proposal: Proposal; proposal: Proposal;
chatId: number;
messageId: number; messageId: number;
} }