@xemahq/ui-kernel 0.9.1 → 0.9.2

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 (275) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/biome-host/biome-builders.d.ts.map +1 -1
  3. package/dist/lib/biome-host/biome-builders.js +3 -1
  4. package/dist/lib/biome-host/biome-builders.js.map +1 -1
  5. package/dist/lib/biome-host/biome-registry.d.ts.map +1 -1
  6. package/dist/lib/biome-host/biome-registry.js.map +1 -1
  7. package/dist/lib/biome-host/biome-scope-hooks.d.ts +11 -0
  8. package/dist/lib/biome-host/biome-scope-hooks.d.ts.map +1 -0
  9. package/dist/lib/biome-host/biome-scope-hooks.js +25 -0
  10. package/dist/lib/biome-host/biome-scope-hooks.js.map +1 -0
  11. package/dist/lib/biome-host/biome-scope.d.ts +1 -10
  12. package/dist/lib/biome-host/biome-scope.d.ts.map +1 -1
  13. package/dist/lib/biome-host/biome-scope.js +1 -24
  14. package/dist/lib/biome-host/biome-scope.js.map +1 -1
  15. package/dist/lib/biome-host/biome-scoped-query.d.ts +1 -1
  16. package/dist/lib/biome-host/biome-scoped-query.d.ts.map +1 -1
  17. package/dist/lib/biome-host/biome-scoped-query.js +1 -1
  18. package/dist/lib/biome-host/biome-scoped-query.js.map +1 -1
  19. package/dist/lib/biome-host/create-biome-orval-config.js +2 -2
  20. package/dist/lib/biome-host/create-biome-orval-config.js.map +1 -1
  21. package/dist/lib/biome-host/errors.js.map +1 -1
  22. package/dist/lib/biome-host/index.d.ts +1 -1
  23. package/dist/lib/biome-host/index.d.ts.map +1 -1
  24. package/dist/lib/biome-host/index.js +1 -1
  25. package/dist/lib/biome-host/index.js.map +1 -1
  26. package/dist/lib/biome-host/session-profiles.d.ts.map +1 -1
  27. package/dist/lib/biome-host/session-profiles.js +4 -1
  28. package/dist/lib/biome-host/session-profiles.js.map +1 -1
  29. package/dist/lib/capabilities/capability-context.d.ts +9 -0
  30. package/dist/lib/capabilities/capability-context.d.ts.map +1 -0
  31. package/dist/lib/capabilities/capability-context.js +13 -0
  32. package/dist/lib/capabilities/capability-context.js.map +1 -0
  33. package/dist/lib/capabilities/capability-provider.d.ts +2 -8
  34. package/dist/lib/capabilities/capability-provider.d.ts.map +1 -1
  35. package/dist/lib/capabilities/capability-provider.js +1 -12
  36. package/dist/lib/capabilities/capability-provider.js.map +1 -1
  37. package/dist/lib/capabilities/index.d.ts +1 -0
  38. package/dist/lib/capabilities/index.d.ts.map +1 -1
  39. package/dist/lib/capabilities/index.js +1 -0
  40. package/dist/lib/capabilities/index.js.map +1 -1
  41. package/dist/lib/capabilities/use-capability.d.ts.map +1 -1
  42. package/dist/lib/capabilities/use-capability.js +1 -1
  43. package/dist/lib/capabilities/use-capability.js.map +1 -1
  44. package/dist/lib/system-bus/deeplink.js.map +1 -1
  45. package/dist/lib/system-bus/host-ports.d.ts +2 -1
  46. package/dist/lib/system-bus/host-ports.d.ts.map +1 -1
  47. package/dist/lib/system-bus/intent-registry.d.ts.map +1 -1
  48. package/dist/lib/system-bus/intent-registry.js.map +1 -1
  49. package/dist/offline/index.d.ts +2 -1
  50. package/dist/offline/index.d.ts.map +1 -1
  51. package/dist/offline/index.js +2 -1
  52. package/dist/offline/index.js.map +1 -1
  53. package/dist/offline/offline-status-badge.js +1 -1
  54. package/dist/offline/offline-status-badge.js.map +1 -1
  55. package/dist/offline/outbox.js.map +1 -1
  56. package/dist/offline/react-hooks.d.ts +7 -0
  57. package/dist/offline/react-hooks.d.ts.map +1 -0
  58. package/dist/offline/react-hooks.js +25 -0
  59. package/dist/offline/react-hooks.js.map +1 -0
  60. package/dist/offline/react.d.ts +0 -4
  61. package/dist/offline/react.d.ts.map +1 -1
  62. package/dist/offline/react.js +2 -24
  63. package/dist/offline/react.js.map +1 -1
  64. package/dist/offline/storage.d.ts.map +1 -1
  65. package/dist/offline/storage.js +8 -5
  66. package/dist/offline/storage.js.map +1 -1
  67. package/dist/registry/index.d.ts +1 -0
  68. package/dist/registry/index.d.ts.map +1 -1
  69. package/dist/registry/index.js +1 -0
  70. package/dist/registry/index.js.map +1 -1
  71. package/dist/registry/lib/primitives/SessionEventCard.d.ts.map +1 -1
  72. package/dist/registry/lib/primitives/SessionEventCard.js +1 -1
  73. package/dist/registry/lib/primitives/SessionEventCard.js.map +1 -1
  74. package/dist/registry/lib/session-context-provider.d.ts +1 -3
  75. package/dist/registry/lib/session-context-provider.d.ts.map +1 -1
  76. package/dist/registry/lib/session-context-provider.js +2 -15
  77. package/dist/registry/lib/session-context-provider.js.map +1 -1
  78. package/dist/registry/lib/session-context.d.ts +5 -0
  79. package/dist/registry/lib/session-context.d.ts.map +1 -0
  80. package/dist/registry/lib/session-context.js +15 -0
  81. package/dist/registry/lib/session-context.js.map +1 -0
  82. package/dist/registry/lib/session-selectors.d.ts.map +1 -1
  83. package/dist/registry/lib/session-selectors.js +3 -1
  84. package/dist/registry/lib/session-selectors.js.map +1 -1
  85. package/dist/ui/branding/app-branding-context.d.ts +9 -0
  86. package/dist/ui/branding/app-branding-context.d.ts.map +1 -0
  87. package/dist/ui/branding/app-branding-context.js +10 -0
  88. package/dist/ui/branding/app-branding-context.js.map +1 -0
  89. package/dist/ui/branding/app-branding-provider.d.ts +0 -6
  90. package/dist/ui/branding/app-branding-provider.d.ts.map +1 -1
  91. package/dist/ui/branding/app-branding-provider.js +1 -8
  92. package/dist/ui/branding/app-branding-provider.js.map +1 -1
  93. package/dist/ui/capture/schema-field-input.js +1 -1
  94. package/dist/ui/capture/schema-field-input.js.map +1 -1
  95. package/dist/ui/capture/use-prefill.d.ts.map +1 -1
  96. package/dist/ui/capture/use-prefill.js +3 -1
  97. package/dist/ui/capture/use-prefill.js.map +1 -1
  98. package/dist/ui/capture/use-wizard.d.ts +1 -1
  99. package/dist/ui/capture/use-wizard.d.ts.map +1 -1
  100. package/dist/ui/capture/use-wizard.js +1 -1
  101. package/dist/ui/capture/use-wizard.js.map +1 -1
  102. package/dist/ui/capture/wizard.d.ts.map +1 -1
  103. package/dist/ui/capture/wizard.js.map +1 -1
  104. package/dist/ui/index.d.ts +5 -1
  105. package/dist/ui/index.d.ts.map +1 -1
  106. package/dist/ui/index.js +5 -1
  107. package/dist/ui/index.js.map +1 -1
  108. package/dist/ui/primitives/alert-dialog.js +1 -1
  109. package/dist/ui/primitives/alert-dialog.js.map +1 -1
  110. package/dist/ui/primitives/badge-variants.d.ts +4 -0
  111. package/dist/ui/primitives/badge-variants.d.ts.map +1 -0
  112. package/dist/ui/primitives/badge-variants.js +15 -0
  113. package/dist/ui/primitives/badge-variants.js.map +1 -0
  114. package/dist/ui/primitives/badge.d.ts +3 -5
  115. package/dist/ui/primitives/badge.d.ts.map +1 -1
  116. package/dist/ui/primitives/badge.js +2 -15
  117. package/dist/ui/primitives/badge.js.map +1 -1
  118. package/dist/ui/primitives/button-variants.d.ts +5 -0
  119. package/dist/ui/primitives/button-variants.d.ts.map +1 -0
  120. package/dist/ui/primitives/button-variants.js +24 -0
  121. package/dist/ui/primitives/button-variants.js.map +1 -0
  122. package/dist/ui/primitives/button.d.ts +3 -6
  123. package/dist/ui/primitives/button.d.ts.map +1 -1
  124. package/dist/ui/primitives/button.js +2 -24
  125. package/dist/ui/primitives/button.js.map +1 -1
  126. package/dist/ui/primitives/form-stepper.d.ts +1 -13
  127. package/dist/ui/primitives/form-stepper.d.ts.map +1 -1
  128. package/dist/ui/primitives/form-stepper.js +1 -6
  129. package/dist/ui/primitives/form-stepper.js.map +1 -1
  130. package/dist/ui/primitives/form-stepper.types.d.ts +14 -0
  131. package/dist/ui/primitives/form-stepper.types.d.ts.map +1 -0
  132. package/dist/ui/primitives/form-stepper.types.js +7 -0
  133. package/dist/ui/primitives/form-stepper.types.js.map +1 -0
  134. package/dist/ui/primitives/preview-device-frame.js +1 -1
  135. package/dist/ui/primitives/preview-device-frame.js.map +1 -1
  136. package/dist/ui/primitives/tag-multi-select.d.ts.map +1 -1
  137. package/dist/ui/primitives/tag-multi-select.js +1 -1
  138. package/dist/ui/primitives/tag-multi-select.js.map +1 -1
  139. package/dist/vista/index.d.ts +4 -0
  140. package/dist/vista/index.d.ts.map +1 -1
  141. package/dist/vista/index.js +4 -0
  142. package/dist/vista/index.js.map +1 -1
  143. package/dist/vista/surfaces/FilesSurface.d.ts +1 -12
  144. package/dist/vista/surfaces/FilesSurface.d.ts.map +1 -1
  145. package/dist/vista/surfaces/FilesSurface.js +2 -46
  146. package/dist/vista/surfaces/FilesSurface.js.map +1 -1
  147. package/dist/vista/surfaces/FormSurface.d.ts.map +1 -1
  148. package/dist/vista/surfaces/FormSurface.js +39 -12
  149. package/dist/vista/surfaces/FormSurface.js.map +1 -1
  150. package/dist/vista/surfaces/elicitation-transport.d.ts +12 -3
  151. package/dist/vista/surfaces/elicitation-transport.d.ts.map +1 -1
  152. package/dist/vista/surfaces/elicitation-transport.js +96 -3
  153. package/dist/vista/surfaces/elicitation-transport.js.map +1 -1
  154. package/dist/vista/surfaces/file-surface-utils.d.ts +13 -0
  155. package/dist/vista/surfaces/file-surface-utils.d.ts.map +1 -0
  156. package/dist/vista/surfaces/file-surface-utils.js +46 -0
  157. package/dist/vista/surfaces/file-surface-utils.js.map +1 -0
  158. package/dist/vista/surfaces/index.d.ts +4 -3
  159. package/dist/vista/surfaces/index.d.ts.map +1 -1
  160. package/dist/vista/surfaces/index.js +4 -81
  161. package/dist/vista/surfaces/index.js.map +1 -1
  162. package/dist/vista/surfaces/register-surfaces.d.ts +2 -0
  163. package/dist/vista/surfaces/register-surfaces.d.ts.map +1 -0
  164. package/dist/vista/surfaces/register-surfaces.js +80 -0
  165. package/dist/vista/surfaces/register-surfaces.js.map +1 -0
  166. package/dist/vista/surfaces/text-surfaces.js +1 -1
  167. package/dist/vista/surfaces/text-surfaces.js.map +1 -1
  168. package/dist/vista/vista-code-renderer-context.d.ts +9 -0
  169. package/dist/vista/vista-code-renderer-context.d.ts.map +1 -0
  170. package/dist/vista/vista-code-renderer-context.js +6 -0
  171. package/dist/vista/vista-code-renderer-context.js.map +1 -0
  172. package/dist/vista/vista-code-renderer.d.ts +1 -6
  173. package/dist/vista/vista-code-renderer.d.ts.map +1 -1
  174. package/dist/vista/vista-code-renderer.js +1 -5
  175. package/dist/vista/vista-code-renderer.js.map +1 -1
  176. package/dist/vista/vista-file-context.d.ts +8 -0
  177. package/dist/vista/vista-file-context.d.ts.map +1 -0
  178. package/dist/vista/vista-file-context.js +6 -0
  179. package/dist/vista/vista-file-context.js.map +1 -0
  180. package/dist/vista/vista-file-source.d.ts +1 -6
  181. package/dist/vista/vista-file-source.d.ts.map +1 -1
  182. package/dist/vista/vista-file-source.js +2 -5
  183. package/dist/vista/vista-file-source.js.map +1 -1
  184. package/dist/vista/vista-form-context.d.ts +13 -0
  185. package/dist/vista/vista-form-context.d.ts.map +1 -0
  186. package/dist/vista/vista-form-context.js +6 -0
  187. package/dist/vista/vista-form-context.js.map +1 -0
  188. package/dist/vista/vista-form-host.d.ts +1 -10
  189. package/dist/vista/vista-form-host.d.ts.map +1 -1
  190. package/dist/vista/vista-form-host.js +1 -5
  191. package/dist/vista/vista-form-host.js.map +1 -1
  192. package/dist/vista/vista-open-context.d.ts +1 -3
  193. package/dist/vista/vista-open-context.d.ts.map +1 -1
  194. package/dist/vista/vista-open-context.js +1 -5
  195. package/dist/vista/vista-open-context.js.map +1 -1
  196. package/dist/vista/vista-open-state.d.ts +5 -0
  197. package/dist/vista/vista-open-state.d.ts.map +1 -0
  198. package/dist/vista/vista-open-state.js +6 -0
  199. package/dist/vista/vista-open-state.js.map +1 -0
  200. package/dist/vista/vista-payloads.d.ts +2 -3
  201. package/dist/vista/vista-payloads.d.ts.map +1 -1
  202. package/dist/vista/vista-payloads.js +18 -21
  203. package/dist/vista/vista-payloads.js.map +1 -1
  204. package/dist/vista/vista-tools.d.ts.map +1 -1
  205. package/dist/vista/vista-tools.js.map +1 -1
  206. package/package.json +3 -3
  207. package/src/lib/biome-host/biome-builders.ts +4 -1
  208. package/src/lib/biome-host/biome-registry.ts +0 -1
  209. package/src/lib/biome-host/biome-scope-hooks.ts +39 -0
  210. package/src/lib/biome-host/biome-scope.tsx +1 -68
  211. package/src/lib/biome-host/biome-scoped-query.ts +4 -1
  212. package/src/lib/biome-host/create-biome-orval-config.ts +2 -2
  213. package/src/lib/biome-host/errors.ts +1 -1
  214. package/src/lib/biome-host/index.ts +1 -1
  215. package/src/lib/biome-host/session-profiles.ts +7 -1
  216. package/src/lib/capabilities/capability-context.ts +25 -0
  217. package/src/lib/capabilities/capability-provider.tsx +5 -34
  218. package/src/lib/capabilities/index.ts +1 -0
  219. package/src/lib/capabilities/use-capability.ts +4 -1
  220. package/src/lib/system-bus/deeplink.ts +1 -1
  221. package/src/lib/system-bus/host-ports.ts +2 -1
  222. package/src/lib/system-bus/intent-registry.ts +1 -2
  223. package/src/offline/index.ts +4 -2
  224. package/src/offline/offline-status-badge.tsx +1 -1
  225. package/src/offline/outbox.test.ts +23 -16
  226. package/src/offline/outbox.ts +1 -1
  227. package/src/offline/react-hooks.ts +37 -0
  228. package/src/offline/react.tsx +1 -40
  229. package/src/offline/storage.ts +11 -6
  230. package/src/registry/index.ts +1 -0
  231. package/src/registry/lib/primitives/SessionEventCard.tsx +4 -2
  232. package/src/registry/lib/session-context-provider.tsx +8 -25
  233. package/src/registry/lib/session-context.ts +21 -0
  234. package/src/registry/lib/session-selectors.ts +3 -1
  235. package/src/ui/branding/app-branding-context.ts +20 -0
  236. package/src/ui/branding/app-branding-provider.tsx +5 -27
  237. package/src/ui/capture/schema-field-input.tsx +7 -1
  238. package/src/ui/capture/use-prefill.ts +4 -1
  239. package/src/ui/capture/use-wizard.ts +2 -2
  240. package/src/ui/capture/wizard.tsx +2 -1
  241. package/src/ui/index.ts +7 -2
  242. package/src/ui/primitives/alert-dialog.tsx +1 -1
  243. package/src/ui/primitives/badge-variants.ts +21 -0
  244. package/src/ui/primitives/badge.tsx +3 -19
  245. package/src/ui/primitives/button-variants.ts +31 -0
  246. package/src/ui/primitives/button.tsx +3 -28
  247. package/src/ui/primitives/form-stepper.tsx +1 -16
  248. package/src/ui/primitives/form-stepper.types.ts +15 -0
  249. package/src/ui/primitives/preview-device-frame.tsx +1 -1
  250. package/src/ui/primitives/tag-multi-select.tsx +3 -4
  251. package/src/vista/form-payload.spec.ts +71 -25
  252. package/src/vista/index.ts +4 -0
  253. package/src/vista/surfaces/FilesSurface.tsx +2 -68
  254. package/src/vista/surfaces/FormSurface.tsx +125 -26
  255. package/src/vista/surfaces/elicitation-transport.spec.ts +205 -23
  256. package/src/vista/surfaces/elicitation-transport.ts +192 -14
  257. package/src/vista/surfaces/file-surface-utils.ts +62 -0
  258. package/src/vista/surfaces/index.ts +24 -0
  259. package/src/vista/surfaces/{index.tsx → register-surfaces.tsx} +1 -21
  260. package/src/vista/surfaces/text-surfaces.tsx +1 -1
  261. package/src/vista/vista-code-renderer-context.ts +16 -0
  262. package/src/vista/vista-code-renderer.tsx +4 -16
  263. package/src/vista/vista-file-context.ts +14 -0
  264. package/src/vista/vista-file-source.tsx +5 -28
  265. package/src/vista/vista-form-context.ts +20 -0
  266. package/src/vista/vista-form-host.tsx +11 -36
  267. package/src/vista/vista-open-context.tsx +4 -14
  268. package/src/vista/vista-open-state.ts +11 -0
  269. package/src/vista/vista-payloads.ts +44 -46
  270. package/src/vista/vista-tools.ts +2 -2
  271. package/dist/lib/biome-host/response-envelope.d.ts +0 -3
  272. package/dist/lib/biome-host/response-envelope.d.ts.map +0 -1
  273. package/dist/lib/biome-host/response-envelope.js +0 -21
  274. package/dist/lib/biome-host/response-envelope.js.map +0 -1
  275. package/src/lib/biome-host/response-envelope.ts +0 -69
@@ -1,34 +1,9 @@
1
1
  import { Slot } from "@radix-ui/react-slot";
2
- import { cva, type VariantProps } from "class-variance-authority";
2
+ import type { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
 
5
5
  import { cn } from '../cn';
6
-
7
- const buttonVariants = cva(
8
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-body-1 font-medium ring-offset-background transition-[background-color,color,border-color,box-shadow] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
- {
10
- variants: {
11
- variant: {
12
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14
- outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
15
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
16
- ghost: "text-ink-2 hover:bg-paper-elev hover:text-ink",
17
- link: "text-primary underline-offset-4 hover:underline",
18
- },
19
- size: {
20
- default: "h-10 px-4 py-2",
21
- sm: "h-9 rounded-md px-3",
22
- lg: "h-11 rounded-md px-8",
23
- icon: "h-10 w-10",
24
- },
25
- },
26
- defaultVariants: {
27
- variant: "default",
28
- size: "default",
29
- },
30
- },
31
- );
6
+ import { buttonVariants } from './button-variants';
32
7
 
33
8
  export interface ButtonProps
34
9
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
@@ -44,4 +19,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
19
  );
45
20
  Button.displayName = "Button";
46
21
 
47
- export { Button, buttonVariants };
22
+ export { Button };
@@ -1,22 +1,7 @@
1
1
  import { Check } from 'lucide-react';
2
2
 
3
- import type { ReactNode } from 'react';
4
-
5
3
  import { cn } from '../cn';
6
-
7
- export enum FormStepStatus {
8
- PENDING = 'pending',
9
- ACTIVE = 'active',
10
- DONE = 'done',
11
- }
12
-
13
- export interface FormStep {
14
- readonly key: string;
15
- readonly label: string;
16
- readonly hint?: string;
17
- readonly status: FormStepStatus;
18
- readonly children: ReactNode;
19
- }
4
+ import { FormStepStatus, type FormStep } from './form-stepper.types';
20
5
 
21
6
  interface FormStepperProps {
22
7
  readonly steps: readonly FormStep[];
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export enum FormStepStatus {
4
+ PENDING = 'pending',
5
+ ACTIVE = 'active',
6
+ DONE = 'done',
7
+ }
8
+
9
+ export interface FormStep {
10
+ readonly key: string;
11
+ readonly label: string;
12
+ readonly hint?: string;
13
+ readonly status: FormStepStatus;
14
+ readonly children: ReactNode;
15
+ }
@@ -181,7 +181,7 @@ function ResponsiveViewport({
181
181
  const observer = new ResizeObserver(compute);
182
182
  observer.observe(el);
183
183
  return () => observer.disconnect();
184
- }, [bezel.width, bezel.height]);
184
+ }, [aspectRatio, bezel.width, bezel.height]);
185
185
 
186
186
  return (
187
187
  <div
@@ -138,9 +138,8 @@ export function TagMultiSelect({
138
138
  className={cn('text-body-1 gap-1 pr-1', chipClass)}
139
139
  >
140
140
  <span className="truncate max-w-[160px]">{opt?.label ?? val}</span>
141
- <span
142
- role="button"
143
- tabIndex={-1}
141
+ <button
142
+ type="button"
144
143
  onClick={(e) => remove(val, e)}
145
144
  onKeyDown={(e) => {
146
145
  if (e.key === 'Enter' || e.key === ' ') {
@@ -153,7 +152,7 @@ export function TagMultiSelect({
153
152
  aria-label={`Remove ${opt?.label ?? val}`}
154
153
  >
155
154
  <X className="h-4 w-4" />
156
- </span>
155
+ </button>
157
156
  </Badge>
158
157
  );
159
158
  })}
@@ -2,22 +2,17 @@
2
2
  * Guards `validateFormPayload` — the structural half of "the value never
3
3
  * reaches the agent".
4
4
  *
5
- * The validator is what makes the submit target un-aimable at the host's own
6
- * origin (where the BFF / chat transport lives): only an ABSOLUTE `http(s)`
7
- * URL is accepted, and a rejected payload draws the typed fallback card rather
8
- * than a form.
5
+ * The payload has no network authority: it carries only a base64url opaque ref,
6
+ * while the trusted service base URL comes from `VistaFormHost`.
9
7
  */
10
8
  import { describe, expect, it } from 'vitest';
11
9
 
12
- import { CaptureFieldType } from '../ui/capture/field-spec';
13
-
14
10
  import { validateFormPayload } from './vista-payloads';
11
+ import { CaptureFieldType } from '../ui/capture/field-spec';
15
12
 
16
13
  const BASE = {
17
14
  ref: 'ref-1',
18
15
  purpose: 'Connect Acme so I can read your tickets.',
19
- submitUrl: 'https://connector-gateway.example.test/credential-elicitations/ref-1/submit',
20
- statusUrl: 'https://connector-gateway.example.test/credential-elicitations/ref-1',
21
16
  fields: [
22
17
  {
23
18
  key: 'apiKey',
@@ -27,7 +22,13 @@ const BASE = {
27
22
  isSecret: true,
28
23
  transform: 'trim',
29
24
  },
30
- { key: 'baseUrl', label: 'Base URL', type: CaptureFieldType.Url, required: true, isSecret: false },
25
+ {
26
+ key: 'baseUrl',
27
+ label: 'Base URL',
28
+ type: CaptureFieldType.Url,
29
+ required: true,
30
+ isSecret: false,
31
+ },
31
32
  ],
32
33
  };
33
34
 
@@ -41,11 +42,17 @@ describe('validateFormPayload', () => {
41
42
  expect(payload?.fields[0]?.transform).toBe('trim');
42
43
  });
43
44
 
44
- it('REJECTS a host-relative submit URL — it could only hit the app itself', () => {
45
- expect(validateFormPayload({ ...BASE, submitUrl: '/credential-elicitations/ref-1/submit' })).toBeNull();
46
- expect(validateFormPayload({ ...BASE, submitUrl: '//evil.example/x' })).toBeNull();
47
- expect(validateFormPayload({ ...BASE, submitUrl: 'javascript:alert(1)' })).toBeNull();
48
- expect(validateFormPayload({ ...BASE, statusUrl: '/credential-elicitations/ref-1' })).toBeNull();
45
+ it('rejects old or malicious payload-selected submit/status authorities', () => {
46
+ for (const target of [
47
+ 'https://evil.example/steal',
48
+ 'https://trusted.example@evil.example/steal',
49
+ '//evil.example/steal',
50
+ '/credential-elicitations/ref-1/submit',
51
+ 'javascript:alert(1)',
52
+ ]) {
53
+ expect(validateFormPayload({ ...BASE, submitUrl: target })).toBeNull();
54
+ expect(validateFormPayload({ ...BASE, statusUrl: target })).toBeNull();
55
+ }
49
56
  });
50
57
 
51
58
  it('accepts an EMPTY field schema — the service is the authority, not the client', () => {
@@ -55,39 +62,78 @@ describe('validateFormPayload', () => {
55
62
  });
56
63
 
57
64
  it('rejects a malformed field spec rather than half-rendering it', () => {
58
- expect(validateFormPayload({ ...BASE, fields: [{ key: 'a', label: 'A', type: 'nope' }] })).toBeNull();
59
- expect(validateFormPayload({ ...BASE, fields: [{ key: 'a', label: 'A' }] })).toBeNull();
60
65
  expect(
61
66
  validateFormPayload({
62
67
  ...BASE,
63
- fields: [{ key: 'a', label: 'A', type: CaptureFieldType.String, isSecret: 'yes' }],
68
+ fields: [{ key: 'a', label: 'A', type: 'nope' }],
69
+ }),
70
+ ).toBeNull();
71
+ expect(
72
+ validateFormPayload({ ...BASE, fields: [{ key: 'a', label: 'A' }] }),
73
+ ).toBeNull();
74
+ expect(
75
+ validateFormPayload({
76
+ ...BASE,
77
+ fields: [
78
+ {
79
+ key: 'a',
80
+ label: 'A',
81
+ type: CaptureFieldType.String,
82
+ isSecret: 'yes',
83
+ },
84
+ ],
64
85
  }),
65
86
  ).toBeNull();
66
87
  expect(validateFormPayload({ ...BASE, fields: {} })).toBeNull();
67
88
  });
68
89
 
69
- it('accepts a launch (oauth2 / app_install) payload with an absolute URL', () => {
90
+ it('accepts a launch payload with a root-relative trusted-service path', () => {
70
91
  const payload = validateFormPayload({
71
92
  ...BASE,
72
93
  fields: [],
73
- launch: { url: 'https://acme.example.test/oauth/authorize?x=1', label: 'Continue to Acme', hint: 'h' },
94
+ launch: {
95
+ path: '/oauth/authorize?x=1',
96
+ label: 'Continue to Acme',
97
+ hint: 'h',
98
+ },
74
99
  });
75
100
  expect(payload?.launch).toEqual({
76
- url: 'https://acme.example.test/oauth/authorize?x=1',
101
+ path: '/oauth/authorize?x=1',
77
102
  label: 'Continue to Acme',
78
103
  hint: 'h',
79
104
  });
80
105
  });
81
106
 
82
- it('rejects a launch with a relative URL or no label', () => {
83
- expect(validateFormPayload({ ...BASE, launch: { url: '/authorize', label: 'Go' } })).toBeNull();
107
+ it('rejects launch authority/scheme confusion and malformed paths', () => {
108
+ for (const path of [
109
+ 'authorize',
110
+ '//evil.example/authorize',
111
+ 'https://evil.example/authorize',
112
+ 'javascript:alert(1)',
113
+ '\\\\evil.example\\authorize',
114
+ ]) {
115
+ expect(
116
+ validateFormPayload({ ...BASE, launch: { path, label: 'Go' } }),
117
+ ).toBeNull();
118
+ }
119
+ expect(
120
+ validateFormPayload({
121
+ ...BASE,
122
+ launch: { url: 'https://evil.example/a', label: 'Go' },
123
+ }),
124
+ ).toBeNull();
84
125
  expect(
85
- validateFormPayload({ ...BASE, launch: { url: 'https://acme.example.test/a', label: '' } }),
126
+ validateFormPayload({
127
+ ...BASE,
128
+ launch: { path: '/authorize', label: '' },
129
+ }),
86
130
  ).toBeNull();
87
131
  });
88
132
 
89
- it('rejects a missing ref', () => {
90
- expect(validateFormPayload({ ...BASE, ref: '' })).toBeNull();
133
+ it('rejects missing refs and path-traversal/encoding input', () => {
134
+ for (const ref of ['', '.', '..', '../steal', 'a/b', '%2e%2e', 'réf']) {
135
+ expect(validateFormPayload({ ...BASE, ref })).toBeNull();
136
+ }
91
137
  expect(validateFormPayload({ ...BASE, ref: undefined })).toBeNull();
92
138
  });
93
139
  });
@@ -36,9 +36,13 @@ export * from './vista-model';
36
36
  export * from './vista-payloads';
37
37
  export * from './vista-tabs-manifest';
38
38
  export * from './vista-file-source';
39
+ export * from './vista-file-context';
39
40
  export * from './vista-form-host';
41
+ export * from './vista-form-context';
40
42
  export * from './vista-code-renderer';
43
+ export * from './vista-code-renderer-context';
41
44
  export * from './vista-open-context';
45
+ export * from './vista-open-state';
42
46
  export * from './use-vista-tabs';
43
47
  export * from './VistaPanel';
44
48
  export * from './vista-tools';
@@ -13,78 +13,12 @@ import { ChevronRight, Folder } from 'lucide-react';
13
13
 
14
14
 
15
15
  import { fileSurface } from '../vista-model';
16
- import { useVistaOpen } from '../vista-open-context';
16
+ import { useVistaOpen } from '../vista-open-state';
17
+ import { fileExtMeta, groupFilesByFolder } from './file-surface-utils';
17
18
 
18
19
  import type { VistaFileEntry, VistaFilesPayload } from '../vista-payloads';
19
20
  import type { ReactElement } from 'react';
20
21
 
21
- /** Extension → colour-coded tile metadata. */
22
- interface FileExtMeta {
23
- readonly color: string;
24
- readonly tag: string;
25
- }
26
-
27
- const EXT_META: Readonly<Record<string, FileExtMeta>> = {
28
- ts: { color: 'hsl(var(--info))', tag: 'TS' },
29
- tsx: { color: 'hsl(var(--info))', tag: 'TSX' },
30
- js: { color: 'hsl(var(--warning))', tag: 'JS' },
31
- jsx: { color: 'hsl(var(--warning))', tag: 'JSX' },
32
- json: { color: 'hsl(var(--ink-3))', tag: 'JSON' },
33
- md: { color: 'hsl(var(--p-people))', tag: 'MD' },
34
- css: { color: 'hsl(var(--p-sales))', tag: 'CSS' },
35
- html: { color: 'hsl(var(--p-sales))', tag: 'HTML' },
36
- yaml: { color: 'hsl(var(--ink-3))', tag: 'YAML' },
37
- yml: { color: 'hsl(var(--ink-3))', tag: 'YAML' },
38
- };
39
-
40
- /** Resolve the colour-coded tile metadata for a filename. */
41
- export function fileExtMeta(name: string): FileExtMeta {
42
- if (/\.test\.[tj]sx?$/.test(name) || /\.spec\.[tj]sx?$/.test(name)) {
43
- return { color: 'hsl(var(--success))', tag: 'TEST' };
44
- }
45
- const ext = name.split('.').pop() ?? '';
46
- return (
47
- EXT_META[ext] ?? {
48
- color: 'hsl(var(--ink-4))',
49
- tag: (ext || 'FILE').toUpperCase().slice(0, 4),
50
- }
51
- );
52
- }
53
-
54
- /** A group of files under one folder caption. */
55
- export interface VistaFileGroup {
56
- /** Folder path (`''` = root). */
57
- readonly dir: string;
58
- readonly files: readonly VistaFileEntry[];
59
- }
60
-
61
- /** Order folder captions: root first, then alphabetical. */
62
- function compareDirs(a: string, b: string): number {
63
- if (a === '') return -1;
64
- if (b === '') return 1;
65
- return a.localeCompare(b);
66
- }
67
-
68
- /** Group a flat file list by folder, root group first, paths sorted. */
69
- export function groupFilesByFolder(
70
- files: readonly VistaFileEntry[],
71
- ): VistaFileGroup[] {
72
- const groups = new Map<string, VistaFileEntry[]>();
73
- for (const file of files) {
74
- const slash = file.path.lastIndexOf('/');
75
- const dir = slash < 0 ? '' : file.path.slice(0, slash);
76
- const bucket = groups.get(dir);
77
- if (bucket) bucket.push(file);
78
- else groups.set(dir, [file]);
79
- }
80
- return [...groups.keys()].sort(compareDirs).map((dir) => ({
81
- dir,
82
- files: [...(groups.get(dir) ?? [])].sort((a, b) =>
83
- a.path.localeCompare(b.path),
84
- ),
85
- }));
86
- }
87
-
88
22
  export function FilesSurface({
89
23
  files,
90
24
  }: Readonly<VistaFilesPayload>): ReactElement {
@@ -13,25 +13,45 @@
13
13
  * • the values live in this component's `useState`; the single reader is
14
14
  * `submitElicitationFields`, which takes them as a parameter and returns an
15
15
  * outcome — the values are not reachable from any return or any throw;
16
- * • the POST target is an ABSOLUTE service URL (`validateFormPayload` rejects
17
- * a relative one), so the request cannot be aimed at the host's own BFF or
18
- * the chat transport;
16
+ * • the payload has NO URL authority. The exact endpoints are derived from
17
+ * the trusted elicitation API base URL supplied by `VistaFormHost`;
19
18
  * • secret fields are DECLARED (`spec.isSecret`) and render write-only; the
20
19
  * surface holds no cache and no read path repopulates them.
21
20
  *
22
21
  * Nothing here is provider-aware. Whether the flow is a field form or an OAuth
23
22
  * handoff is decided by the payload the owning service produced.
24
23
  */
25
- import { AlertTriangle, CheckCircle2, ExternalLink, Loader2, ShieldCheck } from 'lucide-react';
26
- import { useCallback, useEffect, useState, type ReactElement, type ReactNode } from 'react';
24
+ import {
25
+ AlertTriangle,
26
+ CheckCircle2,
27
+ ExternalLink,
28
+ Loader2,
29
+ ShieldCheck,
30
+ } from 'lucide-react';
31
+ import {
32
+ useCallback,
33
+ useEffect,
34
+ useState,
35
+ type ReactElement,
36
+ type ReactNode,
37
+ } from 'react';
27
38
 
28
- import { readElicitationStatus, submitElicitationFields, VistaFormSubmitError } from './elicitation-transport';
39
+ import {
40
+ readElicitationStatus,
41
+ resolveTrustedElicitationEndpoints,
42
+ resolveTrustedElicitationLaunchUrl,
43
+ submitElicitationFields,
44
+ VistaFormSubmitError,
45
+ } from './elicitation-transport';
29
46
  import { SchemaForm } from '../../ui/capture/schema-form';
30
47
  import { captureValuesToStringMap } from '../../ui/capture/submit-values';
31
48
  import { useSchemaValidation } from '../../ui/capture/use-schema-validation';
32
49
  import { cn } from '../../ui/cn';
33
50
  import { Button } from '../../ui/primitives/button';
34
- import { useVistaFormHost, type VistaFormOutcome } from '../vista-form-host';
51
+ import {
52
+ useVistaFormHost,
53
+ type VistaFormOutcome,
54
+ } from '../vista-form-context';
35
55
 
36
56
  import type { CaptureValues } from '../../ui/capture/field-spec';
37
57
  import type { VistaFormPayload } from '../vista-payloads';
@@ -51,6 +71,29 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
51
71
  const [showFieldErrors, setShowFieldErrors] = useState(false);
52
72
  const [outcome, setOutcome] = useState<VistaFormOutcome | null>(null);
53
73
  const { errors, isValid } = useSchemaValidation(payload.fields, values);
74
+ let transportConfigurationError: string | null = null;
75
+ let launchUrl: string | undefined;
76
+ if (host !== null) {
77
+ try {
78
+ // Validate the trust anchor before retrieving a bearer token or accepting
79
+ // a submit action. The transport repeats this check at the fetch boundary.
80
+ resolveTrustedElicitationEndpoints(
81
+ host.elicitationApiBaseUrl,
82
+ payload.ref,
83
+ );
84
+ if (payload.launch !== undefined) {
85
+ launchUrl = resolveTrustedElicitationLaunchUrl(
86
+ host.elicitationApiBaseUrl,
87
+ payload.launch.path,
88
+ );
89
+ }
90
+ } catch (error) {
91
+ transportConfigurationError =
92
+ error instanceof Error
93
+ ? error.message
94
+ : 'The trusted elicitation service is not configured safely.';
95
+ }
96
+ }
54
97
 
55
98
  const settle = useCallback(
56
99
  (next: VistaFormOutcome): void => {
@@ -85,14 +128,14 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
85
128
  .getAccessToken()
86
129
  .then((accessToken) =>
87
130
  submitElicitationFields({
88
- submitUrl: payload.submitUrl,
131
+ apiBaseUrl: host.elicitationApiBaseUrl,
89
132
  ref: payload.ref,
90
133
  accessToken,
91
134
  fields: captureValuesToStringMap(payload.fields, values),
92
135
  }),
93
136
  )
94
137
  .then(settle, failed);
95
- }, [host, isValid, payload.fields, payload.ref, payload.submitUrl, settle, failed, values]);
138
+ }, [host, isValid, payload.fields, payload.ref, settle, failed, values]);
96
139
 
97
140
  const handleCheckStatus = useCallback((): void => {
98
141
  if (host === null) return;
@@ -101,17 +144,23 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
101
144
  void host
102
145
  .getAccessToken()
103
146
  .then((accessToken) =>
104
- readElicitationStatus({ statusUrl: payload.statusUrl, ref: payload.ref, accessToken }),
147
+ readElicitationStatus({
148
+ apiBaseUrl: host.elicitationApiBaseUrl,
149
+ ref: payload.ref,
150
+ accessToken,
151
+ }),
105
152
  )
106
153
  .then((next) => {
107
154
  if (next === null) {
108
- setServiceError('Not completed yet — finish the steps in the other window, then check again.');
155
+ setServiceError(
156
+ 'Not completed yet — finish the steps in the other window, then check again.',
157
+ );
109
158
  setPhase(FormPhase.Editing);
110
159
  return;
111
160
  }
112
161
  settle(next);
113
162
  }, failed);
114
- }, [host, payload.ref, payload.statusUrl, settle, failed]);
163
+ }, [host, payload.ref, settle, failed]);
115
164
 
116
165
  // The consent/install window is a separate browsing context, so the only
117
166
  // signal this tab gets is the user returning to it. Re-checking on focus is
@@ -137,6 +186,19 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
137
186
  );
138
187
  }
139
188
 
189
+ if (transportConfigurationError !== null) {
190
+ return (
191
+ <Shell heading={payload.heading} purpose={payload.purpose}>
192
+ <Notice
193
+ tone="error"
194
+ title="The secure service is not configured safely"
195
+ >
196
+ {transportConfigurationError}
197
+ </Notice>
198
+ </Shell>
199
+ );
200
+ }
201
+
140
202
  if (phase === FormPhase.Settled && outcome !== null) {
141
203
  return (
142
204
  <Shell heading={payload.heading} purpose={payload.purpose}>
@@ -146,7 +208,10 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
146
208
  <p className="text-body-1 font-medium text-ink">Submitted.</p>
147
209
  <p className="text-caption text-ink-3">
148
210
  The value went straight to the service and was never shown to the
149
- agent{outcome.connectionId === null ? '.' : `. Connection ${outcome.connectionId} is ready.`}
211
+ agent
212
+ {outcome.connectionId === null
213
+ ? '.'
214
+ : `. Connection ${outcome.connectionId} is ready.`}
150
215
  </p>
151
216
  </div>
152
217
  </div>
@@ -166,7 +231,9 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
166
231
  <div className="flex flex-wrap items-center gap-2">
167
232
  <Button
168
233
  onClick={() => {
169
- globalThis.open(payload.launch?.url, '_blank', 'noopener,noreferrer');
234
+ // `launchUrl` was derived from the host trust anchor above; the
235
+ // payload contributes only a validated same-origin path.
236
+ globalThis.open(launchUrl, '_blank', 'noopener,noreferrer');
170
237
  }}
171
238
  className="gap-1.5"
172
239
  disabled={working}
@@ -174,7 +241,11 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
174
241
  <ExternalLink className="h-4 w-4" />
175
242
  {payload.launch.label}
176
243
  </Button>
177
- <Button variant="outline" onClick={handleCheckStatus} disabled={working}>
244
+ <Button
245
+ variant="outline"
246
+ onClick={handleCheckStatus}
247
+ disabled={working}
248
+ >
178
249
  {working && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
179
250
  I&apos;ve finished — check
180
251
  </Button>
@@ -184,8 +255,8 @@ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
184
255
  <>
185
256
  {payload.fields.length === 0 && (
186
257
  <Notice tone="warn" title="No form fields were declared">
187
- This connector declares no onboarding form. Submitting will ask the
188
- service, which is the authority on whether that is allowed.
258
+ This connector declares no onboarding form. Submitting will ask
259
+ the service, which is the authority on whether that is allowed.
189
260
  </Notice>
190
261
  )}
191
262
  <SchemaForm
@@ -217,16 +288,25 @@ function Shell({
217
288
  heading,
218
289
  purpose,
219
290
  children,
220
- }: Readonly<{ heading?: string; purpose: string; children: ReactNode }>): ReactElement {
291
+ }: Readonly<{
292
+ heading?: string;
293
+ purpose: string;
294
+ children: ReactNode;
295
+ }>): ReactElement {
221
296
  return (
222
297
  <div className="h-full overflow-auto">
223
298
  <div className="mx-auto max-w-2xl space-y-4 p-6">
224
299
  <div className="space-y-1">
225
- <h2 className="text-subtitle font-semibold text-ink">{heading ?? 'Secure request'}</h2>
226
- {purpose.length > 0 && <p className="text-body-1 text-ink-2">{purpose}</p>}
300
+ <h2 className="text-subtitle font-semibold text-ink">
301
+ {heading ?? 'Secure request'}
302
+ </h2>
303
+ {purpose.length > 0 && (
304
+ <p className="text-body-1 text-ink-2">{purpose}</p>
305
+ )}
227
306
  <p className="flex items-center gap-1.5 text-caption text-ink-3">
228
307
  <ShieldCheck className="h-3.5 w-3.5" />
229
- Sent straight to the service with your own sign-in. The agent never sees what you type.
308
+ Sent straight to the service with your own sign-in. The agent never
309
+ sees what you type.
230
310
  </p>
231
311
  </div>
232
312
  {children}
@@ -239,23 +319,42 @@ function Notice({
239
319
  tone,
240
320
  title,
241
321
  children,
242
- }: Readonly<{ tone: 'error' | 'warn'; title: string; children: ReactNode }>): ReactElement {
322
+ }: Readonly<{
323
+ tone: 'error' | 'warn';
324
+ title: string;
325
+ children: ReactNode;
326
+ }>): ReactElement {
243
327
  return (
244
328
  <div
245
329
  role="alert"
246
330
  className={cn(
247
331
  'flex items-start gap-2 rounded-md border p-3',
248
- tone === 'error' ? 'border-destructive/40 bg-destructive/10' : 'border-rule bg-paper-elev',
332
+ tone === 'error'
333
+ ? 'border-destructive/40 bg-destructive/10'
334
+ : 'border-rule bg-paper-elev',
249
335
  )}
250
336
  >
251
337
  <AlertTriangle
252
- className={cn('mt-0.5 h-4 w-4 shrink-0', tone === 'error' ? 'text-destructive' : 'text-ink-3')}
338
+ className={cn(
339
+ 'mt-0.5 h-4 w-4 shrink-0',
340
+ tone === 'error' ? 'text-destructive' : 'text-ink-3',
341
+ )}
253
342
  />
254
343
  <div className="space-y-0.5">
255
- <p className={cn('text-body-1 font-medium', tone === 'error' ? 'text-destructive' : 'text-ink')}>
344
+ <p
345
+ className={cn(
346
+ 'text-body-1 font-medium',
347
+ tone === 'error' ? 'text-destructive' : 'text-ink',
348
+ )}
349
+ >
256
350
  {title}
257
351
  </p>
258
- <p className={cn('text-body-1', tone === 'error' ? 'text-destructive/90' : 'text-ink-3')}>
352
+ <p
353
+ className={cn(
354
+ 'text-body-1',
355
+ tone === 'error' ? 'text-destructive/90' : 'text-ink-3',
356
+ )}
357
+ >
259
358
  {children}
260
359
  </p>
261
360
  </div>