@tutti-os/workbench-surface 0.0.17 → 0.0.19

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
@@ -5,7 +5,7 @@ import {
5
5
  workbenchHostI18nResources,
6
6
  workbenchWindowChromeI18nNamespace,
7
7
  workbenchWindowChromeI18nResources
8
- } from "./chunk-64XVF4I6.js";
8
+ } from "./chunk-B5MSA47V.js";
9
9
 
10
10
  // src/store/commands.ts
11
11
  function createWorkbenchCommands(store) {
@@ -1417,6 +1417,7 @@ var inactiveMissionControlSnapshotSource = {
1417
1417
  function useWorkbenchMissionControlState({
1418
1418
  adapter,
1419
1419
  mode,
1420
+ nodeIds,
1420
1421
  onRequestClose
1421
1422
  }) {
1422
1423
  const isActive = mode !== null && adapter !== null;
@@ -1432,7 +1433,17 @@ function useWorkbenchMissionControlState({
1432
1433
  );
1433
1434
  const layoutConstraints = snapshot?.layoutConstraints ?? defaultWorkbenchLayoutConstraints;
1434
1435
  const surfaceSize = snapshot?.surfaceSize ?? defaultWorkbenchSurfaceSize;
1435
- const visibleNodes = snapshot?.visibleNodes ?? [];
1436
+ const scopedNodeIdSet = useMemo(
1437
+ () => nodeIds === void 0 ? null : new Set(nodeIds),
1438
+ [nodeIds]
1439
+ );
1440
+ const visibleNodes = useMemo(() => {
1441
+ const nextVisibleNodes = snapshot?.visibleNodes ?? [];
1442
+ if (scopedNodeIdSet === null) {
1443
+ return nextVisibleNodes;
1444
+ }
1445
+ return nextVisibleNodes.filter((node) => scopedNodeIdSet.has(node.id));
1446
+ }, [scopedNodeIdSet, snapshot?.visibleNodes]);
1436
1447
  const [selectedNodeIds, setSelectedNodeIds] = useState3([]);
1437
1448
  useEffect(() => {
1438
1449
  if (mode === null) {
@@ -1511,13 +1522,13 @@ function useWorkbenchMissionControlState({
1511
1522
  [adapter, onRequestClose]
1512
1523
  );
1513
1524
  const applyLayoutAndClose = useCallback2(
1514
- (nodeIds, nextPreset) => {
1515
- if (!adapter || nodeIds.length < 2) {
1525
+ (nodeIds2, nextPreset) => {
1526
+ if (!adapter || nodeIds2.length < 2) {
1516
1527
  return;
1517
1528
  }
1518
1529
  onRequestClose();
1519
1530
  window.requestAnimationFrame(() => {
1520
- adapter.applyLayoutPreset(nodeIds, nextPreset);
1531
+ adapter.applyLayoutPreset(nodeIds2, nextPreset);
1521
1532
  });
1522
1533
  },
1523
1534
  [adapter, onRequestClose]
@@ -1561,9 +1572,17 @@ function useWorkbenchMissionControlState({
1561
1572
  onNodePress: onPreviewPress,
1562
1573
  selectedNodeIds: selectedNodeIdSet
1563
1574
  },
1564
- mode: "mission-control"
1575
+ mode: "mission-control",
1576
+ visibleNodeIds: new Set(orderedNodes.map((node) => node.id))
1565
1577
  },
1566
- [mode, onPreviewPress, onRequestClose, previewItems, selectedNodeIdSet]
1578
+ [
1579
+ mode,
1580
+ onPreviewPress,
1581
+ onRequestClose,
1582
+ orderedNodes,
1583
+ previewItems,
1584
+ selectedNodeIdSet
1585
+ ]
1567
1586
  );
1568
1587
  useEffect(() => {
1569
1588
  if (mode === null) {
@@ -1708,9 +1727,16 @@ function WorkbenchDockFrame({
1708
1727
  const nodes = useWorkbenchSelector(
1709
1728
  selectDockNodes
1710
1729
  );
1730
+ const minimizedNodesWithPending = useMemo2(
1731
+ () => mergePendingMinimizedDockNode(
1732
+ nodes.filter((node) => node.isMinimized),
1733
+ genie.pendingMinimizedNode
1734
+ ),
1735
+ [genie.pendingMinimizedNode, nodes]
1736
+ );
1711
1737
  const minimizedNodes = useMemo2(
1712
- () => nodes.filter((node) => node.isMinimized),
1713
- [nodes]
1738
+ () => minimizedNodesWithPending.filter((node) => node.isMinimized),
1739
+ [minimizedNodesWithPending]
1714
1740
  );
1715
1741
  const focusedNodeId = useWorkbenchSelector(
1716
1742
  (state) => selectFocusedWorkbenchNode(state)?.id ?? null
@@ -1752,6 +1778,9 @@ function WorkbenchDockFrame({
1752
1778
  },
1753
1779
  shouldAnimateMinimizedDockEnter: (nodeID) => {
1754
1780
  return genie.shouldAnimateMinimizedDockEnter(nodeID);
1781
+ },
1782
+ isPendingMinimizedDockNode: (nodeID) => {
1783
+ return genie.isPendingMinimizedDockNode(nodeID);
1755
1784
  }
1756
1785
  },
1757
1786
  minimizedNodes,
@@ -1761,6 +1790,16 @@ function WorkbenchDockFrame({
1761
1790
  )
1762
1791
  ] });
1763
1792
  }
1793
+ function mergePendingMinimizedDockNode(nodes, pendingNode) {
1794
+ if (!pendingNode) {
1795
+ return nodes;
1796
+ }
1797
+ const existingNode = nodes.find((node) => node.id === pendingNode.id);
1798
+ if (existingNode?.isMinimized) {
1799
+ return nodes;
1800
+ }
1801
+ return [...nodes.filter((node) => node.id !== pendingNode.id), pendingNode];
1802
+ }
1764
1803
 
1765
1804
  // src/react/WorkbenchNodeLayer.tsx
1766
1805
  import { memo, useMemo as useMemo3 } from "react";
@@ -2086,7 +2125,8 @@ function WorkbenchWindowFrame({
2086
2125
  const resolvedFullscreenHeaderMode = fullscreenHeaderMode ?? "reveal";
2087
2126
  const presentationMode = presentation?.mode ?? null;
2088
2127
  const presentationFrame = presentation?.frameByNodeId.get(node.id) ?? null;
2089
- const presentationInteraction = interactive && presentationMode === "mission-control" ? presentation?.interaction ?? null : null;
2128
+ const isPresentationHidden = presentationMode === "mission-control" && !presentation?.visibleNodeIds.has(node.id);
2129
+ const presentationInteraction = interactive && presentationMode === "mission-control" && !isPresentationHidden ? presentation?.interaction ?? null : null;
2090
2130
  const isMissionControlSelected = presentationInteraction?.selectedNodeIds.has(node.id) ?? false;
2091
2131
  const presentationScale = presentationMode === "mission-control" && presentationFrame ? Math.min(
2092
2132
  presentationFrame.width / Math.max(1, node.frame.width),
@@ -2100,11 +2140,11 @@ function WorkbenchWindowFrame({
2100
2140
  0,
2101
2141
  (presentationFrame.height - node.frame.height * presentationScale) / 2
2102
2142
  ) - node.frame.y : 0;
2103
- const shellTransform = presentationMode === "mission-control" ? `matrix(${presentationScale}, 0, 0, ${presentationScale}, ${presentationOffsetX}, ${presentationOffsetY})` : void 0;
2143
+ const shellTransform = presentationMode === "mission-control" && presentationFrame ? `matrix(${presentationScale}, 0, 0, ${presentationScale}, ${presentationOffsetX}, ${presentationOffsetY})` : void 0;
2104
2144
  return /* @__PURE__ */ jsxs2(
2105
2145
  "section",
2106
2146
  {
2107
- "aria-hidden": hiddenMounted ? true : void 0,
2147
+ "aria-hidden": hiddenMounted || isPresentationHidden ? true : void 0,
2108
2148
  className: "workbench-window-shell",
2109
2149
  "data-focused": isFocused ? "true" : "false",
2110
2150
  "data-display-mode": node.displayMode,
@@ -2112,6 +2152,7 @@ function WorkbenchWindowFrame({
2112
2152
  "data-launch-source": resolveWorkbenchNodeLaunchSource(node.data),
2113
2153
  "data-minimized-mount": hiddenMounted ? "hidden" : "visible",
2114
2154
  "data-presentation-mode": presentationMode ?? "default",
2155
+ "data-presentation-visibility": isPresentationHidden ? "hidden" : "visible",
2115
2156
  "data-slot": "viewport-menu-boundary",
2116
2157
  "data-workbench-node-type-id": resolveWorkbenchNodeTypeId(node.data),
2117
2158
  "data-workbench-window-id": node.id,
@@ -2126,7 +2167,7 @@ function WorkbenchWindowFrame({
2126
2167
  width: node.frame.width,
2127
2168
  zIndex
2128
2169
  },
2129
- onPointerDown: hiddenMounted || !interactive || presentationMode === "mission-control" ? void 0 : () => controller.commands.focusNode(node.id),
2170
+ onPointerDown: hiddenMounted || isPresentationHidden || !interactive || presentationMode === "mission-control" ? void 0 : () => controller.commands.focusNode(node.id),
2130
2171
  children: [
2131
2172
  /* @__PURE__ */ jsx5("div", { className: "workbench-window-shell__content", children: /* @__PURE__ */ jsxs2(
2132
2173
  "div",
@@ -2505,41 +2546,143 @@ function resolveGenieDirtyRect({
2505
2546
  width: Math.max(0, right - left)
2506
2547
  };
2507
2548
  }
2549
+ function resolveGenieRowProgress({
2550
+ direction,
2551
+ progress,
2552
+ rowProgress,
2553
+ stagger
2554
+ }) {
2555
+ const start = direction === "minimize" ? (1 - rowProgress) * stagger : rowProgress * stagger;
2556
+ return clampGenieProgress((progress - start) / (1 - start));
2557
+ }
2558
+ function resolveGenieRowTargetY({
2559
+ direction,
2560
+ dockPoint,
2561
+ progress,
2562
+ sourceY,
2563
+ textureHeight,
2564
+ textureRect
2565
+ }) {
2566
+ const rowProgress = clampGenieProgress(sourceY / textureHeight);
2567
+ const verticalProgress = resolveGenieRowProgress({
2568
+ direction,
2569
+ progress,
2570
+ rowProgress,
2571
+ stagger: genieVerticalRowStagger
2572
+ });
2573
+ const verticalEase = easeInQuadratic(verticalProgress);
2574
+ return direction === "minimize" ? lerpGenieValue(textureRect.top + sourceY, dockPoint.y, verticalEase) : lerpGenieValue(dockPoint.y, textureRect.top + sourceY, verticalEase);
2575
+ }
2508
2576
  function canInlineComputedStyle(element) {
2509
2577
  return element instanceof HTMLElement || typeof SVGElement !== "undefined" && element instanceof SVGElement;
2510
2578
  }
2511
- function copyGenieComputedStyleTree(source, clone) {
2512
- if (canInlineComputedStyle(source) && canInlineComputedStyle(clone)) {
2513
- const computed = window.getComputedStyle(source);
2514
- if (computed.cssText) {
2515
- clone.style.cssText = computed.cssText;
2516
- } else {
2517
- for (let index = 0; index < computed.length; index += 1) {
2518
- const propertyName = computed.item(index);
2519
- if (!propertyName) {
2520
- continue;
2521
- }
2522
- clone.style.setProperty(
2523
- propertyName,
2524
- computed.getPropertyValue(propertyName),
2525
- computed.getPropertyPriority(propertyName)
2526
- );
2527
- }
2579
+ function isHiddenForGenieTexture(computed) {
2580
+ return computed.display === "none" || computed.visibility === "hidden" || computed.visibility === "collapse" || Number.parseFloat(computed.opacity || "1") <= 0;
2581
+ }
2582
+ function rectsIntersect(a, b) {
2583
+ return a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
2584
+ }
2585
+ function resolveElementSkipReasonForGenieTexture(element, rootRect, computed) {
2586
+ if (isHiddenForGenieTexture(computed)) {
2587
+ return "hidden";
2588
+ }
2589
+ const tagName = element.tagName.toLowerCase();
2590
+ if (tagName === "script" || tagName === "style" || tagName === "template") {
2591
+ return "unsupported";
2592
+ }
2593
+ const rect = element.getBoundingClientRect();
2594
+ if (rect.width <= 0 || rect.height <= 0) {
2595
+ return "zero-size";
2596
+ }
2597
+ return rectsIntersect(
2598
+ {
2599
+ height: rect.height,
2600
+ left: rect.left,
2601
+ top: rect.top,
2602
+ width: rect.width
2603
+ },
2604
+ rootRect
2605
+ ) ? null : "offscreen";
2606
+ }
2607
+ function hasMeaningfulTextNode(node) {
2608
+ return node.textContent?.trim() ? true : false;
2609
+ }
2610
+ function cloneMeaningfulGenieNode(source, rootRect, images, forceInclude = false) {
2611
+ if (source.nodeType === Node.TEXT_NODE) {
2612
+ if (!hasMeaningfulTextNode(source)) {
2613
+ return null;
2614
+ }
2615
+ return source.cloneNode(false);
2616
+ }
2617
+ if (source.nodeType !== Node.ELEMENT_NODE) {
2618
+ return null;
2619
+ }
2620
+ const sourceElement = source;
2621
+ if (!canInlineComputedStyle(sourceElement)) {
2622
+ return null;
2623
+ }
2624
+ const computed = window.getComputedStyle(sourceElement);
2625
+ const skipReason = resolveElementSkipReasonForGenieTexture(
2626
+ sourceElement,
2627
+ rootRect,
2628
+ computed
2629
+ );
2630
+ const includeSelf = forceInclude || skipReason === null;
2631
+ if (!includeSelf && skipReason === "hidden") {
2632
+ return null;
2633
+ }
2634
+ const clone = sourceElement.cloneNode(false);
2635
+ copyGenieComputedStyle(sourceElement, clone, computed);
2636
+ for (const child of Array.from(source.childNodes)) {
2637
+ const childClone = cloneMeaningfulGenieNode(child, rootRect, images);
2638
+ if (childClone) {
2639
+ clone.appendChild(childClone);
2528
2640
  }
2529
- clone.style.animation = "none";
2530
- clone.style.opacity = "1";
2531
- clone.style.transition = "none";
2532
- clone.style.visibility = "visible";
2533
2641
  }
2534
- const sourceChildren = Array.from(source.children);
2535
- const cloneChildren = Array.from(clone.children);
2536
- for (let index = 0; index < sourceChildren.length; index += 1) {
2537
- const sourceChild = sourceChildren[index];
2538
- const cloneChild = cloneChildren[index];
2539
- if (sourceChild && cloneChild) {
2540
- copyGenieComputedStyleTree(sourceChild, cloneChild);
2642
+ if (!includeSelf && clone.childNodes.length === 0) {
2643
+ return null;
2644
+ }
2645
+ if (sourceElement instanceof HTMLImageElement) {
2646
+ const rect = sourceElement.getBoundingClientRect();
2647
+ images.push({
2648
+ displayHeight: rect.height,
2649
+ displayWidth: rect.width,
2650
+ url: sourceElement.currentSrc || sourceElement.src || sourceElement.getAttribute("src") || null
2651
+ });
2652
+ }
2653
+ return clone;
2654
+ }
2655
+ function copyGenieComputedStyle(source, clone, computed = window.getComputedStyle(source)) {
2656
+ if (!canInlineComputedStyle(source) || !canInlineComputedStyle(clone)) {
2657
+ return;
2658
+ }
2659
+ if (computed.cssText) {
2660
+ clone.style.cssText = computed.cssText;
2661
+ } else {
2662
+ for (let index = 0; index < computed.length; index += 1) {
2663
+ const propertyName = computed.item(index);
2664
+ if (!propertyName) {
2665
+ continue;
2666
+ }
2667
+ clone.style.setProperty(
2668
+ propertyName,
2669
+ computed.getPropertyValue(propertyName),
2670
+ computed.getPropertyPriority(propertyName)
2671
+ );
2541
2672
  }
2542
2673
  }
2674
+ clone.style.animation = "none";
2675
+ clone.style.opacity = "1";
2676
+ clone.style.transition = "none";
2677
+ clone.style.visibility = "visible";
2678
+ }
2679
+ function cloneMeaningfulGenieElement(source, rootRect) {
2680
+ const images = [];
2681
+ const clone = cloneMeaningfulGenieNode(source, rootRect, images, true);
2682
+ if (!(clone instanceof HTMLElement)) {
2683
+ return null;
2684
+ }
2685
+ return { clone, images };
2543
2686
  }
2544
2687
  function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2545
2688
  const { direction, dockPoint, texture, textureRect } = frame;
@@ -2563,16 +2706,13 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2563
2706
  const sourceHeight = Math.min(scanlineStride, textureHeight - y);
2564
2707
  const sourceMidY = y + sourceHeight / 2;
2565
2708
  const rowProgress = sourceMidY / textureHeight;
2566
- const horizontalStart = direction === "minimize" ? (1 - rowProgress) * genieHorizontalRowStagger : rowProgress * genieHorizontalRowStagger;
2567
- const horizontalProgress = clampGenieProgress(
2568
- (progress - horizontalStart) / (1 - horizontalStart)
2569
- );
2709
+ const horizontalProgress = resolveGenieRowProgress({
2710
+ direction,
2711
+ progress,
2712
+ rowProgress,
2713
+ stagger: genieHorizontalRowStagger
2714
+ });
2570
2715
  const horizontalEase = easeInOutCubic(horizontalProgress);
2571
- const verticalStart = direction === "minimize" ? (1 - rowProgress) * genieVerticalRowStagger : rowProgress * genieVerticalRowStagger;
2572
- const verticalProgress = clampGenieProgress(
2573
- (progress - verticalStart) / (1 - verticalStart)
2574
- );
2575
- const verticalEase = easeInQuadratic(verticalProgress);
2576
2716
  const left = direction === "minimize" ? lerpGenieValue(textureRect.left, dockPoint.x, horizontalEase) : lerpGenieValue(dockPoint.x, textureRect.left, horizontalEase);
2577
2717
  const right = direction === "minimize" ? lerpGenieValue(
2578
2718
  textureRect.left + textureWidth,
@@ -2583,15 +2723,24 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2583
2723
  textureRect.left + textureWidth,
2584
2724
  horizontalEase
2585
2725
  );
2586
- const targetY = (direction === "minimize" ? lerpGenieValue(
2587
- textureRect.top + sourceMidY,
2588
- dockPoint.y,
2589
- verticalEase
2590
- ) : lerpGenieValue(
2591
- dockPoint.y,
2592
- textureRect.top + sourceMidY,
2593
- verticalEase
2594
- )) - sourceHeight / 2;
2726
+ const targetTop = resolveGenieRowTargetY({
2727
+ direction,
2728
+ dockPoint,
2729
+ progress,
2730
+ sourceY: y,
2731
+ textureHeight,
2732
+ textureRect
2733
+ });
2734
+ const targetBottom = resolveGenieRowTargetY({
2735
+ direction,
2736
+ dockPoint,
2737
+ progress,
2738
+ sourceY: y + sourceHeight,
2739
+ textureHeight,
2740
+ textureRect
2741
+ });
2742
+ const targetY = Math.min(targetTop, targetBottom) - 0.5;
2743
+ const targetHeight = Math.max(1, Math.abs(targetBottom - targetTop) + 1);
2595
2744
  const rowWidth = right - left;
2596
2745
  if (rowWidth < 0.8) {
2597
2746
  continue;
@@ -2605,7 +2754,7 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2605
2754
  left,
2606
2755
  targetY,
2607
2756
  rowWidth,
2608
- Math.max(1, sourceHeight)
2757
+ targetHeight
2609
2758
  );
2610
2759
  }
2611
2760
  const glowProgress = direction === "minimize" ? progress : 1 - progress;
@@ -2635,6 +2784,7 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2635
2784
  // src/react/useWorkbenchGenieAnimation.tsx
2636
2785
  import { jsx as jsx7 } from "react/jsx-runtime";
2637
2786
  var genieDurationMs = 400;
2787
+ var scaleMinimizeDurationMs = 220;
2638
2788
  var genieMaxDevicePixelRatio = 2;
2639
2789
  var genieSnapshotScale = 1;
2640
2790
  var minimizedDockSlotEnterAnimationMs = 720;
@@ -2643,6 +2793,7 @@ var dockPreviewMaxHeight = 170;
2643
2793
  var dockPreviewImageCacheMaxEntries = 96;
2644
2794
  var inlineImageResourceCacheMaxEntries = 160;
2645
2795
  var dockAnchorFallbackSizePx = 43.2;
2796
+ var genieInlineImageMaxDevicePixelRatio = 2;
2646
2797
  var dockPreviewImageByNodeID = /* @__PURE__ */ new Map();
2647
2798
  var inlineImageResourceByUrl = /* @__PURE__ */ new Map();
2648
2799
  function resolveWorkbenchCaptureElement(windowElement) {
@@ -2683,9 +2834,6 @@ function blobToDataUrl(blob) {
2683
2834
  reader.readAsDataURL(blob);
2684
2835
  });
2685
2836
  }
2686
- function resolveImageResourceUrl(image) {
2687
- return image.currentSrc || image.src || image.getAttribute("src") || null;
2688
- }
2689
2837
  async function fetchInlineImageResource(imageUrl) {
2690
2838
  try {
2691
2839
  const response = await fetch(imageUrl);
@@ -2721,21 +2869,31 @@ function readInlineImageResource(imageUrl) {
2721
2869
  }
2722
2870
  async function inlineCloneImageResources({
2723
2871
  cloneRoot,
2724
- imageUrls
2872
+ images
2725
2873
  }) {
2726
2874
  const cloneImages = Array.from(cloneRoot.querySelectorAll("img"));
2727
2875
  await Promise.all(
2728
- imageUrls.map(async (imageUrl, index) => {
2876
+ images.map(async (imageInfo, index) => {
2729
2877
  const cloneImage = cloneImages[index];
2730
2878
  if (!cloneImage) {
2731
2879
  return;
2732
2880
  }
2733
2881
  cloneImage.removeAttribute("srcset");
2734
2882
  cloneImage.removeAttribute("sizes");
2883
+ const imageUrl = imageInfo.url;
2735
2884
  if (!imageUrl) {
2736
2885
  return;
2737
2886
  }
2738
- cloneImage.src = await readInlineImageResource(imageUrl) ?? imageUrl;
2887
+ const inlineImageUrl = await readInlineImageResource(imageUrl) ?? imageUrl;
2888
+ const resizedImageUrl = await resizeInlineImageResourceForGenieTexture(
2889
+ inlineImageUrl,
2890
+ imageInfo
2891
+ );
2892
+ if (resizedImageUrl && resizedImageUrl !== inlineImageUrl) {
2893
+ cloneImage.src = resizedImageUrl;
2894
+ return;
2895
+ }
2896
+ cloneImage.src = inlineImageUrl;
2739
2897
  })
2740
2898
  );
2741
2899
  }
@@ -2745,16 +2903,61 @@ async function loadImageFromSvg(svg) {
2745
2903
  await image.decode();
2746
2904
  return image;
2747
2905
  }
2906
+ function resolveGenieInlineImageTargetSize({
2907
+ displayHeight,
2908
+ displayWidth
2909
+ }) {
2910
+ if (displayWidth <= 0 || displayHeight <= 0) {
2911
+ return null;
2912
+ }
2913
+ const scale = Math.min(
2914
+ window.devicePixelRatio || 1,
2915
+ genieInlineImageMaxDevicePixelRatio
2916
+ );
2917
+ return {
2918
+ height: Math.max(1, Math.ceil(displayHeight * scale)),
2919
+ width: Math.max(1, Math.ceil(displayWidth * scale))
2920
+ };
2921
+ }
2922
+ async function resizeInlineImageResourceForGenieTexture(imageUrl, imageInfo) {
2923
+ const targetSize = resolveGenieInlineImageTargetSize(imageInfo);
2924
+ if (!targetSize) {
2925
+ return null;
2926
+ }
2927
+ const image = new Image();
2928
+ image.src = imageUrl;
2929
+ try {
2930
+ await image.decode();
2931
+ } catch {
2932
+ return null;
2933
+ }
2934
+ if (image.naturalWidth <= targetSize.width && image.naturalHeight <= targetSize.height) {
2935
+ return null;
2936
+ }
2937
+ const canvas = document.createElement("canvas");
2938
+ canvas.width = targetSize.width;
2939
+ canvas.height = targetSize.height;
2940
+ const context = canvas.getContext("2d");
2941
+ if (!context) {
2942
+ return null;
2943
+ }
2944
+ try {
2945
+ context.drawImage(image, 0, 0, targetSize.width, targetSize.height);
2946
+ return canvas.toDataURL("image/png");
2947
+ } catch {
2948
+ return null;
2949
+ }
2950
+ }
2748
2951
  function prepareElementTextureCapture(element) {
2749
2952
  const windowRect = viewportRectFromElement(element);
2750
2953
  if (!isUsableGenieRect(windowRect)) {
2751
2954
  return null;
2752
2955
  }
2753
- const imageUrls = Array.from(element.querySelectorAll("img")).map(
2754
- resolveImageResourceUrl
2755
- );
2756
- const clone = element.cloneNode(true);
2757
- copyGenieComputedStyleTree(element, clone);
2956
+ const clonedElement = cloneMeaningfulGenieElement(element, windowRect);
2957
+ if (!clonedElement) {
2958
+ return null;
2959
+ }
2960
+ const { clone, images } = clonedElement;
2758
2961
  clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2759
2962
  clone.style.position = "relative";
2760
2963
  clone.style.left = "0";
@@ -2765,15 +2968,23 @@ function prepareElementTextureCapture(element) {
2765
2968
  clone.style.opacity = "1";
2766
2969
  clone.style.visibility = "visible";
2767
2970
  clone.style.pointerEvents = "none";
2768
- return { clone, imageUrls, rect: windowRect };
2971
+ return {
2972
+ clone,
2973
+ images,
2974
+ rect: windowRect
2975
+ };
2769
2976
  }
2770
2977
  async function renderPreparedElementTexture({
2771
2978
  clone,
2772
- imageUrls,
2979
+ images,
2773
2980
  rect
2774
2981
  }) {
2775
- await inlineCloneImageResources({ cloneRoot: clone, imageUrls });
2776
- const image = await loadImageFromSvg(createGenieSvgTexture(clone, rect));
2982
+ await inlineCloneImageResources({
2983
+ cloneRoot: clone,
2984
+ images
2985
+ });
2986
+ const svgTexture = createGenieSvgTexture(clone, rect);
2987
+ const image = await loadImageFromSvg(svgTexture);
2777
2988
  const canvas = document.createElement("canvas");
2778
2989
  canvas.width = Math.max(1, Math.round(rect.width * genieSnapshotScale));
2779
2990
  canvas.height = Math.max(1, Math.round(rect.height * genieSnapshotScale));
@@ -2864,20 +3075,16 @@ async function captureWorkbenchNodePreviewImage(nodeID, options = {}) {
2864
3075
  writeCachedWorkbenchNodePreviewImage(nodeID, previewImageUrl);
2865
3076
  return previewImageUrl;
2866
3077
  }
2867
- async function captureWorkbenchNodePreviewImageForNode(node, input = {}) {
3078
+ async function captureProvidedWorkbenchNodePreviewImageForNode(node, input = {}) {
2868
3079
  const previewImageUrl = await Promise.resolve(
2869
3080
  input.captureNodePreviewImage?.(node) ?? null
2870
3081
  ).catch(() => null);
2871
- if (previewImageUrl) {
2872
- writeCachedWorkbenchNodePreviewImage(node.id, previewImageUrl);
2873
- persistWorkbenchNodePreviewImage(node, previewImageUrl, input);
2874
- return previewImageUrl;
3082
+ if (!previewImageUrl) {
3083
+ return null;
2875
3084
  }
2876
- const capturedPreviewImageUrl = await captureWorkbenchNodePreviewImage(
2877
- node.id
2878
- );
2879
- persistWorkbenchNodePreviewImage(node, capturedPreviewImageUrl, input);
2880
- return capturedPreviewImageUrl;
3085
+ writeCachedWorkbenchNodePreviewImage(node.id, previewImageUrl);
3086
+ persistWorkbenchNodePreviewImage(node, previewImageUrl, input);
3087
+ return previewImageUrl;
2881
3088
  }
2882
3089
  function persistWorkbenchNodePreviewImage(node, previewImageUrl, input) {
2883
3090
  if (!previewImageUrl || !input.dockPreviewCache) {
@@ -2917,8 +3124,10 @@ function useWorkbenchGenieAnimation({
2917
3124
  captureNodePreviewImage,
2918
3125
  controller,
2919
3126
  dockPreviewCache,
3127
+ minimizeAnimation = "genie",
2920
3128
  resolveDockAnchorKey,
2921
- resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
3129
+ resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
3130
+ shouldCaptureNodePreviewImage
2922
3131
  }) {
2923
3132
  const dockAnchorElementsRef = useRef4(/* @__PURE__ */ new Map());
2924
3133
  const canvasRef = useRef4(null);
@@ -2933,6 +3142,7 @@ function useWorkbenchGenieAnimation({
2933
3142
  const [genieHiddenNodeIDs, setGenieHiddenNodeIDs] = useState4(
2934
3143
  () => /* @__PURE__ */ new Set()
2935
3144
  );
3145
+ const [pendingMinimizedNode, setPendingMinimizedNode] = useState4(null);
2936
3146
  const registerDockAnchor = useCallback6(
2937
3147
  (anchorKey, element) => {
2938
3148
  if (element) {
@@ -3008,6 +3218,11 @@ function useWorkbenchGenieAnimation({
3008
3218
  return next;
3009
3219
  });
3010
3220
  }, []);
3221
+ const clearPendingMinimizedNode = useCallback6((nodeID) => {
3222
+ setPendingMinimizedNode(
3223
+ (current) => current?.id === nodeID ? null : current
3224
+ );
3225
+ }, []);
3011
3226
  const setupCanvas = useCallback6(() => {
3012
3227
  const canvas = canvasRef.current;
3013
3228
  if (!canvas) {
@@ -3120,10 +3335,102 @@ function useWorkbenchGenieAnimation({
3120
3335
  },
3121
3336
  [setupCanvas, stopAnimation]
3122
3337
  );
3338
+ const runScaleWindowAnimation = useCallback6(
3339
+ ({
3340
+ direction,
3341
+ dockRect,
3342
+ nodeElement,
3343
+ onCancel,
3344
+ onComplete,
3345
+ skipStop
3346
+ }) => {
3347
+ if (!skipStop) {
3348
+ stopAnimation();
3349
+ }
3350
+ const windowRect = viewportRectFromElement(nodeElement);
3351
+ if (!isUsableGenieRect(windowRect) || !isUsableGenieRect(dockRect)) {
3352
+ onComplete();
3353
+ return;
3354
+ }
3355
+ const generation = animationGenerationRef.current;
3356
+ const fromCenter = centerPointFromRect(windowRect);
3357
+ const toCenter = centerPointFromRect(dockRect);
3358
+ const targetScale = Math.max(
3359
+ 0.04,
3360
+ Math.min(
3361
+ 0.32,
3362
+ dockRect.width / Math.max(1, windowRect.width),
3363
+ dockRect.height / Math.max(1, windowRect.height)
3364
+ )
3365
+ );
3366
+ const previousPointerEvents = nodeElement.style.pointerEvents;
3367
+ const previousTransformOrigin = nodeElement.style.transformOrigin;
3368
+ const previousVisibility = nodeElement.style.visibility;
3369
+ const previousZIndex = nodeElement.style.zIndex;
3370
+ nodeElement.style.pointerEvents = "none";
3371
+ nodeElement.style.transformOrigin = "center center";
3372
+ nodeElement.style.visibility = "visible";
3373
+ nodeElement.style.zIndex = "var(--z-workbench-genie)";
3374
+ const dockTransform = `translate3d(${toCenter.x - fromCenter.x}px, ${toCenter.y - fromCenter.y}px, 0) scale(${targetScale})`;
3375
+ const windowTransform = "translate3d(0, 0, 0) scale(1)";
3376
+ const keyframes = direction === "open" ? [
3377
+ {
3378
+ opacity: 0,
3379
+ transform: dockTransform
3380
+ },
3381
+ {
3382
+ opacity: 1,
3383
+ transform: windowTransform
3384
+ }
3385
+ ] : [
3386
+ {
3387
+ opacity: 1,
3388
+ transform: windowTransform
3389
+ },
3390
+ {
3391
+ opacity: 0,
3392
+ transform: dockTransform
3393
+ }
3394
+ ];
3395
+ const animation = nodeElement.animate(keyframes, {
3396
+ duration: scaleMinimizeDurationMs,
3397
+ easing: "cubic-bezier(0.4, 0, 0.2, 1)",
3398
+ fill: "forwards"
3399
+ });
3400
+ const restoreElement = () => {
3401
+ animation.cancel();
3402
+ nodeElement.style.pointerEvents = previousPointerEvents;
3403
+ nodeElement.style.transformOrigin = previousTransformOrigin;
3404
+ nodeElement.style.visibility = previousVisibility;
3405
+ nodeElement.style.zIndex = previousZIndex;
3406
+ };
3407
+ animationCleanupRef.current = () => {
3408
+ restoreElement();
3409
+ onCancel();
3410
+ };
3411
+ animation.finished.then(() => {
3412
+ if (generation !== animationGenerationRef.current) {
3413
+ return;
3414
+ }
3415
+ animationCleanupRef.current = null;
3416
+ restoreElement();
3417
+ onComplete();
3418
+ }).catch(() => {
3419
+ if (generation !== animationGenerationRef.current) {
3420
+ return;
3421
+ }
3422
+ animationCleanupRef.current = null;
3423
+ restoreElement();
3424
+ onComplete();
3425
+ });
3426
+ },
3427
+ [stopAnimation]
3428
+ );
3123
3429
  useEffect3(() => () => stopAnimation(false), [stopAnimation]);
3124
3430
  const startOpenOrRestoreAnimation = useCallback6(
3125
3431
  async (nodeID, anchorKey, generation, dockRectFallback) => {
3126
- if (shouldReduceMotion()) {
3432
+ const effectiveMinimizeAnimation = shouldReduceMotion() ? "off" : minimizeAnimation;
3433
+ if (effectiveMinimizeAnimation === "off") {
3127
3434
  animationCleanupRef.current = null;
3128
3435
  showNodeForGenie(nodeID);
3129
3436
  return;
@@ -3135,6 +3442,32 @@ function useWorkbenchGenieAnimation({
3135
3442
  const resolvedDockRect = resolveDockAnchorRect(anchorKey);
3136
3443
  const dockRect = resolvedDockRect && isUsableGenieRect(resolvedDockRect) ? resolvedDockRect : dockRectFallback;
3137
3444
  const nodeElement = resolveNodeElement(nodeID);
3445
+ if (effectiveMinimizeAnimation === "scale") {
3446
+ if (!dockRect || !nodeElement || !isUsableGenieRect(dockRect)) {
3447
+ animationCleanupRef.current = null;
3448
+ showNodeForGenie(nodeID);
3449
+ return;
3450
+ }
3451
+ flushSync(() => {
3452
+ showNodeForGenie(nodeID);
3453
+ });
3454
+ runScaleWindowAnimation({
3455
+ direction: "open",
3456
+ dockRect,
3457
+ nodeElement,
3458
+ onCancel: () => {
3459
+ flushSync(() => {
3460
+ showNodeForGenie(nodeID);
3461
+ });
3462
+ },
3463
+ onComplete: () => {
3464
+ flushSync(() => {
3465
+ showNodeForGenie(nodeID);
3466
+ });
3467
+ }
3468
+ });
3469
+ return;
3470
+ }
3138
3471
  const captureTarget = nodeElement ? resolveWorkbenchCaptureElement(nodeElement) : null;
3139
3472
  if (!dockRect || !nodeElement || !captureTarget || !isUsableGenieRect(dockRect)) {
3140
3473
  animationCleanupRef.current = null;
@@ -3173,9 +3506,11 @@ function useWorkbenchGenieAnimation({
3173
3506
  },
3174
3507
  [
3175
3508
  clearCanvas,
3509
+ minimizeAnimation,
3176
3510
  resolveDockAnchorRect,
3177
3511
  resolveNodeElement,
3178
3512
  runGenieAnimation,
3513
+ runScaleWindowAnimation,
3179
3514
  showNodeForGenie
3180
3515
  ]
3181
3516
  );
@@ -3192,6 +3527,16 @@ function useWorkbenchGenieAnimation({
3192
3527
  });
3193
3528
  return;
3194
3529
  }
3530
+ const effectiveMinimizeAnimation = shouldReduceMotion() ? "off" : minimizeAnimation;
3531
+ if (effectiveMinimizeAnimation === "off") {
3532
+ stopAnimation();
3533
+ flushSync(() => {
3534
+ showNodeForGenie(nodeID);
3535
+ });
3536
+ void Promise.resolve(launch()).catch(() => {
3537
+ });
3538
+ return;
3539
+ }
3195
3540
  stopAnimation();
3196
3541
  const dockRectFallback = resolveDockAnchorRect(anchorKey);
3197
3542
  hideNodeForGenie(nodeID);
@@ -3216,6 +3561,7 @@ function useWorkbenchGenieAnimation({
3216
3561
  clearCanvas,
3217
3562
  controller,
3218
3563
  hideNodeForGenie,
3564
+ minimizeAnimation,
3219
3565
  resolveDockAnchorRect,
3220
3566
  showNodeForGenie,
3221
3567
  startOpenOrRestoreAnimation,
@@ -3230,23 +3576,137 @@ function useWorkbenchGenieAnimation({
3230
3576
  return;
3231
3577
  }
3232
3578
  const runMinimize = minimize ?? (() => controller.commands.minimizeNode(nodeID));
3233
- if (shouldReduceMotion()) {
3234
- const wasFocusedForCapture2 = isFocusedWorkbenchNode(
3235
- controller,
3236
- nodeID
3237
- );
3238
- if (!wasFocusedForCapture2) {
3579
+ const effectiveMinimizeAnimation = shouldReduceMotion() ? "off" : minimizeAnimation;
3580
+ const shouldCapturePreview = shouldCaptureNodePreviewImage?.(target) ?? true;
3581
+ if (effectiveMinimizeAnimation === "off") {
3582
+ stopAnimation();
3583
+ let frameID = null;
3584
+ let timerID = null;
3585
+ let minimizeCommitted2 = false;
3586
+ const commitMinimize2 = () => {
3587
+ if (minimizeCommitted2) {
3588
+ return;
3589
+ }
3590
+ minimizeCommitted2 = true;
3591
+ if (frameID !== null) {
3592
+ window.cancelAnimationFrame(frameID);
3593
+ frameID = null;
3594
+ }
3595
+ if (timerID !== null) {
3596
+ clearTimeout(timerID);
3597
+ timerID = null;
3598
+ }
3599
+ animationCleanupRef.current = null;
3600
+ flushSync(() => {
3601
+ clearPendingMinimizedNode(nodeID);
3602
+ showNodeForGenie(nodeID);
3603
+ runMinimize();
3604
+ });
3605
+ scheduleReleaseMinimizedDockEnterAnimation(nodeID);
3606
+ };
3607
+ animationCleanupRef.current = () => {
3608
+ releaseMinimizedDockEnterAnimation(nodeID);
3609
+ commitMinimize2();
3610
+ };
3611
+ registerMinimizedDockEnterAnimation(nodeID);
3612
+ flushSync(() => {
3613
+ hideNodeForGenie(nodeID);
3614
+ setPendingMinimizedNode({
3615
+ ...target,
3616
+ isMinimized: true,
3617
+ minimizedAtUnixMs: Date.now()
3618
+ });
3619
+ });
3620
+ if (shouldCapturePreview) {
3621
+ void captureProvidedWorkbenchNodePreviewImageForNode(target, {
3622
+ captureNodePreviewImage,
3623
+ dockPreviewCache,
3624
+ resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
3625
+ });
3626
+ }
3627
+ frameID = window.requestAnimationFrame(() => {
3628
+ frameID = null;
3629
+ timerID = setTimeout(commitMinimize2, 0);
3630
+ });
3631
+ return;
3632
+ }
3633
+ if (effectiveMinimizeAnimation === "scale") {
3634
+ stopAnimation();
3635
+ const generation2 = animationGenerationRef.current;
3636
+ const nodeElement2 = resolveNodeElement(nodeID);
3637
+ if (!nodeElement2) {
3638
+ runMinimize();
3639
+ return;
3640
+ }
3641
+ const wasFocusedForCapture2 = shouldCapturePreview && isFocusedWorkbenchNode(controller, nodeID);
3642
+ if (shouldCapturePreview && !wasFocusedForCapture2) {
3239
3643
  flushSync(() => {
3240
3644
  controller.commands.focusNode(nodeID);
3241
3645
  });
3242
3646
  await waitForNextAnimationFrame();
3647
+ if (generation2 !== animationGenerationRef.current) {
3648
+ return;
3649
+ }
3243
3650
  }
3244
- await captureWorkbenchNodePreviewImageForNode(target, {
3245
- captureNodePreviewImage,
3246
- dockPreviewCache,
3247
- resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
3651
+ if (shouldCapturePreview) {
3652
+ void captureProvidedWorkbenchNodePreviewImageForNode(target, {
3653
+ captureNodePreviewImage,
3654
+ dockPreviewCache,
3655
+ resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
3656
+ });
3657
+ }
3658
+ let minimizeCommitted2 = false;
3659
+ const commitMinimize2 = () => {
3660
+ if (minimizeCommitted2) {
3661
+ return;
3662
+ }
3663
+ minimizeCommitted2 = true;
3664
+ runMinimize();
3665
+ };
3666
+ const cleanupPendingMinimize = () => {
3667
+ releaseMinimizedDockEnterAnimation(nodeID);
3668
+ flushSync(() => {
3669
+ clearPendingMinimizedNode(nodeID);
3670
+ commitMinimize2();
3671
+ });
3672
+ };
3673
+ const pendingMinimizedNode3 = {
3674
+ ...target,
3675
+ isMinimized: true,
3676
+ minimizedAtUnixMs: Date.now()
3677
+ };
3678
+ registerMinimizedDockEnterAnimation(nodeID);
3679
+ flushSync(() => {
3680
+ setPendingMinimizedNode(pendingMinimizedNode3);
3681
+ });
3682
+ animationCleanupRef.current = cleanupPendingMinimize;
3683
+ await waitForNextAnimationFrame();
3684
+ if (generation2 !== animationGenerationRef.current) {
3685
+ return;
3686
+ }
3687
+ const anchorKey2 = resolveAnchorKeyForNode(pendingMinimizedNode3);
3688
+ const dockRect2 = resolveDockAnchorRect(anchorKey2);
3689
+ if (!nodeElement2.isConnected || !dockRect2 || !isUsableGenieRect(dockRect2)) {
3690
+ cleanupPendingMinimize();
3691
+ return;
3692
+ }
3693
+ runScaleWindowAnimation({
3694
+ direction: "minimize",
3695
+ dockRect: dockRect2,
3696
+ nodeElement: nodeElement2,
3697
+ onCancel: () => {
3698
+ cleanupPendingMinimize();
3699
+ },
3700
+ onComplete: () => {
3701
+ animationCleanupRef.current = null;
3702
+ flushSync(() => {
3703
+ clearPendingMinimizedNode(nodeID);
3704
+ commitMinimize2();
3705
+ });
3706
+ scheduleReleaseMinimizedDockEnterAnimation(nodeID);
3707
+ },
3708
+ skipStop: true
3248
3709
  });
3249
- runMinimize();
3250
3710
  return;
3251
3711
  }
3252
3712
  stopAnimation();
@@ -3309,46 +3769,57 @@ function useWorkbenchGenieAnimation({
3309
3769
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
3310
3770
  });
3311
3771
  }
3312
- const cleanupHiddenNode = () => {
3772
+ let minimizeCommitted = false;
3773
+ const commitMinimize = () => {
3774
+ if (minimizeCommitted) {
3775
+ return;
3776
+ }
3777
+ minimizeCommitted = true;
3778
+ runMinimize();
3779
+ };
3780
+ const pendingMinimizedNode2 = {
3781
+ ...target,
3782
+ isMinimized: true,
3783
+ minimizedAtUnixMs: Date.now()
3784
+ };
3785
+ const cleanupPendingGenieMinimize = () => {
3786
+ releaseMinimizedDockEnterAnimation(nodeID);
3313
3787
  flushSync(() => {
3788
+ clearPendingMinimizedNode(nodeID);
3314
3789
  showNodeForGenie(nodeID);
3790
+ commitMinimize();
3315
3791
  });
3316
3792
  clearCanvas();
3317
3793
  };
3318
- animationCleanupRef.current = cleanupHiddenNode;
3794
+ animationCleanupRef.current = cleanupPendingGenieMinimize;
3319
3795
  registerMinimizedDockEnterAnimation(nodeID);
3320
3796
  flushSync(() => {
3797
+ setPendingMinimizedNode(pendingMinimizedNode2);
3321
3798
  hideNodeForGenie(nodeID);
3322
- runMinimize();
3323
3799
  });
3324
3800
  await waitForNextAnimationFrame();
3325
3801
  if (generation !== animationGenerationRef.current) {
3326
- releaseMinimizedDockEnterAnimation(nodeID);
3327
3802
  return;
3328
3803
  }
3329
- const minimizedTarget = controller.getSnapshot().nodes.find((node) => node.id === nodeID) ?? target;
3330
- const anchorKey = resolveAnchorKeyForNode(minimizedTarget);
3804
+ const anchorKey = resolveAnchorKeyForNode(pendingMinimizedNode2);
3331
3805
  const dockRect = resolveDockAnchorRect(anchorKey);
3332
3806
  if (!dockRect || !isUsableGenieRect(dockRect)) {
3333
- releaseMinimizedDockEnterAnimation(nodeID);
3334
3807
  animationCleanupRef.current = null;
3335
- cleanupHiddenNode();
3808
+ cleanupPendingGenieMinimize();
3336
3809
  return;
3337
3810
  }
3338
3811
  runGenieAnimation({
3339
3812
  direction: "minimize",
3340
3813
  dockRect,
3341
3814
  onCancel: () => {
3342
- releaseMinimizedDockEnterAnimation(nodeID);
3343
- flushSync(() => {
3344
- showNodeForGenie(nodeID);
3345
- });
3346
- clearCanvas();
3815
+ cleanupPendingGenieMinimize();
3347
3816
  },
3348
3817
  onComplete: () => {
3349
3818
  scheduleReleaseMinimizedDockEnterAnimation(nodeID);
3350
3819
  flushSync(() => {
3820
+ clearPendingMinimizedNode(nodeID);
3351
3821
  showNodeForGenie(nodeID);
3822
+ commitMinimize();
3352
3823
  });
3353
3824
  clearCanvas();
3354
3825
  },
@@ -3361,8 +3832,10 @@ function useWorkbenchGenieAnimation({
3361
3832
  clearCanvas,
3362
3833
  controller,
3363
3834
  captureNodePreviewImage,
3835
+ clearPendingMinimizedNode,
3364
3836
  dockPreviewCache,
3365
3837
  hideNodeForGenie,
3838
+ minimizeAnimation,
3366
3839
  registerMinimizedDockEnterAnimation,
3367
3840
  releaseMinimizedDockEnterAnimation,
3368
3841
  resolveAnchorKeyForNode,
@@ -3370,7 +3843,9 @@ function useWorkbenchGenieAnimation({
3370
3843
  resolveDockAnchorRect,
3371
3844
  resolveNodeElement,
3372
3845
  runGenieAnimation,
3846
+ runScaleWindowAnimation,
3373
3847
  scheduleReleaseMinimizedDockEnterAnimation,
3848
+ shouldCaptureNodePreviewImage,
3374
3849
  setupCanvas,
3375
3850
  showNodeForGenie,
3376
3851
  stopAnimation
@@ -3403,8 +3878,13 @@ function useWorkbenchGenieAnimation({
3403
3878
  (nodeID) => genieHiddenNodeIDs.has(nodeID),
3404
3879
  [genieHiddenNodeIDs]
3405
3880
  ),
3881
+ isPendingMinimizedDockNode: useCallback6(
3882
+ (nodeID) => pendingMinimizedNode?.id === nodeID,
3883
+ [pendingMinimizedNode]
3884
+ ),
3406
3885
  launchNodeFromAnchor,
3407
3886
  minimizeNodeToAnchor,
3887
+ pendingMinimizedNode,
3408
3888
  registerDockAnchor,
3409
3889
  shouldAnimateMinimizedDockEnter
3410
3890
  };
@@ -3421,6 +3901,7 @@ function WorkbenchSurface({
3421
3901
  interactive,
3422
3902
  layoutConstraints,
3423
3903
  missionControlPhase,
3904
+ minimizeAnimation,
3424
3905
  presentation,
3425
3906
  renderBackdrop,
3426
3907
  renderBottomChrome,
@@ -3435,6 +3916,7 @@ function WorkbenchSurface({
3435
3916
  resolveDockAnchorKey,
3436
3917
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
3437
3918
  shortcutsEnabled,
3919
+ shouldCaptureNodePreviewImage,
3438
3920
  wallpaper,
3439
3921
  windowChromeMode,
3440
3922
  windowChromeI18n
@@ -3449,6 +3931,7 @@ function WorkbenchSurface({
3449
3931
  interactive,
3450
3932
  layoutConstraints,
3451
3933
  missionControlPhase,
3934
+ minimizeAnimation,
3452
3935
  presentation,
3453
3936
  renderBackdrop,
3454
3937
  renderBottomChrome,
@@ -3463,6 +3946,7 @@ function WorkbenchSurface({
3463
3946
  resolveDockAnchorKey,
3464
3947
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
3465
3948
  shortcutsEnabled,
3949
+ shouldCaptureNodePreviewImage,
3466
3950
  wallpaper,
3467
3951
  windowChromeMode,
3468
3952
  windowChromeI18n
@@ -3477,6 +3961,7 @@ function WorkbenchSurfaceInner({
3477
3961
  interactive = true,
3478
3962
  layoutConstraints,
3479
3963
  missionControlPhase,
3964
+ minimizeAnimation,
3480
3965
  presentation,
3481
3966
  renderBackdrop,
3482
3967
  renderBottomChrome,
@@ -3491,6 +3976,7 @@ function WorkbenchSurfaceInner({
3491
3976
  resolveDockAnchorKey,
3492
3977
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
3493
3978
  shortcutsEnabled,
3979
+ shouldCaptureNodePreviewImage,
3494
3980
  wallpaper,
3495
3981
  windowChromeMode,
3496
3982
  windowChromeI18n
@@ -3507,8 +3993,10 @@ function WorkbenchSurfaceInner({
3507
3993
  captureNodePreviewImage,
3508
3994
  controller,
3509
3995
  dockPreviewCache,
3996
+ minimizeAnimation,
3510
3997
  resolveDockAnchorKey,
3511
- resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2
3998
+ resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
3999
+ shouldCaptureNodePreviewImage
3512
4000
  });
3513
4001
  useWorkbenchShortcuts((shortcutsEnabled ?? true) && interactive);
3514
4002
  useEffect4(() => {
@@ -4769,6 +5257,11 @@ var WorkbenchHostSessionController = class {
4769
5257
  this.restoreAndFocusNode(nodeId);
4770
5258
  });
4771
5259
  }
5260
+ minimizeNode(nodeId) {
5261
+ this.runWhenReady(this.loadGeneration, () => {
5262
+ this.controller.commands.minimizeNode(nodeId);
5263
+ });
5264
+ }
4772
5265
  exitFullscreenNode(nodeId) {
4773
5266
  this.runWhenReady(this.loadGeneration, () => {
4774
5267
  this.controller.commands.exitFullscreen(nodeId);
@@ -6244,7 +6737,7 @@ function resolveWorkbenchMinimizedDockAnchorKeyForNode(input) {
6244
6737
  }
6245
6738
  function isWorkbenchMinimizedDockEligibleNode(input) {
6246
6739
  const definition = input.nodeDefinitions.get(input.node.data.typeId);
6247
- return definition?.window?.minimizedDock?.kind === "snapshot";
6740
+ return definition?.window?.minimizedDock !== void 0;
6248
6741
  }
6249
6742
  function orderWorkbenchMinimizedDockNodes(input) {
6250
6743
  return input.nodes.map((node, index) => ({ index, node })).filter(
@@ -6360,7 +6853,18 @@ import {
6360
6853
  useState as useState7
6361
6854
  } from "react";
6362
6855
  import { createPortal as createPortal2 } from "react-dom";
6363
- import { Button as Button3, CloseIcon, FileCreateIcon, cn } from "@tutti-os/ui-system";
6856
+ import {
6857
+ Button as Button3,
6858
+ CheckIcon,
6859
+ CloseIcon,
6860
+ FileCreateIcon,
6861
+ MaximizeIcon as MaximizeIcon2,
6862
+ MinimizeIcon,
6863
+ OverviewLayoutIcon,
6864
+ PinFilledIcon,
6865
+ PinIcon,
6866
+ cn
6867
+ } from "@tutti-os/ui-system";
6364
6868
 
6365
6869
  // src/host/minimizedStackScroll.ts
6366
6870
  var minimizedStackViewportMarginPx = 56;
@@ -6492,7 +6996,7 @@ function resolveMinimizedStackTrackTranslateXPx(input) {
6492
6996
  }
6493
6997
 
6494
6998
  // src/host/WorkbenchHostDockPopup.tsx
6495
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
6999
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
6496
7000
  var dockPopupCardWidthPx = 165;
6497
7001
  var dockPopupGridGapPx = 8;
6498
7002
  var dockPopupPanelPaddingInlinePx = 12;
@@ -6557,9 +7061,14 @@ function writeDockPopupPreviewImage(memoryKey, preview, revision) {
6557
7061
  }
6558
7062
  function WorkbenchHostDockPopup({
6559
7063
  anchorRect,
7064
+ canEnterFullscreen,
7065
+ canShowAllWindows,
6560
7066
  capturePreview,
6561
7067
  debugDiagnostics,
7068
+ dockRetention,
6562
7069
  dockPreviewCache,
7070
+ fullscreenLabel,
7071
+ hideLabel,
6563
7072
  items,
6564
7073
  label,
6565
7074
  labelMode,
@@ -6568,15 +7077,25 @@ function WorkbenchHostDockPopup({
6568
7077
  onClose,
6569
7078
  onCloseNode,
6570
7079
  onCreateNew,
7080
+ onEnterFullscreen,
7081
+ onHide,
7082
+ onRunDockRetentionAction,
6571
7083
  onSelectNode,
7084
+ onShowAllWindows,
7085
+ openLabel,
7086
+ onQuit,
6572
7087
  placement = "bottom",
7088
+ quitLabel,
6573
7089
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
7090
+ showAllWindowsLabel,
6574
7091
  showCreateNew,
7092
+ showOpen,
6575
7093
  variant
6576
7094
  }) {
6577
7095
  const resolvedLabelMode = labelMode ?? "hover-overlay";
6578
7096
  const resolvedVariant = variant ?? "default";
6579
7097
  const isMinimizedStack = resolvedVariant === "minimized-stack";
7098
+ const isContextMenu = resolvedVariant === "context-menu";
6580
7099
  const createCardCount = showCreateNew === false ? 0 : 1;
6581
7100
  const cardElementsRef = useRef7(/* @__PURE__ */ new Map());
6582
7101
  const cardRefCallbacksRef = useRef7(
@@ -6587,8 +7106,8 @@ function WorkbenchHostDockPopup({
6587
7106
  const [pointer, setPointer] = useState7(null);
6588
7107
  const [minimizedStackScrollOffset, setMinimizedStackScrollOffset] = useState7(0);
6589
7108
  const [capturedPreviewByMemoryKey, setCapturedPreviewByMemoryKey] = useState7({});
6590
- const columnCount = Math.min(Math.max(items.length + createCardCount, 1), 3);
6591
- const popupWidthPx = columnCount * dockPopupCardWidthPx + Math.max(0, columnCount - 1) * dockPopupGridGapPx + dockPopupPanelPaddingInlinePx * 2 + dockPopupPanelBorderInlinePx;
7109
+ const columnCount = isContextMenu ? 1 : Math.min(Math.max(items.length + createCardCount, 1), 3);
7110
+ const popupWidthPx = isContextMenu ? 268 : columnCount * dockPopupCardWidthPx + Math.max(0, columnCount - 1) * dockPopupGridGapPx + dockPopupPanelPaddingInlinePx * 2 + dockPopupPanelBorderInlinePx;
6592
7111
  const popupCenterY = anchorRect.top + anchorRect.height / 2;
6593
7112
  const isLeftMinimizedStack = placement === "left" && isMinimizedStack;
6594
7113
  const minimizedStackTrackHeightPx = resolveMinimizedStackTrackHeightPx(
@@ -6766,7 +7285,7 @@ function WorkbenchHostDockPopup({
6766
7285
  return () => viewport.removeEventListener("wheel", handleWheel);
6767
7286
  }, [isMinimizedStack, minimizedStackMaxScrollOffset]);
6768
7287
  useEffect8(() => {
6769
- if (!capturePreview) {
7288
+ if (!capturePreview || isContextMenu) {
6770
7289
  return;
6771
7290
  }
6772
7291
  let cancelled = false;
@@ -6977,6 +7496,7 @@ function WorkbenchHostDockPopup({
6977
7496
  }, [
6978
7497
  capturePreview,
6979
7498
  dockPreviewCache,
7499
+ isContextMenu,
6980
7500
  items,
6981
7501
  previewCaptureKey,
6982
7502
  resolveDockPreviewCacheKey2
@@ -6990,12 +7510,13 @@ function WorkbenchHostDockPopup({
6990
7510
  "data-desktop-dock-popup-root": "true",
6991
7511
  "data-popup-variant": resolvedVariant,
6992
7512
  style: popupStyle,
6993
- children: /* @__PURE__ */ jsxs5(
7513
+ children: /* @__PURE__ */ jsx9(
6994
7514
  "div",
6995
7515
  {
6996
7516
  "aria-label": label,
6997
7517
  className: cn(
6998
- "desktop-dock-popup relative origin-bottom rounded-lg border border-[var(--border-1)] bg-background-fronted p-3 text-[var(--text-primary)] shadow-panel motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:slide-in-from-bottom-2 motion-safe:duration-[175ms] motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none",
7518
+ "desktop-dock-popup relative origin-bottom rounded-lg border border-[var(--border-1)] bg-background-fronted text-[var(--text-primary)] shadow-panel motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:slide-in-from-bottom-2 motion-safe:duration-[175ms] motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none",
7519
+ isContextMenu ? "p-1" : "p-3",
6999
7520
  isLeftMinimizedStack ? "w-full min-w-0 max-w-none" : "w-[min(var(--desktop-dock-popup-width,366px),calc(100vw-32px))]"
7000
7521
  ),
7001
7522
  "data-desktop-dock-popup-panel": "true",
@@ -7005,8 +7526,31 @@ function WorkbenchHostDockPopup({
7005
7526
  onPointerLeave: isMinimizedStack ? () => setPointer(null) : void 0,
7006
7527
  role: "dialog",
7007
7528
  style: panelStyle,
7008
- children: [
7009
- /* @__PURE__ */ jsx9("div", { className: "mb-2.5 flex items-center justify-between", children: /* @__PURE__ */ jsx9("span", { className: "min-w-0 truncate text-sm font-semibold", children: label }) }),
7529
+ children: isContextMenu ? /* @__PURE__ */ jsx9(
7530
+ WorkbenchHostDockContextMenu,
7531
+ {
7532
+ canCreateNew: showCreateNew !== false,
7533
+ canEnterFullscreen: canEnterFullscreen === true,
7534
+ canShowAllWindows: canShowAllWindows === true,
7535
+ dockRetention,
7536
+ fullscreenLabel,
7537
+ hideLabel,
7538
+ items,
7539
+ newWindowLabel,
7540
+ onCreateNew,
7541
+ onEnterFullscreen,
7542
+ onHide,
7543
+ onQuit,
7544
+ onRunDockRetentionAction,
7545
+ onSelectNode,
7546
+ onShowAllWindows,
7547
+ openLabel,
7548
+ quitLabel,
7549
+ showAllWindowsLabel,
7550
+ showOpen: showOpen === true
7551
+ }
7552
+ ) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
7553
+ /* @__PURE__ */ jsx9("div", { className: "mb-2.5 flex items-center justify-between", children: /* @__PURE__ */ jsx9("span", { className: "min-w-0 truncate text-sm font-semibold", children: label }) }),
7010
7554
  isMinimizedStack ? /* @__PURE__ */ jsx9(
7011
7555
  "div",
7012
7556
  {
@@ -7111,7 +7655,7 @@ function WorkbenchHostDockPopup({
7111
7655
  }
7112
7656
  ) : null
7113
7657
  ] })
7114
- ]
7658
+ ] })
7115
7659
  }
7116
7660
  )
7117
7661
  }
@@ -7121,6 +7665,172 @@ function WorkbenchHostDockPopup({
7121
7665
  }
7122
7666
  return createPortal2(content, document.body);
7123
7667
  }
7668
+ function WorkbenchHostDockContextMenu({
7669
+ canCreateNew,
7670
+ canEnterFullscreen,
7671
+ canShowAllWindows,
7672
+ dockRetention,
7673
+ fullscreenLabel,
7674
+ hideLabel,
7675
+ items,
7676
+ newWindowLabel,
7677
+ onCreateNew,
7678
+ onEnterFullscreen,
7679
+ onHide,
7680
+ onQuit,
7681
+ onRunDockRetentionAction,
7682
+ onSelectNode,
7683
+ onShowAllWindows,
7684
+ openLabel,
7685
+ quitLabel,
7686
+ showAllWindowsLabel,
7687
+ showOpen
7688
+ }) {
7689
+ const hasOpenWindows = items.length > 0;
7690
+ const hasNewWindowCommand = hasOpenWindows && canCreateNew;
7691
+ const hasOpenCommand = !hasOpenWindows;
7692
+ const hasDockActionGroup = Boolean(dockRetention) || hasNewWindowCommand || hasOpenCommand;
7693
+ const hasWindowActionGroup = hasOpenWindows;
7694
+ return /* @__PURE__ */ jsxs5(
7695
+ "div",
7696
+ {
7697
+ className: "flex min-w-0 flex-col gap-1",
7698
+ "data-desktop-dock-context-menu": "true",
7699
+ role: "menu",
7700
+ children: [
7701
+ hasOpenWindows ? /* @__PURE__ */ jsx9(Fragment3, { children: /* @__PURE__ */ jsx9("div", { className: "max-h-48 min-w-0 overflow-auto overscroll-contain", children: items.map((item) => /* @__PURE__ */ jsx9(
7702
+ WorkbenchHostDockContextMenuItem,
7703
+ {
7704
+ checked: !item.isMinimized,
7705
+ label: item.title?.trim() || item.node.title,
7706
+ onSelect: () => onSelectNode(item.node.id)
7707
+ },
7708
+ item.node.id
7709
+ )) }) }) : null,
7710
+ hasOpenWindows && (hasDockActionGroup || hasWindowActionGroup) ? /* @__PURE__ */ jsx9(WorkbenchHostDockContextMenuSeparator, {}) : null,
7711
+ dockRetention ? /* @__PURE__ */ jsx9(
7712
+ WorkbenchHostDockContextMenuItem,
7713
+ {
7714
+ checked: dockRetention.checked,
7715
+ checkedIcon: /* @__PURE__ */ jsx9(
7716
+ PinFilledIcon,
7717
+ {
7718
+ "aria-hidden": "true",
7719
+ className: "size-4 text-[var(--tutti-purple)]"
7720
+ }
7721
+ ),
7722
+ disabled: dockRetention.disabled,
7723
+ icon: /* @__PURE__ */ jsx9(PinIcon, { "aria-hidden": "true", className: "size-4" }),
7724
+ label: dockRetention.pendingLabel ?? dockRetention.label,
7725
+ onSelect: onRunDockRetentionAction
7726
+ }
7727
+ ) : null,
7728
+ hasNewWindowCommand ? /* @__PURE__ */ jsx9(
7729
+ WorkbenchHostDockContextMenuItem,
7730
+ {
7731
+ icon: /* @__PURE__ */ jsx9(FileCreateIcon, { "aria-hidden": "true", className: "size-4" }),
7732
+ label: newWindowLabel,
7733
+ onSelect: onCreateNew
7734
+ }
7735
+ ) : null,
7736
+ hasOpenCommand ? /* @__PURE__ */ jsx9(
7737
+ WorkbenchHostDockContextMenuItem,
7738
+ {
7739
+ disabled: !showOpen,
7740
+ icon: /* @__PURE__ */ jsx9(FileCreateIcon, { "aria-hidden": "true", className: "size-4" }),
7741
+ label: openLabel,
7742
+ onSelect: onCreateNew
7743
+ }
7744
+ ) : null,
7745
+ hasOpenWindows ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
7746
+ hasDockActionGroup ? /* @__PURE__ */ jsx9(WorkbenchHostDockContextMenuSeparator, {}) : null,
7747
+ canShowAllWindows && onShowAllWindows ? /* @__PURE__ */ jsx9(
7748
+ WorkbenchHostDockContextMenuItem,
7749
+ {
7750
+ icon: /* @__PURE__ */ jsx9(OverviewLayoutIcon, { "aria-hidden": "true", className: "size-4" }),
7751
+ label: showAllWindowsLabel,
7752
+ onSelect: onShowAllWindows
7753
+ }
7754
+ ) : null,
7755
+ /* @__PURE__ */ jsx9(
7756
+ WorkbenchHostDockContextMenuItem,
7757
+ {
7758
+ disabled: !canEnterFullscreen || !onEnterFullscreen,
7759
+ icon: /* @__PURE__ */ jsx9(MaximizeIcon2, { "aria-hidden": "true", className: "size-4" }),
7760
+ label: fullscreenLabel,
7761
+ onSelect: onEnterFullscreen
7762
+ }
7763
+ ),
7764
+ /* @__PURE__ */ jsx9(
7765
+ WorkbenchHostDockContextMenuItem,
7766
+ {
7767
+ disabled: !onHide,
7768
+ icon: /* @__PURE__ */ jsx9(MinimizeIcon, { "aria-hidden": "true", className: "size-4" }),
7769
+ label: hideLabel,
7770
+ onSelect: onHide
7771
+ }
7772
+ ),
7773
+ /* @__PURE__ */ jsx9(
7774
+ WorkbenchHostDockContextMenuItem,
7775
+ {
7776
+ disabled: !onQuit,
7777
+ icon: /* @__PURE__ */ jsx9(CloseIcon, { "aria-hidden": "true", className: "size-4" }),
7778
+ label: quitLabel,
7779
+ onSelect: onQuit
7780
+ }
7781
+ )
7782
+ ] }) : null
7783
+ ]
7784
+ }
7785
+ );
7786
+ }
7787
+ function WorkbenchHostDockContextMenuItem({
7788
+ checked,
7789
+ checkedIcon,
7790
+ disabled,
7791
+ icon,
7792
+ label,
7793
+ onSelect
7794
+ }) {
7795
+ return /* @__PURE__ */ jsxs5(
7796
+ "button",
7797
+ {
7798
+ className: cn(
7799
+ "flex h-8 w-full min-w-0 items-center gap-2 rounded-md px-2 text-left text-sm text-[var(--text-primary)] transition-colors",
7800
+ disabled ? "cursor-default opacity-45" : "hover:bg-transparency-hover focus-visible:bg-transparency-hover focus-visible:outline-none"
7801
+ ),
7802
+ disabled,
7803
+ role: "menuitem",
7804
+ type: "button",
7805
+ onClick: () => {
7806
+ if (disabled || !onSelect) {
7807
+ return;
7808
+ }
7809
+ onSelect();
7810
+ },
7811
+ children: [
7812
+ /* @__PURE__ */ jsx9("span", { className: "flex size-4 shrink-0 items-center justify-center text-[var(--text-secondary)]", children: checked && checkedIcon ? checkedIcon : checked ? /* @__PURE__ */ jsx9(
7813
+ CheckIcon,
7814
+ {
7815
+ "aria-hidden": "true",
7816
+ className: "size-4 text-[var(--tutti-purple)]"
7817
+ }
7818
+ ) : icon ?? null }),
7819
+ /* @__PURE__ */ jsx9("span", { className: "min-w-0 truncate", children: label })
7820
+ ]
7821
+ }
7822
+ );
7823
+ }
7824
+ function WorkbenchHostDockContextMenuSeparator() {
7825
+ return /* @__PURE__ */ jsx9(
7826
+ "div",
7827
+ {
7828
+ "aria-hidden": "true",
7829
+ className: "mx-2 my-1 h-px bg-[var(--border-1)]",
7830
+ role: "separator"
7831
+ }
7832
+ );
7833
+ }
7124
7834
  function readPersistedDockPreview(dockPreviewCache, cacheKey) {
7125
7835
  if (!cacheKey) {
7126
7836
  return Promise.resolve(null);
@@ -7316,7 +8026,7 @@ var WorkbenchHostDockPopupCard = forwardRef(function WorkbenchHostDockPopupCard2
7316
8026
  "span",
7317
8027
  {
7318
8028
  "aria-hidden": "true",
7319
- className: "pointer-events-none absolute inset-0 z-[3] rounded-[8px] shadow-[inset_0_0_0_2px_var(--border-focus)]",
8029
+ className: "pointer-events-none absolute inset-0 z-[3] rounded-md shadow-[inset_0_0_0_2px_var(--border-focus)]",
7320
8030
  "data-desktop-dock-popup-card-active-overlay": "true"
7321
8031
  }
7322
8032
  ) : null,
@@ -7420,6 +8130,7 @@ function WorkbenchHostDock({
7420
8130
  nodeDefinitions,
7421
8131
  onDockEntryAction,
7422
8132
  onDockEntryClick,
8133
+ onMissionControlRequestOpen,
7423
8134
  workspaceId
7424
8135
  }) {
7425
8136
  const minimizedNodeIDs = useMemo5(
@@ -7452,6 +8163,7 @@ function WorkbenchHostDock({
7452
8163
  /* @__PURE__ */ new Map()
7453
8164
  );
7454
8165
  const previousAttentionTokenByEntryId = useRef8(/* @__PURE__ */ new Map());
8166
+ const dockEntryClickThrottleUntilRef = useRef8(/* @__PURE__ */ new Map());
7455
8167
  const attentionTimeouts = useRef8(
7456
8168
  /* @__PURE__ */ new Map()
7457
8169
  );
@@ -7626,9 +8338,9 @@ function WorkbenchHostDock({
7626
8338
  const minimizedDockSlots = useMemo5(
7627
8339
  () => resolveWorkbenchMinimizedDockSlots({
7628
8340
  nodeDefinitions,
7629
- nodes: context.nodes
8341
+ nodes: context.minimizedNodes
7630
8342
  }),
7631
- [context.nodes, nodeDefinitions]
8343
+ [context.minimizedNodes, nodeDefinitions]
7632
8344
  );
7633
8345
  const { promotedNodeId, stackDispatching } = useMinimizedDockStackPromotion(minimizedDockSlots);
7634
8346
  const dockItems = useMemo5(
@@ -7999,6 +8711,19 @@ function WorkbenchHostDock({
7999
8711
  triggerDockBounce
8000
8712
  ]
8001
8713
  );
8714
+ const isDockEntryClickThrottled = useCallback10(
8715
+ (anchorKey) => {
8716
+ const throttledUntil = dockEntryClickThrottleUntilRef.current.get(anchorKey);
8717
+ return throttledUntil !== void 0 && Date.now() < throttledUntil;
8718
+ },
8719
+ []
8720
+ );
8721
+ const claimDockEntryClick = useCallback10((anchorKey) => {
8722
+ dockEntryClickThrottleUntilRef.current.set(
8723
+ anchorKey,
8724
+ Date.now() + DOCK_ENTRY_CLICK_THROTTLE_MS
8725
+ );
8726
+ }, []);
8002
8727
  const beginDockMinimizedInteraction = useCallback10(
8003
8728
  (anchorKey) => {
8004
8729
  hoverPanelRestTargetRef.current = null;
@@ -8156,9 +8881,13 @@ function WorkbenchHostDock({
8156
8881
  }, [dockPlacement, presentDockItems.length, updateDockScrollState]);
8157
8882
  const hasMinimizedPreviewCapture = minimizedDockSlots.some(
8158
8883
  (slot) => minimizedDockSlotNodes(slot).some(
8159
- (node) => Boolean(
8160
- nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock?.capturePreview
8161
- )
8884
+ (node) => (() => {
8885
+ if (context.genie.isPendingMinimizedDockNode(node.id)) {
8886
+ return false;
8887
+ }
8888
+ const minimizedDock = nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock;
8889
+ return minimizedDock?.kind === "snapshot" && Boolean(minimizedDock.capturePreview);
8890
+ })()
8162
8891
  )
8163
8892
  );
8164
8893
  useEffect9(() => {
@@ -8222,7 +8951,8 @@ function WorkbenchHostDock({
8222
8951
  );
8223
8952
  const captureMinimizedNodePreview = useCallback10(
8224
8953
  async (node) => {
8225
- const capturePreview = nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock?.capturePreview;
8954
+ const minimizedDock = nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock;
8955
+ const capturePreview = minimizedDock?.kind === "snapshot" ? minimizedDock.capturePreview : void 0;
8226
8956
  const externalState = readWorkbenchHostExternalState({
8227
8957
  externalStateSource,
8228
8958
  node,
@@ -8250,6 +8980,41 @@ function WorkbenchHostDock({
8250
8980
  workspaceId
8251
8981
  ]
8252
8982
  );
8983
+ const provideMinimizedNodePreview = useCallback10(
8984
+ (node) => {
8985
+ const minimizedDock = nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock;
8986
+ if (minimizedDock?.kind !== "component") {
8987
+ return null;
8988
+ }
8989
+ const externalState = readWorkbenchHostExternalState({
8990
+ externalStateSource,
8991
+ node,
8992
+ workspaceId
8993
+ });
8994
+ return minimizedDock.providePreview({
8995
+ externalNodeState: externalState.externalNodeState,
8996
+ externalWorkspaceState: externalState.externalWorkspaceState,
8997
+ host,
8998
+ isFocused: context.focusedNodeId === node.id,
8999
+ isMinimized: node.isMinimized,
9000
+ node
9001
+ });
9002
+ },
9003
+ [
9004
+ context.focusedNodeId,
9005
+ externalStateSource,
9006
+ host,
9007
+ nodeDefinitions,
9008
+ workspaceId
9009
+ ]
9010
+ );
9011
+ const provideMinimizedNodePreviewForNode = useCallback10(
9012
+ (node) => {
9013
+ const minimizedDock = nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock;
9014
+ return minimizedDock?.kind === "component" ? provideMinimizedNodePreview : void 0;
9015
+ },
9016
+ [nodeDefinitions, provideMinimizedNodePreview]
9017
+ );
8253
9018
  if (dockItems.length === 0 && presentDockItems.length === 0) {
8254
9019
  return null;
8255
9020
  }
@@ -8303,12 +9068,59 @@ function WorkbenchHostDock({
8303
9068
  dockSlotRefCallbacksRef.current.set(anchorKey, callback);
8304
9069
  return callback;
8305
9070
  }, []);
9071
+ const runDockEntryAction = useCallback10(
9072
+ (entryId, actionId) => {
9073
+ const actionKey = dockActionKey(entryId, actionId);
9074
+ if (pendingActionKeys.has(actionKey)) {
9075
+ return;
9076
+ }
9077
+ setPendingActionKeys((current) => {
9078
+ const next = new Set(current);
9079
+ next.add(actionKey);
9080
+ return next;
9081
+ });
9082
+ void (async () => {
9083
+ try {
9084
+ await onDockEntryAction?.({
9085
+ actionId,
9086
+ entryId,
9087
+ host
9088
+ });
9089
+ } catch {
9090
+ } finally {
9091
+ setPendingActionKeys((current) => {
9092
+ if (!current.has(actionKey)) {
9093
+ return current;
9094
+ }
9095
+ const next = new Set(current);
9096
+ next.delete(actionKey);
9097
+ return next;
9098
+ });
9099
+ }
9100
+ })();
9101
+ },
9102
+ [host, onDockEntryAction, pendingActionKeys]
9103
+ );
8306
9104
  const popupEntry = activePopup === null ? null : resolvedEntries.find(
8307
9105
  (entry) => entry.entry.id === activePopup.entryId
8308
9106
  ) ?? null;
8309
9107
  const activeMinimizedStackSlot = activeMinimizedStackPopup === null ? null : minimizedDockSlots.find(
8310
9108
  (slot) => slot.kind === "stack"
8311
9109
  ) ?? null;
9110
+ const openDockContextMenuNodeIds = popupEntry?.matchedNodes.map((node) => node.id) ?? [];
9111
+ const dockContextMenuInstanceMode = popupEntry === null ? void 0 : resolveDockEntryInstanceMode(popupEntry.entry, nodeDefinitions);
9112
+ const canShowAllWindowsFromDockContextMenu = onMissionControlRequestOpen !== void 0 && dockContextMenuInstanceMode === "multi" && openDockContextMenuNodeIds.length > 1;
9113
+ const fullscreenNodeFromDockContextMenu = popupEntry === null ? null : resolveDockContextMenuFullscreenNode({
9114
+ focusedNodeId: context.focusedNodeId,
9115
+ minimizedNodeIDs,
9116
+ nodeDefinitions,
9117
+ nodes: popupEntry.matchedNodes
9118
+ });
9119
+ const canOpenFromDockContextMenu = popupEntry !== null && popupEntry.matchedNodes.length === 0 && resolveWorkbenchDockEntryClick({
9120
+ entry: popupEntry.entry,
9121
+ instanceMode: dockContextMenuInstanceMode,
9122
+ matchedNodes: popupEntry.matchedNodes
9123
+ }).kind === "launch";
8312
9124
  return /* @__PURE__ */ jsxs6(
8313
9125
  "div",
8314
9126
  {
@@ -8409,13 +9221,26 @@ function WorkbenchHostDock({
8409
9221
  className: "desktop-dock__btn",
8410
9222
  "data-interactive": clickResolution.kind === "blocked" ? "false" : "true",
8411
9223
  type: "button",
8412
- onPointerDown: () => {
9224
+ onPointerDown: (event) => {
9225
+ if (event.button !== 0) {
9226
+ return;
9227
+ }
8413
9228
  if (clickResolution.kind === "blocked") {
8414
9229
  return;
8415
9230
  }
9231
+ if (isDockEntryClickThrottled(anchorKey)) {
9232
+ return;
9233
+ }
8416
9234
  beginDockIconInteraction(anchorKey);
8417
9235
  },
8418
9236
  onClick: (event) => {
9237
+ if (clickResolution.kind === "blocked") {
9238
+ return;
9239
+ }
9240
+ if (isDockEntryClickThrottled(anchorKey)) {
9241
+ return;
9242
+ }
9243
+ claimDockEntryClick(anchorKey);
8419
9244
  logWorkbenchDockDebug("dock.click", debugDiagnostics, {
8420
9245
  anchorKey,
8421
9246
  clickResolution,
@@ -8442,14 +9267,16 @@ function WorkbenchHostDock({
8442
9267
  switch (clickResolution.kind) {
8443
9268
  case "focus-node":
8444
9269
  closePopup();
8445
- void Promise.resolve(
8446
- onDockEntryClick?.({
8447
- entryId: entry.id,
8448
- host,
8449
- nodeId: clickResolution.nodeId
8450
- })
8451
- ).catch(() => {
8452
- });
9270
+ void (async () => {
9271
+ try {
9272
+ await onDockEntryClick?.({
9273
+ entryId: entry.id,
9274
+ host,
9275
+ nodeId: clickResolution.nodeId
9276
+ });
9277
+ } catch {
9278
+ }
9279
+ })();
8453
9280
  context.genie.launchNodeFromAnchor(
8454
9281
  anchorKey,
8455
9282
  clickResolution.nodeId,
@@ -8480,6 +9307,7 @@ function WorkbenchHostDock({
8480
9307
  top: rect.top,
8481
9308
  width: rect.width
8482
9309
  },
9310
+ kind: "preview",
8483
9311
  entryId: entry.id
8484
9312
  }
8485
9313
  );
@@ -8487,14 +9315,16 @@ function WorkbenchHostDock({
8487
9315
  }
8488
9316
  case "action":
8489
9317
  closePopup();
8490
- void Promise.resolve(
8491
- onDockEntryAction?.({
8492
- actionId: clickResolution.actionId,
8493
- entryId: entry.id,
8494
- host
8495
- })
8496
- ).catch(() => {
8497
- });
9318
+ void (async () => {
9319
+ try {
9320
+ await onDockEntryAction?.({
9321
+ actionId: clickResolution.actionId,
9322
+ entryId: entry.id,
9323
+ host
9324
+ });
9325
+ } catch {
9326
+ }
9327
+ })();
8498
9328
  return;
8499
9329
  case "launch":
8500
9330
  closePopup();
@@ -8509,10 +9339,36 @@ function WorkbenchHostDock({
8509
9339
  })
8510
9340
  );
8511
9341
  return;
8512
- case "blocked":
8513
- return;
8514
9342
  }
8515
9343
  },
9344
+ onContextMenu: (event) => {
9345
+ if (clickResolution.kind === "blocked" || !dockEntryHasContextMenu(entry, resolvedEntry)) {
9346
+ return;
9347
+ }
9348
+ event.preventDefault();
9349
+ const rect = event.currentTarget.getBoundingClientRect();
9350
+ logWorkbenchDockDebug(
9351
+ "dock.popup.context_menu",
9352
+ debugDiagnostics,
9353
+ {
9354
+ anchorKey,
9355
+ entryId: entry.id,
9356
+ matchedNodeCount: resolvedEntry.matchedNodes.length,
9357
+ typeId: entry.typeId,
9358
+ workspaceId
9359
+ }
9360
+ );
9361
+ setActivePopup({
9362
+ anchorRect: {
9363
+ height: rect.height,
9364
+ left: rect.left,
9365
+ top: rect.top,
9366
+ width: rect.width
9367
+ },
9368
+ kind: "context-menu",
9369
+ entryId: entry.id
9370
+ });
9371
+ },
8516
9372
  children: /* @__PURE__ */ jsxs6(
8517
9373
  "span",
8518
9374
  {
@@ -8641,7 +9497,10 @@ function WorkbenchHostDock({
8641
9497
  className: "desktop-dock__btn desktop-dock__minimized-btn",
8642
9498
  "data-interactive": "true",
8643
9499
  type: "button",
8644
- onPointerDown: () => {
9500
+ onPointerDown: (event) => {
9501
+ if (event.button !== 0) {
9502
+ return;
9503
+ }
8645
9504
  beginDockMinimizedInteraction();
8646
9505
  },
8647
9506
  onClick: (event) => {
@@ -8683,9 +9542,12 @@ function WorkbenchHostDock({
8683
9542
  className: `desktop-dock__minimized-stack-layer desktop-dock__minimized-stack-layer--${index}`,
8684
9543
  dockPreviewCache,
8685
9544
  node: node2,
9545
+ providePreview: provideMinimizedNodePreviewForNode(
9546
+ node2
9547
+ ),
8686
9548
  workspaceId
8687
9549
  },
8688
- node2.id
9550
+ minimizedDockPreviewFreezeKey(node2)
8689
9551
  );
8690
9552
  }
8691
9553
  return /* @__PURE__ */ jsx10(
@@ -8753,6 +9615,7 @@ function WorkbenchHostDock({
8753
9615
  );
8754
9616
  }
8755
9617
  const node = slot.node;
9618
+ const isPendingMinimizedNode = context.genie.isPendingMinimizedDockNode(node.id);
8756
9619
  const labelTooltipTarget = dockLabelTooltipTarget(
8757
9620
  `minimized-node:${node.id}`,
8758
9621
  node.title
@@ -8763,8 +9626,12 @@ function WorkbenchHostDock({
8763
9626
  "aria-label": i18n.t("launch", { title: node.title }),
8764
9627
  className: "desktop-dock__btn desktop-dock__minimized-btn",
8765
9628
  "data-interactive": "true",
9629
+ disabled: isPendingMinimizedNode,
8766
9630
  type: "button",
8767
- onPointerDown: () => {
9631
+ onPointerDown: (event) => {
9632
+ if (event.button !== 0 || isPendingMinimizedNode) {
9633
+ return;
9634
+ }
8768
9635
  const restoreIntent = resolveWorkbenchMinimizedDockRestoreIntent({
8769
9636
  nodeId: node.id,
8770
9637
  slots: minimizedDockSlots,
@@ -8780,6 +9647,9 @@ function WorkbenchHostDock({
8780
9647
  beginDockMinimizedInteraction();
8781
9648
  },
8782
9649
  onClick: () => {
9650
+ if (isPendingMinimizedNode) {
9651
+ return;
9652
+ }
8783
9653
  const restoreIntent = resolveWorkbenchMinimizedDockRestoreIntent({
8784
9654
  nodeId: node.id,
8785
9655
  slots: minimizedDockSlots,
@@ -8809,11 +9679,14 @@ function WorkbenchHostDock({
8809
9679
  children: /* @__PURE__ */ jsx10(
8810
9680
  WorkbenchHostDockMinimizedNodePreview,
8811
9681
  {
8812
- capturePreview: captureMinimizedNodePreview,
8813
- dockPreviewCache,
9682
+ capturePreview: isPendingMinimizedNode ? void 0 : captureMinimizedNodePreview,
9683
+ deferPreview: isPendingMinimizedNode,
9684
+ dockPreviewCache: isPendingMinimizedNode ? void 0 : dockPreviewCache,
8814
9685
  node,
9686
+ providePreview: isPendingMinimizedNode ? void 0 : provideMinimizedNodePreviewForNode(node),
8815
9687
  workspaceId
8816
- }
9688
+ },
9689
+ minimizedDockPreviewFreezeKey(node)
8817
9690
  )
8818
9691
  }
8819
9692
  );
@@ -8828,6 +9701,7 @@ function WorkbenchHostDock({
8828
9701
  "data-dock-label-tooltip-key": labelTooltipTarget.key,
8829
9702
  "data-dock-label-tooltip-label": labelTooltipTarget.label,
8830
9703
  "data-node-state": "minimized",
9704
+ "data-pending-minimize": isPendingMinimizedNode ? "true" : void 0,
8831
9705
  "data-presence": dockItem.presence,
8832
9706
  "data-promoted-from-stack": promotedNodeId === node.id ? "true" : void 0,
8833
9707
  "data-section-id": "minimized",
@@ -8929,7 +9803,27 @@ function WorkbenchHostDock({
8929
9803
  {
8930
9804
  anchorRect: activePopup.anchorRect,
8931
9805
  placement: dockPlacement,
9806
+ canEnterFullscreen: fullscreenNodeFromDockContextMenu !== null,
9807
+ canShowAllWindows: canShowAllWindowsFromDockContextMenu,
8932
9808
  debugDiagnostics,
9809
+ dockRetention: popupEntry.entry.dockRetention ? {
9810
+ checked: popupEntry.entry.dockRetention.retained,
9811
+ disabled: popupEntry.entry.dockRetention.disabled === true || pendingActionKeys.has(
9812
+ dockActionKey(
9813
+ popupEntry.entry.id,
9814
+ popupEntry.entry.dockRetention.actionId
9815
+ )
9816
+ ),
9817
+ label: i18n.t(
9818
+ popupEntry.entry.dockRetention.retained ? "dockContextMenu.removeFromDock" : "dockContextMenu.keepInDock"
9819
+ ),
9820
+ pendingLabel: pendingActionKeys.has(
9821
+ dockActionKey(
9822
+ popupEntry.entry.id,
9823
+ popupEntry.entry.dockRetention.actionId
9824
+ )
9825
+ ) ? popupEntry.entry.dockRetention.pendingLabel : void 0
9826
+ } : null,
8933
9827
  capturePreview: popupEntry.entry.capturePopupItemPreview ? async (item) => {
8934
9828
  const previewImageUrl = await Promise.resolve(
8935
9829
  popupEntry.entry.capturePopupItemPreview?.(item) ?? null
@@ -8978,7 +9872,10 @@ function WorkbenchHostDock({
8978
9872
  label: popupEntry.entry.label,
8979
9873
  labelMode: popupEntry.entry.popupCardLabelMode,
8980
9874
  newWindowLabel: i18n.t("newWindow"),
9875
+ openLabel: i18n.t("dockContextMenu.open"),
8981
9876
  closeWindowLabel: (title) => i18n.t("closeWindow", { title }),
9877
+ fullscreenLabel: i18n.t("dockContextMenu.fullscreen"),
9878
+ hideLabel: i18n.t("dockContextMenu.hide"),
8982
9879
  onClose: () => {
8983
9880
  logWorkbenchDockDebug(
8984
9881
  "dock.popup.close_requested",
@@ -9013,16 +9910,46 @@ function WorkbenchHostDock({
9013
9910
  })
9014
9911
  );
9015
9912
  },
9913
+ onEnterFullscreen: () => {
9914
+ if (!fullscreenNodeFromDockContextMenu) {
9915
+ return;
9916
+ }
9917
+ context.controller.commands.enterFullscreen(
9918
+ fullscreenNodeFromDockContextMenu.id
9919
+ );
9920
+ closePopup();
9921
+ },
9922
+ onHide: () => {
9923
+ for (const node of popupEntry.matchedNodes) {
9924
+ if (!minimizedNodeIDs.has(node.id)) {
9925
+ host.minimizeNode(node.id);
9926
+ }
9927
+ }
9928
+ closePopup();
9929
+ },
9930
+ onRunDockRetentionAction: popupEntry.entry.dockRetention ? () => {
9931
+ const dockRetention = popupEntry.entry.dockRetention;
9932
+ if (!dockRetention || dockRetention.disabled === true) {
9933
+ return;
9934
+ }
9935
+ runDockEntryAction(
9936
+ popupEntry.entry.id,
9937
+ dockRetention.actionId
9938
+ );
9939
+ closePopup();
9940
+ } : void 0,
9016
9941
  onSelectNode: (nodeId) => {
9017
9942
  closePopup();
9018
- void Promise.resolve(
9019
- onDockEntryClick?.({
9020
- entryId: popupEntry.entry.id,
9021
- host,
9022
- nodeId
9023
- })
9024
- ).catch(() => {
9025
- });
9943
+ void (async () => {
9944
+ try {
9945
+ await onDockEntryClick?.({
9946
+ entryId: popupEntry.entry.id,
9947
+ host,
9948
+ nodeId
9949
+ });
9950
+ } catch {
9951
+ }
9952
+ })();
9026
9953
  context.genie.launchNodeFromAnchor(
9027
9954
  anchorKeyFromPopupEntry(popupEntry),
9028
9955
  nodeId,
@@ -9031,11 +9958,35 @@ function WorkbenchHostDock({
9031
9958
  }
9032
9959
  );
9033
9960
  },
9961
+ onShowAllWindows: () => {
9962
+ closePopup();
9963
+ for (const node of popupEntry.matchedNodes) {
9964
+ if (minimizedNodeIDs.has(node.id)) {
9965
+ context.controller.commands.restoreNode(node.id);
9966
+ }
9967
+ }
9968
+ window.requestAnimationFrame(() => {
9969
+ onMissionControlRequestOpen?.("activate", {
9970
+ nodeIds: openDockContextMenuNodeIds,
9971
+ trigger: "dock-context-menu"
9972
+ });
9973
+ });
9974
+ },
9975
+ onQuit: () => {
9976
+ for (const node of popupEntry.matchedNodes) {
9977
+ host.requestNodeClose(node.id);
9978
+ }
9979
+ closePopup();
9980
+ },
9981
+ quitLabel: i18n.t("dockContextMenu.quit"),
9034
9982
  showCreateNew: canCreateNewWindow(
9035
9983
  popupEntry.entry,
9036
- resolveDockEntryInstanceMode(popupEntry.entry, nodeDefinitions)
9984
+ dockContextMenuInstanceMode
9037
9985
  ),
9038
- resolveDockPreviewCacheKey: (node) => resolveDockPreviewCacheKey(workspaceId, node)
9986
+ showOpen: canOpenFromDockContextMenu,
9987
+ showAllWindowsLabel: i18n.t("dockContextMenu.showAllWindows"),
9988
+ resolveDockPreviewCacheKey: (node) => resolveDockPreviewCacheKey(workspaceId, node),
9989
+ variant: activePopup.kind === "context-menu" ? "context-menu" : "default"
9039
9990
  }
9040
9991
  ) : null,
9041
9992
  activeMinimizedStackSlot && activeMinimizedStackPopup ? /* @__PURE__ */ jsx10(
@@ -9055,6 +10006,7 @@ function WorkbenchHostDock({
9055
10006
  node,
9056
10007
  workspaceId
9057
10008
  });
10009
+ const minimizedDock = nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock;
9058
10010
  return {
9059
10011
  externalNodeState: externalState.externalNodeState,
9060
10012
  externalWorkspaceState: externalState.externalWorkspaceState,
@@ -9062,10 +10014,8 @@ function WorkbenchHostDock({
9062
10014
  isFocused: context.focusedNodeId === node.id,
9063
10015
  isMinimized: true,
9064
10016
  node,
9065
- preview: nodeDefinitions.get(node.data.typeId)?.window?.minimizedDock?.capturePreview ? null : (() => {
9066
- const previewImageUrl = readCachedWorkbenchNodePreviewImage(
9067
- node.id
9068
- );
10017
+ preview: minimizedDock?.kind === "component" ? provideMinimizedNodePreview(node) ?? null : minimizedDock?.kind === "snapshot" && minimizedDock.capturePreview ? null : (() => {
10018
+ const previewImageUrl = readCachedWorkbenchNodePreviewImage(node.id);
9069
10019
  return previewImageUrl ? { kind: "image", src: previewImageUrl } : null;
9070
10020
  })(),
9071
10021
  previewRevision: null,
@@ -9133,14 +10083,69 @@ function WorkbenchHostDock({
9133
10083
  function WorkbenchHostDockMinimizedNodePreview({
9134
10084
  capturePreview,
9135
10085
  className,
10086
+ deferPreview = false,
9136
10087
  dockPreviewCache,
9137
10088
  node,
10089
+ providePreview,
9138
10090
  workspaceId
9139
10091
  }) {
10092
+ const [componentPreview, setComponentPreview] = useState8(void 0);
9140
10093
  const [previewImageUrl, setPreviewImageUrl] = useState8(
9141
- () => readCachedWorkbenchNodePreviewImage(node.id)
10094
+ () => deferPreview || providePreview ? null : readCachedWorkbenchNodePreviewImage(node.id)
9142
10095
  );
9143
10096
  useEffect9(() => {
10097
+ if (deferPreview || !providePreview || componentPreview !== void 0) {
10098
+ return void 0;
10099
+ }
10100
+ let cancelled = false;
10101
+ let frameId = null;
10102
+ let idleId = null;
10103
+ let timeoutId = null;
10104
+ const setDeferredComponentPreview = () => {
10105
+ if (cancelled) {
10106
+ return;
10107
+ }
10108
+ setComponentPreview(providePreview(node) ?? null);
10109
+ };
10110
+ const scheduler = globalThis;
10111
+ if (typeof scheduler.requestIdleCallback === "function") {
10112
+ idleId = scheduler.requestIdleCallback(setDeferredComponentPreview, {
10113
+ timeout: 250
10114
+ });
10115
+ } else if (typeof scheduler.requestAnimationFrame === "function") {
10116
+ frameId = scheduler.requestAnimationFrame(() => {
10117
+ frameId = null;
10118
+ timeoutId = globalThis.setTimeout(setDeferredComponentPreview, 0);
10119
+ });
10120
+ } else {
10121
+ timeoutId = globalThis.setTimeout(setDeferredComponentPreview, 0);
10122
+ }
10123
+ return () => {
10124
+ cancelled = true;
10125
+ if (idleId !== null && typeof scheduler.cancelIdleCallback === "function") {
10126
+ scheduler.cancelIdleCallback(idleId);
10127
+ }
10128
+ if (frameId !== null && typeof scheduler.cancelAnimationFrame === "function") {
10129
+ scheduler.cancelAnimationFrame(frameId);
10130
+ }
10131
+ if (timeoutId !== null) {
10132
+ globalThis.clearTimeout(timeoutId);
10133
+ }
10134
+ };
10135
+ }, [
10136
+ componentPreview,
10137
+ deferPreview,
10138
+ node.data.instanceId,
10139
+ node.data.instanceKey,
10140
+ node.data.typeId,
10141
+ node.id,
10142
+ node.minimizedAtUnixMs,
10143
+ providePreview
10144
+ ]);
10145
+ useEffect9(() => {
10146
+ if (deferPreview || providePreview) {
10147
+ return void 0;
10148
+ }
9144
10149
  let cancelled = false;
9145
10150
  const cachedPreviewImageUrl = readCachedWorkbenchNodePreviewImage(node.id);
9146
10151
  setPreviewImageUrl(cachedPreviewImageUrl);
@@ -9182,14 +10187,22 @@ function WorkbenchHostDockMinimizedNodePreview({
9182
10187
  };
9183
10188
  }, [
9184
10189
  capturePreview,
10190
+ deferPreview,
9185
10191
  dockPreviewCache,
9186
10192
  node.data.instanceId,
9187
10193
  node.data.instanceKey,
9188
10194
  node.data.typeId,
9189
10195
  node.id,
9190
10196
  node.minimizedAtUnixMs,
10197
+ providePreview,
9191
10198
  workspaceId
9192
10199
  ]);
10200
+ if (deferPreview) {
10201
+ return renderMinimizedDockPreviewPlaceholder(className);
10202
+ }
10203
+ if (componentPreview) {
10204
+ return renderMinimizedDockPreviewContent(componentPreview, className);
10205
+ }
9193
10206
  if (previewImageUrl) {
9194
10207
  return /* @__PURE__ */ jsx10(
9195
10208
  "span",
@@ -9212,6 +10225,9 @@ function WorkbenchHostDockMinimizedNodePreview({
9212
10225
  }
9213
10226
  );
9214
10227
  }
10228
+ return renderMinimizedDockPreviewPlaceholder(className);
10229
+ }
10230
+ function renderMinimizedDockPreviewPlaceholder(className) {
9215
10231
  return /* @__PURE__ */ jsxs6(
9216
10232
  "span",
9217
10233
  {
@@ -9225,9 +10241,101 @@ function WorkbenchHostDockMinimizedNodePreview({
9225
10241
  }
9226
10242
  );
9227
10243
  }
10244
+ function renderMinimizedDockPreviewContent(preview, className) {
10245
+ if (preview.kind === "image") {
10246
+ return /* @__PURE__ */ jsx10(
10247
+ "span",
10248
+ {
10249
+ className: [
10250
+ "desktop-dock__minimized-preview",
10251
+ "desktop-dock__minimized-preview--snapshot",
10252
+ className
10253
+ ].filter(Boolean).join(" "),
10254
+ "aria-hidden": "true",
10255
+ children: /* @__PURE__ */ jsx10(
10256
+ "img",
10257
+ {
10258
+ alt: "",
10259
+ className: "desktop-dock__minimized-preview-image",
10260
+ draggable: false,
10261
+ src: preview.src
10262
+ }
10263
+ )
10264
+ }
10265
+ );
10266
+ }
10267
+ return /* @__PURE__ */ jsx10(
10268
+ WorkbenchHostDockFrozenComponentPreview,
10269
+ {
10270
+ className,
10271
+ preview
10272
+ }
10273
+ );
10274
+ }
10275
+ function WorkbenchHostDockFrozenComponentPreview({
10276
+ className,
10277
+ preview
10278
+ }) {
10279
+ const sourceRef = useRef8(null);
10280
+ const [frozenMarkup, setFrozenMarkup] = useState8(null);
10281
+ useLayoutEffect5(() => {
10282
+ if (frozenMarkup !== null) {
10283
+ return;
10284
+ }
10285
+ setFrozenMarkup(sourceRef.current?.innerHTML ?? "");
10286
+ }, [frozenMarkup]);
10287
+ return /* @__PURE__ */ jsx10(
10288
+ "span",
10289
+ {
10290
+ className: [
10291
+ "desktop-dock__minimized-preview",
10292
+ "desktop-dock__minimized-preview--component",
10293
+ className
10294
+ ].filter(Boolean).join(" "),
10295
+ "aria-hidden": "true",
10296
+ children: frozenMarkup === null ? /* @__PURE__ */ jsx10(
10297
+ "span",
10298
+ {
10299
+ ref: sourceRef,
10300
+ className: "desktop-dock__minimized-preview-freeze-source",
10301
+ children: preview.element
10302
+ }
10303
+ ) : /* @__PURE__ */ jsx10(
10304
+ "span",
10305
+ {
10306
+ className: "desktop-dock__minimized-preview-frozen-content",
10307
+ dangerouslySetInnerHTML: { __html: frozenMarkup }
10308
+ }
10309
+ )
10310
+ }
10311
+ );
10312
+ }
10313
+ function minimizedDockPreviewFreezeKey(node) {
10314
+ return `${node.id}:${node.minimizedAtUnixMs ?? "pending"}`;
10315
+ }
9228
10316
  function dockEntryHasHoverPanel(entry) {
9229
10317
  return Boolean(entry.state?.reason?.trim()) || (entry.hoverActions?.length ?? 0) > 0;
9230
10318
  }
10319
+ function dockEntryHasContextMenu(entry, resolvedEntry) {
10320
+ if (resolvedEntry.matchedNodes.length > 0 || entry.dockRetention) {
10321
+ return true;
10322
+ }
10323
+ return entry.clickActionId === void 0;
10324
+ }
10325
+ function resolveDockContextMenuFullscreenNode({
10326
+ focusedNodeId,
10327
+ minimizedNodeIDs,
10328
+ nodeDefinitions,
10329
+ nodes
10330
+ }) {
10331
+ const candidates = nodes.filter((node) => {
10332
+ if (node.displayMode === "fullscreen" || minimizedNodeIDs.has(node.id)) {
10333
+ return false;
10334
+ }
10335
+ return nodeDefinitions.get(node.data.typeId)?.window?.fullscreenable !== false;
10336
+ });
10337
+ return candidates.find((node) => node.id === focusedNodeId) ?? candidates[0] ?? null;
10338
+ }
9231
10339
  function dockLabelTooltipTarget(key, label) {
9232
10340
  return { key, label: label.trim() };
9233
10341
  }
@@ -9347,23 +10455,25 @@ function WorkbenchHostDockHoverPanel({
9347
10455
  next.add(actionKey);
9348
10456
  return next;
9349
10457
  });
9350
- void Promise.resolve(
9351
- onDockEntryAction?.({
9352
- actionId: action.id,
9353
- entryId: entry.id,
9354
- host
9355
- })
9356
- ).catch(() => {
9357
- }).finally(() => {
9358
- setPendingActionKeys((current) => {
9359
- if (!current.has(actionKey)) {
9360
- return current;
9361
- }
9362
- const next = new Set(current);
9363
- next.delete(actionKey);
9364
- return next;
9365
- });
9366
- });
10458
+ void (async () => {
10459
+ try {
10460
+ await onDockEntryAction?.({
10461
+ actionId: action.id,
10462
+ entryId: entry.id,
10463
+ host
10464
+ });
10465
+ } catch {
10466
+ } finally {
10467
+ setPendingActionKeys((current) => {
10468
+ if (!current.has(actionKey)) {
10469
+ return current;
10470
+ }
10471
+ const next = new Set(current);
10472
+ next.delete(actionKey);
10473
+ return next;
10474
+ });
10475
+ }
10476
+ })();
9367
10477
  },
9368
10478
  children: isPending ? action.pendingLabel ?? action.label : action.label
9369
10479
  },
@@ -9838,6 +10948,7 @@ function dockWallpaperToneMapsEqual(left, right) {
9838
10948
  return true;
9839
10949
  }
9840
10950
  var DOCK_BOUNCE_MS = 600;
10951
+ var DOCK_ENTRY_CLICK_THROTTLE_MS = DOCK_BOUNCE_MS;
9841
10952
  function useDockBounce(slotRefs) {
9842
10953
  const timeoutsRef = useRef8(/* @__PURE__ */ new Map());
9843
10954
  const clearDockBounce = useCallback10(
@@ -10027,8 +11138,8 @@ function createWorkbenchHostNodeHeaderContext({
10027
11138
  }
10028
11139
 
10029
11140
  // src/host/WorkbenchHostWindowActions.tsx
10030
- import { Button as Button5, CloseIcon as CloseIcon2, MinimizeIcon } from "@tutti-os/ui-system";
10031
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
11141
+ import { Button as Button5, CloseIcon as CloseIcon2, MinimizeIcon as MinimizeIcon2 } from "@tutti-os/ui-system";
11142
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
10032
11143
  function WorkbenchHostWindowActions({
10033
11144
  context,
10034
11145
  host,
@@ -10041,7 +11152,7 @@ function WorkbenchHostWindowActions({
10041
11152
  }
10042
11153
  const minimizable = definition.window?.minimizable !== false;
10043
11154
  const closable = definition.window?.closable !== false;
10044
- return /* @__PURE__ */ jsxs7(Fragment3, { children: [
11155
+ return /* @__PURE__ */ jsxs7(Fragment4, { children: [
10045
11156
  minimizable ? /* @__PURE__ */ jsx11(
10046
11157
  Button5,
10047
11158
  {
@@ -10057,7 +11168,7 @@ function WorkbenchHostWindowActions({
10057
11168
  () => context.controller.commands.minimizeNode(context.node.id)
10058
11169
  );
10059
11170
  },
10060
- children: /* @__PURE__ */ jsx11(MinimizeIcon, { className: "size-3.5" })
11171
+ children: /* @__PURE__ */ jsx11(MinimizeIcon2, { className: "size-3.5" })
10061
11172
  }
10062
11173
  ) : null,
10063
11174
  closable ? /* @__PURE__ */ jsx11(
@@ -10140,7 +11251,8 @@ function useWorkbenchHostSurfaceRenderers(input) {
10140
11251
  const captureNodePreviewImage = useCallback11(
10141
11252
  async (node) => {
10142
11253
  const definition = input.nodeDefinitionByType.get(node.data.typeId);
10143
- const capturePreview = definition?.window?.minimizedDock?.capturePreview;
11254
+ const minimizedDock = definition?.window?.minimizedDock;
11255
+ const capturePreview = minimizedDock?.kind === "snapshot" ? minimizedDock.capturePreview : void 0;
10144
11256
  const snapshot = input.hostSession.getSnapshot();
10145
11257
  const externalState = readWorkbenchHostExternalState({
10146
11258
  externalStateSource: input.externalStateSource,
@@ -10198,6 +11310,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
10198
11310
  nodeDefinitions: input.nodeDefinitionByType,
10199
11311
  onDockEntryAction: input.onDockEntryAction,
10200
11312
  onDockEntryClick: input.onDockEntryClick,
11313
+ onMissionControlRequestOpen: input.onMissionControlRequestOpen,
10201
11314
  workspaceId: input.workspaceId
10202
11315
  }
10203
11316
  )
@@ -10216,6 +11329,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
10216
11329
  input.nodeDefinitionByType,
10217
11330
  input.onDockEntryAction,
10218
11331
  input.onDockEntryClick,
11332
+ input.onMissionControlRequestOpen,
10219
11333
  input.workspaceId
10220
11334
  ]
10221
11335
  );
@@ -10305,17 +11419,30 @@ function useWorkbenchHostSurfaceRenderers(input) {
10305
11419
  },
10306
11420
  [input.nodeDefinitionByType]
10307
11421
  );
11422
+ const shouldCaptureNodePreviewImage = useCallback11(
11423
+ (node) => {
11424
+ const minimizedDock = input.nodeDefinitionByType.get(node.data.typeId)?.window?.minimizedDock;
11425
+ return minimizedDock?.kind !== "component";
11426
+ },
11427
+ [input.nodeDefinitionByType]
11428
+ );
10308
11429
  const resolveDockAnchorKey = useCallback11(
10309
11430
  (node) => {
10310
11431
  if (node.isMinimized && isWorkbenchMinimizedDockEligibleNode({
10311
11432
  node,
10312
11433
  nodeDefinitions: input.nodeDefinitionByType
10313
11434
  })) {
11435
+ const snapshotNodes = input.hostSession.getSnapshot().nodes;
11436
+ const slotNodes = snapshotNodes.some(
11437
+ (snapshotNode) => snapshotNode.id === node.id
11438
+ ) ? snapshotNodes.map(
11439
+ (snapshotNode) => snapshotNode.id === node.id ? node : snapshotNode
11440
+ ) : [...snapshotNodes, node];
10314
11441
  const minimizedAnchorKey = resolveWorkbenchMinimizedDockAnchorKeyForNode({
10315
11442
  nodeId: node.id,
10316
11443
  slots: resolveWorkbenchMinimizedDockSlots({
10317
11444
  nodeDefinitions: input.nodeDefinitionByType,
10318
- nodes: input.hostSession.getSnapshot().nodes
11445
+ nodes: slotNodes
10319
11446
  })
10320
11447
  });
10321
11448
  if (minimizedAnchorKey) {
@@ -10358,6 +11485,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
10358
11485
  renderTopChrome,
10359
11486
  renderWindowActions,
10360
11487
  renderWindowHeader,
11488
+ shouldCaptureNodePreviewImage,
10361
11489
  shouldKeepMinimizedNodeMounted,
10362
11490
  resolveDockAnchorKey,
10363
11491
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
@@ -10537,12 +11665,14 @@ function WorkbenchHost({
10537
11665
  i18n,
10538
11666
  layoutConstraints,
10539
11667
  missionControl,
11668
+ minimizeAnimation,
10540
11669
  nodes,
10541
11670
  onDockEntryAction,
10542
11671
  onDockEntryClick,
10543
11672
  onHandleReady,
10544
11673
  onLaunchRequest,
10545
11674
  onMissionControlAdapterReady,
11675
+ onMissionControlRequestOpen,
10546
11676
  onNodeCloseRequest,
10547
11677
  projectedNodes,
10548
11678
  renderBottomChrome,
@@ -10576,6 +11706,7 @@ function WorkbenchHost({
10576
11706
  [contributions, dockEntries]
10577
11707
  );
10578
11708
  const missionControlMode = missionControl?.mode ?? null;
11709
+ const missionControlNodeIds = missionControl?.nodeIds;
10579
11710
  const missionControlClose = missionControl?.onRequestClose ?? noop3;
10580
11711
  const missionControlEnabled = missionControlMode !== null || onMissionControlAdapterReady !== void 0;
10581
11712
  const {
@@ -10617,6 +11748,7 @@ function WorkbenchHost({
10617
11748
  nodeDefinitionByType,
10618
11749
  onDockEntryAction,
10619
11750
  onDockEntryClick,
11751
+ onMissionControlRequestOpen,
10620
11752
  renderBottomChrome,
10621
11753
  renderTopChrome,
10622
11754
  workspaceId
@@ -10624,6 +11756,7 @@ function WorkbenchHost({
10624
11756
  const missionControlState = useWorkbenchMissionControlState({
10625
11757
  adapter: missionControlAdapter,
10626
11758
  mode: missionControlMode,
11759
+ nodeIds: missionControlNodeIds,
10627
11760
  onRequestClose: missionControlClose
10628
11761
  });
10629
11762
  const missionControlPresence = useWorkbenchMissionControlPresence(missionControlState);
@@ -10639,6 +11772,7 @@ function WorkbenchHost({
10639
11772
  interactive: !isHydrating,
10640
11773
  layoutConstraints,
10641
11774
  missionControlPhase: missionControlPresence.phase,
11775
+ minimizeAnimation,
10642
11776
  presentation: missionControlState?.presentation ?? null,
10643
11777
  renderBackdrop: missionControlRenderedState ? () => /* @__PURE__ */ jsx13(
10644
11778
  WorkbenchMissionControlBackdrop,
@@ -10666,6 +11800,7 @@ function WorkbenchHost({
10666
11800
  resolveFullscreenHeaderMode: surfaceRenderers.resolveFullscreenHeaderMode,
10667
11801
  resolveDockAnchorKey: surfaceRenderers.resolveDockAnchorKey,
10668
11802
  shortcutsEnabled,
11803
+ shouldCaptureNodePreviewImage: surfaceRenderers.shouldCaptureNodePreviewImage,
10669
11804
  wallpaper,
10670
11805
  windowChromeI18n,
10671
11806
  windowChromeMode: surfaceRenderers.windowChromeMode