Files
OpenMAIC/lib/agent/tools/edit-elements.ts
T
xuyuanwei678 c38da84ef6 feat(renderer): decouple editor UI from the app (#1072)
* docs: define renderer editor task 1 integration

* docs: plan renderer editor task 1

* feat: gate renderer slide editor

* feat: adapt renderer editor intents

* feat: connect renderer editor to slide surface

* test: complete renderer intent shape fixture

* fix: restore renderer editor action previews

* fix: render editor alignment guides

* perf: smooth renderer element dragging

* feat: align renderer hidden element behavior

* feat: add renderer canvas host commands

* feat: add renderer canvas context interactions

* test: cover renderer context menu interaction

* fix: preserve renderer canvas interaction semantics

* docs: design renderer text editing

* docs: plan renderer text editing

* feat(renderer): add ProseMirror text core

* feat(renderer): add rich text editor controller

* feat(renderer): render editable text in place

* feat(renderer): enter text editing on click

* feat: connect renderer rich text editing

* feat: finalize renderer text edit lifecycle

* fix: stabilize renderer text edit commits

* fix(renderer): move elements from selected borders

* docs: design renderer editing UI text toolbar

* feat(renderer): publish editing UI contracts

* feat(renderer): add controlled text format toolbar

* style(renderer): format editing UI contracts

* fix(renderer): address Task 2 review findings

* fix(renderer): handle zero font sizes in toolbar

* feat(renderer): add editing UI color picker

* fix(renderer): address Task 3 color picker review

* fix(renderer): commit native color input changes

* fix(renderer): coordinate native color picker interaction

* feat(renderer): anchor text toolbar to canvas elements

* feat(renderer): compose editable canvas with text UI

* feat(editor): use renderer text toolbar UI

* fix(renderer): make editing UI tests type-safe

* fix(renderer): harden editing UI integration

* feat(renderer): add editable text UI

* feat(renderer): add image and shape editing

* feat(editor): extend renderer editing controls

* feat(editor): add renderer chart insertion

* docs: design renderer latex editor

* docs: plan renderer latex editor

* feat(renderer): add latex editor contract

* feat(renderer): add latex editor dialog

* feat(renderer): compose latex insert and edit UI

* feat(editor): integrate renderer latex editing

* fix(editor): consolidate latex toolbar actions

* fix(renderer): show toolbar hover labels

* test(renderer): cover localized latex tooltips

* chore(renderer): hide latex sample palette

* feat(renderer): add video editing controls

* docs: define renderer audio editing

* feat(renderer): render audio elements

* feat(renderer): add audio editing UI

* feat(renderer): add preview audio controls

* feat(editor): add renderer element clipboard

* feat(renderer): decouple editor UI from app

* chore(renderer): release 0.0.6

* fix(renderer): clear editor CI gates

* fix(editor): adapt renderer canvas to media resolution API

* fix(i18n): complete French editor labels

* fix(renderer): hide toolbar vertical overflow

* chore(renderer): address review cleanup

* chore(docs): remove planning artifacts from pr

* feat(editor): add transactional core package

* chore(editor): ignore local test cache

* build(editor): publish core entrypoint

* refactor(editor): route renderer changes through transactions

* refactor(editor): split renderer editing into editor package

* ci(editor): register editor package for release

* docs(editor): document package dependencies

* ci: rerun validation after editor release registration

* test(editor): validate deletion against existing element

* fix(editor): address transaction and package review findings

* fix(editor): validate transactions and restore editor styles

* fix(editor): protect required updates and react styles

* fix(editor): validate required transaction fields

* fix(editor): prevent insert popovers shifting canvas

* refactor(editor): own renderer editing capabilities

* fix(editor): harden editing transaction boundaries

* refactor(editor): modularize editor UI integration

* docs(editor): harden third-party integration

* fix(editor): preserve video resize aspect ratio

* fix(editor): discard text draft before deletion

* fix(editor): preserve renderer picking and line presets

* chore(editor): remove unused pick-layer import
2026-08-10 19:56:24 +08:00

202 lines
6.8 KiB
TypeScript

/**
* `edit_elements` agent tool
*
* The agent authors guarded RFC 6902 `test`/`add`/`replace` operations against the
* trusted slide canvas. This tool validates the complete batch and converts it
* to the existing host EditIntent vocabulary. The client still owns the live
* document, apply-time revalidation, persistence, and one-step undo.
*/
import { Type, type Static } from 'typebox';
import type { AgentTool } from '@earendil-works/pi-agent-core';
import type { EditIntent } from '@openmaic/editor/core';
import type { PPTElement } from '@openmaic/dsl';
import type { RegenerateActionsDeps, SceneContext } from './regenerate-scene-actions';
import {
buildElementInventory,
elementInventoryFingerprint,
elementInventorySnapshotFingerprint,
type ElementInventoryItem,
} from './edit-elements-gate';
import { mapElementJsonPatchToEditIntents } from './edit-elements-patch';
const ElementPatchOperation = Type.Object(
{
op: Type.Union([Type.Literal('test'), Type.Literal('add'), Type.Literal('replace')]),
path: Type.String({ description: 'RFC 6901 path rooted at /elements.' }),
value: Type.Unknown(),
},
{ additionalProperties: false },
);
export const EditElementsParams = Type.Object({
sceneId: Type.String({
description:
'The id of the SLIDE scene to edit. Use the id of the current scene shown in the system prompt.',
}),
patches: Type.Array(ElementPatchOperation, {
description:
'Guarded JSON Patch operations. Before adding or replacing /elements/N/..., first test /elements/N/id.',
}),
reason: Type.String({
description: 'Short user-facing summary of the requested element edit.',
}),
});
export type EditElementsParams = Static<typeof EditElementsParams>;
export interface EditElementsDetails {
sceneId: string;
intents: EditIntent[] | null;
updateCount: number;
refuseReason?: string;
targetElementTypes?: Record<string, string>;
targetElementFingerprints?: Record<string, string>;
inventoryFingerprint?: string;
}
export type EditElementsDeps = RegenerateActionsDeps;
function targetTypes(
targetIds: readonly string[],
inventory: ElementInventoryItem[],
): Record<string, string> {
const byId = new Map(inventory.map((element) => [element.id, element.type] as const));
return Object.fromEntries(
targetIds.flatMap((id) => {
const type = byId.get(id);
return type ? [[id, type] as const] : [];
}),
);
}
function targetFingerprints(
targetIds: readonly string[],
elements: PPTElement[],
): Record<string, string> {
const byId = new Map(elements.map((element) => [element.id, element] as const));
return Object.fromEntries(
targetIds.flatMap((id) => {
const element = byId.get(id);
return element ? [[id, elementInventoryFingerprint(element)] as const] : [];
}),
);
}
export function makeEditElementsTool(
deps: EditElementsDeps,
): AgentTool<typeof EditElementsParams, EditElementsDetails> {
return {
name: 'edit_elements',
label: 'Edit slide elements',
description:
'Atomically edits existing elements on a SLIDE scene with guarded JSON Patch operations. ' +
'Call read_scene_content first to obtain the indexed element JSON. Before changing any ' +
'/elements/N/... path, test /elements/N/id against the id you just read. Supports existing ' +
'geometry and renderer-visible style properties plus text and shape-label HTML content. ' +
'Supports test, add for optional properties, and replace; it cannot add elements. ' +
'Value contract: geometry/opacity are numbers; colors are CSS color strings; outline is ' +
'{width,style,color}; shadow is {h,v,blur,color}; image filters use unitless numeric strings ' +
'(for example brightness "120" and blur "2"); line width is stroke thickness. Preserve ' +
'existing structured fields and units exactly, and change only requested paths.',
parameters: EditElementsParams,
execute: async (_toolCallId, params) => {
const { sceneId, patches } = params;
const ctxData: SceneContext | undefined = deps.getSceneContext(sceneId);
if (!ctxData) {
return {
content: [
{
type: 'text',
text: `Error: scene context not found for sceneId ${JSON.stringify(String(sceneId).slice(0, 200))}.`,
},
],
details: {
sceneId,
intents: null,
updateCount: 0,
refuseReason: 'scene context not found',
},
isError: true,
};
}
const { content } = ctxData;
if (content.type !== 'slide') {
return {
content: [
{
type: 'text',
text:
'Cannot edit elements on this scene: edit_elements works on SLIDE scenes only. ' +
'For interactive pages use edit_interactive_html; otherwise suggest the canvas.',
},
],
details: {
sceneId,
intents: null,
updateCount: 0,
refuseReason: 'not a slide scene',
},
isError: true,
};
}
const elements = (content.canvas?.elements ?? []) as PPTElement[];
if (elements.length === 0) {
return {
content: [{ type: 'text', text: 'This slide has no elements to edit.' }],
details: {
sceneId,
intents: null,
updateCount: 0,
refuseReason: 'slide has no elements',
},
isError: true,
};
}
const gated = mapElementJsonPatchToEditIntents(patches, elements);
if (!gated.ok) {
return {
content: [
{
type: 'text',
text: `Could not apply the edit: ${gated.reason}. Nothing was changed.`,
},
],
details: {
sceneId,
intents: null,
updateCount: 0,
refuseReason: gated.reason,
},
isError: true,
};
}
const inventory = buildElementInventory(elements);
return {
content: [
{
type: 'text',
text:
`Validated a JSON Patch batch for ${gated.targetIds.length} element(s). ` +
`Do not claim the canvas already changed: the client may still refuse a stale ` +
`slide, a locked target, an active canvas gesture, or a missing Pro edit session.`,
},
],
details: {
sceneId,
intents: gated.intents,
updateCount: gated.targetIds.length,
targetElementTypes: targetTypes(gated.targetIds, inventory),
targetElementFingerprints: targetFingerprints(gated.targetIds, elements),
inventoryFingerprint: elementInventorySnapshotFingerprint(elements),
},
};
},
};
}