@tutti-os/workspace-file-manager 0.0.9 → 0.0.11

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,18 +5,22 @@ import {
5
5
  decodeWorkspaceTextFile,
6
6
  formatWorkspaceFileBytes,
7
7
  formatWorkspaceFileModifiedTime,
8
+ isWorkspaceApplicationBundle,
8
9
  isWorkspaceFileBrowserOpenable,
9
10
  isWorkspaceTextFileTooLarge,
10
11
  looksLikeBinaryText,
11
12
  normalizeWorkspaceFilePath,
12
13
  resolveWorkspaceFileActivationTarget,
14
+ resolveWorkspaceFileEntryIconCacheKey,
13
15
  resolveWorkspaceFileExtension,
14
16
  resolveWorkspaceFileVisualKind,
15
17
  resolveWorkspaceImageMimeType,
18
+ shouldResolveWorkspaceFileEntryIcon,
19
+ shouldUseWorkspaceFileExtensionDocumentIcon,
16
20
  splitWorkspaceFileName,
17
21
  workspaceFilePreviewMaxBytes,
18
22
  workspaceFileTextMaxBytes
19
- } from "./chunk-H3SDVOT7.js";
23
+ } from "./chunk-DXLDUMVG.js";
20
24
  import {
21
25
  createWorkspaceFileManagerI18nRuntime,
22
26
  resolveRevealInFolderLabel,
@@ -25,7 +29,7 @@ import {
25
29
  } from "./chunk-XOFPTWRV.js";
26
30
 
27
31
  // src/ui/WorkspaceFileManager.tsx
28
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState7 } from "react";
32
+ import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef5, useState as useState7 } from "react";
29
33
  import { cn as cn6 } from "@tutti-os/ui-system";
30
34
 
31
35
  // src/ui/WorkspaceFileManagerMenus.tsx
@@ -906,15 +910,15 @@ import {
906
910
  import { WorkspaceFilePreviewSurface as SharedWorkspaceFilePreviewSurface } from "@tutti-os/workspace-file-preview/react";
907
911
  import {
908
912
  useCallback as useCallback2,
909
- useEffect as useEffect2,
913
+ useEffect as useEffect3,
910
914
  useLayoutEffect as useLayoutEffect3,
911
- useRef as useRef3,
915
+ useRef as useRef4,
912
916
  useState as useState2
913
917
  } from "react";
914
918
 
915
919
  // src/ui/WorkspaceFileManagerIconGrid.tsx
916
920
  import { cn as cn3 } from "@tutti-os/ui-system";
917
- import { useLayoutEffect as useLayoutEffect2, useRef as useRef2 } from "react";
921
+ import { useLayoutEffect as useLayoutEffect2, useRef as useRef3 } from "react";
918
922
 
919
923
  // src/ui/WorkspaceFileEntryIcon.tsx
920
924
  import {
@@ -926,46 +930,82 @@ import {
926
930
  VideoFileIcon,
927
931
  cn as cn2
928
932
  } from "@tutti-os/ui-system";
929
-
930
- // src/ui/workspaceFileEntryIconPolicy.ts
931
- function shouldResolveWorkspaceFileEntryIcon(entry) {
932
- if (isWorkspaceApplicationBundle(entry)) {
933
- return true;
934
- }
935
- if (shouldUseWorkspaceFileExtensionDocumentIcon(entry)) {
936
- return false;
937
- }
938
- return entry.kind === "file";
939
- }
940
- function shouldUseWorkspaceFileExtensionDocumentIcon(entry) {
941
- if (entry.kind !== "file") {
942
- return false;
943
- }
944
- const visualKind = resolveWorkspaceFileVisualKind(entry);
945
- return visualKind === "code" || visualKind === "markdown" || classifyWorkspaceFilePreviewKind(entry) === "text";
946
- }
947
- function isWorkspaceApplicationBundle(entry) {
948
- return entry.name.trim().toLowerCase().endsWith(".app");
949
- }
950
- function resolveWorkspaceFileEntryIconCacheKey(entry) {
951
- return `${entry.path}:${entry.mtimeMs ?? 0}`;
952
- }
953
-
954
- // src/ui/WorkspaceFileEntryIcon.tsx
933
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
955
934
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
956
935
  function WorkspaceFileEntryIcon({
957
936
  entry,
958
937
  frameClassName,
959
938
  iconClassName = "size-4",
960
939
  iconUrlByCacheKey,
961
- isEnteringDirectory = false
940
+ isEnteringDirectory = false,
941
+ onViewportLeave,
942
+ onViewportEnter
962
943
  }) {
963
944
  const visualKind = resolveWorkspaceFileVisualKind(entry);
964
945
  const isAppBundle = isWorkspaceApplicationBundle(entry);
965
- const iconUrl = iconUrlByCacheKey?.get(resolveWorkspaceFileEntryIconCacheKey(entry)) ?? null;
946
+ const cacheKey = resolveWorkspaceFileEntryIconCacheKey(entry);
947
+ const iconUrl = iconUrlByCacheKey?.get(cacheKey) ?? null;
948
+ const frameRef = useRef2(null);
949
+ const visibleEntryRef = useRef2(null);
950
+ const visibleEntryCacheKeyRef = useRef2(null);
951
+ useEffect2(() => {
952
+ function reportViewportLeave() {
953
+ const visibleEntry = visibleEntryRef.current;
954
+ if (!visibleEntry) {
955
+ return;
956
+ }
957
+ visibleEntryRef.current = null;
958
+ visibleEntryCacheKeyRef.current = null;
959
+ onViewportLeave?.(visibleEntry);
960
+ }
961
+ function reportViewportEnter() {
962
+ if (visibleEntryCacheKeyRef.current === cacheKey) {
963
+ return;
964
+ }
965
+ reportViewportLeave();
966
+ visibleEntryRef.current = entry;
967
+ visibleEntryCacheKeyRef.current = cacheKey;
968
+ onViewportEnter?.(entry);
969
+ }
970
+ if (!onViewportEnter || isEnteringDirectory) {
971
+ reportViewportLeave();
972
+ return;
973
+ }
974
+ const element = frameRef.current;
975
+ if (!element || typeof IntersectionObserver === "undefined") {
976
+ reportViewportEnter();
977
+ return () => {
978
+ reportViewportLeave();
979
+ };
980
+ }
981
+ const viewportRoot = resolveWorkspaceFileEntryIconViewport(element);
982
+ const observer = new IntersectionObserver(
983
+ (records) => {
984
+ if (records.some(
985
+ (record) => record.isIntersecting && record.intersectionRatio > 0
986
+ )) {
987
+ reportViewportEnter();
988
+ return;
989
+ }
990
+ reportViewportLeave();
991
+ },
992
+ { root: viewportRoot, rootMargin: "0px", threshold: 0 }
993
+ );
994
+ observer.observe(element);
995
+ if (isWorkspaceFileEntryIconVisible(element, viewportRoot)) {
996
+ reportViewportEnter();
997
+ } else {
998
+ reportViewportLeave();
999
+ }
1000
+ return () => {
1001
+ observer.disconnect();
1002
+ reportViewportLeave();
1003
+ };
1004
+ }, [cacheKey, entry, isEnteringDirectory, onViewportLeave, onViewportEnter]);
966
1005
  return /* @__PURE__ */ jsx2(
967
1006
  "span",
968
1007
  {
1008
+ ref: frameRef,
969
1009
  className: cn2(
970
1010
  "grid flex-none place-items-center overflow-hidden",
971
1011
  frameClassName,
@@ -990,6 +1030,19 @@ function WorkspaceFileEntryIcon({
990
1030
  }
991
1031
  );
992
1032
  }
1033
+ function resolveWorkspaceFileEntryIconViewport(element) {
1034
+ return element.closest('[data-slot="scroll-area-viewport"]');
1035
+ }
1036
+ function isWorkspaceFileEntryIconVisible(element, viewportRoot) {
1037
+ const elementRect = element.getBoundingClientRect();
1038
+ const viewportRect = viewportRoot?.getBoundingClientRect() ?? {
1039
+ bottom: window.innerHeight,
1040
+ left: 0,
1041
+ right: window.innerWidth,
1042
+ top: 0
1043
+ };
1044
+ return elementRect.bottom > viewportRect.top && elementRect.top < viewportRect.bottom && elementRect.right > viewportRect.left && elementRect.left < viewportRect.right;
1045
+ }
993
1046
  function DefaultEntryIcon({
994
1047
  entry,
995
1048
  iconClassName,
@@ -1076,6 +1129,8 @@ function WorkspaceFileManagerIconGrid({
1076
1129
  moveDragPreviewEntryPath,
1077
1130
  moveDragTargetEntryPath,
1078
1131
  pendingDirectoryPath,
1132
+ onEntryIconViewportLeave,
1133
+ onEntryIconViewportEnter,
1079
1134
  selectedPath,
1080
1135
  onCancelInlineRename,
1081
1136
  onClearInlineRenameValidation,
@@ -1107,6 +1162,8 @@ function WorkspaceFileManagerIconGrid({
1107
1162
  moveDragActive,
1108
1163
  moveDragSource: moveDragPreviewEntryPath === entry.path,
1109
1164
  moveDragTarget: moveDragTargetEntryPath === entry.path,
1165
+ onEntryIconViewportLeave,
1166
+ onEntryIconViewportEnter,
1110
1167
  selected: selectedPath === entry.path,
1111
1168
  onCancelInlineRename,
1112
1169
  onClearInlineRenameValidation,
@@ -1134,6 +1191,8 @@ function WorkspaceFileManagerIconTile({
1134
1191
  moveDragActive,
1135
1192
  moveDragSource,
1136
1193
  moveDragTarget,
1194
+ onEntryIconViewportLeave,
1195
+ onEntryIconViewportEnter,
1137
1196
  selected,
1138
1197
  onCancelInlineRename,
1139
1198
  onClearInlineRenameValidation,
@@ -1143,7 +1202,7 @@ function WorkspaceFileManagerIconTile({
1143
1202
  onClick,
1144
1203
  onPointerDown
1145
1204
  }) {
1146
- const buttonRef = useRef2(null);
1205
+ const buttonRef = useRef3(null);
1147
1206
  const nameParts = splitWorkspaceFileName(entry.name);
1148
1207
  useLayoutEffect2(() => {
1149
1208
  if (!selected) {
@@ -1169,7 +1228,9 @@ function WorkspaceFileManagerIconTile({
1169
1228
  frameClassName: workspaceFileManagerIconGridFrameClassName(),
1170
1229
  iconClassName: workspaceFileManagerIconGridIconClassName(),
1171
1230
  iconUrlByCacheKey,
1172
- isEnteringDirectory
1231
+ isEnteringDirectory,
1232
+ onViewportLeave: onEntryIconViewportLeave,
1233
+ onViewportEnter: onEntryIconViewportEnter
1173
1234
  }
1174
1235
  );
1175
1236
  if (isInlineRenaming) {
@@ -1243,7 +1304,7 @@ function IconTileRenameInput({
1243
1304
  }) {
1244
1305
  const nameParts = splitWorkspaceFileName(entry.name);
1245
1306
  const hasFileExtension = nameParts.end.length > 0 && nameParts.end.startsWith(".");
1246
- const inputRef = useRef2(null);
1307
+ const inputRef = useRef3(null);
1247
1308
  useLayoutEffect2(() => {
1248
1309
  const input = inputRef.current;
1249
1310
  if (!input) {
@@ -1448,6 +1509,8 @@ function WorkspaceFileManagerPanels({
1448
1509
  layoutMode,
1449
1510
  pendingDirectoryPath,
1450
1511
  previewState,
1512
+ onEntryIconViewportLeave,
1513
+ onEntryIconViewportEnter,
1451
1514
  selectedEntry,
1452
1515
  selectedPath,
1453
1516
  state,
@@ -1465,18 +1528,18 @@ function WorkspaceFileManagerPanels({
1465
1528
  const { rootRef } = useWorkspaceFileManagerStackedLayout(
1466
1529
  workspaceFileManagerStackedBreakpoint
1467
1530
  );
1468
- const previewResizeRef = useRef3(null);
1531
+ const previewResizeRef = useRef4(null);
1469
1532
  const [previewPaneWidth, setPreviewPaneWidth] = useState2(
1470
1533
  workspaceFileManagerPreviewDefaultWidth
1471
1534
  );
1472
1535
  const [moveDragPreview, setMoveDragPreview] = useState2(null);
1473
- const lastEntryClickRef = useRef3(
1536
+ const lastEntryClickRef = useRef4(
1474
1537
  null
1475
1538
  );
1476
- const moveDragAutoScrollRef = useRef3(null);
1477
- const moveDragRef = useRef3(null);
1478
- const suppressNextClickRef = useRef3(false);
1479
- const entriesRef = useRef3(state.entries);
1539
+ const moveDragAutoScrollRef = useRef4(null);
1540
+ const moveDragRef = useRef4(null);
1541
+ const suppressNextClickRef = useRef4(false);
1542
+ const entriesRef = useRef4(state.entries);
1480
1543
  const nativeEntryDragEnabled = onEntryDragStart !== void 0 && (entryDragMode === "external" || entryDragMode === void 0 && !canMove);
1481
1544
  const internalMoveEnabled = canMove && !nativeEntryDragEnabled;
1482
1545
  const dateColumnLabel = resolveWorkspaceFileManagerDateColumnLabel(
@@ -1590,11 +1653,11 @@ function WorkspaceFileManagerPanels({
1590
1653
  },
1591
1654
  [entrySelectionEnabled, onOpenEntry, onSelect, shouldSuppressEntryClick]
1592
1655
  );
1593
- useEffect2(() => {
1656
+ useEffect3(() => {
1594
1657
  lastEntryClickRef.current = null;
1595
1658
  entriesRef.current = state.entries;
1596
1659
  }, [state.entries]);
1597
- useEffect2(() => {
1660
+ useEffect3(() => {
1598
1661
  function handleDocumentPointerMove(event) {
1599
1662
  const drag = moveDragRef.current;
1600
1663
  if (!drag) {
@@ -1783,6 +1846,8 @@ function WorkspaceFileManagerPanels({
1783
1846
  moveDragPreviewEntryPath: moveDragPreview?.entry.path ?? null,
1784
1847
  moveDragTargetEntryPath: moveDragPreview?.targetDirectoryPath ?? null,
1785
1848
  pendingDirectoryPath,
1849
+ onEntryIconViewportLeave,
1850
+ onEntryIconViewportEnter,
1786
1851
  selectedPath,
1787
1852
  onCancelInlineRename,
1788
1853
  onClearInlineRenameValidation,
@@ -1842,6 +1907,8 @@ function WorkspaceFileManagerPanels({
1842
1907
  moveDragTarget: moveDragPreview?.targetDirectoryPath === entry.path,
1843
1908
  tableCellPaddingClassName,
1844
1909
  selected: selectedPath === entry.path,
1910
+ onEntryIconViewportLeave,
1911
+ onEntryIconViewportEnter,
1845
1912
  onCancelInlineRename,
1846
1913
  onClearInlineRenameValidation,
1847
1914
  onConfirmInlineRename,
@@ -1924,7 +1991,9 @@ function WorkspaceFileManagerPanels({
1924
1991
  MoveDragPreview,
1925
1992
  {
1926
1993
  iconUrlByCacheKey,
1927
- preview: moveDragPreview
1994
+ preview: moveDragPreview,
1995
+ onEntryIconViewportLeave,
1996
+ onEntryIconViewportEnter
1928
1997
  }
1929
1998
  ) : null
1930
1999
  ]
@@ -1932,7 +2001,7 @@ function WorkspaceFileManagerPanels({
1932
2001
  );
1933
2002
  }
1934
2003
  function useWorkspaceFileManagerStackedLayout(breakpoint) {
1935
- const rootRef = useRef3(null);
2004
+ const rootRef = useRef4(null);
1936
2005
  const [isStacked, setIsStacked] = useState2(false);
1937
2006
  useLayoutEffect3(() => {
1938
2007
  const element = rootRef.current;
@@ -1991,6 +2060,8 @@ function EntryRow({
1991
2060
  moveDragActive,
1992
2061
  moveDragSource,
1993
2062
  moveDragTarget,
2063
+ onEntryIconViewportLeave,
2064
+ onEntryIconViewportEnter,
1994
2065
  selected,
1995
2066
  tableCellPaddingClassName,
1996
2067
  onCancelInlineRename,
@@ -2001,8 +2072,8 @@ function EntryRow({
2001
2072
  onClick,
2002
2073
  onPointerDown
2003
2074
  }) {
2004
- const buttonRowRef = useRef3(null);
2005
- const divRowRef = useRef3(null);
2075
+ const buttonRowRef = useRef4(null);
2076
+ const divRowRef = useRef4(null);
2006
2077
  useLayoutEffect3(() => {
2007
2078
  if (!selected) {
2008
2079
  return;
@@ -2042,6 +2113,8 @@ function EntryRow({
2042
2113
  isEnteringDirectory,
2043
2114
  isInlineRenaming,
2044
2115
  isRenaming,
2116
+ onEntryIconViewportLeave,
2117
+ onEntryIconViewportEnter,
2045
2118
  onCancelInlineRename,
2046
2119
  onClearInlineRenameValidation,
2047
2120
  onConfirmInlineRename
@@ -2106,7 +2179,9 @@ function EntryRow({
2106
2179
  }
2107
2180
  function MoveDragPreview({
2108
2181
  iconUrlByCacheKey,
2109
- preview
2182
+ preview,
2183
+ onEntryIconViewportLeave,
2184
+ onEntryIconViewportEnter
2110
2185
  }) {
2111
2186
  return /* @__PURE__ */ jsxs4(
2112
2187
  "div",
@@ -2125,7 +2200,9 @@ function MoveDragPreview({
2125
2200
  entry: preview.entry,
2126
2201
  frameClassName: "size-[18px]",
2127
2202
  iconClassName: "size-4",
2128
- iconUrlByCacheKey
2203
+ iconUrlByCacheKey,
2204
+ onViewportLeave: onEntryIconViewportLeave,
2205
+ onViewportEnter: onEntryIconViewportEnter
2129
2206
  }
2130
2207
  ),
2131
2208
  /* @__PURE__ */ jsx4("span", { className: "min-w-0 truncate", children: preview.entry.name })
@@ -2176,6 +2253,8 @@ function EntryNameCell({
2176
2253
  isEnteringDirectory = false,
2177
2254
  isInlineRenaming = false,
2178
2255
  isRenaming = false,
2256
+ onEntryIconViewportLeave,
2257
+ onEntryIconViewportEnter,
2179
2258
  onCancelInlineRename,
2180
2259
  onClearInlineRenameValidation,
2181
2260
  onConfirmInlineRename
@@ -2183,9 +2262,9 @@ function EntryNameCell({
2183
2262
  const nameParts = splitWorkspaceFileName(entry.name);
2184
2263
  const hasFileExtension = nameParts.end.length > 0 && nameParts.end.startsWith(".");
2185
2264
  const [name, setName] = useState2(entry.name);
2186
- const inputRef = useRef3(null);
2187
- const skipBlurRef = useRef3(false);
2188
- useEffect2(() => {
2265
+ const inputRef = useRef4(null);
2266
+ const skipBlurRef = useRef4(false);
2267
+ useEffect3(() => {
2189
2268
  if (!isInlineRenaming) {
2190
2269
  return;
2191
2270
  }
@@ -2222,7 +2301,9 @@ function EntryNameCell({
2222
2301
  entry,
2223
2302
  frameClassName: "size-[18px]",
2224
2303
  iconClassName: "size-4",
2225
- iconUrlByCacheKey
2304
+ iconUrlByCacheKey,
2305
+ onViewportLeave: onEntryIconViewportLeave,
2306
+ onViewportEnter: onEntryIconViewportEnter
2226
2307
  }
2227
2308
  ),
2228
2309
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
@@ -2285,7 +2366,9 @@ function EntryNameCell({
2285
2366
  frameClassName: "size-[18px]",
2286
2367
  iconClassName: "size-4",
2287
2368
  iconUrlByCacheKey,
2288
- isEnteringDirectory
2369
+ isEnteringDirectory,
2370
+ onViewportLeave: onEntryIconViewportLeave,
2371
+ onViewportEnter: onEntryIconViewportEnter
2289
2372
  }
2290
2373
  ),
2291
2374
  /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 max-w-full overflow-hidden whitespace-nowrap text-sm", children: [
@@ -2762,74 +2845,207 @@ function useWorkspaceFileManagerLayoutMode() {
2762
2845
  }
2763
2846
 
2764
2847
  // src/ui/useWorkspaceFileEntryIconUrls.ts
2765
- import { useEffect as useEffect3, useMemo, useState as useState6 } from "react";
2766
- function buildIconTargetSignature(entries) {
2767
- return entries.filter(shouldResolveWorkspaceFileEntryIcon).map((entry) => resolveWorkspaceFileEntryIconCacheKey(entry)).join("\0");
2768
- }
2769
- function useWorkspaceFileEntryIconUrls(input) {
2770
- const { entries, resolveEntryIconUrl } = input;
2771
- const [iconUrlByCacheKey, setIconUrlByCacheKey] = useState6(() => /* @__PURE__ */ new Map());
2772
- const iconTargetSignature = useMemo(
2773
- () => buildIconTargetSignature(entries),
2774
- [entries]
2775
- );
2776
- useEffect3(() => {
2777
- if (!resolveEntryIconUrl) {
2778
- setIconUrlByCacheKey(
2779
- (current) => current.size === 0 ? current : /* @__PURE__ */ new Map()
2780
- );
2781
- return;
2848
+ import { useCallback as useCallback5, useEffect as useEffect4, useMemo, useState as useState6 } from "react";
2849
+
2850
+ // src/ui/workspaceFileEntryIconUrlQueue.ts
2851
+ var defaultMaxConcurrentIconRequests = 3;
2852
+ function createWorkspaceFileEntryIconUrlQueue(options) {
2853
+ const maxConcurrent = Math.max(
2854
+ 1,
2855
+ Math.floor(options.maxConcurrent ?? defaultMaxConcurrentIconRequests)
2856
+ );
2857
+ const iconUrlByCacheKey = /* @__PURE__ */ new Map();
2858
+ const inFlightCacheKeys = /* @__PURE__ */ new Set();
2859
+ const listeners = /* @__PURE__ */ new Set();
2860
+ const queuedEntries = /* @__PURE__ */ new Map();
2861
+ const visibleReferenceCountByCacheKey = /* @__PURE__ */ new Map();
2862
+ let activeCount = 0;
2863
+ let disposed = false;
2864
+ let retainedCacheKeys = null;
2865
+ function publish() {
2866
+ for (const listener of listeners) {
2867
+ listener();
2782
2868
  }
2783
- const targets = entries.filter(shouldResolveWorkspaceFileEntryIcon);
2784
- if (targets.length === 0) {
2785
- setIconUrlByCacheKey(
2786
- (current) => current.size === 0 ? current : /* @__PURE__ */ new Map()
2787
- );
2869
+ }
2870
+ function isRetained(cacheKey) {
2871
+ return retainedCacheKeys === null || retainedCacheKeys.has(cacheKey);
2872
+ }
2873
+ function isVisible(cacheKey) {
2874
+ return (visibleReferenceCountByCacheKey.get(cacheKey) ?? 0) > 0;
2875
+ }
2876
+ function retainVisibleReference(cacheKey) {
2877
+ visibleReferenceCountByCacheKey.set(
2878
+ cacheKey,
2879
+ (visibleReferenceCountByCacheKey.get(cacheKey) ?? 0) + 1
2880
+ );
2881
+ }
2882
+ function releaseVisibleReference(cacheKey) {
2883
+ const nextCount = (visibleReferenceCountByCacheKey.get(cacheKey) ?? 0) - 1;
2884
+ if (nextCount > 0) {
2885
+ visibleReferenceCountByCacheKey.set(cacheKey, nextCount);
2886
+ return false;
2887
+ }
2888
+ visibleReferenceCountByCacheKey.delete(cacheKey);
2889
+ queuedEntries.delete(cacheKey);
2890
+ const hadCachedIcon = iconUrlByCacheKey.delete(cacheKey);
2891
+ if (hadCachedIcon) {
2892
+ publish();
2893
+ }
2894
+ return true;
2895
+ }
2896
+ function drain() {
2897
+ if (disposed || !options.resolveEntryIconUrl) {
2788
2898
  return;
2789
2899
  }
2790
- let cancelled = false;
2791
- void Promise.all(
2792
- targets.map(async (entry) => {
2793
- const cacheKey = resolveWorkspaceFileEntryIconCacheKey(entry);
2794
- try {
2795
- const iconUrl = await resolveEntryIconUrl(entry);
2796
- return [cacheKey, iconUrl?.trim() || null];
2797
- } catch {
2798
- return [cacheKey, null];
2900
+ while (activeCount < maxConcurrent && queuedEntries.size > 0) {
2901
+ const next = queuedEntries.entries().next().value;
2902
+ if (!next) {
2903
+ return;
2904
+ }
2905
+ const [cacheKey, entry] = next;
2906
+ queuedEntries.delete(cacheKey);
2907
+ if (!isVisible(cacheKey) || iconUrlByCacheKey.has(cacheKey) || inFlightCacheKeys.has(cacheKey)) {
2908
+ continue;
2909
+ }
2910
+ activeCount += 1;
2911
+ inFlightCacheKeys.add(cacheKey);
2912
+ void options.resolveEntryIconUrl(entry).then((iconUrl) => {
2913
+ if (!disposed && isRetained(cacheKey) && isVisible(cacheKey)) {
2914
+ iconUrlByCacheKey.set(cacheKey, iconUrl?.trim() || null);
2915
+ publish();
2799
2916
  }
2800
- })
2801
- ).then((results) => {
2802
- if (cancelled) {
2917
+ }).catch(() => {
2918
+ if (!disposed && isRetained(cacheKey) && isVisible(cacheKey)) {
2919
+ iconUrlByCacheKey.set(cacheKey, null);
2920
+ publish();
2921
+ }
2922
+ }).finally(() => {
2923
+ activeCount -= 1;
2924
+ inFlightCacheKeys.delete(cacheKey);
2925
+ drain();
2926
+ });
2927
+ }
2928
+ }
2929
+ return {
2930
+ dispose() {
2931
+ disposed = true;
2932
+ queuedEntries.clear();
2933
+ visibleReferenceCountByCacheKey.clear();
2934
+ listeners.clear();
2935
+ },
2936
+ leaveViewport(entry) {
2937
+ if (disposed || !shouldResolveWorkspaceFileEntryIcon(entry)) {
2803
2938
  return;
2804
2939
  }
2805
- setIconUrlByCacheKey((current) => {
2806
- const nextCacheKeys = new Set(results.map(([cacheKey]) => cacheKey));
2807
- let changed = false;
2808
- const next = /* @__PURE__ */ new Map();
2809
- for (const [cacheKey, iconUrl] of results) {
2810
- next.set(cacheKey, iconUrl);
2811
- if (current.get(cacheKey) !== iconUrl) {
2812
- changed = true;
2813
- }
2940
+ releaseVisibleReference(resolveWorkspaceFileEntryIconCacheKey(entry));
2941
+ },
2942
+ enterViewport(entry) {
2943
+ if (disposed || !options.resolveEntryIconUrl || !shouldResolveWorkspaceFileEntryIcon(entry)) {
2944
+ return;
2945
+ }
2946
+ const cacheKey = resolveWorkspaceFileEntryIconCacheKey(entry);
2947
+ retainVisibleReference(cacheKey);
2948
+ if (iconUrlByCacheKey.has(cacheKey) || inFlightCacheKeys.has(cacheKey) || queuedEntries.has(cacheKey)) {
2949
+ return;
2950
+ }
2951
+ queuedEntries.set(cacheKey, entry);
2952
+ drain();
2953
+ },
2954
+ retainEntries(entries) {
2955
+ retainedCacheKeys = new Set(
2956
+ entries.filter(shouldResolveWorkspaceFileEntryIcon).map((entry) => resolveWorkspaceFileEntryIconCacheKey(entry))
2957
+ );
2958
+ let changed = false;
2959
+ for (const cacheKey of iconUrlByCacheKey.keys()) {
2960
+ if (!isRetained(cacheKey)) {
2961
+ iconUrlByCacheKey.delete(cacheKey);
2962
+ changed = true;
2963
+ continue;
2814
2964
  }
2815
- if (current.size !== next.size) {
2965
+ if (iconUrlByCacheKey.get(cacheKey) === null) {
2966
+ iconUrlByCacheKey.delete(cacheKey);
2816
2967
  changed = true;
2817
- } else if (!changed) {
2818
- for (const cacheKey of current.keys()) {
2819
- if (!nextCacheKeys.has(cacheKey)) {
2820
- changed = true;
2821
- break;
2822
- }
2823
- }
2824
2968
  }
2825
- return changed ? next : current;
2826
- });
2969
+ }
2970
+ for (const cacheKey of queuedEntries.keys()) {
2971
+ if (!isRetained(cacheKey)) {
2972
+ queuedEntries.delete(cacheKey);
2973
+ }
2974
+ }
2975
+ for (const cacheKey of visibleReferenceCountByCacheKey.keys()) {
2976
+ if (!isRetained(cacheKey)) {
2977
+ visibleReferenceCountByCacheKey.delete(cacheKey);
2978
+ }
2979
+ }
2980
+ if (changed) {
2981
+ publish();
2982
+ }
2983
+ },
2984
+ snapshot() {
2985
+ return new Map(iconUrlByCacheKey);
2986
+ },
2987
+ subscribe(listener) {
2988
+ if (disposed) {
2989
+ return () => {
2990
+ };
2991
+ }
2992
+ listeners.add(listener);
2993
+ return () => {
2994
+ listeners.delete(listener);
2995
+ };
2996
+ }
2997
+ };
2998
+ }
2999
+
3000
+ // src/ui/useWorkspaceFileEntryIconUrls.ts
3001
+ function buildIconTargetSignature(entries) {
3002
+ return entries.filter(shouldResolveWorkspaceFileEntryIcon).map((entry) => resolveWorkspaceFileEntryIconCacheKey(entry)).join("\0");
3003
+ }
3004
+ function useWorkspaceFileEntryIconUrls(input) {
3005
+ const { entries, resolveEntryIconUrl } = input;
3006
+ const queue = useMemo(
3007
+ () => createWorkspaceFileEntryIconUrlQueue({
3008
+ resolveEntryIconUrl
3009
+ }),
3010
+ [resolveEntryIconUrl]
3011
+ );
3012
+ const [iconUrlByCacheKey, setIconUrlByCacheKey] = useState6(() => queue.snapshot());
3013
+ const iconTargetSignature = useMemo(
3014
+ () => buildIconTargetSignature(entries),
3015
+ [entries]
3016
+ );
3017
+ useEffect4(() => {
3018
+ queue.retainEntries(entries);
3019
+ }, [entries, iconTargetSignature, queue]);
3020
+ useEffect4(() => {
3021
+ setIconUrlByCacheKey(queue.snapshot());
3022
+ return queue.subscribe(() => {
3023
+ setIconUrlByCacheKey(queue.snapshot());
2827
3024
  });
2828
- return () => {
2829
- cancelled = true;
2830
- };
2831
- }, [entries, iconTargetSignature, resolveEntryIconUrl]);
2832
- return iconUrlByCacheKey;
3025
+ }, [queue]);
3026
+ useEffect4(
3027
+ () => () => {
3028
+ queue.dispose();
3029
+ },
3030
+ [queue]
3031
+ );
3032
+ const reportEntryIconViewportEnter = useCallback5(
3033
+ (entry) => {
3034
+ queue.enterViewport(entry);
3035
+ },
3036
+ [queue]
3037
+ );
3038
+ const reportEntryIconViewportLeave = useCallback5(
3039
+ (entry) => {
3040
+ queue.leaveViewport(entry);
3041
+ },
3042
+ [queue]
3043
+ );
3044
+ return {
3045
+ iconUrlByCacheKey,
3046
+ reportEntryIconViewportEnter,
3047
+ reportEntryIconViewportLeave
3048
+ };
2833
3049
  }
2834
3050
 
2835
3051
  // src/ui/workspaceFileManagerAnalytics.ts
@@ -3016,12 +3232,12 @@ function WorkspaceFileManager({
3016
3232
  session,
3017
3233
  surface = "card"
3018
3234
  }) {
3019
- const rootRef = useRef4(null);
3235
+ const rootRef = useRef5(null);
3020
3236
  const { arrangeMode, setArrangeMode } = useWorkspaceFileManagerArrangeMode();
3021
3237
  const { layoutMode, setLayoutMode } = useWorkspaceFileManagerLayoutMode();
3022
3238
  const rootView = useWorkspaceFileManagerRootView(session);
3023
3239
  const { state: panelsState, view: panelsView } = useWorkspaceFileManagerPanelsView(session);
3024
- useEffect4(() => {
3240
+ useEffect5(() => {
3025
3241
  function handleCopyShortcut(event) {
3026
3242
  if (!(event.metaKey || event.ctrlKey) || event.key !== "c" || event.shiftKey) {
3027
3243
  return;
@@ -3050,7 +3266,7 @@ function WorkspaceFileManager({
3050
3266
  window.removeEventListener("keydown", handleCopyShortcut);
3051
3267
  };
3052
3268
  }, [onCopyEntry, panelsState, panelsView.selectedEntry, session]);
3053
- useEffect4(() => {
3269
+ useEffect5(() => {
3054
3270
  function handleRenameShortcut(event) {
3055
3271
  if (event.key !== "Enter" || event.metaKey || event.ctrlKey || event.altKey) {
3056
3272
  return;
@@ -3076,7 +3292,7 @@ function WorkspaceFileManager({
3076
3292
  window.removeEventListener("keydown", handleRenameShortcut);
3077
3293
  };
3078
3294
  }, [panelsState, panelsView.selectedEntry, session]);
3079
- useEffect4(() => {
3295
+ useEffect5(() => {
3080
3296
  function resetDropOverlay() {
3081
3297
  session.resetDragDepth();
3082
3298
  }
@@ -3293,7 +3509,11 @@ function WorkspaceFileManagerPanelsContainer({
3293
3509
  () => sortWorkspaceFileEntriesForArrangeMode(state.entries, arrangeMode),
3294
3510
  [arrangeMode, state.entries]
3295
3511
  );
3296
- const iconUrlByCacheKey = useWorkspaceFileEntryIconUrls({
3512
+ const {
3513
+ iconUrlByCacheKey,
3514
+ reportEntryIconViewportEnter,
3515
+ reportEntryIconViewportLeave
3516
+ } = useWorkspaceFileEntryIconUrls({
3297
3517
  entries: arrangedEntries,
3298
3518
  resolveEntryIconUrl
3299
3519
  });
@@ -3313,6 +3533,8 @@ function WorkspaceFileManagerPanelsContainer({
3313
3533
  layoutMode,
3314
3534
  pendingDirectoryPath: view.pendingDirectoryPath,
3315
3535
  previewState: view.previewState,
3536
+ onEntryIconViewportEnter: reportEntryIconViewportEnter,
3537
+ onEntryIconViewportLeave: reportEntryIconViewportLeave,
3316
3538
  selectedEntry: view.selectedEntry,
3317
3539
  selectedPath: view.selectedPath,
3318
3540
  showDropOverlay: view.showDropOverlay,
@@ -3426,11 +3648,11 @@ function WorkspaceFileManagerContextMenuContainer({
3426
3648
  resolveOpenWithApplicationIcon,
3427
3649
  session
3428
3650
  }) {
3429
- const contextMenuRef = useRef4(null);
3651
+ const contextMenuRef = useRef5(null);
3430
3652
  const { view } = useWorkspaceFileManagerContextMenuView(session);
3431
3653
  const [openWithApplications, setOpenWithApplications] = useState7([]);
3432
3654
  const [openWithLoading, setOpenWithLoading] = useState7(false);
3433
- useEffect4(() => {
3655
+ useEffect5(() => {
3434
3656
  const entry = view.contextMenu?.entry;
3435
3657
  if (!entry || !view.showOpenWithAction) {
3436
3658
  return;
@@ -3592,7 +3814,7 @@ function WorkspaceFileManagerContextMenuContainer({
3592
3814
  }
3593
3815
  function useCloseContextMenuOnOutsideInteraction(input) {
3594
3816
  const { contextMenuRef, isOpen, session } = input;
3595
- useEffect4(() => {
3817
+ useEffect5(() => {
3596
3818
  if (!isOpen) {
3597
3819
  return;
3598
3820
  }