@xemahq/ui-kernel 0.1.11 → 0.2.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.
Files changed (235) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/biome-host/biome-builders.d.ts +21 -0
  6. package/dist/lib/biome-host/biome-builders.d.ts.map +1 -0
  7. package/dist/lib/biome-host/biome-builders.js +25 -0
  8. package/dist/lib/biome-host/biome-builders.js.map +1 -0
  9. package/dist/lib/biome-host/biome-navigation.d.ts +3 -0
  10. package/dist/lib/biome-host/biome-navigation.d.ts.map +1 -0
  11. package/dist/lib/biome-host/biome-navigation.js +14 -0
  12. package/dist/lib/biome-host/biome-navigation.js.map +1 -0
  13. package/dist/lib/biome-host/biome-scope.d.ts +18 -0
  14. package/dist/lib/biome-host/biome-scope.d.ts.map +1 -0
  15. package/dist/lib/biome-host/biome-scope.js +42 -0
  16. package/dist/lib/biome-host/biome-scope.js.map +1 -0
  17. package/dist/lib/biome-host/biome-scoped-query.d.ts +17 -0
  18. package/dist/lib/biome-host/biome-scoped-query.d.ts.map +1 -0
  19. package/dist/lib/biome-host/biome-scoped-query.js +39 -0
  20. package/dist/lib/biome-host/biome-scoped-query.js.map +1 -0
  21. package/dist/lib/biome-host/host-bridge.d.ts +3 -0
  22. package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
  23. package/dist/lib/biome-host/host-bridge.js.map +1 -1
  24. package/dist/lib/biome-host/index.d.ts +4 -0
  25. package/dist/lib/biome-host/index.d.ts.map +1 -1
  26. package/dist/lib/biome-host/index.js +4 -0
  27. package/dist/lib/biome-host/index.js.map +1 -1
  28. package/dist/lib/capabilities/capability-provider.d.ts +15 -0
  29. package/dist/lib/capabilities/capability-provider.d.ts.map +1 -0
  30. package/dist/lib/capabilities/capability-provider.js +36 -0
  31. package/dist/lib/capabilities/capability-provider.js.map +1 -0
  32. package/dist/lib/capabilities/index.d.ts +4 -0
  33. package/dist/lib/capabilities/index.d.ts.map +1 -0
  34. package/dist/lib/capabilities/index.js +20 -0
  35. package/dist/lib/capabilities/index.js.map +1 -0
  36. package/dist/lib/capabilities/types.d.ts +18 -0
  37. package/dist/lib/capabilities/types.d.ts.map +1 -0
  38. package/dist/lib/capabilities/types.js +3 -0
  39. package/dist/lib/capabilities/types.js.map +1 -0
  40. package/dist/lib/capabilities/use-capability.d.ts +18 -0
  41. package/dist/lib/capabilities/use-capability.d.ts.map +1 -0
  42. package/dist/lib/capabilities/use-capability.js +21 -0
  43. package/dist/lib/capabilities/use-capability.js.map +1 -0
  44. package/dist/session/shell/SessionWorkspaceShell.js +1 -1
  45. package/dist/session/shell/SessionWorkspaceShell.js.map +1 -1
  46. package/dist/session-kit/display/ThinkingPanel.d.ts.map +1 -1
  47. package/dist/session-kit/display/ThinkingPanel.js +3 -0
  48. package/dist/session-kit/display/ThinkingPanel.js.map +1 -1
  49. package/dist/ui/chrome/AsyncBoundary.d.ts +22 -0
  50. package/dist/ui/chrome/AsyncBoundary.d.ts.map +1 -0
  51. package/dist/ui/chrome/AsyncBoundary.js +23 -0
  52. package/dist/ui/chrome/AsyncBoundary.js.map +1 -0
  53. package/dist/ui/chrome/EmptyState.d.ts +34 -0
  54. package/dist/ui/chrome/EmptyState.d.ts.map +1 -0
  55. package/dist/ui/chrome/EmptyState.js +27 -0
  56. package/dist/ui/chrome/EmptyState.js.map +1 -0
  57. package/dist/ui/chrome/ErrorCard.d.ts +11 -0
  58. package/dist/ui/chrome/ErrorCard.d.ts.map +1 -0
  59. package/dist/ui/chrome/ErrorCard.js +21 -0
  60. package/dist/ui/chrome/ErrorCard.js.map +1 -0
  61. package/dist/ui/chrome/LoadingState.d.ts +10 -0
  62. package/dist/ui/chrome/LoadingState.d.ts.map +1 -0
  63. package/dist/ui/chrome/LoadingState.js +17 -0
  64. package/dist/ui/chrome/LoadingState.js.map +1 -0
  65. package/dist/ui/chrome/PageHeader.d.ts +20 -0
  66. package/dist/ui/chrome/PageHeader.d.ts.map +1 -0
  67. package/dist/ui/chrome/PageHeader.js +26 -0
  68. package/dist/ui/chrome/PageHeader.js.map +1 -0
  69. package/dist/ui/chrome/StateCard.d.ts +24 -0
  70. package/dist/ui/chrome/StateCard.d.ts.map +1 -0
  71. package/dist/ui/chrome/StateCard.js +17 -0
  72. package/dist/ui/chrome/StateCard.js.map +1 -0
  73. package/dist/ui/cn.d.ts +3 -0
  74. package/dist/ui/cn.d.ts.map +1 -0
  75. package/dist/ui/cn.js +18 -0
  76. package/dist/ui/cn.js.map +1 -0
  77. package/dist/ui/index.d.ts +33 -0
  78. package/dist/ui/index.d.ts.map +1 -0
  79. package/dist/ui/index.js +61 -0
  80. package/dist/ui/index.js.map +1 -0
  81. package/dist/ui/primitives/alert-dialog.d.ts +21 -0
  82. package/dist/ui/primitives/alert-dialog.d.ts.map +1 -0
  83. package/dist/ui/primitives/alert-dialog.js +72 -0
  84. package/dist/ui/primitives/alert-dialog.js.map +1 -0
  85. package/dist/ui/primitives/badge.d.ts +10 -0
  86. package/dist/ui/primitives/badge.d.ts.map +1 -0
  87. package/dist/ui/primitives/badge.js +60 -0
  88. package/dist/ui/primitives/badge.js.map +1 -0
  89. package/dist/ui/primitives/button.d.ts +12 -0
  90. package/dist/ui/primitives/button.d.ts.map +1 -0
  91. package/dist/ui/primitives/button.js +71 -0
  92. package/dist/ui/primitives/button.js.map +1 -0
  93. package/dist/ui/primitives/card.d.ts +9 -0
  94. package/dist/ui/primitives/card.d.ts.map +1 -0
  95. package/dist/ui/primitives/card.js +58 -0
  96. package/dist/ui/primitives/card.js.map +1 -0
  97. package/dist/ui/primitives/checkbox.d.ts +5 -0
  98. package/dist/ui/primitives/checkbox.d.ts.map +1 -0
  99. package/dist/ui/primitives/checkbox.js +45 -0
  100. package/dist/ui/primitives/checkbox.js.map +1 -0
  101. package/dist/ui/primitives/collapsible.d.ts +6 -0
  102. package/dist/ui/primitives/collapsible.d.ts.map +1 -0
  103. package/dist/ui/primitives/collapsible.js +44 -0
  104. package/dist/ui/primitives/collapsible.js.map +1 -0
  105. package/dist/ui/primitives/dialog.d.ts +22 -0
  106. package/dist/ui/primitives/dialog.d.ts.map +1 -0
  107. package/dist/ui/primitives/dialog.js +68 -0
  108. package/dist/ui/primitives/dialog.js.map +1 -0
  109. package/dist/ui/primitives/dropdown-menu.d.ts +28 -0
  110. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  111. package/dist/ui/primitives/dropdown-menu.js +83 -0
  112. package/dist/ui/primitives/dropdown-menu.js.map +1 -0
  113. package/dist/ui/primitives/input.d.ts +4 -0
  114. package/dist/ui/primitives/input.d.ts.map +1 -0
  115. package/dist/ui/primitives/input.js +45 -0
  116. package/dist/ui/primitives/input.js.map +1 -0
  117. package/dist/ui/primitives/label.d.ts +6 -0
  118. package/dist/ui/primitives/label.d.ts.map +1 -0
  119. package/dist/ui/primitives/label.js +46 -0
  120. package/dist/ui/primitives/label.js.map +1 -0
  121. package/dist/ui/primitives/overflow-tabs.d.ts +18 -0
  122. package/dist/ui/primitives/overflow-tabs.d.ts.map +1 -0
  123. package/dist/ui/primitives/overflow-tabs.js +84 -0
  124. package/dist/ui/primitives/overflow-tabs.js.map +1 -0
  125. package/dist/ui/primitives/popover.d.ts +9 -0
  126. package/dist/ui/primitives/popover.d.ts.map +1 -0
  127. package/dist/ui/primitives/popover.js +48 -0
  128. package/dist/ui/primitives/popover.js.map +1 -0
  129. package/dist/ui/primitives/radio-group.d.ts +6 -0
  130. package/dist/ui/primitives/radio-group.d.ts.map +1 -0
  131. package/dist/ui/primitives/radio-group.js +52 -0
  132. package/dist/ui/primitives/radio-group.js.map +1 -0
  133. package/dist/ui/primitives/resizable.d.ts +12 -0
  134. package/dist/ui/primitives/resizable.d.ts.map +1 -0
  135. package/dist/ui/primitives/resizable.js +18 -0
  136. package/dist/ui/primitives/resizable.js.map +1 -0
  137. package/dist/ui/primitives/scroll-area.d.ts +6 -0
  138. package/dist/ui/primitives/scroll-area.d.ts.map +1 -0
  139. package/dist/ui/primitives/scroll-area.js +47 -0
  140. package/dist/ui/primitives/scroll-area.js.map +1 -0
  141. package/dist/ui/primitives/select.d.ts +14 -0
  142. package/dist/ui/primitives/select.d.ts.map +1 -0
  143. package/dist/ui/primitives/select.js +71 -0
  144. package/dist/ui/primitives/select.js.map +1 -0
  145. package/dist/ui/primitives/separator.d.ts +5 -0
  146. package/dist/ui/primitives/separator.d.ts.map +1 -0
  147. package/dist/ui/primitives/separator.js +44 -0
  148. package/dist/ui/primitives/separator.js.map +1 -0
  149. package/dist/ui/primitives/sheet.d.ts +26 -0
  150. package/dist/ui/primitives/sheet.d.ts.map +1 -0
  151. package/dist/ui/primitives/sheet.js +82 -0
  152. package/dist/ui/primitives/sheet.js.map +1 -0
  153. package/dist/ui/primitives/skeleton.d.ts +13 -0
  154. package/dist/ui/primitives/skeleton.d.ts.map +1 -0
  155. package/dist/ui/primitives/skeleton.js +29 -0
  156. package/dist/ui/primitives/skeleton.js.map +1 -0
  157. package/dist/ui/primitives/switch.d.ts +5 -0
  158. package/dist/ui/primitives/switch.d.ts.map +1 -0
  159. package/dist/ui/primitives/switch.js +44 -0
  160. package/dist/ui/primitives/switch.js.map +1 -0
  161. package/dist/ui/primitives/table.d.ts +11 -0
  162. package/dist/ui/primitives/table.d.ts.map +1 -0
  163. package/dist/ui/primitives/table.js +64 -0
  164. package/dist/ui/primitives/table.js.map +1 -0
  165. package/dist/ui/primitives/tabs.d.ts +8 -0
  166. package/dist/ui/primitives/tabs.d.ts.map +1 -0
  167. package/dist/ui/primitives/tabs.js +52 -0
  168. package/dist/ui/primitives/tabs.js.map +1 -0
  169. package/dist/ui/primitives/tag-multi-select.d.ts +19 -0
  170. package/dist/ui/primitives/tag-multi-select.d.ts.map +1 -0
  171. package/dist/ui/primitives/tag-multi-select.js +92 -0
  172. package/dist/ui/primitives/tag-multi-select.js.map +1 -0
  173. package/dist/ui/primitives/textarea.d.ts +5 -0
  174. package/dist/ui/primitives/textarea.d.ts.map +1 -0
  175. package/dist/ui/primitives/textarea.js +45 -0
  176. package/dist/ui/primitives/textarea.js.map +1 -0
  177. package/dist/ui/primitives/tooltip.d.ts +8 -0
  178. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  179. package/dist/ui/primitives/tooltip.js +50 -0
  180. package/dist/ui/primitives/tooltip.js.map +1 -0
  181. package/package.json +27 -4
  182. package/src/index.ts +1 -0
  183. package/src/lib/biome-host/biome-builders.ts +109 -0
  184. package/src/lib/biome-host/biome-navigation.ts +37 -0
  185. package/src/lib/biome-host/biome-scope.tsx +119 -0
  186. package/src/lib/biome-host/biome-scoped-query.ts +130 -0
  187. package/src/lib/biome-host/host-bridge.ts +23 -0
  188. package/src/lib/biome-host/index.ts +4 -0
  189. package/src/lib/capabilities/capability-provider.tsx +95 -0
  190. package/src/lib/capabilities/index.ts +16 -0
  191. package/src/lib/capabilities/types.ts +69 -0
  192. package/src/lib/capabilities/use-capability.ts +72 -0
  193. package/src/session/shell/SessionWorkspaceShell.tsx +2 -2
  194. package/src/session-kit/display/ThinkingPanel.tsx +3 -0
  195. package/src/ui/chrome/AsyncBoundary.tsx +66 -0
  196. package/src/ui/chrome/EmptyState.tsx +184 -0
  197. package/src/ui/chrome/ErrorCard.tsx +68 -0
  198. package/src/ui/chrome/LoadingState.tsx +61 -0
  199. package/src/ui/chrome/PageHeader.tsx +137 -0
  200. package/src/ui/chrome/StateCard.tsx +150 -0
  201. package/src/ui/cn.ts +32 -0
  202. package/src/ui/index.ts +53 -0
  203. package/src/ui/primitives/alert-dialog.tsx +104 -0
  204. package/src/ui/primitives/badge.tsx +32 -0
  205. package/src/ui/primitives/button.tsx +47 -0
  206. package/src/ui/primitives/card.tsx +43 -0
  207. package/src/ui/primitives/checkbox.tsx +26 -0
  208. package/src/ui/primitives/collapsible.tsx +9 -0
  209. package/src/ui/primitives/dialog.tsx +103 -0
  210. package/src/ui/primitives/dropdown-menu.tsx +179 -0
  211. package/src/ui/primitives/input.tsx +22 -0
  212. package/src/ui/primitives/label.tsx +17 -0
  213. package/src/ui/primitives/overflow-tabs.tsx +281 -0
  214. package/src/ui/primitives/popover.tsx +33 -0
  215. package/src/ui/primitives/radio-group.tsx +36 -0
  216. package/src/ui/primitives/resizable.tsx +67 -0
  217. package/src/ui/primitives/scroll-area.tsx +38 -0
  218. package/src/ui/primitives/select.tsx +143 -0
  219. package/src/ui/primitives/separator.tsx +20 -0
  220. package/src/ui/primitives/sheet.tsx +107 -0
  221. package/src/ui/primitives/skeleton.tsx +99 -0
  222. package/src/ui/primitives/switch.tsx +27 -0
  223. package/src/ui/primitives/table.tsx +72 -0
  224. package/src/ui/primitives/tabs.tsx +53 -0
  225. package/src/ui/primitives/tag-multi-select.tsx +241 -0
  226. package/src/ui/primitives/textarea.tsx +21 -0
  227. package/src/ui/primitives/tooltip.tsx +30 -0
  228. package/dist/lib/biome-host/composition-validation.d.ts +0 -22
  229. package/dist/lib/biome-host/composition-validation.d.ts.map +0 -1
  230. package/dist/lib/biome-host/composition-validation.js +0 -127
  231. package/dist/lib/biome-host/composition-validation.js.map +0 -1
  232. package/dist/registry/lib/composition-validation-host.d.ts +0 -3
  233. package/dist/registry/lib/composition-validation-host.d.ts.map +0 -1
  234. package/dist/registry/lib/composition-validation-host.js +0 -10
  235. package/dist/registry/lib/composition-validation-host.js.map +0 -1
@@ -0,0 +1,95 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { createContext, useContext, type ReactNode } from 'react';
3
+
4
+ import { useHostBridge } from '../biome-host/host-bridge';
5
+
6
+ import type { CapabilityPort, MeCapabilitiesSnapshot } from './types';
7
+
8
+ /**
9
+ * The value exposed by {@link CapabilityProvider} through React context.
10
+ * `snapshot` is `undefined` until the first successful fetch; `isLoading`
11
+ * and `error` let callers distinguish "still unknown" and "fetch failed"
12
+ * from "explicitly denied" — the gate hook must NOT treat an errored fetch
13
+ * as "no capabilities".
14
+ */
15
+ export interface CapabilityContextValue {
16
+ readonly snapshot: MeCapabilitiesSnapshot | undefined;
17
+ readonly isLoading: boolean;
18
+ readonly error: unknown;
19
+ }
20
+
21
+ const CapabilityContext = createContext<CapabilityContextValue | null>(null);
22
+ CapabilityContext.displayName = 'CapabilityContext';
23
+
24
+ /**
25
+ * Stable query-key root for the capability snapshot. Folded with the active
26
+ * org so each org caches independently and a re-fetch fires when the org
27
+ * changes — same tenant-isolation discipline as `useScopedQuery`.
28
+ */
29
+ const CAPABILITIES_QUERY_ROOT = ['xema', 'capabilities', 'me'] as const;
30
+
31
+ export interface CapabilityProviderProps {
32
+ readonly children: ReactNode;
33
+ /**
34
+ * Override the host-provided port. Defaults to `bridge.capabilities`. Exists
35
+ * for tests and for hosts that compose the provider outside a bridge subtree;
36
+ * production wiring reads it OFF the bridge so biomes need no extra prop.
37
+ */
38
+ readonly port?: CapabilityPort;
39
+ /**
40
+ * Override the active org. Defaults to `null` (the host's
41
+ * currently-selected org). When provided, the port is asked for that org and
42
+ * the cache key includes it.
43
+ */
44
+ readonly orgId?: string | null;
45
+ }
46
+
47
+ /**
48
+ * Fetches the caller's server-authoritative capability set once per active org
49
+ * and exposes it (plus loading/error) to descendants via React context.
50
+ *
51
+ * Wiring: the {@link CapabilityPort} is read OFF `HostBridge.capabilities`
52
+ * (not prop-drilled) — the same decoupling the rest of the kernel uses to
53
+ * reach the host. The active org is read from the bridge's auth source. The
54
+ * bridge's `QueryClient` (TanStack Query) owns caching/refetch; the kernel
55
+ * rolls no bespoke state. The query re-runs when the org changes because the
56
+ * org is part of the query key.
57
+ *
58
+ * Rendering is fail-soft (callers can show a loading state), but failures are
59
+ * SURFACED via `error` rather than masquerading as an empty capability set.
60
+ */
61
+ export function CapabilityProvider(props: CapabilityProviderProps): JSX.Element {
62
+ const bridge = useHostBridge();
63
+ const port = props.port ?? bridge.capabilities;
64
+ const orgId = props.orgId !== undefined ? props.orgId : bridge.auth.getOrgId();
65
+
66
+ const query = useQuery<MeCapabilitiesSnapshot>({
67
+ queryKey: [...CAPABILITIES_QUERY_ROOT, orgId],
68
+ queryFn: () => port.list(orgId ? { orgId } : undefined),
69
+ });
70
+
71
+ const value: CapabilityContextValue = {
72
+ snapshot: query.data,
73
+ isLoading: query.isLoading,
74
+ error: query.error,
75
+ };
76
+
77
+ return <CapabilityContext.Provider value={value}>{props.children}</CapabilityContext.Provider>;
78
+ }
79
+
80
+ /**
81
+ * Internal accessor for the capability context. Throws fail-fast if used
82
+ * outside {@link CapabilityProvider} — a missing provider is a wiring bug,
83
+ * not a "deny everything" condition.
84
+ */
85
+ export function useCapabilityContext(): CapabilityContextValue {
86
+ const ctx = useContext(CapabilityContext);
87
+ if (!ctx) {
88
+ throw new Error(
89
+ '[xema-ui-kernel] useCapability()/useCapabilities() called outside ' +
90
+ '<CapabilityProvider>. The host (or biome shell) must mount the ' +
91
+ 'capability provider above any capability-gated subtree.',
92
+ );
93
+ }
94
+ return ctx;
95
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Host-agnostic capabilities SDK for `@xemahq/ui-kernel`.
3
+ *
4
+ * Lets a biome UI gate on SERVER-AUTHORITATIVE capabilities (the caller's
5
+ * effective set from `GET /bff/me/capabilities`) instead of cosmetic role
6
+ * booleans. The kernel defines the contract and the React surface; it never
7
+ * fetches — the HOST implements {@link CapabilityPort} (hung off
8
+ * `HostBridge.capabilities`) and maps its Orval client response onto the
9
+ * local {@link MeCapabilitiesSnapshot} shape.
10
+ *
11
+ * Biome usage:
12
+ * import { CapabilityProvider, useCapability } from '@xemahq/ui-kernel';
13
+ */
14
+ export * from './types';
15
+ export * from './capability-provider';
16
+ export * from './use-capability';
@@ -0,0 +1,69 @@
1
+ import type { ExecutionEnvironmentKind } from '@xemahq/kernel-contracts/execution-environment';
2
+
3
+ /**
4
+ * One server-authoritative capability the caller holds, mirroring the
5
+ * FROZEN `GET /bff/me/capabilities` wire entry.
6
+ *
7
+ * These types are defined LOCALLY in the kernel — deliberately NOT imported
8
+ * from a backend DTO or an Orval client — so `@xemahq/ui-kernel` stays
9
+ * host-agnostic (no fetch lib, no generated client). The concrete host
10
+ * (`submodules/xema-host-web`) maps its client response onto this shape when
11
+ * it implements {@link CapabilityPort}.
12
+ */
13
+ export interface CapabilityEntry {
14
+ /**
15
+ * The capability reference (e.g. `capability:session.launch`). Matched
16
+ * verbatim by {@link useCapability}; the kernel does not parse it.
17
+ */
18
+ readonly ref: string;
19
+ /**
20
+ * The execution environments in which the caller holds this capability.
21
+ * A gate that names an `environment` is allowed only if it appears here.
22
+ */
23
+ readonly environments: ExecutionEnvironmentKind[];
24
+ /**
25
+ * When `true`, holding the capability still requires a human approval step
26
+ * before the side-effect runs — so a UI affordance should route through the
27
+ * approval flow rather than acting directly.
28
+ */
29
+ readonly approvalRequired: boolean;
30
+ }
31
+
32
+ /**
33
+ * The caller's effective capability set for one active org, mirroring the
34
+ * FROZEN `MeCapabilitiesResponse` wire shape.
35
+ */
36
+ export interface MeCapabilitiesSnapshot {
37
+ /** The subject (user) the snapshot was resolved for. */
38
+ readonly subjectId: string;
39
+ /** The org the snapshot is scoped to. */
40
+ readonly orgId: string;
41
+ /** Every capability the subject effectively holds in this org. */
42
+ readonly capabilities: CapabilityEntry[];
43
+ }
44
+
45
+ /**
46
+ * Host-implemented port that fetches the caller's effective capability set.
47
+ *
48
+ * The kernel never calls an API directly — the HOST implements this against
49
+ * its Orval client (`GET /bff/me/capabilities`) and maps the response onto
50
+ * {@link MeCapabilitiesSnapshot}. The implementation is hung off
51
+ * {@link HostBridge.capabilities}; {@link CapabilityProvider} reads it from
52
+ * the bridge and uses the bridge's `QueryClient` for caching/refetch.
53
+ */
54
+ export interface CapabilityPort {
55
+ /**
56
+ * Return the caller's effective capability set for the active org.
57
+ *
58
+ * @param options.orgId The org to resolve for. When omitted the host
59
+ * resolves against its currently-selected org.
60
+ * @param options.environment Optional environment hint the host MAY pass to
61
+ * the backend to narrow the set. Callers gate by environment client-side
62
+ * via {@link useCapability}, so a host MAY ignore this and return the full
63
+ * set; it exists so a host can request a pre-filtered snapshot.
64
+ */
65
+ list(options?: {
66
+ orgId?: string;
67
+ environment?: ExecutionEnvironmentKind;
68
+ }): Promise<MeCapabilitiesSnapshot>;
69
+ }
@@ -0,0 +1,72 @@
1
+ import type { ExecutionEnvironmentKind } from '@xemahq/kernel-contracts/execution-environment';
2
+
3
+ import { useCapabilityContext, type CapabilityContextValue } from './capability-provider';
4
+ import type { MeCapabilitiesSnapshot } from './types';
5
+
6
+ export interface UseCapabilityOptions {
7
+ /**
8
+ * Gate against a specific execution environment. When set, the capability
9
+ * counts only if the snapshot lists it for this environment.
10
+ */
11
+ readonly environment?: ExecutionEnvironmentKind;
12
+ }
13
+
14
+ export interface CapabilityDecision {
15
+ /**
16
+ * `true` only when the caller holds the capability, holds it in the named
17
+ * environment (if one was given), AND it does NOT require approval. A gate
18
+ * that should let the user act directly checks this.
19
+ */
20
+ readonly allowed: boolean;
21
+ /**
22
+ * `true` when the caller holds the capability (in the named environment, if
23
+ * given) but it requires a human approval step first — render the
24
+ * approval-routed affordance instead of acting directly.
25
+ */
26
+ readonly approvalRequired: boolean;
27
+ /** The snapshot is still being fetched; treat as "unknown", not "denied". */
28
+ readonly isLoading: boolean;
29
+ /** The fetch failed; surfaced so callers never conflate failure with denial. */
30
+ readonly error: unknown;
31
+ }
32
+
33
+ /**
34
+ * Server-authoritative capability gate.
35
+ *
36
+ * Resolves `ref` against the cached {@link CapabilityProvider} snapshot:
37
+ * - `allowed` = entry exists AND (no environment asked OR listed for it) AND NOT approvalRequired.
38
+ * - `approvalRequired` = entry exists AND (no environment asked OR listed for it) AND approvalRequired.
39
+ *
40
+ * While the snapshot is loading, `allowed` is `false` and `isLoading` is
41
+ * `true` — callers gate on `isLoading` to distinguish "unknown" from "denied".
42
+ * On error, `allowed` is `false` and `error` is set; the hook never silently
43
+ * degrades a failed fetch into a confident denial.
44
+ */
45
+ export function useCapability(ref: string, opts?: UseCapabilityOptions): CapabilityDecision {
46
+ const { snapshot, isLoading, error } = useCapabilityContext();
47
+
48
+ const entry = snapshot?.capabilities.find((c) => c.ref === ref);
49
+ const inEnvironment =
50
+ !!entry && (!opts?.environment || entry.environments.includes(opts.environment));
51
+
52
+ return {
53
+ allowed: inEnvironment && !entry.approvalRequired,
54
+ approvalRequired: inEnvironment && entry.approvalRequired,
55
+ isLoading,
56
+ error,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Accessor for the raw capability snapshot plus its loading/error state.
62
+ * Use when a component needs to enumerate capabilities rather than gate on a
63
+ * single `ref`.
64
+ */
65
+ export function useCapabilities(): {
66
+ snapshot: MeCapabilitiesSnapshot | undefined;
67
+ isLoading: boolean;
68
+ error: unknown;
69
+ } {
70
+ const ctx: CapabilityContextValue = useCapabilityContext();
71
+ return { snapshot: ctx.snapshot, isLoading: ctx.isLoading, error: ctx.error };
72
+ }
@@ -102,8 +102,8 @@ export function SessionWorkspaceShell({
102
102
  : undefined
103
103
  }
104
104
  >
105
- <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
106
- <div className="relative min-h-0 flex-1">{chatSlot}</div>
105
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
106
+ <div className="relative min-h-0 min-w-0 flex-1">{chatSlot}</div>
107
107
  {chatFooterSlot}
108
108
  </div>
109
109
  </div>
@@ -68,6 +68,9 @@ export function ThinkingPanel({
68
68
  fontStyle: 'italic',
69
69
  color: 'hsl(var(--ink-3))',
70
70
  whiteSpace: 'pre-wrap',
71
+ overflowWrap: 'anywhere',
72
+ wordBreak: 'break-word',
73
+ minWidth: 0,
71
74
  }}
72
75
  >
73
76
  {text}
@@ -0,0 +1,66 @@
1
+ import type { LucideIcon } from 'lucide-react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ import ErrorCard, { type ErrorMessageFormatter } from './ErrorCard';
5
+ import EmptyState from './EmptyState';
6
+ import LoadingState, { type LoadingStateVariant } from './LoadingState';
7
+
8
+ interface AsyncBoundaryEmpty {
9
+ icon: LucideIcon;
10
+ title: string;
11
+ description?: ReactNode;
12
+ }
13
+
14
+ interface AsyncBoundaryProps {
15
+ /** Async in flight — renders the loading affordance. */
16
+ isLoading: boolean;
17
+ /** Resolved error, if any — renders an `ErrorCard`. */
18
+ error?: Error | string | unknown | null;
19
+ /**
20
+ * When true, the resolved data is empty — renders an `EmptyState`. Only
21
+ * checked when `empty` config is supplied; otherwise children render.
22
+ */
23
+ isEmpty?: boolean;
24
+ /** Empty-state config; omit to never render an empty state. */
25
+ empty?: AsyncBoundaryEmpty;
26
+ /** Retry handler threaded to `ErrorCard`. */
27
+ onRetry?: () => void;
28
+ /** Loading affordance variant. */
29
+ loadingVariant?: LoadingStateVariant;
30
+ /** Host error decoder threaded to `ErrorCard`. */
31
+ formatError?: ErrorMessageFormatter;
32
+ children: ReactNode;
33
+ }
34
+
35
+ /**
36
+ * Declarative loading / error / empty wrapper. Collapses the
37
+ * `if (isLoading) … if (error) … if (empty) … else children` ladder that
38
+ * every biome list/detail surface re-implements into one component built on
39
+ * the shared `LoadingState` / `ErrorCard` / `EmptyState` primitives.
40
+ *
41
+ * Precedence: loading → error → empty → children.
42
+ */
43
+ export default function AsyncBoundary({
44
+ isLoading,
45
+ error,
46
+ isEmpty,
47
+ empty,
48
+ onRetry,
49
+ loadingVariant,
50
+ formatError,
51
+ children,
52
+ }: Readonly<AsyncBoundaryProps>) {
53
+ if (isLoading) {
54
+ return <LoadingState variant={loadingVariant} />;
55
+ }
56
+
57
+ if (error) {
58
+ return <ErrorCard error={error} onRetry={onRetry} formatError={formatError} />;
59
+ }
60
+
61
+ if (isEmpty && empty) {
62
+ return <EmptyState icon={empty.icon} title={empty.title} description={empty.description} />;
63
+ }
64
+
65
+ return <>{children}</>;
66
+ }
@@ -0,0 +1,184 @@
1
+ import type { LucideIcon } from 'lucide-react';
2
+ import type { ComponentType, ReactNode } from 'react';
3
+
4
+ import { cn } from '../cn';
5
+ import { Button } from '../primitives/button';
6
+ import { Card, CardContent } from '../primitives/card';
7
+
8
+ export type EmptyStateVariant = 'not-configured' | 'no-results' | 'disabled-context';
9
+
10
+ /**
11
+ * Visual density.
12
+ * - `comfortable` (default): hero empty state for full-page surfaces —
13
+ * big 64px icon, generous padding, hero copy. Use when the page has
14
+ * nothing else to render.
15
+ * - `dense`: inline empty state for surfaces nested inside other
16
+ * content (tabs, sub-panels). Smaller icon, tighter padding, no
17
+ * dashed card frame — reads as a calm placeholder, not a banner.
18
+ * - `inline`: single-line text-only message, no icon, no card. Use
19
+ * inside list sub-sections where even the dense frame is too much.
20
+ */
21
+ export type EmptyStateSize = 'comfortable' | 'dense' | 'inline';
22
+
23
+ /**
24
+ * Polymorphic link element. The host shell renders Next.js `<Link>`;
25
+ * pass it via `linkComponent` to get client-side routing. Defaults to a
26
+ * plain anchor so the primitive stays host-framework-agnostic.
27
+ */
28
+ export type EmptyStateLinkComponent = ComponentType<{
29
+ href: string;
30
+ children: ReactNode;
31
+ }>;
32
+
33
+ const DefaultLink: EmptyStateLinkComponent = ({ href, children }) => (
34
+ <a href={href}>{children}</a>
35
+ );
36
+
37
+ interface EmptyStateAction {
38
+ label: string;
39
+ onClick: () => void;
40
+ icon?: LucideIcon;
41
+ }
42
+
43
+ interface EmptyStateSecondaryAction {
44
+ label: string;
45
+ href: string;
46
+ }
47
+
48
+ interface EmptyStateProps {
49
+ icon: LucideIcon;
50
+ title: string;
51
+ description?: ReactNode;
52
+ /** Optional illustration or extra content above the title */
53
+ illustration?: ReactNode;
54
+ /** Visual variant — drives default iconBg/iconColor unless overridden. */
55
+ variant?: EmptyStateVariant;
56
+ size?: EmptyStateSize;
57
+ /** Icon tint — overrides variant defaults */
58
+ iconBg?: string;
59
+ iconColor?: string;
60
+ action?: EmptyStateAction;
61
+ secondaryAction?: EmptyStateSecondaryAction;
62
+ /** Host link component for `secondaryAction.href` (defaults to `<a>`). */
63
+ linkComponent?: EmptyStateLinkComponent;
64
+ className?: string;
65
+ }
66
+
67
+ const VARIANT_DEFAULTS: Record<EmptyStateVariant, { iconBg: string; iconColor: string }> = {
68
+ 'not-configured': { iconBg: 'bg-info/10', iconColor: 'text-info' },
69
+ 'no-results': { iconBg: 'bg-warning/10', iconColor: 'text-warning' },
70
+ 'disabled-context': { iconBg: 'bg-muted', iconColor: 'text-ink-3' },
71
+ };
72
+
73
+ export default function EmptyState({
74
+ icon: Icon,
75
+ title,
76
+ description,
77
+ illustration,
78
+ variant = 'not-configured',
79
+ size = 'comfortable',
80
+ iconBg,
81
+ iconColor,
82
+ action,
83
+ secondaryAction,
84
+ linkComponent,
85
+ className,
86
+ }: Readonly<EmptyStateProps>) {
87
+ const Link = linkComponent ?? DefaultLink;
88
+ const defaults = VARIANT_DEFAULTS[variant];
89
+ const resolvedIconBg = iconBg ?? defaults.iconBg;
90
+ const resolvedIconColor = iconColor ?? defaults.iconColor;
91
+
92
+ if (size === 'inline') {
93
+ return (
94
+ <div className={cn('flex items-center justify-center px-3 py-6 text-center text-body-2 text-ink-4', className)}>
95
+ <div className="max-w-sm space-y-1">
96
+ <p className="text-ink-3">{title}</p>
97
+ {description && <p className="text-ink-4">{description}</p>}
98
+ {(action || secondaryAction) && (
99
+ <div className="mt-3 flex items-center justify-center gap-2">
100
+ {action && (
101
+ <Button size="sm" variant="outline" onClick={action.onClick} className="h-7 gap-1.5 text-body-2">
102
+ {action.icon && <action.icon className="h-3.5 w-3.5" />}
103
+ {action.label}
104
+ </Button>
105
+ )}
106
+ {secondaryAction && (
107
+ <Button asChild size="sm" variant="ghost" className="h-7 px-2 text-body-2">
108
+ <Link href={secondaryAction.href}>{secondaryAction.label}</Link>
109
+ </Button>
110
+ )}
111
+ </div>
112
+ )}
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ if (size === 'dense') {
119
+ return (
120
+ <div
121
+ className={cn(
122
+ 'flex flex-col items-center justify-center px-4 py-8 text-center',
123
+ className,
124
+ )}
125
+ >
126
+ {illustration ?? (
127
+ <div className={cn('h-9 w-9 rounded-xl flex items-center justify-center mb-3', resolvedIconBg)}>
128
+ <Icon className={cn('h-4 w-4', resolvedIconColor)} />
129
+ </div>
130
+ )}
131
+ <p className="text-body-1 font-semibold text-ink">{title}</p>
132
+ {description && (
133
+ <p className="mt-1 max-w-md text-body-2 leading-snug text-ink-3">{description}</p>
134
+ )}
135
+ {(action || secondaryAction) && (
136
+ <div className="mt-3 flex items-center gap-2">
137
+ {action && (
138
+ <Button size="sm" variant="outline" onClick={action.onClick} className="h-7 gap-1.5 text-body-2">
139
+ {action.icon && <action.icon className="h-3.5 w-3.5" />}
140
+ {action.label}
141
+ </Button>
142
+ )}
143
+ {secondaryAction && (
144
+ <Button asChild size="sm" variant="ghost" className="h-7 px-2 text-body-2">
145
+ <Link href={secondaryAction.href}>{secondaryAction.label}</Link>
146
+ </Button>
147
+ )}
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ return (
155
+ <Card className={cn('border-2 border-dashed border-border/50 rounded-xl', className)}>
156
+ <CardContent className="flex flex-col items-center justify-center py-16 text-center">
157
+ {illustration ?? (
158
+ <div className={`h-16 w-16 rounded-2xl flex items-center justify-center mb-5 ${resolvedIconBg}`}>
159
+ <Icon className={`h-7 w-7 ${resolvedIconColor}`} />
160
+ </div>
161
+ )}
162
+ <p className="text-subtitle font-semibold text-ink">{title}</p>
163
+ {description && (
164
+ <p className="text-body-1 mt-2 max-w-sm text-ink-3 leading-relaxed">{description}</p>
165
+ )}
166
+ {(action || secondaryAction) && (
167
+ <div className="mt-6 flex items-center gap-3">
168
+ {action && (
169
+ <Button size="sm" variant="outline" onClick={action.onClick} className="gap-2 h-10 px-5">
170
+ {action.icon && <action.icon className="h-4 w-4" />}
171
+ {action.label}
172
+ </Button>
173
+ )}
174
+ {secondaryAction && (
175
+ <Button asChild size="sm" variant="ghost" className="h-10 px-3">
176
+ <Link href={secondaryAction.href}>{secondaryAction.label}</Link>
177
+ </Button>
178
+ )}
179
+ </div>
180
+ )}
181
+ </CardContent>
182
+ </Card>
183
+ );
184
+ }
@@ -0,0 +1,68 @@
1
+ import { AlertCircle, RefreshCw } from 'lucide-react';
2
+
3
+ import { Button } from '../primitives/button';
4
+ import { Card, CardContent } from '../primitives/card';
5
+
6
+ /**
7
+ * Host-agnostic error formatter. The host shell owns rich error decoding
8
+ * (Orval envelopes, workflow error codes, …); pass its formatter via
9
+ * `formatError` to preserve that behaviour. The default extracts a plain
10
+ * message and is safe in any consumer.
11
+ */
12
+ export type ErrorMessageFormatter = (error: unknown, fallback: string) => string;
13
+
14
+ const defaultFormatError: ErrorMessageFormatter = (error, fallback) => {
15
+ if (typeof error === 'string') {
16
+ return error || fallback;
17
+ }
18
+ if (error instanceof Error && error.message) {
19
+ return error.message;
20
+ }
21
+ return fallback;
22
+ };
23
+
24
+ interface ErrorCardProps {
25
+ error: Error | string | unknown;
26
+ title?: string;
27
+ onRetry?: () => void;
28
+ /**
29
+ * Optional host error-decoder. Defaults to a plain message extractor.
30
+ * The host shell should pass `getUserFacingErrorMessage` to keep its
31
+ * envelope-aware messaging.
32
+ */
33
+ formatError?: ErrorMessageFormatter;
34
+ fallbackMessage?: string;
35
+ }
36
+
37
+ export default function ErrorCard({
38
+ error,
39
+ title = 'Something went wrong',
40
+ onRetry,
41
+ formatError = defaultFormatError,
42
+ fallbackMessage = 'Unable to load this content right now.',
43
+ }: ErrorCardProps) {
44
+ const message = formatError(error, fallbackMessage);
45
+
46
+ return (
47
+ <Card className="border-destructive/25 bg-destructive/[0.03] rounded-xl">
48
+ <CardContent className="flex flex-col items-center py-14 text-center">
49
+ <div className="h-14 w-14 rounded-2xl bg-destructive/10 flex items-center justify-center mb-4">
50
+ <AlertCircle className="h-6 w-6 text-destructive" />
51
+ </div>
52
+ <p className="text-subtitle font-semibold text-ink">{title}</p>
53
+ <p className="text-body-1 text-ink-3 mt-2 max-w-md leading-relaxed">{message}</p>
54
+ {onRetry && (
55
+ <Button
56
+ size="sm"
57
+ variant="outline"
58
+ onClick={onRetry}
59
+ className="mt-5 gap-2 h-10 border-destructive/20 text-destructive hover:bg-destructive/5"
60
+ >
61
+ <RefreshCw className="h-4 w-4" />
62
+ Retry
63
+ </Button>
64
+ )}
65
+ </CardContent>
66
+ </Card>
67
+ );
68
+ }
@@ -0,0 +1,61 @@
1
+ import { Loader2 } from 'lucide-react';
2
+
3
+ import { cn } from '../cn';
4
+ import { Skeleton } from '../primitives/skeleton';
5
+
6
+ export type LoadingStateVariant = 'spinner' | 'skeleton-list' | 'skeleton-card';
7
+
8
+ interface LoadingStateProps {
9
+ /**
10
+ * `spinner` (default): centred spinner + optional label. `skeleton-list`:
11
+ * N skeleton rows. `skeleton-card`: a single card-shaped skeleton block.
12
+ */
13
+ variant?: LoadingStateVariant;
14
+ /** Accessible label / caption (spinner variant). */
15
+ label?: string;
16
+ /** Row count for `skeleton-list` (default 4). */
17
+ rows?: number;
18
+ className?: string;
19
+ }
20
+
21
+ /**
22
+ * Canonical loading affordance shared across biomes. Replaces the per-biome
23
+ * ad-hoc spinners and inline skeleton stacks.
24
+ */
25
+ export default function LoadingState({
26
+ variant = 'spinner',
27
+ label = 'Loading…',
28
+ rows = 4,
29
+ className,
30
+ }: Readonly<LoadingStateProps>) {
31
+ if (variant === 'skeleton-list') {
32
+ return (
33
+ <div className={cn('space-y-3', className)} role="status" aria-label={label}>
34
+ {Array.from({ length: rows }).map((_, i) => (
35
+ <Skeleton key={i} className="h-12 w-full rounded-xl" />
36
+ ))}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ if (variant === 'skeleton-card') {
42
+ return (
43
+ <div className={cn('space-y-3', className)} role="status" aria-label={label}>
44
+ <Skeleton className="h-40 w-full rounded-2xl" />
45
+ <Skeleton className="h-4 w-2/3 rounded" />
46
+ <Skeleton className="h-4 w-1/2 rounded" />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div
53
+ className={cn('flex flex-col items-center justify-center py-14 text-center', className)}
54
+ role="status"
55
+ aria-label={label}
56
+ >
57
+ <Loader2 className="h-6 w-6 animate-spin text-ink-3" />
58
+ {label && <p className="mt-3 text-body-2 text-ink-3">{label}</p>}
59
+ </div>
60
+ );
61
+ }