@tangle-network/sandbox-ui 0.77.0 → 0.80.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.
package/dist/chat.d.ts CHANGED
@@ -269,6 +269,44 @@ interface AgentSessionControlsProps {
269
269
  */
270
270
  declare function AgentSessionControls({ harness, profile, model, reasoning, filterModelsToHarness, trailing, className, context, layout, menuPlacement, }: AgentSessionControlsProps): react_jsx_runtime.JSX.Element | null;
271
271
 
272
+ /** Limits applied when staging files onto a composer. */
273
+ interface ComposerFileValidationConfig {
274
+ /** Reject any single file larger than this. */
275
+ maxSizeBytes?: number;
276
+ /** Reject files once the staged count would exceed this. */
277
+ maxCount?: number;
278
+ /** Files already staged, counted against `maxCount` before this batch. */
279
+ currentCount?: number;
280
+ /**
281
+ * Comma-separated accept list, same grammar as the native `<input accept>`
282
+ * attribute: file extensions (`.png`), exact MIME types (`image/png`), and
283
+ * MIME wildcards (`image/*`).
284
+ */
285
+ accept?: string;
286
+ }
287
+ /** A file that failed validation, paired with a human-readable reason. */
288
+ interface ComposerFileRejection {
289
+ file: File;
290
+ reason: string;
291
+ }
292
+ /**
293
+ * Checks a single file against a comma-separated `accept` list using the same
294
+ * grammar as the native `<input accept>` attribute. An undefined or empty
295
+ * `accept` accepts everything.
296
+ */
297
+ declare function isAcceptedType(file: File, accept?: string): boolean;
298
+ /**
299
+ * Splits a batch of files into what's safe to stage and what's rejected,
300
+ * checking accept type, then size, then the running count against
301
+ * `currentCount` + `maxCount`. Each rejection carries a reason naming the
302
+ * file and the limit it broke. Pure data in, pure data out — no throwing,
303
+ * no console noise, so callers decide how to surface rejections.
304
+ */
305
+ declare function validateComposerFiles(files: File[] | FileList, config: ComposerFileValidationConfig): {
306
+ accepted: File[];
307
+ rejected: ComposerFileRejection[];
308
+ };
309
+
272
310
  /** A staged attachment shown as a chip above the input. */
273
311
  interface ComposerFile {
274
312
  id: string;
@@ -278,6 +316,14 @@ interface ComposerFile {
278
316
  /** File count for a folder chip. */
279
317
  fileCount?: number;
280
318
  status: "pending" | "uploading" | "ready" | "error";
319
+ /**
320
+ * Object URL for an image thumbnail. The app owns creation and revocation
321
+ * (e.g. `URL.createObjectURL` on stage, `URL.revokeObjectURL` on remove) —
322
+ * the composer only ever reads it.
323
+ */
324
+ previewUrl?: string;
325
+ /** Shown on the chip when `status === "error"`. */
326
+ errorMessage?: string;
281
327
  }
282
328
  interface AgentComposerProps {
283
329
  /** Composer text (controlled). */
@@ -324,9 +370,42 @@ interface AgentComposerProps {
324
370
  onAttachFolder?: (files: FileList) => void;
325
371
  attachments?: ReadonlyArray<ComposerFile>;
326
372
  onRemoveFile?: (id: string) => void;
373
+ /** When set, chips with `status === "error"` render a retry button. */
374
+ onRetryFile?: (id: string) => void;
375
+ /**
376
+ * File types the composer takes, in the native `<input accept>` grammar.
377
+ * Enforced on every ingress path — the picker dialog, drag-and-drop, and
378
+ * clipboard paste — so a type the picker won't offer can't arrive by another
379
+ * route. Non-matching files go to `onRejectFiles` and never reach
380
+ * `onAttach`. Folder attach is exempt (directory selection has no native
381
+ * accept semantics); size/count limits are the app's job in `onAttach`.
382
+ */
327
383
  accept?: string;
384
+ /**
385
+ * Called with the files `accept` filtered out of a drop/paste/pick, each
386
+ * with a human-readable reason. Without it rejection is silent — the same
387
+ * feedback the native picker gives for a type it won't offer.
388
+ */
389
+ onRejectFiles?: (rejections: ComposerFileRejection[]) => void;
328
390
  dropTitle?: string;
329
391
  dropDescription?: string;
392
+ /**
393
+ * `canSend` ignores `busy` so Enter/onSubmit keep firing while a turn is
394
+ * streaming. For composers that queue the next turn instead of blocking on
395
+ * the current one — the Stop-button rendering rule (`busy && onCancel`) is
396
+ * unaffected, so the button still flips to Stop while this is set. Default
397
+ * false.
398
+ */
399
+ canSubmitWhileBusy?: boolean;
400
+ /**
401
+ * Allow submitting with empty text when at least one attachment is staged
402
+ * (an image-only message). Counts attachments of ANY status — a gate on
403
+ * `ready` would make Enter silently dead while an upload is in flight,
404
+ * with no way for the app to explain why. The app's onSubmit owns the send
405
+ * policy (block, queue, or toast on pending/errored files) and what the
406
+ * message body becomes. Default false.
407
+ */
408
+ canSubmitAttachmentsOnly?: boolean;
330
409
  /** Cmd/Ctrl+L focuses the input and shows a hint. Default false. */
331
410
  focusShortcut?: boolean;
332
411
  /** Minimum textarea rows before it grows. Default 2. */
@@ -346,11 +425,13 @@ interface AgentComposerProps {
346
425
  * router-backed composer (no harness) and the sandbox-backed one (with harness).
347
426
  *
348
427
  * Opt-in extras make it the superset of every app's hand-rolled composer:
349
- * file/folder attachments with drag-and-drop + chips, a streaming Stop button
350
- * (`busy` + `onCancel`), a `controls` override slot for bespoke picker rows, and
351
- * a Cmd/Ctrl+L focus shortcut.
428
+ * file/folder attachments with drag-and-drop + chips + clipboard paste
429
+ * (`onAttach` also gates pasting files onto the textarea, auto-naming
430
+ * generic clipboard image blobs `pasted-image-<n>.<ext>`), a streaming Stop
431
+ * button (`busy` + `onCancel`), a `controls` override slot for bespoke
432
+ * picker rows, and a Cmd/Ctrl+L focus shortcut.
352
433
  */
353
- declare function AgentComposer({ value, onChange, onSubmit, placeholder, disabled, busy, onCancel, harness, profile, model, reasoning, context, controls, trailing, onAttach, onAttachFolder, attachments, onRemoveFile, accept, dropTitle, dropDescription, focusShortcut, minRows, maxHeight, className, sendLabel, autoFocus, }: AgentComposerProps): react_jsx_runtime.JSX.Element;
434
+ declare function AgentComposer({ value, onChange, onSubmit, placeholder, disabled, busy, onCancel, harness, profile, model, reasoning, context, controls, trailing, onAttach, onAttachFolder, attachments, onRemoveFile, onRetryFile, accept, onRejectFiles, dropTitle, dropDescription, canSubmitWhileBusy, canSubmitAttachmentsOnly, focusShortcut, minRows, maxHeight, className, sendLabel, autoFocus, }: AgentComposerProps): react_jsx_runtime.JSX.Element;
354
435
 
355
436
  /**
356
437
  * Whether `harness` can run `modelId`. Router-backed harnesses, and provider-less
@@ -464,4 +545,4 @@ declare function createFetchTransport(opts: {
464
545
  fetchImpl?: typeof fetch;
465
546
  }): ArtifactAgentDockTransport;
466
547
 
467
- export { AgentComposer, type AgentComposerProps, type AgentProfileCapability, type AgentProfileDraft, type AgentProfileOption, AgentProfilePicker, type AgentProfilePickerProps, AgentSessionControls, type AgentSessionControlsProps, type AgentSessionHarnessControl, type AgentSessionModelControl, type AgentSessionProfileControl, type AgentSessionReasoningControl, ArtifactAgentDock, type ArtifactAgentDockProps, type ArtifactAgentDockTransport, type ArtifactDockMessage, type ArtifactDockStreamEvent, type ArtifactKind, type ArtifactScope, type ComposerFile, DEFAULT_REASONING_LEVEL_OPTIONS, HARNESS_REASONING_OPTIONS, type ReasoningLevel, type ReasoningLevelOption, ReasoningLevelPicker, type ReasoningLevelPickerProps, createFetchTransport, isModelCompatibleWithHarness, snapModelToHarness };
548
+ export { AgentComposer, type AgentComposerProps, type AgentProfileCapability, type AgentProfileDraft, type AgentProfileOption, AgentProfilePicker, type AgentProfilePickerProps, AgentSessionControls, type AgentSessionControlsProps, type AgentSessionHarnessControl, type AgentSessionModelControl, type AgentSessionProfileControl, type AgentSessionReasoningControl, ArtifactAgentDock, type ArtifactAgentDockProps, type ArtifactAgentDockTransport, type ArtifactDockMessage, type ArtifactDockStreamEvent, type ArtifactKind, type ArtifactScope, type ComposerFile, type ComposerFileRejection, type ComposerFileValidationConfig, DEFAULT_REASONING_LEVEL_OPTIONS, HARNESS_REASONING_OPTIONS, type ReasoningLevel, type ReasoningLevelOption, ReasoningLevelPicker, type ReasoningLevelPickerProps, createFetchTransport, isAcceptedType, isModelCompatibleWithHarness, snapModelToHarness, validateComposerFiles };
package/dist/chat.js CHANGED
@@ -13,11 +13,14 @@ import {
13
13
  ThinkingIndicator,
14
14
  UserMessage,
15
15
  createFetchTransport,
16
+ isAcceptedType,
16
17
  isModelCompatibleWithHarness,
17
18
  modelProvider,
18
19
  snapHarnessToModel,
19
- snapModelToHarness
20
- } from "./chunk-UQXDIQWD.js";
20
+ snapModelToHarness,
21
+ validateComposerFiles
22
+ } from "./chunk-UFVXAJSA.js";
23
+ import "./chunk-TK46XFLM.js";
21
24
  import "./chunk-IWIZ7RZE.js";
22
25
  import "./chunk-L62ZT6RS.js";
23
26
  import "./chunk-EI44GEQ5.js";
@@ -37,8 +40,10 @@ export {
37
40
  ThinkingIndicator,
38
41
  UserMessage,
39
42
  createFetchTransport,
43
+ isAcceptedType,
40
44
  isModelCompatibleWithHarness,
41
45
  modelProvider,
42
46
  snapHarnessToModel,
43
- snapModelToHarness
47
+ snapModelToHarness,
48
+ validateComposerFiles
44
49
  };
@@ -1,3 +1,6 @@
1
+ import {
2
+ formatBytes
3
+ } from "./chunk-TK46XFLM.js";
1
4
  import {
2
5
  HARNESS_OPTIONS,
3
6
  HarnessLogo,
@@ -1064,12 +1067,84 @@ import {
1064
1067
  Folder,
1065
1068
  Loader2,
1066
1069
  Paperclip,
1070
+ RotateCcw,
1067
1071
  Square,
1068
1072
  Upload,
1069
1073
  X
1070
1074
  } from "lucide-react";
1071
1075
  import * as React4 from "react";
1076
+
1077
+ // src/chat/attachment-validation.ts
1078
+ function isAcceptedType(file, accept) {
1079
+ if (!accept || accept.trim().length === 0) return true;
1080
+ const patterns = accept.split(",").map((pattern) => pattern.trim()).filter((pattern) => pattern.length > 0);
1081
+ if (patterns.length === 0) return true;
1082
+ const name = file.name.toLowerCase();
1083
+ const type = (file.type || "").toLowerCase();
1084
+ return patterns.some((pattern) => {
1085
+ const lowerPattern = pattern.toLowerCase();
1086
+ if (lowerPattern.startsWith(".")) {
1087
+ return name.endsWith(lowerPattern);
1088
+ }
1089
+ if (lowerPattern.endsWith("/*")) {
1090
+ const prefix = lowerPattern.slice(0, -1);
1091
+ return type.startsWith(prefix);
1092
+ }
1093
+ return type === lowerPattern;
1094
+ });
1095
+ }
1096
+ function validateComposerFiles(files, config) {
1097
+ const { maxSizeBytes, maxCount, currentCount = 0, accept } = config;
1098
+ const list = Array.isArray(files) ? files : Array.from(files);
1099
+ const accepted = [];
1100
+ const rejected = [];
1101
+ for (const file of list) {
1102
+ if (!isAcceptedType(file, accept)) {
1103
+ rejected.push({
1104
+ file,
1105
+ reason: `"${file.name}" is not an accepted file type (${accept}).`
1106
+ });
1107
+ continue;
1108
+ }
1109
+ if (maxSizeBytes !== void 0 && file.size > maxSizeBytes) {
1110
+ rejected.push({
1111
+ file,
1112
+ reason: `"${file.name}" (${formatBytes(file.size)}) exceeds the ${formatBytes(maxSizeBytes)} limit.`
1113
+ });
1114
+ continue;
1115
+ }
1116
+ if (maxCount !== void 0 && currentCount + accepted.length >= maxCount) {
1117
+ rejected.push({
1118
+ file,
1119
+ reason: `"${file.name}" was not added \u2014 the ${maxCount}-file limit is already reached.`
1120
+ });
1121
+ continue;
1122
+ }
1123
+ accepted.push(file);
1124
+ }
1125
+ return { accepted, rejected };
1126
+ }
1127
+
1128
+ // src/chat/agent-composer.tsx
1072
1129
  import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1130
+ var IMAGE_EXTENSION_BY_MIME = {
1131
+ "image/png": "png",
1132
+ "image/jpeg": "jpg",
1133
+ "image/jpg": "jpg",
1134
+ "image/gif": "gif",
1135
+ "image/webp": "webp",
1136
+ "image/bmp": "bmp",
1137
+ "image/svg+xml": "svg"
1138
+ };
1139
+ function isGenericImageName(name) {
1140
+ return /^(image)?(\.[a-z0-9]+)?$/i.test(name.trim());
1141
+ }
1142
+ function imageExtension(file) {
1143
+ const fromMime = IMAGE_EXTENSION_BY_MIME[file.type.toLowerCase()];
1144
+ if (fromMime) return fromMime;
1145
+ const fromName = /\.([a-z0-9]+)$/i.exec(file.name)?.[1];
1146
+ return fromName?.toLowerCase() ?? "png";
1147
+ }
1073
1148
  function AgentComposer({
1074
1149
  value,
1075
1150
  onChange,
@@ -1089,9 +1164,13 @@ function AgentComposer({
1089
1164
  onAttachFolder,
1090
1165
  attachments = [],
1091
1166
  onRemoveFile,
1167
+ onRetryFile,
1092
1168
  accept,
1169
+ onRejectFiles,
1093
1170
  dropTitle = "Drop files to add context",
1094
1171
  dropDescription = "They attach to your next message.",
1172
+ canSubmitWhileBusy = false,
1173
+ canSubmitAttachmentsOnly = false,
1095
1174
  focusShortcut = false,
1096
1175
  minRows = 2,
1097
1176
  maxHeight = 200,
@@ -1104,6 +1183,7 @@ function AgentComposer({
1104
1183
  const folderInputRef = React4.useRef(null);
1105
1184
  const [dragOver, setDragOver] = React4.useState(false);
1106
1185
  const dragDepth = React4.useRef(0);
1186
+ const pastedImageCount = React4.useRef(0);
1107
1187
  const resize = React4.useCallback(
1108
1188
  (el) => {
1109
1189
  el.style.height = "auto";
@@ -1125,7 +1205,7 @@ function AgentComposer({
1125
1205
  document.addEventListener("keydown", onKeyDown);
1126
1206
  return () => document.removeEventListener("keydown", onKeyDown);
1127
1207
  }, [focusShortcut, disabled]);
1128
- const canSend = !disabled && !busy && value.trim().length > 0;
1208
+ const canSend = !disabled && (!busy || canSubmitWhileBusy) && (value.trim().length > 0 || canSubmitAttachmentsOnly && attachments.length > 0);
1129
1209
  const submit = () => {
1130
1210
  if (!canSend) return;
1131
1211
  onSubmit();
@@ -1154,6 +1234,15 @@ function AgentComposer({
1154
1234
  event.stopPropagation();
1155
1235
  if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
1156
1236
  };
1237
+ const deliverFiles = (files) => {
1238
+ if (!onAttach || files.length === 0) return;
1239
+ const { accepted, rejected } = validateComposerFiles(files, { accept });
1240
+ if (rejected.length > 0) onRejectFiles?.(rejected);
1241
+ if (accepted.length === 0) return;
1242
+ const dt = new DataTransfer();
1243
+ for (const file of accepted) dt.items.add(file);
1244
+ onAttach(dt.files);
1245
+ };
1157
1246
  const handleDrop = (event) => {
1158
1247
  if (!dropEnabled) return;
1159
1248
  event.preventDefault();
@@ -1161,7 +1250,25 @@ function AgentComposer({
1161
1250
  dragDepth.current = 0;
1162
1251
  setDragOver(false);
1163
1252
  const files = event.dataTransfer?.files;
1164
- if (files?.length) onAttach?.(files);
1253
+ if (files?.length) deliverFiles(Array.from(files));
1254
+ };
1255
+ const handlePaste = (event) => {
1256
+ if (!onAttach) return;
1257
+ const clipboardFiles = event.clipboardData?.files;
1258
+ if (!clipboardFiles || clipboardFiles.length === 0) return;
1259
+ event.preventDefault();
1260
+ const files = Array.from(clipboardFiles).map((file) => {
1261
+ if (file.type.startsWith("image/") && isGenericImageName(file.name)) {
1262
+ pastedImageCount.current += 1;
1263
+ return new File(
1264
+ [file],
1265
+ `pasted-image-${pastedImageCount.current}.${imageExtension(file)}`,
1266
+ { type: file.type }
1267
+ );
1268
+ }
1269
+ return file;
1270
+ });
1271
+ deliverFiles(files);
1165
1272
  };
1166
1273
  const folderChips = attachments.filter((file) => file.kind === "folder");
1167
1274
  const fileChips = attachments.filter((file) => file.kind !== "folder");
@@ -1190,36 +1297,60 @@ function AgentComposer({
1190
1297
  disabled && "opacity-60"
1191
1298
  ),
1192
1299
  children: [
1193
- attachments.length > 0 && /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1.5", children: [...folderChips, ...fileChips].map((file) => /* @__PURE__ */ jsxs4(
1194
- "span",
1195
- {
1196
- className: cn(
1197
- "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
1198
- file.status === "error" ? "border-[var(--status-error,#ff4d6d)]/40 text-[var(--status-error,#ff4d6d)]" : "border-[var(--md3-outline-variant)] bg-surface-container text-foreground"
1199
- ),
1200
- children: [
1201
- file.kind === "folder" ? /* @__PURE__ */ jsx4(Folder, { className: "size-3 shrink-0" }) : /* @__PURE__ */ jsx4(Paperclip, { className: "size-3 shrink-0" }),
1202
- /* @__PURE__ */ jsx4("span", { className: "max-w-[150px] truncate", children: file.name }),
1203
- file.fileCount !== void 0 && /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground", children: [
1204
- "(",
1205
- file.fileCount,
1206
- ")"
1207
- ] }),
1208
- file.status === "uploading" && /* @__PURE__ */ jsx4(Loader2, { className: "size-3 animate-spin" }),
1209
- onRemoveFile && /* @__PURE__ */ jsx4(
1210
- "button",
1211
- {
1212
- type: "button",
1213
- "aria-label": `Remove ${file.name}`,
1214
- onClick: () => onRemoveFile(file.id),
1215
- className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1216
- children: /* @__PURE__ */ jsx4(X, { className: "size-3" })
1217
- }
1218
- )
1219
- ]
1220
- },
1221
- file.id
1222
- )) }),
1300
+ attachments.length > 0 && /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1.5", children: [...folderChips, ...fileChips].map((file) => {
1301
+ const isError = file.status === "error";
1302
+ const isPending = file.status === "pending";
1303
+ return /* @__PURE__ */ jsxs4(
1304
+ "span",
1305
+ {
1306
+ title: isError ? file.errorMessage : void 0,
1307
+ className: cn(
1308
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs",
1309
+ isError ? "border-[var(--status-error,#ff4d6d)]/40 text-[var(--status-error,#ff4d6d)]" : "border-[var(--md3-outline-variant)] bg-surface-container text-foreground",
1310
+ isPending && "opacity-60"
1311
+ ),
1312
+ children: [
1313
+ file.kind !== "folder" && file.previewUrl ? /* @__PURE__ */ jsx4(
1314
+ "img",
1315
+ {
1316
+ src: file.previewUrl,
1317
+ alt: "",
1318
+ className: "size-8 rounded object-cover"
1319
+ }
1320
+ ) : file.kind === "folder" ? /* @__PURE__ */ jsx4(Folder, { className: "size-3 shrink-0" }) : /* @__PURE__ */ jsx4(Paperclip, { className: "size-3 shrink-0" }),
1321
+ /* @__PURE__ */ jsx4("span", { className: "max-w-[150px] truncate", children: file.name }),
1322
+ file.fileCount !== void 0 && /* @__PURE__ */ jsxs4("span", { className: "text-muted-foreground", children: [
1323
+ "(",
1324
+ file.fileCount,
1325
+ ")"
1326
+ ] }),
1327
+ file.status === "uploading" && /* @__PURE__ */ jsx4(Loader2, { className: "size-3 animate-spin" }),
1328
+ isError && file.errorMessage && /* @__PURE__ */ jsx4("span", { className: "max-w-[150px] truncate text-[var(--status-error,#ff4d6d)]/80", children: file.errorMessage }),
1329
+ isError && onRetryFile && /* @__PURE__ */ jsx4(
1330
+ "button",
1331
+ {
1332
+ type: "button",
1333
+ "aria-label": `Retry upload ${file.name}`,
1334
+ onClick: () => onRetryFile(file.id),
1335
+ className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1336
+ children: /* @__PURE__ */ jsx4(RotateCcw, { className: "size-3" })
1337
+ }
1338
+ ),
1339
+ onRemoveFile && /* @__PURE__ */ jsx4(
1340
+ "button",
1341
+ {
1342
+ type: "button",
1343
+ "aria-label": `Remove ${file.name}`,
1344
+ onClick: () => onRemoveFile(file.id),
1345
+ className: "rounded p-0.5 text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring",
1346
+ children: /* @__PURE__ */ jsx4(X, { className: "size-3" })
1347
+ }
1348
+ )
1349
+ ]
1350
+ },
1351
+ file.id
1352
+ );
1353
+ }) }),
1223
1354
  /* @__PURE__ */ jsx4(
1224
1355
  "textarea",
1225
1356
  {
@@ -1230,6 +1361,7 @@ function AgentComposer({
1230
1361
  autoFocus,
1231
1362
  onChange: (event) => onChange(event.target.value),
1232
1363
  onInput: (event) => resize(event.currentTarget),
1364
+ onPaste: handlePaste,
1233
1365
  onKeyDown: (event) => {
1234
1366
  if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing && event.keyCode !== 229) {
1235
1367
  event.preventDefault();
@@ -1277,7 +1409,8 @@ function AgentComposer({
1277
1409
  className: "hidden",
1278
1410
  accept,
1279
1411
  onChange: (event) => {
1280
- if (event.target.files?.length) onAttach(event.target.files);
1412
+ if (event.target.files?.length)
1413
+ deliverFiles(Array.from(event.target.files));
1281
1414
  event.target.value = "";
1282
1415
  }
1283
1416
  }
@@ -1700,6 +1833,8 @@ export {
1700
1833
  snapHarnessToModel,
1701
1834
  AgentProfilePicker,
1702
1835
  AgentSessionControls,
1836
+ isAcceptedType,
1837
+ validateComposerFiles,
1703
1838
  AgentComposer,
1704
1839
  ArtifactAgentDock,
1705
1840
  createFetchTransport,