@jbrowse/app-core 4.3.0 → 5.0.0-beta.2

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 (158) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1598 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +225 -0
  8. package/esm/Assemblies/AssembliesMixin.js +149 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +19 -0
  12. package/esm/HistoryManagement/index.js +42 -5
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +171 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +85 -7
  15. package/esm/JBrowseConfig/index.d.ts +173 -84
  16. package/esm/JBrowseConfig/index.js +62 -2
  17. package/esm/JBrowseModel/index.d.ts +180 -95
  18. package/esm/JBrowseModel/index.js +122 -21
  19. package/esm/JbApi/index.d.ts +2 -0
  20. package/esm/JbApi/index.js +1 -0
  21. package/esm/JbApi/jbApi.d.ts +325 -0
  22. package/esm/JbApi/jbApi.js +748 -0
  23. package/esm/RootMenu/index.d.ts +69 -3
  24. package/esm/RootMenu/index.js +96 -61
  25. package/esm/SessionSpec/index.d.ts +6 -0
  26. package/esm/SessionSpec/index.js +3 -0
  27. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  28. package/esm/SessionSpec/lgvUrlInit.js +158 -0
  29. package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
  30. package/esm/SessionSpec/loadSessionSpec.js +392 -0
  31. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
  32. package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
  33. package/esm/SessionSpec/types.d.ts +31 -0
  34. package/esm/SessionSpec/types.js +1 -0
  35. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  36. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  37. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  38. package/esm/WorkspaceLayout/PanelView.js +99 -0
  39. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  40. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  41. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  42. package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
  43. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  44. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  45. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  46. package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
  47. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  48. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  49. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  50. package/esm/WorkspaceLayout/dropZone.js +113 -0
  51. package/esm/WorkspaceLayout/index.d.ts +8 -0
  52. package/esm/WorkspaceLayout/index.js +4 -0
  53. package/esm/WorkspaceLayout/model.d.ts +248 -0
  54. package/esm/WorkspaceLayout/model.js +562 -0
  55. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  56. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  57. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  58. package/esm/WorkspaceLayout/spec.js +226 -0
  59. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  60. package/esm/WorkspaceLayout/splitter.js +37 -0
  61. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  62. package/esm/WorkspaceLayout/tree.js +379 -0
  63. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  64. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  65. package/esm/index.d.ts +18 -9
  66. package/esm/index.js +16 -9
  67. package/esm/menus.d.ts +55 -68
  68. package/esm/menus.js +117 -86
  69. package/esm/queryParams.d.ts +9 -0
  70. package/esm/queryParams.js +92 -0
  71. package/esm/sessionModelFactory.d.ts +18 -0
  72. package/esm/sessionModelFactory.js +1 -0
  73. package/esm/ui/App/App.d.ts +3 -14
  74. package/esm/ui/App/App.js +30 -17
  75. package/esm/ui/App/AppFab.d.ts +12 -3
  76. package/esm/ui/App/AppFab.js +15 -8
  77. package/esm/ui/App/AppReadyMarker.d.ts +43 -0
  78. package/esm/ui/App/AppReadyMarker.js +56 -0
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +23 -24
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerControls.d.ts +1 -5
  88. package/esm/ui/App/DrawerControls.js +1 -25
  89. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  90. package/esm/ui/App/DrawerHeader.js +24 -10
  91. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  92. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  93. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  94. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  95. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  96. package/esm/ui/App/DrawerWidget.js +9 -21
  97. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  98. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +144 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +9 -2
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +42 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +3 -8
  118. package/esm/ui/App/ViewsContainer.js +9 -6
  119. package/esm/ui/App/copyView.js +1 -1
  120. package/esm/ui/App/index.d.ts +2 -1
  121. package/esm/ui/App/index.js +3 -2
  122. package/esm/ui/App/lazyChunk.d.ts +13 -0
  123. package/esm/ui/App/lazyChunk.js +91 -0
  124. package/esm/ui/App/types.d.ts +12 -3
  125. package/esm/ui/App/types.js +1 -1
  126. package/esm/ui/App/useViewVisibility.d.ts +34 -0
  127. package/esm/ui/App/useViewVisibility.js +87 -0
  128. package/esm/ui/App/viewTitle.d.ts +5 -0
  129. package/esm/ui/App/viewTitle.js +9 -0
  130. package/esm/ui/index.d.ts +1 -1
  131. package/esm/ui/index.js +1 -1
  132. package/package.json +12 -17
  133. package/esm/AppFocus/index.d.ts +0 -5
  134. package/esm/AppFocus/index.js +0 -12
  135. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  136. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  137. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  138. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  139. package/esm/DockviewLayout/index.d.ts +0 -35
  140. package/esm/DockviewLayout/index.js +0 -111
  141. package/esm/ui/App/DockviewContext.d.ts +0 -22
  142. package/esm/ui/App/DockviewContext.js +0 -29
  143. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  144. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  145. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  146. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  147. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  148. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  149. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  150. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  151. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  152. package/esm/ui/App/JBrowseViewTab.js +0 -97
  153. package/esm/ui/App/ModalWidget.d.ts +0 -6
  154. package/esm/ui/App/ModalWidget.js +0 -48
  155. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  156. package/esm/ui/App/TiledViewsContainer.js +0 -366
  157. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  158. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -0,0 +1,748 @@
1
+ import { setReExportRegistry } from '@jbrowse/core/ReExports/registry';
2
+ import { getConf, getConfigurationSchemaDefinition, isSlotDefinitionEntry, readConfObject, } from '@jbrowse/core/configuration';
3
+ import { getFeatureAdapterOrThrow } from '@jbrowse/core/data_adapters/getFeatureAdapter';
4
+ import { adapterByteLimit, measureRegionBytes, } from '@jbrowse/core/rpc/byteBudget';
5
+ import { getRpcSessionId, isElectron, isSessionWithAddSessionTrack, objectHash, parseLocString, renameRegionsIfNeeded, } from '@jbrowse/core/util';
6
+ import { createStopToken, stopStopToken } from '@jbrowse/core/util/stopToken';
7
+ import { allSessionTracks, getConfAssemblyNamesOrNone, guessTrackConfForLocation, isSameAssemblyName, viewCanDisplayTrack, viewDisplayNames, } from '@jbrowse/core/util/tracks';
8
+ import * as mst from '@jbrowse/mobx-state-tree';
9
+ import { getSnapshot, getType, isStateTreeNode } from '@jbrowse/mobx-state-tree';
10
+ import * as mobx from 'mobx';
11
+ // relative, not '@jbrowse/app-core': a package self-import would make this
12
+ // module depend on the barrel that exports it
13
+ import { loadSessionSpec } from '../SessionSpec/index.js';
14
+ // Matches BaseTrackModel's DEFAULT_EXPORT_BYTE_LIMIT: both bound a fetch
15
+ // someone asked for by name rather than one a view scrolled into, so they
16
+ // should refuse at the same size.
17
+ const DEFAULT_AGENT_BYTE_LIMIT = 5_000_000;
18
+ /**
19
+ * What jb.getFeatures may pull before it refuses. An explicit request wins so
20
+ * an agent can mean it; otherwise the adapter's own declared limit, so this
21
+ * does not disagree with the size the track's display already refuses to
22
+ * render; otherwise the default.
23
+ */
24
+ export function agentByteLimit(declaredFetchSizeLimit, requested) {
25
+ return (requested ??
26
+ adapterByteLimit(declaredFetchSizeLimit, DEFAULT_AGENT_BYTE_LIMIT));
27
+ }
28
+ function viewSelf(view) {
29
+ return view;
30
+ }
31
+ // The views' own census contract (BaseViewModel derives `allViews` and
32
+ // `ownTracks`), so this module carries no copy of the view nesting: a synteny
33
+ // stack's rows and per-band containers, a breakpoint split view's panels, and
34
+ // any depth of nesting all arrive through each view answering for itself.
35
+ function allViews(session) {
36
+ return session.views.flatMap(v => v.allViews);
37
+ }
38
+ function viewTracks(view) {
39
+ return view.ownTracks;
40
+ }
41
+ function allTracks(session) {
42
+ return allViews(session).flatMap(v => viewTracks(v));
43
+ }
44
+ // The drawn display's on-screen state. `displayPhase` is the tree's own
45
+ // single-sourced vocabulary (render-core/displayPhase.ts) and the same one
46
+ // AppReadyMarker reads; `tooLarge` and `renderError` replace the display
47
+ // subtree rather than raising a snackbar, so a settle reporting only
48
+ // notifications calls them ready.
49
+ function displayState(track) {
50
+ const display = track.displays?.[0];
51
+ if (!display) {
52
+ return {};
53
+ }
54
+ try {
55
+ const phase = display.displayPhase;
56
+ return {
57
+ display: display.type,
58
+ // "make it all fit in the window" is a normal thing to be asked, and
59
+ // without this the only way to answer it is measuring DOM rectangles
60
+ ...(typeof display.height === 'number' ? { height: display.height } : {}),
61
+ ...(phase === undefined || phase === 'ready' ? {} : { phase }),
62
+ ...(phase === 'tooLarge' && display.regionTooLargeReason
63
+ ? { reason: display.regionTooLargeReason }
64
+ : {}),
65
+ ...(display.error ? { error: String(display.error) } : {}),
66
+ };
67
+ }
68
+ catch (e) {
69
+ return { display: display.type, error: String(e) };
70
+ }
71
+ }
72
+ export function sessionOf(pluginManager) {
73
+ return pluginManager?.rootModel?.session;
74
+ }
75
+ function viewSummary(view) {
76
+ const v = viewSelf(view);
77
+ return {
78
+ id: v.id,
79
+ type: v.type,
80
+ ...(v.displayName ? { displayName: v.displayName } : {}),
81
+ ...(v.assemblyNames?.length ? { assemblyNames: v.assemblyNames } : {}),
82
+ ...(v.coarseVisibleLocStrings
83
+ ? { visibleRegion: v.coarseVisibleLocStrings }
84
+ : {}),
85
+ // the stack's own height, beside each track's below it: "does this all fit
86
+ // on screen" is then arithmetic rather than three rounds of screenshotting
87
+ ...(typeof v.height === 'number' ? { height: v.height } : {}),
88
+ ...(viewTracks(view).length
89
+ ? {
90
+ tracks: viewTracks(view).map(t => ({
91
+ trackId: t.configuration.trackId,
92
+ trackType: t.type,
93
+ ...displayState(t),
94
+ })),
95
+ }
96
+ : {}),
97
+ ...(Array.isArray(v.views) && v.views.length
98
+ ? { views: v.views.map(sub => viewSummary(sub)) }
99
+ : {}),
100
+ };
101
+ }
102
+ function sessionSummary(session) {
103
+ return {
104
+ name: session.name,
105
+ assemblyNames: session.assemblyNames,
106
+ views: session.views.map(v => viewSummary(v)),
107
+ };
108
+ }
109
+ // MST nodes serialize as their snapshot (toJSON), so the live-model walk below
110
+ // can stringify whatever it lands on; the replacer guards what a snapshot
111
+ // cannot contain but a getter's return can.
112
+ export function safeJson(value) {
113
+ // the ANCESTOR chain, not every object seen: MST snapshots are cached and
114
+ // structurally shared, so the same frozen object legitimately appears at two
115
+ // paths, and a visited-set reports the second one as a cycle that is not
116
+ // there. `this` is the holder JSON.stringify is currently walking, which is
117
+ // what lets the chain unwind on the way back up — so this must stay a
118
+ // `function`, not an arrow.
119
+ const chain = [];
120
+ // typed by hand: JSON.stringify's declared return is string, but a bare
121
+ // Symbol/undefined at the top level really does yield undefined at runtime
122
+ const json = JSON.stringify(isStateTreeNode(value) ? getSnapshot(value) : value, function (_key, v) {
123
+ while (chain.length > 0 && chain.at(-1) !== this) {
124
+ chain.pop();
125
+ }
126
+ if (typeof v === 'function') {
127
+ return '[function]';
128
+ }
129
+ if (typeof v === 'bigint') {
130
+ return `${v}`;
131
+ }
132
+ if (v !== null && typeof v === 'object') {
133
+ if (chain.includes(v)) {
134
+ return '[circular]';
135
+ }
136
+ chain.push(v);
137
+ }
138
+ return v;
139
+ });
140
+ return json === undefined ? '"[unserializable]"' : json;
141
+ }
142
+ function describeBrief(value) {
143
+ if (Array.isArray(value)) {
144
+ return `array(${value.length})`;
145
+ }
146
+ if (value !== null && typeof value === 'object') {
147
+ const json = safeJson(value);
148
+ return json.length > 120 ? `object(~${json.length} bytes)` : json;
149
+ }
150
+ const json = safeJson(value);
151
+ return json.length > 120 ? `${json.slice(0, 117)}...` : json;
152
+ }
153
+ // The names a live node answers beyond its snapshot: MST defines views as own
154
+ // getter properties on the instance, which is exactly the high-value surface
155
+ // (visibleLocStrings, assemblyNames, totalBp, ...) a snapshot filters out.
156
+ function memberNames(node) {
157
+ const getters = [];
158
+ const methods = [];
159
+ for (const [key, desc] of Object.entries(Object.getOwnPropertyDescriptors(node))) {
160
+ if (!key.startsWith('$') && key !== 'toJSON') {
161
+ if (desc.get) {
162
+ getters.push(key);
163
+ }
164
+ else if (typeof desc.value === 'function') {
165
+ methods.push(key);
166
+ }
167
+ }
168
+ }
169
+ return { getters: getters.sort(), methods: methods.sort() };
170
+ }
171
+ function inspectSession(session, args) {
172
+ const path = typeof args.path === 'string' ? args.path : '';
173
+ const maxBytes = typeof args.maxBytes === 'number' ? args.maxBytes : 20_000;
174
+ let node = session;
175
+ const walked = [];
176
+ for (const segment of path.split('.').filter(Boolean)) {
177
+ node =
178
+ node !== null && typeof node === 'object'
179
+ ? node[segment]
180
+ : undefined;
181
+ if (node === undefined) {
182
+ throw new Error(`Nothing at "${path}" (undefined after "${walked.join('.') || '(root)'}"). Inspect the parent path to see its keys and getters.`);
183
+ }
184
+ walked.push(segment);
185
+ }
186
+ const members = node !== null && typeof node === 'object' && !Array.isArray(node)
187
+ ? memberNames(node)
188
+ : { getters: [], methods: [] };
189
+ const json = safeJson(node);
190
+ const base = {
191
+ path: path || '(session root)',
192
+ // the name the docs tool files a type under: docs topic "model:<modelType>"
193
+ ...(isStateTreeNode(node) ? { modelType: getType(node).name } : {}),
194
+ bytes: json.length,
195
+ ...(members.getters.length ? { getters: members.getters } : {}),
196
+ ...(members.methods.length ? { actions: members.methods } : {}),
197
+ };
198
+ if (json.length <= maxBytes) {
199
+ return { ...base, value: JSON.parse(json) };
200
+ }
201
+ const plain = JSON.parse(json);
202
+ return Array.isArray(plain)
203
+ ? {
204
+ ...base,
205
+ note: `too large to return whole — ${plain.length} items; index in with .N or raise maxBytes`,
206
+ items: plain.slice(0, 20).map(item => describeBrief(item)),
207
+ }
208
+ : {
209
+ ...base,
210
+ note: 'too large to return whole — drill down by path or raise maxBytes',
211
+ keys: Object.fromEntries(Object.entries(plain).map(([k, v]) => [
212
+ k,
213
+ describeBrief(v),
214
+ ])),
215
+ };
216
+ }
217
+ const delay = (ms) => new Promise(resolve => {
218
+ setTimeout(resolve, ms);
219
+ });
220
+ // The readiness contract is AppReadyMarker's, not this file's: it publishes
221
+ // data-app-phase from the session (ready = no view resolving an assembly, no
222
+ // display fetching), and says so in its own comment.
223
+ //
224
+ // The hold is why the number is a second: `ready` has to survive past the
225
+ // ~600ms FetchVisibleRegions debounce, or one sample taken right after a
226
+ // navigation reads the pre-navigation frame as finished. Same reasoning and
227
+ // same constant as `waitForAppSettled` in products/jbrowse-capture/src/waits.ts
228
+ // (APP_SETTLED_HOLD_MS) — reached independently there, and measured; see
229
+ // REJECTED_IDEAS under "Waiting out a screenshot action's work".
230
+ const READY_HOLD_MS = 1000;
231
+ // An error toast stays up until a human dismisses it, and an info toast leaves
232
+ // on its own — so "the last five messages" repeated one stale error in every
233
+ // settle result for the rest of a run and named no level for the rest. Each
234
+ // toast is delivered to the caller once, by identity: the mobx array keeps the
235
+ // object it was pushed as, and removeSnackbarMessage relies on that too.
236
+ const deliveredNotifications = new WeakMap();
237
+ export function undeliveredNotifications(session) {
238
+ const entries = 'snackbarMessages' in session
239
+ ? session.snackbarMessages
240
+ : [];
241
+ const known = deliveredNotifications.get(session);
242
+ const delivered = known ?? new WeakSet();
243
+ if (!known) {
244
+ deliveredNotifications.set(session, delivered);
245
+ }
246
+ const fresh = entries.filter(m => !delivered.has(m));
247
+ for (const m of fresh) {
248
+ delivered.add(m);
249
+ }
250
+ return fresh.map(m => ({
251
+ level: m.level ?? 'info',
252
+ message: m.message,
253
+ }));
254
+ }
255
+ export async function waitReady(timeoutMs, session,
256
+ // scoped rather than document-wide because @jbrowse/react-app2 embeds this
257
+ // app in a host page: two mounted apps publish two ready markers, and a
258
+ // document-wide query answers for whichever is first in document order
259
+ root = document) {
260
+ const deadline = Date.now() + timeoutMs;
261
+ let readySince;
262
+ let outcome;
263
+ while (outcome === undefined) {
264
+ const ready = root.querySelector('[data-app-phase="ready"]') !== null &&
265
+ root.querySelector('[data-testid="loading-overlay"]') === null;
266
+ readySince = ready ? (readySince ?? Date.now()) : undefined;
267
+ if (readySince !== undefined && Date.now() - readySince >= READY_HOLD_MS) {
268
+ outcome = { settled: true };
269
+ }
270
+ else if (Date.now() >= deadline) {
271
+ outcome = {
272
+ settled: false,
273
+ note: 'rendering had not settled by the timeout; tracks may still be loading',
274
+ };
275
+ }
276
+ else {
277
+ await delay(200);
278
+ }
279
+ }
280
+ const messages = session ? undeliveredNotifications(session) : [];
281
+ // A display that refuses to draw — over the fetch-size gate, or errored —
282
+ // replaces its own subtree instead of raising a snackbar, so notifications
283
+ // alone report a clean settle over a browser with a blank track in it.
284
+ const notReady = session
285
+ ? allTracks(session)
286
+ .map(t => ({ trackId: t.configuration.trackId, ...displayState(t) }))
287
+ .filter(t => 'phase' in t || 'error' in t)
288
+ : [];
289
+ const offscreen = session ? offscreenViews(session, root) : undefined;
290
+ return {
291
+ ...outcome,
292
+ ...(messages.length ? { notifications: messages } : {}),
293
+ ...(notReady.length ? { notReady } : {}),
294
+ ...(offscreen ? { offscreen } : {}),
295
+ };
296
+ }
297
+ // A session taller than the window scrolls, and a screenshot of the viewport
298
+ // then shows a plausible browser with the bottom view cut off or the top one
299
+ // scrolled away: every filmed take spent turns shrinking tracks after a
300
+ // picture, and one found its genome view at y = -267. Reported from the DOM
301
+ // the views are laid out in, so the agent knows before it looks.
302
+ function offscreenViews(session, root) {
303
+ const win = root.ownerDocument?.defaultView ?? window;
304
+ const windowHeight = win.innerHeight;
305
+ const pageHeight = win.document.documentElement.scrollHeight;
306
+ const containers = new Map([
307
+ ...root.querySelectorAll('[data-testid^="view-container-"]'),
308
+ ].map(el => [el.dataset.testid.slice('view-container-'.length), el]));
309
+ const views = allViews(session).flatMap(view => {
310
+ const rect = containers.get(view.id)?.getBoundingClientRect();
311
+ return rect && (rect.top < 0 || rect.bottom > windowHeight)
312
+ ? [
313
+ {
314
+ viewId: view.id,
315
+ top: Math.round(rect.top),
316
+ bottom: Math.round(rect.bottom),
317
+ },
318
+ ]
319
+ : [];
320
+ });
321
+ return pageHeight > windowHeight || views.length
322
+ ? {
323
+ pageHeight,
324
+ windowHeight,
325
+ views,
326
+ note: 'the session is taller than the window; a viewport screenshot cuts these views off — shrink track heights, or screenshot with fullPage: true',
327
+ }
328
+ : undefined;
329
+ }
330
+ function trackEntry(conf) {
331
+ const adapter = readConfObject(conf, 'adapter');
332
+ return {
333
+ trackId: conf.trackId,
334
+ name: readConfObject(conf, 'name'),
335
+ type: conf.type,
336
+ ...(adapter.type ? { adapterType: adapter.type } : {}),
337
+ assemblyNames: readConfObject(conf, 'assemblyNames'),
338
+ };
339
+ }
340
+ function listTracks(session, searchArg, limitArg) {
341
+ const search = searchArg?.toLowerCase() ?? '';
342
+ const limit = limitArg ?? 100;
343
+ // allSessionTracks, not session.tracks: connection-supplied tracks (hubs,
344
+ // registries) are absent from the session lists but fully showable — a
345
+ // hand-rolled union here hid them from agents entirely
346
+ const matches = allSessionTracks(session)
347
+ .map(c => trackEntry(c))
348
+ .filter(t => !search ||
349
+ t.trackId.toLowerCase().includes(search) ||
350
+ t.name.toLowerCase().includes(search));
351
+ return {
352
+ total: matches.length,
353
+ ...(matches.length > limit
354
+ ? { note: `showing ${limit} of ${matches.length}; refine with search` }
355
+ : {}),
356
+ tracks: matches.slice(0, limit),
357
+ };
358
+ }
359
+ function pickView(session, args, capability, wants) {
360
+ const viewId = typeof args.viewId === 'string' ? args.viewId : '';
361
+ const candidates = allViews(session);
362
+ if (viewId) {
363
+ const named = candidates.find(v => v.id === viewId);
364
+ if (!named) {
365
+ throw new Error(`No view with id "${viewId}". Open views: ${candidates.map(v => `${v.id} (${v.type})`).join(', ')}`);
366
+ }
367
+ if (typeof viewSelf(named)[capability] !== 'function') {
368
+ throw new Error(`View ${named.id} (${named.type}) does not support this`);
369
+ }
370
+ return named;
371
+ }
372
+ const able = candidates.filter(v => typeof viewSelf(v)[capability] === 'function');
373
+ if (!able.length) {
374
+ throw new Error(`No open view supports this. Open views: ${candidates.map(v => v.type).join(', ') || 'none'} — jb.loadSessionSpec can open one.`);
375
+ }
376
+ // A view on another assembly would show the track and render nothing, with a
377
+ // successful-looking result — the same silent mismatch visibleRegionsOf
378
+ // guards against for reads
379
+ const onAssembly = wants?.assembly
380
+ ? able.filter(v => viewSelf(v).assemblyNames?.some(name => isSameAssemblyName(name, wants.assembly, session.assemblyManager)))
381
+ : able;
382
+ if (!onAssembly.length) {
383
+ throw new Error(`No open view is on assembly "${wants.assembly}" (open views: ${able.map(v => `${v.id} on ${viewSelf(v).assemblyNames?.join(', ')}`).join('; ')}) — open one, or pass show: false`);
384
+ }
385
+ const canDisplay = wants?.trackType && wants.pluginManager
386
+ ? onAssembly.filter(v => viewCanDisplayTrack(wants.pluginManager, viewDisplayNames(wants.pluginManager, v.type), wants.trackType))
387
+ : onAssembly;
388
+ if (!canDisplay.length) {
389
+ throw new Error(`No open view can display a ${wants.trackType} (open views: ${onAssembly.map(v => v.type).join(', ')})`);
390
+ }
391
+ return canDisplay[0];
392
+ }
393
+ function shownTrackModel(session, trackId) {
394
+ return allTracks(session).find(t => t.configuration.trackId === trackId);
395
+ }
396
+ async function locToRegion(session, conf, loc, assemblyArg) {
397
+ // getConfAssemblyNamesOrNone, not the assemblyNames slot: an assembly's own
398
+ // sequence track has no such slot and answers through its parent assembly
399
+ const assemblyName = assemblyArg ?? getConfAssemblyNamesOrNone(conf)[0];
400
+ if (assemblyName === undefined) {
401
+ throw new Error('The track names no assembly; pass assembly explicitly');
402
+ }
403
+ const assembly = await session.assemblyManager.waitForAssembly(assemblyName);
404
+ if (!assembly) {
405
+ throw new Error(`Assembly "${assemblyName}" could not be loaded`);
406
+ }
407
+ const parsed = parseLocString(loc, refName => assembly.isValidRefName(refName));
408
+ const refName = assembly.getCanonicalRefName(parsed.refName) ?? parsed.refName;
409
+ const bounds = assembly.regions?.find(r => r.refName === refName);
410
+ return {
411
+ assemblyName,
412
+ refName,
413
+ start: parsed.start ?? bounds?.start ?? 0,
414
+ end: parsed.end ?? bounds?.end ?? Number.MAX_SAFE_INTEGER,
415
+ };
416
+ }
417
+ async function visibleRegionsOf(session, viewId, preferTrackId) {
418
+ // `in`, not evaluation: visibleRegions is a getter that THROWS ("width
419
+ // undefined") until the view's component mounts and sets a width — a
420
+ // freshly spec-loaded view stays in that state briefly even after the
421
+ // app-phase marker reads ready, since a view with no width has no display
422
+ // fetching anything.
423
+ const candidates = allViews(session).filter(v => (!viewId || v.id === viewId) && 'visibleRegions' in v);
424
+ // among region-bearing views, the one actually showing the track wins — two
425
+ // views on two assemblies would otherwise send the first view's namespace to
426
+ // the second view's file, which answers nothing, silently
427
+ const chosen = candidates.find(v => viewTracks(v).some(t => t.configuration.trackId === preferTrackId)) ?? candidates[0];
428
+ if (!chosen) {
429
+ throw new Error('No view that shows a region — pass loc, or open a linear view first');
430
+ }
431
+ const view = viewSelf(chosen);
432
+ const deadline = Date.now() + 10_000;
433
+ let visible;
434
+ while (visible === undefined) {
435
+ if (view.initialized !== false) {
436
+ try {
437
+ visible = view.visibleRegions;
438
+ }
439
+ catch {
440
+ // not mounted yet
441
+ }
442
+ }
443
+ if (visible?.length) {
444
+ break;
445
+ }
446
+ visible = undefined;
447
+ if (Date.now() >= deadline) {
448
+ throw new Error('The view has not finished initializing a visible region — pass loc, or navigate first');
449
+ }
450
+ await delay(250);
451
+ }
452
+ // whole bases: a block edge is a fractional bp, and 30000.000000000004
453
+ // reads as a bug in every answer that echoes it
454
+ return visible.map(({ refName, start, end, assemblyName }) => ({
455
+ refName,
456
+ start: Math.floor(start),
457
+ end: Math.ceil(end),
458
+ assemblyName,
459
+ }));
460
+ }
461
+ // Main-thread adapter, not the CoreGetFeatures RPC: the RPC serializes every
462
+ // feature in the region across the worker boundary before any limit can
463
+ // apply. Here the features stay objects. The shown track's own rpcSessionId,
464
+ // so this shares the adapter instance — parsed indexes and chunk caches
465
+ // included — that the display already warmed (rpcSessionId lives on track
466
+ // models, so the walk cannot start at the session; session.id is the
467
+ // cold-namespace fallback for un-shown tracks). Regions are renamed the same
468
+ // way the RPC base class renames them: they arrive carrying the assembly's
469
+ // canonical refNames, and a file spelling them differently would otherwise
470
+ // answer nothing, silently.
471
+ async function fetchFeatures(pluginManager, session, trackId, regions, requestedByteLimit) {
472
+ const conf = session.getTrackById(trackId);
473
+ if (!conf) {
474
+ throw new Error(`No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
475
+ }
476
+ const trackModel = shownTrackModel(session, trackId);
477
+ const sessionId = trackModel
478
+ ? getRpcSessionId(trackModel)
479
+ : (session.id ?? 'mcp');
480
+ const renamed = await renameRegionsIfNeeded(session.assemblyManager, {
481
+ regions,
482
+ adapterConfig: readConfObject(conf, 'adapter'),
483
+ sessionId,
484
+ });
485
+ const dataAdapter = await getFeatureAdapterOrThrow({
486
+ pluginManager,
487
+ sessionId,
488
+ adapterConfig: renamed.adapterConfig,
489
+ sequenceAdapter: renamed.sequenceAdapter,
490
+ });
491
+ // The adapter's own declared limit where it has one, so this does not quietly
492
+ // disagree with the size the track's display already refuses to render —
493
+ // the reasoning BaseTrackModel.exportByteLimit spells out for "Save track
494
+ // data", which is the other path that fetches because someone asked rather
495
+ // than because a view scrolled.
496
+ const byteLimit = agentByteLimit(readConfObject(conf, ['adapter', 'fetchSizeLimit']), requestedByteLimit);
497
+ const stopToken = createStopToken();
498
+ // desktop's MCP relay gives up at 150s, so the read must not outlive it —
499
+ // and an agent-triggered read of a dense region wants a ceiling either way
500
+ const stopTimer = setTimeout(() => {
501
+ stopStopToken(stopToken);
502
+ }, 120_000);
503
+ // The agent's "region too large". A display refuses to fetch over its own
504
+ // gate and paints the reason; this path has no display, so without the same
505
+ // question asked here an agent that names a whole chromosome pulls it — over
506
+ // someone else's data host, onto the main thread, in the page it is about to
507
+ // screenshot. Same index-only estimate the gated RPC takes first, and a
508
+ // refusal rather than a truncation: a short answer that looked like the whole
509
+ // answer is the failure this surface exists to prevent.
510
+ const { tooLarge, bytes } = await measureRegionBytes({
511
+ dataAdapter,
512
+ regions: renamed.regions,
513
+ byteLimit,
514
+ stopToken,
515
+ });
516
+ if (tooLarge) {
517
+ clearTimeout(stopTimer);
518
+ throw new Error(`region too large for jb.getFeatures: the largest region is ~${bytes} bytes against a limit of ${byteLimit}. Narrow the region, or pass an explicit byteLimit if you mean to pull this much.`);
519
+ }
520
+ // one array per region, flattened once at the end: a dense region returns
521
+ // hundreds of thousands of features, so neither push(...array) — a
522
+ // RangeError on the argument list — nor a concat per region is available
523
+ const perRegion = [];
524
+ try {
525
+ for (const region of renamed.regions) {
526
+ perRegion.push(await dataAdapter.getFeaturesArray(region, { stopToken }));
527
+ }
528
+ }
529
+ finally {
530
+ clearTimeout(stopTimer);
531
+ }
532
+ return perRegion.flat();
533
+ }
534
+ // A LocalPathLocation only reads under Electron: in a browser openLocation
535
+ // throws "can't use local files in the browser" at the FIRST READ, not here, so
536
+ // accepting one would report a track added and then fail inside the display
537
+ // where the agent is least likely to connect it to what it asked for.
538
+ function fileLocation(spec) {
539
+ if (/^https?:\/\//.test(spec)) {
540
+ return { uri: spec, locationType: 'UriLocation' };
541
+ }
542
+ if (!isElectron) {
543
+ throw new Error(`jb.addTrack needs a URL in a browser, and "${spec}" is a local path. JBrowse Web cannot read local files except through the Add track file picker, which needs a real file chosen by hand.`);
544
+ }
545
+ return { localPath: spec, locationType: 'LocalPathLocation' };
546
+ }
547
+ // A stacked set of bigWigs is one track over a MultiWiggleAdapter, which the
548
+ // per-file guesser cannot express — the web take hand-wrote the config. The
549
+ // subadapters form rather than the `bigWigs` shorthand: that one takes
550
+ // absolute URLs only, and desktop's locations are paths.
551
+ // the file's name without its bigWig extension, or undefined for any other file
552
+ function bigWigStem(location) {
553
+ const fileName = location.split(/[/\\]/).pop() ?? location;
554
+ const match = /^(.+)\.(bw|bigwig)$/i.exec(fileName);
555
+ return match?.[1];
556
+ }
557
+ function multiWiggleTrackConf(locations, assemblyName) {
558
+ const stems = locations.map(l => bigWigStem(l));
559
+ const notBigWig = locations.filter((_, i) => stems[i] === undefined);
560
+ if (notBigWig.length) {
561
+ throw new Error(`jb.addTrack takes a list of locations only for bigWigs (one stacked MultiQuantitativeTrack); not bigWig: ${notBigWig.join(', ')}. Add other formats one at a time.`);
562
+ }
563
+ const adapter = {
564
+ type: 'MultiWiggleAdapter',
565
+ subadapters: locations.map((l, i) => ({
566
+ type: 'BigWigAdapter',
567
+ name: stems[i],
568
+ bigWigLocation: fileLocation(l),
569
+ })),
570
+ };
571
+ return {
572
+ trackId: `multiwiggle-${objectHash(adapter).slice(0, 8)}`,
573
+ type: 'MultiQuantitativeTrack',
574
+ name: stems.join(', '),
575
+ assemblyNames: [assemblyName],
576
+ adapter,
577
+ };
578
+ }
579
+ // one location, a list of them, or nothing usable
580
+ function locationsOf(value) {
581
+ return Array.isArray(value)
582
+ ? value.filter((l) => typeof l === 'string')
583
+ : typeof value === 'string'
584
+ ? value
585
+ : '';
586
+ }
587
+ // Vocabulary introspection: every config slot a live config node's schema
588
+ // defines, so code never has to guess which settings keys exist — an unknown
589
+ // key is otherwise dropped silently, which is this format's known failure mode.
590
+ function describeSlots(conf) {
591
+ const definition = getConfigurationSchemaDefinition(conf) ?? {};
592
+ return Object.fromEntries(Object.entries(definition).flatMap(([name, def]) => isSlotDefinitionEntry(def)
593
+ ? [
594
+ [
595
+ name,
596
+ {
597
+ type: def.type,
598
+ ...(def.description ? { description: def.description } : {}),
599
+ defaultValue: def.defaultValue,
600
+ },
601
+ ],
602
+ ]
603
+ : []));
604
+ }
605
+ // The raw primitive under all of the above: Claude-authored code against the
606
+ // live model graph. The renderer already runs with nodeIntegration and the
607
+ // bridge socket is user-only, so this grants what the surface as a whole
608
+ // already grants — expressed directly instead of through a curated verb.
609
+ // The re-export registry is what pluginManager.jbrequire serves: the same
610
+ // pinned ABI module names external plugins link against (ReExports/modules.ts,
611
+ // abiBaseline.json). It stays empty until a runtime plugin loads, so each
612
+ // evaluate (re)installs it — import() is memoized, so this is one lookup and
613
+ // an idempotent assignment, with no module-level flag to hold.
614
+ export async function ensureReExports() {
615
+ setReExportRegistry((await import('@jbrowse/core/ReExports/modules')).default);
616
+ }
617
+ // The helper library an agent drives the app through. Built from the plugin
618
+ // manager alone, so one of these serves a whole app rather than one session —
619
+ // which is what lets jbrowse-web hand the same object to every caller for the
620
+ // life of a plugin manager.
621
+ export function createJbApi(pluginManager) {
622
+ // Resolved per call, never captured: jb.loadSessionSpec REPLACES the session,
623
+ // and a helper bound to the old one keeps answering from a detached tree —
624
+ // which reads as stale data rather than throwing, so the agent is told about
625
+ // a session that no longer exists. `jb.session` is how code re-reads it after
626
+ // a spec load.
627
+ const live = () => {
628
+ const current = sessionOf(pluginManager);
629
+ if (!current) {
630
+ throw new Error('No session is open');
631
+ }
632
+ return current;
633
+ };
634
+ const jb = {
635
+ get session() {
636
+ return live();
637
+ },
638
+ get rootModel() {
639
+ return pluginManager.rootModel;
640
+ },
641
+ require: pluginManager.jbrequire,
642
+ ensureRequire: ensureReExports,
643
+ mst,
644
+ mobx,
645
+ readConfObject,
646
+ getConf,
647
+ describeSlots,
648
+ parseLocString,
649
+ getFeatureAdapterOrThrow,
650
+ getRpcSessionId,
651
+ renameRegionsIfNeeded,
652
+ createStopToken,
653
+ stopStopToken,
654
+ waitReady: (timeoutMs) => waitReady(timeoutMs, live()),
655
+ sessionSummary: () => sessionSummary(live()),
656
+ inspect: (path, maxInspectBytes) => inspectSession(live(), { path, maxBytes: maxInspectBytes }),
657
+ listTracks: (search, limit) => listTracks(live(), search, limit),
658
+ trackModel: (trackId) => shownTrackModel(live(), trackId),
659
+ visibleRegions: (viewId) => visibleRegionsOf(live(), viewId),
660
+ loadSessionSpec: (spec, settleMs) => loadSpec(pluginManager, { spec, settleMs }),
661
+ addTrack: (opts) => addTrack(pluginManager, live(), opts),
662
+ // Two of four filmed takes wrote jb.getFeatures('trackId', loc) and lost a
663
+ // turn to "No track with trackId undefined" — so the positional form is
664
+ // simply accepted alongside the object it was documented as.
665
+ getFeatures: async (args, positionalLoc) => {
666
+ const fetchArgs = typeof args === 'string' ? { trackId: args, loc: positionalLoc } : args;
667
+ const session = live();
668
+ const conf = session.getTrackById(fetchArgs.trackId);
669
+ if (!conf) {
670
+ throw new Error(`No track with trackId "${fetchArgs.trackId}" — jb.listTracks() shows what is available`);
671
+ }
672
+ const regions = fetchArgs.regions ??
673
+ (fetchArgs.loc !== undefined
674
+ ? [
675
+ await locToRegion(session, conf, fetchArgs.loc, fetchArgs.assembly),
676
+ ]
677
+ : await visibleRegionsOf(session, fetchArgs.viewId, fetchArgs.trackId));
678
+ return fetchFeatures(pluginManager, session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
679
+ },
680
+ };
681
+ return jb;
682
+ }
683
+ async function addTrack(pluginManager, session, args) {
684
+ if (!isSessionWithAddSessionTrack(session)) {
685
+ throw new Error('This session cannot add tracks');
686
+ }
687
+ const location = locationsOf(args.location);
688
+ if (!location.length) {
689
+ throw new Error('jb.addTrack needs a location (local path or URL)');
690
+ }
691
+ const requested = typeof args.assembly === 'string' ? args.assembly : session.assemblyNames[0];
692
+ if (requested === undefined) {
693
+ throw new Error('The session has no assemblies to attach the track to');
694
+ }
695
+ // resolved to the session's own spelling: an alias ("vvx" for "volvox",
696
+ // "GRCh38" for "hg38") names the assembly and would otherwise be refused
697
+ const assembly = session.assemblyNames.find(name => isSameAssemblyName(name, requested, session.assemblyManager));
698
+ if (assembly === undefined) {
699
+ throw new Error(`Assembly "${requested}" is not in this session (has: ${session.assemblyNames.join(', ')})`);
700
+ }
701
+ const conf = Array.isArray(location)
702
+ ? multiWiggleTrackConf(location, assembly)
703
+ : guessTrackConfForLocation(fileLocation(location), typeof args.index === 'string' ? fileLocation(args.index) : undefined, pluginManager, assembly);
704
+ session.addSessionTrackConf({
705
+ ...conf,
706
+ ...(typeof args.name === 'string' ? { name: args.name } : {}),
707
+ });
708
+ const summary = {
709
+ trackId: conf.trackId,
710
+ trackType: conf.type,
711
+ adapterType: conf.adapter.type,
712
+ assembly,
713
+ };
714
+ if (args.show === false) {
715
+ return summary;
716
+ }
717
+ const view = pickView(session, args, 'showTrack', {
718
+ assembly,
719
+ trackType: conf.type,
720
+ pluginManager,
721
+ });
722
+ viewSelf(view).showTrack(conf.trackId);
723
+ // a spec load on a busy machine passed 30s and answered settled:false over
724
+ // views that were fine; the wait exits the moment they are ready
725
+ const settle = await waitReady(60_000, session);
726
+ return { ...summary, ...settle, shownInView: view.id };
727
+ }
728
+ // Under the 45 s one evaluation gets from the Claude in Chrome extension: a
729
+ // cold hosted config settling for the full minute answered nothing there,
730
+ // while the settle result already names what is still not ready
731
+ const SPEC_SETTLE_DEFAULT_MS = 30_000;
732
+ async function loadSpec(pluginManager, args) {
733
+ const spec = args.spec;
734
+ const valid = typeof spec === 'object' &&
735
+ spec !== null &&
736
+ Array.isArray(spec.views);
737
+ if (!valid) {
738
+ throw new Error('loadSessionSpec needs a spec object with a views array');
739
+ }
740
+ await loadSessionSpec(spec, pluginManager);
741
+ // the session was REPLACED by the spec load — settle against the new one
742
+ const session = sessionOf(pluginManager);
743
+ const settle = await waitReady(args.settleMs ?? SPEC_SETTLE_DEFAULT_MS, session);
744
+ return {
745
+ ...settle,
746
+ ...(session ? { session: sessionSummary(session) } : {}),
747
+ };
748
+ }