diff --git a/apps/web/src/agent/context.ts b/apps/web/src/agent/context.ts index 84506129..c7bac4d2 100644 --- a/apps/web/src/agent/context.ts +++ b/apps/web/src/agent/context.ts @@ -3,6 +3,8 @@ import { TICKS_PER_SECOND } from "@/lib/wasm"; import { buildContextFromEditorState } from "@/agent/context-mapper"; import { BatchCommand } from "@/lib/commands"; import { AddTrackCommand, InsertElementCommand } from "@/lib/commands/timeline"; +import { ToggleTrackMuteCommand } from "@/lib/commands/timeline/track/toggle-track-mute"; +import { ToggleTrackVisibilityCommand } from "@/lib/commands/timeline/track/toggle-track-visibility"; import { DEFAULT_NEW_ELEMENT_DURATION } from "@/lib/timeline/creation"; import { buildElementFromMedia, @@ -89,6 +91,64 @@ export const EditorContextAdapter = { return { remainingUndoDepth }; }, + redo(): { remainingRedoDepth: number } | { error: string } { + const core = EditorCore.getInstance(); + if (!core.command.canRedo()) { + return { error: "Nothing to redo" }; + } + core.command.redo(); + const remainingRedoDepth = core.command.canRedo() ? 1 : 0; + return { remainingRedoDepth }; + }, + + toggleTrackMute({ + trackId, + }: { + trackId: string; + }): { trackId: string } | { error: string } { + const core = EditorCore.getInstance(); + const activeScene = core.scenes.getActiveSceneOrNull(); + if (!activeScene) { + return { error: "No active timeline" }; + } + + const track = findTrackInSceneTracks({ + tracks: activeScene.tracks, + trackId, + }); + if (!track) { + return { error: `Track not found: ${trackId}` }; + } + + core.command.execute({ command: new ToggleTrackMuteCommand(trackId) }); + return { trackId }; + }, + + toggleTrackVisibility({ + trackId, + }: { + trackId: string; + }): { trackId: string } | { error: string } { + const core = EditorCore.getInstance(); + const activeScene = core.scenes.getActiveSceneOrNull(); + if (!activeScene) { + return { error: "No active timeline" }; + } + + const track = findTrackInSceneTracks({ + tracks: activeScene.tracks, + trackId, + }); + if (!track) { + return { error: `Track not found: ${trackId}` }; + } + + core.command.execute({ + command: new ToggleTrackVisibilityCommand(trackId), + }); + return { trackId }; + }, + duplicateElements({ elementIds }: { elementIds: string[] }): | { success: boolean; @@ -761,6 +821,8 @@ export const EditorContextAdapter = { elementId, name, mask, + trimStart, + trimEnd, opacity, positionX, positionY, @@ -779,6 +841,8 @@ export const EditorContextAdapter = { maskType?: string; params?: Record; }; + trimStart?: number; + trimEnd?: number; opacity?: number; positionX?: number; positionY?: number; @@ -831,6 +895,29 @@ export const EditorContextAdapter = { applied.muted = muted; } + if (trimStart !== undefined || trimEnd !== undefined) { + if ( + (trimStart !== undefined && typeof trimStart !== "number") || + (trimEnd !== undefined && typeof trimEnd !== "number") + ) { + return { error: "trimStart and trimEnd must be numbers (seconds)" }; + } + if (trimStart !== undefined && trimStart < 0) { + return { error: "trimStart must be >= 0" }; + } + if (trimEnd !== undefined && trimEnd < 0) { + return { error: "trimEnd must be >= 0" }; + } + if (trimStart !== undefined) { + patch.trimStart = secondsToTicks(trimStart); + applied.trimStart = trimStart; + } + if (trimEnd !== undefined) { + patch.trimEnd = secondsToTicks(trimEnd); + applied.trimEnd = trimEnd; + } + } + if (opacity !== undefined) { if (typeof opacity !== "number" || opacity < 0 || opacity > 100) { return { error: "opacity must be a number between 0 and 100" }; diff --git a/apps/web/src/agent/tools/index.ts b/apps/web/src/agent/tools/index.ts index 0ef6b470..9f73e068 100644 --- a/apps/web/src/agent/tools/index.ts +++ b/apps/web/src/agent/tools/index.ts @@ -28,3 +28,6 @@ import "@/agent/tools/apply-effect.tool"; import "@/agent/tools/update-effect.tool"; import "@/agent/tools/update-clip.tool"; import "@/agent/tools/undo.tool"; +import "@/agent/tools/redo.tool"; +import "@/agent/tools/toggle-track-mute.tool"; +import "@/agent/tools/toggle-track-visibility.tool"; diff --git a/apps/web/src/agent/tools/redo.tool.ts b/apps/web/src/agent/tools/redo.tool.ts new file mode 100644 index 00000000..0f364460 --- /dev/null +++ b/apps/web/src/agent/tools/redo.tool.ts @@ -0,0 +1,16 @@ +import type { AgentContext, ToolDefinition } from "@/agent/types"; +import { toolRegistry } from "@/agent/tools/registry"; +import { redoSchema } from "@/agent/tools/schemas"; +import { EditorContextAdapter } from "@/agent/context"; + +const redoTool: ToolDefinition = { + ...redoSchema, + execute: async ( + _args: Record, + _context: AgentContext, + ): Promise<{ remainingRedoDepth: number } | { error: string }> => { + return EditorContextAdapter.redo(); + }, +}; + +toolRegistry.register(redoSchema.name, redoTool); diff --git a/apps/web/src/agent/tools/schemas.ts b/apps/web/src/agent/tools/schemas.ts index 12861f11..4fb67640 100644 --- a/apps/web/src/agent/tools/schemas.ts +++ b/apps/web/src/agent/tools/schemas.ts @@ -171,6 +171,27 @@ export const updateEffectSchema: ToolSchema = { ], }; +export const redoSchema: ToolSchema = { + name: "redo", + description: + "Redoes the last undone action. Only works after an undo. Returns whether there are more actions to redo.", + parameters: [], +}; + +export const toggleTrackMuteSchema: ToolSchema = { + name: "toggle_track_mute", + description: + "Toggles mute on a timeline track. Use list_timeline to discover trackIds. Only works on tracks that support audio (video and audio tracks). Returns the new muted state.", + parameters: [{ key: "trackId", type: "string", required: true }], +}; + +export const toggleTrackVisibilitySchema: ToolSchema = { + name: "toggle_track_visibility", + description: + "Toggles visibility on a timeline track. Hidden tracks are not rendered in the preview. Use list_timeline to discover trackIds. Returns the new hidden state.", + parameters: [{ key: "trackId", type: "string", required: true }], +}; + export const undoSchema: ToolSchema = { name: "undo", description: @@ -195,11 +216,13 @@ export const getElementSchema: ToolSchema = { export const updateClipSchema: ToolSchema = { name: "update_clip", description: - "Updates properties of any timeline element (video, image, graphic, text, sticker, audio, effect). Use list_timeline to discover elementId, then get_element to inspect current values. Only provide the properties you want to change. mask: { action: 'add', maskType } to add, { action: 'update', params: {...} } to modify, { action: 'remove' } to delete. Mask types: rectangle, ellipse, heart, diamond, star, split, cinematic-bars. Only video/image/graphic support masks. name: rename the element. opacity: 0-100. positionX/positionY: position offset. rotation: degrees. scaleX/scaleY: scale factor. blendMode: normal, darken, multiply, screen, etc. hidden: boolean. volume: 0-100 (video/audio only). muted: boolean (video/audio only).", + "Updates properties of any timeline element (video, image, graphic, text, sticker, audio, effect). Use list_timeline to discover elementId, then get_element to inspect current values. Only provide the properties you want to change. mask: { action: 'add', maskType } to add, { action: 'update', params: {...} } to modify, { action: 'remove' } to delete. Mask types: rectangle, ellipse, heart, diamond, star, split, cinematic-bars. Only video/image/graphic support masks. name: rename the element. trimStart/trimEnd: seconds to trim from the source start/end (slip trim without moving the clip). opacity: 0-100. positionX/positionY: position offset. rotation: degrees. scaleX/scaleY: scale factor. blendMode: normal, darken, multiply, screen, etc. hidden: boolean. volume: 0-100 (video/audio only). muted: boolean (video/audio only).", parameters: [ { key: "elementId", type: "string", required: true }, { key: "name", type: "string", required: false }, { key: "mask", type: "object", required: false }, + { key: "trimStart", type: "number", required: false }, + { key: "trimEnd", type: "number", required: false }, { key: "opacity", type: "number", required: false }, { key: "positionX", type: "number", required: false }, { key: "positionY", type: "number", required: false }, @@ -236,4 +259,7 @@ export const providerToolSchemas: ToolSchema[] = [ updateEffectSchema, updateClipSchema, undoSchema, + redoSchema, + toggleTrackMuteSchema, + toggleTrackVisibilitySchema, ]; diff --git a/apps/web/src/agent/tools/toggle-track-mute.tool.ts b/apps/web/src/agent/tools/toggle-track-mute.tool.ts new file mode 100644 index 00000000..bcc1ecd7 --- /dev/null +++ b/apps/web/src/agent/tools/toggle-track-mute.tool.ts @@ -0,0 +1,22 @@ +import type { AgentContext, ToolDefinition } from "@/agent/types"; +import { toolRegistry } from "@/agent/tools/registry"; +import { toggleTrackMuteSchema } from "@/agent/tools/schemas"; +import { EditorContextAdapter } from "@/agent/context"; + +const toggleTrackMuteTool: ToolDefinition = { + ...toggleTrackMuteSchema, + execute: async ( + args: Record, + _context: AgentContext, + ): Promise<{ trackId: string } | { error: string }> => { + const trackId = args.trackId; + + if (typeof trackId !== "string" || !trackId.trim()) { + return { error: "Invalid trackId" }; + } + + return EditorContextAdapter.toggleTrackMute({ trackId }); + }, +}; + +toolRegistry.register(toggleTrackMuteSchema.name, toggleTrackMuteTool); diff --git a/apps/web/src/agent/tools/toggle-track-visibility.tool.ts b/apps/web/src/agent/tools/toggle-track-visibility.tool.ts new file mode 100644 index 00000000..a0932372 --- /dev/null +++ b/apps/web/src/agent/tools/toggle-track-visibility.tool.ts @@ -0,0 +1,25 @@ +import type { AgentContext, ToolDefinition } from "@/agent/types"; +import { toolRegistry } from "@/agent/tools/registry"; +import { toggleTrackVisibilitySchema } from "@/agent/tools/schemas"; +import { EditorContextAdapter } from "@/agent/context"; + +const toggleTrackVisibilityTool: ToolDefinition = { + ...toggleTrackVisibilitySchema, + execute: async ( + args: Record, + _context: AgentContext, + ): Promise<{ trackId: string } | { error: string }> => { + const trackId = args.trackId; + + if (typeof trackId !== "string" || !trackId.trim()) { + return { error: "Invalid trackId" }; + } + + return EditorContextAdapter.toggleTrackVisibility({ trackId }); + }, +}; + +toolRegistry.register( + toggleTrackVisibilitySchema.name, + toggleTrackVisibilityTool, +); diff --git a/apps/web/src/agent/tools/update-clip.tool.ts b/apps/web/src/agent/tools/update-clip.tool.ts index e87682a4..7af177b9 100644 --- a/apps/web/src/agent/tools/update-clip.tool.ts +++ b/apps/web/src/agent/tools/update-clip.tool.ts @@ -21,6 +21,8 @@ const updateClipTool: ToolDefinition = { params?: Record; } | undefined; + const trimStart = args.trimStart as number | undefined; + const trimEnd = args.trimEnd as number | undefined; const opacity = args.opacity as number | undefined; const positionX = args.positionX as number | undefined; const positionY = args.positionY as number | undefined; @@ -39,6 +41,8 @@ const updateClipTool: ToolDefinition = { const hasUpdate = name !== undefined || mask !== undefined || + trimStart !== undefined || + trimEnd !== undefined || opacity !== undefined || positionX !== undefined || positionY !== undefined || @@ -71,6 +75,8 @@ const updateClipTool: ToolDefinition = { elementId, name, mask, + trimStart, + trimEnd, opacity, positionX, positionY,