feat: add undo, duplicate_elements tools and enrich list_timeline hints
- undo: reverts the last editing action via core.command.undo() - duplicate_elements: duplicates elements onto new tracks using DuplicateElementsCommand - list_timeline now includes hasMask, hasEffects, and isHidden hints on elements so the agent can identify clips needing deeper inspection
This commit is contained in:
parent
b48062a5f7
commit
28b825267f
|
|
@ -120,6 +120,9 @@ function toTimelineTrack(
|
|||
...(hasMediaId(element) ? { assetId: element.mediaId } : {}),
|
||||
...(element.name ? { name: element.name } : {}),
|
||||
...(hasTextContent(element) ? { content: element.content } : {}),
|
||||
...(hasNonEmptyArray(element, "masks") ? { hasMask: true } : {}),
|
||||
...(hasNonEmptyArray(element, "effects") ? { hasEffects: true } : {}),
|
||||
...(element.hidden === true ? { isHidden: true } : {}),
|
||||
start: toSeconds(element.startTime, ticksPerSecond),
|
||||
end: toSeconds(element.startTime + element.duration, ticksPerSecond),
|
||||
})),
|
||||
|
|
@ -185,6 +188,9 @@ function hasTimelineElementShape(element: unknown): element is {
|
|||
name?: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
masks?: unknown[];
|
||||
effects?: unknown[];
|
||||
hidden?: boolean;
|
||||
} {
|
||||
return (
|
||||
typeof element === "object" &&
|
||||
|
|
@ -200,3 +206,9 @@ function hasTimelineElementShape(element: unknown): element is {
|
|||
typeof element.duration === "number"
|
||||
);
|
||||
}
|
||||
|
||||
function hasNonEmptyArray(obj: unknown, key: string): boolean {
|
||||
if (typeof obj !== "object" || obj === null || !(key in obj)) return false;
|
||||
const value = (obj as Record<string, unknown>)[key];
|
||||
return Array.isArray(value) && value.length > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,55 @@ export const EditorContextAdapter = {
|
|||
return asset?.hasAudio;
|
||||
},
|
||||
|
||||
undo(): { remainingUndoDepth: number } | { error: string } {
|
||||
const core = EditorCore.getInstance();
|
||||
if (!core.command.canUndo()) {
|
||||
return { error: "Nothing to undo" };
|
||||
}
|
||||
core.command.undo();
|
||||
const remainingUndoDepth = core.command.canUndo() ? 1 : 0;
|
||||
return { remainingUndoDepth };
|
||||
},
|
||||
|
||||
duplicateElements({ elementIds }: { elementIds: string[] }):
|
||||
| {
|
||||
success: boolean;
|
||||
duplicated: Array<{ elementId: string; trackId: string }>;
|
||||
}
|
||||
| { error: string } {
|
||||
const core = EditorCore.getInstance();
|
||||
const activeScene = core.scenes.getActiveSceneOrNull();
|
||||
if (!activeScene) {
|
||||
return { error: "No active timeline" };
|
||||
}
|
||||
|
||||
if (!hasTimelineContent(activeScene.tracks)) {
|
||||
return { error: "No timeline content" };
|
||||
}
|
||||
|
||||
const requestedIds = [...new Set(elementIds)];
|
||||
const elements = findTimelineElementsWithTracksByIds({
|
||||
tracks: activeScene.tracks,
|
||||
elementIds: requestedIds,
|
||||
});
|
||||
const foundIds = new Set(elements.map(({ element }) => element.id));
|
||||
const missingIds = requestedIds.filter((id) => !foundIds.has(id));
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
return {
|
||||
error: `Timeline elements not found: ${missingIds.join(", ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
const refs = elements.map(({ element, track }) => ({
|
||||
trackId: track.id,
|
||||
elementId: element.id,
|
||||
}));
|
||||
|
||||
const duplicated = core.timeline.duplicateElements({ elements: refs });
|
||||
return { success: true, duplicated };
|
||||
},
|
||||
|
||||
splitTimeline({
|
||||
times,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { EditorContextAdapter } from "@/agent/context";
|
||||
import type { AgentContext, ToolDefinition } from "@/agent/types";
|
||||
import { toolRegistry } from "@/agent/tools/registry";
|
||||
import { resolveElementIds } from "@/agent/tools/resolve-element-ids";
|
||||
import { duplicateElementsSchema } from "@/agent/tools/schemas";
|
||||
|
||||
const duplicateElementsTool: ToolDefinition = {
|
||||
...duplicateElementsSchema,
|
||||
execute: async (
|
||||
args: Record<string, unknown>,
|
||||
_context: AgentContext,
|
||||
): Promise<
|
||||
| {
|
||||
success: boolean;
|
||||
duplicated: Array<{ elementId: string; trackId: string }>;
|
||||
}
|
||||
| { error: string }
|
||||
> => {
|
||||
const elementIds = resolveElementIds(args.elementIds);
|
||||
|
||||
if (!elementIds) {
|
||||
return {
|
||||
error:
|
||||
'elementIds must be a non-empty JSON array of strings, e.g. ["id1","id2"]',
|
||||
};
|
||||
}
|
||||
|
||||
return EditorContextAdapter.duplicateElements({ elementIds });
|
||||
},
|
||||
};
|
||||
|
||||
toolRegistry.register(duplicateElementsSchema.name, duplicateElementsTool);
|
||||
|
|
@ -17,6 +17,7 @@ import "@/agent/tools/get-element.tool";
|
|||
import "@/agent/tools/split.tool";
|
||||
import "@/agent/tools/delete-timeline-elements.tool";
|
||||
import "@/agent/tools/move-timeline-elements.tool";
|
||||
import "@/agent/tools/duplicate-elements.tool";
|
||||
import "@/agent/tools/add-media-to-timeline.tool";
|
||||
import "@/agent/tools/update-timeline-element-timing.tool";
|
||||
import "@/agent/tools/add-text.tool";
|
||||
|
|
@ -26,3 +27,4 @@ import "@/agent/tools/get-effect.tool";
|
|||
import "@/agent/tools/apply-effect.tool";
|
||||
import "@/agent/tools/update-effect.tool";
|
||||
import "@/agent/tools/update-clip.tool";
|
||||
import "@/agent/tools/undo.tool";
|
||||
|
|
|
|||
|
|
@ -171,6 +171,20 @@ export const updateEffectSchema: ToolSchema = {
|
|||
],
|
||||
};
|
||||
|
||||
export const undoSchema: ToolSchema = {
|
||||
name: "undo",
|
||||
description:
|
||||
"Undoes the last editing action performed by any tool. Use this to revert mistakes. Returns the remaining undo stack depth. Consecutive calls undo earlier actions.",
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
export const duplicateElementsSchema: ToolSchema = {
|
||||
name: "duplicate_elements",
|
||||
description:
|
||||
"Duplicates one or more timeline elements. The copies are placed on new tracks above the originals. Use list_timeline to discover elementIds first.",
|
||||
parameters: [{ key: "elementIds", type: "string[]", required: true }],
|
||||
};
|
||||
|
||||
export const getElementSchema: ToolSchema = {
|
||||
name: "get_element",
|
||||
description:
|
||||
|
|
@ -211,6 +225,7 @@ export const providerToolSchemas: ToolSchema[] = [
|
|||
splitSchema,
|
||||
deleteTimelineElementsSchema,
|
||||
moveTimelineElementsSchema,
|
||||
duplicateElementsSchema,
|
||||
addMediaToTimelineSchema,
|
||||
updateTimelineElementTimingSchema,
|
||||
addTextSchema,
|
||||
|
|
@ -220,4 +235,5 @@ export const providerToolSchemas: ToolSchema[] = [
|
|||
applyEffectSchema,
|
||||
updateEffectSchema,
|
||||
updateClipSchema,
|
||||
undoSchema,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import type { AgentContext, ToolDefinition } from "@/agent/types";
|
||||
import { toolRegistry } from "@/agent/tools/registry";
|
||||
import { undoSchema } from "@/agent/tools/schemas";
|
||||
import { EditorContextAdapter } from "@/agent/context";
|
||||
|
||||
const undoTool: ToolDefinition = {
|
||||
...undoSchema,
|
||||
execute: async (
|
||||
_args: Record<string, unknown>,
|
||||
_context: AgentContext,
|
||||
): Promise<{ remainingUndoDepth: number } | { error: string }> => {
|
||||
return EditorContextAdapter.undo();
|
||||
},
|
||||
};
|
||||
|
||||
toolRegistry.register(undoSchema.name, undoTool);
|
||||
|
|
@ -58,6 +58,9 @@ export type AgentTimelineTrack = {
|
|||
assetId?: string;
|
||||
name?: string;
|
||||
content?: string;
|
||||
hasMask?: boolean;
|
||||
hasEffects?: boolean;
|
||||
isHidden?: boolean;
|
||||
/** Timeline start in seconds. */
|
||||
start: number;
|
||||
/** Timeline end in seconds. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue