diff --git a/apps/web/src/agent/orchestrator.ts b/apps/web/src/agent/orchestrator.ts index 0beb96e2..dc73dab2 100644 --- a/apps/web/src/agent/orchestrator.ts +++ b/apps/web/src/agent/orchestrator.ts @@ -12,6 +12,27 @@ import { useAgentStore } from "@/stores/agent-store"; const MAX_ITERATIONS = 20; +const WRITE_TOOLS = new Set([ + "split", + "delete_timeline_elements", + "move_timeline_elements", + "duplicate_elements", + "add_media_to_timeline", + "update_timeline_element_timing", + "add_text", + "update_text", + "apply_effect", + "update_effect", + "update_clip", + "undo", + "redo", + "toggle_track_mute", + "toggle_track_visibility", + "upsert_keyframe", + "remove_keyframe", + "update_keyframe_curve", +]); + interface APIResponse { content: string; toolCalls?: ToolCall[]; @@ -188,12 +209,25 @@ async function resolveToolCalls( const results: ToolResult[] = []; for (const tc of toolCalls) { - agentStore.setActiveTool(tc.name); + useAgentStore.getState().setActiveTool(tc.name); + + const currentMode = useAgentStore.getState().permissionMode; + if (currentMode === "ask" && WRITE_TOOLS.has(tc.name)) { + const approved = await requestApproval(tc); + if (!approved) { + results.push({ + toolCallId: tc.id, + name: tc.name, + result: null, + error: "User denied permission", + }); + continue; + } + } try { const tool = toolRegistry.get(tc.name); - // Validate args BEFORE execution const validationError = validateToolArgs(tool, tc.args); if (validationError) { results.push({ @@ -220,3 +254,10 @@ async function resolveToolCalls( agentStore.setActiveTool(null); return results; } + +function requestApproval(toolCall: ToolCall): Promise { + return new Promise((resolve) => { + const agentStore = useAgentStore.getState(); + agentStore.setPendingApproval({ toolCall, resolve }); + }); +} diff --git a/apps/web/src/components/editor/panels/chat/chat-input.tsx b/apps/web/src/components/editor/panels/chat/chat-input.tsx index ac6e82d6..f7218e3b 100644 --- a/apps/web/src/components/editor/panels/chat/chat-input.tsx +++ b/apps/web/src/components/editor/panels/chat/chat-input.tsx @@ -2,9 +2,15 @@ import { useState, useCallback, type KeyboardEvent } from "react"; import { HugeiconsIcon } from "@hugeicons/react"; -import { Sent02Icon } from "@hugeicons/core-free-icons"; +import { + Sent02Icon, + FlashIcon, + Shield01Icon, +} from "@hugeicons/core-free-icons"; import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; +import { useAgentStore, type PermissionMode } from "@/stores/agent-store"; +import { cn } from "@/utils/ui"; interface ChatInputProps { onSend: (content: string) => void; @@ -13,6 +19,9 @@ interface ChatInputProps { export function ChatInput({ onSend, disabled }: ChatInputProps) { const [value, setValue] = useState(""); + const permissionMode = useAgentStore((s) => s.permissionMode); + const setPermissionMode = useAgentStore((s) => s.setPermissionMode); + const pendingApproval = useAgentStore((s) => s.pendingApproval); const handleSend = useCallback(() => { const trimmed = value.trim(); @@ -31,6 +40,14 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) { [handleSend], ); + const toggleMode = useCallback(() => { + const next: PermissionMode = + permissionMode === "skip" ? "ask" : "skip"; + setPermissionMode(next); + }, [permissionMode, setPermissionMode]); + + const isAsk = permissionMode === "ask"; + return (
@@ -43,6 +60,20 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) { rows={1} className="border-border bg-input focus-visible:border-primary/50 focus-visible:ring-primary/20 flex-1 resize-none rounded-md border px-3 py-2 text-sm outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50" /> +
)} + {pendingApproval && ( + + )}
)} diff --git a/apps/web/src/components/editor/panels/chat/tool-permission-request.tsx b/apps/web/src/components/editor/panels/chat/tool-permission-request.tsx new file mode 100644 index 00000000..0a94bc7c --- /dev/null +++ b/apps/web/src/components/editor/panels/chat/tool-permission-request.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useCallback } from "react"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Shield01Icon, Cancel01Icon } from "@hugeicons/core-free-icons"; +import { Button } from "@/components/ui/button"; +import { formatToolCall } from "./tool-formatters"; +import { useAgentStore } from "@/stores/agent-store"; +import type { PendingToolApproval } from "@/stores/agent-store"; + +interface ToolPermissionRequestProps { + pending: PendingToolApproval; +} + +export function ToolPermissionRequest({ pending }: ToolPermissionRequestProps) { + const { toolCall, resolve } = pending; + const summary = formatToolCall(toolCall.name, toolCall.args); + + const handleAllow = useCallback(() => { + useAgentStore.getState().setPendingApproval(null); + resolve(true); + }, [resolve]); + + const handleDeny = useCallback(() => { + useAgentStore.getState().setPendingApproval(null); + resolve(false); + }, [resolve]); + + return ( +
+
+ + + Permission Request + +
+
+ {summary.label} + — {summary.description} +
+ {Object.keys(toolCall.args).length > 0 && ( +
+					{JSON.stringify(toolCall.args, null, 2)}
+				
+ )} +
+ + +
+
+ ); +} diff --git a/apps/web/src/stores/agent-store.ts b/apps/web/src/stores/agent-store.ts index 3fe43345..6367ce80 100644 --- a/apps/web/src/stores/agent-store.ts +++ b/apps/web/src/stores/agent-store.ts @@ -1,5 +1,12 @@ import { create } from "zustand"; -import type { AgentContext, ExecutionState } from "@/agent/types"; +import type { AgentContext, ExecutionState, ToolCall } from "@/agent/types"; + +export type PermissionMode = "ask" | "skip"; + +export interface PendingToolApproval { + toolCall: ToolCall; + resolve: (approved: boolean) => void; +} const DEFAULT_CONTEXT: AgentContext = { projectId: null, @@ -12,9 +19,13 @@ interface AgentState { status: ExecutionState; activeTool: string | null; context: AgentContext; + permissionMode: PermissionMode; + pendingApproval: PendingToolApproval | null; setStatus: (status: ExecutionState) => void; setActiveTool: (tool: string | null) => void; setContext: (context: AgentContext) => void; + setPermissionMode: (mode: PermissionMode) => void; + setPendingApproval: (pending: PendingToolApproval | null) => void; reset: () => void; } @@ -22,9 +33,18 @@ export const useAgentStore = create()((set) => ({ status: "idle", activeTool: null, context: DEFAULT_CONTEXT, + permissionMode: "skip", + pendingApproval: null, setStatus: (status) => set({ status }), setActiveTool: (tool) => set({ activeTool: tool }), setContext: (context) => set({ context }), + setPermissionMode: (mode) => set({ permissionMode: mode }), + setPendingApproval: (pending) => set({ pendingApproval: pending }), reset: () => - set({ status: "idle", activeTool: null, context: DEFAULT_CONTEXT }), + set({ + status: "idle", + activeTool: null, + context: DEFAULT_CONTEXT, + pendingApproval: null, + }), }));