Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions electron/ai-edition/agent-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,11 @@ describe("executeAgentTool", () => {
const zoomId = withZoom.zoomRanges[0].id;
const removed = executeAgentTool(withZoom, "removeModifier", JSON.stringify({ id: zoomId }));
expect(removed.ok).toBe(true);
expect(JSON.parse(removed.resultJson)).toMatchObject({ kind: "zoom" });
expect(JSON.parse(removed.resultJson)).toMatchObject({
kind: "zoom",
removed: zoomId,
removedIds: [zoomId],
});
expect(removed.document?.zoomRanges).toHaveLength(0);

const withSpeed = executeAgentTool(
Expand All @@ -772,7 +776,11 @@ describe("executeAgentTool", () => {
"removeModifier",
JSON.stringify({ id: speedId }),
);
expect(JSON.parse(removedSpeed.resultJson)).toMatchObject({ kind: "speed" });
expect(JSON.parse(removedSpeed.resultJson)).toMatchObject({
kind: "speed",
removed: speedId,
removedIds: [speedId],
});
expect(
(removedSpeed.document?.legacyEditor as Record<string, unknown>).speedRegions,
).toHaveLength(0);
Expand All @@ -787,6 +795,39 @@ describe("executeAgentTool", () => {
expect(wrong.resultJson).toMatch(/removeTrim/);
});

it("removeModifier reports every touching modifier row removed with the pill", () => {
const added = executeAgentTool(
fixtureDocument(),
"addZooms",
JSON.stringify({
regions: [
{ startSec: 0, endSec: 10, depth: 3 },
{ startSec: 10, endSec: 20, depth: 3 },
{ startSec: 20, endSec: 30, depth: 3 },
],
}),
);
expect(added.ok).toBe(true);
const ids = (JSON.parse(added.resultJson).applied as Array<{ zoomId: string }>).map(
(entry) => entry.zoomId,
);
expect(ids).toHaveLength(3);

const removed = executeAgentTool(
added.document as AxcutDocument,
"removeModifier",
JSON.stringify({ id: ids[0] }),
);

expect(removed.ok).toBe(true);
expect(removed.document?.zoomRanges).toHaveLength(0);
expect(JSON.parse(removed.resultJson)).toMatchObject({
removed: ids[0],
removedIds: ids,
kind: "zoom",
});
});

it("addZoom reports the CLAMPED span, not the one it was asked for", () => {
// ponytail: the exact shape of D-HONEST. Ventilation trims the span to the
// clip; the tool used to echo back 20–40 while the document held 20–24.704
Expand Down
32 changes: 29 additions & 3 deletions electron/ai-edition/agent-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,26 @@ function toMs(sec: number): number {
return Math.max(0, Math.round(sec * 1000));
}

type ModifierKind = Exclude<RegionKind, "trim">;

function modifierIds(document: AxcutDocument, kind: ModifierKind): string[] {
const legacy = (document.legacyEditor as Record<string, unknown>) ?? {};
switch (kind) {
case "zoom":
return document.zoomRanges.map((region) => region.id);
case "annotation":
return document.annotations.map((region) => region.id);
case "speed":
return ((legacy.speedRegions as Array<{ id: string }> | undefined) ?? []).map(
(region) => region.id,
);
case "cameraFullscreen":
return ((legacy.cameraFullscreenRegions as Array<{ id: string }> | undefined) ?? []).map(
(region) => region.id,
);
}
}

// For the effect set* tools: keep the stored span unless the caller passes new
// edges, and normalise so start ≤ end. Input seconds are virtual-timeline time.
function resolveSpanMs(
Expand Down Expand Up @@ -1867,7 +1887,7 @@ export function executeAgentTool(
const speedRegions = (legacy.speedRegions as Array<{ id: string }> | undefined) ?? [];
const cameraFullscreenRegions =
(legacy.cameraFullscreenRegions as Array<{ id: string }> | undefined) ?? [];
let kind: RegionKind | null = null;
let kind: ModifierKind | null = null;
if (document.zoomRanges.some((z) => z.id === id)) kind = "zoom";
else if (document.annotations.some((a) => a.id === id)) kind = "annotation";
else if (speedRegions.some((s) => s.id === id)) kind = "speed";
Expand All @@ -1878,12 +1898,18 @@ export function executeAgentTool(
`For a trim use removeTrim; for a clip use removeClip.`,
);
}
const beforeIds = modifierIds(document, kind);
const next = removeRegion(document, kind, id);
const remainingIds = new Set(modifierIds(next, kind));
const removedIds = beforeIds.filter((candidateId) => !remainingIds.has(candidateId));
return {
ok: true,
document: next,
resultJson: JSON.stringify({ removed: id, kind }),
summary: `removed ${kind} ${id}`,
resultJson: JSON.stringify({ removed: id, removedIds, kind }),
summary:
removedIds.length === 1
? `removed ${kind} ${id}`
: `removed ${removedIds.length} ${kind} rows: ${removedIds.join(", ")}`,
};
}

Expand Down
2 changes: 1 addition & 1 deletion electron/ai-edition/deep-agent/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
removeTrim:
"Delete a trim range by id — the cut is undone and that span plays/exports again. This is how you 'remove a trim'; never re-add a trim to undo one.",
removeModifier:
"Delete a modifier (zoom / speed / annotation / camera-fullscreen) by id; the kind is resolved from the id. This is how you 'remove'/'delete' one — never neutralise it (span 0, speed 1×), which leaves it in the document. For a trim use removeTrim; for a clip use removeClip.",
"Delete a modifier (zoom / speed / annotation / camera-fullscreen) by id; the kind is resolved from the id. Touching rows with the same styling render as one pill and are deleted together, so read removedIds in the result for the complete set that disappeared. This is how you 'remove'/'delete' one — never neutralise it (span 0, speed 1×), which leaves it in the document. For a trim use removeTrim; for a clip use removeClip.",
removeClip:
"Delete a placed clip by id; remaining clips close the gap and effects anchored to it are dropped. Use only when the user asks to remove a clip — to shorten one, use setClipRange.",
};
Expand Down
2 changes: 1 addition & 1 deletion technical-documentation/architecture/ai-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The model never free-writes the project document. It can only call the fixed set
| `addCameraFullscreen` | Adds a camera-fullscreen region over virtual timeline time; refused when no clip under the span comes from an asset with a linked `cameraTrack`, since such a region can only render nothing. | `legacyEditor.cameraFullscreenRegions`. |
| `setCameraFullscreen` | Moves or resizes a camera-fullscreen pill, under the same camera requirement as `addCameraFullscreen`. | The corresponding `legacyEditor.cameraFullscreenRegions` fragments. |
| `removeTrim` | Deletes a trim so its source span plays and exports again. | `timeline.trimRanges`. |
| `removeModifier` | Resolves and deletes a zoom, speed, annotation, or camera-fullscreen modifier by ID. | The matching modifier collection. |
| `removeModifier` | Resolves and deletes a zoom, speed, annotation, or camera-fullscreen pill by ID; `removedIds` reports every touching storage row deleted with that pill. | The matching modifier collection. |
| `removeClip` | Deletes a placed clip, closes the gap, and drops effects anchored only to it. The result names the modifiers and trims it took with it. | Timeline clips and affected anchored modifiers. |

Clips and trims use source time. Zoom, speed, annotation, and camera-fullscreen tools use virtual edited-timeline time; the executor converts these spans to the clip-anchored millisecond representation used by the document.
Expand Down
Loading