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.esm.js CHANGED
@@ -63,6 +63,8 @@ const NOT_SINK = /^(__|(props|children|dispose|commands)\$$|(CHILD|PARENT|EFFECT
63
63
  */
64
64
  const abortReason = {};
65
65
  const abort = (reason) => (abortReason.r = reason, ABORT);
66
+ /** Private semantic binding identity, shared by built-in helpers and the runtime. */
67
+ const BINDING = /*#__PURE__*/ Symbol.for('sygnal.binding');
66
68
 
67
69
  /**
68
70
  * PLAN-4.6 (D162): `defineComponent(opts)` builds a component from an options object, for code
@@ -702,6 +704,9 @@ const DEV_CODE_SEVERITY = {
702
704
  SYG128: 'error',
703
705
  // PLAN-4 4-G2: static only (sygnal-check); listed so the entry knows its title
704
706
  SYG129: 'warn',
707
+ // PLAN-10 X-1: static only (sygnal-check)
708
+ SYG170: 'warn',
709
+ SYG171: 'warn',
705
710
  SYG221: 'error',
706
711
  // PLAN-4 GS-4 (state.ts)
707
712
  SYG222: 'warn',
@@ -750,6 +755,8 @@ const DEV_CODE_SEVERITY = {
750
755
  SYG634: 'info',
751
756
  // PLAN-3 5-5 (fetch.ts)
752
757
  SYG635: 'warn',
758
+ // PLAN-11 U-1 (fetch.ts)
759
+ SYG636: 'warn',
753
760
  // PLAN-4 GS-2 (checks/elementCommands.ts, through the core bridge; renderComponent too)
754
761
  SYG640: 'warn',
755
762
  SYG641: 'error',
@@ -866,6 +873,12 @@ const DEV_CODE_SEVERITY = {
866
873
  // PLAN-9 X-2: static only (sygnal-check); listed so the entry knows their titles
867
874
  SYG253: 'warn',
868
875
  SYG254: 'info',
876
+ // PLAN-12: static only; proven after-state output guard contradiction.
877
+ SYG256: 'warn',
878
+ // 7.0: the opt-in timing audit (checks/next.ts; configureTimingAudit)
879
+ SYG257: 'info',
880
+ // PLAN-12: static only for legacy (6.x) binding contracts. Sygnal 7 remounts known targets.
881
+ SYG454: 'warn',
869
882
  };
870
883
  function docsUrlFor(code) {
871
884
  return `${DOCS_BASE_URL}/reference/errors#${code.toLowerCase()}`;
@@ -1261,6 +1274,8 @@ function normalize$1(view, src) {
1261
1274
  const list = [];
1262
1275
  for (const sink in e)
1263
1276
  list.push([sink, e[sink]]), sinks.add(sink);
1277
+ // 7.0: STATE first, so the other sinks get the state it committed
1278
+ list.sort((a, b) => +(a[0] != 'STATE') - +(b[0] != 'STATE'));
1264
1279
  handlers.set(key, list);
1265
1280
  }
1266
1281
  const c = normCalc(name, src.calculated, src.initialState);
@@ -1682,6 +1697,8 @@ class View {
1682
1697
  get state() { return this.i.cell.get(); }
1683
1698
  get props() { return this.i.props; }
1684
1699
  get context() { return this.i.context(); }
1700
+ /** D530: the context keys the last render's view read (null: none rendered, or no context) */
1701
+ get reads() { return this.i.keys; }
1685
1702
  get isRoot() { return !this.i.parent; }
1686
1703
  get kind() { return this.i.kind; }
1687
1704
  /** false on a hidden Switchable page and below it */
@@ -1850,7 +1867,7 @@ function handle(inst, type, data, cause, src) {
1850
1867
  return;
1851
1868
  const def = inst.def;
1852
1869
  const pre = inst.cell.get();
1853
- let props, outs;
1870
+ let props, afterProps, post = pre, outs;
1854
1871
  const next = (t, d, ms = 10, effect) => {
1855
1872
  if (typeof ms !== 'number')
1856
1873
  fail$2('SYG215', inst, `next() delay in '${type}' must be a number`, "Use next('ACTION', data, ms)");
@@ -1865,35 +1882,28 @@ function handle(inst, type, data, cause, src) {
1865
1882
  };
1866
1883
  for (const [sink, h0] of hs) {
1867
1884
  const h = H.wrapHandler ? H.wrapHandler(viewOf(inst), type, sink, h0) || h0 : h0;
1868
- if (sink == 'EFFECT') {
1869
- if (typeof h != 'function')
1870
- continue;
1871
- const failed = (e) => app.caught(inst, 'SYG214', `EFFECT handler '${type}' threw`, e, 'effect', type);
1872
- try {
1873
- const p = { ...(props || (props = inst_props(inst, pre))), signal: (inst.ac || (inst.ac = globalThis.AbortController && new AbortController()))?.signal };
1874
- const r = h(pre, data, (t, d, ms) => next(t, d, ms, true), p);
1875
- if (r?.then)
1876
- r.then(null, failed);
1877
- else if (r !== undefined && !isAbort(r))
1878
- warn('SYG219', inst, `EFFECT handler '${type}' returned a value, which is ignored`, 'Use a STATE or driver sink');
1879
- }
1880
- catch (e) {
1881
- failed(e);
1882
- }
1883
- continue;
1884
- }
1885
+ const effect = sink == 'EFFECT';
1885
1886
  let v;
1886
1887
  if (typeof h == 'function') {
1887
1888
  try {
1888
- v = h(pre, data, next, props || (props = inst_props(inst, pre)));
1889
+ const p = sink == 'STATE' ? props || (props = inst_props(inst, pre, pre)) : afterProps || (afterProps = inst_props(inst, post, pre));
1890
+ 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);
1891
+ if (effect) {
1892
+ if (v?.then)
1893
+ v.then(null, (e) => handlerFailed(inst, type, sink, e));
1894
+ else if (v !== undefined && !isAbort(v))
1895
+ warn('SYG219', inst, `EFFECT handler '${type}' returned a value, which is ignored`, 'Use a STATE or driver sink');
1896
+ }
1889
1897
  }
1890
1898
  catch (e) {
1891
- app.caught(inst, 'SYG216', `Reducer for '${type}' threw; ${sink == 'STATE' ? 'state unchanged' : 'nothing sent'}`, e, 'reducer', type);
1899
+ handlerFailed(inst, type, sink, e);
1892
1900
  continue;
1893
1901
  }
1894
1902
  }
1895
1903
  else
1896
1904
  v = h === undefined || h === true ? data : h;
1905
+ if (effect)
1906
+ continue;
1897
1907
  if (sink == 'STATE' && (isAbort(v) || v === pre)) {
1898
1908
  // D205: the DOM event being dispatched (window.event) is the one this action came from
1899
1909
  if (/^(input|change)$/.test(globalThis.event?.type))
@@ -1906,6 +1916,11 @@ function handle(inst, type, data, cause, src) {
1906
1916
  H.onReducer?.(viewOf(inst), type, pre, v);
1907
1917
  dbg(inst, () => `<${type}> State reducer added`);
1908
1918
  inst.cell.set(v);
1919
+ // a STATE that removed the component's own state (a deleted record it is bound to): the
1920
+ // outputs get its last state, as a Collection item's do
1921
+ post = inst.cell.get();
1922
+ if (post === undefined)
1923
+ post = pre;
1909
1924
  // GS-12: this app's DOM driver patches inside a View Transition (makeViewTransitionDOMDriver
1910
1925
  // reads the flag on its IsolateModule); includes?.: a non-array static lists nothing (G-228).
1911
1926
  // Only for a state that changed structurally: today an equal one renders nothing (its
@@ -1954,9 +1969,14 @@ function handle(inst, type, data, cause, src) {
1954
1969
  send$1(inst, outs[i], outs[i + 1]);
1955
1970
  }
1956
1971
  }
1957
- /** the handlers' 4th argument: the props, children, slots, context, uid and the pre-action state */
1958
- function inst_props(inst, pre) {
1959
- return { ...inst.props, children: inst.children, slots: inst.slots, context: inst.context(), uid: inst.uid, state: pre };
1972
+ /** Keep the same failure boundary for sync handlers and rejected EFFECT promises. */
1973
+ function handlerFailed(inst, type, sink, e) {
1974
+ const effect = sink == 'EFFECT';
1975
+ 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);
1976
+ }
1977
+ /** the handlers' 4th argument: the props, children, slots, context, uid, the state and the pre-action state */
1978
+ function inst_props(inst, state, previousState) {
1979
+ return { ...inst.props, children: inst.children, slots: inst.slots, context: inst.context(), uid: inst.uid, state, previousState };
1960
1980
  }
1961
1981
  /**
1962
1982
  * A sink value of an instance: stamped with its sender (EVENTS; object values for a reply-capable
@@ -2067,9 +2087,11 @@ function isEqualNamespace(a, b) {
2067
2087
  return true;
2068
2088
  }
2069
2089
  // G-261: a DOM change Sygnal makes outside a patch (a Transition's leave, a Portal mounted late)
2070
- // re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root)
2090
+ // re-emits the DOM source of the app whose root contains `el` (makeDOMDriver listens on its root).
2091
+ // The event comes from the element's own window: jsdom refuses an `Event` from another realm (a
2092
+ // Transition's leave timer that fires after a jsdom test environment was torn down)
2071
2093
  const POKE = 'sygnal-dom';
2072
- const pokeDOM = (el) => el?.dispatchEvent(new Event(POKE, { bubbles: true }));
2094
+ const pokeDOM = (el) => el?.dispatchEvent(new (el.ownerDocument?.defaultView?.Event || Event)(POKE, { bubbles: true }));
2073
2095
  const obj = (o) => o !== null && typeof o == 'object' && !Array.isArray(o);
2074
2096
  /**
2075
2097
  * P46-P: two vnodes' data are the same for the DOM modules: the same keys, each value identical
@@ -2188,6 +2210,19 @@ const flat = (v) => {
2188
2210
  * - dispose(): DISPOSE, onDispose, dispose$, the EFFECT signal aborted, the subtree, then the
2189
2211
  * streams (the scoped _remove swap: no xstream stop timers, D165/Q18).
2190
2212
  */
2213
+ // v7: a known binding target joins key/id/path in the lifetime identity. Fresh equivalent
2214
+ // byId helpers retain identity; opaque lenses need an explicit key for a new target.
2215
+ function bindingChanged(a, b) {
2216
+ if (a === b)
2217
+ return false;
2218
+ const x = a?.[BINDING], y = b?.[BINDING];
2219
+ return x && y ? !x.every((v, i) => Object.is(v, y[i]))
2220
+ : !!(x || y || typeof a == 'string' || typeof b == 'string' || a === undefined || b === undefined);
2221
+ }
2222
+ function bindingState(k) {
2223
+ const c = k.base || k.cell || k.arr;
2224
+ return c && (c.has?.() ?? c.get() !== undefined) ? (k.cell || k.arr).get() : undefined;
2225
+ }
2191
2226
  const ERR_FIX$1 = 'See the attached error';
2192
2227
  // 4-I G-557: a fragment root (a Collection's): each of its top-level elements is marked.
2193
2228
  // 4-K G-572: a text root (no element; an attribute would land on a Text node) gets `ready: false`
@@ -2230,8 +2265,8 @@ const idOf$1 = (n, path) => {
2230
2265
  return `${n.sel}::${(s && s.replace(/"/g, '')) || path}`;
2231
2266
  };
2232
2267
  /**
2233
- * a component vnode's props, without `resetState` (D174: read at creation, like `state`, never a
2234
- * prop of the child)
2268
+ * a component vnode's props, without `resetState` (D174: read at creation, never a
2269
+ * prop of the child). Known `state` target changes start a new lifetime in v7.
2235
2270
  */
2236
2271
  function propsOf(p) {
2237
2272
  if (!p)
@@ -2295,10 +2330,14 @@ const EMPTY_CTX = Object.freeze({});
2295
2330
  /** a view read every key (spread, Object.keys): any context change re-renders it */
2296
2331
  const ALL = '\u0000all';
2297
2332
  let IDS = 0;
2298
- /** a child that failed to instantiate: renders the owner's error fallback (SYG408) */
2333
+ /**
2334
+ * a child that failed to instantiate: renders the owner's error fallback (SYG408). It keeps its
2335
+ * props, so only a changed binding target makes another attempt (not every owner render)
2336
+ */
2299
2337
  class Failed {
2300
- constructor(outv) {
2338
+ constructor(outv, props) {
2301
2339
  this.outv = outv;
2340
+ this.props = props;
2302
2341
  this.lr = true;
2303
2342
  this.ready = true;
2304
2343
  this.def = null;
@@ -2306,7 +2345,7 @@ class Failed {
2306
2345
  this.shown = true;
2307
2346
  }
2308
2347
  render() { return this.outv; }
2309
- setProps() { }
2348
+ setProps(p) { this.props = p; }
2310
2349
  dispose() { }
2311
2350
  }
2312
2351
  class Inst {
@@ -2314,6 +2353,7 @@ class Inst {
2314
2353
  this.app = app;
2315
2354
  this.def = def;
2316
2355
  this.parent = parent;
2356
+ this.base = base;
2317
2357
  this.dom = dom;
2318
2358
  this.scope = scope;
2319
2359
  this.kind = kind;
@@ -2480,10 +2520,32 @@ class Inst {
2480
2520
  }
2481
2521
  /** called after each action and before each render pass: STATE.stream emits a new state */
2482
2522
  notify() {
2483
- const v = this.cell.raw();
2484
- if (v !== this.stLast && v !== undefined) {
2485
- this.stLast = v;
2486
- this.st$.shamefullySendNext(v);
2523
+ // A completed action can remove and reinsert a target before the next render. Owners
2524
+ // observe each transition, including hidden pages; the current action finishes first.
2525
+ // only the bound children (a string / byId binding), not every child
2526
+ if (this.bound)
2527
+ for (const k of this.bound) {
2528
+ if (k.disposed) {
2529
+ this.bound.delete(k);
2530
+ continue;
2531
+ }
2532
+ const previous = k.bs, state = k.bs = bindingState(k);
2533
+ if (previous !== undefined && state === undefined) {
2534
+ // Cleanup reads its last owned snapshot. A dying path must not recreate the
2535
+ // deleted target; the fresh lifetime receives a new writable cell at reconciliation.
2536
+ const cell = k.cell || k.arr;
2537
+ cell.get = () => previous;
2538
+ cell.set = () => { };
2539
+ k.dispose();
2540
+ this.forced = true;
2541
+ }
2542
+ }
2543
+ if (this.st$) {
2544
+ const v = this.cell.raw();
2545
+ if (v !== this.stLast && v !== undefined) {
2546
+ this.stLast = v;
2547
+ this.st$.shamefullySendNext(v);
2548
+ }
2487
2549
  }
2488
2550
  }
2489
2551
  dispose$() {
@@ -2732,7 +2794,9 @@ class Inst {
2732
2794
  for (const r of resolvers)
2733
2795
  view = r(view, this) || view;
2734
2796
  let k = this.kids.get(id);
2735
- if (k && view && k.def && k.def.view !== view) {
2797
+ // Collection and VirtualCollection share an array cell; Switchable binds `state`.
2798
+ const binding = host && sel != 'switchable' ? 'from' : 'state';
2799
+ if (k && (k.disposed || (view && k.def && k.def.view !== view) || bindingChanged(k.props?.[binding], props[binding]))) {
2736
2800
  k.dispose();
2737
2801
  this.kids.delete(id);
2738
2802
  k = undefined;
@@ -2740,7 +2804,12 @@ class Inst {
2740
2804
  if (k)
2741
2805
  k.setProps(props, children, n, id);
2742
2806
  else
2743
- this.kids.set(id, host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState));
2807
+ this.kids.set(id, k = host ? this.host(host, props, children, id, n) : this.child(view, props, children, id, !!data.props?.resetState));
2808
+ if (typeof props[binding] == 'string' || props[binding]?.[BINDING]) {
2809
+ k.bs = bindingState(k);
2810
+ (this.bound || (this.bound = new Set())).add(k);
2811
+ this.app.watchers.add(this);
2812
+ }
2744
2813
  return;
2745
2814
  }
2746
2815
  if (posts[sel] && !(ps || (ps = [])).includes(sel))
@@ -2765,11 +2834,11 @@ class Inst {
2765
2834
  return make(this, props, children, id, n);
2766
2835
  }
2767
2836
  catch (err) {
2768
- return this.failed(err);
2837
+ return this.failed(err, props);
2769
2838
  }
2770
2839
  }
2771
2840
  /** the owner's error fallback for a child that failed to instantiate (SYG408) */
2772
- failed(err) {
2841
+ failed(err, props) {
2773
2842
  const e = err instanceof Error ? err : new Error(String(err));
2774
2843
  let out = errorDiv$1(this.def.name);
2775
2844
  if (this.def.onError) {
@@ -2781,7 +2850,7 @@ class Inst {
2781
2850
  }
2782
2851
  }
2783
2852
  this.app.caught(this, 'SYG408', 'Sub-component threw; rendering the error fallback', e, 'instantiate');
2784
- return new Failed(out);
2853
+ return new Failed(out, props);
2785
2854
  }
2786
2855
  /** a tag child: state="key" | state={lens} | none (the parent's state) | isolatedState (D174: `resetState`) */
2787
2856
  child(view, props, children, id, reset = false) {
@@ -2800,7 +2869,7 @@ class Inst {
2800
2869
  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);
2801
2870
  }
2802
2871
  catch (err) {
2803
- return this.failed(err);
2872
+ return this.failed(err, props);
2804
2873
  }
2805
2874
  }
2806
2875
  inject(n, path) {
@@ -2863,8 +2932,7 @@ class Inst {
2863
2932
  tearDown(() => { for (const s of subs)
2864
2933
  s.unsubscribe(); }, app.dq, this);
2865
2934
  this.subs = [];
2866
- if (this.st$)
2867
- app.watchers.delete(this);
2935
+ app.watchers.delete(this);
2868
2936
  this.hub = null;
2869
2937
  }
2870
2938
  }
@@ -2963,7 +3031,7 @@ function arrayCell(owner, from) {
2963
3031
  if (typeof from == 'string')
2964
3032
  return keyCell(oc, from, owner.def.calcNames?.has(pathOf$1(from)[0]) && name, undefined, 'Collection');
2965
3033
  if (isObj$2(from) && typeof from.get == 'function') {
2966
- let lp = {}, lv;
3034
+ let lp = {}, lv, raw;
2967
3035
  return {
2968
3036
  get() {
2969
3037
  const p = oc.get();
@@ -2979,7 +3047,7 @@ function arrayCell(owner, from) {
2979
3047
  owner.app.appError(owner, e, 'view');
2980
3048
  return lv;
2981
3049
  }
2982
- lv = g;
3050
+ raw = lv = g;
2983
3051
  if (!Array.isArray(lv)) {
2984
3052
  warn('SYG401', name, "Collection 'from' getter returned a non-array; it renders nothing", 'Return an array from get()', lv);
2985
3053
  lv = NONE$1;
@@ -2987,6 +3055,7 @@ function arrayCell(owner, from) {
2987
3055
  }
2988
3056
  return lv;
2989
3057
  },
3058
+ has() { this.get(); return raw !== undefined; },
2990
3059
  set(v) {
2991
3060
  if (typeof from.set != 'function')
2992
3061
  return;
@@ -5348,8 +5417,11 @@ const validateWith = async (schema, value) => {
5348
5417
  * declarations in src/index.d.ts and docs/guide/drivers.md. Summary:
5349
5418
  *
5350
5419
  * - Request (sink value): `{ url, ok?, error?, key?, category?, method?, headers?, query?, json?,
5351
- * body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified with a JSON
5352
- * content-type; method defaults to POST with a body/json, else GET. `query` is appended as a
5420
+ * multipart?, body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified
5421
+ * with a JSON content-type; PLAN-11 U-5: `multipart` (an object) becomes a FormData body (a
5422
+ * File / Blob a file part, an array repeats the name, null / undefined skipped, an object JSON,
5423
+ * anything else a string), the content-type left to the browser (its boundary); method defaults
5424
+ * to POST with a body/json/multipart, else GET. `query` is appended as a
5353
5425
  * query string (null/undefined values skipped, arrays repeat the key, before any #fragment).
5354
5426
  * Other fetch options (credentials, mode, cache, ...) go under `init` (R4-5); other top-level
5355
5427
  * keys are the app's own: not sent, but returned on `request`.
@@ -5426,6 +5498,15 @@ const validateWith = async (schema, value) => {
5426
5498
  * the request itself) into requests sent again in parallel and combined, at most 3 deep. One
5427
5499
  * failure fails the request once (`error.status` / `error.body`). No cache entry sharing, no
5428
5500
  * per-part retry. The test fake (`_whole`) sends and answers it as one request. decide() uses it.
5501
+ * - PLAN-11 U-1 (D555): `progress: 'ACTION'` replies to the sender with each progress step,
5502
+ * `progress: true` (or a string with no sender) sends it down `progress(category?)`:
5503
+ * `{ phase: 'upload' | 'download', loaded, total, fraction, category, request }` (`total` /
5504
+ * `fraction` null when the length is unknown). Such a request goes over XMLHttpRequest (the
5505
+ * only transport with upload progress in every engine; S-1), turned back into a Response so
5506
+ * parsing, status, retry and timeouts are the same. Steps are throttled to one per 50 ms per
5507
+ * phase, keeping the first and the last. Not for resources or `batch`; never shares a fetch.
5508
+ * No XMLHttpRequest (Node, a service worker): plain fetch, no steps. The transport is
5509
+ * `options.XMLHttpRequest`, else the global one, read at request time.
5429
5510
  * - dispose / sink completion aborts everything in flight.
5430
5511
  * - `fetch` is read at request time (options.fetch, else globalThis.fetch), so test stubs work.
5431
5512
  * - PLAN-3 5-1 (H-9): renderComponent's HTTP fake runs this driver over an in-memory fetch;
@@ -5519,6 +5600,81 @@ const mergeHeaders = (...all) => {
5519
5600
  h.forEach((v, k) => { out[k] = v; });
5520
5601
  return out;
5521
5602
  };
5603
+ // PLAN-11 U-1: statuses whose Response must have a null body
5604
+ const NULL_BODY = [101, 103, 204, 205, 304];
5605
+ const abortError$1 = () => typeof DOMException == 'function'
5606
+ ? new DOMException('The operation was aborted.', 'AbortError')
5607
+ : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
5608
+ /**
5609
+ * PLAN-11 U-1 (S-1): a fetch-shaped call over XMLHttpRequest that reports progress:
5610
+ * `on(phase, loaded, total)` (total 0 when unknown), at most every 50 ms per phase, always the
5611
+ * first and the final step. Resolves with a real Response (blob body, parsed headers); network
5612
+ * errors and timeouts reject with a TypeError, an abort with an AbortError, like fetch.
5613
+ */
5614
+ const xhrFetch = (X, url, init, on) => new Promise((res, rej) => {
5615
+ const x = new X();
5616
+ const last = {};
5617
+ const step = (phase, loaded, total, final) => {
5618
+ const l = last[phase], now = Date.now();
5619
+ if (l && (l.loaded === loaded || (!final && now - l.at < 50 && !(total && loaded >= total))))
5620
+ return;
5621
+ last[phase] = { loaded, at: now };
5622
+ on(phase, loaded, total);
5623
+ };
5624
+ const ev = (phase) => (e) => step(phase, e.loaded, e.lengthComputable ? e.total : 0);
5625
+ x.open(init.method || 'GET', url);
5626
+ x.responseType = 'blob';
5627
+ const h = init.headers || {};
5628
+ Object.keys(h).forEach(k => x.setRequestHeader(k, h[k]));
5629
+ if (init.credentials == 'include')
5630
+ x.withCredentials = true;
5631
+ const sig = init.signal;
5632
+ if (sig) {
5633
+ if (sig.aborted)
5634
+ return rej(abortError$1());
5635
+ sig.addEventListener?.('abort', () => x.abort(), { once: true });
5636
+ }
5637
+ if (init.body != null && x.upload) {
5638
+ x.upload.onprogress = ev('upload');
5639
+ x.upload.onload = (e) => step('upload', e.loaded, e.lengthComputable ? e.total : e.loaded, 1);
5640
+ }
5641
+ x.onprogress = ev('download');
5642
+ x.onload = () => {
5643
+ const size = x.response?.size ?? last.download?.loaded ?? 0;
5644
+ step('download', size, size, 1);
5645
+ const headers = typeof Headers == 'function' ? new Headers() : undefined;
5646
+ String(x.getAllResponseHeaders?.() || '').trim().split(/[\r\n]+/).forEach((line) => {
5647
+ const i = line.indexOf(':');
5648
+ if (i > 0)
5649
+ try {
5650
+ headers?.append(line.slice(0, i).trim(), line.slice(i + 1).trim());
5651
+ }
5652
+ catch (_) { }
5653
+ });
5654
+ try {
5655
+ res(new Response(NULL_BODY.includes(x.status) ? null : x.response, { status: x.status, statusText: x.statusText, headers }));
5656
+ }
5657
+ catch (e) {
5658
+ rej(e);
5659
+ }
5660
+ };
5661
+ x.onerror = x.ontimeout = () => rej(new TypeError(`Network request failed: ${url}`));
5662
+ x.onabort = () => rej(abortError$1());
5663
+ x.send(init.body ?? null);
5664
+ });
5665
+ /** PLAN-11 U-5: a request's `multipart` object as FormData (see the header) */
5666
+ const toFormData = (parts) => {
5667
+ const fd = new FormData();
5668
+ Object.keys(parts || {}).forEach(k => [].concat(parts[k]).forEach((v) => {
5669
+ if (v == null)
5670
+ return;
5671
+ if (typeof Blob != 'undefined' && v instanceof Blob)
5672
+ fd.append(k, v, v.name ?? 'blob');
5673
+ else
5674
+ fd.append(k, typeof v == 'object' ? JSON.stringify(v) : String(v));
5675
+ }));
5676
+ return fd;
5677
+ };
5522
5678
  const IDEMPOTENT$1 = /^(GET|HEAD)$/i;
5523
5679
  // D80: an invalidate value matches a request by tag, URL prefix ('/…') or predicate
5524
5680
  const hit = (x, q) => {
@@ -5543,6 +5699,22 @@ function makeFetchDriver(options = {}) {
5543
5699
  // q (the request), timer, f (the fetch it waits on), ok / ko (its outcomes) }
5544
5700
  const inflight = new Map();
5545
5701
  const subs = new Set();
5702
+ // PLAN-11 U-1: progress(category?) listeners (not held while nobody listens)
5703
+ const psubs = new Set();
5704
+ const emitProgress = (v) => psubs.forEach(s => {
5705
+ let m = false;
5706
+ try {
5707
+ m = matches(s, v);
5708
+ }
5709
+ catch (_) { }
5710
+ if (m)
5711
+ try {
5712
+ s.l.next(v);
5713
+ }
5714
+ catch (e) {
5715
+ console.error('[Sygnal] makeFetchDriver: a listener threw', e);
5716
+ }
5717
+ });
5546
5718
  // R4-4: replies held until the first select() listener, failures until the first errors()
5547
5719
  // listener (or the next macrotask, when they're logged if nothing listens)
5548
5720
  const started = [false, false];
@@ -5605,6 +5777,10 @@ function makeFetchDriver(options = {}) {
5605
5777
  return {
5606
5778
  select: make(false),
5607
5779
  errors: make(true),
5780
+ progress: (sel) => {
5781
+ let sub;
5782
+ return xs$2.create({ start: l => { psubs.add((sub = { l, sel, ns })); }, stop: () => { psubs.delete(sub); } });
5783
+ },
5608
5784
  isolateSource: (_, scope) => source(ns.concat(scope)),
5609
5785
  isolateSink: (sink$, scope) => sink$.map((req) => tagRequest(req, scope)),
5610
5786
  // PLAN-4.6: the same per value (the core scopes a request without a stream per instance)
@@ -5724,7 +5900,7 @@ function makeFetchDriver(options = {}) {
5724
5900
  req = { url: req };
5725
5901
  if (!req || typeof req != 'object' || disposed || !allowed$1(req, 'makeFetchDriver'))
5726
5902
  return;
5727
- const { url, category, method, headers, query, json, body, latest, timeoutMs, parse, abort, init, ok, error, key, resources, refresh, invalidates, updates, retry, validate, prefetch } = req;
5903
+ 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;
5728
5904
  if ('invalidate' in req)
5729
5905
  return inval(req.invalidate);
5730
5906
  if (prefetch)
@@ -5776,7 +5952,9 @@ function makeFetchDriver(options = {}) {
5776
5952
  }
5777
5953
  const scope = scopeKey(req);
5778
5954
  // reply actions: keyed per (sender, key ?? ok ?? error); a resource per (sender, '\0' + name)
5779
- const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error) ? key ?? ok ?? error : undefined;
5955
+ // PLAN-11 U-1: a progress action is a reply action too (abort on dispose, its own lane)
5956
+ const pr = !pf && rn === undefined && !req.batch && progress;
5957
+ const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error || typeof pr == 'string') ? key ?? ok ?? error ?? pr : undefined;
5780
5958
  if (sender !== undefined && (typeof abort == 'string' || (abort && key !== undefined)))
5781
5959
  return cancel(r => r.sender === sender && r.rk === (key ?? abort));
5782
5960
  // per (scope, category): another component's requests are never cancelled
@@ -5786,7 +5964,7 @@ function makeFetchDriver(options = {}) {
5786
5964
  cancel(rk === undefined ? r => r.rk === undefined && r.scope === scope && r.category === category : r => r.sender === sender && r.rk === rk);
5787
5965
  const base = options.init;
5788
5966
  // a request's URL and fetch() init (the batch's sub-requests inherit the request's fields)
5789
- const prep = ({ url, method, headers, query, json, body, init: own }) => {
5967
+ const prep = ({ url, method, headers, query, json, multipart, body, init: own }) => {
5790
5968
  const h = mergeHeaders(options.headers, base?.headers, own?.headers, headers);
5791
5969
  let b = body ?? own?.body;
5792
5970
  if (json !== undefined) {
@@ -5794,6 +5972,12 @@ function makeFetchDriver(options = {}) {
5794
5972
  if (!Object.keys(h).some(k => k.toLowerCase() == 'content-type'))
5795
5973
  h['content-type'] = 'application/json';
5796
5974
  }
5975
+ else if (multipart !== undefined) {
5976
+ b = toFormData(multipart);
5977
+ // the browser writes multipart/form-data with its boundary; a content-type set here would break it
5978
+ Object.keys(h).forEach(k => { if (k.toLowerCase() == 'content-type')
5979
+ delete h[k]; });
5980
+ }
5797
5981
  return [withQuery((options.baseUrl || '') + (url ?? ''), query), { ...pickInit(base), ...pickInit(own), method: method || own?.method || base?.method || (b !== undefined ? 'POST' : 'GET'), headers: h, body: b }];
5798
5982
  };
5799
5983
  const [href, fetchInit] = prep(req);
@@ -5802,7 +5986,8 @@ function makeFetchDriver(options = {}) {
5802
5986
  // parts, at most 3 deep), their parsed bodies merged by `combine` into one reply. The test
5803
5987
  // fake (_whole) answers such a request as one
5804
5988
  const { batch, split, combine = (x) => x } = options._whole ? {} : req;
5805
- const k = C?.key(req, rn !== undefined || pf);
5989
+ // PLAN-11 U-1: a progress request never shares a fetch (each sender gets its own steps)
5990
+ const k = pr ? undefined : C?.key(req, rn !== undefined || pf);
5806
5991
  const id = ++seq;
5807
5992
  const r = { category, scope, sender, rk };
5808
5993
  inflight.set(id, r);
@@ -5850,7 +6035,19 @@ function makeFetchDriver(options = {}) {
5850
6035
  const parser = parserOf(parse || options.parse);
5851
6036
  // the global fetch is called as a method: a detached window.fetch throws "Illegal invocation"
5852
6037
  const g = globalThis;
5853
- const doFetch = options.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6038
+ let doFetch = options.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6039
+ // PLAN-11 U-1: a progress request over XMLHttpRequest (renderComponent's fake: `_progress`)
6040
+ const X = options.XMLHttpRequest ?? g.XMLHttpRequest;
6041
+ if (pr && (options._progress || typeof X == 'function')) {
6042
+ const step = (phase, loaded, total) => {
6043
+ if (!inflight.has(id) || disposed)
6044
+ return;
6045
+ const t = total > 0 ? total : null;
6046
+ const v = { phase, loaded, total: t, fraction: t ? Math.min(1, loaded / t) : null, category, request: req };
6047
+ typeof pr == 'string' && sender !== undefined ? reply(sender, pr, v) : emitProgress(v);
6048
+ };
6049
+ doFetch = options._progress ? (u, i) => options._progress(u, i, step, req) : (u, i) => xhrFetch(X, u, i, step);
6050
+ }
5854
6051
  // the outcome, to every request waiting on this fetch (the failure once, after the last attempt)
5855
6052
  const end = (e, x, status) => {
5856
6053
  done(F);
@@ -6012,9 +6209,10 @@ function queryCache(o = {}) {
6012
6209
  const key = (q, res) => {
6013
6210
  if (typeof q == 'string')
6014
6211
  q = { url: q };
6015
- const { method, json, body, parse, query } = q;
6016
- const m = (method || (json !== undefined || body !== undefined ? 'POST' : 'GET')).toUpperCase();
6017
- const b = json !== undefined ? JSON.stringify(json) : body ?? '';
6212
+ const { method, json, body, parse, query, multipart } = q;
6213
+ const m = (method || (json !== undefined || body !== undefined || multipart !== undefined ? 'POST' : 'GET')).toUpperCase();
6214
+ // PLAN-11 U-5: a multipart request is never cached (its body isn't a string)
6215
+ const b = json !== undefined ? JSON.stringify(json) : multipart !== undefined ? multipart : body ?? '';
6018
6216
  if (q.cache === false || typeof b != 'string' || typeof parse == 'function' || parse == 'response' ||
6019
6217
  !(q.cache || q.staleTime != null || (res && IDEMPOTENT.test(m))))
6020
6218
  return;
@@ -6083,6 +6281,272 @@ function queryCache(o = {}) {
6083
6281
  return C;
6084
6282
  }
6085
6283
 
6284
+ /*
6285
+ * PLAN-6 L-2: what the HTTP transports share (side-effect free; each transport imports only what
6286
+ * it uses).
6287
+ * - post(): the fetch (the `fetch` option, 0-S5), the headers option (an object or a function of
6288
+ * the request), HTTP errors as Errors with `status` and the provider's message, and SYG670.
6289
+ * - sse(): a fetch Response body as SSE frames ({ event, data }), per the HTML spec's parsing
6290
+ * rules: CRLF / LF, comments, multi-line data. The reader is cancelled when the consumer stops
6291
+ * (abort, a thrown error), so the connection closes.
6292
+ * - SYG670: in a browser, a request with an auth header (Authorization, x-api-key, api-key,
6293
+ * x-goog-api-key) to a host that is neither local (localhost, 127.0.0.1, [::1], *.localhost)
6294
+ * nor the page's own origin is refused unless the transport has `dangerouslyAllowBrowser: true`:
6295
+ * the key would ship to every visitor. Same-origin requests are the app's own server (its
6296
+ * proxy), so a session header there is fine.
6297
+ */
6298
+ const g$6 = globalThis;
6299
+ const AUTH = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
6300
+ const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/;
6301
+ /** the URL a transport posts to, or a SYG670 Error when it must not (see the header) */
6302
+ function guard(url, headers, opts, name) {
6303
+ if (opts.dangerouslyAllowBrowser || typeof g$6.document == 'undefined' || !Object.keys(headers).some(h => AUTH.test(h)))
6304
+ return;
6305
+ const here = g$6.location?.href;
6306
+ let u;
6307
+ try {
6308
+ u = new URL(url, here);
6309
+ }
6310
+ catch (_) {
6311
+ return;
6312
+ }
6313
+ if (LOCAL.test(u.hostname) || (here && u.origin == new URL(here).origin))
6314
+ return;
6315
+ 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 })`;
6316
+ const msg = `${name}: refused to send an auth header from the browser to ${u.origin}: anyone who opens the page can read it`;
6317
+ error('SYG670', undefined, msg, fix);
6318
+ fail$2('SYG670', undefined, msg, fix);
6319
+ }
6320
+ /**
6321
+ * POST JSON; a non-2xx response is an Error with `status`, `body` and the provider's message.
6322
+ * The transport's `body` option and the request's `body` are merged over the built body (so they
6323
+ * can override provider defaults such as `max_tokens`); `fixed` fields are merged last and can't be
6324
+ * overridden (G-654: uiMessageStream's protocol fields, as the AI SDK's DefaultChatTransport)
6325
+ */
6326
+ async function post$1(url, body, req, signal, opts, name, fixed) {
6327
+ const h = typeof opts.headers == 'function' ? await opts.headers(req) : opts.headers || {};
6328
+ guard(url, h, opts, name);
6329
+ const f = opts.fetch || ((u, i) => g$6.fetch(u, i));
6330
+ const res = await f(url, { method: 'POST', headers: { 'content-type': 'application/json', ...h }, body: JSON.stringify({ ...body, ...opts.body, ...req.body, ...fixed }), signal });
6331
+ if (!res.ok) {
6332
+ let text = '', message = '';
6333
+ try {
6334
+ text = await res.text();
6335
+ const j = JSON.parse(text);
6336
+ message = j.error?.message ?? j.error ?? j.message ?? '';
6337
+ }
6338
+ catch (_) {
6339
+ message || (message = text.slice(0, 200));
6340
+ }
6341
+ throw Object.assign(new Error(`${name}: HTTP ${res.status}${message ? ': ' + message : ''}`), { status: res.status, body: text });
6342
+ }
6343
+ if (!res.body)
6344
+ throw new Error(`${name}: the response has no body`);
6345
+ return res;
6346
+ }
6347
+ /**
6348
+ * the SSE frames of a response body: `{ event, data }` (data: the joined data lines). PLAN-11
6349
+ * E-1: `meta` (optional) follows the stream's `id` (the last event id, kept across frames) and
6350
+ * `retry` (ms) fields, as EventSource does, for a reconnect
6351
+ */
6352
+ async function* sse(res, meta) {
6353
+ const reader = res.body.getReader(), dec = new TextDecoder();
6354
+ let buf = '', cr = '', done = false;
6355
+ try {
6356
+ for (;;) {
6357
+ const r = await reader.read();
6358
+ // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
6359
+ let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
6360
+ cr = !r.done && s.endsWith('\r') ? '\r' : '';
6361
+ if (cr)
6362
+ s = s.slice(0, -1);
6363
+ buf += s.replace(/\r\n?/g, '\n');
6364
+ done = r.done;
6365
+ let i;
6366
+ while ((i = buf.indexOf('\n\n')) >= 0) {
6367
+ const frame = buf.slice(0, i);
6368
+ buf = buf.slice(i + 2);
6369
+ let event = 'message', data = [];
6370
+ for (const line of frame.split('\n')) {
6371
+ if (!line || line[0] == ':')
6372
+ continue;
6373
+ const c = line.indexOf(':'), field = c < 0 ? line : line.slice(0, c);
6374
+ const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
6375
+ if (field == 'event')
6376
+ event = v;
6377
+ else if (field == 'data')
6378
+ data.push(v);
6379
+ else if (meta && field == 'id' && !v.includes('\0'))
6380
+ meta.id = v;
6381
+ else if (meta && field == 'retry' && /^\d+$/.test(v))
6382
+ meta.retry = +v;
6383
+ }
6384
+ if (data.length)
6385
+ yield { event, data: data.join('\n') };
6386
+ }
6387
+ if (done)
6388
+ return;
6389
+ }
6390
+ }
6391
+ finally {
6392
+ if (!done)
6393
+ reader.cancel().catch(() => { });
6394
+ }
6395
+ }
6396
+ /** JSON.parse, or the text itself when it isn't JSON (a model's malformed tool arguments go to validation as is) */
6397
+ const json$1 = (s) => {
6398
+ if (typeof s != 'string')
6399
+ return s;
6400
+ try {
6401
+ return JSON.parse(s || '{}');
6402
+ }
6403
+ catch (_) {
6404
+ return s;
6405
+ }
6406
+ };
6407
+ /** a provider's usage as `{ inputTokens, outputTokens, totalTokens, reasoningTokens? }` (AI SDK names) */
6408
+ const usageOf = (u) => u && {
6409
+ inputTokens: u.input_tokens ?? u.prompt_tokens,
6410
+ outputTokens: u.output_tokens ?? u.completion_tokens,
6411
+ totalTokens: u.total_tokens,
6412
+ ...((u.output_tokens_details ?? u.completion_tokens_details)?.reasoning_tokens != null && { reasoningTokens: (u.output_tokens_details ?? u.completion_tokens_details).reasoning_tokens }),
6413
+ };
6414
+ /** the parts of a message (a `content` string is one text part) */
6415
+ const partsOf = (m) => m.parts ?? (typeof m.content == 'string' ? [{ type: 'text', text: m.content }] : []);
6416
+ /** the tool name of a `tool-<name>` / `dynamic-tool` part, or '' */
6417
+ const toolName = (p) => p.type == 'dynamic-tool' ? p.toolName : p.type.startsWith('tool-') ? p.type.slice(5) : '';
6418
+ /** what a tool part sends back to the model, or undefined when it has no result yet */
6419
+ const toolOutput = (p) => p.state == 'output-available' ? (typeof p.output == 'string' ? p.output : JSON.stringify(p.output ?? null))
6420
+ : p.state == 'output-error' ? JSON.stringify({ error: p.errorText ?? 'error' })
6421
+ : p.state == 'output-denied' ? JSON.stringify({ error: 'The user denied this tool call' })
6422
+ : undefined;
6423
+ // dev-only diagnostics (SYG675): the dev entry formats them (checks/chat.ts)
6424
+ const dev$8 = (...a) => g$6.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
6425
+
6426
+ /*
6427
+ * PLAN-11 E-1: an EventSource over fetch, for server-sent events that need request headers
6428
+ * (`Authorization`), which the native EventSource can't send. makeSocketDriver uses it for an
6429
+ * `sse` connection with `headers`. It behaves as EventSource does where the driver relies on it:
6430
+ *
6431
+ * - connects on construction (a microtask later, so handlers can be set first); `readyState`
6432
+ * CONNECTING (0) → OPEN (1) on a 2xx `text/event-stream` response;
6433
+ * - `onmessage` and `addEventListener(type)` get `{ type, data, lastEventId }`;
6434
+ * - the stream ending or a network error: `error`, readyState CONNECTING, a reconnect after
6435
+ * `retry` ms (the stream's `retry:` field, default 3000) with `Last-Event-ID`;
6436
+ * - a non-2xx status or another content type: `error` with readyState CLOSED (no retry; the
6437
+ * driver's reconnect policy decides);
6438
+ * - `close()` aborts the request and stops retrying.
6439
+ */
6440
+ const NAMES = ['CONNECTING', 'OPEN', 'CLOSED'];
6441
+ class FetchEventSource {
6442
+ constructor(url, init = {}) {
6443
+ this.readyState = 0;
6444
+ this.onopen = null;
6445
+ this.onmessage = null;
6446
+ this.onerror = null;
6447
+ this.ls = new Map();
6448
+ this.meta = { id: '', retry: 3000 };
6449
+ this.url = url;
6450
+ this.withCredentials = !!init.withCredentials;
6451
+ this.init = init;
6452
+ queueMicrotask(() => this.connect());
6453
+ }
6454
+ addEventListener(type, f) {
6455
+ let s = this.ls.get(type);
6456
+ if (!s)
6457
+ this.ls.set(type, (s = new Set()));
6458
+ s.add(f);
6459
+ }
6460
+ removeEventListener(type, f) { this.ls.get(type)?.delete(f); }
6461
+ close() {
6462
+ this.readyState = 2;
6463
+ clearTimeout(this.timer);
6464
+ try {
6465
+ this.ctl?.abort();
6466
+ }
6467
+ catch (_) { }
6468
+ }
6469
+ emit(type, ev) {
6470
+ if (type == 'message')
6471
+ this.onmessage?.(ev);
6472
+ else if (type == 'open')
6473
+ this.onopen?.(ev);
6474
+ else if (type == 'error')
6475
+ this.onerror?.(ev);
6476
+ this.ls.get(type)?.forEach(f => { try {
6477
+ f(ev);
6478
+ }
6479
+ catch (e) {
6480
+ console.error(e);
6481
+ } });
6482
+ }
6483
+ async connect() {
6484
+ if (this.readyState == 2)
6485
+ return;
6486
+ const g = globalThis;
6487
+ const f = this.init.fetch || (typeof g.fetch == 'function' && ((u, i) => g.fetch(u, i)));
6488
+ if (typeof f != 'function')
6489
+ return this.fail(new Error('fetch is not available in this environment'));
6490
+ const ctl = this.ctl = typeof AbortController == 'function' ? new AbortController() : {};
6491
+ const headers = { accept: 'text/event-stream', ...this.init.headers };
6492
+ if (this.meta.id)
6493
+ headers['last-event-id'] = this.meta.id;
6494
+ let res;
6495
+ try {
6496
+ res = await f(this.url, { headers, credentials: this.withCredentials ? 'include' : 'same-origin', cache: 'no-store', signal: ctl.signal });
6497
+ }
6498
+ catch (error) {
6499
+ return this.retry(error);
6500
+ }
6501
+ if (this.readyState == 2 || this.ctl !== ctl)
6502
+ return;
6503
+ const type = res.headers?.get?.('content-type') || '';
6504
+ if (!(res.status >= 200 && res.status < 300) || !/^text\/event-stream/i.test(type) || !res.body) {
6505
+ try {
6506
+ res.body?.cancel?.();
6507
+ }
6508
+ catch (_) { }
6509
+ return this.fail(Object.assign(new Error(`SSE ${res.status}${type ? ' ' + type : ''}: ${this.url}`), { status: res.status }));
6510
+ }
6511
+ this.readyState = 1;
6512
+ this.emit('open', { type: 'open' });
6513
+ try {
6514
+ for await (const fr of sse(res, this.meta)) {
6515
+ if (this.readyState == 2 || this.ctl !== ctl)
6516
+ return;
6517
+ this.emit(fr.event, { type: fr.event, data: fr.data, lastEventId: this.meta.id });
6518
+ }
6519
+ }
6520
+ catch (error) {
6521
+ if (this.readyState == 2 || this.ctl !== ctl)
6522
+ return;
6523
+ return this.retry(error);
6524
+ }
6525
+ if (this.readyState != 2 && this.ctl === ctl)
6526
+ this.retry();
6527
+ }
6528
+ /** a drop: error, CONNECTING, try again after `retry` ms (unless the error handler closed it) */
6529
+ retry(error) {
6530
+ if (this.readyState == 2)
6531
+ return;
6532
+ this.readyState = 0;
6533
+ this.emit('error', { type: 'error', error });
6534
+ if (this.readyState == 2)
6535
+ return;
6536
+ this.timer = setTimeout(() => this.connect(), this.meta.retry);
6537
+ }
6538
+ /** a failed connection: error, CLOSED */
6539
+ fail(error) {
6540
+ this.readyState = 2;
6541
+ this.emit('error', { type: 'error', error });
6542
+ }
6543
+ /** the state's name (debugging) */
6544
+ get state() { return NAMES[this.readyState]; }
6545
+ }
6546
+ FetchEventSource.CONNECTING = 0;
6547
+ FetchEventSource.OPEN = 1;
6548
+ FetchEventSource.CLOSED = 2;
6549
+
6086
6550
  /*
6087
6551
  * makeSocketDriver(options?) (PLAN-3 §1.3): WebSocket and server-sent events, declared per
6088
6552
  * component instance and answered with reply actions (./replies.ts). Docs on the declarations
@@ -6091,7 +6555,7 @@ function queryCache(o = {}) {
6091
6555
  * - Sink values:
6092
6556
  * - `{ connections: { [name]: spec | falsy } }`: the sender's whole set of connections. Diffed
6093
6557
  * per (sender, name): a new name opens, a removed or falsy one closes, a changed URL /
6094
- * protocols / withCredentials / share closes and reopens, anything else (action names,
6558
+ * protocols / withCredentials / headers / share closes and reopens, anything else (action names,
6095
6559
  * reconnect) only rebinds.
6096
6560
  * - `{ to: name, json? | text? | binary? }`: send on the sender's connection `name` (json is
6097
6561
  * stringified). Queued while (re)connecting, bounded (`queueLimit`, oldest dropped). A send
@@ -6116,6 +6580,10 @@ function queryCache(o = {}) {
6116
6580
  * - A disposed sender's connections close (and their retries stop); app dispose closes all.
6117
6581
  * - No WebSocket / EventSource (SSR): nothing opens, nothing is reported.
6118
6582
  * - `WebSocket` / `EventSource` are read at connect time: options.WebSocket ?? globalThis.WebSocket.
6583
+ * - PLAN-11 E-1: an `sse` connection with `headers` (data: a token in it comes from state, and a
6584
+ * changed header reconnects) is read with FetchEventSource (fetch, `options.fetch` if given),
6585
+ * since EventSource can't send headers. An `options.EventSource` (a polyfill, the test fake)
6586
+ * still wins and gets the headers in its init.
6119
6587
  */
6120
6588
  const SCOPE$1 = '__sygnalScope';
6121
6589
  const scopeOf = (v) => v[SCOPE$1] || [];
@@ -6215,12 +6683,13 @@ function makeSocketDriver(options = {}) {
6215
6683
  };
6216
6684
  const connect = (t) => {
6217
6685
  t.timer = 0;
6218
- const C = t.sse ? options.EventSource ?? g.EventSource : options.WebSocket ?? g.WebSocket;
6686
+ const hdr = t.sse && t.spec.headers && typeof t.spec.headers == 'object' ? t.spec.headers : undefined;
6687
+ const C = t.sse ? options.EventSource ?? (hdr && typeof g.fetch == 'function' ? FetchEventSource : g.EventSource) : options.WebSocket ?? g.WebSocket;
6219
6688
  if (typeof C != 'function')
6220
6689
  return;
6221
6690
  let s;
6222
6691
  try {
6223
- s = t.sse ? new C(t.url, { withCredentials: !!t.spec.withCredentials }) : new C(t.url, t.spec.protocols);
6692
+ s = t.sse ? new C(t.url, { withCredentials: !!t.spec.withCredentials, ...(hdr && { headers: hdr, fetch: options.fetch }) }) : new C(t.url, t.spec.protocols);
6224
6693
  }
6225
6694
  catch (error) {
6226
6695
  // a URL the constructor rejects never opens: no retry
@@ -6324,7 +6793,7 @@ function makeSocketDriver(options = {}) {
6324
6793
  const sse = typeof spec.sse == 'string';
6325
6794
  const url = resolve(sse ? spec.sse : spec.socket, sse);
6326
6795
  // the connection's identity: a change reconnects, anything else only rebinds
6327
- const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials : 's' + JSON.stringify(spec.protocols)) + url;
6796
+ const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials + (spec.headers ? JSON.stringify(spec.headers) : '') : 's' + JSON.stringify(spec.protocols)) + url;
6328
6797
  if (old && old.key === key) {
6329
6798
  old.spec = spec;
6330
6799
  if (old.t)
@@ -6401,7 +6870,7 @@ const dec = (s) => { try {
6401
6870
  catch (_) {
6402
6871
  return s;
6403
6872
  } };
6404
- const g$6 = globalThis;
6873
+ const g$5 = globalThis;
6405
6874
  const PARAM$1 = /:(\w+)/g;
6406
6875
  // G-571: synchronous navigations (redirects) before a microtask, after which each waits a task
6407
6876
  const HOPS = 32;
@@ -6409,7 +6878,7 @@ const HOPS = 32;
6409
6878
  const paramsOf$2 = (pat) => (pat.match(PARAM$1) || []).map(p => p.slice(1));
6410
6879
  // the core's coded error() (published on the diagnostics bridge): no import, so the router
6411
6880
  // costs nothing more standalone; prints in production too (like SYG611)
6412
- const bad$1 = (v, msg, fix) => (g$6.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
6881
+ const bad$1 = (v, msg, fix) => (g$5.__SYGNAL_DIAGNOSTICS__?.error || console.error)('SYG620', v.__emitterName, `router: ${msg}; not navigated`, fix, v);
6413
6882
  function makeRouter(options = {}) {
6414
6883
  const { mode, prefetch, navigate } = options, routes = options.routes || {};
6415
6884
  const base = (options.base || '').replace(/\/$/, '');
@@ -6418,7 +6887,7 @@ function makeRouter(options = {}) {
6418
6887
  const doScroll = options.scroll ?? !navigate;
6419
6888
  const href = (name, params = {}, query, hash) => {
6420
6889
  // dev entry only (SYG130: unknown name, missing or extra params)
6421
- g$6.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
6890
+ g$5.__SYGNAL_DIAGNOSTICS__?.routerHref?.(routes, name, params);
6422
6891
  const pat = routes[name];
6423
6892
  const path = pat == null || pat == '*' ? '/' : pat.replace(PARAM$1, (_, k) => enc(params[k] ?? ''));
6424
6893
  const q = query && new URLSearchParams(Object.entries(query).filter(([, v]) => v != null)) + '';
@@ -6528,7 +6997,7 @@ function makeRouter(options = {}) {
6528
6997
  if (!hops++)
6529
6998
  queueMicrotask(() => { hops = 0; });
6530
6999
  if (hops > HOPS) {
6531
- if (g$6.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++)
7000
+ if (g$5.__SYGNAL_DIAGNOSTICS__?.hooks?.on?.() && !warned++)
6532
7001
  console.warn(`[Sygnal] router: more than ${HOPS} navigations in one flush (a redirect loop?); each next one waits a task (reported once)`, r.path);
6533
7002
  setTimeout(send);
6534
7003
  }
@@ -8070,7 +8539,7 @@ function makeHeadDriver(options = {}) {
8070
8539
  * - An invalid spec (non-positive `every`, negative `after`, no action) doesn't start; the dev
8071
8540
  * entry reports SYG422 through the `timerSpec` diagnostics hook.
8072
8541
  */
8073
- const g$5 = globalThis;
8542
+ const g$4 = globalThis;
8074
8543
  const now$1 = () => Date.now();
8075
8544
  const valid = (s) => s.frame
8076
8545
  ? typeof s.frame == 'string'
@@ -8080,7 +8549,7 @@ const valid = (s) => s.frame
8080
8549
  // changes) the timer clears the pending one.
8081
8550
  const start$1 = (s, send, e) => {
8082
8551
  let id, n = 0, last;
8083
- const t0 = now$1(), every = s.every, raf = s.frame && g$5.requestAnimationFrame;
8552
+ const t0 = now$1(), every = s.every, raf = s.frame && g$4.requestAnimationFrame;
8084
8553
  const later = (ms) => id = raf ? requestAnimationFrame(tick) : setTimeout(tick, ms);
8085
8554
  // a frame's `ts` is requestAnimationFrame's timestamp (the fallback passes none: the clock)
8086
8555
  const tick = (ts = now$1()) => {
@@ -8125,7 +8594,7 @@ const timerDriver = (runners) => (sink$) => {
8125
8594
  if (e.ok = valid(s))
8126
8595
  e.x = start$1(s, (type, data) => reply(id, type, data), e);
8127
8596
  else
8128
- g$5.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
8597
+ g$4.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
8129
8598
  }
8130
8599
  if (!d)
8131
8600
  runners.delete(id);
@@ -8183,10 +8652,10 @@ const makeTimerDriver = () => timerDriver(new Map());
8183
8652
  * an intersection / resize declaration with nothing to observe (no DOM source bound). A
8184
8653
  * component declaring `browser` with no driver is SYG643, as timers.
8185
8654
  */
8186
- const g$4 = globalThis;
8655
+ const g$3 = globalThis;
8187
8656
  const noop = () => { };
8188
8657
  const KINDS = ['intersection', 'resize', 'media', 'storage', 'visibility', 'online', 'geolocation'];
8189
- const diag = (...a) => g$4.__SYGNAL_DIAGNOSTICS__?.browserSource?.(...a);
8658
+ const diag = (...a) => g$3.__SYGNAL_DIAGNOSTICS__?.browserSource?.(...a);
8190
8659
  // the driver names in use (the definition hook looks each up in a declaring instance's sources)
8191
8660
  const names$1 = new Set();
8192
8661
  let hooked;
@@ -8272,16 +8741,16 @@ const track = (c, kind, target, Obs, opts, data) => {
8272
8741
  };
8273
8742
  /** Element visibility: `{ intersection: '.cover' | true, action, threshold?, rootMargin? }`, data `{ visible, ratio, index, dataset }` */
8274
8743
  const intersectionSource = { d: {
8275
- intersection: (s, c) => track(c, 'i', s.intersection, g$4.IntersectionObserver, { threshold: s.threshold, rootMargin: s.rootMargin }, (e) => ({ visible: e.isIntersecting, ratio: e.intersectionRatio })),
8744
+ intersection: (s, c) => track(c, 'i', s.intersection, g$3.IntersectionObserver, { threshold: s.threshold, rootMargin: s.rootMargin }, (e) => ({ visible: e.isIntersecting, ratio: e.intersectionRatio })),
8276
8745
  } };
8277
8746
  /** Element size: `{ resize: '.chart' | true, action }`, data `{ width, height, index, dataset }` (the content box) */
8278
8747
  const resizeSource = { d: {
8279
- resize: (s, c) => track(c, 'r', s.resize, g$4.ResizeObserver, undefined, (e) => ({ width: e.contentRect.width, height: e.contentRect.height })),
8748
+ resize: (s, c) => track(c, 'r', s.resize, g$3.ResizeObserver, undefined, (e) => ({ width: e.contentRect.width, height: e.contentRect.height })),
8280
8749
  } };
8281
8750
  /** A media query: `{ media: '(prefers-color-scheme: dark)', action }`, data `{ matches, media }` */
8282
8751
  const mediaSource = { d: {
8283
8752
  media: (s, c) => {
8284
- const m = g$4.matchMedia?.(s.media);
8753
+ const m = g$3.matchMedia?.(s.media);
8285
8754
  if (!m)
8286
8755
  return;
8287
8756
  const f = () => c.send({ matches: m.matches, media: s.media });
@@ -8290,7 +8759,7 @@ const mediaSource = { d: {
8290
8759
  return () => m.removeEventListener('change', f);
8291
8760
  },
8292
8761
  } };
8293
- const area = (s) => g$4[s.area == 'session' ? 'sessionStorage' : 'localStorage'];
8762
+ const area = (s) => g$3[s.area == 'session' ? 'sessionStorage' : 'localStorage'];
8294
8763
  const failed$1 = (e) => ({ name: e?.name, message: e?.message });
8295
8764
  /**
8296
8765
  * A storage key (read and observed, other tabs' writes included): `{ storage: 'theme', action,
@@ -8319,8 +8788,8 @@ const storageSource = {
8319
8788
  };
8320
8789
  f();
8321
8790
  watching.add(f);
8322
- g$4.addEventListener?.('storage', f);
8323
- return () => { watching.delete(f); g$4.removeEventListener?.('storage', f); };
8791
+ g$3.addEventListener?.('storage', f);
8792
+ return () => { watching.delete(f); g$3.removeEventListener?.('storage', f); };
8324
8793
  } },
8325
8794
  c: (() => {
8326
8795
  const write = (v, value, ok, fail) => {
@@ -8347,7 +8816,7 @@ const storageSource = {
8347
8816
  /** The document's visibility: `{ visibility: true, action }`, data `{ visible }` */
8348
8817
  const visibilitySource = { d: {
8349
8818
  visibility: (_, c) => {
8350
- const d = g$4.document;
8819
+ const d = g$3.document;
8351
8820
  if (!d)
8352
8821
  return;
8353
8822
  const f = () => c.send({ visible: d.visibilityState != 'hidden' });
@@ -8359,13 +8828,13 @@ const visibilitySource = { d: {
8359
8828
  /** The network: `{ online: true, action }`, data `{ online }` */
8360
8829
  const onlineSource = { d: {
8361
8830
  online: (_, c) => {
8362
- if (!g$4.navigator || !g$4.addEventListener)
8831
+ if (!g$3.navigator || !g$3.addEventListener)
8363
8832
  return;
8364
- const f = () => c.send({ online: g$4.navigator.onLine });
8833
+ const f = () => c.send({ online: g$3.navigator.onLine });
8365
8834
  f();
8366
- g$4.addEventListener('online', f);
8367
- g$4.addEventListener('offline', f);
8368
- return () => { g$4.removeEventListener('online', f); g$4.removeEventListener('offline', f); };
8835
+ g$3.addEventListener('online', f);
8836
+ g$3.addEventListener('offline', f);
8837
+ return () => { g$3.removeEventListener('online', f); g$3.removeEventListener('offline', f); };
8369
8838
  },
8370
8839
  } };
8371
8840
  /**
@@ -8375,7 +8844,7 @@ const onlineSource = { d: {
8375
8844
  */
8376
8845
  const geolocationSource = { d: {
8377
8846
  geolocation: (s, c) => {
8378
- const geo = g$4.navigator?.geolocation;
8847
+ const geo = g$3.navigator?.geolocation;
8379
8848
  if (!geo)
8380
8849
  return c.fail({ code: 2, message: 'Geolocation is not available' });
8381
8850
  const id = geo.watchPosition((p) => {
@@ -8391,11 +8860,11 @@ const geolocationSource = { d: {
8391
8860
  */
8392
8861
  const clipboardSource = { c: {
8393
8862
  copy: (v, ok, fail) => {
8394
- const text = '' + v.copy, cb = g$4.navigator?.clipboard;
8863
+ const text = '' + v.copy, cb = g$3.navigator?.clipboard;
8395
8864
  cb ? cb.writeText(text).then(() => ok({ text }), (e) => fail(failed$1(e))) : fail({ name: 'NotSupportedError', message: 'Clipboard is not available' });
8396
8865
  },
8397
8866
  paste: (_, ok, fail) => {
8398
- const cb = g$4.navigator?.clipboard;
8867
+ const cb = g$3.navigator?.clipboard;
8399
8868
  cb ? cb.readText().then((text) => ok({ text }), (e) => fail(failed$1(e))) : fail({ name: 'NotSupportedError', message: 'Clipboard is not available' });
8400
8869
  },
8401
8870
  } };
@@ -9100,6 +9569,9 @@ class App {
9100
9569
  }
9101
9570
  /** STATE.stream: the instances whose state changed emit it */
9102
9571
  notify() {
9572
+ if (this.nv === this.ver)
9573
+ return;
9574
+ this.nv = this.ver;
9103
9575
  for (const i of this.watchers)
9104
9576
  i.notify();
9105
9577
  }
@@ -9227,7 +9699,7 @@ class App {
9227
9699
  this.ran = false;
9228
9700
  this.last = v;
9229
9701
  this.hooks.onPatch?.(v);
9230
- this.vdomL?.next(v);
9702
+ this.vdomL?.next(v ?? null);
9231
9703
  }
9232
9704
  if (this.dq.length)
9233
9705
  this.stopLater();
@@ -9916,7 +10388,7 @@ const W$1 = '__sw';
9916
10388
  const HOST = /^(id|class(Name)?|style|title|name|placeholder|role|tab[iI]ndex|hidden|lang|dir|attrs)$|^(aria|data)-/;
9917
10389
  /** the dev entry's reporter (checks/widgets.ts); returns the diagnostic when one was reported
9918
10390
  * (r: G-580, a 661 the widget reported through error(e), not a throwing update) */
9919
- const dev$8 = (code, w, o, x, r) => globalThis.__SYGNAL_DIAGNOSTICS__?.widget?.(code, w, o, x, r);
10391
+ const dev$7 = (code, w, o, x, r) => globalThis.__SYGNAL_DIAGNOSTICS__?.widget?.(code, w, o, x, r);
9920
10392
  let ids$1 = 0;
9921
10393
  // shallow; G-362: an object host prop (style, attrs, a class map) compares one level deep, so a
9922
10394
  // new object with the same entries is no change (snabbdom patches the host from it anyway)
@@ -9929,16 +10401,16 @@ const same$2 = (a, b, d) => {
9929
10401
  // reported through error(e)
9930
10402
  function fail$1(o, w, k, code, e, r) {
9931
10403
  e = e instanceof Error ? e : Error(e);
9932
- dev$8(code, w, o, e, r) || console.error(`[Sygnal SYG${code}]`, e);
10404
+ dev$7(code, w, o, e, r) || console.error(`[Sygnal SYG${code}]`, e);
9933
10405
  o.app.appError(o, e, 'widget');
9934
10406
  if (code < 662 && !o.disposed)
9935
10407
  (o.$wf || (o.$wf = new Map())).set(k, [e]), o.refresh();
9936
10408
  }
9937
10409
  function mount(v) {
9938
10410
  const el = v.elm, { ww: w, wp: p, wo: o } = v.data;
9939
- const d = (n, detail) => { dev$8(140, w, o, n); el.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })); };
10411
+ const d = (n, detail) => { dev$7(140, w, o, n); el.dispatchEvent(new CustomEvent(n, { detail, bubbles: true })); };
9940
10412
  let i;
9941
- dev$8(144, w, o, el);
10413
+ dev$7(144, w, o, el);
9942
10414
  // G-409: mount's fourth parameter reports a later failure (an adapter's own redraw): SYG661,
9943
10415
  // the owner's onError fallback in this instance's place, as for a throwing update (G-580: the
9944
10416
  // dev text says error(e) reported it)
@@ -10058,7 +10530,7 @@ function defineWidget(def) {
10058
10530
  },
10059
10531
  $ssr: host,
10060
10532
  // D190/D199: a widget tag is not a selector (SYG143 in dev): select its className or a control
10061
- toString: () => (dev$8(143, w), ':not(*)'),
10533
+ toString: () => (dev$7(143, w), ':not(*)'),
10062
10534
  });
10063
10535
  }
10064
10536
 
@@ -10107,6 +10579,30 @@ function defineWidget(def) {
10107
10579
  * can still be assigned), which reads the host's current `uses`.
10108
10580
  * - Limitations: a host intent must return an object (not one stream).
10109
10581
  */
10582
+ /** A behavior output's slice from before the action (outputs get the updated one): for a transition shared with STATE */
10583
+ const was = (s, p, k) => p?.previousState ? p.previousState[k] : s;
10584
+ /**
10585
+ * A behavior model whose outputs compute from the state before the action (its slice and
10586
+ * `props.state`), for behaviors whose STATE and outputs share one transition; STATE / HOST as is
10587
+ */
10588
+ const fromPrevious = (model) => {
10589
+ const out = {};
10590
+ for (const a in model) {
10591
+ const e = model[a];
10592
+ if (!e || typeof e != 'object') {
10593
+ out[a] = e;
10594
+ continue;
10595
+ }
10596
+ const m = {};
10597
+ for (const s in e) {
10598
+ const f = e[s];
10599
+ m[s] = s == 'STATE' || s == 'HOST' || typeof f != 'function' ? f
10600
+ : (x, d, n, p, o, k) => f(was(x, p, k), d, n, p?.previousState ? { ...p, state: p.previousState } : p, o, k);
10601
+ }
10602
+ out[a] = m;
10603
+ }
10604
+ return out;
10605
+ };
10110
10606
  // a model entry as { sink: fn }: a constant is sent as is, `true` sends the action's data, an
10111
10607
  // EFFECT constant does nothing (as the core treats them)
10112
10608
  const sinksOf = (e, S) => {
@@ -10289,6 +10785,8 @@ const pager = /*#__PURE__*/ defineBehavior({
10289
10785
  });
10290
10786
 
10291
10787
  const isRow = (r) => r?.id != null;
10788
+ /** PLAN-11 F-2: a File / Blob is a field value (a leaf), not an object of fields */
10789
+ const isFile = (v) => typeof Blob != 'undefined' && v instanceof Blob;
10292
10790
  // the key of segment `s` in `v`: an array's row with that id, else the index unless the element
10293
10791
  // there is a row (G-380: a row is named by its id only; -1, no element), an object's key
10294
10792
  const key = (v, s) => {
@@ -10335,7 +10833,7 @@ const setField = (values, name, value) => {
10335
10833
  * array-level error such as "at least one address"); a row's `id` is not a field
10336
10834
  */
10337
10835
  const fieldNames = (v, at = '', row) => {
10338
- if (!v || typeof v != 'object')
10836
+ if (!v || typeof v != 'object' || isFile(v))
10339
10837
  return at ? [at] : [];
10340
10838
  const a = Array.isArray(v), out = a ? [at] : [];
10341
10839
  for (const k in v) {
@@ -10416,7 +10914,12 @@ const focusInvalid = (names, within) => {
10416
10914
  * field.
10417
10915
  *
10418
10916
  * Options: `values` (the start values; D239: or a function of the host's state, called when the
10419
- * form starts), `submit` (host action), `check` (async per-field checks through a driver: a
10917
+ * form starts), `submit` (host action), `request` (PLAN-11 F-3: the form sends the valid submit
10918
+ * itself on the `http` sink: a request, or `(data, hostState) => request`, completed with the
10919
+ * body (`multipart: data` when a top-level value is a File / Blob or an array of them, else
10920
+ * `json: data`, unless it sets json / multipart / body), `progress: '<key>.PROGRESS'` (unless set)
10921
+ * and its own `ok: '<key>.DONE'`, `error: '<key>.ERRORS'`; with `submit` too, that host action is
10922
+ * still dispatched (for state), and should send no request), `check` (async per-field checks through a driver: a
10420
10923
  * request with a reply action, `latest`), `show` ('blur' default: a schema error shows once its
10421
10924
  * field was blurred; 'input'; 'submit'), `form` (selector), `http` (the driver sink of the
10422
10925
  * checks, 'HTTP'), `resetOnShow` (D239), `tool` (PLAN-6 A-3, experimental: `formTool({ … })` from
@@ -10435,6 +10938,8 @@ const focusInvalid = (names, within) => {
10435
10938
  * before that (a child's first render) the fields come from calling it with a blank state.
10436
10939
  *
10437
10940
  * Slice (state.form): values, initial, errors (all current schema errors by name), touched,
10941
+ * progress (PLAN-11 F-2: the submit's last progress step or null), resets (counts RESET and
10942
+ * resetOnShow starts: key a file input with it, since a file input can't be cleared by state),
10438
10943
  * server (server errors), remote (async check results: '' passed), pending (name -> value being
10439
10944
  * checked), submitting, submitted, submitCount, queued (a submit waits for a check or an async
10440
10945
  * schema), validating (an async schema runs), validated (the schema has answered since the start
@@ -10444,11 +10949,14 @@ const focusInvalid = (names, within) => {
10444
10949
  *
10445
10950
  * Actions: form.CHANGE ({ name, value, item? }: G-376: a checkbox gives `checked`, and `item`, its
10446
10951
  * value, which toggles membership when the field is an array; <select multiple> an array of the
10447
- * selected values; type=file is ignored), form.SET ({ values }: G-647, field name -> value, several
10952
+ * selected values; PLAN-11 F-2: a file input its File, `multiple` an array of them, none null),
10953
+ * form.SET ({ values }: G-647, field name -> value, several
10448
10954
  * fields changed at once as if typed: one validation, `touched` as for CHANGE, unknown names
10449
10955
  * skipped; e.g. an AI fill), form.BLUR (name), form.SUBMIT, form.ADD ({ field,
10450
10956
  * value }), form.REMOVE ({ field, id }), form.ERRORS (an error reply or a map: server errors),
10451
- * form.DONE (saved), form.RESET (values?); internal: form.RESULT (an async schema's result),
10957
+ * form.DONE (saved), form.RESET (values?), form.PROGRESS (PLAN-11 F-2: a progress step of the
10958
+ * submit request, `progress: 'form.PROGRESS'`: kept as `progress` while submitting, cleared by
10959
+ * DONE / ERRORS / RESET); internal: form.RESULT (an async schema's result),
10452
10960
  * form.CHECKED_<name> (a check's reply; a failed check passes: the server checks on submit),
10453
10961
  * form.VALIDATE (the first validation, when the host starts: G-375; `valid` is false until the
10454
10962
  * schema answered; with resetOnShow also each time the form element appears, D239).
@@ -10472,8 +10980,14 @@ const focusInvalid = (names, within) => {
10472
10980
  * fields. The host checks (SYG234 submit, SYG235 check names, SYG236 row ids)
10473
10981
  * run when the host is created.
10474
10982
  */
10475
- const dev$7 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.form?.(...a);
10983
+ // PLAN-11 F-3: a submit's data goes as multipart when a top-level value is a file (or files)
10984
+ const hasFile = (d) => !!d && typeof d == 'object' && Object.values(d).some(x => isFile(x) || Array.isArray(x) && x.some(isFile));
10985
+ const dev$6 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.form?.(...a);
10476
10986
  const keys = (o) => Object.keys(o).filter(k => o[k]);
10987
+ // PLAN-11 F-2: values compared as JSON, a File by identity-ish fields (JSON gives every File {})
10988
+ const fileIds = new WeakMap();
10989
+ let fileSeq = 0;
10990
+ const json = (v) => JSON.stringify(v, (_k, x) => isFile(x) ? `\0file:${fileIds.get(x) ?? (fileIds.set(x, ++fileSeq), fileSeq)}` : x);
10477
10991
  const drop = (o, p) => {
10478
10992
  const r = {};
10479
10993
  for (const k in o)
@@ -10482,8 +10996,8 @@ const drop = (o, p) => {
10482
10996
  return r;
10483
10997
  };
10484
10998
  const form = (schema, o = {}) => {
10485
- isStandardSchema(schema) || dev$7(231, schema);
10486
- const { values: vo = {}, submit, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again } = o;
10999
+ isStandardSchema(schema) || dev$6(231, schema);
11000
+ const { values: vo = {}, submit, request: rq, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again } = o;
10487
11001
  // D239: `values` can be a function of the host's state, called when the form starts (and each
10488
11002
  // show, resetOnShow). Before that (the host's first render, before its first action) the
10489
11003
  // fields come from calling it with a blank state: every read gives '' in the result
@@ -10515,7 +11029,7 @@ const form = (schema, o = {}) => {
10515
11029
  // from a named button)
10516
11030
  const known = (s, n, quiet) => {
10517
11031
  const k = n && hasField(s.values, n);
10518
- n && !k && !quiet && dev$7(230, n, s.values);
11032
+ n && !k && !quiet && dev$6(230, n, s.values);
10519
11033
  return k;
10520
11034
  };
10521
11035
  // new values: errors now (sync schema), or after the schema's Promise (RESULT)
@@ -10530,7 +11044,7 @@ const form = (schema, o = {}) => {
10530
11044
  });
10531
11045
  const ask = (s, f, k, queued) => {
10532
11046
  const x = getField(s.values, f), q = checks[f].request(x), ok = `${k}.CHECKED_${f}`;
10533
- dev$7(235, f, q);
11047
+ dev$6(235, f, q);
10534
11048
  return { s: { ...s, queued, pending: { ...s.pending, [f]: x } }, req: { ...q, ok, error: ok, latest: true } };
10535
11049
  };
10536
11050
  // a submit with s's errors: blocked (focus the first), a check to run or to wait for, or sent
@@ -10541,10 +11055,11 @@ const form = (schema, o = {}) => {
10541
11055
  : Object.keys(s.pending).length ? { s: { ...s, queued: true } }
10542
11056
  : { s: req ? { ...s, queued: false, submitting: true } : done({ ...s, queued: false }), send: 1 };
10543
11057
  };
10544
- const base = (vals) => ({ initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
10545
- submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false });
10546
- const fresh = (vals) => edit(base(vals), vals);
10547
- const done = (s) => ({ ...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {} });
11058
+ const base = (vals, resets = 0) => ({ initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
11059
+ submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false, progress: null, resets });
11060
+ // a start over (RESET, resetOnShow): `resets` counts them, so a file input keyed by it is made anew
11061
+ const fresh = (vals, s) => edit(base(vals, s ? (s.resets || 0) + 1 : 0), vals);
11062
+ const done = (s) => ({ ...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {}, progress: null });
10548
11063
  const group = (cur, d) => {
10549
11064
  if (!Array.isArray(cur) || !('item' in d))
10550
11065
  return d.value;
@@ -10553,10 +11068,10 @@ const form = (schema, o = {}) => {
10553
11068
  };
10554
11069
  // fields changed as typed ones ([name, value] pairs; unknown names skipped, SYG230): their
10555
11070
  // server errors, check results and running checks go, `touched` as show 'input' says; one validation
10556
- const put = (s, d) => {
11071
+ const put = (s, d, quiet) => {
10557
11072
  let vals = s.values, server = drop(s.server, ''), remote = s.remote, pending = s.pending, touched = s.touched;
10558
11073
  for (const [n, y] of d)
10559
- if (known(s, n)) {
11074
+ if (known(s, n, quiet)) {
10560
11075
  vals = setField(vals, n, y), server = drop(server, n), remote = drop(remote, n), pending = drop(pending, n);
10561
11076
  if (show == 'input')
10562
11077
  touched = { ...touched, [n]: true };
@@ -10566,7 +11081,9 @@ const form = (schema, o = {}) => {
10566
11081
  const steps = {
10567
11082
  // G-376: a checkbox (`item`: its value) on an array field is one of a group: checked adds
10568
11083
  // its value, unchecked removes it
10569
- CHANGE: (s, d) => put(s, d?.name ? [[d.name, group(getField(s.values, d.name), d)]] : []),
11084
+ // PLAN-11 F-2: a file input whose name isn't in values is left alone, quietly (an app that reads
11085
+ // its files in its own intent, as before 6.7)
11086
+ CHANGE: (s, d) => put(s, d?.name ? [[d.name, group(getField(s.values, d.name), d)]] : [], d?.file),
10570
11087
  BLUR: (s, n, k) => {
10571
11088
  if (!known(s, n, 1))
10572
11089
  return null;
@@ -10575,7 +11092,7 @@ const form = (schema, o = {}) => {
10575
11092
  },
10576
11093
  SUBMIT: (s, _, k) => {
10577
11094
  if (s.submitting || s.queued)
10578
- return void dev$7(232, s);
11095
+ return void dev$6(232, s);
10579
11096
  const n = { ...s, submitCount: s.submitCount + 1 }, r = v(s.values);
10580
11097
  // G-371: a submit waiting for an async schema is queued (a second one is dropped)
10581
11098
  return r.then ? { s: { ...n, validating: true, queued: true }, wait: { values: s.values, submit: 1 } } : attempt({ ...n, errors: r.errors }, k);
@@ -10599,16 +11116,20 @@ const form = (schema, o = {}) => {
10599
11116
  SET: (s, d) => put(s, Object.entries(d?.values || {})),
10600
11117
  ERRORS: (s, d) => {
10601
11118
  const e = replyErrors(d, s.values);
10602
- return { s: { ...s, server: e, submitting: false, queued: false }, focus: focusInvalid(e, sel) };
11119
+ return { s: { ...s, server: e, submitting: false, queued: false, progress: null }, focus: focusInvalid(e, sel) };
10603
11120
  },
10604
11121
  DONE: (s) => ({ s: done(s) }),
10605
- RESET: (s, d) => fresh(d && typeof d == 'object' ? d : s.initial),
11122
+ // PLAN-11 F-2: a progress step of the submit's request (`progress: 'form.PROGRESS'`), while it is pending
11123
+ PROGRESS: (s, d) => s.submitting && d && typeof d == 'object'
11124
+ ? { s: { ...s, progress: { phase: d.phase, loaded: d.loaded, total: d.total ?? null, fraction: d.fraction ?? null } } } : null,
11125
+ // PLAN-11: only a plain object is new values (a click event from `DOM.click('.reset')` is not)
11126
+ RESET: (s, d) => fresh(d && typeof d == 'object' && [Object.prototype, null].includes(Object.getPrototypeOf(d)) ? d : s.initial, s),
10606
11127
  // G-375: the first validation, when the host starts (form() doesn't validate at module load).
10607
11128
  // G-575: a slice kept in parent state from a host that unmounted mid-submit starts unstuck.
10608
11129
  // D239: with resetOnShow (also each time the form element appears again), or a `values`
10609
11130
  // function not called yet, the slice starts over from the start values (h: the host state)
10610
- VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo)
10611
- : edit({ ...s, submitting: false, queued: false, pending: {} }, s.values),
11131
+ VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo, again && s)
11132
+ : edit({ ...s, submitting: false, queued: false, pending: {}, progress: null }, s.values),
10612
11133
  };
10613
11134
  // a check's reply (ok and error: an error reply carries an Error and passes)
10614
11135
  for (const f of checked)
@@ -10618,6 +11139,14 @@ const form = (schema, o = {}) => {
10618
11139
  const n = { ...s, pending: drop(s.pending, f), remote: { ...s.remote, [f]: d?.error instanceof Error ? '' : checks[f].error?.(d) || '' } };
10619
11140
  return s.queued ? attempt(n, k) : { s: n };
10620
11141
  };
11142
+ // PLAN-11 F-3: the `request` option's request for a valid submit (data: the schema's output)
11143
+ const send = (vals, h, k) => {
11144
+ const data = v(vals).value, q = typeof rq == 'function' ? rq(data, h) : rq;
11145
+ if (!q || q === ABORT)
11146
+ return ABORT;
11147
+ const own = 'json' in q || 'multipart' in q || 'body' in q;
11148
+ return { ...(!own && (hasFile(data) ? { multipart: data } : { json: data })), progress: `${k}.PROGRESS`, ...q, ok: `${k}.DONE`, error: `${k}.ERRORS` };
11149
+ };
10621
11150
  const model = {};
10622
11151
  for (const a in steps) {
10623
11152
  let last = [];
@@ -10627,19 +11156,28 @@ const form = (schema, o = {}) => {
10627
11156
  // VALIDATE: on the host's state (HOST), for a `values` function; the slice's own steps on it
10628
11157
  ...a == 'VALIDATE' ? { HOST: (h, d, _n, _p, _o, k) => { const x = out(h[k], d, k, h); return x ? { ...h, [k]: x.s } : h; } }
10629
11158
  : { STATE: (s, d, _n, _p, _o, k) => out(s, d, k)?.s || ABORT },
10630
- EFFECT: (s, d, next, _p, _o, k) => {
11159
+ // outputs share STATE's transition: computed from the slice before the action
11160
+ EFFECT: (s, d, next, p, _o, k) => {
11161
+ s = was(s, p, k);
10631
11162
  const x = out(s, d, k), w = x?.wait;
10632
11163
  if (x?.send) {
10633
11164
  const r = v(x.s.values).value;
10634
- dev$7(233, s.values, r);
11165
+ dev$6(233, s.values, r);
10635
11166
  // G-577: a submit named after one of the form's own actions goes to that one
10636
- submit in steps && dev$7(234, submit, k, b);
10637
- next(submit, r, 0);
11167
+ submit in steps && dev$6(234, submit, k, b);
11168
+ // PLAN-11 F-3: with `request`, `submit` is optional (dispatched for state when given)
11169
+ if (submit !== undefined || !rq)
11170
+ next(submit, r, 0);
10638
11171
  }
10639
11172
  w && Promise.resolve(v(w.values)).then(() => next('RESULT', w, 0));
10640
11173
  },
10641
- ELEMENT: (s, d, _n, _p, _o, k) => out(s, d, k)?.focus || ABORT,
10642
- ...(checked.length && { [http]: (s, d, _n, _p, _o, k) => out(s, d, k)?.req || ABORT }),
11174
+ ELEMENT: (s, d, _n, p, _o, k) => out(was(s, p, k), d, k)?.focus || ABORT,
11175
+ // the checks' requests, and (PLAN-11 F-3) the `request` option's for a valid submit; the
11176
+ // step from the slice before the action (7.0: outputs get the updated one), the host state as committed
11177
+ ...((checked.length || rq) && { [http]: (s, d, _n, p, _o, k) => {
11178
+ const x = out(was(s, p, k), d, k);
11179
+ return x?.req || (x?.send && rq ? send(x.s.values, p?.state, k) : ABORT);
11180
+ } }),
10643
11181
  };
10644
11182
  }
10645
11183
  const b = defineBehavior({
@@ -10655,9 +11193,11 @@ const form = (schema, o = {}) => {
10655
11193
  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),
10656
11194
  // G-376: a checkbox (native, or a hyphenated tag with a boolean `checked`: a
10657
11195
  // form-associated custom checkbox / switch) gives `checked` and its value as `item`; a
10658
- // <select multiple> the selected values; type=file is left alone; others give `value`
10659
- 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 }
10660
- : { name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o) => o.value) : t.value }),
11196
+ // <select multiple> the selected values; PLAN-11 F-2: a file input its File (`multiple`:
11197
+ // an array of them; none: null); others give `value`
11198
+ 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 }
11199
+ : t.type == 'checkbox' || /-/.test(t.tagName) && typeof t.checked == 'boolean' ? { name: t.name, value: t.checked, item: t.value }
11200
+ : { name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o) => o.value) : t.value }),
10661
11201
  BLUR: f.events('focusout').map((e) => e.target.name),
10662
11202
  SUBMIT: f.events('submit', { preventDefault: true }),
10663
11203
  };
@@ -10675,7 +11215,7 @@ const form = (schema, o = {}) => {
10675
11215
  },
10676
11216
  // G-382: an async schema re-validating keeps the last answer's validity (no flicker)
10677
11217
  valid: (s) => s.validated && !keys(s.errors).length && !keys(s.remote).length,
10678
- dirty: (s) => JSON.stringify(s.values) != JSON.stringify(s.initial),
11218
+ dirty: (s) => json(s.values) != json(s.initial),
10679
11219
  error: (s) => s.server[''] || (s.submitCount && s.errors['']) || '',
10680
11220
  },
10681
11221
  })({ ...o, values }), merge = b.merge;
@@ -10683,7 +11223,7 @@ const form = (schema, o = {}) => {
10683
11223
  o.tool?.$?.(b, v);
10684
11224
  b.merge = (c, k) => {
10685
11225
  const e = c.model?.[submit];
10686
- req = !!e && typeof e == 'object' && http in e;
11226
+ req = !!rq || !!e && typeof e == 'object' && http in e;
10687
11227
  merge(c, k);
10688
11228
  };
10689
11229
  return b;
@@ -10832,7 +11372,7 @@ const selection = (options = {}) => {
10832
11372
  * interaction, SYG147 a `from` key that isn't an array in the host state, SYG435 (3-H) a list
10833
11373
  * shown in another order than its array's (a Collection's sort / filter).
10834
11374
  */
10835
- const dev$6 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.sortable?.(...a);
11375
+ const dev$5 = (...a) => globalThis.__SYGNAL_DIAGNOSTICS__?.sortable?.(...a);
10836
11376
  const LIFT = /^( |Enter|Spacebar)$/;
10837
11377
  const KEYS = /^(Arrow(Up|Down|Left|Right)|Home|End|Escape|Tab)$/;
10838
11378
  const S$1 = String;
@@ -10946,7 +11486,7 @@ const sortable = (options = {}) => {
10946
11486
  } } } } };
10947
11487
  };
10948
11488
  // dev: SYG435, the list shown in another order than the array's (G-449)
10949
- const check = (st, seen, k) => seen && dev$6(435, seen, st, lists, idField, k);
11489
+ const check = (st, seen, k) => seen && dev$5(435, seen, st, lists, idField, k);
10950
11490
  // a press or key handler: drag state this instance didn't start (its token `n` isn't the
10951
11491
  // event's: restored, synced) is dropped first (G-452)
10952
11492
  const live = (h) => (st, d, next, _p, _o, k) => {
@@ -10987,7 +11527,7 @@ const sortable = (options = {}) => {
10987
11527
  const gripOf = (e, self) => {
10988
11528
  const t = e.target, r = e.ownerTarget || e.currentTarget;
10989
11529
  if (!checked)
10990
- checked = dev$6(146, r, item, handle, attr);
11530
+ checked = dev$5(146, r, item, handle, attr);
10991
11531
  const g = !claimed.has(e) && t?.closest?.(grip);
10992
11532
  if (!g || self && g !== t)
10993
11533
  return null;
@@ -11001,14 +11541,14 @@ const sortable = (options = {}) => {
11001
11541
  return null;
11002
11542
  const id = idOf(it);
11003
11543
  if (id == null)
11004
- dev$6(145, it, item, attr);
11544
+ dev$5(145, it, item, attr);
11005
11545
  return id;
11006
11546
  };
11007
11547
  const keyed = (keys, kb) => (e) => keys.test(e.key) && (kb || e.key != 'Tab') && gripOf(e, true) != null;
11008
11548
  const lift = keyed(LIFT), step = keyed(KEYS, 1);
11009
11549
  // dev (SYG435): this host's own items' ids in the order they're shown (computed by the
11010
11550
  // diagnostics entry only)
11011
- const shown = (e) => dev$6(435, () => {
11551
+ const shown = (e) => dev$5(435, () => {
11012
11552
  const r = e.ownerTarget || e.currentTarget;
11013
11553
  return [[...r.querySelectorAll(item)].filter((it) => own(it, r)), r];
11014
11554
  }, key, lists, idOf);
@@ -11052,7 +11592,7 @@ const sortable = (options = {}) => {
11052
11592
  // again by HMR, a second host of the slice) is reset, as at END
11053
11593
  INIT: { HOST: (st, _d, _n, _p, _o, k) => {
11054
11594
  for (const l of lists)
11055
- Array.isArray(st?.[l]) || dev$6(147, l, st, k);
11595
+ Array.isArray(st?.[l]) || dev$5(147, l, st, k);
11056
11596
  return st[k].press || st[k].dragging ? back(st, k) : ABORT;
11057
11597
  } },
11058
11598
  // unmounted mid-drag: the drag is cancelled (no DROPPED; a keyboard drag's item goes back
@@ -11464,14 +12004,14 @@ const undo = (options) => {
11464
12004
  * too), once per kind (restore / save / clear; a save that works again re-arms it); the app
11465
12005
  * continues on initialState / unsaved, and a failed save is retried at the next change.
11466
12006
  */
11467
- const g$3 = globalThis;
12007
+ const g$2 = globalThis;
11468
12008
  const setupPersist = (c, o) => {
11469
12009
  const { key, pick, omit, version = 1, migrate, sync, debounceMs = 100 } = o, plain = o.format == 'plain', src = c.sources, m = src.__m;
11470
12010
  const env = src.__storage, calc = c.calculated || {};
11471
12011
  // hydrating: the option, else the integration's signal (__hydrate: Astro, Vike), else whether
11472
12012
  // run()'s mount point starts with renderToString's markup (its root element is marked; a
11473
12013
  // client-only app's loading placeholder isn't server markup)
11474
- const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g$3.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
12014
+ const hydrate = o.hydrate ?? src.__hydrate ?? !!(typeof m == 'string' ? g$2.document?.querySelector(m) : m)?.firstElementChild?.hasAttribute('data-sygnal-ssr');
11475
12015
  let S, t, last, raw, skip, off;
11476
12016
  // (the docs link the message ends with explains the causes and fixes). Once per kind (restore,
11477
12017
  // save, clear): a storage that throws on every write must not warn on every change; a save that
@@ -11544,7 +12084,7 @@ const setupPersist = (c, o) => {
11544
12084
  }
11545
12085
  c.model = model;
11546
12086
  try {
11547
- S = typeof o.storage == 'object' ? o.storage : (env || g$3)[(o.storage || 'local') + (env ? '' : 'Storage')];
12087
+ S = typeof o.storage == 'object' ? o.storage : (env || g$2)[(o.storage || 'local') + (env ? '' : 'Storage')];
11548
12088
  }
11549
12089
  catch (_) { }
11550
12090
  if (!S)
@@ -11581,14 +12121,14 @@ const setupPersist = (c, o) => {
11581
12121
  send(read(raw = v)); };
11582
12122
  const onStorage = (e) => (!e.storageArea || e.storageArea == S) && onSync(e.key, e.newValue);
11583
12123
  if (sync)
11584
- off = S.subscribe ? S.subscribe(onSync) : (g$3.addEventListener?.('storage', onStorage), () => g$3.removeEventListener?.('storage', onStorage));
11585
- g$3.addEventListener?.('pagehide', flush);
12124
+ off = S.subscribe ? S.subscribe(onSync) : (g$2.addEventListener?.('storage', onStorage), () => g$2.removeEventListener?.('storage', onStorage));
12125
+ g$2.addEventListener?.('pagehide', flush);
11586
12126
  env?.f.add(flush);
11587
12127
  c._dispose$.addListener({ next: () => {
11588
12128
  flush();
11589
12129
  state$.removeListener(L);
11590
12130
  off?.();
11591
- g$3.removeEventListener?.('pagehide', flush);
12131
+ g$2.removeEventListener?.('pagehide', flush);
11592
12132
  env?.f.delete(flush);
11593
12133
  } });
11594
12134
  };
@@ -12820,7 +13360,7 @@ const snake = (s) => s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/\W+/g, '_
12820
13360
  const keyOf$1 = (iv) => iv.state?.id;
12821
13361
  const done = new WeakMap();
12822
13362
  /** a dev diagnostic, once per (declaration, what); 'error' mode rethrows outside the caller */
12823
- const dev$5 = (code, decl, what, component, message) => {
13363
+ const dev$4 = (code, decl, what, component, message) => {
12824
13364
  let s = done.get(decl);
12825
13365
  if (!s)
12826
13366
  done.set(decl, s = new Set());
@@ -13029,7 +13569,7 @@ function agentTools(target, options = {}) {
13029
13569
  v = d.read(s);
13030
13570
  }
13031
13571
  catch (e) {
13032
- dev$5('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`);
13572
+ dev$4('SYG241', d, 'read', iv.name, `agent.read threw: ${e?.message ?? e}`);
13033
13573
  }
13034
13574
  reads.set(iv, { s, v });
13035
13575
  return v;
@@ -13048,7 +13588,7 @@ function agentTools(target, options = {}) {
13048
13588
  v = !!w(s);
13049
13589
  }
13050
13590
  catch (e) {
13051
- dev$5('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`);
13591
+ dev$4('SYG241', declOf(iv), 'when' + action, iv.name, `agent.actions.${action}.when threw: ${e?.message ?? e}`);
13052
13592
  }
13053
13593
  m.set(action, { s, v });
13054
13594
  return v;
@@ -13059,7 +13599,7 @@ function agentTools(target, options = {}) {
13059
13599
  return d.label ? String(d.label(iv.state)) : undefined;
13060
13600
  }
13061
13601
  catch (e) {
13062
- dev$5('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`);
13602
+ dev$4('SYG241', d, 'label', iv.name, `agent.label threw: ${e?.message ?? e}`);
13063
13603
  }
13064
13604
  };
13065
13605
  function walk(iv, out) {
@@ -13094,7 +13634,7 @@ function agentTools(target, options = {}) {
13094
13634
  else if (e.decl === d && e.item && item)
13095
13635
  e.ivs.push(iv);
13096
13636
  else
13097
- dev$5('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`);
13637
+ dev$4('SYG440', d, name, iv.name, `two agent declarations are named '${name}'; only the first is offered`);
13098
13638
  }
13099
13639
  for (const e of by.values())
13100
13640
  if (e.item) {
@@ -13103,9 +13643,9 @@ function agentTools(target, options = {}) {
13103
13643
  const k = keyOf$1(iv);
13104
13644
  // an item without an id is keyed by its index (the core's `_i<index>` uid part): unstable
13105
13645
  if (k == null || /-_i\d+$/.test(iv.uid))
13106
- dev$5('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`);
13646
+ dev$4('SYG441', e.decl, 'nokey', iv.name, `items of '${e.name}' have no id: agents address them by index`);
13107
13647
  else if (ks.has(k + ''))
13108
- dev$5('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`);
13648
+ dev$4('SYG441', e.decl, 'dup', iv.name, `two items of '${e.name}' have the id ${JSON.stringify(k)}; calls reach the first`);
13109
13649
  else
13110
13650
  ks.add(k + '');
13111
13651
  }
@@ -13131,11 +13671,11 @@ function agentTools(target, options = {}) {
13131
13671
  for (const [action, a, isQuery] of defs) {
13132
13672
  const name = e.name + '_' + snake(action), comp = e.ivs[0].name, where = `agent.${isQuery ? 'queries' : 'actions'}.${action}`;
13133
13673
  if (isQuery && recs.has(name)) {
13134
- dev$5('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13674
+ dev$4('SYG247', d, action, comp, `${where} has the tool name ${name} of an action; the query is not offered (rename it)`);
13135
13675
  continue;
13136
13676
  }
13137
13677
  if (isQuery && typeof a?.run != 'function') {
13138
- dev$5('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13678
+ dev$4('SYG246', d, action, comp, `${where}.run is not a function; the query is not offered`);
13139
13679
  continue;
13140
13680
  }
13141
13681
  const c = a.input ? toJsonSchema(a.input) : undefined;
@@ -13150,11 +13690,11 @@ function agentTools(target, options = {}) {
13150
13690
  if (c?.error) {
13151
13691
  tool.error = `SYG240 ${c.error}`;
13152
13692
  rec.off = 1;
13153
- dev$5('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13693
+ dev$4('SYG240', d, action, comp, `${where}.input: ${c.error}; the tool is not offered`);
13154
13694
  continue;
13155
13695
  }
13156
13696
  if (c?.lossy)
13157
- dev$5('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13697
+ dev$4('SYG243', d, action, comp, `${where}.input: the model doesn't see ${c.lossy}`);
13158
13698
  const live = e.ivs.filter(iv => whenOk(iv, action, a.when));
13159
13699
  if (!live.length) {
13160
13700
  rec.off = 1;
@@ -13326,7 +13866,7 @@ function agentTools(target, options = {}) {
13326
13866
  return;
13327
13867
  const res = iv.def.view.resources;
13328
13868
  if (!res || !(u in res)) {
13329
- 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`);
13869
+ 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`);
13330
13870
  return;
13331
13871
  }
13332
13872
  return (s) => { const r = s?.[u]; return !r || (r.status != 'loading' && !r.refreshing); };
@@ -13344,7 +13884,7 @@ function agentTools(target, options = {}) {
13344
13884
  return !!test(iv.state);
13345
13885
  }
13346
13886
  catch (err) {
13347
- dev$5('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13887
+ dev$4('SYG241', declOf(iv), 'until' + action, iv.name, `agent.actions.${action}.until threw: ${err?.message ?? err}`);
13348
13888
  return true;
13349
13889
  }
13350
13890
  };
@@ -13370,11 +13910,11 @@ function agentTools(target, options = {}) {
13370
13910
  }
13371
13911
  catch (err) {
13372
13912
  const m = err?.message ?? err;
13373
- dev$5('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13913
+ dev$4('SYG241', declOf(iv), 'query' + query, iv.name, `agent.queries.${query}.run threw: ${m}`);
13374
13914
  return { ok: false, error: `${name} failed: ${m}` };
13375
13915
  }
13376
13916
  if (result && typeof result.then == 'function') {
13377
- 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`);
13917
+ 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`);
13378
13918
  return { ok: false, error: `${name} is not available: it returned a promise` };
13379
13919
  }
13380
13920
  return { ok: true, result };
@@ -13735,28 +14275,28 @@ function withToolResults(message, results) {
13735
14275
  const SCOPE = '__sygnalScope';
13736
14276
  const tag$1 = (v, scope) => v && typeof v == 'object'
13737
14277
  ? Object.defineProperty(keepSender(v, { ...v }), SCOPE, { value: [scope, ...(v[SCOPE] || [])] }) : v;
13738
- const g$2 = globalThis;
13739
- const now = () => g$2.performance?.now?.() ?? Date.now();
14278
+ const g$1 = globalThis;
14279
+ const now = () => g$1.performance?.now?.() ?? Date.now();
13740
14280
  // internal (D254): the 120 Hz floor and the hidden-tab fallback
13741
14281
  const MIN_GAP = 15, FALLBACK = 100;
13742
14282
  /** rAF raced with a timer (a hidden tab never runs rAF), else a 16 ms timer; `f` gets the frame's time */
13743
14283
  const frame = (f) => {
13744
- if (typeof g$2.requestAnimationFrame != 'function')
14284
+ if (typeof g$1.requestAnimationFrame != 'function')
13745
14285
  return void setTimeout(() => f(now()), 16);
13746
14286
  let done = 0, a, t;
13747
14287
  const go = (ts) => {
13748
14288
  if (done++)
13749
14289
  return;
13750
- g$2.cancelAnimationFrame?.(a);
14290
+ g$1.cancelAnimationFrame?.(a);
13751
14291
  clearTimeout(t);
13752
14292
  f(typeof ts == 'number' ? ts : now());
13753
14293
  };
13754
- a = g$2.requestAnimationFrame(go);
14294
+ a = g$1.requestAnimationFrame(go);
13755
14295
  t = setTimeout(go, FALLBACK);
13756
14296
  };
13757
14297
  // dev-only diagnostics (SYG673, SYG677): the dev entry formats them (checks/chat.ts), so their
13758
14298
  // text costs apps nothing; without it they're silent
13759
- const dev$4 = (...a) => g$2.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
14299
+ const dev$3 = (...a) => g$1.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
13760
14300
  const FIX = "Send { messages, ok: 'DONE', error: 'FAILED' }";
13761
14301
  /** why a request can't be sent ('' when it can) */
13762
14302
  const problem = (r) => !Array.isArray(r.messages) ? 'no messages array'
@@ -13778,7 +14318,7 @@ function makeChatDriver(options = {}) {
13778
14318
  const { replies, reply } = makeReplies(sender => abortWhere(r => r.sender === sender));
13779
14319
  // SYG673 (dev): `why` is 'type' | 'delta' | 'name' | 'id'; the first five per driver
13780
14320
  const malformed = (req, ev, why) => { if (warned++ < 5)
13781
- dev$4('SYG673', req, ev, why); };
14321
+ dev$3('SYG673', req, ev, why); };
13782
14322
  const start = async (req, sender) => {
13783
14323
  const key = req.key ?? req.ok ?? req.error ?? 'default';
13784
14324
  if (req.latest !== false)
@@ -13971,7 +14511,7 @@ function makeChatDriver(options = {}) {
13971
14511
  return;
13972
14512
  const sender = senderOf(req);
13973
14513
  if (sender === undefined)
13974
- return void dev$4('SYG677', req);
14514
+ return void dev$3('SYG677', req);
13975
14515
  const { abort } = req;
13976
14516
  if (abort !== undefined && abort !== false) {
13977
14517
  // D469 (PLAN-9 H-2): a list of keys stops each (STOP: the reply and a card being composed)
@@ -15027,6 +15567,17 @@ const validMs = (v) => typeof v == 'number' && Number.isFinite(v) && v >= 0 && v
15027
15567
  // too, so its time is the clock's (Date may be left real by `toFake`), and its waits drive it.
15028
15568
  const fakeClock = () => setTimeout.clock;
15029
15569
  const clockNow = () => { const c = fakeClock(); return c ? c.now : Date.now(); };
15570
+ /** PLAN-11 Q-3: ms until the next pending fake timer (0: none, or real timers) */
15571
+ const nextFakeTimer = () => {
15572
+ const c = fakeClock();
15573
+ let due = Infinity;
15574
+ try {
15575
+ Object.values(c?.timers || {}).forEach((t) => { if (t && t.callAt > c.now && t.callAt < due)
15576
+ due = t.callAt; });
15577
+ }
15578
+ catch (_) { }
15579
+ return due < Infinity ? due - c.now : 0;
15580
+ };
15030
15581
  /**
15031
15582
  * Under fake timers, advance the clock timer by timer (nextAsync flushes promises around each
15032
15583
  * one) until `p` settles. Every pending wait has a timer (its timeout, a quiet-window tick), so
@@ -15217,6 +15768,7 @@ const headFake = () => {
15217
15768
  };
15218
15769
  let savedConfig;
15219
15770
  let savedStrict;
15771
+ let savedAudit;
15220
15772
  /*
15221
15773
  * PLAN-4 GS-2, mock DOM: could the element a command targets have this method? The core runs any
15222
15774
  * method of the element; the mock DOM has no elements, so: the documented commands, common
@@ -15327,7 +15879,7 @@ function fakeElementMethods(W) {
15327
15879
  Object.defineProperty(proto, name, own); });
15328
15880
  }
15329
15881
  function renderComponent(componentDef, options = {}) {
15330
- const { initialState, mockConfig = {}, drivers = {}, diagnostics, strict, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors } = options;
15882
+ const { initialState, mockConfig = {}, drivers = {}, diagnostics, strict, timingAudit, dom = 'mock', autoConnect = true, socketSink = 'WS', resourceSink = 'HTTP', llmSink = 'LLM', http: httpOptions, context: ancestors } = options;
15331
15883
  const { intent, model = {} } = componentDef;
15332
15884
  // E4: real DOM mode
15333
15885
  const real = dom == 'real';
@@ -15375,6 +15927,7 @@ function renderComponent(componentDef, options = {}) {
15375
15927
  if (!active++) {
15376
15928
  savedConfig = _getDiagnosticsConfig();
15377
15929
  savedStrict = core.strict;
15930
+ savedAudit = core.timingAudit;
15378
15931
  // G-051: forget the checks' report dedupe (once()), so a finding from an earlier test is
15379
15932
  // reported again. Only that set: resetChecks() would also drop inspect()'s records of
15380
15933
  // other live instances / apps.
@@ -15383,6 +15936,9 @@ function renderComponent(componentDef, options = {}) {
15383
15936
  configureDiagnostics({ mode: diagnostics || (prevMode == 'off' ? 'collect' : prevMode) });
15384
15937
  if (strict !== undefined)
15385
15938
  core.strict = strict;
15939
+ // 7.0 timing audit (SYG257, 'sygnal/diagnostics'): same bridge flag as configureTimingAudit()
15940
+ if (timingAudit !== undefined)
15941
+ core.timingAudit = timingAudit;
15386
15942
  // PLAN-4 GS-2: element commands run on the real DOM (jsdom gets the missing methods), and their
15387
15943
  // SYG640/SYG641 are reported without the dev entry too
15388
15944
  if (real && !domFakes)
@@ -15573,6 +16129,7 @@ function renderComponent(componentDef, options = {}) {
15573
16129
  if (!--active) {
15574
16130
  configureDiagnostics(savedConfig);
15575
16131
  core.strict = savedStrict;
16132
+ core.timingAudit = savedAudit;
15576
16133
  domFakes?.();
15577
16134
  domFakes = undefined;
15578
16135
  }
@@ -15618,14 +16175,22 @@ function renderComponent(componentDef, options = {}) {
15618
16175
  const lin$ = xs$1.create();
15619
16176
  llm = { in$: lin$, src: makeChatDriver({ transport: tr, _frame: (f) => { frames.push(f); } })(lin$) };
15620
16177
  }
16178
+ let memFetch;
15621
16179
  const http = makeFetchDriver({
15622
16180
  ...httpOptions,
16181
+ // PLAN-11 U-2: a progress request is answered like any other (t.respond / t.fail); its
16182
+ // pending entry keeps the driver's progress callback for t.progress
16183
+ _progress: (url, init, step) => {
16184
+ const r = memFetch(url, init);
16185
+ pending[pending.length - 1].step = step;
16186
+ return r;
16187
+ },
15623
16188
  // 5-3: focus / online come from t.focus() / t.online() only
15624
16189
  _on: (f) => { signals.add(f); return () => signals.delete(f); },
15625
16190
  _tap: (req, res, pf) => { tapped = [req, res, pf]; },
15626
16191
  // D472: a batch / split request (a large decide()) is one fetch here, answered as one
15627
16192
  _whole: true,
15628
- fetch: (url, init) => new Promise((resolve, reject) => {
16193
+ fetch: memFetch = (url, init) => new Promise((resolve, reject) => {
15629
16194
  const [req, res, pf] = tapped || [{ url }];
15630
16195
  tapped = undefined;
15631
16196
  // G-171(1): a resource fetch is listed as { url, ...request, resource: name }; 5-5: a
@@ -15659,6 +16224,7 @@ function renderComponent(componentDef, options = {}) {
15659
16224
  return {
15660
16225
  select: (sel) => xs$1.merge(hs.select(sel), own(false)(sel), sock.select(sel)),
15661
16226
  errors: (sel) => xs$1.merge(hs.errors(sel), own(true)(sel)),
16227
+ progress: (sel) => hs.progress(sel),
15662
16228
  subs, at, pending, in$, http, ws, llm,
15663
16229
  // 5-3: the driver's matcher / inspection (the dev checks' SYG632, t.cache)
15664
16230
  __matches: http.__matches, __inspect: http.__inspect,
@@ -16424,7 +16990,7 @@ function renderComponent(componentDef, options = {}) {
16424
16990
  return { match: cat, desc: catDesc, o };
16425
16991
  if (typeof tg == 'string') {
16426
16992
  // an ok/error action name, key, category, resource name or URL
16427
- 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 };
16993
+ 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 };
16428
16994
  }
16429
16995
  if (typeof tg == 'function') {
16430
16996
  return { match: r => { try {
@@ -16681,6 +17247,27 @@ function renderComponent(componentDef, options = {}) {
16681
17247
  const err = typeof error == 'number' || resp ? Object.assign(new Error(`HTTP ${status}`), { status }) : x;
16682
17248
  return { error: err, category, request: e?.req, status: status ?? x?.status, body };
16683
17249
  });
17250
+ // PLAN-11 U-2: t.progress. One progress step of a pending request sent with `progress` (its
17251
+ // reply action, or progress() for `progress: true`); the request stays pending. Targets as
17252
+ // t.respond's; the phase defaults to 'upload' for a request with a body, else 'download'
17253
+ const progress = (name, step, opts) => {
17254
+ throwFailure();
17255
+ if (drivers[name])
17256
+ throw new Error(`[Sygnal] t.progress('${name}'): ${name} has a real driver (passed in drivers), so there is nothing to script`);
17257
+ if (!step || typeof step != 'object' || typeof step.loaded != 'number')
17258
+ throw new Error(`[Sygnal] t.progress('${name}', step): step is { loaded, total?, phase? } (got ${brief(step)})`);
17259
+ const tg = targetOf(opts);
17260
+ const what = `t.progress('${name}'${typeof opts == 'string' ? `, …, '${opts}'` : ''})`;
17261
+ const now = isReady && !inputs.length ? pick(name, tg) : undefined;
17262
+ if (now && !now.step)
17263
+ throw new Error(`[Sygnal] ${what}: the request ${brief(now.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
17264
+ return scripted(() => pick(name, tg), w => noPending(what, name, tg, w), (e) => {
17265
+ if (!e.step)
17266
+ return new Error(`[Sygnal] ${what}: the request ${brief(e.value)} has no progress: send it with progress: 'UPLOADING' (a reply action) or progress: true`);
17267
+ const r = e.req || {};
17268
+ e.step(step.phase || (r.body !== undefined || r.json !== undefined || r.multipart !== undefined ? 'upload' : 'download'), step.loaded, step.total ?? 0);
17269
+ }, sendDue);
17270
+ };
16684
17271
  // PLAN-6 L-4: t.stream. Targets as t.respond's (an action name or key, a partial request, a
16685
17272
  // predicate, { request, nth }); `end: false` (the last argument, or in the target options)
16686
17273
  // leaves the stream open. Chunks become ChatEvents pushed into the in-memory stream; the fake's
@@ -17185,7 +17772,14 @@ function renderComponent(componentDef, options = {}) {
17185
17772
  let msg = `${name} timed out after ${timeoutMs}ms.`;
17186
17773
  const [why, pending] = explainNext(start);
17187
17774
  msg += why;
17188
- if (pending)
17775
+ if (inputs.length)
17776
+ msg += ` ${inputs.length} simulated input(s) still pending.`;
17777
+ // PLAN-11 Q-3: on the fake clock a state a timer (TIMER driver, periodic, delay) brings
17778
+ // later than the timeout can't arrive in time: name the next pending timer
17779
+ const later = pending ? 0 : nextFakeTimer();
17780
+ if (later)
17781
+ 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.`;
17782
+ if (pending || later)
17189
17783
  msg += ` Wait longer: t.${name}(pred, ms), or the renderComponent timeoutMs option (default for every wait).`;
17190
17784
  // G-065: a state from before the call matched; next() only looks at new ones
17191
17785
  for (let i = from - 1; i >= 0; i--) {
@@ -17385,6 +17979,7 @@ function renderComponent(componentDef, options = {}) {
17385
17979
  requests,
17386
17980
  respond,
17387
17981
  fail,
17982
+ progress,
17388
17983
  stream,
17389
17984
  connections,
17390
17985
  cache,
@@ -17961,7 +18556,7 @@ function jsonSchema(schema, options = {}) {
17961
18556
  * Side-effect free: an app that doesn't import it pays 0 B.
17962
18557
  */
17963
18558
  const NAME = 30, DESC = 500, PARAM = 150, OUT = 1500, SUMMARY = 300;
17964
- const g$1 = globalThis;
18559
+ const g = globalThis;
17965
18560
  /** cut to n characters with an ellipsis, never inside a surrogate pair */
17966
18561
  const cut = (s, n) => {
17967
18562
  if (s.length <= n)
@@ -17978,7 +18573,7 @@ const hash = (s) => {
17978
18573
  return (h % 1679616).toString(36).padStart(4, '0');
17979
18574
  };
17980
18575
  function experimentalExposeWebMcp(app, options = {}) {
17981
- const mc = options.modelContext ?? g$1.document?.modelContext ?? g$1.navigator?.modelContext;
18576
+ const mc = options.modelContext ?? g.document?.modelContext ?? g.navigator?.modelContext;
17982
18577
  const said = new Set();
17983
18578
  const dev = (code, key, message, severity = 'warn') => {
17984
18579
  if (said.has(code + key))
@@ -18181,7 +18776,7 @@ function experimentalExposeWebMcp(app, options = {}) {
18181
18776
  let n$2 = 0;
18182
18777
  /** the default confirmation: a native modal dialog outside the app's tree (Escape and "Deny" decline) */
18183
18778
  function dialog(info, setClose) {
18184
- const doc = g$1.document;
18779
+ const doc = g.document;
18185
18780
  if (!doc?.createElement)
18186
18781
  return Promise.resolve(false);
18187
18782
  return new Promise(resolve => {
@@ -18437,7 +19032,7 @@ const toolsOf$1 = (e) => e.o.agent === false ? null : e.tools || (e.tools = agen
18437
19032
  }),
18438
19033
  }));
18439
19034
  // dev diagnostics (SYG684, SYG685): no-ops with diagnostics off
18440
- const dev$3 = (code, message, fix) => { try {
19035
+ const dev$2 = (code, message, fix) => { try {
18441
19036
  report(code, { component: 'chat', message, fix, severity: 'warn' });
18442
19037
  }
18443
19038
  catch (e) {
@@ -18647,7 +19242,7 @@ function steps(c, o) {
18647
19242
  const openPart = (s, id, what, guard = (_p) => true) => {
18648
19243
  const p = openToolCalls(s).find(x => x.toolCallId === id && isClient(c, x) && guard(x));
18649
19244
  if (!p)
18650
- 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');
19245
+ 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');
18651
19246
  return p;
18652
19247
  };
18653
19248
  /** after a call closed or a card changed: the next queued card, then rule 3 */
@@ -18775,7 +19370,7 @@ function request(e, o, k, messages, fresh, c, host) {
18775
19370
  for (const n in c.client) {
18776
19371
  if (n in tools && !shadowed.has(n)) {
18777
19372
  shadowed.add(n);
18778
- 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');
19373
+ 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');
18779
19374
  }
18780
19375
  const x = c.client[n];
18781
19376
  tools[n] = c.ask && n == 'ask_user' ? c.kit.askTool(c.ask, host) : { description: x.description, inputSchema: schemaOf(x) };
@@ -18892,7 +19487,7 @@ const chat = (options = {}) => {
18892
19487
  ...(c.ask?.controls && DOM && { CONTROL: DOM.select('[data-compose-control]').events('click').map(controlOf$1).filter((x) => !!x) }),
18893
19488
  };
18894
19489
  },
18895
- model: {
19490
+ model: fromPrevious({
18896
19491
  PROMPT: (s, p) => typeof p == 'string' && p !== s.prompt ? { ...s, prompt: p } : ABORT,
18897
19492
  SEND: {
18898
19493
  STATE: (s, d) => sendStep(s, d) || ABORT,
@@ -18990,153 +19585,19 @@ const chat = (options = {}) => {
18990
19585
  ANSWER: { STATE: stateOf(S.answerCard), ...sinks(S.answerCard) },
18991
19586
  CONTROL: (s, d) => S.controlStep(s, d) || ABORT,
18992
19587
  }),
18993
- },
19588
+ }),
18994
19589
  })(options);
18995
19590
  // the `prompt` option is a selector, not the slice's start value (defineBehavior's option override)
18996
19591
  b.state = { ...b.state, prompt: '' };
18997
19592
  return linked(b);
18998
19593
  };
18999
19594
 
19000
- /*
19001
- * PLAN-6 L-2: what the HTTP transports share (side-effect free; each transport imports only what
19002
- * it uses).
19003
- * - post(): the fetch (the `fetch` option, 0-S5), the headers option (an object or a function of
19004
- * the request), HTTP errors as Errors with `status` and the provider's message, and SYG670.
19005
- * - sse(): a fetch Response body as SSE frames ({ event, data }), per the HTML spec's parsing
19006
- * rules: CRLF / LF, comments, multi-line data. The reader is cancelled when the consumer stops
19007
- * (abort, a thrown error), so the connection closes.
19008
- * - SYG670: in a browser, a request with an auth header (Authorization, x-api-key, api-key,
19009
- * x-goog-api-key) to a host that is neither local (localhost, 127.0.0.1, [::1], *.localhost)
19010
- * nor the page's own origin is refused unless the transport has `dangerouslyAllowBrowser: true`:
19011
- * the key would ship to every visitor. Same-origin requests are the app's own server (its
19012
- * proxy), so a session header there is fine.
19013
- */
19014
- const g = globalThis;
19015
- const AUTH = /^(authorization|x-api-key|api-key|x-goog-api-key)$/i;
19016
- const LOCAL = /^(localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/;
19017
- /** the URL a transport posts to, or a SYG670 Error when it must not (see the header) */
19018
- function guard(url, headers, opts, name) {
19019
- if (opts.dangerouslyAllowBrowser || typeof g.document == 'undefined' || !Object.keys(headers).some(h => AUTH.test(h)))
19020
- return;
19021
- const here = g.location?.href;
19022
- let u;
19023
- try {
19024
- u = new URL(url, here);
19025
- }
19026
- catch (_) {
19027
- return;
19028
- }
19029
- if (LOCAL.test(u.hostname) || (here && u.origin == new URL(here).origin))
19030
- return;
19031
- 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 })`;
19032
- const msg = `${name}: refused to send an auth header from the browser to ${u.origin}: anyone who opens the page can read it`;
19033
- error('SYG670', undefined, msg, fix);
19034
- fail$2('SYG670', undefined, msg, fix);
19035
- }
19036
- /**
19037
- * POST JSON; a non-2xx response is an Error with `status`, `body` and the provider's message.
19038
- * The transport's `body` option and the request's `body` are merged over the built body (so they
19039
- * can override provider defaults such as `max_tokens`); `fixed` fields are merged last and can't be
19040
- * overridden (G-654: uiMessageStream's protocol fields, as the AI SDK's DefaultChatTransport)
19041
- */
19042
- async function post$1(url, body, req, signal, opts, name, fixed) {
19043
- const h = typeof opts.headers == 'function' ? await opts.headers(req) : opts.headers || {};
19044
- guard(url, h, opts, name);
19045
- const f = opts.fetch || ((u, i) => g.fetch(u, i));
19046
- const res = await f(url, { method: 'POST', headers: { 'content-type': 'application/json', ...h }, body: JSON.stringify({ ...body, ...opts.body, ...req.body, ...fixed }), signal });
19047
- if (!res.ok) {
19048
- let text = '', message = '';
19049
- try {
19050
- text = await res.text();
19051
- const j = JSON.parse(text);
19052
- message = j.error?.message ?? j.error ?? j.message ?? '';
19053
- }
19054
- catch (_) {
19055
- message || (message = text.slice(0, 200));
19056
- }
19057
- throw Object.assign(new Error(`${name}: HTTP ${res.status}${message ? ': ' + message : ''}`), { status: res.status, body: text });
19058
- }
19059
- if (!res.body)
19060
- throw new Error(`${name}: the response has no body`);
19061
- return res;
19062
- }
19063
- /** the SSE frames of a response body: `{ event, data }` (data: the joined data lines) */
19064
- async function* sse(res) {
19065
- const reader = res.body.getReader(), dec = new TextDecoder();
19066
- let buf = '', cr = '', done = false;
19067
- try {
19068
- for (;;) {
19069
- const r = await reader.read();
19070
- // line ends -> \n (a \r at a chunk's end waits: it may be half of a \r\n)
19071
- let s = cr + (r.done ? '\n\n' : dec.decode(r.value, { stream: true }));
19072
- cr = !r.done && s.endsWith('\r') ? '\r' : '';
19073
- if (cr)
19074
- s = s.slice(0, -1);
19075
- buf += s.replace(/\r\n?/g, '\n');
19076
- done = r.done;
19077
- let i;
19078
- while ((i = buf.indexOf('\n\n')) >= 0) {
19079
- const frame = buf.slice(0, i);
19080
- buf = buf.slice(i + 2);
19081
- let event = 'message', data = [];
19082
- for (const line of frame.split('\n')) {
19083
- if (!line || line[0] == ':')
19084
- continue;
19085
- const c = line.indexOf(':'), field = c < 0 ? line : line.slice(0, c);
19086
- const v = c < 0 ? '' : line.slice(c + 1).replace(/^ /, '');
19087
- if (field == 'event')
19088
- event = v;
19089
- else if (field == 'data')
19090
- data.push(v);
19091
- }
19092
- if (data.length)
19093
- yield { event, data: data.join('\n') };
19094
- }
19095
- if (done)
19096
- return;
19097
- }
19098
- }
19099
- finally {
19100
- if (!done)
19101
- reader.cancel().catch(() => { });
19102
- }
19103
- }
19104
- /** JSON.parse, or the text itself when it isn't JSON (a model's malformed tool arguments go to validation as is) */
19105
- const json = (s) => {
19106
- if (typeof s != 'string')
19107
- return s;
19108
- try {
19109
- return JSON.parse(s || '{}');
19110
- }
19111
- catch (_) {
19112
- return s;
19113
- }
19114
- };
19115
- /** a provider's usage as `{ inputTokens, outputTokens, totalTokens, reasoningTokens? }` (AI SDK names) */
19116
- const usageOf = (u) => u && {
19117
- inputTokens: u.input_tokens ?? u.prompt_tokens,
19118
- outputTokens: u.output_tokens ?? u.completion_tokens,
19119
- totalTokens: u.total_tokens,
19120
- ...((u.output_tokens_details ?? u.completion_tokens_details)?.reasoning_tokens != null && { reasoningTokens: (u.output_tokens_details ?? u.completion_tokens_details).reasoning_tokens }),
19121
- };
19122
- /** the parts of a message (a `content` string is one text part) */
19123
- const partsOf = (m) => m.parts ?? (typeof m.content == 'string' ? [{ type: 'text', text: m.content }] : []);
19124
- /** the tool name of a `tool-<name>` / `dynamic-tool` part, or '' */
19125
- const toolName = (p) => p.type == 'dynamic-tool' ? p.toolName : p.type.startsWith('tool-') ? p.type.slice(5) : '';
19126
- /** what a tool part sends back to the model, or undefined when it has no result yet */
19127
- const toolOutput = (p) => p.state == 'output-available' ? (typeof p.output == 'string' ? p.output : JSON.stringify(p.output ?? null))
19128
- : p.state == 'output-error' ? JSON.stringify({ error: p.errorText ?? 'error' })
19129
- : p.state == 'output-denied' ? JSON.stringify({ error: 'The user denied this tool call' })
19130
- : undefined;
19131
- // dev-only diagnostics (SYG675): the dev entry formats them (checks/chat.ts)
19132
- const dev$2 = (...a) => g.__SYGNAL_DIAGNOSTICS__?.chat?.(...a);
19133
-
19134
19595
  const EMPTY$1 = { type: 'object', properties: {} };
19135
19596
  function prepare(req, strict, seen, transport, dialect = 'openai', limits) {
19136
19597
  const sent = {};
19137
19598
  if (strict && typeof strict != 'function' && !seen.has('')) {
19138
19599
  seen.add('');
19139
- dev$2('SYG672', req, { transport });
19600
+ dev$8('SYG672', req, { transport });
19140
19601
  }
19141
19602
  const fn = typeof strict == 'function' ? strict : undefined;
19142
19603
  // what the request has left of `limits` (strictSchemas counts it down)
@@ -19149,7 +19610,7 @@ function prepare(req, strict, seen, transport, dialect = 'openai', limits) {
19149
19610
  return { schema: s.schema, strict: true };
19150
19611
  if (!seen.has(what)) {
19151
19612
  seen.add(what);
19152
- dev$2('SYG675', req, { tool: what, errors: s.errors });
19613
+ dev$8('SYG675', req, { tool: what, errors: s.errors });
19153
19614
  }
19154
19615
  return { schema, strict: false };
19155
19616
  };
@@ -19220,12 +19681,12 @@ function openResponses(options = {}) {
19220
19681
  c.done = 1;
19221
19682
  called++;
19222
19683
  const name = c.name ?? item?.name;
19223
- return { type: 'tool-call', id: c.call_id ?? item?.call_id ?? id, name, input: p.input(name, json(args ?? item?.arguments ?? c.args)) };
19684
+ return { type: 'tool-call', id: c.call_id ?? item?.call_id ?? id, name, input: p.input(name, json$1(args ?? item?.arguments ?? c.args)) };
19224
19685
  };
19225
19686
  for await (const { data } of sse(res)) {
19226
19687
  if (data == '[DONE]')
19227
19688
  break;
19228
- const e = json(data), t = e?.type;
19689
+ const e = json$1(data), t = e?.type;
19229
19690
  if (typeof t != 'string')
19230
19691
  continue;
19231
19692
  if (t == 'response.output_text.delta' || t == 'response.refusal.delta' || REASONING.test(t)) {
@@ -19317,13 +19778,13 @@ function chatCompletions(options = {}) {
19317
19778
  const done = function* () {
19318
19779
  for (const c of calls)
19319
19780
  if (c)
19320
- yield { type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json(c.args)) };
19781
+ yield { type: 'tool-call', id: c.id, name: c.name, input: p.input(c.name, json$1(c.args)) };
19321
19782
  calls = [];
19322
19783
  };
19323
19784
  for await (const { data } of sse(res)) {
19324
19785
  if (data == '[DONE]')
19325
19786
  break;
19326
- const e = json(data);
19787
+ const e = json$1(data);
19327
19788
  if (!e || typeof e != 'object')
19328
19789
  continue;
19329
19790
  if (e.error)
@@ -19411,7 +19872,7 @@ function uiMessageStream(url, options = {}) {
19411
19872
  for await (const { data } of sse(res)) {
19412
19873
  if (data == '[DONE]')
19413
19874
  break;
19414
- const c = json(data), t = c?.type;
19875
+ const c = json$1(data), t = c?.type;
19415
19876
  if (typeof t != 'string')
19416
19877
  continue;
19417
19878
  if (ONE.test(t)) {
@@ -20142,7 +20603,7 @@ const commandBar = (options = {}) => {
20142
20603
  ...(deny && { DENY: on(DOM, deny) }),
20143
20604
  };
20144
20605
  },
20145
- model: {
20606
+ model: fromPrevious({
20146
20607
  INPUT: (s, v) => typeof v == 'string' && v !== s.text ? { ...s, text: v } : ABORT,
20147
20608
  RUN: {
20148
20609
  STATE: (s, d, _n, p, o, k) => runStep(s, d, E(p, k, o))?.s || ABORT,
@@ -20178,7 +20639,7 @@ const commandBar = (options = {}) => {
20178
20639
  // a command ran (or was refused): the result; the input is cleared when it was this command
20179
20640
  DONE: (s, d) => s.status != 'running' ? ABORT
20180
20641
  : { ...s, status: 'ready', command: null, pending: null, result: d, text: d?.ok && s.text.trim() === d.command ? '' : s.text },
20181
- },
20642
+ }),
20182
20643
  })(options);
20183
20644
  return linked(b);
20184
20645
  };
@@ -20278,7 +20739,7 @@ function anthropicMessages(options = {}) {
20278
20739
  return { inputTokens: i, outputTokens: o, totalTokens: i + o, ...(u.output_tokens_details?.thinking_tokens != null && { reasoningTokens: u.output_tokens_details.thinking_tokens }) };
20279
20740
  };
20280
20741
  for await (const { data } of sse(res)) {
20281
- const e = json(data), t = e?.type;
20742
+ const e = json$1(data), t = e?.type;
20282
20743
  if (typeof t != 'string')
20283
20744
  continue;
20284
20745
  if (t == 'message_start') {
@@ -20314,9 +20775,9 @@ function anthropicMessages(options = {}) {
20314
20775
  if (b?.type == 'thinking' && b.signature)
20315
20776
  yield { type: 'reasoning', delta: '', providerMetadata: { anthropic: { signature: b.signature } } };
20316
20777
  else if (b?.type == 'tool_use')
20317
- yield { type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json(b.json) : b.input ?? {}) };
20778
+ yield { type: 'tool-call', id: b.id, name: b.name, input: p.input(b.name, b.json ? json$1(b.json) : b.input ?? {}) };
20318
20779
  else if (b?.type == 'server_tool_use')
20319
- yield { type: 'tool-call', id: b.id, name: b.name, input: b.json ? json(b.json) : b.input ?? {}, executed: true, providerExecuted: true };
20780
+ yield { type: 'tool-call', id: b.id, name: b.name, input: b.json ? json$1(b.json) : b.input ?? {}, executed: true, providerExecuted: true };
20320
20781
  }
20321
20782
  else if (t == 'message_delta') {
20322
20783
  if (e.delta?.stop_reason)
@@ -20456,10 +20917,10 @@ function agui(url, options = {}) {
20456
20917
  if (!c || c.done)
20457
20918
  return;
20458
20919
  c.done = 1;
20459
- open.push({ type: 'tool-call', id, name: c.name, input: json(c.args || '{}') });
20920
+ open.push({ type: 'tool-call', id, name: c.name, input: json$1(c.args || '{}') });
20460
20921
  };
20461
20922
  for await (const { data } of sse(res)) {
20462
- const e = json(data), t = e?.type;
20923
+ const e = json$1(data), t = e?.type;
20463
20924
  if (typeof t != 'string')
20464
20925
  continue;
20465
20926
  if (t == 'TEXT_MESSAGE_START') {
@@ -20507,7 +20968,7 @@ function agui(url, options = {}) {
20507
20968
  else if (t == 'TOOL_CALL_RESULT') {
20508
20969
  end(e.toolCallId);
20509
20970
  yield* take(e.toolCallId);
20510
- yield { type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json(e.content) : e.content };
20971
+ yield { type: 'tool-result', id: e.toolCallId, output: typeof e.content == 'string' ? json$1(e.content) : e.content };
20511
20972
  }
20512
20973
  else if (t == 'STATE_SNAPSHOT' || t == 'STATE_DELTA') {
20513
20974
  state = t == 'STATE_SNAPSHOT' ? e.snapshot : applyPatch(state, e.delta);
@@ -21708,7 +22169,7 @@ function composeBehavior(catalog, options = {}) {
21708
22169
  .map(({ key, name, value }) => ({ key, name, value })) }),
21709
22170
  };
21710
22171
  },
21711
- model: {
22172
+ model: fromPrevious({
21712
22173
  PROMPT: (s, p) => typeof p == 'string' && p !== s.prompt ? { ...s, prompt: p } : ABORT,
21713
22174
  COMPOSE: { HOST: host(start), [S]: sink(start) },
21714
22175
  REPLY: {
@@ -21750,7 +22211,7 @@ function composeBehavior(catalog, options = {}) {
21750
22211
  CONTROL: (s, c) => c && typeof c.key == 'string' && typeof c.name == 'string'
21751
22212
  ? { ...s, ui: { ...s.ui, [c.key]: { ...s.ui?.[c.key], [c.name]: c.value } } } : ABORT,
21752
22213
  DONE: () => ABORT,
21753
- },
22214
+ }),
21754
22215
  })(rest);
21755
22216
  }
21756
22217
 
@@ -25815,7 +26276,7 @@ const compose = /*#__PURE__*/ Object.assign((catalog, options) => composeBehavio
25815
26276
  * is gone); set: the array with that element replaced (the state unchanged when it is gone).
25816
26277
  * Side-effect free.
25817
26278
  */
25818
- const byId = (field, id, idKey = 'id') => ({
26279
+ const byId = (field, id, idKey = 'id') => Object.defineProperty({
25819
26280
  get: (s) => (Array.isArray(s?.[field]) ? s[field].find((x) => x?.[idKey] === id) : undefined),
25820
26281
  set: (s, v) => {
25821
26282
  const a = s?.[field];
@@ -25823,7 +26284,7 @@ const byId = (field, id, idKey = 'id') => ({
25823
26284
  return s;
25824
26285
  return { ...s, [field]: a.map((x) => (x?.[idKey] === id ? v : x)) };
25825
26286
  },
25826
- });
26287
+ }, BINDING, { value: [field, id, idKey] });
25827
26288
 
25828
26289
  /**
25829
26290
  * ESM ports of xstream's `flattenConcurrently` and `flattenSequentially` extras