@parallelworks/ui 0.18.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 (161) 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-4lx3kE-w.js → CodePreviewEditor-YqQBz1YL.js} +1 -1
  73. package/dist/chunks/ConfirmModal-aOR4O1QE.js +79 -0
  74. package/dist/chunks/{FieldWrapper-Bef01EWO.js → FieldWrapper-Cszwg-4B.js} +3 -7
  75. package/dist/chunks/{CreateModal-BY6DLmur.js → Markdown-CM-FenNE.js} +19 -91
  76. package/dist/chunks/Monaco-DJ8MW-Dq.js +110 -0
  77. package/dist/chunks/{PageHeader-l49p1nM9.js → PageHeader-0kG_iypP.js} +67 -65
  78. package/dist/chunks/{Provider-Ghua9PkN.js → Provider-CDYurraE.js} +54 -20
  79. package/dist/chunks/{SwitchToggle-CHPP8uZQ.js → SwitchToggle-DShNdcao.js} +135 -132
  80. package/dist/chunks/{Table-4OHjdKdO.js → Table-DU2MW8xz.js} +5 -3
  81. package/dist/chunks/{Tooltip-Crxo_eU_.js → Tooltip-K5HJr4_p.js} +31 -4
  82. package/dist/chunks/duration-BOv8Phl8.js +17 -0
  83. package/dist/chunks/{editor-9_uVtUyy.js → editor-Dk1rbV75.js} +32 -18
  84. package/dist/chunks/focus-D_iZJTW1.js +7 -0
  85. package/dist/chunks/{list-qrojZZNv.js → list-CJm875m7.js} +377 -370
  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/Avatar.d.ts +1 -1
  90. package/dist/components/Breadcrumbs.d.ts +1 -1
  91. package/dist/components/Dropdown.d.ts +1 -1
  92. package/dist/components/EmptyState.d.ts +1 -1
  93. package/dist/components/Indicator.d.ts +2 -2
  94. package/dist/components/Input.d.ts +1 -1
  95. package/dist/components/LetterBadge.d.ts +1 -1
  96. package/dist/components/MarkdownImpl.d.ts +1 -1
  97. package/dist/components/Provider.d.ts +16 -65
  98. package/dist/components/SettingRow.d.ts +1 -1
  99. package/dist/components/SettingsGroup.d.ts +1 -1
  100. package/dist/components/focus.d.ts +2 -0
  101. package/dist/components/keys.d.ts +12 -0
  102. package/dist/duration.d.ts +3 -0
  103. package/dist/editor/Monaco.d.ts +1 -20
  104. package/dist/editor/index.d.ts +2 -1
  105. package/dist/editor/index.js +2 -2
  106. package/dist/editor/yaml.d.ts +5 -0
  107. package/dist/engine.d.ts +98 -0
  108. package/dist/file-explorer/FileExplorer.d.ts +11 -2
  109. package/dist/file-explorer/FileExplorerContext.d.ts +1 -1
  110. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +1 -1
  111. package/dist/file-explorer/FilePreview/previewType.d.ts +1 -1
  112. package/dist/file-explorer/FileRow.d.ts +1 -1
  113. package/dist/file-explorer/helpers.d.ts +4 -4
  114. package/dist/file-explorer/index.d.ts +3 -3
  115. package/dist/file-explorer/index.js +613 -647
  116. package/dist/file-explorer/lib/types.d.ts +6 -1
  117. package/dist/file-explorer/lib/utils.d.ts +1 -3
  118. package/dist/file-explorer/useNearEnd.d.ts +1 -1
  119. package/dist/form/FieldWrapper.d.ts +2 -2
  120. package/dist/form/Fields/ResourceOptionIcon.d.ts +1 -1
  121. package/dist/form/Form.d.ts +4 -21
  122. package/dist/form/index.d.ts +11 -12
  123. package/dist/form/index.js +1488 -1611
  124. package/dist/form/lib.d.ts +6 -1
  125. package/dist/form/types/fieldComponentTypes.d.ts +3 -2
  126. package/dist/form/types/fieldTypes.d.ts +6 -20
  127. package/dist/form/useParsedOpts.d.ts +1 -1
  128. package/dist/form/utils/{slurmDuration.d.ts → duration.d.ts} +2 -2
  129. package/dist/{workflow → graph}/MatrixGroup.d.ts +2 -2
  130. package/dist/{workflow → graph}/index.d.ts +1 -1
  131. package/dist/{workflow → graph}/index.js +215 -180
  132. package/dist/{workflow → graph}/types.d.ts +3 -3
  133. package/dist/icons.d.ts +20 -21
  134. package/dist/index.d.ts +38 -40
  135. package/dist/index.js +347 -346
  136. package/dist/list/ListTable.d.ts +1 -1
  137. package/dist/list/ListViewControls.d.ts +2 -2
  138. package/dist/list/index.d.ts +9 -9
  139. package/dist/list/index.js +2 -2
  140. package/dist/logviewer/LogViewer.d.ts +1 -1
  141. package/dist/logviewer/index.d.ts +1 -0
  142. package/dist/logviewer/index.js +2 -742
  143. package/dist/logviewer/segments.d.ts +9 -0
  144. package/dist/styles/fonts.css +4 -4
  145. package/dist/styles/theme.css +6 -4
  146. package/dist/styles.css +1 -1
  147. package/dist/theme/index.js +2 -2
  148. package/package.json +38 -14
  149. package/LICENSE +0 -202
  150. package/dist/chunks/Monaco-C1POOdVv.js +0 -1046
  151. package/dist/editor/usesGitlab.d.ts +0 -97
  152. package/dist/form/Fields/BucketReadOnlyField.d.ts +0 -7
  153. package/dist/form/types/workflow-schema.d.ts +0 -1145
  154. package/dist/form/utils/pwResolver.d.ts +0 -18
  155. package/dist/chunks/{MarkdownImpl-Bq4QhrtA.js → MarkdownImpl-CyeTVvSt.js} +1 -1
  156. /package/dist/{workflow → graph}/AnnotationBanner.d.ts +0 -0
  157. /package/dist/{workflow → graph}/Collapse.d.ts +0 -0
  158. /package/dist/{workflow → graph}/DependencyGraph.d.ts +0 -0
  159. /package/dist/{workflow → graph}/JobSummary.d.ts +0 -0
  160. /package/dist/{workflow → graph}/Reveal.d.ts +0 -0
  161. /package/dist/{workflow → graph}/TreeView.d.ts +0 -0
@@ -1,18 +1,19 @@
1
- import { i as useNotify, l as useSlots, o as useProvisionCorsRules, u as useStrings } from "../chunks/Provider-Ghua9PkN.js";
2
- import { _ as useRowMenuActive, d as assembleRowMenu, g as useRowMenu, h as useCopySubmenu, u as MoreButton } from "../chunks/PageHeader-l49p1nM9.js";
1
+ import { i as useNotify, l as useSlots, s as useProvisionCorsRules, u as useStrings } from "../chunks/Provider-CDYurraE.js";
3
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";
4
- import { c as ConfirmModal, f as Markdown, h as Callout, m as CopyToClipboard, n as CreateModal, p as CopyCodeBlock } from "../chunks/CreateModal-BY6DLmur.js";
5
- import { n as TOOLTIP_ID, r as TooltipInfo } from "../chunks/Tooltip-Crxo_eU_.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";
6
7
  import { t as StatusBadge } from "../chunks/StatusBadge-C1Blx_cW.js";
7
8
  import { t as Loader } from "../chunks/Loader-BXoXTZGc.js";
8
- import { n as Table } from "../chunks/Table-4OHjdKdO.js";
9
- import { E as ListSkeleton, M as listTableProps, j as listTableFixedProps, k as RowSelectCheckbox, w as ListActionsHeader } from "../chunks/list-qrojZZNv.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
13
  import { Suspense, createContext, lazy, memo, use, useCallback, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
12
14
  import { useLocalStorage } from "usehooks-ts";
13
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);
@@ -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",
@@ -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" });
@@ -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-4lx3kE-w.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
  })
@@ -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,
@@ -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$1, { 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", {
@@ -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,7 @@ 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 }) {
4367
4336
  const copySubmenu = useCopySubmenu();
4368
4337
  const t = useStrings().fileExplorer;
4369
4338
  const notify = useNotify();
@@ -4399,7 +4368,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4399
4368
  return `${selectedPath}/`;
4400
4369
  }, [selectedPath, treeMap]);
4401
4370
  const setSelectedPath = onPathChange;
4402
- const [expandedPaths, setExpandedPaths] = useState(() => new Set(["", ...initialExpandedPaths ?? []]));
4371
+ const [expandedPaths, setExpandedPaths] = useState(() => /* @__PURE__ */ new Set(["", ...initialExpandedPaths ?? []]));
4403
4372
  const onExpandedPathsChangeRef = useRef(onExpandedPathsChange);
4404
4373
  onExpandedPathsChangeRef.current = onExpandedPathsChange;
4405
4374
  useEffect(() => {
@@ -4426,7 +4395,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4426
4395
  const selectedNodeCanUpload = selectedStorage?.canUpload !== false;
4427
4396
  const selectedNodeCanShare = selectedStorage?.canShare !== false;
4428
4397
  const selectedNodeCanManageAccess = selectedStorage?.canManageAccess !== false;
4429
- const selectedNodeSupportsPwUri = selectedStorage?.supportsPwUri !== false;
4430
4398
  const [accessDrawerOpen, setAccessDrawerOpen] = useState(false);
4431
4399
  const [connectionIssuesMessage, updateConnectionIssuesMessage] = useState(/* @__PURE__ */ new Map());
4432
4400
  const [hasCorsIssue, setHasCorsIssue] = useState(/* @__PURE__ */ new Map());
@@ -4444,7 +4412,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4444
4412
  useEffect(() => clearPreviewCache, []);
4445
4413
  const { openMenu, contextMenu } = useRowMenu();
4446
4414
  const [sidebarWidth, setSidebarWidth] = useLocalStorage(SIDEBAR_WIDTH_STORAGE_KEY, 300);
4447
- const MAX_UPLOAD_SIZE = 80 * 1024 * 1024 * 1024;
4415
+ const MAX_UPLOAD_SIZE = 85899345920;
4448
4416
  const isUploadSizeExceeded = (size) => {
4449
4417
  return size > MAX_UPLOAD_SIZE;
4450
4418
  };
@@ -4456,7 +4424,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4456
4424
  });
4457
4425
  }, []);
4458
4426
  const collapseAll = useCallback(() => {
4459
- setExpandedPaths(new Set([""]));
4427
+ setExpandedPaths(/* @__PURE__ */ new Set([""]));
4460
4428
  }, []);
4461
4429
  const getStorageProviderAndClient = async (storageId) => {
4462
4430
  const storage = storages.find((s) => s.id === storageId);
@@ -4499,7 +4467,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4499
4467
  const factory = await getStorageProviderAndClient(node.storageId);
4500
4468
  if (!factory) throw new Error("Failed to connect to storage provider.");
4501
4469
  const { storage, provider, client } = factory;
4502
- if (!storage.bucketName && storage.type !== "azure-azfiles") storage.bucketName = client.getStorageName();
4503
4470
  if (provider?.listDirectoryInput && provider?.convertDataToStorageObjects) {
4504
4471
  const dir_path = node.root ? "" : node.relativePath ?? "";
4505
4472
  const input = provider.listDirectoryInput(storage, dir_path, cursor ? { cursor } : {});
@@ -4737,7 +4704,6 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4737
4704
  await navigator.clipboard.writeText(presignedUrl);
4738
4705
  notify.info(t.messages.fileLinkCopied);
4739
4706
  };
4740
- const shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
4741
4707
  const triggerBrowserDownload = (url, filename) => {
4742
4708
  const a = document.createElement("a");
4743
4709
  a.href = url;
@@ -4771,16 +4737,12 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4771
4737
  if (!expandedPaths.has(node.path)) handleToggleExpand(node.path);
4772
4738
  };
4773
4739
  const getNodeUris = (node) => {
4774
- const key = getObjectKeyFromNode(node);
4775
4740
  const storage = storages.find((s) => s.id === node.storageId);
4776
- return {
4777
- pwUri: storage?.supportsPwUri !== false && storage?.user && storage?.name ? getPwUri(storage.user, storage.name, key) : null,
4778
- cspUri: storage?.csp && storage?.bucketName ? getCspUri(storage.csp, storage.bucketName, key) : null
4779
- };
4741
+ if (!storage || !objectUris) return [];
4742
+ return objectUris(storage, getObjectKeyFromNode(node));
4780
4743
  };
4781
4744
  const copyNodeUri = async (node) => {
4782
- const { pwUri, cspUri } = getNodeUris(node);
4783
- const uri = pwUri ?? cspUri;
4745
+ const uri = getNodeUris(node)[0];
4784
4746
  if (!uri) {
4785
4747
  notify.error(t.messages.noUri);
4786
4748
  return;
@@ -4827,7 +4789,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4827
4789
  const nodeCanWrite = storage?.canWrite === true;
4828
4790
  const nodeCanShare = storage?.canShare !== false;
4829
4791
  const isFile = node.type === "file";
4830
- const { pwUri, cspUri } = getNodeUris(node);
4792
+ const uris = getNodeUris(node);
4831
4793
  const actions = [];
4832
4794
  if (isFile && canPreview(node)) actions.push({
4833
4795
  key: "preview",
@@ -4863,7 +4825,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4863
4825
  icon: isFile ? /* @__PURE__ */ jsx(FileTextIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}),
4864
4826
  onSelect: () => handleSelectTreeItem(node.path, node)
4865
4827
  }];
4866
- const uriExtras = [pwUri, cspUri].filter((uri) => !!uri).map((value) => ({
4828
+ const uriExtras = uris.map((value) => ({
4867
4829
  label: `${getUriScheme(value).toUpperCase()} URI`,
4868
4830
  value,
4869
4831
  icon: /* @__PURE__ */ jsx(LinkIcon, {})
@@ -4920,13 +4882,16 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4920
4882
  if (checkedItems.size > 0) filesToDownload = Array.from(checkedItems).map((path) => treeMap[path]).filter((node) => !!node && node.type === "file");
4921
4883
  else if (selectedNode && selectedNode.type === "file") filesToDownload = [selectedNode];
4922
4884
  if (filesToDownload.length === 0) return "";
4923
- const commands = filesToDownload.map((node) => {
4885
+ if (!downloadCommand) return "";
4886
+ const objects = filesToDownload.flatMap((node) => {
4924
4887
  const storage = storages.find((s) => s.id === node.storageId);
4925
- if (!storage?.user || !storage.name) return "";
4926
- 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
+ }] : [];
4927
4893
  });
4928
- if (commands.length === 1) return commands[0];
4929
- else return commands.join(" & \\\n") + " & \\\nwait";
4894
+ return downloadCommand.build(objects);
4930
4895
  };
4931
4896
  const onDeleteClick = () => {
4932
4897
  if (!selectedNodeCanWrite) {
@@ -4965,7 +4930,8 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
4965
4930
  notify.error(t.messages.noItemsForDeletion);
4966
4931
  return;
4967
4932
  }
4968
- const parentNode = treeMap[getParentPath(pendingDelete[0].path)];
4933
+ const parentPath = getParentPath(pendingDelete[0].path);
4934
+ const parentNode = treeMap[parentPath];
4969
4935
  const factory = await getStorageProviderAndClient(pendingDelete[0].storageId);
4970
4936
  if (!factory) return;
4971
4937
  const { storage, provider, client } = factory;
@@ -5320,20 +5286,20 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5320
5286
  type: "button",
5321
5287
  className: "text-sm rounded-sm hover:theme-hover cursor-pointer w-full items-center px-2 py-1.5",
5322
5288
  onClick: handleDownload,
5323
- children: "From browser"
5324
- }) }), selectedNodeSupportsPwUri && /* @__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", {
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", {
5325
5291
  className: "text-sm w-full text-center pointer-events-none px-2 py-1.5",
5326
- children: "Using PW CLI"
5292
+ children: downloadCommand.label
5327
5293
  }), (() => {
5328
5294
  const downloadCommand = generateDownloadCommand();
5329
5295
  return /* @__PURE__ */ jsx(CopyCodeBlock, {
5330
5296
  textToCopy: downloadCommand,
5331
5297
  children: /* @__PURE__ */ jsx("div", {
5332
5298
  className: "overflow-x-auto text-xs",
5333
- 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", {
5334
5300
  className: "whitespace-nowrap",
5335
5301
  children: line
5336
- }, index))
5302
+ }, key))
5337
5303
  })
5338
5304
  });
5339
5305
  })()] }) })] })]
@@ -5401,7 +5367,7 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5401
5367
  }))]
5402
5368
  }),
5403
5369
  selectedNode.type === "file" && (() => {
5404
- const { pwUri, cspUri } = getNodeUris(selectedNode);
5370
+ const uris = getNodeUris(selectedNode);
5405
5371
  const fileIcon = getNodeIcon(selectedNode, slots.storageIconUrl, t.chrome.storageIconAlt) ?? /* @__PURE__ */ jsx(FileIcon, { className: "h-5 w-5 theme-muted-text" });
5406
5372
  const modifiedDt = selectedNode.modified ? DateTime.fromISO(selectedNode.modified) : null;
5407
5373
  const createdDt = selectedNode.created ? DateTime.fromISO(selectedNode.created) : null;
@@ -5465,9 +5431,9 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
5465
5431
  })]
5466
5432
  })]
5467
5433
  }),
5468
- (pwUri || cspUri) && /* @__PURE__ */ jsxs("div", {
5434
+ uris.length > 0 && /* @__PURE__ */ jsx("div", {
5469
5435
  className: "px-6 pt-5 space-y-2",
5470
- children: [pwUri && /* @__PURE__ */ jsx(UriCard, { value: pwUri }), cspUri && /* @__PURE__ */ jsx(UriCard, { value: cspUri })]
5436
+ children: uris.map((uri) => /* @__PURE__ */ jsx(UriCard, { value: uri }, uri))
5471
5437
  }),
5472
5438
  selectedNodeCanShare && /* @__PURE__ */ jsxs("div", {
5473
5439
  className: "flex items-center gap-2 px-6 pt-4",