@parallelworks/ui 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +86 -6
  2. package/dist/ai/adapter/memoryStore.d.ts +8 -0
  3. package/dist/ai/adapter/mock/index.d.ts +2 -0
  4. package/dist/ai/adapter/mock/index.js +81 -0
  5. package/dist/ai/adapter/mock/mockAdapter.d.ts +4 -0
  6. package/dist/ai/adapter/openai/createOpenAIStream.d.ts +9 -0
  7. package/dist/ai/adapter/openai/errors.d.ts +3 -0
  8. package/dist/ai/adapter/openai/index.d.ts +6 -0
  9. package/dist/ai/adapter/openai/index.js +334 -0
  10. package/dist/ai/adapter/openai/openaiAdapter.d.ts +9 -0
  11. package/dist/ai/adapter/openai/responses.d.ts +4 -0
  12. package/dist/ai/adapter/openai/sse.d.ts +24 -0
  13. package/dist/ai/adapter/openai/wire.d.ts +40 -0
  14. package/dist/ai/adapter/types.d.ts +131 -0
  15. package/dist/ai/agent/classifier.d.ts +2 -0
  16. package/dist/ai/agent/diff.d.ts +35 -0
  17. package/dist/ai/agent/diffHighlight.d.ts +6 -0
  18. package/dist/ai/agent/index.d.ts +6 -0
  19. package/dist/ai/agent/permissions.d.ts +11 -0
  20. package/dist/ai/agent/toolfmt.d.ts +18 -0
  21. package/dist/ai/agent/transcript.d.ts +38 -0
  22. package/dist/ai/ai.css +325 -0
  23. package/dist/ai/components/AllocationSelector.d.ts +4 -0
  24. package/dist/ai/components/AttachmentManager.d.ts +1 -0
  25. package/dist/ai/components/AttachmentUpload.d.ts +24 -0
  26. package/dist/ai/components/BlockedGroupBanner.d.ts +1 -0
  27. package/dist/ai/components/BranchNavigator.d.ts +8 -0
  28. package/dist/ai/components/ChatEmptyState.d.ts +3 -0
  29. package/dist/ai/components/ChatInput.d.ts +45 -0
  30. package/dist/ai/components/ChatLayout.d.ts +5 -0
  31. package/dist/ai/components/ChatMessage.d.ts +24 -0
  32. package/dist/ai/components/ChatMessageList.d.ts +35 -0
  33. package/dist/ai/components/ChatSidebar.d.ts +5 -0
  34. package/dist/ai/components/ChatThread.d.ts +3 -0
  35. package/dist/ai/components/ComposerChrome.d.ts +18 -0
  36. package/dist/ai/components/ComposerFrame.d.ts +16 -0
  37. package/dist/ai/components/DragOverlay.d.ts +5 -0
  38. package/dist/ai/components/ModelSelector.d.ts +6 -0
  39. package/dist/ai/components/ShareDialog.d.ts +11 -0
  40. package/dist/ai/components/SlashMenu.d.ts +28 -0
  41. package/dist/ai/components/agent/ActivityLine.d.ts +21 -0
  42. package/dist/ai/components/agent/AgentMessageParts.d.ts +54 -0
  43. package/dist/ai/components/agent/PermissionPicker.d.ts +11 -0
  44. package/dist/ai/components/attachmentKeys.d.ts +4 -0
  45. package/dist/ai/core/ChatProvider.d.ts +66 -0
  46. package/dist/ai/core/KeyboardShortcutsProvider.d.ts +5 -0
  47. package/dist/ai/core/chatReducer.d.ts +114 -0
  48. package/dist/ai/core/config.d.ts +47 -0
  49. package/dist/ai/core/events.d.ts +1 -0
  50. package/dist/ai/core/exportConversation.d.ts +4 -0
  51. package/dist/ai/core/greeting.d.ts +3 -0
  52. package/dist/ai/core/parts.d.ts +11 -0
  53. package/dist/ai/core/useDragDrop.d.ts +14 -0
  54. package/dist/ai/core/useKeyboardShortcuts.d.ts +12 -0
  55. package/dist/ai/core/useSSEStream.d.ts +34 -0
  56. package/dist/ai/index.d.ts +40 -0
  57. package/dist/ai/index.js +6645 -0
  58. package/dist/ai/strings.d.ts +167 -0
  59. package/dist/ai/types.d.ts +236 -0
  60. package/dist/ai/ui/Avatar.d.ts +8 -0
  61. package/dist/ai/ui/Dropdown.d.ts +31 -0
  62. package/dist/ai/ui/Markdown.d.ts +8 -0
  63. package/dist/ai/ui/Notice.d.ts +21 -0
  64. package/dist/ai/ui/buttons.d.ts +1 -0
  65. package/dist/ai/ui/codeBreaks.d.ts +16 -0
  66. package/dist/ai/ui/prose.d.ts +1 -0
  67. package/dist/ai/ui/sidebar.d.ts +81 -0
  68. package/dist/ai/utils.d.ts +4 -0
  69. package/dist/assets/{editor.worker-CuBRIwi-.js → editor.worker-BmHUIVG4.js} +194 -163
  70. package/dist/assets/{json.worker-8ytS02vz.js → json.worker-D8bJ92nw.js} +384 -338
  71. package/dist/assets/{yaml.worker-DJC-VUcv.js → yaml.worker-C4gyGAko.js} +15657 -13823
  72. package/dist/chunks/{CodePreviewEditor-UAtnpmOV.js → CodePreviewEditor-YqQBz1YL.js} +2 -2
  73. package/dist/chunks/ConfirmModal-aOR4O1QE.js +79 -0
  74. package/dist/chunks/{lib-CUTRDpa6.js → FieldWrapper-Cszwg-4B.js} +52 -55
  75. package/dist/chunks/{CreateModal-BbV5cTj7.js → Markdown-CM-FenNE.js} +25 -97
  76. package/dist/chunks/Monaco-DJ8MW-Dq.js +110 -0
  77. package/dist/chunks/{PageHeader-MxuOs9cH.js → PageHeader-0kG_iypP.js} +170 -105
  78. package/dist/chunks/{Provider-DHmZ3v9i.js → Provider-CDYurraE.js} +60 -24
  79. package/dist/chunks/{SwitchToggle-BRQoEwLZ.js → SwitchToggle-DShNdcao.js} +143 -140
  80. package/dist/chunks/{Table-C6JdFFiJ.js → Table-DU2MW8xz.js} +6 -4
  81. package/dist/chunks/{Tooltip-n9w7Tkt1.js → Tooltip-K5HJr4_p.js} +32 -5
  82. package/dist/chunks/duration-BOv8Phl8.js +17 -0
  83. package/dist/chunks/{editor-mNznodJN.js → editor-Dk1rbV75.js} +33 -19
  84. package/dist/chunks/focus-D_iZJTW1.js +7 -0
  85. package/dist/chunks/{list-7k0eaI5N.js → list-CJm875m7.js} +497 -379
  86. package/dist/chunks/logviewer-BTKuQWzw.js +828 -0
  87. package/dist/chunks/memoryStore-CC91S5LP.js +152 -0
  88. package/dist/chunks/types-Yy0NrerF.js +11 -0
  89. package/dist/components/AddMenu.d.ts +1 -0
  90. package/dist/components/Avatar.d.ts +13 -0
  91. package/dist/components/Breadcrumbs.d.ts +1 -1
  92. package/dist/components/Dropdown.d.ts +3 -1
  93. package/dist/components/EmptyState.d.ts +1 -1
  94. package/dist/components/Indicator.d.ts +2 -2
  95. package/dist/components/Input.d.ts +1 -1
  96. package/dist/components/LetterBadge.d.ts +1 -1
  97. package/dist/components/MarkdownImpl.d.ts +1 -1
  98. package/dist/components/Provider.d.ts +18 -65
  99. package/dist/components/SettingRow.d.ts +1 -1
  100. package/dist/components/SettingsGroup.d.ts +1 -1
  101. package/dist/components/focus.d.ts +2 -0
  102. package/dist/components/keys.d.ts +12 -0
  103. package/dist/duration.d.ts +3 -0
  104. package/dist/editor/Monaco.d.ts +1 -20
  105. package/dist/editor/index.d.ts +2 -1
  106. package/dist/editor/index.js +2 -2
  107. package/dist/editor/yaml.d.ts +5 -0
  108. package/dist/engine.d.ts +98 -0
  109. package/dist/file-explorer/FileExplorer.d.ts +11 -2
  110. package/dist/file-explorer/FileExplorerContext.d.ts +1 -1
  111. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +1 -1
  112. package/dist/file-explorer/FilePreview/previewType.d.ts +1 -1
  113. package/dist/file-explorer/FileRow.d.ts +1 -1
  114. package/dist/file-explorer/helpers.d.ts +4 -4
  115. package/dist/file-explorer/index.d.ts +3 -3
  116. package/dist/file-explorer/index.js +632 -665
  117. package/dist/file-explorer/lib/types.d.ts +6 -1
  118. package/dist/file-explorer/lib/utils.d.ts +1 -3
  119. package/dist/file-explorer/useNearEnd.d.ts +1 -1
  120. package/dist/form/FieldWrapper.d.ts +2 -1
  121. package/dist/form/Fields/ResourceOptionIcon.d.ts +1 -1
  122. package/dist/form/Form.d.ts +7 -22
  123. package/dist/form/index.d.ts +11 -12
  124. package/dist/form/index.js +1333 -1454
  125. package/dist/form/lib.d.ts +6 -1
  126. package/dist/form/types/fieldComponentTypes.d.ts +4 -2
  127. package/dist/form/types/fieldTypes.d.ts +6 -20
  128. package/dist/form/useParsedOpts.d.ts +2 -2
  129. package/dist/form/utils/{slurmDuration.d.ts → duration.d.ts} +2 -2
  130. package/dist/{workflow → graph}/MatrixGroup.d.ts +2 -2
  131. package/dist/{workflow → graph}/index.d.ts +1 -1
  132. package/dist/{workflow → graph}/index.js +220 -185
  133. package/dist/{workflow → graph}/types.d.ts +3 -3
  134. package/dist/icons.d.ts +20 -21
  135. package/dist/icons.js +3 -3
  136. package/dist/index.d.ts +38 -39
  137. package/dist/index.js +355 -354
  138. package/dist/list/ListTable.d.ts +1 -1
  139. package/dist/list/ListViewControls.d.ts +2 -2
  140. package/dist/list/RowContextMenu.d.ts +9 -9
  141. package/dist/list/UserHoverCard.d.ts +45 -0
  142. package/dist/list/index.d.ts +9 -8
  143. package/dist/list/index.js +3 -3
  144. package/dist/logviewer/LogViewer.d.ts +1 -1
  145. package/dist/logviewer/index.d.ts +1 -0
  146. package/dist/logviewer/index.js +2 -742
  147. package/dist/logviewer/segments.d.ts +9 -0
  148. package/dist/styles/Geist-Variable.woff2 +0 -0
  149. package/dist/styles/GeistMono-Variable.woff2 +0 -0
  150. package/dist/styles/base.css +57 -0
  151. package/dist/styles/fonts.css +15 -0
  152. package/dist/styles/theme.css +9 -41
  153. package/dist/styles.css +1 -1
  154. package/dist/theme/index.js +2 -2
  155. package/package.json +45 -20
  156. package/LICENSE +0 -21
  157. package/dist/chunks/Monaco-DVyIpyv4.js +0 -1046
  158. package/dist/editor/usesGitlab.d.ts +0 -97
  159. package/dist/form/Fields/BucketReadOnlyField.d.ts +0 -7
  160. package/dist/form/types/workflow-schema.d.ts +0 -1145
  161. package/dist/form/utils/pwResolver.d.ts +0 -18
  162. package/dist/chunks/{BareModal-BqroLbZ5.js → BareModal-BtRiQezg.js} +1 -1
  163. package/dist/chunks/{MarkdownImpl-Bq4QhrtA.js → MarkdownImpl-CyeTVvSt.js} +1 -1
  164. /package/dist/{workflow → graph}/AnnotationBanner.d.ts +0 -0
  165. /package/dist/{workflow → graph}/Collapse.d.ts +0 -0
  166. /package/dist/{workflow → graph}/DependencyGraph.d.ts +0 -0
  167. /package/dist/{workflow → graph}/JobSummary.d.ts +0 -0
  168. /package/dist/{workflow → graph}/Reveal.d.ts +0 -0
  169. /package/dist/{workflow → graph}/TreeView.d.ts +0 -0
@@ -1,18 +1,19 @@
1
+ import { i as useNotify, l as useSlots, s as useProvisionCorsRules, u as useStrings } from "../chunks/Provider-CDYurraE.js";
1
2
  import { AccessIcon, AlertIcon, AngleDownIcon, AngleRightIcon, AngleUpIcon, ChevronLeftIcon, ChevronRightIcon, ClipboardIcon, CloseIcon, CloudUploadIcon, CollapseIcon, CustomizeIcon, DiskIcon, DocumentIcon, DownloadFileIcon, ExpandIcon, ExpandVerticalIcon, EyeIcon, FileIcon, FileTextIcon, FolderIcon, FolderOpenIcon, ImageIcon, LinkIcon, LockIcon, MinimizeIcon, MusicIcon, PdfIcon, RefreshIcon, ShareIcon, SpreadsheetIcon, SuccessIcon, TrashIcon, UploadIcon, UserIcon, VideoIcon, ZipIcon, ZoomInIcon, ZoomOutIcon } from "../icons.js";
2
- import { c as ConfirmModal, f as Markdown, h as Callout, m as CopyToClipboard, n as CreateModal, p as CopyCodeBlock } from "../chunks/CreateModal-BbV5cTj7.js";
3
- import { i as useNotify, l as useSlots, o as useProvisionCorsRules, u as useStrings } from "../chunks/Provider-DHmZ3v9i.js";
4
- import { n as TOOLTIP_ID, r as TooltipInfo } from "../chunks/Tooltip-n9w7Tkt1.js";
3
+ import { c as withPositionKeys, n as TOOLTIP_ID, o as keyedByContent, r as TooltipInfo, s as positionKeys } from "../chunks/Tooltip-K5HJr4_p.js";
4
+ import { _ as useRowMenuActive, d as assembleRowMenu, g as useRowMenu, h as useCopySubmenu, u as MoreButton } from "../chunks/PageHeader-0kG_iypP.js";
5
+ import { d as Callout, l as CopyCodeBlock, r as CreateModal, t as Markdown, u as CopyToClipboard } from "../chunks/Markdown-CM-FenNE.js";
6
+ import { t as ConfirmModal } from "../chunks/ConfirmModal-aOR4O1QE.js";
5
7
  import { t as StatusBadge } from "../chunks/StatusBadge-C1Blx_cW.js";
6
8
  import { t as Loader } from "../chunks/Loader-BXoXTZGc.js";
7
- import { _ as useRowMenuActive, d as assembleRowMenu, f as copySubmenu, g as useRowMenu, u as MoreButton } from "../chunks/PageHeader-MxuOs9cH.js";
8
- import { n as Table } from "../chunks/Table-C6JdFFiJ.js";
9
- import { C as ListSkeleton, E as RowSelectCheckbox, O as listTableFixedProps, k as listTableProps, x as ListActionsHeader } from "../chunks/list-7k0eaI5N.js";
9
+ import { n as Table } from "../chunks/Table-DU2MW8xz.js";
10
+ import { A as listTableProps, D as RowSelectCheckbox, S as ListActionsHeader, k as listTableFixedProps, w as ListSkeleton } from "../chunks/list-CJm875m7.js";
11
+ import { Menu, MenuButton, MenuItem, MenuItems, MenuSeparator } from "@headlessui/react";
10
12
  import cx from "classnames";
11
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
13
  import { Suspense, createContext, lazy, memo, use, useCallback, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
13
14
  import { useLocalStorage } from "usehooks-ts";
15
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
14
16
  import { createPortal } from "react-dom";
15
- import { Menu, MenuButton, MenuItem, MenuItems, MenuSeparator } from "@headlessui/react";
16
17
  import Skeleton from "react-loading-skeleton";
17
18
  import { DateTime } from "luxon";
18
19
  import { Resizable } from "re-resizable";
@@ -264,19 +265,6 @@ async function collectObjectKeysToDelete(nodes, listKeysUnderPrefix) {
264
265
  }
265
266
  return Array.from(keys);
266
267
  }
267
- function getPwUri(username, pwName, key) {
268
- return `pw://${username}/${pwName}/${key}`;
269
- }
270
- var cspUriSchemes = {
271
- aws: "s3",
272
- google: "gs",
273
- azure: "az"
274
- };
275
- function getCspUri(csp, cspBucketName, key) {
276
- const scheme = cspUriSchemes[csp];
277
- if (!scheme) return null;
278
- return `${scheme}://${cspBucketName}/${key}`;
279
- }
280
268
  function getUriScheme(uri) {
281
269
  return uri.match(/^([a-z0-9+.-]+):\/\//i)?.[1] ?? "";
282
270
  }
@@ -398,17 +386,7 @@ function FileProgressBar({ fileProgress, fileName, progressBarColorScheme, onCan
398
386
  const isFailed = fileProgress.status === "failed";
399
387
  const isUploading = fileProgress.status === "uploading" || !isCompleted && !isCancelled && !isFailed;
400
388
  const canCancel = isUploading && fileProgress.abortController && !isCompleted && !isCancelled && !isFailed;
401
- const barColor = useMemo(() => {
402
- if (isCompleted) return progressBarColorScheme.completed;
403
- if (isCancelled) return progressBarColorScheme.cancelled;
404
- if (isFailed) return progressBarColorScheme.failed;
405
- return progressBarColorScheme.uploading;
406
- }, [
407
- isCompleted,
408
- isCancelled,
409
- isFailed,
410
- isUploading
411
- ]);
389
+ const barColor = isCompleted ? progressBarColorScheme.completed : isCancelled ? progressBarColorScheme.cancelled : isFailed ? progressBarColorScheme.failed : progressBarColorScheme.uploading;
412
390
  const handleCancelClick = (e) => {
413
391
  e.stopPropagation();
414
392
  setShowCancelConfirm(true);
@@ -451,7 +429,7 @@ function FileProgressBar({ fileProgress, fileName, progressBarColorScheme, onCan
451
429
  children: [/* @__PURE__ */ jsx("div", {
452
430
  className: cx("h-full transition-all duration-300 relative flex items-center justify-center", barColor, isHovered && "opacity-80"),
453
431
  style: { width: `${percentage}%` }
454
- }), isHovered && !isCompleted && totalBytes > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
432
+ }), isHovered && !isCompleted && totalBytes > 0 && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
455
433
  className: "absolute inset-0 flex items-center justify-center text-xs whitespace-nowrap p-0.5 z-10",
456
434
  style: {
457
435
  WebkitClipPath: `inset(0 ${100 - percentage}% 0 0)`,
@@ -533,17 +511,7 @@ function UploadSessionToast({ session, queueLength, onCancel, onCancelFile }) {
533
511
  failed: "bg-red-600",
534
512
  cancelled: "bg-(--theme-muted-text-color)"
535
513
  };
536
- const barColor = useMemo(() => {
537
- if (isCompleted) return progressBarColorScheme.completed;
538
- if (isCancelled) return progressBarColorScheme.cancelled;
539
- if (isFailed) return progressBarColorScheme.failed;
540
- return progressBarColorScheme.uploading;
541
- }, [
542
- isCompleted,
543
- isCancelled,
544
- isFailed,
545
- isUploading
546
- ]);
514
+ const barColor = isCompleted ? progressBarColorScheme.completed : isCancelled ? progressBarColorScheme.cancelled : isFailed ? progressBarColorScheme.failed : progressBarColorScheme.uploading;
547
515
  const allFiles = session.progress?.allFiles ? Array.from(session.progress.allFiles.values()) : [];
548
516
  const uploadingFiles = session.progress?.allFiles ? Array.from(session.progress.allFiles.values()).filter((file) => file.percentage && file.percentage >= 0 && file.percentage < 100) : [];
549
517
  const shouldShowSessionCancelButton = !showCancelConfirm && (isUploading || isQueued) && session.uploadNodes.length > 1;
@@ -603,10 +571,10 @@ function UploadSessionToast({ session, queueLength, onCancel, onCancelFile }) {
603
571
  className: "text-xs theme-muted-text mt-0.5",
604
572
  children: [
605
573
  session.progress?.completedFiles || 0,
606
- " /",
607
- " ",
574
+ " / ",
608
575
  session.progress?.totalFiles || 0,
609
- " files done"
576
+ " ",
577
+ "files done"
610
578
  ]
611
579
  }), /* @__PURE__ */ jsx("div", {
612
580
  className: "text-xs theme-muted-text mt-0.5",
@@ -625,7 +593,7 @@ function UploadSessionToast({ session, queueLength, onCancel, onCancelFile }) {
625
593
  })
626
594
  ]
627
595
  })]
628
- }), isToastExpanded && /* @__PURE__ */ jsxs(Fragment, { children: [
596
+ }), isToastExpanded && /* @__PURE__ */ jsxs(Fragment$1, { children: [
629
597
  /* @__PURE__ */ jsxs("div", {
630
598
  className: "flex items-center justify-between",
631
599
  children: [/* @__PURE__ */ jsxs("div", {
@@ -642,10 +610,10 @@ function UploadSessionToast({ session, queueLength, onCancel, onCancelFile }) {
642
610
  uploadingFiles.length > 0 && `${uploadingFiles.length} uploading `,
643
611
  "(",
644
612
  session.progress?.completedFiles || 0,
645
- " /",
646
- " ",
613
+ " / ",
647
614
  session.progress?.totalFiles || 0,
648
- " done)"
615
+ " ",
616
+ "done)"
649
617
  ]
650
618
  })] })]
651
619
  }), /* @__PURE__ */ jsx("button", {
@@ -1048,11 +1016,11 @@ function FileExplorerProvider({ children }) {
1048
1016
  }),
1049
1017
  /* @__PURE__ */ jsx("ul", {
1050
1018
  className: "text-sm space-y-1 max-h-32 overflow-y-auto",
1051
- children: failedFileNames.map((name, i) => /* @__PURE__ */ jsxs("li", { children: [
1019
+ children: keyedByContent(failedFileNames, (n) => n).map(({ key, item: name }, i) => /* @__PURE__ */ jsxs("li", { children: [
1052
1020
  "• ",
1053
1021
  name,
1054
1022
  actualFailedResults[i]?.reason ? `: ${actualFailedResults[i].reason}` : ""
1055
- ] }, i))
1023
+ ] }, key))
1056
1024
  }),
1057
1025
  /* @__PURE__ */ jsx("div", {
1058
1026
  className: "mt-2 text-sm",
@@ -1127,6 +1095,166 @@ function useFileExplorer() {
1127
1095
  return context;
1128
1096
  }
1129
1097
  //#endregion
1098
+ //#region src/file-explorer/FilePreview/previewType.ts
1099
+ /** Short-lived so the link doesn't linger; an hour covers a viewing session. */
1100
+ var PREVIEW_URL_EXPIRES_IN = 3600;
1101
+ /** Cap text pulled into the tab; media streams through native elements uncapped. */
1102
+ var CODE_PREVIEW_MAX_BYTES = 10485760;
1103
+ var TABULAR_PREVIEW_MAX_BYTES = 20971520;
1104
+ /** A line cap bounds what a text preview holds and renders; the byte cap above cuts
1105
+ * short whatever the line cap hasn't, and refuses outright only content with no line
1106
+ * break to cut on. Well under Monaco's 20MB and 300K-line thresholds, past which it
1107
+ * stops tokenizing and highlighting dies. */
1108
+ var CODE_PREVIEW_MAX_LINES = 5e3;
1109
+ /** Fetched into a blob URL to render inline regardless of served content type; falls back to download above this. */
1110
+ var PDF_PREVIEW_MAX_BYTES = 52428800;
1111
+ var IMAGE_EXT = /* @__PURE__ */ new Set([
1112
+ "png",
1113
+ "jpg",
1114
+ "jpeg",
1115
+ "gif",
1116
+ "webp",
1117
+ "bmp",
1118
+ "ico",
1119
+ "avif",
1120
+ "svg"
1121
+ ]);
1122
+ var VIDEO_EXT = /* @__PURE__ */ new Set([
1123
+ "mp4",
1124
+ "mov",
1125
+ "webm",
1126
+ "m4v",
1127
+ "ogv"
1128
+ ]);
1129
+ var AUDIO_EXT = /* @__PURE__ */ new Set([
1130
+ "mp3",
1131
+ "wav",
1132
+ "ogg",
1133
+ "oga",
1134
+ "flac",
1135
+ "m4a",
1136
+ "aac"
1137
+ ]);
1138
+ var ARCHIVE_EXT = /* @__PURE__ */ new Set([
1139
+ "zip",
1140
+ "tar",
1141
+ "gz",
1142
+ "tgz",
1143
+ "bz2",
1144
+ "xz",
1145
+ "7z",
1146
+ "rar"
1147
+ ]);
1148
+ var LANGUAGE_BY_EXT = {
1149
+ py: "python",
1150
+ ipy: "python",
1151
+ js: "javascript",
1152
+ jsx: "javascript",
1153
+ ts: "typescript",
1154
+ tsx: "typescript",
1155
+ go: "go",
1156
+ rs: "rust",
1157
+ java: "java",
1158
+ c: "c",
1159
+ h: "c",
1160
+ cpp: "cpp",
1161
+ cc: "cpp",
1162
+ hpp: "cpp",
1163
+ cs: "csharp",
1164
+ rb: "ruby",
1165
+ php: "php",
1166
+ swift: "swift",
1167
+ kt: "kotlin",
1168
+ scala: "scala",
1169
+ r: "r",
1170
+ jl: "julia",
1171
+ sh: "shell",
1172
+ bash: "shell",
1173
+ zsh: "shell",
1174
+ fish: "shell",
1175
+ ps1: "powershell",
1176
+ sql: "sql",
1177
+ json: "json",
1178
+ jsonl: "json",
1179
+ yaml: "yaml",
1180
+ yml: "yaml",
1181
+ toml: "ini",
1182
+ ini: "ini",
1183
+ cfg: "ini",
1184
+ conf: "ini",
1185
+ env: "ini",
1186
+ xml: "xml",
1187
+ html: "html",
1188
+ htm: "html",
1189
+ css: "css",
1190
+ scss: "scss",
1191
+ md: "markdown",
1192
+ markdown: "markdown",
1193
+ tex: "plaintext",
1194
+ txt: "plaintext",
1195
+ log: "plaintext",
1196
+ gitignore: "plaintext"
1197
+ };
1198
+ var LANGUAGE_BY_FILENAME = {
1199
+ dockerfile: "dockerfile",
1200
+ makefile: "plaintext"
1201
+ };
1202
+ function getMonacoLanguage(node) {
1203
+ const ext = getFileExtension(node.name);
1204
+ if (ext && LANGUAGE_BY_EXT[ext]) return LANGUAGE_BY_EXT[ext];
1205
+ return LANGUAGE_BY_FILENAME[node.name.toLowerCase()] ?? "plaintext";
1206
+ }
1207
+ function isCodeFile(node) {
1208
+ const ext = getFileExtension(node.name);
1209
+ return ext !== null && ext in LANGUAGE_BY_EXT || node.name.toLowerCase() in LANGUAGE_BY_FILENAME;
1210
+ }
1211
+ function kindFromContentType(ct) {
1212
+ if (ct === "application/pdf") return "pdf";
1213
+ if (ct === "application/x-ipynb+json") return "notebook";
1214
+ if (ct === "text/csv") return "csv";
1215
+ if (ct.startsWith("image/")) return "image";
1216
+ if (ct.startsWith("video/")) return "video";
1217
+ if (ct.startsWith("audio/")) return "audio";
1218
+ if (ct.startsWith("text/") || ct === "application/json" || ct === "application/xml") return "code";
1219
+ return null;
1220
+ }
1221
+ function kindFromExtension(ext) {
1222
+ if (ext === "pdf") return "pdf";
1223
+ if (ext === "ipynb") return "notebook";
1224
+ if (ext === "csv" || ext === "tsv") return "csv";
1225
+ if (IMAGE_EXT.has(ext)) return "image";
1226
+ if (VIDEO_EXT.has(ext)) return "video";
1227
+ if (AUDIO_EXT.has(ext)) return "audio";
1228
+ if (ARCHIVE_EXT.has(ext)) return "archive";
1229
+ if (ext in LANGUAGE_BY_EXT) return "code";
1230
+ return null;
1231
+ }
1232
+ /** Content type wins when present; extension is the fallback. */
1233
+ function getPreviewKind(node) {
1234
+ if (node.type !== "file") return "unsupported";
1235
+ const ct = (node.contentType ?? "").split(";")[0]?.toLowerCase().trim() ?? "";
1236
+ const ext = getFileExtension(node.name);
1237
+ const ctKind = kindFromContentType(ct);
1238
+ const extKind = ext !== null ? kindFromExtension(ext) : null;
1239
+ if (ctKind === "code" && (extKind === "csv" || extKind === "notebook")) return extKind;
1240
+ return ctKind ?? extKind ?? (isCodeFile(node) ? "code" : "unsupported");
1241
+ }
1242
+ function canPreview(node) {
1243
+ const kind = getPreviewKind(node);
1244
+ return kind !== "unsupported" && kind !== "archive";
1245
+ }
1246
+ //#endregion
1247
+ //#region src/file-explorer/FilePreview/StreamedBadge.tsx
1248
+ function StreamedBadge() {
1249
+ const t = useStrings().fileExplorer;
1250
+ return /* @__PURE__ */ jsxs("span", {
1251
+ className: "inline-flex shrink-0 items-center gap-1 rounded-full border border-green-500/40 bg-green-500/15 px-2 py-0.5 text-[11px] text-green-600",
1252
+ "data-tooltip-id": TOOLTIP_ID,
1253
+ "data-tooltip-content": t.preview.streamedTooltip,
1254
+ children: [/* @__PURE__ */ jsx(LockIcon, { className: "h-2.5 w-2.5" }), t.preview.streamed]
1255
+ });
1256
+ }
1257
+ //#endregion
1130
1258
  //#region src/file-explorer/lib/errors.ts
1131
1259
  var CorsError = class extends Error {
1132
1260
  constructor(message) {
@@ -1135,70 +1263,236 @@ var CorsError = class extends Error {
1135
1263
  }
1136
1264
  };
1137
1265
  //#endregion
1138
- //#region src/file-explorer/helpers.tsx
1139
- var SIDEBAR_WIDTH_STORAGE_KEY = "fileExplorerSidebarWidth";
1140
- var contentTypeIconMap = [
1141
- {
1142
- pattern: /^image\//,
1143
- icon: /* @__PURE__ */ jsx(ImageIcon, { className: "h-4 w-4 text-blue-500" })
1144
- },
1145
- {
1146
- pattern: /^video\//,
1147
- icon: /* @__PURE__ */ jsx(VideoIcon, { className: "h-4 w-4 text-purple-500" })
1148
- },
1149
- {
1150
- pattern: /^application\/pdf$/,
1151
- icon: /* @__PURE__ */ jsx(PdfIcon, { className: "h-4 w-4 text-red-500" })
1152
- },
1153
- {
1154
- pattern: /(spreadsheet|csv|excel)/,
1155
- icon: /* @__PURE__ */ jsx(SpreadsheetIcon, { className: "h-4 w-4 text-green-500" })
1156
- },
1157
- {
1158
- pattern: /(document|word)/,
1159
- icon: /* @__PURE__ */ jsx(DocumentIcon, { className: "h-4 w-4 text-blue-400" })
1160
- },
1161
- {
1162
- pattern: /(javascript|json|css|html)/,
1163
- icon: /* @__PURE__ */ jsx(FileTextIcon, { className: "h-4 w-4 text-yellow-500" })
1164
- },
1165
- {
1166
- pattern: /(zip|gzip|tar)/,
1167
- icon: /* @__PURE__ */ jsx(ZipIcon, { className: "h-4 w-4 text-purple-600" })
1266
+ //#region src/file-explorer/FilePreview/previewCache.ts
1267
+ var MAX_ENTRIES = 2;
1268
+ var urlCache = /* @__PURE__ */ new Map();
1269
+ var contentCache = /* @__PURE__ */ new Map();
1270
+ function trim(cache, onEvict) {
1271
+ while (cache.size > MAX_ENTRIES) {
1272
+ const oldest = cache.keys().next().value;
1273
+ if (oldest === void 0) return;
1274
+ const evicted = cache.get(oldest);
1275
+ cache.delete(oldest);
1276
+ if (evicted !== void 0) onEvict(evicted);
1168
1277
  }
1169
- ];
1170
- var extensionIconMap = [
1171
- {
1172
- pattern: /^(jpe?g|png|gif|svg|webp)$/i,
1173
- icon: /* @__PURE__ */ jsx(ImageIcon, { className: "h-4 w-4 text-blue-500" })
1174
- },
1175
- {
1176
- pattern: /^(mp4|mov|avi|mkv)$/i,
1177
- icon: /* @__PURE__ */ jsx(VideoIcon, { className: "h-4 w-4 text-purple-500" })
1178
- },
1179
- {
1180
- pattern: /^pdf$/i,
1181
- icon: /* @__PURE__ */ jsx(PdfIcon, { className: "h-4 w-4 text-red-500" })
1182
- },
1183
- {
1184
- pattern: /^(xlsx?|csv|ods)$/i,
1185
- icon: /* @__PURE__ */ jsx(SpreadsheetIcon, { className: "h-4 w-4 text-green-500" })
1186
- },
1187
- {
1188
- pattern: /^(docx?|odt)$/i,
1189
- icon: /* @__PURE__ */ jsx(DocumentIcon, { className: "h-4 w-4 text-blue-400" })
1190
- },
1191
- {
1192
- pattern: /^(js|ts|json|css|html|md|txt)$/i,
1193
- icon: /* @__PURE__ */ jsx(FileTextIcon, { className: "h-4 w-4 text-yellow-500" })
1194
- },
1195
- {
1196
- pattern: /^(zip|gz|gzip|tar|rar)$/i,
1197
- icon: /* @__PURE__ */ jsx(ZipIcon, { className: "h-4 w-4 text-purple-600" })
1278
+ }
1279
+ function disposeContent(record) {
1280
+ record.controller.abort();
1281
+ if (record.value !== void 0) record.dispose?.(record.value);
1282
+ }
1283
+ function moveToNewest(cache, path, record) {
1284
+ cache.delete(path);
1285
+ cache.set(path, record);
1286
+ }
1287
+ /** Request a presigned URL for `path` once and remember the result; a failed or
1288
+ * empty result is not cached so a later mount retries. */
1289
+ function loadPreviewUrl(path, loader) {
1290
+ const existing = urlCache.get(path);
1291
+ if (existing) {
1292
+ moveToNewest(urlCache, path, existing);
1293
+ return existing;
1198
1294
  }
1199
- ];
1200
- function getNodeIcon(node, storageIconUrl, storageIconAlt = "Storage icon") {
1201
- const isUserNode = !node.storageId;
1295
+ const record = { promise: loader() };
1296
+ record.promise.then((value) => {
1297
+ if (value[0] && !value[1]) record.value = value;
1298
+ else if (urlCache.get(path) === record) urlCache.delete(path);
1299
+ }).catch(() => {
1300
+ if (urlCache.get(path) === record) urlCache.delete(path);
1301
+ });
1302
+ urlCache.set(path, record);
1303
+ trim(urlCache, () => {});
1304
+ return record;
1305
+ }
1306
+ /** Fetch the object for `path` once and remember it; `dispose` frees the value
1307
+ * (e.g. revokes a blob URL) when the entry is evicted. A rejected fetch is not
1308
+ * cached so a later mount retries. */
1309
+ function loadPreviewContent(path, loader, dispose) {
1310
+ const existing = contentCache.get(path);
1311
+ if (existing) {
1312
+ moveToNewest(contentCache, path, existing);
1313
+ return existing;
1314
+ }
1315
+ const controller = new AbortController();
1316
+ const record = {
1317
+ promise: loader(controller.signal),
1318
+ controller,
1319
+ dispose
1320
+ };
1321
+ record.promise.then((value) => {
1322
+ record.value = value;
1323
+ }).catch(() => {
1324
+ if (contentCache.get(path) === record) contentCache.delete(path);
1325
+ });
1326
+ contentCache.set(path, record);
1327
+ trim(contentCache, disposeContent);
1328
+ return record;
1329
+ }
1330
+ /** Fetch a preview object over its signed URL; a 401/403 means the URL is stale
1331
+ * or unauthorized, so drop it from the cache and let the next mount fetch a fresh
1332
+ * one instead of replaying the dead one. */
1333
+ async function fetchPreviewObject(path, url, signal, headers) {
1334
+ let response;
1335
+ try {
1336
+ response = await fetch(url, {
1337
+ signal,
1338
+ ...headers ? { headers } : {}
1339
+ });
1340
+ } catch (error) {
1341
+ if (error instanceof TypeError) throw new CorsError();
1342
+ throw error;
1343
+ }
1344
+ if (!response.ok) {
1345
+ if (response.status === 401 || response.status === 403) urlCache.delete(path);
1346
+ throw new Error(`Failed to load file (${response.status})`);
1347
+ }
1348
+ return response;
1349
+ }
1350
+ function peekPreviewUrl(path) {
1351
+ return urlCache.get(path)?.value;
1352
+ }
1353
+ function peekPreviewContent(path) {
1354
+ return contentCache.get(path)?.value;
1355
+ }
1356
+ function clearPreviewCache() {
1357
+ for (const record of contentCache.values()) disposeContent(record);
1358
+ contentCache.clear();
1359
+ urlCache.clear();
1360
+ }
1361
+ //#endregion
1362
+ //#region src/file-explorer/FilePreview/usePresignedUrl.ts
1363
+ function toState$2([url, error], linkErrorMessage) {
1364
+ if (error || !url) return {
1365
+ url: null,
1366
+ loading: false,
1367
+ error: error?.message ?? linkErrorMessage
1368
+ };
1369
+ return {
1370
+ url,
1371
+ loading: false,
1372
+ error: null
1373
+ };
1374
+ }
1375
+ /** Fetch a presigned URL per file, cached so repeated mounts share one request; callbacks come from refs so the effect keys only on file identity. */
1376
+ function usePresignedUrl(node, getPresignedUrl, { enabled, linkErrorMessage }) {
1377
+ const path = enabled ? node?.path ?? null : null;
1378
+ const [state, setState] = useState({
1379
+ url: null,
1380
+ loading: false,
1381
+ error: null
1382
+ });
1383
+ const [statePath, setStatePath] = useState(path);
1384
+ if (statePath !== path) {
1385
+ setStatePath(path);
1386
+ setState({
1387
+ url: null,
1388
+ loading: path !== null,
1389
+ error: null
1390
+ });
1391
+ }
1392
+ const getUrlRef = useRef(getPresignedUrl);
1393
+ getUrlRef.current = getPresignedUrl;
1394
+ const linkErrorRef = useRef(linkErrorMessage);
1395
+ linkErrorRef.current = linkErrorMessage;
1396
+ const nodeRef = useRef(node);
1397
+ nodeRef.current = node;
1398
+ useEffect(() => {
1399
+ const node = nodeRef.current;
1400
+ if (!path || !node) {
1401
+ setState({
1402
+ url: null,
1403
+ loading: false,
1404
+ error: null
1405
+ });
1406
+ return;
1407
+ }
1408
+ let cancelled = false;
1409
+ const record = loadPreviewUrl(path, () => getUrlRef.current(node));
1410
+ setState(record.value ? toState$2(record.value, linkErrorRef.current) : {
1411
+ url: null,
1412
+ loading: true,
1413
+ error: null
1414
+ });
1415
+ record.promise.then((result) => {
1416
+ if (!cancelled) setState(toState$2(result, linkErrorRef.current));
1417
+ }).catch((err) => {
1418
+ if (!cancelled) setState({
1419
+ url: null,
1420
+ loading: false,
1421
+ error: err instanceof Error ? err.message : linkErrorRef.current
1422
+ });
1423
+ });
1424
+ return () => {
1425
+ cancelled = true;
1426
+ };
1427
+ }, [path]);
1428
+ const cached = path ? peekPreviewUrl(path) : void 0;
1429
+ return cached ? toState$2(cached, linkErrorRef.current) : state;
1430
+ }
1431
+ //#endregion
1432
+ //#region src/file-explorer/helpers.tsx
1433
+ var SIDEBAR_WIDTH_STORAGE_KEY = "fileExplorerSidebarWidth";
1434
+ var contentTypeIconMap = [
1435
+ {
1436
+ pattern: /^image\//,
1437
+ icon: /* @__PURE__ */ jsx(ImageIcon, { className: "h-4 w-4 text-blue-500" })
1438
+ },
1439
+ {
1440
+ pattern: /^video\//,
1441
+ icon: /* @__PURE__ */ jsx(VideoIcon, { className: "h-4 w-4 text-purple-500" })
1442
+ },
1443
+ {
1444
+ pattern: /^application\/pdf$/,
1445
+ icon: /* @__PURE__ */ jsx(PdfIcon, { className: "h-4 w-4 text-red-500" })
1446
+ },
1447
+ {
1448
+ pattern: /(spreadsheet|csv|excel)/,
1449
+ icon: /* @__PURE__ */ jsx(SpreadsheetIcon, { className: "h-4 w-4 text-green-500" })
1450
+ },
1451
+ {
1452
+ pattern: /(document|word)/,
1453
+ icon: /* @__PURE__ */ jsx(DocumentIcon, { className: "h-4 w-4 text-blue-400" })
1454
+ },
1455
+ {
1456
+ pattern: /(javascript|json|css|html)/,
1457
+ icon: /* @__PURE__ */ jsx(FileTextIcon, { className: "h-4 w-4 text-yellow-500" })
1458
+ },
1459
+ {
1460
+ pattern: /(zip|gzip|tar)/,
1461
+ icon: /* @__PURE__ */ jsx(ZipIcon, { className: "h-4 w-4 text-purple-600" })
1462
+ }
1463
+ ];
1464
+ var extensionIconMap = [
1465
+ {
1466
+ pattern: /^(jpe?g|png|gif|svg|webp)$/i,
1467
+ icon: /* @__PURE__ */ jsx(ImageIcon, { className: "h-4 w-4 text-blue-500" })
1468
+ },
1469
+ {
1470
+ pattern: /^(mp4|mov|avi|mkv)$/i,
1471
+ icon: /* @__PURE__ */ jsx(VideoIcon, { className: "h-4 w-4 text-purple-500" })
1472
+ },
1473
+ {
1474
+ pattern: /^pdf$/i,
1475
+ icon: /* @__PURE__ */ jsx(PdfIcon, { className: "h-4 w-4 text-red-500" })
1476
+ },
1477
+ {
1478
+ pattern: /^(xlsx?|csv|ods)$/i,
1479
+ icon: /* @__PURE__ */ jsx(SpreadsheetIcon, { className: "h-4 w-4 text-green-500" })
1480
+ },
1481
+ {
1482
+ pattern: /^(docx?|odt)$/i,
1483
+ icon: /* @__PURE__ */ jsx(DocumentIcon, { className: "h-4 w-4 text-blue-400" })
1484
+ },
1485
+ {
1486
+ pattern: /^(js|ts|json|css|html|md|txt)$/i,
1487
+ icon: /* @__PURE__ */ jsx(FileTextIcon, { className: "h-4 w-4 text-yellow-500" })
1488
+ },
1489
+ {
1490
+ pattern: /^(zip|gz|gzip|tar|rar)$/i,
1491
+ icon: /* @__PURE__ */ jsx(ZipIcon, { className: "h-4 w-4 text-purple-600" })
1492
+ }
1493
+ ];
1494
+ function getNodeIcon(node, storageIconUrl, storageIconAlt = "Storage icon") {
1495
+ const isUserNode = !node.storageId;
1202
1496
  const isStorageNode = node.root;
1203
1497
  if (isUserNode) return /* @__PURE__ */ jsx(UserIcon, { className: "h-3 w-3 mr-2" });
1204
1498
  if (isStorageNode && node.storageType === "workspace") return /* @__PURE__ */ jsx(DiskIcon, { className: "h-4 w-4 mr-2" });
@@ -1493,7 +1787,7 @@ function PropagationStage({ status, label }) {
1493
1787
  "aria-hidden": "true",
1494
1788
  children: /* @__PURE__ */ jsx("polyline", { points: "3.5 8.5 6.5 11.5 12.5 5" })
1495
1789
  }),
1496
- status === "active" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400/60" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-blue-500" })] }),
1790
+ status === "active" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400/60" }), /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-blue-500" })] }),
1497
1791
  status === "pending" && /* @__PURE__ */ jsx("span", { className: "inline-flex h-1.5 w-1.5 rounded-full theme-muted-text opacity-60" })
1498
1792
  ]
1499
1793
  }), /* @__PURE__ */ jsx("span", {
@@ -1577,7 +1871,7 @@ function SkeletonPreview() {
1577
1871
  })
1578
1872
  }), /* @__PURE__ */ jsx("div", {
1579
1873
  className: "overflow-auto flex-1 px-2 py-2 space-y-1.5",
1580
- children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsx(Skeleton, { height: 14 }, i))
1874
+ children: positionKeys(6, "skeleton").map((key) => /* @__PURE__ */ jsx(Skeleton, { height: 14 }, key))
1581
1875
  })]
1582
1876
  })
1583
1877
  }), /* @__PURE__ */ jsxs("div", {
@@ -1618,493 +1912,45 @@ function ShareFileButton({ node, onShare }) {
1618
1912
  });
1619
1913
  },
1620
1914
  disabled: isPending,
1621
- children: [/* @__PURE__ */ jsx(ShareIcon, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
1622
- className: "hidden md:inline",
1623
- children: t.empty.shareThisFile
1624
- })]
1625
- });
1626
- }
1627
- async function addCorsRules({ provision, notify, strings, csp, type, username, storageName, onRefresh }) {
1628
- if (!storageName || !username || !csp) {
1629
- notify.error(strings.corsNoStorage);
1630
- return;
1631
- }
1632
- const res = await provision({
1633
- csp,
1634
- type,
1635
- username,
1636
- storageName
1637
- });
1638
- if (res.error) {
1639
- notify.error(res.error);
1640
- return;
1641
- }
1642
- if (!onRefresh) {
1643
- notify.success(strings.corsAdded);
1644
- return;
1645
- }
1646
- const pollBudgetMs = 9e4;
1647
- const pollIntervalMs = 3e3;
1648
- const deadline = Date.now() + pollBudgetMs;
1649
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1650
- while (Date.now() < deadline) {
1651
- const result = await onRefresh({ silent: true });
1652
- if (!result || !result.corsError) {
1653
- await onRefresh();
1654
- notify.success(strings.corsAdded);
1655
- return;
1656
- }
1657
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1658
- }
1659
- notify.warning(strings.corsPropagating);
1660
- }
1661
- //#endregion
1662
- //#region src/file-explorer/lib/paging.ts
1663
- /** Both providers already cap an unqualified list at 1,000, so stating it leaves
1664
- * single-page directories byte-identical. */
1665
- var LIST_PAGE_SIZE = 1e3;
1666
- function isListed(listings, path) {
1667
- return (listings.get(path)?.pagesLoaded ?? 0) > 0;
1668
- }
1669
- /** True even while a request is in flight, so the sentinel stays put. */
1670
- function hasMore(listings, path) {
1671
- return !!listings.get(path)?.cursor;
1672
- }
1673
- /** A failed page needs an explicit retry, so it stops auto-loading here. */
1674
- function canLoadMore(listings, path) {
1675
- const listing = listings.get(path);
1676
- return !!listing?.cursor && !listing.loadingMore && !listing.error;
1677
- }
1678
- /** Auto-load stops at the cap; past it the user asks for each page. */
1679
- function canAutoLoadMore(listings, path) {
1680
- return canLoadMore(listings, path) && (listings.get(path)?.pagesLoaded ?? 0) < 5;
1681
- }
1682
- function isLoadingMore(listings, path) {
1683
- return listings.get(path)?.loadingMore === true;
1684
- }
1685
- function loadMoreError(listings, path) {
1686
- return listings.get(path)?.error;
1687
- }
1688
- function beginLoadingMore(listings, path) {
1689
- const listing = listings.get(path);
1690
- if (!listing) return listings;
1691
- return new Map(listings).set(path, {
1692
- ...listing,
1693
- loadingMore: true,
1694
- error: void 0
1695
- });
1696
- }
1697
- function advanceListing(listings, path, page) {
1698
- const listing = listings.get(path);
1699
- return new Map(listings).set(path, {
1700
- cursor: page.cursor,
1701
- pagesLoaded: (listing?.pagesLoaded ?? 0) + 1,
1702
- loadingMore: false,
1703
- entries: [...listing?.entries ?? [], ...page.entries]
1704
- });
1705
- }
1706
- /** Subfolders the page no longer names lose their records, or a re-created folder
1707
- * would show old contents; the rest keep theirs, since a refresh lists them
1708
- * alongside this one and a parent landing later must not undo a child's page. */
1709
- function replaceListing(listings, path, page) {
1710
- const prefix = ensureTrailingSlash(path);
1711
- const subfolders = new Set(page.entries.filter((entry) => entry.type === "directory").map(getNodeName));
1712
- const next = new Map(listings);
1713
- next.delete(path);
1714
- for (const key of listings.keys()) {
1715
- if (!key.startsWith(prefix)) continue;
1716
- if (!subfolders.has(key.slice(prefix.length).split("/")[0] ?? "")) next.delete(key);
1717
- }
1718
- return advanceListing(next, path, page);
1719
- }
1720
- /** `pagesLoaded` stays put, so a failed first page still reads as unlisted. */
1721
- function recordListingError(listings, path, error) {
1722
- const listing = listings.get(path);
1723
- return new Map(listings).set(path, {
1724
- cursor: listing?.cursor,
1725
- pagesLoaded: listing?.pagesLoaded ?? 0,
1726
- loadingMore: false,
1727
- error,
1728
- entries: listing?.entries ?? []
1729
- });
1730
- }
1731
- /** Keeps the cursor and pages, so a retry resumes paging instead of starting over. */
1732
- function clearListingError(listings, path) {
1733
- const listing = listings.get(path);
1734
- if (!listing) return listings;
1735
- return new Map(listings).set(path, {
1736
- ...listing,
1737
- error: void 0
1738
- });
1739
- }
1740
- function clearListingsUnder(listings, path) {
1741
- const prefix = ensureTrailingSlash(path);
1742
- const next = new Map(listings);
1743
- for (const key of listings.keys()) if (key === path || key.startsWith(prefix)) next.delete(key);
1744
- return next;
1745
- }
1746
- /** Tree paths whose children should keep arrival order, since sorting them would
1747
- * reshuffle rows under the user mid-scroll. */
1748
- function incompletePaths(listings) {
1749
- const paths = /* @__PURE__ */ new Set();
1750
- for (const [path, listing] of listings) if (listing.cursor) paths.add(path);
1751
- return paths;
1752
- }
1753
- /** What a deep link needs next. Pure so the walk is table-testable. */
1754
- function nextHydrationStep({ targetResolved, ancestors, isListableDir, listings, inFlight, pagesPulled }) {
1755
- for (const path of ancestors) {
1756
- if (!isListableDir(path)) continue;
1757
- if (inFlight.has(path)) return { kind: "done" };
1758
- if (!listings.has(path)) return {
1759
- kind: "fetch",
1760
- path
1761
- };
1762
- }
1763
- if (targetResolved) return { kind: "done" };
1764
- const deepest = [...ancestors].reverse().find(isListableDir);
1765
- if (!deepest) return { kind: "done" };
1766
- if (!hasMore(listings, deepest)) return {
1767
- kind: "stuck",
1768
- path: deepest
1769
- };
1770
- if (loadMoreError(listings, deepest)) return {
1771
- kind: "stuck",
1772
- path: deepest
1773
- };
1774
- if (pagesPulled >= 20) return {
1775
- kind: "stuck",
1776
- path: deepest
1777
- };
1778
- return isLoadingMore(listings, deepest) ? { kind: "done" } : {
1779
- kind: "more",
1780
- path: deepest
1781
- };
1782
- }
1783
- //#endregion
1784
- //#region src/file-explorer/FilePreview/StreamedBadge.tsx
1785
- function StreamedBadge() {
1786
- const t = useStrings().fileExplorer;
1787
- return /* @__PURE__ */ jsxs("span", {
1788
- className: "inline-flex shrink-0 items-center gap-1 rounded-full border border-green-500/40 bg-green-500/15 px-2 py-0.5 text-[11px] text-green-600",
1789
- "data-tooltip-id": TOOLTIP_ID,
1790
- "data-tooltip-content": t.preview.streamedTooltip,
1791
- children: [/* @__PURE__ */ jsx(LockIcon, { className: "h-2.5 w-2.5" }), t.preview.streamed]
1792
- });
1793
- }
1794
- //#endregion
1795
- //#region src/file-explorer/FilePreview/previewType.ts
1796
- /** Short-lived so the link doesn't linger; an hour covers a viewing session. */
1797
- var PREVIEW_URL_EXPIRES_IN = 3600;
1798
- /** Cap text pulled into the tab; media streams through native elements uncapped. */
1799
- var CODE_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
1800
- var TABULAR_PREVIEW_MAX_BYTES = 20 * 1024 * 1024;
1801
- /** A line cap bounds what a text preview holds and renders; the byte cap above cuts
1802
- * short whatever the line cap hasn't, and refuses outright only content with no line
1803
- * break to cut on. Well under Monaco's 20MB and 300K-line thresholds, past which it
1804
- * stops tokenizing and highlighting dies. */
1805
- var CODE_PREVIEW_MAX_LINES = 5e3;
1806
- /** Fetched into a blob URL to render inline regardless of served content type; falls back to download above this. */
1807
- var PDF_PREVIEW_MAX_BYTES = 50 * 1024 * 1024;
1808
- var IMAGE_EXT = new Set([
1809
- "png",
1810
- "jpg",
1811
- "jpeg",
1812
- "gif",
1813
- "webp",
1814
- "bmp",
1815
- "ico",
1816
- "avif",
1817
- "svg"
1818
- ]);
1819
- var VIDEO_EXT = new Set([
1820
- "mp4",
1821
- "mov",
1822
- "webm",
1823
- "m4v",
1824
- "ogv"
1825
- ]);
1826
- var AUDIO_EXT = new Set([
1827
- "mp3",
1828
- "wav",
1829
- "ogg",
1830
- "oga",
1831
- "flac",
1832
- "m4a",
1833
- "aac"
1834
- ]);
1835
- var ARCHIVE_EXT = new Set([
1836
- "zip",
1837
- "tar",
1838
- "gz",
1839
- "tgz",
1840
- "bz2",
1841
- "xz",
1842
- "7z",
1843
- "rar"
1844
- ]);
1845
- var LANGUAGE_BY_EXT = {
1846
- py: "python",
1847
- ipy: "python",
1848
- js: "javascript",
1849
- jsx: "javascript",
1850
- ts: "typescript",
1851
- tsx: "typescript",
1852
- go: "go",
1853
- rs: "rust",
1854
- java: "java",
1855
- c: "c",
1856
- h: "c",
1857
- cpp: "cpp",
1858
- cc: "cpp",
1859
- hpp: "cpp",
1860
- cs: "csharp",
1861
- rb: "ruby",
1862
- php: "php",
1863
- swift: "swift",
1864
- kt: "kotlin",
1865
- scala: "scala",
1866
- r: "r",
1867
- jl: "julia",
1868
- sh: "shell",
1869
- bash: "shell",
1870
- zsh: "shell",
1871
- fish: "shell",
1872
- ps1: "powershell",
1873
- sql: "sql",
1874
- json: "json",
1875
- jsonl: "json",
1876
- yaml: "yaml",
1877
- yml: "yaml",
1878
- toml: "ini",
1879
- ini: "ini",
1880
- cfg: "ini",
1881
- conf: "ini",
1882
- env: "ini",
1883
- xml: "xml",
1884
- html: "html",
1885
- htm: "html",
1886
- css: "css",
1887
- scss: "scss",
1888
- md: "markdown",
1889
- markdown: "markdown",
1890
- tex: "plaintext",
1891
- txt: "plaintext",
1892
- log: "plaintext",
1893
- gitignore: "plaintext"
1894
- };
1895
- var LANGUAGE_BY_FILENAME = {
1896
- dockerfile: "dockerfile",
1897
- makefile: "plaintext"
1898
- };
1899
- function getMonacoLanguage(node) {
1900
- const ext = getFileExtension(node.name);
1901
- if (ext && LANGUAGE_BY_EXT[ext]) return LANGUAGE_BY_EXT[ext];
1902
- return LANGUAGE_BY_FILENAME[node.name.toLowerCase()] ?? "plaintext";
1903
- }
1904
- function isCodeFile(node) {
1905
- const ext = getFileExtension(node.name);
1906
- return ext !== null && ext in LANGUAGE_BY_EXT || node.name.toLowerCase() in LANGUAGE_BY_FILENAME;
1907
- }
1908
- function kindFromContentType(ct) {
1909
- if (ct === "application/pdf") return "pdf";
1910
- if (ct === "application/x-ipynb+json") return "notebook";
1911
- if (ct === "text/csv") return "csv";
1912
- if (ct.startsWith("image/")) return "image";
1913
- if (ct.startsWith("video/")) return "video";
1914
- if (ct.startsWith("audio/")) return "audio";
1915
- if (ct.startsWith("text/") || ct === "application/json" || ct === "application/xml") return "code";
1916
- return null;
1917
- }
1918
- function kindFromExtension(ext) {
1919
- if (ext === "pdf") return "pdf";
1920
- if (ext === "ipynb") return "notebook";
1921
- if (ext === "csv" || ext === "tsv") return "csv";
1922
- if (IMAGE_EXT.has(ext)) return "image";
1923
- if (VIDEO_EXT.has(ext)) return "video";
1924
- if (AUDIO_EXT.has(ext)) return "audio";
1925
- if (ARCHIVE_EXT.has(ext)) return "archive";
1926
- if (ext in LANGUAGE_BY_EXT) return "code";
1927
- return null;
1928
- }
1929
- /** Content type wins when present; extension is the fallback. */
1930
- function getPreviewKind(node) {
1931
- if (node.type !== "file") return "unsupported";
1932
- const ct = (node.contentType ?? "").split(";")[0]?.toLowerCase().trim() ?? "";
1933
- const ext = getFileExtension(node.name);
1934
- const ctKind = kindFromContentType(ct);
1935
- const extKind = ext !== null ? kindFromExtension(ext) : null;
1936
- if (ctKind === "code" && (extKind === "csv" || extKind === "notebook")) return extKind;
1937
- return ctKind ?? extKind ?? (isCodeFile(node) ? "code" : "unsupported");
1938
- }
1939
- function canPreview(node) {
1940
- const kind = getPreviewKind(node);
1941
- return kind !== "unsupported" && kind !== "archive";
1942
- }
1943
- //#endregion
1944
- //#region src/file-explorer/FilePreview/previewCache.ts
1945
- var MAX_ENTRIES = 2;
1946
- var urlCache = /* @__PURE__ */ new Map();
1947
- var contentCache = /* @__PURE__ */ new Map();
1948
- function trim(cache, onEvict) {
1949
- while (cache.size > MAX_ENTRIES) {
1950
- const oldest = cache.keys().next().value;
1951
- if (oldest === void 0) return;
1952
- const evicted = cache.get(oldest);
1953
- cache.delete(oldest);
1954
- if (evicted !== void 0) onEvict(evicted);
1955
- }
1956
- }
1957
- function disposeContent(record) {
1958
- record.controller.abort();
1959
- if (record.value !== void 0) record.dispose?.(record.value);
1960
- }
1961
- function moveToNewest(cache, path, record) {
1962
- cache.delete(path);
1963
- cache.set(path, record);
1964
- }
1965
- /** Request a presigned URL for `path` once and remember the result; a failed or
1966
- * empty result is not cached so a later mount retries. */
1967
- function loadPreviewUrl(path, loader) {
1968
- const existing = urlCache.get(path);
1969
- if (existing) {
1970
- moveToNewest(urlCache, path, existing);
1971
- return existing;
1972
- }
1973
- const record = { promise: loader() };
1974
- record.promise.then((value) => {
1975
- if (value[0] && !value[1]) record.value = value;
1976
- else if (urlCache.get(path) === record) urlCache.delete(path);
1977
- }).catch(() => {
1978
- if (urlCache.get(path) === record) urlCache.delete(path);
1979
- });
1980
- urlCache.set(path, record);
1981
- trim(urlCache, () => {});
1982
- return record;
1983
- }
1984
- /** Fetch the object for `path` once and remember it; `dispose` frees the value
1985
- * (e.g. revokes a blob URL) when the entry is evicted. A rejected fetch is not
1986
- * cached so a later mount retries. */
1987
- function loadPreviewContent(path, loader, dispose) {
1988
- const existing = contentCache.get(path);
1989
- if (existing) {
1990
- moveToNewest(contentCache, path, existing);
1991
- return existing;
1992
- }
1993
- const controller = new AbortController();
1994
- const record = {
1995
- promise: loader(controller.signal),
1996
- controller,
1997
- dispose
1998
- };
1999
- record.promise.then((value) => {
2000
- record.value = value;
2001
- }).catch(() => {
2002
- if (contentCache.get(path) === record) contentCache.delete(path);
1915
+ children: [/* @__PURE__ */ jsx(ShareIcon, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", {
1916
+ className: "hidden md:inline",
1917
+ children: t.empty.shareThisFile
1918
+ })]
2003
1919
  });
2004
- contentCache.set(path, record);
2005
- trim(contentCache, disposeContent);
2006
- return record;
2007
1920
  }
2008
- /** Fetch a preview object over its signed URL; a 401/403 means the URL is stale
2009
- * or unauthorized, so drop it from the cache and let the next mount fetch a fresh
2010
- * one instead of replaying the dead one. */
2011
- async function fetchPreviewObject(path, url, signal, headers) {
2012
- let response;
2013
- try {
2014
- response = await fetch(url, {
2015
- signal,
2016
- ...headers ? { headers } : {}
2017
- });
2018
- } catch (error) {
2019
- if (error instanceof TypeError) throw new CorsError();
2020
- throw error;
2021
- }
2022
- if (!response.ok) {
2023
- if (response.status === 401 || response.status === 403) urlCache.delete(path);
2024
- throw new Error(`Failed to load file (${response.status})`);
1921
+ async function addCorsRules({ provision, notify, strings, csp, type, username, storageName, onRefresh }) {
1922
+ if (!storageName || !username || !csp) {
1923
+ notify.error(strings.corsNoStorage);
1924
+ return;
2025
1925
  }
2026
- return response;
2027
- }
2028
- function peekPreviewUrl(path) {
2029
- return urlCache.get(path)?.value;
2030
- }
2031
- function peekPreviewContent(path) {
2032
- return contentCache.get(path)?.value;
2033
- }
2034
- function clearPreviewCache() {
2035
- for (const record of contentCache.values()) disposeContent(record);
2036
- contentCache.clear();
2037
- urlCache.clear();
2038
- }
2039
- //#endregion
2040
- //#region src/file-explorer/FilePreview/usePresignedUrl.ts
2041
- function toState$2([url, error], linkErrorMessage) {
2042
- if (error || !url) return {
2043
- url: null,
2044
- loading: false,
2045
- error: error?.message ?? linkErrorMessage
2046
- };
2047
- return {
2048
- url,
2049
- loading: false,
2050
- error: null
2051
- };
2052
- }
2053
- /** Fetch a presigned URL per file, cached so repeated mounts share one request; callbacks come from refs so the effect keys only on file identity. */
2054
- function usePresignedUrl(node, getPresignedUrl, { enabled, linkErrorMessage }) {
2055
- const path = enabled ? node?.path ?? null : null;
2056
- const [state, setState] = useState({
2057
- url: null,
2058
- loading: false,
2059
- error: null
1926
+ const res = await provision({
1927
+ csp,
1928
+ type,
1929
+ username,
1930
+ storageName
2060
1931
  });
2061
- const [statePath, setStatePath] = useState(path);
2062
- if (statePath !== path) {
2063
- setStatePath(path);
2064
- setState({
2065
- url: null,
2066
- loading: path !== null,
2067
- error: null
2068
- });
1932
+ if (res.error) {
1933
+ notify.error(res.error);
1934
+ return;
2069
1935
  }
2070
- const getUrlRef = useRef(getPresignedUrl);
2071
- getUrlRef.current = getPresignedUrl;
2072
- const linkErrorRef = useRef(linkErrorMessage);
2073
- linkErrorRef.current = linkErrorMessage;
2074
- const nodeRef = useRef(node);
2075
- nodeRef.current = node;
2076
- useEffect(() => {
2077
- const node = nodeRef.current;
2078
- if (!path || !node) {
2079
- setState({
2080
- url: null,
2081
- loading: false,
2082
- error: null
2083
- });
1936
+ if (!onRefresh) {
1937
+ notify.success(strings.corsAdded);
1938
+ return;
1939
+ }
1940
+ const pollBudgetMs = 9e4;
1941
+ const pollIntervalMs = 3e3;
1942
+ const deadline = Date.now() + pollBudgetMs;
1943
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1944
+ while (Date.now() < deadline) {
1945
+ const result = await onRefresh({ silent: true });
1946
+ if (!result || !result.corsError) {
1947
+ await onRefresh();
1948
+ notify.success(strings.corsAdded);
2084
1949
  return;
2085
1950
  }
2086
- let cancelled = false;
2087
- const record = loadPreviewUrl(path, () => getUrlRef.current(node));
2088
- setState(record.value ? toState$2(record.value, linkErrorRef.current) : {
2089
- url: null,
2090
- loading: true,
2091
- error: null
2092
- });
2093
- record.promise.then((result) => {
2094
- if (!cancelled) setState(toState$2(result, linkErrorRef.current));
2095
- }).catch((err) => {
2096
- if (!cancelled) setState({
2097
- url: null,
2098
- loading: false,
2099
- error: err instanceof Error ? err.message : linkErrorRef.current
2100
- });
2101
- });
2102
- return () => {
2103
- cancelled = true;
2104
- };
2105
- }, [path]);
2106
- const cached = path ? peekPreviewUrl(path) : void 0;
2107
- return cached ? toState$2(cached, linkErrorRef.current) : state;
1951
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1952
+ }
1953
+ notify.warning(strings.corsPropagating);
2108
1954
  }
2109
1955
  //#endregion
2110
1956
  //#region src/file-explorer/FilePreview/parse.ts
@@ -2118,11 +1964,12 @@ function parseDelimited(text, delimiter, maxRows) {
2118
1964
  for (let i = 0; i < text.length; i++) {
2119
1965
  const char = text[i];
2120
1966
  if (inQuotes) {
2121
- if (char === "\"") if (text[i + 1] === "\"") {
2122
- field += "\"";
2123
- i++;
2124
- } else inQuotes = false;
2125
- else field += char;
1967
+ if (char === "\"") {
1968
+ if (text[i + 1] === "\"") {
1969
+ field += "\"";
1970
+ i++;
1971
+ } else inQuotes = false;
1972
+ } else field += char;
2126
1973
  continue;
2127
1974
  }
2128
1975
  if (char === "\"") inQuotes = true;
@@ -2176,12 +2023,12 @@ async function readCappedBytes(response, maxBytes) {
2176
2023
  const header = response.headers.get("content-length");
2177
2024
  const declared = header === null ? NaN : Number(header);
2178
2025
  if (!Number.isFinite(declared) || declared > maxBytes) return {
2179
- bytes: new Uint8Array(0),
2026
+ bytes: /* @__PURE__ */ new Uint8Array(0),
2180
2027
  tooLarge: true
2181
2028
  };
2182
2029
  const buffer = new Uint8Array(await response.arrayBuffer());
2183
2030
  return buffer.byteLength > maxBytes ? {
2184
- bytes: new Uint8Array(0),
2031
+ bytes: /* @__PURE__ */ new Uint8Array(0),
2185
2032
  tooLarge: true
2186
2033
  } : {
2187
2034
  bytes: buffer,
@@ -2195,7 +2042,7 @@ async function readCappedBytes(response, maxBytes) {
2195
2042
  if (total > maxBytes) {
2196
2043
  await reader.cancel();
2197
2044
  return {
2198
- bytes: new Uint8Array(0),
2045
+ bytes: /* @__PURE__ */ new Uint8Array(0),
2199
2046
  tooLarge: true
2200
2047
  };
2201
2048
  }
@@ -2557,7 +2404,7 @@ function useObjectText(url, { enabled, maxBytes, maxLines, knownSize, cacheKey,
2557
2404
  }
2558
2405
  //#endregion
2559
2406
  //#region src/file-explorer/FilePreview/Viewer.tsx
2560
- var CodePreviewEditor = lazy(() => import("../chunks/CodePreviewEditor-UAtnpmOV.js"));
2407
+ var CodePreviewEditor = lazy(() => import("../chunks/CodePreviewEditor-YqQBz1YL.js"));
2561
2408
  var PREVIEW_MARKDOWN_CONTROLS = {
2562
2409
  table: { fullscreen: false },
2563
2410
  mermaid: { fullscreen: false }
@@ -2873,20 +2720,20 @@ function CsvTable({ node, text }) {
2873
2720
  children: [/* @__PURE__ */ jsx("th", {
2874
2721
  className: "w-11 border-b theme-border px-3.5 py-2 text-right font-semibold theme-muted-text",
2875
2722
  children: "#"
2876
- }), Array.from({ length: columnCount }, (_, i) => /* @__PURE__ */ jsx("th", {
2723
+ }), positionKeys(columnCount, "column").map((key, i) => /* @__PURE__ */ jsx("th", {
2877
2724
  className: "border-b theme-border px-3.5 py-2 text-left font-semibold",
2878
2725
  children: header[i] ?? ""
2879
- }, i))]
2880
- }) }), /* @__PURE__ */ jsx("tbody", { children: shown.map((row, r) => /* @__PURE__ */ jsxs("tr", {
2726
+ }, key))]
2727
+ }) }), /* @__PURE__ */ jsx("tbody", { children: withPositionKeys(shown, "row").map(({ key, item: row }, r) => /* @__PURE__ */ jsxs("tr", {
2881
2728
  className: cx(r % 2 === 1 && "bg-(--theme-panel-bg)/50"),
2882
2729
  children: [/* @__PURE__ */ jsx("td", {
2883
2730
  className: "px-3.5 py-1.5 text-right theme-muted-text",
2884
2731
  children: r + 1
2885
- }), Array.from({ length: columnCount }, (_, c) => /* @__PURE__ */ jsx("td", {
2732
+ }), positionKeys(columnCount, "cell").map((cellKey, c) => /* @__PURE__ */ jsx("td", {
2886
2733
  className: "px-3.5 py-1.5",
2887
2734
  children: row[c] ?? ""
2888
- }, c))]
2889
- }, r)) })]
2735
+ }, cellKey))]
2736
+ }, key)) })]
2890
2737
  })
2891
2738
  }), /* @__PURE__ */ jsx("div", {
2892
2739
  className: "shrink-0 border-t theme-border px-3.5 py-1.5 font-mono text-[11px] theme-muted-text",
@@ -2926,7 +2773,7 @@ function NotebookBody({ text, onDownload }) {
2926
2773
  className: "h-full w-full overflow-auto bg-(--theme-app-bg) p-4",
2927
2774
  children: /* @__PURE__ */ jsx("div", {
2928
2775
  className: "mx-auto flex max-w-4xl flex-col gap-3",
2929
- children: notebook.cells.map((cell, i) => {
2776
+ children: withPositionKeys(notebook.cells, "cell").map(({ key, item: cell }) => {
2930
2777
  const source = joinSource(cell.source);
2931
2778
  if (cell.cell_type === "markdown") return /* @__PURE__ */ jsx("div", {
2932
2779
  className: "rounded-lg border theme-border px-4 py-3",
@@ -2934,7 +2781,7 @@ function NotebookBody({ text, onDownload }) {
2934
2781
  controls: PREVIEW_MARKDOWN_CONTROLS,
2935
2782
  children: source
2936
2783
  })
2937
- }, i);
2784
+ }, key);
2938
2785
  if (cell.cell_type === "code") return /* @__PURE__ */ jsxs("div", {
2939
2786
  className: "flex gap-2",
2940
2787
  children: [/* @__PURE__ */ jsx("span", {
@@ -2945,9 +2792,9 @@ function NotebookBody({ text, onDownload }) {
2945
2792
  children: [/* @__PURE__ */ jsx("div", {
2946
2793
  className: "rounded-lg border theme-border px-1 py-1 text-xs",
2947
2794
  children: /* @__PURE__ */ jsx(Markdown, { children: toFencedCode(source, "python") })
2948
- }), (cell.outputs ?? []).map((output, o) => /* @__PURE__ */ jsx(NotebookOutputView, { output }, o))]
2795
+ }), withPositionKeys(cell.outputs ?? [], "output").map(({ key: outputKey, item: output }) => /* @__PURE__ */ jsx(NotebookOutputView, { output }, outputKey))]
2949
2796
  })]
2950
- }, i);
2797
+ }, key);
2951
2798
  return null;
2952
2799
  })
2953
2800
  })
@@ -2998,7 +2845,7 @@ function PreviewFetchState({ loading, tooLarge, error, corsError, storage, onCor
2998
2845
  });
2999
2846
  if (tooLarge) return /* @__PURE__ */ jsx(TooLargeInline, { onDownload });
3000
2847
  if (error) return /* @__PURE__ */ jsx(PreviewError, { onDownload });
3001
- return /* @__PURE__ */ jsx(Fragment, { children });
2848
+ return /* @__PURE__ */ jsx(Fragment$1, { children });
3002
2849
  }
3003
2850
  function NotPreviewable({ icon, title, message, onDownload }) {
3004
2851
  const t = useStrings().fileExplorer;
@@ -3247,7 +3094,7 @@ function FilePreview({ open, node, siblings, storage, getPresignedUrl, onNavigat
3247
3094
  onClick: (e) => {
3248
3095
  if (e.target === e.currentTarget) onClose();
3249
3096
  },
3250
- children: [isOverlay && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(EdgeButton, {
3097
+ children: [isOverlay && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(EdgeButton, {
3251
3098
  side: "left",
3252
3099
  label: t.preview.previousFile,
3253
3100
  disabled: !hasPrev,
@@ -3325,13 +3172,14 @@ function FilePreviewInline({ node, storage, getPresignedUrl, onOpenFull, onDownl
3325
3172
  enabled: active,
3326
3173
  linkErrorMessage: t.preview.linkError
3327
3174
  });
3175
+ const kind = getPreviewKind(node);
3328
3176
  return /* @__PURE__ */ jsxs("div", {
3329
3177
  className: "flex h-full min-h-0 flex-col",
3330
3178
  children: [/* @__PURE__ */ jsx("div", {
3331
3179
  className: "min-h-0 flex-1 overflow-hidden",
3332
3180
  children: /* @__PURE__ */ jsx(FilePreviewViewer, {
3333
3181
  node,
3334
- kind: getPreviewKind(node),
3182
+ kind,
3335
3183
  url,
3336
3184
  urlLoading: loading,
3337
3185
  urlError: error,
@@ -3592,7 +3440,7 @@ function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeI
3592
3440
  const storageIconAlt = useStrings().fileExplorer.chrome.storageIconAlt;
3593
3441
  useNestedScrollerWarning(scrollEl, nodes.length);
3594
3442
  useRowPitchAudit(scrollEl, nodes, items);
3595
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3443
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3596
3444
  padTop > 0 && /* @__PURE__ */ jsx(Spacer, { height: padTop }, "pad-top"),
3597
3445
  items.map((item) => {
3598
3446
  const node = nodes[item.index];
@@ -3625,7 +3473,7 @@ function FileRowsWindow({ items, padTop, padBottom, nodes, checkedItems, activeI
3625
3473
  ] });
3626
3474
  }
3627
3475
  function Spacer({ height }) {
3628
- return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("tr", {
3476
+ return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx("tr", {
3629
3477
  "aria-hidden": "true",
3630
3478
  className: "border-0",
3631
3479
  children: /* @__PURE__ */ jsx("td", {
@@ -3658,7 +3506,7 @@ function useRowPitchAudit(scrollEl, nodes, items) {
3658
3506
  * would land in `<tbody>`, since Children.toArray only flattens arrays. */
3659
3507
  function FileTableHeaders({ allSelected, someSelected, onSelectAll }) {
3660
3508
  const t = useStrings().fileExplorer;
3661
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3509
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3662
3510
  /* @__PURE__ */ jsx(Table.Header, {
3663
3511
  caps: false,
3664
3512
  className: "py-2",
@@ -3768,7 +3616,6 @@ function FileTable({ nodes, checkedItems, allSelected, someSelected, onSelectAll
3768
3616
  case "End":
3769
3617
  go(nodes.length - 1);
3770
3618
  return;
3771
- default: break;
3772
3619
  }
3773
3620
  if (e.target !== e.currentTarget || from < 0) return;
3774
3621
  const node = nodes[from];
@@ -3859,6 +3706,128 @@ function FileTable({ nodes, checkedItems, allSelected, someSelected, onSelectAll
3859
3706
  })
3860
3707
  });
3861
3708
  }
3709
+ //#endregion
3710
+ //#region src/file-explorer/lib/paging.ts
3711
+ /** Both providers already cap an unqualified list at 1,000, so stating it leaves
3712
+ * single-page directories byte-identical. */
3713
+ var LIST_PAGE_SIZE = 1e3;
3714
+ function isListed(listings, path) {
3715
+ return (listings.get(path)?.pagesLoaded ?? 0) > 0;
3716
+ }
3717
+ /** True even while a request is in flight, so the sentinel stays put. */
3718
+ function hasMore(listings, path) {
3719
+ return !!listings.get(path)?.cursor;
3720
+ }
3721
+ /** A failed page needs an explicit retry, so it stops auto-loading here. */
3722
+ function canLoadMore(listings, path) {
3723
+ const listing = listings.get(path);
3724
+ return !!listing?.cursor && !listing.loadingMore && !listing.error;
3725
+ }
3726
+ /** Auto-load stops at the cap; past it the user asks for each page. */
3727
+ function canAutoLoadMore(listings, path) {
3728
+ return canLoadMore(listings, path) && (listings.get(path)?.pagesLoaded ?? 0) < 5;
3729
+ }
3730
+ function isLoadingMore(listings, path) {
3731
+ return listings.get(path)?.loadingMore === true;
3732
+ }
3733
+ function loadMoreError(listings, path) {
3734
+ return listings.get(path)?.error;
3735
+ }
3736
+ function beginLoadingMore(listings, path) {
3737
+ const listing = listings.get(path);
3738
+ if (!listing) return listings;
3739
+ return new Map(listings).set(path, {
3740
+ ...listing,
3741
+ loadingMore: true,
3742
+ error: void 0
3743
+ });
3744
+ }
3745
+ function advanceListing(listings, path, page) {
3746
+ const listing = listings.get(path);
3747
+ return new Map(listings).set(path, {
3748
+ cursor: page.cursor,
3749
+ pagesLoaded: (listing?.pagesLoaded ?? 0) + 1,
3750
+ loadingMore: false,
3751
+ entries: [...listing?.entries ?? [], ...page.entries]
3752
+ });
3753
+ }
3754
+ /** Subfolders the page no longer names lose their records, or a re-created folder
3755
+ * would show old contents; the rest keep theirs, since a refresh lists them
3756
+ * alongside this one and a parent landing later must not undo a child's page. */
3757
+ function replaceListing(listings, path, page) {
3758
+ const prefix = ensureTrailingSlash(path);
3759
+ const subfolders = new Set(page.entries.filter((entry) => entry.type === "directory").map(getNodeName));
3760
+ const next = new Map(listings);
3761
+ next.delete(path);
3762
+ for (const key of listings.keys()) {
3763
+ if (!key.startsWith(prefix)) continue;
3764
+ if (!subfolders.has(key.slice(prefix.length).split("/")[0] ?? "")) next.delete(key);
3765
+ }
3766
+ return advanceListing(next, path, page);
3767
+ }
3768
+ /** `pagesLoaded` stays put, so a failed first page still reads as unlisted. */
3769
+ function recordListingError(listings, path, error) {
3770
+ const listing = listings.get(path);
3771
+ return new Map(listings).set(path, {
3772
+ cursor: listing?.cursor,
3773
+ pagesLoaded: listing?.pagesLoaded ?? 0,
3774
+ loadingMore: false,
3775
+ error,
3776
+ entries: listing?.entries ?? []
3777
+ });
3778
+ }
3779
+ /** Keeps the cursor and pages, so a retry resumes paging instead of starting over. */
3780
+ function clearListingError(listings, path) {
3781
+ const listing = listings.get(path);
3782
+ if (!listing) return listings;
3783
+ return new Map(listings).set(path, {
3784
+ ...listing,
3785
+ error: void 0
3786
+ });
3787
+ }
3788
+ function clearListingsUnder(listings, path) {
3789
+ const prefix = ensureTrailingSlash(path);
3790
+ const next = new Map(listings);
3791
+ for (const key of listings.keys()) if (key === path || key.startsWith(prefix)) next.delete(key);
3792
+ return next;
3793
+ }
3794
+ /** Tree paths whose children should keep arrival order, since sorting them would
3795
+ * reshuffle rows under the user mid-scroll. */
3796
+ function incompletePaths(listings) {
3797
+ const paths = /* @__PURE__ */ new Set();
3798
+ for (const [path, listing] of listings) if (listing.cursor) paths.add(path);
3799
+ return paths;
3800
+ }
3801
+ /** What a deep link needs next. Pure so the walk is table-testable. */
3802
+ function nextHydrationStep({ targetResolved, ancestors, isListableDir, listings, inFlight, pagesPulled }) {
3803
+ for (const path of ancestors) {
3804
+ if (!isListableDir(path)) continue;
3805
+ if (inFlight.has(path)) return { kind: "done" };
3806
+ if (!listings.has(path)) return {
3807
+ kind: "fetch",
3808
+ path
3809
+ };
3810
+ }
3811
+ if (targetResolved) return { kind: "done" };
3812
+ const deepest = [...ancestors].reverse().find(isListableDir);
3813
+ if (!deepest) return { kind: "done" };
3814
+ if (!hasMore(listings, deepest)) return {
3815
+ kind: "stuck",
3816
+ path: deepest
3817
+ };
3818
+ if (loadMoreError(listings, deepest)) return {
3819
+ kind: "stuck",
3820
+ path: deepest
3821
+ };
3822
+ if (pagesPulled >= 20) return {
3823
+ kind: "stuck",
3824
+ path: deepest
3825
+ };
3826
+ return isLoadingMore(listings, deepest) ? { kind: "done" } : {
3827
+ kind: "more",
3828
+ path: deepest
3829
+ };
3830
+ }
3862
3831
  /** `stepRow` skips these, so neither kind is ever an arrow-key stop. */
3863
3832
  function statusRow(kind, node, depth, hiddenFiles = 0) {
3864
3833
  return {
@@ -4100,11 +4069,11 @@ function indentStyle(depth, isDirectory) {
4100
4069
  }
4101
4070
  function IndentGuides({ depth }) {
4102
4071
  if (depth === 0) return null;
4103
- return /* @__PURE__ */ jsx(Fragment, { children: Array.from({ length: depth }, (_, level) => /* @__PURE__ */ jsx("span", {
4072
+ return /* @__PURE__ */ jsx(Fragment$1, { children: positionKeys(depth, "indent").map((key, level) => /* @__PURE__ */ jsx("span", {
4104
4073
  "aria-hidden": "true",
4105
4074
  className: "pointer-events-none absolute inset-y-0 w-px bg-(--theme-border)/60",
4106
4075
  style: { left: GUIDE_OFFSET + level * INDENT_PER_LEVEL }
4107
- }, level)) });
4076
+ }, key)) });
4108
4077
  }
4109
4078
  function RowIcons({ isDirectory, isExpanded, customIcon }) {
4110
4079
  if (!isDirectory) return /* @__PURE__ */ jsx("div", {
@@ -4112,7 +4081,7 @@ function RowIcons({ isDirectory, isExpanded, customIcon }) {
4112
4081
  children: customIcon
4113
4082
  });
4114
4083
  const folder = isExpanded ? /* @__PURE__ */ jsx(FolderOpenIcon, { className: "h-4 w-4 mr-1 text-sky-500" }) : /* @__PURE__ */ jsx(FolderIcon, { className: "h-4 w-4 mr-1 text-sky-500/80" });
4115
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(AngleRightIcon, { className: cx("h-3.5 w-3.5 shrink-0 theme-muted-text transition-transform duration-150", isExpanded && "rotate-90") }), customIcon ?? folder] });
4084
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(AngleRightIcon, { className: cx("h-3.5 w-3.5 shrink-0 theme-muted-text transition-transform duration-150", isExpanded && "rotate-90") }), customIcon ?? folder] });
4116
4085
  }
4117
4086
  /** Sized by the slot, not by its contents: react-loading-skeleton wraps each bar
4118
4087
  * in an inline span, so a bar occupies a line box taller than its `height` and
@@ -4208,7 +4177,7 @@ function useSpringLoadedDrop({ node, dropPath, isDirectory, isExpanded, onToggle
4208
4177
  };
4209
4178
  }
4210
4179
  /** Keys a row handles itself; the arrows that move between rows delegate upward,
4211
- * where the flat array is. The set is deliberately narrow, matching CORE-3765:
4180
+ * where the flat array is. The set is deliberately narrow:
4212
4181
  * no Home/End, no typeahead, and ArrowLeft/Right only toggle a directory. */
4213
4182
  function dispatchRowKey(e, { node, isDirectory, isExpanded, onSelect, onToggleExpand, onDelegate }) {
4214
4183
  if (e.key === "Enter") {
@@ -4363,7 +4332,8 @@ function TreeFrame({ treeOnly, sidebarWidth, onWidthChange, children }) {
4363
4332
  children
4364
4333
  });
4365
4334
  }
4366
- function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange }) {
4335
+ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHierarchy = false, groups = [], groupsLoading = false, selectedPath, onPathChange, getProviderAndClient, treeOnly = false, initialExpandedPaths, onExpandedPathsChange, objectUris, downloadCommand }) {
4336
+ const copySubmenu = useCopySubmenu();
4367
4337
  const t = useStrings().fileExplorer;
4368
4338
  const notify = useNotify();
4369
4339
  const slots = useSlots();
@@ -4398,7 +4368,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4398
4368
  return `${selectedPath}/`;
4399
4369
  }, [selectedPath, treeMap]);
4400
4370
  const setSelectedPath = onPathChange;
4401
- const [expandedPaths, setExpandedPaths] = useState(() => new Set(["", ...initialExpandedPaths ?? []]));
4371
+ const [expandedPaths, setExpandedPaths] = useState(() => /* @__PURE__ */ new Set(["", ...initialExpandedPaths ?? []]));
4402
4372
  const onExpandedPathsChangeRef = useRef(onExpandedPathsChange);
4403
4373
  onExpandedPathsChangeRef.current = onExpandedPathsChange;
4404
4374
  useEffect(() => {
@@ -4425,7 +4395,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4425
4395
  const selectedNodeCanUpload = selectedStorage?.canUpload !== false;
4426
4396
  const selectedNodeCanShare = selectedStorage?.canShare !== false;
4427
4397
  const selectedNodeCanManageAccess = selectedStorage?.canManageAccess !== false;
4428
- const selectedNodeSupportsPwUri = selectedStorage?.supportsPwUri !== false;
4429
4398
  const [accessDrawerOpen, setAccessDrawerOpen] = useState(false);
4430
4399
  const [connectionIssuesMessage, updateConnectionIssuesMessage] = useState(/* @__PURE__ */ new Map());
4431
4400
  const [hasCorsIssue, setHasCorsIssue] = useState(/* @__PURE__ */ new Map());
@@ -4443,7 +4412,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4443
4412
  useEffect(() => clearPreviewCache, []);
4444
4413
  const { openMenu, contextMenu } = useRowMenu();
4445
4414
  const [sidebarWidth, setSidebarWidth] = useLocalStorage(SIDEBAR_WIDTH_STORAGE_KEY, 300);
4446
- const MAX_UPLOAD_SIZE = 80 * 1024 * 1024 * 1024;
4415
+ const MAX_UPLOAD_SIZE = 85899345920;
4447
4416
  const isUploadSizeExceeded = (size) => {
4448
4417
  return size > MAX_UPLOAD_SIZE;
4449
4418
  };
@@ -4455,7 +4424,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4455
4424
  });
4456
4425
  }, []);
4457
4426
  const collapseAll = useCallback(() => {
4458
- setExpandedPaths(new Set([""]));
4427
+ setExpandedPaths(/* @__PURE__ */ new Set([""]));
4459
4428
  }, []);
4460
4429
  const getStorageProviderAndClient = async (storageId) => {
4461
4430
  const storage = storages.find((s) => s.id === storageId);
@@ -4498,7 +4467,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4498
4467
  const factory = await getStorageProviderAndClient(node.storageId);
4499
4468
  if (!factory) throw new Error("Failed to connect to storage provider.");
4500
4469
  const { storage, provider, client } = factory;
4501
- if (!storage.bucketName && storage.type !== "azure-azfiles") storage.bucketName = client.getStorageName();
4502
4470
  if (provider?.listDirectoryInput && provider?.convertDataToStorageObjects) {
4503
4471
  const dir_path = node.root ? "" : node.relativePath ?? "";
4504
4472
  const input = provider.listDirectoryInput(storage, dir_path, cursor ? { cursor } : {});
@@ -4736,7 +4704,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4736
4704
  await navigator.clipboard.writeText(presignedUrl);
4737
4705
  notify.info(t.messages.fileLinkCopied);
4738
4706
  };
4739
- const shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
4740
4707
  const triggerBrowserDownload = (url, filename) => {
4741
4708
  const a = document.createElement("a");
4742
4709
  a.href = url;
@@ -4770,16 +4737,12 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4770
4737
  if (!expandedPaths.has(node.path)) handleToggleExpand(node.path);
4771
4738
  };
4772
4739
  const getNodeUris = (node) => {
4773
- const key = getObjectKeyFromNode(node);
4774
4740
  const storage = storages.find((s) => s.id === node.storageId);
4775
- return {
4776
- pwUri: storage?.supportsPwUri !== false && storage?.user && storage?.name ? getPwUri(storage.user, storage.name, key) : null,
4777
- cspUri: storage?.csp && storage?.bucketName ? getCspUri(storage.csp, storage.bucketName, key) : null
4778
- };
4741
+ if (!storage || !objectUris) return [];
4742
+ return objectUris(storage, getObjectKeyFromNode(node));
4779
4743
  };
4780
4744
  const copyNodeUri = async (node) => {
4781
- const { pwUri, cspUri } = getNodeUris(node);
4782
- const uri = pwUri ?? cspUri;
4745
+ const uri = getNodeUris(node)[0];
4783
4746
  if (!uri) {
4784
4747
  notify.error(t.messages.noUri);
4785
4748
  return;
@@ -4826,7 +4789,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4826
4789
  const nodeCanWrite = storage?.canWrite === true;
4827
4790
  const nodeCanShare = storage?.canShare !== false;
4828
4791
  const isFile = node.type === "file";
4829
- const { pwUri, cspUri } = getNodeUris(node);
4792
+ const uris = getNodeUris(node);
4830
4793
  const actions = [];
4831
4794
  if (isFile && canPreview(node)) actions.push({
4832
4795
  key: "preview",
@@ -4862,7 +4825,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4862
4825
  icon: isFile ? /* @__PURE__ */ jsx(FileTextIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}),
4863
4826
  onSelect: () => handleSelectTreeItem(node.path, node)
4864
4827
  }];
4865
- const uriExtras = [pwUri, cspUri].filter((uri) => !!uri).map((value) => ({
4828
+ const uriExtras = uris.map((value) => ({
4866
4829
  label: `${getUriScheme(value).toUpperCase()} URI`,
4867
4830
  value,
4868
4831
  icon: /* @__PURE__ */ jsx(LinkIcon, {})
@@ -4919,13 +4882,16 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4919
4882
  if (checkedItems.size > 0) filesToDownload = Array.from(checkedItems).map((path) => treeMap[path]).filter((node) => !!node && node.type === "file");
4920
4883
  else if (selectedNode && selectedNode.type === "file") filesToDownload = [selectedNode];
4921
4884
  if (filesToDownload.length === 0) return "";
4922
- const commands = filesToDownload.map((node) => {
4885
+ if (!downloadCommand) return "";
4886
+ const objects = filesToDownload.flatMap((node) => {
4923
4887
  const storage = storages.find((s) => s.id === node.storageId);
4924
- if (!storage?.user || !storage.name) return "";
4925
- return `pw buckets cp ${shellSingleQuote(getPwUri(storage.user, storage.name, getObjectKeyFromNode(node)))} ${shellSingleQuote(`./${node.name}`)}`;
4888
+ return storage ? [{
4889
+ storage,
4890
+ key: getObjectKeyFromNode(node),
4891
+ name: node.name
4892
+ }] : [];
4926
4893
  });
4927
- if (commands.length === 1) return commands[0];
4928
- else return commands.join(" & \\\n") + " & \\\nwait";
4894
+ return downloadCommand.build(objects);
4929
4895
  };
4930
4896
  const onDeleteClick = () => {
4931
4897
  if (!selectedNodeCanWrite) {
@@ -4964,7 +4930,8 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4964
4930
  notify.error(t.messages.noItemsForDeletion);
4965
4931
  return;
4966
4932
  }
4967
- const parentNode = treeMap[getParentPath(pendingDelete[0].path)];
4933
+ const parentPath = getParentPath(pendingDelete[0].path);
4934
+ const parentNode = treeMap[parentPath];
4968
4935
  const factory = await getStorageProviderAndClient(pendingDelete[0].storageId);
4969
4936
  if (!factory) return;
4970
4937
  const { storage, provider, client } = factory;
@@ -5228,7 +5195,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5228
5195
  selectedStorage: null,
5229
5196
  onRefresh: handleRefresh
5230
5197
  })]
5231
- }) : !selectedNode ? /* @__PURE__ */ jsx(NoFileSelectedPreview, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
5198
+ }) : !selectedNode ? /* @__PURE__ */ jsx(NoFileSelectedPreview, {}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
5232
5199
  /* @__PURE__ */ jsxs("div", {
5233
5200
  className: "flex items-center justify-between gap-3 px-4 h-12 shrink-0 border-b theme-border",
5234
5201
  children: [renderBreadcrumb(), /* @__PURE__ */ jsxs("div", {
@@ -5319,20 +5286,20 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5319
5286
  type: "button",
5320
5287
  className: "text-sm rounded-sm hover:theme-hover cursor-pointer w-full items-center px-2 py-1.5",
5321
5288
  onClick: handleDownload,
5322
- children: "From browser"
5323
- }) }), selectedNodeSupportsPwUri && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, { className: "my-0.5 h-px theme-border" }), /* @__PURE__ */ jsx(MenuItem, { children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
5289
+ children: t.chrome.downloadFromBrowser
5290
+ }) }), downloadCommand && generateDownloadCommand() && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MenuSeparator, { className: "my-0.5 h-px theme-border" }), /* @__PURE__ */ jsx(MenuItem, { children: /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
5324
5291
  className: "text-sm w-full text-center pointer-events-none px-2 py-1.5",
5325
- children: "Using PW CLI"
5292
+ children: downloadCommand.label
5326
5293
  }), (() => {
5327
5294
  const downloadCommand = generateDownloadCommand();
5328
5295
  return /* @__PURE__ */ jsx(CopyCodeBlock, {
5329
5296
  textToCopy: downloadCommand,
5330
5297
  children: /* @__PURE__ */ jsx("div", {
5331
5298
  className: "overflow-x-auto text-xs",
5332
- children: downloadCommand.split("\n").map((line, index) => /* @__PURE__ */ jsx("div", {
5299
+ children: keyedByContent(downloadCommand.split("\n"), (l) => l).map(({ key, item: line }) => /* @__PURE__ */ jsx("div", {
5333
5300
  className: "whitespace-nowrap",
5334
5301
  children: line
5335
- }, index))
5302
+ }, key))
5336
5303
  })
5337
5304
  });
5338
5305
  })()] }) })] })]
@@ -5400,7 +5367,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5400
5367
  }))]
5401
5368
  }),
5402
5369
  selectedNode.type === "file" && (() => {
5403
- const { pwUri, cspUri } = getNodeUris(selectedNode);
5370
+ const uris = getNodeUris(selectedNode);
5404
5371
  const fileIcon = getNodeIcon(selectedNode, slots.storageIconUrl, t.chrome.storageIconAlt) ?? /* @__PURE__ */ jsx(FileIcon, { className: "h-5 w-5 theme-muted-text" });
5405
5372
  const modifiedDt = selectedNode.modified ? DateTime.fromISO(selectedNode.modified) : null;
5406
5373
  const createdDt = selectedNode.created ? DateTime.fromISO(selectedNode.created) : null;
@@ -5456,7 +5423,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5456
5423
  className: "tabular-nums",
5457
5424
  children: formatFileSize(selectedNode.size)
5458
5425
  }),
5459
- modifiedDt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
5426
+ modifiedDt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
5460
5427
  "aria-hidden": true,
5461
5428
  children: "·"
5462
5429
  }), /* @__PURE__ */ jsxs("span", { children: ["Modified ", modifiedDt.toRelative()] })] })
@@ -5464,9 +5431,9 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5464
5431
  })]
5465
5432
  })]
5466
5433
  }),
5467
- (pwUri || cspUri) && /* @__PURE__ */ jsxs("div", {
5434
+ uris.length > 0 && /* @__PURE__ */ jsx("div", {
5468
5435
  className: "px-6 pt-5 space-y-2",
5469
- children: [pwUri && /* @__PURE__ */ jsx(UriCard, { value: pwUri }), cspUri && /* @__PURE__ */ jsx(UriCard, { value: cspUri })]
5436
+ children: uris.map((uri) => /* @__PURE__ */ jsx(UriCard, { value: uri }, uri))
5470
5437
  }),
5471
5438
  selectedNodeCanShare && /* @__PURE__ */ jsxs("div", {
5472
5439
  className: "flex items-center gap-2 px-6 pt-4",
@@ -5486,21 +5453,21 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5486
5453
  }), /* @__PURE__ */ jsxs("dl", {
5487
5454
  className: "grid grid-cols-[minmax(0,140px)_1fr] gap-x-6 gap-y-2 text-sm",
5488
5455
  children: [
5489
- typeof selectedNode.size === "number" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5456
+ typeof selectedNode.size === "number" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5490
5457
  className: "theme-muted-text",
5491
5458
  children: t.chrome.size
5492
5459
  }), /* @__PURE__ */ jsx("dd", {
5493
5460
  className: "tabular-nums",
5494
5461
  children: formatFileSize(selectedNode.size)
5495
5462
  })] }),
5496
- selectedNode.contentType && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5463
+ selectedNode.contentType && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5497
5464
  className: "theme-muted-text",
5498
5465
  children: "Content Type"
5499
5466
  }), /* @__PURE__ */ jsx("dd", {
5500
5467
  className: "font-mono text-xs",
5501
5468
  children: selectedNode.contentType
5502
5469
  })] }),
5503
- createdDt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5470
+ createdDt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5504
5471
  className: "theme-muted-text",
5505
5472
  children: t.chrome.created
5506
5473
  }), /* @__PURE__ */ jsxs("dd", { children: [
@@ -5511,7 +5478,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5511
5478
  children: ["· ", createdDt.toRelative()]
5512
5479
  })
5513
5480
  ] })] }),
5514
- modifiedDt && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5481
+ modifiedDt && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5515
5482
  className: "theme-muted-text",
5516
5483
  children: "Last Modified"
5517
5484
  }), /* @__PURE__ */ jsxs("dd", { children: [
@@ -5522,14 +5489,14 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5522
5489
  children: ["· ", modifiedDt.toRelative()]
5523
5490
  })
5524
5491
  ] })] }),
5525
- selectedNode.hash && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5492
+ selectedNode.hash && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5526
5493
  className: "theme-muted-text",
5527
5494
  children: t.chrome.hash
5528
5495
  }), /* @__PURE__ */ jsx("dd", {
5529
5496
  className: "font-mono text-xs break-all",
5530
5497
  children: selectedNode.hash.replace(/^"|"$/g, "")
5531
5498
  })] }),
5532
- selectedNode.storageClass && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("dt", {
5499
+ selectedNode.storageClass && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("dt", {
5533
5500
  className: "theme-muted-text",
5534
5501
  children: "Storage Class"
5535
5502
  }), /* @__PURE__ */ jsx("dd", {