@openvcs/sdk 0.2.12 → 0.2.14

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.
@@ -5,6 +5,7 @@ export { createDefaultPluginDelegates, createRuntimeDispatcher } from './dispatc
5
5
  export { isPluginFailure, pluginError } from './errors';
6
6
  export { createPluginRuntime } from './factory';
7
7
  export { createHost } from './host';
8
+ export { ModalBuilder } from './modal';
8
9
  export { bootstrapPluginModule, createRegisteredPluginRuntime, } from './registration';
9
10
  export { VcsDelegateBase } from './vcs-delegate-base';
10
11
  export type { VcsDelegateAssignments } from './vcs-delegate-metadata';
@@ -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.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;
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.ModalBuilder = 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; } });
@@ -14,6 +14,8 @@ var factory_1 = require("./factory");
14
14
  Object.defineProperty(exports, "createPluginRuntime", { enumerable: true, get: function () { return factory_1.createPluginRuntime; } });
15
15
  var host_1 = require("./host");
16
16
  Object.defineProperty(exports, "createHost", { enumerable: true, get: function () { return host_1.createHost; } });
17
+ var modal_1 = require("./modal");
18
+ Object.defineProperty(exports, "ModalBuilder", { enumerable: true, get: function () { return modal_1.ModalBuilder; } });
17
19
  var registration_1 = require("./registration");
18
20
  Object.defineProperty(exports, "bootstrapPluginModule", { enumerable: true, get: function () { return registration_1.bootstrapPluginModule; } });
19
21
  Object.defineProperty(exports, "createRegisteredPluginRuntime", { enumerable: true, get: function () { return registration_1.createRegisteredPluginRuntime; } });
@@ -6,7 +6,7 @@ type MenubarMenuOptions = {
6
6
  after?: string;
7
7
  };
8
8
  /** Runs a registered action handler by id. */
9
- export declare function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean>;
9
+ export declare function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<unknown>;
10
10
  export interface MenuHandle {
11
11
  id: string;
12
12
  addItem(item: MenubarItem): void;
@@ -159,12 +159,11 @@ function serializeMenus() {
159
159
  async function runRegisteredAction(actionId, ...args) {
160
160
  const id = String(actionId || '').trim();
161
161
  if (!id)
162
- return false;
162
+ return null;
163
163
  const handler = actionHandlers.get(id);
164
164
  if (!handler)
165
- return false;
166
- await handler(...args);
167
- return true;
165
+ return null;
166
+ return await handler(...args);
168
167
  }
169
168
  /** Creates a stable handle for one stored menu. */
170
169
  function createMenuHandle(menuId) {
@@ -291,7 +290,7 @@ function createMenuPluginDelegates() {
291
290
  async 'plugin.handle_action'(params) {
292
291
  const actionId = String(params?.action_id || '').trim();
293
292
  if (actionId) {
294
- await runRegisteredAction(actionId);
293
+ return await runRegisteredAction(actionId, params?.payload);
295
294
  }
296
295
  return null;
297
296
  },
@@ -0,0 +1,74 @@
1
+ import type { ModalButtonVariant, ModalContentAlign, ModalInputKind, ModalListRowDefinition, ModalSelectOptionDefinition, PluginModalDefinition } from '../types/modal.js';
2
+ /** Describes the options accepted by `ModalBuilder.button()`. */
3
+ export interface ModalBuilderButtonOptions {
4
+ /** Stores the optional tooltip text. */
5
+ title?: string;
6
+ /** Stores the visual button variant. */
7
+ variant?: ModalButtonVariant;
8
+ /** Stores the alignment hint. */
9
+ align?: ModalContentAlign;
10
+ /** Stores a static payload merged into the action payload. */
11
+ payload?: Record<string, unknown>;
12
+ }
13
+ /** Describes the options accepted by `ModalBuilder.text()`. */
14
+ export interface ModalBuilderTextOptions {
15
+ /** Stores the optional tooltip text. */
16
+ title?: string;
17
+ /** Stores the alignment hint. */
18
+ align?: ModalContentAlign;
19
+ }
20
+ /** Describes the options accepted by `ModalBuilder.input()`. */
21
+ export interface ModalBuilderInputOptions {
22
+ /** Stores the input kind. */
23
+ kind?: ModalInputKind;
24
+ /** Stores the default value. */
25
+ value?: string;
26
+ /** Stores the placeholder text. */
27
+ placeholder?: string;
28
+ /** Stores whether the field is required. */
29
+ required?: boolean;
30
+ /** Stores the alignment hint. */
31
+ align?: ModalContentAlign;
32
+ }
33
+ /** Describes the options accepted by `ModalBuilder.select()`. */
34
+ export interface ModalBuilderSelectOptions {
35
+ /** Stores the available options. */
36
+ options: ModalSelectOptionDefinition[];
37
+ /** Stores the default value. */
38
+ value?: string;
39
+ /** Stores the alignment hint. */
40
+ align?: ModalContentAlign;
41
+ }
42
+ /** Describes the options accepted by `ModalBuilder.list()`. */
43
+ export interface ModalBuilderListOptions {
44
+ /** Stores the list label. */
45
+ label?: string;
46
+ /** Stores the empty-state text. */
47
+ emptyText?: string;
48
+ /** Stores the alignment hint. */
49
+ align?: ModalContentAlign;
50
+ /** Stores the list rows. */
51
+ items: ModalListRowDefinition[];
52
+ }
53
+ /** Builds a structured modal definition with a fluent class API. */
54
+ export declare class ModalBuilder {
55
+ private readonly definition;
56
+ /** Creates a new modal builder with the provided title. */
57
+ constructor(title: string);
58
+ /** Adds a text block to the modal body. */
59
+ text(content: string, options?: ModalBuilderTextOptions): this;
60
+ /** Adds a separator to the modal body. */
61
+ separator(): this;
62
+ /** Adds a button to the modal body. */
63
+ button(id: string, content: string, options?: ModalBuilderButtonOptions): this;
64
+ /** Adds a text input to the modal body. */
65
+ input(id: string, label: string, options?: ModalBuilderInputOptions): this;
66
+ /** Adds a select field to the modal body. */
67
+ select(id: string, label: string, options: ModalBuilderSelectOptions): this;
68
+ /** Adds a list block to the modal body. */
69
+ list(id: string, options: ModalBuilderListOptions): this;
70
+ /** Returns the serialized modal payload. */
71
+ build(): PluginModalDefinition;
72
+ /** Returns the serialized modal payload for a host request. */
73
+ open(): Promise<PluginModalDefinition>;
74
+ }
@@ -0,0 +1,94 @@
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.ModalBuilder = void 0;
6
+ /** Builds a structured modal definition with a fluent class API. */
7
+ class ModalBuilder {
8
+ definition;
9
+ /** Creates a new modal builder with the provided title. */
10
+ constructor(title) {
11
+ this.definition = {
12
+ title: String(title || '').trim(),
13
+ content: [],
14
+ };
15
+ }
16
+ /** Adds a text block to the modal body. */
17
+ text(content, options = {}) {
18
+ this.definition.content.push({
19
+ type: 'text',
20
+ content: String(content || ''),
21
+ ...(options.title ? { title: options.title } : {}),
22
+ ...(options.align ? { align: options.align } : {}),
23
+ });
24
+ return this;
25
+ }
26
+ /** Adds a separator to the modal body. */
27
+ separator() {
28
+ this.definition.content.push({ type: 'separator' });
29
+ return this;
30
+ }
31
+ /** Adds a button to the modal body. */
32
+ button(id, content, options = {}) {
33
+ this.definition.content.push({
34
+ type: 'button',
35
+ id: String(id || '').trim(),
36
+ content: String(content || ''),
37
+ ...(options.title ? { title: options.title } : {}),
38
+ ...(options.variant && options.variant !== 'default' ? { variant: options.variant } : {}),
39
+ ...(options.align ? { align: options.align } : {}),
40
+ ...(options.payload ? { payload: options.payload } : {}),
41
+ });
42
+ return this;
43
+ }
44
+ /** Adds a text input to the modal body. */
45
+ input(id, label, options = {}) {
46
+ this.definition.content.push({
47
+ type: 'input',
48
+ id: String(id || '').trim(),
49
+ label: String(label || '').trim(),
50
+ ...(options.kind ? { kind: options.kind } : {}),
51
+ ...(options.value !== undefined ? { value: options.value } : {}),
52
+ ...(options.placeholder ? { placeholder: options.placeholder } : {}),
53
+ ...(options.required ? { required: true } : {}),
54
+ ...(options.align ? { align: options.align } : {}),
55
+ });
56
+ return this;
57
+ }
58
+ /** Adds a select field to the modal body. */
59
+ select(id, label, options) {
60
+ this.definition.content.push({
61
+ type: 'select',
62
+ id: String(id || '').trim(),
63
+ label: String(label || '').trim(),
64
+ options: Array.isArray(options.options) ? options.options : [],
65
+ ...(options.value !== undefined ? { value: options.value } : {}),
66
+ ...(options.align ? { align: options.align } : {}),
67
+ });
68
+ return this;
69
+ }
70
+ /** Adds a list block to the modal body. */
71
+ list(id, options) {
72
+ this.definition.content.push({
73
+ type: 'list',
74
+ id: String(id || '').trim(),
75
+ ...(options.label ? { label: options.label } : {}),
76
+ ...(options.emptyText ? { emptyText: options.emptyText } : {}),
77
+ ...(options.align ? { align: options.align } : {}),
78
+ items: Array.isArray(options.items) ? options.items : [],
79
+ });
80
+ return this;
81
+ }
82
+ /** Returns the serialized modal payload. */
83
+ build() {
84
+ return {
85
+ title: this.definition.title,
86
+ content: this.definition.content.map((item) => ({ ...item })),
87
+ };
88
+ }
89
+ /** Returns the serialized modal payload for a host request. */
90
+ async open() {
91
+ return this.build();
92
+ }
93
+ }
94
+ exports.ModalBuilder = ModalBuilder;
@@ -27,8 +27,11 @@ function createRegisteredPluginRuntime(definition = {}) {
27
27
  },
28
28
  'plugin.handle_action': async (params, ctx) => {
29
29
  const actionId = String(params?.action_id || '').trim();
30
- if (actionId && (await (0, menu_1.runRegisteredAction)(actionId))) {
31
- return null;
30
+ if (actionId) {
31
+ const result = await (0, menu_1.runRegisteredAction)(actionId, params?.payload);
32
+ if (result !== null && result !== undefined) {
33
+ return result;
34
+ }
32
35
  }
33
36
  if (explicitHandleAction) {
34
37
  return explicitHandleAction(params, ctx);
@@ -1,4 +1,5 @@
1
1
  export * from './host';
2
+ export * from './modal';
2
3
  export * from './menubar';
3
4
  export * from './plugin';
4
5
  export * from './protocol';
@@ -17,6 +17,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
17
  };
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
19
  __exportStar(require("./host"), exports);
20
+ __exportStar(require("./modal"), exports);
20
21
  __exportStar(require("./menubar"), exports);
21
22
  __exportStar(require("./plugin"), exports);
22
23
  __exportStar(require("./protocol"), exports);
@@ -0,0 +1,129 @@
1
+ /** Describes horizontal alignment hints for modal content. */
2
+ export type ModalContentAlign = 'left' | 'centered' | 'right';
3
+ /** Describes visual emphasis for modal buttons. */
4
+ export type ModalButtonVariant = 'default' | 'primary' | 'danger';
5
+ /** Describes the text input kinds supported by plugin modals. */
6
+ export type ModalInputKind = 'text' | 'search' | 'password' | 'url' | 'number';
7
+ /** Describes one button option rendered inside a modal. */
8
+ export interface ModalButtonDefinition {
9
+ /** Stores the action id triggered when the button is pressed. */
10
+ id: string;
11
+ /** Stores the button label. */
12
+ content: string;
13
+ /** Stores the optional tooltip text. */
14
+ title?: string;
15
+ /** Stores the visual button variant. */
16
+ variant?: ModalButtonVariant;
17
+ /** Stores the button alignment hint. */
18
+ align?: ModalContentAlign;
19
+ /** Stores a static payload merged into the action payload. */
20
+ payload?: Record<string, unknown>;
21
+ }
22
+ /** Describes one text block rendered inside a modal. */
23
+ export interface ModalTextDefinition {
24
+ /** Always stores `text`. */
25
+ type: 'text';
26
+ /** Stores the block text. */
27
+ content: string;
28
+ /** Stores an optional tooltip. */
29
+ title?: string;
30
+ /** Stores the alignment hint. */
31
+ align?: ModalContentAlign;
32
+ }
33
+ /** Describes one separator rendered inside a modal. */
34
+ export interface ModalSeparatorDefinition {
35
+ /** Always stores `separator`. */
36
+ type: 'separator';
37
+ }
38
+ /** Describes one button rendered inside a modal body. */
39
+ export interface ModalButtonItemDefinition extends ModalButtonDefinition {
40
+ /** Always stores `button`. */
41
+ type: 'button';
42
+ }
43
+ /** Describes one text input rendered inside a modal. */
44
+ export interface ModalInputDefinition {
45
+ /** Always stores `input`. */
46
+ type: 'input';
47
+ /** Stores the input field id. */
48
+ id: string;
49
+ /** Stores the label text. */
50
+ label: string;
51
+ /** Stores the input kind. */
52
+ kind?: ModalInputKind;
53
+ /** Stores the default value. */
54
+ value?: string;
55
+ /** Stores the placeholder text. */
56
+ placeholder?: string;
57
+ /** Stores whether the field is required. */
58
+ required?: boolean;
59
+ /** Stores the alignment hint. */
60
+ align?: ModalContentAlign;
61
+ }
62
+ /** Describes one select option rendered inside a modal. */
63
+ export interface ModalSelectOptionDefinition {
64
+ /** Stores the option label. */
65
+ label: string;
66
+ /** Stores the option value. */
67
+ value: string;
68
+ /** Stores whether the option is selected by default. */
69
+ selected?: boolean;
70
+ }
71
+ /** Describes one select field rendered inside a modal. */
72
+ export interface ModalSelectDefinition {
73
+ /** Always stores `select`. */
74
+ type: 'select';
75
+ /** Stores the field id. */
76
+ id: string;
77
+ /** Stores the label text. */
78
+ label: string;
79
+ /** Stores the available options. */
80
+ options: ModalSelectOptionDefinition[];
81
+ /** Stores the default value. */
82
+ value?: string;
83
+ /** Stores the alignment hint. */
84
+ align?: ModalContentAlign;
85
+ }
86
+ /** Describes one row-level action rendered in a list item. */
87
+ export interface ModalListActionDefinition extends ModalButtonDefinition {
88
+ /** Always stores `button`. */
89
+ type: 'button';
90
+ }
91
+ /** Describes one list row rendered inside a modal list. */
92
+ export interface ModalListRowDefinition {
93
+ /** Stores the row id. */
94
+ id: string;
95
+ /** Stores the row title. */
96
+ title: string;
97
+ /** Stores a short status label. */
98
+ status?: string;
99
+ /** Stores a secondary metadata label. */
100
+ meta?: string;
101
+ /** Stores longer descriptive text. */
102
+ description?: string;
103
+ /** Stores the row actions. */
104
+ actions?: ModalListActionDefinition[];
105
+ }
106
+ /** Describes one list block rendered inside a modal. */
107
+ export interface ModalListDefinition {
108
+ /** Always stores `list`. */
109
+ type: 'list';
110
+ /** Stores the list id. */
111
+ id: string;
112
+ /** Stores the list label. */
113
+ label?: string;
114
+ /** Stores the empty-state text. */
115
+ emptyText?: string;
116
+ /** Stores the alignment hint. */
117
+ align?: ModalContentAlign;
118
+ /** Stores the list rows. */
119
+ items: ModalListRowDefinition[];
120
+ }
121
+ /** Describes one plugin modal content item. */
122
+ export type PluginModalContentItem = ModalTextDefinition | ModalSeparatorDefinition | ModalButtonItemDefinition | ModalInputDefinition | ModalSelectDefinition | ModalListDefinition;
123
+ /** Describes the structured payload used to render a plugin modal. */
124
+ export interface PluginModalDefinition {
125
+ /** Stores the modal title. */
126
+ title: string;
127
+ /** Stores the ordered list of modal content items. */
128
+ content: PluginModalContentItem[];
129
+ }
@@ -0,0 +1,4 @@
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 });
@@ -23,6 +23,8 @@ export type PluginSettingsValue = Record<string, unknown>;
23
23
  export interface PluginHandleActionParams extends RequestParams {
24
24
  /** Stores the action id selected by the user. */
25
25
  action_id?: string;
26
+ /** Stores an optional payload supplied by the triggering UI. */
27
+ payload?: Record<string, unknown>;
26
28
  }
27
29
  /** Describes the params shape for plugin settings callbacks. */
28
30
  export interface PluginSettingsValuesParams extends RequestParams {
@@ -42,7 +44,7 @@ export interface PluginDelegates<TContext = unknown> {
42
44
  /** Returns plugin-contributed menus. */
43
45
  'plugin.get_menus'?: RpcMethodHandler<RequestParams, PluginMenuDefinition[], TContext>;
44
46
  /** Handles a contributed plugin action. */
45
- 'plugin.handle_action'?: RpcMethodHandler<PluginHandleActionParams, null, TContext>;
47
+ 'plugin.handle_action'?: RpcMethodHandler<PluginHandleActionParams, unknown, TContext>;
46
48
  /** Returns the default settings values for the plugin. */
47
49
  'plugin.settings.defaults'?: RpcMethodHandler<RequestParams, PluginSettingsValue[], TContext>;
48
50
  /** Transforms settings values when they are loaded. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openvcs/sdk",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
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/",
@@ -48,7 +48,7 @@ export function createDefaultPluginDelegates<
48
48
  async 'plugin.get_menus'(): Promise<[]> {
49
49
  return [];
50
50
  },
51
- async 'plugin.handle_action'(): Promise<null> {
51
+ async 'plugin.handle_action'(): Promise<unknown> {
52
52
  return null;
53
53
  },
54
54
  async 'plugin.settings.defaults'(): Promise<[]> {
@@ -22,6 +22,7 @@ export { createDefaultPluginDelegates, createRuntimeDispatcher } from './dispatc
22
22
  export { isPluginFailure, pluginError } from './errors';
23
23
  export { createPluginRuntime } from './factory';
24
24
  export { createHost } from './host';
25
+ export { ModalBuilder } from './modal';
25
26
  export {
26
27
  bootstrapPluginModule,
27
28
  createRegisteredPluginRuntime,
@@ -216,15 +216,14 @@ function serializeMenus(): SerializedMenuDefinition[] {
216
216
  }
217
217
 
218
218
  /** Runs a registered action handler by id. */
219
- export async function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<boolean> {
219
+ export async function runRegisteredAction(actionId: string, ...args: unknown[]): Promise<unknown> {
220
220
  const id = String(actionId || '').trim();
221
- if (!id) return false;
221
+ if (!id) return null;
222
222
 
223
223
  const handler = actionHandlers.get(id);
224
- if (!handler) return false;
224
+ if (!handler) return null;
225
225
 
226
- await handler(...args);
227
- return true;
226
+ return await handler(...args);
228
227
  }
229
228
 
230
229
  export interface MenuHandle {
@@ -362,10 +361,10 @@ export function createMenuPluginDelegates(): PluginDelegates<PluginRuntimeContex
362
361
  async 'plugin.get_menus'(): Promise<PluginMenuDefinition[]> {
363
362
  return serializeMenus() as unknown as PluginMenuDefinition[];
364
363
  },
365
- async 'plugin.handle_action'(params: PluginHandleActionParams): Promise<null> {
364
+ async 'plugin.handle_action'(params: PluginHandleActionParams): Promise<unknown> {
366
365
  const actionId = String(params?.action_id || '').trim();
367
366
  if (actionId) {
368
- await runRegisteredAction(actionId);
367
+ return await runRegisteredAction(actionId, params?.payload);
369
368
  }
370
369
  return null;
371
370
  },
@@ -0,0 +1,170 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import type {
5
+ ModalButtonDefinition,
6
+ ModalButtonVariant,
7
+ ModalContentAlign,
8
+ ModalInputDefinition,
9
+ ModalInputKind,
10
+ ModalListDefinition,
11
+ ModalListRowDefinition,
12
+ ModalSelectDefinition,
13
+ ModalSelectOptionDefinition,
14
+ PluginModalContentItem,
15
+ PluginModalDefinition,
16
+ } from '../types/modal.js';
17
+
18
+ /** Describes the options accepted by `ModalBuilder.button()`. */
19
+ export interface ModalBuilderButtonOptions {
20
+ /** Stores the optional tooltip text. */
21
+ title?: string;
22
+ /** Stores the visual button variant. */
23
+ variant?: ModalButtonVariant;
24
+ /** Stores the alignment hint. */
25
+ align?: ModalContentAlign;
26
+ /** Stores a static payload merged into the action payload. */
27
+ payload?: Record<string, unknown>;
28
+ }
29
+
30
+ /** Describes the options accepted by `ModalBuilder.text()`. */
31
+ export interface ModalBuilderTextOptions {
32
+ /** Stores the optional tooltip text. */
33
+ title?: string;
34
+ /** Stores the alignment hint. */
35
+ align?: ModalContentAlign;
36
+ }
37
+
38
+ /** Describes the options accepted by `ModalBuilder.input()`. */
39
+ export interface ModalBuilderInputOptions {
40
+ /** Stores the input kind. */
41
+ kind?: ModalInputKind;
42
+ /** Stores the default value. */
43
+ value?: string;
44
+ /** Stores the placeholder text. */
45
+ placeholder?: string;
46
+ /** Stores whether the field is required. */
47
+ required?: boolean;
48
+ /** Stores the alignment hint. */
49
+ align?: ModalContentAlign;
50
+ }
51
+
52
+ /** Describes the options accepted by `ModalBuilder.select()`. */
53
+ export interface ModalBuilderSelectOptions {
54
+ /** Stores the available options. */
55
+ options: ModalSelectOptionDefinition[];
56
+ /** Stores the default value. */
57
+ value?: string;
58
+ /** Stores the alignment hint. */
59
+ align?: ModalContentAlign;
60
+ }
61
+
62
+ /** Describes the options accepted by `ModalBuilder.list()`. */
63
+ export interface ModalBuilderListOptions {
64
+ /** Stores the list label. */
65
+ label?: string;
66
+ /** Stores the empty-state text. */
67
+ emptyText?: string;
68
+ /** Stores the alignment hint. */
69
+ align?: ModalContentAlign;
70
+ /** Stores the list rows. */
71
+ items: ModalListRowDefinition[];
72
+ }
73
+
74
+ /** Builds a structured modal definition with a fluent class API. */
75
+ export class ModalBuilder {
76
+ private readonly definition: PluginModalDefinition;
77
+
78
+ /** Creates a new modal builder with the provided title. */
79
+ constructor(title: string) {
80
+ this.definition = {
81
+ title: String(title || '').trim(),
82
+ content: [],
83
+ };
84
+ }
85
+
86
+ /** Adds a text block to the modal body. */
87
+ text(content: string, options: ModalBuilderTextOptions = {}): this {
88
+ this.definition.content.push({
89
+ type: 'text',
90
+ content: String(content || ''),
91
+ ...(options.title ? { title: options.title } : {}),
92
+ ...(options.align ? { align: options.align } : {}),
93
+ });
94
+ return this;
95
+ }
96
+
97
+ /** Adds a separator to the modal body. */
98
+ separator(): this {
99
+ this.definition.content.push({ type: 'separator' });
100
+ return this;
101
+ }
102
+
103
+ /** Adds a button to the modal body. */
104
+ button(id: string, content: string, options: ModalBuilderButtonOptions = {}): this {
105
+ this.definition.content.push({
106
+ type: 'button',
107
+ id: String(id || '').trim(),
108
+ content: String(content || ''),
109
+ ...(options.title ? { title: options.title } : {}),
110
+ ...(options.variant && options.variant !== 'default' ? { variant: options.variant } : {}),
111
+ ...(options.align ? { align: options.align } : {}),
112
+ ...(options.payload ? { payload: options.payload } : {}),
113
+ });
114
+ return this;
115
+ }
116
+
117
+ /** Adds a text input to the modal body. */
118
+ input(id: string, label: string, options: ModalBuilderInputOptions = {}): this {
119
+ this.definition.content.push({
120
+ type: 'input',
121
+ id: String(id || '').trim(),
122
+ label: String(label || '').trim(),
123
+ ...(options.kind ? { kind: options.kind } : {}),
124
+ ...(options.value !== undefined ? { value: options.value } : {}),
125
+ ...(options.placeholder ? { placeholder: options.placeholder } : {}),
126
+ ...(options.required ? { required: true } : {}),
127
+ ...(options.align ? { align: options.align } : {}),
128
+ });
129
+ return this;
130
+ }
131
+
132
+ /** Adds a select field to the modal body. */
133
+ select(id: string, label: string, options: ModalBuilderSelectOptions): this {
134
+ this.definition.content.push({
135
+ type: 'select',
136
+ id: String(id || '').trim(),
137
+ label: String(label || '').trim(),
138
+ options: Array.isArray(options.options) ? options.options : [],
139
+ ...(options.value !== undefined ? { value: options.value } : {}),
140
+ ...(options.align ? { align: options.align } : {}),
141
+ });
142
+ return this;
143
+ }
144
+
145
+ /** Adds a list block to the modal body. */
146
+ list(id: string, options: ModalBuilderListOptions): this {
147
+ this.definition.content.push({
148
+ type: 'list',
149
+ id: String(id || '').trim(),
150
+ ...(options.label ? { label: options.label } : {}),
151
+ ...(options.emptyText ? { emptyText: options.emptyText } : {}),
152
+ ...(options.align ? { align: options.align } : {}),
153
+ items: Array.isArray(options.items) ? options.items : [],
154
+ });
155
+ return this;
156
+ }
157
+
158
+ /** Returns the serialized modal payload. */
159
+ build(): PluginModalDefinition {
160
+ return {
161
+ title: this.definition.title,
162
+ content: this.definition.content.map((item) => ({ ...item })) as PluginModalContentItem[],
163
+ };
164
+ }
165
+
166
+ /** Returns the serialized modal payload for a host request. */
167
+ async open(): Promise<PluginModalDefinition> {
168
+ return this.build();
169
+ }
170
+ }
@@ -80,8 +80,11 @@ export function createRegisteredPluginRuntime(
80
80
  },
81
81
  'plugin.handle_action': async (params, ctx) => {
82
82
  const actionId = String(params?.action_id || '').trim();
83
- if (actionId && (await runRegisteredAction(actionId))) {
84
- return null;
83
+ if (actionId) {
84
+ const result = await runRegisteredAction(actionId, params?.payload);
85
+ if (result !== null && result !== undefined) {
86
+ return result;
87
+ }
85
88
  }
86
89
  if (explicitHandleAction) {
87
90
  return explicitHandleAction(params, ctx);
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: GPL-3.0-or-later
3
3
 
4
4
  export * from './host';
5
+ export * from './modal';
5
6
  export * from './menubar';
6
7
  export * from './plugin';
7
8
  export * from './protocol';
@@ -0,0 +1,152 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ /** Describes horizontal alignment hints for modal content. */
5
+ export type ModalContentAlign = 'left' | 'centered' | 'right';
6
+
7
+ /** Describes visual emphasis for modal buttons. */
8
+ export type ModalButtonVariant = 'default' | 'primary' | 'danger';
9
+
10
+ /** Describes the text input kinds supported by plugin modals. */
11
+ export type ModalInputKind = 'text' | 'search' | 'password' | 'url' | 'number';
12
+
13
+ /** Describes one button option rendered inside a modal. */
14
+ export interface ModalButtonDefinition {
15
+ /** Stores the action id triggered when the button is pressed. */
16
+ id: string;
17
+ /** Stores the button label. */
18
+ content: string;
19
+ /** Stores the optional tooltip text. */
20
+ title?: string;
21
+ /** Stores the visual button variant. */
22
+ variant?: ModalButtonVariant;
23
+ /** Stores the button alignment hint. */
24
+ align?: ModalContentAlign;
25
+ /** Stores a static payload merged into the action payload. */
26
+ payload?: Record<string, unknown>;
27
+ }
28
+
29
+ /** Describes one text block rendered inside a modal. */
30
+ export interface ModalTextDefinition {
31
+ /** Always stores `text`. */
32
+ type: 'text';
33
+ /** Stores the block text. */
34
+ content: string;
35
+ /** Stores an optional tooltip. */
36
+ title?: string;
37
+ /** Stores the alignment hint. */
38
+ align?: ModalContentAlign;
39
+ }
40
+
41
+ /** Describes one separator rendered inside a modal. */
42
+ export interface ModalSeparatorDefinition {
43
+ /** Always stores `separator`. */
44
+ type: 'separator';
45
+ }
46
+
47
+ /** Describes one button rendered inside a modal body. */
48
+ export interface ModalButtonItemDefinition extends ModalButtonDefinition {
49
+ /** Always stores `button`. */
50
+ type: 'button';
51
+ }
52
+
53
+ /** Describes one text input rendered inside a modal. */
54
+ export interface ModalInputDefinition {
55
+ /** Always stores `input`. */
56
+ type: 'input';
57
+ /** Stores the input field id. */
58
+ id: string;
59
+ /** Stores the label text. */
60
+ label: string;
61
+ /** Stores the input kind. */
62
+ kind?: ModalInputKind;
63
+ /** Stores the default value. */
64
+ value?: string;
65
+ /** Stores the placeholder text. */
66
+ placeholder?: string;
67
+ /** Stores whether the field is required. */
68
+ required?: boolean;
69
+ /** Stores the alignment hint. */
70
+ align?: ModalContentAlign;
71
+ }
72
+
73
+ /** Describes one select option rendered inside a modal. */
74
+ export interface ModalSelectOptionDefinition {
75
+ /** Stores the option label. */
76
+ label: string;
77
+ /** Stores the option value. */
78
+ value: string;
79
+ /** Stores whether the option is selected by default. */
80
+ selected?: boolean;
81
+ }
82
+
83
+ /** Describes one select field rendered inside a modal. */
84
+ export interface ModalSelectDefinition {
85
+ /** Always stores `select`. */
86
+ type: 'select';
87
+ /** Stores the field id. */
88
+ id: string;
89
+ /** Stores the label text. */
90
+ label: string;
91
+ /** Stores the available options. */
92
+ options: ModalSelectOptionDefinition[];
93
+ /** Stores the default value. */
94
+ value?: string;
95
+ /** Stores the alignment hint. */
96
+ align?: ModalContentAlign;
97
+ }
98
+
99
+ /** Describes one row-level action rendered in a list item. */
100
+ export interface ModalListActionDefinition extends ModalButtonDefinition {
101
+ /** Always stores `button`. */
102
+ type: 'button';
103
+ }
104
+
105
+ /** Describes one list row rendered inside a modal list. */
106
+ export interface ModalListRowDefinition {
107
+ /** Stores the row id. */
108
+ id: string;
109
+ /** Stores the row title. */
110
+ title: string;
111
+ /** Stores a short status label. */
112
+ status?: string;
113
+ /** Stores a secondary metadata label. */
114
+ meta?: string;
115
+ /** Stores longer descriptive text. */
116
+ description?: string;
117
+ /** Stores the row actions. */
118
+ actions?: ModalListActionDefinition[];
119
+ }
120
+
121
+ /** Describes one list block rendered inside a modal. */
122
+ export interface ModalListDefinition {
123
+ /** Always stores `list`. */
124
+ type: 'list';
125
+ /** Stores the list id. */
126
+ id: string;
127
+ /** Stores the list label. */
128
+ label?: string;
129
+ /** Stores the empty-state text. */
130
+ emptyText?: string;
131
+ /** Stores the alignment hint. */
132
+ align?: ModalContentAlign;
133
+ /** Stores the list rows. */
134
+ items: ModalListRowDefinition[];
135
+ }
136
+
137
+ /** Describes one plugin modal content item. */
138
+ export type PluginModalContentItem =
139
+ | ModalTextDefinition
140
+ | ModalSeparatorDefinition
141
+ | ModalButtonItemDefinition
142
+ | ModalInputDefinition
143
+ | ModalSelectDefinition
144
+ | ModalListDefinition;
145
+
146
+ /** Describes the structured payload used to render a plugin modal. */
147
+ export interface PluginModalDefinition {
148
+ /** Stores the modal title. */
149
+ title: string;
150
+ /** Stores the ordered list of modal content items. */
151
+ content: PluginModalContentItem[];
152
+ }
@@ -32,6 +32,8 @@ export type PluginSettingsValue = Record<string, unknown>;
32
32
  export interface PluginHandleActionParams extends RequestParams {
33
33
  /** Stores the action id selected by the user. */
34
34
  action_id?: string;
35
+ /** Stores an optional payload supplied by the triggering UI. */
36
+ payload?: Record<string, unknown>;
35
37
  }
36
38
 
37
39
  /** Describes the params shape for plugin settings callbacks. */
@@ -74,7 +76,7 @@ export interface PluginDelegates<TContext = unknown> {
74
76
  /** Handles a contributed plugin action. */
75
77
  'plugin.handle_action'?: RpcMethodHandler<
76
78
  PluginHandleActionParams,
77
- null,
79
+ unknown,
78
80
  TContext
79
81
  >;
80
82
  /** Returns the default settings values for the plugin. */
@@ -0,0 +1,24 @@
1
+ // Copyright © 2025-2026 OpenVCS Contributors
2
+ // SPDX-License-Identifier: GPL-3.0-or-later
3
+
4
+ import assert from 'node:assert/strict';
5
+ import { describe, it } from 'node:test';
6
+
7
+ import { ModalBuilder } from '../src/lib/runtime/index.js';
8
+
9
+ describe('ModalBuilder', () => {
10
+ it('builds a structured modal definition', () => {
11
+ const modal = new ModalBuilder('Manage Submodules')
12
+ .text('Hello, World!')
13
+ .button('new-button', 'Test button, push me!', { align: 'centered' })
14
+ .build();
15
+
16
+ assert.deepStrictEqual(modal, {
17
+ title: 'Manage Submodules',
18
+ content: [
19
+ { type: 'text', content: 'Hello, World!' },
20
+ { type: 'button', id: 'new-button', content: 'Test button, push me!', align: 'centered' },
21
+ ],
22
+ });
23
+ });
24
+ });