@happyvertical/smrt-svelte 0.30.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 (357) hide show
  1. package/AGENTS.md +317 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +185 -0
  5. package/dist/Provider.svelte +204 -0
  6. package/dist/Provider.svelte.d.ts +73 -0
  7. package/dist/Provider.svelte.d.ts.map +1 -0
  8. package/dist/__tests__/app-state.test.js +156 -0
  9. package/dist/__tests__/warm-clients.test.js +186 -0
  10. package/dist/browser-ai/adapters/llm/factory.d.ts +38 -0
  11. package/dist/browser-ai/adapters/llm/factory.d.ts.map +1 -0
  12. package/dist/browser-ai/adapters/llm/factory.js +91 -0
  13. package/dist/browser-ai/adapters/llm/index.d.ts +7 -0
  14. package/dist/browser-ai/adapters/llm/index.d.ts.map +1 -0
  15. package/dist/browser-ai/adapters/llm/index.js +6 -0
  16. package/dist/browser-ai/adapters/llm/types.d.ts +182 -0
  17. package/dist/browser-ai/adapters/llm/types.d.ts.map +1 -0
  18. package/dist/browser-ai/adapters/llm/types.js +43 -0
  19. package/dist/browser-ai/adapters/llm/webllm.d.ts +33 -0
  20. package/dist/browser-ai/adapters/llm/webllm.d.ts.map +1 -0
  21. package/dist/browser-ai/adapters/llm/webllm.js +225 -0
  22. package/dist/browser-ai/adapters/stt/browser-speech.d.ts +31 -0
  23. package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -0
  24. package/dist/browser-ai/adapters/stt/browser-speech.js +217 -0
  25. package/dist/browser-ai/adapters/stt/factory.d.ts +49 -0
  26. package/dist/browser-ai/adapters/stt/factory.d.ts.map +1 -0
  27. package/dist/browser-ai/adapters/stt/factory.js +110 -0
  28. package/dist/browser-ai/adapters/stt/index.d.ts +9 -0
  29. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -0
  30. package/dist/browser-ai/adapters/stt/index.js +8 -0
  31. package/dist/browser-ai/adapters/stt/types.d.ts +154 -0
  32. package/dist/browser-ai/adapters/stt/types.d.ts.map +1 -0
  33. package/dist/browser-ai/adapters/stt/types.js +4 -0
  34. package/dist/browser-ai/adapters/stt/whisper-cpp.d.ts +46 -0
  35. package/dist/browser-ai/adapters/stt/whisper-cpp.d.ts.map +1 -0
  36. package/dist/browser-ai/adapters/stt/whisper-cpp.js +348 -0
  37. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts +51 -0
  38. package/dist/browser-ai/adapters/stt/whisper-wasm.d.ts.map +1 -0
  39. package/dist/browser-ai/adapters/stt/whisper-wasm.js +380 -0
  40. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +42 -0
  41. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -0
  42. package/dist/browser-ai/adapters/tts/browser-synthesis.js +235 -0
  43. package/dist/browser-ai/adapters/tts/factory.d.ts +53 -0
  44. package/dist/browser-ai/adapters/tts/factory.d.ts.map +1 -0
  45. package/dist/browser-ai/adapters/tts/factory.js +92 -0
  46. package/dist/browser-ai/adapters/tts/index.d.ts +7 -0
  47. package/dist/browser-ai/adapters/tts/index.d.ts.map +1 -0
  48. package/dist/browser-ai/adapters/tts/index.js +6 -0
  49. package/dist/browser-ai/adapters/tts/types.d.ts +140 -0
  50. package/dist/browser-ai/adapters/tts/types.d.ts.map +1 -0
  51. package/dist/browser-ai/adapters/tts/types.js +4 -0
  52. package/dist/browser-ai/capabilities/detector.d.ts +38 -0
  53. package/dist/browser-ai/capabilities/detector.d.ts.map +1 -0
  54. package/dist/browser-ai/capabilities/detector.js +211 -0
  55. package/dist/browser-ai/core/errors.d.ts +62 -0
  56. package/dist/browser-ai/core/errors.d.ts.map +1 -0
  57. package/dist/browser-ai/core/errors.js +92 -0
  58. package/dist/browser-ai/core/index.d.ts +6 -0
  59. package/dist/browser-ai/core/index.d.ts.map +1 -0
  60. package/dist/browser-ai/core/index.js +5 -0
  61. package/dist/browser-ai/core/types.d.ts +115 -0
  62. package/dist/browser-ai/core/types.d.ts.map +1 -0
  63. package/dist/browser-ai/core/types.js +34 -0
  64. package/dist/browser-ai/index.d.ts +12 -0
  65. package/dist/browser-ai/index.d.ts.map +1 -0
  66. package/dist/browser-ai/index.js +16 -0
  67. package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte +77 -0
  68. package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte.d.ts +16 -0
  69. package/dist/browser-ai/svelte/components/AILoadingOverlay.svelte.d.ts.map +1 -0
  70. package/dist/browser-ai/svelte/components/CapabilityGate.svelte +57 -0
  71. package/dist/browser-ai/svelte/components/CapabilityGate.svelte.d.ts +15 -0
  72. package/dist/browser-ai/svelte/components/CapabilityGate.svelte.d.ts.map +1 -0
  73. package/dist/browser-ai/svelte/components/DownloadProgress.svelte +141 -0
  74. package/dist/browser-ai/svelte/components/DownloadProgress.svelte.d.ts +15 -0
  75. package/dist/browser-ai/svelte/components/DownloadProgress.svelte.d.ts.map +1 -0
  76. package/dist/browser-ai/svelte/components/STTTest.svelte +379 -0
  77. package/dist/browser-ai/svelte/components/STTTest.svelte.d.ts +9 -0
  78. package/dist/browser-ai/svelte/components/STTTest.svelte.d.ts.map +1 -0
  79. package/dist/browser-ai/svelte/components/VoiceInput.svelte +200 -0
  80. package/dist/browser-ai/svelte/components/VoiceInput.svelte.d.ts +16 -0
  81. package/dist/browser-ai/svelte/components/VoiceInput.svelte.d.ts.map +1 -0
  82. package/dist/browser-ai/svelte/index.d.ts +15 -0
  83. package/dist/browser-ai/svelte/index.d.ts.map +1 -0
  84. package/dist/browser-ai/svelte/index.js +28 -0
  85. package/dist/browser-ai/ui.d.ts +16 -0
  86. package/dist/browser-ai/ui.d.ts.map +1 -0
  87. package/dist/browser-ai/ui.js +67 -0
  88. package/dist/components/admin/AgentAdminPanel.svelte +111 -0
  89. package/dist/components/admin/AgentAdminPanel.svelte.d.ts +25 -0
  90. package/dist/components/admin/AgentAdminPanel.svelte.d.ts.map +1 -0
  91. package/dist/components/admin/AgentAdminTabs.svelte +280 -0
  92. package/dist/components/admin/AgentAdminTabs.svelte.d.ts +23 -0
  93. package/dist/components/admin/AgentAdminTabs.svelte.d.ts.map +1 -0
  94. package/dist/components/admin/AgentSettingsShell.svelte +257 -0
  95. package/dist/components/admin/AgentSettingsShell.svelte.d.ts +33 -0
  96. package/dist/components/admin/AgentSettingsShell.svelte.d.ts.map +1 -0
  97. package/dist/components/admin/index.d.ts +5 -0
  98. package/dist/components/admin/index.d.ts.map +1 -0
  99. package/dist/components/admin/index.js +6 -0
  100. package/dist/components/forms/AddressInput.svelte +500 -0
  101. package/dist/components/forms/AddressInput.svelte.d.ts +36 -0
  102. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -0
  103. package/dist/components/forms/CheckboxInput.svelte +208 -0
  104. package/dist/components/forms/CheckboxInput.svelte.d.ts +20 -0
  105. package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -0
  106. package/dist/components/forms/DateRangeInput.svelte +628 -0
  107. package/dist/components/forms/DateRangeInput.svelte.d.ts +33 -0
  108. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -0
  109. package/dist/components/forms/DateTimeInput.svelte +521 -0
  110. package/dist/components/forms/DateTimeInput.svelte.d.ts +24 -0
  111. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -0
  112. package/dist/components/forms/FileUpload.svelte +358 -0
  113. package/dist/components/forms/FileUpload.svelte.d.ts +22 -0
  114. package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -0
  115. package/dist/components/forms/Form.svelte +771 -0
  116. package/dist/components/forms/Form.svelte.d.ts +26 -0
  117. package/dist/components/forms/Form.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/FormGroup.svelte +86 -0
  119. package/dist/components/forms/FormGroup.svelte.d.ts +13 -0
  120. package/dist/components/forms/FormGroup.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/FormMicButton.svelte +179 -0
  122. package/dist/components/forms/FormMicButton.svelte.d.ts +10 -0
  123. package/dist/components/forms/FormMicButton.svelte.d.ts.map +1 -0
  124. package/dist/components/forms/Input.svelte +83 -0
  125. package/dist/components/forms/Input.svelte.d.ts +9 -0
  126. package/dist/components/forms/Input.svelte.d.ts.map +1 -0
  127. package/dist/components/forms/MeasurementInput.svelte +505 -0
  128. package/dist/components/forms/MeasurementInput.svelte.d.ts +35 -0
  129. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -0
  130. package/dist/components/forms/MoneyInput.svelte +412 -0
  131. package/dist/components/forms/MoneyInput.svelte.d.ts +30 -0
  132. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -0
  133. package/dist/components/forms/NumberInput.svelte +310 -0
  134. package/dist/components/forms/NumberInput.svelte.d.ts +28 -0
  135. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -0
  136. package/dist/components/forms/PhoneInput.svelte +530 -0
  137. package/dist/components/forms/PhoneInput.svelte.d.ts +22 -0
  138. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -0
  139. package/dist/components/forms/SearchInput.svelte +358 -0
  140. package/dist/components/forms/SearchInput.svelte.d.ts +33 -0
  141. package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
  142. package/dist/components/forms/Select.svelte +83 -0
  143. package/dist/components/forms/Select.svelte.d.ts +11 -0
  144. package/dist/components/forms/Select.svelte.d.ts.map +1 -0
  145. package/dist/components/forms/SelectInput.svelte +254 -0
  146. package/dist/components/forms/SelectInput.svelte.d.ts +25 -0
  147. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -0
  148. package/dist/components/forms/TextInput.svelte +415 -0
  149. package/dist/components/forms/TextInput.svelte.d.ts +26 -0
  150. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -0
  151. package/dist/components/forms/Textarea.svelte +85 -0
  152. package/dist/components/forms/Textarea.svelte.d.ts +10 -0
  153. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -0
  154. package/dist/components/forms/TextareaInput.svelte +386 -0
  155. package/dist/components/forms/TextareaInput.svelte.d.ts +26 -0
  156. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -0
  157. package/dist/components/forms/Toggle.svelte +217 -0
  158. package/dist/components/forms/Toggle.svelte.d.ts +37 -0
  159. package/dist/components/forms/Toggle.svelte.d.ts.map +1 -0
  160. package/dist/components/forms/__tests__/AddressInput.behavior.test.js +122 -0
  161. package/dist/components/forms/__tests__/CheckboxInput.test.js +92 -0
  162. package/dist/components/forms/__tests__/DateRangeInput.behavior.test.js +135 -0
  163. package/dist/components/forms/__tests__/DateTimeInput.behavior.test.js +103 -0
  164. package/dist/components/forms/__tests__/FileUpload.test.js +90 -0
  165. package/dist/components/forms/__tests__/Form.behavior.test.js +137 -0
  166. package/dist/components/forms/__tests__/Form.test.js +58 -0
  167. package/dist/components/forms/__tests__/FormGroup.test.js +48 -0
  168. package/dist/components/forms/__tests__/FormMicButton.test.js +86 -0
  169. package/dist/components/forms/__tests__/Input.test.js +49 -0
  170. package/dist/components/forms/__tests__/MeasurementInput.behavior.test.js +129 -0
  171. package/dist/components/forms/__tests__/MoneyInput.behavior.test.js +124 -0
  172. package/dist/components/forms/__tests__/NumberInput.behavior.test.js +141 -0
  173. package/dist/components/forms/__tests__/PhoneInput.behavior.test.js +96 -0
  174. package/dist/components/forms/__tests__/SearchInput.test.js +79 -0
  175. package/dist/components/forms/__tests__/Select.test.js +37 -0
  176. package/dist/components/forms/__tests__/SelectInput.behavior.test.js +132 -0
  177. package/dist/components/forms/__tests__/TextInput.behavior.test.js +131 -0
  178. package/dist/components/forms/__tests__/Textarea.test.js +39 -0
  179. package/dist/components/forms/__tests__/TextareaInput.behavior.test.js +96 -0
  180. package/dist/components/forms/__tests__/Toggle.test.js +87 -0
  181. package/dist/components/forms/__tests__/composite-inputs-a11y.test.js +69 -0
  182. package/dist/components/forms/__tests__/form-group-input.fixture.svelte +16 -0
  183. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +9 -0
  184. package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +1 -0
  185. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +33 -0
  186. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +12 -0
  187. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -0
  188. package/dist/components/forms/__tests__/rich-inputs-a11y.test.js +87 -0
  189. package/dist/components/forms/index.d.ts +25 -0
  190. package/dist/components/forms/index.d.ts.map +1 -0
  191. package/dist/components/forms/index.js +25 -0
  192. package/dist/components/forms/types.d.ts +33 -0
  193. package/dist/components/forms/types.d.ts.map +1 -0
  194. package/dist/components/forms/types.js +4 -0
  195. package/dist/components/module/ModulePanel.svelte +134 -0
  196. package/dist/components/module/ModulePanel.svelte.d.ts +22 -0
  197. package/dist/components/module/ModulePanel.svelte.d.ts.map +1 -0
  198. package/dist/components/module/index.d.ts +5 -0
  199. package/dist/components/module/index.d.ts.map +1 -0
  200. package/dist/components/module/index.js +4 -0
  201. package/dist/components/workspace/Breadcrumbs.svelte +141 -0
  202. package/dist/components/workspace/Breadcrumbs.svelte.d.ts +21 -0
  203. package/dist/components/workspace/Breadcrumbs.svelte.d.ts.map +1 -0
  204. package/dist/components/workspace/NavTree.svelte +354 -0
  205. package/dist/components/workspace/NavTree.svelte.d.ts +45 -0
  206. package/dist/components/workspace/NavTree.svelte.d.ts.map +1 -0
  207. package/dist/components/workspace/README.md +34 -0
  208. package/dist/components/workspace/RoleShell.svelte +309 -0
  209. package/dist/components/workspace/RoleShell.svelte.d.ts +91 -0
  210. package/dist/components/workspace/RoleShell.svelte.d.ts.map +1 -0
  211. package/dist/components/workspace/WorkspaceShell.svelte +951 -0
  212. package/dist/components/workspace/WorkspaceShell.svelte.d.ts +112 -0
  213. package/dist/components/workspace/WorkspaceShell.svelte.d.ts.map +1 -0
  214. package/dist/components/workspace/__tests__/RoleShell.test.js +772 -0
  215. package/dist/components/workspace/__tests__/WorkspaceShell.test.js +630 -0
  216. package/dist/components/workspace/__tests__/breadcrumbs-helpers.test.js +141 -0
  217. package/dist/components/workspace/__tests__/context-forwarding-harness.svelte +45 -0
  218. package/dist/components/workspace/__tests__/context-forwarding-harness.svelte.d.ts +21 -0
  219. package/dist/components/workspace/__tests__/context-forwarding-harness.svelte.d.ts.map +1 -0
  220. package/dist/components/workspace/__tests__/define-tools-dock.test.js +1010 -0
  221. package/dist/components/workspace/__tests__/harness.svelte +25 -0
  222. package/dist/components/workspace/__tests__/harness.svelte.d.ts +14 -0
  223. package/dist/components/workspace/__tests__/harness.svelte.d.ts.map +1 -0
  224. package/dist/components/workspace/__tests__/index.test.js +37 -0
  225. package/dist/components/workspace/__tests__/manifest-nav-helpers.test.js +24 -0
  226. package/dist/components/workspace/__tests__/manifest-nav.test.js +599 -0
  227. package/dist/components/workspace/__tests__/nav-helpers.test.js +95 -0
  228. package/dist/components/workspace/__tests__/render-harness.svelte +66 -0
  229. package/dist/components/workspace/__tests__/render-harness.svelte.d.ts +32 -0
  230. package/dist/components/workspace/__tests__/render-harness.svelte.d.ts.map +1 -0
  231. package/dist/components/workspace/__tests__/render-tools-dock.test.js +243 -0
  232. package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte +58 -0
  233. package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte.d.ts +16 -0
  234. package/dist/components/workspace/__tests__/role-shell-bind-harness.svelte.d.ts.map +1 -0
  235. package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte +41 -0
  236. package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte.d.ts +13 -0
  237. package/dist/components/workspace/__tests__/role-shell-switch-harness.svelte.d.ts.map +1 -0
  238. package/dist/components/workspace/__tests__/test-icon.svelte +17 -0
  239. package/dist/components/workspace/__tests__/test-icon.svelte.d.ts +19 -0
  240. package/dist/components/workspace/__tests__/test-icon.svelte.d.ts.map +1 -0
  241. package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte +38 -0
  242. package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte.d.ts +22 -0
  243. package/dist/components/workspace/__tests__/typed-tool-fixture/TypedTool.svelte.d.ts.map +1 -0
  244. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts +65 -0
  245. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.d.ts.map +1 -0
  246. package/dist/components/workspace/__tests__/typed-tool-fixture/register-typed-tool.js +115 -0
  247. package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.d.ts +15 -0
  248. package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.d.ts.map +1 -0
  249. package/dist/components/workspace/__tests__/typed-tool-fixture/typed-tool-types.js +7 -0
  250. package/dist/components/workspace/__tests__/typed-tool-fixture.test.js +115 -0
  251. package/dist/components/workspace/__tests__/use-harness-orphan.svelte +9 -0
  252. package/dist/components/workspace/__tests__/use-harness-orphan.svelte.d.ts +19 -0
  253. package/dist/components/workspace/__tests__/use-harness-orphan.svelte.d.ts.map +1 -0
  254. package/dist/components/workspace/__tests__/use-harness.svelte +23 -0
  255. package/dist/components/workspace/__tests__/use-harness.svelte.d.ts +8 -0
  256. package/dist/components/workspace/__tests__/use-harness.svelte.d.ts.map +1 -0
  257. package/dist/components/workspace/__tests__/use-tools-dock.test.js +33 -0
  258. package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte +43 -0
  259. package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte.d.ts +11 -0
  260. package/dist/components/workspace/__tests__/workspace-shell-bind-harness.svelte.d.ts.map +1 -0
  261. package/dist/components/workspace/breadcrumbs-helpers.d.ts +44 -0
  262. package/dist/components/workspace/breadcrumbs-helpers.d.ts.map +1 -0
  263. package/dist/components/workspace/breadcrumbs-helpers.js +88 -0
  264. package/dist/components/workspace/index.d.ts +16 -0
  265. package/dist/components/workspace/index.d.ts.map +1 -0
  266. package/dist/components/workspace/index.js +14 -0
  267. package/dist/components/workspace/manifest-nav.d.ts +200 -0
  268. package/dist/components/workspace/manifest-nav.d.ts.map +1 -0
  269. package/dist/components/workspace/manifest-nav.js +408 -0
  270. package/dist/components/workspace/nav-helpers.d.ts +36 -0
  271. package/dist/components/workspace/nav-helpers.d.ts.map +1 -0
  272. package/dist/components/workspace/nav-helpers.js +60 -0
  273. package/dist/components/workspace/server/__tests__/compose-availability.test.js +383 -0
  274. package/dist/components/workspace/server/__tests__/typed-context-fixture.d.ts +78 -0
  275. package/dist/components/workspace/server/__tests__/typed-context-fixture.d.ts.map +1 -0
  276. package/dist/components/workspace/server/__tests__/typed-context-fixture.js +104 -0
  277. package/dist/components/workspace/server/compose-availability.d.ts +73 -0
  278. package/dist/components/workspace/server/compose-availability.d.ts.map +1 -0
  279. package/dist/components/workspace/server/compose-availability.js +114 -0
  280. package/dist/components/workspace/server/index.d.ts +13 -0
  281. package/dist/components/workspace/server/index.d.ts.map +1 -0
  282. package/dist/components/workspace/server/index.js +11 -0
  283. package/dist/components/workspace/server/types.d.ts +108 -0
  284. package/dist/components/workspace/server/types.d.ts.map +1 -0
  285. package/dist/components/workspace/server/types.js +11 -0
  286. package/dist/components/workspace/tools-dock/ToolsDock.svelte +565 -0
  287. package/dist/components/workspace/tools-dock/ToolsDock.svelte.d.ts +14 -0
  288. package/dist/components/workspace/tools-dock/ToolsDock.svelte.d.ts.map +1 -0
  289. package/dist/components/workspace/tools-dock/define-tools-dock.svelte.d.ts +143 -0
  290. package/dist/components/workspace/tools-dock/define-tools-dock.svelte.d.ts.map +1 -0
  291. package/dist/components/workspace/tools-dock/define-tools-dock.svelte.js +487 -0
  292. package/dist/components/workspace/tools-dock/use-tools-dock.d.ts +41 -0
  293. package/dist/components/workspace/tools-dock/use-tools-dock.d.ts.map +1 -0
  294. package/dist/components/workspace/tools-dock/use-tools-dock.js +50 -0
  295. package/dist/components/workspace/types.d.ts +372 -0
  296. package/dist/components/workspace/types.d.ts.map +1 -0
  297. package/dist/components/workspace/types.js +6 -0
  298. package/dist/hooks/index.d.ts +11 -0
  299. package/dist/hooks/index.d.ts.map +1 -0
  300. package/dist/hooks/index.js +10 -0
  301. package/dist/hooks/useAppState.svelte.d.ts +46 -0
  302. package/dist/hooks/useAppState.svelte.d.ts.map +1 -0
  303. package/dist/hooks/useAppState.svelte.js +59 -0
  304. package/dist/hooks/useAuth.svelte.d.ts +41 -0
  305. package/dist/hooks/useAuth.svelte.d.ts.map +1 -0
  306. package/dist/hooks/useAuth.svelte.js +43 -0
  307. package/dist/hooks/useLLM.svelte.d.ts +69 -0
  308. package/dist/hooks/useLLM.svelte.d.ts.map +1 -0
  309. package/dist/hooks/useLLM.svelte.js +85 -0
  310. package/dist/hooks/useSTT.svelte.d.ts +68 -0
  311. package/dist/hooks/useSTT.svelte.d.ts.map +1 -0
  312. package/dist/hooks/useSTT.svelte.js +97 -0
  313. package/dist/hooks/useSocket.svelte.d.ts +45 -0
  314. package/dist/hooks/useSocket.svelte.d.ts.map +1 -0
  315. package/dist/hooks/useSocket.svelte.js +54 -0
  316. package/dist/hooks/useTTS.svelte.d.ts +65 -0
  317. package/dist/hooks/useTTS.svelte.d.ts.map +1 -0
  318. package/dist/hooks/useTTS.svelte.js +93 -0
  319. package/dist/hooks/useTheme.d.ts +13 -0
  320. package/dist/hooks/useTheme.d.ts.map +1 -0
  321. package/dist/hooks/useTheme.js +16 -0
  322. package/dist/i18n/__tests__/server.spec.js +50 -0
  323. package/dist/i18n/server.d.ts +47 -0
  324. package/dist/i18n/server.d.ts.map +1 -0
  325. package/dist/i18n/server.js +58 -0
  326. package/dist/i18n/strings.forms.d.ts +33 -0
  327. package/dist/i18n/strings.forms.d.ts.map +1 -0
  328. package/dist/i18n/strings.forms.js +54 -0
  329. package/dist/i18n/strings.workspace.d.ts +34 -0
  330. package/dist/i18n/strings.workspace.d.ts.map +1 -0
  331. package/dist/i18n/strings.workspace.js +40 -0
  332. package/dist/index.d.ts +18 -0
  333. package/dist/index.d.ts.map +1 -0
  334. package/dist/index.js +23 -0
  335. package/dist/state/__tests__/warm-clients.test.js +40 -0
  336. package/dist/state/app-state.d.ts +308 -0
  337. package/dist/state/app-state.d.ts.map +1 -0
  338. package/dist/state/app-state.js +64 -0
  339. package/dist/state/app-state.svelte.d.ts +196 -0
  340. package/dist/state/app-state.svelte.d.ts.map +1 -0
  341. package/dist/state/app-state.svelte.js +774 -0
  342. package/dist/state/context.d.ts +23 -0
  343. package/dist/state/context.d.ts.map +1 -0
  344. package/dist/state/context.js +32 -0
  345. package/dist/state/form-context.d.ts +59 -0
  346. package/dist/state/form-context.d.ts.map +1 -0
  347. package/dist/state/form-context.js +31 -0
  348. package/dist/state/form-group-context.d.ts +13 -0
  349. package/dist/state/form-group-context.d.ts.map +1 -0
  350. package/dist/state/form-group-context.js +28 -0
  351. package/dist/state/index.d.ts +9 -0
  352. package/dist/state/index.d.ts.map +1 -0
  353. package/dist/state/index.js +8 -0
  354. package/dist/state/warm-clients.d.ts +136 -0
  355. package/dist/state/warm-clients.d.ts.map +1 -0
  356. package/dist/state/warm-clients.js +231 -0
  357. package/package.json +137 -0
@@ -0,0 +1,1010 @@
1
+ /**
2
+ * Tests for defineToolsDock factory.
3
+ *
4
+ * These tests exercise the reactive API surface directly. Svelte `setContext`
5
+ * is called inside the factory, but the returned `ToolsDockInstance` is the
6
+ * subject under test — we don't need a full component tree to verify the
7
+ * state machine.
8
+ *
9
+ * `setContext` only works inside a component init scope, so we invoke the
10
+ * factory inside Svelte's `mount` (via a thin host component) so that all
11
+ * `$state` runes wire up correctly.
12
+ */
13
+ import { flushSync, mount, unmount } from 'svelte';
14
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
15
+ import HostHarness from './harness.svelte';
16
+ const noopTool = {};
17
+ function mountDock(options) {
18
+ const target = document.createElement('div');
19
+ document.body.appendChild(target);
20
+ const exposed = {
21
+ dock: undefined,
22
+ };
23
+ const component = mount(HostHarness, {
24
+ target,
25
+ props: {
26
+ options,
27
+ onReady: (dock) => {
28
+ exposed.dock = dock;
29
+ },
30
+ },
31
+ });
32
+ return {
33
+ dock: exposed.dock,
34
+ teardown: () => {
35
+ unmount(component);
36
+ target.remove();
37
+ },
38
+ };
39
+ }
40
+ describe('defineToolsDock', () => {
41
+ let cleanup = [];
42
+ beforeEach(() => {
43
+ cleanup = [];
44
+ if (typeof window !== 'undefined') {
45
+ window.localStorage.clear();
46
+ }
47
+ });
48
+ afterEach(() => {
49
+ for (const fn of cleanup.splice(0))
50
+ fn();
51
+ });
52
+ function track(value) {
53
+ cleanup.push(value.teardown);
54
+ return value;
55
+ }
56
+ it('creates an API with sensible defaults', () => {
57
+ const { dock } = track(mountDock({
58
+ tools: [
59
+ { id: 'chat', label: 'Chat', component: noopTool },
60
+ { id: 'jobs', label: 'Jobs', component: noopTool },
61
+ ],
62
+ }));
63
+ expect(dock.isOpen).toBe(false);
64
+ expect(dock.activeTool).toBeNull();
65
+ expect(dock.layout).toBe('rail');
66
+ expect(dock.storageKey).toBeNull();
67
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat', 'jobs']);
68
+ });
69
+ it('open() / close() / toggle() update isOpen and activeTool', () => {
70
+ const { dock } = track(mountDock({
71
+ tools: [
72
+ { id: 'chat', label: 'Chat', component: noopTool },
73
+ { id: 'jobs', label: 'Jobs', component: noopTool },
74
+ ],
75
+ }));
76
+ dock.open('chat');
77
+ flushSync();
78
+ expect(dock.isOpen).toBe(true);
79
+ expect(dock.activeTool).toBe('chat');
80
+ dock.close();
81
+ flushSync();
82
+ expect(dock.isOpen).toBe(false);
83
+ // activeTool persists so re-opening lands on the same tool.
84
+ expect(dock.activeTool).toBe('chat');
85
+ dock.toggle('jobs');
86
+ flushSync();
87
+ expect(dock.isOpen).toBe(true);
88
+ expect(dock.activeTool).toBe('jobs');
89
+ // toggle on the same id closes.
90
+ dock.toggle('jobs');
91
+ flushSync();
92
+ expect(dock.isOpen).toBe(false);
93
+ // toggle without id flips just isOpen.
94
+ dock.toggle();
95
+ flushSync();
96
+ expect(dock.isOpen).toBe(true);
97
+ });
98
+ it('open() on unknown id is a no-op (does not crash, does not open)', () => {
99
+ const { dock } = track(mountDock({
100
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
101
+ }));
102
+ dock.open('does-not-exist');
103
+ flushSync();
104
+ expect(dock.isOpen).toBe(false);
105
+ expect(dock.activeTool).toBeNull();
106
+ });
107
+ it('respects initialOpen and picks first tool when toggled', () => {
108
+ const { dock } = track(mountDock({
109
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
110
+ initialOpen: true,
111
+ }));
112
+ expect(dock.isOpen).toBe(true);
113
+ expect(dock.activeTool).toBeNull();
114
+ dock.toggle();
115
+ flushSync();
116
+ // First toggle while no tool active selects the first available.
117
+ // (Was open → now closed; activeTool now set on the next open.)
118
+ expect(dock.isOpen).toBe(false);
119
+ dock.toggle();
120
+ flushSync();
121
+ expect(dock.isOpen).toBe(true);
122
+ expect(dock.activeTool).toBe('chat');
123
+ });
124
+ it('setContext updates context and triggers fetchAvailability', async () => {
125
+ const fetchAvailability = vi
126
+ .fn()
127
+ .mockResolvedValueOnce([{ id: 'chat' }])
128
+ .mockResolvedValueOnce([{ id: 'chat' }, { id: 'jobs' }]);
129
+ const { dock } = track(mountDock({
130
+ tools: [
131
+ { id: 'chat', label: 'Chat', component: noopTool },
132
+ { id: 'jobs', label: 'Jobs', component: noopTool },
133
+ { id: 'hidden', label: 'Hidden', component: noopTool },
134
+ ],
135
+ fetchAvailability,
136
+ }));
137
+ // No call yet — fetchAvailability only fires on setContext.
138
+ expect(fetchAvailability).not.toHaveBeenCalled();
139
+ dock.setContext({ type: 'thing', title: 'A' });
140
+ // setContext invokes fetchAvailability synchronously; the promise resolves
141
+ // on the next microtask, so flushSync after a few microtasks is required
142
+ // for Svelte to settle the resulting state update.
143
+ expect(fetchAvailability).toHaveBeenCalledTimes(1);
144
+ await new Promise((r) => setTimeout(r, 0));
145
+ flushSync();
146
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat']);
147
+ dock.setContext({ type: 'thing', title: 'B' });
148
+ expect(fetchAvailability).toHaveBeenCalledTimes(2);
149
+ await new Promise((r) => setTimeout(r, 0));
150
+ flushSync();
151
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat', 'jobs']);
152
+ });
153
+ it('drops stale fetchAvailability results (race-safety)', async () => {
154
+ let resolveFirst = null;
155
+ let resolveSecond = null;
156
+ const fetchAvailability = vi
157
+ .fn()
158
+ .mockImplementationOnce(() => new Promise((r) => {
159
+ resolveFirst = r;
160
+ }))
161
+ .mockImplementationOnce(() => new Promise((r) => {
162
+ resolveSecond = r;
163
+ }));
164
+ const { dock } = track(mountDock({
165
+ tools: [
166
+ { id: 'chat', label: 'Chat', component: noopTool },
167
+ { id: 'jobs', label: 'Jobs', component: noopTool },
168
+ ],
169
+ fetchAvailability,
170
+ }));
171
+ dock.setContext({ type: 'A' });
172
+ await Promise.resolve();
173
+ dock.setContext({ type: 'B' });
174
+ await Promise.resolve();
175
+ // Resolve the *stale* (first) call after the newer one. Then resolve the
176
+ // newer one. The stale value must not stomp the fresh value.
177
+ resolveFirst?.([{ id: 'chat' }]);
178
+ resolveSecond?.([{ id: 'jobs' }]);
179
+ await Promise.resolve();
180
+ await Promise.resolve();
181
+ flushSync();
182
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['jobs']);
183
+ });
184
+ it('emit / on / unsubscribe', () => {
185
+ const { dock } = track(mountDock({
186
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
187
+ }));
188
+ const handler = vi.fn();
189
+ const off = dock.on('test', handler);
190
+ dock.emit('test', { ok: true });
191
+ expect(handler).toHaveBeenCalledWith({ ok: true });
192
+ off();
193
+ dock.emit('test', { ok: false });
194
+ expect(handler).toHaveBeenCalledTimes(1);
195
+ });
196
+ it('emit catches handler errors without dropping later handlers', () => {
197
+ const { dock } = track(mountDock({
198
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
199
+ }));
200
+ const throwing = vi.fn(() => {
201
+ throw new Error('boom');
202
+ });
203
+ const after = vi.fn();
204
+ dock.on('test', throwing);
205
+ dock.on('test', after);
206
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
207
+ expect(() => dock.emit('test', null)).not.toThrow();
208
+ expect(throwing).toHaveBeenCalled();
209
+ expect(after).toHaveBeenCalled();
210
+ errSpy.mockRestore();
211
+ });
212
+ it('persists state to localStorage when storageKey provided', () => {
213
+ const { dock } = track(mountDock({
214
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
215
+ storageKey: 'tdtest:v1',
216
+ }));
217
+ dock.open('chat');
218
+ flushSync();
219
+ const raw = window.localStorage.getItem('tdtest:v1');
220
+ expect(raw).not.toBeNull();
221
+ const parsed = JSON.parse(raw);
222
+ expect(parsed).toEqual({ isOpen: true, activeTool: 'chat' });
223
+ });
224
+ it('hydrates state from localStorage on hydrate()', () => {
225
+ window.localStorage.setItem('tdtest:v2', JSON.stringify({ isOpen: true, activeTool: 'jobs' }));
226
+ const { dock } = track(mountDock({
227
+ tools: [
228
+ { id: 'chat', label: 'Chat', component: noopTool },
229
+ { id: 'jobs', label: 'Jobs', component: noopTool },
230
+ ],
231
+ storageKey: 'tdtest:v2',
232
+ }));
233
+ dock.hydrate();
234
+ flushSync();
235
+ expect(dock.isOpen).toBe(true);
236
+ expect(dock.activeTool).toBe('jobs');
237
+ });
238
+ it('hydrate() ignores activeTool that no longer matches a registered tool', () => {
239
+ window.localStorage.setItem('tdtest:v3', JSON.stringify({ isOpen: true, activeTool: 'gone' }));
240
+ const { dock } = track(mountDock({
241
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
242
+ storageKey: 'tdtest:v3',
243
+ }));
244
+ dock.hydrate();
245
+ flushSync();
246
+ expect(dock.isOpen).toBe(true);
247
+ expect(dock.activeTool).toBeNull();
248
+ });
249
+ it('exposes context on the public ToolsDockApi surface', () => {
250
+ const { dock } = track(mountDock({
251
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
252
+ }));
253
+ expect(dock.context).toBeNull();
254
+ dock.setContext({ type: 'route', title: 'Home' });
255
+ flushSync();
256
+ expect(dock.context).toEqual({ type: 'route', title: 'Home' });
257
+ dock.setContext(null);
258
+ flushSync();
259
+ expect(dock.context).toBeNull();
260
+ });
261
+ it('badge: null in availability clears a registered default (not falls back)', async () => {
262
+ const fetchAvailability = vi
263
+ .fn()
264
+ .mockResolvedValueOnce([{ id: 'chat', badge: null }]);
265
+ const { dock } = track(mountDock({
266
+ tools: [{ id: 'chat', label: 'Chat', badge: 3, component: noopTool }],
267
+ fetchAvailability,
268
+ }));
269
+ dock.setContext({ type: 'a' });
270
+ await new Promise((r) => setTimeout(r, 0));
271
+ flushSync();
272
+ // `badge: null` is explicit ("clear it"), not a fallback signal.
273
+ expect(dock.availableTools[0].badge).toBeNull();
274
+ });
275
+ it('handles synchronous throws from fetchAvailability without surfacing them', async () => {
276
+ const fetchAvailability = vi.fn(() => {
277
+ throw new Error('sync boom');
278
+ });
279
+ const { dock } = track(mountDock({
280
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
281
+ fetchAvailability: fetchAvailability,
282
+ }));
283
+ // The sync throw must be caught by the wrapped promise chain, not
284
+ // propagate out of setContext to the caller.
285
+ expect(() => dock.setContext({ type: 'a' })).not.toThrow();
286
+ // The internal `.catch` applies an empty-availability result.
287
+ await new Promise((r) => setTimeout(r, 0));
288
+ flushSync();
289
+ expect(dock.availableTools).toEqual([]);
290
+ });
291
+ describe('refreshAvailability()', () => {
292
+ it('re-runs fetchAvailability with the current context', async () => {
293
+ let availability = [{ id: 'chat' }];
294
+ const fetchAvailability = vi.fn(async () => availability);
295
+ const { dock } = track(mountDock({
296
+ tools: [
297
+ { id: 'chat', label: 'Chat', component: noopTool },
298
+ { id: 'jobs', label: 'Jobs', component: noopTool },
299
+ ],
300
+ fetchAvailability,
301
+ }));
302
+ const ctx = { type: 'thing', title: 'A' };
303
+ dock.setContext(ctx);
304
+ await new Promise((r) => setTimeout(r, 0));
305
+ flushSync();
306
+ expect(fetchAvailability).toHaveBeenCalledTimes(1);
307
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat']);
308
+ // Side-channel signal: availability changed without a context change.
309
+ // setContext(ctx) would short-circuit; refreshAvailability() forces it.
310
+ availability = [{ id: 'chat' }, { id: 'jobs' }];
311
+ dock.refreshAvailability();
312
+ expect(fetchAvailability).toHaveBeenCalledTimes(2);
313
+ // Called with the same (unchanged) context.
314
+ expect(fetchAvailability).toHaveBeenLastCalledWith(ctx);
315
+ await new Promise((r) => setTimeout(r, 0));
316
+ flushSync();
317
+ // Second fetch's result is reflected in availableTools.
318
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat', 'jobs']);
319
+ });
320
+ it('resets to all registered tools when no fetchAvailability is configured', () => {
321
+ const { dock } = track(mountDock({
322
+ tools: [
323
+ { id: 'chat', label: 'Chat', component: noopTool },
324
+ { id: 'jobs', label: 'Jobs', component: noopTool },
325
+ ],
326
+ }));
327
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat', 'jobs']);
328
+ // No-op semantically (all tools were already available), but the
329
+ // method should not crash without a fetchAvailability callback.
330
+ expect(() => dock.refreshAvailability()).not.toThrow();
331
+ flushSync();
332
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat', 'jobs']);
333
+ });
334
+ it("emits 'dock:change' when refresh changes only badge values", async () => {
335
+ // Regression for the badge-only refresh case: a side-channel signal
336
+ // fires (e.g. a Jobs-count websocket message) and the consumer calls
337
+ // `refreshAvailability()` to repaint badges. The tool ids haven't
338
+ // changed and the active tool stays put, but `availableTools` now
339
+ // carries a different badge — consumers mirroring this into a topbar
340
+ // count must see a `'dock:change'` event to repaint without polling.
341
+ let availability = [
342
+ { id: 'chat', badge: 0 },
343
+ { id: 'jobs', badge: 0 },
344
+ ];
345
+ const fetchAvailability = vi.fn(async () => availability);
346
+ const { dock } = track(mountDock({
347
+ tools: [
348
+ { id: 'chat', label: 'Chat', component: noopTool },
349
+ { id: 'jobs', label: 'Jobs', component: noopTool },
350
+ ],
351
+ fetchAvailability,
352
+ }));
353
+ dock.setContext({ type: 'thing' });
354
+ await new Promise((r) => setTimeout(r, 0));
355
+ flushSync();
356
+ const handler = vi.fn();
357
+ dock.on('dock:change', handler);
358
+ // Side-channel update: same tool ids, different badge values.
359
+ availability = [
360
+ { id: 'chat', badge: 0 },
361
+ { id: 'jobs', badge: 7 },
362
+ ];
363
+ dock.refreshAvailability();
364
+ await new Promise((r) => setTimeout(r, 0));
365
+ flushSync();
366
+ // Exactly one emit reflecting the new badge.
367
+ expect(handler).toHaveBeenCalledTimes(1);
368
+ expect(handler).toHaveBeenLastCalledWith({
369
+ isOpen: false,
370
+ activeTool: null,
371
+ context: { type: 'thing' },
372
+ });
373
+ const jobsAfter = dock.availableTools.find((t) => t.id === 'jobs');
374
+ expect(jobsAfter?.badge).toBe(7);
375
+ // Calling refreshAvailability again with byte-identical results must
376
+ // NOT emit a second time. The discriminating shape check keeps no-op
377
+ // refreshes silent so consumer mirrors don't see spurious updates.
378
+ dock.refreshAvailability();
379
+ await new Promise((r) => setTimeout(r, 0));
380
+ flushSync();
381
+ expect(handler).toHaveBeenCalledTimes(1);
382
+ });
383
+ it("emits 'dock:change' when refresh clears the active tool", async () => {
384
+ // Direct coverage for the active-tool-clear branch of
385
+ // `applyAvailability` reached via `refreshAvailability()` (rather
386
+ // than via a `setContext()` → refresh chain).
387
+ let availability = [{ id: 'chat' }, { id: 'jobs' }];
388
+ const fetchAvailability = vi.fn(async () => availability);
389
+ const { dock } = track(mountDock({
390
+ tools: [
391
+ { id: 'chat', label: 'Chat', component: noopTool },
392
+ { id: 'jobs', label: 'Jobs', component: noopTool },
393
+ ],
394
+ fetchAvailability,
395
+ }));
396
+ dock.setContext({ type: 'thing' });
397
+ await new Promise((r) => setTimeout(r, 0));
398
+ flushSync();
399
+ dock.open('jobs');
400
+ flushSync();
401
+ expect(dock.activeTool).toBe('jobs');
402
+ const handler = vi.fn();
403
+ dock.on('dock:change', handler);
404
+ // Side-channel availability shrink — 'jobs' is no longer available.
405
+ availability = [{ id: 'chat' }];
406
+ dock.refreshAvailability();
407
+ await new Promise((r) => setTimeout(r, 0));
408
+ flushSync();
409
+ expect(dock.activeTool).toBeNull();
410
+ expect(handler).toHaveBeenCalledTimes(1);
411
+ expect(handler).toHaveBeenLastCalledWith({
412
+ isOpen: true,
413
+ activeTool: null,
414
+ context: { type: 'thing' },
415
+ });
416
+ });
417
+ it('drops stale results from a prior refreshAvailability call (race-safety)', async () => {
418
+ let resolveFirst = null;
419
+ let resolveSecond = null;
420
+ const fetchAvailability = vi
421
+ .fn()
422
+ .mockImplementationOnce(() => new Promise((r) => {
423
+ resolveFirst = r;
424
+ }))
425
+ .mockImplementationOnce(() => new Promise((r) => {
426
+ resolveSecond = r;
427
+ }));
428
+ const { dock } = track(mountDock({
429
+ tools: [
430
+ { id: 'chat', label: 'Chat', component: noopTool },
431
+ { id: 'jobs', label: 'Jobs', component: noopTool },
432
+ ],
433
+ fetchAvailability,
434
+ }));
435
+ // First call via refreshAvailability (no prior setContext).
436
+ dock.refreshAvailability();
437
+ await Promise.resolve();
438
+ // Second call before the first resolves.
439
+ dock.refreshAvailability();
440
+ await Promise.resolve();
441
+ // Resolve stale first AFTER fresh second was requested.
442
+ resolveFirst?.([{ id: 'chat' }]);
443
+ resolveSecond?.([{ id: 'jobs' }]);
444
+ await Promise.resolve();
445
+ await Promise.resolve();
446
+ flushSync();
447
+ // Only the most recent result applies.
448
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['jobs']);
449
+ });
450
+ });
451
+ it('clears activeTool when availability removes the active tool', async () => {
452
+ let availability = [{ id: 'chat' }, { id: 'jobs' }];
453
+ const fetchAvailability = vi.fn(async () => availability);
454
+ const { dock } = track(mountDock({
455
+ tools: [
456
+ { id: 'chat', label: 'Chat', component: noopTool },
457
+ { id: 'jobs', label: 'Jobs', component: noopTool },
458
+ ],
459
+ fetchAvailability,
460
+ }));
461
+ dock.setContext({ type: 'a' });
462
+ await Promise.resolve();
463
+ await Promise.resolve();
464
+ flushSync();
465
+ dock.open('jobs');
466
+ flushSync();
467
+ expect(dock.activeTool).toBe('jobs');
468
+ availability = [{ id: 'chat' }];
469
+ dock.setContext({ type: 'b' });
470
+ await Promise.resolve();
471
+ await Promise.resolve();
472
+ flushSync();
473
+ expect(dock.availableTools.map((t) => t.id)).toEqual(['chat']);
474
+ expect(dock.activeTool).toBeNull();
475
+ });
476
+ describe("'dock:change' event", () => {
477
+ it('does not fire during factory construction', () => {
478
+ // Subscribing in a separate tick would miss any event fired during the
479
+ // factory body. To assert "no construction-time emit" we count via a
480
+ // listener added *before* the factory runs — but the factory's
481
+ // listener bus is internal, so instead we rely on the fact that
482
+ // `'dock:change'` is gated by the `ready` flag and verify
483
+ // post-construction state is clean (no recursive errors etc.) here.
484
+ const { dock } = track(mountDock({
485
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
486
+ initialOpen: true,
487
+ }));
488
+ // Sanity: initial state reflects options without any emit having fired.
489
+ expect(dock.isOpen).toBe(true);
490
+ expect(dock.activeTool).toBeNull();
491
+ });
492
+ it('fires on open() with the post-update state', () => {
493
+ const { dock } = track(mountDock({
494
+ tools: [
495
+ { id: 'chat', label: 'Chat', component: noopTool },
496
+ { id: 'jobs', label: 'Jobs', component: noopTool },
497
+ ],
498
+ }));
499
+ const handler = vi.fn();
500
+ dock.on('dock:change', handler);
501
+ dock.open('chat');
502
+ flushSync();
503
+ expect(handler).toHaveBeenCalledTimes(1);
504
+ expect(handler).toHaveBeenLastCalledWith({
505
+ isOpen: true,
506
+ activeTool: 'chat',
507
+ context: null,
508
+ });
509
+ });
510
+ it('fires on close() with the post-update state', () => {
511
+ const { dock } = track(mountDock({
512
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
513
+ }));
514
+ dock.open('chat');
515
+ flushSync();
516
+ const handler = vi.fn();
517
+ dock.on('dock:change', handler);
518
+ dock.close();
519
+ flushSync();
520
+ expect(handler).toHaveBeenCalledTimes(1);
521
+ expect(handler).toHaveBeenLastCalledWith({
522
+ // close() leaves activeTool intact so re-opens land on the same tool.
523
+ isOpen: false,
524
+ activeTool: 'chat',
525
+ context: null,
526
+ });
527
+ });
528
+ it('fires on toggle() with the post-update state', () => {
529
+ const { dock } = track(mountDock({
530
+ tools: [
531
+ { id: 'chat', label: 'Chat', component: noopTool },
532
+ { id: 'jobs', label: 'Jobs', component: noopTool },
533
+ ],
534
+ }));
535
+ const handler = vi.fn();
536
+ dock.on('dock:change', handler);
537
+ dock.toggle('chat');
538
+ flushSync();
539
+ expect(handler).toHaveBeenCalledTimes(1);
540
+ expect(handler).toHaveBeenLastCalledWith({
541
+ isOpen: true,
542
+ activeTool: 'chat',
543
+ context: null,
544
+ });
545
+ dock.toggle('chat');
546
+ flushSync();
547
+ expect(handler).toHaveBeenCalledTimes(2);
548
+ expect(handler).toHaveBeenLastCalledWith({
549
+ isOpen: false,
550
+ activeTool: 'chat',
551
+ context: null,
552
+ });
553
+ dock.toggle();
554
+ flushSync();
555
+ expect(handler).toHaveBeenCalledTimes(3);
556
+ expect(handler).toHaveBeenLastCalledWith({
557
+ isOpen: true,
558
+ activeTool: 'chat',
559
+ context: null,
560
+ });
561
+ });
562
+ it('fires on setContext() with the new context', () => {
563
+ const { dock } = track(mountDock({
564
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
565
+ }));
566
+ const handler = vi.fn();
567
+ dock.on('dock:change', handler);
568
+ dock.setContext({ type: 'route', title: 'Home' });
569
+ flushSync();
570
+ expect(handler).toHaveBeenCalledTimes(1);
571
+ expect(handler).toHaveBeenLastCalledWith({
572
+ isOpen: false,
573
+ activeTool: null,
574
+ context: { type: 'route', title: 'Home' },
575
+ });
576
+ });
577
+ it('fires once with cleared state when availability removes the active tool', async () => {
578
+ let availability = [{ id: 'chat' }, { id: 'jobs' }];
579
+ const fetchAvailability = vi.fn(async () => availability);
580
+ const { dock } = track(mountDock({
581
+ tools: [
582
+ { id: 'chat', label: 'Chat', component: noopTool },
583
+ { id: 'jobs', label: 'Jobs', component: noopTool },
584
+ ],
585
+ fetchAvailability,
586
+ }));
587
+ dock.setContext({ type: 'a' });
588
+ await Promise.resolve();
589
+ await Promise.resolve();
590
+ flushSync();
591
+ dock.open('jobs');
592
+ flushSync();
593
+ expect(dock.activeTool).toBe('jobs');
594
+ const handler = vi.fn();
595
+ dock.on('dock:change', handler);
596
+ availability = [{ id: 'chat' }];
597
+ dock.setContext({ type: 'b' });
598
+ // setContext emits synchronously …
599
+ expect(handler).toHaveBeenCalledTimes(1);
600
+ // … then the async availability refresh resolves, clears the active
601
+ // tool, and emits a second 'dock:change' with the cleared state.
602
+ await Promise.resolve();
603
+ await Promise.resolve();
604
+ flushSync();
605
+ expect(handler).toHaveBeenCalledTimes(2);
606
+ expect(handler).toHaveBeenLastCalledWith({
607
+ isOpen: true,
608
+ activeTool: null,
609
+ context: { type: 'b' },
610
+ });
611
+ });
612
+ it('stops calling a handler after its unsubscribe function runs', () => {
613
+ const { dock } = track(mountDock({
614
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
615
+ }));
616
+ const handler = vi.fn();
617
+ const off = dock.on('dock:change', handler);
618
+ dock.open('chat');
619
+ flushSync();
620
+ expect(handler).toHaveBeenCalledTimes(1);
621
+ off();
622
+ dock.close();
623
+ flushSync();
624
+ expect(handler).toHaveBeenCalledTimes(1);
625
+ });
626
+ it('payload type is inferred from ToolsDockEvents', () => {
627
+ const { dock } = track(mountDock({
628
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
629
+ }));
630
+ // Compile-time check: handler arg should be the `dock:change` payload
631
+ // shape without an explicit generic.
632
+ dock.on('dock:change', (e) => {
633
+ const _isOpen = e.isOpen;
634
+ const _activeTool = e.activeTool;
635
+ void _isOpen;
636
+ void _activeTool;
637
+ });
638
+ // Runtime sanity check that the test compiles & wires.
639
+ dock.open('chat');
640
+ flushSync();
641
+ expect(dock.isOpen).toBe(true);
642
+ });
643
+ it('non-"dock:*" event names use the stringly-typed overload (e.g. "change" is not reserved)', () => {
644
+ // After namespacing built-ins under `'dock:*'`, the literal `'change'`
645
+ // is no longer a key of ToolsDockEvents, so consumers may use it
646
+ // freely with an explicit payload generic via the stringly-typed
647
+ // overload.
648
+ const { dock } = track(mountDock({
649
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
650
+ }));
651
+ const customHandler = vi.fn();
652
+ const off = dock.on('change', customHandler);
653
+ dock.emit('change', { selected: 'row-1' });
654
+ expect(customHandler).toHaveBeenCalledWith({ selected: 'row-1' });
655
+ // Also confirm a namespaced consumer event works.
656
+ const appHandler = vi.fn();
657
+ dock.on('app:custom', appHandler);
658
+ dock.emit('app:custom', { id: 42 });
659
+ expect(appHandler).toHaveBeenCalledWith({ id: 42 });
660
+ // Built-in 'dock:change' must NOT trigger the custom 'change' listener.
661
+ dock.open('chat');
662
+ flushSync();
663
+ expect(customHandler).toHaveBeenCalledTimes(1);
664
+ off();
665
+ });
666
+ // ─────────────────────────────────────────────────────────────
667
+ // Regression tests: no-op state changes must NOT fire 'dock:change'.
668
+ //
669
+ // Before this guard, idempotent calls like `dock.open(activeId)` when
670
+ // already open emitted spurious events. Combined with effects that
671
+ // call setContext on every prop change, that produced redundant work
672
+ // (availability refetch) and could amplify into noisy loops when
673
+ // consumers wired the 'dock:change' event back into upstream state.
674
+ // ─────────────────────────────────────────────────────────────
675
+ it('does NOT fire on open() when already at that state', () => {
676
+ const { dock } = track(mountDock({
677
+ tools: [
678
+ { id: 'chat', label: 'Chat', component: noopTool },
679
+ { id: 'jobs', label: 'Jobs', component: noopTool },
680
+ ],
681
+ }));
682
+ dock.open('chat');
683
+ flushSync();
684
+ const handler = vi.fn();
685
+ dock.on('dock:change', handler);
686
+ // Same id, already open — no observable state change.
687
+ dock.open('chat');
688
+ flushSync();
689
+ expect(handler).not.toHaveBeenCalled();
690
+ // open() with no id, already open with an active tool — still a no-op.
691
+ dock.open();
692
+ flushSync();
693
+ expect(handler).not.toHaveBeenCalled();
694
+ });
695
+ it('does NOT fire on close() when already closed', () => {
696
+ const { dock } = track(mountDock({
697
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
698
+ }));
699
+ const handler = vi.fn();
700
+ dock.on('dock:change', handler);
701
+ // Initial state is closed; calling close again must not emit.
702
+ dock.close();
703
+ flushSync();
704
+ expect(handler).not.toHaveBeenCalled();
705
+ });
706
+ it('does NOT fire on setContext() with the same reference', async () => {
707
+ const fetchAvailability = vi.fn().mockResolvedValue([{ id: 'chat' }]);
708
+ const { dock } = track(mountDock({
709
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
710
+ fetchAvailability,
711
+ }));
712
+ const ctx = { type: 'route', title: 'Home' };
713
+ dock.setContext(ctx);
714
+ // Initial emit + initial fetchAvailability call.
715
+ await new Promise((r) => setTimeout(r, 0));
716
+ flushSync();
717
+ expect(fetchAvailability).toHaveBeenCalledTimes(1);
718
+ const handler = vi.fn();
719
+ dock.on('dock:change', handler);
720
+ // Same reference — must short-circuit (no emit, no refetch).
721
+ dock.setContext(ctx);
722
+ flushSync();
723
+ await new Promise((r) => setTimeout(r, 0));
724
+ flushSync();
725
+ expect(handler).not.toHaveBeenCalled();
726
+ expect(fetchAvailability).toHaveBeenCalledTimes(1);
727
+ });
728
+ it('does NOT fire on toggle(id) that resolves to the same state', () => {
729
+ // toggle(sameId) when open+active flips to closed; toggle again
730
+ // flips back to open+active. Each transition is real, so each emits.
731
+ // The "no observable change" branch for toggle is hard to trigger
732
+ // through the API surface alone (toggle always flips at minimum
733
+ // `isOpen`), so the assertion here is the symmetric one: the emit
734
+ // counts match the number of real state transitions.
735
+ const { dock } = track(mountDock({
736
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
737
+ }));
738
+ const handler = vi.fn();
739
+ dock.on('dock:change', handler);
740
+ dock.toggle('chat'); // open+active
741
+ flushSync();
742
+ dock.toggle('chat'); // close
743
+ flushSync();
744
+ expect(handler).toHaveBeenCalledTimes(2);
745
+ });
746
+ it('does not retrigger context-forwarding $effect when state mutates', async () => {
747
+ // Regression guard for the reactive-read leak in emitChange. Without
748
+ // `untrack` around the $state reads, an $effect calling
749
+ // dock.setContext would also depend on isOpen/activeTool/context
750
+ // (read inside the emitChange invoked by setContext). A subsequent
751
+ // dock.open() would mutate those, re-run the effect, call setContext
752
+ // again, etc. Here we verify open() does NOT cause the forwarding
753
+ // effect's tracked count to grow.
754
+ const fetchAvailability = vi.fn().mockResolvedValue([{ id: 'chat' }]);
755
+ const { dock } = track(mountDock({
756
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
757
+ fetchAvailability,
758
+ }));
759
+ // Mount a child component whose $effect mirrors a `currentCtx` $state
760
+ // into dock.setContext — same pattern <ToolsDock>'s context prop uses.
761
+ const target = document.createElement('div');
762
+ document.body.appendChild(target);
763
+ cleanup.push(() => target.remove());
764
+ // We use the harness pattern: a tiny inline component would require
765
+ // its own file. Instead, count how many times fetchAvailability runs
766
+ // after the initial setContext — open() / close() must not cause it
767
+ // to re-run via a re-triggered forwarding effect.
768
+ dock.setContext({ type: 'a' });
769
+ await new Promise((r) => setTimeout(r, 0));
770
+ flushSync();
771
+ const initialFetchCount = fetchAvailability.mock.calls.length;
772
+ const handler = vi.fn();
773
+ dock.on('dock:change', handler);
774
+ dock.open('chat');
775
+ flushSync();
776
+ dock.close();
777
+ flushSync();
778
+ // Two real state transitions → two emits, but the context-forwarding
779
+ // chain stays quiet: no extra fetchAvailability calls beyond the
780
+ // initial setContext.
781
+ expect(handler).toHaveBeenCalledTimes(2);
782
+ expect(fetchAvailability.mock.calls.length).toBe(initialFetchCount);
783
+ });
784
+ });
785
+ // ─────────────────────────────────────────────────────────────
786
+ // Granular events: 'dock:state-changed' and 'dock:context-changed'
787
+ // split the legacy 'dock:change' so consumers can subscribe to just
788
+ // the slice they care about. The legacy event still fires (back-compat).
789
+ // ─────────────────────────────────────────────────────────────
790
+ describe("'dock:state-changed' event", () => {
791
+ it('fires on open() / close() / toggle() with state-only payload', () => {
792
+ const { dock } = track(mountDock({
793
+ tools: [
794
+ { id: 'chat', label: 'Chat', component: noopTool },
795
+ { id: 'jobs', label: 'Jobs', component: noopTool },
796
+ ],
797
+ }));
798
+ const handler = vi.fn();
799
+ dock.on('dock:state-changed', handler);
800
+ dock.open('chat');
801
+ flushSync();
802
+ expect(handler).toHaveBeenCalledTimes(1);
803
+ expect(handler).toHaveBeenLastCalledWith({
804
+ isOpen: true,
805
+ activeTool: 'chat',
806
+ });
807
+ dock.close();
808
+ flushSync();
809
+ expect(handler).toHaveBeenCalledTimes(2);
810
+ expect(handler).toHaveBeenLastCalledWith({
811
+ isOpen: false,
812
+ activeTool: 'chat',
813
+ });
814
+ dock.toggle('jobs');
815
+ flushSync();
816
+ expect(handler).toHaveBeenCalledTimes(3);
817
+ expect(handler).toHaveBeenLastCalledWith({
818
+ isOpen: true,
819
+ activeTool: 'jobs',
820
+ });
821
+ });
822
+ it('does NOT fire on setContext() (context-only mutation)', () => {
823
+ const { dock } = track(mountDock({
824
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
825
+ }));
826
+ const handler = vi.fn();
827
+ dock.on('dock:state-changed', handler);
828
+ dock.setContext({ type: 'route', title: 'Home' });
829
+ flushSync();
830
+ // setContext alone does not change isOpen/activeTool — must stay silent.
831
+ expect(handler).not.toHaveBeenCalled();
832
+ });
833
+ it('fires when availability clears the active tool', async () => {
834
+ let availability = [{ id: 'chat' }, { id: 'jobs' }];
835
+ const fetchAvailability = vi.fn(async () => availability);
836
+ const { dock } = track(mountDock({
837
+ tools: [
838
+ { id: 'chat', label: 'Chat', component: noopTool },
839
+ { id: 'jobs', label: 'Jobs', component: noopTool },
840
+ ],
841
+ fetchAvailability,
842
+ }));
843
+ dock.setContext({ type: 'a' });
844
+ await new Promise((r) => setTimeout(r, 0));
845
+ flushSync();
846
+ dock.open('jobs');
847
+ flushSync();
848
+ const handler = vi.fn();
849
+ dock.on('dock:state-changed', handler);
850
+ // Availability shrink → 'jobs' is no longer available, activeTool clears.
851
+ availability = [{ id: 'chat' }];
852
+ dock.refreshAvailability();
853
+ await new Promise((r) => setTimeout(r, 0));
854
+ flushSync();
855
+ expect(handler).toHaveBeenCalledTimes(1);
856
+ expect(handler).toHaveBeenLastCalledWith({
857
+ isOpen: true,
858
+ activeTool: null,
859
+ });
860
+ });
861
+ it('does NOT fire on no-op open() / close() / toggle()', () => {
862
+ const { dock } = track(mountDock({
863
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
864
+ }));
865
+ dock.open('chat');
866
+ flushSync();
867
+ const handler = vi.fn();
868
+ dock.on('dock:state-changed', handler);
869
+ dock.open('chat'); // already open + active
870
+ flushSync();
871
+ dock.close();
872
+ flushSync();
873
+ dock.close(); // already closed
874
+ flushSync();
875
+ // One observable transition (open → close), so exactly one emit.
876
+ expect(handler).toHaveBeenCalledTimes(1);
877
+ });
878
+ it('does NOT fire on badge-only availability refresh', async () => {
879
+ // Pure badge churn: no state change, no context change. The legacy
880
+ // 'dock:change' event still fires (for back-compat consumers
881
+ // mirroring availableTools), but 'dock:state-changed' stays silent.
882
+ let availability = [
883
+ { id: 'chat', badge: 0 },
884
+ ];
885
+ const fetchAvailability = vi.fn(async () => availability);
886
+ const { dock } = track(mountDock({
887
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
888
+ fetchAvailability,
889
+ }));
890
+ dock.setContext({ type: 'a' });
891
+ await new Promise((r) => setTimeout(r, 0));
892
+ flushSync();
893
+ const stateHandler = vi.fn();
894
+ dock.on('dock:state-changed', stateHandler);
895
+ availability = [{ id: 'chat', badge: 7 }];
896
+ dock.refreshAvailability();
897
+ await new Promise((r) => setTimeout(r, 0));
898
+ flushSync();
899
+ expect(stateHandler).not.toHaveBeenCalled();
900
+ });
901
+ });
902
+ describe("'dock:context-changed' event", () => {
903
+ it('fires on setContext() with a different reference', () => {
904
+ const { dock } = track(mountDock({
905
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
906
+ }));
907
+ const handler = vi.fn();
908
+ dock.on('dock:context-changed', handler);
909
+ const ctx = { type: 'route', title: 'Home' };
910
+ dock.setContext(ctx);
911
+ flushSync();
912
+ expect(handler).toHaveBeenCalledTimes(1);
913
+ expect(handler).toHaveBeenLastCalledWith({ context: ctx });
914
+ dock.setContext(null);
915
+ flushSync();
916
+ expect(handler).toHaveBeenCalledTimes(2);
917
+ expect(handler).toHaveBeenLastCalledWith({ context: null });
918
+ });
919
+ it('does NOT fire on open() / close() / toggle() (state-only mutations)', () => {
920
+ const { dock } = track(mountDock({
921
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
922
+ }));
923
+ const handler = vi.fn();
924
+ dock.on('dock:context-changed', handler);
925
+ dock.open('chat');
926
+ flushSync();
927
+ dock.close();
928
+ flushSync();
929
+ dock.toggle('chat');
930
+ flushSync();
931
+ expect(handler).not.toHaveBeenCalled();
932
+ });
933
+ it('does NOT fire on setContext() with the same reference', () => {
934
+ const { dock } = track(mountDock({
935
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
936
+ }));
937
+ const ctx = { type: 'route' };
938
+ dock.setContext(ctx);
939
+ flushSync();
940
+ const handler = vi.fn();
941
+ dock.on('dock:context-changed', handler);
942
+ dock.setContext(ctx); // same reference — no-op-guarded
943
+ flushSync();
944
+ expect(handler).not.toHaveBeenCalled();
945
+ });
946
+ it('does NOT fire on a badge-only availability refresh', async () => {
947
+ let availability = [
948
+ { id: 'chat', badge: 0 },
949
+ ];
950
+ const fetchAvailability = vi.fn(async () => availability);
951
+ const { dock } = track(mountDock({
952
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
953
+ fetchAvailability,
954
+ }));
955
+ dock.setContext({ type: 'a' });
956
+ await new Promise((r) => setTimeout(r, 0));
957
+ flushSync();
958
+ const handler = vi.fn();
959
+ dock.on('dock:context-changed', handler);
960
+ availability = [{ id: 'chat', badge: 7 }];
961
+ dock.refreshAvailability();
962
+ await new Promise((r) => setTimeout(r, 0));
963
+ flushSync();
964
+ // Context didn't change — badge refresh stays silent on this event.
965
+ expect(handler).not.toHaveBeenCalled();
966
+ });
967
+ });
968
+ describe("'dock:change' event continues to fire (back-compat)", () => {
969
+ it('fires on state, context, and badge-only availability transitions', async () => {
970
+ // Use a fetchAvailability that yields the same shape the dock seeded
971
+ // from the registered tools (no badge), so the post-setContext
972
+ // availability application is a no-op and we can count emits cleanly.
973
+ const fetchAvailability = vi
974
+ .fn()
975
+ .mockResolvedValue([{ id: 'chat' }]);
976
+ const { dock } = track(mountDock({
977
+ tools: [{ id: 'chat', label: 'Chat', component: noopTool }],
978
+ fetchAvailability,
979
+ }));
980
+ const legacyHandler = vi.fn();
981
+ dock.on('dock:change', legacyHandler);
982
+ // 1. setContext → 1 synchronous emit. The async availability fetch
983
+ // resolves to the same shape the registered tools already had
984
+ // (id='chat', no badge), so applyAvailability sees no change and
985
+ // does NOT emit a second time.
986
+ dock.setContext({ type: 'a' });
987
+ flushSync();
988
+ await new Promise((r) => setTimeout(r, 0));
989
+ flushSync();
990
+ expect(legacyHandler).toHaveBeenCalledTimes(1);
991
+ // 2. open() → 1 more emit (state change).
992
+ dock.open('chat');
993
+ flushSync();
994
+ expect(legacyHandler).toHaveBeenCalledTimes(2);
995
+ // 3. badge-only availability refresh → 1 more emit. This is the
996
+ // back-compat slice: granular 'dock:state-changed' and
997
+ // 'dock:context-changed' stay silent, but legacy mirrors of
998
+ // availableTools still see the update via 'dock:change'.
999
+ fetchAvailability.mockResolvedValueOnce([{ id: 'chat', badge: 5 }]);
1000
+ dock.refreshAvailability();
1001
+ await new Promise((r) => setTimeout(r, 0));
1002
+ flushSync();
1003
+ expect(legacyHandler).toHaveBeenCalledTimes(3);
1004
+ // 4. close() → 1 more emit (state change).
1005
+ dock.close();
1006
+ flushSync();
1007
+ expect(legacyHandler).toHaveBeenCalledTimes(4);
1008
+ });
1009
+ });
1010
+ });