@nxisai/agent-harness 0.4.0 → 0.5.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 (103) hide show
  1. package/README.md +4 -2
  2. package/dist/harness/harness.d.ts.map +1 -1
  3. package/dist/harness/harness.js +31 -9
  4. package/dist/harness/harness.js.map +1 -1
  5. package/dist/harness/tools/action-tools.d.ts.map +1 -1
  6. package/dist/harness/tools/action-tools.js +7 -5
  7. package/dist/harness/tools/action-tools.js.map +1 -1
  8. package/dist/harness/tools/bridge.d.ts.map +1 -1
  9. package/dist/harness/tools/bridge.js +33 -12
  10. package/dist/harness/tools/bridge.js.map +1 -1
  11. package/dist/harness/tools/local-context.d.ts +49 -9
  12. package/dist/harness/tools/local-context.d.ts.map +1 -1
  13. package/dist/harness/tools/local-context.js +377 -22
  14. package/dist/harness/tools/local-context.js.map +1 -1
  15. package/dist/harness/tools/open-page.d.ts +47 -1
  16. package/dist/harness/tools/open-page.d.ts.map +1 -1
  17. package/dist/harness/tools/open-page.js +119 -33
  18. package/dist/harness/tools/open-page.js.map +1 -1
  19. package/dist/harness/tools/retry.d.ts +53 -0
  20. package/dist/harness/tools/retry.d.ts.map +1 -0
  21. package/dist/harness/tools/retry.js +96 -0
  22. package/dist/harness/tools/retry.js.map +1 -0
  23. package/dist/harness/tools/schema.d.ts +18 -0
  24. package/dist/harness/tools/schema.d.ts.map +1 -1
  25. package/dist/harness/tools/schema.js +38 -0
  26. package/dist/harness/tools/schema.js.map +1 -1
  27. package/dist/harness/tools/site-actions.d.ts.map +1 -1
  28. package/dist/harness/tools/site-actions.js +3 -2
  29. package/dist/harness/tools/site-actions.js.map +1 -1
  30. package/dist/harness/tools/site-nav.d.ts +168 -0
  31. package/dist/harness/tools/site-nav.d.ts.map +1 -0
  32. package/dist/harness/tools/site-nav.js +316 -0
  33. package/dist/harness/tools/site-nav.js.map +1 -0
  34. package/dist/harness/tools/site-pages.d.ts +9 -4
  35. package/dist/harness/tools/site-pages.d.ts.map +1 -1
  36. package/dist/harness/tools/site-pages.js +46 -7
  37. package/dist/harness/tools/site-pages.js.map +1 -1
  38. package/dist/harness/tools/source.d.ts +11 -2
  39. package/dist/harness/tools/source.d.ts.map +1 -1
  40. package/dist/harness/tools/source.js +55 -15
  41. package/dist/harness/tools/source.js.map +1 -1
  42. package/dist/harness/tools/webmcp.d.ts +39 -3
  43. package/dist/harness/tools/webmcp.d.ts.map +1 -1
  44. package/dist/harness/tools/webmcp.js +93 -14
  45. package/dist/harness/tools/webmcp.js.map +1 -1
  46. package/dist/index.d.ts +7 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +4 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/types.d.ts +2 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/ui/agent-fullscreen.d.ts.map +1 -1
  53. package/dist/ui/agent-fullscreen.js +13 -2
  54. package/dist/ui/agent-fullscreen.js.map +1 -1
  55. package/dist/ui/tailwind.generated.d.ts +1 -1
  56. package/dist/ui/tailwind.generated.d.ts.map +1 -1
  57. package/dist/ui/tailwind.generated.js +1 -1
  58. package/dist/ui/tailwind.generated.js.map +1 -1
  59. package/package.json +2 -1
  60. package/src/a2ui/cards.ts +177 -0
  61. package/src/a2ui/emit.ts +147 -0
  62. package/src/a2ui/form.ts +117 -0
  63. package/src/a2ui/host.ts +73 -0
  64. package/src/a2ui/index.ts +22 -0
  65. package/src/a2ui/stepper.ts +211 -0
  66. package/src/a2ui/theme.ts +134 -0
  67. package/src/a2ui/to-v09.ts +118 -0
  68. package/src/engine.ts +701 -0
  69. package/src/harness/harness.ts +956 -0
  70. package/src/harness/plan.ts +91 -0
  71. package/src/harness/policy.ts +59 -0
  72. package/src/harness/renderer.ts +208 -0
  73. package/src/harness/router/coordinator.ts +220 -0
  74. package/src/harness/router/rules.ts +65 -0
  75. package/src/harness/slots.ts +117 -0
  76. package/src/harness/steps.ts +156 -0
  77. package/src/harness/tools/action-tools.ts +870 -0
  78. package/src/harness/tools/bridge.ts +393 -0
  79. package/src/harness/tools/client.ts +26 -0
  80. package/src/harness/tools/discovery.ts +55 -0
  81. package/src/harness/tools/local-context.ts +480 -0
  82. package/src/harness/tools/open-page.ts +273 -0
  83. package/src/harness/tools/pending-action.ts +232 -0
  84. package/src/harness/tools/resume.ts +134 -0
  85. package/src/harness/tools/retry.ts +117 -0
  86. package/src/harness/tools/schema.ts +245 -0
  87. package/src/harness/tools/site-actions.ts +356 -0
  88. package/src/harness/tools/site-nav.ts +432 -0
  89. package/src/harness/tools/site-pages.ts +288 -0
  90. package/src/harness/tools/source.ts +353 -0
  91. package/src/harness/tools/webmcp.ts +200 -0
  92. package/src/index.ts +193 -0
  93. package/src/providers/capability.ts +167 -0
  94. package/src/providers/proxy.ts +191 -0
  95. package/src/providers/types.ts +150 -0
  96. package/src/types.ts +258 -0
  97. package/src/ui/agent-fullscreen.ts +3984 -0
  98. package/src/ui/agent.tw.css +49 -0
  99. package/src/ui/config.ts +138 -0
  100. package/src/ui/index.ts +32 -0
  101. package/src/ui/journey.ts +57 -0
  102. package/src/ui/tailwind.generated.ts +2 -0
  103. package/src/webmcp-contract.ts +65 -0
@@ -1,3 +1,3 @@
1
1
  /* GENERATED by scripts/css-to-ts.mjs — do not edit. Source: src/ui/agent.tw.css */
2
- export const TAILWIND_CSS = "/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:\"Inter\", system-ui, sans-serif;--font-mono:\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-2xl:1rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#fff;--color-surface:#f5f7fb;--color-border-hair:#e4e9f2;--color-ink:#0f172a;--color-ink-2:#475569;--color-ink-3:#8a97ac;--color-blue:#2563eb;--color-blue-strong:#1d4ed8;--color-blue-tint:#eef3ff;--color-green-strong:#16a34a;--color-red:#b91c1c;--font-display:\"Manrope\", system-ui, sans-serif;--radius-card:14px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.fixed{position:fixed}.static{position:static}.sticky{position:sticky}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-1{margin-top:var(--spacing)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.table{display:table}.w-full{width:100%}.max-w-\\[76\\%\\]{max-width:76%}.max-w-\\[420px\\]{max-width:420px}.flex-shrink{flex-shrink:1}.border-collapse{border-collapse:collapse}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.gap-1{gap:var(--spacing)}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-card{border-radius:var(--radius-card)}.rounded-br-md{border-bottom-right-radius:var(--radius-md)}.rounded-bl-md{border-bottom-left-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-border-hair{border-color:var(--color-border-hair)}.border-red{border-color:var(--color-red)}.border-red\\/25{border-color:#b91c1c40}@supports (color:color-mix(in lab, red, red)){.border-red\\/25{border-color:color-mix(in oklab, var(--color-red) 25%, transparent)}}.bg-blue{background-color:var(--color-blue)}.bg-surface{background-color:var(--color-surface)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\\.5{padding-inline:calc(var(--spacing) * 3.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\\.5{padding-block:calc(var(--spacing) * 2.5)}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-\\[11px\\]{font-size:11px}.text-\\[13px\\]{font-size:13px}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-\\[0\\.14em\\]{--tw-tracking:.14em;letter-spacing:.14em}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-green-strong{color:var(--color-green-strong)}.text-ink{color:var(--color-ink)}.text-ink-2{color:var(--color-ink-2)}.text-red{color:var(--color-red)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-resting{--tw-shadow:0 1px 2px var(--tw-shadow-color,#0f172a0d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow\\/border{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.\\!ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}}@property --tw-rotate-x{syntax:\"*\";inherits:false}@property --tw-rotate-y{syntax:\"*\";inherits:false}@property --tw-rotate-z{syntax:\"*\";inherits:false}@property --tw-skew-x{syntax:\"*\";inherits:false}@property --tw-skew-y{syntax:\"*\";inherits:false}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-tracking{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-blur{syntax:\"*\";inherits:false}@property --tw-brightness{syntax:\"*\";inherits:false}@property --tw-contrast{syntax:\"*\";inherits:false}@property --tw-grayscale{syntax:\"*\";inherits:false}@property --tw-hue-rotate{syntax:\"*\";inherits:false}@property --tw-invert{syntax:\"*\";inherits:false}@property --tw-opacity{syntax:\"*\";inherits:false}@property --tw-saturate{syntax:\"*\";inherits:false}@property --tw-sepia{syntax:\"*\";inherits:false}@property --tw-drop-shadow{syntax:\"*\";inherits:false}@property --tw-drop-shadow-color{syntax:\"*\";inherits:false}@property --tw-drop-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:\"*\";inherits:false}@property --tw-ease{syntax:\"*\";inherits:false}";
2
+ export const TAILWIND_CSS = "/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:\"Inter\", system-ui, sans-serif;--font-mono:\"JetBrains Mono\", ui-monospace, SFMono-Regular, Menlo, monospace;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--leading-normal:1.5;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-2xl:1rem;--shadow-sm:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#fff;--color-surface:#f5f7fb;--color-border-hair:#e4e9f2;--color-ink:#0f172a;--color-ink-2:#475569;--color-ink-3:#8a97ac;--color-blue:#2563eb;--color-blue-strong:#1d4ed8;--color-blue-tint:#eef3ff;--color-green-strong:#16a34a;--color-red:#b91c1c;--font-display:\"Manrope\", system-ui, sans-serif;--radius-card:14px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mt-1{margin-top:var(--spacing)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.table{display:table}.w-full{width:100%}.max-w-\\[76\\%\\]{max-width:76%}.max-w-\\[420px\\]{max-width:420px}.flex-shrink{flex-shrink:1}.border-collapse{border-collapse:collapse}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.gap-1{gap:var(--spacing)}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-card{border-radius:var(--radius-card)}.rounded-br-md{border-bottom-right-radius:var(--radius-md)}.rounded-bl-md{border-bottom-left-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-border-hair{border-color:var(--color-border-hair)}.border-red{border-color:var(--color-red)}.border-red\\/25{border-color:#b91c1c40}@supports (color:color-mix(in lab, red, red)){.border-red\\/25{border-color:color-mix(in oklab, var(--color-red) 25%, transparent)}}.bg-blue{background-color:var(--color-blue)}.bg-surface{background-color:var(--color-surface)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\\.5{padding-inline:calc(var(--spacing) * 3.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\\.5{padding-block:calc(var(--spacing) * 2.5)}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-\\[11px\\]{font-size:11px}.text-\\[13px\\]{font-size:13px}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.tracking-\\[0\\.14em\\]{--tw-tracking:.14em;letter-spacing:.14em}.break-words{overflow-wrap:break-word}.whitespace-pre-wrap{white-space:pre-wrap}.text-green-strong{color:var(--color-green-strong)}.text-ink{color:var(--color-ink)}.text-ink-2{color:var(--color-ink-2)}.text-red{color:var(--color-red)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-resting{--tw-shadow:0 1px 2px var(--tw-shadow-color,#0f172a0d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow\\/border{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.\\!ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)!important}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}}@property --tw-rotate-x{syntax:\"*\";inherits:false}@property --tw-rotate-y{syntax:\"*\";inherits:false}@property --tw-rotate-z{syntax:\"*\";inherits:false}@property --tw-skew-x{syntax:\"*\";inherits:false}@property --tw-skew-y{syntax:\"*\";inherits:false}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-tracking{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-blur{syntax:\"*\";inherits:false}@property --tw-brightness{syntax:\"*\";inherits:false}@property --tw-contrast{syntax:\"*\";inherits:false}@property --tw-grayscale{syntax:\"*\";inherits:false}@property --tw-hue-rotate{syntax:\"*\";inherits:false}@property --tw-invert{syntax:\"*\";inherits:false}@property --tw-opacity{syntax:\"*\";inherits:false}@property --tw-saturate{syntax:\"*\";inherits:false}@property --tw-sepia{syntax:\"*\";inherits:false}@property --tw-drop-shadow{syntax:\"*\";inherits:false}@property --tw-drop-shadow-color{syntax:\"*\";inherits:false}@property --tw-drop-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:\"*\";inherits:false}@property --tw-ease{syntax:\"*\";inherits:false}";
3
3
  //# sourceMappingURL=tailwind.generated.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tailwind.generated.js","sourceRoot":"","sources":["../../src/ui/tailwind.generated.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,CAAC,MAAM,YAAY,GAAG,44ZAA44Z,CAAC"}
1
+ {"version":3,"file":"tailwind.generated.js","sourceRoot":"","sources":["../../src/ui/tailwind.generated.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,CAAC,MAAM,YAAY,GAAG,o8ZAAo8Z,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxisai/agent-harness",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "An on-device agent for the browser: a tool-calling harness over WebMCP and remote MCP, plus a drop-in chat UI. Decides whether to run a tool, ask for what's missing, or confirm before writing.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "files": [
27
27
  "dist",
28
+ "src",
28
29
  "docs/remote-providers.md",
29
30
  "docs/*.svg",
30
31
  "README.md",
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Renders typed-retrieval results as A2UI cards.
3
+ *
4
+ * The component count is fixed regardless of how many results there are: one
5
+ * result and twelve emit the same components, differing only in the data at
6
+ * `/results`. A result arriving late is therefore a data-model patch rather
7
+ * than new components, which keeps rendering incremental.
8
+ *
9
+ * Conditionality lives in the data rather than the components, so an entity
10
+ * with no rating simply has fewer entries. The basic catalog has no conditional
11
+ * rendering and none is needed.
12
+ */
13
+
14
+ import type { A2uiMessage } from './emit.js';
15
+
16
+ const CATALOG = 'https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json';
17
+
18
+ interface Component { id: string; component: string; [prop: string]: unknown }
19
+
20
+ /** One label and value line on a result card. */
21
+ export interface CardRow { label: string; value: string }
22
+ /** A short pill shown on a result card. */
23
+ export interface CardTag { label: string }
24
+ /** One record, in the shape a result card renders. */
25
+ export interface ResultCardData {
26
+ title: string;
27
+ sub: string;
28
+ tags: CardTag[];
29
+ rows: CardRow[];
30
+ }
31
+
32
+ const str = (v: unknown): string => (v == null ? '' : String(Array.isArray(v) ? v[0] : v));
33
+
34
+ const humanize = (s: string): string =>
35
+ s.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')
36
+ .replace(/^\w/, c => c.toUpperCase());
37
+
38
+ /**
39
+ * Normalise one schema.org entity into flat card data.
40
+ *
41
+ * Mirrors `_entityCard` in agent-fullscreen.ts, with the same
42
+ * title/type/rating rules and 4-row cap, so the A2UI surface and inline card show
43
+ * the same thing. If this ships, the two should share one implementation rather
44
+ * than drift; this copy exists to keep the sketch self-contained.
45
+ */
46
+ export function toCardData(entity: Record<string, unknown>): ResultCardData {
47
+ const title = str(entity.name ?? entity.title ?? entity.headline ?? entity['@id'] ?? 'Result');
48
+ const tags: CardTag[] = [];
49
+
50
+ const rawType = entity['@type'];
51
+ if (rawType) tags.push({ label: humanize(str(rawType)) });
52
+
53
+ const ar = entity.aggregateRating as Record<string, unknown> | undefined;
54
+ if (ar && typeof ar === 'object' && ar.ratingValue != null) {
55
+ const stars = '★'.repeat(Math.max(1, Math.min(5, Math.round(Number(ar.ratingValue) || 0))));
56
+ tags.push({
57
+ label: ar.reviewCount != null
58
+ ? `${stars} ${str(ar.ratingValue)} · ${str(ar.reviewCount)}`
59
+ : `${stars} ${str(ar.ratingValue)}`,
60
+ });
61
+ }
62
+
63
+ // 24/7 is only ever implied by round-the-clock opening hours.
64
+ const hours = entity.openingHoursSpecification;
65
+ if (Array.isArray(hours) && hours.some(h =>
66
+ h && typeof h === 'object'
67
+ && String((h as Record<string, unknown>).opens) === '00:00'
68
+ && String((h as Record<string, unknown>).closes) === '23:59')) {
69
+ tags.push({ label: '24/7' });
70
+ }
71
+
72
+ const desc = str(entity.description).trim();
73
+ const sub = desc.length > 150 ? desc.slice(0, 147) + '…' : desc;
74
+
75
+ const rows: CardRow[] = [];
76
+ const push = (label: string, value: unknown) => {
77
+ const v = str(value).trim();
78
+ if (v && rows.length < 4 && !rows.some(r => r.label === label)) {
79
+ rows.push({ label, value: v.length > 90 ? v.slice(0, 87) + '…' : v });
80
+ }
81
+ };
82
+
83
+ const addr = entity.address as Record<string, unknown> | undefined;
84
+ if (addr && typeof addr === 'object') {
85
+ const parts = [addr.streetAddress, addr.addressLocality, addr.addressRegion]
86
+ .filter(Boolean).map(str);
87
+ if (parts.length) push('Address', parts.join(', '));
88
+ }
89
+ for (const key of ['telephone', 'email', 'url']) {
90
+ const v = entity[key];
91
+ if (typeof v === 'string' || typeof v === 'number') {
92
+ push(humanize(key === 'url' ? 'website' : key), v);
93
+ }
94
+ }
95
+
96
+ return { title, sub, tags, rows };
97
+ }
98
+
99
+ /** Fixed component set — independent of how many results there are. */
100
+ function cardComponents(heading: string): Component[] {
101
+ return [
102
+ { id: 'root', component: 'Column', children: ['heading', 'results'] },
103
+ { id: 'heading', component: 'Text', text: heading, variant: 'body' },
104
+
105
+ // Outer list: one Card per entry in /results
106
+ { id: 'results', component: 'List',
107
+ children: { path: '/results', componentId: 'card_tpl' }, direction: 'vertical' },
108
+ { id: 'card_tpl', component: 'Card', child: 'card_body' },
109
+ { id: 'card_body', component: 'Column', children: ['card_title', 'card_tags', 'card_sub', 'card_rows'] },
110
+ // No bold: `weight` in this catalog is a flex weight (number), and Text
111
+ // offers only variant caption/body. Title emphasis is not expressible in
112
+ // the basic catalog: it needs a custom catalog or renderer styling.
113
+ { id: 'card_title', component: 'Text', text: '${title}', variant: 'body' },
114
+
115
+ // Relative paths resolve inside the current item's scope.
116
+ { id: 'card_tags', component: 'List',
117
+ children: { path: 'tags', componentId: 'tag_tpl' }, direction: 'horizontal' },
118
+ { id: 'tag_tpl', component: 'Text', text: '${label}', variant: 'caption' },
119
+
120
+ { id: 'card_sub', component: 'Text', text: '${sub}', variant: 'caption' },
121
+
122
+ { id: 'card_rows', component: 'List',
123
+ children: { path: 'rows', componentId: 'row_tpl' }, direction: 'vertical' },
124
+ { id: 'row_tpl', component: 'Row', children: ['row_label', 'row_value'] },
125
+ { id: 'row_label', component: 'Text', text: '${label}', variant: 'caption' },
126
+ { id: 'row_value', component: 'Text', text: '${value}', variant: 'caption' },
127
+ ];
128
+ }
129
+
130
+ /**
131
+ * Build a surface from card data that has already been normalised.
132
+ *
133
+ * agent-fullscreen has its own `_entityCard`, and it is the one that decides
134
+ * what a card shows. Taking its output rather than re-deriving from entities
135
+ * keeps the A2UI surface and the legacy inline card identical by construction.
136
+ */
137
+ export function emitCardsSurface(
138
+ heading: string,
139
+ results: ResultCardData[],
140
+ surfaceId = 'results'
141
+ ): A2uiMessage[] {
142
+ return [{
143
+ version: 'v1.0',
144
+ createSurface: {
145
+ surfaceId, catalogId: CATALOG,
146
+ components: cardComponents(heading),
147
+ dataModel: { results: results.slice(0, 12) },
148
+ },
149
+ }];
150
+ }
151
+
152
+ /**
153
+ * Build the A2UI messages for a set of results.
154
+ *
155
+ * @returns The messages to render, as one surface.
156
+ */
157
+ export function emitResultsSurface(
158
+ toolName: string,
159
+ entities: Record<string, unknown>[],
160
+ surfaceId = 'results'
161
+ ): A2uiMessage[] {
162
+ const results = entities.slice(0, 12).map(toCardData);
163
+ const heading = results.length
164
+ ? `${humanize(toolName)} — ${results.length} result${results.length === 1 ? '' : 's'}`
165
+ : `${humanize(toolName)} — no results`;
166
+ return emitCardsSurface(heading, results, surfaceId);
167
+ }
168
+
169
+ /** A late-arriving result appends to the list; no components are re-sent. */
170
+ export function emitAppendResult(
171
+ surfaceId: string, index: number, entity: Record<string, unknown>
172
+ ): A2uiMessage {
173
+ return {
174
+ version: 'v1.0',
175
+ updateDataModel: { surfaceId, path: `/results/${index}`, value: toCardData(entity) },
176
+ };
177
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Turns a parsed WebMCP action into an A2UI message stream.
3
+ *
4
+ * The model never authors A2UI: it picks a tool and fills parameters, and this
5
+ * deterministic code writes every envelope. Asking a small model to produce
6
+ * nested JSON with JSON Pointers and catalog constraints would be the least
7
+ * reliable part of an in-browser A2UI setup, and this avoids it.
8
+ *
9
+ * Transport is a function call, since the agent and renderer share a JavaScript
10
+ * context, which satisfies A2UI's ordered-delivery requirement for free.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ import type { NxisAction, NxisPropertyValueSpecification } from '../webmcp-contract.js';
16
+
17
+ const CATALOG = 'https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json';
18
+
19
+ /** One component in the adjacency list. */
20
+ interface Component {
21
+ id: string;
22
+ component: string;
23
+ [prop: string]: unknown;
24
+ }
25
+
26
+ /** One A2UI protocol message, authored against the v1.0 spec. */
27
+ export interface A2uiMessage {
28
+ version: 'v1.0';
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ /**
33
+ * A leading option that carries no label and reads as a prompt ("Select…",
34
+ * "Choose a service…") is the HTML `<option>` placeholder convention, not a
35
+ * real choice. A2UI has no placeholder slot on ChoicePicker, so it becomes the
36
+ * label and is dropped from `options`, since otherwise the agent can submit
37
+ * "Choose a service…" as a literal answer.
38
+ */
39
+ function splitPlaceholder(
40
+ options: NonNullable<NxisPropertyValueSpecification['nxis:valueOption']>
41
+ ): { placeholder?: string; rest: typeof options } {
42
+ const [first, ...rest] = options;
43
+ const looksLikePrompt = first && !first.name && /[…:]\s*$/.test(first.value);
44
+ return looksLikePrompt
45
+ ? { placeholder: first.value, rest }
46
+ : { rest: options };
47
+ }
48
+
49
+ /** Map one PropertyValueSpecification to a component, or null if it is data-only. */
50
+ function fieldComponent(pvs: NxisPropertyValueSpecification): Component | null {
51
+ const id = `field_${pvs.valueName}`;
52
+ const path = { path: `/form/${pvs.valueName}` };
53
+ const label = pvs.description ?? pvs.valueName;
54
+
55
+ // A prefilled field with no description is context the form carries, not
56
+ // something to ask a human about. It stays in the data model and is submitted;
57
+ // it just gets no component. `business` is exactly this.
58
+ if (!pvs.description && pvs.defaultValue !== undefined) return null;
59
+
60
+ if (pvs['nxis:valueOption']?.length) {
61
+ const { placeholder, rest } = splitPlaceholder(pvs['nxis:valueOption']);
62
+ return {
63
+ id,
64
+ component: 'ChoicePicker',
65
+ label: placeholder ? `${label} — ${placeholder}` : label,
66
+ variant: 'mutuallyExclusive',
67
+ options: rest.map((o: { name?: string; value: string }) => ({ label: o.name ?? o.value, value: o.value })),
68
+ value: path,
69
+ };
70
+ }
71
+
72
+ // valueMaxLength is absent on this action, so long-text is inferred from the
73
+ // field name. Crude, but wrong only in that a textarea renders as one line.
74
+ const longText = /description|message|notes|details/.test(pvs.valueName);
75
+ return {
76
+ id,
77
+ component: 'TextField',
78
+ label,
79
+ variant: longText ? 'longText' : 'shortText',
80
+ value: path,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Build the full message stream for an action.
86
+ *
87
+ * @param action - parsed action from nxis.js `readActions()`
88
+ * @param prefill - values already known (the device-local profile), keyed by valueName
89
+ */
90
+ export function emitActionSurface(
91
+ action: NxisAction,
92
+ prefill: Record<string, unknown> = {}
93
+ ): A2uiMessage[] {
94
+ const fields = action.input ?? [];
95
+
96
+ // Data model first: every field, whether or not it gets a component.
97
+ const dataModel: Record<string, unknown> = {};
98
+ for (const pvs of fields) {
99
+ dataModel[pvs.valueName] =
100
+ prefill[pvs.valueName] ?? pvs.defaultValue ?? '';
101
+ }
102
+
103
+ const fieldComponents: Component[] = fields
104
+ .map(fieldComponent)
105
+ .filter((c: Component | null): c is Component => c !== null);
106
+
107
+ const components: Component[] = [
108
+ // Inline components are cards. See stepper.ts.
109
+ { id: 'root', component: 'Card', child: 'body' },
110
+ { id: 'body', component: 'Column', children: [
111
+ 'heading', ...fieldComponents.map((c: Component) => c.id), 'submit',
112
+ ] },
113
+ { id: 'heading', component: 'Text', text: action.description ?? action.name, variant: 'body' },
114
+ ...fieldComponents,
115
+ { id: 'submit_label', component: 'Text', text: 'Submit' },
116
+ {
117
+ id: 'submit',
118
+ component: 'Button',
119
+ child: 'submit_label',
120
+ variant: 'primary',
121
+ // The event name carries the action @id, so the handler knows which
122
+ // WebMCP tool to invoke without a second lookup.
123
+ action: { event: { name: 'nxis.submit', context: { actionId: action['@id'] } } },
124
+ },
125
+ ];
126
+
127
+ const surfaceId = `action_${action.name}`;
128
+
129
+ return [
130
+ {
131
+ version: 'v1.0',
132
+ createSurface: { surfaceId, catalogId: CATALOG, components, dataModel: { form: dataModel } },
133
+ },
134
+ ];
135
+ }
136
+
137
+ /**
138
+ * Values the agent infers mid-conversation arrive as data-model patches rather
139
+ * than a rebuilt surface, so the form updates under the user without losing
140
+ * focus or anything they have already typed.
141
+ */
142
+ export function emitPrefill(surfaceId: string, valueName: string, value: unknown): A2uiMessage {
143
+ return {
144
+ version: 'v1.0',
145
+ updateDataModel: { surfaceId, path: `/form/${valueName}`, value },
146
+ };
147
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Builds an A2UI form from a tool's JSON Schema.
3
+ *
4
+ * The general counterpart to `emit.ts`, which starts from a WebMCP action
5
+ * extracted from page JSON-LD. This one starts from the schema that remote MCP
6
+ * tools expose and that {@link toolSchema} normalises to, producing the same
7
+ * output surface from a different input.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { JsonSchema } from '../types.js';
13
+ import type { A2uiMessage } from './emit.js';
14
+
15
+ const CATALOG = 'https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json';
16
+
17
+ interface Component { id: string; component: string; [prop: string]: unknown }
18
+
19
+ const humanize = (s: string): string =>
20
+ s.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')
21
+ .replace(/^\w/, c => c.toUpperCase());
22
+
23
+ /** Enum values may be bare scalars or {const,title}-style objects. */
24
+ function enumOptions(values: unknown[]): { label: string; value: string }[] {
25
+ return values.map(v => {
26
+ if (v && typeof v === 'object') {
27
+ const o = v as Record<string, unknown>;
28
+ const value = String(o.const ?? o.value ?? '');
29
+ return { label: String(o.title ?? o.name ?? value), value };
30
+ }
31
+ return { label: humanize(String(v)), value: String(v) };
32
+ });
33
+ }
34
+
35
+ function fieldComponent(key: string, spec: JsonSchema, required: boolean): Component {
36
+ const id = `f_${key}`;
37
+ const path = { path: `/form/${key}` };
38
+ // The required marker is part of the label, since the basic catalog has no
39
+ // requiredness of its own. Validation stays with the caller regardless.
40
+ const label = `${spec.description || humanize(key)}${required ? ' *' : ''}`;
41
+
42
+ const enumValues = spec.enum ?? spec.items?.enum;
43
+ if (Array.isArray(enumValues) && enumValues.length) {
44
+ return {
45
+ id, component: 'ChoicePicker', label,
46
+ variant: spec.type === 'array' ? 'multipleSelection' : 'mutuallyExclusive',
47
+ options: enumOptions(enumValues),
48
+ value: path,
49
+ };
50
+ }
51
+
52
+ if (spec.type === 'boolean') return { id, component: 'CheckBox', label, value: path };
53
+
54
+ const variant =
55
+ spec.type === 'number' || spec.type === 'integer' ? 'number'
56
+ : spec.format === 'password' ? 'obscured'
57
+ : /description|message|notes|details|body/.test(key) ? 'longText'
58
+ : 'shortText';
59
+
60
+ return { id, component: 'TextField', label, variant, value: path };
61
+ }
62
+
63
+ /**
64
+ * Build the form surface for a tool.
65
+ *
66
+ * Submit and Cancel dispatch action events carrying the tool name, so one
67
+ * handler can serve every form without tracking which surface is live.
68
+ */
69
+ export function emitToolForm(
70
+ toolName: string,
71
+ description: string,
72
+ schema: JsonSchema,
73
+ values: Record<string, unknown> = {},
74
+ surfaceId = 'toolform',
75
+ ): A2uiMessage[] {
76
+ const props = schema.properties ?? {};
77
+ const required = new Set(schema.required ?? []);
78
+
79
+ const fields = Object.entries(props).map(([key, spec]) =>
80
+ fieldComponent(key, spec ?? {}, required.has(key)));
81
+
82
+ // Seed by type: a CheckBox bound to '' is not merely empty, it is the wrong
83
+ // type for the component and does not round-trip through the data model.
84
+ const dataModel: Record<string, unknown> = {};
85
+ for (const [key, spec] of Object.entries(props)) {
86
+ const fallback = spec?.type === 'boolean' ? false
87
+ : spec?.type === 'number' || spec?.type === 'integer' ? null
88
+ : '';
89
+ dataModel[key] = values[key] ?? fallback;
90
+ }
91
+
92
+ const components: Component[] = [
93
+ // Inline components are cards. See stepper.ts.
94
+ { id: 'root', component: 'Card', child: 'body' },
95
+ { id: 'body', component: 'Column',
96
+ children: ['eyebrow', 'title', ...(description ? ['desc'] : []), ...fields.map(f => f.id), 'actions'] },
97
+ { id: 'eyebrow', component: 'Text', text: 'DETAILS NEEDED', variant: 'caption' },
98
+ // Body, not a heading: markdown headings would pick up
99
+ // --a2ui-font-family-title, but only with a MarkdownRenderer in context.
100
+ // Without one the raw "###" is printed. See stepper.ts.
101
+ { id: 'title', component: 'Text', text: humanize(toolName), variant: 'body' },
102
+ ...(description ? [{ id: 'desc', component: 'Text', text: description, variant: 'caption' }] : []),
103
+ ...fields,
104
+ { id: 'actions', component: 'Row', children: ['cancel', 'submit'], justify: 'end' },
105
+ { id: 'cancel_label', component: 'Text', text: 'Cancel' },
106
+ { id: 'cancel', component: 'Button', child: 'cancel_label',
107
+ action: { event: { name: 'nxis.cancel', context: { tool: toolName } } } },
108
+ { id: 'submit_label', component: 'Text', text: 'Submit' },
109
+ { id: 'submit', component: 'Button', child: 'submit_label', variant: 'primary',
110
+ action: { event: { name: 'nxis.submit', context: { tool: toolName } } } },
111
+ ];
112
+
113
+ return [{
114
+ version: 'v1.0',
115
+ createSurface: { surfaceId, catalogId: CATALOG, components, dataModel: { form: dataModel } },
116
+ }];
117
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Mounts A2UI messages into a container element.
3
+ *
4
+ * Wraps the emit, downgrade, process and mount sequence every A2UI surface
5
+ * needs, so callers deal in messages and a container rather than in message
6
+ * processor lifecycles.
7
+ *
8
+ * Works inside a shadow root, since `<a2ui-surface>` is a custom element and
9
+ * upgrades wherever it is appended.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import { MessageProcessor } from '@a2ui/web_core/v0_9';
15
+ import { basicCatalog } from '@a2ui/lit/v0_9';
16
+ import '@a2ui/lit/v0_9';
17
+
18
+ import type { A2uiMessage } from './emit.js';
19
+ import { toV09 } from './to-v09.js';
20
+
21
+ /** A live A2UI surface, and the handle used to tear it down. */
22
+ export interface MountedSurface {
23
+ /** The processor, for reading the live data model or sending further messages. */
24
+ processor: any;
25
+ /** The mounted custom element. */
26
+ element: HTMLElement;
27
+ /** Current value of one top-level data-model key on this surface. */
28
+ read(key: string): unknown;
29
+ /** Push a v1.0 message (e.g. a prefill patch) into the live surface. */
30
+ send(messages: A2uiMessage[]): void;
31
+ dispose(): void;
32
+ }
33
+
34
+ /**
35
+ * Render `messages` into `container`.
36
+ *
37
+ * @param onAction - Called when the renderer dispatches a button action. The
38
+ * event carries whatever `action.event.context` the emitter attached.
39
+ */
40
+ export function mountSurface(
41
+ container: HTMLElement,
42
+ messages: A2uiMessage[],
43
+ onAction?: (event: any) => void,
44
+ ): MountedSurface | null {
45
+ const processor = new MessageProcessor(
46
+ [basicCatalog as any],
47
+ (event: any) => onAction?.(event),
48
+ );
49
+
50
+ processor.processMessages(toV09(messages) as any);
51
+
52
+ // One surface per mount; the first is the one just created.
53
+ const first = processor.model.surfacesMap.entries().next();
54
+ if (first.done) return null;
55
+ const [, surface] = first.value as [string, unknown];
56
+
57
+ const element = document.createElement('a2ui-surface') as any;
58
+ element.surface = surface;
59
+ container.appendChild(element);
60
+
61
+ return {
62
+ processor,
63
+ element,
64
+ // DataModel exposes get(path); reading the surface's own model avoids
65
+ // depending on the sendDataModel flag that getClientDataModel requires.
66
+ read: (key: string) => (surface as any)?.dataModel?.get(`/${key}`),
67
+ send: (msgs: A2uiMessage[]) => processor.processMessages(toV09(msgs) as any),
68
+ dispose: () => {
69
+ element.remove();
70
+ (processor as any).dispose?.();
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * A2UI surface emitters.
3
+ *
4
+ * Turn a tool's schema, a clarification plan, or typed-retrieval results into
5
+ * A2UI messages, for rendering through an A2UI renderer rather than the
6
+ * packaged element.
7
+ *
8
+ * Messages are authored against the v1.0 spec and downgraded by {@link toV09}
9
+ * before rendering, since published renderers support v0.9 and earlier.
10
+ *
11
+ * @module
12
+ */
13
+ export { emitActionSurface, emitPrefill } from './emit.js';
14
+ export type { A2uiMessage } from './emit.js';
15
+ export { emitToolForm } from './form.js';
16
+ export { emitAsk, emitStep, emitConfirm } from './stepper.js';
17
+ export { emitCardsSurface, emitResultsSurface, emitAppendResult, toCardData } from './cards.js';
18
+ export type { ResultCardData, CardRow, CardTag } from './cards.js';
19
+ export { mountSurface } from './host.js';
20
+ export type { MountedSurface } from './host.js';
21
+ export { toV09 } from './to-v09.js';
22
+ export { A2UI_THEME_CSS, NXIS_PRIMARY } from './theme.js';