@jbrowse/app-core 5.0.0-beta.4 → 5.0.0-beta.8

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.
@@ -63,25 +63,76 @@ function displayState(track) {
63
63
  ? { reason: display.regionTooLargeReason }
64
64
  : {}),
65
65
  ...(display.error ? { error: String(display.error) } : {}),
66
+ ...(display.renderError
67
+ ? { renderError: String(display.renderError) }
68
+ : {}),
66
69
  };
67
70
  }
68
71
  catch (e) {
69
72
  return { display: display.type, error: String(e) };
70
73
  }
71
74
  }
75
+ /**
76
+ * The drawer and any modal, for the same reason `offscreen` is reported: both
77
+ * are on top of the views in a screenshot that otherwise looks right.
78
+ *
79
+ * The drawer takes a column off every view rather than covering them, so a
80
+ * figure framed with the track selector open is narrower than the one asked
81
+ * for — which every filmed take worked around by telling the agent in its
82
+ * system prompt to close it. A modal covers the app outright.
83
+ */
84
+ function sessionChrome(session) {
85
+ const s = session;
86
+ const drawer = s.drawerVisible && s.visibleWidget
87
+ ? {
88
+ drawer: {
89
+ widget: s.visibleWidget.type,
90
+ ...(typeof s.drawerWidth === 'number'
91
+ ? { width: s.drawerWidth }
92
+ : {}),
93
+ note: 'the drawer takes this width off every view; session.hideAllWidgets() closes it',
94
+ },
95
+ }
96
+ : {};
97
+ const dialog = s.DialogComponent
98
+ ? {
99
+ dialog: {
100
+ note: 'a modal dialog is open over the app, so a screenshot is of the dialog',
101
+ },
102
+ }
103
+ : {};
104
+ return { ...drawer, ...dialog };
105
+ }
72
106
  export function sessionOf(pluginManager) {
73
107
  return pluginManager?.rootModel?.session;
74
108
  }
75
109
  // A view whose init failed (its assembly was never found) paints the error in
76
110
  // place of a genome and raises no toast, and stays uninitialized: without
77
111
  // this, a spec with "volvix" for "volvox" settled false with nothing said.
112
+ /**
113
+ * Why a view is not finished, or nothing if it is.
114
+ *
115
+ * The three terms are AppReadyMarker's, and they have to be: the marker decides
116
+ * whether the settle ends, this decides what it says when it doesn't, and a
117
+ * term in one and not the other is a settle that answers `false` with an empty
118
+ * `notReady`. `launching` is the one that was missing — `initialized` goes true
119
+ * the moment a linear view's regions land, while the same apply pass still has
120
+ * the spec's tracks to attach, so a spec load timing out there reported no
121
+ * reason at all.
122
+ */
78
123
  function viewState(v) {
79
- return {
80
- ...(v.error === undefined ? {} : { error: String(v.error) }),
81
- ...(v.error === undefined && v.initialized === false
82
- ? { phase: 'initializing' }
83
- : {}),
84
- };
124
+ if (v.error !== undefined) {
125
+ return { error: String(v.error) };
126
+ }
127
+ if (v.initialized === false) {
128
+ return { phase: 'initializing' };
129
+ }
130
+ return v.pendingLaunch === undefined
131
+ ? {}
132
+ : {
133
+ phase: 'launching',
134
+ reason: 'the view is still applying what it was launched with (navigation, tracks)',
135
+ };
85
136
  }
86
137
  function viewSummary(view) {
87
138
  const v = viewSelf(view);
@@ -115,6 +166,7 @@ function sessionSummary(session) {
115
166
  return {
116
167
  name: session.name,
117
168
  assemblyNames: session.assemblyNames,
169
+ ...sessionChrome(session),
118
170
  views: session.views.map(v => viewSummary(v)),
119
171
  };
120
172
  }
@@ -312,6 +364,7 @@ root = document) {
312
364
  ...(messages.length ? { notifications: messages } : {}),
313
365
  ...(notReady.length ? { notReady } : {}),
314
366
  ...(offscreen ? { offscreen } : {}),
367
+ ...(session ? sessionChrome(session) : {}),
315
368
  };
316
369
  }
317
370
  // A session taller than the window scrolls, and a screenshot of the viewport
@@ -471,7 +524,7 @@ function shownTrackModel(session, trackId, viewId) {
471
524
  // it was
472
525
  if (!track) {
473
526
  throw new Error(session.getTrackById(trackId)
474
- ? `"${trackId}" is not shown in ${viewId === undefined ? 'any open view' : `view ${viewId}`} — view.showTrack("${trackId}") first; jb.sessionSummary() lists what each view shows`
527
+ ? `"${trackId}" is not shown in ${viewId === undefined ? 'any open view' : `view ${viewId}`} — await view.launchTrack("${trackId}") first; jb.sessionSummary() lists what each view shows`
475
528
  : `No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
476
529
  }
477
530
  return track;
@@ -715,16 +768,16 @@ const JB_HELP = `jb drives this JBrowse app programmatically (window.jb in a bro
715
768
 
716
769
  Orient first: jb.sessionSummary(). Introspect, never guess: jb.listTracks(search?, limit?) answers { total, tracks } with the trackIds; jb.describeSlots(jb.trackModel('someTrackId').activeDisplay.configuration) for the settings keys a display accepts — an unknown settings key is not an error, it lands in applyDisplaySettings' "unapplied" list, so read the report; jb.inspect('views.0') for a live node's getters, actions and modelType.
717
770
 
718
- The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }); arrange the views already open into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }) — leaves name view ids or indexes into session.views; add data with jb.addTrack({ location }) (an absolute path or a URL); read data with await jb.getFeatures({ trackId, loc?, assembly?, byteLimit? }) (or jb.getFeatures(trackId, loc?, opts?)), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses — raw adapter code must call jb.renameRegionsIfNeeded itself. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
771
+ The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }); arrange the views already open into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }) — leaves name view ids or indexes into session.views; add data with jb.addTrack({ location }) (an absolute path or a URL); read data with await jb.getFeatures({ trackId, loc?, assembly?, viewId?, regions?, byteLimit? }) (or jb.getFeatures(trackId, loc?, opts?)), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses — raw adapter code must call jb.renameRegionsIfNeeded itself. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
719
772
 
720
- Views nest and several can be open. jb.view(viewId?) is the open view, and jb.view(), jb.trackModel(trackId), jb.visibleRegions() and jb.addTrack throw naming the candidates rather than picking one when more than one view could answer — pass viewId (from jb.sessionSummary()) to say which.
773
+ Views nest and several can be open. jb.view(viewId?) is the open view, and jb.view(), jb.trackModel(trackId), jb.visibleRegions(), jb.addTrack and jb.getFeatures over a visible region throw naming the candidates rather than picking one when more than one view could answer — pass viewId (from jb.sessionSummary()) to say which.
721
774
 
722
775
  Full guide: https://jbrowse.org/jb2/docs/agents_live_model (JBrowse Desktop serves the same guide offline through its MCP docs tool — Help menu, "Connect an AI agent...").`;
723
776
  // The helper library an agent drives the app through. Built from the plugin
724
777
  // manager alone, so one of these serves a whole app rather than one session —
725
778
  // which is what lets jbrowse-web hand the same object to every caller for the
726
779
  // life of a plugin manager.
727
- export function createJbApi(pluginManager) {
780
+ export function createJbApi(pluginManager, options = {}) {
728
781
  // Resolved per call, never captured: jb.loadSessionSpec REPLACES the session,
729
782
  // and a helper bound to the old one keeps answering from a detached tree —
730
783
  // which reads as stale data rather than throwing, so the agent is told about
@@ -737,6 +790,18 @@ export function createJbApi(pluginManager) {
737
790
  }
738
791
  return current;
739
792
  };
793
+ // Every settle an agent can reach goes through here, so a notification
794
+ // cannot be consumed by one of these helpers without the caller hearing
795
+ // about it.
796
+ // a declaration, not a generic arrow: `<T>(x: T) => …` in a .ts file is a
797
+ // JSX tag to babel, which is what jest parses these with
798
+ function reported(settle) {
799
+ const messages = settle.notifications;
800
+ if (Array.isArray(messages) && messages.length > 0) {
801
+ options.onNotifications?.(messages);
802
+ }
803
+ return settle;
804
+ }
740
805
  const jb = {
741
806
  help: JB_HELP,
742
807
  get session() {
@@ -760,15 +825,15 @@ export function createJbApi(pluginManager) {
760
825
  stopStopToken,
761
826
  // a default because an omitted number made the deadline NaN, and a view
762
827
  // that never readied then held the call open to the relay's own timeout
763
- waitReady: (timeoutMs = 30_000) => waitReady(timeoutMs, live()),
828
+ waitReady: async (timeoutMs = 30_000) => reported(await waitReady(timeoutMs, live())),
764
829
  sessionSummary: () => sessionSummary(live()),
765
830
  inspect: (path, maxInspectBytes) => inspectSession(live(), { path, maxBytes: maxInspectBytes }),
766
831
  listTracks: (search, limit) => listTracks(live(), search, limit),
767
832
  view: (viewId) => viewById(live(), viewId),
768
833
  trackModel: (trackId, viewId) => shownTrackModel(live(), trackId, viewId),
769
834
  visibleRegions: (viewId) => visibleRegionsOf(live(), viewId),
770
- loadSessionSpec: (spec, settleMs) => loadSpec(pluginManager, { spec, settleMs }),
771
- addTrack: (opts) => addTrack(pluginManager, live(), opts),
835
+ loadSessionSpec: async (spec, settleMs) => reported(await loadSpec(pluginManager, { spec, settleMs })),
836
+ addTrack: (opts) => addTrack(pluginManager, live(), opts).then(reported),
772
837
  // Two of four filmed takes wrote jb.getFeatures('trackId', loc) and lost a
773
838
  // turn to "No track with trackId undefined", and a third wrote
774
839
  // jb.getFeatures('trackId', loc, { assembly }) and had the options
@@ -831,12 +896,16 @@ async function addTrack(pluginManager, session, args) {
831
896
  if (args.show === false) {
832
897
  return summary;
833
898
  }
834
- const view = pickView(session, args, 'showTrack', {
899
+ const view = pickView(session, args, 'launchTrack', {
835
900
  assembly,
836
901
  trackType: conf.type,
837
902
  pluginManager,
838
903
  });
839
- viewSelf(view).showTrack(conf.trackId);
904
+ // awaited: a display state model is lazy in every plugin here, and the
905
+ // synchronous showTrack answers before the chunk lands — so the settle below
906
+ // would run against a session that does not hold the track yet, and report
907
+ // ready over it
908
+ await viewSelf(view).launchTrack(conf.trackId);
840
909
  const shown = { ...summary, shownInView: view.id };
841
910
  // 0 skips the settle, so several adds can share one jb.waitReady
842
911
  const settleMs = typeof args.settleMs === 'number' ? args.settleMs : ADD_TRACK_SETTLE_MS;
@@ -4,14 +4,30 @@ import { observer } from 'mobx-react';
4
4
  function trackLoading(track) {
5
5
  return track.displays.some(d => d.displayPhase === 'loading');
6
6
  }
7
- // A view whose init failed (its assembly was never found) stays uninitialized
8
- // for good and paints its error, so like a display's terminal phase it is
9
- // finished, not pending: without this the whole app read `loading` for as long
10
- // as that view was open, and every settle answered false with no reason.
7
+ /**
8
+ * A view whose init failed (its assembly was never found) stays uninitialized
9
+ * for good and paints its error, so like a display's terminal phase it is
10
+ * finished, not pending: without this the whole app read `loading` for as long
11
+ * as that view was open, and every settle answered false with no reason.
12
+ *
13
+ * `pendingLaunch` is the third term and the one that was missing. `initialized`
14
+ * goes true mid-launch — LGV's flips the moment displayedRegions land, while
15
+ * the same apply pass still has the spec's tracks to attach — and `showLoading`
16
+ * goes false at the same moment, deliberately, so the reader sees the
17
+ * navigated view rather than a spinner while its tracks arrive. That left a
18
+ * window, exactly as wide as attaching the tracks, in which the marker said
19
+ * `ready` over a session missing everything it had been asked for.
20
+ * `whenViewSettled` states the same rule for the callers that await a single
21
+ * view, and says why in more detail. Not shared as one predicate: that one
22
+ * requires `initialized`, while absent here means not loading, and an adapter
23
+ * between the two null rules would read as agreement it does not have.
24
+ */
11
25
  function viewLoading(view) {
12
26
  const flags = view;
13
27
  return (flags.error === undefined &&
14
- (flags.showLoading === true || flags.initialized === false));
28
+ (flags.showLoading === true ||
29
+ flags.initialized === false ||
30
+ flags.pendingLaunch !== undefined));
15
31
  }
16
32
  /**
17
33
  * One element that says whether the whole app has finished, and what is open.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/app-core",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.8",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 code shared between the 'full featured' apps e.g. jbrowse-web and jbrowse-desktop",
6
6
  "keywords": [
@@ -29,8 +29,8 @@
29
29
  "@mui/material": "^9.4.0",
30
30
  "mobx": "^7.0.3",
31
31
  "mobx-react": "^10.0.2",
32
- "@jbrowse/core": "^5.0.0-beta.4",
33
- "@jbrowse/product-core": "^5.0.0-beta.4"
32
+ "@jbrowse/product-core": "^5.0.0-beta.8",
33
+ "@jbrowse/core": "^5.0.0-beta.8"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=19.0.0",