sygnal 5.4.0 → 6.1.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 (360) hide show
  1. package/CHANGELOG.md +716 -0
  2. package/README.md +24 -9
  3. package/dist/ai.cjs.js +115 -0
  4. package/dist/ai.cjs.js.map +1 -0
  5. package/dist/ai.esm.js +2 -0
  6. package/dist/ai.esm.js.map +1 -0
  7. package/dist/astro/client.cjs.js +16 -9
  8. package/dist/astro/client.cjs.js.map +1 -1
  9. package/dist/astro/client.mjs +16 -9
  10. package/dist/astro/client.mjs.map +1 -1
  11. package/dist/astro/index.cjs.js +958 -92
  12. package/dist/astro/index.cjs.js.map +1 -1
  13. package/dist/astro/index.mjs +958 -93
  14. package/dist/astro/index.mjs.map +1 -1
  15. package/dist/astro/server.cjs.js +3396 -165
  16. package/dist/astro/server.cjs.js.map +1 -1
  17. package/dist/astro/server.mjs +3396 -165
  18. package/dist/astro/server.mjs.map +1 -1
  19. package/dist/devtools.cjs.js +1840 -0
  20. package/dist/devtools.cjs.js.map +1 -0
  21. package/dist/devtools.esm.js +1827 -0
  22. package/dist/devtools.esm.js.map +1 -0
  23. package/dist/diagnostics.cjs.js +3190 -296
  24. package/dist/diagnostics.cjs.js.map +1 -1
  25. package/dist/diagnostics.esm.js +3190 -296
  26. package/dist/diagnostics.esm.js.map +1 -1
  27. package/dist/element.cjs.js +230 -0
  28. package/dist/element.cjs.js.map +1 -0
  29. package/dist/element.esm.js +228 -0
  30. package/dist/element.esm.js.map +1 -0
  31. package/dist/guide/accessibility.md +310 -0
  32. package/dist/guide/adapters.md +221 -0
  33. package/dist/guide/agent.md +588 -0
  34. package/dist/guide/ai-chat.md +693 -0
  35. package/dist/guide/ai-decisions.md +330 -0
  36. package/dist/guide/behaviors.md +312 -0
  37. package/dist/guide/browser-sources.md +203 -0
  38. package/dist/guide/drag-and-drop.md +342 -0
  39. package/dist/guide/element-commands.md +306 -0
  40. package/dist/guide/error-boundaries.md +169 -0
  41. package/dist/guide/forms-reference.md +409 -0
  42. package/dist/guide/forms.md +251 -0
  43. package/dist/guide/http.md +301 -0
  44. package/dist/guide/inputs.md +173 -0
  45. package/dist/guide/mcp-apps.md +272 -0
  46. package/dist/guide/persistence.md +245 -0
  47. package/dist/guide/recipes/ai-form-fill.md +163 -0
  48. package/dist/guide/recipes/ai-summarize.md +155 -0
  49. package/dist/guide/recipes/ai-support-inbox.md +190 -0
  50. package/dist/guide/recipes/carousel.md +155 -0
  51. package/dist/guide/recipes/charts.md +177 -0
  52. package/dist/guide/recipes/code-editor.md +135 -0
  53. package/dist/guide/recipes/data-grid.md +155 -0
  54. package/dist/guide/recipes/data-table.md +196 -0
  55. package/dist/guide/recipes/i18n.md +222 -0
  56. package/dist/guide/recipes/icons.md +115 -0
  57. package/dist/guide/recipes/overview.md +43 -0
  58. package/dist/guide/recipes/rich-text.md +154 -0
  59. package/dist/guide/resources.md +471 -0
  60. package/dist/guide/ssr.md +255 -0
  61. package/dist/guide/timers.md +178 -0
  62. package/dist/guide/ui/accordion.md +93 -0
  63. package/dist/guide/ui/combobox.md +144 -0
  64. package/dist/guide/ui/dialog.md +124 -0
  65. package/dist/guide/ui/disclosure.md +60 -0
  66. package/dist/guide/ui/menu.md +125 -0
  67. package/dist/guide/ui/overview.md +166 -0
  68. package/dist/guide/ui/popover.md +101 -0
  69. package/dist/guide/ui/select.md +114 -0
  70. package/dist/guide/ui/tabs.md +161 -0
  71. package/dist/guide/ui/toaster.md +152 -0
  72. package/dist/guide/ui/tooltip.md +103 -0
  73. package/dist/guide/undo.md +178 -0
  74. package/dist/guide/virtual-collections.md +183 -0
  75. package/dist/guide/web-components.md +179 -0
  76. package/dist/guide/webmcp.md +226 -0
  77. package/dist/guide/widgets.md +217 -0
  78. package/dist/index.cjs.js +19288 -7408
  79. package/dist/index.cjs.js.map +1 -1
  80. package/dist/index.d.ts +4466 -552
  81. package/dist/index.esm.js +19274 -7461
  82. package/dist/index.esm.js.map +1 -1
  83. package/dist/jsx-dev-runtime.cjs.js +26 -334
  84. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  85. package/dist/jsx-dev-runtime.esm.js +19 -327
  86. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  87. package/dist/jsx-runtime.cjs.js +26 -334
  88. package/dist/jsx-runtime.cjs.js.map +1 -1
  89. package/dist/jsx-runtime.esm.js +19 -327
  90. package/dist/jsx-runtime.esm.js.map +1 -1
  91. package/dist/jsx.cjs.js +10 -314
  92. package/dist/jsx.cjs.js.map +1 -1
  93. package/dist/jsx.esm.js +8 -315
  94. package/dist/jsx.esm.js.map +1 -1
  95. package/dist/react.cjs.js +117 -0
  96. package/dist/react.cjs.js.map +1 -0
  97. package/dist/react.esm.js +115 -0
  98. package/dist/react.esm.js.map +1 -0
  99. package/dist/shims/globalthis.cjs +20 -0
  100. package/dist/sygnal.min.js +1 -1
  101. package/dist/sygnal.min.js.map +1 -1
  102. package/dist/ui-combobox.cjs.js +169 -0
  103. package/dist/ui-combobox.cjs.js.map +1 -0
  104. package/dist/ui-combobox.esm.js +148 -0
  105. package/dist/ui-combobox.esm.js.map +1 -0
  106. package/dist/ui-menu.cjs.js +87 -0
  107. package/dist/ui-menu.cjs.js.map +1 -0
  108. package/dist/ui-menu.esm.js +66 -0
  109. package/dist/ui-menu.esm.js.map +1 -0
  110. package/dist/ui-select.cjs.js +104 -0
  111. package/dist/ui-select.cjs.js.map +1 -0
  112. package/dist/ui-select.esm.js +83 -0
  113. package/dist/ui-select.esm.js.map +1 -0
  114. package/dist/ui.cjs.js +738 -0
  115. package/dist/ui.cjs.js.map +1 -0
  116. package/dist/ui.esm.js +727 -0
  117. package/dist/ui.esm.js.map +1 -0
  118. package/dist/vike/ClientOnly.cjs.js +4 -14
  119. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  120. package/dist/vike/ClientOnly.mjs +4 -14
  121. package/dist/vike/ClientOnly.mjs.map +1 -1
  122. package/dist/vike/{+config.js → config/+config.js} +9 -1
  123. package/dist/vike/config/+config.js.map +1 -0
  124. package/dist/vike/config/package.json +6 -0
  125. package/dist/vike/onRenderClient.cjs.js +165 -95
  126. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  127. package/dist/vike/onRenderClient.mjs +165 -95
  128. package/dist/vike/onRenderClient.mjs.map +1 -1
  129. package/dist/vike/onRenderHtml.cjs.js +59 -24
  130. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  131. package/dist/vike/onRenderHtml.mjs +60 -25
  132. package/dist/vike/onRenderHtml.mjs.map +1 -1
  133. package/dist/vite/plugin.cjs.js +919 -89
  134. package/dist/vite/plugin.cjs.js.map +1 -1
  135. package/dist/vite/plugin.mjs +919 -90
  136. package/dist/vite/plugin.mjs.map +1 -1
  137. package/dist/zag.cjs.js +408 -0
  138. package/dist/zag.cjs.js.map +1 -0
  139. package/dist/zag.esm.js +405 -0
  140. package/dist/zag.esm.js.map +1 -0
  141. package/llms.txt +172 -103
  142. package/package.json +112 -14
  143. package/src/ai.d.ts +1186 -0
  144. package/src/ai.ts +23 -0
  145. package/src/astro/client.ts +27 -16
  146. package/src/astro/index.d.ts +24 -0
  147. package/src/astro/index.ts +69 -3
  148. package/src/astro/server.ts +8 -2
  149. package/src/collection.ts +6 -93
  150. package/src/core/actions.ts +134 -0
  151. package/src/core/cell.ts +202 -0
  152. package/src/core/debug.ts +23 -0
  153. package/src/core/define.ts +242 -0
  154. package/src/core/hooks.ts +314 -0
  155. package/src/core/hosts/collection.ts +325 -0
  156. package/src/core/hosts/switchable.ts +146 -0
  157. package/src/core/instance.ts +592 -0
  158. package/src/core/markers/clientonly.ts +13 -0
  159. package/src/core/markers/lazy.ts +40 -0
  160. package/src/core/markers/portal.ts +96 -0
  161. package/src/core/markers/suspense.ts +41 -0
  162. package/src/core/markers/transition.ts +70 -0
  163. package/src/core/registry.ts +25 -0
  164. package/src/core/runtime.ts +573 -0
  165. package/src/core/statics.ts +104 -0
  166. package/src/core/teardown.ts +60 -0
  167. package/src/core/view.ts +37 -0
  168. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  169. package/src/cycle/dom/ElementFinder.ts +7 -9
  170. package/src/cycle/dom/EventDelegator.ts +174 -223
  171. package/src/cycle/dom/IsolateModule.ts +65 -35
  172. package/src/cycle/dom/MainDOMSource.ts +9 -2
  173. package/src/cycle/dom/PriorityQueue.ts +4 -2
  174. package/src/cycle/dom/SymbolTree.ts +18 -47
  175. package/src/cycle/dom/classNameModule.ts +9 -7
  176. package/src/cycle/dom/controlledInputModule.ts +23 -6
  177. package/src/cycle/dom/enrichEventStream.ts +25 -47
  178. package/src/cycle/dom/fragment.ts +7 -0
  179. package/src/cycle/dom/isolate.ts +30 -27
  180. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  181. package/src/cycle/dom/mockDOMSource.ts +21 -1
  182. package/src/cycle/dom/modules.ts +16 -3
  183. package/src/cycle/dom/propsModule.ts +65 -0
  184. package/src/cycle/dom/selectModule.ts +22 -17
  185. package/src/cycle/dom/snabbdom.ts +1 -6
  186. package/src/cycle/dom/thunk.ts +3 -0
  187. package/src/cycle/dom/utils.ts +98 -0
  188. package/src/cycle/dom/viewTransition.ts +43 -0
  189. package/src/cycle/state/StateSource.ts +19 -3
  190. package/src/cycle/state/objIsEqual.ts +50 -0
  191. package/src/cycle/state/types.ts +0 -10
  192. package/src/defineComponent.ts +34 -0
  193. package/src/devtools.d.ts +170 -0
  194. package/src/devtools.ts +37 -0
  195. package/src/element.d.ts +59 -0
  196. package/src/element.ts +235 -0
  197. package/src/extra/ai/agent/index.ts +484 -0
  198. package/src/extra/ai/answers.ts +115 -0
  199. package/src/extra/ai/chat/behavior.ts +336 -0
  200. package/src/extra/ai/chat/driver.ts +271 -0
  201. package/src/extra/ai/chat/memoryTransport.ts +61 -0
  202. package/src/extra/ai/chat/output.ts +37 -0
  203. package/src/extra/ai/commandBar.ts +281 -0
  204. package/src/extra/ai/decide.ts +104 -0
  205. package/src/extra/ai/index.ts +42 -0
  206. package/src/extra/ai/link.ts +90 -0
  207. package/src/extra/ai/mcpApp.ts +314 -0
  208. package/src/extra/ai/messages.ts +51 -0
  209. package/src/extra/ai/schema/index.ts +166 -0
  210. package/src/extra/ai/schema/jsonSchema.ts +72 -0
  211. package/src/extra/ai/schema/strict.ts +152 -0
  212. package/src/extra/ai/transports/agui.ts +177 -0
  213. package/src/extra/ai/transports/anthropicMessages.ts +175 -0
  214. package/src/extra/ai/transports/chatCompletions.ts +105 -0
  215. package/src/extra/ai/transports/chromePrompt.ts +77 -0
  216. package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
  217. package/src/extra/ai/transports/fromAISDK.ts +126 -0
  218. package/src/extra/ai/transports/openResponses.ts +105 -0
  219. package/src/extra/ai/transports/shared.ts +125 -0
  220. package/src/extra/ai/transports/tools.ts +59 -0
  221. package/src/extra/ai/transports/uiMessageStream.ts +107 -0
  222. package/src/extra/ai/webmcp.ts +260 -0
  223. package/src/extra/backoff.ts +9 -0
  224. package/src/extra/behaviors.ts +169 -0
  225. package/src/extra/browserSignals.ts +23 -0
  226. package/src/extra/browserSources.ts +326 -0
  227. package/src/extra/command.ts +3 -3
  228. package/src/extra/controls.ts +47 -0
  229. package/src/extra/copyAsTest.ts +286 -0
  230. package/src/extra/devMcp.ts +386 -0
  231. package/src/extra/devtools.ts +101 -31
  232. package/src/extra/devtoolsActions.ts +379 -0
  233. package/src/extra/devtoolsHook.ts +9 -0
  234. package/src/extra/devtoolsNext.ts +83 -0
  235. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  236. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  237. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  238. package/src/extra/diagnostics/checks/chat.ts +71 -0
  239. package/src/extra/diagnostics/checks/collections.ts +1 -1
  240. package/src/extra/diagnostics/checks/controls.ts +148 -0
  241. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  242. package/src/extra/diagnostics/checks/dom.ts +20 -11
  243. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  244. package/src/extra/diagnostics/checks/events.ts +17 -2
  245. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  246. package/src/extra/diagnostics/checks/forms.ts +171 -0
  247. package/src/extra/diagnostics/checks/index.ts +103 -4
  248. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  249. package/src/extra/diagnostics/checks/next.ts +417 -0
  250. package/src/extra/diagnostics/checks/persist.ts +54 -0
  251. package/src/extra/diagnostics/checks/props.ts +10 -5
  252. package/src/extra/diagnostics/checks/public.d.ts +115 -7
  253. package/src/extra/diagnostics/checks/replies.ts +128 -0
  254. package/src/extra/diagnostics/checks/router.ts +100 -0
  255. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  256. package/src/extra/diagnostics/checks/shared.ts +86 -2
  257. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  258. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  259. package/src/extra/diagnostics/checks/state.ts +100 -7
  260. package/src/extra/diagnostics/checks/statics.ts +48 -0
  261. package/src/extra/diagnostics/checks/strict.ts +10 -67
  262. package/src/extra/diagnostics/checks/timers.ts +80 -0
  263. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  264. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  265. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  266. package/src/extra/diagnostics/checks/wiring.ts +69 -10
  267. package/src/extra/diagnostics/codes.ts +335 -23
  268. package/src/extra/diagnostics/index.ts +34 -9
  269. package/src/extra/diagnostics/legacy.ts +17 -0
  270. package/src/extra/driverFactories.ts +83 -19
  271. package/src/extra/elementCommands.ts +53 -0
  272. package/src/extra/fetchDriver.ts +519 -0
  273. package/src/extra/focusWithin.ts +27 -0
  274. package/src/extra/form.ts +284 -0
  275. package/src/extra/formHelpers.ts +139 -0
  276. package/src/extra/formTool.ts +150 -0
  277. package/src/extra/head.ts +106 -0
  278. package/src/extra/hmr.ts +2 -3
  279. package/src/extra/owned.ts +24 -0
  280. package/src/extra/pager.ts +50 -0
  281. package/src/extra/persist.ts +133 -0
  282. package/src/extra/pwa.ts +1 -1
  283. package/src/extra/queryCache.ts +107 -0
  284. package/src/extra/reducers.ts +1 -1
  285. package/src/extra/reduxDevtools.ts +92 -0
  286. package/src/extra/replies.ts +54 -0
  287. package/src/extra/router.ts +323 -0
  288. package/src/extra/run.ts +90 -217
  289. package/src/extra/selection.ts +76 -0
  290. package/src/extra/socketDriver.ts +265 -0
  291. package/src/extra/sortable.ts +377 -0
  292. package/src/extra/ssr.ts +427 -160
  293. package/src/extra/standardSchema.ts +27 -0
  294. package/src/extra/testing.ts +2550 -175
  295. package/src/extra/timers.ts +95 -0
  296. package/src/extra/undo.ts +261 -0
  297. package/src/extra/viewTransitions.ts +38 -0
  298. package/src/extra/virtual.ts +513 -0
  299. package/src/extra/widget.ts +201 -0
  300. package/src/index.d.ts +2754 -127
  301. package/src/index.ts +45 -4
  302. package/src/jsx-runtime.ts +15 -1
  303. package/src/jsx.ts +2 -1
  304. package/src/lazy.ts +50 -7
  305. package/src/portal.ts +3 -1
  306. package/src/pragma/index.ts +256 -133
  307. package/src/react-peers.d.ts +5 -0
  308. package/src/react.d.ts +43 -0
  309. package/src/react.ts +110 -0
  310. package/src/shared.ts +57 -0
  311. package/src/slot.ts +1 -1
  312. package/src/suspense.ts +3 -1
  313. package/src/switchable.ts +6 -121
  314. package/src/transition.ts +3 -1
  315. package/src/ui/accordion.ts +64 -0
  316. package/src/ui/dialog.ts +129 -0
  317. package/src/ui/disclosure.ts +35 -0
  318. package/src/ui/popover.ts +45 -0
  319. package/src/ui/shared.ts +94 -0
  320. package/src/ui/tabs.ts +82 -0
  321. package/src/ui/toaster.ts +198 -0
  322. package/src/ui/tooltip.ts +70 -0
  323. package/src/ui/zag/combobox.ts +115 -0
  324. package/src/ui/zag/menu.ts +40 -0
  325. package/src/ui/zag/select.ts +54 -0
  326. package/src/ui/zag/shared.ts +43 -0
  327. package/src/ui-combobox.d.ts +20 -0
  328. package/src/ui-combobox.ts +6 -0
  329. package/src/ui-menu.d.ts +30 -0
  330. package/src/ui-menu.ts +6 -0
  331. package/src/ui-select.d.ts +17 -0
  332. package/src/ui-select.ts +6 -0
  333. package/src/ui-zag-types.d.ts +42 -0
  334. package/src/ui.d.ts +208 -0
  335. package/src/ui.ts +15 -0
  336. package/src/vike/+config.ts +9 -1
  337. package/src/vike/ClientOnly.ts +1 -1
  338. package/src/vike/onRenderClient.ts +144 -96
  339. package/src/vike/onRenderHtml.ts +69 -26
  340. package/src/vike/types.ts +7 -1
  341. package/src/vite/globalthis-shim.ts +18 -0
  342. package/src/vite/globalthis.ts +56 -0
  343. package/src/vite/mcp.ts +568 -0
  344. package/src/vite/plugin.d.ts +47 -3
  345. package/src/vite/plugin.ts +262 -25
  346. package/src/zag.d.ts +64 -0
  347. package/src/zag.ts +238 -0
  348. package/dist/vike/+config.cjs.js +0 -66
  349. package/dist/vike/+config.cjs.js.map +0 -1
  350. package/dist/vike/+config.js.map +0 -1
  351. package/src/component.ts +0 -2301
  352. package/src/cycle/isolate/index.ts +0 -196
  353. package/src/cycle/run/index.ts +0 -151
  354. package/src/cycle/run/internals.ts +0 -143
  355. package/src/cycle/state/Collection.ts +0 -184
  356. package/src/cycle/state/index.ts +0 -5
  357. package/src/cycle/state/pickCombine.ts +0 -201
  358. package/src/cycle/state/pickMerge.ts +0 -122
  359. package/src/cycle/state/withState.ts +0 -47
  360. package/src/pragma/fn.ts +0 -55
@@ -1,5 +1,11 @@
1
1
  import {getDiagnostics} from './diagnostics/index';
2
2
  import type {Diagnostic} from './diagnostics/index';
3
+ import {recordActions, clearActions, getActions, onAction, getSession, preview} from './devtoolsActions';
4
+ import type {DevtoolsAction, SessionRecording} from './devtoolsActions';
5
+ import {sessionToTest} from './copyAsTest';
6
+ import type {CopyAsTestOptions, CopyAsTestResult} from './copyAsTest';
7
+
8
+ import {addLayer, devtoolsHooks} from './devtoolsNext';
3
9
 
4
10
  const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
5
11
  const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
@@ -9,13 +15,10 @@ declare global {
9
15
  interface Window {
10
16
  __SYGNAL_DEVTOOLS__?: SygnalDevTools;
11
17
  __SYGNAL_DEVTOOLS_APP__?: {
12
- sinks?: { STATE?: { shamefullySendNext: (fn: any) => void } };
18
+ __runtime?: { setState: (id: any, fn: (s: any) => any) => void };
13
19
  };
14
20
  __SYGNAL_DEVTOOLS_PAGE__?: boolean;
15
21
  SYGNAL_DEBUG?: string | false;
16
- __SYGNAL_HMR_UPDATING?: boolean;
17
- __SYGNAL_HMR_STATE?: unknown;
18
- __SYGNAL_HMR_PERSISTED_STATE?: unknown;
19
22
  __SYGNAL_HMR_LAST_CAPTURED_STATE?: unknown;
20
23
  Cyclejs?: { sinks?: any };
21
24
  CyclejsDevTool_startGraphSerializer?: (sinks: any) => void;
@@ -58,11 +61,16 @@ interface ExtensionMessage {
58
61
  source?: string;
59
62
  }
60
63
 
61
- class SygnalDevTools {
64
+ export class SygnalDevTools {
62
65
  _connected: boolean;
66
+ _initialized = false;
63
67
  _components: Map<number, ComponentMeta>;
64
68
  _stateHistory: StateHistoryEntry[];
65
69
  _maxHistory: number;
70
+ /** PLAN-4 3-E: actions changed since the last ACTIONS post (flushed on a timeout) */
71
+ _dirty = new Set<DevtoolsAction>();
72
+ _flush: any = null;
73
+ _copyOptions: CopyAsTestOptions = {};
66
74
 
67
75
  constructor() {
68
76
  this._connected = false;
@@ -80,11 +88,24 @@ class SygnalDevTools {
80
88
  return this._connected && typeof window !== 'undefined';
81
89
  }
82
90
 
91
+ /** Publish the bridge on window and listen for the extension. Idempotent (D77: called once, by 'sygnal/devtools'). */
83
92
  init(): void {
84
- if (typeof window === 'undefined') return;
93
+ if (typeof window === 'undefined' || this._initialized) return;
94
+ this._initialized = true;
85
95
 
86
96
  window.__SYGNAL_DEVTOOLS__ = this;
87
97
 
98
+ // PLAN-4 3-E (GS-10): the action log (recorded from now on, connected or not)
99
+ recordActions();
100
+ // PLAN-4.6 R4: the apps read this bridge's hooks (devtoolsNext.ts)
101
+ addLayer((api: any) => devtoolsHooks(this, api));
102
+ onAction((a, kind) => {
103
+ if (!this.connected) return;
104
+ if (kind == 'reset') { this._dirty.clear(); this._post('ACTIONS_RESET', {actions: []}); return; }
105
+ this._dirty.add(a!);
106
+ if (this._flush === null) this._flush = setTimeout(() => this._flushActions(), 0);
107
+ });
108
+
88
109
  window.addEventListener('message', (event: MessageEvent) => {
89
110
  if (event.source !== window) return;
90
111
  if (event.data?.source === EXTENSION_SOURCE) {
@@ -99,6 +120,13 @@ class SygnalDevTools {
99
120
  this._connected = true;
100
121
  if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
101
122
  this._sendFullTree();
123
+ this._post('ACTIONS_RESET', {actions: getActions().slice(-500).map(a => this._serializeAction(a))});
124
+ break;
125
+ case 'CLEAR_ACTIONS':
126
+ clearActions();
127
+ break;
128
+ case 'COPY_AS_TEST':
129
+ this._post('COPY_AS_TEST_RESULT', this._copyAsTest(msg.payload?.instance));
102
130
  break;
103
131
  case 'DISCONNECT':
104
132
  this._connected = false;
@@ -130,7 +158,7 @@ class SygnalDevTools {
130
158
  hasIntent: !!instance.intent,
131
159
  hasContext: !!instance.context,
132
160
  hasCalculated: !!instance.calculated,
133
- components: Object.keys(instance.components || {}),
161
+ components: [],
134
162
  parentId: null,
135
163
  children: [],
136
164
  debug: instance._debug,
@@ -283,6 +311,42 @@ class SygnalDevTools {
283
311
  });
284
312
  }
285
313
 
314
+ /** PLAN-4 3-E: post the actions added or changed since the last flush */
315
+ _flushActions(): void {
316
+ this._flush = null;
317
+ if (!this.connected || !this._dirty.size) { this._dirty.clear(); return; }
318
+ const list = [...this._dirty].sort((a, b) => a.seq - b.seq).map(a => this._serializeAction(a));
319
+ this._dirty.clear();
320
+ this._post('ACTIONS', {actions: list});
321
+ }
322
+
323
+ _serializeAction(a: DevtoolsAction): any {
324
+ return {
325
+ seq: a.seq, type: a.type, data: preview(a.data), component: a.component, instance: a.instance, parent: a.parent,
326
+ sinks: [...a.sinks], cause: a.cause, at: a.at,
327
+ ...('after' in a ? {before: this._safeClone(a.before), after: this._safeClone(a.after)} : {}),
328
+ };
329
+ }
330
+
331
+ /** "Copy as test" for the panel: the session of `instance` (default: the root) */
332
+ _copyAsTest(instance?: string | number): CopyAsTestResult | {error: string} {
333
+ try {
334
+ return sessionToTest(getSession(instance ?? undefined), this._copyOptions);
335
+ } catch (e: any) {
336
+ return {error: String(e?.message || e)};
337
+ }
338
+ }
339
+
340
+ /** Defaults for "Copy as test" from the panel (componentImport, drivers, imports, ...) */
341
+ configureCopyAsTest(options: CopyAsTestOptions): void {
342
+ this._copyOptions = {...this._copyOptions, ...options};
343
+ }
344
+
345
+ /** The recorded session of an instance (see sygnal/devtools getSession) */
346
+ getSession(target?: any): SessionRecording {
347
+ return getSession(target);
348
+ }
349
+
286
350
  _setDebug({componentId, enabled}: {componentId?: number; enabled: boolean}): void {
287
351
  if (typeof componentId === 'undefined' || componentId === null) {
288
352
  if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
@@ -294,7 +358,8 @@ class SygnalDevTools {
294
358
  if (meta && meta._instanceRef) {
295
359
  const instance = meta._instanceRef.deref();
296
360
  if (instance) {
297
- instance._debug = enabled;
361
+ // (through the app's runtime API)
362
+ instance.__api?.setDebug(componentId, enabled);
298
363
  meta.debug = enabled;
299
364
  this._post('DEBUG_TOGGLED', {componentId, enabled});
300
365
  }
@@ -311,40 +376,27 @@ class SygnalDevTools {
311
376
 
312
377
  const newState = this._safeClone(state);
313
378
 
314
- // Try per-component time-travel via the component's STATE sink (reducer stream)
379
+ // Per-component time travel through the app's runtime API
315
380
  const meta = this._components.get(componentId);
316
381
  if (meta) {
317
382
  const instance = meta._instanceRef?.deref();
318
383
  if (!instance) {
319
384
  console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
320
- } else {
321
- // sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
322
- const stateSinkName = instance.stateSourceName || 'STATE';
323
- const stateSink = instance.sinks?.[stateSinkName];
324
- if (stateSink?.shamefullySendNext) {
325
- stateSink.shamefullySendNext(() => ({...newState}));
326
- this._post('TIME_TRAVEL_APPLIED', {
327
- componentId,
328
- componentName,
329
- state: newState,
330
- });
331
- return;
332
- }
333
- console.warn(`[Sygnal DevTools] _timeTravel: component #${componentId} (${componentName}) has no STATE sink with shamefullySendNext. sinkName=${stateSinkName}, hasSinks=${!!instance.sinks}, sinkKeys=${instance.sinks ? Object.keys(instance.sinks).join(',') : 'none'}`);
385
+ } else if (instance.__api) {
386
+ // the runtime's setState (a queued action, one patch)
387
+ instance.__api.setState(componentId, () => ({...newState}));
388
+ this._post('TIME_TRAVEL_APPLIED', {componentId, componentName, state: newState});
389
+ return;
334
390
  }
335
391
  } else {
336
392
  console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
337
393
  }
338
394
 
339
- // Fall back to root STATE sink for root-level components
395
+ // Fall back to the root's state
340
396
  const app = window.__SYGNAL_DEVTOOLS_APP__;
341
- if (app?.sinks?.STATE?.shamefullySendNext) {
342
- app.sinks.STATE.shamefullySendNext(() => ({...newState}));
343
- this._post('TIME_TRAVEL_APPLIED', {
344
- componentId,
345
- componentName,
346
- state: newState,
347
- });
397
+ if (app?.__runtime) {
398
+ app.__runtime.setState('root', () => ({...newState}));
399
+ this._post('TIME_TRAVEL_APPLIED', {componentId, componentName, state: newState});
348
400
  } else {
349
401
  console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
350
402
  }
@@ -506,9 +558,27 @@ class SygnalDevTools {
506
558
 
507
559
  let instance: SygnalDevTools | null = null;
508
560
 
561
+ /**
562
+ * The bridge singleton of the 'sygnal/devtools' entry. Not part of the core
563
+ * (D77): the core only calls the hooks of window.__SYGNAL_DEVTOOLS__ when it exists.
564
+ */
509
565
  export function getDevTools(): SygnalDevTools {
510
566
  if (!instance) instance = new SygnalDevTools();
511
567
  return instance;
512
568
  }
513
569
 
570
+ /**
571
+ * Install the bridge (what run() did before 6.0): window.__SYGNAL_DEVTOOLS__ is
572
+ * set and the extension can connect. A bridge already on window (e.g. a second
573
+ * copy of this entry) is kept. Nothing happens outside a browser.
574
+ */
575
+ export function installDevTools(): SygnalDevTools | undefined {
576
+ if (typeof window === 'undefined') return undefined;
577
+ const existing = window.__SYGNAL_DEVTOOLS__;
578
+ if (existing) return existing;
579
+ const dt = getDevTools();
580
+ dt.init();
581
+ return dt;
582
+ }
583
+
514
584
  export default getDevTools;
@@ -0,0 +1,379 @@
1
+ /**
2
+ * PLAN-4 3-E (GS-10): the DevTools action log, in the dev-only 'sygnal/devtools' entry (0 B in
3
+ * apps: nothing in the core calls it).
4
+ *
5
+ * It records every action of every component instance through 2-C's recorder
6
+ * (./diagnostics/checks/actionLog: actionHooks), a hook layer every app reads from the core's
7
+ * diagnostics bridge (globalThis.__SYGNAL_DIAGNOSTICS__.layers), so it records with diagnostics
8
+ * off too. On top of 2-C's record it keeps:
9
+ * seq, at a sequence number, ms since the session started
10
+ * parent the parent instance id (null: a root)
11
+ * before, after the instance's state around the action's STATE reducer (onReducer), when it
12
+ * changed the state (`after` without calculated fields)
13
+ * replySink for a reply action (and RESOURCE): the source that delivered it, and
14
+ * replyKind 'fetch' when that source is makeFetchDriver's (renderComponent fakes it)
15
+ *
16
+ * A session runs from the recorder's start (or the last clearActions()) to now. getSession()
17
+ * turns it into a SessionRecording for one instance (./copyAsTest generates the test).
18
+ */
19
+ import {clockNow, actionHooks} from './diagnostics/checks/actionLog'
20
+ import {addLayer, devFacade} from './devtoolsNext'
21
+ import type {ActionListener, ActionRecord, ActionCause} from './diagnostics/checks/actionLog'
22
+
23
+ export type {ActionCause}
24
+
25
+ export interface DevtoolsAction {
26
+ seq: number
27
+ type: string
28
+ data: any
29
+ component: string
30
+ instance: string
31
+ parent: string | null
32
+ /** live: fills in as the action's reducers run */
33
+ sinks: string[]
34
+ cause: ActionCause
35
+ /** ms since the session started */
36
+ at: number
37
+ before?: any
38
+ after?: any
39
+ replySink?: string
40
+ replyKind?: 'fetch' | 'other'
41
+ /** an intent action whose data is the instance's own state (an intent over STATE.stream) */
42
+ echo?: true
43
+ }
44
+
45
+ export interface SessionAction {
46
+ type: string
47
+ data: any
48
+ cause: ActionCause
49
+ sinks: string[]
50
+ at: number
51
+ replySink?: string
52
+ replyKind?: 'fetch' | 'other'
53
+ echo?: true
54
+ }
55
+
56
+ /** One instance's session: what copyAsTest() needs (plain data, raw action data) */
57
+ export interface SessionRecording {
58
+ version: 1
59
+ component: string
60
+ instance: string
61
+ /** the state when the session started for this instance (undefined: unknown) */
62
+ initialState?: any
63
+ /** the component's own `initialState` (renderComponent's default) */
64
+ definitionInitialState?: any
65
+ finalState: any
66
+ /** the action names the component can be sent (model keys, behavior actions); undefined: any */
67
+ actionNames?: string[]
68
+ /** source names beyond DOM / EVENTS / STATE / LOG / CHILD / PARENT / READY */
69
+ drivers: string[]
70
+ /** those of `drivers` renderComponent fakes (makeFetchDriver sources: t.respond / t.fail) */
71
+ fakeable: string[]
72
+ /** the instance's own actions, in order */
73
+ actions: SessionAction[]
74
+ /** state changes in descendant instances a root-level replay can't reproduce */
75
+ foreign: Array<{type: string; component: string; instance: string; cause: ActionCause}>
76
+ /** the session lost its oldest actions (more than the recorder keeps) */
77
+ truncated?: boolean
78
+ }
79
+
80
+ export interface ActionFilter {
81
+ component?: string
82
+ instance?: string | number
83
+ type?: string | RegExp
84
+ cause?: ActionCause | ActionCause[]
85
+ }
86
+
87
+ interface Inst {
88
+ c: any
89
+ id: string
90
+ name: string
91
+ parent: string | null
92
+ stateName: string
93
+ /** created after the session started */
94
+ inSession: boolean
95
+ hasInitial: boolean
96
+ initial?: any
97
+ disposed: boolean
98
+ }
99
+
100
+ const MAX = 5000
101
+ const STANDARD = /^(DOM|EVENTS|LOG|CHILD|PARENT|READY|props\$|children\$|dispose\$)$/
102
+
103
+ let actions: DevtoolsAction[] = []
104
+ let seq = 0
105
+ let epoch = clockNow()
106
+ let truncated = false
107
+ let created = 0
108
+ const insts = new Map<string, Inst>()
109
+ const byRec = new WeakMap<ActionRecord, DevtoolsAction>()
110
+ /** instance -> the action whose STATE reducer just produced a value (its onReducer follows) */
111
+ const pendingState = new WeakMap<any, DevtoolsAction>()
112
+ const subscribers = new Set<(a: DevtoolsAction | null, kind: 'add' | 'update' | 'reset') => void>()
113
+
114
+ const notify = (a: DevtoolsAction | null, kind: 'add' | 'update' | 'reset') => {
115
+ for (const s of subscribers) try { s(a, kind) } catch (_) { /* a subscriber never breaks the app */ }
116
+ }
117
+
118
+ const idOf = (c: any) => String(c && c._componentNumber)
119
+
120
+ function ensure(c: any): Inst {
121
+ const id = idOf(c)
122
+ let i = insts.get(id)
123
+ if (!i || i.c !== c) {
124
+ const p = c && c.__next && c.__next.parentId
125
+ i = {c, id, name: c && c.name, parent: typeof p == 'number' ? String(p) : null, stateName: (c && c.stateSourceName) || 'STATE',
126
+ inSession: true, hasInitial: false, disposed: false}
127
+ ;(i as any).order = ++created
128
+ insts.set(id, i)
129
+ }
130
+ return i
131
+ }
132
+
133
+ function clean(c: any, s: any): any {
134
+ try { return typeof c.cleanupCalculated == 'function' ? c.cleanupCalculated(s) : s } catch (_) { return s }
135
+ }
136
+
137
+ const listener: ActionListener = {
138
+ action(r, c) {
139
+ const info = ensure(c)
140
+ // the instance's state when its session started: INITIALIZE's data (its initial state), else
141
+ // the state it had at its first action
142
+ if (r.type == 'INITIALIZE' && info.inSession) { info.initial = r.data; info.hasInitial = true }
143
+ else if (!info.hasInitial) { info.initial = c.currentState; info.hasInitial = true }
144
+ const a: DevtoolsAction = {seq: ++seq, type: r.type, data: r.data, component: r.component, instance: r.instance,
145
+ parent: info.parent, sinks: r.sinks, cause: r.cause, at: r.time - epoch}
146
+ const slot = (r as any).source
147
+ if (slot !== undefined && (r.cause == 'reply' || r.type == 'RESOURCE')) {
148
+ a.replySink = slot
149
+ a.replyKind = c.sources && c.sources[slot] && c.sources[slot].__sygnalStatic === 'resources' ? 'fetch' : 'other'
150
+ }
151
+ if (r.cause == 'intent' && isStateEcho(r.data, c.currentState)) a.echo = true
152
+ byRec.set(r, a)
153
+ actions.push(a)
154
+ if (actions.length > MAX) { actions.shift(); truncated = true }
155
+ notify(a, 'add')
156
+ },
157
+ sink(r, sink) {
158
+ const a = byRec.get(r)
159
+ if (!a) return
160
+ const info = insts.get(r.instance)
161
+ if (info && info.c && sink == info.stateName) pendingState.set(info.c, a)
162
+ notify(a, 'update')
163
+ },
164
+ }
165
+
166
+ function onReducer(c: any, name: string, prev: any, next: any): void {
167
+ const a = pendingState.get(c)
168
+ if (!a || a.type !== name || 'after' in a) return
169
+ pendingState.delete(c)
170
+ a.before = prev
171
+ a.after = clean(c, next)
172
+ notify(a, 'update')
173
+ }
174
+
175
+ function onDispose(c: any): void {
176
+ const i = insts.get(idOf(c))
177
+ if (i && i.c === c) { i.disposed = true; i.c = undefined }
178
+ }
179
+
180
+ let unregister: (() => void) | undefined
181
+
182
+ /** PLAN-4.6 R4: the same recorder on the core, from its hooks (per app; a facade per instance) */
183
+ function nextLayer(api: any): any {
184
+ const L: ActionListener = {
185
+ action: (r, iv) => listener.action(r, devFacade(iv, api)),
186
+ sink: (r, sink, reducer) => listener.sink!(r, sink, reducer),
187
+ }
188
+ const log = actionHooks(L)
189
+ return {
190
+ ...log,
191
+ onCreate(iv: any) { ensure(devFacade(iv, api)); log.onCreate(iv) },
192
+ onReducer(iv: any, name: string, prev: any, next: any) { onReducer(devFacade(iv, api), name, prev, next) },
193
+ onDispose(iv: any) { onDispose(devFacade(iv, api)) },
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Start recording actions (idempotent; 'sygnal/devtools' does it on import in a browser). Only
199
+ * components created afterwards are recorded. Returns a function that stops recording.
200
+ */
201
+ export function recordActions(): () => void {
202
+ if (!unregister) {
203
+ const core = (globalThis as any).__SYGNAL_DIAGNOSTICS__
204
+ if (!core) return () => {}
205
+ unregister = addLayer(nextLayer)
206
+ }
207
+ return stopRecording
208
+ }
209
+
210
+ function stopRecording(): void {
211
+ if (unregister) { unregister(); unregister = undefined }
212
+ }
213
+
214
+ export const isRecording = (): boolean => !!unregister
215
+
216
+ /** Start a new session: forget the recorded actions; each live instance's state is its new initial state. */
217
+ export function clearActions(): void {
218
+ actions = []
219
+ epoch = clockNow()
220
+ truncated = false
221
+ for (const [id, i] of insts) {
222
+ if (i.disposed) { insts.delete(id); continue }
223
+ i.inSession = false
224
+ i.initial = i.c && i.c.currentState
225
+ i.hasInitial = true
226
+ }
227
+ notify(null, 'reset')
228
+ }
229
+
230
+ const matches = (a: DevtoolsAction, f: ActionFilter) =>
231
+ (f.component === undefined || a.component === f.component) &&
232
+ (f.instance === undefined || a.instance === String(f.instance)) &&
233
+ (f.type === undefined || (typeof f.type == 'string' ? a.type === f.type : f.type.test(a.type))) &&
234
+ (f.cause === undefined || ([] as ActionCause[]).concat(f.cause).includes(a.cause))
235
+
236
+ /** The session's actions (oldest first), optionally filtered by component, instance, type or cause. */
237
+ export function getActions(filter?: ActionFilter): DevtoolsAction[] {
238
+ return filter ? actions.filter(a => matches(a, filter)) : actions.slice()
239
+ }
240
+
241
+ /** Subscribe to the log: (action, 'add' | 'update') and (null, 'reset'). Returns unsubscribe. */
242
+ export function onAction(fn: (a: DevtoolsAction | null, kind: 'add' | 'update' | 'reset') => void): () => void {
243
+ subscribers.add(fn)
244
+ return () => { subscribers.delete(fn) }
245
+ }
246
+
247
+ /** the live instances, newest first */
248
+ const live = () => [...insts.values()].filter(i => !i.disposed && i.c).sort((a, b) => (b as any).order - (a as any).order)
249
+
250
+ /**
251
+ * The instance a session target names: undefined (the newest live root), an instance id, a
252
+ * component function (its newest live instance) or run()'s result (its root, by DOM source).
253
+ */
254
+ export function resolveInstance(target?: any): Inst | undefined {
255
+ const all = live()
256
+ if (target === undefined || target === null) return all.find(i => i.parent === null)
257
+ if (typeof target == 'string' || typeof target == 'number') return insts.get(String(target))
258
+ if (typeof target == 'function') return all.find(i => i.c.view === target)
259
+ if (typeof target == 'object') {
260
+ const s = target.sources
261
+ if (s && typeof s == 'object') {
262
+ return all.find(i => i.parent === null && Object.keys(s).some(k => k != 'STATE' && s[k] && i.c.sources && i.c.sources[k] === s[k])) ||
263
+ all.find(i => i.parent === null)
264
+ }
265
+ }
266
+ return undefined
267
+ }
268
+
269
+ function descendants(id: string): Set<string> {
270
+ const out = new Set<string>()
271
+ let grew = true
272
+ while (grew) {
273
+ grew = false
274
+ for (const i of insts.values()) if (i.parent !== null && (i.parent === id || out.has(i.parent)) && !out.has(i.id)) { out.add(i.id); grew = true }
275
+ }
276
+ // instances already pruned: their actions still name their parent
277
+ for (const a of actions) if (a.parent !== null && (a.parent === id || out.has(a.parent))) out.add(a.instance)
278
+ return out
279
+ }
280
+
281
+ const REPLAYED = new Set<ActionCause>(['intent', 'simulateAction', 'behavior', 'reply', 'agent'])
282
+
283
+ /**
284
+ * The session of one instance (see resolveInstance for `target`) as plain data: its initial
285
+ * and final state, its own actions, and the state changes of its descendants (a replay at this
286
+ * instance can't reproduce them).
287
+ */
288
+ export function getSession(target?: any): SessionRecording {
289
+ const info = resolveInstance(target)
290
+ if (!info) throw new Error('[Sygnal DevTools] getSession: no live component instance matches' + (target === undefined ? ' (no root recorded: import sygnal/devtools before run())' : ''))
291
+ const c = info.c
292
+ const desc = descendants(info.id)
293
+ const stateNameOf = (id: string) => insts.get(id)?.stateName || 'STATE'
294
+ const model = c.model && typeof c.model == 'object' ? Object.keys(c.model).map(k => k.split('|')[0].trim()) : undefined
295
+ const behaviors = c._behaviorActions && typeof c._behaviorActions == 'object' ? Object.keys(c._behaviorActions) : []
296
+ const sources = c.sources || {}
297
+ const drivers = (c.sourceNames || Object.keys(sources)).filter((n: string) =>
298
+ !STANDARD.test(n) && n !== info.stateName && n != 'STATE' && n != 'state' && !n.startsWith('__'))
299
+ return {
300
+ version: 1,
301
+ component: info.name,
302
+ instance: info.id,
303
+ initialState: info.hasInitial ? info.initial : undefined,
304
+ definitionInitialState: c.view ? c.view.initialState : undefined,
305
+ finalState: c.currentState,
306
+ actionNames: model ? [...model, ...behaviors] : undefined,
307
+ drivers,
308
+ fakeable: drivers.filter((n: string) => sources[n] && sources[n].__sygnalStatic === 'resources'),
309
+ actions: actions.filter(a => a.instance === info.id).map(a => ({
310
+ type: a.type, data: a.data, cause: a.cause, sinks: [...a.sinks], at: a.at,
311
+ ...(a.replySink !== undefined ? {replySink: a.replySink, replyKind: a.replyKind} : {}),
312
+ ...(a.echo ? {echo: true as const} : {}),
313
+ })),
314
+ foreign: actions.filter(a => desc.has(a.instance) && REPLAYED.has(a.cause) && a.sinks.includes(stateNameOf(a.instance)))
315
+ .map(a => ({type: a.type, component: a.component, instance: a.instance, cause: a.cause})),
316
+ ...(truncated ? {truncated: true} : {}),
317
+ }
318
+ }
319
+
320
+ /** A JSON-safe copy for the panel: DOM elements and events become short descriptions. */
321
+ export function preview(v: any): any {
322
+ if (v === undefined) return undefined
323
+ const seen = new WeakSet<object>()
324
+ try {
325
+ return JSON.parse(JSON.stringify(v, (_k, x) => {
326
+ if (typeof x == 'function') return `[Function ${x.name || 'anonymous'}]`
327
+ if (typeof x == 'symbol') return String(x)
328
+ if (typeof x == 'bigint') return `${x}n`
329
+ if (x && typeof x == 'object') {
330
+ if (isElement(x)) return describeElement(x)
331
+ if (isEvent(x)) return `[${x.constructor && x.constructor.name || 'Event'} ${x.type}]`
332
+ if (x instanceof Map) return {'[Map]': [...x.entries()]}
333
+ if (x instanceof Set) return {'[Set]': [...x.values()]}
334
+ if (seen.has(x)) return '[Circular]'
335
+ seen.add(x)
336
+ }
337
+ return x
338
+ }))
339
+ } catch (_) {
340
+ return '[unserializable]'
341
+ }
342
+ }
343
+
344
+ const isPlain = (x: any) => !!x && typeof x == 'object' && (Object.getPrototypeOf(x) === Object.prototype || Object.getPrototypeOf(x) === null)
345
+
346
+ export function deepEqual(a: any, b: any): boolean {
347
+ if (Object.is(a, b)) return true
348
+ if (!a || !b || typeof a != 'object' || typeof b != 'object') return false
349
+ if (Array.isArray(a) !== Array.isArray(b)) return false
350
+ const ka = Object.keys(a).filter(k => a[k] !== undefined), kb = Object.keys(b).filter(k => b[k] !== undefined)
351
+ return ka.length == kb.length && ka.every(k => deepEqual(a[k], b[k]))
352
+ }
353
+
354
+ /**
355
+ * The data is the state itself (STATE.stream in the intent; calculated fields may be added): the
356
+ * replay sends it again by itself, so it isn't replayed.
357
+ */
358
+ function isStateEcho(data: any, state: any): boolean {
359
+ if (!isPlain(data) || !isPlain(state)) return false
360
+ const keys = Object.keys(state)
361
+ return keys.length > 0 && (data === state || keys.every(k => deepEqual(data[k], state[k])))
362
+ }
363
+
364
+ export const isElement = (x: any): boolean => !!x && typeof x == 'object' && x.nodeType === 1 && typeof x.tagName == 'string'
365
+ export const isEvent = (x: any): boolean => !!x && typeof x == 'object' && typeof x.type == 'string' && typeof x.preventDefault == 'function' && 'target' in x
366
+
367
+ const describeElement = (el: any) => {
368
+ const cls = typeof el.className == 'string' && el.className.trim() ? '.' + el.className.trim().split(/\s+/).join('.') : ''
369
+ return `<${String(el.tagName).toLowerCase()}${el.id ? '#' + el.id : ''}${cls}>`
370
+ }
371
+
372
+ /** test seam: forget everything (sessions, instances); recording stays as it is */
373
+ export function _resetActionLog(): void {
374
+ actions = []
375
+ seq = 0
376
+ epoch = clockNow()
377
+ truncated = false
378
+ insts.clear()
379
+ }
@@ -0,0 +1,9 @@
1
+ import type {SygnalDevTools} from './devtools';
2
+
3
+ /**
4
+ * The installed DevTools bridge, or undefined (D77: the bridge itself lives in the
5
+ * 'sygnal/devtools' dev entry, which sygnal/vite injects in dev).
6
+ */
7
+ export function getDevTools(): SygnalDevTools | undefined {
8
+ return (globalThis as any).__SYGNAL_DEVTOOLS__;
9
+ }