@jbrowse/product-core 4.3.0 → 5.0.0-beta.10

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 (157) hide show
  1. package/esm/RootModel/BaseRootModel.d.ts +514 -323
  2. package/esm/RootModel/BaseRootModel.js +141 -14
  3. package/esm/RootModel/HierarchicalConfig.d.ts +30 -20
  4. package/esm/RootModel/HierarchicalConfig.js +36 -0
  5. package/esm/RootModel/InternetAccounts.d.ts +18 -1
  6. package/esm/RootModel/InternetAccounts.js +120 -22
  7. package/esm/RootModel/PreferencesConfig.d.ts +37 -0
  8. package/esm/RootModel/PreferencesConfig.js +75 -0
  9. package/esm/RootModel/createConfigModel.d.ts +257 -0
  10. package/esm/RootModel/createConfigModel.js +77 -0
  11. package/esm/RootModel/index.d.ts +10 -5
  12. package/esm/RootModel/index.js +10 -5
  13. package/esm/RootModel/menuItems.d.ts +24 -0
  14. package/esm/RootModel/menuItems.js +141 -0
  15. package/esm/Session/BaseSession.d.ts +285 -11
  16. package/esm/Session/BaseSession.js +383 -15
  17. package/esm/Session/Connections.d.ts +115 -8
  18. package/esm/Session/Connections.js +196 -74
  19. package/esm/Session/DrawerWidgets.d.ts +145 -13
  20. package/esm/Session/DrawerWidgets.js +219 -49
  21. package/esm/Session/MultipleViews.d.ts +1067 -1177
  22. package/esm/Session/MultipleViews.js +395 -64
  23. package/esm/Session/Preferences.d.ts +1389 -0
  24. package/esm/Session/Preferences.js +58 -0
  25. package/esm/Session/ReferenceManagement.d.ts +28 -3
  26. package/esm/Session/ReferenceManagement.js +56 -41
  27. package/esm/Session/SessionTracks.d.ts +1101 -1180
  28. package/esm/Session/SessionTracks.js +584 -34
  29. package/esm/Session/Themes.d.ts +136 -2
  30. package/esm/Session/Themes.js +267 -21
  31. package/esm/Session/TrackMenu.d.ts +88 -0
  32. package/esm/Session/TrackMenu.js +175 -0
  33. package/esm/Session/TrackMenuSessionMixin.d.ts +27 -0
  34. package/esm/Session/TrackMenuSessionMixin.js +42 -0
  35. package/esm/Session/Tracks.d.ts +945 -1159
  36. package/esm/Session/Tracks.js +229 -30
  37. package/esm/Session/assertSessionModel.d.ts +21 -0
  38. package/esm/Session/assertSessionModel.js +1 -0
  39. package/esm/Session/copyTrackSnapshot.d.ts +21 -0
  40. package/esm/Session/copyTrackSnapshot.js +20 -0
  41. package/esm/Session/developerMode.d.ts +7 -0
  42. package/esm/Session/developerMode.js +43 -0
  43. package/esm/Session/finalizeSession.d.ts +15 -0
  44. package/esm/Session/finalizeSession.js +28 -0
  45. package/esm/Session/hydratedForms.d.ts +16 -0
  46. package/esm/Session/hydratedForms.js +13 -0
  47. package/esm/Session/index.d.ts +27 -10
  48. package/esm/Session/index.js +16 -10
  49. package/esm/Session/pluginExtraTrackItems.d.ts +11 -0
  50. package/esm/Session/pluginExtraTrackItems.js +13 -0
  51. package/esm/Session/readdedTrackConf.d.ts +8 -0
  52. package/esm/Session/readdedTrackConf.js +24 -0
  53. package/esm/Session/shareableSnapshot.d.ts +23 -0
  54. package/esm/Session/shareableSnapshot.js +66 -0
  55. package/esm/Session/temporaryAssemblyTracks.d.ts +24 -0
  56. package/esm/Session/temporaryAssemblyTracks.js +37 -0
  57. package/esm/assertExtends.d.ts +55 -0
  58. package/esm/assertExtends.js +1 -0
  59. package/esm/controllerTracks.d.ts +76 -0
  60. package/esm/controllerTracks.js +125 -0
  61. package/esm/destroyViewState.d.ts +21 -0
  62. package/esm/destroyViewState.js +29 -0
  63. package/esm/embeddedController.d.ts +57 -0
  64. package/esm/embeddedController.js +100 -0
  65. package/esm/index.d.ts +32 -4
  66. package/esm/index.js +31 -4
  67. package/esm/loadPlugins.d.ts +38 -0
  68. package/esm/loadPlugins.js +45 -0
  69. package/esm/localFiles.d.ts +74 -0
  70. package/esm/localFiles.js +156 -0
  71. package/esm/observeSession.d.ts +55 -0
  72. package/esm/observeSession.js +90 -0
  73. package/esm/parallelChunkLoading.d.ts +30 -0
  74. package/esm/parallelChunkLoading.js +71 -0
  75. package/esm/pluginInput.d.ts +32 -0
  76. package/esm/pluginInput.js +19 -0
  77. package/esm/pruneUnbuildableNodes.d.ts +109 -0
  78. package/esm/pruneUnbuildableNodes.js +562 -0
  79. package/esm/resolveAssemblies.d.ts +68 -0
  80. package/esm/resolveAssemblies.js +77 -0
  81. package/esm/rpcWorker.d.ts +36 -4
  82. package/esm/rpcWorker.js +126 -20
  83. package/esm/sessionMigrations/index.d.ts +15 -0
  84. package/esm/sessionMigrations/index.js +178 -0
  85. package/esm/sessionUrl.d.ts +40 -0
  86. package/esm/sessionUrl.js +66 -0
  87. package/esm/sessionUtils.d.ts +74 -0
  88. package/esm/sessionUtils.js +599 -0
  89. package/esm/siblingCast.d.ts +14 -0
  90. package/esm/siblingCast.js +16 -0
  91. package/esm/snapshotUtils.d.ts +8 -0
  92. package/esm/snapshotUtils.js +12 -0
  93. package/esm/ui/AboutDialog.d.ts +8 -3
  94. package/esm/ui/AboutDialog.js +14 -5
  95. package/esm/ui/AboutDialogContents.d.ts +2 -6
  96. package/esm/ui/AboutDialogContents.js +13 -17
  97. package/esm/ui/AssemblyInfoPanel.d.ts +5 -0
  98. package/esm/ui/AssemblyInfoPanel.js +63 -0
  99. package/esm/ui/DescriptionPanel.d.ts +3 -0
  100. package/esm/ui/DescriptionPanel.js +25 -0
  101. package/esm/ui/Drawer.d.ts +21 -0
  102. package/esm/ui/Drawer.js +60 -0
  103. package/esm/ui/DrawerControls.d.ts +5 -0
  104. package/esm/ui/DrawerControls.js +28 -0
  105. package/esm/ui/DrawerHeader.d.ts +6 -0
  106. package/esm/ui/DrawerHeader.js +36 -0
  107. package/esm/ui/DrawerWidget.d.ts +5 -0
  108. package/esm/ui/DrawerWidget.js +12 -0
  109. package/esm/ui/DrawerWidgetSelector.d.ts +5 -0
  110. package/esm/ui/DrawerWidgetSelector.js +40 -0
  111. package/esm/ui/FileInfoPanel.d.ts +3 -6
  112. package/esm/ui/FileInfoPanel.js +36 -26
  113. package/esm/ui/GeneralPreferencesTab.d.ts +17 -0
  114. package/esm/ui/GeneralPreferencesTab.js +34 -0
  115. package/esm/ui/HeaderButtons.d.ts +2 -1
  116. package/esm/ui/HeaderButtons.js +15 -17
  117. package/esm/ui/ModalWidget.d.ts +13 -0
  118. package/esm/ui/ModalWidget.js +48 -0
  119. package/esm/ui/ModalWidgetAppBar.d.ts +8 -0
  120. package/esm/ui/ModalWidgetAppBar.js +9 -0
  121. package/esm/ui/PreferenceCheckbox.d.ts +6 -0
  122. package/esm/ui/PreferenceCheckbox.js +16 -0
  123. package/esm/ui/PreferencesDialog.d.ts +34 -0
  124. package/esm/ui/PreferencesDialog.js +55 -0
  125. package/esm/ui/PreferencesResetDialog.d.ts +8 -0
  126. package/esm/ui/PreferencesResetDialog.js +19 -0
  127. package/esm/ui/RefNameAliasesDialog.d.ts +9 -0
  128. package/esm/ui/RefNameAliasesDialog.js +65 -0
  129. package/esm/ui/RefNameInfoDialog.d.ts +3 -6
  130. package/esm/ui/RefNameInfoDialog.js +45 -63
  131. package/esm/ui/ViewPreferencesTab.d.ts +12 -0
  132. package/esm/ui/ViewPreferencesTab.js +15 -0
  133. package/esm/ui/WidgetBody.d.ts +18 -0
  134. package/esm/ui/WidgetBody.js +22 -0
  135. package/esm/ui/WidgetHeading.d.ts +7 -0
  136. package/esm/ui/WidgetHeading.js +7 -0
  137. package/esm/ui/aboutTestUtils.d.ts +140 -0
  138. package/esm/ui/aboutTestUtils.js +25 -0
  139. package/esm/ui/descriptionHtml.d.ts +25 -0
  140. package/esm/ui/descriptionHtml.js +64 -0
  141. package/esm/ui/drawerLayout.d.ts +18 -0
  142. package/esm/ui/drawerLayout.js +22 -0
  143. package/esm/ui/index.d.ts +15 -1
  144. package/esm/ui/index.js +13 -1
  145. package/esm/ui/preferencesReset.d.ts +18 -0
  146. package/esm/ui/preferencesReset.js +69 -0
  147. package/esm/ui/util.d.ts +22 -7
  148. package/esm/ui/util.js +19 -41
  149. package/esm/useEngineLifecycle.d.ts +19 -0
  150. package/esm/useEngineLifecycle.js +44 -0
  151. package/package.json +15 -18
  152. package/esm/RootModel/FormatAbout.d.ts +0 -12
  153. package/esm/RootModel/FormatAbout.js +0 -15
  154. package/esm/RootModel/FormatDetails.d.ts +0 -24
  155. package/esm/RootModel/FormatDetails.js +0 -27
  156. package/esm/Session/DialogQueue.d.ts +0 -17
  157. package/esm/Session/DialogQueue.js +0 -31
@@ -1,88 +1,419 @@
1
- import { localStorageGetBoolean, localStorageSetBoolean, } from '@jbrowse/core/util';
2
- import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree';
1
+ import { applyOrderWithin, localStorageGetBoolean, localStorageSetBoolean, reorderWithin, scheduleDetachedDestroy, } from '@jbrowse/core/util';
2
+ import { addDisposer, applyPatch, cast, detach, getMembers, getParent, hasParent, isReferenceType, isStateTreeNode, types, } from '@jbrowse/mobx-state-tree';
3
3
  import { autorun } from 'mobx';
4
- import { BaseSessionModel, isBaseSession } from "./BaseSession.js";
5
- import { DrawerWidgetSessionMixin } from "./DrawerWidgets.js";
4
+ import { BaseSessionModel, isBaseSession } from './BaseSession.js';
5
+ import { DrawerWidgetSessionMixin } from './DrawerWidgets.js';
6
+ // Whether `node` IS `view` or lives somewhere under it. A view can CONTAIN
7
+ // views, so `takeOut` matches widgets to the view leaving by containment rather
8
+ // than by `widget.view.id === view.id`. ADR-069 says what that cost.
9
+ //
10
+ // Deliberately not `findParentThat`, which warns and throws when the walk runs
11
+ // out: this asks the question of every reference every widget holds, and most
12
+ // of them point somewhere else entirely, so "no" is an ordinary answer here
13
+ // rather than a fault.
14
+ function isWithin(node, view) {
15
+ if (!isStateTreeNode(node)) {
16
+ return false;
17
+ }
18
+ let cur = node;
19
+ while (cur !== view) {
20
+ if (!hasParent(cur)) {
21
+ return false;
22
+ }
23
+ cur = getParent(cur);
24
+ }
25
+ return true;
26
+ }
27
+ function viewIdsWithin(view) {
28
+ return [view.id, ...(view.views ?? []).flatMap(v => viewIdsWithin(v))];
29
+ }
30
+ // Empty every reference `holder` has into `view`, and say whether it had any.
31
+ //
32
+ // MST invalidates a reference when its TARGET node is detached or destroyed, so
33
+ // a widget's reference to the view itself empties on its way out and one
34
+ // pointing INSIDE it does not: `openFeatureWidget` stores the clicked feature's
35
+ // track, and that node is neither detached nor destroyed — it just leaves the
36
+ // session's identifier cache with the view, which makes reading the reference
37
+ // THROW where `safeReference` promises undefined. The widget's own autoruns go
38
+ // on running whether or not its panel is on screen, so that throw comes out of
39
+ // a reaction, uncatchable by the caller.
40
+ //
41
+ // So do here what `onInvalidated` would have done, while the reference can
42
+ // still be read. Only where the type allows undefined — a required reference
43
+ // has no empty value to move to, and the widget is hidden either way. ADR-069.
44
+ function dropRefsInto(holder, view) {
45
+ let found = false;
46
+ for (const [key, prop] of Object.entries(getMembers(holder).properties)) {
47
+ if (isReferenceType(prop) && isWithin(holder[key], view)) {
48
+ found = true;
49
+ if (prop.is(undefined)) {
50
+ applyPatch(holder, { op: 'replace', path: `/${key}`, value: undefined });
51
+ }
52
+ }
53
+ }
54
+ return found;
55
+ }
56
+ // A session snapshot's `views`, or undefined for anything that is not one
57
+ function viewsOfSessionSnapshot(snapshot) {
58
+ const views = typeof snapshot === 'object' && snapshot !== null && 'views' in snapshot
59
+ ? snapshot.views
60
+ : undefined;
61
+ return Array.isArray(views) ? views : undefined;
62
+ }
63
+ /**
64
+ * #stateModel MultipleViewsSessionMixin
65
+ */
6
66
  export function MultipleViewsSessionMixin(pluginManager) {
67
+ // an unknown type name passes through to fail at the union as it always has
68
+ function assertViewStateModelLoaded(typeName) {
69
+ if (pluginManager.getElementTypeRecord('view').has(typeName) &&
70
+ !pluginManager.getViewType(typeName).isStateModelLoaded) {
71
+ throw new Error(`state model for view type ${typeName} is not loaded yet — use launchView(), or await getViewType('${typeName}').loadStateModel() first`);
72
+ }
73
+ }
7
74
  return types
8
75
  .compose(BaseSessionModel(pluginManager), DrawerWidgetSessionMixin(pluginManager), types.model({
76
+ /**
77
+ * #property
78
+ */
9
79
  views: types.array(pluginManager.pluggableMstType('view', 'stateModel')),
80
+ /**
81
+ * #property
82
+ */
10
83
  stickyViewHeaders: types.optional(types.boolean, () => localStorageGetBoolean('stickyViewHeaders', true)),
11
- useWorkspaces: types.optional(types.boolean, () => localStorageGetBoolean('useWorkspaces', false)),
84
+ /**
85
+ * #property
86
+ * enables the tabbed/tiled workspace layout for this session. Undefined
87
+ * means unspecified — read `effectiveUseWorkspaces`.
88
+ */
89
+ useWorkspaces: types.stripDefault(types.maybe(types.boolean), undefined),
12
90
  }))
13
- .actions(self => ({
14
- moveViewDown(id) {
15
- const idx = self.views.findIndex(v => v.id === id);
16
- if (idx !== -1 && idx < self.views.length - 1) {
17
- self.views.splice(idx, 2, self.views[idx + 1], self.views[idx]);
18
- }
19
- },
20
- moveViewUp(id) {
21
- const idx = self.views.findIndex(view => view.id === id);
22
- if (idx > 0) {
23
- self.views.splice(idx - 1, 2, self.views[idx], self.views[idx - 1]);
24
- }
91
+ .views(self => ({
92
+ /**
93
+ * #getter
94
+ * resolved workspaces flag (never undefined): this session's value, else
95
+ * the user preference over the `configuration.preferences.useWorkspaces`
96
+ * admin default. Every consumer reads this, not the raw property — only
97
+ * sessions built from a snapshot or a spec `layout` set that, so the
98
+ * admin default is what reaches the arrivals that bypass defaultSession.
99
+ */
100
+ get effectiveUseWorkspaces() {
101
+ return (self.useWorkspaces ?? self.getPreference('useWorkspaces') === true);
25
102
  },
26
- moveViewToTop(id) {
27
- const idx = self.views.findIndex(view => view.id === id);
28
- self.views = cast([
29
- self.views[idx],
30
- ...self.views.filter(view => view.id !== id),
31
- ]);
103
+ /**
104
+ * #getter
105
+ * what `effectiveUseWorkspaces` becomes after `resetUseWorkspaces` — the
106
+ * admin default, with both this session's own value and the user's
107
+ * override out of the way. The Preferences reset diff needs this rather
108
+ * than the override map, which can't see a session-scoped value (a spec
109
+ * `layout`, a "move view to a tab") and so reported nothing to reset.
110
+ */
111
+ get defaultUseWorkspaces() {
112
+ return self.getPreferenceDefault('useWorkspaces') === true;
32
113
  },
33
- moveViewToBottom(id) {
34
- const idx = self.views.findIndex(view => view.id === id);
35
- self.views = cast([
36
- ...self.views.filter(view => view.id !== id),
37
- self.views[idx],
38
- ]);
39
- },
40
- addView(typeName, initialState = {}) {
41
- const typeDefinition = pluginManager.getElementType('view', typeName);
42
- if (!typeDefinition) {
43
- throw new Error(`unknown view type ${typeName}`);
114
+ }))
115
+ .actions(self => {
116
+ // `scopeIds` narrows the move to a subset of the stack: in a tabbed
117
+ // workspace "move this view up" means up past the previous view IN THIS
118
+ // PANEL, and the view that happens to precede it in `self.views` may be
119
+ // in another panel, where moving past it would look like a no-op. Omitted
120
+ // (the classic stack) every view is in scope and this is a plain reorder.
121
+ const move = (id, direction, scopeIds) => {
122
+ const idx = self.views.findIndex(v => v.id === id);
123
+ const scope = scopeIds && new Set(scopeIds);
124
+ self.views = cast(reorderWithin(self.views, idx, direction, v => scope ? scope.has(v.id) : true));
125
+ };
126
+ // The body of removeView, callable from replaceView below — an action
127
+ // can't reach a sibling action through `self` from inside the same
128
+ // .actions() block, since neither is attached yet.
129
+ //
130
+ // **Detached, not removed.** `views.remove` destroys the view and
131
+ // everything under it right here, inside the action, and MobX runs the
132
+ // action's pending reactions at the `endBatch` closing it — so every
133
+ // component still mounted over that view gets a final run against nodes
134
+ // that just died. ADR-069.
135
+ //
136
+ // A view is the worse half of that rule rather than the `setSession`
137
+ // half, because what is mounted over it is a DISPLAY, and a display's
138
+ // reads reach `getContainingView`, which walks parents and THROWS when
139
+ // there is none. So this was not only a noisy console: on
140
+ // `cancer_sv/multihop_split_view` the throw went to an ErrorBoundary with
141
+ // no view left under it and took the page. Detaching leaves the walk
142
+ // something to find.
143
+ const takeOut = (view) => {
144
+ // Membership first, and before anything reads through `view`. A view
145
+ // already out of the session reaches here — `replaceView` documents
146
+ // that case below — and by then it is detached, or destroyed by the
147
+ // task scheduled at the bottom. MST's `detach` throws on a node with no
148
+ // parent, and the widgets holding it were emptied when it left.
149
+ if (!self.views.includes(view)) {
150
+ return;
44
151
  }
45
- const length = self.views.push({
46
- ...initialState,
47
- type: typeName,
48
- });
49
- return self.views[length - 1];
50
- },
51
- removeView(view) {
52
- for (const [, widget] of self.activeWidgets) {
53
- if (widget.view?.id === view.id) {
152
+ // The focus is a persisted id, not a reference, so nothing drops it
153
+ // when the view it names goes. Every consumer compares
154
+ // `focusedViewId === view.id`, so a stale one matches nothing — the
155
+ // focus ring vanishes and the drawer stops naming a view, with nothing
156
+ // to say why — and it ships in a saved or shared session. `replaceView`
157
+ // captures `wasFocused` above this and re-points the focus at the
158
+ // replacement afterwards, which is the one case where the focus moves
159
+ // rather than ending.
160
+ const { focusedViewId } = self;
161
+ if (focusedViewId !== undefined &&
162
+ viewIdsWithin(view).includes(focusedViewId)) {
163
+ self.setFocusedViewId(undefined);
164
+ }
165
+ // Every widget, not the active ones: a widget the user closed keeps
166
+ // its references and its autoruns. One that pointed into the view is
167
+ // now empty, so hide it rather than leave the panel showing what it no
168
+ // longer has.
169
+ for (const [, widget] of self.widgets) {
170
+ if (dropRefsInto(widget, view)) {
54
171
  self.hideWidget(widget);
55
172
  }
56
173
  }
57
- self.views.remove(view);
58
- },
59
- setStickyViewHeaders(sticky) {
60
- self.stickyViewHeaders = sticky;
61
- },
62
- setUseWorkspaces(useWorkspaces) {
63
- self.useWorkspaces = useWorkspaces;
64
- },
65
- afterAttach() {
66
- addDisposer(self, autorun(function stickyViewHeadersAutorun() {
67
- localStorageSetBoolean('stickyViewHeaders', self.stickyViewHeaders);
68
- }, { name: 'StickyViewHeaders' }));
69
- addDisposer(self, autorun(function useWorkspacesAutorun() {
70
- localStorageSetBoolean('useWorkspaces', self.useWorkspaces);
71
- }, { name: 'UseWorkspaces' }));
174
+ // MST fires the view's `beforeDetach` here, while it is still attached
175
+ // and the walk to the session still resolves. That matters: anything of
176
+ // a view's that reaches OUTSIDE its own tree has to move to that hook,
177
+ // because after this line the view is a root and `getSession` from a
178
+ // root throws. ADR-069's detach-time disposer, at view scope — the
179
+ // comparative views' `releaseTemporaryAssemblies` is the one case.
180
+ detach(view);
181
+ scheduleDetachedDestroy(view);
182
+ };
183
+ return {
184
+ /**
185
+ * #action
186
+ */
187
+ moveViewDown(id, scopeIds) {
188
+ move(id, 'down', scopeIds);
189
+ },
190
+ /**
191
+ * #action
192
+ */
193
+ moveViewUp(id, scopeIds) {
194
+ move(id, 'up', scopeIds);
195
+ },
196
+ /**
197
+ * #action
198
+ */
199
+ moveViewToTop(id, scopeIds) {
200
+ move(id, 'top', scopeIds);
201
+ },
202
+ /**
203
+ * #action
204
+ */
205
+ moveViewToBottom(id, scopeIds) {
206
+ move(id, 'bottom', scopeIds);
207
+ },
208
+ /**
209
+ * #action
210
+ * Put the named views into the given relative order, leaving views not
211
+ * named in their own slots.
212
+ *
213
+ * `session.views` is the one ordering, so this is how a channel that
214
+ * states an order in some other vocabulary gets it applied: a session
215
+ * spec's `layout` names views per panel, top to bottom, and that used to
216
+ * be honoured by the panel assignment array's order. Now the assignment
217
+ * carries membership only, so the layout says it here instead, once.
218
+ */
219
+ orderViews(ids) {
220
+ self.views = cast(applyOrderWithin(self.views, ids, v => v.id));
221
+ },
222
+ /**
223
+ * #action
224
+ * Requires the view type's state model to be loaded; for a lazily
225
+ * registered type use `launchView`.
226
+ */
227
+ addView(typeName, initialState) {
228
+ assertViewStateModelLoaded(typeName);
229
+ const length = self.views.push({
230
+ ...initialState,
231
+ type: typeName,
232
+ });
233
+ return self.views[length - 1];
234
+ },
235
+ /**
236
+ * #action
237
+ * swap `view` for a new view of `typeName`, in the slot it occupied.
238
+ *
239
+ * The launchers that build a view of a DIFFERENT type out of one you are
240
+ * already looking at — launch synteny view, read-vs-ref, split view —
241
+ * otherwise append, leaving the source view above the thing it produced
242
+ * and a stack of two views showing the same locus. This is the "replace"
243
+ * half of that offer, and a slot swap rather than remove-then-add so the
244
+ * new view lands where the reader was looking instead of at the bottom of
245
+ * the session.
246
+ *
247
+ * A launcher whose result is the SAME type as its source does not come
248
+ * here and should not: collapse introns offers "Replace current view" by
249
+ * navigating the view it was invoked on, which keeps the view's identity
250
+ * and so lets its snackbar offer an Undo. Swapping in a new node would
251
+ * leave nothing to undo onto.
252
+ *
253
+ * The slot is `session.views`, which is the order views render in under
254
+ * both layout modes (a tab's `viewIds` says which tab a view is in, not
255
+ * where in it), so the swap lands in place either way.
256
+ *
257
+ * WHICH panel is a separate question and this does not answer it: the
258
+ * new view arrives in no tab, and `homeUnassignedViews` puts it in the
259
+ * active panel — the same panel only when the replaced view was in the
260
+ * active one. In practice it is, since clicking into a view activates
261
+ * its panel, and the launch that offers a replace is a click on that
262
+ * view's own menu.
263
+ */
264
+ replaceView(view, typeName, initialState) {
265
+ assertViewStateModelLoaded(typeName);
266
+ // read before the removal, which is what makes both stale
267
+ const idx = self.views.indexOf(view);
268
+ // `idx` first, so a view already gone from the session short-circuits
269
+ // before `view.id` is read: that node is detached and, a task later,
270
+ // destroyed — and MST warns on any read through a destroyed one
271
+ const { focusedViewId } = self;
272
+ const wasFocused = idx !== -1 &&
273
+ focusedViewId !== undefined &&
274
+ viewIdsWithin(view).includes(focusedViewId);
275
+ takeOut(view);
276
+ // a view already gone from the session appends, rather than throwing
277
+ // or silently dropping the launch
278
+ const at = idx === -1 ? self.views.length : idx;
279
+ self.views.splice(at, 0, { ...initialState, type: typeName });
280
+ const created = self.views[at];
281
+ // The replacement takes the slot, so it takes the focus with it.
282
+ // Every consumer compares `focusedViewId === view.id`, so a
283
+ // replacement that left the old id in place would simply match
284
+ // nothing: the focus ring would vanish and the drawer would stop
285
+ // naming a view, with nothing to say why.
286
+ if (wasFocused) {
287
+ self.setFocusedViewId(created.id);
288
+ }
289
+ return created;
290
+ },
291
+ /**
292
+ * #action
293
+ */
294
+ removeView(view) {
295
+ takeOut(view);
296
+ },
297
+ /**
298
+ * #action
299
+ * Take out every view the given session snapshot will not keep, before
300
+ * a whole-tree `applySnapshot` destroys it where it stands.
301
+ *
302
+ * Undo and redo are `applySnapshot` on the session
303
+ * (`core/util/TimeTraveller`, which calls this), and every view carries
304
+ * an `ElementId` — so MST reconciles by identifier and destroys what the
305
+ * target lacks, inside the action, with the components still mounted.
306
+ * Measured on a redo across a closed view, with the restored view
307
+ * repainted first: 4 liveliness reads of its display before this, 0
308
+ * after.
309
+ *
310
+ * A view that is present but has changed `type` is destroyed by that
311
+ * same reconciliation (`areSame` runs the type's `is()` before its id
312
+ * check), so identity here is the pair, not the id.
313
+ *
314
+ * **Views, and deliberately not what is under them.** A track or a
315
+ * display the snapshot drops is still destroyed in place, and that is
316
+ * not this door being worse than the others: `hideTrackGeneric` is a
317
+ * plain `tracks.remove` and measures louder (5 reads to undo's 4).
318
+ * Detaching one would be worse than the destroy it replaced — a
319
+ * detached display is a LIVE root, and `getContainingView` walks
320
+ * parents and throws when the walk finds no view, where a dead node
321
+ * only warns. At or above the view is the only place that walk still
322
+ * lands, which is why ADR-069's rule stops there.
323
+ */
324
+ takeOutViewsMissingFrom(snapshot) {
325
+ const incoming = viewsOfSessionSnapshot(snapshot);
326
+ // an argument that is not a session snapshot used to read as "keep
327
+ // none" and close every view: an agent introspecting the actions
328
+ // called this with a tab id and a keep-list and lost its session
329
+ if (!incoming) {
330
+ throw new Error('takeOutViewsMissingFrom takes a session snapshot (it is what undo and redo call to close the views a snapshot no longer has); to close one view, session.removeView(view)');
331
+ }
332
+ const kept = new Map(incoming.map(v => [v.id, v.type]));
333
+ // a copy, because `takeOut` splices the array it is iterating
334
+ for (const view of [...self.views]) {
335
+ if (kept.get(view.id) !== view.type) {
336
+ takeOut(view);
337
+ }
338
+ }
339
+ },
340
+ /**
341
+ * #action
342
+ */
343
+ setStickyViewHeaders(sticky) {
344
+ self.stickyViewHeaders = sticky;
345
+ },
346
+ /**
347
+ * #action
348
+ * set the workspaces layout for this session only, leaving the user's
349
+ * personal default untouched. For session-scoped intent — a spec
350
+ * carrying a `layout`, or an ad-hoc "move view to a tab/split" — where
351
+ * rewriting the visitor's global preference would be a surprise. The
352
+ * user-facing default toggle is `setUseWorkspacesPreference`.
353
+ */
354
+ setUseWorkspaces(useWorkspaces) {
355
+ self.useWorkspaces = useWorkspaces;
356
+ },
357
+ /**
358
+ * #action
359
+ * the user-facing workspaces toggle: applies to this session and
360
+ * becomes their default for sessions that don't specify one. Persisted
361
+ * only here, on an explicit toggle — an autorun mirroring the resolved
362
+ * value would bake the admin default into every visitor's localStorage
363
+ * on first load, so a later admin change could never reach them.
364
+ */
365
+ setUseWorkspacesPreference(useWorkspaces) {
366
+ self.useWorkspaces = useWorkspaces;
367
+ self.setPreferenceOverride('useWorkspaces', useWorkspaces);
368
+ },
369
+ /**
370
+ * #action
371
+ * drop both this session's explicit value and the user's override so
372
+ * workspaces falls back to the admin default
373
+ */
374
+ resetUseWorkspaces() {
375
+ self.useWorkspaces = undefined;
376
+ self.clearPreferenceOverride('useWorkspaces');
377
+ },
378
+ afterAttach() {
379
+ addDisposer(self, autorun(function stickyViewHeadersAutorun() {
380
+ localStorageSetBoolean('stickyViewHeaders', self.stickyViewHeaders);
381
+ }, { name: 'StickyViewHeaders' }));
382
+ },
383
+ };
384
+ })
385
+ .actions(self => ({
386
+ /**
387
+ * #action
388
+ * `addView` for view types whose state model may be lazily loaded. The
389
+ * view is not in `session.views` until the promise resolves.
390
+ * `getViewType` first so an unknown type name fails by that name; the
391
+ * snapshot walk skips unregistered names and covers the displays of the
392
+ * tracks a launch opens with.
393
+ */
394
+ async launchView(typeName, initialState) {
395
+ await pluginManager.getViewType(typeName).loadStateModel();
396
+ await pluginManager.preloadSessionTypes({
397
+ views: [{ type: typeName, ...initialState }],
398
+ });
399
+ return self.addView(typeName, initialState);
72
400
  },
73
401
  }))
74
402
  .postProcessSnapshot(snap => {
403
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
75
404
  if (!snap) {
76
405
  return snap;
77
406
  }
78
- const { stickyViewHeaders, useWorkspaces, ...rest } = snap;
79
- return {
80
- ...rest,
81
- ...(!stickyViewHeaders ? { stickyViewHeaders } : {}),
82
- ...(useWorkspaces ? { useWorkspaces } : {}),
83
- };
407
+ // stickyViewHeaders is a personal per-browser UI preference, not shared
408
+ // view state: destructure it out so it never lands in the snapshot. It
409
+ // stays localStorage-backed, so each browser keeps its own value.
410
+ // useWorkspaces stays: it changes layout intent (and pairs with the
411
+ // `layout` tree the snapshot carries), which is meaningful to share.
412
+ const { stickyViewHeaders, ...rest } = snap;
413
+ return rest;
84
414
  });
85
415
  }
416
+ /** Type guard for SessionWithMultipleViews */
86
417
  export function isSessionWithMultipleViews(session) {
87
418
  return isBaseSession(session) && 'views' in session;
88
419
  }