@tutti-os/workbench-surface 0.0.18 → 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) {
@@ -2106,7 +2125,8 @@ function WorkbenchWindowFrame({
2106
2125
  const resolvedFullscreenHeaderMode = fullscreenHeaderMode ?? "reveal";
2107
2126
  const presentationMode = presentation?.mode ?? null;
2108
2127
  const presentationFrame = presentation?.frameByNodeId.get(node.id) ?? null;
2109
- 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;
2110
2130
  const isMissionControlSelected = presentationInteraction?.selectedNodeIds.has(node.id) ?? false;
2111
2131
  const presentationScale = presentationMode === "mission-control" && presentationFrame ? Math.min(
2112
2132
  presentationFrame.width / Math.max(1, node.frame.width),
@@ -2120,11 +2140,11 @@ function WorkbenchWindowFrame({
2120
2140
  0,
2121
2141
  (presentationFrame.height - node.frame.height * presentationScale) / 2
2122
2142
  ) - node.frame.y : 0;
2123
- 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;
2124
2144
  return /* @__PURE__ */ jsxs2(
2125
2145
  "section",
2126
2146
  {
2127
- "aria-hidden": hiddenMounted ? true : void 0,
2147
+ "aria-hidden": hiddenMounted || isPresentationHidden ? true : void 0,
2128
2148
  className: "workbench-window-shell",
2129
2149
  "data-focused": isFocused ? "true" : "false",
2130
2150
  "data-display-mode": node.displayMode,
@@ -2132,6 +2152,7 @@ function WorkbenchWindowFrame({
2132
2152
  "data-launch-source": resolveWorkbenchNodeLaunchSource(node.data),
2133
2153
  "data-minimized-mount": hiddenMounted ? "hidden" : "visible",
2134
2154
  "data-presentation-mode": presentationMode ?? "default",
2155
+ "data-presentation-visibility": isPresentationHidden ? "hidden" : "visible",
2135
2156
  "data-slot": "viewport-menu-boundary",
2136
2157
  "data-workbench-node-type-id": resolveWorkbenchNodeTypeId(node.data),
2137
2158
  "data-workbench-window-id": node.id,
@@ -2146,7 +2167,7 @@ function WorkbenchWindowFrame({
2146
2167
  width: node.frame.width,
2147
2168
  zIndex
2148
2169
  },
2149
- 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),
2150
2171
  children: [
2151
2172
  /* @__PURE__ */ jsx5("div", { className: "workbench-window-shell__content", children: /* @__PURE__ */ jsxs2(
2152
2173
  "div",
@@ -2525,41 +2546,143 @@ function resolveGenieDirtyRect({
2525
2546
  width: Math.max(0, right - left)
2526
2547
  };
2527
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
+ }
2528
2576
  function canInlineComputedStyle(element) {
2529
2577
  return element instanceof HTMLElement || typeof SVGElement !== "undefined" && element instanceof SVGElement;
2530
2578
  }
2531
- function copyGenieComputedStyleTree(source, clone) {
2532
- if (canInlineComputedStyle(source) && canInlineComputedStyle(clone)) {
2533
- const computed = window.getComputedStyle(source);
2534
- if (computed.cssText) {
2535
- clone.style.cssText = computed.cssText;
2536
- } else {
2537
- for (let index = 0; index < computed.length; index += 1) {
2538
- const propertyName = computed.item(index);
2539
- if (!propertyName) {
2540
- continue;
2541
- }
2542
- clone.style.setProperty(
2543
- propertyName,
2544
- computed.getPropertyValue(propertyName),
2545
- computed.getPropertyPriority(propertyName)
2546
- );
2547
- }
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);
2548
2640
  }
2549
- clone.style.animation = "none";
2550
- clone.style.opacity = "1";
2551
- clone.style.transition = "none";
2552
- clone.style.visibility = "visible";
2553
2641
  }
2554
- const sourceChildren = Array.from(source.children);
2555
- const cloneChildren = Array.from(clone.children);
2556
- for (let index = 0; index < sourceChildren.length; index += 1) {
2557
- const sourceChild = sourceChildren[index];
2558
- const cloneChild = cloneChildren[index];
2559
- if (sourceChild && cloneChild) {
2560
- 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
+ );
2561
2672
  }
2562
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 };
2563
2686
  }
2564
2687
  function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2565
2688
  const { direction, dockPoint, texture, textureRect } = frame;
@@ -2583,16 +2706,13 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2583
2706
  const sourceHeight = Math.min(scanlineStride, textureHeight - y);
2584
2707
  const sourceMidY = y + sourceHeight / 2;
2585
2708
  const rowProgress = sourceMidY / textureHeight;
2586
- const horizontalStart = direction === "minimize" ? (1 - rowProgress) * genieHorizontalRowStagger : rowProgress * genieHorizontalRowStagger;
2587
- const horizontalProgress = clampGenieProgress(
2588
- (progress - horizontalStart) / (1 - horizontalStart)
2589
- );
2709
+ const horizontalProgress = resolveGenieRowProgress({
2710
+ direction,
2711
+ progress,
2712
+ rowProgress,
2713
+ stagger: genieHorizontalRowStagger
2714
+ });
2590
2715
  const horizontalEase = easeInOutCubic(horizontalProgress);
2591
- const verticalStart = direction === "minimize" ? (1 - rowProgress) * genieVerticalRowStagger : rowProgress * genieVerticalRowStagger;
2592
- const verticalProgress = clampGenieProgress(
2593
- (progress - verticalStart) / (1 - verticalStart)
2594
- );
2595
- const verticalEase = easeInQuadratic(verticalProgress);
2596
2716
  const left = direction === "minimize" ? lerpGenieValue(textureRect.left, dockPoint.x, horizontalEase) : lerpGenieValue(dockPoint.x, textureRect.left, horizontalEase);
2597
2717
  const right = direction === "minimize" ? lerpGenieValue(
2598
2718
  textureRect.left + textureWidth,
@@ -2603,15 +2723,24 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2603
2723
  textureRect.left + textureWidth,
2604
2724
  horizontalEase
2605
2725
  );
2606
- const targetY = (direction === "minimize" ? lerpGenieValue(
2607
- textureRect.top + sourceMidY,
2608
- dockPoint.y,
2609
- verticalEase
2610
- ) : lerpGenieValue(
2611
- dockPoint.y,
2612
- textureRect.top + sourceMidY,
2613
- verticalEase
2614
- )) - 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);
2615
2744
  const rowWidth = right - left;
2616
2745
  if (rowWidth < 0.8) {
2617
2746
  continue;
@@ -2625,7 +2754,7 @@ function renderGenieScanlines(context, viewportWidth, viewportHeight, frame) {
2625
2754
  left,
2626
2755
  targetY,
2627
2756
  rowWidth,
2628
- Math.max(1, sourceHeight)
2757
+ targetHeight
2629
2758
  );
2630
2759
  }
2631
2760
  const glowProgress = direction === "minimize" ? progress : 1 - progress;
@@ -2664,6 +2793,7 @@ var dockPreviewMaxHeight = 170;
2664
2793
  var dockPreviewImageCacheMaxEntries = 96;
2665
2794
  var inlineImageResourceCacheMaxEntries = 160;
2666
2795
  var dockAnchorFallbackSizePx = 43.2;
2796
+ var genieInlineImageMaxDevicePixelRatio = 2;
2667
2797
  var dockPreviewImageByNodeID = /* @__PURE__ */ new Map();
2668
2798
  var inlineImageResourceByUrl = /* @__PURE__ */ new Map();
2669
2799
  function resolveWorkbenchCaptureElement(windowElement) {
@@ -2704,9 +2834,6 @@ function blobToDataUrl(blob) {
2704
2834
  reader.readAsDataURL(blob);
2705
2835
  });
2706
2836
  }
2707
- function resolveImageResourceUrl(image) {
2708
- return image.currentSrc || image.src || image.getAttribute("src") || null;
2709
- }
2710
2837
  async function fetchInlineImageResource(imageUrl) {
2711
2838
  try {
2712
2839
  const response = await fetch(imageUrl);
@@ -2742,21 +2869,31 @@ function readInlineImageResource(imageUrl) {
2742
2869
  }
2743
2870
  async function inlineCloneImageResources({
2744
2871
  cloneRoot,
2745
- imageUrls
2872
+ images
2746
2873
  }) {
2747
2874
  const cloneImages = Array.from(cloneRoot.querySelectorAll("img"));
2748
2875
  await Promise.all(
2749
- imageUrls.map(async (imageUrl, index) => {
2876
+ images.map(async (imageInfo, index) => {
2750
2877
  const cloneImage = cloneImages[index];
2751
2878
  if (!cloneImage) {
2752
2879
  return;
2753
2880
  }
2754
2881
  cloneImage.removeAttribute("srcset");
2755
2882
  cloneImage.removeAttribute("sizes");
2883
+ const imageUrl = imageInfo.url;
2756
2884
  if (!imageUrl) {
2757
2885
  return;
2758
2886
  }
2759
- 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;
2760
2897
  })
2761
2898
  );
2762
2899
  }
@@ -2766,16 +2903,61 @@ async function loadImageFromSvg(svg) {
2766
2903
  await image.decode();
2767
2904
  return image;
2768
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
+ }
2769
2951
  function prepareElementTextureCapture(element) {
2770
2952
  const windowRect = viewportRectFromElement(element);
2771
2953
  if (!isUsableGenieRect(windowRect)) {
2772
2954
  return null;
2773
2955
  }
2774
- const imageUrls = Array.from(element.querySelectorAll("img")).map(
2775
- resolveImageResourceUrl
2776
- );
2777
- const clone = element.cloneNode(true);
2778
- copyGenieComputedStyleTree(element, clone);
2956
+ const clonedElement = cloneMeaningfulGenieElement(element, windowRect);
2957
+ if (!clonedElement) {
2958
+ return null;
2959
+ }
2960
+ const { clone, images } = clonedElement;
2779
2961
  clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2780
2962
  clone.style.position = "relative";
2781
2963
  clone.style.left = "0";
@@ -2786,15 +2968,23 @@ function prepareElementTextureCapture(element) {
2786
2968
  clone.style.opacity = "1";
2787
2969
  clone.style.visibility = "visible";
2788
2970
  clone.style.pointerEvents = "none";
2789
- return { clone, imageUrls, rect: windowRect };
2971
+ return {
2972
+ clone,
2973
+ images,
2974
+ rect: windowRect
2975
+ };
2790
2976
  }
2791
2977
  async function renderPreparedElementTexture({
2792
2978
  clone,
2793
- imageUrls,
2979
+ images,
2794
2980
  rect
2795
2981
  }) {
2796
- await inlineCloneImageResources({ cloneRoot: clone, imageUrls });
2797
- 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);
2798
2988
  const canvas = document.createElement("canvas");
2799
2989
  canvas.width = Math.max(1, Math.round(rect.width * genieSnapshotScale));
2800
2990
  canvas.height = Math.max(1, Math.round(rect.height * genieSnapshotScale));
@@ -2934,7 +3124,7 @@ function useWorkbenchGenieAnimation({
2934
3124
  captureNodePreviewImage,
2935
3125
  controller,
2936
3126
  dockPreviewCache,
2937
- minimizeAnimation = "scale",
3127
+ minimizeAnimation = "genie",
2938
3128
  resolveDockAnchorKey,
2939
3129
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
2940
3130
  shouldCaptureNodePreviewImage
@@ -6663,7 +6853,18 @@ import {
6663
6853
  useState as useState7
6664
6854
  } from "react";
6665
6855
  import { createPortal as createPortal2 } from "react-dom";
6666
- 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";
6667
6868
 
6668
6869
  // src/host/minimizedStackScroll.ts
6669
6870
  var minimizedStackViewportMarginPx = 56;
@@ -6795,7 +6996,7 @@ function resolveMinimizedStackTrackTranslateXPx(input) {
6795
6996
  }
6796
6997
 
6797
6998
  // src/host/WorkbenchHostDockPopup.tsx
6798
- 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";
6799
7000
  var dockPopupCardWidthPx = 165;
6800
7001
  var dockPopupGridGapPx = 8;
6801
7002
  var dockPopupPanelPaddingInlinePx = 12;
@@ -6860,9 +7061,14 @@ function writeDockPopupPreviewImage(memoryKey, preview, revision) {
6860
7061
  }
6861
7062
  function WorkbenchHostDockPopup({
6862
7063
  anchorRect,
7064
+ canEnterFullscreen,
7065
+ canShowAllWindows,
6863
7066
  capturePreview,
6864
7067
  debugDiagnostics,
7068
+ dockRetention,
6865
7069
  dockPreviewCache,
7070
+ fullscreenLabel,
7071
+ hideLabel,
6866
7072
  items,
6867
7073
  label,
6868
7074
  labelMode,
@@ -6871,15 +7077,25 @@ function WorkbenchHostDockPopup({
6871
7077
  onClose,
6872
7078
  onCloseNode,
6873
7079
  onCreateNew,
7080
+ onEnterFullscreen,
7081
+ onHide,
7082
+ onRunDockRetentionAction,
6874
7083
  onSelectNode,
7084
+ onShowAllWindows,
7085
+ openLabel,
7086
+ onQuit,
6875
7087
  placement = "bottom",
7088
+ quitLabel,
6876
7089
  resolveDockPreviewCacheKey: resolveDockPreviewCacheKey2,
7090
+ showAllWindowsLabel,
6877
7091
  showCreateNew,
7092
+ showOpen,
6878
7093
  variant
6879
7094
  }) {
6880
7095
  const resolvedLabelMode = labelMode ?? "hover-overlay";
6881
7096
  const resolvedVariant = variant ?? "default";
6882
7097
  const isMinimizedStack = resolvedVariant === "minimized-stack";
7098
+ const isContextMenu = resolvedVariant === "context-menu";
6883
7099
  const createCardCount = showCreateNew === false ? 0 : 1;
6884
7100
  const cardElementsRef = useRef7(/* @__PURE__ */ new Map());
6885
7101
  const cardRefCallbacksRef = useRef7(
@@ -6890,8 +7106,8 @@ function WorkbenchHostDockPopup({
6890
7106
  const [pointer, setPointer] = useState7(null);
6891
7107
  const [minimizedStackScrollOffset, setMinimizedStackScrollOffset] = useState7(0);
6892
7108
  const [capturedPreviewByMemoryKey, setCapturedPreviewByMemoryKey] = useState7({});
6893
- const columnCount = Math.min(Math.max(items.length + createCardCount, 1), 3);
6894
- 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;
6895
7111
  const popupCenterY = anchorRect.top + anchorRect.height / 2;
6896
7112
  const isLeftMinimizedStack = placement === "left" && isMinimizedStack;
6897
7113
  const minimizedStackTrackHeightPx = resolveMinimizedStackTrackHeightPx(
@@ -7069,7 +7285,7 @@ function WorkbenchHostDockPopup({
7069
7285
  return () => viewport.removeEventListener("wheel", handleWheel);
7070
7286
  }, [isMinimizedStack, minimizedStackMaxScrollOffset]);
7071
7287
  useEffect8(() => {
7072
- if (!capturePreview) {
7288
+ if (!capturePreview || isContextMenu) {
7073
7289
  return;
7074
7290
  }
7075
7291
  let cancelled = false;
@@ -7280,6 +7496,7 @@ function WorkbenchHostDockPopup({
7280
7496
  }, [
7281
7497
  capturePreview,
7282
7498
  dockPreviewCache,
7499
+ isContextMenu,
7283
7500
  items,
7284
7501
  previewCaptureKey,
7285
7502
  resolveDockPreviewCacheKey2
@@ -7293,12 +7510,13 @@ function WorkbenchHostDockPopup({
7293
7510
  "data-desktop-dock-popup-root": "true",
7294
7511
  "data-popup-variant": resolvedVariant,
7295
7512
  style: popupStyle,
7296
- children: /* @__PURE__ */ jsxs5(
7513
+ children: /* @__PURE__ */ jsx9(
7297
7514
  "div",
7298
7515
  {
7299
7516
  "aria-label": label,
7300
7517
  className: cn(
7301
- "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",
7302
7520
  isLeftMinimizedStack ? "w-full min-w-0 max-w-none" : "w-[min(var(--desktop-dock-popup-width,366px),calc(100vw-32px))]"
7303
7521
  ),
7304
7522
  "data-desktop-dock-popup-panel": "true",
@@ -7308,7 +7526,30 @@ function WorkbenchHostDockPopup({
7308
7526
  onPointerLeave: isMinimizedStack ? () => setPointer(null) : void 0,
7309
7527
  role: "dialog",
7310
7528
  style: panelStyle,
7311
- children: [
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: [
7312
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 }) }),
7313
7554
  isMinimizedStack ? /* @__PURE__ */ jsx9(
7314
7555
  "div",
@@ -7414,7 +7655,7 @@ function WorkbenchHostDockPopup({
7414
7655
  }
7415
7656
  ) : null
7416
7657
  ] })
7417
- ]
7658
+ ] })
7418
7659
  }
7419
7660
  )
7420
7661
  }
@@ -7424,6 +7665,172 @@ function WorkbenchHostDockPopup({
7424
7665
  }
7425
7666
  return createPortal2(content, document.body);
7426
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
+ }
7427
7834
  function readPersistedDockPreview(dockPreviewCache, cacheKey) {
7428
7835
  if (!cacheKey) {
7429
7836
  return Promise.resolve(null);
@@ -7619,7 +8026,7 @@ var WorkbenchHostDockPopupCard = forwardRef(function WorkbenchHostDockPopupCard2
7619
8026
  "span",
7620
8027
  {
7621
8028
  "aria-hidden": "true",
7622
- 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)]",
7623
8030
  "data-desktop-dock-popup-card-active-overlay": "true"
7624
8031
  }
7625
8032
  ) : null,
@@ -7723,6 +8130,7 @@ function WorkbenchHostDock({
7723
8130
  nodeDefinitions,
7724
8131
  onDockEntryAction,
7725
8132
  onDockEntryClick,
8133
+ onMissionControlRequestOpen,
7726
8134
  workspaceId
7727
8135
  }) {
7728
8136
  const minimizedNodeIDs = useMemo5(
@@ -8660,12 +9068,59 @@ function WorkbenchHostDock({
8660
9068
  dockSlotRefCallbacksRef.current.set(anchorKey, callback);
8661
9069
  return callback;
8662
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
+ );
8663
9104
  const popupEntry = activePopup === null ? null : resolvedEntries.find(
8664
9105
  (entry) => entry.entry.id === activePopup.entryId
8665
9106
  ) ?? null;
8666
9107
  const activeMinimizedStackSlot = activeMinimizedStackPopup === null ? null : minimizedDockSlots.find(
8667
9108
  (slot) => slot.kind === "stack"
8668
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";
8669
9124
  return /* @__PURE__ */ jsxs6(
8670
9125
  "div",
8671
9126
  {
@@ -8766,7 +9221,10 @@ function WorkbenchHostDock({
8766
9221
  className: "desktop-dock__btn",
8767
9222
  "data-interactive": clickResolution.kind === "blocked" ? "false" : "true",
8768
9223
  type: "button",
8769
- onPointerDown: () => {
9224
+ onPointerDown: (event) => {
9225
+ if (event.button !== 0) {
9226
+ return;
9227
+ }
8770
9228
  if (clickResolution.kind === "blocked") {
8771
9229
  return;
8772
9230
  }
@@ -8809,14 +9267,16 @@ function WorkbenchHostDock({
8809
9267
  switch (clickResolution.kind) {
8810
9268
  case "focus-node":
8811
9269
  closePopup();
8812
- void Promise.resolve(
8813
- onDockEntryClick?.({
8814
- entryId: entry.id,
8815
- host,
8816
- nodeId: clickResolution.nodeId
8817
- })
8818
- ).catch(() => {
8819
- });
9270
+ void (async () => {
9271
+ try {
9272
+ await onDockEntryClick?.({
9273
+ entryId: entry.id,
9274
+ host,
9275
+ nodeId: clickResolution.nodeId
9276
+ });
9277
+ } catch {
9278
+ }
9279
+ })();
8820
9280
  context.genie.launchNodeFromAnchor(
8821
9281
  anchorKey,
8822
9282
  clickResolution.nodeId,
@@ -8847,6 +9307,7 @@ function WorkbenchHostDock({
8847
9307
  top: rect.top,
8848
9308
  width: rect.width
8849
9309
  },
9310
+ kind: "preview",
8850
9311
  entryId: entry.id
8851
9312
  }
8852
9313
  );
@@ -8854,14 +9315,16 @@ function WorkbenchHostDock({
8854
9315
  }
8855
9316
  case "action":
8856
9317
  closePopup();
8857
- void Promise.resolve(
8858
- onDockEntryAction?.({
8859
- actionId: clickResolution.actionId,
8860
- entryId: entry.id,
8861
- host
8862
- })
8863
- ).catch(() => {
8864
- });
9318
+ void (async () => {
9319
+ try {
9320
+ await onDockEntryAction?.({
9321
+ actionId: clickResolution.actionId,
9322
+ entryId: entry.id,
9323
+ host
9324
+ });
9325
+ } catch {
9326
+ }
9327
+ })();
8865
9328
  return;
8866
9329
  case "launch":
8867
9330
  closePopup();
@@ -8878,6 +9341,34 @@ function WorkbenchHostDock({
8878
9341
  return;
8879
9342
  }
8880
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
+ },
8881
9372
  children: /* @__PURE__ */ jsxs6(
8882
9373
  "span",
8883
9374
  {
@@ -9006,7 +9497,10 @@ function WorkbenchHostDock({
9006
9497
  className: "desktop-dock__btn desktop-dock__minimized-btn",
9007
9498
  "data-interactive": "true",
9008
9499
  type: "button",
9009
- onPointerDown: () => {
9500
+ onPointerDown: (event) => {
9501
+ if (event.button !== 0) {
9502
+ return;
9503
+ }
9010
9504
  beginDockMinimizedInteraction();
9011
9505
  },
9012
9506
  onClick: (event) => {
@@ -9134,8 +9628,8 @@ function WorkbenchHostDock({
9134
9628
  "data-interactive": "true",
9135
9629
  disabled: isPendingMinimizedNode,
9136
9630
  type: "button",
9137
- onPointerDown: () => {
9138
- if (isPendingMinimizedNode) {
9631
+ onPointerDown: (event) => {
9632
+ if (event.button !== 0 || isPendingMinimizedNode) {
9139
9633
  return;
9140
9634
  }
9141
9635
  const restoreIntent = resolveWorkbenchMinimizedDockRestoreIntent({
@@ -9309,7 +9803,27 @@ function WorkbenchHostDock({
9309
9803
  {
9310
9804
  anchorRect: activePopup.anchorRect,
9311
9805
  placement: dockPlacement,
9806
+ canEnterFullscreen: fullscreenNodeFromDockContextMenu !== null,
9807
+ canShowAllWindows: canShowAllWindowsFromDockContextMenu,
9312
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,
9313
9827
  capturePreview: popupEntry.entry.capturePopupItemPreview ? async (item) => {
9314
9828
  const previewImageUrl = await Promise.resolve(
9315
9829
  popupEntry.entry.capturePopupItemPreview?.(item) ?? null
@@ -9358,7 +9872,10 @@ function WorkbenchHostDock({
9358
9872
  label: popupEntry.entry.label,
9359
9873
  labelMode: popupEntry.entry.popupCardLabelMode,
9360
9874
  newWindowLabel: i18n.t("newWindow"),
9875
+ openLabel: i18n.t("dockContextMenu.open"),
9361
9876
  closeWindowLabel: (title) => i18n.t("closeWindow", { title }),
9877
+ fullscreenLabel: i18n.t("dockContextMenu.fullscreen"),
9878
+ hideLabel: i18n.t("dockContextMenu.hide"),
9362
9879
  onClose: () => {
9363
9880
  logWorkbenchDockDebug(
9364
9881
  "dock.popup.close_requested",
@@ -9393,16 +9910,46 @@ function WorkbenchHostDock({
9393
9910
  })
9394
9911
  );
9395
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,
9396
9941
  onSelectNode: (nodeId) => {
9397
9942
  closePopup();
9398
- void Promise.resolve(
9399
- onDockEntryClick?.({
9400
- entryId: popupEntry.entry.id,
9401
- host,
9402
- nodeId
9403
- })
9404
- ).catch(() => {
9405
- });
9943
+ void (async () => {
9944
+ try {
9945
+ await onDockEntryClick?.({
9946
+ entryId: popupEntry.entry.id,
9947
+ host,
9948
+ nodeId
9949
+ });
9950
+ } catch {
9951
+ }
9952
+ })();
9406
9953
  context.genie.launchNodeFromAnchor(
9407
9954
  anchorKeyFromPopupEntry(popupEntry),
9408
9955
  nodeId,
@@ -9411,11 +9958,35 @@ function WorkbenchHostDock({
9411
9958
  }
9412
9959
  );
9413
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"),
9414
9982
  showCreateNew: canCreateNewWindow(
9415
9983
  popupEntry.entry,
9416
- resolveDockEntryInstanceMode(popupEntry.entry, nodeDefinitions)
9984
+ dockContextMenuInstanceMode
9417
9985
  ),
9418
- 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"
9419
9990
  }
9420
9991
  ) : null,
9421
9992
  activeMinimizedStackSlot && activeMinimizedStackPopup ? /* @__PURE__ */ jsx10(
@@ -9745,6 +10316,26 @@ function minimizedDockPreviewFreezeKey(node) {
9745
10316
  function dockEntryHasHoverPanel(entry) {
9746
10317
  return Boolean(entry.state?.reason?.trim()) || (entry.hoverActions?.length ?? 0) > 0;
9747
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
+ }
9748
10339
  function dockLabelTooltipTarget(key, label) {
9749
10340
  return { key, label: label.trim() };
9750
10341
  }
@@ -9864,23 +10455,25 @@ function WorkbenchHostDockHoverPanel({
9864
10455
  next.add(actionKey);
9865
10456
  return next;
9866
10457
  });
9867
- void Promise.resolve(
9868
- onDockEntryAction?.({
9869
- actionId: action.id,
9870
- entryId: entry.id,
9871
- host
9872
- })
9873
- ).catch(() => {
9874
- }).finally(() => {
9875
- setPendingActionKeys((current) => {
9876
- if (!current.has(actionKey)) {
9877
- return current;
9878
- }
9879
- const next = new Set(current);
9880
- next.delete(actionKey);
9881
- return next;
9882
- });
9883
- });
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
+ })();
9884
10477
  },
9885
10478
  children: isPending ? action.pendingLabel ?? action.label : action.label
9886
10479
  },
@@ -10545,8 +11138,8 @@ function createWorkbenchHostNodeHeaderContext({
10545
11138
  }
10546
11139
 
10547
11140
  // src/host/WorkbenchHostWindowActions.tsx
10548
- import { Button as Button5, CloseIcon as CloseIcon2, MinimizeIcon } from "@tutti-os/ui-system";
10549
- 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";
10550
11143
  function WorkbenchHostWindowActions({
10551
11144
  context,
10552
11145
  host,
@@ -10559,7 +11152,7 @@ function WorkbenchHostWindowActions({
10559
11152
  }
10560
11153
  const minimizable = definition.window?.minimizable !== false;
10561
11154
  const closable = definition.window?.closable !== false;
10562
- return /* @__PURE__ */ jsxs7(Fragment3, { children: [
11155
+ return /* @__PURE__ */ jsxs7(Fragment4, { children: [
10563
11156
  minimizable ? /* @__PURE__ */ jsx11(
10564
11157
  Button5,
10565
11158
  {
@@ -10575,7 +11168,7 @@ function WorkbenchHostWindowActions({
10575
11168
  () => context.controller.commands.minimizeNode(context.node.id)
10576
11169
  );
10577
11170
  },
10578
- children: /* @__PURE__ */ jsx11(MinimizeIcon, { className: "size-3.5" })
11171
+ children: /* @__PURE__ */ jsx11(MinimizeIcon2, { className: "size-3.5" })
10579
11172
  }
10580
11173
  ) : null,
10581
11174
  closable ? /* @__PURE__ */ jsx11(
@@ -10717,6 +11310,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
10717
11310
  nodeDefinitions: input.nodeDefinitionByType,
10718
11311
  onDockEntryAction: input.onDockEntryAction,
10719
11312
  onDockEntryClick: input.onDockEntryClick,
11313
+ onMissionControlRequestOpen: input.onMissionControlRequestOpen,
10720
11314
  workspaceId: input.workspaceId
10721
11315
  }
10722
11316
  )
@@ -10735,6 +11329,7 @@ function useWorkbenchHostSurfaceRenderers(input) {
10735
11329
  input.nodeDefinitionByType,
10736
11330
  input.onDockEntryAction,
10737
11331
  input.onDockEntryClick,
11332
+ input.onMissionControlRequestOpen,
10738
11333
  input.workspaceId
10739
11334
  ]
10740
11335
  );
@@ -11077,6 +11672,7 @@ function WorkbenchHost({
11077
11672
  onHandleReady,
11078
11673
  onLaunchRequest,
11079
11674
  onMissionControlAdapterReady,
11675
+ onMissionControlRequestOpen,
11080
11676
  onNodeCloseRequest,
11081
11677
  projectedNodes,
11082
11678
  renderBottomChrome,
@@ -11110,6 +11706,7 @@ function WorkbenchHost({
11110
11706
  [contributions, dockEntries]
11111
11707
  );
11112
11708
  const missionControlMode = missionControl?.mode ?? null;
11709
+ const missionControlNodeIds = missionControl?.nodeIds;
11113
11710
  const missionControlClose = missionControl?.onRequestClose ?? noop3;
11114
11711
  const missionControlEnabled = missionControlMode !== null || onMissionControlAdapterReady !== void 0;
11115
11712
  const {
@@ -11151,6 +11748,7 @@ function WorkbenchHost({
11151
11748
  nodeDefinitionByType,
11152
11749
  onDockEntryAction,
11153
11750
  onDockEntryClick,
11751
+ onMissionControlRequestOpen,
11154
11752
  renderBottomChrome,
11155
11753
  renderTopChrome,
11156
11754
  workspaceId
@@ -11158,6 +11756,7 @@ function WorkbenchHost({
11158
11756
  const missionControlState = useWorkbenchMissionControlState({
11159
11757
  adapter: missionControlAdapter,
11160
11758
  mode: missionControlMode,
11759
+ nodeIds: missionControlNodeIds,
11161
11760
  onRequestClose: missionControlClose
11162
11761
  });
11163
11762
  const missionControlPresence = useWorkbenchMissionControlPresence(missionControlState);