diff --git a/apps/web/src/agent/system-prompt.ts b/apps/web/src/agent/system-prompt.ts index 88f22752..213e0a13 100644 --- a/apps/web/src/agent/system-prompt.ts +++ b/apps/web/src/agent/system-prompt.ts @@ -20,8 +20,7 @@ You are in PLAN mode. You CANNOT make any edits to the timeline. You can only: 1. **Analyze**: Use read-only tools to understand what media and timeline state exist. Use load_context to actually see/hear the footage. 2. **Discover**: If the user's request matches a known editing pattern, call list_skills to find relevant techniques. 3. **Clarify**: If anything is ambiguous, use ask_user to get clarification before planning. -4. **Plan**: Once you understand the footage and the goal, call submit_plan with a structured step-by-step plan. -5. **Approval**: Immediately after submit_plan succeeds, call request_plan_approval. Do NOT edit until the user approves. +4. **Plan**: Once you understand the footage and the goal, call submit_plan with a structured step-by-step plan. This shows the user a plan card with Go edit / Keep planning and waits for their choice. ## PLAN QUALITY RULES: @@ -38,7 +37,7 @@ Submit the plan when: - All clarifying questions have been answered - You have a concrete, actionable plan -After submitting, you MUST ask for approval through request_plan_approval. The user will choose Keep planning or Go edit. +After submit_plan returns approved=true, continue with the plan in execute mode. If it returns approved=false, keep discussing and refining the plan. `; const EXECUTE_MODE_INSTRUCTIONS = ` @@ -52,8 +51,7 @@ For any complex editing request that would require multiple timeline edits (cuts 1. Analyze the project with read-only tools. 2. Call submit_plan with a concise structured checklist. -3. Call request_plan_approval. -4. Wait for the user to choose Go edit. +3. Wait for the user to choose Go edit. Do NOT perform write tools for complex edits before approval. This is required even if the current mode is EXECUTE. @@ -67,7 +65,7 @@ Do NOT perform write tools for complex edits before approval. This is required e ## IF NO ACTIVE PLAN: -Simple one-shot edits may execute directly. Complex edits must go through submit_plan + request_plan_approval first. +Simple one-shot edits may execute directly. Complex edits must go through submit_plan first. `; export function buildSystemPrompt( diff --git a/apps/web/src/agent/tools/schemas.ts b/apps/web/src/agent/tools/schemas.ts index 175579c9..1e819a9e 100644 --- a/apps/web/src/agent/tools/schemas.ts +++ b/apps/web/src/agent/tools/schemas.ts @@ -308,7 +308,7 @@ export const loadSkillSchema: ToolSchema = { export const submitPlanSchema: ToolSchema = { name: "submit_plan", description: - "Submits a structured editing plan with steps. Use this before complex edits, even from execute mode. Each step should describe a specific action with the tools to use. After submission, call request_plan_approval and wait for the user before any write tools.", + "Submits a structured editing plan with steps and waits for user approval through a plan card with Go edit and Keep planning actions. Use this before complex edits, even from execute mode. Each step should describe a specific action with the tools to use. If approved=true is returned, continue executing the plan. If approved=false is returned, keep refining the plan with the user.", parameters: [ { key: "summary", type: "string", required: true }, { key: "steps", type: "array", required: true }, @@ -329,7 +329,7 @@ export const askUserSchema: ToolSchema = { export const requestPlanApprovalSchema: ToolSchema = { name: "request_plan_approval", description: - "Requests user approval to switch from planning to editing. The user sees a modal with Keep planning and Go edit options. ALWAYS requires explicit user approval — there is no auto-approve. Use immediately after submit_plan succeeds.", + "Requests user approval to switch from planning to editing for an already submitted plan. Usually you do not need this because submit_plan already waits for approval. The user sees a plan card with Keep planning and Go edit options.", parameters: [], }; diff --git a/apps/web/src/agent/tools/submit-plan.tool.ts b/apps/web/src/agent/tools/submit-plan.tool.ts index b2668936..65b028fc 100644 --- a/apps/web/src/agent/tools/submit-plan.tool.ts +++ b/apps/web/src/agent/tools/submit-plan.tool.ts @@ -3,6 +3,7 @@ import { toolRegistry } from "@/agent/tools/registry"; import { submitPlanSchema } from "@/agent/tools/schemas"; import { usePlanStore, createPlanFromSteps } from "@/stores/plan-store"; import { useAgentStore } from "@/stores/agent-store"; +import type { AgentMode } from "@/agent/types"; type SubmittedStep = { description: string; tools: string[] }; @@ -15,7 +16,8 @@ const submitPlanTool: ToolDefinition = { | { planId: string; stepCount: number; - nextAction: "request_plan_approval"; + approved: boolean; + mode: AgentMode; } | { error: string } > => { @@ -49,11 +51,27 @@ const submitPlanTool: ToolDefinition = { usePlanStore.getState().setPlan(plan); useAgentStore.getState().setMode("plan"); - return { - planId: plan.id, - stepCount: plan.steps.length, - nextAction: "request_plan_approval", - }; + return new Promise((resolve) => { + useAgentStore.getState().setPendingModeTransition({ + targetMode: "execute", + resolve: (approved) => { + useAgentStore.getState().setPendingModeTransition(null); + if (approved) { + useAgentStore.getState().setMode("execute"); + usePlanStore.getState().updatePlanStatus("executing"); + } else { + useAgentStore.getState().setMode("plan"); + usePlanStore.getState().updatePlanStatus("awaiting_approval"); + } + resolve({ + planId: plan.id, + stepCount: plan.steps.length, + approved, + mode: approved ? "execute" : "plan", + }); + }, + }); + }); }, }; 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 f18a2975..91084b34 100644 --- a/apps/web/src/components/editor/panels/chat/chat-input.tsx +++ b/apps/web/src/components/editor/panels/chat/chat-input.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, type KeyboardEvent } from "react"; +import { useState, useCallback, useRef, type KeyboardEvent } from "react"; import { HugeiconsIcon } from "@hugeicons/react"; import { Sent02Icon, @@ -12,9 +12,7 @@ import { import { Button } from "@/components/ui/button"; import { Spinner } from "@/components/ui/spinner"; import { useAgentStore, type PermissionMode } from "@/stores/agent-store"; -import { usePlanStore } from "@/stores/plan-store"; import { cn } from "@/utils/ui"; -import type { AgentMode } from "@/agent/types"; interface ChatInputProps { onSend: (content: string) => void; @@ -23,46 +21,48 @@ interface ChatInputProps { export function ChatInput({ onSend, disabled }: ChatInputProps) { const [value, setValue] = useState(""); + const shiftHandledRef = useRef(false); const permissionMode = useAgentStore((s) => s.permissionMode); const setPermissionMode = useAgentStore((s) => s.setPermissionMode); const pendingApproval = useAgentStore((s) => s.pendingApproval); const mode = useAgentStore((s) => s.mode); const setMode = useAgentStore((s) => s.setMode); const pendingTransition = useAgentStore((s) => s.pendingModeTransition); - const plan = usePlanStore((s) => s.plan); + const isPlanMode = mode === "plan"; + const toggleMode = useCallback(() => { + setMode(isPlanMode ? "execute" : "plan"); + }, [isPlanMode, setMode]); - const handleSend = useCallback( - (forcedMode?: AgentMode) => { - const trimmed = value.trim(); - if (!trimmed || disabled) return; - const targetMode = forcedMode ?? mode; - if (targetMode !== mode) { - setMode(targetMode); - } - onSend(trimmed); - setValue(""); - }, - [value, disabled, onSend, mode, setMode], - ); + const handleSend = useCallback(() => { + const trimmed = value.trim(); + if (!trimmed || disabled) return; + onSend(trimmed); + setValue(""); + }, [value, disabled, onSend]); const handleKeyDown = useCallback( (e: KeyboardEvent) => { + if (e.key === "Shift") { + if (!shiftHandledRef.current) { + shiftHandledRef.current = true; + toggleMode(); + } + return; + } + if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); - handleSend("execute"); - } - if (e.key === "Enter" && e.shiftKey) { - e.preventDefault(); - handleSend("plan"); + handleSend(); } }, - [handleSend], + [handleSend, toggleMode], ); - const toggleMode = useCallback(() => { - const next: AgentMode = mode === "execute" ? "plan" : "execute"; - setMode(next); - }, [mode, setMode]); + const handleKeyUp = useCallback((e: KeyboardEvent) => { + if (e.key === "Shift") { + shiftHandledRef.current = false; + } + }, []); const togglePermission = useCallback(() => { const next: PermissionMode = permissionMode === "skip" ? "ask" : "skip"; @@ -70,15 +70,36 @@ export function ChatInput({ onSend, disabled }: ChatInputProps) { }, [permissionMode, setPermissionMode]); const isAsk = permissionMode === "ask"; - const isPlanMode = mode === "plan"; return (
+
+ + + Press Shift to switch + +