From 5789581afeb519a4988004e9f14982b179ac9a8e Mon Sep 17 00:00:00 2001 From: Arham Wani Date: Fri, 28 Aug 2026 07:48:04 +0530 Subject: [PATCH] fix(ai): report every modifier removed with a pill --- electron/ai-edition/agent-tools.test.ts | 45 ++++++++++++++++++- electron/ai-edition/agent-tools.ts | 32 +++++++++++-- electron/ai-edition/deep-agent/service.ts | 2 +- .../architecture/ai-agent.md | 2 +- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/electron/ai-edition/agent-tools.test.ts b/electron/ai-edition/agent-tools.test.ts index 8f8361e59..e0f7459af 100644 --- a/electron/ai-edition/agent-tools.test.ts +++ b/electron/ai-edition/agent-tools.test.ts @@ -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( @@ -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).speedRegions, ).toHaveLength(0); @@ -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 diff --git a/electron/ai-edition/agent-tools.ts b/electron/ai-edition/agent-tools.ts index 5a0966398..dc5bc24ae 100644 --- a/electron/ai-edition/agent-tools.ts +++ b/electron/ai-edition/agent-tools.ts @@ -69,6 +69,26 @@ function toMs(sec: number): number { return Math.max(0, Math.round(sec * 1000)); } +type ModifierKind = Exclude; + +function modifierIds(document: AxcutDocument, kind: ModifierKind): string[] { + const legacy = (document.legacyEditor as Record) ?? {}; + 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( @@ -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"; @@ -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(", ")}`, }; } diff --git a/electron/ai-edition/deep-agent/service.ts b/electron/ai-edition/deep-agent/service.ts index d804a3b1a..830e10e8e 100644 --- a/electron/ai-edition/deep-agent/service.ts +++ b/electron/ai-edition/deep-agent/service.ts @@ -173,7 +173,7 @@ export const TOOL_DESCRIPTIONS: Record = { 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.", }; diff --git a/technical-documentation/architecture/ai-agent.md b/technical-documentation/architecture/ai-agent.md index 6278474e3..3536f67cc 100644 --- a/technical-documentation/architecture/ai-agent.md +++ b/technical-documentation/architecture/ai-agent.md @@ -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.