@xemahq/ui-kernel 0.2.0 → 0.4.0
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/lib/biome-host/composition-validation.d.ts +22 -0
- package/dist/lib/biome-host/composition-validation.d.ts.map +1 -0
- package/dist/lib/biome-host/composition-validation.js +127 -0
- package/dist/lib/biome-host/composition-validation.js.map +1 -0
- package/dist/lib/biome-host/define-web-biome.d.ts +30 -0
- package/dist/lib/biome-host/define-web-biome.d.ts.map +1 -0
- package/dist/lib/biome-host/define-web-biome.js +42 -0
- package/dist/lib/biome-host/define-web-biome.js.map +1 -0
- package/dist/lib/biome-host/errors.d.ts +2 -0
- package/dist/lib/biome-host/errors.d.ts.map +1 -0
- package/dist/lib/biome-host/errors.js +146 -0
- package/dist/lib/biome-host/errors.js.map +1 -0
- package/dist/lib/biome-host/frontend-biome.d.ts +1 -0
- package/dist/lib/biome-host/frontend-biome.d.ts.map +1 -1
- package/dist/lib/biome-host/host-bridge.d.ts +6 -0
- package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
- package/dist/lib/biome-host/host-bridge.js.map +1 -1
- package/dist/lib/biome-host/index.d.ts +5 -0
- package/dist/lib/biome-host/index.d.ts.map +1 -1
- package/dist/lib/biome-host/index.js +5 -0
- package/dist/lib/biome-host/index.js.map +1 -1
- package/dist/lib/biome-host/realtime-hooks.d.ts +5 -0
- package/dist/lib/biome-host/realtime-hooks.d.ts.map +1 -0
- package/dist/lib/biome-host/realtime-hooks.js +28 -0
- package/dist/lib/biome-host/realtime-hooks.js.map +1 -0
- package/dist/lib/biome-host/realtime-port.d.ts +30 -0
- package/dist/lib/biome-host/realtime-port.d.ts.map +1 -0
- package/dist/lib/biome-host/realtime-port.js +3 -0
- package/dist/lib/biome-host/realtime-port.js.map +1 -0
- package/dist/lib/biome-host/response-envelope.d.ts +3 -0
- package/dist/lib/biome-host/response-envelope.d.ts.map +1 -0
- package/dist/lib/biome-host/response-envelope.js +25 -0
- package/dist/lib/biome-host/response-envelope.js.map +1 -0
- package/dist/registry/lib/composition-validation-host.d.ts +3 -0
- package/dist/registry/lib/composition-validation-host.d.ts.map +1 -0
- package/dist/registry/lib/composition-validation-host.js +10 -0
- package/dist/registry/lib/composition-validation-host.js.map +1 -0
- package/dist/session/shell/SessionWorkspaceShell.d.ts.map +1 -1
- package/dist/session/shell/SessionWorkspaceShell.js +9 -6
- package/dist/session/shell/SessionWorkspaceShell.js.map +1 -1
- package/dist/ui/chrome/ErrorCard.d.ts.map +1 -1
- package/dist/ui/chrome/ErrorCard.js +2 -9
- package/dist/ui/chrome/ErrorCard.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/biome-host/define-web-biome.ts +161 -0
- package/src/lib/biome-host/errors.ts +220 -0
- package/src/lib/biome-host/frontend-biome.ts +20 -2
- package/src/lib/biome-host/host-bridge.ts +44 -0
- package/src/lib/biome-host/index.ts +5 -0
- package/src/lib/biome-host/realtime-hooks.ts +74 -0
- package/src/lib/biome-host/realtime-port.ts +109 -0
- package/src/lib/biome-host/response-envelope.ts +69 -0
- package/src/session/shell/SessionWorkspaceShell.tsx +19 -13
- package/src/ui/chrome/ErrorCard.tsx +8 -13
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unwrapList = unwrapList;
|
|
4
|
+
exports.unwrapData = unwrapData;
|
|
5
|
+
function unwrapList(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
if (value !== null &&
|
|
10
|
+
typeof value === 'object' &&
|
|
11
|
+
Array.isArray(value.data)) {
|
|
12
|
+
return value.data;
|
|
13
|
+
}
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
function unwrapData(value) {
|
|
17
|
+
if (value !== null &&
|
|
18
|
+
typeof value === 'object' &&
|
|
19
|
+
'data' in value &&
|
|
20
|
+
!('pagination' in value)) {
|
|
21
|
+
return value.data;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=response-envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-envelope.js","sourceRoot":"","sources":["../../../src/lib/biome-host/response-envelope.ts"],"names":[],"mappings":";;AA+BA,gCAYC;AAeD,gCAUC;AArCD,SAAgB,UAAU,CAAI,KAAc;IAC1C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAY,CAAC;IACtB,CAAC;IACD,IACE,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,OAAO,CAAE,KAA4B,CAAC,IAAI,CAAC,EACjD,CAAC;QACD,OAAQ,KAAuB,CAAC,IAAI,CAAC;IACvC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAeD,SAAgB,UAAU,CAAI,KAAc;IAC1C,IACE,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,IAAK,KAAiC;QAC5C,CAAC,CAAC,YAAY,IAAK,KAAiC,CAAC,EACrD,CAAC;QACD,OAAQ,KAAqB,CAAC,IAAI,CAAC;IACrC,CAAC;IACD,OAAO,KAAU,CAAC;AACpB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-validation-host.d.ts","sourceRoot":"","sources":["../../../src/registry/lib/composition-validation-host.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAEnB,MAAM,aAAa,CAAC;AAkBrB,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,SAAS,qBAAqB,EAAE,CAElC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateHostBiomeComposition = validateHostBiomeComposition;
|
|
4
|
+
const index_1 = require("../../index");
|
|
5
|
+
const extension_points_1 = require("./extension-points");
|
|
6
|
+
const HOST_SLOT_IDS = new Set(Object.values(extension_points_1.HostExtensionSlots));
|
|
7
|
+
function validateHostBiomeComposition(biomes) {
|
|
8
|
+
return (0, index_1.validateBiomeComposition)(biomes, { knownSlots: HOST_SLOT_IDS });
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=composition-validation-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composition-validation-host.js","sourceRoot":"","sources":["../../../src/registry/lib/composition-validation-host.ts"],"names":[],"mappings":";;AAgCA,oEAIC;AA1BD,uCAIqB;AAErB,yDAAwD;AAOxD,MAAM,aAAa,GAAwB,IAAI,GAAG,CAChD,MAAM,CAAC,MAAM,CAAC,qCAAkB,CAAC,CAClC,CAAC;AAOF,SAAgB,4BAA4B,CAC1C,MAAgC;IAEhC,OAAO,IAAA,gCAAwB,EAAC,MAAM,EAAE,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC;AACzE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionWorkspaceShell.d.ts","sourceRoot":"","sources":["../../../src/session/shell/SessionWorkspaceShell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"SessionWorkspaceShell.d.ts","sourceRoot":"","sources":["../../../src/session/shell/SessionWorkspaceShell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI7E,UAAU,0BAA0B;IAKlC,QAAQ,CAAC,iBAAiB,EAAE,SAAS,CAAC,cAAc,CAAC,CAAC;IAEtD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAEjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAE7B,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAG/D,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAQ7B,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC;IAMhC,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,CAAC;IAMrC,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC;IAMpC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAGrC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAiBD,wBAAgB,qBAAqB,CAAC,EACpC,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,WAAW,EACX,QAAQ,EACR,WAAW,EACX,eAAe,EACf,cAAc,EACd,iBAAyB,EACzB,gBAAsB,GACvB,EAAE,0BAA0B,+BAsE5B"}
|
|
@@ -5,11 +5,14 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
5
5
|
const cn_1 = require("../lib/cn");
|
|
6
6
|
function SessionWorkspaceShell({ splitContainerRef, chatPanePercent, isResizing, startResize, chatSlot, previewSlot, rightHeaderSlot, chatFooterSlot, previewFullscreen = false, minLeftPaneWidth = 320, }) {
|
|
7
7
|
const hasPreviewSlot = previewSlot !== null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const leftPaneStyle = hasPreviewSlot
|
|
9
|
+
? {
|
|
10
|
+
'--sws-min-left': `${minLeftPaneWidth}px`,
|
|
11
|
+
'--sws-chat-pct': `${chatPanePercent}%`,
|
|
12
|
+
}
|
|
13
|
+
: undefined;
|
|
14
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: splitContainerRef, className: (0, cn_1.cn)('relative flex h-full w-full flex-col overflow-hidden lg:flex-row', isResizing && 'cursor-col-resize'), children: [!previewFullscreen && ((0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)('flex w-full min-h-0 flex-col overflow-hidden bg-paper', hasPreviewSlot
|
|
15
|
+
? 'flex-1 lg:h-full lg:w-[var(--sws-chat-pct)] lg:min-w-[var(--sws-min-left)] lg:flex-none'
|
|
16
|
+
: 'h-full'), style: leftPaneStyle, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", children: [(0, jsx_runtime_1.jsx)("div", { className: "relative min-h-0 min-w-0 flex-1", children: chatSlot }), chatFooterSlot] }) })), !previewFullscreen && hasPreviewSlot && ((0, jsx_runtime_1.jsx)("div", { className: "relative hidden w-px shrink-0 bg-rule/30 lg:block", children: (0, jsx_runtime_1.jsx)("button", { type: "button", onMouseDown: startResize, "aria-label": "Resize chat and preview panels", title: "Drag to resize", className: "group absolute inset-y-0 left-1/2 w-2 -translate-x-1/2 cursor-col-resize", children: (0, jsx_runtime_1.jsx)("span", { "aria-hidden": true, className: "absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-transparent transition-colors group-hover:bg-primary/40" }) }) })), hasPreviewSlot && ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden bg-paper", children: [rightHeaderSlot, (0, jsx_runtime_1.jsx)("div", { className: "min-h-0 flex-1 overflow-hidden", children: previewSlot })] }))] }));
|
|
14
17
|
}
|
|
15
18
|
//# sourceMappingURL=SessionWorkspaceShell.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionWorkspaceShell.js","sourceRoot":"","sources":["../../../src/session/shell/SessionWorkspaceShell.tsx"],"names":[],"mappings":";;AAiEA,
|
|
1
|
+
{"version":3,"file":"SessionWorkspaceShell.js","sourceRoot":"","sources":["../../../src/session/shell/SessionWorkspaceShell.tsx"],"names":[],"mappings":";;AAiEA,sDAiFC;;AAhJD,kCAA+B;AA+D/B,SAAgB,qBAAqB,CAAC,EACpC,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,WAAW,EACX,QAAQ,EACR,WAAW,EACX,eAAe,EACf,cAAc,EACd,iBAAiB,GAAG,KAAK,EACzB,gBAAgB,GAAG,GAAG,GACK;IAI3B,MAAM,cAAc,GAAG,WAAW,KAAK,IAAI,CAAC;IAM5C,MAAM,aAAa,GAA8B,cAAc;QAC7D,CAAC,CAAE;YACC,gBAAgB,EAAE,GAAG,gBAAgB,IAAI;YACzC,gBAAgB,EAAE,GAAG,eAAe,GAAG;SACtB;QACrB,CAAC,CAAC,SAAS,CAAC;IACd,OAAO,CACL,iCACE,GAAG,EAAE,iBAAiB,EACtB,SAAS,EAAE,IAAA,OAAE,EACX,kEAAkE,EAClE,UAAU,IAAI,mBAAmB,CAClC,aAEA,CAAC,iBAAiB,IAAI,CACrB,gCACE,SAAS,EAAE,IAAA,OAAE,EACX,uDAAuD,EACvD,cAAc;oBACZ,CAAC,CAAC,yFAAyF;oBAC3F,CAAC,CAAC,QAAQ,CACb,EACD,KAAK,EAAE,aAAa,YAEpB,iCAAK,SAAS,EAAC,sDAAsD,aACnE,gCAAK,SAAS,EAAC,iCAAiC,YAAE,QAAQ,GAAO,EAChE,cAAc,IACX,GACF,CACP,EAEA,CAAC,iBAAiB,IAAI,cAAc,IAAI,CACvC,gCAAK,SAAS,EAAC,mDAAmD,YAKhE,mCACE,IAAI,EAAC,QAAQ,EACb,WAAW,EAAE,WAAW,gBACb,gCAAgC,EAC3C,KAAK,EAAC,gBAAgB,EACtB,SAAS,EAAC,0EAA0E,YAEpF,sDAEE,SAAS,EAAC,8GAA8G,GACxH,GACK,GACL,CACP,EAEA,cAAc,IAAI,CACjB,iCAAK,SAAS,EAAC,+DAA+D,aAC3E,eAAe,EAChB,gCAAK,SAAS,EAAC,gCAAgC,YAAE,WAAW,GAAO,IAC/D,CACP,IACG,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorCard.d.ts","sourceRoot":"","sources":["../../../src/ui/chrome/ErrorCard.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ErrorCard.d.ts","sourceRoot":"","sources":["../../../src/ui/chrome/ErrorCard.tsx"],"names":[],"mappings":"AAaA,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;AAKjF,UAAU,cAAc;IACtB,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAMrB,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAChC,KAAK,EACL,KAA8B,EAC9B,OAAO,EACP,WAAgC,EAChC,eAA0D,GAC3D,EAAE,cAAc,+BAyBhB"}
|
|
@@ -3,17 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.default = ErrorCard;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const lucide_react_1 = require("lucide-react");
|
|
6
|
+
const errors_1 = require("../../lib/biome-host/errors");
|
|
6
7
|
const button_1 = require("../primitives/button");
|
|
7
8
|
const card_1 = require("../primitives/card");
|
|
8
|
-
const defaultFormatError = (error, fallback) =>
|
|
9
|
-
if (typeof error === 'string') {
|
|
10
|
-
return error || fallback;
|
|
11
|
-
}
|
|
12
|
-
if (error instanceof Error && error.message) {
|
|
13
|
-
return error.message;
|
|
14
|
-
}
|
|
15
|
-
return fallback;
|
|
16
|
-
};
|
|
9
|
+
const defaultFormatError = (error, fallback) => (0, errors_1.getUserFacingErrorMessage)(error, fallback);
|
|
17
10
|
function ErrorCard({ error, title = 'Something went wrong', onRetry, formatError = defaultFormatError, fallbackMessage = 'Unable to load this content right now.', }) {
|
|
18
11
|
const message = formatError(error, fallbackMessage);
|
|
19
12
|
return ((0, jsx_runtime_1.jsx)(card_1.Card, { className: "border-destructive/25 bg-destructive/[0.03] rounded-xl", children: (0, jsx_runtime_1.jsxs)(card_1.CardContent, { className: "flex flex-col items-center py-14 text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-14 w-14 rounded-2xl bg-destructive/10 flex items-center justify-center mb-4", children: (0, jsx_runtime_1.jsx)(lucide_react_1.AlertCircle, { className: "h-6 w-6 text-destructive" }) }), (0, jsx_runtime_1.jsx)("p", { className: "text-subtitle font-semibold text-ink", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "text-body-1 text-ink-3 mt-2 max-w-md leading-relaxed", children: message }), onRetry && ((0, jsx_runtime_1.jsxs)(button_1.Button, { size: "sm", variant: "outline", onClick: onRetry, className: "mt-5 gap-2 h-10 border-destructive/20 text-destructive hover:bg-destructive/5", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.RefreshCw, { className: "h-4 w-4" }), "Retry"] }))] }) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ErrorCard.js","sourceRoot":"","sources":["../../../src/ui/chrome/ErrorCard.tsx"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"ErrorCard.js","sourceRoot":"","sources":["../../../src/ui/chrome/ErrorCard.tsx"],"names":[],"mappings":";;AA+BA,4BA+BC;;AA9DD,+CAAsD;AAEtD,wDAAwE;AACxE,iDAA8C;AAC9C,6CAAuD;AAWvD,MAAM,kBAAkB,GAA0B,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CACpE,IAAA,kCAAyB,EAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;AAe7C,SAAwB,SAAS,CAAC,EAChC,KAAK,EACL,KAAK,GAAG,sBAAsB,EAC9B,OAAO,EACP,WAAW,GAAG,kBAAkB,EAChC,eAAe,GAAG,wCAAwC,GAC3C;IACf,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;IAEpD,OAAO,CACL,uBAAC,WAAI,IAAC,SAAS,EAAC,wDAAwD,YACtE,wBAAC,kBAAW,IAAC,SAAS,EAAC,8CAA8C,aACnE,gCAAK,SAAS,EAAC,+EAA+E,YAC5F,uBAAC,0BAAW,IAAC,SAAS,EAAC,0BAA0B,GAAG,GAChD,EACN,8BAAG,SAAS,EAAC,sCAAsC,YAAE,KAAK,GAAK,EAC/D,8BAAG,SAAS,EAAC,sDAAsD,YAAE,OAAO,GAAK,EAChF,OAAO,IAAI,CACV,wBAAC,eAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,SAAS,EACjB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAC,+EAA+E,aAEzF,uBAAC,wBAAS,IAAC,SAAS,EAAC,SAAS,GAAG,aAE1B,CACV,IACW,GACT,CACR,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/ui-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xema",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineWebBiome` — the authoring helper for `target: 'web'` biomes.
|
|
3
|
+
*
|
|
4
|
+
* Every web biome used to hand-repeat the same ~30 lines: one
|
|
5
|
+
* `lazy(() => import('./pages/X'))` per page, a `navItems` array, and a
|
|
6
|
+
* parallel `routes` array — writing the slug TWICE (once as `navItem.route`,
|
|
7
|
+
* once as `route.path`), the classic source of "nav points at a path no route
|
|
8
|
+
* serves" bugs.
|
|
9
|
+
*
|
|
10
|
+
* `defineWebBiome` collapses that to one declarative page list. Each
|
|
11
|
+
* {@link WebBiomePage} single-sources its `slug` for BOTH the nav route and the
|
|
12
|
+
* route path, so they can never drift. The helper emits one nav item (unless
|
|
13
|
+
* `navHidden`) and one route per page, wrapping the lazily-loaded page in a
|
|
14
|
+
* `<Suspense fallback={null}>` boundary via {@link lazyRoute}.
|
|
15
|
+
*
|
|
16
|
+
* Authors declare WHAT each surface is (`category`) and let the HOST own WHERE
|
|
17
|
+
* it renders in the global menu — preferring `category` over the legacy
|
|
18
|
+
* `section`/`weight`. `init`/`dispose`/`panels`/`session`/`outputRenderers`
|
|
19
|
+
* pass through to the `FrontendBiome` untouched, so bespoke biomes (one-shot
|
|
20
|
+
* registrations, slot fillers, session contributions) still use this helper.
|
|
21
|
+
*
|
|
22
|
+
* Host-framework-agnostic: pure React (`lazy`/`Suspense`/`createElement` via
|
|
23
|
+
* `lazyRoute`), no router and no Next.js.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* export default defineWebBiome({
|
|
27
|
+
* id: 'spaces-web',
|
|
28
|
+
* displayName: 'Spaces',
|
|
29
|
+
* pages: [
|
|
30
|
+
* {
|
|
31
|
+
* slug: 'system/spaces',
|
|
32
|
+
* label: 'Spaces',
|
|
33
|
+
* icon: Layers,
|
|
34
|
+
* category: 'knowledge',
|
|
35
|
+
* load: () => import('./pages/SpacesPage'),
|
|
36
|
+
* },
|
|
37
|
+
* ],
|
|
38
|
+
* });
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { lazyRoute } from './biome-builders';
|
|
42
|
+
|
|
43
|
+
import type {
|
|
44
|
+
FrontendBiome,
|
|
45
|
+
FrontendBiomeFactory,
|
|
46
|
+
NavItemContribution,
|
|
47
|
+
RouteAccess,
|
|
48
|
+
RouteContribution,
|
|
49
|
+
} from './frontend-biome';
|
|
50
|
+
import type { ComponentType } from 'react';
|
|
51
|
+
|
|
52
|
+
export interface WebBiomePage {
|
|
53
|
+
/**
|
|
54
|
+
* Single source of truth for BOTH the nav route AND the route path — the
|
|
55
|
+
* helper sets `navItem.route === route.path === slug`, so they can never
|
|
56
|
+
* diverge. May include `:param` segments (e.g. `system/concepts/:slug`).
|
|
57
|
+
*/
|
|
58
|
+
readonly slug: string;
|
|
59
|
+
/**
|
|
60
|
+
* Stable nav-item id used for active-state matching + analytics. Defaults to
|
|
61
|
+
* {@link slug} when omitted — pass it explicitly only to preserve a
|
|
62
|
+
* pre-existing short id (e.g. `grants`) that differs from the route slug
|
|
63
|
+
* (`system/grants`). Must be unique across the biome's pages.
|
|
64
|
+
*/
|
|
65
|
+
readonly id?: string;
|
|
66
|
+
/** Displayed nav label. */
|
|
67
|
+
readonly label: string;
|
|
68
|
+
/** Optional nav icon. Typed to match {@link NavItemContribution.icon}. */
|
|
69
|
+
readonly icon?: ComponentType<{ className?: string | undefined }>;
|
|
70
|
+
/**
|
|
71
|
+
* Host menu category — the biome's INTENT (`primary` | `build` | `operate` |
|
|
72
|
+
* `knowledge` | `admin` | `account`). The host maps it to a concrete rail
|
|
73
|
+
* group + order. Prefer this over `weight`.
|
|
74
|
+
*/
|
|
75
|
+
readonly category?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Route scope. `'org'` (default) mounts at the org root; `'project'` mounts
|
|
78
|
+
* under `/projects/:projectId`. Maps to {@link RouteContribution.projectScoped}.
|
|
79
|
+
*/
|
|
80
|
+
readonly scope?: 'org' | 'project';
|
|
81
|
+
/** Access tier required to render the route. Defaults to `'member'`. */
|
|
82
|
+
readonly access?: RouteAccess;
|
|
83
|
+
/** Dynamic import of the page module (`() => import('./pages/X')`). */
|
|
84
|
+
readonly load: () => Promise<{ default: ComponentType<unknown> }>;
|
|
85
|
+
/**
|
|
86
|
+
* Optional legacy intra-category sort weight. Prefer host-owned `category`
|
|
87
|
+
* ordering; retained only as a tiebreak for un-migrated taxonomies.
|
|
88
|
+
*/
|
|
89
|
+
readonly weight?: number;
|
|
90
|
+
/**
|
|
91
|
+
* When true the route is registered but NO nav item is emitted — for hidden
|
|
92
|
+
* surfaces such as a `:param` detail route reached only by deeplink.
|
|
93
|
+
*/
|
|
94
|
+
readonly navHidden?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface DefineWebBiomeOptions {
|
|
98
|
+
/** Stable biome id; matches `xema-biome.json` `xema.id`. */
|
|
99
|
+
readonly id: string;
|
|
100
|
+
/** Displayed name. */
|
|
101
|
+
readonly displayName: string;
|
|
102
|
+
/** One-shot initializer; passed through to {@link FrontendBiome.init}. */
|
|
103
|
+
readonly init?: FrontendBiome['init'];
|
|
104
|
+
/** Teardown counterpart; passed through to {@link FrontendBiome.dispose}. */
|
|
105
|
+
readonly dispose?: FrontendBiome['dispose'];
|
|
106
|
+
/** The biome's pages — one nav item (unless `navHidden`) + one route each. */
|
|
107
|
+
readonly pages: readonly WebBiomePage[];
|
|
108
|
+
/** Slot fillers; passed through to {@link FrontendBiome.panels}. */
|
|
109
|
+
readonly panels?: FrontendBiome['panels'];
|
|
110
|
+
/** Session-shell contributions; passed through to {@link FrontendBiome.session}. */
|
|
111
|
+
readonly session?: FrontendBiome['session'];
|
|
112
|
+
/** Output renderers; passed through to {@link FrontendBiome.outputRenderers}. */
|
|
113
|
+
readonly outputRenderers?: FrontendBiome['outputRenderers'];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build the `FrontendBiomeFactory` a web biome default-exports. See the
|
|
118
|
+
* module doc-comment for the rationale and an example.
|
|
119
|
+
*/
|
|
120
|
+
export function defineWebBiome(
|
|
121
|
+
options: DefineWebBiomeOptions,
|
|
122
|
+
): FrontendBiomeFactory {
|
|
123
|
+
return () => {
|
|
124
|
+
const navItems: NavItemContribution[] = [];
|
|
125
|
+
const routes: RouteContribution[] = [];
|
|
126
|
+
|
|
127
|
+
for (const page of options.pages) {
|
|
128
|
+
routes.push({
|
|
129
|
+
path: page.slug,
|
|
130
|
+
projectScoped: page.scope === 'project',
|
|
131
|
+
element: lazyRoute(page.load),
|
|
132
|
+
...(page.access ? { access: page.access } : {}),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!page.navHidden) {
|
|
136
|
+
navItems.push({
|
|
137
|
+
id: page.id ?? page.slug,
|
|
138
|
+
label: page.label,
|
|
139
|
+
route: page.slug,
|
|
140
|
+
...(page.icon ? { icon: page.icon } : {}),
|
|
141
|
+
...(page.category !== undefined ? { category: page.category } : {}),
|
|
142
|
+
...(page.weight !== undefined ? { weight: page.weight } : {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
id: options.id,
|
|
149
|
+
displayName: options.displayName,
|
|
150
|
+
navItems,
|
|
151
|
+
routes,
|
|
152
|
+
...(options.init ? { init: options.init } : {}),
|
|
153
|
+
...(options.dispose ? { dispose: options.dispose } : {}),
|
|
154
|
+
...(options.panels ? { panels: options.panels } : {}),
|
|
155
|
+
...(options.session ? { session: options.session } : {}),
|
|
156
|
+
...(options.outputRenderers
|
|
157
|
+
? { outputRenderers: options.outputRenderers }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical user-facing error decoder — ONE implementation biomes, the host
|
|
3
|
+
* shell, and the kernel's {@link ErrorCard} default all share.
|
|
4
|
+
*
|
|
5
|
+
* Biomes were each re-implementing "pull a human message off an error"
|
|
6
|
+
* (envelope shapes, status-code copy, "failed to fetch"). This is the single
|
|
7
|
+
* host-agnostic decoder. It is a PURE function: no fetch, no i18n, no config,
|
|
8
|
+
* no `instanceof` against host classes. It recognises the two error shapes the
|
|
9
|
+
* Xema frontend produces by their structural signature:
|
|
10
|
+
*
|
|
11
|
+
* - the host's `ApiClientError` (`name === 'ApiClientError'`, `statusCode`,
|
|
12
|
+
* `message`, optional `errorType`), thrown by the hand-rolled HTTP client;
|
|
13
|
+
* - Orval's generated `ClientError` (`name === 'ClientError'`, `status`,
|
|
14
|
+
* `message`, `body`), thrown by the generated clients. Its `body` carries
|
|
15
|
+
* the backend `{ message, code, details }` envelope (and the workflow
|
|
16
|
+
* wallet-missing / `session_busy` special cases).
|
|
17
|
+
*
|
|
18
|
+
* Hosts that previously kept their own `getUserFacingErrorMessage` should
|
|
19
|
+
* re-export THIS one (or delete theirs and import it) so there is a single
|
|
20
|
+
* decoder. {@link ErrorCard} defaults to it, so biomes get envelope-aware copy
|
|
21
|
+
* without wiring anything.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function collapseWhitespace(value: string): string {
|
|
25
|
+
return value.split(/\s+/).join(' ').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getGenericErrorMessage(message: string, fallback: string): string {
|
|
29
|
+
const normalized = collapseWhitespace(message);
|
|
30
|
+
if (/failed to fetch/i.test(normalized)) {
|
|
31
|
+
return 'Could not reach the service. Check your connection and try again.';
|
|
32
|
+
}
|
|
33
|
+
return normalized || fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeValidationMessage(rawMessage: string): string {
|
|
37
|
+
const parts = rawMessage
|
|
38
|
+
.split(',')
|
|
39
|
+
.map((part) => collapseWhitespace(part))
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
|
|
42
|
+
if (parts.length === 0) {
|
|
43
|
+
return 'Some required fields are missing or invalid. Please review your input and try again.';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const preview = parts.slice(0, 2).join('; ');
|
|
47
|
+
const suffix = parts.length > 2 ? '; and more.' : '.';
|
|
48
|
+
return `Some required fields are missing or invalid (${preview}${suffix})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Structural signature of the host's `ApiClientError`. */
|
|
52
|
+
interface ApiClientErrorLike {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly statusCode: number;
|
|
55
|
+
readonly message: string;
|
|
56
|
+
readonly errorType?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isApiClientErrorLike(error: unknown): error is ApiClientErrorLike {
|
|
60
|
+
if (!error || typeof error !== 'object') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const record = error as Record<string, unknown>;
|
|
64
|
+
return (
|
|
65
|
+
record['name'] === 'ApiClientError' &&
|
|
66
|
+
typeof record['statusCode'] === 'number' &&
|
|
67
|
+
typeof record['message'] === 'string'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Structural signature of an Orval-generated `ClientError`. */
|
|
72
|
+
interface OrvalClientErrorLike {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly status: number;
|
|
75
|
+
readonly message: string;
|
|
76
|
+
readonly body: unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isOrvalClientErrorLike(error: unknown): error is OrvalClientErrorLike {
|
|
80
|
+
if (!error || typeof error !== 'object') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const record = error as Record<string, unknown>;
|
|
84
|
+
return (
|
|
85
|
+
record['name'] === 'ClientError' &&
|
|
86
|
+
typeof record['status'] === 'number' &&
|
|
87
|
+
typeof record['message'] === 'string' &&
|
|
88
|
+
Object.hasOwn(record, 'body')
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface WorkflowErrorEnvelope {
|
|
93
|
+
readonly code?: string;
|
|
94
|
+
readonly message?: string;
|
|
95
|
+
readonly details?: Record<string, unknown>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readStringList(value: unknown): string[] | null {
|
|
99
|
+
if (!Array.isArray(value)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (value.some((entry) => typeof entry !== 'string')) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeWorkflowWalletMissingMessage(envelope: WorkflowErrorEnvelope): string | null {
|
|
109
|
+
if (envelope.code !== 'WORKFLOW_WALLET_NOT_FOUND') {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const missing = readStringList(envelope.details?.['missing']);
|
|
113
|
+
if (missing === null || missing.length === 0) {
|
|
114
|
+
return 'One or more required wallets are missing for this project.';
|
|
115
|
+
}
|
|
116
|
+
return `Missing wallet${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}. Add ${
|
|
117
|
+
missing.length > 1 ? 'them' : 'it'
|
|
118
|
+
} in Project Settings -> Wallets.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getApiClientErrorMessage(error: ApiClientErrorLike, fallback: string): string {
|
|
122
|
+
if (error.statusCode === 0) {
|
|
123
|
+
return collapseWhitespace(error.message) || fallback;
|
|
124
|
+
}
|
|
125
|
+
if (error.statusCode === 400) {
|
|
126
|
+
const raw = collapseWhitespace(error.message);
|
|
127
|
+
if (raw.includes(' must ') || raw.includes('regular expression')) {
|
|
128
|
+
return normalizeValidationMessage(raw);
|
|
129
|
+
}
|
|
130
|
+
return raw || 'The request is invalid. Please check your input and try again.';
|
|
131
|
+
}
|
|
132
|
+
if (error.statusCode === 401) {
|
|
133
|
+
return 'Your session expired. Please sign in again.';
|
|
134
|
+
}
|
|
135
|
+
if (error.statusCode === 403) {
|
|
136
|
+
return 'You do not have permission to perform this action.';
|
|
137
|
+
}
|
|
138
|
+
if (error.statusCode === 404) {
|
|
139
|
+
return 'The requested resource was not found.';
|
|
140
|
+
}
|
|
141
|
+
if (error.statusCode >= 500) {
|
|
142
|
+
return 'The service is temporarily unavailable. Please try again in a moment.';
|
|
143
|
+
}
|
|
144
|
+
return collapseWhitespace(error.message) || fallback;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getOrvalClientErrorMessage(error: OrvalClientErrorLike, fallback: string): string {
|
|
148
|
+
if (error.status >= 500) {
|
|
149
|
+
return 'The service is temporarily unavailable. Please try again in a moment.';
|
|
150
|
+
}
|
|
151
|
+
// session-api refuses mutations while a prompt turn is in flight; the 409
|
|
152
|
+
// body is `{ error: 'session_busy', retryAfterTurnDone: true }`. Surface a
|
|
153
|
+
// clear retry hint instead of the generic "Conflict" copy.
|
|
154
|
+
if (error.status === 409) {
|
|
155
|
+
const body = error.body;
|
|
156
|
+
if (body && typeof body === 'object') {
|
|
157
|
+
const record = body as Record<string, unknown>;
|
|
158
|
+
if (record['error'] === 'session_busy') {
|
|
159
|
+
return 'Your last message is still running. Wait for the agent to finish, then try again.';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const body = error.body;
|
|
164
|
+
if (body && typeof body === 'object') {
|
|
165
|
+
const record = body as Record<string, unknown>;
|
|
166
|
+
const payload: WorkflowErrorEnvelope = (() => {
|
|
167
|
+
const message = record['message'];
|
|
168
|
+
if (message && typeof message === 'object') {
|
|
169
|
+
return message as WorkflowErrorEnvelope;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
code: typeof record['code'] === 'string' ? record['code'] : undefined,
|
|
173
|
+
message: typeof record['message'] === 'string' ? record['message'] : undefined,
|
|
174
|
+
details:
|
|
175
|
+
record['details'] && typeof record['details'] === 'object'
|
|
176
|
+
? (record['details'] as Record<string, unknown>)
|
|
177
|
+
: undefined,
|
|
178
|
+
};
|
|
179
|
+
})();
|
|
180
|
+
|
|
181
|
+
const workflowWalletMessage = normalizeWorkflowWalletMissingMessage(payload);
|
|
182
|
+
if (workflowWalletMessage !== null) {
|
|
183
|
+
return workflowWalletMessage;
|
|
184
|
+
}
|
|
185
|
+
if (typeof payload.message === 'string' && payload.message.trim().length > 0) {
|
|
186
|
+
return collapseWhitespace(payload.message);
|
|
187
|
+
}
|
|
188
|
+
if (typeof record['message'] === 'string' && record['message'].trim().length > 0) {
|
|
189
|
+
return collapseWhitespace(record['message']);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return getGenericErrorMessage(error.message, fallback);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Decode any thrown value into a human, user-facing message. The single decoder
|
|
197
|
+
* for biomes, the host shell, and {@link ErrorCard}'s default.
|
|
198
|
+
*
|
|
199
|
+
* @param error Any thrown value (string, `Error`, `ApiClientError`, Orval
|
|
200
|
+
* `ClientError`, or unknown).
|
|
201
|
+
* @param fallback Copy returned when nothing better can be extracted.
|
|
202
|
+
*/
|
|
203
|
+
export function getUserFacingErrorMessage(
|
|
204
|
+
error: unknown,
|
|
205
|
+
fallback = 'Something went wrong. Please try again.',
|
|
206
|
+
): string {
|
|
207
|
+
if (typeof error === 'string') {
|
|
208
|
+
return collapseWhitespace(error) || fallback;
|
|
209
|
+
}
|
|
210
|
+
if (isApiClientErrorLike(error)) {
|
|
211
|
+
return getApiClientErrorMessage(error, fallback);
|
|
212
|
+
}
|
|
213
|
+
if (isOrvalClientErrorLike(error)) {
|
|
214
|
+
return getOrvalClientErrorMessage(error, fallback);
|
|
215
|
+
}
|
|
216
|
+
if (error instanceof Error) {
|
|
217
|
+
return getGenericErrorMessage(error.message, fallback);
|
|
218
|
+
}
|
|
219
|
+
return fallback;
|
|
220
|
+
}
|
|
@@ -27,9 +27,27 @@ export interface NavItemContribution {
|
|
|
27
27
|
* className it computes from its own theme.
|
|
28
28
|
*/
|
|
29
29
|
readonly icon?: ComponentType<{ className?: string | undefined }>;
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Semantic menu category the biome declares its INTENT to live in (e.g.
|
|
32
|
+
* `build`, `operate`, `knowledge`, `account`). The HOST owns the menu
|
|
33
|
+
* information-architecture: it maps a category to a concrete rail group,
|
|
34
|
+
* heading, and order. Authors declare WHAT a surface is, not WHERE in the
|
|
35
|
+
* global menu it renders — so the platform can re-organise the menu without
|
|
36
|
+
* editing every biome. Prefer this over `section`/`weight`. When omitted,
|
|
37
|
+
* the host falls back to its own taxonomy (keyed on the nav-item id) and,
|
|
38
|
+
* last, to the legacy `section`.
|
|
39
|
+
*/
|
|
40
|
+
readonly category?: string;
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Legacy direct section-heading placement. The host now owns
|
|
43
|
+
* grouping via `category` + its taxonomy; retained only until every biome
|
|
44
|
+
* declares a `category`. Do NOT use in new biomes.
|
|
45
|
+
*/
|
|
31
46
|
readonly section?: string;
|
|
32
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated Legacy intra-section sort weight. Ordering is host-owned per
|
|
49
|
+
* category now. Retained only for un-migrated biomes.
|
|
50
|
+
*/
|
|
33
51
|
readonly weight?: number;
|
|
34
52
|
}
|
|
35
53
|
|
|
@@ -5,6 +5,8 @@ import { createContext, useContext, type ReactNode } from 'react';
|
|
|
5
5
|
import type { CapabilityPort } from '../capabilities';
|
|
6
6
|
import type { SystemBus } from '../system-bus';
|
|
7
7
|
|
|
8
|
+
import type { RealtimeSource } from './realtime-port';
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* `HostBridge` is the host-agnostic abstraction every frontend biome
|
|
10
12
|
* receives at registration time. It decouples biome code from any
|
|
@@ -113,6 +115,26 @@ export interface HostBridgeRequestContext {
|
|
|
113
115
|
readonly correlationId: string;
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Error-decoding surface — the canonical "turn a thrown value into a
|
|
120
|
+
* user-facing message" decoder, exposed on the bridge so biomes use ONE
|
|
121
|
+
* implementation instead of each re-deriving envelope/status-code copy.
|
|
122
|
+
*
|
|
123
|
+
* The decoder is a PURE function (no host context), so the kernel also ships it
|
|
124
|
+
* directly as {@link getUserFacingErrorMessage} and {@link ErrorCard} defaults
|
|
125
|
+
* to it. It is ALSO hung off the bridge so a host can override it with a
|
|
126
|
+
* shell-specific decoder (extra error codes, i18n) without every biome wiring a
|
|
127
|
+
* prop — the bridge value is the single override point.
|
|
128
|
+
*/
|
|
129
|
+
export interface HostBridgeErrors {
|
|
130
|
+
/**
|
|
131
|
+
* Decode any thrown value into a user-facing message. `fallback` is returned
|
|
132
|
+
* when nothing better can be extracted. Defaults (in the kernel default
|
|
133
|
+
* bridge wiring) to {@link getUserFacingErrorMessage}.
|
|
134
|
+
*/
|
|
135
|
+
getUserFacingErrorMessage(error: unknown, fallback?: string): string;
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
/**
|
|
117
139
|
* Where the topbar's leading back-button takes you. Pages that have a
|
|
118
140
|
* meaningful "parent context" register this; pages without one omit it
|
|
@@ -201,6 +223,28 @@ export interface HostBridge {
|
|
|
201
223
|
* `<CapabilityProvider>` + `useCapability()` (see `../capabilities`).
|
|
202
224
|
*/
|
|
203
225
|
readonly capabilities: CapabilityPort;
|
|
226
|
+
/**
|
|
227
|
+
* Realtime (CloudEvents over SSE) port. The HOST injects a concrete
|
|
228
|
+
* {@link RealtimeSource} backed by its realtime transport
|
|
229
|
+
* (`@xemahq/realtime-client`); biomes reach it through the kernel
|
|
230
|
+
* `useCloudEvent` / `useRealtimeStatus` / `useEventScope` hooks so biome code
|
|
231
|
+
* never imports the SSE client directly — same decoupling as `navigation`.
|
|
232
|
+
*
|
|
233
|
+
* OPTIONAL: a host without a realtime transport simply omits it; the kernel
|
|
234
|
+
* `useCloudEvent`/`useRealtimeStatus`/`useEventScope` hooks fail fast with an
|
|
235
|
+
* actionable error if a biome uses them before the host wires `bridge.realtime`.
|
|
236
|
+
*/
|
|
237
|
+
readonly realtime?: RealtimeSource;
|
|
238
|
+
/**
|
|
239
|
+
* Error-decoding surface — the canonical user-facing error decoder. Defaults
|
|
240
|
+
* (in the kernel default bridge wiring) to {@link getUserFacingErrorMessage};
|
|
241
|
+
* a host MAY override it. Biomes + {@link ErrorCard} read ONE decoder instead
|
|
242
|
+
* of each re-deriving message extraction.
|
|
243
|
+
*
|
|
244
|
+
* OPTIONAL: when omitted, consumers fall back to the kernel default
|
|
245
|
+
* {@link getUserFacingErrorMessage} (which ErrorCard already uses directly).
|
|
246
|
+
*/
|
|
247
|
+
readonly errors?: HostBridgeErrors;
|
|
204
248
|
}
|
|
205
249
|
|
|
206
250
|
export const HostBridgeContext = createContext<HostBridge | null>(null);
|
|
@@ -18,6 +18,7 @@ export * from './biome-navigation';
|
|
|
18
18
|
export * from './biome-scope';
|
|
19
19
|
export * from './biome-scoped-query';
|
|
20
20
|
export * from './biome-builders';
|
|
21
|
+
export * from './define-web-biome';
|
|
21
22
|
export * from './create-biome-orval-config';
|
|
22
23
|
export * from './biome-registry';
|
|
23
24
|
export * from './session-contributions';
|
|
@@ -26,3 +27,7 @@ export * from './host-sources';
|
|
|
26
27
|
export * from './nav';
|
|
27
28
|
export * from './biome-mode';
|
|
28
29
|
export * from './agent-validation';
|
|
30
|
+
export * from './realtime-port';
|
|
31
|
+
export * from './realtime-hooks';
|
|
32
|
+
export * from './errors';
|
|
33
|
+
export * from './response-envelope';
|