@flamingo-stack/openframe-frontend-core 0.0.690-snapshot.20261001124055 → 0.0.690

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 (149) hide show
  1. package/dist/{chunk-2YVTGPM7.js → chunk-22RSJ57Z.js} +2 -2
  2. package/dist/{chunk-JSG7WZKV.js → chunk-25X43LSD.js} +4 -4
  3. package/dist/{chunk-UVUNBXS6.js → chunk-2KILTCND.js} +1 -6
  4. package/dist/chunk-2KILTCND.js.map +1 -0
  5. package/dist/{chunk-ESFPY5FF.cjs → chunk-2MGTNYHW.cjs} +100 -100
  6. package/dist/{chunk-ESFPY5FF.cjs.map → chunk-2MGTNYHW.cjs.map} +1 -1
  7. package/dist/{chunk-SDORFSFK.cjs → chunk-2OD2PPNO.cjs} +37 -37
  8. package/dist/{chunk-SDORFSFK.cjs.map → chunk-2OD2PPNO.cjs.map} +1 -1
  9. package/dist/{chunk-DC3G4XGP.js → chunk-2PQKZWWP.js} +6 -6
  10. package/dist/{chunk-XAOAYDGR.js → chunk-2V4KPBNZ.js} +3 -3
  11. package/dist/{chunk-H54RDLVC.cjs → chunk-3ZAIHRQH.cjs} +35 -35
  12. package/dist/{chunk-H54RDLVC.cjs.map → chunk-3ZAIHRQH.cjs.map} +1 -1
  13. package/dist/{chunk-TSNGVWQO.js → chunk-42RQ4NIL.js} +6 -6
  14. package/dist/{chunk-Y7ZEVTLC.js → chunk-4BUPMSLY.js} +3 -3
  15. package/dist/{chunk-SA4KQCNB.cjs → chunk-4HBYEZ3I.cjs} +82 -82
  16. package/dist/{chunk-SA4KQCNB.cjs.map → chunk-4HBYEZ3I.cjs.map} +1 -1
  17. package/dist/{chunk-LCENRN2N.cjs → chunk-54X75QTC.cjs} +14 -14
  18. package/dist/{chunk-LCENRN2N.cjs.map → chunk-54X75QTC.cjs.map} +1 -1
  19. package/dist/{chunk-Z5FHXCXF.js → chunk-5A3WSWZD.js} +8 -8
  20. package/dist/{chunk-WZYIAKIH.cjs → chunk-5CBQBTGN.cjs} +7 -7
  21. package/dist/{chunk-WZYIAKIH.cjs.map → chunk-5CBQBTGN.cjs.map} +1 -1
  22. package/dist/{chunk-KMW2PXBU.js → chunk-AIXPQT3G.js} +11 -52
  23. package/dist/chunk-AIXPQT3G.js.map +1 -0
  24. package/dist/{chunk-LNKLB7GM.cjs → chunk-BJHM2OEY.cjs} +17 -17
  25. package/dist/{chunk-LNKLB7GM.cjs.map → chunk-BJHM2OEY.cjs.map} +1 -1
  26. package/dist/{chunk-N3526CTH.cjs → chunk-BOM6MMWB.cjs} +3 -3
  27. package/dist/{chunk-N3526CTH.cjs.map → chunk-BOM6MMWB.cjs.map} +1 -1
  28. package/dist/{chunk-N4S7RBUM.cjs → chunk-D7BFS7N6.cjs} +3 -3
  29. package/dist/{chunk-N4S7RBUM.cjs.map → chunk-D7BFS7N6.cjs.map} +1 -1
  30. package/dist/{chunk-C2SVJVEB.js → chunk-DLRE5Y3G.js} +2 -2
  31. package/dist/{chunk-USKQURNF.cjs → chunk-DMXI5D2Q.cjs} +3 -3
  32. package/dist/{chunk-USKQURNF.cjs.map → chunk-DMXI5D2Q.cjs.map} +1 -1
  33. package/dist/{chunk-7WYWPELF.js → chunk-DOUG4UEZ.js} +11 -11
  34. package/dist/{chunk-Y3BC7TOJ.cjs → chunk-FCQR6VY4.cjs} +26 -26
  35. package/dist/{chunk-Y3BC7TOJ.cjs.map → chunk-FCQR6VY4.cjs.map} +1 -1
  36. package/dist/{chunk-7EPGJOKN.js → chunk-HLEXYTOE.js} +4 -4
  37. package/dist/{chunk-MIZU2OTO.cjs → chunk-JGVJRUGB.cjs} +4 -4
  38. package/dist/{chunk-MIZU2OTO.cjs.map → chunk-JGVJRUGB.cjs.map} +1 -1
  39. package/dist/{chunk-DDWLF2JV.cjs → chunk-KZXKT5AX.cjs} +14 -14
  40. package/dist/{chunk-DDWLF2JV.cjs.map → chunk-KZXKT5AX.cjs.map} +1 -1
  41. package/dist/{chunk-4N2JOPAQ.cjs → chunk-MDYTAKJ5.cjs} +22 -22
  42. package/dist/{chunk-4N2JOPAQ.cjs.map → chunk-MDYTAKJ5.cjs.map} +1 -1
  43. package/dist/{chunk-DVSPRY2V.cjs → chunk-NMXL34Z2.cjs} +3 -3
  44. package/dist/{chunk-DVSPRY2V.cjs.map → chunk-NMXL34Z2.cjs.map} +1 -1
  45. package/dist/{chunk-CMYDGVTZ.js → chunk-OC3QH5WG.js} +2 -2
  46. package/dist/{chunk-GZRU2A4L.cjs → chunk-OD2WMUAO.cjs} +197 -197
  47. package/dist/{chunk-GZRU2A4L.cjs.map → chunk-OD2WMUAO.cjs.map} +1 -1
  48. package/dist/{chunk-DZMDG7PC.js → chunk-OQVHGPHA.js} +3 -3
  49. package/dist/{chunk-Y3Q76RAA.cjs → chunk-P27OGSQL.cjs} +37 -37
  50. package/dist/{chunk-Y3Q76RAA.cjs.map → chunk-P27OGSQL.cjs.map} +1 -1
  51. package/dist/{chunk-HM7ZGABI.js → chunk-Q76YVNCQ.js} +2 -2
  52. package/dist/{chunk-53O6PQ3F.cjs → chunk-QNWQQ4RT.cjs} +7 -7
  53. package/dist/{chunk-53O6PQ3F.cjs.map → chunk-QNWQQ4RT.cjs.map} +1 -1
  54. package/dist/{chunk-BXE2RZEV.cjs → chunk-QUGV2GT3.cjs} +34 -34
  55. package/dist/{chunk-BXE2RZEV.cjs.map → chunk-QUGV2GT3.cjs.map} +1 -1
  56. package/dist/{chunk-SC44YJCF.js → chunk-RF2EYX3B.js} +2 -2
  57. package/dist/{chunk-FNMQ2J44.js → chunk-S5M5SEW2.js} +5 -5
  58. package/dist/{chunk-KVDLL4LU.js → chunk-TBJWPNHU.js} +4 -4
  59. package/dist/chunk-TBJWPNHU.js.map +1 -0
  60. package/dist/{chunk-MJI5GVT7.js → chunk-TSENCZUL.js} +11 -11
  61. package/dist/{chunk-XBYF4SGD.js → chunk-U4OJCJTH.js} +4 -4
  62. package/dist/{chunk-GDZ7IC5B.js → chunk-UAKWSR3U.js} +2 -2
  63. package/dist/{chunk-ZLYLHP64.cjs → chunk-UFXV64WQ.cjs} +9 -9
  64. package/dist/{chunk-ZLYLHP64.cjs.map → chunk-UFXV64WQ.cjs.map} +1 -1
  65. package/dist/{chunk-4HZXOQRM.js → chunk-V5NOURSR.js} +5 -5
  66. package/dist/{chunk-CWXRZVDI.cjs → chunk-VLWUKFQE.cjs} +710 -751
  67. package/dist/chunk-VLWUKFQE.cjs.map +1 -0
  68. package/dist/{chunk-5N2ZSB42.cjs → chunk-X4WBGDNW.cjs} +18 -18
  69. package/dist/{chunk-5N2ZSB42.cjs.map → chunk-X4WBGDNW.cjs.map} +1 -1
  70. package/dist/{chunk-6Z26ZMR4.js → chunk-XRSJER2B.js} +4 -4
  71. package/dist/{chunk-I27PWVWE.cjs → chunk-YCKGUQA2.cjs} +12 -17
  72. package/dist/chunk-YCKGUQA2.cjs.map +1 -0
  73. package/dist/{chunk-Y7ZQZOE6.js → chunk-Z622ACFD.js} +5 -5
  74. package/dist/{chunk-BXAGR3PO.cjs → chunk-ZBQZJAC7.cjs} +7 -7
  75. package/dist/chunk-ZBQZJAC7.cjs.map +1 -0
  76. package/dist/components/case-studies/index.cjs +20 -20
  77. package/dist/components/case-studies/index.js +7 -7
  78. package/dist/components/chat/index.cjs +4 -4
  79. package/dist/components/chat/index.js +3 -3
  80. package/dist/components/contact/index.cjs +8 -8
  81. package/dist/components/contact/index.js +7 -7
  82. package/dist/components/docs/index.cjs +11 -11
  83. package/dist/components/docs/index.js +10 -10
  84. package/dist/components/embeds/index.cjs +8 -8
  85. package/dist/components/embeds/index.js +7 -7
  86. package/dist/components/faq/index.cjs +9 -9
  87. package/dist/components/faq/index.js +8 -8
  88. package/dist/components/features/index.cjs +7 -7
  89. package/dist/components/features/index.js +6 -6
  90. package/dist/components/help-center-pages/index.cjs +101 -101
  91. package/dist/components/help-center-pages/index.js +21 -21
  92. package/dist/components/index.cjs +65 -65
  93. package/dist/components/index.js +21 -21
  94. package/dist/components/meeting-scheduler/index.cjs +58 -58
  95. package/dist/components/meeting-scheduler/index.js +8 -8
  96. package/dist/components/navigation/index.cjs +10 -10
  97. package/dist/components/navigation/index.js +9 -9
  98. package/dist/components/onboarding-guides/index.cjs +11 -11
  99. package/dist/components/onboarding-guides/index.js +10 -10
  100. package/dist/components/related-content/index.cjs +6 -6
  101. package/dist/components/related-content/index.js +5 -5
  102. package/dist/components/tickets/index.cjs +12 -12
  103. package/dist/components/tickets/index.js +11 -11
  104. package/dist/components/tickets/ticket-live-provider.d.ts.map +1 -1
  105. package/dist/components/ui/index.cjs +7 -7
  106. package/dist/components/ui/index.js +6 -6
  107. package/dist/hooks/index.cjs +4 -4
  108. package/dist/hooks/index.js +3 -3
  109. package/dist/index.cjs +12 -12
  110. package/dist/index.js +10 -10
  111. package/dist/utils/embed-authed-fetch.d.ts +0 -18
  112. package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
  113. package/dist/utils/index.cjs +6 -50
  114. package/dist/utils/index.cjs.map +1 -1
  115. package/dist/utils/index.js +6 -50
  116. package/dist/utils/index.js.map +1 -1
  117. package/dist/utils/sse-subscription.d.ts +11 -29
  118. package/dist/utils/sse-subscription.d.ts.map +1 -1
  119. package/package.json +1 -1
  120. package/src/components/tickets/ticket-live-provider.tsx +12 -14
  121. package/src/utils/embed-authed-fetch.ts +0 -21
  122. package/src/utils/sse-subscription.ts +20 -107
  123. package/dist/chunk-BXAGR3PO.cjs.map +0 -1
  124. package/dist/chunk-CWXRZVDI.cjs.map +0 -1
  125. package/dist/chunk-I27PWVWE.cjs.map +0 -1
  126. package/dist/chunk-KMW2PXBU.js.map +0 -1
  127. package/dist/chunk-KVDLL4LU.js.map +0 -1
  128. package/dist/chunk-UVUNBXS6.js.map +0 -1
  129. /package/dist/{chunk-2YVTGPM7.js.map → chunk-22RSJ57Z.js.map} +0 -0
  130. /package/dist/{chunk-JSG7WZKV.js.map → chunk-25X43LSD.js.map} +0 -0
  131. /package/dist/{chunk-DC3G4XGP.js.map → chunk-2PQKZWWP.js.map} +0 -0
  132. /package/dist/{chunk-XAOAYDGR.js.map → chunk-2V4KPBNZ.js.map} +0 -0
  133. /package/dist/{chunk-TSNGVWQO.js.map → chunk-42RQ4NIL.js.map} +0 -0
  134. /package/dist/{chunk-Y7ZEVTLC.js.map → chunk-4BUPMSLY.js.map} +0 -0
  135. /package/dist/{chunk-Z5FHXCXF.js.map → chunk-5A3WSWZD.js.map} +0 -0
  136. /package/dist/{chunk-C2SVJVEB.js.map → chunk-DLRE5Y3G.js.map} +0 -0
  137. /package/dist/{chunk-7WYWPELF.js.map → chunk-DOUG4UEZ.js.map} +0 -0
  138. /package/dist/{chunk-7EPGJOKN.js.map → chunk-HLEXYTOE.js.map} +0 -0
  139. /package/dist/{chunk-CMYDGVTZ.js.map → chunk-OC3QH5WG.js.map} +0 -0
  140. /package/dist/{chunk-DZMDG7PC.js.map → chunk-OQVHGPHA.js.map} +0 -0
  141. /package/dist/{chunk-HM7ZGABI.js.map → chunk-Q76YVNCQ.js.map} +0 -0
  142. /package/dist/{chunk-SC44YJCF.js.map → chunk-RF2EYX3B.js.map} +0 -0
  143. /package/dist/{chunk-FNMQ2J44.js.map → chunk-S5M5SEW2.js.map} +0 -0
  144. /package/dist/{chunk-MJI5GVT7.js.map → chunk-TSENCZUL.js.map} +0 -0
  145. /package/dist/{chunk-XBYF4SGD.js.map → chunk-U4OJCJTH.js.map} +0 -0
  146. /package/dist/{chunk-GDZ7IC5B.js.map → chunk-UAKWSR3U.js.map} +0 -0
  147. /package/dist/{chunk-4HZXOQRM.js.map → chunk-V5NOURSR.js.map} +0 -0
  148. /package/dist/{chunk-6Z26ZMR4.js.map → chunk-XRSJER2B.js.map} +0 -0
  149. /package/dist/{chunk-Y7ZQZOE6.js.map → chunk-Z622ACFD.js.map} +0 -0
@@ -16,26 +16,11 @@
16
16
  * (INCLUDING `: keepalive` comment lines — they reset the timer
17
17
  * before frame parsing drops them) count as life. Silence beyond
18
18
  * `silenceTimeoutMs` (default 45s = 3× keepalive) aborts + reconnects.
19
- * - Terminal responses: any 4xx except 408/429 — the `x-block-layer`
20
- * header, when readable, is logged for attribution only, never used
21
- * as a retry gate (proxies may strip it); and 204 → a distinct
22
- * `no-stream` status (the caller decides when to try again).
23
- * Backoff-retry is for transport errors, 408/429, and 5xx.
24
- * - Long-lived streams (`pauseWhenHidden`) never stay terminal, because
25
- * a page a desktop shell keeps open for days would otherwise stay
26
- * dead until reloaded:
27
- * · 401 is retried on a slow timer (30s doubling to 5min, plus
28
- * jitter). The default fetch already refreshed + retried once, so
29
- * this is a session the host could not renew yet — and every
30
- * attempt makes it try again, which rotates a refresh token. So
31
- * visible / focus do NOT shortcut this timer. What does: a new
32
- * credential reported by the host (`EmbedAuthAdapter.subscribe`,
33
- * default fetch only), plus the unconditional reconnect paths
34
- * (`online`, resuming after a suspend, `reconnectNow()`).
35
- * · Any other terminal 4xx is retried on visible / window focus, no
36
- * sooner than 30s after the last connect attempt.
37
- * Finite streams keep the terminal behavior: their monitors have no
38
- * auth adapter to recover with and must reach a final state.
19
+ * - Terminal responses (NO retry): any 4xx except 408/429 — the
20
+ * `x-block-layer` header, when readable, is logged for attribution
21
+ * only, never used as a retry gate (proxies may strip it); and
22
+ * 204 → a distinct `no-stream` status (the caller decides when to
23
+ * try again). Backoff-retry is for transport errors, 408/429, and 5xx.
39
24
  *
40
25
  * THE common SSE client for lib + hosts — exported from the `utils`
41
26
  * barrel. Consumers: `TicketLiveProvider` (lib) and the hub's
@@ -46,8 +31,7 @@
46
31
  * never-terminating reconnect loop re-opens the finished stream.
47
32
  */
48
33
  /** Transport-level status. `suspended` = paused by `pauseWhenHidden`
49
- * after the hidden grace elapsed (resumes automatically on visible).
50
- * `terminal` is final only without `pauseWhenHidden`. */
34
+ * after the hidden grace elapsed (resumes automatically on visible). */
51
35
  export type SseTransportStatus = 'connecting' | 'open' | 'reconnecting' | 'no-stream' | 'terminal' | 'suspended' | 'closed';
52
36
  export interface SseSubscriptionOptions {
53
37
  url: string;
@@ -75,11 +59,10 @@ export interface SseSubscriptionOptions {
75
59
  /**
76
60
  * Pause the subscription while the tab is hidden (45s grace so
77
61
  * Cmd-Tab thrash doesn't churn connections), resume + reconnect on
78
- * visible, and reconnect on `online`. Also makes the stream
79
- * self-healing after a 401 or terminal 4xx (see the module header).
80
- * OPT-IN — short-lived streams (workflow/invocation monitors) must
81
- * keep running in background tabs. Long-lived per-user streams should
82
- * enable it (scale relief: a hidden tab holds no server invocation).
62
+ * visible, and reconnect on `online`. OPT-IN — short-lived streams
63
+ * (workflow/invocation monitors) must keep running in background
64
+ * tabs. Long-lived per-user streams should enable it (scale relief:
65
+ * a hidden tab holds no server invocation).
83
66
  */
84
67
  pauseWhenHidden?: boolean;
85
68
  /** Silence threshold before the connection is presumed dead. MUST be
@@ -93,8 +76,7 @@ export interface SseSubscription {
93
76
  /** Permanently stop — no further reconnects, no callbacks. */
94
77
  close: () => void;
95
78
  /** Drop the current connection (if any) and reconnect immediately,
96
- * resetting the transport backoff (a 401 backoff carries over until a
97
- * successful open). No-op after `close()`. */
79
+ * resetting backoff. No-op after `close()`. */
98
80
  reconnectNow: () => void;
99
81
  }
100
82
  export declare function createSseSubscription(options: SseSubscriptionOptions): SseSubscription;
@@ -1 +1 @@
1
- {"version":3,"file":"sse-subscription.d.ts","sourceRoot":"","sources":["../../src/utils/sse-subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAIH;;0DAE0D;AAC1D,MAAM,MAAM,kBAAkB,GAC5B,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE7F,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnE;;;2EAGuE;IACvE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACtD;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;2DAEuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;mDAE+C;IAC/C,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AA0BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CA2atF"}
1
+ {"version":3,"file":"sse-subscription.d.ts","sourceRoot":"","sources":["../../src/utils/sse-subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAIH;yEACyE;AACzE,MAAM,MAAM,kBAAkB,GAC5B,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE7F,MAAM,WAAW,sBAAsB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnE;;;2EAGuE;IACvE,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACtD;;;;;;;;;;;OAWG;IACH,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;2DAEuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;oDACgD;IAChD,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAmBD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CA6WtF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.690-snapshot.20261001124055",
3
+ "version": "0.0.690",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -3,10 +3,10 @@
3
3
  /**
4
4
  * TicketLiveProvider — the ONE client home for support-ticket realtime.
5
5
  *
6
- * DOMAIN logic only — connection lifecycle is owned entirely by the
7
- * common SSE client (`createSseSubscription`; its module header lists
8
- * the policies); this provider consumes its consolidated
9
- * `onConnectedChange` signal.
6
+ * DOMAIN logic only — connection lifecycle (subscribed-confirm, server
7
+ * retry grace, failed-reconnect pacing, hidden-tab suspend/resume) is
8
+ * owned entirely by the common SSE client (`createSseSubscription`);
9
+ * this provider consumes its consolidated `onConnectedChange` signal.
10
10
  *
11
11
  * Owns:
12
12
  * - the unread summary — delivered EXCLUSIVELY by the stream
@@ -20,9 +20,8 @@
20
20
  * - the ticket-contract `no-stream` (204) retry policy.
21
21
  *
22
22
  * There is NO polling anywhere and NO summary endpoint. Hosts whose
23
- * stream endpoint 404s or is absent get no unread indication — the
24
- * indication IS a realtime feature (a 404ing endpoint is re-probed on
25
- * presence, rate-limited by the client).
23
+ * stream endpoint 404s (terminal) or is absent get no unread indication
24
+ * — the indication IS a realtime feature.
26
25
  *
27
26
  * Invalidation semantics on events — deliberately `invalidateQueries`,
28
27
  * NOT `setQueryData` patches: the ticket hooks run `gcTime: 0`, so
@@ -287,10 +286,11 @@ export function TicketLiveProvider({ children, enabled = true }: TicketLiveProvi
287
286
  };
288
287
 
289
288
  const handleStatus = (status: SseTransportStatus) => {
290
- // The client owns connection lifecycle. The only transport status
291
- // with DOMAIN meaning is the ticket contract's `no-stream` (204 =
292
- // zero owned tickets): remember it so a domain signal (own
293
- // create_ticket, window focus) can retry.
289
+ // The client owns connection lifecycle (confirm timeout, server
290
+ // retry grace, failed-reconnect pacing, hidden-suspend). The only
291
+ // transport status with DOMAIN meaning is the ticket contract's
292
+ // `no-stream` (204 = zero owned tickets): remember it so a domain
293
+ // signal (own create_ticket, window focus) can retry.
294
294
  if (status === 'open') noStreamRef.current = false;
295
295
  if (status === 'no-stream') noStreamRef.current = true;
296
296
  };
@@ -324,9 +324,7 @@ export function TicketLiveProvider({ children, enabled = true }: TicketLiveProvi
324
324
  });
325
325
 
326
326
  const retryIfIdle = () => {
327
- // After a 204 (no-stream), a focus signal is the retry trigger. Only
328
- // 204 — the client's contract leaves that retry to the caller; it
329
- // retries terminal / 401 states on its own.
327
+ // After a 204 (no-stream), a focus signal is the retry trigger.
330
328
  if (noStreamRef.current) {
331
329
  noStreamRef.current = false;
332
330
  subscriptionRef.current?.reconnectNow();
@@ -99,16 +99,6 @@ export interface EmbedAuthAdapter {
99
99
  * remains the rule, this is the narrow exception.
100
100
  */
101
101
  allowedOrigins?: string[];
102
- /**
103
- * Optional: call `listener` whenever `getHeaders()` starts returning a NEW
104
- * credential (a rotation the host ran on its own — e.g. a native shell's
105
- * background refresh). Returns an unsubscribe. Lets a long-lived surface
106
- * that stopped after a 401 retry as soon as a usable credential exists,
107
- * instead of on its next backoff step. Fire only on a real change: a
108
- * listener may respond with a request, and through `refresh` that request
109
- * can lead the host to notify again.
110
- */
111
- subscribe?: (listener: () => void) => () => void;
112
102
  }
113
103
 
114
104
  /**
@@ -166,17 +156,6 @@ export function hasEmbedAuthAdapter(): boolean {
166
156
  return getRegisteredAuthAdapter() !== null;
167
157
  }
168
158
 
169
- /**
170
- * Subscribe to the registered adapter's credential changes
171
- * (`EmbedAuthAdapter.subscribe`). A no-op when no adapter is registered, or
172
- * the adapter doesn't report changes. Reads the adapter once, at call time:
173
- * an adapter registered (or replaced) after a subscriber starts is not
174
- * picked up — register it before the surfaces that need it mount.
175
- */
176
- export function subscribeEmbedCredentialChange(listener: () => void): () => void {
177
- return getRegisteredAuthAdapter()?.subscribe?.(listener) ?? (() => {});
178
- }
179
-
180
159
  /**
181
160
  * Whether `url` is an asset the browser CANNOT load natively because its
182
161
  * auth rides in request headers.
@@ -16,26 +16,11 @@
16
16
  * (INCLUDING `: keepalive` comment lines — they reset the timer
17
17
  * before frame parsing drops them) count as life. Silence beyond
18
18
  * `silenceTimeoutMs` (default 45s = 3× keepalive) aborts + reconnects.
19
- * - Terminal responses: any 4xx except 408/429 — the `x-block-layer`
20
- * header, when readable, is logged for attribution only, never used
21
- * as a retry gate (proxies may strip it); and 204 → a distinct
22
- * `no-stream` status (the caller decides when to try again).
23
- * Backoff-retry is for transport errors, 408/429, and 5xx.
24
- * - Long-lived streams (`pauseWhenHidden`) never stay terminal, because
25
- * a page a desktop shell keeps open for days would otherwise stay
26
- * dead until reloaded:
27
- * · 401 is retried on a slow timer (30s doubling to 5min, plus
28
- * jitter). The default fetch already refreshed + retried once, so
29
- * this is a session the host could not renew yet — and every
30
- * attempt makes it try again, which rotates a refresh token. So
31
- * visible / focus do NOT shortcut this timer. What does: a new
32
- * credential reported by the host (`EmbedAuthAdapter.subscribe`,
33
- * default fetch only), plus the unconditional reconnect paths
34
- * (`online`, resuming after a suspend, `reconnectNow()`).
35
- * · Any other terminal 4xx is retried on visible / window focus, no
36
- * sooner than 30s after the last connect attempt.
37
- * Finite streams keep the terminal behavior: their monitors have no
38
- * auth adapter to recover with and must reach a final state.
19
+ * - Terminal responses (NO retry): any 4xx except 408/429 — the
20
+ * `x-block-layer` header, when readable, is logged for attribution
21
+ * only, never used as a retry gate (proxies may strip it); and
22
+ * 204 → a distinct `no-stream` status (the caller decides when to
23
+ * try again). Backoff-retry is for transport errors, 408/429, and 5xx.
39
24
  *
40
25
  * THE common SSE client for lib + hosts — exported from the `utils`
41
26
  * barrel. Consumers: `TicketLiveProvider` (lib) and the hub's
@@ -46,11 +31,10 @@
46
31
  * never-terminating reconnect loop re-opens the finished stream.
47
32
  */
48
33
 
49
- import { embedAuthedFetch, subscribeEmbedCredentialChange } from './embed-authed-fetch';
34
+ import { embedAuthedFetch } from './embed-authed-fetch';
50
35
 
51
36
  /** Transport-level status. `suspended` = paused by `pauseWhenHidden`
52
- * after the hidden grace elapsed (resumes automatically on visible).
53
- * `terminal` is final only without `pauseWhenHidden`. */
37
+ * after the hidden grace elapsed (resumes automatically on visible). */
54
38
  export type SseTransportStatus =
55
39
  'connecting' | 'open' | 'reconnecting' | 'no-stream' | 'terminal' | 'suspended' | 'closed';
56
40
 
@@ -80,11 +64,10 @@ export interface SseSubscriptionOptions {
80
64
  /**
81
65
  * Pause the subscription while the tab is hidden (45s grace so
82
66
  * Cmd-Tab thrash doesn't churn connections), resume + reconnect on
83
- * visible, and reconnect on `online`. Also makes the stream
84
- * self-healing after a 401 or terminal 4xx (see the module header).
85
- * OPT-IN — short-lived streams (workflow/invocation monitors) must
86
- * keep running in background tabs. Long-lived per-user streams should
87
- * enable it (scale relief: a hidden tab holds no server invocation).
67
+ * visible, and reconnect on `online`. OPT-IN — short-lived streams
68
+ * (workflow/invocation monitors) must keep running in background
69
+ * tabs. Long-lived per-user streams should enable it (scale relief:
70
+ * a hidden tab holds no server invocation).
88
71
  */
89
72
  pauseWhenHidden?: boolean;
90
73
  /** Silence threshold before the connection is presumed dead. MUST be
@@ -99,8 +82,7 @@ export interface SseSubscription {
99
82
  /** Permanently stop — no further reconnects, no callbacks. */
100
83
  close: () => void;
101
84
  /** Drop the current connection (if any) and reconnect immediately,
102
- * resetting the transport backoff (a 401 backoff carries over until a
103
- * successful open). No-op after `close()`. */
85
+ * resetting backoff. No-op after `close()`. */
104
86
  reconnectNow: () => void;
105
87
  }
106
88
 
@@ -120,13 +102,6 @@ const SERVER_RETRY_GRACE_MS = 90_000;
120
102
  const FAILED_RECONNECT_DELAY_MS = 30_000;
121
103
  /** Grace before a hidden tab's subscription is suspended. */
122
104
  const HIDDEN_GRACE_MS = 45_000;
123
- /** 401 retry backoff for long-lived streams — slow on purpose, see the
124
- * module header. */
125
- const AUTH_RETRY_INITIAL_MS = 30_000;
126
- const AUTH_RETRY_MAX_MS = 300_000;
127
- /** Minimum time since the last connect attempt before a presence signal
128
- * retries a terminal stream — focus can fire many times a minute. */
129
- const PRESENCE_RETRY_MIN_INTERVAL_MS = 30_000;
130
105
 
131
106
  export function createSseSubscription(options: SseSubscriptionOptions): SseSubscription {
132
107
  const {
@@ -144,18 +119,6 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
144
119
  let closed = false;
145
120
  let suspended = false;
146
121
  let attempt = 0;
147
- let authAttempt = 0;
148
- /** Stopped on a terminal response — what a presence signal retries.
149
- * Cleared by every connect(). */
150
- let stalled = false;
151
- /** A 401 retry timer is pending — the only state a host credential change
152
- * acts on, so routine rotations never tear down a live stream. Cleared by
153
- * every connect(): a change reported while a request is in flight is
154
- * indistinguishable from the default fetch's own refresh, so it is not
155
- * acted on (a 401 that follows waits for the next backoff step) — acting
156
- * on it would let a 401 → refresh → notify cycle loop. */
157
- let awaitingAuth = false;
158
- let lastConnectAt = 0;
159
122
  /** Connection generation — bumped by each connect() and by
160
123
  * reconnectNow(). A stale loop (aborted, still unwinding) compares
161
124
  * its captured generation before scheduling a reconnect, so an
@@ -260,8 +223,7 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
260
223
  confirmTimerId = clearLifecycleTimer(confirmTimerId);
261
224
  serverGraceTimerId = clearLifecycleTimer(serverGraceTimerId);
262
225
  failedDelayTimerId = clearLifecycleTimer(failedDelayTimerId);
263
- // NOT the hidden-grace timer: it tracks the page's visibility, not this
264
- // connection, and a reconnect while hidden must still suspend on time.
226
+ hiddenGraceTimerId = clearLifecycleTimer(hiddenGraceTimerId);
265
227
  };
266
228
 
267
229
  const armSilenceTimer = () => {
@@ -273,29 +235,16 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
273
235
  }, silenceTimeoutMs);
274
236
  };
275
237
 
276
- /** `delayMs` overrides the transport backoff (auth-failure pacing). */
277
- const scheduleReconnect = (delayMs?: number) => {
238
+ const scheduleReconnect = () => {
278
239
  if (closed || suspended || retryTimerId) return;
279
- if (failedDelayTimerId) {
280
- // A server `reconnect_failed` already scheduled the next attempt at the
281
- // no-stampede delay. A transport retry here would jump that queue, and
282
- // the pending timer would then abort the connection it opened.
283
- setStatus('reconnecting');
284
- return;
285
- }
286
- let backoff = delayMs;
287
- if (backoff === undefined) {
288
- backoff = Math.min(maxBackoffMs, initialBackoffMs * 2 ** attempt);
289
- attempt += 1;
290
- }
240
+ setStatus('reconnecting');
241
+ const backoff = Math.min(maxBackoffMs, initialBackoffMs * 2 ** attempt);
291
242
  const jitter = backoff * 0.25 * Math.random();
243
+ attempt += 1;
292
244
  retryTimerId = setTimeout(() => {
293
245
  retryTimerId = null;
294
246
  void connect();
295
247
  }, backoff + jitter);
296
- // After arming: a status callback that calls reconnectNow() must find
297
- // the timer to clear, or it would later fire behind a live stream.
298
- setStatus('reconnecting');
299
248
  };
300
249
 
301
250
  const parseFrame = (rawFrame: string) => {
@@ -336,9 +285,6 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
336
285
  async function connect(): Promise<void> {
337
286
  if (closed || suspended) return;
338
287
  const gen = ++generation;
339
- stalled = false;
340
- awaitingAuth = false;
341
- lastConnectAt = Date.now();
342
288
  setStatus('connecting');
343
289
  abortController = new AbortController();
344
290
 
@@ -370,23 +316,12 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
370
316
  scheduleReconnect();
371
317
  return;
372
318
  }
373
- // x-block-layer (proxy surface block) is logged for attribution
374
- // when readable — never gates the decision.
319
+ // Terminal 4xx. x-block-layer (proxy surface block) is logged for
320
+ // attribution when readable — never gates the decision.
375
321
  const blockLayer = response.headers.get('x-block-layer');
376
- const blockNote = blockLayer ? ` (x-block-layer: ${blockLayer})` : '';
377
- if (response.status === 401 && pauseWhenHidden) {
378
- const delayMs = Math.min(AUTH_RETRY_MAX_MS, AUTH_RETRY_INITIAL_MS * 2 ** authAttempt);
379
- authAttempt += 1;
380
- console.warn(`[sse-subscription] 401 for ${url}${blockNote} — retrying in ~${Math.round(delayMs / 1000)}s`);
381
- awaitingAuth = true;
382
- scheduleReconnect(delayMs);
383
- return;
384
- }
385
322
  console.warn(
386
- `[sse-subscription] terminal ${response.status} for ${url}${blockNote} — ${pauseWhenHidden ? 'retrying on presence' : 'not retrying'}`,
323
+ `[sse-subscription] terminal ${response.status} for ${url}${blockLayer ? ` (x-block-layer: ${blockLayer})` : ''} — not retrying`,
387
324
  );
388
- // Before the status callback, which may re-enter via reconnectNow().
389
- stalled = pauseWhenHidden;
390
325
  setStatus('terminal');
391
326
  return;
392
327
  }
@@ -397,7 +332,6 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
397
332
 
398
333
  // Successful open — reset backoff, start liveness accounting.
399
334
  attempt = 0;
400
- authAttempt = 0;
401
335
  setStatus('open');
402
336
  armSilenceTimer();
403
337
  // Transport open ≠ connected: require the server's `subscribed`
@@ -484,12 +418,6 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
484
418
  abortController?.abort();
485
419
  setStatus('suspended');
486
420
  };
487
- /** A person is back at the page: the moment a fixed backend or a
488
- * restored permission can be noticed without a reload. */
489
- const retryIfStalled = () => {
490
- if (!stalled || Date.now() - lastConnectAt < PRESENCE_RETRY_MIN_INTERVAL_MS) return;
491
- doReconnect();
492
- };
493
421
  const onVisibilityChange = () => {
494
422
  if (document.visibilityState === 'hidden') {
495
423
  if (!hiddenGraceTimerId) {
@@ -507,27 +435,15 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
507
435
  if (suspended) {
508
436
  suspended = false;
509
437
  void connect();
510
- return;
511
438
  }
512
- retryIfStalled();
513
439
  };
514
440
  const onOnline = () => {
515
441
  if (!suspended) doReconnect();
516
442
  };
517
- // Only requests that carry the adapter's credentials can be rescued by a
518
- // new one, and only long-lived streams ever wait out a 401. Subscribed
519
- // before the DOM listeners, so a throwing host adapter leaks none of them.
520
- const unsubscribeCredentials =
521
- pauseWhenHidden && fetchImpl === embedAuthedFetch
522
- ? subscribeEmbedCredentialChange(() => {
523
- if (awaitingAuth) doReconnect();
524
- })
525
- : null;
526
443
  const listenersActive = pauseWhenHidden && typeof document !== 'undefined' && typeof window !== 'undefined';
527
444
  if (listenersActive) {
528
445
  document.addEventListener('visibilitychange', onVisibilityChange);
529
446
  window.addEventListener('online', onOnline);
530
- window.addEventListener('focus', retryIfStalled);
531
447
  }
532
448
 
533
449
  void connect();
@@ -538,14 +454,11 @@ export function createSseSubscription(options: SseSubscriptionOptions): SseSubsc
538
454
  closed = true;
539
455
  generation += 1;
540
456
  clearTimers();
541
- hiddenGraceTimerId = clearLifecycleTimer(hiddenGraceTimerId);
542
457
  abortController?.abort();
543
458
  if (listenersActive) {
544
459
  document.removeEventListener('visibilitychange', onVisibilityChange);
545
460
  window.removeEventListener('online', onOnline);
546
- window.removeEventListener('focus', retryIfStalled);
547
461
  }
548
- unsubscribeCredentials?.();
549
462
  setConnected(false);
550
463
  onStatusChange?.('closed');
551
464
  },
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-BXAGR3PO.cjs","../src/components/tickets/ticket-live-provider.tsx","../src/components/tickets/types.ts"],"names":[],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-BXAGR3PO.cjs","sourcesContent":[null,"'use client';\n\n/**\n * TicketLiveProvider — the ONE client home for support-ticket realtime.\n *\n * DOMAIN logic only — connection lifecycle is owned entirely by the\n * common SSE client (`createSseSubscription`; its module header lists\n * the policies); this provider consumes its consolidated\n * `onConnectedChange` signal.\n *\n * Owns:\n * - the unread summary — delivered EXCLUSIVELY by the stream\n * (`ticket-summary` frames on connect + debounced after events) and\n * by `ticket-read` responses. The client holds NO summary fetch\n * path: resync-on-(re)connect is served by the SERVER (every\n * connect pushes a fresh summary);\n * - query invalidation on events (list + open drawer refresh live);\n * - `markRead` with local write-through zeroing for 0-latency,\n * reconciled by the response's server-computed summary;\n * - the ticket-contract `no-stream` (204) retry policy.\n *\n * There is NO polling anywhere and NO summary endpoint. Hosts whose\n * stream endpoint 404s or is absent get no unread indication — the\n * indication IS a realtime feature (a 404ing endpoint is re-probed on\n * presence, rate-limited by the client).\n *\n * Invalidation semantics on events — deliberately `invalidateQueries`,\n * NOT `setQueryData` patches: the ticket hooks run `gcTime: 0`, so\n * there is usually no cache entry to patch, and `TicketsCacheSlot`\n * shape rules apply when there is.\n */\n\nimport { useQueryClient } from '@tanstack/react-query';\nimport React from 'react';\nimport { useChatRuntime } from '../../contexts/chat-runtime-context';\nimport { embedAuthedFetch } from '../../utils/embed-authed-fetch';\nimport { createSseSubscription, type SseSubscription, type SseTransportStatus } from '../../utils/sse-subscription';\nimport { useChatIdentity } from '../chat/hooks/use-chat-identity';\nimport type { TicketMessageStreamData, TicketStreamEvent, TicketUnreadSummary } from './types';\n\n// Ticket realtime is its OWN api surface — deliberately NOT under\n// `/api/chat/agent/*` (that prefix is the chat agent's tool surface;\n// ticket streaming/read-receipts are unrelated to chat).\nconst TICKET_STREAM_ENDPOINT = '/api/tickets/stream';\nconst TICKET_READ_ENDPOINT = '/api/tickets/read';\n\nconst EMPTY_SUMMARY: TicketUnreadSummary = { totalUnread: 0, tickets: {}, latestUnreadAt: {} };\n\n/** ISO stamps can carry mixed timezone offsets (server `+00:00` vs an\n * optimistic local `Z`) — lexicographic string comparison mis-orders\n * those, so every stamp comparison goes through parsed epoch ms.\n * Missing/unparseable stamps sort oldest. */\nconst stampMs = (stamp: string | null | undefined): number => {\n if (!stamp) return 0;\n const ms = Date.parse(stamp);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\n/** Trailing debounce for event-burst invalidations (an agent pasting 5\n * messages must not fire 5 list refetches). */\nconst INVALIDATE_DEBOUNCE_MS = 1_500;\n\nexport interface TicketLiveContextValue {\n /** Viewer has a resolved, non-anon chat identity. Surfaces (e.g. the\n * header cell) hide themselves when false — computed here so they\n * don't each spin up their own identity resolution. */\n authed: boolean;\n /** Realtime-subscribed (server-confirmed), not merely HTTP-open. */\n connected: boolean;\n /** Unread total across tickets — drives the header count badge. */\n unreadTotal: number;\n /** Per-ticket unread counts (missing key = 0) — drives row dots.\n * The currently-open ticket is masked to 0. */\n unreadByTicket: Record<string, number>;\n /** The ticket with the NEWEST unread reply (open ticket excluded) —\n * the header cell routes to it via the SSOT deep link\n * (`buildTicketOpenHref`). Null when nothing is unread. */\n nextUnreadTicketId: string | null;\n /** Mark a ticket read: POSTs the receipt, zeroes the ticket locally\n * (0-latency), then applies the response's server-computed summary. */\n markRead: (ticketExternalId: string) => Promise<void>;\n /** Drawer open/close registration — suppresses unread bumps for the\n * open ticket and masks it in the derived map. */\n setOpenTicketId: (ticketExternalId: string | null) => void;\n /** Hint that the viewer just created a ticket — reconnects the stream\n * (fresh ownership set + fresh server-pushed summary). */\n notifyTicketCreated: () => void;\n}\n\nconst TicketLiveContext = React.createContext<TicketLiveContextValue | null>(null);\n\nexport function useOptionalTicketLive(): TicketLiveContextValue | null {\n return React.useContext(TicketLiveContext);\n}\n\nexport function useTicketLive(): TicketLiveContextValue {\n const ctx = React.useContext(TicketLiveContext);\n if (!ctx) {\n throw new Error('useTicketLive must be used within <TicketLiveProvider>');\n }\n return ctx;\n}\n\nfunction isTicketMessageData(value: unknown): value is TicketMessageStreamData {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as TicketMessageStreamData).ticket_external_id === 'string'\n );\n}\n\nfunction isSummaryData(value: unknown): value is TicketUnreadSummary {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as TicketUnreadSummary).totalUnread === 'number' &&\n typeof (value as TicketUnreadSummary).tickets === 'object'\n );\n}\n\nexport interface TicketLiveProviderProps {\n children: React.ReactNode;\n /**\n * Whether ticket realtime is available to this host at all (feature flag,\n * session, entitlement). `false` provides NO context — `useOptionalTicketLive()`\n * reads `null`, `useTicketLive()` throws, and no identity or stream request is\n * made: byte-for-byte what a host got by not mounting this at all.\n *\n * It exists so a host never has to mount this CONDITIONALLY. A host that writes\n * `enabled ? <TicketLiveProvider>{children}</TicketLiveProvider> : <>{children}</>`\n * changes the element TYPE at that position the moment its flag answers, and React\n * tears down and remounts everything below. In openframe-frontend that wrapper sat\n * around the whole `AppLayout`, so every cold load remounted the app shell and the\n * page under it — visibly, as a chat drawer opened by a deep link replaying its\n * open animation. Mount it always and pass the answer here instead.\n *\n * One consequence of mounting it always: a `ChatRuntime` must be above it even\n * while disabled. The identity resolver reads one unconditionally (Rules of\n * Hooks) and throws without it; `enabled` gates the request, not the read.\n */\n enabled?: boolean;\n}\n\nexport function TicketLiveProvider({ children, enabled = true }: TicketLiveProviderProps) {\n const runtime = useChatRuntime();\n const identity = useChatIdentity(enabled);\n const queryClient = useQueryClient();\n\n const streamUrl = runtime?.endpoints.ticketStreamUrl ?? TICKET_STREAM_ENDPOINT;\n const readUrl = runtime?.endpoints.ticketReadUrl ?? TICKET_READ_ENDPOINT;\n\n // Gates the stream below (via `streamEnabled`) as well as every surface that\n // reads `authed` — one predicate, so a disabled provider is also a silent one.\n const authed = enabled && identity.authTier !== 'anon' && !!identity.user?.email;\n\n const [connected, setConnected] = React.useState(false);\n // The SINGLE unread source — written ONLY from server-computed\n // summaries (stream frames + markRead responses) plus the two\n // 0-latency optimistic writes (event bump, markRead zero), both\n // reconciled by the next server summary.\n const [summary, setSummary] = React.useState<TicketUnreadSummary>(EMPTY_SUMMARY);\n // State + ref for the open drawer: state drives the derived map,\n // the ref lets stream callbacks read it without re-subscribing.\n const [openTicketIdState, setOpenTicketIdState] = React.useState<string | null>(null);\n const openTicketIdRef = React.useRef<string | null>(null);\n\n // ---- Debounced (trailing) invalidation of ticket queries on events ----\n const pendingTicketIdsRef = React.useRef<Set<string>>(new Set());\n const invalidateTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const flushInvalidations = React.useCallback(() => {\n invalidateTimerRef.current = null;\n const ids = Array.from(pendingTicketIdsRef.current);\n pendingTicketIdsRef.current.clear();\n void queryClient.invalidateQueries({ queryKey: ['tickets'] });\n for (const id of ids) {\n void queryClient.invalidateQueries({ queryKey: ['ticket-engagements', id] });\n }\n }, [queryClient]);\n const scheduleInvalidation = React.useCallback(\n (ticketId?: string) => {\n if (ticketId) pendingTicketIdsRef.current.add(ticketId);\n if (invalidateTimerRef.current) return;\n invalidateTimerRef.current = setTimeout(flushInvalidations, INVALIDATE_DEBOUNCE_MS);\n },\n [flushInvalidations],\n );\n\n // ---- Optimistic unread writes (reconciled by server summaries) ----\n const bumpUnread = React.useCallback((ticketId: string, createdAt?: string | null) => {\n setSummary(prev => {\n const stamp = createdAt ?? new Date().toISOString();\n const existing = prev.latestUnreadAt[ticketId];\n return {\n totalUnread: prev.totalUnread + 1,\n tickets: { ...prev.tickets, [ticketId]: (prev.tickets[ticketId] ?? 0) + 1 },\n latestUnreadAt: {\n ...prev.latestUnreadAt,\n [ticketId]: existing && stampMs(existing) > stampMs(stamp) ? existing : stamp,\n },\n };\n });\n }, []);\n\n const zeroUnread = React.useCallback((ticketId: string) => {\n setSummary(prev => {\n const current = prev.tickets[ticketId] ?? 0;\n if (current === 0) return prev;\n const tickets = { ...prev.tickets };\n delete tickets[ticketId];\n const latestUnreadAt = { ...prev.latestUnreadAt };\n delete latestUnreadAt[ticketId];\n return { totalUnread: Math.max(0, prev.totalUnread - current), tickets, latestUnreadAt };\n });\n }, []);\n\n // ---- Stream lifecycle (owned by the SSE CLIENT — the provider only\n // consumes its consolidated signals) ----\n const subscriptionRef = React.useRef<SseSubscription | null>(null);\n const noStreamRef = React.useRef(false);\n\n // Stable handler refs so the subscription effect doesn't churn.\n const handlersRef = React.useRef<{\n onEvent: (eventName: string, data: unknown) => void;\n onStatus: (status: SseTransportStatus) => void;\n onConnected: (connected: boolean) => void;\n }>({ onEvent: () => {}, onStatus: () => {}, onConnected: () => {} });\n\n const handleConnected = (isConnected: boolean) => {\n setConnected(isConnected);\n if (isConnected) {\n // Event-gap healing on every (re)connect: the SERVER pushes a\n // fresh summary; the open drawer refetches through the ACL'd path.\n const openId = openTicketIdRef.current;\n if (openId) {\n void queryClient.invalidateQueries({ queryKey: ['ticket-engagements', openId] });\n }\n }\n };\n\n const handleEvent = (eventName: string, data: unknown) => {\n if (eventName === 'ticket-summary') {\n const frame = data as TicketStreamEvent | null;\n if (isSummaryData(frame?.data)) {\n setSummary({\n totalUnread: frame.data.totalUnread,\n tickets: frame.data.tickets ?? {},\n latestUnreadAt: frame.data.latestUnreadAt ?? {},\n });\n }\n return;\n }\n\n if (eventName === 'ticket-resync') {\n // Ownership gained / degraded payload — a fresh ticket-summary\n // frame follows from the server; refresh the query surfaces.\n scheduleInvalidation();\n return;\n }\n\n if (eventName === 'ticket-status') {\n // Meaningful ticket-row change (close/reopen/pipeline move). The\n // server pushes an updated summary right after (closures count as\n // unread server-side); here we just refresh the query surfaces so\n // the status badge flips live.\n const frame = data as TicketStreamEvent | null;\n const ticketId = (frame?.data as { ticket_external_id?: string } | undefined)?.ticket_external_id;\n scheduleInvalidation(ticketId);\n return;\n }\n\n if (eventName === 'ticket-message') {\n const frame = data as TicketStreamEvent | null;\n const payload = frame?.data;\n if (!isTicketMessageData(payload)) {\n scheduleInvalidation();\n return;\n }\n // Always invalidate (own replies from another tab render live);\n // bump unread ONLY on the server-stamped predicate, and never for\n // the ticket whose drawer is open. The server's debounced summary\n // push reconciles moments later.\n scheduleInvalidation(payload.ticket_external_id);\n if (payload.countsAsUnread && payload.ticket_external_id !== openTicketIdRef.current) {\n bumpUnread(payload.ticket_external_id, payload.hubspot_created_at);\n }\n }\n };\n\n const handleStatus = (status: SseTransportStatus) => {\n // The client owns connection lifecycle. The only transport status\n // with DOMAIN meaning is the ticket contract's `no-stream` (204 =\n // zero owned tickets): remember it so a domain signal (own\n // create_ticket, window focus) can retry.\n if (status === 'open') noStreamRef.current = false;\n if (status === 'no-stream') noStreamRef.current = true;\n };\n\n // Publish the three handlers into the stable ref. In an unconditional effect\n // rather than in the render body: every caller is an SSE transport callback,\n // which can only fire after a commit, and a render attempt React discards\n // must not be able to leave a handler behind that closes over state the user\n // never saw. Declared BEFORE the subscription effect so it is filled first\n // in the same flush.\n React.useEffect(() => {\n handlersRef.current.onConnected = handleConnected;\n handlersRef.current.onEvent = handleEvent;\n handlersRef.current.onStatus = handleStatus;\n });\n\n // Mount/unmount of the subscription — gated on auth + a usable URL.\n const streamEnabled = authed && !!streamUrl;\n React.useEffect(() => {\n if (!streamEnabled) return undefined;\n if (typeof window === 'undefined') return undefined;\n\n subscriptionRef.current = createSseSubscription({\n url: streamUrl,\n onEvent: (name, data) => handlersRef.current.onEvent(name, data),\n onStatusChange: status => handlersRef.current.onStatus(status),\n onConnectedChange: isConnected => handlersRef.current.onConnected(isConnected),\n // Long-lived per-user stream: suspend while hidden (scale relief),\n // resume + reconnect on visible/online — all inside the client.\n pauseWhenHidden: true,\n });\n\n const retryIfIdle = () => {\n // After a 204 (no-stream), a focus signal is the retry trigger. Only\n // 204 — the client's contract leaves that retry to the caller; it\n // retries terminal / 401 states on its own.\n if (noStreamRef.current) {\n noStreamRef.current = false;\n subscriptionRef.current?.reconnectNow();\n }\n };\n window.addEventListener('focus', retryIfIdle);\n\n return () => {\n window.removeEventListener('focus', retryIfIdle);\n if (invalidateTimerRef.current) {\n clearTimeout(invalidateTimerRef.current);\n invalidateTimerRef.current = null;\n }\n subscriptionRef.current?.close();\n subscriptionRef.current = null;\n setConnected(false);\n setSummary(EMPTY_SUMMARY);\n };\n // handlersRef is stable; streamUrl/auth changes remount the stream.\n }, [streamEnabled, streamUrl]);\n\n // ---- Public API ----\n const markRead = React.useCallback(\n async (ticketExternalId: string) => {\n try {\n const response = await embedAuthedFetch(readUrl, {\n method: 'POST',\n body: JSON.stringify({ ticket_id: ticketExternalId }),\n });\n if (!response.ok) return;\n // 0-latency local zero, then reconcile with the response's\n // server-computed summary (THE truth — includes closure\n // accounting and other tickets).\n zeroUnread(ticketExternalId);\n const body = (await response.json().catch(() => null)) as { summary?: TicketUnreadSummary } | null;\n if (isSummaryData(body?.summary)) {\n setSummary({\n totalUnread: body.summary.totalUnread,\n tickets: body.summary.tickets ?? {},\n latestUnreadAt: body.summary.latestUnreadAt ?? {},\n });\n }\n } catch {\n // Non-fatal — the receipt lands on the next markRead; unread\n // state reconciles via the next server summary push.\n }\n },\n [readUrl, zeroUnread],\n );\n\n const setOpenTicketId = React.useCallback((ticketExternalId: string | null) => {\n openTicketIdRef.current = ticketExternalId;\n setOpenTicketIdState(ticketExternalId);\n }, []);\n\n const notifyTicketCreated = React.useCallback(() => {\n // Fresh connect → fresh ownership set + fresh server summary\n // (covers both the no-stream 204 wait state and an already-live\n // stream whose set predates the new ticket).\n noStreamRef.current = false;\n subscriptionRef.current?.reconnectNow();\n }, []);\n\n // Derived unread map — open ticket masked to 0 so a server summary\n // landing inside the markRead debounce window can't transiently\n // re-badge it.\n const value = React.useMemo<TicketLiveContextValue>(() => {\n let unreadByTicket = summary.tickets;\n let unreadTotal = summary.totalUnread;\n if (openTicketIdState && unreadByTicket[openTicketIdState]) {\n const masked = unreadByTicket[openTicketIdState];\n unreadByTicket = { ...unreadByTicket };\n delete unreadByTicket[openTicketIdState];\n unreadTotal = Math.max(0, unreadTotal - masked);\n }\n // Ticket with the NEWEST unread reply (compared as parsed epoch ms\n // via `stampMs` — mixed timezone offsets break string ordering) —\n // header cell routes there. Falls back to any unread key if a\n // timestamp is missing.\n let nextUnreadTicketId: string | null = null;\n let nextStampMs = 0;\n for (const id of Object.keys(unreadByTicket)) {\n const ms = stampMs(summary.latestUnreadAt[id]);\n if (nextUnreadTicketId === null || ms > nextStampMs) {\n nextUnreadTicketId = id;\n nextStampMs = ms;\n }\n }\n return {\n authed,\n connected,\n unreadTotal,\n unreadByTicket,\n nextUnreadTicketId,\n markRead,\n setOpenTicketId,\n notifyTicketCreated,\n };\n }, [summary, authed, connected, openTicketIdState, markRead, setOpenTicketId, notifyTicketCreated]);\n\n return <TicketLiveContext.Provider value={enabled ? value : null}>{children}</TicketLiveContext.Provider>;\n}\n","/**\n * Wire shape of a row returned by `POST /api/chat/agent/find-ticket`.\n * Mirrors the executor's projection at `lib/data/hubspot-tools.ts`\n * (`FIND_TICKET_SELECT` / `FindTicketResult`).\n *\n * Cross-repo duplication is INTENTIONAL: this lib ships independently\n * of the hub, so we can't import `FindTicketResult` from\n * `hubspot-tools.ts` directly. If the server adds a column to\n * `FIND_TICKET_SELECT`, also add it here. The smoke test in §F of the\n * plan covers the happy path; a wire-contract test belongs in the hub.\n *\n * `find-ticket` returns `customer_emails: string[]` (jsonb array), NOT\n * a single `customer_email`. The list is server-self-scoped to the\n * caller's session email; the array is exposed for admin/staff\n * surfaces, which the ticket center doesn't render.\n */\nexport interface TicketData {\n id: string;\n /** HubSpot ticket id (display number, e.g. \"1234\"). */\n external_id: string;\n subject: string | null;\n /** Short (≤400 char) HTML-stripped preview of the ticket body —\n * used for the list-card subtitle when needed. */\n preview: string | null;\n /** Longer (≤4k char) sanitized body. INCLUDES every appended\n * `content_addendum` comment because the `update_ticket` executor\n * reads + re-writes the `content` property server-side with a\n * `---` separator. Render this in the drawer's description block\n * and the user sees both the original message + every comment they\n * (or staff) added. */\n body: string | null;\n /** Canonical OPEN | CLOSED (HubSpot pipeline derived). */\n status: string | null;\n /** Human label like \"New\" / \"Working on it\" / \"Waiting on contact\" /\n * \"Closed\". Drives the badge text; canonical status drives color. */\n pipeline_stage_label: string | null;\n clickup_task_id: string | null;\n /** Snapshot of the linked ClickUp delivery task — populated server-side\n * via the `clickup_tasks` mirror when `clickup_task_id` is set. Drives\n * the \"Linked delivery\" card surface on the ticket drawer (status\n * badge + ClickUp deep link). `null` when no link OR the ClickUp row\n * was deleted / not yet synced. */\n clickup: TicketClickupSummary | null;\n priority: string | null;\n customer_emails: string[];\n customer_company: string | null;\n /** HubSpot contact's display name. Drives the customer attribution\n * on the drawer when the viewer is NOT the customer themselves\n * (admin browsing / multi-contact second viewer). Conversations\n * API messages don't carry per-message sender info on Custom\n * Channels, so this is the only reliable source for \"what's the\n * customer's name.\" */\n customer_name: string | null;\n /** HubSpot owner id of the agent assigned to this ticket. Carried as\n * raw id for debugging; rendering goes through `assignedOwner`. Null\n * when unassigned. */\n assigned_to: string | null;\n /** Resolved assigned-owner profile — name + email + avatar. Populated\n * server-side via `attachOwnerProfiles` which joins through the\n * `hubspot_owners` mirror to `profiles` by email. Drives the\n * \"Assigned to\" attribution in the drawer header. Null when\n * unassigned OR the owner couldn't be resolved (rare — only when\n * the agent was deleted from HubSpot between the ticket update and\n * the next owners reconcile). */\n assignedOwner: TicketAssignedOwner | null;\n hubspot_updated_at: string;\n}\n\n/** Resolved profile of a ticket's assigned agent — surfaced in the\n * drawer header. Subset of the server's `MirroredOwnerProfile`\n * trimmed to just the rendering fields. */\nexport interface TicketAssignedOwner {\n name: string | null;\n email: string | null;\n avatarUrl: string | null;\n}\n\n/** Compact projection of a linked ClickUp task — matches the server's\n * `ClickupSummary` and aligns with `DeliveryItem` so the linked-card\n * on a ticket can render through the same `DeliveryRow` primitive used\n * on `/bug-fixes-and-enhancements`. */\nexport interface TicketClickupSummary {\n /** ClickUp task external_id (e.g. \"86ad4e022\"). Used as the\n * `?focus=<id>` URL param to scroll the public delivery page to\n * this row. */\n external_id: string;\n title: string | null;\n description: string | null;\n /** ClickUp status name — e.g. \"complete\" / \"working\" / \"design approved\"\n * / \"waiting for release\". Used as the badge label. */\n status: string | null;\n /** ClickUp's per-status hex color (e.g. \"#008844\"). Forwarded to the\n * badge so colors match the ClickUp board exactly. */\n status_color: string | null;\n /** Bucket — `'backlog' | 'working' | 'complete' | 'unknown'`. Used as\n * a fallback when status_color is missing. */\n status_category: string | null;\n /** ClickUp custom item label (`'Bug'` / `'Request'`) — drives the\n * type badge (\"BUG-FIX\" / \"ENHANCEMENT\"). */\n task_type: string | null;\n custom_item_id: number | null;\n /** Every ClickUp list the task is associated with. UI joins with \", \". */\n list_names: string[];\n /** Unix-ms timestamps so the row's \"ACTIVE X ago\" subtitle uses the\n * shared `getRelativeTime()` helper. */\n date_opened: number | null;\n date_updated: number | null;\n date_closed: number | null;\n /** Direct https://app.clickup.com/t/<id> deep link. Kept on the wire\n * for admin surfaces; the customer-facing linked card navigates\n * internally instead. */\n clickup_url: string | null;\n /** Composed server-side via the SAME `buildDevSectionUrl` helper the\n * chat-inline delivery card uses. Carries `?search=<id>` so the\n * delivery list filters to that single task on landing. */\n delivery_href: string;\n /** Target platform name for the host's `useNavLink` to decide\n * same-tab vs new-tab on cross-platform links. */\n delivery_target_platform: string;\n /** Release version label set by the delivery team, e.g. \"0.9\" / \"1.0\".\n * Shown beside the status when present. */\n target_version: string | null;\n}\n\n/**\n * Optimistic placeholder a `submitTicket` call prepends to the list\n * BEFORE the server roundtrip resolves. Drawer is hidden until the\n * real id arrives. The wrapper destructures `_optimistic` before\n * forwarding to `<ChatTicketItem>` so the DOM doesn't see an unknown\n * prop.\n */\nexport interface OptimisticTicket extends TicketData {\n _optimistic: true;\n}\n\nexport type AnyTicket = TicketData | OptimisticTicket;\n\nexport function isOptimistic(t: AnyTicket): t is OptimisticTicket {\n return (t as OptimisticTicket)._optimistic === true;\n}\n\n/**\n * Shape of a single `['tickets', …]` TanStack-Query cache slot.\n * Mirrors `FindTicketResponse` in `hooks/use-tickets-list.ts` — kept\n * here because the cache-mutation call sites in `useTicketActions` and\n * `<HelpCenterList>` would otherwise have to redeclare the shape inline.\n *\n * A 2026-05-29 prod regression (`t.map is not a function` on\n * close/reopen) was caused by assuming the cache held a bare\n * `TicketData[]` instead of this wrapper — every helper that calls\n * `queryClient.setQueriesData` / `getQueriesData` on `['tickets']`\n * MUST type the value through this shape and project / reassemble\n * `tickets` explicitly.\n */\nexport interface TicketsCacheSlot {\n tickets?: TicketData[];\n count?: number;\n totalCount?: number;\n page?: number;\n pageSize?: number;\n totalPages?: number;\n scope?: 'self' | 'all';\n}\n\n/**\n * Stable server-side error codes the ticket-action helpers route\n * through `mapTicketActionError`. Anything else is treated as a generic\n * server error.\n *\n * Reply-specific codes (`HUBSPOT_5XX` / `HUBSPOT_400_VALIDATION` /\n * `HUBSPOT_404_THREAD` / `HUBSPOT_REPLY_UNKNOWN`) drive the drawer\n * banner that appears above the composer when a customer reply fails.\n * Distinct from `HUBSPOT_DISCONNECTED` (whole-system) and\n * `TICKET_NOT_FOUND` (terminal-row) — the reply codes are per-attempt\n * + retryable except for `HUBSPOT_404_THREAD`.\n */\nexport type TicketActionErrorCode =\n | 'PROPOSAL_NOT_CLAIMABLE'\n | 'TICKET_NOT_FOUND'\n | 'TICKET_OWNERSHIP_DENIED'\n | 'HUBSPOT_DISCONNECTED'\n | 'RATE_LIMITED'\n | 'INVALID_TOOL_ARGS'\n | 'HUBSPOT_5XX'\n | 'HUBSPOT_400_VALIDATION'\n | 'HUBSPOT_404_THREAD'\n | 'HUBSPOT_REPLY_UNKNOWN'\n | 'UNKNOWN';\n\nexport interface MappedTicketActionError {\n code: TicketActionErrorCode;\n /** Human-readable copy safe to show in a toast. */\n message: string;\n /** When true, the form should disable submit + show the\n * support-down banner. Set only for HUBSPOT_DISCONNECTED. */\n supportSystemDown: boolean;\n /** When true, the helper should remove the affected row optimistically\n * (TICKET_NOT_FOUND). */\n removeRowFromCache: boolean;\n /** Retry hint surfaced from a 429 response. Caller decides whether\n * to mention it in the toast. */\n retryAfterSeconds?: number;\n}\n\n/**\n * Defensive client-side cap on ticket text (initial content + comment\n * addendums). HubSpot Note engagements accept more, but a 100KB paste\n * should fail fast at the UI rather than burning a server round-trip.\n * Both the open-ticket form and the per-row comment textarea import\n * this so a future server-side hardening only touches one place.\n */\nexport const TICKET_TEXT_MAX_CHARS = 5000;\n\n/**\n * Wire contract of the ticket live stream (`GET /api/chat/agent/\n * ticket-stream`, SSE). Producer (the hub route's `mapEvent`) imports\n * THIS type — same producer/consumer SSOT pattern as\n * `ConversationEngagementWire = TicketEngagement`. A schema drift here\n * would be a silent serialization bug; keep one named contract.\n *\n * Frames carry METADATA ONLY — never message bodies. Content is always\n * refetched through the ACL'd `list-engagements` path (one read path).\n *\n * Event names (`eventType`):\n * - `ticket-message` — a message row landed in the mirror for one of\n * the viewer's tickets. `data` carries the metadata.\n * - `ticket-status` — a MEANINGFUL ticket-row change (status /\n * pipeline stage / closure) landed in the mirror. Server-side\n * old-vs-new diffing drops no-op writes (sync stamps). Clients\n * invalidate + refetch the summary (closures count as unread\n * server-side via `closed_at` vs the read receipt).\n * - `ticket-summary` — the server-computed `TicketUnreadSummary`.\n * THE stream is the summary webservice: pushed on connect (server-\n * side resync-on-reconnect) and re-pushed (debounced) after every\n * delivered event. Clients hold NO summary fetch path.\n * - `ticket-resync` — degraded frame: something happened that the\n * server couldn't fully describe (truncated Realtime payload, or\n * the connection's ownership set just gained tickets). A fresh\n * `ticket-summary` follows it; clients invalidate queries.\n *\n * The server also emits `status` frames (`subscribed` / `retrying` /\n * `reconnect_failed`) from the shared SSE utility — clients derive\n * `connected` from those, NOT from the HTTP stream being open.\n */\nexport type TicketStreamEventType = 'ticket-message' | 'ticket-status' | 'ticket-summary' | 'ticket-resync';\n\n/** Metadata payload of a `ticket-message` frame. */\nexport type TicketMessageStreamData = {\n ticket_external_id: string;\n /** Mirror row's HubSpot message id. */\n message_id: string;\n /** Server-stamped via the same direction predicate the wire mapper\n * uses. Clients never re-derive author classification. */\n authorRole: 'customer' | 'support';\n /** Server-stamped via the visibility+direction unread predicate\n * (`countsAsUnread` in the hub's conversations DAL). ONLY frames\n * with `true` may bump client-side unread state. */\n countsAsUnread: boolean;\n hubspot_created_at: string | null;\n};\n\n/** Metadata payload of a `ticket-status` frame. */\nexport type TicketStatusStreamData = {\n ticket_external_id: string;\n /** Canonical OPEN | CLOSED after the change. */\n status: string | null;\n pipeline_stage_label: string | null;\n closed_at: string | null;\n};\n\nexport interface TicketStreamEvent {\n eventType: TicketStreamEventType;\n /** Human-readable label (SSE envelope compat) — clients ignore it. */\n message: string;\n /** `ticket-message` → TicketMessageStreamData; `ticket-status` →\n * TicketStatusStreamData; `ticket-summary` → TicketUnreadSummary;\n * absent on `ticket-resync`. */\n data?: TicketMessageStreamData | TicketStatusStreamData | TicketUnreadSummary;\n}\n\n/**\n * The SINGLE unread source, delivered EXCLUSIVELY over the stream\n * (`ticket-summary` frames on connect + after events) and in\n * `ticket-read` responses. There is no summary fetch endpoint. Header\n * badge total AND per-row dots both read this via the\n * `TicketLiveProvider` map; missing keys mean 0.\n */\nexport type TicketUnreadSummary = {\n totalUnread: number;\n tickets: Record<string, number>;\n /** ISO timestamp of the NEWEST unread message per ticket (same keys as\n * `tickets`). Drives \"route to the most recent update\" on the header\n * cell: the ticket with the max value here is `nextUnreadTicketId`. */\n latestUnreadAt: Record<string, string>;\n};\n\n/**\n * Trailing debounce for `markRead` while the drawer is open and new\n * messages stream in — flushed on drawer close so a fast open/close\n * still persists the receipt. Named beside its sibling cadences so\n * every timing knob lives in one file.\n */\nexport const TICKET_MARK_READ_DEBOUNCE_MS = 2000;\n\n/**\n * Ticket deep-link SSOT — DEFINED in the server-safe param-keys module\n * (`utils/dev-sections/dev-section-param-keys.ts`, beside its sibling\n * `devSectionAnchorId`) because producers span both worlds: the hub's\n * SERVER-side chat-ref builder and the client header cell. Re-exported\n * here so ticket-surface consumers keep one import home.\n */\nexport { TICKET_OPEN_PARAM, buildTicketOpenHref } from '../../utils/dev-sections/dev-section-param-keys';\n\n/**\n * Centralized toast copy. Keep all wording here so QA / localization\n * can find every user-visible string in one file.\n */\nexport const TOAST_COPY = {\n open_success: { title: 'Ticket opened', description: 'We received your message and will follow up shortly.' },\n open_mirror_pending: { title: 'Ticket opened', description: 'Syncing — your ticket will appear momentarily.' },\n close_success: { title: 'Ticket closed' },\n reopen_success: { title: 'Ticket reopened' },\n comment_success: { title: 'Comment added' },\n attach_success: { title: 'Files attached' },\n // Failure variants are constructed dynamically from MappedTicketActionError.\n} as const;\n"]}