@rainbow-robotics/plugin-ui 0.1.2-dev.5 → 0.1.2-dev.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +236 -75
  2. package/build-dist.mjs +55 -17
  3. package/dist/host-ui.d.ts +49 -0
  4. package/dist/host-ui.d.ts.map +1 -0
  5. package/dist/host-ui.js +33 -0
  6. package/dist/host-ui.js.map +7 -0
  7. package/dist/manifest.d.ts +20 -4
  8. package/dist/manifest.d.ts.map +1 -1
  9. package/dist/manifest.js +1 -1
  10. package/dist/manifest.js.map +2 -2
  11. package/dist/pluginData.d.ts +62 -0
  12. package/dist/pluginData.d.ts.map +1 -0
  13. package/dist/react.d.ts +3430 -36
  14. package/dist/react.js +5 -54
  15. package/dist/react.js.map +2 -2
  16. package/dist/sdk.d.ts +54 -4
  17. package/dist/sdk.d.ts.map +1 -1
  18. package/dist/sdk.js +44 -10
  19. package/dist/sdk.js.map +2 -2
  20. package/dist/viteHost.d.ts +46 -0
  21. package/dist/viteHost.d.ts.map +1 -0
  22. package/dist/viteHost.js +262 -0
  23. package/dist/viteHost.js.map +7 -0
  24. package/dist/ws.d.ts +73 -0
  25. package/dist/ws.d.ts.map +1 -0
  26. package/dist/ws.js +129 -0
  27. package/dist/ws.js.map +7 -0
  28. package/package.json +41 -22
  29. package/src/host-ui.ts +80 -0
  30. package/src/host.tsx +225 -295
  31. package/src/index.d.ts +1 -0
  32. package/src/manifest.ts +25 -4
  33. package/src/pluginData.tsx +232 -0
  34. package/src/react.tsx +9 -127
  35. package/src/sdk.ts +119 -18
  36. package/src/viteHost.ts +309 -0
  37. package/src/ws.ts +204 -0
  38. package/build-browser.mjs +0 -84
  39. package/dist/connect.d.ts +0 -12
  40. package/dist/connect.d.ts.map +0 -1
  41. package/dist/connect.js +0 -29
  42. package/dist/connect.js.map +0 -7
  43. package/dist/elements.d.ts +0 -195
  44. package/dist/elements.d.ts.map +0 -1
  45. package/dist/elements.js +0 -115
  46. package/dist/elements.js.map +0 -7
  47. package/dist/react.d.ts.map +0 -1
  48. package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-AMCIKVG7.js +0 -1
  49. package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-GJWPOWHG.js +0 -1
  50. package/dist-browser/@rainbow-robotics/plugin-ui/chunks/chunk-JRCDA3EG.js +0 -1
  51. package/dist-browser/@rainbow-robotics/plugin-ui/connect.js +0 -1
  52. package/dist-browser/@rainbow-robotics/plugin-ui/elements.js +0 -1
  53. package/dist-browser/@rainbow-robotics/plugin-ui/react.js +0 -1
  54. package/dist-browser/@rainbow-robotics/plugin-ui/sdk.js +0 -1
  55. package/dist-browser/vendor/react-dom-client.js +0 -1
  56. package/dist-browser/vendor/react-jsx-dev-runtime.js +0 -1
  57. package/dist-browser/vendor/react-jsx-runtime.js +0 -1
  58. package/dist-browser/vendor/react-runtime.js +0 -65
  59. package/dist-browser/vendor/react.js +0 -1
  60. package/src/connect.ts +0 -68
  61. package/src/elements.ts +0 -257
package/src/host.tsx CHANGED
@@ -1,343 +1,273 @@
1
1
  /**
2
- * Host bindings — used by nexus (Muscat) only.
2
+ * Host mounting (nexus only). A plugin renders directly in the host document —
3
+ * no iframe. Two strategies, per `PluginModuleSource.kind` (see ./pluginData):
3
4
  *
4
- * Maps the rb-* remote element contract onto the *real* @repo/rb-components,
5
- * and renders a sandboxed plugin's remote tree with those real components.
6
- * The plugin never ships rb-components; it only emits rb-* name tags, which the
7
- * host resolves here. This keeps plugin bundles small and lets rb-components
8
- * evolve freely behind the stable contract.
5
+ * - "esm" (built plugin / robot runtime): dynamic `import()` of the plugin's
6
+ * built entry. react / react-dom / rb-components / plugin-ui are external bare
7
+ * specifiers, resolved by the host page's import map to the host's own loaded
8
+ * instances (see ./viteHost.ts). So the plugin shares the host's React,
9
+ * rb-components, and RbContext, with no duplicate React instance.
9
10
  *
10
- * Verified wiring (@remote-dom/react@1.2.x host + @quilted/threads@4.x):
11
- * - `RemoteReceiver` / `RemoteRootRenderer` / `createRemoteComponentRenderer`
12
- * from `@remote-dom/react/host`
13
- * - host calls `thread.imports.render(receiver.connection, api)`; the sandbox
14
- * (see ./connect) consumes that connection with a RemoteMutationObserver.
11
+ * - "dev" (`muscat-pk dev`): the plugin's Vite dev server serves a bootstrap
12
+ * module exposing `mount(container, opts)`, mounted as an independent React
13
+ * root using the plugin's own deps. HostServices passed in are plain
14
+ * functions (see ./host-ui), so they cross the React-instance boundary safely.
15
15
  */
16
16
  import {
17
- forwardRef,
17
+ useCallback,
18
18
  useEffect,
19
19
  useMemo,
20
20
  useRef,
21
+ useState,
21
22
  type ComponentType,
22
- type ReactNode,
23
23
  } from "react";
24
- import {
25
- RemoteReceiver,
26
- RemoteRootRenderer,
27
- createRemoteComponentRenderer,
28
- type RemoteComponentRendererMap,
29
- } from "@remote-dom/react/host";
30
- import { ThreadWindow } from "@quilted/threads";
31
- import {
32
- RBSlider,
33
- RBLabelButton,
34
- RBIconButton,
35
- RBSwitch,
36
- RBSelectBox,
37
- RBSelectOption,
38
- RBTextInput,
39
- RBIcons,
40
- } from "@repo/rb-components/new_components";
41
- import type { RbSelectOption } from "./elements";
42
- import type { RbSdk } from "./sdk";
24
+ import { RbContext, type RbSdk } from "./sdk";
25
+ import { RbWs, createLazyRbSdk } from "./ws";
26
+ // Import via the package specifier, not a relative "./host-ui": this file ships
27
+ // as source (exports "./host" -> src/host.tsx), so a relative path would hit
28
+ // src/host-ui.ts while other consumers hit dist/host-ui.js — two modules, two
29
+ // `services` singletons.
30
+ import { getHostServices } from "@rainbow-robotics/plugin-ui/host-ui";
31
+ import { usePluginData, type PluginModuleSource } from "./pluginData";
43
32
 
44
- type IconComponent = ComponentType<{ title?: string }>;
45
-
46
- function resolveIcon(name?: string): ReactNode {
47
- if (!name) return null;
48
- const Icon = (RBIcons as Record<string, IconComponent | undefined>)[name];
49
- return Icon ? <Icon /> : null;
33
+ function pluginRootAttrs(pluginId: string) {
34
+ return {
35
+ "data-muscat-plugin-root": "",
36
+ "data-muscat-plugin-id": pluginId,
37
+ };
50
38
  }
51
39
 
52
40
  /* -------------------------------------------------------------------------- */
53
- /* Adapters: rb-* contract props → real rb-components props */
41
+ /* rb sdk — pub/sub/action/serve over the plugin's Zenoh WebSocket */
54
42
  /* -------------------------------------------------------------------------- */
55
43
 
56
- /**
57
- * `createRemoteComponentRenderer` forwards a ref to the wrapped component. Our
58
- * elements expose no remote methods, so wrap each adapter in `forwardRef` (the
59
- * ref is intentionally unused) to avoid React's "function components cannot be
60
- * given refs" warning.
61
- */
62
- function adapt<P extends object>(
63
- Component: (props: P) => ReactNode,
64
- ): ComponentType<P> {
65
- const Wrapped = forwardRef<unknown, P>(function Adapted(props, _ref) {
66
- return Component(props as P);
67
- });
68
- return Wrapped as unknown as ComponentType<P>;
69
- }
44
+ /** Per-mount host-provided bits (initial data + save registration). */
45
+ type PluginMount = {
46
+ data?: unknown;
47
+ onSave: (getPayload: () => unknown) => () => void;
48
+ };
70
49
 
71
- function SliderAdapter({
72
- value,
73
- min = 0,
74
- max = 100,
75
- step,
76
- disabled,
77
- onChange,
78
- }: {
79
- value?: number;
80
- min?: number;
81
- max?: number;
82
- step?: number;
83
- disabled?: boolean;
84
- onChange?: (value: number) => void;
85
- }) {
86
- return (
87
- <RBSlider
88
- value={[value ?? min]}
89
- min={min}
90
- max={max}
91
- step={step}
92
- disabled={disabled}
93
- onValueChange={(values) => onChange?.(values[0] ?? min)}
94
- />
95
- );
96
- }
50
+ function useRbSdk(
51
+ socketUrl: string,
52
+ pluginId: string,
53
+ mount: PluginMount,
54
+ ): RbSdk {
55
+ const wsRef = useRef<Promise<RbWs> | null>(null);
56
+ const keyRef = useRef("");
57
+ const key = `${socketUrl}::${pluginId}`;
58
+ if (keyRef.current !== key) {
59
+ keyRef.current = key;
60
+ wsRef.current = RbWs.connect(socketUrl, pluginId);
61
+ }
97
62
 
98
- function ButtonAdapter({
99
- label,
100
- variant,
101
- size,
102
- disabled,
103
- onPress,
104
- children,
105
- }: {
106
- label?: string;
107
- variant?: string;
108
- size?: string;
109
- disabled?: boolean;
110
- onPress?: () => void;
111
- children?: ReactNode;
112
- }) {
113
- return (
114
- <RBLabelButton
115
- variant={variant as never}
116
- size={size as never}
117
- disabled={disabled}
118
- onClick={() => onPress?.()}
119
- >
120
- {label ?? children}
121
- </RBLabelButton>
63
+ return useMemo<RbSdk>(
64
+ // `mount.data` is captured at first render (stable for a form's lifetime);
65
+ // `mount.onSave` is a stable callback. Both intentionally out of deps.
66
+ () => createLazyRbSdk(wsRef.current!, getHostServices(), mount),
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ [key],
122
69
  );
123
70
  }
124
71
 
125
- function IconButtonAdapter({
126
- icon,
127
- variant,
128
- size,
129
- disabled,
130
- onPress,
72
+ /* -------------------------------------------------------------------------- */
73
+ /* ESM mount (prod / robot runtime — import map resolves shared externals) */
74
+ /* -------------------------------------------------------------------------- */
75
+
76
+ function EsmPluginHost({
77
+ source,
78
+ socketUrl,
79
+ pluginId,
80
+ mount,
131
81
  }: {
132
- icon?: string;
133
- variant?: string;
134
- size?: string;
135
- disabled?: boolean;
136
- onPress?: () => void;
82
+ source: Extract<PluginModuleSource, { kind: "esm" }>;
83
+ socketUrl: string;
84
+ pluginId: string;
85
+ mount: PluginMount;
137
86
  }) {
138
- return (
139
- <RBIconButton
140
- icon={resolveIcon(icon)}
141
- variant={variant as never}
142
- size={size as never}
143
- disabled={disabled}
144
- onClick={() => onPress?.()}
145
- />
146
- );
147
- }
87
+ const rb = useRbSdk(socketUrl, pluginId, mount);
88
+ const [App, setApp] = useState<ComponentType | null>(null);
89
+ const [error, setError] = useState<Error | null>(null);
148
90
 
149
- function TextAdapter({ children }: { children?: ReactNode }) {
150
- return <span>{children}</span>;
151
- }
91
+ useEffect(() => {
92
+ let cancelled = false;
93
+ setApp(null);
94
+ setError(null);
95
+ import(/* @vite-ignore */ source.entryUrl)
96
+ .then((mod: { default?: ComponentType }) => {
97
+ if (cancelled) return;
98
+ if (!mod.default) {
99
+ throw new Error(
100
+ `plugin entry ${source.entryUrl} has no default export`,
101
+ );
102
+ }
103
+ setApp(() => mod.default!);
104
+ })
105
+ .catch((e: unknown) => {
106
+ if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
107
+ });
108
+ return () => {
109
+ cancelled = true;
110
+ };
111
+ }, [source.entryUrl]);
152
112
 
153
- function TextInputAdapter({
154
- value,
155
- placeholder,
156
- disabled,
157
- onInput,
158
- }: {
159
- value?: string;
160
- placeholder?: string;
161
- disabled?: boolean;
162
- onInput?: (value: string) => void;
163
- }) {
113
+ if (error) {
114
+ return (
115
+ <div className="text-fg-danger p-4 text-sm">
116
+ Failed to load plugin: {error.message}
117
+ </div>
118
+ );
119
+ }
120
+ if (!App) {
121
+ return (
122
+ <div className="flex items-center justify-center p-4">
123
+ <div className="border-default size-5 animate-spin rounded-full border-2 border-t-transparent" />
124
+ </div>
125
+ );
126
+ }
164
127
  return (
165
- <RBTextInput
166
- value={value ?? ""}
167
- placeholder={placeholder}
168
- disabled={disabled}
169
- onChange={(e) => onInput?.(e.target.value)}
170
- />
128
+ <div {...pluginRootAttrs(pluginId)}>
129
+ <RbContext.Provider value={rb}>
130
+ <App />
131
+ </RbContext.Provider>
132
+ </div>
171
133
  );
172
134
  }
173
135
 
174
- function SwitchAdapter({
175
- checked,
176
- disabled,
177
- onChange,
178
- }: {
179
- checked?: boolean;
180
- disabled?: boolean;
181
- onChange?: (checked: boolean) => void;
182
- }) {
183
- return (
184
- <RBSwitch
185
- checked={checked ?? false}
186
- disabled={disabled}
187
- onCheckedChange={(next) => onChange?.(next)}
188
- />
189
- );
136
+ /* -------------------------------------------------------------------------- */
137
+ /* Dev mount (muscat-pk dev) */
138
+ /* -------------------------------------------------------------------------- */
139
+
140
+ interface DevBootstrapModule {
141
+ mount: (
142
+ container: HTMLElement,
143
+ opts: {
144
+ socketUrl: string;
145
+ pluginId: string;
146
+ hostServices: ReturnType<typeof getHostServices>;
147
+ data?: unknown;
148
+ onSave: PluginMount["onSave"];
149
+ },
150
+ ) => () => void;
190
151
  }
191
152
 
192
- function SelectAdapter({
193
- value,
194
- options = [],
195
- placeholder,
196
- disabled,
197
- onChange,
153
+ function DevPluginHost({
154
+ source,
155
+ socketUrl,
156
+ pluginId,
157
+ mount,
198
158
  }: {
199
- value?: string;
200
- options?: RbSelectOption[];
201
- placeholder?: string;
202
- disabled?: boolean;
203
- onChange?: (value: string) => void;
159
+ source: Extract<PluginModuleSource, { kind: "dev" }>;
160
+ socketUrl: string;
161
+ pluginId: string;
162
+ mount: PluginMount;
204
163
  }) {
205
- return (
206
- <RBSelectBox
207
- value={value ?? ""}
208
- placeholder={placeholder}
209
- disabled={disabled}
210
- onChange={(e) => onChange?.((e.target as HTMLInputElement).value)}
211
- >
212
- {options.map((option) => (
213
- <RBSelectOption key={option.value} value={option.value}>
214
- {option.label}
215
- </RBSelectOption>
216
- ))}
217
- </RBSelectBox>
218
- );
219
- }
164
+ const containerRef = useRef<HTMLDivElement>(null);
165
+ const [error, setError] = useState<Error | null>(null);
220
166
 
221
- /* -------------------------------------------------------------------------- */
222
- /* Component map: tag → renderer */
223
- /* -------------------------------------------------------------------------- */
167
+ useEffect(() => {
168
+ const container = containerRef.current;
169
+ if (!container) return;
170
+ let unmount: (() => void) | undefined;
171
+ let cancelled = false;
172
+ const deferUnmount = () => queueMicrotask(() => unmount?.());
224
173
 
225
- export const RB_COMPONENT_MAP: RemoteComponentRendererMap = new Map([
226
- [
227
- "rb-slider",
228
- createRemoteComponentRenderer(adapt(SliderAdapter), {
229
- eventProps: { onChange: { event: "change" } },
230
- }),
231
- ],
232
- [
233
- "rb-button",
234
- createRemoteComponentRenderer(adapt(ButtonAdapter), {
235
- eventProps: { onPress: { event: "press" } },
236
- }),
237
- ],
238
- [
239
- "rb-icon-button",
240
- createRemoteComponentRenderer(adapt(IconButtonAdapter), {
241
- eventProps: { onPress: { event: "press" } },
242
- }),
243
- ],
244
- ["rb-text", createRemoteComponentRenderer(adapt(TextAdapter))],
245
- [
246
- "rb-text-input",
247
- createRemoteComponentRenderer(adapt(TextInputAdapter), {
248
- eventProps: { onInput: { event: "input" } },
249
- }),
250
- ],
251
- [
252
- "rb-switch",
253
- createRemoteComponentRenderer(adapt(SwitchAdapter), {
254
- eventProps: { onChange: { event: "change" } },
255
- }),
256
- ],
257
- [
258
- "rb-select",
259
- createRemoteComponentRenderer(adapt(SelectAdapter), {
260
- eventProps: { onChange: { event: "change" } },
261
- }),
262
- ],
263
- ]);
174
+ import(/* @vite-ignore */ source.bootstrapUrl)
175
+ .then((mod: DevBootstrapModule) => {
176
+ if (cancelled) return;
177
+ unmount = mod.mount(container, {
178
+ socketUrl,
179
+ pluginId,
180
+ hostServices: getHostServices(),
181
+ data: mount.data,
182
+ onSave: mount.onSave,
183
+ });
184
+ })
185
+ .catch((e: unknown) => {
186
+ if (!cancelled) setError(e instanceof Error ? e : new Error(String(e)));
187
+ });
188
+
189
+ return () => {
190
+ cancelled = true;
191
+ deferUnmount();
192
+ };
193
+ }, [source.bootstrapUrl, socketUrl, pluginId]);
194
+
195
+ if (error) {
196
+ return (
197
+ <div className="text-fg-danger p-4 text-sm">
198
+ Failed to load plugin (dev): {error.message}
199
+ </div>
200
+ );
201
+ }
202
+ return <div ref={containerRef} {...pluginRootAttrs(pluginId)} />;
203
+ }
264
204
 
265
205
  /* -------------------------------------------------------------------------- */
266
- /* Host SDK contract + mount component */
206
+ /* Entry point */
267
207
  /* -------------------------------------------------------------------------- */
268
208
 
269
- export type RbSdkImpl = RbSdk;
209
+ /** Renders a plugin's UI. Requires a `PluginDataProvider` above. */
210
+ export function PluginHost({
211
+ pluginId,
212
+ ui,
213
+ data,
214
+ onRegisterSave,
215
+ }: {
216
+ pluginId: string;
217
+ ui: string;
218
+ /** Initial data handed to the plugin (e.g. previously-saved form values). */
219
+ data?: unknown;
220
+ /**
221
+ * Called once with a getter the host can invoke (e.g. from a Save button) to
222
+ * collect the plugin's current payload — `undefined` until the plugin
223
+ * registers via `useRbSave`, or if it never does.
224
+ */
225
+ onRegisterSave?: (getPayload: () => unknown) => void;
226
+ }) {
227
+ const { getModuleSource, getSocketUrl } = usePluginData();
228
+ const source = useMemo(
229
+ () => getModuleSource(pluginId, ui),
230
+ [getModuleSource, pluginId, ui],
231
+ );
232
+ const socketUrl = getSocketUrl(pluginId);
270
233
 
271
- /** Shape the host exposes to the sandbox over the thread (see ./connect). */
272
- interface HostThreadExports {
273
- connect: () => {
274
- connection: {
275
- mutate: (records: unknown) => void;
276
- call: (id: string, method: string, ...args: unknown[]) => unknown;
234
+ // The plugin registers its payload getter here; the host reads it back
235
+ // through the stable getter handed to `onRegisterSave`.
236
+ const saveRef = useRef<(() => unknown) | null>(null);
237
+ const registerSave = useCallback((getPayload: () => unknown) => {
238
+ saveRef.current = getPayload;
239
+ return () => {
240
+ if (saveRef.current === getPayload) saveRef.current = null;
277
241
  };
278
- action: (id: string, payload?: unknown) => Promise<unknown>;
279
- notify: (message: string, type?: string) => void;
280
- };
281
- }
282
-
283
- export interface PluginContentProps {
284
- /** URL of the plugin sandbox document (e.g. /plugin/:id/sandbox.html). */
285
- sandboxUrl: string;
286
- /** Host SDK implementation handed to the plugin. */
287
- rb: RbSdkImpl;
288
- }
289
-
290
- /**
291
- * Renders a sandboxed plugin: spins up a hidden iframe running the plugin's
292
- * remote tree, and mirrors that tree with real rb-components in the host.
293
- */
294
- export function PluginContent({ sandboxUrl, rb }: PluginContentProps) {
295
- const receiver = useMemo(() => new RemoteReceiver(), []);
296
- const iframeRef = useRef<HTMLIFrameElement>(null);
297
- const rbRef = useRef(rb);
298
- rbRef.current = rb;
242
+ }, []);
299
243
 
300
244
  useEffect(() => {
301
- const iframe = iframeRef.current;
302
- if (!iframe) return;
245
+ onRegisterSave?.(() => saveRef.current?.());
246
+ }, [onRegisterSave]);
303
247
 
304
- const controller = new AbortController();
305
- // Host listens from mount; the sandbox calls connect() once it is ready.
306
- ThreadWindow.iframe<Record<string, never>, HostThreadExports>(iframe, {
307
- targetOrigin: "*",
308
- signal: controller.signal,
309
- exports: {
310
- connect() {
311
- return {
312
- // forward the receiver's low-level connection across the boundary
313
- connection: {
314
- mutate: (records: unknown) =>
315
- receiver.connection.mutate(records as never),
316
- call: (id: string, method: string, ...args: unknown[]) =>
317
- receiver.connection.call(id, method, ...args),
318
- },
319
- action: (id: string, payload?: unknown) =>
320
- rbRef.current.action(id, payload),
321
- notify: (message: string, type?: string) =>
322
- rbRef.current.notify(message, type as never),
323
- };
324
- },
325
- },
326
- });
327
-
328
- return () => controller.abort();
329
- }, [receiver]);
248
+ const mount = useMemo<PluginMount>(
249
+ // data captured once (stable per form lifetime); registerSave is stable.
250
+ () => ({ data, onSave: registerSave }),
251
+ // eslint-disable-next-line react-hooks/exhaustive-deps
252
+ [registerSave],
253
+ );
330
254
 
331
- return (
332
- <>
333
- <iframe
334
- ref={iframeRef}
335
- src={sandboxUrl}
336
- sandbox="allow-scripts"
337
- hidden
338
- title="rb-plugin-sandbox"
255
+ if (source.kind === "dev") {
256
+ return (
257
+ <DevPluginHost
258
+ source={source}
259
+ socketUrl={socketUrl}
260
+ pluginId={pluginId}
261
+ mount={mount}
339
262
  />
340
- <RemoteRootRenderer receiver={receiver} components={RB_COMPONENT_MAP} />
341
- </>
263
+ );
264
+ }
265
+ return (
266
+ <EsmPluginHost
267
+ source={source}
268
+ socketUrl={socketUrl}
269
+ pluginId={pluginId}
270
+ mount={mount}
271
+ />
342
272
  );
343
273
  }
package/src/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="@repo/typescript-config" />
package/src/manifest.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  /**
2
3
  * Plugin manifest schema — the single source of truth shared by the front end
3
4
  * (nexus reads it) and the back end (rby2 mirrors it for validation).
@@ -7,7 +8,7 @@
7
8
 
8
9
  /** Known contribution points the host understands. */
9
10
  export const CONTRIBUTION_POINTS = [
10
- "header.toolbar",
11
+ "toolbar",
11
12
  "lnb",
12
13
  "move.panel",
13
14
  "program.panel",
@@ -21,12 +22,26 @@ export type ContributionOpens = "popover" | "panel" | "page";
21
22
  export interface PluginContribution {
22
23
  /** Where this contribution mounts. */
23
24
  point: ContributionPoint | (string & {});
25
+ /** UI entry name relative to ui/ (without extension). Defaults to "content". */
26
+ ui?: string;
24
27
  /** Icon path relative to the plugin root (e.g. "assets/icon.svg"). */
25
- icon?: string;
28
+ icon?:
29
+ | string
30
+ | {
31
+ "16"?: string;
32
+ "48"?: string;
33
+ "126"?: string;
34
+ };
26
35
  /** Display label. */
27
- label?: string;
36
+ label?: string | Record<string, string>;
28
37
  /** For header.toolbar: how the contribution surfaces. */
29
38
  opens?: ContributionOpens;
39
+ commandInfo?: {
40
+ name: string;
41
+ icon: string;
42
+ funcName: string;
43
+ args?: Record<string, any>;
44
+ };
30
45
  }
31
46
 
32
47
  export interface PluginAction {
@@ -58,7 +73,13 @@ export interface PluginManifest {
58
73
  /** Minimum host (Muscat) version this plugin supports. */
59
74
  minAppVersion?: string;
60
75
  /** Icon path relative to plugin root. */
61
- icon?: string;
76
+ icons:
77
+ | string
78
+ | {
79
+ "16"?: string;
80
+ "48"?: string;
81
+ "128"?: string;
82
+ };
62
83
  /** UI / chrome contributions. */
63
84
  contributes?: PluginContribution[];
64
85
  /** Host actions the plugin can invoke. */