@tutti-os/workbench-surface 0.0.230 → 0.0.232

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.
package/dist/index.js CHANGED
@@ -2546,10 +2546,13 @@ import { createPortal } from "react-dom";
2546
2546
 
2547
2547
  // src/react/WorkbenchWindowFrame.tsx
2548
2548
  import {
2549
+ createContext as createContext2,
2549
2550
  useCallback as useCallback6,
2551
+ useContext as useContext2,
2550
2552
  useLayoutEffect as useLayoutEffect3,
2551
2553
  useMemo as useMemo4,
2552
- useRef as useRef3
2554
+ useRef as useRef3,
2555
+ useSyncExternalStore
2553
2556
  } from "react";
2554
2557
  import { createI18nRuntime } from "@tutti-os/ui-i18n-runtime";
2555
2558
  import { Checkbox } from "@tutti-os/ui-system";
@@ -2919,6 +2922,10 @@ var defaultWindowChromeI18n = createWorkbenchWindowChromeI18nRuntime(
2919
2922
  dictionaries: [workbenchWindowChromeI18nResources.en]
2920
2923
  })
2921
2924
  );
2925
+ var WorkbenchWindowPresentationVisibilityContext = createContext2(true);
2926
+ function useWorkbenchWindowPresentationVisibility() {
2927
+ return useContext2(WorkbenchWindowPresentationVisibilityContext);
2928
+ }
2922
2929
  function resolveWorkbenchNodeLaunchSource(data) {
2923
2930
  if (data && typeof data === "object" && "launchSource" in data && typeof data.launchSource === "string" && data.launchSource.length > 0) {
2924
2931
  return data.launchSource;
@@ -2934,9 +2941,10 @@ function resolveWorkbenchNodeTypeId(data) {
2934
2941
  function WorkbenchWindowFrame({
2935
2942
  children,
2936
2943
  edgeSnapEnabled = false,
2937
- genie,
2944
+ genieNodeVisibility,
2938
2945
  hiddenMounted = false,
2939
2946
  interactive = true,
2947
+ minimizeNodeToAnchor: minimizeNodeToAnchorProp,
2940
2948
  node,
2941
2949
  presentation = null,
2942
2950
  renderActions,
@@ -2971,10 +2979,23 @@ function WorkbenchWindowFrame({
2971
2979
  controller.commands.focusNode(node.id);
2972
2980
  controller.commands.applySnapTarget(node.id, "top");
2973
2981
  };
2974
- const minimizeNodeToAnchorRef = useRef3(genie.minimizeNodeToAnchor);
2982
+ const subscribeGenieVisibility = useCallback6(
2983
+ (listener) => genieNodeVisibility.subscribe(node.id, listener),
2984
+ [genieNodeVisibility, node.id]
2985
+ );
2986
+ const readGenieVisibility = useCallback6(
2987
+ () => genieNodeVisibility.getSnapshot(node.id),
2988
+ [genieNodeVisibility, node.id]
2989
+ );
2990
+ const isGenieHidden = useSyncExternalStore(
2991
+ subscribeGenieVisibility,
2992
+ readGenieVisibility,
2993
+ readGenieVisibility
2994
+ );
2995
+ const minimizeNodeToAnchorRef = useRef3(minimizeNodeToAnchorProp);
2975
2996
  useLayoutEffect3(() => {
2976
- minimizeNodeToAnchorRef.current = genie.minimizeNodeToAnchor;
2977
- }, [genie.minimizeNodeToAnchor]);
2997
+ minimizeNodeToAnchorRef.current = minimizeNodeToAnchorProp;
2998
+ }, [minimizeNodeToAnchorProp]);
2978
2999
  const minimizeNodeToAnchor = useCallback6(
2979
3000
  (nodeID, minimize) => {
2980
3001
  minimizeNodeToAnchorRef.current(nodeID, minimize);
@@ -3043,6 +3064,7 @@ function WorkbenchWindowFrame({
3043
3064
  const presentationMode = presentation?.mode ?? null;
3044
3065
  const presentationFrame = presentation?.frameByNodeId.get(node.id) ?? null;
3045
3066
  const isPresentationHidden = presentationMode === "mission-control" && !presentation?.visibleNodeIds.has(node.id);
3067
+ const isWindowPresentationVisible = !hiddenMounted && !isGenieHidden && presentationMode === null;
3046
3068
  const presentationInteraction = interactive && presentationMode === "mission-control" && !isPresentationHidden ? presentation?.interaction ?? null : null;
3047
3069
  const isMissionControlSelected = presentationInteraction?.selectedNodeIds.has(node.id) ?? false;
3048
3070
  const presentationScale = presentationMode === "mission-control" && presentationFrame ? Math.min(
@@ -3065,7 +3087,7 @@ function WorkbenchWindowFrame({
3065
3087
  className: "workbench-window-shell",
3066
3088
  "data-focused": isFocused ? "true" : "false",
3067
3089
  "data-display-mode": node.displayMode,
3068
- "data-genie-state": genie.isNodeGenieHidden(node.id) ? "hidden" : "visible",
3090
+ "data-genie-state": isGenieHidden ? "hidden" : "visible",
3069
3091
  "data-launch-source": resolveWorkbenchNodeLaunchSource(node.data),
3070
3092
  "data-minimized-mount": hiddenMounted ? "hidden" : "visible",
3071
3093
  "data-presentation-mode": presentationMode ?? "default",
@@ -3120,7 +3142,13 @@ function WorkbenchWindowFrame({
3120
3142
  ] })
3121
3143
  }
3122
3144
  ),
3123
- /* @__PURE__ */ jsx7("div", { className: "workbench-window__body", children })
3145
+ /* @__PURE__ */ jsx7("div", { className: "workbench-window__body", children: /* @__PURE__ */ jsx7(
3146
+ WorkbenchWindowPresentationVisibilityContext.Provider,
3147
+ {
3148
+ value: isWindowPresentationVisible,
3149
+ children
3150
+ }
3151
+ ) })
3124
3152
  ]
3125
3153
  }
3126
3154
  ),
@@ -3169,6 +3197,7 @@ function ResizeHandle({
3169
3197
  }
3170
3198
 
3171
3199
  // src/react/renderedNodeIds.ts
3200
+ var emptyNodeIDs = [];
3172
3201
  function createRenderedWorkbenchNodeIDsSelector(shouldKeepMinimizedNodeMounted = void 0) {
3173
3202
  let previousIDs = null;
3174
3203
  return (state) => {
@@ -3182,6 +3211,50 @@ function createRenderedWorkbenchNodeIDsSelector(shouldKeepMinimizedNodeMounted =
3182
3211
  return nextIDs;
3183
3212
  };
3184
3213
  }
3214
+ function createWorkbenchNodeLayerNodeIDsSelector({
3215
+ missionControl,
3216
+ resolveWindowSurfaceLayer,
3217
+ shouldKeepMinimizedNodeMounted
3218
+ }) {
3219
+ const selectRenderedNodeIDs = createRenderedWorkbenchNodeIDsSelector(
3220
+ shouldKeepMinimizedNodeMounted
3221
+ );
3222
+ let previousSelection = null;
3223
+ return (state) => {
3224
+ const renderedNodeIDs = selectRenderedNodeIDs(state);
3225
+ let nextDefaultNodeIDs = renderedNodeIDs;
3226
+ let nextDialogPopoverNodeIDs = emptyNodeIDs;
3227
+ if (resolveWindowSurfaceLayer && !missionControl) {
3228
+ const nodeByID = new Map(state.nodes.map((node) => [node.id, node]));
3229
+ const defaultNodeIDs = [];
3230
+ const dialogPopoverNodeIDs = [];
3231
+ for (const nodeID of renderedNodeIDs) {
3232
+ const node = nodeByID.get(nodeID);
3233
+ if (node && resolveWindowSurfaceLayer({ node }) === "dialog-popover") {
3234
+ dialogPopoverNodeIDs.push(nodeID);
3235
+ } else {
3236
+ defaultNodeIDs.push(nodeID);
3237
+ }
3238
+ }
3239
+ nextDefaultNodeIDs = preserveStringArrayIdentity(
3240
+ previousSelection?.defaultNodeIDs,
3241
+ defaultNodeIDs
3242
+ );
3243
+ nextDialogPopoverNodeIDs = preserveStringArrayIdentity(
3244
+ previousSelection?.dialogPopoverNodeIDs,
3245
+ dialogPopoverNodeIDs
3246
+ );
3247
+ }
3248
+ if (previousSelection?.defaultNodeIDs === nextDefaultNodeIDs && previousSelection.dialogPopoverNodeIDs === nextDialogPopoverNodeIDs) {
3249
+ return previousSelection;
3250
+ }
3251
+ previousSelection = {
3252
+ defaultNodeIDs: nextDefaultNodeIDs,
3253
+ dialogPopoverNodeIDs: nextDialogPopoverNodeIDs
3254
+ };
3255
+ return previousSelection;
3256
+ };
3257
+ }
3185
3258
  function shouldRenderWorkbenchNode(node, shouldKeepMinimizedNodeMounted) {
3186
3259
  return !node.isMinimized || shouldKeepMinimizedNodeMounted?.(node) === true;
3187
3260
  }
@@ -3191,6 +3264,9 @@ function stringArraysEqual(left, right) {
3191
3264
  }
3192
3265
  return left.every((value, index) => value === right[index]);
3193
3266
  }
3267
+ function preserveStringArrayIdentity(previous, next) {
3268
+ return previous && stringArraysEqual(previous, next) ? previous : next;
3269
+ }
3194
3270
 
3195
3271
  // src/react/WorkbenchNodeLayer.tsx
3196
3272
  import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -3210,46 +3286,32 @@ function WorkbenchNodeLayer({
3210
3286
  windowChromeMode,
3211
3287
  windowChromeI18n
3212
3288
  }) {
3213
- const selectRenderedNodeIDs = useMemo5(
3214
- () => createRenderedWorkbenchNodeIDsSelector(shouldKeepMinimizedNodeMounted),
3215
- [shouldKeepMinimizedNodeMounted]
3289
+ const selectNodeLayerNodeIDs = useMemo5(
3290
+ () => createWorkbenchNodeLayerNodeIDsSelector({
3291
+ missionControl: presentation?.mode === "mission-control",
3292
+ resolveWindowSurfaceLayer,
3293
+ shouldKeepMinimizedNodeMounted
3294
+ }),
3295
+ [
3296
+ presentation?.mode,
3297
+ resolveWindowSurfaceLayer,
3298
+ shouldKeepMinimizedNodeMounted
3299
+ ]
3216
3300
  );
3217
- const nodeIDs = useWorkbenchSelector(
3218
- selectRenderedNodeIDs
3301
+ const { defaultNodeIDs, dialogPopoverNodeIDs } = useWorkbenchSelector(
3302
+ selectNodeLayerNodeIDs
3219
3303
  );
3220
- const { defaultNodeIDs, dialogPopoverNodeIDs } = useWorkbenchSelector((state) => {
3221
- if (!resolveWindowSurfaceLayer || presentation?.mode === "mission-control") {
3222
- return {
3223
- defaultNodeIDs: nodeIDs,
3224
- dialogPopoverNodeIDs: []
3225
- };
3226
- }
3227
- const nodeByID = new Map(state.nodes.map((node) => [node.id, node]));
3228
- const nextDefaultNodeIDs = [];
3229
- const nextDialogPopoverNodeIDs = [];
3230
- for (const nodeID of nodeIDs) {
3231
- const node = nodeByID.get(nodeID);
3232
- if (node && resolveWindowSurfaceLayer({ node }) === "dialog-popover") {
3233
- nextDialogPopoverNodeIDs.push(nodeID);
3234
- } else {
3235
- nextDefaultNodeIDs.push(nodeID);
3236
- }
3237
- }
3238
- return {
3239
- defaultNodeIDs: nextDefaultNodeIDs,
3240
- dialogPopoverNodeIDs: nextDialogPopoverNodeIDs
3241
- };
3242
- });
3243
3304
  const snapPreviewRect = useWorkbenchSelector(selectWorkbenchSnapPreviewRect);
3244
3305
  const presentationInteraction = interactive && presentation?.mode === "mission-control" ? presentation.interaction ?? null : null;
3245
3306
  const dialogPopoverLayer = dialogPopoverNodeIDs.length > 0 ? /* @__PURE__ */ jsx8(
3246
- WorkbenchNodeLayerGroup,
3307
+ MemoizedWorkbenchNodeLayerGroup,
3247
3308
  {
3248
3309
  className: "workbench-node-layer workbench-node-layer--dialog-popover",
3249
3310
  edgeSnapEnabled,
3250
3311
  fullscreenHeaderMode: resolveFullscreenHeaderMode,
3251
- genie,
3312
+ genieNodeVisibility: genie.nodeVisibility,
3252
3313
  interactive,
3314
+ minimizeNodeToAnchor: genie.minimizeNodeToAnchor,
3253
3315
  nodeIDs: dialogPopoverNodeIDs,
3254
3316
  presentation,
3255
3317
  renderNode,
@@ -3263,13 +3325,14 @@ function WorkbenchNodeLayer({
3263
3325
  ) : null;
3264
3326
  return /* @__PURE__ */ jsxs5(Fragment3, { children: [
3265
3327
  /* @__PURE__ */ jsx8(
3266
- WorkbenchNodeLayerGroup,
3328
+ MemoizedWorkbenchNodeLayerGroup,
3267
3329
  {
3268
3330
  className: "workbench-node-layer",
3269
3331
  edgeSnapEnabled,
3270
3332
  fullscreenHeaderMode: resolveFullscreenHeaderMode,
3271
- genie,
3333
+ genieNodeVisibility: genie.nodeVisibility,
3272
3334
  interactive,
3335
+ minimizeNodeToAnchor: genie.minimizeNodeToAnchor,
3273
3336
  nodeIDs: defaultNodeIDs,
3274
3337
  onBackdropPress: presentationInteraction?.onBackdropPress,
3275
3338
  presentation,
@@ -3290,8 +3353,9 @@ function WorkbenchNodeLayerGroup({
3290
3353
  className,
3291
3354
  edgeSnapEnabled,
3292
3355
  fullscreenHeaderMode,
3293
- genie,
3356
+ genieNodeVisibility,
3294
3357
  interactive,
3358
+ minimizeNodeToAnchor,
3295
3359
  nodeIDs,
3296
3360
  onBackdropPress,
3297
3361
  presentation,
@@ -3332,9 +3396,10 @@ function WorkbenchNodeLayerGroup({
3332
3396
  MemoizedWorkbenchNodeLayerItem,
3333
3397
  {
3334
3398
  fullscreenHeaderMode,
3335
- genie,
3399
+ genieNodeVisibility,
3336
3400
  edgeSnapEnabled,
3337
3401
  interactive,
3402
+ minimizeNodeToAnchor,
3338
3403
  nodeID,
3339
3404
  presentation,
3340
3405
  renderNode,
@@ -3351,11 +3416,15 @@ function WorkbenchNodeLayerGroup({
3351
3416
  }
3352
3417
  );
3353
3418
  }
3419
+ var MemoizedWorkbenchNodeLayerGroup = memo(
3420
+ WorkbenchNodeLayerGroup
3421
+ );
3354
3422
  function WorkbenchNodeLayerItem({
3355
3423
  fullscreenHeaderMode,
3356
- genie,
3424
+ genieNodeVisibility,
3357
3425
  edgeSnapEnabled,
3358
3426
  interactive,
3427
+ minimizeNodeToAnchor,
3359
3428
  nodeID,
3360
3429
  presentation,
3361
3430
  renderNode,
@@ -3393,7 +3462,8 @@ function WorkbenchNodeLayerItem({
3393
3462
  interactive,
3394
3463
  presentation,
3395
3464
  node,
3396
- genie,
3465
+ genieNodeVisibility,
3466
+ minimizeNodeToAnchor,
3397
3467
  resolveWindowZIndex,
3398
3468
  fullscreenHeaderMode: fullscreenHeaderMode?.({
3399
3469
  controller,
@@ -3612,6 +3682,10 @@ var genieVerticalRowStagger = 0.2;
3612
3682
  var genieDockGlowRadius = 55;
3613
3683
  var genieScanlineStrideThresholdPx = 640;
3614
3684
  var genieMaxScanlineStride = 3;
3685
+ var genieWarmupTextureWidthRatio = 0.65;
3686
+ var genieWarmupTextureHeightRatio = 0.7;
3687
+ var genieWarmupTextureMaxWidth = 1280;
3688
+ var genieWarmupTextureMaxHeight = 800;
3615
3689
  function clampGenieProgress(value) {
3616
3690
  return Math.max(0, Math.min(1, value));
3617
3691
  }
@@ -3642,6 +3716,9 @@ function viewportRectFromElement(element) {
3642
3716
  function isUsableGenieRect(rect) {
3643
3717
  return rect.width > 0 && rect.height > 0;
3644
3718
  }
3719
+ function isGenieTextureResolutionSufficient(source, target) {
3720
+ return Number.isFinite(source.width) && Number.isFinite(source.height) && source.width >= Math.max(1, Math.round(target.width)) && source.height >= Math.max(1, Math.round(target.height));
3721
+ }
3645
3722
  function centerPointFromRect(rect) {
3646
3723
  return {
3647
3724
  x: rect.left + rect.width / 2,
@@ -3818,9 +3895,117 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
3818
3895
  dirtyRect.height
3819
3896
  );
3820
3897
  }
3898
+ function resolveGenieWarmupTextureSize(viewportWidth, viewportHeight) {
3899
+ return {
3900
+ height: Math.max(
3901
+ 1,
3902
+ Math.min(
3903
+ genieWarmupTextureMaxHeight,
3904
+ Math.round(viewportHeight * genieWarmupTextureHeightRatio)
3905
+ )
3906
+ ),
3907
+ width: Math.max(
3908
+ 1,
3909
+ Math.min(
3910
+ genieWarmupTextureMaxWidth,
3911
+ Math.round(viewportWidth * genieWarmupTextureWidthRatio)
3912
+ )
3913
+ )
3914
+ };
3915
+ }
3916
+ function renderGenieWarmupFrames(context, viewportWidth, viewportHeight, texture) {
3917
+ const textureRect = {
3918
+ height: texture.height,
3919
+ left: (viewportWidth - texture.width) / 2,
3920
+ top: (viewportHeight - texture.height) / 2,
3921
+ width: texture.width
3922
+ };
3923
+ const dockPoint = {
3924
+ x: viewportWidth / 2,
3925
+ y: Math.max(0, viewportHeight - 24)
3926
+ };
3927
+ renderGenieScanlines(context, viewportWidth, viewportHeight, {
3928
+ direction: "minimize",
3929
+ dockPoint,
3930
+ progress: 0,
3931
+ texture,
3932
+ textureRect
3933
+ });
3934
+ renderGenieScanlines(context, viewportWidth, viewportHeight, {
3935
+ direction: "minimize",
3936
+ dockPoint,
3937
+ progress: 0.5,
3938
+ texture,
3939
+ textureRect
3940
+ });
3941
+ renderGenieScanlines(context, viewportWidth, viewportHeight, {
3942
+ direction: "open",
3943
+ dockPoint,
3944
+ progress: 0,
3945
+ texture,
3946
+ textureRect
3947
+ });
3948
+ context.clearRect(0, 0, viewportWidth, viewportHeight);
3949
+ }
3821
3950
 
3822
3951
  // src/react/genieTextureCapture.ts
3952
+ var genieCloneMaskImageProperties = [
3953
+ "-webkit-mask-image",
3954
+ "mask-image"
3955
+ ];
3823
3956
  var readableStylesheetTextByDocument = /* @__PURE__ */ new WeakMap();
3957
+ function readSingleCssUrl(value) {
3958
+ const normalized = value.trim();
3959
+ if (!normalized.startsWith("url(") || !normalized.endsWith(")")) {
3960
+ return null;
3961
+ }
3962
+ const inner = normalized.slice(4, -1).trim();
3963
+ if (!inner) {
3964
+ return null;
3965
+ }
3966
+ const quote = inner[0];
3967
+ if (quote === '"' || quote === "'") {
3968
+ return inner.endsWith(quote) ? inner.slice(1, -1) : null;
3969
+ }
3970
+ return inner.includes('"') || inner.includes("'") ? null : inner;
3971
+ }
3972
+ async function inlineGenieCloneMaskImageResources({
3973
+ cloneRoot,
3974
+ readResource
3975
+ }) {
3976
+ const referencesByUrl = /* @__PURE__ */ new Map();
3977
+ const elements = [
3978
+ cloneRoot,
3979
+ ...Array.from(cloneRoot.querySelectorAll("[style]"))
3980
+ ];
3981
+ for (const element of elements) {
3982
+ for (const property of genieCloneMaskImageProperties) {
3983
+ const url = readSingleCssUrl(element.style.getPropertyValue(property));
3984
+ if (!url) {
3985
+ continue;
3986
+ }
3987
+ const references = referencesByUrl.get(url) ?? [];
3988
+ references.push({ element, property });
3989
+ referencesByUrl.set(url, references);
3990
+ }
3991
+ }
3992
+ await Promise.all(
3993
+ Array.from(referencesByUrl.entries(), async ([url, references], index) => {
3994
+ const inlineUrl = await readResource(url);
3995
+ if (!inlineUrl) {
3996
+ return;
3997
+ }
3998
+ const variableName = `--workbench-genie-capture-mask-${index}`;
3999
+ cloneRoot.style.setProperty(
4000
+ variableName,
4001
+ `url(${JSON.stringify(inlineUrl)})`
4002
+ );
4003
+ for (const { element, property } of references) {
4004
+ element.style.setProperty(property, `var(${variableName})`);
4005
+ }
4006
+ })
4007
+ );
4008
+ }
3824
4009
  function hasSameStylesheetFingerprints(left, right) {
3825
4010
  return left.length === right.length && left.every((fingerprint, index) => {
3826
4011
  const candidate = right[index];
@@ -3947,6 +4132,224 @@ function prepareGenieTextureCapture(element) {
3947
4132
  };
3948
4133
  }
3949
4134
 
4135
+ // src/react/genieTextureCache.ts
4136
+ function readWorkbenchGenieTextureCacheEntry(cache, nodeID) {
4137
+ const value = cache.get(nodeID) ?? null;
4138
+ if (!value) {
4139
+ return null;
4140
+ }
4141
+ cache.delete(nodeID);
4142
+ cache.set(nodeID, value);
4143
+ return value;
4144
+ }
4145
+ function writeWorkbenchGenieTextureCacheEntry(cache, nodeID, value, limits) {
4146
+ cache.delete(nodeID);
4147
+ cache.set(nodeID, value);
4148
+ pruneWorkbenchGenieTextureCache(cache, limits);
4149
+ }
4150
+ function estimateWorkbenchGenieTextureBytes(value) {
4151
+ return Math.max(0, value.canvas.width) * Math.max(0, value.canvas.height) * 4;
4152
+ }
4153
+ function pruneRemovedWorkbenchGenieTextureCacheEntries(cache, existingNodeIDs) {
4154
+ for (const nodeID of cache.keys()) {
4155
+ if (!existingNodeIDs.has(nodeID)) {
4156
+ cache.delete(nodeID);
4157
+ }
4158
+ }
4159
+ }
4160
+ function pruneWorkbenchGenieTextureCache(cache, limits) {
4161
+ let estimatedBytes = 0;
4162
+ for (const value of cache.values()) {
4163
+ estimatedBytes += estimateWorkbenchGenieTextureBytes(value);
4164
+ }
4165
+ while (cache.size > limits.maxEntries || estimatedBytes > limits.maxBytes) {
4166
+ const oldest = cache.entries().next().value;
4167
+ if (!oldest) {
4168
+ return;
4169
+ }
4170
+ cache.delete(oldest[0]);
4171
+ estimatedBytes -= estimateWorkbenchGenieTextureBytes(oldest[1]);
4172
+ }
4173
+ }
4174
+
4175
+ // src/react/genieNodeVisibility.ts
4176
+ function createWorkbenchGenieNodeVisibilityStore() {
4177
+ const hiddenNodeIDs = /* @__PURE__ */ new Set();
4178
+ const hiddenTokenByNodeID = /* @__PURE__ */ new Map();
4179
+ const listenersByNodeID = /* @__PURE__ */ new Map();
4180
+ const setHidden = (nodeID, hidden) => {
4181
+ if (hiddenNodeIDs.has(nodeID) === hidden) {
4182
+ return;
4183
+ }
4184
+ if (hidden) {
4185
+ hiddenNodeIDs.add(nodeID);
4186
+ } else {
4187
+ hiddenNodeIDs.delete(nodeID);
4188
+ }
4189
+ for (const listener of listenersByNodeID.get(nodeID) ?? []) {
4190
+ listener();
4191
+ }
4192
+ };
4193
+ const hide = (nodeID) => {
4194
+ const token = Symbol(nodeID);
4195
+ hiddenTokenByNodeID.set(nodeID, token);
4196
+ setHidden(nodeID, true);
4197
+ return token;
4198
+ };
4199
+ const show = (nodeID, token) => {
4200
+ if (token && hiddenTokenByNodeID.get(nodeID) !== token) {
4201
+ return false;
4202
+ }
4203
+ hiddenTokenByNodeID.delete(nodeID);
4204
+ setHidden(nodeID, false);
4205
+ return true;
4206
+ };
4207
+ return {
4208
+ dispose() {
4209
+ hiddenNodeIDs.clear();
4210
+ hiddenTokenByNodeID.clear();
4211
+ listenersByNodeID.clear();
4212
+ },
4213
+ getSnapshot(nodeID) {
4214
+ return hiddenNodeIDs.has(nodeID);
4215
+ },
4216
+ hide,
4217
+ setHidden(nodeID, hidden) {
4218
+ if (hidden) {
4219
+ hide(nodeID);
4220
+ } else {
4221
+ show(nodeID);
4222
+ }
4223
+ },
4224
+ show,
4225
+ subscribe(nodeID, listener) {
4226
+ const listeners = listenersByNodeID.get(nodeID) ?? /* @__PURE__ */ new Set();
4227
+ listeners.add(listener);
4228
+ listenersByNodeID.set(nodeID, listeners);
4229
+ return () => {
4230
+ listeners.delete(listener);
4231
+ if (listeners.size === 0) {
4232
+ listenersByNodeID.delete(nodeID);
4233
+ }
4234
+ };
4235
+ }
4236
+ };
4237
+ }
4238
+
4239
+ // src/react/workbenchGenieScheduling.ts
4240
+ function startCachedWorkbenchGenieRestore({
4241
+ launch,
4242
+ onLaunchSettled,
4243
+ scheduleTask,
4244
+ startAnimation
4245
+ }) {
4246
+ let launchScheduled = false;
4247
+ const launchAfterAnimation = () => {
4248
+ if (launchScheduled) {
4249
+ return;
4250
+ }
4251
+ launchScheduled = true;
4252
+ scheduleTask(() => {
4253
+ void Promise.resolve(launch()).then(onLaunchSettled, onLaunchSettled);
4254
+ });
4255
+ };
4256
+ startAnimation(launchAfterAnimation);
4257
+ }
4258
+ function scheduleWorkbenchGenieWarmup({
4259
+ isAnimationActive,
4260
+ isWarmupComplete,
4261
+ renderWarmup,
4262
+ scheduler
4263
+ }) {
4264
+ let cancelled = false;
4265
+ let idleID = null;
4266
+ const schedule = () => {
4267
+ idleID = scheduler.requestIdleCallback(runWarmup);
4268
+ };
4269
+ const runWarmup = () => {
4270
+ idleID = null;
4271
+ if (cancelled || isWarmupComplete()) {
4272
+ return;
4273
+ }
4274
+ if (isAnimationActive()) {
4275
+ schedule();
4276
+ return;
4277
+ }
4278
+ renderWarmup();
4279
+ };
4280
+ schedule();
4281
+ return () => {
4282
+ cancelled = true;
4283
+ if (idleID !== null) {
4284
+ scheduler.cancelIdleCallback(idleID);
4285
+ }
4286
+ };
4287
+ }
4288
+ function scheduleWorkbenchGeniePostAnimationIdleTask({
4289
+ isAnimationActive,
4290
+ isCancelled,
4291
+ runTask,
4292
+ scheduler
4293
+ }) {
4294
+ let cancelled = false;
4295
+ let idleID = null;
4296
+ const schedule = () => {
4297
+ idleID = scheduler.requestIdleCallback(run);
4298
+ };
4299
+ const run = () => {
4300
+ idleID = null;
4301
+ if (cancelled || isCancelled()) {
4302
+ return;
4303
+ }
4304
+ if (isAnimationActive()) {
4305
+ schedule();
4306
+ return;
4307
+ }
4308
+ runTask();
4309
+ };
4310
+ schedule();
4311
+ return () => {
4312
+ cancelled = true;
4313
+ if (idleID !== null) {
4314
+ scheduler.cancelIdleCallback(idleID);
4315
+ }
4316
+ };
4317
+ }
4318
+ async function resolveNativeFirstGenieTexture({
4319
+ nativeImageUrlPromise,
4320
+ renderDomFallback,
4321
+ renderNativeImage,
4322
+ timeoutMs
4323
+ }) {
4324
+ const nativeCapture = await new Promise((resolve) => {
4325
+ let settled = false;
4326
+ const settleResolved = (nativeImageUrl) => {
4327
+ if (settled) {
4328
+ return;
4329
+ }
4330
+ settled = true;
4331
+ clearTimeout(timer);
4332
+ resolve({ nativeImageUrl, nativeStatus: "resolved" });
4333
+ };
4334
+ const timer = setTimeout(() => {
4335
+ if (settled) {
4336
+ return;
4337
+ }
4338
+ settled = true;
4339
+ resolve({ nativeImageUrl: null, nativeStatus: "pending" });
4340
+ }, timeoutMs);
4341
+ nativeImageUrlPromise.then(settleResolved, () => settleResolved(null));
4342
+ });
4343
+ const nativeTexture = nativeCapture.nativeImageUrl ? await Promise.resolve(
4344
+ renderNativeImage(nativeCapture.nativeImageUrl)
4345
+ ).catch(() => null) : null;
4346
+ const texture = nativeTexture ?? await Promise.resolve(renderDomFallback()).catch(() => null);
4347
+ return {
4348
+ ...nativeCapture,
4349
+ texture
4350
+ };
4351
+ }
4352
+
3950
4353
  // src/react/useWorkbenchGenieAnimation.tsx
3951
4354
  import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
3952
4355
  var genieDurationMs = 400;
@@ -3960,6 +4363,8 @@ var dockAnchorResolveMaxAnimationFrames = 3;
3960
4363
  var dockPreviewMaxWidth = 260;
3961
4364
  var dockPreviewMaxHeight = 170;
3962
4365
  var dockPreviewImageCacheMaxEntries = 96;
4366
+ var minimizedGenieTextureCacheMaxBytes = 64 * 1024 * 1024;
4367
+ var minimizedGenieTextureCacheMaxEntries = 8;
3963
4368
  var inlineImageResourceCacheMaxEntries = 160;
3964
4369
  var inlineImageResizeTargetCacheMaxEntries = 4;
3965
4370
  var dockAnchorFallbackSizePx = 43.2;
@@ -3977,26 +4382,6 @@ function waitForNextAnimationFrame() {
3977
4382
  window.requestAnimationFrame(() => resolve());
3978
4383
  });
3979
4384
  }
3980
- function racePreviewImageCapture(promise, timeoutMs) {
3981
- return new Promise((resolve) => {
3982
- let settled = false;
3983
- const timer = setTimeout(() => {
3984
- if (settled) {
3985
- return;
3986
- }
3987
- settled = true;
3988
- resolve({ previewImageUrl: null, status: "pending" });
3989
- }, timeoutMs);
3990
- promise.then((previewImageUrl) => {
3991
- if (settled) {
3992
- return;
3993
- }
3994
- settled = true;
3995
- clearTimeout(timer);
3996
- resolve({ previewImageUrl, status: "resolved" });
3997
- });
3998
- });
3999
- }
4000
4385
  function isFocusedWorkbenchNode(controller, nodeID) {
4001
4386
  return controller.getSnapshot().nodeStack.at(-1) === nodeID;
4002
4387
  }
@@ -4094,8 +4479,12 @@ async function inlineCloneImageResources({
4094
4479
  images
4095
4480
  }) {
4096
4481
  const cloneImages = Array.from(cloneRoot.querySelectorAll("img"));
4097
- await Promise.all(
4098
- images.map(async (imageInfo, index) => {
4482
+ await Promise.all([
4483
+ inlineGenieCloneMaskImageResources({
4484
+ cloneRoot,
4485
+ readResource: readInlineImageResource
4486
+ }),
4487
+ ...images.map(async (imageInfo, index) => {
4099
4488
  const cloneImage = cloneImages[index];
4100
4489
  if (!cloneImage) {
4101
4490
  return;
@@ -4118,7 +4507,7 @@ async function inlineCloneImageResources({
4118
4507
  }
4119
4508
  cloneImage.src = inlineImageUrl;
4120
4509
  })
4121
- );
4510
+ ]);
4122
4511
  }
4123
4512
  async function loadImageFromSvg(svg) {
4124
4513
  const image = new Image();
@@ -4293,6 +4682,12 @@ async function renderPreviewImageTexture({
4293
4682
  const image = new Image();
4294
4683
  image.src = previewImageUrl;
4295
4684
  await image.decode();
4685
+ if (!isGenieTextureResolutionSufficient(
4686
+ { height: image.naturalHeight, width: image.naturalWidth },
4687
+ rect
4688
+ )) {
4689
+ return null;
4690
+ }
4296
4691
  const canvas = document.createElement("canvas");
4297
4692
  canvas.width = Math.max(1, Math.round(rect.width * genieSnapshotScale));
4298
4693
  canvas.height = Math.max(1, Math.round(rect.height * genieSnapshotScale));
@@ -4415,6 +4810,7 @@ function createGenieSvgTexture(element, rect, outputSize = {
4415
4810
  }
4416
4811
  function useWorkbenchGenieAnimation({
4417
4812
  captureNodePreviewImage,
4813
+ captureNodePreviewImages,
4418
4814
  controller,
4419
4815
  debugDiagnostics,
4420
4816
  dockPreviewCache,
@@ -4427,6 +4823,7 @@ function useWorkbenchGenieAnimation({
4427
4823
  const dockAnchorElementsRef = useRef5(/* @__PURE__ */ new Map());
4428
4824
  const canvasRef = useRef5(null);
4429
4825
  const rafRef = useRef5(null);
4826
+ const isMountedRef = useRef5(true);
4430
4827
  const animationGenerationRef = useRef5(0);
4431
4828
  const animationCleanupRef = useRef5(null);
4432
4829
  const minimizedDockEnterAnimationNodeIdsRef = useRef5(/* @__PURE__ */ new Set());
@@ -4436,13 +4833,14 @@ function useWorkbenchGenieAnimation({
4436
4833
  const minimizedGenieTextureByNodeIDRef = useRef5(
4437
4834
  /* @__PURE__ */ new Map()
4438
4835
  );
4836
+ const genieCanvasWarmupCompletedRef = useRef5(false);
4439
4837
  const renderedPreviewCaptureIDRef = useRef5(0);
4440
4838
  const renderedPreviewCaptureElementRef = useRef5(null);
4441
4839
  const pendingRenderedPreviewCaptureRef = useRef5(null);
4840
+ const nodeVisibilityStoreRef = useRef5(null);
4841
+ nodeVisibilityStoreRef.current ??= createWorkbenchGenieNodeVisibilityStore();
4842
+ const nodeVisibility = nodeVisibilityStoreRef.current;
4442
4843
  const [isCanvasActive, setIsCanvasActive] = useState4(false);
4443
- const [genieHiddenNodeIDs, setGenieHiddenNodeIDs] = useState4(
4444
- () => /* @__PURE__ */ new Set()
4445
- );
4446
4844
  const [pendingMinimizedNode, setPendingMinimizedNode] = useState4(null);
4447
4845
  const [pendingRenderedPreviewCapture, setPendingRenderedPreviewCapture] = useState4(null);
4448
4846
  const registerDockAnchor = useCallback7(
@@ -4520,53 +4918,49 @@ function useWorkbenchGenieAnimation({
4520
4918
  return minimizedDockEnterAnimationNodeIdsRef.current.has(nodeID);
4521
4919
  }, []);
4522
4920
  const readMinimizedGenieTexture = useCallback7((nodeID) => {
4523
- return minimizedGenieTextureByNodeIDRef.current.get(nodeID) ?? null;
4921
+ return readWorkbenchGenieTextureCacheEntry(
4922
+ minimizedGenieTextureByNodeIDRef.current,
4923
+ nodeID
4924
+ );
4524
4925
  }, []);
4525
4926
  const writeMinimizedGenieTexture = useCallback7(
4526
4927
  (nodeID, texture) => {
4527
- const cache = minimizedGenieTextureByNodeIDRef.current;
4528
- cache.delete(nodeID);
4529
- cache.set(nodeID, texture);
4928
+ writeWorkbenchGenieTextureCacheEntry(
4929
+ minimizedGenieTextureByNodeIDRef.current,
4930
+ nodeID,
4931
+ texture,
4932
+ {
4933
+ maxBytes: minimizedGenieTextureCacheMaxBytes,
4934
+ maxEntries: minimizedGenieTextureCacheMaxEntries
4935
+ }
4936
+ );
4530
4937
  },
4531
4938
  []
4532
4939
  );
4533
4940
  const clearMinimizedGenieTexture = useCallback7((nodeID) => {
4534
4941
  minimizedGenieTextureByNodeIDRef.current.delete(nodeID);
4535
4942
  }, []);
4536
- const pruneMinimizedGenieTextures = useCallback7(
4537
- (retainNodeID) => {
4538
- const minimizedNodeIDs = new Set(
4539
- controller.getSnapshot().nodes.filter((node) => node.isMinimized === true).map((node) => node.id)
4540
- );
4541
- if (retainNodeID) {
4542
- minimizedNodeIDs.add(retainNodeID);
4543
- }
4544
- for (const nodeID of minimizedGenieTextureByNodeIDRef.current.keys()) {
4545
- if (!minimizedNodeIDs.has(nodeID)) {
4546
- minimizedGenieTextureByNodeIDRef.current.delete(nodeID);
4547
- }
4548
- }
4943
+ const pruneRemovedNodeGenieTextures = useCallback7(() => {
4944
+ const existingNodeIDs = new Set(
4945
+ controller.getSnapshot().nodes.map((node) => node.id)
4946
+ );
4947
+ pruneRemovedWorkbenchGenieTextureCacheEntries(
4948
+ minimizedGenieTextureByNodeIDRef.current,
4949
+ existingNodeIDs
4950
+ );
4951
+ }, [controller]);
4952
+ const hideNodeForGenie = useCallback7(
4953
+ (nodeID) => {
4954
+ return nodeVisibility.hide(nodeID);
4549
4955
  },
4550
- [controller]
4956
+ [nodeVisibility]
4957
+ );
4958
+ const showNodeForGenie = useCallback7(
4959
+ (nodeID, token) => {
4960
+ return nodeVisibility.show(nodeID, token);
4961
+ },
4962
+ [nodeVisibility]
4551
4963
  );
4552
- const hideNodeForGenie = useCallback7((nodeID) => {
4553
- setGenieHiddenNodeIDs((current) => {
4554
- if (current.has(nodeID)) {
4555
- return current;
4556
- }
4557
- return /* @__PURE__ */ new Set([...current, nodeID]);
4558
- });
4559
- }, []);
4560
- const showNodeForGenie = useCallback7((nodeID) => {
4561
- setGenieHiddenNodeIDs((current) => {
4562
- if (!current.has(nodeID)) {
4563
- return current;
4564
- }
4565
- const next = new Set(current);
4566
- next.delete(nodeID);
4567
- return next;
4568
- });
4569
- }, []);
4570
4964
  const clearPendingMinimizedNode = useCallback7((nodeID) => {
4571
4965
  setPendingMinimizedNode(
4572
4966
  (current) => current?.id === nodeID ? null : current
@@ -4658,8 +5052,20 @@ function useWorkbenchGenieAnimation({
4658
5052
  );
4659
5053
  const viewportWidth = window.innerWidth;
4660
5054
  const viewportHeight = window.innerHeight;
4661
- canvas.width = Math.max(1, Math.round(viewportWidth * devicePixelRatio));
4662
- canvas.height = Math.max(1, Math.round(viewportHeight * devicePixelRatio));
5055
+ const pixelWidth = Math.max(
5056
+ 1,
5057
+ Math.round(viewportWidth * devicePixelRatio)
5058
+ );
5059
+ const pixelHeight = Math.max(
5060
+ 1,
5061
+ Math.round(viewportHeight * devicePixelRatio)
5062
+ );
5063
+ if (canvas.width !== pixelWidth) {
5064
+ canvas.width = pixelWidth;
5065
+ }
5066
+ if (canvas.height !== pixelHeight) {
5067
+ canvas.height = pixelHeight;
5068
+ }
4663
5069
  canvas.style.width = `${viewportWidth}px`;
4664
5070
  canvas.style.height = `${viewportHeight}px`;
4665
5071
  const context = canvas.getContext("2d");
@@ -4669,6 +5075,48 @@ function useWorkbenchGenieAnimation({
4669
5075
  context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
4670
5076
  return { context, viewportHeight, viewportWidth };
4671
5077
  }, []);
5078
+ useEffect4(() => {
5079
+ if (minimizeAnimation !== "genie" || shouldReduceMotion() || typeof window.requestIdleCallback !== "function") {
5080
+ return;
5081
+ }
5082
+ return scheduleWorkbenchGenieWarmup({
5083
+ isAnimationActive: () => rafRef.current !== null,
5084
+ isWarmupComplete: () => !isMountedRef.current || genieCanvasWarmupCompletedRef.current,
5085
+ renderWarmup: function warmGenieCanvas() {
5086
+ const setup = setupCanvas();
5087
+ if (!setup) {
5088
+ return;
5089
+ }
5090
+ const warmupSize = resolveGenieWarmupTextureSize(
5091
+ setup.viewportWidth,
5092
+ setup.viewportHeight
5093
+ );
5094
+ const warmupTexture = document.createElement("canvas");
5095
+ warmupTexture.width = warmupSize.width;
5096
+ warmupTexture.height = warmupSize.height;
5097
+ const warmupContext = warmupTexture.getContext("2d");
5098
+ warmupContext?.fillRect(
5099
+ 0,
5100
+ 0,
5101
+ warmupTexture.width,
5102
+ warmupTexture.height
5103
+ );
5104
+ renderGenieWarmupFrames(
5105
+ setup.context,
5106
+ setup.viewportWidth,
5107
+ setup.viewportHeight,
5108
+ warmupTexture
5109
+ );
5110
+ genieCanvasWarmupCompletedRef.current = true;
5111
+ },
5112
+ scheduler: {
5113
+ cancelIdleCallback: (idleID) => {
5114
+ window.cancelIdleCallback(idleID);
5115
+ },
5116
+ requestIdleCallback: (callback) => window.requestIdleCallback(callback)
5117
+ }
5118
+ });
5119
+ }, [minimizeAnimation, setupCanvas]);
4672
5120
  const clearCanvas = useCallback7(() => {
4673
5121
  const canvas = canvasRef.current;
4674
5122
  const context = canvas?.getContext("2d") ?? null;
@@ -4989,7 +5437,52 @@ function useWorkbenchGenieAnimation({
4989
5437
  }
4990
5438
  stopAnimation();
4991
5439
  const dockRectFallback = resolveDockAnchorRect(anchorKey);
4992
- hideNodeForGenie(nodeID);
5440
+ let visibilityToken = null;
5441
+ flushSync(() => {
5442
+ visibilityToken = hideNodeForGenie(nodeID);
5443
+ });
5444
+ let launchPromise = null;
5445
+ const launchOnce = () => {
5446
+ if (!isMountedRef.current) {
5447
+ return Promise.resolve(null);
5448
+ }
5449
+ launchPromise ??= Promise.resolve(launch()).catch(() => null);
5450
+ return launchPromise;
5451
+ };
5452
+ const cachedTexture = readMinimizedGenieTexture(nodeID);
5453
+ if (effectiveMinimizeAnimation === "genie" && cachedTexture && dockRectFallback && isUsableGenieRect(dockRectFallback)) {
5454
+ const generation2 = animationGenerationRef.current;
5455
+ const revealLaunchedNode = () => {
5456
+ const settledVisibilityToken = visibilityToken;
5457
+ if (!isMountedRef.current || !settledVisibilityToken) {
5458
+ return;
5459
+ }
5460
+ flushSync(() => {
5461
+ showNodeForGenie(nodeID, settledVisibilityToken);
5462
+ });
5463
+ if (generation2 === animationGenerationRef.current) {
5464
+ clearCanvas();
5465
+ }
5466
+ };
5467
+ startCachedWorkbenchGenieRestore({
5468
+ launch: launchOnce,
5469
+ onLaunchSettled: revealLaunchedNode,
5470
+ scheduleTask: (callback) => {
5471
+ window.setTimeout(callback, 0);
5472
+ },
5473
+ startAnimation: (onAnimationSettled) => {
5474
+ runGenieAnimation({
5475
+ direction: "open",
5476
+ dockRect: dockRectFallback,
5477
+ onCancel: onAnimationSettled,
5478
+ onComplete: onAnimationSettled,
5479
+ skipStop: true,
5480
+ texture: cachedTexture
5481
+ });
5482
+ }
5483
+ });
5484
+ return;
5485
+ }
4993
5486
  animationCleanupRef.current = () => {
4994
5487
  if (rafRef.current !== null) {
4995
5488
  window.cancelAnimationFrame(rafRef.current);
@@ -5007,7 +5500,7 @@ function useWorkbenchGenieAnimation({
5007
5500
  if (generation !== animationGenerationRef.current) {
5008
5501
  return;
5009
5502
  }
5010
- void Promise.resolve(launch()).catch(() => null).then(() => {
5503
+ void launchOnce().then(() => {
5011
5504
  if (generation !== animationGenerationRef.current) {
5012
5505
  return;
5013
5506
  }
@@ -5027,7 +5520,9 @@ function useWorkbenchGenieAnimation({
5027
5520
  controller,
5028
5521
  hideNodeForGenie,
5029
5522
  minimizeAnimation,
5523
+ readMinimizedGenieTexture,
5030
5524
  resolveDockAnchorRect,
5525
+ runGenieAnimation,
5031
5526
  showNodeForGenie,
5032
5527
  startOpenOrRestoreAnimation,
5033
5528
  stopAnimation
@@ -5036,9 +5531,9 @@ function useWorkbenchGenieAnimation({
5036
5531
  const minimizeNodeToAnchor = useCallback7(
5037
5532
  (nodeID, minimize) => {
5038
5533
  void (async () => {
5039
- pruneMinimizedGenieTextures();
5040
5534
  const target = controller.getSnapshot().nodes.find((node) => node.id === nodeID);
5041
5535
  if (!target) {
5536
+ pruneRemovedNodeGenieTextures();
5042
5537
  logWorkbenchGenieDiagnostic(
5043
5538
  debugDiagnostics,
5044
5539
  "workbench.genie.minimize.skipped",
@@ -5050,9 +5545,12 @@ function useWorkbenchGenieAnimation({
5050
5545
  );
5051
5546
  return;
5052
5547
  }
5053
- clearMinimizedGenieTexture(nodeID);
5548
+ pruneRemovedNodeGenieTextures();
5054
5549
  const runMinimize = minimize ?? (() => controller.commands.minimizeNode(nodeID));
5055
5550
  const effectiveMinimizeAnimation = shouldReduceMotion() ? "off" : minimizeAnimation;
5551
+ if (effectiveMinimizeAnimation !== "genie") {
5552
+ clearMinimizedGenieTexture(nodeID);
5553
+ }
5056
5554
  const shouldCapturePreview = shouldCaptureNodePreviewImage?.(target) ?? true;
5057
5555
  if (effectiveMinimizeAnimation === "off") {
5058
5556
  stopAnimation();
@@ -5255,14 +5753,16 @@ function useWorkbenchGenieAnimation({
5255
5753
  isMinimized: true,
5256
5754
  minimizedAtUnixMs: Date.now()
5257
5755
  };
5258
- const componentPreviewTexture = shouldCapturePreview ? null : await requestRenderedGeniePreviewTexture({
5756
+ const cachedTexture = shouldCapturePreview ? readMinimizedGenieTexture(nodeID) : null;
5757
+ const reusableTexture = cachedTexture && isGenieTextureResolutionSufficient(cachedTexture.canvas, windowRect) ? { canvas: cachedTexture.canvas, rect: windowRect } : null;
5758
+ const componentPreviewTexture = reusableTexture || shouldCapturePreview ? null : await requestRenderedGeniePreviewTexture({
5259
5759
  node: pendingMinimizedNode2,
5260
5760
  textureRect: windowRect
5261
5761
  }).catch(() => null);
5262
5762
  if (generation !== animationGenerationRef.current) {
5263
5763
  return;
5264
5764
  }
5265
- if (!shouldCapturePreview) {
5765
+ if (!reusableTexture && !shouldCapturePreview) {
5266
5766
  if (!componentPreviewTexture) {
5267
5767
  logWorkbenchGenieDiagnostic(
5268
5768
  debugDiagnostics,
@@ -5280,25 +5780,6 @@ function useWorkbenchGenieAnimation({
5280
5780
  return;
5281
5781
  }
5282
5782
  }
5283
- const captureTarget = resolveWorkbenchCaptureElement(nodeElement);
5284
- const preparedTexture = shouldCapturePreview ? prepareGenieTextureCapture(captureTarget) : null;
5285
- if (!preparedTexture) {
5286
- if (!componentPreviewTexture) {
5287
- logWorkbenchGenieDiagnostic(
5288
- debugDiagnostics,
5289
- "workbench.genie.minimize.skipped",
5290
- {
5291
- ...describeGenieNode(target),
5292
- mode: "genie",
5293
- reason: "prepared_texture_missing",
5294
- shouldCapturePreview
5295
- },
5296
- "warn"
5297
- );
5298
- runMinimize();
5299
- return;
5300
- }
5301
- }
5302
5783
  const wasFocusedForCapture = isFocusedWorkbenchNode(controller, nodeID);
5303
5784
  if (shouldCapturePreview && !wasFocusedForCapture) {
5304
5785
  flushSync(() => {
@@ -5309,43 +5790,96 @@ function useWorkbenchGenieAnimation({
5309
5790
  return;
5310
5791
  }
5311
5792
  }
5312
- const previewImageUrlPromise = shouldCapturePreview ? Promise.resolve(captureNodePreviewImage?.(target) ?? null).catch(
5793
+ const previewImagesPromise = shouldCapturePreview ? captureNodePreviewImages ? Promise.resolve(captureNodePreviewImages(target)).catch(
5313
5794
  () => null
5314
- ) : Promise.resolve(null);
5315
- const previewCaptureOutcome = await racePreviewImageCapture(
5316
- previewImageUrlPromise,
5317
- previewCaptureRaceTimeoutMs
5795
+ ) : Promise.resolve(captureNodePreviewImage?.(target) ?? null).then(
5796
+ (previewImageUrl) => previewImageUrl ? {
5797
+ dockPreviewImageUrl: previewImageUrl,
5798
+ genieImageUrl: previewImageUrl
5799
+ } : null
5800
+ ).catch(() => null) : Promise.resolve(null);
5801
+ const previewImageUrlPromise = previewImagesPromise.then(
5802
+ (images) => images?.genieImageUrl ?? null
5318
5803
  );
5319
- const previewImageUrl = previewCaptureOutcome.previewImageUrl;
5320
- if (previewCaptureOutcome.status === "pending") {
5804
+ let preparedTexture = null;
5805
+ let previewImageTexture = null;
5806
+ const nativeFirstResult = shouldCapturePreview && !reusableTexture ? await resolveNativeFirstGenieTexture({
5807
+ nativeImageUrlPromise: previewImageUrlPromise,
5808
+ renderDomFallback: () => {
5809
+ preparedTexture = prepareGenieTextureCapture(
5810
+ resolveWorkbenchCaptureElement(nodeElement)
5811
+ );
5812
+ return preparedTexture ? renderPreparedElementTexture(preparedTexture) : null;
5813
+ },
5814
+ renderNativeImage: async (previewImageUrl) => {
5815
+ previewImageTexture = await renderPreviewImageTexture({
5816
+ previewImageUrl,
5817
+ rect: windowRect
5818
+ }).catch(() => null);
5819
+ return previewImageTexture;
5820
+ },
5821
+ timeoutMs: previewCaptureRaceTimeoutMs
5822
+ }) : null;
5823
+ const previewImages = nativeFirstResult?.nativeStatus === "resolved" ? await previewImagesPromise : null;
5824
+ const dockPreviewImageUrl = previewImages?.dockPreviewImageUrl ?? null;
5825
+ if (reusableTexture || nativeFirstResult?.nativeStatus === "pending") {
5321
5826
  logWorkbenchGenieDiagnostic(
5322
5827
  debugDiagnostics,
5323
5828
  "workbench.genie.preview_capture.deferred",
5324
5829
  {
5325
5830
  ...describeGenieNode(target),
5326
5831
  mode: "genie",
5832
+ reusedCachedTexture: Boolean(reusableTexture),
5327
5833
  timeoutMs: previewCaptureRaceTimeoutMs
5328
5834
  },
5329
5835
  "debug"
5330
5836
  );
5331
- void previewImageUrlPromise.then((latePreviewImageUrl) => {
5332
- if (!latePreviewImageUrl || generation !== animationGenerationRef.current) {
5837
+ void previewImagesPromise.then((latePreviewImages) => {
5838
+ const latePreviewImageUrl = latePreviewImages?.genieImageUrl;
5839
+ const lateDockPreviewImageUrl = latePreviewImages?.dockPreviewImageUrl;
5840
+ if (generation !== animationGenerationRef.current) {
5333
5841
  return;
5334
5842
  }
5335
- writeCachedWorkbenchNodePreviewImage(nodeID, latePreviewImageUrl);
5336
- persistWorkbenchNodePreviewImage(target, latePreviewImageUrl, {
5337
- dockPreviewCache,
5338
- resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
5843
+ if (lateDockPreviewImageUrl) {
5844
+ writeCachedWorkbenchNodePreviewImage(
5845
+ nodeID,
5846
+ lateDockPreviewImageUrl
5847
+ );
5848
+ persistWorkbenchNodePreviewImage(
5849
+ target,
5850
+ lateDockPreviewImageUrl,
5851
+ {
5852
+ dockPreviewCache,
5853
+ resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
5854
+ }
5855
+ );
5856
+ }
5857
+ if (!latePreviewImageUrl || typeof window.requestIdleCallback !== "function") {
5858
+ return;
5859
+ }
5860
+ scheduleWorkbenchGeniePostAnimationIdleTask({
5861
+ isAnimationActive: () => rafRef.current !== null,
5862
+ isCancelled: () => !isMountedRef.current || generation !== animationGenerationRef.current,
5863
+ runTask: () => {
5864
+ void renderPreviewImageTexture({
5865
+ previewImageUrl: latePreviewImageUrl,
5866
+ rect: windowRect
5867
+ }).catch(() => null).then((refreshedTexture) => {
5868
+ if (refreshedTexture && generation === animationGenerationRef.current) {
5869
+ writeMinimizedGenieTexture(nodeID, refreshedTexture);
5870
+ }
5871
+ });
5872
+ },
5873
+ scheduler: {
5874
+ cancelIdleCallback: (idleID) => {
5875
+ window.cancelIdleCallback(idleID);
5876
+ },
5877
+ requestIdleCallback: (callback) => window.requestIdleCallback(callback)
5878
+ }
5339
5879
  });
5340
5880
  });
5341
5881
  }
5342
- const previewImageTexture = shouldCapturePreview && previewImageUrl ? await renderPreviewImageTexture({
5343
- previewImageUrl,
5344
- rect: windowRect
5345
- }).catch(() => null) : null;
5346
- const texture = componentPreviewTexture ?? previewImageTexture ?? (preparedTexture ? await renderPreparedElementTexture(preparedTexture).catch(
5347
- () => null
5348
- ) : null);
5882
+ const texture = reusableTexture ?? componentPreviewTexture ?? nativeFirstResult?.texture ?? null;
5349
5883
  if (generation !== animationGenerationRef.current) {
5350
5884
  return;
5351
5885
  }
@@ -5364,9 +5898,9 @@ function useWorkbenchGenieAnimation({
5364
5898
  },
5365
5899
  "warn"
5366
5900
  );
5367
- if (previewImageUrl) {
5368
- writeCachedWorkbenchNodePreviewImage(nodeID, previewImageUrl);
5369
- persistWorkbenchNodePreviewImage(target, previewImageUrl, {
5901
+ if (dockPreviewImageUrl) {
5902
+ writeCachedWorkbenchNodePreviewImage(nodeID, dockPreviewImageUrl);
5903
+ persistWorkbenchNodePreviewImage(target, dockPreviewImageUrl, {
5370
5904
  dockPreviewCache,
5371
5905
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
5372
5906
  });
@@ -5374,9 +5908,9 @@ function useWorkbenchGenieAnimation({
5374
5908
  runMinimize();
5375
5909
  return;
5376
5910
  }
5377
- if (previewImageUrl) {
5378
- writeCachedWorkbenchNodePreviewImage(nodeID, previewImageUrl);
5379
- persistWorkbenchNodePreviewImage(target, previewImageUrl, {
5911
+ if (dockPreviewImageUrl) {
5912
+ writeCachedWorkbenchNodePreviewImage(nodeID, dockPreviewImageUrl);
5913
+ persistWorkbenchNodePreviewImage(target, dockPreviewImageUrl, {
5380
5914
  dockPreviewCache,
5381
5915
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
5382
5916
  });
@@ -5391,7 +5925,7 @@ function useWorkbenchGenieAnimation({
5391
5925
  });
5392
5926
  }
5393
5927
  writeMinimizedGenieTexture(nodeID, texture);
5394
- pruneMinimizedGenieTextures(nodeID);
5928
+ pruneRemovedNodeGenieTextures();
5395
5929
  let minimizeCommitted = false;
5396
5930
  const commitMinimize = () => {
5397
5931
  if (minimizeCommitted) {
@@ -5453,13 +5987,19 @@ function useWorkbenchGenieAnimation({
5453
5987
  cleanupPendingGenieMinimize();
5454
5988
  },
5455
5989
  onComplete: () => {
5456
- scheduleReleaseMinimizedDockEnterAnimation(nodeID);
5457
- flushSync(() => {
5458
- clearPendingMinimizedNode(nodeID);
5459
- showNodeForGenie(nodeID);
5460
- commitMinimize();
5461
- });
5462
- clearCanvas();
5990
+ window.setTimeout(() => {
5991
+ if (!isMountedRef.current) {
5992
+ commitMinimize();
5993
+ return;
5994
+ }
5995
+ scheduleReleaseMinimizedDockEnterAnimation(nodeID);
5996
+ flushSync(() => {
5997
+ clearPendingMinimizedNode(nodeID);
5998
+ showNodeForGenie(nodeID);
5999
+ commitMinimize();
6000
+ });
6001
+ clearCanvas();
6002
+ }, 0);
5463
6003
  },
5464
6004
  skipStop: true,
5465
6005
  texture
@@ -5474,12 +6014,14 @@ function useWorkbenchGenieAnimation({
5474
6014
  clearMinimizedGenieTexture,
5475
6015
  controller,
5476
6016
  captureNodePreviewImage,
6017
+ captureNodePreviewImages,
5477
6018
  clearPendingMinimizedNode,
5478
6019
  debugDiagnostics,
5479
6020
  dockPreviewCache,
5480
6021
  hideNodeForGenie,
5481
6022
  minimizeAnimation,
5482
- pruneMinimizedGenieTextures,
6023
+ pruneRemovedNodeGenieTextures,
6024
+ readMinimizedGenieTexture,
5483
6025
  registerMinimizedDockEnterAnimation,
5484
6026
  releaseMinimizedDockEnterAnimation,
5485
6027
  requestRenderedGeniePreviewTexture,
@@ -5497,17 +6039,25 @@ function useWorkbenchGenieAnimation({
5497
6039
  writeMinimizedGenieTexture
5498
6040
  ]
5499
6041
  );
5500
- useEffect4(
5501
- () => () => {
6042
+ useEffect4(() => {
6043
+ pruneRemovedNodeGenieTextures();
6044
+ return controller.subscribe(() => {
6045
+ pruneRemovedNodeGenieTextures();
6046
+ });
6047
+ }, [controller, pruneRemovedNodeGenieTextures]);
6048
+ useEffect4(() => {
6049
+ isMountedRef.current = true;
6050
+ return () => {
6051
+ isMountedRef.current = false;
5502
6052
  for (const timer of minimizedDockEnterAnimationTimersRef.current.values()) {
5503
6053
  clearTimeout(timer);
5504
6054
  }
5505
6055
  minimizedDockEnterAnimationTimersRef.current.clear();
5506
6056
  minimizedDockEnterAnimationNodeIdsRef.current.clear();
5507
6057
  minimizedGenieTextureByNodeIDRef.current.clear();
5508
- },
5509
- []
5510
- );
6058
+ nodeVisibility.dispose();
6059
+ };
6060
+ }, [nodeVisibility]);
5511
6061
  const minimizeNodeToAnchorRef = useRef5(minimizeNodeToAnchor);
5512
6062
  useLayoutEffect5(() => {
5513
6063
  minimizeNodeToAnchorRef.current = minimizeNodeToAnchor;
@@ -5551,10 +6101,6 @@ function useWorkbenchGenieAnimation({
5551
6101
  ),
5552
6102
  [isCanvasActive, pendingRenderedPreviewCapture]
5553
6103
  );
5554
- const isNodeGenieHidden = useCallback7(
5555
- (nodeID) => genieHiddenNodeIDs.has(nodeID),
5556
- [genieHiddenNodeIDs]
5557
- );
5558
6104
  const isPendingMinimizedDockNode = useCallback7(
5559
6105
  (nodeID) => pendingMinimizedNode?.id === nodeID,
5560
6106
  [pendingMinimizedNode]
@@ -5562,19 +6108,19 @@ function useWorkbenchGenieAnimation({
5562
6108
  return useMemo6(
5563
6109
  () => ({
5564
6110
  genieLayer,
5565
- isNodeGenieHidden,
5566
6111
  isPendingMinimizedDockNode,
5567
6112
  launchNodeFromAnchor,
5568
6113
  minimizeNodeToAnchor: stableMinimizeNodeToAnchor,
6114
+ nodeVisibility,
5569
6115
  pendingMinimizedNode,
5570
6116
  registerDockAnchor,
5571
6117
  shouldAnimateMinimizedDockEnter
5572
6118
  }),
5573
6119
  [
5574
6120
  genieLayer,
5575
- isNodeGenieHidden,
5576
6121
  isPendingMinimizedDockNode,
5577
6122
  launchNodeFromAnchor,
6123
+ nodeVisibility,
5578
6124
  pendingMinimizedNode,
5579
6125
  registerDockAnchor,
5580
6126
  shouldAnimateMinimizedDockEnter,
@@ -5587,6 +6133,7 @@ function useWorkbenchGenieAnimation({
5587
6133
  import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
5588
6134
  function WorkbenchSurface({
5589
6135
  captureNodePreviewImage,
6136
+ captureNodePreviewImages,
5590
6137
  className,
5591
6138
  controller,
5592
6139
  debugDiagnostics,
@@ -5624,6 +6171,7 @@ function WorkbenchSurface({
5624
6171
  WorkbenchSurfaceInner,
5625
6172
  {
5626
6173
  captureNodePreviewImage,
6174
+ captureNodePreviewImages,
5627
6175
  className,
5628
6176
  debugDiagnostics,
5629
6177
  dockPreviewCache,
@@ -5660,6 +6208,7 @@ function WorkbenchSurface({
5660
6208
  }
5661
6209
  function WorkbenchSurfaceInner({
5662
6210
  captureNodePreviewImage,
6211
+ captureNodePreviewImages,
5663
6212
  className,
5664
6213
  debugDiagnostics,
5665
6214
  dockPreviewCache,
@@ -5702,6 +6251,7 @@ function WorkbenchSurfaceInner({
5702
6251
  const ref = useWorkbenchSurfaceSize(onSizeChange);
5703
6252
  const genie = useWorkbenchGenieAnimation({
5704
6253
  captureNodePreviewImage,
6254
+ captureNodePreviewImages,
5705
6255
  controller,
5706
6256
  debugDiagnostics,
5707
6257
  dockPreviewCache,
@@ -6973,7 +7523,7 @@ function nullableRectsEqual(left, right) {
6973
7523
  }
6974
7524
 
6975
7525
  // src/host/externalState.ts
6976
- import { useCallback as useCallback9, useMemo as useMemo7, useSyncExternalStore } from "react";
7526
+ import { useCallback as useCallback9, useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore2 } from "react";
6977
7527
  var noopSubscribe = () => () => {
6978
7528
  };
6979
7529
  function createWorkbenchHostExternalStateLookupInput(input) {
@@ -7018,12 +7568,12 @@ function useWorkbenchHostExternalState(input) {
7018
7568
  [input.externalStateSource, workspaceInput]
7019
7569
  );
7020
7570
  return {
7021
- externalNodeState: useSyncExternalStore(
7571
+ externalNodeState: useSyncExternalStore2(
7022
7572
  subscribeNodeState,
7023
7573
  getNodeState,
7024
7574
  getNodeState
7025
7575
  ),
7026
- externalWorkspaceState: useSyncExternalStore(
7576
+ externalWorkspaceState: useSyncExternalStore2(
7027
7577
  subscribeWorkspaceState,
7028
7578
  getWorkspaceState,
7029
7579
  getWorkspaceState
@@ -13715,6 +14265,7 @@ function createWorkbenchHostNodeBodyContext({
13715
14265
  externalState,
13716
14266
  externalStateSource,
13717
14267
  host,
14268
+ isVisible,
13718
14269
  workspaceId
13719
14270
  }) {
13720
14271
  const resolvedExternalState = externalState ?? readWorkbenchHostExternalState({
@@ -13736,6 +14287,7 @@ function createWorkbenchHostNodeBodyContext({
13736
14287
  isDragging: context.isDragging,
13737
14288
  isFocused: selectFocusedWorkbenchNode(host.getSnapshot())?.id === context.node.id,
13738
14289
  isResizing: context.isResizing,
14290
+ isVisible,
13739
14291
  presentationMode: context.layout.presentation?.mode ?? null,
13740
14292
  node: context.node,
13741
14293
  setNodeRuntimeState(state) {
@@ -13873,7 +14425,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
13873
14425
  input.renderTopChrome,
13874
14426
  input.workspaceId
13875
14427
  ]);
13876
- const captureNodePreviewImage = useCallback13(
14428
+ const captureNodeDefinitionPreviewImage = useCallback13(
13877
14429
  async (node) => {
13878
14430
  const definition = input.nodeDefinitionByType.get(node.data.typeId);
13879
14431
  const minimizedDock = definition?.window?.minimizedDock;
@@ -13884,7 +14436,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
13884
14436
  node,
13885
14437
  workspaceId: input.workspaceId
13886
14438
  });
13887
- const nodePreview = await Promise.resolve(
14439
+ return Promise.resolve(
13888
14440
  capturePreview?.({
13889
14441
  externalNodeState: externalState.externalNodeState,
13890
14442
  externalWorkspaceState: externalState.externalWorkspaceState,
@@ -13893,19 +14445,49 @@ function useWorkbenchHostSurfaceRenderers(input) {
13893
14445
  isMinimized: node.isMinimized,
13894
14446
  node
13895
14447
  }) ?? null
13896
- ).catch(() => null) ?? await Promise.resolve(
13897
- input.captureNodePreviewImage?.(node) ?? null
13898
14448
  ).catch(() => null);
13899
- return nodePreview;
13900
14449
  },
13901
14450
  [
13902
- input.captureNodePreviewImage,
13903
14451
  input.externalStateSource,
13904
14452
  input.hostSession,
13905
14453
  input.nodeDefinitionByType,
13906
14454
  input.workspaceId
13907
14455
  ]
13908
14456
  );
14457
+ const captureNodePreviewImage = useCallback13(
14458
+ async (node) => await captureNodeDefinitionPreviewImage(node) ?? await Promise.resolve(
14459
+ input.captureNodePreviewImage?.(node) ?? null
14460
+ ).catch(() => null),
14461
+ [captureNodeDefinitionPreviewImage, input.captureNodePreviewImage]
14462
+ );
14463
+ const captureNodePreviewImages = useCallback13(
14464
+ async (node) => {
14465
+ const [images, definitionDockPreviewImageUrl] = await Promise.all([
14466
+ Promise.resolve(input.captureNodePreviewImages?.(node) ?? null).catch(
14467
+ () => null
14468
+ ),
14469
+ captureNodeDefinitionPreviewImage(node)
14470
+ ]);
14471
+ if (!images) {
14472
+ const legacyPreviewImageUrl = definitionDockPreviewImageUrl ?? await Promise.resolve(
14473
+ input.captureNodePreviewImage?.(node) ?? null
14474
+ ).catch(() => null);
14475
+ return legacyPreviewImageUrl ? {
14476
+ dockPreviewImageUrl: legacyPreviewImageUrl,
14477
+ genieImageUrl: legacyPreviewImageUrl
14478
+ } : null;
14479
+ }
14480
+ return {
14481
+ ...images,
14482
+ dockPreviewImageUrl: definitionDockPreviewImageUrl ?? images.dockPreviewImageUrl
14483
+ };
14484
+ },
14485
+ [
14486
+ captureNodeDefinitionPreviewImage,
14487
+ input.captureNodePreviewImage,
14488
+ input.captureNodePreviewImages
14489
+ ]
14490
+ );
13909
14491
  const renderDock = useCallback13(
13910
14492
  (context) => /* @__PURE__ */ jsx14(
13911
14493
  WorkbenchHostSurfaceRenderErrorBoundary,
@@ -14131,6 +14713,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
14131
14713
  );
14132
14714
  return {
14133
14715
  captureNodePreviewImage,
14716
+ captureNodePreviewImages,
14134
14717
  renderBottomChrome,
14135
14718
  renderDock,
14136
14719
  renderNode,
@@ -14150,6 +14733,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
14150
14733
  };
14151
14734
  }
14152
14735
  function WorkbenchHostNodeRenderer(input) {
14736
+ const isVisible = useWorkbenchWindowPresentationVisibility();
14153
14737
  const externalState = useWorkbenchHostExternalState({
14154
14738
  externalStateSource: input.externalStateSource,
14155
14739
  node: input.context.node,
@@ -14161,6 +14745,7 @@ function WorkbenchHostNodeRenderer(input) {
14161
14745
  externalState,
14162
14746
  externalStateSource: input.externalStateSource,
14163
14747
  host: input.host,
14748
+ isVisible,
14164
14749
  workspaceId: input.workspaceId
14165
14750
  });
14166
14751
  const resetKey = useMemo10(
@@ -14428,6 +15013,7 @@ var noop3 = () => {
14428
15013
  };
14429
15014
  function WorkbenchHost({
14430
15015
  captureNodePreviewImage,
15016
+ captureNodePreviewImages,
14431
15017
  className,
14432
15018
  contributions,
14433
15019
  debugDiagnostics,
@@ -14512,6 +15098,7 @@ function WorkbenchHost({
14512
15098
  });
14513
15099
  const surfaceRenderers = useWorkbenchHostSurfaceRenderers({
14514
15100
  captureNodePreviewImage,
15101
+ captureNodePreviewImages,
14515
15102
  chromeContext,
14516
15103
  debugDiagnostics,
14517
15104
  dockPreviewCache,
@@ -14543,6 +15130,7 @@ function WorkbenchHost({
14543
15130
  {
14544
15131
  className,
14545
15132
  captureNodePreviewImage: surfaceRenderers.captureNodePreviewImage,
15133
+ captureNodePreviewImages: surfaceRenderers.captureNodePreviewImages,
14546
15134
  controller: hostSession.controller,
14547
15135
  debugDiagnostics,
14548
15136
  dockPreviewCache,