@openvcs/sdk 0.2.10 → 0.2.12

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.
@@ -1,8 +1,12 @@
1
1
  import type { MenubarItem } from '../types/menubar.js';
2
+ import type { PluginDelegates } from '../types/plugin.js';
3
+ import type { PluginRuntimeContext } from './contracts.js';
2
4
  type MenubarMenuOptions = {
3
5
  before?: string;
4
6
  after?: string;
5
7
  };
8
+ /** Runs a registered action handler by id. */
9
+ export declare function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean>;
6
10
  export interface MenuHandle {
7
11
  id: string;
8
12
  addItem(item: MenubarItem): void;
@@ -11,15 +15,28 @@ export interface MenuHandle {
11
15
  hideItem(actionId: string): void;
12
16
  showItem(actionId: string): void;
13
17
  }
18
+ /** Returns a menu by id, or null when it does not exist. */
14
19
  export declare function getMenu(menuId: string): MenuHandle | null;
20
+ /** Returns a menu by id, creating it if needed. */
15
21
  export declare function getOrCreateMenu(menuId: string, label: string): MenuHandle | null;
22
+ /** Creates a menu at a specific position. */
16
23
  export declare function createMenu(menuId: string, label: string, options?: MenubarMenuOptions): MenuHandle | null;
24
+ /** Adds one item to a menu. */
17
25
  export declare function addMenuItem(menuId: string, item: MenubarItem): void;
26
+ /** Adds one separator to a menu. */
18
27
  export declare function addMenuSeparator(menuId: string, beforeAction?: string): void;
28
+ /** Removes one menu from the registry. */
19
29
  export declare function removeMenu(menuId: string): void;
30
+ /** Hides one menu from the registry. */
20
31
  export declare function hideMenu(menuId: string): void;
32
+ /** Shows one menu from the registry. */
21
33
  export declare function showMenu(menuId: string): void;
34
+ /** Registers an action handler by id. */
22
35
  export declare function registerAction(id: string, handler: (...args: unknown[]) => unknown): void;
36
+ /** Invokes a host command when a host helper is available. */
23
37
  export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
38
+ /** Emits a notification when the host helper is available. */
24
39
  export declare function notify(msg: string): void;
40
+ /** Builds SDK delegates from the local menu/action registries. */
41
+ export declare function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContext>;
25
42
  export {};
@@ -2,6 +2,7 @@
2
2
  // Copyright © 2025-2026 OpenVCS Contributors
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.runRegisteredAction = runRegisteredAction;
5
6
  exports.getMenu = getMenu;
6
7
  exports.getOrCreateMenu = getOrCreateMenu;
7
8
  exports.createMenu = createMenu;
@@ -13,64 +14,286 @@ exports.showMenu = showMenu;
13
14
  exports.registerAction = registerAction;
14
15
  exports.invoke = invoke;
15
16
  exports.notify = notify;
17
+ exports.createMenuPluginDelegates = createMenuPluginDelegates;
18
+ const menus = new Map();
19
+ const menuOrder = [];
20
+ const actionHandlers = new Map();
21
+ let syntheticId = 0;
22
+ /** Returns the host-side OpenVCS helper, when the environment provides one. */
16
23
  function getOpenVCS() {
17
24
  return globalThis.OpenVCS;
18
25
  }
19
- function castMenuHandle(menu) {
20
- if (!menu)
21
- return null;
26
+ /** Normalizes a menu id for stable map lookup. */
27
+ function normalizeMenuId(menuId) {
28
+ return String(menuId || '').trim();
29
+ }
30
+ /** Allocates a stable synthetic id for generated menu entries. */
31
+ function allocateSyntheticId(prefix) {
32
+ syntheticId += 1;
33
+ return `${prefix}-${syntheticId}`;
34
+ }
35
+ /** Returns the stored menu state for one id, if present. */
36
+ function getStoredMenu(menuId) {
37
+ return menus.get(normalizeMenuId(menuId)) || null;
38
+ }
39
+ /** Removes one menu id from the ordering list. */
40
+ function removeMenuId(menuId) {
41
+ const id = normalizeMenuId(menuId);
42
+ const index = menuOrder.indexOf(id);
43
+ if (index >= 0)
44
+ menuOrder.splice(index, 1);
45
+ }
46
+ /** Inserts one menu id into the ordering list. */
47
+ function placeMenuId(menuId, options) {
48
+ const id = normalizeMenuId(menuId);
49
+ removeMenuId(id);
50
+ const beforeId = normalizeMenuId(options?.before || '');
51
+ const afterId = normalizeMenuId(options?.after || '');
52
+ if (afterId) {
53
+ const afterIndex = menuOrder.indexOf(afterId);
54
+ if (afterIndex >= 0) {
55
+ menuOrder.splice(afterIndex + 1, 0, id);
56
+ return;
57
+ }
58
+ }
59
+ if (beforeId) {
60
+ const beforeIndex = menuOrder.indexOf(beforeId);
61
+ if (beforeIndex >= 0) {
62
+ menuOrder.splice(beforeIndex, 0, id);
63
+ return;
64
+ }
65
+ }
66
+ menuOrder.push(id);
67
+ }
68
+ /** Ensures a menu record exists for one id. */
69
+ function ensureStoredMenu(menuId, label, options) {
70
+ const id = normalizeMenuId(menuId);
71
+ const safeLabel = String(label || '').trim() || id;
72
+ let menu = menus.get(id);
73
+ if (!menu) {
74
+ menu = { id, label: safeLabel, items: [] };
75
+ menus.set(id, menu);
76
+ }
77
+ else {
78
+ menu.label = safeLabel;
79
+ }
80
+ placeMenuId(id, options);
22
81
  return menu;
23
82
  }
83
+ /** Finds a stored item by action id. */
84
+ function findStoredItem(menu, actionId) {
85
+ const id = normalizeMenuId(actionId);
86
+ return menu.items.find((item) => item.action === id) || null;
87
+ }
88
+ /** Inserts a menu item at the requested position. */
89
+ function insertMenuItem(menu, item, before, after) {
90
+ const beforeId = normalizeMenuId(before || '');
91
+ const afterId = normalizeMenuId(after || '');
92
+ const removeExisting = () => {
93
+ const index = menu.items.findIndex((entry) => entry.id === item.id || entry.action === item.action);
94
+ if (index >= 0)
95
+ menu.items.splice(index, 1);
96
+ };
97
+ if (beforeId) {
98
+ const beforeIndex = menu.items.findIndex((entry) => entry.action === beforeId);
99
+ if (beforeIndex >= 0) {
100
+ removeExisting();
101
+ menu.items.splice(beforeIndex, 0, item);
102
+ return;
103
+ }
104
+ }
105
+ if (afterId) {
106
+ const afterIndex = menu.items.findIndex((entry) => entry.action === afterId);
107
+ if (afterIndex >= 0) {
108
+ removeExisting();
109
+ menu.items.splice(afterIndex + 1, 0, item);
110
+ return;
111
+ }
112
+ }
113
+ removeExisting();
114
+ menu.items.push(item);
115
+ }
116
+ /** Converts one stored item into a serializable menu payload element. */
117
+ function serializeMenuItem(item) {
118
+ if (item.hidden)
119
+ return null;
120
+ if (item.kind === 'separator') {
121
+ return {
122
+ type: 'text',
123
+ id: item.id,
124
+ content: item.content || '—',
125
+ };
126
+ }
127
+ if (item.kind === 'text') {
128
+ return {
129
+ type: 'text',
130
+ id: item.id,
131
+ content: item.content || item.label,
132
+ };
133
+ }
134
+ return {
135
+ type: 'button',
136
+ id: item.action || item.id,
137
+ label: item.label,
138
+ };
139
+ }
140
+ /** Serializes the local registry into plugin menu payloads. */
141
+ function serializeMenus() {
142
+ return menuOrder
143
+ .map((menuId, index) => {
144
+ const menu = menus.get(menuId);
145
+ if (!menu || menu.hidden)
146
+ return null;
147
+ return {
148
+ id: menu.id,
149
+ label: menu.label,
150
+ order: index + 1,
151
+ elements: menu.items
152
+ .map((item) => serializeMenuItem(item))
153
+ .filter((item) => Boolean(item)),
154
+ };
155
+ })
156
+ .filter((menu) => Boolean(menu));
157
+ }
158
+ /** Runs a registered action handler by id. */
159
+ async function runRegisteredAction(actionId, ...args) {
160
+ const id = String(actionId || '').trim();
161
+ if (!id)
162
+ return false;
163
+ const handler = actionHandlers.get(id);
164
+ if (!handler)
165
+ return false;
166
+ await handler(...args);
167
+ return true;
168
+ }
169
+ /** Creates a stable handle for one stored menu. */
170
+ function createMenuHandle(menuId) {
171
+ const id = normalizeMenuId(menuId);
172
+ return {
173
+ id,
174
+ addItem(item) {
175
+ const label = String(item?.label || '').trim();
176
+ const action = String(item?.action || '').trim();
177
+ if (!label || !action)
178
+ return;
179
+ const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
180
+ insertMenuItem(menu, {
181
+ kind: 'button',
182
+ id: action,
183
+ label,
184
+ title: item.title,
185
+ action,
186
+ }, item.before, item.after);
187
+ },
188
+ addSeparator(beforeAction) {
189
+ const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
190
+ insertMenuItem(menu, {
191
+ kind: 'separator',
192
+ id: allocateSyntheticId(`${menu.id}-separator`),
193
+ label: 'Separator',
194
+ content: '—',
195
+ }, beforeAction);
196
+ },
197
+ removeItem(actionId) {
198
+ const menu = getStoredMenu(this.id);
199
+ if (!menu)
200
+ return;
201
+ const idToRemove = normalizeMenuId(actionId);
202
+ menu.items = menu.items.filter((item) => item.action !== idToRemove);
203
+ },
204
+ hideItem(actionId) {
205
+ const menu = getStoredMenu(this.id);
206
+ if (!menu)
207
+ return;
208
+ const item = findStoredItem(menu, actionId);
209
+ if (item)
210
+ item.hidden = true;
211
+ },
212
+ showItem(actionId) {
213
+ const menu = getStoredMenu(this.id);
214
+ if (!menu)
215
+ return;
216
+ const item = findStoredItem(menu, actionId);
217
+ if (item)
218
+ item.hidden = false;
219
+ },
220
+ };
221
+ }
222
+ /** Returns a menu by id, or null when it does not exist. */
24
223
  function getMenu(menuId) {
25
- const openvcs = getOpenVCS();
26
- if (!openvcs?.menus)
224
+ const stored = getStoredMenu(menuId);
225
+ if (!stored)
27
226
  return null;
28
- return castMenuHandle(openvcs.menus.get(menuId));
227
+ return createMenuHandle(stored.id);
29
228
  }
229
+ /** Returns a menu by id, creating it if needed. */
30
230
  function getOrCreateMenu(menuId, label) {
31
- const openvcs = getOpenVCS();
32
- if (!openvcs?.menus)
33
- return null;
34
- return castMenuHandle(openvcs.menus.getOrCreate(menuId, label));
231
+ const stored = ensureStoredMenu(menuId, label);
232
+ return createMenuHandle(stored.id);
35
233
  }
234
+ /** Creates a menu at a specific position. */
36
235
  function createMenu(menuId, label, options) {
37
- const openvcs = getOpenVCS();
38
- if (!openvcs?.menus)
39
- return null;
40
- return castMenuHandle(openvcs.menus.create(menuId, label, options));
236
+ const stored = ensureStoredMenu(menuId, label, options);
237
+ return createMenuHandle(stored.id);
41
238
  }
239
+ /** Adds one item to a menu. */
42
240
  function addMenuItem(menuId, item) {
43
- const openvcs = getOpenVCS();
44
- openvcs?.menus?.addMenuItem(menuId, item);
241
+ createMenuHandle(menuId).addItem(item);
45
242
  }
243
+ /** Adds one separator to a menu. */
46
244
  function addMenuSeparator(menuId, beforeAction) {
47
- const openvcs = getOpenVCS();
48
- openvcs?.menus?.addMenuSeparator(menuId, beforeAction);
245
+ createMenuHandle(menuId).addSeparator(beforeAction);
49
246
  }
247
+ /** Removes one menu from the registry. */
50
248
  function removeMenu(menuId) {
51
- const openvcs = getOpenVCS();
52
- openvcs?.menus?.remove(menuId);
249
+ const id = normalizeMenuId(menuId);
250
+ menus.delete(id);
251
+ removeMenuId(id);
53
252
  }
253
+ /** Hides one menu from the registry. */
54
254
  function hideMenu(menuId) {
55
- const openvcs = getOpenVCS();
56
- openvcs?.menus?.hide(menuId);
255
+ const menu = getStoredMenu(menuId);
256
+ if (menu)
257
+ menu.hidden = true;
57
258
  }
259
+ /** Shows one menu from the registry. */
58
260
  function showMenu(menuId) {
59
- const openvcs = getOpenVCS();
60
- openvcs?.menus?.show(menuId);
261
+ const menu = getStoredMenu(menuId);
262
+ if (menu)
263
+ menu.hidden = false;
61
264
  }
265
+ /** Registers an action handler by id. */
62
266
  function registerAction(id, handler) {
63
- const openvcs = getOpenVCS();
64
- openvcs?.registerAction(id, handler);
267
+ const key = String(id || '').trim();
268
+ if (!key)
269
+ return;
270
+ actionHandlers.set(key, handler);
65
271
  }
272
+ /** Invokes a host command when a host helper is available. */
66
273
  function invoke(cmd, args) {
67
274
  const openvcs = getOpenVCS();
68
275
  if (!openvcs) {
69
- return Promise.reject(new Error('OpenVCS not available'));
276
+ return Promise.reject(new Error('OpenVCS host is not available in this runtime'));
70
277
  }
71
278
  return openvcs.invoke(cmd, args);
72
279
  }
280
+ /** Emits a notification when the host helper is available. */
73
281
  function notify(msg) {
74
282
  const openvcs = getOpenVCS();
75
283
  openvcs?.notify(msg);
76
284
  }
285
+ /** Builds SDK delegates from the local menu/action registries. */
286
+ function createMenuPluginDelegates() {
287
+ return {
288
+ async 'plugin.get_menus'() {
289
+ return serializeMenus();
290
+ },
291
+ async 'plugin.handle_action'(params) {
292
+ const actionId = String(params?.action_id || '').trim();
293
+ if (actionId) {
294
+ await runRegisteredAction(actionId);
295
+ }
296
+ return null;
297
+ },
298
+ };
299
+ }
@@ -5,10 +5,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.createRegisteredPluginRuntime = createRegisteredPluginRuntime;
6
6
  exports.bootstrapPluginModule = bootstrapPluginModule;
7
7
  const factory_1 = require("./factory");
8
+ const menu_1 = require("./menu");
8
9
  /** Creates a runtime from one declarative plugin module definition. */
9
10
  function createRegisteredPluginRuntime(definition = {}) {
11
+ const menuDelegates = (0, menu_1.createMenuPluginDelegates)();
12
+ const explicitPluginDelegates = definition.plugin ?? {};
13
+ const explicitGetMenus = explicitPluginDelegates['plugin.get_menus'];
14
+ const explicitHandleAction = explicitPluginDelegates['plugin.handle_action'];
10
15
  const options = {
11
- plugin: definition.plugin,
16
+ plugin: {
17
+ ...explicitPluginDelegates,
18
+ 'plugin.get_menus': async (params, ctx) => {
19
+ const menus = await menuDelegates['plugin.get_menus']?.(params, ctx);
20
+ const explicitMenus = explicitGetMenus
21
+ ? await explicitGetMenus(params, ctx)
22
+ : [];
23
+ return [
24
+ ...(Array.isArray(explicitMenus) ? explicitMenus : []),
25
+ ...(Array.isArray(menus) ? menus : []),
26
+ ];
27
+ },
28
+ 'plugin.handle_action': async (params, ctx) => {
29
+ const actionId = String(params?.action_id || '').trim();
30
+ if (actionId && (await (0, menu_1.runRegisteredAction)(actionId))) {
31
+ return null;
32
+ }
33
+ if (explicitHandleAction) {
34
+ return explicitHandleAction(params, ctx);
35
+ }
36
+ return null;
37
+ },
38
+ },
12
39
  vcs: definition.vcs,
13
40
  implements: definition.implements,
14
41
  logTarget: definition.logTarget,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openvcs/sdk",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "OpenVCS SDK CLI for plugin scaffolding and .ovcsp tar.gz packaging",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "homepage": "https://openvcs.app/",
@@ -2,31 +2,231 @@
2
2
  // SPDX-License-Identifier: GPL-3.0-or-later
3
3
 
4
4
  import type { MenubarItem } from '../types/menubar.js';
5
+ import type {
6
+ PluginDelegates,
7
+ PluginHandleActionParams,
8
+ PluginMenuDefinition,
9
+ } from '../types/plugin.js';
10
+
11
+ import type { PluginRuntimeContext } from './contracts.js';
5
12
 
6
13
  type MenubarMenuOptions = { before?: string; after?: string };
14
+ type MenuEntryKind = 'button' | 'text' | 'separator';
7
15
 
8
16
  type OpenVCSGlobal = typeof globalThis & {
9
17
  OpenVCS?: {
10
- menus?: {
11
- get(menuId: string): unknown;
12
- getOrCreate(menuId: string, label: string): unknown;
13
- create(menuId: string, label: string, options?: MenubarMenuOptions): unknown;
14
- addMenuItem(menuId: string, item: MenubarItem): void;
15
- addMenuSeparator(menuId: string, beforeAction?: string): void;
16
- remove(menuId: string): void;
17
- hide(menuId: string): void;
18
- show(menuId: string): void;
19
- };
20
- registerAction(id: string, handler: (...args: unknown[]) => unknown): void;
21
18
  invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
22
19
  notify(msg: string): void;
23
20
  };
24
21
  };
25
22
 
23
+ interface StoredMenuItem {
24
+ kind: MenuEntryKind;
25
+ id: string;
26
+ label: string;
27
+ title?: string;
28
+ content?: string;
29
+ action?: string;
30
+ hidden?: boolean;
31
+ }
32
+
33
+ interface StoredMenuState {
34
+ id: string;
35
+ label: string;
36
+ hidden?: boolean;
37
+ items: StoredMenuItem[];
38
+ }
39
+
40
+ interface SerializedMenuItem {
41
+ type: 'button' | 'text';
42
+ id: string;
43
+ label?: string;
44
+ content?: string;
45
+ }
46
+
47
+ interface SerializedMenuDefinition {
48
+ id: string;
49
+ label: string;
50
+ order: number;
51
+ elements: SerializedMenuItem[];
52
+ }
53
+
54
+ const menus = new Map<string, StoredMenuState>();
55
+ const menuOrder: string[] = [];
56
+ const actionHandlers = new Map<string, (...args: unknown[]) => unknown>();
57
+ let syntheticId = 0;
58
+
59
+ /** Returns the host-side OpenVCS helper, when the environment provides one. */
26
60
  function getOpenVCS() {
27
61
  return (globalThis as OpenVCSGlobal).OpenVCS;
28
62
  }
29
63
 
64
+ /** Normalizes a menu id for stable map lookup. */
65
+ function normalizeMenuId(menuId: string): string {
66
+ return String(menuId || '').trim();
67
+ }
68
+
69
+ /** Allocates a stable synthetic id for generated menu entries. */
70
+ function allocateSyntheticId(prefix: string): string {
71
+ syntheticId += 1;
72
+ return `${prefix}-${syntheticId}`;
73
+ }
74
+
75
+ /** Returns the stored menu state for one id, if present. */
76
+ function getStoredMenu(menuId: string): StoredMenuState | null {
77
+ return menus.get(normalizeMenuId(menuId)) || null;
78
+ }
79
+
80
+ /** Removes one menu id from the ordering list. */
81
+ function removeMenuId(menuId: string): void {
82
+ const id = normalizeMenuId(menuId);
83
+ const index = menuOrder.indexOf(id);
84
+ if (index >= 0) menuOrder.splice(index, 1);
85
+ }
86
+
87
+ /** Inserts one menu id into the ordering list. */
88
+ function placeMenuId(menuId: string, options?: MenubarMenuOptions): void {
89
+ const id = normalizeMenuId(menuId);
90
+ removeMenuId(id);
91
+
92
+ const beforeId = normalizeMenuId(options?.before || '');
93
+ const afterId = normalizeMenuId(options?.after || '');
94
+
95
+ if (afterId) {
96
+ const afterIndex = menuOrder.indexOf(afterId);
97
+ if (afterIndex >= 0) {
98
+ menuOrder.splice(afterIndex + 1, 0, id);
99
+ return;
100
+ }
101
+ }
102
+
103
+ if (beforeId) {
104
+ const beforeIndex = menuOrder.indexOf(beforeId);
105
+ if (beforeIndex >= 0) {
106
+ menuOrder.splice(beforeIndex, 0, id);
107
+ return;
108
+ }
109
+ }
110
+
111
+ menuOrder.push(id);
112
+ }
113
+
114
+ /** Ensures a menu record exists for one id. */
115
+ function ensureStoredMenu(
116
+ menuId: string,
117
+ label: string,
118
+ options?: MenubarMenuOptions,
119
+ ): StoredMenuState {
120
+ const id = normalizeMenuId(menuId);
121
+ const safeLabel = String(label || '').trim() || id;
122
+ let menu = menus.get(id);
123
+
124
+ if (!menu) {
125
+ menu = { id, label: safeLabel, items: [] };
126
+ menus.set(id, menu);
127
+ } else {
128
+ menu.label = safeLabel;
129
+ }
130
+
131
+ placeMenuId(id, options);
132
+ return menu;
133
+ }
134
+
135
+ /** Finds a stored item by action id. */
136
+ function findStoredItem(menu: StoredMenuState, actionId: string): StoredMenuItem | null {
137
+ const id = normalizeMenuId(actionId);
138
+ return menu.items.find((item) => item.action === id) || null;
139
+ }
140
+
141
+ /** Inserts a menu item at the requested position. */
142
+ function insertMenuItem(menu: StoredMenuState, item: StoredMenuItem, before?: string, after?: string): void {
143
+ const beforeId = normalizeMenuId(before || '');
144
+ const afterId = normalizeMenuId(after || '');
145
+
146
+ const removeExisting = () => {
147
+ const index = menu.items.findIndex((entry) => entry.id === item.id || entry.action === item.action);
148
+ if (index >= 0) menu.items.splice(index, 1);
149
+ };
150
+
151
+ if (beforeId) {
152
+ const beforeIndex = menu.items.findIndex((entry) => entry.action === beforeId);
153
+ if (beforeIndex >= 0) {
154
+ removeExisting();
155
+ menu.items.splice(beforeIndex, 0, item);
156
+ return;
157
+ }
158
+ }
159
+
160
+ if (afterId) {
161
+ const afterIndex = menu.items.findIndex((entry) => entry.action === afterId);
162
+ if (afterIndex >= 0) {
163
+ removeExisting();
164
+ menu.items.splice(afterIndex + 1, 0, item);
165
+ return;
166
+ }
167
+ }
168
+
169
+ removeExisting();
170
+ menu.items.push(item);
171
+ }
172
+
173
+ /** Converts one stored item into a serializable menu payload element. */
174
+ function serializeMenuItem(item: StoredMenuItem): SerializedMenuItem | null {
175
+ if (item.hidden) return null;
176
+
177
+ if (item.kind === 'separator') {
178
+ return {
179
+ type: 'text',
180
+ id: item.id,
181
+ content: item.content || '—',
182
+ };
183
+ }
184
+
185
+ if (item.kind === 'text') {
186
+ return {
187
+ type: 'text',
188
+ id: item.id,
189
+ content: item.content || item.label,
190
+ };
191
+ }
192
+
193
+ return {
194
+ type: 'button',
195
+ id: item.action || item.id,
196
+ label: item.label,
197
+ };
198
+ }
199
+
200
+ /** Serializes the local registry into plugin menu payloads. */
201
+ function serializeMenus(): SerializedMenuDefinition[] {
202
+ return menuOrder
203
+ .map((menuId, index) => {
204
+ const menu = menus.get(menuId);
205
+ if (!menu || menu.hidden) return null;
206
+ return {
207
+ id: menu.id,
208
+ label: menu.label,
209
+ order: index + 1,
210
+ elements: menu.items
211
+ .map((item) => serializeMenuItem(item))
212
+ .filter((item): item is SerializedMenuItem => Boolean(item)),
213
+ };
214
+ })
215
+ .filter((menu): menu is SerializedMenuDefinition => Boolean(menu));
216
+ }
217
+
218
+ /** Runs a registered action handler by id. */
219
+ export async function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean> {
220
+ const id = String(actionId || '').trim();
221
+ if (!id) return false;
222
+
223
+ const handler = actionHandlers.get(id);
224
+ if (!handler) return false;
225
+
226
+ await handler(...args);
227
+ return true;
228
+ }
229
+
30
230
  export interface MenuHandle {
31
231
  id: string;
32
232
  addItem(item: MenubarItem): void;
@@ -36,68 +236,138 @@ export interface MenuHandle {
36
236
  showItem(actionId: string): void;
37
237
  }
38
238
 
39
- function castMenuHandle(menu: unknown): MenuHandle | null {
40
- if (!menu) return null;
41
- return menu as MenuHandle;
239
+ /** Creates a stable handle for one stored menu. */
240
+ function createMenuHandle(menuId: string): MenuHandle {
241
+ const id = normalizeMenuId(menuId);
242
+
243
+ return {
244
+ id,
245
+ addItem(item: MenubarItem) {
246
+ const label = String(item?.label || '').trim();
247
+ const action = String(item?.action || '').trim();
248
+ if (!label || !action) return;
249
+
250
+ const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
251
+ insertMenuItem(menu, {
252
+ kind: 'button',
253
+ id: action,
254
+ label,
255
+ title: item.title,
256
+ action,
257
+ }, item.before, item.after);
258
+ },
259
+ addSeparator(beforeAction?: string) {
260
+ const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
261
+ insertMenuItem(menu, {
262
+ kind: 'separator',
263
+ id: allocateSyntheticId(`${menu.id}-separator`),
264
+ label: 'Separator',
265
+ content: '—',
266
+ }, beforeAction);
267
+ },
268
+ removeItem(actionId: string) {
269
+ const menu = getStoredMenu(this.id);
270
+ if (!menu) return;
271
+ const idToRemove = normalizeMenuId(actionId);
272
+ menu.items = menu.items.filter((item) => item.action !== idToRemove);
273
+ },
274
+ hideItem(actionId: string) {
275
+ const menu = getStoredMenu(this.id);
276
+ if (!menu) return;
277
+ const item = findStoredItem(menu, actionId);
278
+ if (item) item.hidden = true;
279
+ },
280
+ showItem(actionId: string) {
281
+ const menu = getStoredMenu(this.id);
282
+ if (!menu) return;
283
+ const item = findStoredItem(menu, actionId);
284
+ if (item) item.hidden = false;
285
+ },
286
+ };
42
287
  }
43
288
 
289
+ /** Returns a menu by id, or null when it does not exist. */
44
290
  export function getMenu(menuId: string): MenuHandle | null {
45
- const openvcs = getOpenVCS();
46
- if (!openvcs?.menus) return null;
47
- return castMenuHandle(openvcs.menus.get(menuId));
291
+ const stored = getStoredMenu(menuId);
292
+ if (!stored) return null;
293
+ return createMenuHandle(stored.id);
48
294
  }
49
295
 
296
+ /** Returns a menu by id, creating it if needed. */
50
297
  export function getOrCreateMenu(menuId: string, label: string): MenuHandle | null {
51
- const openvcs = getOpenVCS();
52
- if (!openvcs?.menus) return null;
53
- return castMenuHandle(openvcs.menus.getOrCreate(menuId, label));
298
+ const stored = ensureStoredMenu(menuId, label);
299
+ return createMenuHandle(stored.id);
54
300
  }
55
301
 
302
+ /** Creates a menu at a specific position. */
56
303
  export function createMenu(menuId: string, label: string, options?: MenubarMenuOptions): MenuHandle | null {
57
- const openvcs = getOpenVCS();
58
- if (!openvcs?.menus) return null;
59
- return castMenuHandle(openvcs.menus.create(menuId, label, options));
304
+ const stored = ensureStoredMenu(menuId, label, options);
305
+ return createMenuHandle(stored.id);
60
306
  }
61
307
 
308
+ /** Adds one item to a menu. */
62
309
  export function addMenuItem(menuId: string, item: MenubarItem): void {
63
- const openvcs = getOpenVCS();
64
- openvcs?.menus?.addMenuItem(menuId, item);
310
+ createMenuHandle(menuId).addItem(item);
65
311
  }
66
312
 
313
+ /** Adds one separator to a menu. */
67
314
  export function addMenuSeparator(menuId: string, beforeAction?: string): void {
68
- const openvcs = getOpenVCS();
69
- openvcs?.menus?.addMenuSeparator(menuId, beforeAction);
315
+ createMenuHandle(menuId).addSeparator(beforeAction);
70
316
  }
71
317
 
318
+ /** Removes one menu from the registry. */
72
319
  export function removeMenu(menuId: string): void {
73
- const openvcs = getOpenVCS();
74
- openvcs?.menus?.remove(menuId);
320
+ const id = normalizeMenuId(menuId);
321
+ menus.delete(id);
322
+ removeMenuId(id);
75
323
  }
76
324
 
325
+ /** Hides one menu from the registry. */
77
326
  export function hideMenu(menuId: string): void {
78
- const openvcs = getOpenVCS();
79
- openvcs?.menus?.hide(menuId);
327
+ const menu = getStoredMenu(menuId);
328
+ if (menu) menu.hidden = true;
80
329
  }
81
330
 
331
+ /** Shows one menu from the registry. */
82
332
  export function showMenu(menuId: string): void {
83
- const openvcs = getOpenVCS();
84
- openvcs?.menus?.show(menuId);
333
+ const menu = getStoredMenu(menuId);
334
+ if (menu) menu.hidden = false;
85
335
  }
86
336
 
337
+ /** Registers an action handler by id. */
87
338
  export function registerAction(id: string, handler: (...args: unknown[]) => unknown): void {
88
- const openvcs = getOpenVCS();
89
- openvcs?.registerAction(id, handler);
339
+ const key = String(id || '').trim();
340
+ if (!key) return;
341
+ actionHandlers.set(key, handler);
90
342
  }
91
343
 
344
+ /** Invokes a host command when a host helper is available. */
92
345
  export function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T> {
93
346
  const openvcs = getOpenVCS();
94
347
  if (!openvcs) {
95
- return Promise.reject(new Error('OpenVCS not available'));
348
+ return Promise.reject(new Error('OpenVCS host is not available in this runtime'));
96
349
  }
97
350
  return openvcs.invoke(cmd, args);
98
351
  }
99
352
 
353
+ /** Emits a notification when the host helper is available. */
100
354
  export function notify(msg: string): void {
101
355
  const openvcs = getOpenVCS();
102
356
  openvcs?.notify(msg);
103
357
  }
358
+
359
+ /** Builds SDK delegates from the local menu/action registries. */
360
+ export function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContext> {
361
+ return {
362
+ async 'plugin.get_menus'(): Promise<PluginMenuDefinition[]> {
363
+ return serializeMenus() as unknown as PluginMenuDefinition[];
364
+ },
365
+ async 'plugin.handle_action'(params: PluginHandleActionParams): Promise<null> {
366
+ const actionId = String(params?.action_id || '').trim();
367
+ if (actionId) {
368
+ await runRegisteredAction(actionId);
369
+ }
370
+ return null;
371
+ },
372
+ };
373
+ }
@@ -14,6 +14,10 @@ import type {
14
14
  PluginRuntimeTransport,
15
15
  } from './contracts';
16
16
  import { createPluginRuntime } from './factory';
17
+ import {
18
+ createMenuPluginDelegates,
19
+ runRegisteredAction,
20
+ } from './menu';
17
21
 
18
22
  /** Describes the plugin module startup hook invoked by the generated bootstrap. */
19
23
  export type OnPluginStartHandler = () => void | Promise<void>;
@@ -56,8 +60,35 @@ export interface BootstrapPluginModuleOptions {
56
60
  export function createRegisteredPluginRuntime(
57
61
  definition: PluginModuleDefinition = {},
58
62
  ): PluginRuntime {
63
+ const menuDelegates = createMenuPluginDelegates();
64
+ const explicitPluginDelegates = definition.plugin ?? {};
65
+ const explicitGetMenus = explicitPluginDelegates['plugin.get_menus'];
66
+ const explicitHandleAction = explicitPluginDelegates['plugin.handle_action'];
67
+
59
68
  const options: CreatePluginRuntimeOptions = {
60
- plugin: definition.plugin,
69
+ plugin: {
70
+ ...explicitPluginDelegates,
71
+ 'plugin.get_menus': async (params, ctx) => {
72
+ const menus = await menuDelegates['plugin.get_menus']?.(params, ctx);
73
+ const explicitMenus = explicitGetMenus
74
+ ? await explicitGetMenus(params, ctx)
75
+ : [];
76
+ return [
77
+ ...(Array.isArray(explicitMenus) ? explicitMenus : []),
78
+ ...(Array.isArray(menus) ? menus : []),
79
+ ];
80
+ },
81
+ 'plugin.handle_action': async (params, ctx) => {
82
+ const actionId = String(params?.action_id || '').trim();
83
+ if (actionId && (await runRegisteredAction(actionId))) {
84
+ return null;
85
+ }
86
+ if (explicitHandleAction) {
87
+ return explicitHandleAction(params, ctx);
88
+ }
89
+ return null;
90
+ },
91
+ },
61
92
  vcs: definition.vcs,
62
93
  implements: definition.implements,
63
94
  logTarget: definition.logTarget,