@xemahq/ui-kernel 0.9.0 → 0.9.1
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/biome-host/biome-registry.d.ts.map +1 -1
- package/dist/lib/biome-host/biome-registry.js +14 -9
- package/dist/lib/biome-host/biome-registry.js.map +1 -1
- package/dist/lib/contribution-registry/contribution-registry.d.ts +24 -0
- package/dist/lib/contribution-registry/contribution-registry.d.ts.map +1 -0
- package/dist/lib/contribution-registry/contribution-registry.js +67 -0
- package/dist/lib/contribution-registry/contribution-registry.js.map +1 -0
- package/dist/lib/contribution-registry/index.d.ts +3 -0
- package/dist/lib/contribution-registry/index.d.ts.map +1 -0
- package/dist/lib/contribution-registry/index.js +3 -0
- package/dist/lib/contribution-registry/index.js.map +1 -0
- package/dist/lib/contribution-registry/renderable-registry.d.ts +33 -0
- package/dist/lib/contribution-registry/renderable-registry.d.ts.map +1 -0
- package/dist/lib/contribution-registry/renderable-registry.js +51 -0
- package/dist/lib/contribution-registry/renderable-registry.js.map +1 -0
- package/dist/ui/capture/field-spec-to-zod.d.ts +1 -0
- package/dist/ui/capture/field-spec-to-zod.d.ts.map +1 -1
- package/dist/ui/capture/field-spec-to-zod.js +19 -0
- package/dist/ui/capture/field-spec-to-zod.js.map +1 -1
- package/dist/ui/capture/field-spec-validate.d.ts +4 -0
- package/dist/ui/capture/field-spec-validate.d.ts.map +1 -0
- package/dist/ui/capture/field-spec-validate.js +129 -0
- package/dist/ui/capture/field-spec-validate.js.map +1 -0
- package/dist/ui/capture/field-spec.d.ts +13 -1
- package/dist/ui/capture/field-spec.d.ts.map +1 -1
- package/dist/ui/capture/field-spec.js +25 -0
- package/dist/ui/capture/field-spec.js.map +1 -1
- package/dist/ui/capture/index.d.ts +4 -2
- package/dist/ui/capture/index.d.ts.map +1 -1
- package/dist/ui/capture/index.js +4 -2
- package/dist/ui/capture/index.js.map +1 -1
- package/dist/ui/capture/schema-field-input.d.ts.map +1 -1
- package/dist/ui/capture/schema-field-input.js +39 -12
- package/dist/ui/capture/schema-field-input.js.map +1 -1
- package/dist/ui/capture/submit-values.d.ts +6 -0
- package/dist/ui/capture/submit-values.d.ts.map +1 -0
- package/dist/ui/capture/submit-values.js +28 -0
- package/dist/ui/capture/submit-values.js.map +1 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/primitives/overflow-tabs.d.ts +5 -0
- package/dist/ui/primitives/overflow-tabs.d.ts.map +1 -1
- package/dist/ui/primitives/overflow-tabs.js +4 -4
- package/dist/ui/primitives/overflow-tabs.js.map +1 -1
- package/dist/ui/primitives/url-preview-chrome.d.ts +8 -0
- package/dist/ui/primitives/url-preview-chrome.d.ts.map +1 -0
- package/dist/ui/primitives/url-preview-chrome.js +40 -0
- package/dist/ui/primitives/url-preview-chrome.js.map +1 -0
- package/dist/vista/GenericCatalogueSurface.d.ts +10 -0
- package/dist/vista/GenericCatalogueSurface.d.ts.map +1 -0
- package/dist/vista/GenericCatalogueSurface.js +80 -0
- package/dist/vista/GenericCatalogueSurface.js.map +1 -0
- package/dist/vista/SurfaceFallbackCard.d.ts +9 -0
- package/dist/vista/SurfaceFallbackCard.d.ts.map +1 -0
- package/dist/vista/SurfaceFallbackCard.js +44 -0
- package/dist/vista/SurfaceFallbackCard.js.map +1 -0
- package/dist/vista/VistaPanel.d.ts +21 -0
- package/dist/vista/VistaPanel.d.ts.map +1 -0
- package/dist/vista/VistaPanel.js +31 -0
- package/dist/vista/VistaPanel.js.map +1 -0
- package/dist/vista/html/CustomUiFrame.d.ts +20 -0
- package/dist/vista/html/CustomUiFrame.d.ts.map +1 -0
- package/dist/vista/html/CustomUiFrame.js +69 -0
- package/dist/vista/html/CustomUiFrame.js.map +1 -0
- package/dist/vista/html/custom-ui-frame-bridge.d.ts +13 -0
- package/dist/vista/html/custom-ui-frame-bridge.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-frame-bridge.js +30 -0
- package/dist/vista/html/custom-ui-frame-bridge.js.map +1 -0
- package/dist/vista/html/custom-ui-frame-contract.d.ts +29 -0
- package/dist/vista/html/custom-ui-frame-contract.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-frame-contract.js +16 -0
- package/dist/vista/html/custom-ui-frame-contract.js.map +1 -0
- package/dist/vista/html/custom-ui-intents.d.ts +20 -0
- package/dist/vista/html/custom-ui-intents.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-intents.js +57 -0
- package/dist/vista/html/custom-ui-intents.js.map +1 -0
- package/dist/vista/html/custom-ui-srcdoc.d.ts +17 -0
- package/dist/vista/html/custom-ui-srcdoc.d.ts.map +1 -0
- package/dist/vista/html/custom-ui-srcdoc.js +70 -0
- package/dist/vista/html/custom-ui-srcdoc.js.map +1 -0
- package/dist/vista/html/index.d.ts +6 -0
- package/dist/vista/html/index.d.ts.map +1 -0
- package/dist/vista/html/index.js +6 -0
- package/dist/vista/html/index.js.map +1 -0
- package/dist/vista/index.d.ts +18 -0
- package/dist/vista/index.d.ts.map +1 -0
- package/dist/vista/index.js +18 -0
- package/dist/vista/index.js.map +1 -0
- package/dist/vista/surface-catalogue.d.ts +5 -0
- package/dist/vista/surface-catalogue.d.ts.map +1 -0
- package/dist/vista/surface-catalogue.js +30 -0
- package/dist/vista/surface-catalogue.js.map +1 -0
- package/dist/vista/surface-icons.d.ts +7 -0
- package/dist/vista/surface-icons.d.ts.map +1 -0
- package/dist/vista/surface-icons.js +26 -0
- package/dist/vista/surface-icons.js.map +1 -0
- package/dist/vista/surface-registry.d.ts +7 -0
- package/dist/vista/surface-registry.d.ts.map +1 -0
- package/dist/vista/surface-registry.js +7 -0
- package/dist/vista/surface-registry.js.map +1 -0
- package/dist/vista/surfaces/ChartSurface.d.ts +4 -0
- package/dist/vista/surfaces/ChartSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/ChartSurface.js +17 -0
- package/dist/vista/surfaces/ChartSurface.js.map +1 -0
- package/dist/vista/surfaces/FilesSurface.d.ts +15 -0
- package/dist/vista/surfaces/FilesSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/FilesSurface.js +121 -0
- package/dist/vista/surfaces/FilesSurface.js.map +1 -0
- package/dist/vista/surfaces/FormSurface.d.ts +4 -0
- package/dist/vista/surfaces/FormSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/FormSurface.js +97 -0
- package/dist/vista/surfaces/FormSurface.js.map +1 -0
- package/dist/vista/surfaces/HtmlSurface.d.ts +4 -0
- package/dist/vista/surfaces/HtmlSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/HtmlSurface.js +11 -0
- package/dist/vista/surfaces/HtmlSurface.js.map +1 -0
- package/dist/vista/surfaces/TableSurface.d.ts +4 -0
- package/dist/vista/surfaces/TableSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/TableSurface.js +42 -0
- package/dist/vista/surfaces/TableSurface.js.map +1 -0
- package/dist/vista/surfaces/UrlSurface.d.ts +4 -0
- package/dist/vista/surfaces/UrlSurface.d.ts.map +1 -0
- package/dist/vista/surfaces/UrlSurface.js +7 -0
- package/dist/vista/surfaces/UrlSurface.js.map +1 -0
- package/dist/vista/surfaces/blob-surfaces.d.ts +6 -0
- package/dist/vista/surfaces/blob-surfaces.d.ts.map +1 -0
- package/dist/vista/surfaces/blob-surfaces.js +38 -0
- package/dist/vista/surfaces/blob-surfaces.js.map +1 -0
- package/dist/vista/surfaces/elicitation-transport.d.ts +18 -0
- package/dist/vista/surfaces/elicitation-transport.d.ts.map +1 -0
- package/dist/vista/surfaces/elicitation-transport.js +100 -0
- package/dist/vista/surfaces/elicitation-transport.js.map +1 -0
- package/dist/vista/surfaces/index.d.ts +11 -0
- package/dist/vista/surfaces/index.d.ts.map +1 -0
- package/dist/vista/surfaces/index.js +89 -0
- package/dist/vista/surfaces/index.js.map +1 -0
- package/dist/vista/surfaces/text-surfaces.d.ts +8 -0
- package/dist/vista/surfaces/text-surfaces.d.ts.map +1 -0
- package/dist/vista/surfaces/text-surfaces.js +54 -0
- package/dist/vista/surfaces/text-surfaces.js.map +1 -0
- package/dist/vista/use-vista-tabs.d.ts +30 -0
- package/dist/vista/use-vista-tabs.d.ts.map +1 -0
- package/dist/vista/use-vista-tabs.js +77 -0
- package/dist/vista/use-vista-tabs.js.map +1 -0
- package/dist/vista/vista-code-renderer.d.ts +12 -0
- package/dist/vista/vista-code-renderer.d.ts.map +1 -0
- package/dist/vista/vista-code-renderer.js +10 -0
- package/dist/vista/vista-code-renderer.js.map +1 -0
- package/dist/vista/vista-file-source.d.ts +20 -0
- package/dist/vista/vista-file-source.d.ts.map +1 -0
- package/dist/vista/vista-file-source.js +69 -0
- package/dist/vista/vista-file-source.js.map +1 -0
- package/dist/vista/vista-form-host.d.ts +16 -0
- package/dist/vista/vista-form-host.d.ts.map +1 -0
- package/dist/vista/vista-form-host.js +10 -0
- package/dist/vista/vista-form-host.js.map +1 -0
- package/dist/vista/vista-model.d.ts +12 -0
- package/dist/vista/vista-model.d.ts.map +1 -0
- package/dist/vista/vista-model.js +72 -0
- package/dist/vista/vista-model.js.map +1 -0
- package/dist/vista/vista-open-context.d.ts +9 -0
- package/dist/vista/vista-open-context.d.ts.map +1 -0
- package/dist/vista/vista-open-context.js +10 -0
- package/dist/vista/vista-open-context.js.map +1 -0
- package/dist/vista/vista-payloads.d.ts +107 -0
- package/dist/vista/vista-payloads.d.ts.map +1 -0
- package/dist/vista/vista-payloads.js +305 -0
- package/dist/vista/vista-payloads.js.map +1 -0
- package/dist/vista/vista-tabs-manifest.d.ts +17 -0
- package/dist/vista/vista-tabs-manifest.d.ts.map +1 -0
- package/dist/vista/vista-tabs-manifest.js +60 -0
- package/dist/vista/vista-tabs-manifest.js.map +1 -0
- package/dist/vista/vista-tools.d.ts +41 -0
- package/dist/vista/vista-tools.d.ts.map +1 -0
- package/dist/vista/vista-tools.js +113 -0
- package/dist/vista/vista-tools.js.map +1 -0
- package/package.json +3 -3
- package/src/index.ts +5 -0
- package/src/lib/biome-host/biome-registry.ts +31 -11
- package/src/lib/contribution-registry/contribution-registry.spec.ts +141 -0
- package/src/lib/contribution-registry/contribution-registry.ts +202 -0
- package/src/lib/contribution-registry/index.ts +9 -0
- package/src/lib/contribution-registry/renderable-registry.ts +142 -0
- package/src/ui/capture/field-spec-to-zod.ts +22 -0
- package/src/ui/capture/field-spec-validate.ts +147 -0
- package/src/ui/capture/field-spec.ts +62 -0
- package/src/ui/capture/index.ts +9 -1
- package/src/ui/capture/schema-field-input.tsx +134 -14
- package/src/ui/capture/submit-values.test.ts +70 -0
- package/src/ui/capture/submit-values.ts +63 -0
- package/src/ui/index.ts +4 -0
- package/src/ui/primitives/overflow-tabs.tsx +24 -4
- package/src/ui/primitives/url-preview-chrome.tsx +102 -0
- package/src/vista/GenericCatalogueSurface.tsx +177 -0
- package/src/vista/SurfaceFallbackCard.tsx +109 -0
- package/src/vista/VistaPanel.tsx +155 -0
- package/src/vista/form-payload.spec.ts +93 -0
- package/src/vista/html/CustomUiFrame.spec.tsx +125 -0
- package/src/vista/html/CustomUiFrame.tsx +204 -0
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +191 -0
- package/src/vista/html/custom-ui-frame-bridge.ts +92 -0
- package/src/vista/html/custom-ui-frame-contract.ts +88 -0
- package/src/vista/html/custom-ui-intents.spec.ts +133 -0
- package/src/vista/html/custom-ui-intents.ts +151 -0
- package/src/vista/html/custom-ui-srcdoc.spec.ts +151 -0
- package/src/vista/html/custom-ui-srcdoc.ts +193 -0
- package/src/vista/html/index.ts +13 -0
- package/src/vista/index.ts +48 -0
- package/src/vista/surface-catalogue.spec.ts +140 -0
- package/src/vista/surface-catalogue.ts +84 -0
- package/src/vista/surface-icons.ts +57 -0
- package/src/vista/surface-registry.ts +56 -0
- package/src/vista/surfaces/ChartSurface.tsx +34 -0
- package/src/vista/surfaces/FilesSurface.tsx +268 -0
- package/src/vista/surfaces/FormSurface.tsx +264 -0
- package/src/vista/surfaces/HtmlSurface.tsx +43 -0
- package/src/vista/surfaces/TableSurface.tsx +99 -0
- package/src/vista/surfaces/UrlSurface.tsx +40 -0
- package/src/vista/surfaces/blob-surfaces.tsx +129 -0
- package/src/vista/surfaces/elicitation-transport.spec.ts +187 -0
- package/src/vista/surfaces/elicitation-transport.ts +175 -0
- package/src/vista/surfaces/index.tsx +157 -0
- package/src/vista/surfaces/text-surfaces.tsx +119 -0
- package/src/vista/use-vista-tabs.ts +188 -0
- package/src/vista/vista-code-renderer.tsx +46 -0
- package/src/vista/vista-file-source.tsx +163 -0
- package/src/vista/vista-form-host.tsx +78 -0
- package/src/vista/vista-model.ts +130 -0
- package/src/vista/vista-open-context.tsx +42 -0
- package/src/vista/vista-payloads.ts +514 -0
- package/src/vista/vista-tabs-manifest.ts +123 -0
- package/src/vista/vista-tools.ts +220 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { VistaTabsController } from './use-vista-tabs';
|
|
3
|
+
export declare enum VistaToolName {
|
|
4
|
+
Open = "vista_open",
|
|
5
|
+
Update = "vista_update",
|
|
6
|
+
Close = "vista_close",
|
|
7
|
+
List = "vista_list"
|
|
8
|
+
}
|
|
9
|
+
export declare const VistaOpenArgsSchema: z.ZodType<import("@xemahq/kernel-contracts/surface").VistaSurface, unknown, z.core.$ZodTypeInternals<import("@xemahq/kernel-contracts/surface").VistaSurface, unknown>>;
|
|
10
|
+
export type VistaOpenArgs = z.infer<typeof VistaOpenArgsSchema>;
|
|
11
|
+
export declare const VistaUpdateArgsSchema: z.ZodObject<{
|
|
12
|
+
tabId: z.ZodString;
|
|
13
|
+
title: z.ZodOptional<z.ZodString>;
|
|
14
|
+
payload: z.ZodOptional<z.ZodUnknown>;
|
|
15
|
+
pinned: z.ZodOptional<z.ZodBoolean>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type VistaUpdateArgs = z.infer<typeof VistaUpdateArgsSchema>;
|
|
18
|
+
export declare const VistaCloseArgsSchema: z.ZodObject<{
|
|
19
|
+
tabId: z.ZodString;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
export type VistaCloseArgs = z.infer<typeof VistaCloseArgsSchema>;
|
|
22
|
+
export declare const VistaListArgsSchema: z.ZodObject<{}, z.core.$strip>;
|
|
23
|
+
export type VistaListArgs = z.infer<typeof VistaListArgsSchema>;
|
|
24
|
+
export declare enum VistaToolError {
|
|
25
|
+
NoPanel = "no-panel",
|
|
26
|
+
InvalidArgs = "invalid-args",
|
|
27
|
+
UnknownTool = "unknown-tool",
|
|
28
|
+
UnknownTab = "unknown-tab",
|
|
29
|
+
TabRefused = "tab-refused"
|
|
30
|
+
}
|
|
31
|
+
export type VistaToolResult<TValue = unknown> = {
|
|
32
|
+
readonly ok: true;
|
|
33
|
+
readonly value: TValue;
|
|
34
|
+
} | {
|
|
35
|
+
readonly ok: false;
|
|
36
|
+
readonly error: VistaToolError;
|
|
37
|
+
readonly detail?: string;
|
|
38
|
+
};
|
|
39
|
+
export declare const VistaToolResultSchema: z.ZodType<VistaToolResult>;
|
|
40
|
+
export declare function dispatchVistaTool(controller: VistaTabsController | null, tool: string, args: unknown): VistaToolResult;
|
|
41
|
+
//# sourceMappingURL=vista-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vista-tools.d.ts","sourceRoot":"","sources":["../../src/vista/vista-tools.ts"],"names":[],"mappings":"AAkCA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAqB,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAG/E,oBAAY,aAAa;IACvB,IAAI,eAAe;IACnB,MAAM,iBAAiB;IACvB,KAAK,gBAAgB;IACrB,IAAI,eAAe;CACpB;AAUD,eAAO,MAAM,mBAAmB,yKAAqB,CAAC;AACtD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAWhE,eAAO,MAAM,qBAAqB;;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAGpE,eAAO,MAAM,oBAAoB;;iBAAyC,CAAC;AAC3E,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAGlE,eAAO,MAAM,mBAAmB,gCAAe,CAAC;AAChD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,oBAAY,cAAc;IAExB,OAAO,aAAa;IAEpB,WAAW,iBAAiB;IAE5B,WAAW,iBAAiB;IAE5B,UAAU,gBAAgB;IAM1B,UAAU,gBAAgB;CAC3B;AAGD,MAAM,MAAM,eAAe,CAAC,MAAM,GAAG,OAAO,IACxC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAQN,eAAO,MAAM,qBAAqB,EAAE,CAAC,CAAC,OAAO,CAAC,eAAe,CAO7B,CAAC;AAWjC,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,mBAAmB,GAAG,IAAI,EACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,GACZ,eAAe,CAsDjB"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { VistaSurfaceSchema } from '@xemahq/kernel-contracts/surface';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export var VistaToolName;
|
|
4
|
+
(function (VistaToolName) {
|
|
5
|
+
VistaToolName["Open"] = "vista_open";
|
|
6
|
+
VistaToolName["Update"] = "vista_update";
|
|
7
|
+
VistaToolName["Close"] = "vista_close";
|
|
8
|
+
VistaToolName["List"] = "vista_list";
|
|
9
|
+
})(VistaToolName || (VistaToolName = {}));
|
|
10
|
+
export const VistaOpenArgsSchema = VistaSurfaceSchema;
|
|
11
|
+
export const VistaUpdateArgsSchema = z.object({
|
|
12
|
+
tabId: z.string().min(1),
|
|
13
|
+
title: z.string().min(1).optional(),
|
|
14
|
+
payload: z.unknown().optional(),
|
|
15
|
+
pinned: z.boolean().optional(),
|
|
16
|
+
});
|
|
17
|
+
export const VistaCloseArgsSchema = z.object({ tabId: z.string().min(1) });
|
|
18
|
+
export const VistaListArgsSchema = z.object({});
|
|
19
|
+
export var VistaToolError;
|
|
20
|
+
(function (VistaToolError) {
|
|
21
|
+
VistaToolError["NoPanel"] = "no-panel";
|
|
22
|
+
VistaToolError["InvalidArgs"] = "invalid-args";
|
|
23
|
+
VistaToolError["UnknownTool"] = "unknown-tool";
|
|
24
|
+
VistaToolError["UnknownTab"] = "unknown-tab";
|
|
25
|
+
VistaToolError["TabRefused"] = "tab-refused";
|
|
26
|
+
})(VistaToolError || (VistaToolError = {}));
|
|
27
|
+
export const VistaToolResultSchema = z.union([
|
|
28
|
+
z.object({ ok: z.literal(true), value: z.unknown() }),
|
|
29
|
+
z.object({
|
|
30
|
+
ok: z.literal(false),
|
|
31
|
+
error: z.nativeEnum(VistaToolError),
|
|
32
|
+
detail: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
export function dispatchVistaTool(controller, tool, args) {
|
|
36
|
+
if (controller === null) {
|
|
37
|
+
return { ok: false, error: VistaToolError.NoPanel };
|
|
38
|
+
}
|
|
39
|
+
switch (tool) {
|
|
40
|
+
case VistaToolName.Open: {
|
|
41
|
+
const parsed = VistaOpenArgsSchema.safeParse(args);
|
|
42
|
+
if (!parsed.success)
|
|
43
|
+
return invalid(parsed.error);
|
|
44
|
+
return { ok: true, value: { tabId: controller.open(parsed.data) } };
|
|
45
|
+
}
|
|
46
|
+
case VistaToolName.Update: {
|
|
47
|
+
const parsed = VistaUpdateArgsSchema.safeParse(args);
|
|
48
|
+
if (!parsed.success)
|
|
49
|
+
return invalid(parsed.error);
|
|
50
|
+
const { tabId, ...patch } = parsed.data;
|
|
51
|
+
const refusal = refusalFor(controller, tabId);
|
|
52
|
+
if (refusal !== null)
|
|
53
|
+
return refusal;
|
|
54
|
+
if (!controller.update(tabId, patch)) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: VistaToolError.TabRefused,
|
|
58
|
+
detail: `Tab "${tabId}" is host-owned; its content is driven by the host, not by vista_update.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, value: { updated: true } };
|
|
62
|
+
}
|
|
63
|
+
case VistaToolName.Close: {
|
|
64
|
+
const parsed = VistaCloseArgsSchema.safeParse(args);
|
|
65
|
+
if (!parsed.success)
|
|
66
|
+
return invalid(parsed.error);
|
|
67
|
+
const { tabId } = parsed.data;
|
|
68
|
+
const refusal = refusalFor(controller, tabId);
|
|
69
|
+
if (refusal !== null)
|
|
70
|
+
return refusal;
|
|
71
|
+
if (!controller.close(tabId)) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: VistaToolError.TabRefused,
|
|
75
|
+
detail: `Tab "${tabId}" is pinned and cannot be closed.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, value: { closed: true } };
|
|
79
|
+
}
|
|
80
|
+
case VistaToolName.List: {
|
|
81
|
+
const parsed = VistaListArgsSchema.safeParse(args ?? {});
|
|
82
|
+
if (!parsed.success)
|
|
83
|
+
return invalid(parsed.error);
|
|
84
|
+
return { ok: true, value: { tabs: controller.list() } };
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: VistaToolError.UnknownTool,
|
|
90
|
+
detail: `'${tool}' is not a Vista tool.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function refusalFor(controller, tabId) {
|
|
95
|
+
if (controller.list().some((tab) => tab.tabId === tabId)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: VistaToolError.UnknownTab,
|
|
101
|
+
detail: `No tab "${tabId}" is open.`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function invalid(error) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: VistaToolError.InvalidArgs,
|
|
108
|
+
detail: error.issues
|
|
109
|
+
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
|
|
110
|
+
.join('; '),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=vista-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vista-tools.js","sourceRoot":"","sources":["../../src/vista/vista-tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/ui-kernel",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Host-framework-agnostic UI kernel for the Xema OS. Defines the SystemBus orchestration contract (capability.invoke, cross-biome intents, command palette, xema:// deeplinks, window manager) AND the biome-host contract surface (FrontendBiome/FrontendBiomeFactory, HostBridge, the singleton biomeRegistry, session contributions) that every frontend biome composes against. No Vite, Next.js, or React-Router — React itself IS allowed as the shared component model (the contracts traffic in ReactNode/ComponentType and a React context). Concrete host adapters (router/auth/toast wiring) live in separate packages that consume this kernel. The SystemBus is pure orchestration: it never authorizes, the backend capability-router enforces all policy.",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@tanstack/react-query": "^5.0.0",
|
|
65
65
|
"react": "^18.3.1",
|
|
66
|
-
"@xemahq/kernel-contracts": ">=0.
|
|
66
|
+
"@xemahq/kernel-contracts": ">=0.55.0"
|
|
67
67
|
},
|
|
68
68
|
"peerDependenciesMeta": {
|
|
69
69
|
"@tanstack/react-query": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"prettier": "3.6.2",
|
|
78
78
|
"react": "^18.3.1",
|
|
79
79
|
"typescript": "5.9.3",
|
|
80
|
-
"@xemahq/kernel-contracts": "^0.
|
|
80
|
+
"@xemahq/kernel-contracts": "^0.58.0"
|
|
81
81
|
},
|
|
82
82
|
"exports": {
|
|
83
83
|
".": {
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,11 @@ export * from './lib/system-bus';
|
|
|
2
2
|
export * from './lib/biome-host';
|
|
3
3
|
export * from './lib/capabilities';
|
|
4
4
|
|
|
5
|
+
// The shared closed-first-party + open-biome contribution registry core (and
|
|
6
|
+
// its versioned/validated renderable flavour). Every plane that lets a biome
|
|
7
|
+
// contribute into a set the platform also ships builtins for is built on this.
|
|
8
|
+
export * from './lib/contribution-registry';
|
|
9
|
+
|
|
5
10
|
// Guided-capture UX kit (B5). Also available at the `@xemahq/ui-kernel/ui`
|
|
6
11
|
// subpath alongside the rest of the shared UI surface; re-exported here so the
|
|
7
12
|
// capture primitives are reachable from the package root too.
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Registry } from '@xemahq/registry';
|
|
2
2
|
import { useSyncExternalStore } from 'react';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
DuplicateContributionPolicy,
|
|
6
|
+
createContributionRegistry,
|
|
7
|
+
} from '../contribution-registry';
|
|
8
|
+
|
|
4
9
|
import type {
|
|
5
10
|
FrontendBiome,
|
|
6
11
|
FrontendBiomeFactory,
|
|
@@ -153,23 +158,38 @@ class FrontendBiomeRegistry extends Registry<string, FrontendBiome & { key: stri
|
|
|
153
158
|
* light up the moment the biome's frontend loads. Duplicate kinds
|
|
154
159
|
* across biomes are surfaced as a console error and the first
|
|
155
160
|
* registration wins (deterministic + fail-loud, not silent).
|
|
161
|
+
*
|
|
162
|
+
* Built on the shared `createContributionRegistry` core — the same
|
|
163
|
+
* closed+open two-layer bookkeeping the ChatWidget, reply-variant and
|
|
164
|
+
* Vista-surface registries use. This overlay is rebuilt on every call
|
|
165
|
+
* (it is read during render), so it takes the `FirstWins` duplicate
|
|
166
|
+
* policy rather than `Throw`: a duplicate must not take down a render.
|
|
167
|
+
* `FirstWins` still REQUIRES the `onDuplicate` reporter below — the
|
|
168
|
+
* rejected contribution is never dropped silently.
|
|
156
169
|
*/
|
|
157
170
|
outputRenderers(): ReadonlyMap<string, OutputRendererContribution> {
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
if (!this.enabled) return new Map<string, OutputRendererContribution>();
|
|
172
|
+
// `onDuplicate` fires SYNCHRONOUSLY from inside `register()`, so this
|
|
173
|
+
// cursor always names the biome whose contribution was rejected — the
|
|
174
|
+
// contribution object itself does not carry its owner.
|
|
175
|
+
let contributingBiomeId = '';
|
|
176
|
+
const overlay = createContributionRegistry<OutputRendererContribution>({
|
|
177
|
+
name: 'biome outputRenderer',
|
|
178
|
+
duplicatePolicy: DuplicateContributionPolicy.FirstWins,
|
|
179
|
+
onDuplicate: (outputKind) => {
|
|
180
|
+
console.error(
|
|
181
|
+
`[xema-ui-kernel] duplicate outputRenderer for kind '${outputKind}' ` +
|
|
182
|
+
`from biome '${contributingBiomeId}'; first registration wins.`,
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
});
|
|
160
186
|
for (const biome of this.list()) {
|
|
187
|
+
contributingBiomeId = biome.id;
|
|
161
188
|
for (const entry of biome.outputRenderers ?? []) {
|
|
162
|
-
|
|
163
|
-
console.error(
|
|
164
|
-
`[xema-ui-kernel] duplicate outputRenderer for kind '${entry.outputKind}' ` +
|
|
165
|
-
`from biome '${biome.id}'; first registration wins.`,
|
|
166
|
-
);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
out.set(entry.outputKind, entry);
|
|
189
|
+
overlay.register(entry.outputKind, entry);
|
|
170
190
|
}
|
|
171
191
|
}
|
|
172
|
-
return
|
|
192
|
+
return overlay.entries();
|
|
173
193
|
}
|
|
174
194
|
|
|
175
195
|
private notify(): void {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guards the shared closed+open contribution registry core — the ONE
|
|
3
|
+
* implementation behind the ChatWidget, biome output-renderer, reply-variant
|
|
4
|
+
* and Vista-surface planes. Every property the hand-written copies had must
|
|
5
|
+
* survive the extraction.
|
|
6
|
+
*
|
|
7
|
+
* Runs under the host-web vitest mirror (this package carries no vitest dep).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ContributionFallbackReason,
|
|
13
|
+
DuplicateContributionPolicy,
|
|
14
|
+
createContributionRegistry,
|
|
15
|
+
createRenderableContributionRegistry,
|
|
16
|
+
} from './index';
|
|
17
|
+
|
|
18
|
+
function makeRegistry(): ReturnType<typeof createContributionRegistry<string>> {
|
|
19
|
+
return createContributionRegistry<string>({
|
|
20
|
+
name: 'test',
|
|
21
|
+
reservedKinds: ['plan', 'diff'],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('createContributionRegistry', () => {
|
|
26
|
+
it('resolves first-party before open, and lists both', () => {
|
|
27
|
+
const registry = makeRegistry();
|
|
28
|
+
registry.registerFirstParty('plan', 'builtin-plan');
|
|
29
|
+
registry.register('acme:gantt', 'acme-gantt');
|
|
30
|
+
expect(registry.get('plan')).toBe('builtin-plan');
|
|
31
|
+
expect(registry.get('acme:gantt')).toBe('acme-gantt');
|
|
32
|
+
expect(registry.has('nope')).toBe(false);
|
|
33
|
+
expect(registry.list()).toEqual(['acme:gantt', 'plan']);
|
|
34
|
+
expect(registry.listOpen()).toEqual(['acme:gantt']);
|
|
35
|
+
expect([...registry.entries().keys()].sort()).toEqual(['acme:gantt', 'plan']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('refuses to let the open set claim a reserved first-party kind', () => {
|
|
39
|
+
const registry = makeRegistry();
|
|
40
|
+
expect(registry.isReserved('diff')).toBe(true);
|
|
41
|
+
expect(() => registry.register('diff', 'hijack')).toThrow(/reserved/);
|
|
42
|
+
// The namespaced form is fine.
|
|
43
|
+
expect(() => registry.register('acme:diff', 'ok')).not.toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is idempotent for the identical entry and throws for a different one', () => {
|
|
47
|
+
const registry = makeRegistry();
|
|
48
|
+
registry.register('acme:gantt', 'entry');
|
|
49
|
+
expect(() => registry.register('acme:gantt', 'entry')).not.toThrow();
|
|
50
|
+
expect(() => registry.register('acme:gantt', 'other')).toThrow(
|
|
51
|
+
/already registered/,
|
|
52
|
+
);
|
|
53
|
+
registry.registerFirstParty('plan', 'builtin');
|
|
54
|
+
expect(() => registry.registerFirstParty('plan', 'builtin')).not.toThrow();
|
|
55
|
+
expect(() => registry.registerFirstParty('plan', 'other')).toThrow(
|
|
56
|
+
/already registered/,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('resets only the OPEN set', () => {
|
|
61
|
+
const registry = makeRegistry();
|
|
62
|
+
registry.registerFirstParty('plan', 'builtin');
|
|
63
|
+
registry.register('acme:gantt', 'entry');
|
|
64
|
+
registry.__resetOpenForTests();
|
|
65
|
+
expect(registry.list()).toEqual(['plan']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('reports — never swallows — a duplicate under FirstWins', () => {
|
|
69
|
+
const onDuplicate = vi.fn();
|
|
70
|
+
const registry = createContributionRegistry<string>({
|
|
71
|
+
name: 'overlay',
|
|
72
|
+
duplicatePolicy: DuplicateContributionPolicy.FirstWins,
|
|
73
|
+
onDuplicate,
|
|
74
|
+
});
|
|
75
|
+
registry.register('k', 'first');
|
|
76
|
+
registry.register('k', 'second');
|
|
77
|
+
expect(registry.get('k')).toBe('first');
|
|
78
|
+
expect(onDuplicate).toHaveBeenCalledWith('k', 'first', 'second');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('refuses FirstWins without a reporter', () => {
|
|
82
|
+
expect(() =>
|
|
83
|
+
createContributionRegistry<string>({
|
|
84
|
+
name: 'overlay',
|
|
85
|
+
duplicatePolicy: DuplicateContributionPolicy.FirstWins,
|
|
86
|
+
}),
|
|
87
|
+
).toThrow(/onDuplicate/);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('createRenderableContributionRegistry', () => {
|
|
92
|
+
const registry = createRenderableContributionRegistry<string>({
|
|
93
|
+
name: 'renderable',
|
|
94
|
+
reservedKinds: ['plan'],
|
|
95
|
+
});
|
|
96
|
+
registry.registerFirstParty<{ label: string }>('plan', {
|
|
97
|
+
version: 2,
|
|
98
|
+
validate: (payload) =>
|
|
99
|
+
typeof payload === 'object' &&
|
|
100
|
+
payload !== null &&
|
|
101
|
+
typeof (payload as { label?: unknown }).label === 'string'
|
|
102
|
+
? (payload as { label: string })
|
|
103
|
+
: null,
|
|
104
|
+
render: (payload) => `plan:${payload.label}`,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders a matching envelope', () => {
|
|
108
|
+
const resolution = registry.resolve({
|
|
109
|
+
kind: 'plan',
|
|
110
|
+
version: 1,
|
|
111
|
+
payload: { label: 'ship it' },
|
|
112
|
+
});
|
|
113
|
+
expect(resolution).toEqual({
|
|
114
|
+
matched: true,
|
|
115
|
+
kind: 'plan',
|
|
116
|
+
version: 2,
|
|
117
|
+
rendered: 'plan:ship it',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('never throws — it returns a typed reason instead', () => {
|
|
122
|
+
expect(
|
|
123
|
+
registry.resolve({ kind: 'nope', version: 1, payload: {} }),
|
|
124
|
+
).toMatchObject({
|
|
125
|
+
matched: false,
|
|
126
|
+
reason: ContributionFallbackReason.UnknownKind,
|
|
127
|
+
});
|
|
128
|
+
expect(
|
|
129
|
+
registry.resolve({ kind: 'plan', version: 99, payload: {} }),
|
|
130
|
+
).toMatchObject({
|
|
131
|
+
matched: false,
|
|
132
|
+
reason: ContributionFallbackReason.UnsupportedVersion,
|
|
133
|
+
});
|
|
134
|
+
expect(
|
|
135
|
+
registry.resolve({ kind: 'plan', version: 1, payload: { label: 7 } }),
|
|
136
|
+
).toMatchObject({
|
|
137
|
+
matched: false,
|
|
138
|
+
reason: ContributionFallbackReason.InvalidPayload,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createContributionRegistry` — the ONE two-layer closed+open registry core
|
|
3
|
+
* the Xema frontend uses everywhere a biome may contribute into a set the
|
|
4
|
+
* platform also ships a closed first-party version of.
|
|
5
|
+
*
|
|
6
|
+
* Before this module the exact same bookkeeping was hand-written four times
|
|
7
|
+
* (in-chat widgets, biome output renderers, open reply variants, and — next —
|
|
8
|
+
* Vista surfaces), each with slightly different guards. This is that core,
|
|
9
|
+
* extracted once (reuse register U1).
|
|
10
|
+
*
|
|
11
|
+
* ── The two layers ──────────────────────────────────────────────────────────
|
|
12
|
+
* • a CLOSED first-party set, registered at module load and keyed by a
|
|
13
|
+
* platform enum value;
|
|
14
|
+
* • an OPEN biome set, registered at runtime (a biome registers from its
|
|
15
|
+
* `FrontendBiome.init()` hook) and keyed by a namespaced `<biomeId>:<kind>`
|
|
16
|
+
* slug.
|
|
17
|
+
*
|
|
18
|
+
* ── The invariants (all fail-loud, none silent) ─────────────────────────────
|
|
19
|
+
* • RESERVED KINDS. Every first-party kind is reserved: `register()` throws
|
|
20
|
+
* if a biome tries to claim one. The open set can never shadow the closed
|
|
21
|
+
* one.
|
|
22
|
+
* • DUPLICATE GUARD. Registering the EXACT same entry object twice is a
|
|
23
|
+
* no-op (this is what tolerates React Fast Refresh / a double-invoked
|
|
24
|
+
* `init()`); registering a DIFFERENT entry for a kind that is already
|
|
25
|
+
* taken is an error — two biomes cannot silently shadow each other.
|
|
26
|
+
* • The one exception is {@link DuplicateContributionPolicy.FirstWins}, for
|
|
27
|
+
* registries rebuilt on a hot path where a throw would crash a render
|
|
28
|
+
* (the biome output-renderer overlay). It is NOT a silent fallback: an
|
|
29
|
+
* `onDuplicate` reporter is REQUIRED, and the registry factory throws at
|
|
30
|
+
* construction if it is missing.
|
|
31
|
+
*
|
|
32
|
+
* Resolution itself lives in the caller (or in `renderable-registry.ts` for the
|
|
33
|
+
* versioned + payload-validated flavour) and NEVER throws.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** What to do when a DIFFERENT entry is registered for an already-taken kind. */
|
|
37
|
+
export enum DuplicateContributionPolicy {
|
|
38
|
+
/**
|
|
39
|
+
* Throw. The default, and correct for every registry whose registrations
|
|
40
|
+
* happen once at module load or once per biome `init()`.
|
|
41
|
+
*/
|
|
42
|
+
Throw = 'throw',
|
|
43
|
+
/**
|
|
44
|
+
* Keep the first registration and report the loser through `onDuplicate`.
|
|
45
|
+
* For registries REBUILT on a hot path (e.g. per render) where throwing
|
|
46
|
+
* would take down the UI. Requires `onDuplicate` — there is no silent mode.
|
|
47
|
+
*/
|
|
48
|
+
FirstWins = 'first-wins',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Options for {@link createContributionRegistry}. */
|
|
52
|
+
export interface ContributionRegistryOptions<TEntry> {
|
|
53
|
+
/** Registry name, used verbatim in every thrown message. */
|
|
54
|
+
readonly name: string;
|
|
55
|
+
/**
|
|
56
|
+
* The CLOSED first-party kinds. Reserved: `register()` refuses them so a
|
|
57
|
+
* biome can never shadow a platform kind. Typically `Object.values(SomeEnum)`.
|
|
58
|
+
*/
|
|
59
|
+
readonly reservedKinds?: Iterable<string>;
|
|
60
|
+
/** Duplicate handling. Defaults to {@link DuplicateContributionPolicy.Throw}. */
|
|
61
|
+
readonly duplicatePolicy?: DuplicateContributionPolicy;
|
|
62
|
+
/**
|
|
63
|
+
* Reporter for a rejected duplicate under
|
|
64
|
+
* {@link DuplicateContributionPolicy.FirstWins}. REQUIRED under that policy.
|
|
65
|
+
*/
|
|
66
|
+
readonly onDuplicate?: (
|
|
67
|
+
kind: string,
|
|
68
|
+
existing: TEntry,
|
|
69
|
+
rejected: TEntry,
|
|
70
|
+
) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The two-layer registry surface. */
|
|
74
|
+
export interface ContributionRegistry<TEntry> {
|
|
75
|
+
/** Registry name (diagnostics). */
|
|
76
|
+
readonly name: string;
|
|
77
|
+
/**
|
|
78
|
+
* Register a FIRST-PARTY entry for a closed platform kind. Called once per
|
|
79
|
+
* kind at module load.
|
|
80
|
+
*/
|
|
81
|
+
registerFirstParty(kind: string, entry: TEntry): void;
|
|
82
|
+
/**
|
|
83
|
+
* OPEN seam: register a biome entry under its namespaced `<biomeId>:<kind>`
|
|
84
|
+
* slug. Throws when the slug is a reserved first-party kind.
|
|
85
|
+
*/
|
|
86
|
+
register(kind: string, entry: TEntry): void;
|
|
87
|
+
/** True when SOME entry (first-party or biome) is registered for the kind. */
|
|
88
|
+
has(kind: string): boolean;
|
|
89
|
+
/** The entry for a kind — first-party wins — or `undefined`. */
|
|
90
|
+
get(kind: string): TEntry | undefined;
|
|
91
|
+
/** True when the kind belongs to the closed first-party set. */
|
|
92
|
+
isReserved(kind: string): boolean;
|
|
93
|
+
/** Every registered kind (sorted) — diagnostics + tests. */
|
|
94
|
+
list(): readonly string[];
|
|
95
|
+
/** Only the OPEN (biome-contributed) kinds (sorted). */
|
|
96
|
+
listOpen(): readonly string[];
|
|
97
|
+
/** Flattened `kind -> entry` view, first-party wins. Diagnostics + overlays. */
|
|
98
|
+
entries(): ReadonlyMap<string, TEntry>;
|
|
99
|
+
/** Test-only: clear the OPEN set (the first-party set stays). */
|
|
100
|
+
__resetOpenForTests(): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createContributionRegistry<TEntry>(
|
|
104
|
+
options: ContributionRegistryOptions<TEntry>,
|
|
105
|
+
): ContributionRegistry<TEntry> {
|
|
106
|
+
const {
|
|
107
|
+
name,
|
|
108
|
+
reservedKinds,
|
|
109
|
+
duplicatePolicy = DuplicateContributionPolicy.Throw,
|
|
110
|
+
onDuplicate,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
duplicatePolicy === DuplicateContributionPolicy.FirstWins &&
|
|
115
|
+
onDuplicate === undefined
|
|
116
|
+
) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`${name} registry: DuplicateContributionPolicy.FirstWins requires an ` +
|
|
119
|
+
`'onDuplicate' reporter — a dropped contribution must never be silent.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const reserved = new Set<string>(reservedKinds ?? []);
|
|
124
|
+
const firstParty = new Map<string, TEntry>();
|
|
125
|
+
const open = new Map<string, TEntry>();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Idempotent for the IDENTICAL entry (Fast Refresh / double-invoked
|
|
129
|
+
* `init()`); an error for a DIFFERENT one. Returns whether the entry landed.
|
|
130
|
+
*/
|
|
131
|
+
function put(target: Map<string, TEntry>, kind: string, entry: TEntry): void {
|
|
132
|
+
const existing = target.get(kind);
|
|
133
|
+
if (existing === undefined) {
|
|
134
|
+
target.set(kind, entry);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (existing === entry) return;
|
|
138
|
+
if (duplicatePolicy === DuplicateContributionPolicy.FirstWins) {
|
|
139
|
+
// `onDuplicate` is guaranteed present under this policy (checked above).
|
|
140
|
+
(onDuplicate as (k: string, e: TEntry, r: TEntry) => void)(
|
|
141
|
+
kind,
|
|
142
|
+
existing,
|
|
143
|
+
entry,
|
|
144
|
+
);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(
|
|
148
|
+
`${name} registry: a DIFFERENT entry is already registered for kind ` +
|
|
149
|
+
`'${kind}'. Two contributors cannot claim the same slug.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name,
|
|
155
|
+
|
|
156
|
+
registerFirstParty(kind: string, entry: TEntry): void {
|
|
157
|
+
put(firstParty, kind, entry);
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
register(kind: string, entry: TEntry): void {
|
|
161
|
+
if (reserved.has(kind)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${name} registry: '${kind}' is a reserved first-party kind and ` +
|
|
164
|
+
`cannot be overridden by a biome. Use a namespaced ` +
|
|
165
|
+
`'<biomeId>:${kind}' slug.`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
put(open, kind, entry);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
has(kind: string): boolean {
|
|
172
|
+
return firstParty.has(kind) || open.has(kind);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
get(kind: string): TEntry | undefined {
|
|
176
|
+
return firstParty.get(kind) ?? open.get(kind);
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
isReserved(kind: string): boolean {
|
|
180
|
+
return reserved.has(kind);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
list(): readonly string[] {
|
|
184
|
+
return [...firstParty.keys(), ...open.keys()].sort();
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
listOpen(): readonly string[] {
|
|
188
|
+
return [...open.keys()].sort();
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
entries(): ReadonlyMap<string, TEntry> {
|
|
192
|
+
// First-party last so it wins on a key present in both (which `register`
|
|
193
|
+
// already forbids for reserved kinds, but `entries()` must not depend on
|
|
194
|
+
// that to be correct).
|
|
195
|
+
return new Map<string, TEntry>([...open, ...firstParty]);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
__resetOpenForTests(): void {
|
|
199
|
+
open.clear();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared two-layer (closed first-party + open biome) contribution registry
|
|
3
|
+
* core, and its versioned/validated renderable flavour.
|
|
4
|
+
*
|
|
5
|
+
* One implementation, four consumers: in-chat ChatWidgets, biome output
|
|
6
|
+
* renderers, open reply variants, and Vista surfaces.
|
|
7
|
+
*/
|
|
8
|
+
export * from './contribution-registry';
|
|
9
|
+
export * from './renderable-registry';
|