bitboss-ui 3.0.0-beta.41 → 3.0.0-beta.43

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 (372) hide show
  1. package/README.md +57 -7
  2. package/bin/bitboss-ui-mcp.mjs +1034 -85
  3. package/bin/bitboss-ui.mjs +16 -3
  4. package/dist/ai/BbAccordion.md +4 -4
  5. package/dist/ai/BbAlert.md +4 -4
  6. package/dist/ai/BbAsterisk.md +11 -7
  7. package/dist/ai/BbAvatar.md +2 -2
  8. package/dist/ai/BbBadge.md +7 -4
  9. package/dist/ai/BbBadgeButton.md +4 -1
  10. package/dist/ai/BbBaseButton.md +2 -2
  11. package/dist/ai/BbBreadcrumbs.md +22 -16
  12. package/dist/ai/BbButton.md +32 -4
  13. package/dist/ai/BbCalendar.md +12 -4
  14. package/dist/ai/BbCheckbox.md +13 -4
  15. package/dist/ai/BbCheckboxGroup.md +4 -3
  16. package/dist/ai/BbColorInput.md +6 -4
  17. package/dist/ai/BbColorPalette.md +13 -7
  18. package/dist/ai/BbConfirm.md +2 -2
  19. package/dist/ai/BbDatePicker.md +4 -4
  20. package/dist/ai/BbDatePickerInput.md +15 -8
  21. package/dist/ai/BbDialog.md +2 -2
  22. package/dist/ai/BbDropdown.md +12 -8
  23. package/dist/ai/BbDropdownButton.md +9 -8
  24. package/dist/ai/BbDropzone.md +1 -1
  25. package/dist/ai/BbForm.md +124 -39
  26. package/dist/ai/BbIcon.md +3 -1
  27. package/dist/ai/BbNumberInput.md +12 -8
  28. package/dist/ai/BbOffCanvas.md +3 -3
  29. package/dist/ai/BbPagination.md +5 -2
  30. package/dist/ai/BbPopover.md +4 -3
  31. package/dist/ai/BbRadio.md +5 -4
  32. package/dist/ai/BbRadioGroup.md +4 -3
  33. package/dist/ai/BbRating.md +19 -10
  34. package/dist/ai/BbSelect.md +32 -18
  35. package/dist/ai/BbSelectPopover.md +12 -6
  36. package/dist/ai/BbSlider.md +11 -5
  37. package/dist/ai/BbSmoothHeight.md +3 -2
  38. package/dist/ai/BbSpinner.md +1 -1
  39. package/dist/ai/BbSwitch.md +12 -5
  40. package/dist/ai/BbSwitchGroup.md +4 -3
  41. package/dist/ai/BbTable.md +144 -58
  42. package/dist/ai/BbTabs.md +9 -7
  43. package/dist/ai/BbTabsList.md +3 -3
  44. package/dist/ai/BbTabsPanels.md +1 -1
  45. package/dist/ai/BbTabsRoot.md +1 -1
  46. package/dist/ai/BbTag.md +16 -9
  47. package/dist/ai/BbTextInput.md +17 -4
  48. package/dist/ai/BbTextarea.md +21 -14
  49. package/dist/ai/BbTimePicker.md +7 -4
  50. package/dist/ai/BbTimePickerInput.md +10 -5
  51. package/dist/ai/BbToast.md +2 -2
  52. package/dist/ai/BbTooltip.md +2 -2
  53. package/dist/ai/BbTree.md +1 -1
  54. package/dist/ai/ChipsBox.md +6 -3
  55. package/dist/ai/ClearableButton.md +1 -1
  56. package/dist/ai/CommonField.md +10 -1
  57. package/dist/ai/CommonFieldInput.md +3 -2
  58. package/dist/ai/CommonTimeSelector.md +2 -2
  59. package/dist/ai/ErrorIcon.md +1 -1
  60. package/dist/ai/FlatListBox.md +3 -3
  61. package/dist/ai/GroupedListBox.md +3 -3
  62. package/dist/ai/ListBox.md +3 -3
  63. package/dist/ai/WarningIcon.md +1 -1
  64. package/dist/ai/changelog.json +1109 -8
  65. package/dist/ai/components.json +298 -57860
  66. package/dist/ai/composables/useBbFormContext.md +19 -19
  67. package/dist/ai/composables/useBbFormField.md +14 -3
  68. package/dist/ai/design-tokens.json +46 -19
  69. package/dist/ai/design-tokens.manifest.yaml +5 -5
  70. package/dist/ai/guides/agent-contract.md +15 -4
  71. package/dist/ai/guides/ai-router.md +26 -24
  72. package/dist/ai/guides/design-language.md +13 -10
  73. package/dist/ai/guides/design-tokens.md +130 -43
  74. package/dist/ai/guides/icons-policy.md +3 -5
  75. package/dist/ai/guides/installation-and-plugin-setup.md +9 -5
  76. package/dist/ai/guides/migration/components/bb-breadcrumbs.md +1 -0
  77. package/dist/ai/guides/migration/components/bb-slider.md +5 -0
  78. package/dist/ai/guides/migration/v2-to-v3.md +56 -17
  79. package/dist/ai/guides/migration/v3-beta-tooling.md +79 -5
  80. package/dist/ai/guides/passthrough.md +19 -9
  81. package/dist/ai/guides/preset-components.md +6 -3
  82. package/dist/ai/guides/ssr-prefill.md +27 -4
  83. package/dist/ai/guides/validated-forms.md +21 -16
  84. package/dist/ai/guides/validation-providers.md +36 -23
  85. package/dist/ai/index.md +6 -6
  86. package/dist/ai/manifest/components/BbAccordion.json +41 -0
  87. package/dist/ai/manifest/components/BbAccordion.tree.json +25 -0
  88. package/dist/ai/manifest/components/BbAlert.json +56 -0
  89. package/dist/ai/manifest/components/BbAlert.tree.json +80 -0
  90. package/dist/ai/manifest/components/BbAsterisk.json +26 -0
  91. package/dist/ai/manifest/components/BbAsterisk.tree.json +13 -0
  92. package/dist/ai/manifest/components/BbAvatar.json +38 -0
  93. package/dist/ai/manifest/components/BbAvatar.tree.json +39 -0
  94. package/dist/ai/manifest/components/BbBadge.json +50 -0
  95. package/dist/ai/manifest/components/BbBadge.tree.json +86 -0
  96. package/dist/ai/manifest/components/BbBadgeButton.json +67 -0
  97. package/dist/ai/manifest/components/BbBadgeButton.tree.json +32 -0
  98. package/dist/ai/manifest/components/BbBaseButton.json +69 -0
  99. package/dist/ai/manifest/components/BbBaseButton.tree.json +25 -0
  100. package/dist/ai/manifest/components/BbBaseCheckboxIcon.json +46 -0
  101. package/dist/ai/manifest/components/BbBaseCheckboxIcon.tree.json +41 -0
  102. package/dist/ai/manifest/components/BbBaseRadioIcon.json +44 -0
  103. package/dist/ai/manifest/components/BbBaseRadioIcon.tree.json +36 -0
  104. package/dist/ai/manifest/components/BbBaseSwitchIcon.json +46 -0
  105. package/dist/ai/manifest/components/BbBaseSwitchIcon.tree.json +44 -0
  106. package/dist/ai/manifest/components/BbBreadcrumbs.json +52 -0
  107. package/dist/ai/manifest/components/BbBreadcrumbs.tree.json +45 -0
  108. package/dist/ai/manifest/components/BbButton.json +101 -0
  109. package/dist/ai/manifest/components/BbButton.tree.json +206 -0
  110. package/dist/ai/manifest/components/BbCalendar.json +84 -0
  111. package/dist/ai/manifest/components/BbCalendar.tree.json +99 -0
  112. package/dist/ai/manifest/components/BbCheckbox.json +97 -0
  113. package/dist/ai/manifest/components/BbCheckbox.tree.json +22 -0
  114. package/dist/ai/manifest/components/BbCheckboxGroup.json +127 -0
  115. package/dist/ai/manifest/components/BbCheckboxGroup.tree.json +52 -0
  116. package/dist/ai/manifest/components/BbCollapsible.json +32 -0
  117. package/dist/ai/manifest/components/BbCollapsible.tree.json +17 -0
  118. package/dist/ai/manifest/components/BbColorInput.json +131 -0
  119. package/dist/ai/manifest/components/BbColorInput.tree.json +37 -0
  120. package/dist/ai/manifest/components/BbColorPalette.json +73 -0
  121. package/dist/ai/manifest/components/BbColorPalette.tree.json +55 -0
  122. package/dist/ai/manifest/components/BbConfirm.json +34 -0
  123. package/dist/ai/manifest/components/BbConfirm.tree.json +30 -0
  124. package/dist/ai/manifest/components/BbConfirmPortal.json +18 -0
  125. package/dist/ai/manifest/components/BbDatePicker.json +106 -0
  126. package/dist/ai/manifest/components/BbDatePicker.tree.json +54 -0
  127. package/dist/ai/manifest/components/BbDatePickerInput.json +163 -0
  128. package/dist/ai/manifest/components/BbDatePickerInput.tree.json +50 -0
  129. package/dist/ai/manifest/components/BbDialog.json +76 -0
  130. package/dist/ai/manifest/components/BbDialog.tree.json +74 -0
  131. package/dist/ai/manifest/components/BbDropdown.json +98 -0
  132. package/dist/ai/manifest/components/BbDropdown.tree.json +120 -0
  133. package/dist/ai/manifest/components/BbDropdownButton.json +104 -0
  134. package/dist/ai/manifest/components/BbDropdownButton.tree.json +28 -0
  135. package/dist/ai/manifest/components/BbDropdownGroup.json +25 -0
  136. package/dist/ai/manifest/components/BbDropzone.json +66 -0
  137. package/dist/ai/manifest/components/BbDropzone.tree.json +35 -0
  138. package/dist/ai/manifest/components/BbForm.json +41 -0
  139. package/dist/ai/manifest/components/BbIcon.json +33 -0
  140. package/dist/ai/manifest/components/BbIcon.tree.json +19 -0
  141. package/dist/ai/manifest/components/BbIndicator.json +39 -0
  142. package/dist/ai/manifest/components/BbIndicator.tree.json +67 -0
  143. package/dist/ai/manifest/components/BbNumberInput.json +121 -0
  144. package/dist/ai/manifest/components/BbNumberInput.tree.json +27 -0
  145. package/dist/ai/manifest/components/BbOffCanvas.json +72 -0
  146. package/dist/ai/manifest/components/BbOffCanvas.tree.json +115 -0
  147. package/dist/ai/manifest/components/BbPagination.json +61 -0
  148. package/dist/ai/manifest/components/BbPagination.tree.json +69 -0
  149. package/dist/ai/manifest/components/BbPopover.json +81 -0
  150. package/dist/ai/manifest/components/BbPopover.tree.json +36 -0
  151. package/dist/ai/manifest/components/BbProgress.json +31 -0
  152. package/dist/ai/manifest/components/BbProgress.tree.json +25 -0
  153. package/dist/ai/manifest/components/BbPtScope.json +21 -0
  154. package/dist/ai/manifest/components/BbRadio.json +86 -0
  155. package/dist/ai/manifest/components/BbRadio.tree.json +22 -0
  156. package/dist/ai/manifest/components/BbRadioGroup.json +126 -0
  157. package/dist/ai/manifest/components/BbRadioGroup.tree.json +52 -0
  158. package/dist/ai/manifest/components/BbRating.json +98 -0
  159. package/dist/ai/manifest/components/BbRating.tree.json +55 -0
  160. package/dist/ai/manifest/components/BbSelect.json +170 -0
  161. package/dist/ai/manifest/components/BbSelect.tree.json +53 -0
  162. package/dist/ai/manifest/components/BbSelectPopover.json +122 -0
  163. package/dist/ai/manifest/components/BbSelectPopover.tree.json +108 -0
  164. package/dist/ai/manifest/components/BbSlider.json +107 -0
  165. package/dist/ai/manifest/components/BbSlider.tree.json +89 -0
  166. package/dist/ai/manifest/components/BbSmoothHeight.json +31 -0
  167. package/dist/ai/manifest/components/BbSmoothHeight.tree.json +16 -0
  168. package/dist/ai/manifest/components/BbSpinner.json +31 -0
  169. package/dist/ai/manifest/components/BbSpinner.tree.json +31 -0
  170. package/dist/ai/manifest/components/BbSwitch.json +97 -0
  171. package/dist/ai/manifest/components/BbSwitch.tree.json +22 -0
  172. package/dist/ai/manifest/components/BbSwitchGroup.json +127 -0
  173. package/dist/ai/manifest/components/BbSwitchGroup.tree.json +52 -0
  174. package/dist/ai/manifest/components/BbTable.json +150 -0
  175. package/dist/ai/manifest/components/BbTable.tree.json +199 -0
  176. package/dist/ai/manifest/components/BbTabs.json +83 -0
  177. package/dist/ai/manifest/components/BbTabs.tree.json +88 -0
  178. package/dist/ai/manifest/components/BbTabsList.json +47 -0
  179. package/dist/ai/manifest/components/BbTabsList.tree.json +94 -0
  180. package/dist/ai/manifest/components/BbTabsPanels.json +32 -0
  181. package/dist/ai/manifest/components/BbTabsPanels.tree.json +69 -0
  182. package/dist/ai/manifest/components/BbTabsRoot.json +61 -0
  183. package/dist/ai/manifest/components/BbTag.json +128 -0
  184. package/dist/ai/manifest/components/BbTag.tree.json +40 -0
  185. package/dist/ai/manifest/components/BbTextInput.json +122 -0
  186. package/dist/ai/manifest/components/BbTextInput.tree.json +27 -0
  187. package/dist/ai/manifest/components/BbTextarea.json +117 -0
  188. package/dist/ai/manifest/components/BbTextarea.tree.json +37 -0
  189. package/dist/ai/manifest/components/BbTimePicker.json +76 -0
  190. package/dist/ai/manifest/components/BbTimePicker.tree.json +37 -0
  191. package/dist/ai/manifest/components/BbTimePickerInput.json +131 -0
  192. package/dist/ai/manifest/components/BbTimePickerInput.tree.json +44 -0
  193. package/dist/ai/manifest/components/BbToast.json +43 -0
  194. package/dist/ai/manifest/components/BbToast.tree.json +91 -0
  195. package/dist/ai/manifest/components/BbToastPortal.json +18 -0
  196. package/dist/ai/manifest/components/BbTooltip.json +60 -0
  197. package/dist/ai/manifest/components/BbTooltip.tree.json +41 -0
  198. package/dist/ai/manifest/components/BbTree.json +51 -0
  199. package/dist/ai/manifest/components/BbTree.tree.json +31 -0
  200. package/dist/ai/manifest/components/ChipsBox.json +32 -0
  201. package/dist/ai/manifest/components/ChipsBox.tree.json +11 -0
  202. package/dist/ai/manifest/components/ClearableButton.json +19 -0
  203. package/dist/ai/manifest/components/ClearableButton.tree.json +19 -0
  204. package/dist/ai/manifest/components/CommaBox.json +30 -0
  205. package/dist/ai/manifest/components/CommonField.json +53 -0
  206. package/dist/ai/manifest/components/CommonField.tree.json +92 -0
  207. package/dist/ai/manifest/components/CommonFieldInput.json +47 -0
  208. package/dist/ai/manifest/components/CommonFieldInput.tree.json +95 -0
  209. package/dist/ai/manifest/components/CommonPopover.json +44 -0
  210. package/dist/ai/manifest/components/CommonPopover.tree.json +26 -0
  211. package/dist/ai/manifest/components/CommonTimeSelector.json +31 -0
  212. package/dist/ai/manifest/components/CommonTimeSelector.tree.json +43 -0
  213. package/dist/ai/manifest/components/ErrorIcon.json +17 -0
  214. package/dist/ai/manifest/components/ErrorIcon.tree.json +22 -0
  215. package/dist/ai/manifest/components/FlatListBox.json +42 -0
  216. package/dist/ai/manifest/components/FlatListBox.tree.json +15 -0
  217. package/dist/ai/manifest/components/GroupedListBox.json +44 -0
  218. package/dist/ai/manifest/components/GroupedListBox.tree.json +15 -0
  219. package/dist/ai/manifest/components/GuidesComponent.json +17 -0
  220. package/dist/ai/manifest/components/ListBox.json +42 -0
  221. package/dist/ai/manifest/components/ListBox.tree.json +9 -0
  222. package/dist/ai/manifest/components/OptionsContainer.json +41 -0
  223. package/dist/ai/manifest/components/WarningIcon.json +17 -0
  224. package/dist/ai/manifest/components/WarningIcon.tree.json +22 -0
  225. package/dist/ai/manifest/index.json +118 -0
  226. package/dist/ai/manifest/meta.json +430 -0
  227. package/dist/ai/manifest/types.advanced.json +55 -0
  228. package/dist/ai/manifest/types.api.json +43 -0
  229. package/dist/ai/manifest/types.wrapper.json +115 -0
  230. package/dist/ai/recipes/inertia/inline-edit-workspace.md +1 -1
  231. package/dist/ai/recipes/inertia/record-form.md +1 -1
  232. package/dist/ai/recipes/inertia/settings-workspace.md +109 -79
  233. package/dist/ai/recipes/nuxt/inline-edit-workspace.md +1 -1
  234. package/dist/ai/recipes/nuxt/record-form.md +1 -1
  235. package/dist/ai/recipes/nuxt/settings-workspace.md +108 -76
  236. package/dist/ai/recipes/vue/inline-edit-workspace.md +1 -1
  237. package/dist/ai/recipes/vue/record-form.md +1 -1
  238. package/dist/ai/recipes/vue/settings-workspace.md +108 -76
  239. package/dist/ai/source/BbBadge.md +34 -0
  240. package/dist/ai/source/BbBadgeButton.md +34 -0
  241. package/dist/ai/source/BbBaseCheckboxIcon.md +3 -1
  242. package/dist/ai/source/BbBaseRadioIcon.md +2 -1
  243. package/dist/ai/source/BbBaseSwitchIcon.md +7 -5
  244. package/dist/ai/source/BbBreadcrumbs.md +44 -17
  245. package/dist/ai/source/BbButton.md +66 -6
  246. package/dist/ai/source/BbCalendar.md +117 -6
  247. package/dist/ai/source/BbCheckbox.md +24 -5
  248. package/dist/ai/source/BbCheckboxGroup.md +9 -1
  249. package/dist/ai/source/BbColorInput.md +30 -0
  250. package/dist/ai/source/BbColorPalette.md +54 -7
  251. package/dist/ai/source/BbDatePickerInput.md +91 -29
  252. package/dist/ai/source/BbDropdownButton.md +29 -1
  253. package/dist/ai/source/BbForm.md +130 -27
  254. package/dist/ai/source/BbIcon.md +5 -2
  255. package/dist/ai/source/BbNumberInput.md +25 -8
  256. package/dist/ai/source/BbPagination.md +18 -0
  257. package/dist/ai/source/BbPopover.md +12 -0
  258. package/dist/ai/source/BbRadio.md +2 -2
  259. package/dist/ai/source/BbRadioGroup.md +11 -1
  260. package/dist/ai/source/BbRating.md +21 -5
  261. package/dist/ai/source/BbSelect.md +17 -5
  262. package/dist/ai/source/BbSelectPopover.md +72 -2
  263. package/dist/ai/source/BbSlider.md +10 -9
  264. package/dist/ai/source/BbSmoothHeight.md +6 -2
  265. package/dist/ai/source/BbSwitch.md +22 -3
  266. package/dist/ai/source/BbSwitchGroup.md +9 -1
  267. package/dist/ai/source/BbTable.md +181 -2
  268. package/dist/ai/source/BbTag.md +73 -29
  269. package/dist/ai/source/BbTextInput.md +32 -1
  270. package/dist/ai/source/BbTextarea.md +39 -45
  271. package/dist/ai/source/BbTimePickerInput.md +40 -2
  272. package/dist/ai/source/BbTree.md +15 -3
  273. package/dist/ai/source/CommonField.md +103 -0
  274. package/dist/ai/source/CommonFieldInput.md +26 -5
  275. package/dist/ai/source/CommonTimeSelector.md +10 -0
  276. package/dist/ai/source/FlatListBox.md +4 -0
  277. package/dist/ai/source/GroupedListBox.md +4 -0
  278. package/dist/ai/source/ListBox.md +4 -0
  279. package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +109 -107
  280. package/dist/components/BbBreadcrumbs/types.d.ts +1 -1
  281. package/dist/components/BbCalendar/BbCalendar.vue_vue_type_script_setup_true_lang.js +151 -149
  282. package/dist/components/BbCalendar/CalendarYearPanel.vue_vue_type_script_setup_true_lang.js +51 -48
  283. package/dist/components/BbCalendar/types.d.ts +5 -1
  284. package/dist/components/BbCheckbox/BbCheckbox.vue_vue_type_script_setup_true_lang.js +61 -57
  285. package/dist/components/BbCheckbox/types.d.ts +2 -2
  286. package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +43 -38
  287. package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +53 -51
  288. package/dist/components/BbColorPalette/BbColorPalette.vue_vue_type_script_setup_true_lang.js +257 -242
  289. package/dist/components/BbColorPalette/types.d.ts +10 -3
  290. package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +320 -305
  291. package/dist/components/BbDatePickerInput/types.d.ts +3 -3
  292. package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +64 -57
  293. package/dist/components/BbForm/BbForm.vue.d.ts +30 -12
  294. package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +170 -139
  295. package/dist/components/BbForm/BbFormCustomField.js +34 -28
  296. package/dist/components/BbForm/bindFormField.d.ts +27 -0
  297. package/dist/components/BbForm/bindFormField.js +9 -7
  298. package/dist/components/BbForm/formFields.d.ts +10 -2
  299. package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +24 -23
  300. package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +72 -67
  301. package/dist/components/BbNumberInput/types.d.ts +3 -2
  302. package/dist/components/BbRadio/types.d.ts +2 -2
  303. package/dist/components/BbRadioGroup/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +36 -29
  304. package/dist/components/BbRating/BbRating.vue_vue_type_script_setup_true_lang.js +26 -22
  305. package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +108 -104
  306. package/dist/components/BbSelect/types.d.ts +9 -3
  307. package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +376 -357
  308. package/dist/components/BbSelectPopover/sheetChipAttrs.d.ts +18 -0
  309. package/dist/components/BbSelectPopover/sheetChipAttrs.js +5 -0
  310. package/dist/components/BbSelectPopover/types.d.ts +3 -1
  311. package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +99 -102
  312. package/dist/components/BbSlider/types.d.ts +0 -4
  313. package/dist/components/BbSwitch/BbSwitch.vue_vue_type_script_setup_true_lang.js +71 -67
  314. package/dist/components/BbSwitchGroup/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +75 -70
  315. package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +803 -765
  316. package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +138 -124
  317. package/dist/components/BbTag/types.d.ts +2 -1
  318. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +88 -80
  319. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +71 -65
  320. package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +216 -203
  321. package/dist/components/BbTree/BbTree.vue_vue_type_script_setup_true_lang.js +2 -4
  322. package/dist/components/CommonField/CommonField.vue_vue_type_script_setup_true_lang.js +92 -86
  323. package/dist/components/CommonField/types.d.ts +7 -0
  324. package/dist/components/CommonTimeSelector/CommonTimeSelector.vue_vue_type_script_setup_true_lang.js +4 -0
  325. package/dist/components/FlatListBox/types.d.ts +4 -0
  326. package/dist/components/GroupedListBox/types.d.ts +4 -0
  327. package/dist/components/ListBox/types.d.ts +4 -0
  328. package/dist/composables/ssrMarker.d.ts +15 -0
  329. package/dist/composables/ssrMarker.js +19 -0
  330. package/dist/composables/useBaseOptions.js +7 -1
  331. package/dist/composables/useBbFormContext.d.ts +15 -4
  332. package/dist/composables/useBbFormContext.js +18 -5
  333. package/dist/composables/useBbFormField.d.ts +13 -2
  334. package/dist/composables/useBbFormField.js +40 -24
  335. package/dist/composables/useFormBinding.d.ts +11 -3
  336. package/dist/composables/useFormBinding.js +31 -21
  337. package/dist/composables/useItemsGetter.d.ts +28 -0
  338. package/dist/composables/useItemsGetter.js +45 -33
  339. package/dist/composables/usePrefill.js +9 -8
  340. package/dist/composables/useServerPrefetch.d.ts +28 -0
  341. package/dist/composables/useServerPrefetch.js +9 -0
  342. package/dist/composables/useSsrState.d.ts +9 -1
  343. package/dist/deprecation/ai-deprecations.json.d.ts +35 -0
  344. package/dist/deprecation/ai-deprecations.json.js +1 -1
  345. package/dist/deprecation/removedProps.js +2 -2
  346. package/dist/llms-full.txt +1514 -772
  347. package/dist/llms-medium.txt +65 -45
  348. package/dist/llms.txt +2 -2
  349. package/dist/styles.css +2 -2
  350. package/dist/utilities/functions/toggleInput.d.ts +16 -0
  351. package/dist/utilities/functions/toggleInput.js +7 -2
  352. package/dist/utilities/functions/warnRemovedAttrs.d.ts +1 -1
  353. package/dist/utilities/functions/warnRemovedAttrs.js +11 -1
  354. package/dist/utilities/misc/ssrStateInjectionKey.d.ts +8 -3
  355. package/dist/utilities/misc/ssrValue.d.ts +20 -0
  356. package/dist/utilities/misc/ssrValue.js +7 -0
  357. package/dist/utils/versionCheck.js +1 -1
  358. package/dist/vite.js +175 -168
  359. package/llms.txt +2 -2
  360. package/package.json +3 -2
  361. package/scripts/lib/agent-manifest.mjs +509 -0
  362. package/scripts/lib/css-color-resolve.mjs +128 -4
  363. package/scripts/lib/design-token-manifest.mjs +11 -4
  364. package/scripts/lib/doc-sections.mjs +5 -3
  365. package/scripts/lib/eslint-disable.mjs +5 -0
  366. package/scripts/lib/eslint-plugin.d.ts +5 -1
  367. package/scripts/lib/eslint-plugin.mjs +240 -0
  368. package/scripts/lib/form-v-model.mjs +190 -0
  369. package/scripts/lib/manifest-pointers.mjs +71 -0
  370. package/scripts/lib/project-variant-dts.mjs +25 -3
  371. package/scripts/lib/reactive-v-model.mjs +221 -0
  372. package/scripts/lib/validate-bb-markup.mjs +116 -8
@@ -31,7 +31,7 @@
31
31
 
32
32
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
33
33
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
34
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
34
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
35
35
  import { dirname, join, sep } from 'node:path';
36
36
  import { fileURLToPath } from 'node:url';
37
37
  import { z } from 'zod';
@@ -48,6 +48,10 @@ import {
48
48
  possessive,
49
49
  validateVueSnippet,
50
50
  } from '../scripts/lib/validate-bb-markup.mjs';
51
+ import {
52
+ agentComponent,
53
+ firstSentence,
54
+ } from '../scripts/lib/agent-manifest.mjs';
51
55
 
52
56
  const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
53
57
  const AI_ROOT = join(PACKAGE_ROOT, 'dist', 'ai');
@@ -65,19 +69,38 @@ function packageVersion() {
65
69
  }
66
70
  }
67
71
 
72
+ /*
73
+ * Parsed once per build of components.json (Q56.46): the 1.2 MB file was
74
+ * re-read and re-parsed on every tool call. Both caches below (the raw
75
+ * manifest for the lookup tools, the validator's map for `validate`) are
76
+ * keyed on the file's mtime, so a rebuild under a running server (library
77
+ * repo, `npm run build`) is picked up by every tool.
78
+ */
79
+ let cachedManifest = null;
68
80
  function loadComponentsManifest() {
69
81
  if (!existsSync(MANIFEST_PATH)) {
70
82
  throw new Error(
71
83
  `components.json not found at ${MANIFEST_PATH} — reinstall bitboss-ui (or run \`npm run build\` when working in the library repo).`
72
84
  );
73
85
  }
74
- return JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'));
86
+ const mtime = statSync(MANIFEST_PATH).mtimeMs;
87
+ if (cachedManifest?.mtime !== mtime)
88
+ cachedManifest = {
89
+ mtime,
90
+ value: JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8')),
91
+ };
92
+ return cachedManifest.value;
75
93
  }
76
94
 
77
95
  let cachedManifestMap = null;
78
96
  function getManifestMap() {
79
- cachedManifestMap ??= loadManifest(MANIFEST_PATH);
80
- return cachedManifestMap;
97
+ // Missing file: loadManifest throws the install hint.
98
+ const mtime = existsSync(MANIFEST_PATH)
99
+ ? statSync(MANIFEST_PATH).mtimeMs
100
+ : null;
101
+ if (cachedManifestMap?.mtime !== mtime || mtime === null)
102
+ cachedManifestMap = { mtime, value: loadManifest(MANIFEST_PATH) };
103
+ return cachedManifestMap.value;
81
104
  }
82
105
 
83
106
  /** `search_components({ query })` — token match over name/description. */
@@ -441,7 +464,7 @@ function searchComponents({ query, limit, includeReferenceOnly } = {}) {
441
464
  * is what makes an agent invent API, which is precisely what this server
442
465
  * exists to prevent.
443
466
  */
444
- const MARKDOWN_DEFAULT_MAX_CHARS = 40000;
467
+ const MARKDOWN_DEFAULT_MAX_CHARS = 16000; // was 40 000 (MCP panel 2026-09-26)
445
468
 
446
469
  /**
447
470
  * Cut `text` to `maxChars` on a line boundary, reporting what was dropped.
@@ -580,6 +603,13 @@ function resolveComponent(manifest, name) {
580
603
 
581
604
  function nearestComponents(manifest, name, count = 3) {
582
605
  const needle = pascal(name).toLowerCase().replace(/^bb/, '');
606
+ // A partial name (`BbInput`) matches the components that contain it
607
+ // (BbTextInput, BbNumberInput…) before any edit-distance guess.
608
+ const containing = manifest.components
609
+ .filter((c) => !c.referenceOnly)
610
+ .map((c) => c.name)
611
+ .filter((n) => needle.length >= 3 && n.toLowerCase().includes(needle));
612
+ if (containing.length) return containing.slice(0, Math.max(count, 5));
583
613
  return manifest.components
584
614
  .filter((c) => !c.referenceOnly)
585
615
  .map((c) => ({
@@ -662,8 +692,346 @@ function contractGuideSections(markdown) {
662
692
  return all.slice(start + 1, end === -1 ? undefined : end);
663
693
  }
664
694
 
665
- /** Sections every agent needs, served whole in the default answer. */
695
+ /*
696
+ * A section is served whole up to SECTION_CAP chars. Past that (the review
697
+ * found a 142.9K `migration/v2-to-v3.md` section behind one search hit) it
698
+ * answers its own outline: ask again for a sub-section, or pass maxChars: 0.
699
+ */
700
+ const SECTION_CAP = 16000;
701
+ function sectionAnswer(sec, all, maxChars) {
702
+ if (maxChars === 0) return { title: sec.title, text: sec.text };
703
+ const cap = maxChars > 0 ? maxChars : SECTION_CAP;
704
+ if (sec.text.length <= cap) return { title: sec.title, text: sec.text };
705
+ const inner = all.filter(
706
+ (other) =>
707
+ other !== sec && other.level > sec.level && sec.text.includes(other.text)
708
+ );
709
+ // No sub-sections to point at (v2-to-v3 § 5.6, 68K), or an explicit cap:
710
+ // the text itself, cut — never an empty outline (adversarial review 2).
711
+ if (!inner.length || maxChars > 0) {
712
+ const cut = sec.text.slice(0, cap);
713
+ const end = cut.lastIndexOf('\n');
714
+ return {
715
+ title: sec.title,
716
+ text: end > cap / 2 ? cut.slice(0, end) : cut,
717
+ truncated: true,
718
+ chars: sec.text.length,
719
+ hint: `Cut at ${cap} of ${sec.text.length} chars. maxChars: 0 returns all of it${inner.length ? ', or ask for a sub-section by title' : ''}.`,
720
+ };
721
+ }
722
+ // Sub-sections: the section's own lead-in (up to 4K) and its outline.
723
+ const own = sec.own.length > 4000 ? `${sec.own.slice(0, 4000)} …` : sec.own;
724
+ return {
725
+ title: sec.title,
726
+ chars: sec.text.length,
727
+ text: own,
728
+ outline: outline(inner),
729
+ hint: `This section is ${sec.text.length} chars: its lead-in is above; ask for a sub-section by title, or pass maxChars: 0 for all of it.`,
730
+ };
731
+ }
732
+
733
+ /*
734
+ * A multi-part answer (several sections, members or recipe files) stops at
735
+ * ANSWER_CAP chars and names the rest: one call per part never came to 149K
736
+ * (BbTable, every section at once) or 52K (every member) by accident.
737
+ */
738
+ const ANSWER_CAP = 24000;
739
+ function capParts(parts, label) {
740
+ const kept = [];
741
+ let size = 0;
742
+ for (const part of parts) {
743
+ const n = JSON.stringify(part).length;
744
+ if (kept.length && size + n > ANSWER_CAP) break;
745
+ kept.push(part);
746
+ size += n;
747
+ }
748
+ const rest = parts.slice(kept.length);
749
+ return {
750
+ kept,
751
+ ...(rest.length
752
+ ? {
753
+ [`remaining${label}`]: rest.map((part) => part.title ?? part.name),
754
+ }
755
+ : {}),
756
+ };
757
+ }
758
+
759
+ /** Sections every agent needs, served whole in the full answer. */
666
760
  const ALWAYS_SECTIONS = [/^gotchas/i, /^accessibility/i];
761
+ /** The summary keeps only these whole; the rest are in the outline (MCP panel 2026-09-26). */
762
+ const SUMMARY_SECTIONS = [/^gotchas/i];
763
+
764
+ /*
765
+ * SUMMARY ROWS (MCP panel 2026-09-26, owner: compact by default, expand on
766
+ * request). Blind Haiku runs saved `get_component BbTable` (38–47 KB) to a file
767
+ * to read it; Claude Code warns above ~10K tokens of tool output. The summary
768
+ * keeps EVERY name (a partial prop list is what makes agents invent API) as one
769
+ * line — name, type, default, the description's first sentence — and marks a
770
+ * cut sentence with " …": `members: ['name']` returns the full row.
771
+ * `firstSentence` lives in scripts/lib/agent-manifest.mjs: the manifest
772
+ * index (Q56.46) cuts descriptions the same way.
773
+ */
774
+
775
+ /** A type as the summary prints it: no ` | undefined` tail, long ones cut. */
776
+ function shortType(type, cap = 180) {
777
+ const t = String(type ?? '')
778
+ .replace(/\s*\|\s*undefined$/, '')
779
+ .replace(/\s+/g, ' ');
780
+ return t.length > cap ? `${t.slice(0, cap)}…` : t;
781
+ }
782
+
783
+ let sharedPropKeys = null;
784
+ /**
785
+ * A prop folds only when EVERYTHING an agent reads off it is the same on
786
+ * every component: type, default, values and description (the adversarial
787
+ * review found `items` / `multiple` / `padding` folded with different types
788
+ * and defaults). A required prop never folds: its own line carries the fact.
789
+ */
790
+ const foldKey = (p) =>
791
+ [
792
+ p.name,
793
+ p.type,
794
+ p.default ?? '',
795
+ (p.values ?? []).join('|'),
796
+ p.description ?? '',
797
+ ].join('\u0000');
798
+ /**
799
+ * Prop rows that are identical (name AND description) on 3+ public components:
800
+ * the form-field set on 16 controls, the Inertia link props on the buttons.
801
+ * The summary lists them by name once (`common`) instead of repeating ~4–8 KB
802
+ * of the same prose per component; `members` returns any of them whole.
803
+ */
804
+ function sharedProps(manifest) {
805
+ if (sharedPropKeys) return sharedPropKeys;
806
+ const count = new Map();
807
+ for (const component of manifest.components ?? []) {
808
+ if (component.referenceOnly) continue;
809
+ for (const prop of component.props ?? []) {
810
+ if (!prop.description || prop.name.startsWith('pt')) continue;
811
+ const key = foldKey(prop);
812
+ if (prop.required) continue;
813
+ count.set(key, (count.get(key) ?? 0) + 1);
814
+ }
815
+ }
816
+ sharedPropKeys = new Set(
817
+ [...count].filter(([, n]) => n >= 3).map(([key]) => key)
818
+ );
819
+ return sharedPropKeys;
820
+ }
821
+
822
+ const propLine = (p) => {
823
+ const values =
824
+ p.values?.length &&
825
+ !/Registry|VariantType|BbPtScopeName|LocaleKey/.test(String(p.type)) &&
826
+ !p.values.some((v) => /\s/.test(String(v))) &&
827
+ !p.values.every((v) => String(p.type).includes(String(v)))
828
+ ? ` ∈ ${p.values.map((v) => String(v)).join('|')}`.slice(0, 120)
829
+ : '';
830
+ return `${p.name}${p.required ? '' : '?'}: ${shortType(p.type)}${values}${p.default != null && p.default !== '' ? ` = ${p.default}` : ''}${p.description ? ` — ${firstSentence(p.description)}` : ''}`;
831
+ };
832
+ const eventLine = (e) =>
833
+ `${e.name}${String(e.signature ?? '').replace(/: void$/, '')}${e.description ? ` — ${firstSentence(e.description)}` : ''}`;
834
+ /** The top-level keys of a slot scope type `{ a?: …; b: … }`. */
835
+ function scopeKeys(type) {
836
+ const t = String(type ?? '').trim();
837
+ if (!t.startsWith('{')) return [];
838
+ const keys = [];
839
+ let depth = 0;
840
+ let token = '';
841
+ const inner = t.slice(1, -1);
842
+ for (let i = 0; i < inner.length; i++) {
843
+ const ch = inner[i];
844
+ if ('{([<'.includes(ch)) depth++;
845
+ // The `>` of an arrow `=>` closes nothing.
846
+ else if ('})]'.includes(ch) || (ch === '>' && inner[i - 1] !== '='))
847
+ depth--;
848
+ if (depth === 0 && (ch === ';' || ch === ',')) {
849
+ const key = token.trim().match(/^([\w$]+)\??\s*:/)?.[1];
850
+ if (key) keys.push(key);
851
+ token = '';
852
+ } else token += ch;
853
+ }
854
+ const last = token.trim().match(/^([\w$]+)\??\s*:/)?.[1];
855
+ if (last) keys.push(last);
856
+ return keys;
857
+ }
858
+ const slotLine = (sl) => {
859
+ // A named scope type (BbSelectOptionSlotProps…) carries resolved `fields`.
860
+ const keys = sl.fields?.length
861
+ ? sl.fields.map((f) => String(f).match(/^([\w$]+)/)?.[1]).filter(Boolean)
862
+ : scopeKeys(sl.type);
863
+ return `#${sl.name}${keys.length ? ` { ${keys.join(', ')} }` : ''}${sl.description ? ` — ${firstSentence(sl.description)}` : ''}`;
864
+ };
865
+
866
+ /** One line per v2/beta rename or removal (include 'history' for the records). */
867
+ function historyLines(entry) {
868
+ const renamedFrom = new Set((entry.renamed ?? []).map((r) => r.from));
869
+ // A slot row reads as a slot (`#no-data → #empty`), a removal with no
870
+ // single successor carries its hint (Q51.16).
871
+ const spell = (r, name) => (r.surface === 'slot' ? `#${name}` : name);
872
+ // A rename that also maps VALUES says which: `inputDirection →
873
+ // listLayout` alone sent 2 of 3 answerers to `list-layout="vertical"`
874
+ // when the map is vertical → vertical-stretch (2026-09-24).
875
+ const values = (r) =>
876
+ Object.entries(r.fix?.valueMap ?? {})
877
+ .filter(([from, to]) => from !== to)
878
+ .map(([from, to]) => `"${from}" → "${to}"`)
879
+ .join(', ');
880
+ return [
881
+ ...(entry.renamed ?? []).map(
882
+ (r) =>
883
+ `${spell(r, r.from)} → ${spell(r, r.to)}${r.fix?.kind && r.fix.kind !== 'rename' ? ` (${r.fix.kind})` : ''}${values(r) ? ` (values: ${values(r)})` : ''}`
884
+ ),
885
+ ...(entry.removed ?? [])
886
+ .filter((r) => !renamedFrom.has(r.name))
887
+ .map(
888
+ (r) =>
889
+ `${spell(r, r.name)} removed${r.new ? ` → ${spell(r, r.new)}` : ''}${r.hint ? ` — ${r.hint}` : ''}`
890
+ ),
891
+ ];
892
+ }
893
+
894
+ /** The compact `component` of the summary answer. */
895
+ function summarizeComponent(entry, manifest) {
896
+ const shared = sharedProps(manifest);
897
+ const own = [];
898
+ const common = [];
899
+ for (const p of entry.props ?? []) {
900
+ if (p.name === 'pt' || p.name.startsWith('pt:')) continue;
901
+ if (!p.required && shared.has(foldKey(p))) common.push(p.name);
902
+ else own.push(propLine(p));
903
+ }
904
+ return {
905
+ name: entry.name,
906
+ ...(entry.group ? { group: entry.group } : {}),
907
+ ...(entry.description ? { description: entry.description } : {}),
908
+ props: own,
909
+ ...(common.length
910
+ ? {
911
+ commonProps: `${common.join(', ')} — optional props identical (type, default, description) on 3+ components (form fields, link props); members: ['${common[0]}'] returns one whole`,
912
+ }
913
+ : {}),
914
+ ...(entry.models?.length
915
+ ? {
916
+ models: entry.models.map(
917
+ (m) =>
918
+ `v-model${m.name === 'modelValue' ? '' : `:${m.name}`}: ${shortType(m.type)}`
919
+ ),
920
+ }
921
+ : {}),
922
+ ...(entry.events?.length ? { events: entry.events.map(eventLine) } : {}),
923
+ ...(entry.slots?.length ? { slots: entry.slots.map(slotLine) } : {}),
924
+ ...(entry.exposed?.length
925
+ ? { exposed: entry.exposed.map((x) => `${x.name}: ${shortType(x.type)}`) }
926
+ : {}),
927
+ ...(entry.pt?.parts?.length
928
+ ? {
929
+ pt: `parts: ${entry.pt.parts.map((x) => x.name).join(', ')}${entry.pt.states?.length ? `; states: ${entry.pt.states.map((x) => x.name).join(', ')}` : ''}. Each is a \`pt:<part>\` / \`pt:<part>:<state>\` prop; the \`pt\` object takes the same keys WITHOUT the \`pt:\` prefix: { ${ptExample(entry)} }. What a part or state is: members: ['<part>'].`,
930
+ }
931
+ : {}),
932
+ ...(entry.cssProps?.length
933
+ ? { cssProps: entry.cssProps.map((c) => c.name ?? c) }
934
+ : {}),
935
+ ...(entry.removed?.length || entry.renamed?.length
936
+ ? { history: historyLines(entry) }
937
+ : {}),
938
+ ...(entry.attrTargets ? { attrTargets: entry.attrTargets } : {}),
939
+ ...(entry.referenceOnly ? { referenceOnly: true } : {}),
940
+ };
941
+ }
942
+
943
+ /**
944
+ * `members`: the whole rows for the names asked — props, events, slots
945
+ * (`#name` or `name`), v-models (`v-model:sort`) and pt parts / states
946
+ * (`pt:row`, `row`), whose descriptions the summary line leaves out.
947
+ */
948
+ function pickMembers(entry, names) {
949
+ const norm = (n) =>
950
+ String(n)
951
+ .trim()
952
+ .replace(/^#/, '')
953
+ .replace(/^v-model:/, '')
954
+ .toLowerCase();
955
+ const wanted = names.map(norm);
956
+ const hit = new Set();
957
+ const take = (list, key = (x) => x.name) =>
958
+ (list ?? []).filter((x) => {
959
+ const k = String(key(x)).toLowerCase();
960
+ const ok = wanted.includes(k);
961
+ if (ok) wanted.forEach((w, i) => w === k && hit.add(i));
962
+ return ok;
963
+ });
964
+ const props = take(entry.props);
965
+ const events = take(entry.events);
966
+ const slots = take(entry.slots);
967
+ const models = (entry.models ?? []).filter((m) =>
968
+ names.some(
969
+ (n) =>
970
+ String(n).trim() ===
971
+ (m.name === 'modelValue' ? 'v-model' : `v-model:${m.name}`)
972
+ )
973
+ );
974
+ names.forEach((n, i) => {
975
+ if (/^v-model$/.test(String(n).trim()) && models.length) hit.add(i);
976
+ });
977
+ // pt: `row`, `pt:row`, `row:selected` / `pt:row:selected` (the part AND
978
+ // the state, both with their descriptions).
979
+ const ptWanted = names.map((n) =>
980
+ String(n).trim().replace(/^pt:/, '').toLowerCase().split(':')
981
+ );
982
+ const parts = (entry.pt?.parts ?? []).filter((x) =>
983
+ ptWanted.some(([part], i) => {
984
+ const ok = part === x.name.toLowerCase();
985
+ if (ok) hit.add(i);
986
+ return ok;
987
+ })
988
+ );
989
+ const states = (entry.pt?.states ?? []).filter((x) =>
990
+ ptWanted.some((segments, i) => {
991
+ const ok = segments.at(-1) === x.name.toLowerCase();
992
+ if (ok) hit.add(i);
993
+ return ok;
994
+ })
995
+ );
996
+ const notFound = names.filter((_, i) => !hit.has(i));
997
+ // Names built from a requested WORD (`loading` → `interactiveWhileLoading`,
998
+ // `loadingText`): a members-only call skips the summary, and a blind run
999
+ // concluded v2's enabled-while-loading had no v3 equivalent. Whole camel
1000
+ // words only (`key` does not match `keyboardNavigation`).
1001
+ const words = (n) =>
1002
+ n
1003
+ .replace(/^#/, '')
1004
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
1005
+ .toLowerCase()
1006
+ .split(/[\s:_-]+/);
1007
+ const asked = names.map(norm).filter((n) => n.length >= 3);
1008
+ const related = [
1009
+ ...(entry.props ?? []).map((p) => p.name),
1010
+ ...(entry.events ?? []).map((e) => e.name),
1011
+ ...(entry.slots ?? []).map((sl) => `#${sl.name}`),
1012
+ ].filter(
1013
+ (n) =>
1014
+ !n.startsWith('pt:') &&
1015
+ !asked.includes(n.replace(/^#/, '').toLowerCase()) &&
1016
+ asked.some((a) => words(n).includes(a))
1017
+ );
1018
+ return {
1019
+ ...(props.length ? { props } : {}),
1020
+ ...(events.length ? { events } : {}),
1021
+ ...(slots.length ? { slots } : {}),
1022
+ ...(models.length ? { models } : {}),
1023
+ ...(parts.length || states.length
1024
+ ? {
1025
+ pt: {
1026
+ ...(parts.length ? { parts } : {}),
1027
+ ...(states.length ? { states } : {}),
1028
+ },
1029
+ }
1030
+ : {}),
1031
+ ...(related.length ? { related } : {}),
1032
+ ...(notFound.length ? { notFound } : {}),
1033
+ };
1034
+ }
667
1035
 
668
1036
  /**
669
1037
  * `get_component({ name, sections?, include?, maxChars? })`.
@@ -679,7 +1047,14 @@ const ALWAYS_SECTIONS = [/^gotchas/i, /^accessibility/i];
679
1047
  * sections, the component tree, the CSS variables with their descriptions
680
1048
  * and the whole contract come on request.
681
1049
  */
682
- function getComponent({ name, sections, include, maxChars } = {}) {
1050
+ function getComponent({
1051
+ name,
1052
+ sections,
1053
+ include,
1054
+ maxChars,
1055
+ detail,
1056
+ members,
1057
+ } = {}) {
683
1058
  const manifest = loadComponentsManifest();
684
1059
  const resolved = resolveComponent(manifest, name);
685
1060
  const entry = resolved?.entry;
@@ -807,56 +1182,144 @@ function getComponent({ name, sections, include, maxChars } = {}) {
807
1182
  const ptRows = entry.props.filter(
808
1183
  (p) => p.name === 'pt' || p.name.startsWith('pt:')
809
1184
  );
1185
+ const found = {
1186
+ found: true,
1187
+ ...(resolved.resolvedFrom ? { resolvedFrom: resolved.resolvedFrom } : {}),
1188
+ };
1189
+
1190
+ // `members`: exactly the rows asked for, nothing else.
1191
+ if (members?.length) {
1192
+ const picked = pickMembers(entry, members);
1193
+ // Rows past the cap are named, not dropped.
1194
+ for (const kind of ['props', 'events', 'slots']) {
1195
+ if (!picked[kind]) continue;
1196
+ const { kept, ...rest } = capParts(picked[kind], 'Members');
1197
+ picked[kind] = kept;
1198
+ if (rest.remainingMembers)
1199
+ picked.remainingMembers = [
1200
+ ...(picked.remainingMembers ?? []),
1201
+ ...rest.remainingMembers,
1202
+ ];
1203
+ }
1204
+ return { ...found, name: entry.name, ...picked };
1205
+ }
1206
+
1207
+ const full = detail === 'full';
1208
+ const asked = (sections?.length ?? 0) + (include?.length ?? 0) > 0;
1209
+ const { picked, missing } = pickSections(guide, sections);
1210
+ const guideOutline = guide
1211
+ .filter((s) => s.level === 3)
1212
+ .map((s) => ({ title: s.title, chars: s.text.length }));
1213
+
1214
+ // A follow-up for sections / include parts returns ONLY those parts: the
1215
+ // summary was already read (it used to be resent whole: BbTable + one
1216
+ // section was 49K chars for a ~9K section).
1217
+ if (asked && !full) {
1218
+ return {
1219
+ ...found,
1220
+ name: entry.name,
1221
+ ...(picked.length
1222
+ ? (() => {
1223
+ const { kept, ...rest } = capParts(
1224
+ picked.map((sec) => sectionAnswer(sec, guide, maxChars)),
1225
+ 'Sections'
1226
+ );
1227
+ return { sections: kept, ...rest };
1228
+ })()
1229
+ : {}),
1230
+ // A missed title gets the outline, never the whole answer.
1231
+ ...(missing.length ? { notFound: missing, outline: guideOutline } : {}),
1232
+ ...(wanted.has('tree') && entry.tree ? { tree: entry.tree } : {}),
1233
+ ...(wanted.has('cssProps') && entry.cssProps
1234
+ ? { cssProps: entry.cssProps }
1235
+ : {}),
1236
+ ...(wanted.has('css') && entry.css ? { css: entry.css } : {}),
1237
+ ...(wanted.has('ptProps')
1238
+ ? (() => {
1239
+ const { kept, ...rest } = capParts(ptRows, 'PtProps');
1240
+ return { ptProps: kept, ...rest, pt: entry.pt };
1241
+ })()
1242
+ : {}),
1243
+ ...(wanted.has('history')
1244
+ ? { removed: entry.removed ?? [], renamed: entry.renamed ?? [] }
1245
+ : {}),
1246
+ ...(wanted.has('contract')
1247
+ ? (() => {
1248
+ const c = truncateMarkdown(
1249
+ raw,
1250
+ maxChars,
1251
+ 'Pass maxChars: 0 for the whole contract.'
1252
+ );
1253
+ return {
1254
+ contract: c.content,
1255
+ ...(c.truncated
1256
+ ? { contractTruncated: true, contractHint: c.hint }
1257
+ : {}),
1258
+ };
1259
+ })()
1260
+ : {}),
1261
+ };
1262
+ }
1263
+
1264
+ if (!full) {
1265
+ const kept = guide.filter((s) =>
1266
+ SUMMARY_SECTIONS.some((re) => re.test(s.title))
1267
+ );
1268
+ return {
1269
+ ...found,
1270
+ component: summarizeComponent(entry, manifest),
1271
+ ...(types.length > 0 ? { types } : {}),
1272
+ ...(entry.pt?.parts?.length ? { ptTyping: ptTyping(entry) } : {}),
1273
+ ...(seeAlso(entry, raw).length ? { seeAlso: seeAlso(entry, raw) } : {}),
1274
+ contractPath,
1275
+ guide: {
1276
+ sections: guideOutline,
1277
+ ...(kept.length
1278
+ ? { always: kept.map((s) => ({ title: s.title, text: s.text })) }
1279
+ : {}),
1280
+ },
1281
+ hint: `Summary: every name is listed; a line ending " …" is cut. Whole rows: members: ['<name>']; guide prose: sections: ['<title>']; include: ['tree' | 'cssProps' | 'css' | 'ptProps' | 'history' | 'contract']; detail: 'full' for the whole entry.`,
1282
+ };
1283
+ }
1284
+
1285
+ /*
1286
+ * The whole entry is the shipped agent file, dist/ai/manifest/components/
1287
+ * <Name>.json (Q56.46: one builder), with the MCP's own compaction on top:
1288
+ * the types go in the answer's `types` (with import lines), the cssProps
1289
+ * as names, the register as one history line per old name.
1290
+ */
1291
+ const { types: _agentTypes, ...agent } = agentComponent(entry, manifest);
810
1292
  const component = {
811
- ...entry,
812
- props: (wanted.has('ptProps')
813
- ? entry.props
814
- : entry.props.filter((p) => !ptRows.includes(p))
815
- ).map(({ required, ...prop }) => (required ? { ...prop, required } : prop)),
816
- ...(entry.cssProps && !wanted.has('cssProps')
817
- ? { cssProps: entry.cssProps.map((c) => c.name ?? c) }
1293
+ ...agent,
1294
+ ...(wanted.has('ptProps')
1295
+ ? {
1296
+ props: entry.props.map(({ required, ...prop }) =>
1297
+ required ? { ...prop, required } : prop
1298
+ ),
1299
+ }
818
1300
  : {}),
1301
+ ...(entry.cssProps
1302
+ ? {
1303
+ cssProps: wanted.has('cssProps')
1304
+ ? entry.cssProps
1305
+ : entry.cssProps.map((c) => c.name ?? c),
1306
+ }
1307
+ : {}),
1308
+ ...(wanted.has('tree') && entry.tree ? { tree: entry.tree } : {}),
1309
+ // The rendered `.bb-*` classes (Q46.5) are for tools (`check`,
1310
+ // `validate`); an agent restyles through pt and the locals.
1311
+ ...(wanted.has('css') && entry.css ? { css: entry.css } : {}),
819
1312
  };
820
- if (!wanted.has('tree')) delete component.tree;
821
- // The rendered `.bb-*` classes (Q46.5) are for tools (`check`, `validate`);
822
- // an agent restyles through pt and the locals. include: ['css'] lists them.
823
- if (!wanted.has('css')) delete component.css;
824
1313
  // `removed` and `renamed` repeat each other with migration metadata; one
825
1314
  // line per old name answers "what happened to X" (include 'history' for
826
1315
  // the records the fixers use).
827
- if (
828
- !wanted.has('history') &&
829
- (entry.removed?.length || entry.renamed?.length)
830
- ) {
831
- const renamedFrom = new Set((entry.renamed ?? []).map((r) => r.from));
832
- // A slot row reads as a slot (`#no-data → #empty`), a removal with no
833
- // single successor carries its hint (Q51.16).
834
- const spell = (r, name) => (r.surface === 'slot' ? `#${name}` : name);
835
- // A rename that also maps VALUES says which: `inputDirection →
836
- // listLayout` alone sent 2 of 3 answerers to `list-layout="vertical"`
837
- // when the map is vertical → vertical-stretch (2026-09-24).
838
- const values = (r) =>
839
- Object.entries(r.fix?.valueMap ?? {})
840
- .filter(([from, to]) => from !== to)
841
- .map(([from, to]) => `"${from}" → "${to}"`)
842
- .join(', ');
843
- component.history = [
844
- ...(entry.renamed ?? []).map(
845
- (r) =>
846
- `${spell(r, r.from)} → ${spell(r, r.to)}${r.fix?.kind && r.fix.kind !== 'rename' ? ` (${r.fix.kind})` : ''}${values(r) ? ` (values: ${values(r)})` : ''}`
847
- ),
848
- ...(entry.removed ?? [])
849
- .filter((r) => !renamedFrom.has(r.name))
850
- .map(
851
- (r) =>
852
- `${spell(r, r.name)} removed${r.new ? ` → ${spell(r, r.new)}` : ''}${r.hint ? ` — ${r.hint}` : ''}`
853
- ),
854
- ];
1316
+ if (wanted.has('history')) {
1317
+ if (entry.renamed) component.renamed = entry.renamed;
1318
+ } else if (entry.removed?.length || entry.renamed?.length) {
1319
+ component.history = historyLines(entry);
855
1320
  delete component.removed;
856
- delete component.renamed;
857
1321
  }
858
1322
 
859
- const { picked, missing } = pickSections(guide, sections);
860
1323
  const always = guide.filter(
861
1324
  (s) => ALWAYS_SECTIONS.some((re) => re.test(s.title)) && !picked.includes(s)
862
1325
  );
@@ -989,8 +1452,16 @@ function* eachTokenDeclaration(css) {
989
1452
  const stack = [];
990
1453
  let buffer = '';
991
1454
  let quote = null;
1455
+ // Parenthesis depth: CSS `if()` separates its branches with `;`, and a
1456
+ // branch may read `style(--bb-x: …)`. Only a top-level `;` ends a
1457
+ // declaration, and only a name at its START is the property it sets
1458
+ // (Q56.48: `--fg-hover: if(style(--bb-primary-fg: …)…)` was served as a
1459
+ // local rebind of --bb-primary-fg).
1460
+ let depth = 0;
992
1461
  const declaration = () => {
993
- const m = buffer.match(/(--bb-[a-z0-9-]+)\s*:\s*([\s\S]+)/);
1462
+ const m = buffer
1463
+ .replace(/\/\*[\s\S]*?\*\//g, '')
1464
+ .match(/^\s*(--bb-[a-z0-9-]+)\s*:\s*([\s\S]+)/);
994
1465
  buffer = '';
995
1466
  return m ? { name: m[1], value: m[2].trim() } : null;
996
1467
  };
@@ -1003,6 +1474,11 @@ function* eachTokenDeclaration(css) {
1003
1474
  if (char === '"' || char === "'") {
1004
1475
  quote = char;
1005
1476
  buffer += char;
1477
+ } else if (char === '(' || char === ')') {
1478
+ depth = Math.max(0, depth + (char === '(' ? 1 : -1));
1479
+ buffer += char;
1480
+ } else if (depth > 0) {
1481
+ buffer += char;
1006
1482
  } else if (char === '{') {
1007
1483
  // A comment before a rule would otherwise prefix its selector or
1008
1484
  // hide the `@supports` it opens.
@@ -1178,7 +1654,7 @@ function listGuides() {
1178
1654
 
1179
1655
  /** Guides this size or larger answer with an outline unless asked for more. */
1180
1656
  // The router (13 KB, the documented entry point) still comes whole.
1181
- const GUIDE_OUTLINE_OVER = 16000;
1657
+ const GUIDE_OUTLINE_OVER = 8000; // was 16 000: ai-router.md (16K, "a tiny map") came back whole (MCP panel 2026-09-26)
1182
1658
 
1183
1659
  /**
1184
1660
  * The shipped guide a pointer means. Pointers come from everywhere — the
@@ -1205,6 +1681,38 @@ function resolveGuidePath(name) {
1205
1681
 
1206
1682
  /** `get_guide({ name, sections?, maxChars? })`. */
1207
1683
  function getGuide({ name, sections, maxChars } = {}) {
1684
+ // The package README, which search_docs indexes (MCP panel 2026-09-26).
1685
+ if (/^readme(\.md)?$/i.test(String(name ?? '').trim())) {
1686
+ const readme = join(PACKAGE_ROOT, 'README.md');
1687
+ if (existsSync(readme)) {
1688
+ const markdown = readFileSync(readme, 'utf-8');
1689
+ const all = splitSections(markdown);
1690
+ if (sections?.length) {
1691
+ const { picked, missing } = pickSections(all, sections);
1692
+ return {
1693
+ found: true,
1694
+ name: 'README',
1695
+ ...(() => {
1696
+ const { kept, ...rest } = capParts(
1697
+ picked.map((sec) => sectionAnswer(sec, all, maxChars)),
1698
+ 'Sections'
1699
+ );
1700
+ return { sections: kept, ...rest };
1701
+ })(),
1702
+ ...(missing.length
1703
+ ? { notFound: missing, outline: outline(all) }
1704
+ : {}),
1705
+ };
1706
+ }
1707
+ return {
1708
+ found: true,
1709
+ name: 'README',
1710
+ chars: markdown.length,
1711
+ outline: outline(all),
1712
+ hint: "Read what you need: get_guide({ name: 'README', sections: ['<title>'] }).",
1713
+ };
1714
+ }
1715
+ }
1208
1716
  const resolved = resolveGuidePath(name);
1209
1717
  if (!resolved) {
1210
1718
  const all = collectGuidePaths(join(AI_ROOT, 'guides'));
@@ -1216,10 +1724,25 @@ function getGuide({ name, sections, maxChars } = {}) {
1216
1724
  .some((w) => w.length > 3 && p.toLowerCase().includes(w))
1217
1725
  )
1218
1726
  .slice(0, 5);
1727
+ // A dead end cost the agent a turn (`get_guide('theming')`): answer with
1728
+ // the sections that match the name as a query.
1729
+ let found = [];
1730
+ try {
1731
+ found = searchSections(docIndex(), String(name ?? ''), 3).results;
1732
+ } catch {
1733
+ /* an unbuilt package has no index: the plain miss still answers */
1734
+ }
1735
+ const hits = found.map((r) => ({
1736
+ section: `${r.source} › ${r.section}`,
1737
+ read: r.read?.tool
1738
+ ? `${r.read.tool}(${JSON.stringify(r.read.args ?? {})})`
1739
+ : r.read,
1740
+ }));
1219
1741
  return {
1220
1742
  found: false,
1221
1743
  error: `Guide not found: ${name}`,
1222
1744
  ...(near.length ? { didYouMean: near } : {}),
1745
+ ...(hits.length ? { related: hits } : {}),
1223
1746
  hint: 'Call list_guides for the names, or search_docs to find the section by topic.',
1224
1747
  };
1225
1748
  }
@@ -1236,7 +1759,13 @@ function getGuide({ name, sections, maxChars } = {}) {
1236
1759
  const { picked, missing } = pickSections(all, sections);
1237
1760
  return {
1238
1761
  ...base,
1239
- sections: picked.map((s) => ({ title: s.title, text: s.text })),
1762
+ ...(() => {
1763
+ const { kept, ...rest } = capParts(
1764
+ picked.map((sec) => sectionAnswer(sec, all, maxChars)),
1765
+ 'Sections'
1766
+ );
1767
+ return { sections: kept, ...rest };
1768
+ })(),
1240
1769
  ...(missing.length ? { notFound: missing, outline: outline(all) } : {}),
1241
1770
  };
1242
1771
  }
@@ -1336,7 +1865,11 @@ function docIndex() {
1336
1865
  source: `component ${component.name}`,
1337
1866
  section: `prop ${prop.name}`,
1338
1867
  text: `${prop.name}: ${prop.type}${prop.default ? ` = ${prop.default}` : ''}. ${prop.description ?? ''}`,
1339
- read: { tool: 'get_component', args: { name: component.name } },
1868
+ // The one row, not the component (~0.5K instead of up to 39K).
1869
+ read: {
1870
+ tool: 'get_component',
1871
+ args: { name: component.name, members: [prop.name] },
1872
+ },
1340
1873
  });
1341
1874
  }
1342
1875
  for (const event of component.events ?? []) {
@@ -1344,7 +1877,10 @@ function docIndex() {
1344
1877
  source: `component ${component.name}`,
1345
1878
  section: `event ${event.name}`,
1346
1879
  text: `@${event.name} ${event.signature}. ${event.description ?? ''}`,
1347
- read: { tool: 'get_component', args: { name: component.name } },
1880
+ read: {
1881
+ tool: 'get_component',
1882
+ args: { name: component.name, members: [event.name] },
1883
+ },
1348
1884
  });
1349
1885
  }
1350
1886
  /*
@@ -1489,9 +2025,11 @@ function docIndex() {
1489
2025
  }
1490
2026
  const readme = join(PACKAGE_ROOT, 'README.md');
1491
2027
  if (existsSync(readme)) {
1492
- addSections('README', readFileSync(readme, 'utf-8'), () => ({
1493
- tool: 'read file',
1494
- args: { path: 'node_modules/bitboss-ui/README.md' },
2028
+ // Served by get_guide under the name 'README' (it used to point at a
2029
+ // "read file" call, which is not a tool).
2030
+ addSections('README', readFileSync(readme, 'utf-8'), (title) => ({
2031
+ tool: 'get_guide',
2032
+ args: { name: 'README', sections: [title] },
1495
2033
  }));
1496
2034
  }
1497
2035
  cachedDocIndex = entries;
@@ -1500,13 +2038,16 @@ function docIndex() {
1500
2038
 
1501
2039
  function searchDocs({ query, limit } = {}) {
1502
2040
  const max = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 20) : 8;
1503
- const { tokens, total, results } = searchSections(docIndex(), query, max);
2041
+ const { total, results } = searchSections(docIndex(), query, max);
2042
+ // `read` as the call itself, one string: the object form cost ~100 chars a
2043
+ // result and agents copy the call anyway.
2044
+ const call = (read) =>
2045
+ read?.tool ? `${read.tool}(${JSON.stringify(read.args ?? {})})` : read;
1504
2046
  return {
1505
2047
  query: query ?? '',
1506
- tokens,
1507
2048
  count: results.length,
1508
2049
  ...(total > results.length ? { totalMatches: total } : {}),
1509
- results,
2050
+ results: results.map((r) => ({ ...r, read: call(r.read) })),
1510
2051
  hint: results.length
1511
2052
  ? 'Each result is one section; `read` is the call that returns it whole. Prefer the top results: they match the most query words.'
1512
2053
  : 'Nothing matched. Use fewer, more specific words (a prop, option or component name), or search_components for components.',
@@ -1514,7 +2055,7 @@ function searchDocs({ query, limit } = {}) {
1514
2055
  }
1515
2056
 
1516
2057
  /** `get_composable({ name })` — one composable's doc (useToast, useConfirm…). */
1517
- function getComposable({ name } = {}) {
2058
+ function getComposable({ name, sections, maxChars } = {}) {
1518
2059
  const dir = join(AI_ROOT, 'composables');
1519
2060
  const files = existsSync(dir)
1520
2061
  ? readdirSync(dir).filter((f) => f.endsWith('.md'))
@@ -1540,12 +2081,44 @@ function getComposable({ name } = {}) {
1540
2081
  const host = frontmatter(markdown).summary?.match(
1541
2082
  /Requires (exactly one <Bb\w+ \/> host[^.]*)\./
1542
2083
  )?.[1];
1543
- return {
2084
+ const base = {
1544
2085
  found: true,
1545
2086
  name: file.slice(0, -3),
1546
2087
  ...(host ? { requires: host } : {}),
1547
- content: stripFrontmatter(markdown),
1548
2088
  };
2089
+ const body = stripFrontmatter(markdown);
2090
+ const all = splitSections(markdown);
2091
+ if (sections?.length) {
2092
+ const { picked, missing } = pickSections(all, sections);
2093
+ return {
2094
+ ...base,
2095
+ ...(() => {
2096
+ const { kept, ...rest } = capParts(
2097
+ picked.map((sec) => sectionAnswer(sec, all, maxChars)),
2098
+ 'Sections'
2099
+ );
2100
+ return { sections: kept, ...rest };
2101
+ })(),
2102
+ ...(missing.length ? { notFound: missing, outline: outline(all) } : {}),
2103
+ };
2104
+ }
2105
+ // Same rule as get_guide (MCP panel 2026-09-26): past 8K, the intro and the
2106
+ // outline; useConfirm's doc was 18K in one answer.
2107
+ if (maxChars === undefined && body.length >= GUIDE_OUTLINE_OVER) {
2108
+ return {
2109
+ ...base,
2110
+ chars: body.length,
2111
+ intro: body.split(/^## /m)[0].trim().slice(0, 3000),
2112
+ outline: outline(all),
2113
+ hint: `Read what you need: get_composable({ name: '${base.name}', sections: ['<title from outline>'] }); maxChars: 0 returns it whole.`,
2114
+ };
2115
+ }
2116
+ const { content, ...cut } = truncateMarkdown(
2117
+ markdown,
2118
+ maxChars,
2119
+ 'The cut tail is later sections.'
2120
+ );
2121
+ return { ...base, content: stripFrontmatter(content), ...cut };
1549
2122
  }
1550
2123
 
1551
2124
  function listRecipes({ platform } = {}) {
@@ -1590,7 +2163,7 @@ function listRecipes({ platform } = {}) {
1590
2163
  }
1591
2164
 
1592
2165
  /** `get_recipe({ platform, name })` — read one recipe's markdown content. */
1593
- function getRecipe({ platform, name, sections, maxChars } = {}) {
2166
+ function getRecipe({ platform, name, sections, files, maxChars } = {}) {
1594
2167
  if (!RECIPE_PLATFORMS.includes(platform)) {
1595
2168
  return {
1596
2169
  found: false,
@@ -1628,15 +2201,102 @@ function getRecipe({ platform, name, sections, maxChars } = {}) {
1628
2201
  found: true,
1629
2202
  platform,
1630
2203
  name: fileName.slice(0, -3),
1631
- sections: picked.map((section) => ({
1632
- title: section.title,
1633
- text: section.text,
1634
- })),
2204
+ ...(() => {
2205
+ const { kept, ...rest } = capParts(
2206
+ picked.map((sec) => sectionAnswer(sec, all, maxChars)),
2207
+ 'Sections'
2208
+ );
2209
+ return { sections: kept, ...rest };
2210
+ })(),
1635
2211
  ...(missing.length ? { notFound: missing, outline: outline(all) } : {}),
1636
2212
  };
1637
2213
  }
1638
- // Whole by default (review D6-14): a recipe is copied whole, and the old
1639
- // 40 000-char default cut 7 of 9 vue recipes inside their page code.
2214
+ /*
2215
+ * OUTLINE FIRST (MCP panel 2026-09-26, owner ruling M3). Whole recipes were
2216
+ * median ~59K chars, max 129K (~32K tokens): past Claude Code's 25K cut, so
2217
+ * "whole by default" did not reach agents whole anyway. The default is the
2218
+ * prose that explains the page (every section outside "The code"), the code
2219
+ * files with their sizes, and the shipped file's path — an agent that can
2220
+ * read files copies it from there; one that can't asks for `files`.
2221
+ * `maxChars: 0` still returns the markdown whole.
2222
+ */
2223
+ const all = splitSections(markdown);
2224
+ const code = all.find(
2225
+ (sec) => sec.level === 2 && /^the (code|files)\b/i.test(sec.title)
2226
+ );
2227
+ const codeFiles = code
2228
+ ? all.filter(
2229
+ (sec) => sec.level === 3 && code.text.includes(sec.text) && sec !== code
2230
+ )
2231
+ : [];
2232
+ if (files?.length) {
2233
+ // An exact title first, else a substring: `RejectDialog.vue` must not
2234
+ // also return BulkRejectDialog.vue.
2235
+ const hits = [];
2236
+ for (const f of files) {
2237
+ const w = String(f).toLowerCase();
2238
+ const exact = codeFiles.filter((sec) => sec.title.toLowerCase() === w);
2239
+ for (const sec of exact.length
2240
+ ? exact
2241
+ : codeFiles.filter((sec) => sec.title.toLowerCase().includes(w)))
2242
+ if (!hits.includes(sec)) hits.push(sec);
2243
+ }
2244
+ const missingFiles = files.filter(
2245
+ (f) =>
2246
+ !codeFiles.some((sec) =>
2247
+ sec.title.toLowerCase().includes(String(f).toLowerCase())
2248
+ )
2249
+ );
2250
+ return {
2251
+ found: true,
2252
+ platform,
2253
+ name: fileName.slice(0, -3),
2254
+ ...(() => {
2255
+ const { kept, ...rest } = capParts(
2256
+ hits.map((sec) => ({ title: sec.title, text: sec.text })),
2257
+ 'Files'
2258
+ );
2259
+ return { files: kept, ...rest };
2260
+ })(),
2261
+ ...(missingFiles.length
2262
+ ? {
2263
+ notFound: missingFiles,
2264
+ available: codeFiles.map((sec) => sec.title),
2265
+ }
2266
+ : {}),
2267
+ };
2268
+ }
2269
+ if (maxChars == null && code) {
2270
+ const prose = all.filter((sec) => sec.level === 2 && sec !== code);
2271
+ const meta = frontmatter(markdown);
2272
+ // The words between the title and the first ## (the adversarial review:
2273
+ // "read the XSRF-TOKEN cookie … or every upload 419s" lived there), and
2274
+ // the lead-in of the code section before its first file.
2275
+ const body = stripFrontmatter(markdown);
2276
+ const intro = body
2277
+ .split(/^## /m)[0]
2278
+ .replace(/^# .*$/m, '')
2279
+ .trim();
2280
+ const codeIntro = code.own.replace(/^## .*$/m, '').trim();
2281
+ return {
2282
+ found: true,
2283
+ platform,
2284
+ name: fileName.slice(0, -3),
2285
+ ...(meta.title ? { title: meta.title } : {}),
2286
+ path: filePath,
2287
+ chars: markdown.length,
2288
+ ...(intro ? { intro } : {}),
2289
+ prose: prose.map((sec) => ({ title: sec.title, text: sec.text })),
2290
+ ...(codeIntro ? { codeIntro } : {}),
2291
+ files: codeFiles.map((sec) => ({
2292
+ title: sec.title,
2293
+ chars: sec.text.length,
2294
+ })),
2295
+ hint: `Copy the page whole: read the file at \`path\`, or fetch the code with files: ['<title>'] (a few per call). maxChars: 0 returns the whole markdown in one answer.`,
2296
+ };
2297
+ }
2298
+ // Whole on request (review D6-14): the old 40 000-char default cut 7 of 9
2299
+ // vue recipes inside their page code.
1640
2300
  const { content, ...meta } = truncateMarkdown(
1641
2301
  markdown,
1642
2302
  maxChars ?? 0,
@@ -1758,10 +2418,96 @@ function compactChanges(rows) {
1758
2418
  const ROW_FIELDS =
1759
2419
  '`surface` says what a rename names (prop | slot | pt | cssVar | class | option); `kind` is only rename vs behaviour; `firstReleasedIn` is the first npm release with the change.';
1760
2420
 
1761
- function listChanges({ since: rawSince, until: rawUntil } = {}) {
1762
- // The repo's own tags are spelled `v3.0.0-beta.33` (review D6-12).
2421
+ /*
2422
+ * STAGED ANSWERS (MCP panel 2026-09-26). The whole v2 → v3 list was 93K chars
2423
+ * (~23K tokens, over Claude Code's 25K cap with the envelope) and one upgrade
2424
+ * hop 20–33K, 59% of it behaviour prose. Now: no range → counts per release;
2425
+ * a range or filter → ONE LINE per change, grouped by release, behaviour rows
2426
+ * as their hand-written `summary`; `detail: 'rows'` → the full rows as before.
2427
+ */
2428
+ const CHANGES_PAGE = 60;
2429
+
2430
+ function changeLine(row, focus) {
2431
+ // 16 form controls in full is ~250 chars of the line; the component filter
2432
+ // and `detail: 'rows'` name them all.
2433
+ const list = row.components ?? [];
2434
+ // The component asked about always shows, never hidden in "+N more".
2435
+ const shown =
2436
+ focus && list.includes(focus)
2437
+ ? [focus, ...list.filter((c) => c !== focus)]
2438
+ : list;
2439
+ const who =
2440
+ shown.length > 3
2441
+ ? `${shown.slice(0, 2).join(' / ')} +${shown.length - 2} more`
2442
+ : shown.join(' / ');
2443
+ if (row.kind === 'rename') {
2444
+ const to = row.to == null ? 'removed' : row.to;
2445
+ return `${row.id} ${who ? `${who} ` : ''}${row.surface ?? 'prop'} ${row.from} → ${to}${row.note ? ` — ${firstSentence(row.note, 140)}` : ''}`;
2446
+ }
2447
+ return `${row.id} ${row.summary ?? firstSentence(row.description, 160)}`;
2448
+ }
2449
+
2450
+ function changeLines(rows, cursor, withClassMap = false, focus) {
2451
+ const lines = [];
2452
+ const classRenames = {};
2453
+ for (const row of rows) {
2454
+ if (row.surface === 'class' && String(row.id).startsWith('css:')) {
2455
+ classRenames[row.from] = row.to;
2456
+ continue;
2457
+ }
2458
+ lines.push([row.firstReleasedIn ?? 'unreleased', changeLine(row, focus)]);
2459
+ }
2460
+ // Release order, unreleased last, so pages read oldest → newest.
2461
+ lines.sort(([a], [b]) =>
2462
+ a === b
2463
+ ? 0
2464
+ : a === 'unreleased'
2465
+ ? 1
2466
+ : b === 'unreleased'
2467
+ ? -1
2468
+ : compareVersions(a, b)
2469
+ );
2470
+ const start = Math.max(0, Number(cursor ?? 0) || 0);
2471
+ const page = lines.slice(start, start + CHANGES_PAGE);
2472
+ const byRelease = {};
2473
+ for (const [release, line] of page) (byRelease[release] ??= []).push(line);
2474
+ return {
2475
+ changes: byRelease,
2476
+ ...(start + CHANGES_PAGE < lines.length
2477
+ ? {
2478
+ nextCursor: String(start + CHANGES_PAGE),
2479
+ remaining: lines.length - start - CHANGES_PAGE,
2480
+ }
2481
+ : {}),
2482
+ ...(Object.keys(classRenames).length && start === 0 && !withClassMap
2483
+ ? {
2484
+ classRenames: `${Object.keys(classRenames).length} .bb-* class renames (the beta.36 sweep): the map with surface: 'class'`,
2485
+ }
2486
+ : {}),
2487
+ ...(Object.keys(classRenames).length && start === 0 && withClassMap
2488
+ ? {
2489
+ classRenames,
2490
+ classRenamesNote:
2491
+ 'class renames from the beta.36 sweep map: modifiers follow (.old--x → .new--x); ai/guides/migration/v3-beta-css-classes.md',
2492
+ }
2493
+ : {}),
2494
+ };
2495
+ }
2496
+
2497
+ function listChanges({
2498
+ since: rawSince,
2499
+ until: rawUntil,
2500
+ component,
2501
+ surface,
2502
+ kind,
2503
+ detail,
2504
+ cursor,
2505
+ } = {}) {
2506
+ // The repo's own tags are spelled `v3.0.0-beta.33` (review D6-12). A v2
2507
+ // version means "the whole v2 → v3 upgrade": every row, one line each.
2508
+ const fromV2 = rawSince != null && /^v?2(\.|$)/.test(String(rawSince));
1763
2509
  const since =
1764
- rawSince == null ? rawSince : String(rawSince).replace(/^v/, '');
2510
+ rawSince == null || fromV2 ? undefined : String(rawSince).replace(/^v/, '');
1765
2511
  const until =
1766
2512
  rawUntil == null ? rawUntil : String(rawUntil).replace(/^v/, '');
1767
2513
  if (!existsSync(CHANGELOG_PATH)) {
@@ -1793,12 +2539,114 @@ function listChanges({ since: rawSince, until: rawUntil } = {}) {
1793
2539
  return {
1794
2540
  error: `Unknown ${label} version '${value}'.`,
1795
2541
  knownReleases: releases,
1796
- hint: 'Pass a version exactly as listed in knownReleases, or omit both to get every break in the v2 → v3 upgrade.',
2542
+ hint: 'Pass a version exactly as listed in knownReleases; coming from v2, pass since: "2" for every break of the v2 → v3 upgrade.',
1797
2543
  };
1798
2544
  }
1799
2545
  }
1800
2546
 
1801
- const all = doc.breaking ?? [];
2547
+ if (
2548
+ since != null &&
2549
+ until != null &&
2550
+ compareVersions(String(since), String(until)) >= 0
2551
+ ) {
2552
+ return {
2553
+ error: `since (${since}) must be older than until (${until}).`,
2554
+ knownReleases: releases,
2555
+ };
2556
+ }
2557
+ // Resolve the component the way get_component does ('bb-select',
2558
+ // 'Select'), and say so when it is no component at all — an empty list
2559
+ // would read as "nothing breaks".
2560
+ if (component) {
2561
+ const resolved = resolveComponent(loadComponentsManifest(), component);
2562
+ // A retired name (BbTabsPanes) is still a key in the rows it renamed.
2563
+ const retiredName =
2564
+ !resolved?.entry && loadRetired().has(String(component).trim());
2565
+ if (!resolved?.entry && !retiredName) {
2566
+ return {
2567
+ error: `Unknown component '${component}'.`,
2568
+ didYouMean: nearestComponents(loadComponentsManifest(), component),
2569
+ };
2570
+ }
2571
+ if (resolved?.entry) component = resolved.entry.name;
2572
+ }
2573
+ const rows = detail === 'rows';
2574
+ const filtered = (doc.breaking ?? []).filter(
2575
+ (row) =>
2576
+ (!component ||
2577
+ (row.components ?? []).some(
2578
+ (c) => c.toLowerCase() === String(component).toLowerCase()
2579
+ )) &&
2580
+ (!surface ||
2581
+ (row.surface ?? (row.kind === 'rename' ? 'prop' : null)) === surface) &&
2582
+ (!kind || row.kind === kind)
2583
+ );
2584
+ const all = filtered;
2585
+ const filteredBy = {
2586
+ ...(component ? { component } : {}),
2587
+ ...(surface ? { surface } : {}),
2588
+ ...(kind ? { kind } : {}),
2589
+ };
2590
+
2591
+ // No range and no filter: counts per release — the map, not the territory.
2592
+ if (
2593
+ since == null &&
2594
+ until == null &&
2595
+ !rows &&
2596
+ !fromV2 &&
2597
+ !Object.keys(filteredBy).length
2598
+ ) {
2599
+ const perRelease = {};
2600
+ for (const row of all) {
2601
+ const at = row.firstReleasedIn ?? 'unreleased';
2602
+ const bucket = (perRelease[at] ??= {
2603
+ renames: 0,
2604
+ behaviour: 0,
2605
+ classRenames: 0,
2606
+ });
2607
+ if (row.surface === 'class' && String(row.id).startsWith('css:'))
2608
+ bucket.classRenames++;
2609
+ else if (row.kind === 'rename') bucket.renames++;
2610
+ else bucket.behaviour++;
2611
+ }
2612
+ return {
2613
+ version: doc.version,
2614
+ upgrade: doc.upgrade,
2615
+ guide: doc.guide,
2616
+ count: all.length,
2617
+ releases,
2618
+ perRelease: Object.fromEntries(
2619
+ Object.entries(perRelease).sort(([a], [b]) =>
2620
+ a === 'unreleased'
2621
+ ? 1
2622
+ : b === 'unreleased'
2623
+ ? -1
2624
+ : compareVersions(a, b)
2625
+ )
2626
+ ),
2627
+ hint: 'Counts only. What breaks when you bump: list_changes({ since: "<the version you have>" }) — one line per change; coming from v2: since: "2". One component: { component: "BbSelect" }. Full rows: detail: "rows".',
2628
+ };
2629
+ }
2630
+
2631
+ // A filter with no range, or `since` a v2 version: one line per change
2632
+ // (the v2 → v3 upgrade, or that slice of it).
2633
+ if (since == null && until == null && !rows) {
2634
+ return {
2635
+ version: doc.version,
2636
+ filteredBy,
2637
+ count: all.length,
2638
+ ...changeLines(all, cursor, surface === 'class', component),
2639
+ guide: doc.guide,
2640
+ ...(component
2641
+ ? {
2642
+ libraryWide: (doc.breaking ?? []).filter((row) => row.libraryWide)
2643
+ .length,
2644
+ }
2645
+ : {}),
2646
+ hint: `One line per change (id, components, old → new, or the break in one sentence).${component ? ' `libraryWide` more changes touch every app (CLI, plugin, machine-readable files): list_changes({ kind: "behaviour" }).' : ''} Whole rows: detail: "rows" with the same filter. Next page: cursor: nextCursor.`,
2647
+ };
2648
+ }
2649
+
1802
2650
  // No range asked for: the whole v2 → v3 set, which is what the file has
1803
2651
  // always meant.
1804
2652
  if (since == null && until == null) {
@@ -1824,6 +2672,22 @@ function listChanges({ since: rawSince, until: rawUntil } = {}) {
1824
2672
  return true;
1825
2673
  });
1826
2674
 
2675
+ if (!rows) {
2676
+ return {
2677
+ version: doc.version,
2678
+ since: since ?? null,
2679
+ until: until ?? doc.version,
2680
+ ...(Object.keys(filteredBy).length ? { filteredBy } : {}),
2681
+ count: inRange.length,
2682
+ ...changeLines(inRange, cursor, surface === 'class', component),
2683
+ guide: doc.guide,
2684
+ hint:
2685
+ inRange.length === 0
2686
+ ? 'No breaking changes recorded in that range — features and fixes are deliberately not tracked here, so an empty result means nothing BREAKS, not that nothing changed.'
2687
+ : 'One line per change, grouped by the release that shipped it (id, components, old → new, or the break in one sentence). Whole rows: detail: "rows". The `guide` page explains each. Next page: cursor: nextCursor.',
2688
+ };
2689
+ }
2690
+
1827
2691
  return {
1828
2692
  version: doc.version,
1829
2693
  since: since ?? null,
@@ -1845,7 +2709,17 @@ function toolCallback(fn) {
1845
2709
  return {
1846
2710
  // Compact JSON: indentation was a quarter of every answer (review D6).
1847
2711
  content: [{ type: 'text', text: JSON.stringify(result) }],
1848
- isError: false,
2712
+ // A miss is an error the model should act on (MCP spec: tool
2713
+ // execution errors go in the result with isError). Every miss
2714
+ // still carries its next step: didYouMean, related, hint.
2715
+ // A redirect (a retired v2 name, a picker verdict like BbDrawer →
2716
+ // BbOffCanvas) is an ANSWER, not an error: some clients collapse or
2717
+ // raise on isError (adversarial review 2026-09-26).
2718
+ isError:
2719
+ (result?.found === false &&
2720
+ !result?.retired &&
2721
+ !result?.notInLibrary?.length) ||
2722
+ typeof result?.error === 'string',
1849
2723
  };
1850
2724
  } catch (error) {
1851
2725
  const message = error instanceof Error ? error.message : String(error);
@@ -1867,12 +2741,12 @@ const SERVER_INSTRUCTIONS = [
1867
2741
  'bitboss-ui: a Vue 3 component library. Answer from these tools, never from memory — agents that guess invent props.',
1868
2742
  "- Tailwind v4 app: set cssLayers: TAILWIND_V4_LAYERS ('bitboss-ui/vite') at setup, or utilities lose.",
1869
2743
  '- A topic or "how do I…" question (dark mode, cascade layers, SSR, a plugin option, a CLI command): search_docs, then open the section it points to.',
1870
- '- Which component: search_components (empty query = the whole catalog). One component: get_component({ name }) — compact; pass sections for guide prose, include for more.',
2744
+ '- Which component: search_components (empty query = the whole catalog). One component: get_component({ name }), one line per member; members: [names] for whole rows, sections for prose.',
1871
2745
  '- A type in a props table (BbTableColumn, ButtonVariantType): get_component({ name }) returns its definition and values.',
1872
2746
  '- After writing markup: validate. Before custom CSS: list_tokens.',
1873
- '- An old name (v2 or earlier-beta prop, slot, pt key, class) that stopped working: search_docs(the old name). Which release changed something, or what a bump breaks: list_changes({ since }), each row has firstReleasedIn.',
2747
+ '- An old name (v2 or earlier-beta prop, slot, pt key, class) that stopped working: search_docs(the old name). What a bump breaks: list_changes({ since: <your version> }), one line per change.',
1874
2748
  "- pt in <script> (a const, a wrapper's prop): type it <Name>Props['pt']; Pt* types are not exported.",
1875
- '- Composables (useToast, useConfirm…): get_composable. Whole pages: list_recipes, get_recipe. Long guides answer with an outline: pass sections.',
2749
+ '- Composables: get_composable. Whole pages: list_recipes, get_recipe (prose + files). Long docs answer with an outline: pass sections.',
1876
2750
  ].join('\n');
1877
2751
 
1878
2752
  /** Register the knowledge-base tools on a fresh McpServer instance. */
@@ -1881,12 +2755,33 @@ function buildServer() {
1881
2755
  { name: 'bitboss-ui', version: packageVersion() },
1882
2756
  { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
1883
2757
  );
2758
+ // Every tool reads the installed package and nothing else: say so, so a
2759
+ // client can call them without a confirmation (MCP tool annotations).
2760
+ const registerTool = server.registerTool.bind(server);
2761
+ server.registerTool = (name, config, callback) =>
2762
+ registerTool(
2763
+ name,
2764
+ {
2765
+ title: name
2766
+ .split('_')
2767
+ .map((w, i) => (i ? w : w[0].toUpperCase() + w.slice(1)))
2768
+ .join(' '),
2769
+ ...config,
2770
+ annotations: {
2771
+ readOnlyHint: true,
2772
+ destructiveHint: false,
2773
+ idempotentHint: true,
2774
+ openWorldHint: false,
2775
+ },
2776
+ },
2777
+ callback
2778
+ );
1884
2779
 
1885
2780
  server.registerTool(
1886
2781
  'search_components',
1887
2782
  {
1888
2783
  description:
1889
- 'Search the bitboss-ui component manifest by name and description, ranked by relevance (exact/prefix name matches first, description matches last). Multi-word queries are matched per word, so "data table" finds BbTable. Also returns `notInLibrary`: the component picker\'s authoritative verdicts for names agents commonly invent — either a redirect ("BbDrawer" is BbOffCanvas) or a confirmation that it does not exist and what to do instead. Returns consumer-API components only by default. Use this to find the right Bb* component before writing usage code. An empty result does NOT mean the need is uncovered — read the `component-picker` guide before hand-rolling anything.',
2784
+ 'Find the right Bb* component by name or need ("data table" → BbTable). Also returns `notInLibrary`: verdicts for names agents invent ("BbDrawer" is BbOffCanvas). An empty result does not mean the need is uncovered: read the component-picker guide before hand-rolling anything.',
1890
2785
  inputSchema: {
1891
2786
  query: z
1892
2787
  .string()
@@ -1914,7 +2809,7 @@ function buildServer() {
1914
2809
  'get_component',
1915
2810
  {
1916
2811
  description:
1917
- "One bitboss-ui component: every prop, event, slot, v-model and pt part (never cut), the types its API uses with their values, the guide's outline and its Gotchas + Accessibility sections. Compact by default; pass `sections` for guide prose and `include` for more. Also answers for a public TYPE name (its definition and values), a v2/beta name that was removed or renamed (what replaced it), and near-miss spellings ('bb-button', 'Button').",
2812
+ "One bitboss-ui component, compact: every prop, event and slot as one line (props: name, type, allowed values, default, first sentence; optional props identical across components folded into `commonProps`), v-models, pt parts and states, the types its API uses, the guide's outline and its Gotchas. Then ask for exactly what you need: `members` for whole rows, `sections` for guide prose, `include` for extra parts (each returns only that), `detail: 'full'` for everything. Also answers for a public TYPE name, a removed or renamed v2/beta name, and near-miss spellings ('bb-button', 'Button').",
1918
2813
  inputSchema: {
1919
2814
  name: z
1920
2815
  .string()
@@ -1948,6 +2843,18 @@ function buildServer() {
1948
2843
  .describe(
1949
2844
  `With include: ['contract'] only — cap on the contract markdown (default ${MARKDOWN_DEFAULT_MAX_CHARS}; 0 = no cap).`
1950
2845
  ),
2846
+ members: z
2847
+ .array(z.string())
2848
+ .optional()
2849
+ .describe(
2850
+ "Props, events or slots to return whole, by name, e.g. ['columns', 'click:row', '#actions']. Returns only those rows."
2851
+ ),
2852
+ detail: z
2853
+ .enum(['summary', 'full'])
2854
+ .optional()
2855
+ .describe(
2856
+ "'summary' (default): one line per member. 'full': the whole manifest entry, every description, the Gotchas and Accessibility sections."
2857
+ ),
1951
2858
  },
1952
2859
  },
1953
2860
  toolCallback(getComponent)
@@ -1984,7 +2891,7 @@ function buildServer() {
1984
2891
  'list_changes',
1985
2892
  {
1986
2893
  description:
1987
- 'Breaking changes between two bitboss-ui releases — what breaks if you bump. Call with since=<the version you are on> before upgrading. Omit both arguments for every break in the v2 → v3 upgrade. Only BREAKING changes are tracked; features and fixes are not.',
2894
+ 'Breaking changes between bitboss-ui releases — what breaks if you bump. Call with since=<the version you are on>: one line per change, grouped by release. No arguments: counts per release. Narrow with component / surface / kind; detail: "rows" for the full records. Only BREAKING changes are tracked; features and fixes are not.',
1988
2895
  inputSchema: {
1989
2896
  since: z
1990
2897
  .string()
@@ -1998,6 +2905,30 @@ function buildServer() {
1998
2905
  .describe(
1999
2906
  'Inclusive upper bound (default: the installed version). Unreleased breaks are included only when this is omitted.'
2000
2907
  ),
2908
+ component: z
2909
+ .string()
2910
+ .optional()
2911
+ .describe("Only this component's breaks, e.g. 'BbSelect'."),
2912
+ surface: z
2913
+ .enum(['prop', 'slot', 'pt', 'cssVar', 'class', 'option', 'method'])
2914
+ .optional()
2915
+ .describe('Only renames of this surface.'),
2916
+ kind: z
2917
+ .enum(['rename', 'behaviour'])
2918
+ .optional()
2919
+ .describe('Only renames, or only behaviour changes.'),
2920
+ detail: z
2921
+ .enum(['lines', 'rows'])
2922
+ .optional()
2923
+ .describe(
2924
+ "'lines' (default): one line per change. 'rows': the full records (description, guide, default, fix)."
2925
+ ),
2926
+ cursor: z
2927
+ .string()
2928
+ .optional()
2929
+ .describe(
2930
+ 'The `nextCursor` of the previous answer, for the next page.'
2931
+ ),
2001
2932
  },
2002
2933
  },
2003
2934
  toolCallback(listChanges)
@@ -2051,12 +2982,18 @@ function buildServer() {
2051
2982
  'get_recipe',
2052
2983
  {
2053
2984
  description:
2054
- 'Read one recipe by platform and name — whole by default (recipes are copied whole); pass `sections` to read part of a long one.',
2985
+ 'Read one page recipe by platform and name: its prose (anatomy, key ideas, why), the list of code files with sizes, and the path of the shipped markdown to copy from. Fetch code with `files`, other parts with `sections`; `maxChars: 0` returns the whole markdown.',
2055
2986
  inputSchema: {
2056
2987
  sections: z
2057
2988
  .array(z.string())
2058
2989
  .optional()
2059
- .describe('Section titles to return instead of the whole recipe.'),
2990
+ .describe('Section titles to return instead of the default.'),
2991
+ files: z
2992
+ .array(z.string())
2993
+ .optional()
2994
+ .describe(
2995
+ "Code files to return whole, by title from `files`, e.g. ['Index.vue', 'context.ts']."
2996
+ ),
2060
2997
  platform: platformSchema.describe(
2061
2998
  'Which platform variant of the recipe to read.'
2062
2999
  ),
@@ -2068,7 +3005,9 @@ function buildServer() {
2068
3005
  .int()
2069
3006
  .min(0)
2070
3007
  .optional()
2071
- .describe('Optional cap on the returned markdown (default: no cap).'),
3008
+ .describe(
3009
+ 'Return the markdown itself instead of the outline: 0 = whole, N = cut at N chars.'
3010
+ ),
2072
3011
  },
2073
3012
  },
2074
3013
  toolCallback(getRecipe)
@@ -2100,9 +3039,19 @@ function buildServer() {
2100
3039
  'get_composable',
2101
3040
  {
2102
3041
  description:
2103
- "Read one composable's doc (useToast, useConfirm, useBbTableContext, useMobile…): what it returns, what host it needs, examples.",
3042
+ "Read one composable's doc (useToast, useConfirm, useBbTableContext, useMobile…): what it returns, what host it needs, examples. A long doc answers its intro and outline; pass `sections`.",
2104
3043
  inputSchema: {
2105
3044
  name: z.string().describe("Composable name, e.g. 'useToast'."),
3045
+ sections: z
3046
+ .array(z.string())
3047
+ .optional()
3048
+ .describe('Section titles to return, from `outline`.'),
3049
+ maxChars: z
3050
+ .number()
3051
+ .int()
3052
+ .min(0)
3053
+ .optional()
3054
+ .describe('Return the doc itself: 0 = whole, N = cut at N chars.'),
2106
3055
  },
2107
3056
  },
2108
3057
  toolCallback(getComposable)