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