@forgeax/interface 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +51 -0
  2. package/dist/package/browser/ApplicationShell.js +1 -1
  3. package/dist/package/browser/appHostBootstrap.js +0 -2
  4. package/dist/package/browser/application.d.ts +0 -10
  5. package/dist/package/browser/application.js +0 -10
  6. package/dist/package/browser/components/CommandPalette/CommandPalette.js +1 -1
  7. package/dist/package/browser/core/app-shell/host.js +5 -2
  8. package/dist/package/browser/core/app-shell/types.d.ts +3 -1
  9. package/dist/package/browser/core/extensions/builtin-commands.js +1 -0
  10. package/dist/package/browser/core/extensions/trajectory.js +1 -1
  11. package/dist/package/browser/i18n/locales/en.json +0 -10
  12. package/dist/package/browser/i18n/locales/zh.json +0 -10
  13. package/dist/package/browser/lib/action-dom-discovery.js +1 -1
  14. package/dist/package/browser/lib/ai-intents.js +1 -1
  15. package/dist/package/browser/lib/builtin-actions.js +1 -1
  16. package/dist/package/browser/lib/global-shortcuts.d.ts +5 -74
  17. package/dist/package/browser/lib/global-shortcuts.js +29 -228
  18. package/dist/package/browser/lib/ui-action-highlight.js +1 -1
  19. package/dist/package/browser/lib/ui-bridge.js +1 -1
  20. package/dist/package/browser/lib/ui-trajectory.js +1 -1
  21. package/dist/package/browser/lib/vag-action-bridge.js +1 -1
  22. package/dist/package/node/ApplicationShell.js +1 -1
  23. package/dist/package/node/appHostBootstrap.js +0 -2
  24. package/dist/package/node/application.js +0 -10
  25. package/dist/package/node/components/CommandPalette/CommandPalette.js +1 -1
  26. package/dist/package/node/core/app-shell/host.js +5 -2
  27. package/dist/package/node/core/extensions/builtin-commands.js +1 -0
  28. package/dist/package/node/core/extensions/trajectory.js +1 -1
  29. package/dist/package/node/i18n/locales/en.json +0 -10
  30. package/dist/package/node/i18n/locales/zh.json +0 -10
  31. package/dist/package/node/lib/action-dom-discovery.js +1 -1
  32. package/dist/package/node/lib/ai-intents.js +1 -1
  33. package/dist/package/node/lib/builtin-actions.js +1 -1
  34. package/dist/package/node/lib/global-shortcuts.js +29 -228
  35. package/dist/package/node/lib/ui-action-highlight.js +1 -1
  36. package/dist/package/node/lib/ui-bridge.js +1 -1
  37. package/dist/package/node/lib/ui-trajectory.js +1 -1
  38. package/dist/package/node/lib/vag-action-bridge.js +1 -1
  39. package/package.json +2 -2
  40. package/dist/package/browser/core/extensions/editor-commands.d.ts +0 -2
  41. package/dist/package/browser/core/extensions/editor-commands.js +0 -182
  42. package/dist/package/browser/lib/action-registry.d.ts +0 -94
  43. package/dist/package/browser/lib/action-registry.js +0 -210
  44. package/dist/package/node/core/extensions/editor-commands.js +0 -182
  45. package/dist/package/node/lib/action-registry.js +0 -210
@@ -1,94 +0,0 @@
1
- /** action-registry —— UI 语义操作层(产品 AI 化 P0)的单一真值源:
2
- * 「本产品当前暴露哪些可被人 / AI 调用的功能」。
3
- *
4
- * 一张注册表服务三个消费者(压缩公理:消掉「AI 能做的和人能做的是不是同一套?」):
5
- * 1. 按钮 onClick —— 组件经 `dispatchAction(id, args)` 派发(与 AI 同一条路);
6
- * 2. 命令面板(P2,cmdk 激活时数据源即本表);
7
- * 3. AI 工具调用 —— `ui_snapshot` 读清单 / `ui_invoke` 派发,经 lib/ui-bridge.ts
8
- * 的 perception 往返接到编排层(@forgeax/orchestrator)。
9
- *
10
- * 契约形态(评审 2.1):`schema` 的 SSOT 是 **JSON Schema 纯对象**(AI 侧 ToolSpec 与
11
- * postMessage 结构化克隆都只吃 JSON;函数永不过 wire)。`available` / `run` 只活在
12
- * 本模块,manifest(`buildManifest`)只导出可序列化子集。
13
- *
14
- * headless 边界(方案 §5):`surface:'both'` 的 action,UI `run()` 必须调 server 的
15
- * 同一 HTTP API(server 是行为 SSOT),UI 侧只许加视觉反馈——不许长出独立业务逻辑。
16
- */
17
- /** 与编排层 trust-gate 的 Capability 8 类对齐(权限分级的声明值,manifest 里出墙)。 */
18
- export type UiCapability = 'read' | 'write' | 'delete' | 'exec' | 'network' | 'credential' | 'delegate' | 'other';
19
- /** 派发结果(评审 2.2 act→observe 合并):
20
- * - completed:已完成,`stateDigest` 携带可观察的状态变化(多数场景免掉后续 snapshot);
21
- * - accepted:已受理、异步在跑(慢 action 快速返回,勿等勿重试;P0 期完成态靠下次 snapshot 观察);
22
- * - rejected:未执行,`reason` 给人话原因(未注册 / 不可用 / 参数校验失败 / run 抛错)。 */
23
- export interface UiActionResult {
24
- status: 'completed' | 'accepted' | 'rejected';
25
- reason?: string;
26
- stateDigest?: unknown;
27
- }
28
- /** JSON Schema 纯对象(不引 zod 类型;zod 只是书写便利,登记时即转换落表)。 */
29
- export type JsonSchemaObject = Record<string, unknown>;
30
- export interface UiActionDef {
31
- /** 稳定 id,'domain.verb' 形("game.switch" / "panel.toggle_sidebar")。 */
32
- id: string;
33
- /** 人读标题(菜单 / 命令面板 / 权限卡展示)。 */
34
- title: string;
35
- /** AI 读说明(manifest 出墙;写清做什么 + 何时用)。 */
36
- description?: string;
37
- /** 参数契约(JSON Schema 纯对象)。缺省 = 无参数。 */
38
- schema?: JsonSchemaObject;
39
- /** 权限分级声明 —— 编排层 trust-gate 按此弹卡/直放(delete/credential 会请求用户确认)。 */
40
- capability: UiCapability;
41
- /** headless 可用性:'ui'(纯视图,须 UI 在线)/ 'server'/'both'(状态型,server 是 SSOT)。 */
42
- surface?: 'ui' | 'server' | 'both';
43
- /** 预期执行时长(ms)——编排层往返超时据此放宽;慢 action 的正道是快速回 accepted。 */
44
- timeoutMs?: number;
45
- /** 当前是否可调。返回 string = 不可用 + 人话原因(AI 可读可解释);缺省恒可用。 */
46
- available?: () => true | string;
47
- /** P1-9 一等工具化:标 true → 编排层从 manifest 派生独立 ToolSpec(ui_act_*)下发
48
- * 模型,免一次 snapshot 发现往返。只给高频 action 打标(编排层有数量上限)。 */
49
- firstClass?: boolean;
50
- /** 「人」界面(命令面板)用的**动态候选值**提供器,按参数名给一列合法取值 —— 让
51
- * 自由文本参数(如 game.switch 的 slug)变成下拉,避免瞎填后触发 server 404。
52
- * 仅活在客户端(同 run/available,不进 manifest);AI 侧靠 ui_snapshot / 状态片发现,
53
- * 不依赖此。静态 `schema.enum` 优先于此(两者都无则退回文本框)。 */
54
- choices?: Record<string, () => string[] | Promise<string[]>>;
55
- /** 执行体。返回 void 视为 completed;抛错翻成 rejected(fail-soft,不炸往返)。 */
56
- run: (args: Record<string, unknown>) => UiActionResult | void | Promise<UiActionResult | void>;
57
- }
58
- /** snapshot 状态摘要的一片(评审 2.5:注册式 derive,禁手写台账)。selector 从 store
59
- * 等真值源读,值必须可序列化(过 JSON.stringify)。 */
60
- export type StateSliceSelector = () => unknown;
61
- /** 注册一个 action(同 id 重复注册 = 替换,幂等;HMR 安全)。返回注销函数。 */
62
- export declare function registerAction(def: UiActionDef): () => void;
63
- /** 注册一片状态摘要(同 id 替换,幂等)。返回注销函数。 */
64
- export declare function registerStateSlice(id: string, selector: StateSliceSelector): () => void;
65
- /** 注册表(action 或 state slice)变更订阅 —— ui-bridge 据此 debounce 重推 manifest。 */
66
- export declare function onRegistryChange(cb: () => void): () => void;
67
- export declare function getAction(id: string): UiActionDef | undefined;
68
- /** AI 派发时打的来源标记;window 事件供 ghost 高亮层(P1)与 telemetry / 轨迹追踪衔接。
69
- * detail 形:`{ id, source, args }` —— args 让轨迹追踪(lib/ui-trajectory)记得下「做了啥」,
70
- * ghost 高亮只用 id/source(向后兼容,多带的字段无害)。 */
71
- export declare const UI_ACTION_DISPATCH_EVENT = "forgeax:ui-action-dispatch";
72
- /** 派发一个 action —— 按钮与 AI 共用的**唯一**执行入口(未注册的 id 拒绝,fail-closed)。 */
73
- export declare function dispatchAction(id: string, args?: Record<string, unknown>, opts?: {
74
- source?: 'human' | 'ai';
75
- }): Promise<UiActionResult>;
76
- export interface UiActionSummary {
77
- id: string;
78
- title: string;
79
- available: boolean;
80
- reason?: string;
81
- /** detail:'schema' 时补上。 */
82
- description?: string;
83
- inputSchema?: JsonSchemaObject;
84
- }
85
- /** ui_snapshot 的 action 视图。分层(评审 2.7):默认轻量清单;detail:'schema' + ids
86
- * 按需展开 schema 与详细说明(长尾几百 action 也不炸 token)。 */
87
- export declare function snapshotActions(detail?: string, ids?: string[]): UiActionSummary[];
88
- /** ui_snapshot 的状态摘要:逐片 derive,单片异常不传染(fail-soft)。 */
89
- export declare function snapshotState(): Record<string, unknown>;
90
- /** manifest(POST /:sid/ui-manifest 的 payload)—— 只含可序列化声明,函数永不出墙。
91
- * 它是编排层 trust-gate 的权限输入:capability 必须如实声明。 */
92
- export declare function buildManifest(): Array<Record<string, unknown>>;
93
- /** 测试用:清空注册表(生产代码不要调)。 */
94
- export declare function __resetRegistryForTest(): void;
@@ -1,210 +0,0 @@
1
- /** action-registry —— UI 语义操作层(产品 AI 化 P0)的单一真值源:
2
- * 「本产品当前暴露哪些可被人 / AI 调用的功能」。
3
- *
4
- * 一张注册表服务三个消费者(压缩公理:消掉「AI 能做的和人能做的是不是同一套?」):
5
- * 1. 按钮 onClick —— 组件经 `dispatchAction(id, args)` 派发(与 AI 同一条路);
6
- * 2. 命令面板(P2,cmdk 激活时数据源即本表);
7
- * 3. AI 工具调用 —— `ui_snapshot` 读清单 / `ui_invoke` 派发,经 lib/ui-bridge.ts
8
- * 的 perception 往返接到编排层(@forgeax/orchestrator)。
9
- *
10
- * 契约形态(评审 2.1):`schema` 的 SSOT 是 **JSON Schema 纯对象**(AI 侧 ToolSpec 与
11
- * postMessage 结构化克隆都只吃 JSON;函数永不过 wire)。`available` / `run` 只活在
12
- * 本模块,manifest(`buildManifest`)只导出可序列化子集。
13
- *
14
- * headless 边界(方案 §5):`surface:'both'` 的 action,UI `run()` 必须调 server 的
15
- * 同一 HTTP API(server 是行为 SSOT),UI 侧只许加视觉反馈——不许长出独立业务逻辑。
16
- */
17
- // ─── 注册表本体(模块级 Map,页级单例)────────────────────────────────────────
18
- const actions = new Map();
19
- const stateSlices = new Map();
20
- const changeListeners = new Set();
21
- function notifyChange() {
22
- for (const cb of changeListeners) {
23
- try {
24
- cb();
25
- }
26
- catch {
27
- /* listener 异常不传染 */
28
- }
29
- }
30
- }
31
- /** 注册一个 action(同 id 重复注册 = 替换,幂等;HMR 安全)。返回注销函数。 */
32
- export function registerAction(def) {
33
- actions.set(def.id, def);
34
- notifyChange();
35
- return () => {
36
- if (actions.get(def.id) === def) {
37
- actions.delete(def.id);
38
- notifyChange();
39
- }
40
- };
41
- }
42
- /** 注册一片状态摘要(同 id 替换,幂等)。返回注销函数。 */
43
- export function registerStateSlice(id, selector) {
44
- stateSlices.set(id, selector);
45
- notifyChange();
46
- return () => {
47
- if (stateSlices.get(id) === selector) {
48
- stateSlices.delete(id);
49
- notifyChange();
50
- }
51
- };
52
- }
53
- /** 注册表(action 或 state slice)变更订阅 —— ui-bridge 据此 debounce 重推 manifest。 */
54
- export function onRegistryChange(cb) {
55
- changeListeners.add(cb);
56
- return () => changeListeners.delete(cb);
57
- }
58
- export function getAction(id) {
59
- return actions.get(id);
60
- }
61
- // ─── 派发单入口(评审 2.4:data-* 只做发现,执行永远走这里)──────────────────
62
- /** AI 派发时打的来源标记;window 事件供 ghost 高亮层(P1)与 telemetry / 轨迹追踪衔接。
63
- * detail 形:`{ id, source, args }` —— args 让轨迹追踪(lib/ui-trajectory)记得下「做了啥」,
64
- * ghost 高亮只用 id/source(向后兼容,多带的字段无害)。 */
65
- export const UI_ACTION_DISPATCH_EVENT = 'forgeax:ui-action-dispatch';
66
- /** 极简 JSON Schema 参数校验(P0:required + 顶层 properties 的原始类型)。
67
- * 不引 ajv——契约漂移由编排层契约测试兜,这里挡住明显错参即可(Fail Fast)。 */
68
- function validateArgs(schema, args) {
69
- if (!schema)
70
- return true;
71
- const required = Array.isArray(schema.required) ? schema.required : [];
72
- for (const k of required) {
73
- if (typeof k === 'string' && !(k in args))
74
- return `missing required arg "${k}"`;
75
- }
76
- const props = schema.properties && typeof schema.properties === 'object'
77
- ? schema.properties
78
- : {};
79
- for (const [k, v] of Object.entries(args)) {
80
- const p = props[k];
81
- if (!p)
82
- continue; // 未声明的多余参数放过(向前兼容),由 run 自行忽略
83
- const t = p.type;
84
- if (typeof t === 'string') {
85
- const actual = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
86
- const okType = (t === 'string' && actual === 'string') ||
87
- (t === 'number' && actual === 'number') ||
88
- (t === 'integer' && actual === 'number' && Number.isInteger(v)) ||
89
- (t === 'boolean' && actual === 'boolean') ||
90
- (t === 'object' && actual === 'object') ||
91
- (t === 'array' && actual === 'array');
92
- if (!okType)
93
- return `arg "${k}" should be ${t}, got ${actual}`;
94
- }
95
- if (Array.isArray(p.enum) && !p.enum.includes(v)) {
96
- return `arg "${k}" must be one of ${JSON.stringify(p.enum)}`;
97
- }
98
- }
99
- return true;
100
- }
101
- /** 派发一个 action —— 按钮与 AI 共用的**唯一**执行入口(未注册的 id 拒绝,fail-closed)。 */
102
- export async function dispatchAction(id, args = {}, opts = {}) {
103
- const src = opts.source ?? 'human';
104
- // [fx-action-trace] 定位「点了/调了没反应」的最小充分打点:
105
- // ① entry —— 派发是否发生 + args/source(缺此行 = 根本没触发 dispatch)。
106
- // ② terminal —— 唯一终态行:completed/accepted 带 stateDigest(判「到底改了啥」),
107
- // rejected 带 reason(未注册/不可用/参数错/run 抛错 已统一收敛进 reason)。
108
- // 两行即可定位:有 entry 无 terminal = 卡在 run();rejected+reason = 为何被拒;
109
- // completed 但 stateDigest 没变 = action 效果与 UI 脱节。故只留这两点(+抛错补栈)。
110
- console.info(`%c[fx-action]%c ▶ ${id} (${src})`, 'color:#818cf8;font-weight:bold', 'color:inherit', { args });
111
- const done = (r) => {
112
- if (r.status === 'rejected') {
113
- console.warn(`[fx-action] ✗ ${id} rejected: ${r.reason ?? ''}`);
114
- }
115
- else {
116
- console.info(`%c[fx-action]%c ✔ ${id} → ${r.status}`, 'color:#34d399;font-weight:bold', 'color:inherit', r.stateDigest !== undefined ? { stateDigest: r.stateDigest } : {});
117
- }
118
- return r;
119
- };
120
- const def = actions.get(id);
121
- if (!def)
122
- return done({ status: 'rejected', reason: `unknown action "${id}" (not in the registry)` });
123
- const avail = def.available ? safeAvailable(def) : true;
124
- if (avail !== true)
125
- return done({ status: 'rejected', reason: avail });
126
- const valid = validateArgs(def.schema, args);
127
- if (valid !== true)
128
- return done({ status: 'rejected', reason: valid });
129
- try {
130
- window.dispatchEvent(new CustomEvent(UI_ACTION_DISPATCH_EVENT, { detail: { id, source: src, args } }));
131
- }
132
- catch {
133
- /* SSR / no window — ignore */
134
- }
135
- try {
136
- const out = await def.run(args);
137
- return done(out ?? { status: 'completed' });
138
- }
139
- catch (e) {
140
- console.error(`[fx-action] ✗ ${id} threw`, e); // 抛错补一行栈(定位真异常)
141
- return done({ status: 'rejected', reason: `action "${id}" threw: ${e instanceof Error ? e.message : String(e)}` });
142
- }
143
- }
144
- function safeAvailable(def) {
145
- try {
146
- return def.available();
147
- }
148
- catch (e) {
149
- return `availability check threw: ${e instanceof Error ? e.message : String(e)}`;
150
- }
151
- }
152
- /** ui_snapshot 的 action 视图。分层(评审 2.7):默认轻量清单;detail:'schema' + ids
153
- * 按需展开 schema 与详细说明(长尾几百 action 也不炸 token)。 */
154
- export function snapshotActions(detail, ids) {
155
- const expand = detail === 'schema' ? new Set(ids ?? []) : null;
156
- const out = [];
157
- for (const def of actions.values()) {
158
- const avail = def.available ? safeAvailable(def) : true;
159
- const row = {
160
- id: def.id,
161
- title: def.title,
162
- available: avail === true,
163
- ...(avail === true ? {} : { reason: avail }),
164
- };
165
- if (expand?.has(def.id)) {
166
- if (def.description)
167
- row.description = def.description;
168
- row.inputSchema = def.schema ?? { type: 'object', properties: {} };
169
- }
170
- out.push(row);
171
- }
172
- return out;
173
- }
174
- /** ui_snapshot 的状态摘要:逐片 derive,单片异常不传染(fail-soft)。 */
175
- export function snapshotState() {
176
- const out = {};
177
- for (const [id, selector] of stateSlices) {
178
- try {
179
- out[id] = selector();
180
- }
181
- catch (e) {
182
- out[id] = { error: `state slice threw: ${e instanceof Error ? e.message : String(e)}` };
183
- }
184
- }
185
- return out;
186
- }
187
- /** manifest(POST /:sid/ui-manifest 的 payload)—— 只含可序列化声明,函数永不出墙。
188
- * 它是编排层 trust-gate 的权限输入:capability 必须如实声明。 */
189
- export function buildManifest() {
190
- const out = [];
191
- for (const def of actions.values()) {
192
- out.push({
193
- id: def.id,
194
- title: def.title,
195
- ...(def.description ? { description: def.description } : {}),
196
- inputSchema: def.schema ?? { type: 'object', properties: {} },
197
- capability: def.capability,
198
- ...(def.surface ? { surface: def.surface } : {}),
199
- ...(def.timeoutMs ? { timeoutMs: def.timeoutMs } : {}),
200
- ...(def.firstClass ? { firstClass: true } : {}),
201
- });
202
- }
203
- return out;
204
- }
205
- /** 测试用:清空注册表(生产代码不要调)。 */
206
- export function __resetRegistryForTest() {
207
- actions.clear();
208
- stateSlices.clear();
209
- changeListeners.clear();
210
- }
@@ -1,182 +0,0 @@
1
- // packages/interface/src/core/extensions/editor-commands.ts
2
- //
3
- // Command-bus wrappers for editor-domain actions (Play/Stop/Save/Undo/Redo/
4
- // select-all/delete/frame/toggle-display/deselect/…). These commands are the
5
- // single entry point the top menu bar (builtin-menus.ts) and the keyboard
6
- // router share so palette/menu/shortcut hit the same call path.
7
- //
8
- // The interface package is editor-agnostic (lint:agnostic forbids importing
9
- // @forgeax/editor), so every command routes through the injected
10
- // KeyboardRouterDeps — the host editor (studio/main.tsx) calls
11
- // registerKeyboardRouterDeps() at boot BEFORE <App> mounts, and each execute
12
- // resolves the deps LAZILY via getKeyboardRouterDeps(). Capture-at-setup
13
- // would freeze deps to null since setup runs during bootstrapAppHost() which
14
- // happens before main.tsx has finished mounting <App>; lazy resolution keeps
15
- // this file boot-order-safe regardless of whether registration happened first.
16
- //
17
- // import + reveal: NOT registered. Content-browser import and OS file-reveal
18
- // live outside KeyboardRouterDeps and can't be reached editor-agnostically
19
- // from the interface foundation. The corresponding menu items in builtin-menus.ts carry no
20
- // commandId and stay disabled — the correct signal for a not-yet-wired
21
- // capability. Flip them on when a dep or command later lands.
22
- //
23
- // reloadPreview: registered as a best-effort event emit (`preview:reload` on
24
- // ctx.bus). No listener consumes it yet — the Play iframe self-reload lives
25
- // in @forgeax/editor/PlaySurface which the interface foundation cannot import. Emitting the event
26
- // keeps the menu item live and gives a future HMR/preview owner a stable hook
27
- // to subscribe to without touching this file.
28
- import { APPLICATION_KEYBINDING_SCOPE } from '../contextual-keybindings.js';
29
- import { getKeyboardRouterDeps } from '../../lib/global-shortcuts.js';
30
- import { executeFocusedTextEditAction } from '../../lib/text-edit-actions.js';
31
- /** Resolve the injected router deps or throw a clear error naming the missing
32
- * wiring — makes a mis-boot fail loudly at command-invoke time instead of
33
- * silently no-op'ing. */
34
- function requireDeps() {
35
- const deps = getKeyboardRouterDeps();
36
- if (!deps)
37
- throw new Error('editor-commands: editor deps not injected (host must call registerKeyboardRouterDeps before invoking editor.* commands)');
38
- return deps;
39
- }
40
- export const editorCommandsExtension = {
41
- id: 'editor-commands',
42
- version: '1.0.0',
43
- requires: ['commands', 'keybindings'],
44
- setup(ctx) {
45
- const { registerCommand, host } = ctx;
46
- const cleanups = [];
47
- cleanups.push(registerCommand({
48
- id: 'editor.play',
49
- title: '开始预览 (Play)',
50
- execute: () => {
51
- requireDeps().dispatch({ kind: 'play' }, 'human');
52
- return { status: 'completed' };
53
- },
54
- }));
55
- cleanups.push(registerCommand({
56
- id: 'editor.stop',
57
- title: '停止预览 (Stop)',
58
- execute: () => {
59
- requireDeps().dispatch({ kind: 'stop' }, 'human');
60
- return { status: 'completed' };
61
- },
62
- }));
63
- cleanups.push(registerCommand({
64
- id: 'editor.toggleDisplay',
65
- title: '切换 Scene / Game 视图',
66
- execute: () => {
67
- const deps = requireDeps();
68
- const next = deps.getDisplay() === 'scene' ? 'game' : 'scene';
69
- deps.dispatch({ kind: 'setDisplay', display: next }, 'human');
70
- return { status: 'completed' };
71
- },
72
- }));
73
- cleanups.push(registerCommand({
74
- id: 'editor.undo',
75
- title: '撤销',
76
- execute: () => {
77
- requireDeps().undo();
78
- return { status: 'completed' };
79
- },
80
- }));
81
- cleanups.push(registerCommand({
82
- id: 'editor.redo',
83
- title: '重做',
84
- execute: () => {
85
- requireDeps().redo();
86
- return { status: 'completed' };
87
- },
88
- }));
89
- cleanups.push(registerCommand({
90
- id: 'editor.save',
91
- title: '保存',
92
- execute: () => {
93
- requireDeps().save();
94
- return { status: 'completed' };
95
- },
96
- }));
97
- // Application-scoped Mod+S runs before legacy edit shortcuts and is not
98
- // gated on the viewport surface anchor. Material / Texture / other asset
99
- // document pages replace the viewport dock layout, so isEditorSurfaceActive()
100
- // is false there — without this binding Ctrl+S falls through to the browser
101
- // "save page" dialog instead of editor.save → material-first save diversion.
102
- cleanups.push(host.keybindings.register({
103
- commandId: 'editor.save',
104
- keys: 'Mod+S',
105
- scope: APPLICATION_KEYBINDING_SCOPE,
106
- allowInEditable: true,
107
- priority: 100,
108
- }));
109
- cleanups.push(registerCommand({
110
- id: 'editor.selectAll',
111
- title: '全选实体',
112
- execute: async () => {
113
- if (await executeFocusedTextEditAction('selectAll')) {
114
- return { status: 'completed' };
115
- }
116
- requireDeps().selectAllEntities();
117
- return { status: 'completed' };
118
- },
119
- }));
120
- cleanups.push(registerCommand({
121
- id: 'editor.deselect',
122
- title: '清除选择',
123
- execute: () => {
124
- requireDeps().dispatch({ kind: 'setSelection', id: null }, 'human');
125
- return { status: 'completed' };
126
- },
127
- }));
128
- cleanups.push(registerCommand({
129
- id: 'editor.frameSelected',
130
- title: '聚焦所选',
131
- execute: () => {
132
- requireDeps().dispatch({ kind: 'requestFrame' }, 'human');
133
- return { status: 'completed' };
134
- },
135
- }));
136
- cleanups.push(registerCommand({
137
- id: 'editor.delete',
138
- title: '删除所选实体',
139
- execute: () => {
140
- const deps = requireDeps();
141
- const ids = deps.getEntitySelection();
142
- if (ids.length > 0)
143
- deps.deleteEntities(ids);
144
- return { status: 'completed' };
145
- },
146
- }));
147
- // Partial: bus event only, no listener wired yet (see file header). A
148
- // future preview/HMR owner in a standalone application subscribes to 'preview:reload'
149
- // and forwards to the Play iframe self-reload. `ctx.bus.emit` is typed
150
- // against AppBusEventMap which extends Record<string, unknown>, so an
151
- // ad-hoc topic name is accepted with an `unknown` payload.
152
- cleanups.push(registerCommand({
153
- id: 'editor.reloadPreview',
154
- title: '重载预览 (partial: 事件已发,尚无消费者)',
155
- execute: () => {
156
- // Not calling requireDeps() — reloadPreview is decoupled from the
157
- // editor gateway; the current path is a bus-event handoff, and a
158
- // future consumer may not even need the router deps.
159
- ctx.bus.emit('preview:reload', {});
160
- return { status: 'completed' };
161
- },
162
- }));
163
- cleanups.push(registerCommand({
164
- id: 'editor.restartPreview',
165
- title: '重建预览运行时',
166
- execute: () => {
167
- const restart = requireDeps().restartPreview;
168
- if (!restart)
169
- throw new Error('editor.restartPreview: host did not provide restartPreview');
170
- restart();
171
- return { status: 'completed' };
172
- },
173
- }));
174
- return () => {
175
- // Reverse order so first-registered is torn down last — same pattern
176
- // as builtin-commands.ts; `.slice()` clones the array so a repeat
177
- // unload doesn't mutate the closed-over `cleanups`.
178
- for (const c of cleanups.slice().reverse())
179
- c();
180
- };
181
- },
182
- };