feat: add redo, toggle track mute/visibility, and trim support

- redo: re-applies the last undone action via core.command.redo()
- toggle_track_mute: toggles mute on video/audio tracks via
  ToggleTrackMuteCommand
- toggle_track_visibility: toggles visibility on overlay tracks via
  ToggleTrackVisibilityCommand
- update_clip now supports trimStart/trimEnd for slip-trimming clips
  without moving them on the timeline
This commit is contained in:
Luis Esteban Acevedo Ladino 2026-04-26 19:02:39 -05:00
parent 28b825267f
commit f474a87267
7 changed files with 186 additions and 1 deletions

View File

@ -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<string, number | string | boolean>;
};
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" };

View File

@ -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";

View File

@ -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<string, unknown>,
_context: AgentContext,
): Promise<{ remainingRedoDepth: number } | { error: string }> => {
return EditorContextAdapter.redo();
},
};
toolRegistry.register(redoSchema.name, redoTool);

View File

@ -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,
];

View File

@ -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<string, unknown>,
_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);

View File

@ -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<string, unknown>,
_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,
);

View File

@ -21,6 +21,8 @@ const updateClipTool: ToolDefinition = {
params?: Record<string, number | string | boolean>;
}
| 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,