@tai42/studio-sdk 15.0.0 → 16.0.1

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 (322) hide show
  1. package/dist/components/explorer-body.d.ts +49 -0
  2. package/dist/components/explorer-body.d.ts.map +1 -0
  3. package/dist/components/explorer-body.js +67 -0
  4. package/dist/components/explorer-body.js.map +1 -0
  5. package/dist/components/explorer-page-size.d.ts +4 -0
  6. package/dist/components/explorer-page-size.d.ts.map +1 -0
  7. package/dist/components/explorer-page-size.js +36 -0
  8. package/dist/components/explorer-page-size.js.map +1 -0
  9. package/dist/components/explorer-tag-filter.d.ts +17 -0
  10. package/dist/components/explorer-tag-filter.d.ts.map +1 -0
  11. package/dist/components/explorer-tag-filter.js +29 -0
  12. package/dist/components/explorer-tag-filter.js.map +1 -0
  13. package/dist/components/explorer-tags.d.ts +22 -0
  14. package/dist/components/explorer-tags.d.ts.map +1 -0
  15. package/dist/components/explorer-tags.js +51 -0
  16. package/dist/components/explorer-tags.js.map +1 -0
  17. package/dist/components/explorer-view.d.ts +3 -17
  18. package/dist/components/explorer-view.d.ts.map +1 -1
  19. package/dist/components/explorer-view.js +53 -170
  20. package/dist/components/explorer-view.js.map +1 -1
  21. package/dist/components/extension-combo-builder.d.ts +0 -5
  22. package/dist/components/extension-combo-builder.d.ts.map +1 -1
  23. package/dist/components/extension-combo-builder.js +4 -42
  24. package/dist/components/extension-combo-builder.js.map +1 -1
  25. package/dist/components/extension-combo-parts.d.ts +44 -0
  26. package/dist/components/extension-combo-parts.d.ts.map +1 -0
  27. package/dist/components/extension-combo-parts.js +57 -0
  28. package/dist/components/extension-combo-parts.js.map +1 -0
  29. package/dist/components/fleet-report.d.ts.map +1 -1
  30. package/dist/components/fleet-report.js +51 -37
  31. package/dist/components/fleet-report.js.map +1 -1
  32. package/dist/components/icons/chrome-marks.d.ts +50 -0
  33. package/dist/components/icons/chrome-marks.d.ts.map +1 -0
  34. package/dist/components/icons/chrome-marks.js +51 -0
  35. package/dist/components/icons/chrome-marks.js.map +1 -0
  36. package/dist/components/icons/icon-frame.d.ts +29 -0
  37. package/dist/components/icons/icon-frame.d.ts.map +1 -0
  38. package/dist/components/icons/icon-frame.js +19 -0
  39. package/dist/components/icons/icon-frame.js.map +1 -0
  40. package/dist/components/icons/index.d.ts +40 -0
  41. package/dist/components/icons/index.d.ts.map +1 -0
  42. package/dist/components/icons/index.js +6 -0
  43. package/dist/components/icons/index.js.map +1 -0
  44. package/dist/components/icons/nav-map.d.ts +14 -0
  45. package/dist/components/icons/nav-map.d.ts.map +1 -0
  46. package/dist/components/icons/nav-map.js +28 -0
  47. package/dist/components/icons/nav-map.js.map +1 -0
  48. package/dist/components/icons/nav-marks.d.ts +44 -0
  49. package/dist/components/icons/nav-marks.d.ts.map +1 -0
  50. package/dist/components/icons/nav-marks.js +45 -0
  51. package/dist/components/icons/nav-marks.js.map +1 -0
  52. package/dist/components/icons/status-marks.d.ts +21 -0
  53. package/dist/components/icons/status-marks.d.ts.map +1 -0
  54. package/dist/components/icons/status-marks.js +22 -0
  55. package/dist/components/icons/status-marks.js.map +1 -0
  56. package/dist/components/icons/theme-marks.d.ts +9 -0
  57. package/dist/components/icons/theme-marks.d.ts.map +1 -0
  58. package/dist/components/icons/theme-marks.js +10 -0
  59. package/dist/components/icons/theme-marks.js.map +1 -0
  60. package/dist/components/json-diff.d.ts.map +1 -1
  61. package/dist/components/json-diff.js +1 -3
  62. package/dist/components/json-diff.js.map +1 -1
  63. package/dist/components/json-tree-context.d.ts +13 -0
  64. package/dist/components/json-tree-context.d.ts.map +1 -0
  65. package/dist/components/json-tree-context.js +15 -0
  66. package/dist/components/json-tree-context.js.map +1 -0
  67. package/dist/components/json-tree-model.d.ts +56 -0
  68. package/dist/components/json-tree-model.d.ts.map +1 -0
  69. package/dist/components/json-tree-model.js +125 -0
  70. package/dist/components/json-tree-model.js.map +1 -0
  71. package/dist/components/json-tree-nodes.d.ts +30 -0
  72. package/dist/components/json-tree-nodes.d.ts.map +1 -0
  73. package/dist/components/json-tree-nodes.js +118 -0
  74. package/dist/components/json-tree-nodes.js.map +1 -0
  75. package/dist/components/json-tree.d.ts.map +1 -1
  76. package/dist/components/json-tree.js +7 -295
  77. package/dist/components/json-tree.js.map +1 -1
  78. package/dist/components/json-value-format.d.ts +20 -0
  79. package/dist/components/json-value-format.d.ts.map +1 -0
  80. package/dist/components/json-value-format.js +63 -0
  81. package/dist/components/json-value-format.js.map +1 -0
  82. package/dist/components/markdown-blocks.d.ts +38 -0
  83. package/dist/components/markdown-blocks.d.ts.map +1 -0
  84. package/dist/components/markdown-blocks.js +168 -0
  85. package/dist/components/markdown-blocks.js.map +1 -0
  86. package/dist/components/markdown-inline.d.ts +24 -0
  87. package/dist/components/markdown-inline.d.ts.map +1 -0
  88. package/dist/components/markdown-inline.js +257 -0
  89. package/dist/components/markdown-inline.js.map +1 -0
  90. package/dist/components/markdown.d.ts +3 -52
  91. package/dist/components/markdown.d.ts.map +1 -1
  92. package/dist/components/markdown.js +4 -376
  93. package/dist/components/markdown.js.map +1 -1
  94. package/dist/components/observer-registry.d.ts +38 -0
  95. package/dist/components/observer-registry.d.ts.map +1 -0
  96. package/dist/components/observer-registry.js +131 -0
  97. package/dist/components/observer-registry.js.map +1 -0
  98. package/dist/components/overflow-measure.d.ts +38 -0
  99. package/dist/components/overflow-measure.d.ts.map +1 -0
  100. package/dist/components/overflow-measure.js +54 -0
  101. package/dist/components/overflow-measure.js.map +1 -0
  102. package/dist/components/prose-regions.d.ts +81 -0
  103. package/dist/components/prose-regions.d.ts.map +1 -0
  104. package/dist/components/prose-regions.js +128 -0
  105. package/dist/components/prose-regions.js.map +1 -0
  106. package/dist/components/scroll-region.d.ts.map +1 -1
  107. package/dist/components/scroll-region.js +3 -298
  108. package/dist/components/scroll-region.js.map +1 -1
  109. package/dist/components/tab-stop-parse.d.ts +53 -0
  110. package/dist/components/tab-stop-parse.d.ts.map +1 -0
  111. package/dist/components/tab-stop-parse.js +202 -0
  112. package/dist/components/tab-stop-parse.js.map +1 -0
  113. package/dist/components/templated-text-field.d.ts.map +1 -1
  114. package/dist/components/templated-text-field.js +10 -148
  115. package/dist/components/templated-text-field.js.map +1 -1
  116. package/dist/components/templated-text-kwargs-editor.d.ts +24 -0
  117. package/dist/components/templated-text-kwargs-editor.d.ts.map +1 -0
  118. package/dist/components/templated-text-kwargs-editor.js +48 -0
  119. package/dist/components/templated-text-kwargs-editor.js.map +1 -0
  120. package/dist/components/templated-text-kwargs.d.ts +37 -0
  121. package/dist/components/templated-text-kwargs.d.ts.map +1 -0
  122. package/dist/components/templated-text-kwargs.js +66 -0
  123. package/dist/components/templated-text-kwargs.js.map +1 -0
  124. package/dist/components/templated-text-source.d.ts +33 -0
  125. package/dist/components/templated-text-source.d.ts.map +1 -0
  126. package/dist/components/templated-text-source.js +53 -0
  127. package/dist/components/templated-text-source.js.map +1 -0
  128. package/dist/components/tool-picker-parts.d.ts +39 -0
  129. package/dist/components/tool-picker-parts.d.ts.map +1 -0
  130. package/dist/components/tool-picker-parts.js +87 -0
  131. package/dist/components/tool-picker-parts.js.map +1 -0
  132. package/dist/components/tool-picker.d.ts +1 -2
  133. package/dist/components/tool-picker.d.ts.map +1 -1
  134. package/dist/components/tool-picker.js +4 -66
  135. package/dist/components/tool-picker.js.map +1 -1
  136. package/dist/components/version-history-panel.d.ts.map +1 -1
  137. package/dist/components/version-history-panel.js +7 -34
  138. package/dist/components/version-history-panel.js.map +1 -1
  139. package/dist/components/version-history-rows.d.ts +48 -0
  140. package/dist/components/version-history-rows.d.ts.map +1 -0
  141. package/dist/components/version-history-rows.js +51 -0
  142. package/dist/components/version-history-rows.js.map +1 -0
  143. package/dist/guards.d.ts +6 -0
  144. package/dist/guards.d.ts.map +1 -0
  145. package/dist/guards.js +8 -0
  146. package/dist/guards.js.map +1 -0
  147. package/dist/hooks/interactions-connection.d.ts +43 -0
  148. package/dist/hooks/interactions-connection.d.ts.map +1 -0
  149. package/dist/hooks/interactions-connection.js +171 -0
  150. package/dist/hooks/interactions-connection.js.map +1 -0
  151. package/dist/hooks/interactions-stream.d.ts +80 -0
  152. package/dist/hooks/interactions-stream.d.ts.map +1 -0
  153. package/dist/hooks/interactions-stream.js +233 -0
  154. package/dist/hooks/interactions-stream.js.map +1 -0
  155. package/dist/hooks/useApi.d.ts +2409 -2293
  156. package/dist/hooks/useApi.d.ts.map +1 -1
  157. package/dist/hooks/useSse.d.ts +3 -6
  158. package/dist/hooks/useSse.d.ts.map +1 -1
  159. package/dist/hooks/useSse.js +38 -451
  160. package/dist/hooks/useSse.js.map +1 -1
  161. package/dist/navigation/app-link.d.ts +25 -0
  162. package/dist/navigation/app-link.d.ts.map +1 -0
  163. package/dist/navigation/app-link.js +33 -0
  164. package/dist/navigation/app-link.js.map +1 -0
  165. package/dist/navigation/context.d.ts +3 -20
  166. package/dist/navigation/context.d.ts.map +1 -1
  167. package/dist/navigation/context.js +7 -267
  168. package/dist/navigation/context.js.map +1 -1
  169. package/dist/navigation/entry-state.d.ts +23 -0
  170. package/dist/navigation/entry-state.d.ts.map +1 -0
  171. package/dist/navigation/entry-state.js +39 -0
  172. package/dist/navigation/entry-state.js.map +1 -0
  173. package/dist/navigation/guard-registry.d.ts +30 -0
  174. package/dist/navigation/guard-registry.d.ts.map +1 -0
  175. package/dist/navigation/guard-registry.js +46 -0
  176. package/dist/navigation/guard-registry.js.map +1 -0
  177. package/dist/navigation/guarded-navigation.d.ts +24 -0
  178. package/dist/navigation/guarded-navigation.d.ts.map +1 -0
  179. package/dist/navigation/guarded-navigation.js +102 -0
  180. package/dist/navigation/guarded-navigation.js.map +1 -0
  181. package/dist/navigation/index.d.ts +3 -2
  182. package/dist/navigation/index.d.ts.map +1 -1
  183. package/dist/navigation/index.js +2 -1
  184. package/dist/navigation/index.js.map +1 -1
  185. package/dist/navigation/types.d.ts +8 -11
  186. package/dist/navigation/types.d.ts.map +1 -1
  187. package/dist/navigation/use-back-forward-guard.d.ts +5 -0
  188. package/dist/navigation/use-back-forward-guard.d.ts.map +1 -0
  189. package/dist/navigation/use-back-forward-guard.js +82 -0
  190. package/dist/navigation/use-back-forward-guard.js.map +1 -0
  191. package/dist/plugin/registry.d.ts +6 -15
  192. package/dist/plugin/registry.d.ts.map +1 -1
  193. package/dist/plugin/registry.js +83 -75
  194. package/dist/plugin/registry.js.map +1 -1
  195. package/dist/schema-editor/SchemaEditor.d.ts +8 -1
  196. package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
  197. package/dist/schema-editor/SchemaEditor.js +2 -2
  198. package/dist/schema-editor/SchemaEditor.js.map +1 -1
  199. package/dist/schema-form/SecretRefField.d.ts +2 -3
  200. package/dist/schema-form/SecretRefField.d.ts.map +1 -1
  201. package/dist/schema-form/SecretRefField.js +23 -12
  202. package/dist/schema-form/SecretRefField.js.map +1 -1
  203. package/dist/schema-form/classify.d.ts +1 -122
  204. package/dist/schema-form/classify.d.ts.map +1 -1
  205. package/dist/schema-form/classify.js +174 -237
  206. package/dist/schema-form/classify.js.map +1 -1
  207. package/dist/schema-form/default-value.d.ts.map +1 -1
  208. package/dist/schema-form/default-value.js +42 -19
  209. package/dist/schema-form/default-value.js.map +1 -1
  210. package/dist/schema-form/enum-field.d.ts +1 -1
  211. package/dist/schema-form/enum-field.d.ts.map +1 -1
  212. package/dist/schema-form/field-model.d.ts +121 -0
  213. package/dist/schema-form/field-model.d.ts.map +1 -0
  214. package/dist/schema-form/field-model.js +2 -0
  215. package/dist/schema-form/field-model.js.map +1 -0
  216. package/dist/schema-form/field-node.d.ts.map +1 -1
  217. package/dist/schema-form/field-node.js +25 -14
  218. package/dist/schema-form/field-node.js.map +1 -1
  219. package/dist/schema-form/media-field.d.ts +1 -1
  220. package/dist/schema-form/media-field.d.ts.map +1 -1
  221. package/dist/schema-form/media-field.js +47 -28
  222. package/dist/schema-form/media-field.js.map +1 -1
  223. package/dist/schema-form/string-annotations.d.ts +20 -0
  224. package/dist/schema-form/string-annotations.d.ts.map +1 -0
  225. package/dist/schema-form/string-annotations.js +105 -0
  226. package/dist/schema-form/string-annotations.js.map +1 -0
  227. package/dist/schema-form/string-field.d.ts +1 -1
  228. package/dist/schema-form/string-field.d.ts.map +1 -1
  229. package/dist/schema-form/union-field.d.ts +1 -1
  230. package/dist/schema-form/union-field.d.ts.map +1 -1
  231. package/dist/schema-form/union-field.js.map +1 -1
  232. package/dist/schema-form/union.d.ts +1 -1
  233. package/dist/schema-form/union.d.ts.map +1 -1
  234. package/dist/schema-form/validate.d.ts.map +1 -1
  235. package/dist/schema-form/validate.js +126 -105
  236. package/dist/schema-form/validate.js.map +1 -1
  237. package/dist/state-binding/AdapterMapping.d.ts.map +1 -1
  238. package/dist/state-binding/AdapterMapping.js +0 -0
  239. package/dist/state-binding/AdapterMapping.js.map +1 -1
  240. package/dist/state-binding/StateAttachRow.d.ts.map +1 -1
  241. package/dist/state-binding/StateAttachRow.js +52 -36
  242. package/dist/state-binding/StateAttachRow.js.map +1 -1
  243. package/dist/state-binding/adapter-mapping-row.d.ts +17 -0
  244. package/dist/state-binding/adapter-mapping-row.d.ts.map +1 -0
  245. package/dist/state-binding/adapter-mapping-row.js +87 -0
  246. package/dist/state-binding/adapter-mapping-row.js.map +1 -0
  247. package/dist/structured-output/StructuredOutput.d.ts.map +1 -1
  248. package/dist/structured-output/StructuredOutput.js +1 -3
  249. package/dist/structured-output/StructuredOutput.js.map +1 -1
  250. package/package.json +2 -2
  251. package/src/components/explorer-body.tsx +239 -0
  252. package/src/components/explorer-page-size.ts +39 -0
  253. package/src/components/explorer-tag-filter.tsx +80 -0
  254. package/src/components/explorer-tags.ts +64 -0
  255. package/src/components/explorer-view.tsx +114 -366
  256. package/src/components/extension-combo-builder.tsx +35 -152
  257. package/src/components/extension-combo-parts.tsx +237 -0
  258. package/src/components/fleet-report.tsx +97 -54
  259. package/src/components/icons/chrome-marks.tsx +195 -0
  260. package/src/components/icons/icon-frame.tsx +50 -0
  261. package/src/components/icons/index.ts +84 -0
  262. package/src/components/icons/nav-map.ts +55 -0
  263. package/src/components/icons/nav-marks.tsx +205 -0
  264. package/src/components/icons/status-marks.tsx +46 -0
  265. package/src/components/icons/theme-marks.tsx +33 -0
  266. package/src/components/json-diff.tsx +1 -4
  267. package/src/components/json-tree-context.ts +26 -0
  268. package/src/components/json-tree-model.ts +139 -0
  269. package/src/components/json-tree-nodes.tsx +224 -0
  270. package/src/components/json-tree.tsx +15 -434
  271. package/src/components/json-value-format.ts +64 -0
  272. package/src/components/markdown-blocks.ts +198 -0
  273. package/src/components/markdown-inline.tsx +365 -0
  274. package/src/components/markdown.tsx +5 -471
  275. package/src/components/observer-registry.ts +139 -0
  276. package/src/components/overflow-measure.ts +69 -0
  277. package/src/components/prose-regions.ts +153 -0
  278. package/src/components/scroll-region.tsx +20 -360
  279. package/src/components/tab-stop-parse.ts +217 -0
  280. package/src/components/templated-text-field.tsx +38 -330
  281. package/src/components/templated-text-kwargs-editor.tsx +147 -0
  282. package/src/components/templated-text-kwargs.ts +92 -0
  283. package/src/components/templated-text-source.tsx +226 -0
  284. package/src/components/tool-picker-parts.tsx +184 -0
  285. package/src/components/tool-picker.tsx +22 -114
  286. package/src/components/version-history-panel.tsx +38 -142
  287. package/src/components/version-history-rows.tsx +220 -0
  288. package/src/guards.ts +7 -0
  289. package/src/hooks/interactions-connection.ts +227 -0
  290. package/src/hooks/interactions-stream.ts +274 -0
  291. package/src/hooks/useSse.ts +50 -477
  292. package/src/navigation/app-link.tsx +70 -0
  293. package/src/navigation/context.tsx +11 -330
  294. package/src/navigation/entry-state.ts +42 -0
  295. package/src/navigation/guard-registry.ts +65 -0
  296. package/src/navigation/guarded-navigation.ts +147 -0
  297. package/src/navigation/index.ts +2 -2
  298. package/src/navigation/types.ts +8 -11
  299. package/src/navigation/use-back-forward-guard.ts +90 -0
  300. package/src/plugin/registry.ts +111 -90
  301. package/src/schema-editor/SchemaEditor.tsx +9 -1
  302. package/src/schema-form/SecretRefField.tsx +121 -71
  303. package/src/schema-form/classify.ts +244 -395
  304. package/src/schema-form/default-value.ts +48 -17
  305. package/src/schema-form/enum-field.tsx +1 -1
  306. package/src/schema-form/field-model.ts +133 -0
  307. package/src/schema-form/field-node.tsx +94 -38
  308. package/src/schema-form/media-field.tsx +134 -65
  309. package/src/schema-form/string-annotations.ts +111 -0
  310. package/src/schema-form/string-field.tsx +1 -1
  311. package/src/schema-form/union-field.tsx +2 -1
  312. package/src/schema-form/union.ts +1 -1
  313. package/src/schema-form/validate.ts +184 -103
  314. package/src/state-binding/AdapterMapping.tsx +0 -0
  315. package/src/state-binding/StateAttachRow.tsx +222 -147
  316. package/src/state-binding/adapter-mapping-row.tsx +232 -0
  317. package/src/structured-output/StructuredOutput.tsx +1 -4
  318. package/dist/components/icons.d.ts +0 -160
  319. package/dist/components/icons.d.ts.map +0 -1
  320. package/dist/components/icons.js +0 -160
  321. package/dist/components/icons.js.map +0 -1
  322. package/src/components/icons.tsx +0 -586
@@ -6,87 +6,17 @@ import {
6
6
  useEffect,
7
7
  useMemo,
8
8
  useRef,
9
- useState,
10
- type AriaAttributes,
11
- type CSSProperties,
12
- type MouseEvent,
13
9
  type ReactNode,
14
10
  } from 'react';
15
11
 
16
- import type {
17
- NavigationContextValue,
18
- NavigationGuardHandler,
19
- RouteSearch,
20
- RouteToken,
21
- } from './types';
12
+ import { NavigationGuardRegistry, type GuardEntry } from './guard-registry';
13
+ import { buildGuardedNavigation, type CommittedEntry } from './guarded-navigation';
14
+ import { useBackForwardGuard } from './use-back-forward-guard';
15
+ import type { NavigationContextValue, NavigationGuardHandler } from './types';
22
16
 
23
17
  /** Shell-provided navigation; no default, so use outside {@link NavigationProvider} throws. */
24
18
  const NavigationContext = createContext<NavigationContextValue | null>(null);
25
19
 
26
- /**
27
- * One armed navigation guard. The registry holds this exact object (not a
28
- * snapshot), so flipping `when` or swapping `handler` on re-render takes effect
29
- * without re-registering.
30
- */
31
- interface GuardEntry {
32
- when: boolean;
33
- handler: NavigationGuardHandler;
34
- }
35
-
36
- /**
37
- * The guards active under one {@link NavigationProvider} — the single registry
38
- * every SDK-controlled navigation consults, so feature and plugin-page guards
39
- * compose against the same set. `run` stops at the first veto (at most one confirm
40
- * dialog); `subscribe` re-evaluates browser-level listeners when the armed set changes.
41
- */
42
- class NavigationGuardRegistry {
43
- private readonly guards = new Set<GuardEntry>();
44
- private readonly listeners = new Set<() => void>();
45
-
46
- /** Register a guard; returns the unregister function. */
47
- add(entry: GuardEntry): () => void {
48
- this.guards.add(entry);
49
- this.emit();
50
- return () => {
51
- this.guards.delete(entry);
52
- this.emit();
53
- };
54
- }
55
-
56
- /** Notify subscribers that a guard's `when` may have changed. */
57
- touch(): void {
58
- this.emit();
59
- }
60
-
61
- subscribe(listener: () => void): () => void {
62
- this.listeners.add(listener);
63
- return () => {
64
- this.listeners.delete(listener);
65
- };
66
- }
67
-
68
- hasArmedGuards(): boolean {
69
- for (const entry of this.guards) {
70
- if (entry.when) return true;
71
- }
72
- return false;
73
- }
74
-
75
- /** Resolve `true` only if every armed guard allows the navigation. */
76
- async run(): Promise<boolean> {
77
- for (const entry of this.guards) {
78
- if (!entry.when) continue;
79
- const allowed = await entry.handler();
80
- if (!allowed) return false;
81
- }
82
- return true;
83
- }
84
-
85
- private emit(): void {
86
- for (const listener of this.listeners) listener();
87
- }
88
- }
89
-
90
20
  /**
91
21
  * Carries the provider's guard registry to {@link useNavigationGuard}. Separate from
92
22
  * {@link NavigationContext} so the shell-supplied value stays exactly the shell's
@@ -108,42 +38,6 @@ function useGuardRegistry(): NavigationGuardRegistry {
108
38
  return registry;
109
39
  }
110
40
 
111
- /**
112
- * The `history.state` namespace under which the host stores per-history-entry plugin
113
- * state, keyed by pluginId. Shared, by literal contract, with the shell's
114
- * `createNavigation` (which WRITES the slot) and the plugin catch-all page (which
115
- * READS it) — this SDK layer only reconstructs it for the committed-entry mirror.
116
- */
117
- const ENTRY_STATE_KEY = 'studioPluginEntryState';
118
-
119
- /**
120
- * Reconstruct the `history.state` bag a guard-approved plugin navigation LANDS on, so
121
- * a subsequently vetoed Back restores the entry with the state it just wrote intact
122
- * rather than clobbering it with null. Mirrors the host's
123
- * `{ studioPluginEntryState: { [pluginId]: state } }` namespace. TanStack's own
124
- * `key`/`__TSR_*` keys are unobservable at this layer, so this is a best-effort bag —
125
- * no worse than the `null` it replaces for those keys, and correct for the plugin slot.
126
- */
127
- function reconstructEntryState(pluginId: string, state: unknown): unknown {
128
- return { [ENTRY_STATE_KEY]: { [pluginId]: state } };
129
- }
130
-
131
- /**
132
- * Merge one plugin's state slot into a prior `history.state` bag, preserving BOTH the
133
- * router's own keys and every other plugin's slot. Used to refresh the committed entry
134
- * after {@link NavigationContextValue.updatePluginEntryState}, so a later vetoed Back
135
- * restores the UPDATED slot instead of the pre-update one.
136
- */
137
- function mergeEntryState(prev: unknown, pluginId: string, state: unknown): unknown {
138
- const base = prev !== null && typeof prev === 'object' ? (prev as Record<string, unknown>) : {};
139
- const priorSlot = base[ENTRY_STATE_KEY];
140
- const priorBag =
141
- priorSlot !== null && typeof priorSlot === 'object'
142
- ? (priorSlot as Record<string, unknown>)
143
- : {};
144
- return { ...base, [ENTRY_STATE_KEY]: { ...priorBag, [pluginId]: state } };
145
- }
146
-
147
41
  export function NavigationProvider({
148
42
  value,
149
43
  children,
@@ -158,174 +52,16 @@ export function NavigationProvider({
158
52
  // The last committed history entry the SDK observed (URL + `history.state`): what a
159
53
  // canceled back/forward restores to. URL changes made outside the SDK's navigate
160
54
  // entry points are unobservable by design at this layer.
161
- const committedRef = useRef<{ href: string; state: unknown } | null>(null);
162
-
163
- // Mirror the registry's armed state into React so the browser listeners attach only
164
- // while at least one guard is armed.
165
- const [armed, setArmed] = useState(false);
166
- useEffect(() => {
167
- setArmed(registry.hasArmedGuards());
168
- return registry.subscribe(() => {
169
- setArmed(registry.hasArmedGuards());
170
- });
171
- }, [registry]);
172
-
173
- useEffect(() => {
174
- if (!armed) return;
175
-
176
- // The protected entry (URL + state); a back/forward landing elsewhere is first
177
- // canceled back to here while the guard is consulted.
178
- committedRef.current = { href: window.location.href, state: window.history.state };
179
- // Set while replaying a guard-approved back/forward, so its `popstate` passes
180
- // through instead of being re-intercepted.
181
- let bypass = false;
182
- // Latched while a decision is pending, so a second Back mid-decision keeps canceling
183
- // without starting a concurrent `registry.run()` — one gesture, one dialog.
184
- let pending = false;
185
- // Restore pushes stacked for the pending decision, so an approved replay unwinds
186
- // exactly that far in one `go(-depth)`. Reset per decision.
187
- let restores = 0;
188
-
189
- const onPopState = () => {
190
- if (bypass) {
191
- bypass = false;
192
- committedRef.current = { href: window.location.href, state: window.history.state };
193
- return;
194
- }
195
- if (!registry.hasArmedGuards()) {
196
- committedRef.current = { href: window.location.href, state: window.history.state };
197
- return;
198
- }
199
- const committed = committedRef.current;
200
- if (committed === null) return;
201
- // Cancel the browser's move by pushing the committed entry on top of wherever it
202
- // landed. popstate exposes no direction/distance and the router owns
203
- // `history.state`, so restore can only push, not delete. A vetoed forward thus
204
- // strands a stub entry — an accepted limit the next pushState truncates.
205
- window.history.pushState(committed.state, '', committed.href);
206
- restores += 1;
207
- if (pending) return;
208
- pending = true;
209
- void registry.run().then((allowed) => {
210
- pending = false;
211
- const depth = restores;
212
- restores = 0;
213
- if (!allowed) return;
214
- bypass = true;
215
- window.history.go(-depth);
216
- });
217
- };
218
-
219
- const onBeforeUnload = (event: BeforeUnloadEvent) => {
220
- event.preventDefault();
221
- // Browsers below the floor (Chrome 108) gate the prompt on a set `returnValue`,
222
- // not `preventDefault` alone; the string is ignored. Typed deprecated, still required.
223
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- required beforeunload trigger for the supported browser range.
224
- event.returnValue = '';
225
- };
55
+ const committedRef = useRef<CommittedEntry | null>(null);
226
56
 
227
- window.addEventListener('popstate', onPopState);
228
- window.addEventListener('beforeunload', onBeforeUnload);
229
- return () => {
230
- committedRef.current = null;
231
- window.removeEventListener('popstate', onPopState);
232
- window.removeEventListener('beforeunload', onBeforeUnload);
233
- };
234
- }, [armed, registry]);
57
+ useBackForwardGuard(registry, committedRef);
235
58
 
236
59
  // Wrap the shell's transitions so an armed guard is consulted before they commit.
237
60
  // With no guard armed the call stays synchronous — identical to the raw shell value.
238
- const guarded = useMemo<NavigationContextValue>(() => {
239
- // Forwarded as a REST tuple, so the shell's `navigate` is called with exactly
240
- // the arguments the caller passed — a caller that wants the default history
241
- // behaviour never turns into one that passed an explicit `undefined` for it.
242
- const navigate: NavigationContextValue['navigate'] = (...args) => {
243
- const [token, search] = args;
244
- if (!registry.hasArmedGuards()) {
245
- value.navigate(...args);
246
- return;
247
- }
248
- void registry.run().then((allowed) => {
249
- if (!allowed) return;
250
- value.navigate(...args);
251
- // Advance the committed entry to the resolved destination (the router may commit
252
- // async, so `window.location` would be stale). Its router state is unobservable
253
- // here, hence `state: null`.
254
- if (committedRef.current !== null) {
255
- committedRef.current = { href: value.resolvePath(token, search), state: null };
256
- }
257
- });
258
- };
259
- const navigatePlugin: NavigationContextValue['navigatePlugin'] = (
260
- pluginId,
261
- pagePath,
262
- params,
263
- search,
264
- ) => {
265
- if (!registry.hasArmedGuards()) {
266
- value.navigatePlugin(pluginId, pagePath, params, search);
267
- return;
268
- }
269
- void registry.run().then((allowed) => {
270
- if (!allowed) return;
271
- value.navigatePlugin(pluginId, pagePath, params, search);
272
- if (committedRef.current !== null) {
273
- committedRef.current = {
274
- href: value.resolvePluginPath(pluginId, pagePath, params, search),
275
- state: null,
276
- };
277
- }
278
- });
279
- };
280
- const result: NavigationContextValue = { ...value, navigate, navigatePlugin };
281
-
282
- // navigatePluginWithOptions: guard-gated exactly like navigatePlugin (a veto never
283
- // reaches the host member — no entry, no state). On approval, refresh the committed
284
- // entry with the reconstructed state bag so a subsequent vetoed Back restores the
285
- // slot this navigation just wrote, not null. Only wrapped when the host provides it;
286
- // an older host leaves it undefined and the plugin-facing hook throws loudly.
287
- const hostNavigateWithOptions = value.navigatePluginWithOptions;
288
- if (hostNavigateWithOptions !== undefined) {
289
- result.navigatePluginWithOptions = (pluginId, pagePath, params, search, options) => {
290
- if (!registry.hasArmedGuards()) {
291
- hostNavigateWithOptions(pluginId, pagePath, params, search, options);
292
- return;
293
- }
294
- void registry.run().then((allowed) => {
295
- if (!allowed) return;
296
- hostNavigateWithOptions(pluginId, pagePath, params, search, options);
297
- if (committedRef.current !== null) {
298
- committedRef.current = {
299
- href: value.resolvePluginPath(pluginId, pagePath, params, search),
300
- state:
301
- options?.state !== undefined
302
- ? reconstructEntryState(pluginId, options.state)
303
- : null,
304
- };
305
- }
306
- });
307
- };
308
- }
309
-
310
- // updatePluginEntryState: guard-FREE — rewriting the current entry's state is not a
311
- // navigation away, so no guard is consulted. But the SDK MUST wrap it to refresh the
312
- // committed entry with the merged slot, so a later vetoed Back restores the UPDATED
313
- // state rather than the pre-update one. URL is unchanged, so href is preserved.
314
- const hostUpdateEntryState = value.updatePluginEntryState;
315
- if (hostUpdateEntryState !== undefined) {
316
- result.updatePluginEntryState = (pluginId, state) => {
317
- hostUpdateEntryState(pluginId, state);
318
- if (committedRef.current !== null) {
319
- committedRef.current = {
320
- href: committedRef.current.href,
321
- state: mergeEntryState(committedRef.current.state, pluginId, state),
322
- };
323
- }
324
- };
325
- }
326
-
327
- return result;
328
- }, [value, registry]);
61
+ const guarded = useMemo<NavigationContextValue>(
62
+ () => buildGuardedNavigation(value, registry, committedRef),
63
+ [value, registry],
64
+ );
329
65
 
330
66
  const commitHref = useCallback((href: string) => {
331
67
  if (committedRef.current !== null) committedRef.current = { href, state: null };
@@ -342,7 +78,7 @@ export function NavigationProvider({
342
78
  );
343
79
  }
344
80
 
345
- function useNavigation(): NavigationContextValue {
81
+ export function useNavigation(): NavigationContextValue {
346
82
  const ctx = useContext(NavigationContext);
347
83
  if (ctx === null) {
348
84
  throw new Error(
@@ -446,58 +182,3 @@ export function usePluginEntryNavigation(): {
446
182
  }
447
183
  return { navigatePluginWithOptions, updatePluginEntryState };
448
184
  }
449
-
450
- export interface AppLinkProps<T extends RouteToken> {
451
- to: T;
452
- search?: RouteSearch<T>;
453
- children: ReactNode;
454
- className?: string;
455
- 'aria-label'?: string;
456
- 'aria-current'?: AriaAttributes['aria-current'];
457
- /** Native tooltip text (also a sensible non-AT hover hint). */
458
- title?: string;
459
- /** Inline styles for the anchor. */
460
- style?: CSSProperties;
461
- }
462
-
463
- /**
464
- * A real anchor (so middle-click / open-in-new-tab keep working) that drives a
465
- * client-side transition on plain left-click. Modified clicks (new tab/window,
466
- * download) fall through to the browser's default handling.
467
- */
468
- export function AppLink<T extends RouteToken>({
469
- to,
470
- search,
471
- children,
472
- className,
473
- 'aria-label': ariaLabel,
474
- 'aria-current': ariaCurrent,
475
- title,
476
- style,
477
- }: AppLinkProps<T>): ReactNode {
478
- const { navigate, resolvePath } = useNavigation();
479
- const href = resolvePath(to, search);
480
- const onClick = useCallback(
481
- (event: MouseEvent<HTMLAnchorElement>) => {
482
- if (event.defaultPrevented) return;
483
- if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
484
- return;
485
- event.preventDefault();
486
- navigate(to, search);
487
- },
488
- [navigate, to, search],
489
- );
490
- return createElement(
491
- 'a',
492
- {
493
- href,
494
- className,
495
- 'aria-label': ariaLabel,
496
- 'aria-current': ariaCurrent,
497
- title,
498
- style,
499
- onClick,
500
- },
501
- children,
502
- );
503
- }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The committed-entry mirror of the host's per-history-entry plugin state. The SDK
3
+ * reconstructs and merges the `{ studioPluginEntryState: { [pluginId]: state } }`
4
+ * `history.state` namespace so a vetoed Back restores the entry with the slot a
5
+ * plugin navigation just wrote, rather than clobbering it with null.
6
+ */
7
+
8
+ /**
9
+ * The `history.state` namespace under which the host stores per-history-entry plugin
10
+ * state, keyed by pluginId. Shared, by literal contract, with the shell's
11
+ * `createNavigation` (which WRITES the slot) and the plugin catch-all page (which
12
+ * READS it) — this SDK layer only reconstructs it for the committed-entry mirror.
13
+ */
14
+ const ENTRY_STATE_KEY = 'studioPluginEntryState';
15
+
16
+ /**
17
+ * Reconstruct the `history.state` bag a guard-approved plugin navigation LANDS on, so
18
+ * a subsequently vetoed Back restores the entry with the state it just wrote intact
19
+ * rather than clobbering it with null. Mirrors the host's
20
+ * `{ studioPluginEntryState: { [pluginId]: state } }` namespace. TanStack's own
21
+ * `key`/`__TSR_*` keys are unobservable at this layer, so this is a best-effort bag —
22
+ * no worse than the `null` it replaces for those keys, and correct for the plugin slot.
23
+ */
24
+ export function reconstructEntryState(pluginId: string, state: unknown): unknown {
25
+ return { [ENTRY_STATE_KEY]: { [pluginId]: state } };
26
+ }
27
+
28
+ /**
29
+ * Merge one plugin's state slot into a prior `history.state` bag, preserving BOTH the
30
+ * router's own keys and every other plugin's slot. Used to refresh the committed entry
31
+ * after an entry-state update, so a later vetoed Back restores the UPDATED slot instead
32
+ * of the pre-update one.
33
+ */
34
+ export function mergeEntryState(prev: unknown, pluginId: string, state: unknown): unknown {
35
+ const base = prev !== null && typeof prev === 'object' ? (prev as Record<string, unknown>) : {};
36
+ const priorSlot = base[ENTRY_STATE_KEY];
37
+ const priorBag =
38
+ priorSlot !== null && typeof priorSlot === 'object'
39
+ ? (priorSlot as Record<string, unknown>)
40
+ : {};
41
+ return { ...base, [ENTRY_STATE_KEY]: { ...priorBag, [pluginId]: state } };
42
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The armed-navigation-guard registry: the single set every SDK-controlled
3
+ * navigation consults, so feature and plugin-page guards compose against the same
4
+ * guards. `run` stops at the first veto (at most one confirm dialog); `subscribe`
5
+ * lets the provider re-evaluate browser-level listeners when the armed set changes.
6
+ */
7
+ import type { NavigationGuardHandler } from './types';
8
+
9
+ /**
10
+ * One armed navigation guard. The registry holds this exact object (not a
11
+ * snapshot), so flipping `when` or swapping `handler` on re-render takes effect
12
+ * without re-registering.
13
+ */
14
+ export interface GuardEntry {
15
+ when: boolean;
16
+ handler: NavigationGuardHandler;
17
+ }
18
+
19
+ export class NavigationGuardRegistry {
20
+ private readonly guards = new Set<GuardEntry>();
21
+ private readonly listeners = new Set<() => void>();
22
+
23
+ /** Register a guard; returns the unregister function. */
24
+ add(entry: GuardEntry): () => void {
25
+ this.guards.add(entry);
26
+ this.emit();
27
+ return () => {
28
+ this.guards.delete(entry);
29
+ this.emit();
30
+ };
31
+ }
32
+
33
+ /** Notify subscribers that a guard's `when` may have changed. */
34
+ touch(): void {
35
+ this.emit();
36
+ }
37
+
38
+ subscribe(listener: () => void): () => void {
39
+ this.listeners.add(listener);
40
+ return () => {
41
+ this.listeners.delete(listener);
42
+ };
43
+ }
44
+
45
+ hasArmedGuards(): boolean {
46
+ for (const entry of this.guards) {
47
+ if (entry.when) return true;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ /** Resolve `true` only if every armed guard allows the navigation. */
53
+ async run(): Promise<boolean> {
54
+ for (const entry of this.guards) {
55
+ if (!entry.when) continue;
56
+ const allowed = await entry.handler();
57
+ if (!allowed) return false;
58
+ }
59
+ return true;
60
+ }
61
+
62
+ private emit(): void {
63
+ for (const listener of this.listeners) listener();
64
+ }
65
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Wrap a shell-supplied {@link NavigationContextValue} so every SDK-controlled
3
+ * transition consults the armed guards before it commits, and — on approval —
4
+ * advances the committed-entry mirror to the resolved destination. With no guard
5
+ * armed each call stays synchronous, identical to the raw shell value.
6
+ */
7
+ import type { RefObject } from 'react';
8
+
9
+ import { mergeEntryState, reconstructEntryState } from './entry-state';
10
+ import type { NavigationGuardRegistry } from './guard-registry';
11
+ import type { NavigationContextValue } from './types';
12
+
13
+ /** The last committed history entry the SDK observed: what a canceled back/forward restores to. */
14
+ export interface CommittedEntry {
15
+ href: string;
16
+ state: unknown;
17
+ }
18
+
19
+ type CommittedRef = RefObject<CommittedEntry | null>;
20
+
21
+ // navigate: forwarded as a REST tuple so the shell's `navigate` is called with
22
+ // exactly the arguments the caller passed — a caller wanting the default history
23
+ // behaviour never turns into one that passed an explicit `undefined` for it.
24
+ function buildNavigate(
25
+ value: NavigationContextValue,
26
+ registry: NavigationGuardRegistry,
27
+ committedRef: CommittedRef,
28
+ ): NavigationContextValue['navigate'] {
29
+ return (...args) => {
30
+ const [token, search] = args;
31
+ if (!registry.hasArmedGuards()) {
32
+ value.navigate(...args);
33
+ return;
34
+ }
35
+ void registry.run().then((allowed) => {
36
+ if (!allowed) return;
37
+ value.navigate(...args);
38
+ // Advance the committed entry to the resolved destination (the router may commit
39
+ // async, so `window.location` would be stale). Router state is unobservable here.
40
+ if (committedRef.current !== null) {
41
+ committedRef.current = { href: value.resolvePath(token, search), state: null };
42
+ }
43
+ });
44
+ };
45
+ }
46
+
47
+ function buildNavigatePlugin(
48
+ value: NavigationContextValue,
49
+ registry: NavigationGuardRegistry,
50
+ committedRef: CommittedRef,
51
+ ): NavigationContextValue['navigatePlugin'] {
52
+ return (pluginId, pagePath, params, search) => {
53
+ if (!registry.hasArmedGuards()) {
54
+ value.navigatePlugin(pluginId, pagePath, params, search);
55
+ return;
56
+ }
57
+ void registry.run().then((allowed) => {
58
+ if (!allowed) return;
59
+ value.navigatePlugin(pluginId, pagePath, params, search);
60
+ if (committedRef.current !== null) {
61
+ committedRef.current = {
62
+ href: value.resolvePluginPath(pluginId, pagePath, params, search),
63
+ state: null,
64
+ };
65
+ }
66
+ });
67
+ };
68
+ }
69
+
70
+ // navigatePluginWithOptions: guard-gated exactly like navigatePlugin (a veto never
71
+ // reaches the host member). On approval, refresh the committed entry with the
72
+ // reconstructed state bag so a subsequent vetoed Back restores the slot this
73
+ // navigation just wrote, not null.
74
+ function buildNavigateWithOptions(
75
+ hostNavigateWithOptions: NonNullable<NavigationContextValue['navigatePluginWithOptions']>,
76
+ value: NavigationContextValue,
77
+ registry: NavigationGuardRegistry,
78
+ committedRef: CommittedRef,
79
+ ): NonNullable<NavigationContextValue['navigatePluginWithOptions']> {
80
+ return (pluginId, pagePath, params, search, options) => {
81
+ if (!registry.hasArmedGuards()) {
82
+ hostNavigateWithOptions(pluginId, pagePath, params, search, options);
83
+ return;
84
+ }
85
+ void registry.run().then((allowed) => {
86
+ if (!allowed) return;
87
+ hostNavigateWithOptions(pluginId, pagePath, params, search, options);
88
+ if (committedRef.current !== null) {
89
+ committedRef.current = {
90
+ href: value.resolvePluginPath(pluginId, pagePath, params, search),
91
+ state:
92
+ options?.state !== undefined ? reconstructEntryState(pluginId, options.state) : null,
93
+ };
94
+ }
95
+ });
96
+ };
97
+ }
98
+
99
+ // updatePluginEntryState: guard-FREE — rewriting the current entry's state is not a
100
+ // navigation away. The SDK still wraps it to refresh the committed entry with the
101
+ // merged slot, so a later vetoed Back restores the UPDATED state. URL is unchanged.
102
+ function buildUpdateEntryState(
103
+ hostUpdateEntryState: NonNullable<NavigationContextValue['updatePluginEntryState']>,
104
+ committedRef: CommittedRef,
105
+ ): NonNullable<NavigationContextValue['updatePluginEntryState']> {
106
+ return (pluginId, state) => {
107
+ hostUpdateEntryState(pluginId, state);
108
+ if (committedRef.current !== null) {
109
+ committedRef.current = {
110
+ href: committedRef.current.href,
111
+ state: mergeEntryState(committedRef.current.state, pluginId, state),
112
+ };
113
+ }
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Build the guarded navigation value. The two host-optional members
119
+ * (`navigatePluginWithOptions`, `updatePluginEntryState`) are wrapped only when the
120
+ * host provides them; an older host leaves them undefined and the plugin-facing hook
121
+ * throws loudly.
122
+ */
123
+ export function buildGuardedNavigation(
124
+ value: NavigationContextValue,
125
+ registry: NavigationGuardRegistry,
126
+ committedRef: CommittedRef,
127
+ ): NavigationContextValue {
128
+ const result: NavigationContextValue = {
129
+ ...value,
130
+ navigate: buildNavigate(value, registry, committedRef),
131
+ navigatePlugin: buildNavigatePlugin(value, registry, committedRef),
132
+ };
133
+ const hostNavigateWithOptions = value.navigatePluginWithOptions;
134
+ if (hostNavigateWithOptions !== undefined) {
135
+ result.navigatePluginWithOptions = buildNavigateWithOptions(
136
+ hostNavigateWithOptions,
137
+ value,
138
+ registry,
139
+ committedRef,
140
+ );
141
+ }
142
+ const hostUpdateEntryState = value.updatePluginEntryState;
143
+ if (hostUpdateEntryState !== undefined) {
144
+ result.updatePluginEntryState = buildUpdateEntryState(hostUpdateEntryState, committedRef);
145
+ }
146
+ return result;
147
+ }
@@ -1,6 +1,5 @@
1
1
  export {
2
2
  NavigationProvider,
3
- AppLink,
4
3
  useAppNavigate,
5
4
  useResolvePath,
6
5
  usePluginNavigation,
@@ -8,7 +7,8 @@ export {
8
7
  useNavigationGuard,
9
8
  useNavigationGate,
10
9
  } from './context';
11
- export type { AppLinkProps } from './context';
10
+ export { AppLink } from './app-link';
11
+ export type { AppLinkProps } from './app-link';
12
12
  export { useSearchCommit } from './use-search-commit';
13
13
  export type { SearchCommitParams } from './use-search-commit';
14
14
  export type {
@@ -193,21 +193,18 @@ export interface NavigationContextValue {
193
193
  ) => string;
194
194
  /**
195
195
  * ADDITIVE plugin-page navigation that also carries per-history-entry
196
- * {@link PluginNavigateOptions}. OPTIONAL because a Studio host built before this
197
- * feature does not provide it; plugins reach it — and require a host that does —
198
- * through {@link usePluginEntryNavigation}, which throws loudly on an older host.
196
+ * {@link PluginNavigateOptions}. OPTIONAL because a host that does not implement it
197
+ * leaves it undefined; plugins reach it — and require a host that provides it —
198
+ * through {@link usePluginEntryNavigation}, which throws loudly when it is absent.
199
199
  * It behaves exactly like {@link navigatePlugin} for path / params / search, and in
200
200
  * addition writes `options.state` onto the DESTINATION entry under the plugin's own
201
201
  * namespace slot (see {@link PluginPageProps.entryState}).
202
202
  *
203
- * NEXT-MAJOR (9.0) CONSOLIDATION NOTE: fold `options` into {@link navigatePlugin}'s
204
- * own signature and RETIRE this sibling. It exists as a separate optional member
205
- * ONLY to stay additive under the plugin-API equality gate — a new optional member
206
- * does not move {@link STUDIO_PLUGIN_API_VERSION}, so every existing plugin keeps
207
- * loading. Cutting a MAJOR now purely to rename one method onto another is the
208
- * disproportion anti-pattern: a breaking change for a cosmetic merge. The merge
209
- * therefore waits for the next major that carries real breaks; until then both
210
- * coexist and {@link navigatePlugin} stays byte-for-byte stable.
203
+ * It is a separate optional member rather than an extra parameter on
204
+ * {@link navigatePlugin} so it stays additive under the plugin-API equality gate: a
205
+ * new optional member does not move {@link STUDIO_PLUGIN_API_VERSION}, so every
206
+ * existing plugin keeps loading and {@link navigatePlugin}'s own signature stays
207
+ * stable.
211
208
  */
212
209
  navigatePluginWithOptions?: (
213
210
  pluginId: string,