diff --git a/apps/web/src/agent/context.ts b/apps/web/src/agent/context.ts index 5b6b9557..2cbf8a91 100644 --- a/apps/web/src/agent/context.ts +++ b/apps/web/src/agent/context.ts @@ -617,6 +617,66 @@ export const EditorContextAdapter = { appliedParams: element.params, }; }, + + updateEffectElement({ + elementId, + params, + }: { + elementId: string; + params: Record; + }): + | { + success: boolean; + elementId: string; + appliedParams: Record; + } + | { error: string } { + const core = EditorCore.getInstance(); + const activeScene = core.scenes.getActiveSceneOrNull(); + if (!activeScene) { + return { error: "No active timeline" }; + } + + const [resolved] = findTimelineElementsWithTracksByIds({ + tracks: activeScene.tracks, + elementIds: [elementId], + }); + if (!resolved) { + return { error: `Timeline element not found: ${elementId}` }; + } + if (resolved.element.type !== "effect") { + return { error: "Element is not an effect" }; + } + + const effectElement = resolved.element as import("@/lib/timeline/types").EffectElement; + const definition = effectsRegistry.get(effectElement.effectType); + if (!definition) { + return { error: `Effect not found: ${effectElement.effectType}` }; + } + + const validationError = validateEffectParams(definition.params, params); + if (validationError) { + return { error: validationError }; + } + + const mergedParams = { ...effectElement.params, ...params }; + + core.timeline.updateElements({ + updates: [ + { + trackId: resolved.track.id, + elementId, + patch: { params: mergedParams } as Partial, + }, + ], + }); + + return { + success: true, + elementId, + appliedParams: mergedParams, + }; + }, }; function secondsToTicks(seconds: number): number { diff --git a/apps/web/src/agent/tools/index.ts b/apps/web/src/agent/tools/index.ts index 9c73cce2..c21904fe 100644 --- a/apps/web/src/agent/tools/index.ts +++ b/apps/web/src/agent/tools/index.ts @@ -23,3 +23,4 @@ import "@/agent/tools/update-text.tool"; import "@/agent/tools/list-effects.tool"; import "@/agent/tools/get-effect.tool"; import "@/agent/tools/apply-effect.tool"; +import "@/agent/tools/update-effect.tool"; diff --git a/apps/web/src/agent/tools/schemas.ts b/apps/web/src/agent/tools/schemas.ts index 24e788a1..cb0bc7c1 100644 --- a/apps/web/src/agent/tools/schemas.ts +++ b/apps/web/src/agent/tools/schemas.ts @@ -161,6 +161,16 @@ export const applyEffectSchema: ToolSchema = { ], }; +export const updateEffectSchema: ToolSchema = { + name: "update_effect", + description: + "Updates parameters of an existing effect element on the timeline. Use list_timeline to find the elementId of the effect, then pass the params you want to change. Only the provided parameters are updated; others keep their current values. Use get_effect to discover valid parameter keys and ranges.", + parameters: [ + { key: "elementId", type: "string", required: true }, + { key: "params", type: "object", required: true }, + ], +}; + /** * The exact list of schemas exposed to the LLM. * Excludes internal-only tools (transcribe_video, mock). @@ -179,4 +189,5 @@ export const providerToolSchemas: ToolSchema[] = [ listEffectsSchema, getEffectSchema, applyEffectSchema, + updateEffectSchema, ]; diff --git a/apps/web/src/agent/tools/update-effect.tool.ts b/apps/web/src/agent/tools/update-effect.tool.ts new file mode 100644 index 00000000..f81d8990 --- /dev/null +++ b/apps/web/src/agent/tools/update-effect.tool.ts @@ -0,0 +1,39 @@ +import type { AgentContext, ToolDefinition } from "@/agent/types"; +import { toolRegistry } from "@/agent/tools/registry"; +import { updateEffectSchema } from "@/agent/tools/schemas"; +import { EditorContextAdapter } from "@/agent/context"; + +const updateEffectTool: ToolDefinition = { + ...updateEffectSchema, + execute: async ( + args: Record, + _context: AgentContext, + ): Promise< + | { + success: boolean; + elementId: string; + appliedParams: Record; + } + | { error: string } + > => { + const elementId = args.elementId; + const params = args.params as + | Record + | undefined; + + if (typeof elementId !== "string" || !elementId.trim()) { + return { error: "Invalid element id" }; + } + + if (!params || typeof params !== "object" || Object.keys(params).length === 0) { + return { error: "params is required and must be a non-empty object" }; + } + + return EditorContextAdapter.updateEffectElement({ + elementId, + params, + }); + }, +}; + +toolRegistry.register(updateEffectSchema.name, updateEffectTool); diff --git a/docs/agent-tool-specs.md b/docs/agent-tool-specs.md index c29a75b6..6b07c2be 100644 --- a/docs/agent-tool-specs.md +++ b/docs/agent-tool-specs.md @@ -654,6 +654,49 @@ Agregar un efecto como elemento standalone al timeline en una pista de efectos, --- +## 17. `update_effect` + +### Propósito +Actualizar los parámetros de un elemento de efecto existente en el timeline. Solo se actualizan los parámetros proporcionados; el resto mantiene sus valores actuales. + +### Input +```ts +{ + elementId: string; + params: Record; +} +``` + +### Output +```ts +{ + success: boolean; + elementId: string; + appliedParams: Record; +} +``` + +### Requirements +- MUST validate `elementId` exists in the active timeline. +- MUST validate the target element is an effect element (`type: "effect"`). +- MUST validate `params` against the effect's parameter definitions (types, ranges). +- MUST merge provided `params` with existing params (only override specified keys). +- MUST use `updateElements` to apply the param patch. +- MUST preserve undo/redo behavior. +- MUST return the full merged params after update. + +### Errors +- Invalid element id: `{ error: "Invalid element id" }`. +- Missing params: `{ error: "params is required and must be a non-empty object" }`. +- No active timeline: `{ error: "No active timeline" }`. +- Missing element: `{ error: "Timeline element not found: " }`. +- Wrong type: `{ error: "Element is not an effect" }`. +- Effect not found: `{ error: "Effect not found: " }`. +- Unknown parameter: `{ error: "Unknown parameter: " }`. +- Out of range: `{ error: "Parameter '' must be >= " }`. + +--- + ## Existing/secondary tool: `transcribe_video` ### Propósito