@openvcs/sdk 0.2.9 → 0.2.11
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.
- package/lib/runtime/index.d.ts +2 -0
- package/lib/runtime/index.js +13 -1
- package/lib/runtime/menu.d.ts +42 -0
- package/lib/runtime/menu.js +335 -0
- package/lib/runtime/registration.js +28 -1
- package/package.json +1 -1
- package/src/lib/runtime/index.ts +14 -0
- package/src/lib/runtime/menu.ts +422 -0
- package/src/lib/runtime/registration.ts +32 -1
package/lib/runtime/index.d.ts
CHANGED
|
@@ -8,5 +8,7 @@ export { createHost } from './host';
|
|
|
8
8
|
export { bootstrapPluginModule, createRegisteredPluginRuntime, } from './registration';
|
|
9
9
|
export { VcsDelegateBase } from './vcs-delegate-base';
|
|
10
10
|
export type { VcsDelegateAssignments } from './vcs-delegate-metadata';
|
|
11
|
+
export { getMenu, getOrCreateMenu, createMenu, addMenuItem, addMenuSeparator, removeMenu, hideMenu, showMenu, registerAction, invoke, notify, } from './menu';
|
|
12
|
+
export type { MenuHandle } from './menu';
|
|
11
13
|
/** Starts a previously created plugin runtime on process stdio. */
|
|
12
14
|
export declare function startPluginRuntime(runtime: PluginRuntime, transport?: PluginRuntimeTransport): void;
|
package/lib/runtime/index.js
CHANGED
|
@@ -2,7 +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.VcsDelegateBase = exports.createRegisteredPluginRuntime = exports.bootstrapPluginModule = exports.createHost = exports.createPluginRuntime = exports.pluginError = exports.isPluginFailure = exports.createRuntimeDispatcher = exports.createDefaultPluginDelegates = void 0;
|
|
5
|
+
exports.notify = exports.invoke = exports.registerAction = exports.showMenu = exports.hideMenu = exports.removeMenu = exports.addMenuSeparator = exports.addMenuItem = exports.createMenu = exports.getOrCreateMenu = exports.getMenu = exports.VcsDelegateBase = exports.createRegisteredPluginRuntime = exports.bootstrapPluginModule = exports.createHost = exports.createPluginRuntime = exports.pluginError = exports.isPluginFailure = exports.createRuntimeDispatcher = exports.createDefaultPluginDelegates = void 0;
|
|
6
6
|
exports.startPluginRuntime = startPluginRuntime;
|
|
7
7
|
var dispatcher_1 = require("./dispatcher");
|
|
8
8
|
Object.defineProperty(exports, "createDefaultPluginDelegates", { enumerable: true, get: function () { return dispatcher_1.createDefaultPluginDelegates; } });
|
|
@@ -19,6 +19,18 @@ Object.defineProperty(exports, "bootstrapPluginModule", { enumerable: true, get:
|
|
|
19
19
|
Object.defineProperty(exports, "createRegisteredPluginRuntime", { enumerable: true, get: function () { return registration_1.createRegisteredPluginRuntime; } });
|
|
20
20
|
var vcs_delegate_base_1 = require("./vcs-delegate-base");
|
|
21
21
|
Object.defineProperty(exports, "VcsDelegateBase", { enumerable: true, get: function () { return vcs_delegate_base_1.VcsDelegateBase; } });
|
|
22
|
+
var menu_1 = require("./menu");
|
|
23
|
+
Object.defineProperty(exports, "getMenu", { enumerable: true, get: function () { return menu_1.getMenu; } });
|
|
24
|
+
Object.defineProperty(exports, "getOrCreateMenu", { enumerable: true, get: function () { return menu_1.getOrCreateMenu; } });
|
|
25
|
+
Object.defineProperty(exports, "createMenu", { enumerable: true, get: function () { return menu_1.createMenu; } });
|
|
26
|
+
Object.defineProperty(exports, "addMenuItem", { enumerable: true, get: function () { return menu_1.addMenuItem; } });
|
|
27
|
+
Object.defineProperty(exports, "addMenuSeparator", { enumerable: true, get: function () { return menu_1.addMenuSeparator; } });
|
|
28
|
+
Object.defineProperty(exports, "removeMenu", { enumerable: true, get: function () { return menu_1.removeMenu; } });
|
|
29
|
+
Object.defineProperty(exports, "hideMenu", { enumerable: true, get: function () { return menu_1.hideMenu; } });
|
|
30
|
+
Object.defineProperty(exports, "showMenu", { enumerable: true, get: function () { return menu_1.showMenu; } });
|
|
31
|
+
Object.defineProperty(exports, "registerAction", { enumerable: true, get: function () { return menu_1.registerAction; } });
|
|
32
|
+
Object.defineProperty(exports, "invoke", { enumerable: true, get: function () { return menu_1.invoke; } });
|
|
33
|
+
Object.defineProperty(exports, "notify", { enumerable: true, get: function () { return menu_1.notify; } });
|
|
22
34
|
/** Starts a previously created plugin runtime on process stdio. */
|
|
23
35
|
function startPluginRuntime(runtime, transport) {
|
|
24
36
|
runtime.start(transport);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { MenubarItem } from '../types/menubar.js';
|
|
2
|
+
import type { PluginDelegates } from '../types/plugin.js';
|
|
3
|
+
import type { PluginRuntimeContext } from './contracts.js';
|
|
4
|
+
type MenubarMenuOptions = {
|
|
5
|
+
before?: string;
|
|
6
|
+
after?: string;
|
|
7
|
+
};
|
|
8
|
+
/** Runs one registered action handler by id. */
|
|
9
|
+
export declare function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean>;
|
|
10
|
+
export interface MenuHandle {
|
|
11
|
+
id: string;
|
|
12
|
+
addItem(item: MenubarItem): void;
|
|
13
|
+
addSeparator(beforeAction?: string): void;
|
|
14
|
+
removeItem(actionId: string): void;
|
|
15
|
+
hideItem(actionId: string): void;
|
|
16
|
+
showItem(actionId: string): void;
|
|
17
|
+
}
|
|
18
|
+
/** Returns a menu by id, or null when it does not exist. */
|
|
19
|
+
export declare function getMenu(menuId: string): MenuHandle | null;
|
|
20
|
+
/** Returns a menu by id, creating it when necessary. */
|
|
21
|
+
export declare function getOrCreateMenu(menuId: string, label: string): MenuHandle | null;
|
|
22
|
+
/** Creates a menu at a specific insertion point. */
|
|
23
|
+
export declare function createMenu(menuId: string, label: string, options?: MenubarMenuOptions): MenuHandle | null;
|
|
24
|
+
/** Adds one action item to a menu. */
|
|
25
|
+
export declare function addMenuItem(menuId: string, item: MenubarItem): void;
|
|
26
|
+
/** Adds one separator item to a menu. */
|
|
27
|
+
export declare function addMenuSeparator(menuId: string, beforeAction?: string): void;
|
|
28
|
+
/** Removes one menu entirely. */
|
|
29
|
+
export declare function removeMenu(menuId: string): void;
|
|
30
|
+
/** Hides one menu from the host UI. */
|
|
31
|
+
export declare function hideMenu(menuId: string): void;
|
|
32
|
+
/** Shows one previously hidden menu. */
|
|
33
|
+
export declare function showMenu(menuId: string): void;
|
|
34
|
+
/** Registers an action handler by id. */
|
|
35
|
+
export declare function registerAction(id: string, handler: (...args: unknown[]) => unknown): void;
|
|
36
|
+
/** Invokes a host command when the host exposes a direct invoke helper. */
|
|
37
|
+
export declare function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
38
|
+
/** Emits a user notification when the host exposes one. */
|
|
39
|
+
export declare function notify(msg: string): void;
|
|
40
|
+
/** Builds the SDK plugin delegates contributed by the menu registry. */
|
|
41
|
+
export declare function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContext>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.runRegisteredAction = runRegisteredAction;
|
|
6
|
+
exports.getMenu = getMenu;
|
|
7
|
+
exports.getOrCreateMenu = getOrCreateMenu;
|
|
8
|
+
exports.createMenu = createMenu;
|
|
9
|
+
exports.addMenuItem = addMenuItem;
|
|
10
|
+
exports.addMenuSeparator = addMenuSeparator;
|
|
11
|
+
exports.removeMenu = removeMenu;
|
|
12
|
+
exports.hideMenu = hideMenu;
|
|
13
|
+
exports.showMenu = showMenu;
|
|
14
|
+
exports.registerAction = registerAction;
|
|
15
|
+
exports.invoke = invoke;
|
|
16
|
+
exports.notify = notify;
|
|
17
|
+
exports.createMenuPluginDelegates = createMenuPluginDelegates;
|
|
18
|
+
const menus = new Map();
|
|
19
|
+
const menuOrder = [];
|
|
20
|
+
const actionHandlers = new Map();
|
|
21
|
+
let nextSyntheticId = 0;
|
|
22
|
+
/** Returns the OpenVCS global, when the host exposes one. */
|
|
23
|
+
function getOpenVCS() {
|
|
24
|
+
return globalThis.OpenVCS;
|
|
25
|
+
}
|
|
26
|
+
/** Normalizes menu ids for stable map lookups. */
|
|
27
|
+
function normalizeMenuId(menuId) {
|
|
28
|
+
return String(menuId || '').trim();
|
|
29
|
+
}
|
|
30
|
+
/** Allocates a stable synthetic id for a generated menu item. */
|
|
31
|
+
function allocateSyntheticId(prefix) {
|
|
32
|
+
nextSyntheticId += 1;
|
|
33
|
+
return `${prefix}-${nextSyntheticId}`;
|
|
34
|
+
}
|
|
35
|
+
/** Returns a stored menu state or null if it does not exist. */
|
|
36
|
+
function getStoredMenu(menuId) {
|
|
37
|
+
return menus.get(normalizeMenuId(menuId)) || null;
|
|
38
|
+
}
|
|
39
|
+
/** Inserts a menu id into the ordering list. */
|
|
40
|
+
function placeMenuId(menuId, options) {
|
|
41
|
+
const id = normalizeMenuId(menuId);
|
|
42
|
+
const existingIndex = menuOrder.indexOf(id);
|
|
43
|
+
if (existingIndex >= 0) {
|
|
44
|
+
menuOrder.splice(existingIndex, 1);
|
|
45
|
+
}
|
|
46
|
+
const beforeId = normalizeMenuId(options?.before || '');
|
|
47
|
+
const afterId = normalizeMenuId(options?.after || '');
|
|
48
|
+
if (afterId) {
|
|
49
|
+
const afterIndex = menuOrder.indexOf(afterId);
|
|
50
|
+
if (afterIndex >= 0) {
|
|
51
|
+
menuOrder.splice(afterIndex + 1, 0, id);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (beforeId) {
|
|
56
|
+
const beforeIndex = menuOrder.indexOf(beforeId);
|
|
57
|
+
if (beforeIndex >= 0) {
|
|
58
|
+
menuOrder.splice(beforeIndex, 0, id);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
menuOrder.push(id);
|
|
63
|
+
}
|
|
64
|
+
/** Ensures a menu record exists for the provided id. */
|
|
65
|
+
function ensureStoredMenu(menuId, label, options) {
|
|
66
|
+
const id = normalizeMenuId(menuId);
|
|
67
|
+
const safeLabel = String(label || '').trim() || id;
|
|
68
|
+
let menu = menus.get(id);
|
|
69
|
+
if (!menu) {
|
|
70
|
+
menu = { id, label: safeLabel, items: [] };
|
|
71
|
+
menus.set(id, menu);
|
|
72
|
+
placeMenuId(id, options);
|
|
73
|
+
return menu;
|
|
74
|
+
}
|
|
75
|
+
menu.label = safeLabel;
|
|
76
|
+
placeMenuId(id, options);
|
|
77
|
+
return menu;
|
|
78
|
+
}
|
|
79
|
+
/** Finds one menu item by action id. */
|
|
80
|
+
function findMenuItem(menu, actionId) {
|
|
81
|
+
const id = normalizeMenuId(actionId);
|
|
82
|
+
return menu.items.find((item) => item.action === id) || null;
|
|
83
|
+
}
|
|
84
|
+
/** Inserts a menu item relative to before/after anchors when provided. */
|
|
85
|
+
function insertMenuItem(menu, item, before, after) {
|
|
86
|
+
const beforeId = normalizeMenuId(before || '');
|
|
87
|
+
const afterId = normalizeMenuId(after || '');
|
|
88
|
+
const insertAt = (index) => {
|
|
89
|
+
const existingIndex = menu.items.findIndex((entry) => entry.id === item.id || entry.action === item.action);
|
|
90
|
+
if (existingIndex >= 0) {
|
|
91
|
+
menu.items.splice(existingIndex, 1);
|
|
92
|
+
}
|
|
93
|
+
menu.items.splice(index, 0, item);
|
|
94
|
+
};
|
|
95
|
+
if (beforeId) {
|
|
96
|
+
const beforeIndex = menu.items.findIndex((entry) => entry.action === beforeId);
|
|
97
|
+
if (beforeIndex >= 0) {
|
|
98
|
+
insertAt(beforeIndex);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (afterId) {
|
|
103
|
+
const afterIndex = menu.items.findIndex((entry) => entry.action === afterId);
|
|
104
|
+
if (afterIndex >= 0) {
|
|
105
|
+
insertAt(afterIndex + 1);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
insertAt(menu.items.length);
|
|
110
|
+
}
|
|
111
|
+
/** Converts one stored menu item into a serializable plugin payload element. */
|
|
112
|
+
function serializeMenuItem(item) {
|
|
113
|
+
if (item.hidden)
|
|
114
|
+
return null;
|
|
115
|
+
if (item.kind === 'separator') {
|
|
116
|
+
return {
|
|
117
|
+
type: 'text',
|
|
118
|
+
id: item.id,
|
|
119
|
+
content: item.content || '—',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (item.kind === 'text') {
|
|
123
|
+
return {
|
|
124
|
+
type: 'text',
|
|
125
|
+
id: item.id,
|
|
126
|
+
content: item.content || item.label,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
type: 'button',
|
|
131
|
+
id: item.action || item.id,
|
|
132
|
+
label: item.label,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Serializes the current menu registry for the host runtime. */
|
|
136
|
+
function serializeMenus() {
|
|
137
|
+
return menuOrder
|
|
138
|
+
.map((menuId, index) => {
|
|
139
|
+
const menu = menus.get(menuId);
|
|
140
|
+
if (!menu || menu.hidden)
|
|
141
|
+
return null;
|
|
142
|
+
return {
|
|
143
|
+
id: menu.id,
|
|
144
|
+
label: menu.label,
|
|
145
|
+
order: index + 1,
|
|
146
|
+
elements: menu.items
|
|
147
|
+
.map((item) => serializeMenuItem(item))
|
|
148
|
+
.filter((item) => Boolean(item)),
|
|
149
|
+
};
|
|
150
|
+
})
|
|
151
|
+
.filter((menu) => Boolean(menu));
|
|
152
|
+
}
|
|
153
|
+
/** Runs one registered action handler by id. */
|
|
154
|
+
async function runRegisteredAction(actionId, ...args) {
|
|
155
|
+
const id = String(actionId || '').trim();
|
|
156
|
+
if (!id)
|
|
157
|
+
return false;
|
|
158
|
+
const handler = actionHandlers.get(id);
|
|
159
|
+
if (!handler)
|
|
160
|
+
return false;
|
|
161
|
+
await handler(...args);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
/** Creates a stable, mutation-based handle for one menu id. */
|
|
165
|
+
function createMenuHandle(menuId) {
|
|
166
|
+
const id = normalizeMenuId(menuId);
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
addItem(item) {
|
|
170
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
171
|
+
const label = String(item?.label || '').trim();
|
|
172
|
+
const action = String(item?.action || '').trim();
|
|
173
|
+
if (!label || !action)
|
|
174
|
+
return;
|
|
175
|
+
const entry = {
|
|
176
|
+
kind: 'button',
|
|
177
|
+
id: action,
|
|
178
|
+
label,
|
|
179
|
+
title: item.title,
|
|
180
|
+
action,
|
|
181
|
+
};
|
|
182
|
+
insertMenuItem(menu, entry, item.before, item.after);
|
|
183
|
+
const openvcs = getOpenVCS();
|
|
184
|
+
if (!openvcs?.menus)
|
|
185
|
+
return;
|
|
186
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
187
|
+
openvcs.menus.addMenuItem(menu.id, item);
|
|
188
|
+
},
|
|
189
|
+
addSeparator(beforeAction) {
|
|
190
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
191
|
+
const entry = {
|
|
192
|
+
kind: 'separator',
|
|
193
|
+
id: allocateSyntheticId(`${menu.id}-separator`),
|
|
194
|
+
label: 'Separator',
|
|
195
|
+
content: '—',
|
|
196
|
+
};
|
|
197
|
+
insertMenuItem(menu, entry, beforeAction);
|
|
198
|
+
const openvcs = getOpenVCS();
|
|
199
|
+
if (!openvcs?.menus)
|
|
200
|
+
return;
|
|
201
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
202
|
+
openvcs.menus.addMenuSeparator(menu.id, beforeAction);
|
|
203
|
+
},
|
|
204
|
+
removeItem(actionId) {
|
|
205
|
+
const menu = getStoredMenu(this.id);
|
|
206
|
+
if (!menu)
|
|
207
|
+
return;
|
|
208
|
+
const idToRemove = normalizeMenuId(actionId);
|
|
209
|
+
menu.items = menu.items.filter((item) => item.action !== idToRemove);
|
|
210
|
+
const openvcs = getOpenVCS();
|
|
211
|
+
if (openvcs?.menus) {
|
|
212
|
+
const handle = openvcs.menus.get(this.id);
|
|
213
|
+
handle?.removeItem(idToRemove);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
hideItem(actionId) {
|
|
217
|
+
const menu = getStoredMenu(this.id);
|
|
218
|
+
if (!menu)
|
|
219
|
+
return;
|
|
220
|
+
const item = findMenuItem(menu, actionId);
|
|
221
|
+
if (item)
|
|
222
|
+
item.hidden = true;
|
|
223
|
+
const openvcs = getOpenVCS();
|
|
224
|
+
if (!openvcs?.menus)
|
|
225
|
+
return;
|
|
226
|
+
const handle = openvcs.menus.get(this.id);
|
|
227
|
+
handle?.hideItem(actionId);
|
|
228
|
+
},
|
|
229
|
+
showItem(actionId) {
|
|
230
|
+
const menu = getStoredMenu(this.id);
|
|
231
|
+
if (!menu)
|
|
232
|
+
return;
|
|
233
|
+
const item = findMenuItem(menu, actionId);
|
|
234
|
+
if (item)
|
|
235
|
+
item.hidden = false;
|
|
236
|
+
const openvcs = getOpenVCS();
|
|
237
|
+
if (!openvcs?.menus)
|
|
238
|
+
return;
|
|
239
|
+
const handle = openvcs.menus.get(this.id);
|
|
240
|
+
handle?.showItem(actionId);
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Returns a menu by id, or null when it does not exist. */
|
|
245
|
+
function getMenu(menuId) {
|
|
246
|
+
const stored = getStoredMenu(menuId);
|
|
247
|
+
if (!stored)
|
|
248
|
+
return null;
|
|
249
|
+
return createMenuHandle(stored.id);
|
|
250
|
+
}
|
|
251
|
+
/** Returns a menu by id, creating it when necessary. */
|
|
252
|
+
function getOrCreateMenu(menuId, label) {
|
|
253
|
+
const stored = ensureStoredMenu(menuId, label);
|
|
254
|
+
return createMenuHandle(stored.id);
|
|
255
|
+
}
|
|
256
|
+
/** Creates a menu at a specific insertion point. */
|
|
257
|
+
function createMenu(menuId, label, options) {
|
|
258
|
+
const stored = ensureStoredMenu(menuId, label, options);
|
|
259
|
+
return createMenuHandle(stored.id);
|
|
260
|
+
}
|
|
261
|
+
/** Adds one action item to a menu. */
|
|
262
|
+
function addMenuItem(menuId, item) {
|
|
263
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
264
|
+
const handle = createMenuHandle(menu.id);
|
|
265
|
+
handle.addItem(item);
|
|
266
|
+
}
|
|
267
|
+
/** Adds one separator item to a menu. */
|
|
268
|
+
function addMenuSeparator(menuId, beforeAction) {
|
|
269
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
270
|
+
const handle = createMenuHandle(menu.id);
|
|
271
|
+
handle.addSeparator(beforeAction);
|
|
272
|
+
}
|
|
273
|
+
/** Removes one menu entirely. */
|
|
274
|
+
function removeMenu(menuId) {
|
|
275
|
+
const id = normalizeMenuId(menuId);
|
|
276
|
+
menus.delete(id);
|
|
277
|
+
const index = menuOrder.indexOf(id);
|
|
278
|
+
if (index >= 0)
|
|
279
|
+
menuOrder.splice(index, 1);
|
|
280
|
+
const openvcs = getOpenVCS();
|
|
281
|
+
openvcs?.menus?.remove(id);
|
|
282
|
+
}
|
|
283
|
+
/** Hides one menu from the host UI. */
|
|
284
|
+
function hideMenu(menuId) {
|
|
285
|
+
const menu = getStoredMenu(menuId);
|
|
286
|
+
if (menu)
|
|
287
|
+
menu.hidden = true;
|
|
288
|
+
const openvcs = getOpenVCS();
|
|
289
|
+
openvcs?.menus?.hide(normalizeMenuId(menuId));
|
|
290
|
+
}
|
|
291
|
+
/** Shows one previously hidden menu. */
|
|
292
|
+
function showMenu(menuId) {
|
|
293
|
+
const menu = getStoredMenu(menuId);
|
|
294
|
+
if (menu)
|
|
295
|
+
menu.hidden = false;
|
|
296
|
+
const openvcs = getOpenVCS();
|
|
297
|
+
openvcs?.menus?.show(normalizeMenuId(menuId));
|
|
298
|
+
}
|
|
299
|
+
/** Registers an action handler by id. */
|
|
300
|
+
function registerAction(id, handler) {
|
|
301
|
+
const key = String(id || '').trim();
|
|
302
|
+
if (!key)
|
|
303
|
+
return;
|
|
304
|
+
actionHandlers.set(key, handler);
|
|
305
|
+
const openvcs = getOpenVCS();
|
|
306
|
+
openvcs?.registerAction(key, handler);
|
|
307
|
+
}
|
|
308
|
+
/** Invokes a host command when the host exposes a direct invoke helper. */
|
|
309
|
+
function invoke(cmd, args) {
|
|
310
|
+
const openvcs = getOpenVCS();
|
|
311
|
+
if (!openvcs) {
|
|
312
|
+
return Promise.reject(new Error('OpenVCS not available'));
|
|
313
|
+
}
|
|
314
|
+
return openvcs.invoke(cmd, args);
|
|
315
|
+
}
|
|
316
|
+
/** Emits a user notification when the host exposes one. */
|
|
317
|
+
function notify(msg) {
|
|
318
|
+
const openvcs = getOpenVCS();
|
|
319
|
+
openvcs?.notify(msg);
|
|
320
|
+
}
|
|
321
|
+
/** Builds the SDK plugin delegates contributed by the menu registry. */
|
|
322
|
+
function createMenuPluginDelegates() {
|
|
323
|
+
return {
|
|
324
|
+
async 'plugin.get_menus'() {
|
|
325
|
+
return serializeMenus();
|
|
326
|
+
},
|
|
327
|
+
async 'plugin.handle_action'(params) {
|
|
328
|
+
const actionId = String(params?.action_id || '').trim();
|
|
329
|
+
if (!actionId)
|
|
330
|
+
return null;
|
|
331
|
+
await runRegisteredAction(actionId);
|
|
332
|
+
return null;
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
@@ -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:
|
|
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
package/src/lib/runtime/index.ts
CHANGED
|
@@ -28,6 +28,20 @@ export {
|
|
|
28
28
|
} from './registration';
|
|
29
29
|
export { VcsDelegateBase } from './vcs-delegate-base';
|
|
30
30
|
export type { VcsDelegateAssignments } from './vcs-delegate-metadata';
|
|
31
|
+
export {
|
|
32
|
+
getMenu,
|
|
33
|
+
getOrCreateMenu,
|
|
34
|
+
createMenu,
|
|
35
|
+
addMenuItem,
|
|
36
|
+
addMenuSeparator,
|
|
37
|
+
removeMenu,
|
|
38
|
+
hideMenu,
|
|
39
|
+
showMenu,
|
|
40
|
+
registerAction,
|
|
41
|
+
invoke,
|
|
42
|
+
notify,
|
|
43
|
+
} from './menu';
|
|
44
|
+
export type { MenuHandle } from './menu';
|
|
31
45
|
|
|
32
46
|
/** Starts a previously created plugin runtime on process stdio. */
|
|
33
47
|
export function startPluginRuntime(
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// Copyright © 2025-2026 OpenVCS Contributors
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
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';
|
|
12
|
+
|
|
13
|
+
type MenubarMenuOptions = { before?: string; after?: string };
|
|
14
|
+
|
|
15
|
+
type MenuEntryKind = 'button' | 'text' | 'separator';
|
|
16
|
+
|
|
17
|
+
type OpenVCSGlobal = typeof globalThis & {
|
|
18
|
+
OpenVCS?: {
|
|
19
|
+
menus?: {
|
|
20
|
+
get(menuId: string): unknown;
|
|
21
|
+
getOrCreate(menuId: string, label: string): unknown;
|
|
22
|
+
create(menuId: string, label: string, options?: MenubarMenuOptions): unknown;
|
|
23
|
+
addMenuItem(menuId: string, item: MenubarItem): void;
|
|
24
|
+
addMenuSeparator(menuId: string, beforeAction?: string): void;
|
|
25
|
+
remove(menuId: string): void;
|
|
26
|
+
hide(menuId: string): void;
|
|
27
|
+
show(menuId: string): void;
|
|
28
|
+
};
|
|
29
|
+
registerAction(id: string, handler: (...args: unknown[]) => unknown): void;
|
|
30
|
+
invoke<T = unknown>(cmd: string, args?: unknown): Promise<T>;
|
|
31
|
+
notify(msg: string): void;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface StoredMenuItem {
|
|
36
|
+
kind: MenuEntryKind;
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
title?: string;
|
|
40
|
+
content?: string;
|
|
41
|
+
action?: string;
|
|
42
|
+
hidden?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface StoredMenuState {
|
|
46
|
+
id: string;
|
|
47
|
+
label: string;
|
|
48
|
+
hidden?: boolean;
|
|
49
|
+
items: StoredMenuItem[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SerializedMenuItem {
|
|
53
|
+
type: 'button' | 'text';
|
|
54
|
+
id: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
content?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SerializedMenuDefinition {
|
|
60
|
+
id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
order: number;
|
|
63
|
+
elements: SerializedMenuItem[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const menus = new Map<string, StoredMenuState>();
|
|
67
|
+
const menuOrder: string[] = [];
|
|
68
|
+
const actionHandlers = new Map<string, (...args: unknown[]) => unknown>();
|
|
69
|
+
let nextSyntheticId = 0;
|
|
70
|
+
|
|
71
|
+
/** Returns the OpenVCS global, when the host exposes one. */
|
|
72
|
+
function getOpenVCS() {
|
|
73
|
+
return (globalThis as OpenVCSGlobal).OpenVCS;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Normalizes menu ids for stable map lookups. */
|
|
77
|
+
function normalizeMenuId(menuId: string): string {
|
|
78
|
+
return String(menuId || '').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Allocates a stable synthetic id for a generated menu item. */
|
|
82
|
+
function allocateSyntheticId(prefix: string): string {
|
|
83
|
+
nextSyntheticId += 1;
|
|
84
|
+
return `${prefix}-${nextSyntheticId}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Returns a stored menu state or null if it does not exist. */
|
|
88
|
+
function getStoredMenu(menuId: string): StoredMenuState | null {
|
|
89
|
+
return menus.get(normalizeMenuId(menuId)) || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Inserts a menu id into the ordering list. */
|
|
93
|
+
function placeMenuId(menuId: string, options?: MenubarMenuOptions): void {
|
|
94
|
+
const id = normalizeMenuId(menuId);
|
|
95
|
+
const existingIndex = menuOrder.indexOf(id);
|
|
96
|
+
if (existingIndex >= 0) {
|
|
97
|
+
menuOrder.splice(existingIndex, 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const beforeId = normalizeMenuId(options?.before || '');
|
|
101
|
+
const afterId = normalizeMenuId(options?.after || '');
|
|
102
|
+
|
|
103
|
+
if (afterId) {
|
|
104
|
+
const afterIndex = menuOrder.indexOf(afterId);
|
|
105
|
+
if (afterIndex >= 0) {
|
|
106
|
+
menuOrder.splice(afterIndex + 1, 0, id);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (beforeId) {
|
|
112
|
+
const beforeIndex = menuOrder.indexOf(beforeId);
|
|
113
|
+
if (beforeIndex >= 0) {
|
|
114
|
+
menuOrder.splice(beforeIndex, 0, id);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
menuOrder.push(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Ensures a menu record exists for the provided id. */
|
|
123
|
+
function ensureStoredMenu(menuId: string, label: string, options?: MenubarMenuOptions): StoredMenuState {
|
|
124
|
+
const id = normalizeMenuId(menuId);
|
|
125
|
+
const safeLabel = String(label || '').trim() || id;
|
|
126
|
+
let menu = menus.get(id);
|
|
127
|
+
|
|
128
|
+
if (!menu) {
|
|
129
|
+
menu = { id, label: safeLabel, items: [] };
|
|
130
|
+
menus.set(id, menu);
|
|
131
|
+
placeMenuId(id, options);
|
|
132
|
+
return menu;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
menu.label = safeLabel;
|
|
136
|
+
placeMenuId(id, options);
|
|
137
|
+
return menu;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Finds one menu item by action id. */
|
|
141
|
+
function findMenuItem(menu: StoredMenuState, actionId: string): StoredMenuItem | null {
|
|
142
|
+
const id = normalizeMenuId(actionId);
|
|
143
|
+
return menu.items.find((item) => item.action === id) || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Inserts a menu item relative to before/after anchors when provided. */
|
|
147
|
+
function insertMenuItem(menu: StoredMenuState, item: StoredMenuItem, before?: string, after?: string): void {
|
|
148
|
+
const beforeId = normalizeMenuId(before || '');
|
|
149
|
+
const afterId = normalizeMenuId(after || '');
|
|
150
|
+
|
|
151
|
+
const insertAt = (index: number) => {
|
|
152
|
+
const existingIndex = menu.items.findIndex((entry) => entry.id === item.id || entry.action === item.action);
|
|
153
|
+
if (existingIndex >= 0) {
|
|
154
|
+
menu.items.splice(existingIndex, 1);
|
|
155
|
+
}
|
|
156
|
+
menu.items.splice(index, 0, item);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (beforeId) {
|
|
160
|
+
const beforeIndex = menu.items.findIndex((entry) => entry.action === beforeId);
|
|
161
|
+
if (beforeIndex >= 0) {
|
|
162
|
+
insertAt(beforeIndex);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (afterId) {
|
|
168
|
+
const afterIndex = menu.items.findIndex((entry) => entry.action === afterId);
|
|
169
|
+
if (afterIndex >= 0) {
|
|
170
|
+
insertAt(afterIndex + 1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
insertAt(menu.items.length);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Converts one stored menu item into a serializable plugin payload element. */
|
|
179
|
+
function serializeMenuItem(item: StoredMenuItem): SerializedMenuItem | null {
|
|
180
|
+
if (item.hidden) return null;
|
|
181
|
+
|
|
182
|
+
if (item.kind === 'separator') {
|
|
183
|
+
return {
|
|
184
|
+
type: 'text',
|
|
185
|
+
id: item.id,
|
|
186
|
+
content: item.content || '—',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (item.kind === 'text') {
|
|
191
|
+
return {
|
|
192
|
+
type: 'text',
|
|
193
|
+
id: item.id,
|
|
194
|
+
content: item.content || item.label,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
type: 'button',
|
|
200
|
+
id: item.action || item.id,
|
|
201
|
+
label: item.label,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Serializes the current menu registry for the host runtime. */
|
|
206
|
+
function serializeMenus(): SerializedMenuDefinition[] {
|
|
207
|
+
return menuOrder
|
|
208
|
+
.map((menuId, index) => {
|
|
209
|
+
const menu = menus.get(menuId);
|
|
210
|
+
if (!menu || menu.hidden) return null;
|
|
211
|
+
return {
|
|
212
|
+
id: menu.id,
|
|
213
|
+
label: menu.label,
|
|
214
|
+
order: index + 1,
|
|
215
|
+
elements: menu.items
|
|
216
|
+
.map((item) => serializeMenuItem(item))
|
|
217
|
+
.filter((item): item is SerializedMenuItem => Boolean(item)),
|
|
218
|
+
};
|
|
219
|
+
})
|
|
220
|
+
.filter((menu): menu is SerializedMenuDefinition => Boolean(menu));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Runs one registered action handler by id. */
|
|
224
|
+
export async function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean> {
|
|
225
|
+
const id = String(actionId || '').trim();
|
|
226
|
+
if (!id) return false;
|
|
227
|
+
|
|
228
|
+
const handler = actionHandlers.get(id);
|
|
229
|
+
if (!handler) return false;
|
|
230
|
+
|
|
231
|
+
await handler(...args);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface MenuHandle {
|
|
236
|
+
id: string;
|
|
237
|
+
addItem(item: MenubarItem): void;
|
|
238
|
+
addSeparator(beforeAction?: string): void;
|
|
239
|
+
removeItem(actionId: string): void;
|
|
240
|
+
hideItem(actionId: string): void;
|
|
241
|
+
showItem(actionId: string): void;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Creates a stable, mutation-based handle for one menu id. */
|
|
245
|
+
function createMenuHandle(menuId: string): MenuHandle {
|
|
246
|
+
const id = normalizeMenuId(menuId);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
id,
|
|
250
|
+
addItem(item: MenubarItem) {
|
|
251
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
252
|
+
const label = String(item?.label || '').trim();
|
|
253
|
+
const action = String(item?.action || '').trim();
|
|
254
|
+
if (!label || !action) return;
|
|
255
|
+
|
|
256
|
+
const entry: StoredMenuItem = {
|
|
257
|
+
kind: 'button',
|
|
258
|
+
id: action,
|
|
259
|
+
label,
|
|
260
|
+
title: item.title,
|
|
261
|
+
action,
|
|
262
|
+
};
|
|
263
|
+
insertMenuItem(menu, entry, item.before, item.after);
|
|
264
|
+
|
|
265
|
+
const openvcs = getOpenVCS();
|
|
266
|
+
if (!openvcs?.menus) return;
|
|
267
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
268
|
+
openvcs.menus.addMenuItem(menu.id, item);
|
|
269
|
+
},
|
|
270
|
+
addSeparator(beforeAction?: string) {
|
|
271
|
+
const menu = getStoredMenu(this.id) || ensureStoredMenu(this.id, this.id);
|
|
272
|
+
const entry: StoredMenuItem = {
|
|
273
|
+
kind: 'separator',
|
|
274
|
+
id: allocateSyntheticId(`${menu.id}-separator`),
|
|
275
|
+
label: 'Separator',
|
|
276
|
+
content: '—',
|
|
277
|
+
};
|
|
278
|
+
insertMenuItem(menu, entry, beforeAction);
|
|
279
|
+
|
|
280
|
+
const openvcs = getOpenVCS();
|
|
281
|
+
if (!openvcs?.menus) return;
|
|
282
|
+
openvcs.menus.getOrCreate(menu.id, menu.label);
|
|
283
|
+
openvcs.menus.addMenuSeparator(menu.id, beforeAction);
|
|
284
|
+
},
|
|
285
|
+
removeItem(actionId: string) {
|
|
286
|
+
const menu = getStoredMenu(this.id);
|
|
287
|
+
if (!menu) return;
|
|
288
|
+
const idToRemove = normalizeMenuId(actionId);
|
|
289
|
+
menu.items = menu.items.filter((item) => item.action !== idToRemove);
|
|
290
|
+
|
|
291
|
+
const openvcs = getOpenVCS();
|
|
292
|
+
if (openvcs?.menus) {
|
|
293
|
+
const handle = openvcs.menus.get(this.id) as MenuHandle | null;
|
|
294
|
+
handle?.removeItem(idToRemove);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
hideItem(actionId: string) {
|
|
298
|
+
const menu = getStoredMenu(this.id);
|
|
299
|
+
if (!menu) return;
|
|
300
|
+
const item = findMenuItem(menu, actionId);
|
|
301
|
+
if (item) item.hidden = true;
|
|
302
|
+
|
|
303
|
+
const openvcs = getOpenVCS();
|
|
304
|
+
if (!openvcs?.menus) return;
|
|
305
|
+
const handle = openvcs.menus.get(this.id) as MenuHandle | null;
|
|
306
|
+
handle?.hideItem(actionId);
|
|
307
|
+
},
|
|
308
|
+
showItem(actionId: string) {
|
|
309
|
+
const menu = getStoredMenu(this.id);
|
|
310
|
+
if (!menu) return;
|
|
311
|
+
const item = findMenuItem(menu, actionId);
|
|
312
|
+
if (item) item.hidden = false;
|
|
313
|
+
|
|
314
|
+
const openvcs = getOpenVCS();
|
|
315
|
+
if (!openvcs?.menus) return;
|
|
316
|
+
const handle = openvcs.menus.get(this.id) as MenuHandle | null;
|
|
317
|
+
handle?.showItem(actionId);
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Returns a menu by id, or null when it does not exist. */
|
|
323
|
+
export function getMenu(menuId: string): MenuHandle | null {
|
|
324
|
+
const stored = getStoredMenu(menuId);
|
|
325
|
+
if (!stored) return null;
|
|
326
|
+
return createMenuHandle(stored.id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Returns a menu by id, creating it when necessary. */
|
|
330
|
+
export function getOrCreateMenu(menuId: string, label: string): MenuHandle | null {
|
|
331
|
+
const stored = ensureStoredMenu(menuId, label);
|
|
332
|
+
return createMenuHandle(stored.id);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Creates a menu at a specific insertion point. */
|
|
336
|
+
export function createMenu(menuId: string, label: string, options?: MenubarMenuOptions): MenuHandle | null {
|
|
337
|
+
const stored = ensureStoredMenu(menuId, label, options);
|
|
338
|
+
return createMenuHandle(stored.id);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Adds one action item to a menu. */
|
|
342
|
+
export function addMenuItem(menuId: string, item: MenubarItem): void {
|
|
343
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
344
|
+
const handle = createMenuHandle(menu.id);
|
|
345
|
+
handle.addItem(item);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Adds one separator item to a menu. */
|
|
349
|
+
export function addMenuSeparator(menuId: string, beforeAction?: string): void {
|
|
350
|
+
const menu = getStoredMenu(menuId) || ensureStoredMenu(menuId, menuId);
|
|
351
|
+
const handle = createMenuHandle(menu.id);
|
|
352
|
+
handle.addSeparator(beforeAction);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Removes one menu entirely. */
|
|
356
|
+
export function removeMenu(menuId: string): void {
|
|
357
|
+
const id = normalizeMenuId(menuId);
|
|
358
|
+
menus.delete(id);
|
|
359
|
+
const index = menuOrder.indexOf(id);
|
|
360
|
+
if (index >= 0) menuOrder.splice(index, 1);
|
|
361
|
+
|
|
362
|
+
const openvcs = getOpenVCS();
|
|
363
|
+
openvcs?.menus?.remove(id);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Hides one menu from the host UI. */
|
|
367
|
+
export function hideMenu(menuId: string): void {
|
|
368
|
+
const menu = getStoredMenu(menuId);
|
|
369
|
+
if (menu) menu.hidden = true;
|
|
370
|
+
|
|
371
|
+
const openvcs = getOpenVCS();
|
|
372
|
+
openvcs?.menus?.hide(normalizeMenuId(menuId));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Shows one previously hidden menu. */
|
|
376
|
+
export function showMenu(menuId: string): void {
|
|
377
|
+
const menu = getStoredMenu(menuId);
|
|
378
|
+
if (menu) menu.hidden = false;
|
|
379
|
+
|
|
380
|
+
const openvcs = getOpenVCS();
|
|
381
|
+
openvcs?.menus?.show(normalizeMenuId(menuId));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Registers an action handler by id. */
|
|
385
|
+
export function registerAction(id: string, handler: (...args: unknown[]) => unknown): void {
|
|
386
|
+
const key = String(id || '').trim();
|
|
387
|
+
if (!key) return;
|
|
388
|
+
actionHandlers.set(key, handler);
|
|
389
|
+
|
|
390
|
+
const openvcs = getOpenVCS();
|
|
391
|
+
openvcs?.registerAction(key, handler);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Invokes a host command when the host exposes a direct invoke helper. */
|
|
395
|
+
export function invoke<T = unknown>(cmd: string, args?: unknown): Promise<T> {
|
|
396
|
+
const openvcs = getOpenVCS();
|
|
397
|
+
if (!openvcs) {
|
|
398
|
+
return Promise.reject(new Error('OpenVCS not available'));
|
|
399
|
+
}
|
|
400
|
+
return openvcs.invoke(cmd, args);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Emits a user notification when the host exposes one. */
|
|
404
|
+
export function notify(msg: string): void {
|
|
405
|
+
const openvcs = getOpenVCS();
|
|
406
|
+
openvcs?.notify(msg);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Builds the SDK plugin delegates contributed by the menu registry. */
|
|
410
|
+
export function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContext> {
|
|
411
|
+
return {
|
|
412
|
+
async 'plugin.get_menus'(): Promise<PluginMenuDefinition[]> {
|
|
413
|
+
return serializeMenus() as unknown as PluginMenuDefinition[];
|
|
414
|
+
},
|
|
415
|
+
async 'plugin.handle_action'(params: PluginHandleActionParams): Promise<null> {
|
|
416
|
+
const actionId = String(params?.action_id || '').trim();
|
|
417
|
+
if (!actionId) return null;
|
|
418
|
+
await runRegisteredAction(actionId);
|
|
419
|
+
return null;
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
@@ -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:
|
|
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,
|