@xemahq/ui-kernel 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) 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-registry.d.ts.map +1 -1
  6. package/dist/lib/biome-host/biome-registry.js +14 -9
  7. package/dist/lib/biome-host/biome-registry.js.map +1 -1
  8. package/dist/lib/contribution-registry/contribution-registry.d.ts +24 -0
  9. package/dist/lib/contribution-registry/contribution-registry.d.ts.map +1 -0
  10. package/dist/lib/contribution-registry/contribution-registry.js +67 -0
  11. package/dist/lib/contribution-registry/contribution-registry.js.map +1 -0
  12. package/dist/lib/contribution-registry/index.d.ts +3 -0
  13. package/dist/lib/contribution-registry/index.d.ts.map +1 -0
  14. package/dist/lib/contribution-registry/index.js +3 -0
  15. package/dist/lib/contribution-registry/index.js.map +1 -0
  16. package/dist/lib/contribution-registry/renderable-registry.d.ts +33 -0
  17. package/dist/lib/contribution-registry/renderable-registry.d.ts.map +1 -0
  18. package/dist/lib/contribution-registry/renderable-registry.js +51 -0
  19. package/dist/lib/contribution-registry/renderable-registry.js.map +1 -0
  20. package/dist/ui/capture/field-spec-to-zod.d.ts +1 -0
  21. package/dist/ui/capture/field-spec-to-zod.d.ts.map +1 -1
  22. package/dist/ui/capture/field-spec-to-zod.js +19 -0
  23. package/dist/ui/capture/field-spec-to-zod.js.map +1 -1
  24. package/dist/ui/capture/field-spec-validate.d.ts +4 -0
  25. package/dist/ui/capture/field-spec-validate.d.ts.map +1 -0
  26. package/dist/ui/capture/field-spec-validate.js +129 -0
  27. package/dist/ui/capture/field-spec-validate.js.map +1 -0
  28. package/dist/ui/capture/field-spec.d.ts +13 -1
  29. package/dist/ui/capture/field-spec.d.ts.map +1 -1
  30. package/dist/ui/capture/field-spec.js +25 -0
  31. package/dist/ui/capture/field-spec.js.map +1 -1
  32. package/dist/ui/capture/index.d.ts +4 -2
  33. package/dist/ui/capture/index.d.ts.map +1 -1
  34. package/dist/ui/capture/index.js +4 -2
  35. package/dist/ui/capture/index.js.map +1 -1
  36. package/dist/ui/capture/schema-field-input.d.ts.map +1 -1
  37. package/dist/ui/capture/schema-field-input.js +39 -12
  38. package/dist/ui/capture/schema-field-input.js.map +1 -1
  39. package/dist/ui/capture/submit-values.d.ts +6 -0
  40. package/dist/ui/capture/submit-values.d.ts.map +1 -0
  41. package/dist/ui/capture/submit-values.js +28 -0
  42. package/dist/ui/capture/submit-values.js.map +1 -0
  43. package/dist/ui/index.d.ts +1 -0
  44. package/dist/ui/index.d.ts.map +1 -1
  45. package/dist/ui/index.js +1 -0
  46. package/dist/ui/index.js.map +1 -1
  47. package/dist/ui/primitives/overflow-tabs.d.ts +5 -0
  48. package/dist/ui/primitives/overflow-tabs.d.ts.map +1 -1
  49. package/dist/ui/primitives/overflow-tabs.js +4 -4
  50. package/dist/ui/primitives/overflow-tabs.js.map +1 -1
  51. package/dist/ui/primitives/url-preview-chrome.d.ts +8 -0
  52. package/dist/ui/primitives/url-preview-chrome.d.ts.map +1 -0
  53. package/dist/ui/primitives/url-preview-chrome.js +40 -0
  54. package/dist/ui/primitives/url-preview-chrome.js.map +1 -0
  55. package/dist/vista/GenericCatalogueSurface.d.ts +10 -0
  56. package/dist/vista/GenericCatalogueSurface.d.ts.map +1 -0
  57. package/dist/vista/GenericCatalogueSurface.js +80 -0
  58. package/dist/vista/GenericCatalogueSurface.js.map +1 -0
  59. package/dist/vista/SurfaceFallbackCard.d.ts +9 -0
  60. package/dist/vista/SurfaceFallbackCard.d.ts.map +1 -0
  61. package/dist/vista/SurfaceFallbackCard.js +44 -0
  62. package/dist/vista/SurfaceFallbackCard.js.map +1 -0
  63. package/dist/vista/VistaPanel.d.ts +21 -0
  64. package/dist/vista/VistaPanel.d.ts.map +1 -0
  65. package/dist/vista/VistaPanel.js +31 -0
  66. package/dist/vista/VistaPanel.js.map +1 -0
  67. package/dist/vista/html/CustomUiFrame.d.ts +20 -0
  68. package/dist/vista/html/CustomUiFrame.d.ts.map +1 -0
  69. package/dist/vista/html/CustomUiFrame.js +69 -0
  70. package/dist/vista/html/CustomUiFrame.js.map +1 -0
  71. package/dist/vista/html/custom-ui-frame-bridge.d.ts +13 -0
  72. package/dist/vista/html/custom-ui-frame-bridge.d.ts.map +1 -0
  73. package/dist/vista/html/custom-ui-frame-bridge.js +30 -0
  74. package/dist/vista/html/custom-ui-frame-bridge.js.map +1 -0
  75. package/dist/vista/html/custom-ui-frame-contract.d.ts +29 -0
  76. package/dist/vista/html/custom-ui-frame-contract.d.ts.map +1 -0
  77. package/dist/vista/html/custom-ui-frame-contract.js +16 -0
  78. package/dist/vista/html/custom-ui-frame-contract.js.map +1 -0
  79. package/dist/vista/html/custom-ui-intents.d.ts +20 -0
  80. package/dist/vista/html/custom-ui-intents.d.ts.map +1 -0
  81. package/dist/vista/html/custom-ui-intents.js +57 -0
  82. package/dist/vista/html/custom-ui-intents.js.map +1 -0
  83. package/dist/vista/html/custom-ui-srcdoc.d.ts +17 -0
  84. package/dist/vista/html/custom-ui-srcdoc.d.ts.map +1 -0
  85. package/dist/vista/html/custom-ui-srcdoc.js +70 -0
  86. package/dist/vista/html/custom-ui-srcdoc.js.map +1 -0
  87. package/dist/vista/html/index.d.ts +6 -0
  88. package/dist/vista/html/index.d.ts.map +1 -0
  89. package/dist/vista/html/index.js +6 -0
  90. package/dist/vista/html/index.js.map +1 -0
  91. package/dist/vista/index.d.ts +18 -0
  92. package/dist/vista/index.d.ts.map +1 -0
  93. package/dist/vista/index.js +18 -0
  94. package/dist/vista/index.js.map +1 -0
  95. package/dist/vista/surface-catalogue.d.ts +5 -0
  96. package/dist/vista/surface-catalogue.d.ts.map +1 -0
  97. package/dist/vista/surface-catalogue.js +30 -0
  98. package/dist/vista/surface-catalogue.js.map +1 -0
  99. package/dist/vista/surface-icons.d.ts +7 -0
  100. package/dist/vista/surface-icons.d.ts.map +1 -0
  101. package/dist/vista/surface-icons.js +26 -0
  102. package/dist/vista/surface-icons.js.map +1 -0
  103. package/dist/vista/surface-registry.d.ts +7 -0
  104. package/dist/vista/surface-registry.d.ts.map +1 -0
  105. package/dist/vista/surface-registry.js +7 -0
  106. package/dist/vista/surface-registry.js.map +1 -0
  107. package/dist/vista/surfaces/ChartSurface.d.ts +4 -0
  108. package/dist/vista/surfaces/ChartSurface.d.ts.map +1 -0
  109. package/dist/vista/surfaces/ChartSurface.js +17 -0
  110. package/dist/vista/surfaces/ChartSurface.js.map +1 -0
  111. package/dist/vista/surfaces/FilesSurface.d.ts +15 -0
  112. package/dist/vista/surfaces/FilesSurface.d.ts.map +1 -0
  113. package/dist/vista/surfaces/FilesSurface.js +121 -0
  114. package/dist/vista/surfaces/FilesSurface.js.map +1 -0
  115. package/dist/vista/surfaces/FormSurface.d.ts +4 -0
  116. package/dist/vista/surfaces/FormSurface.d.ts.map +1 -0
  117. package/dist/vista/surfaces/FormSurface.js +97 -0
  118. package/dist/vista/surfaces/FormSurface.js.map +1 -0
  119. package/dist/vista/surfaces/HtmlSurface.d.ts +4 -0
  120. package/dist/vista/surfaces/HtmlSurface.d.ts.map +1 -0
  121. package/dist/vista/surfaces/HtmlSurface.js +11 -0
  122. package/dist/vista/surfaces/HtmlSurface.js.map +1 -0
  123. package/dist/vista/surfaces/TableSurface.d.ts +4 -0
  124. package/dist/vista/surfaces/TableSurface.d.ts.map +1 -0
  125. package/dist/vista/surfaces/TableSurface.js +42 -0
  126. package/dist/vista/surfaces/TableSurface.js.map +1 -0
  127. package/dist/vista/surfaces/UrlSurface.d.ts +4 -0
  128. package/dist/vista/surfaces/UrlSurface.d.ts.map +1 -0
  129. package/dist/vista/surfaces/UrlSurface.js +7 -0
  130. package/dist/vista/surfaces/UrlSurface.js.map +1 -0
  131. package/dist/vista/surfaces/blob-surfaces.d.ts +6 -0
  132. package/dist/vista/surfaces/blob-surfaces.d.ts.map +1 -0
  133. package/dist/vista/surfaces/blob-surfaces.js +38 -0
  134. package/dist/vista/surfaces/blob-surfaces.js.map +1 -0
  135. package/dist/vista/surfaces/elicitation-transport.d.ts +18 -0
  136. package/dist/vista/surfaces/elicitation-transport.d.ts.map +1 -0
  137. package/dist/vista/surfaces/elicitation-transport.js +100 -0
  138. package/dist/vista/surfaces/elicitation-transport.js.map +1 -0
  139. package/dist/vista/surfaces/index.d.ts +11 -0
  140. package/dist/vista/surfaces/index.d.ts.map +1 -0
  141. package/dist/vista/surfaces/index.js +89 -0
  142. package/dist/vista/surfaces/index.js.map +1 -0
  143. package/dist/vista/surfaces/text-surfaces.d.ts +8 -0
  144. package/dist/vista/surfaces/text-surfaces.d.ts.map +1 -0
  145. package/dist/vista/surfaces/text-surfaces.js +54 -0
  146. package/dist/vista/surfaces/text-surfaces.js.map +1 -0
  147. package/dist/vista/use-vista-tabs.d.ts +30 -0
  148. package/dist/vista/use-vista-tabs.d.ts.map +1 -0
  149. package/dist/vista/use-vista-tabs.js +77 -0
  150. package/dist/vista/use-vista-tabs.js.map +1 -0
  151. package/dist/vista/vista-code-renderer.d.ts +12 -0
  152. package/dist/vista/vista-code-renderer.d.ts.map +1 -0
  153. package/dist/vista/vista-code-renderer.js +10 -0
  154. package/dist/vista/vista-code-renderer.js.map +1 -0
  155. package/dist/vista/vista-file-source.d.ts +20 -0
  156. package/dist/vista/vista-file-source.d.ts.map +1 -0
  157. package/dist/vista/vista-file-source.js +69 -0
  158. package/dist/vista/vista-file-source.js.map +1 -0
  159. package/dist/vista/vista-form-host.d.ts +16 -0
  160. package/dist/vista/vista-form-host.d.ts.map +1 -0
  161. package/dist/vista/vista-form-host.js +10 -0
  162. package/dist/vista/vista-form-host.js.map +1 -0
  163. package/dist/vista/vista-model.d.ts +12 -0
  164. package/dist/vista/vista-model.d.ts.map +1 -0
  165. package/dist/vista/vista-model.js +72 -0
  166. package/dist/vista/vista-model.js.map +1 -0
  167. package/dist/vista/vista-open-context.d.ts +9 -0
  168. package/dist/vista/vista-open-context.d.ts.map +1 -0
  169. package/dist/vista/vista-open-context.js +10 -0
  170. package/dist/vista/vista-open-context.js.map +1 -0
  171. package/dist/vista/vista-payloads.d.ts +107 -0
  172. package/dist/vista/vista-payloads.d.ts.map +1 -0
  173. package/dist/vista/vista-payloads.js +305 -0
  174. package/dist/vista/vista-payloads.js.map +1 -0
  175. package/dist/vista/vista-tabs-manifest.d.ts +17 -0
  176. package/dist/vista/vista-tabs-manifest.d.ts.map +1 -0
  177. package/dist/vista/vista-tabs-manifest.js +60 -0
  178. package/dist/vista/vista-tabs-manifest.js.map +1 -0
  179. package/dist/vista/vista-tools.d.ts +41 -0
  180. package/dist/vista/vista-tools.d.ts.map +1 -0
  181. package/dist/vista/vista-tools.js +113 -0
  182. package/dist/vista/vista-tools.js.map +1 -0
  183. package/package.json +3 -3
  184. package/src/index.ts +5 -0
  185. package/src/lib/biome-host/biome-registry.ts +31 -11
  186. package/src/lib/contribution-registry/contribution-registry.spec.ts +141 -0
  187. package/src/lib/contribution-registry/contribution-registry.ts +202 -0
  188. package/src/lib/contribution-registry/index.ts +9 -0
  189. package/src/lib/contribution-registry/renderable-registry.ts +142 -0
  190. package/src/ui/capture/field-spec-to-zod.ts +22 -0
  191. package/src/ui/capture/field-spec-validate.ts +147 -0
  192. package/src/ui/capture/field-spec.ts +62 -0
  193. package/src/ui/capture/index.ts +9 -1
  194. package/src/ui/capture/schema-field-input.tsx +134 -14
  195. package/src/ui/capture/submit-values.test.ts +70 -0
  196. package/src/ui/capture/submit-values.ts +63 -0
  197. package/src/ui/index.ts +4 -0
  198. package/src/ui/primitives/overflow-tabs.tsx +24 -4
  199. package/src/ui/primitives/url-preview-chrome.tsx +102 -0
  200. package/src/vista/GenericCatalogueSurface.tsx +177 -0
  201. package/src/vista/SurfaceFallbackCard.tsx +109 -0
  202. package/src/vista/VistaPanel.tsx +155 -0
  203. package/src/vista/form-payload.spec.ts +93 -0
  204. package/src/vista/html/CustomUiFrame.spec.tsx +125 -0
  205. package/src/vista/html/CustomUiFrame.tsx +204 -0
  206. package/src/vista/html/custom-ui-frame-bridge.spec.ts +191 -0
  207. package/src/vista/html/custom-ui-frame-bridge.ts +92 -0
  208. package/src/vista/html/custom-ui-frame-contract.ts +88 -0
  209. package/src/vista/html/custom-ui-intents.spec.ts +133 -0
  210. package/src/vista/html/custom-ui-intents.ts +151 -0
  211. package/src/vista/html/custom-ui-srcdoc.spec.ts +151 -0
  212. package/src/vista/html/custom-ui-srcdoc.ts +193 -0
  213. package/src/vista/html/index.ts +13 -0
  214. package/src/vista/index.ts +48 -0
  215. package/src/vista/surface-catalogue.spec.ts +140 -0
  216. package/src/vista/surface-catalogue.ts +84 -0
  217. package/src/vista/surface-icons.ts +57 -0
  218. package/src/vista/surface-registry.ts +56 -0
  219. package/src/vista/surfaces/ChartSurface.tsx +34 -0
  220. package/src/vista/surfaces/FilesSurface.tsx +268 -0
  221. package/src/vista/surfaces/FormSurface.tsx +264 -0
  222. package/src/vista/surfaces/HtmlSurface.tsx +43 -0
  223. package/src/vista/surfaces/TableSurface.tsx +99 -0
  224. package/src/vista/surfaces/UrlSurface.tsx +40 -0
  225. package/src/vista/surfaces/blob-surfaces.tsx +129 -0
  226. package/src/vista/surfaces/elicitation-transport.spec.ts +187 -0
  227. package/src/vista/surfaces/elicitation-transport.ts +175 -0
  228. package/src/vista/surfaces/index.tsx +157 -0
  229. package/src/vista/surfaces/text-surfaces.tsx +119 -0
  230. package/src/vista/use-vista-tabs.ts +188 -0
  231. package/src/vista/vista-code-renderer.tsx +46 -0
  232. package/src/vista/vista-file-source.tsx +163 -0
  233. package/src/vista/vista-form-host.tsx +78 -0
  234. package/src/vista/vista-model.ts +130 -0
  235. package/src/vista/vista-open-context.tsx +42 -0
  236. package/src/vista/vista-payloads.ts +514 -0
  237. package/src/vista/vista-tabs-manifest.ts +123 -0
  238. package/src/vista/vista-tools.ts +220 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Vista surface registry — resolves a `VistaSurface` envelope to a renderer.
3
+ *
4
+ * Vista is the agent-driven, biome-extensible tabbed preview plane (Canopy
5
+ * Plane ④). This registry is the same two-layer closed+open contribution
6
+ * registry the ChatWidget plane uses, instantiated over the kernel
7
+ * `SurfaceKind` set:
8
+ *
9
+ * • a CLOSED first-party set, keyed by the `SurfaceKind` enum and registered
10
+ * at module load by the first-party surface renderers;
11
+ * • an OPEN biome set, keyed by a namespaced `<biomeId>:<kind>` slug — a
12
+ * biome registers from its `FrontendBiome.init()` hook, or the catalogue
13
+ * sync registers a generic schema-driven renderer on its behalf.
14
+ *
15
+ * Every first-party `SurfaceKind` value is RESERVED: `register()` throws if a
16
+ * biome tries to claim one. Resolution NEVER throws — an unknown kind, a
17
+ * payload the renderer rejects, or a `version` newer than the renderer supports
18
+ * all yield a typed `ContributionFallbackReason` the caller draws as
19
+ * `SurfaceFallbackCard`.
20
+ *
21
+ * NOTE: this unit ships the registry + the fallback layers only. First-party
22
+ * renderers and the tab shell land with the Vista shell unit; until then a
23
+ * first-party kind resolves to `UnknownKind` — visibly, never blank.
24
+ */
25
+ import { SurfaceKind } from '@xemahq/kernel-contracts/surface';
26
+ import { type ReactElement } from 'react';
27
+
28
+ import {
29
+ createRenderableContributionRegistry,
30
+ type ContributionEnvelope,
31
+ type RenderableContributionEntry,
32
+ type RenderableResolution,
33
+ } from '../lib/contribution-registry';
34
+
35
+ /** A Vista surface renderer: versioned, payload-narrowing, returns an element. */
36
+ export type VistaSurfaceEntry<TPayload = unknown> = RenderableContributionEntry<
37
+ ReactElement,
38
+ TPayload
39
+ >;
40
+
41
+ /** The outcome of resolving a `VistaSurface` — inspectable for the debugger. */
42
+ export type VistaSurfaceResolution = RenderableResolution<ReactElement>;
43
+
44
+ /** The minimum a caller must pass to {@link vistaSurfaceRegistry.resolve}. */
45
+ export type VistaSurfaceEnvelope = ContributionEnvelope;
46
+
47
+ /**
48
+ * The singleton Vista surface registry. Import it to register a first-party
49
+ * renderer (`registerFirstParty`), to contribute a biome renderer
50
+ * (`register('<biomeId>:<kind>', …)`), or to resolve an envelope (`resolve`).
51
+ */
52
+ export const vistaSurfaceRegistry =
53
+ createRenderableContributionRegistry<ReactElement>({
54
+ name: 'vista-surface',
55
+ reservedKinds: Object.values(SurfaceKind),
56
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `SurfaceKind.Chart` — a chart over labelled series.
3
+ *
4
+ * Drawn with the kernel's OWN pure-SVG chart primitives (`BarChart` /
5
+ * `LineChart` / `DonutChart`). No chart library is added: the kernel ships none
6
+ * on purpose, because a strict-CSP frame blocks external scripts.
7
+ *
8
+ * `bar` and `donut` are single-series shapes — they render the FIRST series.
9
+ * The validator enforces at least one series, so there is no empty-render path.
10
+ */
11
+ import { BarChart, DonutChart, LineChart } from '../../ui/charts';
12
+ import { VistaChartKind, type VistaChartPayload } from '../vista-payloads';
13
+
14
+ import type { ReactElement, ReactNode } from 'react';
15
+
16
+
17
+ function chartBody({ kind, title, series }: VistaChartPayload): ReactNode {
18
+ switch (kind) {
19
+ case VistaChartKind.Line:
20
+ return <LineChart title={title} series={series} />;
21
+ case VistaChartKind.Bar:
22
+ return <BarChart title={title} data={series[0].points} />;
23
+ case VistaChartKind.Donut:
24
+ return <DonutChart title={title} data={series[0].points} />;
25
+ }
26
+ }
27
+
28
+ export function ChartSurface(payload: VistaChartPayload): ReactElement {
29
+ return (
30
+ <div style={{ height: '100%', overflow: 'auto', padding: 16 }}>
31
+ {chartBody(payload)}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * `SurfaceKind.Files` — the grouped file browser, and the pinned first tab of
3
+ * every file-backed Vista panel.
4
+ *
5
+ * Moved out of the thin session workspace (`WorkspaceFileBrowser` +
6
+ * `workspace-model`'s grouping/extension helpers) so the Design-System-Builder
7
+ * file tab, the session workspace, and any agent-opened `files` surface draw the
8
+ * SAME browser. Grouped by folder with a caption header; each row is a
9
+ * colour-coded extension tile + filename + change badges. Clicking a row opens
10
+ * the file as its own tab through {@link useVistaOpen}.
11
+ */
12
+ import { ChevronRight, Folder } from 'lucide-react';
13
+
14
+
15
+ import { fileSurface } from '../vista-model';
16
+ import { useVistaOpen } from '../vista-open-context';
17
+
18
+ import type { VistaFileEntry, VistaFilesPayload } from '../vista-payloads';
19
+ import type { ReactElement } from 'react';
20
+
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
+ export function FilesSurface({
89
+ files,
90
+ }: Readonly<VistaFilesPayload>): ReactElement {
91
+ const open = useVistaOpen();
92
+ const groups = groupFilesByFolder(files);
93
+ if (groups.length === 0) {
94
+ return (
95
+ <div
96
+ style={{
97
+ height: '100%',
98
+ display: 'grid',
99
+ placeItems: 'center',
100
+ fontSize: 12.5,
101
+ color: 'hsl(var(--ink-4))',
102
+ }}
103
+ >
104
+ No files in the workspace yet.
105
+ </div>
106
+ );
107
+ }
108
+ return (
109
+ <div style={{ height: '100%', overflowY: 'auto', padding: '14px 12px' }}>
110
+ {groups.map((group) => (
111
+ <section key={group.dir || 'root'} style={{ marginBottom: 16 }}>
112
+ <div
113
+ style={{
114
+ display: 'flex',
115
+ alignItems: 'center',
116
+ gap: 6,
117
+ padding: '0 8px 8px',
118
+ }}
119
+ >
120
+ <Folder size={12} style={{ color: 'hsl(var(--ink-4))' }} />
121
+ <span
122
+ className="cap"
123
+ style={{ fontSize: 9.5, letterSpacing: '0.07em' }}
124
+ >
125
+ {group.dir ? group.dir.replaceAll('/', ' / ') : 'Root'}
126
+ </span>
127
+ </div>
128
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
129
+ {group.files.map((file) => (
130
+ <FileRow
131
+ key={file.path}
132
+ file={file}
133
+ onOpen={
134
+ open === null
135
+ ? null
136
+ : () => {
137
+ open(fileSurface(file.path));
138
+ }
139
+ }
140
+ />
141
+ ))}
142
+ </div>
143
+ </section>
144
+ ))}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ function FileRow({
150
+ file,
151
+ onOpen,
152
+ }: Readonly<{
153
+ file: VistaFileEntry;
154
+ onOpen: (() => void) | null;
155
+ }>): ReactElement {
156
+ const name = file.path.split('/').pop() ?? file.path;
157
+ const meta = fileExtMeta(name);
158
+ const changed = file.add !== undefined || file.del !== undefined;
159
+ const body = (
160
+ <>
161
+ <span
162
+ style={{
163
+ width: 28,
164
+ height: 28,
165
+ borderRadius: 8,
166
+ flexShrink: 0,
167
+ display: 'grid',
168
+ placeItems: 'center',
169
+ background: `color-mix(in oklch, ${meta.color} 13%, transparent)`,
170
+ border: `1px solid color-mix(in oklch, ${meta.color} 26%, transparent)`,
171
+ }}
172
+ >
173
+ <span
174
+ className="mono"
175
+ style={{
176
+ fontSize: 8,
177
+ fontWeight: 700,
178
+ color: meta.color,
179
+ letterSpacing: '0.02em',
180
+ }}
181
+ >
182
+ {meta.tag}
183
+ </span>
184
+ </span>
185
+ <span style={{ flex: 1, minWidth: 0 }}>
186
+ <span
187
+ style={{
188
+ display: 'block',
189
+ fontSize: 13,
190
+ fontWeight: 500,
191
+ color: 'hsl(var(--ink))',
192
+ overflow: 'hidden',
193
+ textOverflow: 'ellipsis',
194
+ whiteSpace: 'nowrap',
195
+ }}
196
+ >
197
+ {name}
198
+ </span>
199
+ {changed && (
200
+ <span
201
+ className="mono"
202
+ style={{ fontSize: 10, color: 'hsl(var(--ink-4))' }}
203
+ >
204
+ changed
205
+ </span>
206
+ )}
207
+ </span>
208
+ {changed && (
209
+ <span style={{ display: 'inline-flex', gap: 6, flexShrink: 0 }}>
210
+ {file.add !== undefined && file.add > 0 && (
211
+ <span
212
+ className="mono"
213
+ style={{ fontSize: 10.5, color: 'hsl(var(--success))' }}
214
+ >
215
+ +{file.add}
216
+ </span>
217
+ )}
218
+ {file.del !== undefined && file.del > 0 && (
219
+ <span
220
+ className="mono"
221
+ style={{
222
+ fontSize: 10.5,
223
+ color: 'hsl(var(--destructive, var(--danger)))',
224
+ }}
225
+ >
226
+ −{file.del}
227
+ </span>
228
+ )}
229
+ </span>
230
+ )}
231
+ {onOpen !== null && (
232
+ <ChevronRight
233
+ size={13}
234
+ style={{ color: 'hsl(var(--ink-4))', flexShrink: 0, opacity: 0.6 }}
235
+ />
236
+ )}
237
+ </>
238
+ );
239
+
240
+ const rowStyle = {
241
+ display: 'flex',
242
+ alignItems: 'center',
243
+ gap: 11,
244
+ width: '100%',
245
+ padding: '8px 9px',
246
+ borderRadius: 9,
247
+ textAlign: 'left',
248
+ } as const;
249
+
250
+ if (onOpen === null) {
251
+ return <div style={rowStyle}>{body}</div>;
252
+ }
253
+ return (
254
+ <button
255
+ type="button"
256
+ onClick={onOpen}
257
+ style={{ ...rowStyle, transition: 'background 140ms ease' }}
258
+ onMouseEnter={(e) => {
259
+ e.currentTarget.style.background = 'hsl(var(--paper-elev))';
260
+ }}
261
+ onMouseLeave={(e) => {
262
+ e.currentTarget.style.background = 'transparent';
263
+ }}
264
+ >
265
+ {body}
266
+ </button>
267
+ );
268
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * `SurfaceKind.Form` — the secure-elicitation surface (Canopy §8.4).
3
+ *
4
+ * The agent asks for a credential it must never see. This surface renders the
5
+ * connector's ONBOARDING form (or, for `oauth2` / `app_install`, the consent
6
+ * link), captures the values in local state, and POSTs them BROWSER → OWNING
7
+ * SERVICE with the SUBJECT's own JWT. The agent learns only
8
+ * `{ submitted, ref }`.
9
+ *
10
+ * ── What makes the value unreachable from the agent ─────────────────────────
11
+ * • the only outbound channel is `host.onOutcome(VistaFormOutcome)`, whose
12
+ * type has three scalar members and no place a value could be assigned;
13
+ * • the values live in this component's `useState`; the single reader is
14
+ * `submitElicitationFields`, which takes them as a parameter and returns an
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;
19
+ * • secret fields are DECLARED (`spec.isSecret`) and render write-only; the
20
+ * surface holds no cache and no read path repopulates them.
21
+ *
22
+ * Nothing here is provider-aware. Whether the flow is a field form or an OAuth
23
+ * handoff is decided by the payload the owning service produced.
24
+ */
25
+ import { AlertTriangle, CheckCircle2, ExternalLink, Loader2, ShieldCheck } from 'lucide-react';
26
+ import { useCallback, useEffect, useState, type ReactElement, type ReactNode } from 'react';
27
+
28
+ import { readElicitationStatus, submitElicitationFields, VistaFormSubmitError } from './elicitation-transport';
29
+ import { SchemaForm } from '../../ui/capture/schema-form';
30
+ import { captureValuesToStringMap } from '../../ui/capture/submit-values';
31
+ import { useSchemaValidation } from '../../ui/capture/use-schema-validation';
32
+ import { cn } from '../../ui/cn';
33
+ import { Button } from '../../ui/primitives/button';
34
+ import { useVistaFormHost, type VistaFormOutcome } from '../vista-form-host';
35
+
36
+ import type { CaptureValues } from '../../ui/capture/field-spec';
37
+ import type { VistaFormPayload } from '../vista-payloads';
38
+
39
+ /** Explicit lifecycle. No implicit "maybe submitted" state. */
40
+ enum FormPhase {
41
+ Editing = 'editing',
42
+ Working = 'working',
43
+ Settled = 'settled',
44
+ }
45
+
46
+ export function FormSurface(payload: Readonly<VistaFormPayload>): ReactElement {
47
+ const host = useVistaFormHost();
48
+ const [values, setValues] = useState<CaptureValues>({});
49
+ const [phase, setPhase] = useState<FormPhase>(FormPhase.Editing);
50
+ const [serviceError, setServiceError] = useState<string | null>(null);
51
+ const [showFieldErrors, setShowFieldErrors] = useState(false);
52
+ const [outcome, setOutcome] = useState<VistaFormOutcome | null>(null);
53
+ const { errors, isValid } = useSchemaValidation(payload.fields, values);
54
+
55
+ const settle = useCallback(
56
+ (next: VistaFormOutcome): void => {
57
+ // The values are dropped the moment the elicitation settles: nothing in
58
+ // this component may outlive the submission holding secret material.
59
+ setValues({});
60
+ setOutcome(next);
61
+ setPhase(FormPhase.Settled);
62
+ host?.onOutcome(next);
63
+ },
64
+ [host],
65
+ );
66
+
67
+ const failed = useCallback((error: unknown): void => {
68
+ setServiceError(
69
+ error instanceof VistaFormSubmitError || error instanceof Error
70
+ ? error.message
71
+ : 'The submission could not be completed.',
72
+ );
73
+ setPhase(FormPhase.Editing);
74
+ }, []);
75
+
76
+ const handleSubmit = useCallback((): void => {
77
+ if (host === null) return;
78
+ setShowFieldErrors(true);
79
+ if (!isValid) return;
80
+ setServiceError(null);
81
+ setPhase(FormPhase.Working);
82
+ // The values object is a PARAMETER here and nowhere else. It is never
83
+ // returned, logged, or handed to a callback.
84
+ void host
85
+ .getAccessToken()
86
+ .then((accessToken) =>
87
+ submitElicitationFields({
88
+ submitUrl: payload.submitUrl,
89
+ ref: payload.ref,
90
+ accessToken,
91
+ fields: captureValuesToStringMap(payload.fields, values),
92
+ }),
93
+ )
94
+ .then(settle, failed);
95
+ }, [host, isValid, payload.fields, payload.ref, payload.submitUrl, settle, failed, values]);
96
+
97
+ const handleCheckStatus = useCallback((): void => {
98
+ if (host === null) return;
99
+ setServiceError(null);
100
+ setPhase(FormPhase.Working);
101
+ void host
102
+ .getAccessToken()
103
+ .then((accessToken) =>
104
+ readElicitationStatus({ statusUrl: payload.statusUrl, ref: payload.ref, accessToken }),
105
+ )
106
+ .then((next) => {
107
+ if (next === null) {
108
+ setServiceError('Not completed yet — finish the steps in the other window, then check again.');
109
+ setPhase(FormPhase.Editing);
110
+ return;
111
+ }
112
+ settle(next);
113
+ }, failed);
114
+ }, [host, payload.ref, payload.statusUrl, settle, failed]);
115
+
116
+ // The consent/install window is a separate browsing context, so the only
117
+ // signal this tab gets is the user returning to it. Re-checking on focus is
118
+ // an event, not a timer — there is no polling loop and no timeout.
119
+ const isLaunch = payload.launch !== undefined;
120
+ useEffect(() => {
121
+ if (!isLaunch || phase === FormPhase.Settled || host === null) return;
122
+ const onFocus = (): void => handleCheckStatus();
123
+ globalThis.addEventListener?.('focus', onFocus);
124
+ return () => globalThis.removeEventListener?.('focus', onFocus);
125
+ // `handleCheckStatus` is stable per (host, payload) and re-subscribing on a
126
+ // phase change is intended: a settled surface stops listening.
127
+ }, [isLaunch, phase, host, handleCheckStatus]);
128
+
129
+ if (host === null) {
130
+ return (
131
+ <Shell heading={payload.heading} purpose={payload.purpose}>
132
+ <Notice tone="error" title="This form cannot be submitted here">
133
+ No form host is mounted for this panel, so there is no way to sign the
134
+ request as you. Open the request from a surface that provides one.
135
+ </Notice>
136
+ </Shell>
137
+ );
138
+ }
139
+
140
+ if (phase === FormPhase.Settled && outcome !== null) {
141
+ return (
142
+ <Shell heading={payload.heading} purpose={payload.purpose}>
143
+ <div className="flex items-start gap-2 rounded-md border border-rule bg-paper-elev p-3">
144
+ <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
145
+ <div className="space-y-0.5">
146
+ <p className="text-body-1 font-medium text-ink">Submitted.</p>
147
+ <p className="text-caption text-ink-3">
148
+ The value went straight to the service and was never shown to the
149
+ agent{outcome.connectionId === null ? '.' : `. Connection ${outcome.connectionId} is ready.`}
150
+ </p>
151
+ </div>
152
+ </div>
153
+ </Shell>
154
+ );
155
+ }
156
+
157
+ const working = phase === FormPhase.Working;
158
+
159
+ return (
160
+ <Shell heading={payload.heading} purpose={payload.purpose}>
161
+ {payload.launch !== undefined ? (
162
+ <>
163
+ {payload.launch.hint !== undefined && (
164
+ <p className="text-body-1 text-ink-3">{payload.launch.hint}</p>
165
+ )}
166
+ <div className="flex flex-wrap items-center gap-2">
167
+ <Button
168
+ onClick={() => {
169
+ globalThis.open(payload.launch?.url, '_blank', 'noopener,noreferrer');
170
+ }}
171
+ className="gap-1.5"
172
+ disabled={working}
173
+ >
174
+ <ExternalLink className="h-4 w-4" />
175
+ {payload.launch.label}
176
+ </Button>
177
+ <Button variant="outline" onClick={handleCheckStatus} disabled={working}>
178
+ {working && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
179
+ I&apos;ve finished — check
180
+ </Button>
181
+ </div>
182
+ </>
183
+ ) : (
184
+ <>
185
+ {payload.fields.length === 0 && (
186
+ <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.
189
+ </Notice>
190
+ )}
191
+ <SchemaForm
192
+ fields={payload.fields}
193
+ values={values}
194
+ onChange={setValues}
195
+ disabled={working}
196
+ {...(showFieldErrors ? { errors } : {})}
197
+ />
198
+ <div className="flex items-center gap-2">
199
+ <Button onClick={handleSubmit} disabled={working}>
200
+ {working && <Loader2 className="mr-1 h-4 w-4 animate-spin" />}
201
+ {payload.submitLabel ?? 'Submit securely'}
202
+ </Button>
203
+ </div>
204
+ </>
205
+ )}
206
+
207
+ {serviceError !== null && (
208
+ <Notice tone="error" title="The service rejected this">
209
+ {serviceError}
210
+ </Notice>
211
+ )}
212
+ </Shell>
213
+ );
214
+ }
215
+
216
+ function Shell({
217
+ heading,
218
+ purpose,
219
+ children,
220
+ }: Readonly<{ heading?: string; purpose: string; children: ReactNode }>): ReactElement {
221
+ return (
222
+ <div className="h-full overflow-auto">
223
+ <div className="mx-auto max-w-2xl space-y-4 p-6">
224
+ <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>}
227
+ <p className="flex items-center gap-1.5 text-caption text-ink-3">
228
+ <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.
230
+ </p>
231
+ </div>
232
+ {children}
233
+ </div>
234
+ </div>
235
+ );
236
+ }
237
+
238
+ function Notice({
239
+ tone,
240
+ title,
241
+ children,
242
+ }: Readonly<{ tone: 'error' | 'warn'; title: string; children: ReactNode }>): ReactElement {
243
+ return (
244
+ <div
245
+ role="alert"
246
+ className={cn(
247
+ 'flex items-start gap-2 rounded-md border p-3',
248
+ tone === 'error' ? 'border-destructive/40 bg-destructive/10' : 'border-rule bg-paper-elev',
249
+ )}
250
+ >
251
+ <AlertTriangle
252
+ className={cn('mt-0.5 h-4 w-4 shrink-0', tone === 'error' ? 'text-destructive' : 'text-ink-3')}
253
+ />
254
+ <div className="space-y-0.5">
255
+ <p className={cn('text-body-1 font-medium', tone === 'error' ? 'text-destructive' : 'text-ink')}>
256
+ {title}
257
+ </p>
258
+ <p className={cn('text-body-1', tone === 'error' ? 'text-destructive/90' : 'text-ink-3')}>
259
+ {children}
260
+ </p>
261
+ </div>
262
+ </div>
263
+ );
264
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `SurfaceKind.Html` — the "render anything, safely" surface.
3
+ *
4
+ * It draws through the promoted {@link CustomUiFrame}: an opaque-origin,
5
+ * `allow-scripts`-only sandbox iframe with a `<meta>` CSP of
6
+ * `default-src 'none'; script-src <pinned>; style-src 'unsafe-inline';
7
+ * base-uri 'none'; form-action 'none'`.
8
+ *
9
+ * Two forms, one frame:
10
+ * • `{ document }` — untrusted, agent-authored HTML. `script-src 'none'`, and
11
+ * the frame is mounted with {@link NO_CUSTOM_UI_INTENTS}: it can render, and
12
+ * it can do NOTHING else. This is what an agent's `html` tab uses.
13
+ * • `{ compiledOutput, integrity }` — a server-compiled, SRI-pinned chunk.
14
+ * `script-src '<sha384-…>'` executes exactly those bytes and nothing else.
15
+ *
16
+ * A surface that wants intents (App Forge's runtime views) mounts
17
+ * `CustomUiFrame` directly with its own closed set; the generic `html` TAB
18
+ * deliberately grants none — an agent-authored document must not be able to
19
+ * drive the host.
20
+ */
21
+
22
+ import { NO_CUSTOM_UI_INTENTS } from '../html/custom-ui-intents';
23
+ import { CustomUiFrame } from '../html/CustomUiFrame';
24
+
25
+ import type { VistaHtmlPayload } from '../vista-payloads';
26
+ import type { ReactElement } from 'react';
27
+
28
+ export function HtmlSurface(payload: VistaHtmlPayload): ReactElement {
29
+ const content =
30
+ 'document' in payload
31
+ ? { document: payload.document }
32
+ : { compiledOutput: payload.compiledOutput, integrity: payload.integrity };
33
+ const styleCss = 'document' in payload ? undefined : payload.styleCss;
34
+ return (
35
+ <CustomUiFrame
36
+ content={content}
37
+ intents={NO_CUSTOM_UI_INTENTS}
38
+ title="Rendered view"
39
+ className="h-full p-2"
40
+ {...(styleCss === undefined ? {} : { styleCss })}
41
+ />
42
+ );
43
+ }