sygnal 6.5.0 → 7.0.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 (104) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +2 -0
  3. package/dist/astro/index.cjs.js +423 -27
  4. package/dist/astro/index.cjs.js.map +1 -1
  5. package/dist/astro/index.mjs +423 -27
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/server.cjs.js.map +1 -1
  8. package/dist/astro/server.mjs.map +1 -1
  9. package/dist/devtools.cjs.js +141 -3
  10. package/dist/devtools.cjs.js.map +1 -1
  11. package/dist/devtools.esm.js +141 -4
  12. package/dist/devtools.esm.js.map +1 -1
  13. package/dist/diagnostics.cjs.js +169 -14
  14. package/dist/diagnostics.cjs.js.map +1 -1
  15. package/dist/diagnostics.esm.js +168 -15
  16. package/dist/diagnostics.esm.js.map +1 -1
  17. package/dist/guide/accessibility.md +2 -0
  18. package/dist/guide/ai-chat.md +6 -6
  19. package/dist/guide/behaviors.md +1 -1
  20. package/dist/guide/element-commands.md +4 -2
  21. package/dist/guide/forms-reference.md +89 -3
  22. package/dist/guide/forms.md +8 -8
  23. package/dist/guide/http.md +77 -7
  24. package/dist/guide/migrating-to-7.md +314 -0
  25. package/dist/guide/recipes/ai-summarize.md +2 -1
  26. package/dist/guide/recipes/i18n.md +1 -1
  27. package/dist/guide/ui/tabs.md +5 -4
  28. package/dist/guide/ui/tooltip.md +24 -0
  29. package/dist/index.cjs.js +781 -320
  30. package/dist/index.cjs.js.map +1 -1
  31. package/dist/index.d.ts +319 -43
  32. package/dist/index.esm.js +781 -320
  33. package/dist/index.esm.js.map +1 -1
  34. package/dist/sygnal.min.js +1 -1
  35. package/dist/sygnal.min.js.map +1 -1
  36. package/dist/ui.cjs.js +96 -11
  37. package/dist/ui.cjs.js.map +1 -1
  38. package/dist/ui.esm.js +96 -12
  39. package/dist/ui.esm.js.map +1 -1
  40. package/dist/vike/config/package.json +1 -1
  41. package/dist/vike/onRenderClient.cjs.js +63 -43
  42. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  43. package/dist/vike/onRenderClient.mjs +63 -43
  44. package/dist/vike/onRenderClient.mjs.map +1 -1
  45. package/dist/vike/onRenderHtml.cjs.js +21 -25
  46. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  47. package/dist/vike/onRenderHtml.mjs +21 -25
  48. package/dist/vike/onRenderHtml.mjs.map +1 -1
  49. package/dist/vite/plugin.cjs.js +410 -27
  50. package/dist/vite/plugin.cjs.js.map +1 -1
  51. package/dist/vite/plugin.mjs +410 -27
  52. package/dist/vite/plugin.mjs.map +1 -1
  53. package/llms.txt +17 -16
  54. package/package.json +1 -1
  55. package/src/astro/index.d.ts +8 -0
  56. package/src/astro/index.ts +18 -0
  57. package/src/core/actions.ts +28 -21
  58. package/src/core/define.ts +2 -0
  59. package/src/core/hooks.ts +8 -2
  60. package/src/core/hosts/collection.ts +4 -3
  61. package/src/core/instance.ts +60 -16
  62. package/src/core/runtime.ts +5 -1
  63. package/src/core/view.ts +2 -0
  64. package/src/cycle/dom/utils.ts +4 -2
  65. package/src/devtools.d.ts +13 -0
  66. package/src/devtools.ts +3 -0
  67. package/src/extra/ai/chat/behavior.ts +3 -3
  68. package/src/extra/ai/commandBar.ts +3 -3
  69. package/src/extra/ai/compose/behavior.ts +3 -3
  70. package/src/extra/ai/transports/shared.ts +8 -2
  71. package/src/extra/behaviors.ts +21 -0
  72. package/src/extra/devGraph.ts +111 -0
  73. package/src/extra/devMcp.ts +1 -1
  74. package/src/extra/devtools.ts +18 -0
  75. package/src/extra/diagnostics/checks/fetch.ts +21 -1
  76. package/src/extra/diagnostics/checks/forms.ts +4 -3
  77. package/src/extra/diagnostics/checks/index.ts +2 -1
  78. package/src/extra/diagnostics/checks/inspect.ts +68 -6
  79. package/src/extra/diagnostics/checks/next.ts +3 -0
  80. package/src/extra/diagnostics/checks/public.d.ts +133 -12
  81. package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
  82. package/src/extra/diagnostics/codes.ts +19 -0
  83. package/src/extra/fetchDriver.ts +112 -7
  84. package/src/extra/fetchEventSource.ts +113 -0
  85. package/src/extra/form.ts +68 -26
  86. package/src/extra/formHelpers.ts +3 -1
  87. package/src/extra/lens.ts +3 -2
  88. package/src/extra/queryCache.ts +4 -3
  89. package/src/extra/socketDriver.ts +10 -4
  90. package/src/extra/testing.ts +68 -11
  91. package/src/index.d.ts +199 -30
  92. package/src/index.ts +1 -0
  93. package/src/shared.ts +3 -0
  94. package/src/ui/dialog.ts +3 -2
  95. package/src/ui/tooltips.ts +92 -0
  96. package/src/ui.d.ts +35 -0
  97. package/src/ui.ts +1 -0
  98. package/src/vike/onRenderClient.ts +44 -46
  99. package/src/vike/onRenderHtml.ts +5 -26
  100. package/src/vike/shell.ts +33 -0
  101. package/src/vite/graph.ts +255 -0
  102. package/src/vite/mcp.ts +3 -3
  103. package/src/vite/plugin.d.ts +19 -0
  104. package/src/vite/plugin.ts +144 -25
package/dist/index.cjs.js CHANGED
@@ -82,6 +82,8 @@ const NOT_SINK = /^(__|(props|children|dispose|commands)\$$|(CHILD|PARENT|EFFECT
82
82
  */
83
83
  const abortReason = {};
84
84
  const abort = (reason) => (abortReason.r = reason, ABORT);
85
+ /** Private semantic binding identity, shared by built-in helpers and the runtime. */
86
+ const BINDING = /*#__PURE__*/ Symbol.for('sygnal.binding');
85
87
 
86
88
  /**
87
89
  * PLAN-4.6 (D162): `defineComponent(opts)` builds a component from an options object, for code
@@ -721,6 +723,9 @@ const DEV_CODE_SEVERITY = {
721
723
  SYG128: 'error',
722
724
  // PLAN-4 4-G2: static only (sygnal-check); listed so the entry knows its title
723
725
  SYG129: 'warn',
726
+ // PLAN-10 X-1: static only (sygnal-check)
727
+ SYG170: 'warn',
728
+ SYG171: 'warn',
724
729
  SYG221: 'error',
725
730
  // PLAN-4 GS-4 (state.ts)
726
731
  SYG222: 'warn',
@@ -769,6 +774,8 @@ const DEV_CODE_SEVERITY = {
769
774
  SYG634: 'info',
770
775
  // PLAN-3 5-5 (fetch.ts)
771
776
  SYG635: 'warn',
777
+ // PLAN-11 U-1 (fetch.ts)
778
+ SYG636: 'warn',
772
779
  // PLAN-4 GS-2 (checks/elementCommands.ts, through the core bridge; renderComponent too)
773
780
  SYG640: 'warn',
774
781
  SYG641: 'error',
@@ -885,6 +892,12 @@ const DEV_CODE_SEVERITY = {
885
892
  // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
886
893
  SYG253: 'warn',
887
894
  SYG254: 'info',
895
+ // PLAN-12: static only; proven after-state output guard contradiction.
896
+ SYG256: 'warn',
897
+ // 7.0: the opt-in timing audit (checks/next.ts; configureTimingAudit)
898
+ SYG257: 'info',
899
+ // PLAN-12: static only for legacy (6.x) binding contracts. Sygnal 7 remounts known targets.
900
+ SYG454: 'warn',
888
901
  };
889
902
  function docsUrlFor(code) {
890
903
  return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`;
@@ -1280,6 +1293,8 @@ function normalize$1(view, src) {
1280
1293
  const list = [];
1281
1294
  for (const sink in e)
1282
1295
  list.push([sink, e[sink]]), sinks.add(sink);
1296
+ // 7.0: STATE first, so the other sinks get the state it committed
1297
+ list.sort((a, b) => +(a[0] != 'STATE') - +(b[0] != 'STATE'));
1283
1298
  handlers.set(key, list);
1284
1299
  }
1285
1300
  const c = normCalc(name, src.calculated, src.initialState);
@@ -1701,6 +1716,8 @@ class View {
1701
1716
  get state() { return this.i.cell.get(); }
1702
1717
  get props() { return this.i.props; }
1703
1718
  get context() { return this.i.context(); }
1719
+ /** D530: the context keys the last render's view read (null: none rendered, or no context) */
1720
+ get reads() { return this.i.keys; }
1704
1721
  get isRoot() { return !this.i.parent; }
1705
1722
  get kind() { return this.i.kind; }
1706
1723
  /** false on a hidden Switchable page and below it */
@@ -1869,7 +1886,7 @@ function handle(inst, type, data, cause, src) {
1869
1886
  return;
1870
1887
  const def = inst.def;
1871
1888
  const pre = inst.cell.get();
1872
- let props, outs;
1889
+ let props, afterProps, post = pre, outs;
1873
1890
  const next = (t, d, ms = 10, effect) => {
1874
1891
  if (typeof ms !== 'number')
1875
1892
  fail$2('SYG215', inst, `next() delay in '${type}' must be a number`, "Use next('ACTION', data, ms)");
@@ -1884,35 +1901,28 @@ function handle(inst, type, data, cause, src) {
1884
1901
  };
1885
1902
  for (const [sink, h0] of hs) {
1886
1903
  const h = H.wrapHandler ? H.wrapHandler(viewOf(inst), type, sink, h0) || h0 : h0;
1887
- if (sink == 'EFFECT') {
1888
- if (typeof h != 'function')
1889
- continue;
1890
- const failed = (e) => app.caught(inst, 'SYG214', `EFFECT handler '${type}' threw`, e, 'effect', type);
1891
- try {
1892
- const p = { ...(props || (props = inst_props(inst, pre))), signal: (inst.ac || (inst.ac = globalThis.AbortController && new AbortController()))?.signal };
1893
- const r = h(pre, data, (t, d, ms) => next(t, d, ms, true), p);
1894
- if (r?.then)
1895
- r.then(null, failed);
1896
- else if (r !== undefined && !isAbort(r))
1897
- warn('SYG219', inst, `EFFECT handler '${type}' returned a value, which is ignored`, 'Use a STATE or driver sink');
1898
- }
1899
- catch (e) {
1900
- failed(e);
1901
- }
1902
- continue;
1903
- }
1904
+ const effect = sink == 'EFFECT';
1904
1905
  let v;
1905
1906
  if (typeof h == 'function') {
1906
1907
  try {
1907
- v = h(pre, data, next, props || (props = inst_props(inst, pre)));
1908
+ const p = sink == 'STATE' ? props || (props = inst_props(inst, pre, pre)) : afterProps || (afterProps = inst_props(inst, post, pre));
1909
+ v = h(p.state, data, effect ? (t, d, ms) => next(t, d, ms, true) : next, effect ? { ...p, signal: (inst.ac || (inst.ac = globalThis.AbortController && new AbortController()))?.signal } : p);
1910
+ if (effect) {
1911
+ if (v?.then)
1912
+ v.then(null, (e) => handlerFailed(inst, type, sink, e));
1913
+ else if (v !== undefined && !isAbort(v))
1914
+ warn('SYG219', inst, `EFFECT handler '${type}' returned a value, which is ignored`, 'Use a STATE or driver sink');
1915
+ }
1908
1916
  }
1909
1917
  catch (e) {
1910
- app.caught(inst, 'SYG216', `Reducer for '${type}' threw; ${sink == 'STATE' ? 'state unchanged' : 'nothing sent'}`, e, 'reducer', type);
1918
+ handlerFailed(inst, type, sink, e);
1911
1919
  continue;
1912
1920
  }
1913
1921
  }
1914
1922
  else
1915
1923
  v = h === undefined || h === true ? data : h;
1924
+ if (effect)
1925
+ continue;
1916
1926
  if (sink == 'STATE' && (isAbort(v) || v === pre)) {
1917
1927
  // D205: the DOM event being dispatched (window.event) is the one this action came from
1918
1928
  if (/^(input|change)$/.test(globalThis.event?.type))
@@ -1925,6 +1935,11 @@ function handle(inst, type, data, cause, src) {
1925
1935
  H.onReducer?.(viewOf(inst), type, pre, v);
1926
1936
  dbg(inst, () => `<${type}> State reducer added`);
1927
1937
  inst.cell.set(v);
1938
+ // a STATE that removed the component's own state (a deleted record it is bound to): the
1939
+ // outputs get its last state, as a Collection item's do
1940
+ post = inst.cell.get();
1941
+ if (post === undefined)
1942
+ post = pre;
1928
1943
  // GS-12: this app's DOM driver patches inside a View Transition (makeViewTransitionDOMDriver
1929
1944
  // reads the flag on its IsolateModule); includes?.: a non-array static lists nothing (G-228).
1930
1945
  // Only for a state that changed structurally: today an equal one renders nothing (its
@@ -1973,9 +1988,14 @@ function handle(inst, type, data, cause, src) {
1973
1988
  send$1(inst, outs[i], outs[i + 1]);
1974
1989
  }
1975
1990
  }
1976
- /** the handlers' 4th argument: the props, children, slots, context, uid and the pre-action state */
1977
- function inst_props(inst, pre) {
1978
- return { ...inst.props, children: inst.children, slots: inst.slots, context: inst.context(), uid: inst.uid, state: pre };
1991
+ /** Keep the same failure boundary for sync handlers and rejected EFFECT promises. */
1992
+ function handlerFailed(inst, type, sink, e) {
1993
+ const effect = sink == 'EFFECT';
1994
+ inst.app.caught(inst, effect ? 'SYG214' : 'SYG216', effect ? `EFFECT handler '${type}' threw` : `Reducer for '${type}' threw; ${sink == 'STATE' ? 'state unchanged' : 'nothing sent'}`, e, effect ? 'effect' : 'reducer', type);
1995
+ }
1996
+ /** the handlers' 4th argument: the props, children, slots, context, uid, the state and the pre-action state */
1997
+ function inst_props(inst, state, previousState) {
1998
+ return { ...inst.props, children: inst.children, slots: inst.slots, context: inst.context(), uid: inst.uid, state, previousState };
1979
1999
  }
1980
2000
  /**
1981
2001
  * A sink value of an instance: stamped with its sender (EVENTS; object values for a reply-capable
@@ -2086,9 +2106,11 @@ function isEqualNamespace(a, b) {
2086
2106
  return true;
2087
2107
  }
2088
2108
  // G-261: a DOM change Sygnal makes outside a patch (a Transition's leave, a Portal mounted late)
2089
- // re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root)
2109
+ // re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root).
2110
+ // The event comes from the element's own window: jsdom refuses an `Event` from another realm (a
2111
+ // Transition's leave timer that fires after a jsdom test environment was torn down)
2090
2112
  const POKE = 'sygnal-dom';
2091
- const pokeDOM = (el) => el?.dispatchEvent(new Event(POKE, { bubbles: true }));
2113
+ const pokeDOM = (el) => el?.dispatchEvent(new (el.ownerDocument?.defaultView?.Event || Event)(POKE, { bubbles: true }));
2092
2114
  const obj = (o) => o !== null && typeof o == 'object' && !Array.isArray(o);
2093
2115
  /**
2094
2116
  * P46-P: two vnodes' data are the same for the DOM modules: the same keys, each value identical
@@ -2207,6 +2229,19 @@ const flat = (v) => {
2207
2229
  * - dispose(): DISPOSE, onDispose, dispose$, the EFFECT signal aborted, the subtree, then the
2208
2230
  * streams (the scoped _remove swap: no xstream stop timers, D165/Q18).
2209
2231
  */
2232
+ // v7: a known binding target joins key/id/path in the lifetime identity. Fresh equivalent
2233
+ // byId helpers retain identity; opaque lenses need an explicit key for a new target.
2234
+ function bindingChanged(a, b) {
2235
+ if (a === b)
2236
+ return false;
2237
+ const x = a?.[BINDING], y = b?.[BINDING];
2238
+ return x && y ? !x.every((v, i) => Object.is(v, y[i]))
2239
+ : !!(x || y || typeof a == 'string' || typeof b == 'string' || a === undefined || b === undefined);
2240
+ }
2241
+ function bindingState(k) {
2242
+ const c = k.base || k.cell || k.arr;
2243
+ return c && (c.has?.() ?? c.get() !== undefined) ? (k.cell || k.arr).get() : undefined;
2244
+ }
2210
2245
  const ERR_FIX$1 = 'See the attached error';
2211
2246
  // 4-I G-557: a fragment root (a Collection's): each of its top-level elements is marked.
2212
2247
  // 4-K G-572: a text root (no element; an attribute would land on a Text node) gets `ready: false`
@@ -2249,8 +2284,8 @@ const idOf$1 = (n, path) => {
2249
2284
  return `${n.sel}::${(s && s.replace(/"/g, '')) || path}`;
2250
2285
  };
2251
2286
  /**
2252
- * a component vnode's props, without `resetState` (D174: read at creation, like `state`, never a
2253
- * prop of the child)
2287
+ * a component vnode's props, without `resetState` (D174: read at creation, never a
2288
+ * prop of the child). Known `state` target changes start a new lifetime in v7.
2254
2289
  */
2255
2290
  function propsOf(p) {
2256
2291
  if (!p)
@@ -2314,10 +2349,14 @@ const EMPTY_CTX = Object.freeze({});
2314
2349
  /** a view read every key (spread, Object.keys): any context change re-renders it */
2315
2350
  const ALL = '\u0000all';
2316
2351
  let IDS = 0;
2317
- /** a child that failed to instantiate: renders the owner's error fallback (SYG408) */
2352
+ /**
2353
+ * a child that failed to instantiate: renders the owner's error fallback (SYG408). It keeps its
2354
+ * props, so only a changed binding target makes another attempt (not every owner render)
2355
+ */
2318
2356
  class Failed {
2319
- constructor(outv) {
2357
+ constructor(outv, props) {
2320
2358
  this.outv = outv;
2359
+ this.props = props;
2321
2360
  this.lr = true;
2322
2361
  this.ready = true;
2323
2362
  this.def = null;
@@ -2325,7 +2364,7 @@ class Failed {
2325
2364
  this.shown = true;
2326
2365
  }
2327
2366
  render() { return this.outv; }
2328
- setProps() { }
2367
+ setProps(p) { this.props = p; }
2329
2368
  dispose() { }
2330
2369
  }
2331
2370
  class Inst {
@@ -2333,6 +2372,7 @@ class Inst {
2333
2372
  this.app = app;
2334
2373
  this.def = def;
2335
2374
  this.parent = parent;
2375
+ this.base = base;
2336
2376
  this.dom = dom;
2337
2377
  this.scope = scope;
2338
2378
  this.kind = kind;
@@ -2499,10 +2539,32 @@ class Inst {
2499
2539
  }
2500
2540
  /** called after each action and before each render pass: STATE.stream emits a new state */
2501
2541
  notify() {
2502
- const v = this.cell.raw();
2503
- if (v !== this.stLast && v !== undefined) {
2504
- this.stLast = v;
2505
- this.st$.shamefullySendNext(v);
2542
+ // A completed action can remove and reinsert a target before the next render. Owners
2543
+ // observe each transition, including hidden pages; the current action finishes first.
2544
+ // only the bound children (a string / byId binding), not every child
2545
+ if (this.bound)
2546
+ for (const k of this.bound) {
2547
+ if (k.disposed) {
2548
+ this.bound.delete(k);
2549
+ continue;
2550
+ }
2551
+ const previous = k.bs, state = k.bs = bindingState(k);
2552
+ if (previous !== undefined && state === undefined) {
2553
+ // Cleanup reads its last owned snapshot. A dying path must not recreate the
2554
+ // deleted target; the fresh lifetime receives a new writable cell at reconciliation.
2555
+ const cell = k.cell || k.arr;
2556
+ cell.get = () => previous;
2557
+ cell.set = () => { };
2558
+ k.dispose();
2559
+ this.forced = true;
2560
+ }
2561
+ }
2562
+ if (this.st$) {
2563
+ const v = this.cell.raw();
2564
+ if (v !== this.stLast && v !== undefined) {
2565
+ this.stLast = v;
2566
+ this.st$.shamefullySendNext(v);
2567
+ }
2506
2568
  }
2507
2569
  }
2508
2570
  dispose$() {
@@ -2751,7 +2813,9 @@ class Inst {
2751
2813
  for (const r of resolvers)
2752
2814
  view = r(view, this) || view;
2753
2815
  let k = this.kids.get(id);
2754
- if (k && view && k.def && k.def.view !== view) {
2816
+ // Collection and VirtualCollection share an array cell; Switchable binds `state`.
2817
+ const binding = host && sel != 'switchable' ? 'from' : 'state';
2818
+ if (k && (k.disposed || (view && k.def && k.def.view !== view) || bindingChanged(k.props?.[binding], props[binding]))) {
2755
2819
  k.dispose();
2756
2820
  this.kids.delete(id);
2757
2821
  k = undefined;
@@ -2759,7 +2823,12 @@ class Inst {
2759
2823
  if (k)
2760
2824
  k.setProps(props, children, n, id);
2761
2825
  else
2762
- this.kids.set(id, host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState));
2826
+ this.kids.set(id, k = host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState));
2827
+ if (typeof props[binding] == 'string' || props[binding]?.[BINDING]) {
2828
+ k.bs = bindingState(k);
2829
+ (this.bound || (this.bound = new Set())).add(k);
2830
+ this.app.watchers.add(this);
2831
+ }
2763
2832
  return;
2764
2833
  }
2765
2834
  if (posts[sel] && !(ps || (ps = [])).includes(sel))
@@ -2784,11 +2853,11 @@ class Inst {
2784
2853
  return make(this, props, children, id, n);
2785
2854
  }
2786
2855
  catch (err) {
2787
- return this.failed(err);
2856
+ return this.failed(err, props);
2788
2857
  }
2789
2858
  }
2790
2859
  /** the owner's error fallback for a child that failed to instantiate (SYG408) */
2791
- failed(err) {
2860
+ failed(err, props) {
2792
2861
  const e = err instanceof Error ? err : new Error(String(err));
2793
2862
  let out = errorDiv$1(this.def.name);
2794
2863
  if (this.def.onError) {
@@ -2800,7 +2869,7 @@ class Inst {
2800
2869
  }
2801
2870
  }
2802
2871
  this.app.caught(this, 'SYG408', 'Sub-component threw; rendering the error fallback', e, 'instantiate');
2803
- return new Failed(out);
2872
+ return new Failed(out, props);
2804
2873
  }
2805
2874
  /** a tag child: state="key" | state={lens} | none (the parent's state) | isolatedState (D174: `resetState`) */
2806
2875
  child(view, props, children, id, reset = false) {
@@ -2819,7 +2888,7 @@ class Inst {
2819
2888
  return new Inst(app, def, this, cell, this.dom && this.dom.isolateSource(this.dom, scope), props, children, scope, this.uid(uidPart(id.replace(/.*::(r\.)?/, ''))), 'child', reset);
2820
2889
  }
2821
2890
  catch (err) {
2822
- return this.failed(err);
2891
+ return this.failed(err, props);
2823
2892
  }
2824
2893
  }
2825
2894
  inject(n, path) {
@@ -2882,8 +2951,7 @@ class Inst {
2882
2951
  tearDown(() => { for (const s of subs)
2883
2952
  s.unsubscribe(); }, app.dq, this);
2884
2953
  this.subs = [];
2885
- if (this.st$)
2886
- app.watchers.delete(this);
2954
+ app.watchers.delete(this);
2887
2955
  this.hub = null;
2888
2956
  }
2889
2957
  }
@@ -2982,7 +3050,7 @@ function arrayCell(owner, from) {
2982
3050
  if (typeof from == 'string')
2983
3051
  return keyCell(oc, from, owner.def.calcNames?.has(pathOf$1(from)[0]) && name, undefined, 'Collection');
2984
3052
  if (isObj$2(from) && typeof from.get == 'function') {
2985
- let lp = {}, lv;
3053
+ let lp = {}, lv, raw;
2986
3054
  return {
2987
3055
  get() {
2988
3056
  const p = oc.get();
@@ -2998,7 +3066,7 @@ function arrayCell(owner, from) {
2998
3066
  owner.app.appError(owner, e, 'view');
2999
3067
  return lv;
3000
3068
  }
3001
- lv = g;
3069
+ raw = lv = g;
3002
3070
  if (!Array.isArray(lv)) {
3003
3071
  warn('SYG401', name, "Collection 'from' getter returned a non-array; it renders nothing", 'Return an array from get()', lv);
3004
3072
  lv = NONE$1;
@@ -3006,6 +3074,7 @@ function arrayCell(owner, from) {
3006
3074
  }
3007
3075
  return lv;
3008
3076
  },
3077
+ has() { this.get(); return raw !== undefined; },
3009
3078
  set(v) {
3010
3079
  if (typeof from.set != 'function')
3011
3080
  return;
@@ -5367,8 +5436,11 @@ const validateWith = async (schema, value) => {
5367
5436
  * declarations in src/index.d.ts and docs/guide/drivers.md. Summary:
5368
5437
  *
5369
5438
  * - Request (sink value): `{ url, ok?, error?, key?, category?, method?, headers?, query?, json?,
5370
- * body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified with a JSON
5371
- * content-type; method defaults to POST with a body/json, else GET. `query` is appended as a
5439
+ * multipart?, body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified
5440
+ * with a JSON content-type; PLAN-11 U-5: `multipart` (an object) becomes a FormData body (a
5441
+ * File / Blob a file part, an array repeats the name, null / undefined skipped, an object JSON,
5442
+ * anything else a string), the content-type left to the browser (its boundary); method defaults
5443
+ * to POST with a body/json/multipart, else GET. `query` is appended as a
5372
5444
  * query string (null/undefined values skipped, arrays repeat the key, before any #fragment).
5373
5445
  * Other fetch options (credentials, mode, cache, ...) go under `init` (R4-5); other top-level
5374
5446
  * keys are the app's own: not sent, but returned on `request`.
@@ -5445,6 +5517,15 @@ const validateWith = async (schema, value) => {
5445
5517
  * the request itself) into requests sent again in parallel and combined, at most 3 deep. One
5446
5518
  * failure fails the request once (`error.status` / `error.body`). No cache entry sharing, no
5447
5519
  * per-part retry. The test fake (`_whole`) sends and answers it as one request. decide() uses it.
5520
+ * - PLAN-11 U-1 (D555): `progress: 'ACTION'` replies to the sender with each progress step,
5521
+ * `progress: true` (or a string with no sender) sends it down `progress(category?)`:
5522
+ * `{ phase: 'upload' | 'download', loaded, total, fraction, category, request }` (`total` /
5523
+ * `fraction` null when the length is unknown). Such a request goes over XMLHttpRequest (the
5524
+ * only transport with upload progress in every engine; S-1), turned back into a Response so
5525
+ * parsing, status, retry and timeouts are the same. Steps are throttled to one per 50 ms per
5526
+ * phase, keeping the first and the last. Not for resources or `batch`; never shares a fetch.
5527
+ * No XMLHttpRequest (Node, a service worker): plain fetch, no steps. The transport is
5528
+ * `options.XMLHttpRequest`, else the global one, read at request time.
5448
5529
  * - dispose / sink completion aborts everything in flight.
5449
5530
  * - `fetch` is read at request time (options.fetch, else globalThis.fetch), so test stubs work.
5450
5531
  * - PLAN-3 5-1 (H-9): renderComponent's HTTP fake runs this driver over an in-memory fetch;
@@ -5538,6 +5619,81 @@ const mergeHeaders = (...all) => {
5538
5619
  h.forEach((v, k) => { out[k] = v; });
5539
5620
  return out;
5540
5621
  };
5622
+ // PLAN-11 U-1: statuses whose Response must have a null body
5623
+ const NULL_BODY = [101, 103, 204, 205, 304];
5624
+ const abortError$1 = () => typeof DOMException == 'function'
5625
+ ? new DOMException('The operation was aborted.', 'AbortError')
5626
+ : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
5627
+ /**
5628
+ * PLAN-11 U-1 (S-1): a fetch-shaped call over XMLHttpRequest that reports progress:
5629
+ * `on(phase, loaded, total)` (total 0 when unknown), at most every 50 ms per phase, always the
5630
+ * first and the final step. Resolves with a real Response (blob body, parsed headers); network
5631
+ * errors and timeouts reject with a TypeError, an abort with an AbortError, like fetch.
5632
+ */
5633
+ const xhrFetch = (X, url, init, on) => new Promise((res, rej) => {
5634
+ const x = new X();
5635
+ const last = {};
5636
+ const step = (phase, loaded, total, final) => {
5637
+ const l = last[phase], now = Date.now();
5638
+ if (l && (l.loaded === loaded || (!final && now - l.at < 50 && !(total && loaded >= total))))
5639
+ return;
5640
+ last[phase] = { loaded, at: now };
5641
+ on(phase, loaded, total);
5642
+ };
5643
+ const ev = (phase) => (e) => step(phase, e.loaded, e.lengthComputable ? e.total : 0);
5644
+ x.open(init.method || 'GET', url);
5645
+ x.responseType = 'blob';
5646
+ const h = init.headers || {};
5647
+ Object.keys(h).forEach(k => x.setRequestHeader(k, h[k]));
5648
+ if (init.credentials == 'include')
5649
+ x.withCredentials = true;
5650
+ const sig = init.signal;
5651
+ if (sig) {
5652
+ if (sig.aborted)
5653
+ return rej(abortError$1());
5654
+ sig.addEventListener?.('abort', () => x.abort(), { once: true });
5655
+ }
5656
+ if (init.body != null && x.upload) {
5657
+ x.upload.onprogress = ev('upload');
5658
+ x.upload.onload = (e) => step('upload', e.loaded, e.lengthComputable ? e.total : e.loaded, 1);
5659
+ }
5660
+ x.onprogress = ev('download');
5661
+ x.onload = () => {
5662
+ const size = x.response?.size ?? last.download?.loaded ?? 0;
5663
+ step('download', size, size, 1);
5664
+ const headers = typeof Headers == 'function' ? new Headers() : undefined;
5665
+ String(x.getAllResponseHeaders?.() || '').trim().split(/[\r\n]+/).forEach((line) => {
5666
+ const i = line.indexOf(':');
5667
+ if (i > 0)
5668
+ try {
5669
+ headers?.append(line.slice(0, i).trim(), line.slice(i + 1).trim());
5670
+ }
5671
+ catch (_) { }
5672
+ });
5673
+ try {
5674
+ res(new Response(NULL_BODY.includes(x.status) ? null : x.response, { status: x.status, statusText: x.statusText, headers }));
5675
+ }
5676
+ catch (e) {
5677
+ rej(e);
5678
+ }
5679
+ };
5680
+ x.onerror = x.ontimeout = () => rej(new TypeError(`Network request failed: ${url}`));
5681
+ x.onabort = () => rej(abortError$1());
5682
+ x.send(init.body ?? null);
5683
+ });
5684
+ /** PLAN-11 U-5: a request's `multipart` object as FormData (see the header) */
5685
+ const toFormData = (parts) => {
5686
+ const fd = new FormData();
5687
+ Object.keys(parts || {}).forEach(k => [].concat(parts[k]).forEach((v) => {
5688
+ if (v == null)
5689
+ return;
5690
+ if (typeof Blob != 'undefined' && v instanceof Blob)
5691
+ fd.append(k, v, v.name ?? 'blob');
5692
+ else
5693
+ fd.append(k, typeof v == 'object' ? JSON.stringify(v) : String(v));
5694
+ }));
5695
+ return fd;
5696
+ };
5541
5697
  const IDEMPOTENT$1 = /^(GET|HEAD)$/i;
5542
5698
  // D80: an invalidate value matches a request by tag, URL prefix ('/…') or predicate
5543
5699
  const hit = (x, q) => {
@@ -5562,6 +5718,22 @@ function makeFetchDriver(options = {}) {
5562
5718
  // q (the request), timer, f (the fetch it waits on), ok / ko (its outcomes) }
5563
5719
  const inflight = new Map();
5564
5720
  const subs = new Set();
5721
+ // PLAN-11 U-1: progress(category?) listeners (not held while nobody listens)
5722
+ const psubs = new Set();
5723
+ const emitProgress = (v) => psubs.forEach(s => {
5724
+ let m = false;
5725
+ try {
5726
+ m = matches(s, v);
5727
+ }
5728
+ catch (_) { }
5729
+ if (m)
5730
+ try {
5731
+ s.l.next(v);
5732
+ }
5733
+ catch (e) {
5734
+ console.error('[Sygnal] makeFetchDriver: a listener threw', e);
5735
+ }
5736
+ });
5565
5737
  // R4-4: replies held until the first select() listener, failures until the first errors()
5566
5738
  // listener (or the next macrotask, when they're logged if nothing listens)
5567
5739
  const started = [false, false];
@@ -5624,6 +5796,10 @@ function makeFetchDriver(options = {}) {
5624
5796
  return {
5625
5797
  select: make(false),
5626
5798
  errors: make(true),
5799
+ progress: (sel) => {
5800
+ let sub;
5801
+ return xs$2.create({ start: l => { psubs.add((sub = { l, sel, ns })); }, stop: () => { psubs.delete(sub); } });
5802
+ },
5627
5803
  isolateSource: (_, scope) => source(ns.concat(scope)),
5628
5804
  isolateSink: (sink$, scope) => sink$.map((req) => tagRequest(req, scope)),
5629
5805
  // PLAN-4.6: the same per value (the core scopes a request without a stream per instance)
@@ -5743,7 +5919,7 @@ function makeFetchDriver(options = {}) {
5743
5919
  req = { url: req };
5744
5920
  if (!req || typeof req != 'object' || disposed || !allowed$1(req, 'makeFetchDriver'))
5745
5921
  return;
5746
- const { url, category, method, headers, query, json, body, latest, timeoutMs, parse, abort, init, ok, error, key, resources, refresh, invalidates, updates, retry, validate, prefetch } = req;
5922
+ const { url, category, method, headers, query, json, body, latest, timeoutMs, parse, abort, init, ok, error, key, resources, refresh, invalidates, updates, retry, validate, prefetch, progress } = req;
5747
5923
  if ('invalidate' in req)
5748
5924
  return inval(req.invalidate);
5749
5925
  if (prefetch)
@@ -5795,7 +5971,9 @@ function makeFetchDriver(options = {}) {
5795
5971
  }
5796
5972
  const scope = scopeKey(req);
5797
5973
  // reply actions: keyed per (sender, key ?? ok ?? error); a resource per (sender, '\0' + name)
5798
- const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error) ? key ?? ok ?? error : undefined;
5974
+ // PLAN-11 U-1: a progress action is a reply action too (abort on dispose, its own lane)
5975
+ const pr = !pf && rn === undefined && !req.batch && progress;
5976
+ const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error || typeof pr == 'string') ? key ?? ok ?? error ?? pr : undefined;
5799
5977
  if (sender !== undefined && (typeof abort == 'string' || (abort && key !== undefined)))
5800
5978
  return cancel(r => r.sender === sender && r.rk === (key ?? abort));
5801
5979
  // per (scope, category): another component's requests are never cancelled
@@ -5805,7 +5983,7 @@ function makeFetchDriver(options = {}) {
5805
5983
  cancel(rk === undefined ? r => r.rk === undefined && r.scope === scope && r.category === category : r => r.sender === sender && r.rk === rk);
5806
5984
  const base = options.init;
5807
5985
  // a request's URL and fetch() init (the batch's sub-requests inherit the request's fields)
5808
- const prep = ({ url, method, headers, query, json, body, init: own }) => {
5986
+ const prep = ({ url, method, headers, query, json, multipart, body, init: own }) => {
5809
5987
  const h = mergeHeaders(options.headers, base?.headers, own?.headers, headers);
5810
5988
  let b = body ?? own?.body;
5811
5989
  if (json !== undefined) {
@@ -5813,6 +5991,12 @@ function makeFetchDriver(options = {}) {
5813
5991
  if (!Object.keys(h).some(k => k.toLowerCase() == 'content-type'))
5814
5992
  h['content-type'] = 'application/json';
5815
5993
  }
5994
+ else if (multipart !== undefined) {
5995
+ b = toFormData(multipart);
5996
+ // the browser writes multipart/form-data with its boundary; a content-type set here would break it
5997
+ Object.keys(h).forEach(k => { if (k.toLowerCase() == 'content-type')
5998
+ delete h[k]; });
5999
+ }
5816
6000
  return [withQuery((options.baseUrl || '') + (url ?? ''), query), { ...pickInit(base), ...pickInit(own), method: method || own?.method || base?.method || (b !== undefined ? 'POST' : 'GET'), headers: h, body: b }];
5817
6001
  };
5818
6002
  const [href, fetchInit] = prep(req);
@@ -5821,7 +6005,8 @@ function makeFetchDriver(options = {}) {
5821
6005
  // parts, at most 3 deep), their parsed bodies merged by `combine` into one reply. The test
5822
6006
  // fake (_whole) answers such a request as one
5823
6007
  const { batch, split, combine = (x) => x } = options._whole ? {} : req;
5824
- const k = C?.key(req, rn !== undefined || pf);
6008
+ // PLAN-11 U-1: a progress request never shares a fetch (each sender gets its own steps)
6009
+ const k = pr ? undefined : C?.key(req, rn !== undefined || pf);
5825
6010
  const id = ++seq;
5826
6011
  const r = { category, scope, sender, rk };
5827
6012
  inflight.set(id, r);
@@ -5869,7 +6054,19 @@ function makeFetchDriver(options = {}) {
5869
6054
  const parser = parserOf(parse || options.parse);
5870
6055
  // the global fetch is called as a method: a detached window.fetch throws "Illegal invocation"
5871
6056
  const g = globalThis;
5872
- const doFetch = options.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6057
+ let doFetch = options.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6058
+ // PLAN-11 U-1: a progress request over XMLHttpRequest (renderComponent's fake: `_progress`)
6059
+ const X = options.XMLHttpRequest ?? g.XMLHttpRequest;
6060
+ if (pr && (options._progress || typeof X == 'function')) {
6061
+ const step = (phase, loaded, total) => {
6062
+ if (!inflight.has(id) || disposed)
6063
+ return;
6064
+ const t = total > 0 ? total : null;
6065
+ const v = { phase, loaded, total: t, fraction: t ? Math.min(1, loaded / t) : null, category, request: req };
6066
+ typeof pr == 'string' && sender !== undefined ? reply(sender, pr, v) : emitProgress(v);
6067
+ };
6068
+ doFetch = options._progress ? (u, i) => options._progress(u, i, step, req) : (u, i) => xhrFetch(X, u, i, step);
6069
+ }
5873
6070
  // the outcome, to every request waiting on this fetch (the failure once, after the last attempt)
5874
6071
  const end = (e, x, status) => {
5875
6072
  done(F);
@@ -6031,9 +6228,10 @@ function queryCache(o = {}) {
6031
6228
  const key = (q, res) => {
6032
6229
  if (typeof q == 'string')
6033
6230
  q = { url: q };
6034
- const { method, json, body, parse, query } = q;
6035
- const m = (method || (json !== undefined || body !== undefined ? 'POST' : 'GET')).toUpperCase();
6036
- const b = json !== undefined ? JSON.stringify(json) : body ?? '';
6231
+ const { method, json, body, parse, query, multipart } = q;
6232
+ const m = (method || (json !== undefined || body !== undefined || multipart !== undefined ? 'POST' : 'GET')).toUpperCase();
6233
+ // PLAN-11 U-5: a multipart request is never cached (its body isn't a string)
6234
+ const b = json !== undefined ? JSON.stringify(json) : multipart !== undefined ? multipart : body ?? '';
6037
6235
  if (q.cache === false || typeof b != 'string' || typeof parse == 'function' || parse == 'response' ||
6038
6236
  !(q.cache || q.staleTime != null || (res && IDEMPOTENT.test(m))))
6039
6237
  return;
@@ -6102,6 +6300,272 @@ function queryCache(o = {}) {
6102
6300
  return C;
6103
6301
  }
6104
6302
 
6303
+ /*
6304
+ * PLAN-6 L-2: what the HTTP transports share (side-effect free; each transport imports only what
6305
+ * it uses).
6306
+ * - post(): the fetch (the `fetch` option, 0-S5), the headers option (an object or a function of
6307
+ * the request), HTTP errors as Errors with `status` and the provider's message, and SYG670.
6308
+ * - sse(): a fetch Response body as SSE frames ({ event, data }), per the HTML spec's parsing
6309
+ * rules: CRLF / LF, comments, multi-line data. The reader is cancelled when the consumer stops
6310
+ * (abort, a thrown error), so the connection closes.
6311
+ * - SYG670: in a browser, a request with an auth header (Authorization, x-api-key, api-key,
6312
+ * x-goog-api-key) to a host that is neither local (localhost, 127.0.0.1, [::1], *.localhost)
6313
+ * nor the page's own origin is refused unless the transport has `dangerouslyAllowBrowser: true`:
6314
+ * the key would ship to every visitor. Same-origin requests are the app's own server (its
6315
+ * proxy), so a session header there is fine.
6316
+ */
6317
+ const g$6 = globalThis;
6318
+ const AUTH = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
6319
+ const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/;
6320
+ /** the URL a transport posts to, or a SYG670 Error when it must not (see the header) */
6321
+ function guard(url, headers, opts, name) {
6322
+ if (opts.dangerouslyAllowBrowser || typeof g$6.document == 'undefined' || !Object.keys(headers).some(h => AUTH.test(h)))
6323
+ return;
6324
+ const here = g$6.location?.href;
6325
+ let u;
6326
+ try {
6327
+ u = new URL(url, here);
6328
+ }
6329
+ catch (_) {
6330
+ return;
6331
+ }
6332
+ if (LOCAL.test(u.hostname) || (here && u.origin == new URL(here).origin))
6333
+ return;
6334
+ const fix = `Call ${u.origin} from your server and point the transport at that route (an AI SDK route with uiMessageStream('/api/chat'), or your own proxy), so the key stays there. For a key the user typed in themselves, pass ${name}({ ..., dangerouslyAllowBrowser: true })`;
6335
+ const msg = `${name}: refused to send an auth header from the browser to ${u.origin}: anyone who opens the page can read it`;
6336
+ error('SYG670', undefined, msg, fix);
6337
+ fail$2('SYG670', undefined, msg, fix);
6338
+ }
6339
+ /**
6340
+ * POST JSON; a non-2xx response is an Error with `status`, `body` and the provider's message.
6341
+ * The transport's `body` option and the request's `body` are merged over the built body (so they
6342
+ * can override provider defaults such as `max_tokens`); `fixed` fields are merged last and can't be
6343
+ * overridden (G-654: uiMessageStream's protocol fields, as the AI SDK's DefaultChatTransport)
6344
+ */
6345
+ async function post$1(url, body, req, signal, opts, name, fixed) {
6346
+ const h = typeof opts.headers == 'function' ? await opts.headers(req) : opts.headers || {};
6347
+ guard(url, h, opts, name);
6348
+ const f = opts.fetch || ((u, i) => g$6.fetch(u, i));
6349
+ const res = await f(url, { method: 'POST', headers: { 'content-type': 'application/json', ...h }, body: JSON.stringify({ ...body, ...opts.body, ...req.body, ...fixed }), signal });
6350
+ if (!res.ok) {
6351
+ let text = '', message = '';
6352
+ try {
6353
+ text = await res.text();
6354
+ const j = JSON.parse(text);
6355
+ message = j.error?.message ?? j.error ?? j.message ?? '';
6356
+ }
6357
+ catch (_) {
6358
+ message || (message = text.slice(0, 200));
6359
+ }
6360
+ throw Object.assign(new Error(`${name}: HTTP ${res.status}${message ? ': ' + message : ''}`), { status: res.status, body: text });
6361
+ }
6362
+ if (!res.body)
6363
+ throw new Error(`${name}: the response has no body`);
6364
+ return res;
6365
+ }
6366
+ /**
6367
+ * the SSE frames of a response body: `{ event, data }` (data: the joined data lines). PLAN-11
6368
+ * E-1: `meta` (optional) follows the stream's `id` (the last event id, kept across frames) and
6369
+ * `retry` (ms) fields, as EventSource does, for a reconnect
6370
+ */
6371
+ async function* sse(res, meta) {
6372
+ const reader = res.body.getReader(), dec = new TextDecoder();
6373
+ let buf = '', cr = '', done = false;
6374
+ try {
6375
+ for (;;) {
6376
+ const r = await reader.read();
6377
+ // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
6378
+ let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
6379
+ cr = !r.done && s.endsWith('\r') ? '\r' : '';
6380
+ if (cr)
6381
+ s = s.slice(0, -1);
6382
+ buf += s.replace(/\r\n?/g, '\n');
6383
+ done = r.done;
6384
+ let i;
6385
+ while ((i = buf.indexOf('\n\n')) >= 0) {
6386
+ const frame = buf.slice(0, i);
6387
+ buf = buf.slice(i + 2);
6388
+ let event = 'message', data = [];
6389
+ for (const line of frame.split('\n')) {
6390
+ if (!line || line[0] == ':')
6391
+ continue;
6392
+ const c = line.indexOf(':'), field = c < 0 ? line : line.slice(0, c);
6393
+ const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
6394
+ if (field == 'event')
6395
+ event = v;
6396
+ else if (field == 'data')
6397
+ data.push(v);
6398
+ else if (meta && field == 'id' && !v.includes('\0'))
6399
+ meta.id = v;
6400
+ else if (meta && field == 'retry' && /^\d+$/.test(v))
6401
+ meta.retry = +v;
6402
+ }
6403
+ if (data.length)
6404
+ yield { event, data: data.join('\n') };
6405
+ }
6406
+ if (done)
6407
+ return;
6408
+ }
6409
+ }
6410
+ finally {
6411
+ if (!done)
6412
+ reader.cancel().catch(() => { });
6413
+ }
6414
+ }
6415
+ /** JSON.parse, or the text itself when it isn't JSON (a model's malformed tool arguments go to validation as is) */
6416
+ const json$1 = (s) => {
6417
+ if (typeof s != 'string')
6418
+ return s;
6419
+ try {
6420
+ return JSON.parse(s || '{}');
6421
+ }
6422
+ catch (_) {
6423
+ return s;
6424
+ }
6425
+ };
6426
+ /** a provider's usage as `{ inputTokens, outputTokens, totalTokens, reasoningTokens? }` (AI SDK names) */
6427
+ const usageOf = (u) => u && {
6428
+ inputTokens: u.input_tokens ?? u.prompt_tokens,
6429
+ outputTokens: u.output_tokens ?? u.completion_tokens,
6430
+ totalTokens: u.total_tokens,
6431
+ ...((u.output_tokens_details ?? u.completion_tokens_details)?.reasoning_tokens != null && { reasoningTokens: (u.output_tokens_details ?? u.completion_tokens_details).reasoning_tokens }),
6432
+ };
6433
+ /** the parts of a message (a `content` string is one text part) */
6434
+ const partsOf = (m) => m.parts ?? (typeof m.content == 'string' ? [{ type: 'text', text: m.content }] : []);
6435
+ /** the tool name of a `tool-<name>` / `dynamic-tool` part, or '' */
6436
+ const toolName = (p) => p.type == 'dynamic-tool' ? p.toolName : p.type.startsWith('tool-') ? p.type.slice(5) : '';
6437
+ /** what a tool part sends back to the model, or undefined when it has no result yet */
6438
+ const toolOutput = (p) => p.state == 'output-available' ? (typeof p.output == 'string' ? p.output : JSON.stringify(p.output ?? null))
6439
+ : p.state == 'output-error' ? JSON.stringify({ error: p.errorText ?? 'error' })
6440
+ : p.state == 'output-denied' ? JSON.stringify({ error: 'The user denied this tool call' })
6441
+ : undefined;
6442
+ // dev-only diagnostics (SYG675): the dev entry formats them (checks/chat.ts)
6443
+ const dev$8 = (...a) => g$6.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
6444
+
6445
+ /*
6446
+ * PLAN-11 E-1: an EventSource over fetch, for server-sent events that need request headers
6447
+ * (`Authorization`), which the native EventSource can't send. makeSocketDriver uses it for an
6448
+ * `sse` connection with `headers`. It behaves as EventSource does where the driver relies on it:
6449
+ *
6450
+ * - connects on construction (a microtask later, so handlers can be set first); `readyState`
6451
+ * CONNECTING (0) → OPEN (1) on a 2xx `text/event-stream` response;
6452
+ * - `onmessage` and `addEventListener(type)` get `{ type, data, lastEventId }`;
6453
+ * - the stream ending or a network error: `error`, readyState CONNECTING, a reconnect after
6454
+ * `retry` ms (the stream's `retry:` field, default 3000) with `Last-Event-ID`;
6455
+ * - a non-2xx status or another content type: `error` with readyState CLOSED (no retry; the
6456
+ * driver's reconnect policy decides);
6457
+ * - `close()` aborts the request and stops retrying.
6458
+ */
6459
+ const NAMES = ['CONNECTING', 'OPEN', 'CLOSED'];
6460
+ class FetchEventSource {
6461
+ constructor(url, init = {}) {
6462
+ this.readyState = 0;
6463
+ this.onopen = null;
6464
+ this.onmessage = null;
6465
+ this.onerror = null;
6466
+ this.ls = new Map();
6467
+ this.meta = { id: '', retry: 3000 };
6468
+ this.url = url;
6469
+ this.withCredentials = !!init.withCredentials;
6470
+ this.init = init;
6471
+ queueMicrotask(() => this.connect());
6472
+ }
6473
+ addEventListener(type, f) {
6474
+ let s = this.ls.get(type);
6475
+ if (!s)
6476
+ this.ls.set(type, (s = new Set()));
6477
+ s.add(f);
6478
+ }
6479
+ removeEventListener(type, f) { this.ls.get(type)?.delete(f); }
6480
+ close() {
6481
+ this.readyState = 2;
6482
+ clearTimeout(this.timer);
6483
+ try {
6484
+ this.ctl?.abort();
6485
+ }
6486
+ catch (_) { }
6487
+ }
6488
+ emit(type, ev) {
6489
+ if (type == 'message')
6490
+ this.onmessage?.(ev);
6491
+ else if (type == 'open')
6492
+ this.onopen?.(ev);
6493
+ else if (type == 'error')
6494
+ this.onerror?.(ev);
6495
+ this.ls.get(type)?.forEach(f => { try {
6496
+ f(ev);
6497
+ }
6498
+ catch (e) {
6499
+ console.error(e);
6500
+ } });
6501
+ }
6502
+ async connect() {
6503
+ if (this.readyState == 2)
6504
+ return;
6505
+ const g = globalThis;
6506
+ const f = this.init.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6507
+ if (typeof f != 'function')
6508
+ return this.fail(new Error('fetch is not available in this environment'));
6509
+ const ctl = this.ctl = typeof AbortController == 'function' ? new AbortController() : {};
6510
+ const headers = { accept: 'text/event-stream', ...this.init.headers };
6511
+ if (this.meta.id)
6512
+ headers['last-event-id'] = this.meta.id;
6513
+ let res;
6514
+ try {
6515
+ res = await f(this.url, { headers, credentials: this.withCredentials ? 'include' : 'same-origin', cache: 'no-store', signal: ctl.signal });
6516
+ }
6517
+ catch (error) {
6518
+ return this.retry(error);
6519
+ }
6520
+ if (this.readyState == 2 || this.ctl !== ctl)
6521
+ return;
6522
+ const type = res.headers?.get?.('content-type') || '';
6523
+ if (!(res.status >= 200 && res.status < 300) || !/^text\/event-stream/i.test(type) || !res.body) {
6524
+ try {
6525
+ res.body?.cancel?.();
6526
+ }
6527
+ catch (_) { }
6528
+ return this.fail(Object.assign(new Error(`SSE ${res.status}${type ? ' ' + type : ''}: ${this.url}`), { status: res.status }));
6529
+ }
6530
+ this.readyState = 1;
6531
+ this.emit('open', { type: 'open' });
6532
+ try {
6533
+ for await (const fr of sse(res, this.meta)) {
6534
+ if (this.readyState == 2 || this.ctl !== ctl)
6535
+ return;
6536
+ this.emit(fr.event, { type: fr.event, data: fr.data, lastEventId: this.meta.id });
6537
+ }
6538
+ }
6539
+ catch (error) {
6540
+ if (this.readyState == 2 || this.ctl !== ctl)
6541
+ return;
6542
+ return this.retry(error);
6543
+ }
6544
+ if (this.readyState != 2 && this.ctl === ctl)
6545
+ this.retry();
6546
+ }
6547
+ /** a drop: error, CONNECTING, try again after `retry` ms (unless the error handler closed it) */
6548
+ retry(error) {
6549
+ if (this.readyState == 2)
6550
+ return;
6551
+ this.readyState = 0;
6552
+ this.emit('error', { type: 'error', error });
6553
+ if (this.readyState == 2)
6554
+ return;
6555
+ this.timer = setTimeout(() => this.connect(), this.meta.retry);
6556
+ }
6557
+ /** a failed connection: error, CLOSED */
6558
+ fail(error) {
6559
+ this.readyState = 2;
6560
+ this.emit('error', { type: 'error', error });
6561
+ }
6562
+ /** the state's name (debugging) */
6563
+ get state() { return NAMES[this.readyState]; }
6564
+ }
6565
+ FetchEventSource.CONNECTING = 0;
6566
+ FetchEventSource.OPEN = 1;
6567
+ FetchEventSource.CLOSED = 2;
6568
+
6105
6569
  /*
6106
6570
  * makeSocketDriver(options?) (PLAN-3 §1.3): WebSocket and server-sent events, declared per
6107
6571
  * component instance and answered with reply actions (./replies.ts). Docs on the declarations
@@ -6110,7 +6574,7 @@ function queryCache(o = {}) {
6110
6574
  * - Sink values:
6111
6575
  * - `{ connections: { [name]: spec | falsy } }`: the sender's whole set of connections. Diffed
6112
6576
  * per (sender, name): a new name opens, a removed or falsy one closes, a changed URL /
6113
- * protocols / withCredentials / share closes and reopens, anything else (action names,
6577
+ * protocols / withCredentials / headers / share closes and reopens, anything else (action names,
6114
6578
  * reconnect) only rebinds.
6115
6579
  * - `{ to: name, json? | text? | binary? }`: send on the sender's connection `name` (json is
6116
6580
  * stringified). Queued while (re)connecting, bounded (`queueLimit`, oldest dropped). A send
@@ -6135,6 +6599,10 @@ function queryCache(o = {}) {
6135
6599
  * - A disposed sender's connections close (and their retries stop); app dispose closes all.
6136
6600
  * - No WebSocket / EventSource (SSR): nothing opens, nothing is reported.
6137
6601
  * - `WebSocket` / `EventSource` are read at connect time: options.WebSocket ?? globalThis.WebSocket.
6602
+ * - PLAN-11 E-1: an `sse` connection with `headers` (data: a token in it comes from state, and a
6603
+ * changed header reconnects) is read with FetchEventSource (fetch, `options.fetch` if given),
6604
+ * since EventSource can't send headers. An `options.EventSource` (a polyfill, the test fake)
6605
+ * still wins and gets the headers in its init.
6138
6606
  */
6139
6607
  const SCOPE$1 = '__sygnalScope';
6140
6608
  const scopeOf = (v) => v[SCOPE$1] || [];
@@ -6234,12 +6702,13 @@ function makeSocketDriver(options = {}) {
6234
6702
  };
6235
6703
  const connect = (t) => {
6236
6704
  t.timer = 0;
6237
- const C = t.sse ? options.EventSource ?? g.EventSource : options.WebSocket ?? g.WebSocket;
6705
+ const hdr = t.sse && t.spec.headers && typeof t.spec.headers == 'object' ? t.spec.headers : undefined;
6706
+ const C = t.sse ? options.EventSource ?? (hdr && typeof g.fetch == 'function' ? FetchEventSource : g.EventSource) : options.WebSocket ?? g.WebSocket;
6238
6707
  if (typeof C != 'function')
6239
6708
  return;
6240
6709
  let s;
6241
6710
  try {
6242
- s = t.sse ? new C(t.url, { withCredentials: !!t.spec.withCredentials }) : new C(t.url, t.spec.protocols);
6711
+ s = t.sse ? new C(t.url, { withCredentials: !!t.spec.withCredentials, ...(hdr && { headers: hdr, fetch: options.fetch }) }) : new C(t.url, t.spec.protocols);
6243
6712
  }
6244
6713
  catch (error) {
6245
6714
  // a URL the constructor rejects never opens: no retry
@@ -6343,7 +6812,7 @@ function makeSocketDriver(options = {}) {
6343
6812
  const sse = typeof spec.sse == 'string';
6344
6813
  const url = resolve(sse ? spec.sse : spec.socket, sse);
6345
6814
  // the connection's identity: a change reconnects, anything else only rebinds
6346
- const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials : 's' + JSON.stringify(spec.protocols)) + url;
6815
+ const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials + (spec.headers ? JSON.stringify(spec.headers) : '') : 's' + JSON.stringify(spec.protocols)) + url;
6347
6816
  if (old && old.key === key) {
6348
6817
  old.spec = spec;
6349
6818
  if (old.t)
@@ -6420,7 +6889,7 @@ const dec = (s) => { try {
6420
6889
  catch (_) {
6421
6890
  return s;
6422
6891
  } };
6423
- const g$6 = globalThis;
6892
+ const g$5 = globalThis;
6424
6893
  const PARAM$1 = /:(\w+)/g;
6425
6894
  // G-571: synchronous navigations (redirects) before a microtask, after which each waits a task
6426
6895
  const HOPS = 32;
@@ -6428,7 +6897,7 @@ const HOPS = 32;
6428
6897
  const paramsOf$2 = (pat) => (pat.match(PARAM$1) || []).map(p => p.slice(1));
6429
6898
  // the core's coded error() (published on the diagnostics bridge): no import, so the router
6430
6899
  // costs nothing more standalone; prints in production too (like SYG611)
6431
- const bad$1 = (v, msg, fix) => (g$6.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
6900
+ const bad$1 = (v, msg, fix) => (g$5.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
6432
6901
  function makeRouter(options = {}) {
6433
6902
  const { mode, prefetch, navigate } = options, routes = options.routes || {};
6434
6903
  const base = (options.base || '').replace(/\/$/, '');
@@ -6437,7 +6906,7 @@ function makeRouter(options = {}) {
6437
6906
  const doScroll = options.scroll ?? !navigate;
6438
6907
  const href = (name, params = {}, query, hash) => {
6439
6908
  // dev entry only (SYG130: unknown name, missing or extra params)
6440
- g$6.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
6909
+ g$5.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
6441
6910
  const pat = routes[name];
6442
6911
  const path = pat == null || pat == '*' ? '/' : pat.replace(PARAM$1, (_, k) => enc(params[k] ?? ''));
6443
6912
  const q = query && new URLSearchParams(Object.entries(query).filter(([, v]) => v != null)) + '';
@@ -6547,7 +7016,7 @@ function makeRouter(options = {}) {
6547
7016
  if (!hops++)
6548
7017
  queueMicrotask(() => { hops = 0; });
6549
7018
  if (hops > HOPS) {
6550
- if (g$6.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++)
7019
+ if (g$5.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++)
6551
7020
  console.warn(`[Sygnal] router: more than ${HOPS} navigations in one flush (a redirect loop?); each next one waits a task (reported once)`, r.path);
6552
7021
  setTimeout(send);
6553
7022
  }
@@ -8089,7 +8558,7 @@ function makeHeadDriver(options = {}) {
8089
8558
  * - An invalid spec (non-positive `every`, negative `after`, no action) doesn't start; the dev
8090
8559
  * entry reports SYG422 through the `timerSpec` diagnostics hook.
8091
8560
  */
8092
- const g$5 = globalThis;
8561
+ const g$4 = globalThis;
8093
8562
  const now$1 = () => Date.now();
8094
8563
  const valid = (s) => s.frame
8095
8564
  ? typeof s.frame == 'string'
@@ -8099,7 +8568,7 @@ const valid = (s) => s.frame
8099
8568
  // changes) the timer clears the pending one.
8100
8569
  const start$1 = (s, send, e) => {
8101
8570
  let id, n = 0, last;
8102
- const t0 = now$1(), every = s.every, raf = s.frame && g$5.requestAnimationFrame;
8571
+ const t0 = now$1(), every = s.every, raf = s.frame && g$4.requestAnimationFrame;
8103
8572
  const later = (ms) => id = raf ? requestAnimationFrame(tick) : setTimeout(tick, ms);
8104
8573
  // a frame's `ts` is requestAnimationFrame's timestamp (the fallback passes none: the clock)
8105
8574
  const tick = (ts = now$1()) => {
@@ -8144,7 +8613,7 @@ const timerDriver = (runners) => (sink$) => {
8144
8613
  if (e.ok = valid(s))
8145
8614
  e.x = start$1(s, (type, data) => reply(id, type, data), e);
8146
8615
  else
8147
- g$5.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
8616
+ g$4.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
8148
8617
  }
8149
8618
  if (!d)
8150
8619
  runners.delete(id);
@@ -8202,10 +8671,10 @@ const makeTimerDriver = () => timerDriver(new Map());
8202
8671
  * an intersection / resize declaration with nothing to observe (no DOM source bound). A
8203
8672
  * component declaring `browser` with no driver is SYG643, as timers.
8204
8673
  */
8205
- const g$4 = globalThis;
8674
+ const g$3 = globalThis;
8206
8675
  const noop = () => { };
8207
8676
  const KINDS = ['intersection', 'resize', 'media', 'storage', 'visibility', 'online', 'geolocation'];
8208
- const diag = (...a) => g$4.__SYGNAL_DIAGNOSTICS__?.browserSource?.(...a);
8677
+ const diag = (...a) => g$3.__SYGNAL_DIAGNOSTICS__?.browserSource?.(...a);
8209
8678
  // the driver names in use (the definition hook looks each up in a declaring instance's sources)
8210
8679
  const names$1 = new Set();
8211
8680
  let hooked;
@@ -8291,16 +8760,16 @@ const track = (c, kind, target, Obs, opts, data) => {
8291
8760
  };
8292
8761
  /** Element visibility: `{ intersection: '.cover' | true, action, threshold?, rootMargin? }`, data `{ visible, ratio, index, dataset }` */
8293
8762
  const intersectionSource = { d: {
8294
- intersection: (s, c) => track(c, 'i', s.intersection, g$4.IntersectionObserver, { threshold: s.threshold, rootMargin: s.rootMargin }, (e) => ({ visible: e.isIntersecting, ratio: e.intersectionRatio })),
8763
+ intersection: (s, c) => track(c, 'i', s.intersection, g$3.IntersectionObserver, { threshold: s.threshold, rootMargin: s.rootMargin }, (e) => ({ visible: e.isIntersecting, ratio: e.intersectionRatio })),
8295
8764
  } };
8296
8765
  /** Element size: `{ resize: '.chart' | true, action }`, data `{ width, height, index, dataset }` (the content box) */
8297
8766
  const resizeSource = { d: {
8298
- resize: (s, c) => track(c, 'r', s.resize, g$4.ResizeObserver, undefined, (e) => ({ width: e.contentRect.width, height: e.contentRect.height })),
8767
+ resize: (s, c) => track(c, 'r', s.resize, g$3.ResizeObserver, undefined, (e) => ({ width: e.contentRect.width, height: e.contentRect.height })),
8299
8768
  } };
8300
8769
  /** A media query: `{ media: '(prefers-color-scheme: dark)', action }`, data `{ matches, media }` */
8301
8770
  const mediaSource = { d: {
8302
8771
  media: (s, c) => {
8303
- const m = g$4.matchMedia?.(s.media);
8772
+ const m = g$3.matchMedia?.(s.media);
8304
8773
  if (!m)
8305
8774
  return;
8306
8775
  const f = () => c.send({ matches: m.matches, media: s.media });
@@ -8309,7 +8778,7 @@ const mediaSource = { d: {
8309
8778
  return () => m.removeEventListener('change', f);
8310
8779
  },
8311
8780
  } };
8312
- const area = (s) => g$4[s.area == 'session' ? 'sessionStorage' : 'localStorage'];
8781
+ const area = (s) => g$3[s.area == 'session' ? 'sessionStorage' : 'localStorage'];
8313
8782
  const failed$1 = (e) => ({ name: e?.name, message: e?.message });
8314
8783
  /**
8315
8784
  * A storage key (read and observed, other tabs' writes included): `{ storage: 'theme', action,
@@ -8338,8 +8807,8 @@ const storageSource = {
8338
8807
  };
8339
8808
  f();
8340
8809
  watching.add(f);
8341
- g$4.addEventListener?.('storage', f);
8342
- return () => { watching.delete(f); g$4.removeEventListener?.('storage', f); };
8810
+ g$3.addEventListener?.('storage', f);
8811
+ return () => { watching.delete(f); g$3.removeEventListener?.('storage', f); };
8343
8812
  } },
8344
8813
  c: (() => {
8345
8814
  const write = (v, value, ok, fail) => {
@@ -8366,7 +8835,7 @@ const storageSource = {
8366
8835
  /** The document's visibility: `{ visibility: true, action }`, data `{ visible }` */
8367
8836
  const visibilitySource = { d: {
8368
8837
  visibility: (_, c) => {
8369
- const d = g$4.document;
8838
+ const d = g$3.document;
8370
8839
  if (!d)
8371
8840
  return;
8372
8841
  const f = () => c.send({ visible: d.visibilityState != 'hidden' });
@@ -8378,13 +8847,13 @@ const visibilitySource = { d: {
8378
8847
  /** The network: `{ online: true, action }`, data `{ online }` */
8379
8848
  const onlineSource = { d: {
8380
8849
  online: (_, c) => {
8381
- if (!g$4.navigator || !g$4.addEventListener)
8850
+ if (!g$3.navigator || !g$3.addEventListener)
8382
8851
  return;
8383
- const f = () => c.send({ online: g$4.navigator.onLine });
8852
+ const f = () => c.send({ online: g$3.navigator.onLine });
8384
8853
  f();
8385
- g$4.addEventListener('online', f);
8386
- g$4.addEventListener('offline', f);
8387
- return () => { g$4.removeEventListener('online', f); g$4.removeEventListener('offline', f); };
8854
+ g$3.addEventListener('online', f);
8855
+ g$3.addEventListener('offline', f);
8856
+ return () => { g$3.removeEventListener('online', f); g$3.removeEventListener('offline', f); };
8388
8857
  },
8389
8858
  } };
8390
8859
  /**
@@ -8394,7 +8863,7 @@ const onlineSource = { d: {
8394
8863
  */
8395
8864
  const geolocationSource = { d: {
8396
8865
  geolocation: (s, c) => {
8397
- const geo = g$4.navigator?.geolocation;
8866
+ const geo = g$3.navigator?.geolocation;
8398
8867
  if (!geo)
8399
8868
  return c.fail({ code: 2, message: 'Geolocation is not available' });
8400
8869
  const id = geo.watchPosition((p) => {
@@ -8410,11 +8879,11 @@ const geolocationSource = { d: {
8410
8879
  */
8411
8880
  const clipboardSource = { c: {
8412
8881
  copy: (v, ok, fail) => {
8413
- const text = '' + v.copy, cb = g$4.navigator?.clipboard;
8882
+ const text = '' + v.copy, cb = g$3.navigator?.clipboard;
8414
8883
  cb ? cb.writeText(text).then(() => ok({ text }), (e) => fail(failed$1(e))) : fail({ name: 'NotSupportedError', message: 'Clipboard is not available' });
8415
8884
  },
8416
8885
  paste: (_, ok, fail) => {
8417
- const cb = g$4.navigator?.clipboard;
8886
+ const cb = g$3.navigator?.clipboard;
8418
8887
  cb ? cb.readText().then((text) => ok({ text }), (e) => fail(failed$1(e))) : fail({ name: 'NotSupportedError', message: 'Clipboard is not available' });
8419
8888
  },
8420
8889
  } };
@@ -9119,6 +9588,9 @@ class App {
9119
9588
  }
9120
9589
  /** STATE.stream: the instances whose state changed emit it */
9121
9590
  notify() {
9591
+ if (this.nv === this.ver)
9592
+ return;
9593
+ this.nv = this.ver;
9122
9594
  for (const i of this.watchers)
9123
9595
  i.notify();
9124
9596
  }
@@ -9246,7 +9718,7 @@ class App {
9246
9718
  this.ran = false;
9247
9719
  this.last = v;
9248
9720
  this.hooks.onPatch?.(v);
9249
- this.vdomL?.next(v);
9721
+ this.vdomL?.next(v ?? null);
9250
9722
  }
9251
9723
  if (this.dq.length)
9252
9724
  this.stopLater();
@@ -9935,7 +10407,7 @@ const W$1 = '__sw';
9935
10407
  const HOST = /^(id|class(Name)?|style|title|name|placeholder|role|tab[iI]ndex|hidden|lang|dir|attrs)$|^(aria|data)-/;
9936
10408
  /** the dev entry's reporter (checks/widgets.ts); returns the diagnostic when one was reported
9937
10409
  * (r: G-580, a 661 the widget reported through error(e), not a throwing update) */
9938
- const dev$8 = (code, w, o, x, r) => globalThis.__SYGNAL_DIAGNOSTICS__?.widget?.(code, w, o, x, r);
10410
+ const dev$7 = (code, w, o, x, r) => globalThis.__SYGNAL_DIAGNOSTICS__?.widget?.(code, w, o, x, r);
9939
10411
  let ids$1 = 0;
9940
10412
  // shallow; G-362: an object host prop (style, attrs, a class map) compares one level deep, so a
9941
10413
  // new object with the same entries is no change (snabbdom patches the host from it anyway)
@@ -9948,16 +10420,16 @@ const same$2 = (a, b, d) => {
9948
10420
  // reported through error(e)
9949
10421
  function fail$1(o, w, k, code, e, r) {
9950
10422
  e = e instanceof Error ? e : Error(e);
9951
- dev$8(code, w, o, e, r) || console.error(`[Sygnal SYG${code}]`, e);
10423
+ dev$7(code, w, o, e, r) || console.error(`[Sygnal SYG${code}]`, e);
9952
10424
  o.app.appError(o, e, 'widget');
9953
10425
  if (code < 662 && !o.disposed)
9954
10426
  (o.$wf || (o.$wf = new Map())).set(k, [e]), o.refresh();
9955
10427
  }
9956
10428
  function mount(v) {
9957
10429
  const el = v.elm, { ww: w, wp: p, wo: o } = v.data;
9958
- const d = (n, detail) => { dev$8(140, w, o, n); el.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })); };
10430
+ const d = (n, detail) => { dev$7(140, w, o, n); el.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })); };
9959
10431
  let i;
9960
- dev$8(144, w, o, el);
10432
+ dev$7(144, w, o, el);
9961
10433
  // G-409: mount's fourth parameter reports a later failure (an adapter's own redraw): SYG661,
9962
10434
  // the owner's onError fallback in this instance's place, as for a throwing update (G-580: the
9963
10435
  // dev text says error(e) reported it)
@@ -10077,7 +10549,7 @@ function defineWidget(def) {
10077
10549
  },
10078
10550
  $ssr: host,
10079
10551
  // D190/D199: a widget tag is not a selector (SYG143 in dev): select its className or a control
10080
- toString: () => (dev$8(143, w), ':not(*)'),
10552
+ toString: () => (dev$7(143, w), ':not(*)'),
10081
10553
  });
10082
10554
  }
10083
10555
 
@@ -10126,6 +10598,30 @@ function defineWidget(def) {
10126
10598
  * can still be assigned), which reads the host's current `uses`.
10127
10599
  * - Limitations: a host intent must return an object (not one stream).
10128
10600
  */
10601
+ /** A behavior output's slice from before the action (outputs get the updated one): for a transition shared with STATE */
10602
+ const was = (s, p, k) => p?.previousState ? p.previousState[k] : s;
10603
+ /**
10604
+ * A behavior model whose outputs compute from the state before the action (its slice and
10605
+ * `props.state`), for behaviors whose STATE and outputs share one transition; STATE / HOST as is
10606
+ */
10607
+ const fromPrevious = (model) => {
10608
+ const out = {};
10609
+ for (const a in model) {
10610
+ const e = model[a];
10611
+ if (!e || typeof e != 'object') {
10612
+ out[a] = e;
10613
+ continue;
10614
+ }
10615
+ const m = {};
10616
+ for (const s in e) {
10617
+ const f = e[s];
10618
+ m[s] = s == 'STATE' || s == 'HOST' || typeof f != 'function' ? f
10619
+ : (x, d, n, p, o, k) => f(was(x, p, k), d, n, p?.previousState ? { ...p, state: p.previousState } : p, o, k);
10620
+ }
10621
+ out[a] = m;
10622
+ }
10623
+ return out;
10624
+ };
10129
10625
  // a model entry as { sink: fn }: a constant is sent as is, `true` sends the action's data, an
10130
10626
  // EFFECT constant does nothing (as the core treats them)
10131
10627
  const sinksOf = (e, S) => {
@@ -10308,6 +10804,8 @@ const pager = /*#__PURE__*/ defineBehavior({
10308
10804
  });
10309
10805
 
10310
10806
  const isRow = (r) => r?.id != null;
10807
+ /** PLAN-11 F-2: a File / Blob is a field value (a leaf), not an object of fields */
10808
+ const isFile = (v) => typeof Blob != 'undefined' && v instanceof Blob;
10311
10809
  // the key of segment `s` in `v`: an array's row with that id, else the index unless the element
10312
10810
  // there is a row (G-380: a row is named by its id only; -1, no element), an object's key
10313
10811
  const key = (v, s) => {
@@ -10354,7 +10852,7 @@ const setField = (values, name, value) => {
10354
10852
  * array-level error such as "at least one address"); a row's `id` is not a field
10355
10853
  */
10356
10854
  const fieldNames = (v, at = '', row) => {
10357
- if (!v || typeof v != 'object')
10855
+ if (!v || typeof v != 'object' || isFile(v))
10358
10856
  return at ? [at] : [];
10359
10857
  const a = Array.isArray(v), out = a ? [at] : [];
10360
10858
  for (const k in v) {
@@ -10435,7 +10933,12 @@ const focusInvalid = (names, within) => {
10435
10933
  * field.
10436
10934
  *
10437
10935
  * Options: `values` (the start values; D239: or a function of the host's state, called when the
10438
- * form starts), `submit` (host action), `check` (async per-field checks through a driver: a
10936
+ * form starts), `submit` (host action), `request` (PLAN-11 F-3: the form sends the valid submit
10937
+ * itself on the `http` sink: a request, or `(data, hostState) => request`, completed with the
10938
+ * body (`multipart: data` when a top-level value is a File / Blob or an array of them, else
10939
+ * `json: data`, unless it sets json / multipart / body), `progress: '<key>.PROGRESS'` (unless set)
10940
+ * and its own `ok: '<key>.DONE'`, `error: '<key>.ERRORS'`; with `submit` too, that host action is
10941
+ * still dispatched (for state), and should send no request), `check` (async per-field checks through a driver: a
10439
10942
  * request with a reply action, `latest`), `show` ('blur' default: a schema error shows once its
10440
10943
  * field was blurred; 'input'; 'submit'), `form` (selector), `http` (the driver sink of the
10441
10944
  * checks, 'HTTP'), `resetOnShow` (D239), `tool` (PLAN-6 A-3, experimental: `formTool({ … })` from
@@ -10454,6 +10957,8 @@ const focusInvalid = (names, within) => {
10454
10957
  * before that (a child's first render) the fields come from calling it with a blank state.
10455
10958
  *
10456
10959
  * Slice (state.form): values, initial, errors (all current schema errors by name), touched,
10960
+ * progress (PLAN-11 F-2: the submit's last progress step or null), resets (counts RESET and
10961
+ * resetOnShow starts: key a file input with it, since a file input can't be cleared by state),
10457
10962
  * server (server errors), remote (async check results: '' passed), pending (name -> value being
10458
10963
  * checked), submitting, submitted, submitCount, queued (a submit waits for a check or an async
10459
10964
  * schema), validating (an async schema runs), validated (the schema has answered since the start
@@ -10463,11 +10968,14 @@ const focusInvalid = (names, within) => {
10463
10968
  *
10464
10969
  * Actions: form.CHANGE ({ name, value, item? }: G-376: a checkbox gives `checked`, and `item`, its
10465
10970
  * value, which toggles membership when the field is an array; <select multiple> an array of the
10466
- * selected values; type=file is ignored), form.SET ({ values }: G-647, field name -> value, several
10971
+ * selected values; PLAN-11 F-2: a file input its File, `multiple` an array of them, none null),
10972
+ * form.SET ({ values }: G-647, field name -> value, several
10467
10973
  * fields changed at once as if typed: one validation, `touched` as for CHANGE, unknown names
10468
10974
  * skipped; e.g. an AI fill), form.BLUR (name), form.SUBMIT, form.ADD ({ field,
10469
10975
  * value }), form.REMOVE ({ field, id }), form.ERRORS (an error reply or a map: server errors),
10470
- * form.DONE (saved), form.RESET (values?); internal: form.RESULT (an async schema's result),
10976
+ * form.DONE (saved), form.RESET (values?), form.PROGRESS (PLAN-11 F-2: a progress step of the
10977
+ * submit request, `progress: 'form.PROGRESS'`: kept as `progress` while submitting, cleared by
10978
+ * DONE / ERRORS / RESET); internal: form.RESULT (an async schema's result),
10471
10979
  * form.CHECKED_<name> (a check's reply; a failed check passes: the server checks on submit),
10472
10980
  * form.VALIDATE (the first validation, when the host starts: G-375; `valid` is false until the
10473
10981
  * schema answered; with resetOnShow also each time the form element appears, D239).
@@ -10491,8 +10999,14 @@ const focusInvalid = (names, within) => {
10491
10999
  * fields. The host checks (SYG234 submit, SYG235 check names, SYG236 row ids)
10492
11000
  * run when the host is created.
10493
11001
  */
10494
- const dev$7 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.form?.(...a);
11002
+ // PLAN-11 F-3: a submit's data goes as multipart when a top-level value is a file (or files)
11003
+ const hasFile = (d) => !!d && typeof d == 'object' && Object.values(d).some(x => isFile(x) || Array.isArray(x) && x.some(isFile));
11004
+ const dev$6 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.form?.(...a);
10495
11005
  const keys = (o) => Object.keys(o).filter(k => o[k]);
11006
+ // PLAN-11 F-2: values compared as JSON, a File by identity-ish fields (JSON gives every File {})
11007
+ const fileIds = new WeakMap();
11008
+ let fileSeq = 0;
11009
+ const json = (v) => JSON.stringify(v, (_k, x) => isFile(x) ? `\0file:${fileIds.get(x) ?? (fileIds.set(x, ++fileSeq), fileSeq)}` : x);
10496
11010
  const drop = (o, p) => {
10497
11011
  const r = {};
10498
11012
  for (const k in o)
@@ -10501,8 +11015,8 @@ const drop = (o, p) => {
10501
11015
  return r;
10502
11016
  };
10503
11017
  const form = (schema, o = {}) => {
10504
- isStandardSchema(schema) || dev$7(231, schema);
10505
- const { values: vo = {}, submit, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again } = o;
11018
+ isStandardSchema(schema) || dev$6(231, schema);
11019
+ const { values: vo = {}, submit, request: rq, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again } = o;
10506
11020
  // D239: `values` can be a function of the host's state, called when the form starts (and each
10507
11021
  // show, resetOnShow). Before that (the host's first render, before its first action) the
10508
11022
  // fields come from calling it with a blank state: every read gives '' in the result
@@ -10534,7 +11048,7 @@ const form = (schema, o = {}) => {
10534
11048
  // from a named button)
10535
11049
  const known = (s, n, quiet) => {
10536
11050
  const k = n && hasField(s.values, n);
10537
- n && !k && !quiet && dev$7(230, n, s.values);
11051
+ n && !k && !quiet && dev$6(230, n, s.values);
10538
11052
  return k;
10539
11053
  };
10540
11054
  // new values: errors now (sync schema), or after the schema's Promise (RESULT)
@@ -10549,7 +11063,7 @@ const form = (schema, o = {}) => {
10549
11063
  });
10550
11064
  const ask = (s, f, k, queued) => {
10551
11065
  const x = getField(s.values, f), q = checks[f].request(x), ok = `${k}.CHECKED_${f}`;
10552
- dev$7(235, f, q);
11066
+ dev$6(235, f, q);
10553
11067
  return { s: { ...s, queued, pending: { ...s.pending, [f]: x } }, req: { ...q, ok, error: ok, latest: true } };
10554
11068
  };
10555
11069
  // a submit with s's errors: blocked (focus the first), a check to run or to wait for, or sent
@@ -10560,10 +11074,11 @@ const form = (schema, o = {}) => {
10560
11074
  : Object.keys(s.pending).length ? { s: { ...s, queued: true } }
10561
11075
  : { s: req ? { ...s, queued: false, submitting: true } : done({ ...s, queued: false }), send: 1 };
10562
11076
  };
10563
- const base = (vals) => ({ initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
10564
- submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false });
10565
- const fresh = (vals) => edit(base(vals), vals);
10566
- const done = (s) => ({ ...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {} });
11077
+ const base = (vals, resets = 0) => ({ initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
11078
+ submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false, progress: null, resets });
11079
+ // a start over (RESET, resetOnShow): `resets` counts them, so a file input keyed by it is made anew
11080
+ const fresh = (vals, s) => edit(base(vals, s ? (s.resets || 0) + 1 : 0), vals);
11081
+ const done = (s) => ({ ...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {}, progress: null });
10567
11082
  const group = (cur, d) => {
10568
11083
  if (!Array.isArray(cur) || !('item' in d))
10569
11084
  return d.value;
@@ -10572,10 +11087,10 @@ const form = (schema, o = {}) => {
10572
11087
  };
10573
11088
  // fields changed as typed ones ([name, value] pairs; unknown names skipped, SYG230): their
10574
11089
  // server errors, check results and running checks go, `touched` as show 'input' says; one validation
10575
- const put = (s, d) => {
11090
+ const put = (s, d, quiet) => {
10576
11091
  let vals = s.values, server = drop(s.server, ''), remote = s.remote, pending = s.pending, touched = s.touched;
10577
11092
  for (const [n, y] of d)
10578
- if (known(s, n)) {
11093
+ if (known(s, n, quiet)) {
10579
11094
  vals = setField(vals, n, y), server = drop(server, n), remote = drop(remote, n), pending = drop(pending, n);
10580
11095
  if (show == 'input')
10581
11096
  touched = { ...touched, [n]: true };
@@ -10585,7 +11100,9 @@ const form = (schema, o = {}) => {
10585
11100
  const steps = {
10586
11101
  // G-376: a checkbox (`item`: its value) on an array field is one of a group: checked adds
10587
11102
  // its value, unchecked removes it
10588
- CHANGE: (s, d) => put(s, d?.name ? [[d.name, group(getField(s.values, d.name), d)]] : []),
11103
+ // PLAN-11 F-2: a file input whose name isn't in values is left alone, quietly (an app that reads
11104
+ // its files in its own intent, as before 6.7)
11105
+ CHANGE: (s, d) => put(s, d?.name ? [[d.name, group(getField(s.values, d.name), d)]] : [], d?.file),
10589
11106
  BLUR: (s, n, k) => {
10590
11107
  if (!known(s, n, 1))
10591
11108
  return null;
@@ -10594,7 +11111,7 @@ const form = (schema, o = {}) => {
10594
11111
  },
10595
11112
  SUBMIT: (s, _, k) => {
10596
11113
  if (s.submitting || s.queued)
10597
- return void dev$7(232, s);
11114
+ return void dev$6(232, s);
10598
11115
  const n = { ...s, submitCount: s.submitCount + 1 }, r = v(s.values);
10599
11116
  // G-371: a submit waiting for an async schema is queued (a second one is dropped)
10600
11117
  return r.then ? { s: { ...n, validating: true, queued: true }, wait: { values: s.values, submit: 1 } } : attempt({ ...n, errors: r.errors }, k);
@@ -10618,16 +11135,20 @@ const form = (schema, o = {}) => {
10618
11135
  SET: (s, d) => put(s, Object.entries(d?.values || {})),
10619
11136
  ERRORS: (s, d) => {
10620
11137
  const e = replyErrors(d, s.values);
10621
- return { s: { ...s, server: e, submitting: false, queued: false }, focus: focusInvalid(e, sel) };
11138
+ return { s: { ...s, server: e, submitting: false, queued: false, progress: null }, focus: focusInvalid(e, sel) };
10622
11139
  },
10623
11140
  DONE: (s) => ({ s: done(s) }),
10624
- RESET: (s, d) => fresh(d && typeof d == 'object' ? d : s.initial),
11141
+ // PLAN-11 F-2: a progress step of the submit's request (`progress: 'form.PROGRESS'`), while it is pending
11142
+ PROGRESS: (s, d) => s.submitting && d && typeof d == 'object'
11143
+ ? { s: { ...s, progress: { phase: d.phase, loaded: d.loaded, total: d.total ?? null, fraction: d.fraction ?? null } } } : null,
11144
+ // PLAN-11: only a plain object is new values (a click event from `DOM.click('.reset')` is not)
11145
+ RESET: (s, d) => fresh(d && typeof d == 'object' && [Object.prototype, null].includes(Object.getPrototypeOf(d)) ? d : s.initial, s),
10625
11146
  // G-375: the first validation, when the host starts (form() doesn't validate at module load).
10626
11147
  // G-575: a slice kept in parent state from a host that unmounted mid-submit starts unstuck.
10627
11148
  // D239: with resetOnShow (also each time the form element appears again), or a `values`
10628
11149
  // function not called yet, the slice starts over from the start values (h: the host state)
10629
- VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo)
10630
- : edit({ ...s, submitting: false, queued: false, pending: {} }, s.values),
11150
+ VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo, again && s)
11151
+ : edit({ ...s, submitting: false, queued: false, pending: {}, progress: null }, s.values),
10631
11152
  };
10632
11153
  // a check's reply (ok and error: an error reply carries an Error and passes)
10633
11154
  for (const f of checked)
@@ -10637,6 +11158,14 @@ const form = (schema, o = {}) => {
10637
11158
  const n = { ...s, pending: drop(s.pending, f), remote: { ...s.remote, [f]: d?.error instanceof Error ? '' : checks[f].error?.(d) || '' } };
10638
11159
  return s.queued ? attempt(n, k) : { s: n };
10639
11160
  };
11161
+ // PLAN-11 F-3: the `request` option's request for a valid submit (data: the schema's output)
11162
+ const send = (vals, h, k) => {
11163
+ const data = v(vals).value, q = typeof rq == 'function' ? rq(data, h) : rq;
11164
+ if (!q || q === ABORT)
11165
+ return ABORT;
11166
+ const own = 'json' in q || 'multipart' in q || 'body' in q;
11167
+ return { ...(!own && (hasFile(data) ? { multipart: data } : { json: data })), progress: `${k}.PROGRESS`, ...q, ok: `${k}.DONE`, error: `${k}.ERRORS` };
11168
+ };
10640
11169
  const model = {};
10641
11170
  for (const a in steps) {
10642
11171
  let last = [];
@@ -10646,19 +11175,28 @@ const form = (schema, o = {}) => {
10646
11175
  // VALIDATE: on the host's state (HOST), for a `values` function; the slice's own steps on it
10647
11176
  ...a == 'VALIDATE' ? { HOST: (h, d, _n, _p, _o, k) => { const x = out(h[k], d, k, h); return x ? { ...h, [k]: x.s } : h; } }
10648
11177
  : { STATE: (s, d, _n, _p, _o, k) => out(s, d, k)?.s || ABORT },
10649
- EFFECT: (s, d, next, _p, _o, k) => {
11178
+ // outputs share STATE's transition: computed from the slice before the action
11179
+ EFFECT: (s, d, next, p, _o, k) => {
11180
+ s = was(s, p, k);
10650
11181
  const x = out(s, d, k), w = x?.wait;
10651
11182
  if (x?.send) {
10652
11183
  const r = v(x.s.values).value;
10653
- dev$7(233, s.values, r);
11184
+ dev$6(233, s.values, r);
10654
11185
  // G-577: a submit named after one of the form's own actions goes to that one
10655
- submit in steps && dev$7(234, submit, k, b);
10656
- next(submit, r, 0);
11186
+ submit in steps && dev$6(234, submit, k, b);
11187
+ // PLAN-11 F-3: with `request`, `submit` is optional (dispatched for state when given)
11188
+ if (submit !== undefined || !rq)
11189
+ next(submit, r, 0);
10657
11190
  }
10658
11191
  w && Promise.resolve(v(w.values)).then(() => next('RESULT', w, 0));
10659
11192
  },
10660
- ELEMENT: (s, d, _n, _p, _o, k) => out(s, d, k)?.focus || ABORT,
10661
- ...(checked.length && { [http]: (s, d, _n, _p, _o, k) => out(s, d, k)?.req || ABORT }),
11193
+ ELEMENT: (s, d, _n, p, _o, k) => out(was(s, p, k), d, k)?.focus || ABORT,
11194
+ // the checks' requests, and (PLAN-11 F-3) the `request` option's for a valid submit; the
11195
+ // step from the slice before the action (7.0: outputs get the updated one), the host state as committed
11196
+ ...((checked.length || rq) && { [http]: (s, d, _n, p, _o, k) => {
11197
+ const x = out(was(s, p, k), d, k);
11198
+ return x?.req || (x?.send && rq ? send(x.s.values, p?.state, k) : ABORT);
11199
+ } }),
10662
11200
  };
10663
11201
  }
10664
11202
  const b = defineBehavior({
@@ -10674,9 +11212,11 @@ const form = (schema, o = {}) => {
10674
11212
  VALIDATE: again ? xs$1.merge(xs$1.of(0), f.elements().filter((e) => { const r = e.length && !on; on = e.length; return r; })) : xs$1.of(0),
10675
11213
  // G-376: a checkbox (native, or a hyphenated tag with a boolean `checked`: a
10676
11214
  // form-associated custom checkbox / switch) gives `checked` and its value as `item`; a
10677
- // <select multiple> the selected values; type=file is left alone; others give `value`
10678
- CHANGE: f.events('input').filter(({ target: t }) => t.type != 'file').map(({ target: t }) => t.type == 'checkbox' || /-/.test(t.tagName) && typeof t.checked == 'boolean' ? { name: t.name, value: t.checked, item: t.value }
10679
- : { name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o) => o.value) : t.value }),
11215
+ // <select multiple> the selected values; PLAN-11 F-2: a file input its File (`multiple`:
11216
+ // an array of them; none: null); others give `value`
11217
+ CHANGE: f.events('input').map(({ target: t }) => t.type == 'file' ? { name: t.name, value: t.multiple || t.getAttribute?.('multiple') != null ? [...(t.files || [])] : t.files?.[0] ?? null, file: true }
11218
+ : t.type == 'checkbox' || /-/.test(t.tagName) && typeof t.checked == 'boolean' ? { name: t.name, value: t.checked, item: t.value }
11219
+ : { name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o) => o.value) : t.value }),
10680
11220
  BLUR: f.events('focusout').map((e) => e.target.name),
10681
11221
  SUBMIT: f.events('submit', { preventDefault: true }),
10682
11222
  };
@@ -10694,7 +11234,7 @@ const form = (schema, o = {}) => {
10694
11234
  },
10695
11235
  // G-382: an async schema re-validating keeps the last answer's validity (no flicker)
10696
11236
  valid: (s) => s.validated && !keys(s.errors).length && !keys(s.remote).length,
10697
- dirty: (s) => JSON.stringify(s.values) != JSON.stringify(s.initial),
11237
+ dirty: (s) => json(s.values) != json(s.initial),
10698
11238
  error: (s) => s.server[''] || (s.submitCount && s.errors['']) || '',
10699
11239
  },
10700
11240
  })({ ...o, values }), merge = b.merge;
@@ -10702,7 +11242,7 @@ const form = (schema, o = {}) => {
10702
11242
  o.tool?.$?.(b, v);
10703
11243
  b.merge = (c, k) => {
10704
11244
  const e = c.model?.[submit];
10705
- req = !!e && typeof e == 'object' && http in e;
11245
+ req = !!rq || !!e && typeof e == 'object' && http in e;
10706
11246
  merge(c, k);
10707
11247
  };
10708
11248
  return b;
@@ -10851,7 +11391,7 @@ const selection = (options = {}) => {
10851
11391
  * interaction, SYG147 a `from` key that isn't an array in the host state, SYG435 (3-H) a list
10852
11392
  * shown in another order than its array's (a Collection's sort / filter).
10853
11393
  */
10854
- const dev$6 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.sortable?.(...a);
11394
+ const dev$5 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.sortable?.(...a);
10855
11395
  const LIFT = /^( |Enter|Spacebar)$/;
10856
11396
  const KEYS = /^(Arrow(Up|Down|Left|Right)|Home|End|Escape|Tab)$/;
10857
11397
  const S$1 = String;
@@ -10965,7 +11505,7 @@ const sortable = (options = {}) => {
10965
11505
  } } } } };
10966
11506
  };
10967
11507
  // dev: SYG435, the list shown in another order than the array's (G-449)
10968
- const check = (st, seen, k) => seen && dev$6(435, seen, st, lists, idField, k);
11508
+ const check = (st, seen, k) => seen && dev$5(435, seen, st, lists, idField, k);
10969
11509
  // a press or key handler: drag state this instance didn't start (its token `n` isn't the
10970
11510
  // event's: restored, synced) is dropped first (G-452)
10971
11511
  const live = (h) => (st, d, next, _p, _o, k) => {
@@ -11006,7 +11546,7 @@ const sortable = (options = {}) => {
11006
11546
  const gripOf = (e, self) => {
11007
11547
  const t = e.target, r = e.ownerTarget || e.currentTarget;
11008
11548
  if (!checked)
11009
- checked = dev$6(146, r, item, handle, attr);
11549
+ checked = dev$5(146, r, item, handle, attr);
11010
11550
  const g = !claimed.has(e) && t?.closest?.(grip);
11011
11551
  if (!g || self && g !== t)
11012
11552
  return null;
@@ -11020,14 +11560,14 @@ const sortable = (options = {}) => {
11020
11560
  return null;
11021
11561
  const id = idOf(it);
11022
11562
  if (id == null)
11023
- dev$6(145, it, item, attr);
11563
+ dev$5(145, it, item, attr);
11024
11564
  return id;
11025
11565
  };
11026
11566
  const keyed = (keys, kb) => (e) => keys.test(e.key) && (kb || e.key != 'Tab') && gripOf(e, true) != null;
11027
11567
  const lift = keyed(LIFT), step = keyed(KEYS, 1);
11028
11568
  // dev (SYG435): this host's own items' ids in the order they're shown (computed by the
11029
11569
  // diagnostics entry only)
11030
- const shown = (e) => dev$6(435, () => {
11570
+ const shown = (e) => dev$5(435, () => {
11031
11571
  const r = e.ownerTarget || e.currentTarget;
11032
11572
  return [[...r.querySelectorAll(item)].filter((it) => own(it, r)), r];
11033
11573
  }, key, lists, idOf);
@@ -11071,7 +11611,7 @@ const sortable = (options = {}) => {
11071
11611
  // again by HMR, a second host of the slice) is reset, as at END
11072
11612
  INIT: { HOST: (st, _d, _n, _p, _o, k) => {
11073
11613
  for (const l of lists)
11074
- Array.isArray(st?.[l]) || dev$6(147, l, st, k);
11614
+ Array.isArray(st?.[l]) || dev$5(147, l, st, k);
11075
11615
  return st[k].press || st[k].dragging ? back(st, k) : ABORT;
11076
11616
  } },
11077
11617
  // unmounted mid-drag: the drag is cancelled (no DROPPED; a keyboard drag's item goes back
@@ -11483,14 +12023,14 @@ const undo = (options) => {
11483
12023
  * too), once per kind (restore / save / clear; a save that works again re-arms it); the app
11484
12024
  * continues on initialState / unsaved, and a failed save is retried at the next change.
11485
12025
  */
11486
- const g$3 = globalThis;
12026
+ const g$2 = globalThis;
11487
12027
  const setupPersist = (c, o) => {
11488
12028
  const { key, pick, omit, version = 1, migrate, sync, debounceMs = 100 } = o, plain = o.format == 'plain', src = c.sources, m = src.__m;
11489
12029
  const env = src.__storage, calc = c.calculated || {};
11490
12030
  // hydrating: the option, else the integration's signal (__hydrate: Astro, Vike), else whether
11491
12031
  // run()'s mount point starts with renderToString's markup (its root element is marked; a
11492
12032
  // client-only app's loading placeholder isn't server markup)
11493
- const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g$3.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
12033
+ const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g$2.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
11494
12034
  let S, t, last, raw, skip, off;
11495
12035
  // (the docs link the message ends with explains the causes and fixes). Once per kind (restore,
11496
12036
  // save, clear): a storage that throws on every write must not warn on every change; a save that
@@ -11563,7 +12103,7 @@ const setupPersist = (c, o) => {
11563
12103
  }
11564
12104
  c.model = model;
11565
12105
  try {
11566
- S = typeof o.storage == 'object' ? o.storage : (env || g$3)[(o.storage || 'local') + (env ? '' : 'Storage')];
12106
+ S = typeof o.storage == 'object' ? o.storage : (env || g$2)[(o.storage || 'local') + (env ? '' : 'Storage')];
11567
12107
  }
11568
12108
  catch (_) { }
11569
12109
  if (!S)
@@ -11600,14 +12140,14 @@ const setupPersist = (c, o) => {
11600
12140
  send(read(raw = v)); };
11601
12141
  const onStorage = (e) => (!e.storageArea || e.storageArea == S) && onSync(e.key, e.newValue);
11602
12142
  if (sync)
11603
- off = S.subscribe ? S.subscribe(onSync) : (g$3.addEventListener?.('storage', onStorage), () => g$3.removeEventListener?.('storage', onStorage));
11604
- g$3.addEventListener?.('pagehide', flush);
12143
+ off = S.subscribe ? S.subscribe(onSync) : (g$2.addEventListener?.('storage', onStorage), () => g$2.removeEventListener?.('storage', onStorage));
12144
+ g$2.addEventListener?.('pagehide', flush);
11605
12145
  env?.f.add(flush);
11606
12146
  c._dispose$.addListener({ next: () => {
11607
12147
  flush();
11608
12148
  state$.removeListener(L);
11609
12149
  off?.();
11610
- g$3.removeEventListener?.('pagehide', flush);
12150
+ g$2.removeEventListener?.('pagehide', flush);
11611
12151
  env?.f.delete(flush);
11612
12152
  } });
11613
12153
  };
@@ -12839,7 +13379,7 @@ const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/\W+/g, '_
12839
13379
  const keyOf$1 = (iv) => iv.state?.id;
12840
13380
  const done = new WeakMap();
12841
13381
  /** a dev diagnostic, once per (declaration, what); 'error' mode rethrows outside the caller */
12842
- const dev$5 = (code, decl, what, component, message) => {
13382
+ const dev$4 = (code, decl, what, component, message) => {
12843
13383
  let s = done.get(decl);
12844
13384
  if (!s)
12845
13385
  done.set(decl, s = new Set());
@@ -13048,7 +13588,7 @@ function agentTools(target, options = {}) {
13048
13588
  v = d.read(s);
13049
13589
  }
13050
13590
  catch (e) {
13051
- dev$5('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`);
13591
+ dev$4('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`);
13052
13592
  }
13053
13593
  reads.set(iv, { s, v });
13054
13594
  return v;
@@ -13067,7 +13607,7 @@ function agentTools(target, options = {}) {
13067
13607
  v = !!w(s);
13068
13608
  }
13069
13609
  catch (e) {
13070
- dev$5('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`);
13610
+ dev$4('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`);
13071
13611
  }
13072
13612
  m.set(action, { s, v });
13073
13613
  return v;
@@ -13078,7 +13618,7 @@ function agentTools(target, options = {}) {
13078
13618
  return d.label ? String(d.label(iv.state)) : undefined;
13079
13619
  }
13080
13620
  catch (e) {
13081
- dev$5('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`);
13621
+ dev$4('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`);
13082
13622
  }
13083
13623
  };
13084
13624
  function walk(iv, out) {
@@ -13113,7 +13653,7 @@ function agentTools(target, options = {}) {
13113
13653
  else if (e.decl === d && e.item && item)
13114
13654
  e.ivs.push(iv);
13115
13655
  else
13116
- dev$5('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`);
13656
+ dev$4('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`);
13117
13657
  }
13118
13658
  for (const e of by.values())
13119
13659
  if (e.item) {
@@ -13122,9 +13662,9 @@ function agentTools(target, options = {}) {
13122
13662
  const k = keyOf$1(iv);
13123
13663
  // an item without an id is keyed by its index (the core's `_i<index>` uid part): unstable
13124
13664
  if (k == null || /-_i\d+$/.test(iv.uid))
13125
- dev$5('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`);
13665
+ dev$4('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`);
13126
13666
  else if (ks.has(k + ''))
13127
- dev$5('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`);
13667
+ dev$4('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`);
13128
13668
  else
13129
13669
  ks.add(k + '');
13130
13670
  }
@@ -13150,11 +13690,11 @@ function agentTools(target, options = {}) {
13150
13690
  for (const [action, a, isQuery] of defs) {
13151
13691
  const name = e.name + '_' + snake(action), comp = e.ivs[0].name, where = `agent.${isQuery ? 'queries' : 'actions'}.${action}`;
13152
13692
  if (isQuery && recs.has(name)) {
13153
- dev$5('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13693
+ dev$4('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13154
13694
  continue;
13155
13695
  }
13156
13696
  if (isQuery && typeof a?.run != 'function') {
13157
- dev$5('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13697
+ dev$4('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13158
13698
  continue;
13159
13699
  }
13160
13700
  const c = a.input ? toJsonSchema(a.input) : undefined;
@@ -13169,11 +13709,11 @@ function agentTools(target, options = {}) {
13169
13709
  if (c?.error) {
13170
13710
  tool.error = `SYG240 ${c.error}`;
13171
13711
  rec.off = 1;
13172
- dev$5('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13712
+ dev$4('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13173
13713
  continue;
13174
13714
  }
13175
13715
  if (c?.lossy)
13176
- dev$5('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13716
+ dev$4('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13177
13717
  const live = e.ivs.filter(iv => whenOk(iv, action, a.when));
13178
13718
  if (!live.length) {
13179
13719
  rec.off = 1;
@@ -13345,7 +13885,7 @@ function agentTools(target, options = {}) {
13345
13885
  return;
13346
13886
  const res = iv.def.view.resources;
13347
13887
  if (!res || !(u in res)) {
13348
- dev$5('SYG248', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until names '${u}', which is not one of ${iv.name}'s resources${res ? ` (${Object.keys(res).join(', ')})` : ''}; the call answers at once`);
13888
+ dev$4('SYG248', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until names '${u}', which is not one of ${iv.name}'s resources${res ? ` (${Object.keys(res).join(', ')})` : ''}; the call answers at once`);
13349
13889
  return;
13350
13890
  }
13351
13891
  return (s) => { const r = s?.[u]; return !r || (r.status != 'loading' && !r.refreshing); };
@@ -13363,7 +13903,7 @@ function agentTools(target, options = {}) {
13363
13903
  return !!test(iv.state);
13364
13904
  }
13365
13905
  catch (err) {
13366
- dev$5('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13906
+ dev$4('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13367
13907
  return true;
13368
13908
  }
13369
13909
  };
@@ -13389,11 +13929,11 @@ function agentTools(target, options = {}) {
13389
13929
  }
13390
13930
  catch (err) {
13391
13931
  const m = err?.message ?? err;
13392
- dev$5('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13932
+ dev$4('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13393
13933
  return { ok: false, error: `${name} failed: ${m}` };
13394
13934
  }
13395
13935
  if (result && typeof result.then == 'function') {
13396
- dev$5('SYG246', declOf(iv), 'promise' + query, iv.name, `agent.queries.${query}.run returned a promise: a query is a pure, synchronous function of the state`);
13936
+ dev$4('SYG246', declOf(iv), 'promise' + query, iv.name, `agent.queries.${query}.run returned a promise: a query is a pure, synchronous function of the state`);
13397
13937
  return { ok: false, error: `${name} is not available: it returned a promise` };
13398
13938
  }
13399
13939
  return { ok: true, result };
@@ -13754,28 +14294,28 @@ function withToolResults(message, results) {
13754
14294
  const SCOPE = '__sygnalScope';
13755
14295
  const tag$1 = (v, scope) => v && typeof v == 'object'
13756
14296
  ? Object.defineProperty(keepSender(v, { ...v }), SCOPE, { value: [scope, ...(v[SCOPE] || [])] }) : v;
13757
- const g$2 = globalThis;
13758
- const now = () => g$2.performance?.now?.() ?? Date.now();
14297
+ const g$1 = globalThis;
14298
+ const now = () => g$1.performance?.now?.() ?? Date.now();
13759
14299
  // internal (D254): the 120 Hz floor and the hidden-tab fallback
13760
14300
  const MIN_GAP = 15, FALLBACK = 100;
13761
14301
  /** rAF raced with a timer (a hidden tab never runs rAF), else a 16 ms timer; `f` gets the frame's time */
13762
14302
  const frame = (f) => {
13763
- if (typeof g$2.requestAnimationFrame != 'function')
14303
+ if (typeof g$1.requestAnimationFrame != 'function')
13764
14304
  return void setTimeout(() => f(now()), 16);
13765
14305
  let done = 0, a, t;
13766
14306
  const go = (ts) => {
13767
14307
  if (done++)
13768
14308
  return;
13769
- g$2.cancelAnimationFrame?.(a);
14309
+ g$1.cancelAnimationFrame?.(a);
13770
14310
  clearTimeout(t);
13771
14311
  f(typeof ts == 'number' ? ts : now());
13772
14312
  };
13773
- a = g$2.requestAnimationFrame(go);
14313
+ a = g$1.requestAnimationFrame(go);
13774
14314
  t = setTimeout(go, FALLBACK);
13775
14315
  };
13776
14316
  // dev-only diagnostics (SYG673, SYG677): the dev entry formats them (checks/chat.ts), so their
13777
14317
  // text costs apps nothing; without it they're silent
13778
- const dev$4 = (...a) => g$2.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
14318
+ const dev$3 = (...a) => g$1.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
13779
14319
  const FIX = "Send { messages, ok: 'DONE', error: 'FAILED' }";
13780
14320
  /** why a request can't be sent ('' when it can) */
13781
14321
  const problem = (r) => !Array.isArray(r.messages) ? 'no messages array'
@@ -13797,7 +14337,7 @@ function makeChatDriver(options = {}) {
13797
14337
  const { replies, reply } = makeReplies(sender => abortWhere(r => r.sender === sender));
13798
14338
  // SYG673 (dev): `why` is 'type' | 'delta' | 'name' | 'id'; the first five per driver
13799
14339
  const malformed = (req, ev, why) => { if (warned++ < 5)
13800
- dev$4('SYG673', req, ev, why); };
14340
+ dev$3('SYG673', req, ev, why); };
13801
14341
  const start = async (req, sender) => {
13802
14342
  const key = req.key ?? req.ok ?? req.error ?? 'default';
13803
14343
  if (req.latest !== false)
@@ -13990,7 +14530,7 @@ function makeChatDriver(options = {}) {
13990
14530
  return;
13991
14531
  const sender = senderOf(req);
13992
14532
  if (sender === undefined)
13993
- return void dev$4('SYG677', req);
14533
+ return void dev$3('SYG677', req);
13994
14534
  const { abort } = req;
13995
14535
  if (abort !== undefined && abort !== false) {
13996
14536
  // D469 (PLAN-9 H-2): a list of keys stops each (STOP: the reply and a card being composed)
@@ -15046,6 +15586,17 @@ const validMs = (v) => typeof v == 'number' && Number.isFinite(v) && v >= 0 && v
15046
15586
  // too, so its time is the clock's (Date may be left real by `toFake`), and its waits drive it.
15047
15587
  const fakeClock = () => setTimeout.clock;
15048
15588
  const clockNow = () => { const c = fakeClock(); return c ? c.now : Date.now(); };
15589
+ /** PLAN-11 Q-3: ms until the next pending fake timer (0: none, or real timers) */
15590
+ const nextFakeTimer = () => {
15591
+ const c = fakeClock();
15592
+ let due = Infinity;
15593
+ try {
15594
+ Object.values(c?.timers || {}).forEach((t) => { if (t && t.callAt > c.now && t.callAt < due)
15595
+ due = t.callAt; });
15596
+ }
15597
+ catch (_) { }
15598
+ return due < Infinity ? due - c.now : 0;
15599
+ };
15049
15600
  /**
15050
15601
  * Under fake timers, advance the clock timer by timer (nextAsync flushes promises around each
15051
15602
  * one) until `p` settles. Every pending wait has a timer (its timeout, a quiet-window tick), so
@@ -15236,6 +15787,7 @@ const headFake = () => {
15236
15787
  };
15237
15788
  let savedConfig;
15238
15789
  let savedStrict;
15790
+ let savedAudit;
15239
15791
  /*
15240
15792
  * PLAN-4 GS-2, mock DOM: could the element a command targets have this method? The core runs any
15241
15793
  * method of the element; the mock DOM has no elements, so: the documented commands, common
@@ -15346,7 +15898,7 @@ function fakeElementMethods(W) {
15346
15898
  Object.defineProperty(proto, name, own); });
15347
15899
  }
15348
15900
  function renderComponent(componentDef, options = {}) {
15349
- const { initialState, mockConfig = {}, drivers = {}, diagnostics, strict, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors } = options;
15901
+ const { initialState, mockConfig = {}, drivers = {}, diagnostics, strict, timingAudit, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors } = options;
15350
15902
  const { intent, model = {} } = componentDef;
15351
15903
  // E4: real DOM mode
15352
15904
  const real = dom == 'real';
@@ -15394,6 +15946,7 @@ function renderComponent(componentDef, options = {}) {
15394
15946
  if (!active++) {
15395
15947
  savedConfig = _getDiagnosticsConfig();
15396
15948
  savedStrict = core.strict;
15949
+ savedAudit = core.timingAudit;
15397
15950
  // G-051: forget the checks' report dedupe (once()), so a finding from an earlier test is
15398
15951
  // reported again. Only that set: resetChecks() would also drop inspect()'s records of
15399
15952
  // other live instances / apps.
@@ -15402,6 +15955,9 @@ function renderComponent(componentDef, options = {}) {
15402
15955
  configureDiagnostics({ mode: diagnostics || (prevMode == 'off' ? 'collect' : prevMode) });
15403
15956
  if (strict !== undefined)
15404
15957
  core.strict = strict;
15958
+ // 7.0 timing audit (SYG257, 'sygnal/diagnostics'): same bridge flag as configureTimingAudit()
15959
+ if (timingAudit !== undefined)
15960
+ core.timingAudit = timingAudit;
15405
15961
  // PLAN-4 GS-2: element commands run on the real DOM (jsdom gets the missing methods), and their
15406
15962
  // SYG640/SYG641 are reported without the dev entry too
15407
15963
  if (real && !domFakes)
@@ -15592,6 +16148,7 @@ function renderComponent(componentDef, options = {}) {
15592
16148
  if (!--active) {
15593
16149
  configureDiagnostics(savedConfig);
15594
16150
  core.strict = savedStrict;
16151
+ core.timingAudit = savedAudit;
15595
16152
  domFakes?.();
15596
16153
  domFakes = undefined;
15597
16154
  }
@@ -15637,14 +16194,22 @@ function renderComponent(componentDef, options = {}) {
15637
16194
  const lin$ = xs$1.create();
15638
16195
  llm = { in$: lin$, src: makeChatDriver({ transport: tr, _frame: (f) => { frames.push(f); } })(lin$) };
15639
16196
  }
16197
+ let memFetch;
15640
16198
  const http = makeFetchDriver({
15641
16199
  ...httpOptions,
16200
+ // PLAN-11 U-2: a progress request is answered like any other (t.respond / t.fail); its
16201
+ // pending entry keeps the driver's progress callback for t.progress
16202
+ _progress: (url, init, step) => {
16203
+ const r = memFetch(url, init);
16204
+ pending[pending.length - 1].step = step;
16205
+ return r;
16206
+ },
15642
16207
  // 5-3: focus / online come from t.focus() / t.online() only
15643
16208
  _on: (f) => { signals.add(f); return () => signals.delete(f); },
15644
16209
  _tap: (req, res, pf) => { tapped = [req, res, pf]; },
15645
16210
  // D472: a batch / split request (a large decide()) is one fetch here, answered as one
15646
16211
  _whole: true,
15647
- fetch: (url, init) => new Promise((resolve, reject) => {
16212
+ fetch: memFetch = (url, init) => new Promise((resolve, reject) => {
15648
16213
  const [req, res, pf] = tapped || [{ url }];
15649
16214
  tapped = undefined;
15650
16215
  // G-171(1): a resource fetch is listed as { url, ...request, resource: name }; 5-5: a
@@ -15678,6 +16243,7 @@ function renderComponent(componentDef, options = {}) {
15678
16243
  return {
15679
16244
  select: (sel) => xs$1.merge(hs.select(sel), own(false)(sel), sock.select(sel)),
15680
16245
  errors: (sel) => xs$1.merge(hs.errors(sel), own(true)(sel)),
16246
+ progress: (sel) => hs.progress(sel),
15681
16247
  subs, at, pending, in$, http, ws, llm,
15682
16248
  // 5-3: the driver's matcher / inspection (the dev checks' SYG632, t.cache)
15683
16249
  __matches: http.__matches, __inspect: http.__inspect,
@@ -16443,7 +17009,7 @@ function renderComponent(componentDef, options = {}) {
16443
17009
  return { match: cat, desc: catDesc, o };
16444
17010
  if (typeof tg == 'string') {
16445
17011
  // an ok/error action name, key, category, resource name or URL
16446
- return { match: r => { const v = r.value || {}; return cat(r) && [v.ok, v.error, v.key, v.category, r.res, v.url].includes(tg); }, desc: ` matching '${tg}'${catDesc}`, o };
17012
+ return { match: r => { const v = r.value || {}; return cat(r) && [v.ok, v.error, v.progress, v.key, v.category, r.res, v.url].includes(tg); }, desc: ` matching '${tg}'${catDesc}`, o };
16447
17013
  }
16448
17014
  if (typeof tg == 'function') {
16449
17015
  return { match: r => { try {
@@ -16700,6 +17266,27 @@ function renderComponent(componentDef, options = {}) {
16700
17266
  const err = typeof error == 'number' || resp ? Object.assign(new Error(`HTTP ${status}`), { status }) : x;
16701
17267
  return { error: err, category, request: e?.req, status: status ?? x?.status, body };
16702
17268
  });
17269
+ // PLAN-11 U-2: t.progress. One progress step of a pending request sent with `progress` (its
17270
+ // reply action, or progress() for `progress: true`); the request stays pending. Targets as
17271
+ // t.respond's; the phase defaults to 'upload' for a request with a body, else 'download'
17272
+ const progress = (name, step, opts) => {
17273
+ throwFailure();
17274
+ if (drivers[name])
17275
+ throw new Error(`[Sygnal] t.progress('${name}'): ${name} has a real driver (passed in drivers), so there is nothing to script`);
17276
+ if (!step || typeof step != 'object' || typeof step.loaded != 'number')
17277
+ throw new Error(`[Sygnal] t.progress('${name}', step): step is { loaded, total?, phase? } (got ${brief(step)})`);
17278
+ const tg = targetOf(opts);
17279
+ const what = `t.progress('${name}'${typeof opts == 'string' ? `, …, '${opts}'` : ''})`;
17280
+ const now = isReady && !inputs.length ? pick(name, tg) : undefined;
17281
+ if (now && !now.step)
17282
+ throw new Error(`[Sygnal] ${what}: the request ${brief(now.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
17283
+ return scripted(() => pick(name, tg), w => noPending(what, name, tg, w), (e) => {
17284
+ if (!e.step)
17285
+ return new Error(`[Sygnal] ${what}: the request ${brief(e.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
17286
+ const r = e.req || {};
17287
+ e.step(step.phase || (r.body !== undefined || r.json !== undefined || r.multipart !== undefined ? 'upload' : 'download'), step.loaded, step.total ?? 0);
17288
+ }, sendDue);
17289
+ };
16703
17290
  // PLAN-6 L-4: t.stream. Targets as t.respond's (an action name or key, a partial request, a
16704
17291
  // predicate, { request, nth }); `end: false` (the last argument, or in the target options)
16705
17292
  // leaves the stream open. Chunks become ChatEvents pushed into the in-memory stream; the fake's
@@ -17204,7 +17791,14 @@ function renderComponent(componentDef, options = {}) {
17204
17791
  let msg = `${name} timed out after ${timeoutMs}ms.`;
17205
17792
  const [why, pending] = explainNext(start);
17206
17793
  msg += why;
17207
- if (pending)
17794
+ if (inputs.length)
17795
+ msg += ` ${inputs.length} simulated input(s) still pending.`;
17796
+ // PLAN-11 Q-3: on the fake clock a state a timer (TIMER driver, periodic, delay) brings
17797
+ // later than the timeout can't arrive in time: name the next pending timer
17798
+ const later = pending ? 0 : nextFakeTimer();
17799
+ if (later)
17800
+ msg += ` A timer is still pending on the fake clock (it fires ${later}ms after this timeout): if the state comes from it, this wait ended first.`;
17801
+ if (pending || later)
17208
17802
  msg += ` Wait longer: t.${name}(pred, ms), or the renderComponent timeoutMs option (default for every wait).`;
17209
17803
  // G-065: a state from before the call matched; next() only looks at new ones
17210
17804
  for (let i = from - 1; i >= 0; i--) {
@@ -17404,6 +17998,7 @@ function renderComponent(componentDef, options = {}) {
17404
17998
  requests,
17405
17999
  respond,
17406
18000
  fail,
18001
+ progress,
17407
18002
  stream,
17408
18003
  connections,
17409
18004
  cache,
@@ -17980,7 +18575,7 @@ function jsonSchema(schema, options = {}) {
17980
18575
  * Side-effect free: an app that doesn't import it pays 0 B.
17981
18576
  */
17982
18577
  const NAME = 30, DESC = 500, PARAM = 150, OUT = 1500, SUMMARY = 300;
17983
- const g$1 = globalThis;
18578
+ const g = globalThis;
17984
18579
  /** cut to n characters with an ellipsis, never inside a surrogate pair */
17985
18580
  const cut = (s, n) => {
17986
18581
  if (s.length <= n)
@@ -17997,7 +18592,7 @@ const hash = (s) => {
17997
18592
  return (h % 1679616).toString(36).padStart(4, '0');
17998
18593
  };
17999
18594
  function experimentalExposeWebMcp(app, options = {}) {
18000
- const mc = options.modelContext ?? g$1.document?.modelContext ?? g$1.navigator?.modelContext;
18595
+ const mc = options.modelContext ?? g.document?.modelContext ?? g.navigator?.modelContext;
18001
18596
  const said = new Set();
18002
18597
  const dev = (code, key, message, severity = 'warn') => {
18003
18598
  if (said.has(code + key))
@@ -18200,7 +18795,7 @@ function experimentalExposeWebMcp(app, options = {}) {
18200
18795
  let n$2 = 0;
18201
18796
  /** the default confirmation: a native modal dialog outside the app's tree (Escape and "Deny" decline) */
18202
18797
  function dialog(info, setClose) {
18203
- const doc = g$1.document;
18798
+ const doc = g.document;
18204
18799
  if (!doc?.createElement)
18205
18800
  return Promise.resolve(false);
18206
18801
  return new Promise(resolve => {
@@ -18456,7 +19051,7 @@ const toolsOf$1 = (e) => e.o.agent === false ? null : e.tools || (e.tools = agen
18456
19051
  }),
18457
19052
  }));
18458
19053
  // dev diagnostics (SYG684, SYG685): no-ops with diagnostics off
18459
- const dev$3 = (code, message, fix) => { try {
19054
+ const dev$2 = (code, message, fix) => { try {
18460
19055
  report(code, { component: 'chat', message, fix, severity: 'warn' });
18461
19056
  }
18462
19057
  catch (e) {
@@ -18666,7 +19261,7 @@ function steps(c, o) {
18666
19261
  const openPart = (s, id, what, guard = (_p) => true) => {
18667
19262
  const p = openToolCalls(s).find(x => x.toolCallId === id && isClient(c, x) && guard(x));
18668
19263
  if (!p)
18669
- dev$3('SYG685', `${what} for the tool call '${id}', which is not waiting for an answer (answered, closed, or not a client tool call of the last message); it is ignored`, 'Answer only the calls in openToolCalls(state); a late answer after STOP, SEND or REGENERATE is dropped on purpose');
19264
+ dev$2('SYG685', `${what} for the tool call '${id}', which is not waiting for an answer (answered, closed, or not a client tool call of the last message); it is ignored`, 'Answer only the calls in openToolCalls(state); a late answer after STOP, SEND or REGENERATE is dropped on purpose');
18670
19265
  return p;
18671
19266
  };
18672
19267
  /** after a call closed or a card changed: the next queued card, then rule 3 */
@@ -18794,7 +19389,7 @@ function request(e, o, k, messages, fresh, c, host) {
18794
19389
  for (const n in c.client) {
18795
19390
  if (n in tools && !shadowed.has(n)) {
18796
19391
  shadowed.add(n);
18797
- dev$3('SYG684', `the client tool '${n}' has the name of an agent tool; the client tool is sent and its calls are the app's to answer`, 'Rename the client tool, or the agent declaration or action');
19392
+ dev$2('SYG684', `the client tool '${n}' has the name of an agent tool; the client tool is sent and its calls are the app's to answer`, 'Rename the client tool, or the agent declaration or action');
18798
19393
  }
18799
19394
  const x = c.client[n];
18800
19395
  tools[n] = c.ask && n == 'ask_user' ? c.kit.askTool(c.ask, host) : { description: x.description, inputSchema: schemaOf(x) };
@@ -18911,7 +19506,7 @@ const chat = (options = {}) => {
18911
19506
  ...(c.ask?.controls && DOM && { CONTROL: DOM.select('[data-compose-control]').events('click').map(controlOf$1).filter((x) => !!x) }),
18912
19507
  };
18913
19508
  },
18914
- model: {
19509
+ model: fromPrevious({
18915
19510
  PROMPT: (s, p) => typeof p == 'string' && p !== s.prompt ? { ...s, prompt: p } : ABORT,
18916
19511
  SEND: {
18917
19512
  STATE: (s, d) => sendStep(s, d) || ABORT,
@@ -19009,153 +19604,19 @@ const chat = (options = {}) => {
19009
19604
  ANSWER: { STATE: stateOf(S.answerCard), ...sinks(S.answerCard) },
19010
19605
  CONTROL: (s, d) => S.controlStep(s, d) || ABORT,
19011
19606
  }),
19012
- },
19607
+ }),
19013
19608
  })(options);
19014
19609
  // the `prompt` option is a selector, not the slice's start value (defineBehavior's option override)
19015
19610
  b.state = { ...b.state, prompt: '' };
19016
19611
  return linked(b);
19017
19612
  };
19018
19613
 
19019
- /*
19020
- * PLAN-6 L-2: what the HTTP transports share (side-effect free; each transport imports only what
19021
- * it uses).
19022
- * - post(): the fetch (the `fetch` option, 0-S5), the headers option (an object or a function of
19023
- * the request), HTTP errors as Errors with `status` and the provider's message, and SYG670.
19024
- * - sse(): a fetch Response body as SSE frames ({ event, data }), per the HTML spec's parsing
19025
- * rules: CRLF / LF, comments, multi-line data. The reader is cancelled when the consumer stops
19026
- * (abort, a thrown error), so the connection closes.
19027
- * - SYG670: in a browser, a request with an auth header (Authorization, x-api-key, api-key,
19028
- * x-goog-api-key) to a host that is neither local (localhost, 127.0.0.1, [::1], *.localhost)
19029
- * nor the page's own origin is refused unless the transport has `dangerouslyAllowBrowser: true`:
19030
- * the key would ship to every visitor. Same-origin requests are the app's own server (its
19031
- * proxy), so a session header there is fine.
19032
- */
19033
- const g = globalThis;
19034
- const AUTH = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
19035
- const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/;
19036
- /** the URL a transport posts to, or a SYG670 Error when it must not (see the header) */
19037
- function guard(url, headers, opts, name) {
19038
- if (opts.dangerouslyAllowBrowser || typeof g.document == 'undefined' || !Object.keys(headers).some(h => AUTH.test(h)))
19039
- return;
19040
- const here = g.location?.href;
19041
- let u;
19042
- try {
19043
- u = new URL(url, here);
19044
- }
19045
- catch (_) {
19046
- return;
19047
- }
19048
- if (LOCAL.test(u.hostname) || (here && u.origin == new URL(here).origin))
19049
- return;
19050
- const fix = `Call ${u.origin} from your server and point the transport at that route (an AI SDK route with uiMessageStream('/api/chat'), or your own proxy), so the key stays there. For a key the user typed in themselves, pass ${name}({ ..., dangerouslyAllowBrowser: true })`;
19051
- const msg = `${name}: refused to send an auth header from the browser to ${u.origin}: anyone who opens the page can read it`;
19052
- error('SYG670', undefined, msg, fix);
19053
- fail$2('SYG670', undefined, msg, fix);
19054
- }
19055
- /**
19056
- * POST JSON; a non-2xx response is an Error with `status`, `body` and the provider's message.
19057
- * The transport's `body` option and the request's `body` are merged over the built body (so they
19058
- * can override provider defaults such as `max_tokens`); `fixed` fields are merged last and can't be
19059
- * overridden (G-654: uiMessageStream's protocol fields, as the AI SDK's DefaultChatTransport)
19060
- */
19061
- async function post$1(url, body, req, signal, opts, name, fixed) {
19062
- const h = typeof opts.headers == 'function' ? await opts.headers(req) : opts.headers || {};
19063
- guard(url, h, opts, name);
19064
- const f = opts.fetch || ((u, i) => g.fetch(u, i));
19065
- const res = await f(url, { method: 'POST', headers: { 'content-type': 'application/json', ...h }, body: JSON.stringify({ ...body, ...opts.body, ...req.body, ...fixed }), signal });
19066
- if (!res.ok) {
19067
- let text = '', message = '';
19068
- try {
19069
- text = await res.text();
19070
- const j = JSON.parse(text);
19071
- message = j.error?.message ?? j.error ?? j.message ?? '';
19072
- }
19073
- catch (_) {
19074
- message || (message = text.slice(0, 200));
19075
- }
19076
- throw Object.assign(new Error(`${name}: HTTP ${res.status}${message ? ': ' + message : ''}`), { status: res.status, body: text });
19077
- }
19078
- if (!res.body)
19079
- throw new Error(`${name}: the response has no body`);
19080
- return res;
19081
- }
19082
- /** the SSE frames of a response body: `{ event, data }` (data: the joined data lines) */
19083
- async function* sse(res) {
19084
- const reader = res.body.getReader(), dec = new TextDecoder();
19085
- let buf = '', cr = '', done = false;
19086
- try {
19087
- for (;;) {
19088
- const r = await reader.read();
19089
- // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
19090
- let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
19091
- cr = !r.done && s.endsWith('\r') ? '\r' : '';
19092
- if (cr)
19093
- s = s.slice(0, -1);
19094
- buf += s.replace(/\r\n?/g, '\n');
19095
- done = r.done;
19096
- let i;
19097
- while ((i = buf.indexOf('\n\n')) >= 0) {
19098
- const frame = buf.slice(0, i);
19099
- buf = buf.slice(i + 2);
19100
- let event = 'message', data = [];
19101
- for (const line of frame.split('\n')) {
19102
- if (!line || line[0] == ':')
19103
- continue;
19104
- const c = line.indexOf(':'), field = c < 0 ? line : line.slice(0, c);
19105
- const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
19106
- if (field == 'event')
19107
- event = v;
19108
- else if (field == 'data')
19109
- data.push(v);
19110
- }
19111
- if (data.length)
19112
- yield { event, data: data.join('\n') };
19113
- }
19114
- if (done)
19115
- return;
19116
- }
19117
- }
19118
- finally {
19119
- if (!done)
19120
- reader.cancel().catch(() => { });
19121
- }
19122
- }
19123
- /** JSON.parse, or the text itself when it isn't JSON (a model's malformed tool arguments go to validation as is) */
19124
- const json = (s) => {
19125
- if (typeof s != 'string')
19126
- return s;
19127
- try {
19128
- return JSON.parse(s || '{}');
19129
- }
19130
- catch (_) {
19131
- return s;
19132
- }
19133
- };
19134
- /** a provider's usage as `{ inputTokens, outputTokens, totalTokens, reasoningTokens? }` (AI SDK names) */
19135
- const usageOf = (u) => u && {
19136
- inputTokens: u.input_tokens ?? u.prompt_tokens,
19137
- outputTokens: u.output_tokens ?? u.completion_tokens,
19138
- totalTokens: u.total_tokens,
19139
- ...((u.output_tokens_details ?? u.completion_tokens_details)?.reasoning_tokens != null && { reasoningTokens: (u.output_tokens_details ?? u.completion_tokens_details).reasoning_tokens }),
19140
- };
19141
- /** the parts of a message (a `content` string is one text part) */
19142
- const partsOf = (m) => m.parts ?? (typeof m.content == 'string' ? [{ type: 'text', text: m.content }] : []);
19143
- /** the tool name of a `tool-<name>` / `dynamic-tool` part, or '' */
19144
- const toolName = (p) => p.type == 'dynamic-tool' ? p.toolName : p.type.startsWith('tool-') ? p.type.slice(5) : '';
19145
- /** what a tool part sends back to the model, or undefined when it has no result yet */
19146
- const toolOutput = (p) => p.state == 'output-available' ? (typeof p.output == 'string' ? p.output : JSON.stringify(p.output ?? null))
19147
- : p.state == 'output-error' ? JSON.stringify({ error: p.errorText ?? 'error' })
19148
- : p.state == 'output-denied' ? JSON.stringify({ error: 'The user denied this tool call' })
19149
- : undefined;
19150
- // dev-only diagnostics (SYG675): the dev entry formats them (checks/chat.ts)
19151
- const dev$2 = (...a) => g.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
19152
-
19153
19614
  const EMPTY$1 = { type: 'object', properties: {} };
19154
19615
  function prepare(req, strict, seen, transport, dialect = 'openai', limits) {
19155
19616
  const sent = {};
19156
19617
  if (strict && typeof strict != 'function' && !seen.has('')) {
19157
19618
  seen.add('');
19158
- dev$2('SYG672', req, { transport });
19619
+ dev$8('SYG672', req, { transport });
19159
19620
  }
19160
19621
  const fn = typeof strict == 'function' ? strict : undefined;
19161
19622
  // what the request has left of `limits` (strictSchemas counts it down)
@@ -19168,7 +19629,7 @@ function prepare(req, strict, seen, transport, dialect = 'openai', limits) {
19168
19629
  return { schema: s.schema, strict: true };
19169
19630
  if (!seen.has(what)) {
19170
19631
  seen.add(what);
19171
- dev$2('SYG675', req, { tool: what, errors: s.errors });
19632
+ dev$8('SYG675', req, { tool: what, errors: s.errors });
19172
19633
  }
19173
19634
  return { schema, strict: false };
19174
19635
  };
@@ -19239,12 +19700,12 @@ function openResponses(options = {}) {
19239
19700
  c.done = 1;
19240
19701
  called++;
19241
19702
  const name = c.name ?? item?.name;
19242
- return { type: 'tool-call', id: c.call_id ?? item?.call_id ?? id, name, input: p.input(name, json(args ?? item?.arguments ?? c.args)) };
19703
+ return { type: 'tool-call', id: c.call_id ?? item?.call_id ?? id, name, input: p.input(name, json$1(args ?? item?.arguments ?? c.args)) };
19243
19704
  };
19244
19705
  for await (const { data } of sse(res)) {
19245
19706
  if (data == '[DONE]')
19246
19707
  break;
19247
- const e = json(data), t = e?.type;
19708
+ const e = json$1(data), t = e?.type;
19248
19709
  if (typeof t != 'string')
19249
19710
  continue;
19250
19711
  if (t == 'response.output_text.delta' || t == 'response.refusal.delta' || REASONING.test(t)) {
@@ -19336,13 +19797,13 @@ function chatCompletions(options = {}) {
19336
19797
  const done = function* () {
19337
19798
  for (const c of calls)
19338
19799
  if (c)
19339
- yield { type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json(c.args)) };
19800
+ yield { type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json$1(c.args)) };
19340
19801
  calls = [];
19341
19802
  };
19342
19803
  for await (const { data } of sse(res)) {
19343
19804
  if (data == '[DONE]')
19344
19805
  break;
19345
- const e = json(data);
19806
+ const e = json$1(data);
19346
19807
  if (!e || typeof e != 'object')
19347
19808
  continue;
19348
19809
  if (e.error)
@@ -19430,7 +19891,7 @@ function uiMessageStream(url, options = {}) {
19430
19891
  for await (const { data } of sse(res)) {
19431
19892
  if (data == '[DONE]')
19432
19893
  break;
19433
- const c = json(data), t = c?.type;
19894
+ const c = json$1(data), t = c?.type;
19434
19895
  if (typeof t != 'string')
19435
19896
  continue;
19436
19897
  if (ONE.test(t)) {
@@ -20161,7 +20622,7 @@ const commandBar = (options = {}) => {
20161
20622
  ...(deny && { DENY: on(DOM, deny) }),
20162
20623
  };
20163
20624
  },
20164
- model: {
20625
+ model: fromPrevious({
20165
20626
  INPUT: (s, v) => typeof v == 'string' && v !== s.text ? { ...s, text: v } : ABORT,
20166
20627
  RUN: {
20167
20628
  STATE: (s, d, _n, p, o, k) => runStep(s, d, E(p, k, o))?.s || ABORT,
@@ -20197,7 +20658,7 @@ const commandBar = (options = {}) => {
20197
20658
  // a command ran (or was refused): the result; the input is cleared when it was this command
20198
20659
  DONE: (s, d) => s.status != 'running' ? ABORT
20199
20660
  : { ...s, status: 'ready', command: null, pending: null, result: d, text: d?.ok && s.text.trim() === d.command ? '' : s.text },
20200
- },
20661
+ }),
20201
20662
  })(options);
20202
20663
  return linked(b);
20203
20664
  };
@@ -20297,7 +20758,7 @@ function anthropicMessages(options = {}) {
20297
20758
  return { inputTokens: i, outputTokens: o, totalTokens: i + o, ...(u.output_tokens_details?.thinking_tokens != null && { reasoningTokens: u.output_tokens_details.thinking_tokens }) };
20298
20759
  };
20299
20760
  for await (const { data } of sse(res)) {
20300
- const e = json(data), t = e?.type;
20761
+ const e = json$1(data), t = e?.type;
20301
20762
  if (typeof t != 'string')
20302
20763
  continue;
20303
20764
  if (t == 'message_start') {
@@ -20333,9 +20794,9 @@ function anthropicMessages(options = {}) {
20333
20794
  if (b?.type == 'thinking' && b.signature)
20334
20795
  yield { type: 'reasoning', delta: '', providerMetadata: { anthropic: { signature: b.signature } } };
20335
20796
  else if (b?.type == 'tool_use')
20336
- yield { type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json(b.json) : b.input ?? {}) };
20797
+ yield { type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json$1(b.json) : b.input ?? {}) };
20337
20798
  else if (b?.type == 'server_tool_use')
20338
- yield { type: 'tool-call', id: b.id, name: b.name, input: b.json ? json(b.json) : b.input ?? {}, executed: true, providerExecuted: true };
20799
+ yield { type: 'tool-call', id: b.id, name: b.name, input: b.json ? json$1(b.json) : b.input ?? {}, executed: true, providerExecuted: true };
20339
20800
  }
20340
20801
  else if (t == 'message_delta') {
20341
20802
  if (e.delta?.stop_reason)
@@ -20475,10 +20936,10 @@ function agui(url, options = {}) {
20475
20936
  if (!c || c.done)
20476
20937
  return;
20477
20938
  c.done = 1;
20478
- open.push({ type: 'tool-call', id, name: c.name, input: json(c.args || '{}') });
20939
+ open.push({ type: 'tool-call', id, name: c.name, input: json$1(c.args || '{}') });
20479
20940
  };
20480
20941
  for await (const { data } of sse(res)) {
20481
- const e = json(data), t = e?.type;
20942
+ const e = json$1(data), t = e?.type;
20482
20943
  if (typeof t != 'string')
20483
20944
  continue;
20484
20945
  if (t == 'TEXT_MESSAGE_START') {
@@ -20526,7 +20987,7 @@ function agui(url, options = {}) {
20526
20987
  else if (t == 'TOOL_CALL_RESULT') {
20527
20988
  end(e.toolCallId);
20528
20989
  yield* take(e.toolCallId);
20529
- yield { type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json(e.content) : e.content };
20990
+ yield { type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json$1(e.content) : e.content };
20530
20991
  }
20531
20992
  else if (t == 'STATE_SNAPSHOT' || t == 'STATE_DELTA') {
20532
20993
  state = t == 'STATE_SNAPSHOT' ? e.snapshot : applyPatch(state, e.delta);
@@ -21727,7 +22188,7 @@ function composeBehavior(catalog, options = {}) {
21727
22188
  .map(({ key, name, value }) => ({ key, name, value })) }),
21728
22189
  };
21729
22190
  },
21730
- model: {
22191
+ model: fromPrevious({
21731
22192
  PROMPT: (s, p) => typeof p == 'string' && p !== s.prompt ? { ...s, prompt: p } : ABORT,
21732
22193
  COMPOSE: { HOST: host(start), [S]: sink(start) },
21733
22194
  REPLY: {
@@ -21769,7 +22230,7 @@ function composeBehavior(catalog, options = {}) {
21769
22230
  CONTROL: (s, c) => c && typeof c.key == 'string' && typeof c.name == 'string'
21770
22231
  ? { ...s, ui: { ...s.ui, [c.key]: { ...s.ui?.[c.key], [c.name]: c.value } } } : ABORT,
21771
22232
  DONE: () => ABORT,
21772
- },
22233
+ }),
21773
22234
  })(rest);
21774
22235
  }
21775
22236
 
@@ -25834,7 +26295,7 @@ const compose = /*#__PURE__*/ Object.assign((catalog, options) => composeBehavio
25834
26295
  * is gone); set: the array with that element replaced (the state unchanged when it is gone).
25835
26296
  * Side-effect free.
25836
26297
  */
25837
- const byId = (field, id, idKey = 'id') => ({
26298
+ const byId = (field, id, idKey = 'id') => Object.defineProperty({
25838
26299
  get: (s) => (Array.isArray(s?.[field]) ? s[field].find((x) => x?.[idKey] === id) : undefined),
25839
26300
  set: (s, v) => {
25840
26301
  const a = s?.[field];
@@ -25842,7 +26303,7 @@ const byId = (field, id, idKey = 'id') => ({
25842
26303
  return s;
25843
26304
  return { ...s, [field]: a.map((x) => (x?.[idKey] === id ? v : x)) };
25844
26305
  },
25845
- });
26306
+ }, BINDING, { value: [field, id, idKey] });
25846
26307
 
25847
26308
  /**
25848
26309
  * ESM ports of xstream's `flattenConcurrently` and `flattenSequentially` extras