agent-panel

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Agent Panel

Agent面板

The
EFAgentPanel
captures
ef-edit
CustomEvents dispatched whenever a user makes a meaningful composition edit in the GUI, deduplicates them by element+property via a keyed registry, groups them by selector, and renders a copy-able coding-agent prompt.
EFAgentPanel
会捕获用户在GUI中进行有意义的合成编辑时触发的
ef-edit
CustomEvent,通过带键的注册表按元素+属性去重,按选择器分组,并渲染可复制的编码Agent提示语。

Event Pipeline

事件管道

GUI interaction (canvas drag, inspector change, trim drag, loop toggle)
  └─ component.dispatchEvent(createEditCustomEvent(editEvent))
       └─ [bubbles: true, composed: true]
            └─ EFWorkbench listener → agentPanel.addEdit(event)
                 └─ Map<editChangeKey, EditEvent> registry (deduplicates)
                      └─ groupEditsBySelector() → buildAgentPrompt() → copy button
EFWorkbench
registers
addEventListener('ef-edit', ...)
on itself — because
ef-edit
bubbles and is composed, any component in any slot (timeline, hierarchy, canvas) can dispatch it without special routing.
GUI交互(画布拖拽、检查器变更、修剪拖拽、循环切换)
  └─ component.dispatchEvent(createEditCustomEvent(editEvent))
       └─ [bubbles: true, composed: true]
            └─ EFWorkbench 监听器 → agentPanel.addEdit(event)
                 └─ Map<editChangeKey, EditEvent> 注册表(去重)
                      └─ groupEditsBySelector() → buildAgentPrompt() → 复制按钮
EFWorkbench
在自身上注册
addEventListener('ef-edit', ...)
—— 由于
ef-edit
支持冒泡且已合成,任何插槽(时间线、层级结构、画布)中的组件都无需特殊路由即可触发该事件。

Key Types (
editEvents.ts
)

关键类型(
editEvents.ts

ts
// EditEvent — what the panel accumulates
interface EditEvent {
  operation: EditOperation;
  description: string;   // human sentence
  selector: string;      // CSS path from composition root
  elementHtml: string;   // cleaned outerHTML snippet
  timestamp: number;
}

// Operation union — extend here when adding new interaction types
type EditOperation =
  | { type: "element-property-changed"; elementId; tagName; label; property; oldValue; newValue }
  | { type: "element-moved";   elementId; tagName; label; fromX; fromY; toX; toY }
  | { type: "element-resized"; elementId; tagName; label; x; y; width; height }
  | { type: "element-rotated"; elementId; tagName; label; rotation }
Registry key format — determines deduplication:
  • Property change:
    selector::prop:propName
  • Move/resize/rotate:
    selector::move
    ,
    selector::resize
    ,
    selector::rotate
ts
// EditEvent — what the panel accumulates
interface EditEvent {
  operation: EditOperation;
  description: string;   // human sentence
  selector: string;      // CSS path from composition root
  elementHtml: string;   // cleaned outerHTML snippet
  timestamp: number;
}

// Operation union — extend here when adding new interaction types
type EditOperation =
  | { type: "element-property-changed"; elementId; tagName; label; property; oldValue; newValue }
  | { type: "element-moved";   elementId; tagName; label; fromX; fromY; toX; toY }
  | { type: "element-resized"; elementId; tagName; label; x; y; width; height }
  | { type: "element-rotated"; elementId; tagName; label; rotation }
注册表键格式 —— 用于判断去重规则:
  • 属性变更:
    selector::prop:propName
  • 移动/缩放/旋转:
    selector::move
    ,
    selector::resize
    ,
    selector::rotate

Pattern: Adding a New ef-edit Dispatch Point

模式:添加新的ef-edit触发点

ts
import {
  createEditCustomEvent, buildSelectorPath, getElementHtml,
  buildEditDescription, type ElementPropertyChangedOperation,
} from "../../editEvents.js";

const oldValue = element.someProperty;
element.someProperty = newValue;

const op: ElementPropertyChangedOperation = {
  type: "element-property-changed",
  elementId: el.id,
  tagName: el.tagName.toLowerCase(),
  label: el.textContent?.trim().slice(0, 30) || el.id || el.tagName.toLowerCase(),
  property: "attr-name",   // attribute name as used in HTML source
  oldValue,
  newValue,
};
this.dispatchEvent(createEditCustomEvent({
  operation: op,
  description: buildEditDescription(op),
  selector: buildSelectorPath(el),
  elementHtml: getElementHtml(el),
  timestamp: Date.now(),
}));
buildPropertyEditEvent(element, property, oldValue, newValue)
in
EFInspector.ts
is a shorter form for simple property edits.
ts
import {
  createEditCustomEvent, buildSelectorPath, getElementHtml,
  buildEditDescription, type ElementPropertyChangedOperation,
} from "../../editEvents.js";

const oldValue = element.someProperty;
element.someProperty = newValue;

const op: ElementPropertyChangedOperation = {
  type: "element-property-changed",
  elementId: el.id,
  tagName: el.tagName.toLowerCase(),
  label: el.textContent?.trim().slice(0, 30) || el.id || el.tagName.toLowerCase(),
  property: "attr-name",   // attribute name as used in HTML source
  oldValue,
  newValue,
};
this.dispatchEvent(createEditCustomEvent({
  operation: op,
  description: buildEditDescription(op),
  selector: buildSelectorPath(el),
  elementHtml: getElementHtml(el),
  timestamp: Date.now(),
}));
EFInspector.ts
中的
buildPropertyEditEvent(element, property, oldValue, newValue)
是简单属性编辑的简化写法。

Finding Current Dispatch Points

查找当前触发点

bash
undefined
bash
undefined

Every call site that produces an ef-edit event

Every call site that produces an ef-edit event

rg 'createEditCustomEvent' elements/packages/elements/src/
rg 'createEditCustomEvent' elements/packages/elements/src/

Track subclasses that override render() — each must bind @trim-change-end

Track subclasses that override render() — each must bind @trim-change-end

rg 'override render' elements/packages/elements/src/gui/timeline/tracks/
rg 'override render' elements/packages/elements/src/gui/timeline/tracks/

Registered element schemas

Registered element schemas

rg 'SCHEMA_REGISTRY' elements/packages/elements/src/gui/elementPropertySchema.ts
undefined
rg 'SCHEMA_REGISTRY' elements/packages/elements/src/gui/elementPropertySchema.ts
undefined

What is NOT Wired (intentional)

未接入的功能(有意设计)

Playback controls (play/pause/seek), pan-zoom/zoom level, and toolbar settings are display-only state — they should not produce
ef-edit
events. Hierarchy reorder (
hierarchy-reorder
from
EFHierarchy
) would require a new
ElementReorderedOperation
type; not yet implemented.
播放控制(播放/暂停/跳转)、平移缩放/缩放级别、工具栏设置均为仅显示状态 —— 它们不应触发
ef-edit
事件。层级重排(来自
EFHierarchy
hierarchy-reorder
)需要新增
ElementReorderedOperation
类型,目前尚未实现。

Shadow DOM Event Routing Gotcha

Shadow DOM事件路由陷阱

trim-change-end
bubbles with
composed: true
from
EFTrimHandles
.
TrackItem.render()
binds
@trim-change-end
on the
ef-trim-handles
element.
EFVideoTrack
overrides
render()
entirely and provides its own
ef-trim-handles
binding.
Any future track subclass that overrides
render()
and includes
ef-trim-handles
must bind both
@trim-change
and
@trim-change-end
— the base
TrackItem.render()
handlers are not inherited when
render()
is overridden.
trim-change-end
会从
EFTrimHandles
composed: true
的方式冒泡。
TrackItem.render()
会在
ef-trim-handles
元素上绑定
@trim-change-end
EFVideoTrack
完全重写了
render()
并提供了自己的
ef-trim-handles
绑定。
任何未来重写
render()
且包含
ef-trim-handles
的轨道子类都必须同时绑定**
@trim-change
@trim-change-end
** —— 当
render()
被重写时,基类
TrackItem.render()
的处理器不会被继承。

Element Property Schema (
elementPropertySchema.ts
)

元素属性Schema(
elementPropertySchema.ts

Static descriptors drive the inspector UI. Register new element types at the bottom via
SCHEMA_REGISTRY
. Use the factory functions:
  • timeDescriptor(attr, label, opts)
    — time values (stored as
    ${ms}ms
    attributes)
  • enumDescriptor(attr, label, options, condition?)
  • boolDescriptor(attr, label, condition?)
  • numberDescriptor(attr, label, opts)
  • stringDescriptor(attr, label)
Elements with no registry entry are invisible to the inspector. Check
elementPropertySchema.test.ts
for the test pattern.
静态描述器驱动检查器UI。在文件底部通过
SCHEMA_REGISTRY
注册新元素类型。使用以下工厂函数:
  • timeDescriptor(attr, label, opts)
    —— 时间值(以
    ${ms}ms
    属性存储)
  • enumDescriptor(attr, label, options, condition?)
  • boolDescriptor(attr, label, condition?)
  • numberDescriptor(attr, label, opts)
  • stringDescriptor(attr, label)
没有注册表条目的元素对检查器不可见。可查看
elementPropertySchema.test.ts
了解测试模式。

Testing ef-edit Events

测试ef-edit事件

Unit tests (Node/vitest): use for
editEvents.ts
helpers —
editChangeKey
,
rollUpEdits
,
groupEditsBySelector
,
buildAgentPrompt
.
Browser tests (Chromium/vitest): required for any test involving DOM events or shadow DOM.
For trim ef-edit, dispatch
trim-change-end
directly on the
ef-trim-handles
host element — do not try to simulate full pointer events through shadow DOM layers, as pointer capture mechanics are unreliable in test environments:
ts
const trimHandles = track.shadowRoot?.querySelector("ef-trim-handles");
trimHandles?.dispatchEvent(new CustomEvent("trim-change-end", {
  detail: { elementId: video.id, type: "start" },
  bubbles: true, composed: true,
}));
For loop ef-edit, click the loop button via
shadowRoot.querySelector("button[title='Loop']").click()
.
Listen at
document
level —
ef-edit
is
bubbles: true, composed: true
, so it propagates from any component to
document
.
单元测试(Node/vitest):用于测试
editEvents.ts
中的辅助函数 ——
editChangeKey
,
rollUpEdits
,
groupEditsBySelector
,
buildAgentPrompt
浏览器测试(Chromium/vitest):涉及DOM事件或Shadow DOM的测试必须使用浏览器测试。
对于修剪操作的ef-edit测试,直接在
ef-trim-handles
宿主元素上触发
trim-change-end
—— 不要尝试通过Shadow DOM层模拟完整的指针事件,因为测试环境中的指针捕获机制不可靠:
ts
const trimHandles = track.shadowRoot?.querySelector("ef-trim-handles");
trimHandles?.dispatchEvent(new CustomEvent("trim-change-end", {
  detail: { elementId: video.id, type: "start" },
  bubbles: true, composed: true,
}));
对于循环操作的ef-edit测试,通过
shadowRoot.querySelector("button[title='Loop']").click()
点击循环按钮。
document
级别监听事件 ——
ef-edit
bubbles: true, composed: true
属性使其可以从任何组件传播到
document