shadow-claw 1.23.3 → 1.23.4

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 (89) hide show
  1. package/bin/assert-version-bump.mjs +60 -38
  2. package/bin/assert-version-bump.test.mjs +74 -11
  3. package/dist/public/agent.worker.js +6 -6
  4. package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
  5. package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
  6. package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
  7. package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
  8. package/dist/public/config-BYAuxx3j.js +1 -0
  9. package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-B4Ym9VMj.js} +1 -1
  10. package/dist/public/{connections-xd_66YVU.js → connections-BEn6JYdu.js} +1 -1
  11. package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-DdYlUozq.js} +1 -1
  12. package/dist/public/docs/architecture/storage.md +5 -2
  13. package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +5 -2
  14. package/dist/public/docs/example/article/index.html +2 -2
  15. package/dist/public/docs/publishing/index.html +19 -76
  16. package/dist/public/docs/skill-creator/index.html +2 -2
  17. package/dist/public/{downloadGroupFile-BABv_1mJ.js → downloadGroupFile-Drdvg1Ev.js} +1 -1
  18. package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-BQ4KzUt5.js} +1 -1
  19. package/dist/public/{file-viewer-CCSABMsE.js → file-viewer-CcZYw-PS.js} +1 -1
  20. package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
  21. package/dist/public/{git-BkjrsRXX.js → git-DEUvQTUA.js} +2 -2
  22. package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-0NS4XLOd.js} +1 -1
  23. package/dist/public/index.html +2 -2
  24. package/dist/public/index.js +1 -1
  25. package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-llGkSJXg.js} +1 -1
  26. package/dist/public/main/index.html +2 -2
  27. package/dist/public/main/memory/index.html +2 -2
  28. package/dist/public/{markdown-6MBrIo-S.js → markdown-CyYR4App.js} +1 -1
  29. package/dist/public/mcp-reconnect-CSx4aMwy.js +1 -0
  30. package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-Dwg8NhAZ.js} +1 -1
  31. package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-DRfEudXk.js} +18 -18
  32. package/dist/public/orchestrator-o9Fj67BN.js +15 -0
  33. package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
  34. package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-CurBuKvk.js} +1 -1
  35. package/dist/public/{push-client-Dqq06wbB.js → push-client-Dii-vPEh.js} +1 -1
  36. package/dist/public/service-worker/fetch-proxy.js +1 -1
  37. package/dist/public/service-worker.js +1 -1
  38. package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-5IHaDDMJ.js} +2 -2
  39. package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-CZvz4iQz.js} +3 -3
  40. package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BKXFyRS3.js} +1 -1
  41. package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-CCLuhWdI.js} +3 -3
  42. package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-DV-87UUh.js} +1 -1
  43. package/dist/public/shadow-claw-file-viewer-Dk2dbAUd.js +577 -0
  44. package/dist/public/shadow-claw-files-BpfpJIuP.js +840 -0
  45. package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-DRA7_qck.js} +3 -3
  46. package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-1Xu5snuV.js} +1 -1
  47. package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-6Aci1BDP.js} +2 -2
  48. package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-BUjAeVXC.js} +1 -1
  49. package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-FQe4nCp3.js} +1 -1
  50. package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-DV2wibL3.js} +1 -1
  51. package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-B63S4HTU.js} +1 -1
  52. package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DE3jWj5K.js} +3 -3
  53. package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-S6DAKfYx.js} +3 -3
  54. package/dist/public/shadow-claw-storage-DaacA3_K.js +219 -0
  55. package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-IjKX042p.js} +1 -1
  56. package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Cy0JYmGU.js} +4 -4
  57. package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-znAt7HyP.js} +1 -1
  58. package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-Cutv9Si5.js} +2 -2
  59. package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BH8mPDEu.js} +1 -1
  60. package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
  61. package/dist/public/static-main-manifest.json +1 -1
  62. package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-BqJxaMGR.js} +1 -1
  63. package/package.json +1 -1
  64. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
  65. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +44 -0
  66. package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
  67. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +105 -0
  68. package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
  69. package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
  70. package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
  71. package/src/components/shadow-claw-files/shadow-claw-files.test.ts +219 -0
  72. package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
  73. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
  74. package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
  75. package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
  76. package/src/config/config.test.ts +9 -0
  77. package/src/config/config.ts +5 -0
  78. package/src/core/app-routes.test.ts +56 -0
  79. package/src/core/app-routes.ts +8 -1
  80. package/src/db/openDatabase.test.ts +10 -1
  81. package/src/db/openDatabase.ts +3 -1
  82. package/src/storage/migrateLegacyOpfs.test.ts +327 -0
  83. package/src/storage/migrateLegacyOpfs.ts +117 -0
  84. package/dist/public/config-AL77QAx4.js +0 -1
  85. package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
  86. package/dist/public/orchestrator-4su7iyWj.js +0 -15
  87. package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
  88. package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
  89. package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
@@ -251,6 +251,11 @@ export class ShadowClawFileViewer extends ShadowClawElement {
251
251
  return;
252
252
  }
253
253
 
254
+ const content = root.querySelector(".file-content");
255
+ content?.addEventListener("click", (event: Event) => {
256
+ void this.handlePreviewLinkClick(event as MouseEvent);
257
+ });
258
+
254
259
  const modal = root.querySelector(".file-modal");
255
260
  modal?.addEventListener("cancel", (event: Event) => {
256
261
  event.preventDefault();
@@ -706,9 +711,8 @@ export class ShadowClawFileViewer extends ShadowClawElement {
706
711
 
707
712
  const current = fileViewerStore.file;
708
713
  const filePath = current?.path || current?.name || "";
709
- const routeDir = getFileRouteDirPath(
710
- orchestratorStore.activeGroupId,
711
- filePath,
714
+ const routeDir = applyBasePath(
715
+ getFileRouteDirPath(orchestratorStore.activeGroupId, filePath),
712
716
  );
713
717
  const resolved = resolveHrefAgainstRoute(
714
718
  payload.href,
@@ -1110,9 +1114,8 @@ export class ShadowClawFileViewer extends ShadowClawElement {
1110
1114
  return html;
1111
1115
  }
1112
1116
 
1113
- const routeDir = getFileRouteDirPath(
1114
- orchestratorStore.activeGroupId,
1115
- filePath,
1117
+ const routeDir = applyBasePath(
1118
+ getFileRouteDirPath(orchestratorStore.activeGroupId, filePath),
1116
1119
  );
1117
1120
  const parsed = new DOMParser().parseFromString(html, "text/html");
1118
1121
 
@@ -1509,9 +1512,8 @@ export class ShadowClawFileViewer extends ShadowClawElement {
1509
1512
  const current = fileViewerStore.file;
1510
1513
  const basePath = current?.path || current?.name || "";
1511
1514
  const href = link.getAttribute("href") || "";
1512
- const routeDir = getFileRouteDirPath(
1513
- orchestratorStore.activeGroupId,
1514
- basePath,
1515
+ const routeDir = applyBasePath(
1516
+ getFileRouteDirPath(orchestratorStore.activeGroupId, basePath),
1515
1517
  );
1516
1518
  const resolved = resolveHrefAgainstRoute(
1517
1519
  href,
@@ -361,7 +361,8 @@
361
361
 
362
362
  .files__new-label,
363
363
  .files__rename-label,
364
- .files__paste-rename-label {
364
+ .files__paste-rename-label,
365
+ .files__upload-conflict-rename-label {
365
366
  color: var(--shadow-claw-text-secondary);
366
367
  font-size: var(--shadow-claw-font-size-sm);
367
368
  font-weight: 600;
@@ -369,7 +370,8 @@
369
370
 
370
371
  .files__new-input,
371
372
  .files__rename-input,
372
- .files__paste-rename-input {
373
+ .files__paste-rename-input,
374
+ .files__upload-conflict-rename-input {
373
375
  background-color: var(--shadow-claw-bg-primary);
374
376
  border: 0.0625rem solid var(--shadow-claw-border-color);
375
377
  border-radius: var(--shadow-claw-radius-s);
@@ -379,7 +381,8 @@
379
381
  padding: 0.375rem 0.5rem;
380
382
  }
381
383
 
382
- .files__paste-rename-container {
384
+ .files__paste-rename-container,
385
+ .files__upload-conflict-rename-container {
383
386
  display: flex;
384
387
  flex-direction: column;
385
388
  gap: 0.5rem;
@@ -401,7 +404,8 @@
401
404
 
402
405
  .files__new-input:focus,
403
406
  .files__rename-input:focus,
404
- .files__paste-rename-input:focus {
407
+ .files__paste-rename-input:focus,
408
+ .files__upload-conflict-rename-input:focus {
405
409
  border-color: var(--shadow-claw-accent-primary);
406
410
  box-shadow: 0 0 0 0.125rem var(--shadow-claw-bg-tertiary);
407
411
  outline: none;
@@ -469,7 +473,8 @@
469
473
  opacity: 0.5;
470
474
  }
471
475
 
472
- .files__paste-dialog {
476
+ .files__paste-dialog,
477
+ .files__upload-conflict-dialog {
473
478
  background-color: var(--shadow-claw-bg-primary);
474
479
  border: 0.0625rem solid var(--shadow-claw-border-color);
475
480
  border-radius: var(--shadow-claw-radius-l, 1.5rem);
@@ -479,31 +484,36 @@
479
484
  width: calc(100vw - 2rem);
480
485
  }
481
486
 
482
- .files__paste-dialog::backdrop {
487
+ .files__paste-dialog::backdrop,
488
+ .files__upload-conflict-dialog::backdrop {
483
489
  background-color: rgba(0, 0, 0, 0.35);
484
490
  }
485
491
 
486
- .files__paste-form {
492
+ .files__paste-form,
493
+ .files__upload-conflict-form {
487
494
  display: flex;
488
495
  flex-direction: column;
489
496
  gap: 1rem;
490
497
  padding: 1.5rem;
491
498
  }
492
499
 
493
- .files__paste-title {
500
+ .files__paste-title,
501
+ .files__upload-conflict-title {
494
502
  font-size: 1.1rem;
495
503
  font-weight: 700;
496
504
  margin: 0;
497
505
  }
498
506
 
499
- .files__paste-message {
507
+ .files__paste-message,
508
+ .files__upload-conflict-message {
500
509
  color: var(--shadow-claw-text-secondary);
501
510
  font-size: var(--shadow-claw-font-size-sm);
502
511
  line-height: 1.4;
503
512
  margin: 0;
504
513
  }
505
514
 
506
- .files__paste-actions {
515
+ .files__paste-actions,
516
+ .files__upload-conflict-actions {
507
517
  display: flex;
508
518
  gap: 0.5rem;
509
519
  justify-content: flex-end;
@@ -514,7 +524,10 @@
514
524
  .files__paste-continue,
515
525
  .files__paste-apply,
516
526
  .files__paste-ok,
517
- .files__paste-overwrite {
527
+ .files__paste-overwrite,
528
+ .files__upload-conflict-cancel,
529
+ .files__upload-conflict-ok,
530
+ .files__upload-conflict-overwrite {
518
531
  background-color: transparent;
519
532
  border: 0.0625rem solid var(--shadow-claw-border-color);
520
533
  border-radius: var(--shadow-claw-radius-pill);
@@ -528,13 +541,16 @@
528
541
  transition: all 0.15s ease;
529
542
  }
530
543
 
531
- .files__paste-overwrite {
544
+ .files__paste-overwrite,
545
+ .files__upload-conflict-overwrite {
532
546
  border-color: var(--shadow-claw-error-color);
533
547
  color: var(--shadow-claw-error-color);
534
548
  }
535
549
 
536
550
  .files__paste-overwrite:hover,
537
- .files__paste-overwrite:focus-visible {
551
+ .files__paste-overwrite:focus-visible,
552
+ .files__upload-conflict-overwrite:hover,
553
+ .files__upload-conflict-overwrite:focus-visible {
538
554
  background-color: var(--shadow-claw-error-color);
539
555
  color: var(--shadow-claw-on-error, white);
540
556
  outline: none;
@@ -542,7 +558,8 @@
542
558
 
543
559
  .files__paste-continue,
544
560
  .files__paste-apply,
545
- .files__paste-ok {
561
+ .files__paste-ok,
562
+ .files__upload-conflict-ok {
546
563
  background-color: var(--shadow-claw-text-primary);
547
564
  border-color: var(--shadow-claw-text-primary);
548
565
  color: var(--shadow-claw-bg-primary);
@@ -553,7 +570,9 @@
553
570
  .files__paste-apply:hover,
554
571
  .files__paste-apply:focus-visible,
555
572
  .files__paste-ok:hover,
556
- .files__paste-ok:focus-visible {
573
+ .files__paste-ok:focus-visible,
574
+ .files__upload-conflict-ok:hover,
575
+ .files__upload-conflict-ok:focus-visible {
557
576
  background-color: var(--shadow-claw-accent-primary);
558
577
  border-color: var(--shadow-claw-accent-primary);
559
578
  color: var(--shadow-claw-on-primary);
@@ -561,7 +580,9 @@
561
580
  }
562
581
 
563
582
  .files__paste-cancel:hover,
564
- .files__paste-cancel:focus-visible {
583
+ .files__paste-cancel:focus-visible,
584
+ .files__upload-conflict-cancel:hover,
585
+ .files__upload-conflict-cancel:focus-visible {
565
586
  border-color: var(--shadow-claw-text-primary);
566
587
  box-shadow: var(--shadow-claw-shadow-md);
567
588
  outline: none;
@@ -570,7 +591,10 @@
570
591
  .files__paste-cancel:disabled,
571
592
  .files__paste-continue:disabled,
572
593
  .files__paste-apply:disabled,
573
- .files__paste-ok:disabled {
594
+ .files__paste-ok:disabled,
595
+ .files__upload-conflict-cancel:disabled,
596
+ .files__upload-conflict-ok:disabled,
597
+ .files__upload-conflict-overwrite:disabled {
574
598
  cursor: not-allowed;
575
599
  opacity: 0.6;
576
600
  }
@@ -172,6 +172,41 @@
172
172
  </div>
173
173
  </form>
174
174
  </shadow-claw-dialog>
175
+ <shadow-claw-dialog
176
+ dialog-class="files__upload-conflict-dialog"
177
+ dialog-aria-label="File upload conflict"
178
+ >
179
+ <form class="files__upload-conflict-form" method="dialog">
180
+ <h3 class="files__upload-conflict-title">File Already Exists</h3>
181
+ <p class="files__upload-conflict-message"></p>
182
+ <div class="files__upload-conflict-rename-container">
183
+ <label
184
+ class="files__upload-conflict-rename-label"
185
+ for="files-upload-conflict-name"
186
+ >New Name</label
187
+ >
188
+ <input
189
+ class="files__upload-conflict-rename-input"
190
+ id="files-upload-conflict-name"
191
+ name="files-upload-conflict-name"
192
+ type="text"
193
+ autocomplete="off"
194
+ required
195
+ />
196
+ </div>
197
+ <div class="files__upload-conflict-actions">
198
+ <button class="files__upload-conflict-cancel" type="button">
199
+ Cancel
200
+ </button>
201
+ <button class="files__upload-conflict-overwrite" type="button">
202
+ Overwrite
203
+ </button>
204
+ <button class="files__upload-conflict-ok" type="submit">
205
+ Rename
206
+ </button>
207
+ </div>
208
+ </form>
209
+ </shadow-claw-dialog>
175
210
  <div class="files__content">
176
211
  <div class="files__terminal-slot" data-terminal-slot hidden></div>
177
212
  <p class="files__drop-hint">Drop files here to upload</p>
@@ -41,6 +41,14 @@ jest.unstable_mockModule("../../storage/uploadGroupFile.js", () => ({
41
41
  uploadGroupFile: jest.fn(),
42
42
  }));
43
43
 
44
+ jest.unstable_mockModule("../../db/getConfig.js", () => ({
45
+ getConfig: jest.fn<any>().mockResolvedValue(undefined),
46
+ }));
47
+
48
+ jest.unstable_mockModule("../../utils/ulid.js", () => ({
49
+ ulid: jest.fn(() => "mock-ulid"),
50
+ }));
51
+
44
52
  jest.unstable_mockModule("../../storage/writeGroupFile.js", () => ({
45
53
  writeGroupFile: jest.fn(),
46
54
  }));
@@ -104,6 +112,9 @@ jest.unstable_mockModule("../../db/db.js", () => ({
104
112
  getDb: jest.fn<any>().mockResolvedValue({} as any),
105
113
  }));
106
114
 
115
+ const { CONFIG_KEYS } = await import("../../config/config.js");
116
+ const { getConfig } = await import("../../db/getConfig.js");
117
+ const { uploadGroupFile } = await import("../../storage/uploadGroupFile.js");
107
118
  const { ShadowClawFiles } = await import("./shadow-claw-files.js");
108
119
  const { deleteGroupFile } = await import("../../storage/deleteGroupFile.js");
109
120
  const { renameGroupEntry } = await import("../../storage/renameGroupEntry.js");
@@ -145,6 +156,8 @@ describe("shadow-claw-files", () => {
145
156
 
146
157
  expect(template).toContain("files__rename-dialog");
147
158
 
159
+ expect(template).toContain("files__upload-conflict-dialog");
160
+
148
161
  expect(template).toContain("files__drop-hint");
149
162
 
150
163
  expect(template).toContain("files__upload-progress");
@@ -910,4 +923,210 @@ describe("shadow-claw-files", () => {
910
923
 
911
924
  document.body.removeChild(component);
912
925
  });
926
+
927
+ it("uploads files without ULID prefix by default", async () => {
928
+ const component = new ShadowClawFiles();
929
+ document.body.appendChild(component);
930
+ await component.render();
931
+
932
+ (getConfig as any).mockResolvedValue(undefined);
933
+ (orchestratorStore as any).currentPath = ".";
934
+ (orchestratorStore as any).files = [];
935
+
936
+ const file = new File(["content"], "document.pdf", {
937
+ type: "application/pdf",
938
+ });
939
+ await component.uploadFileList({} as any, [file]);
940
+
941
+ expect(uploadGroupFile).toHaveBeenCalledWith(
942
+ {} as any,
943
+ "default",
944
+ "document.pdf",
945
+ file,
946
+ );
947
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
948
+
949
+ document.body.removeChild(component);
950
+ });
951
+
952
+ it("uploads files with ULID prefix when setting is enabled", async () => {
953
+ const component = new ShadowClawFiles();
954
+ document.body.appendChild(component);
955
+ await component.render();
956
+
957
+ (getConfig as any).mockImplementation((_db: any, key: string) => {
958
+ if (key === CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID) {
959
+ return Promise.resolve("true");
960
+ }
961
+ return Promise.resolve(undefined);
962
+ });
963
+ (orchestratorStore as any).currentPath = "subfolder";
964
+ (orchestratorStore as any).files = [];
965
+
966
+ const file = new File(["content"], "image.png", {
967
+ type: "image/png",
968
+ });
969
+ await component.uploadFileList({} as any, [file]);
970
+
971
+ expect(uploadGroupFile).toHaveBeenCalledWith(
972
+ {} as any,
973
+ "default",
974
+ "subfolder/mock-ulid-image.png",
975
+ file,
976
+ );
977
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
978
+
979
+ document.body.removeChild(component);
980
+ });
981
+
982
+ it("prompts conflict dialog and overwrites existing file when selected", async () => {
983
+ const component = new ShadowClawFiles();
984
+ document.body.appendChild(component);
985
+ await component.render();
986
+
987
+ (getConfig as any).mockResolvedValue(undefined);
988
+ (orchestratorStore as any).currentPath = ".";
989
+ (orchestratorStore as any).files = ["existing.txt"];
990
+ (orchestratorStore as any).pages = [
991
+ { groupId: "default", path: "existing.txt" },
992
+ ];
993
+
994
+ jest.spyOn(component, "promptUploadConflict").mockResolvedValue({
995
+ action: "overwrite",
996
+ });
997
+
998
+ const file = new File(["new content"], "existing.txt", {
999
+ type: "text/plain",
1000
+ });
1001
+ await component.uploadFileList({} as any, [file]);
1002
+
1003
+ expect(component.promptUploadConflict).toHaveBeenCalledWith(
1004
+ "existing.txt",
1005
+ expect.any(Set),
1006
+ );
1007
+ expect(mockRemovePage).toHaveBeenCalledWith(
1008
+ {} as any,
1009
+ "existing.txt",
1010
+ "default",
1011
+ );
1012
+ expect(uploadGroupFile).toHaveBeenCalledWith(
1013
+ {} as any,
1014
+ "default",
1015
+ "existing.txt",
1016
+ file,
1017
+ );
1018
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
1019
+
1020
+ document.body.removeChild(component);
1021
+ });
1022
+
1023
+ it("prompts conflict dialog and renames file when new name is chosen", async () => {
1024
+ const component = new ShadowClawFiles();
1025
+ document.body.appendChild(component);
1026
+ await component.render();
1027
+
1028
+ (getConfig as any).mockResolvedValue(undefined);
1029
+ (orchestratorStore as any).currentPath = "docs";
1030
+ (orchestratorStore as any).files = ["existing.txt"];
1031
+
1032
+ jest.spyOn(component, "promptUploadConflict").mockResolvedValue({
1033
+ action: "rename",
1034
+ name: "existing-renamed.txt",
1035
+ });
1036
+
1037
+ const file = new File(["content"], "existing.txt", {
1038
+ type: "text/plain",
1039
+ });
1040
+ await component.uploadFileList({} as any, [file]);
1041
+
1042
+ expect(component.promptUploadConflict).toHaveBeenCalledWith(
1043
+ "existing.txt",
1044
+ expect.any(Set),
1045
+ );
1046
+ expect(uploadGroupFile).toHaveBeenCalledWith(
1047
+ {} as any,
1048
+ "default",
1049
+ "docs/existing-renamed.txt",
1050
+ file,
1051
+ );
1052
+ expect(showSuccess).toHaveBeenCalledWith("Uploaded 1 file", 3000);
1053
+
1054
+ document.body.removeChild(component);
1055
+ });
1056
+
1057
+ it("prompts conflict dialog and skips upload when user cancels", async () => {
1058
+ const component = new ShadowClawFiles();
1059
+ document.body.appendChild(component);
1060
+ await component.render();
1061
+
1062
+ (getConfig as any).mockResolvedValue(undefined);
1063
+ (orchestratorStore as any).currentPath = ".";
1064
+ (orchestratorStore as any).files = ["existing.txt"];
1065
+
1066
+ jest.spyOn(component, "promptUploadConflict").mockResolvedValue(null);
1067
+
1068
+ const file = new File(["content"], "existing.txt", {
1069
+ type: "text/plain",
1070
+ });
1071
+ await component.uploadFileList({} as any, [file]);
1072
+
1073
+ expect(component.promptUploadConflict).toHaveBeenCalledWith(
1074
+ "existing.txt",
1075
+ expect.any(Set),
1076
+ );
1077
+ expect(uploadGroupFile).not.toHaveBeenCalled();
1078
+
1079
+ document.body.removeChild(component);
1080
+ });
1081
+
1082
+ it("handles conflict dialog UI submission and validation", async () => {
1083
+ const component = new ShadowClawFiles();
1084
+ document.body.appendChild(component);
1085
+ await component.render();
1086
+
1087
+ const existingNames = new Set(["existing.txt", "taken.txt"]);
1088
+ const conflictPromise = component.promptUploadConflict(
1089
+ "existing.txt",
1090
+ existingNames,
1091
+ );
1092
+
1093
+ const input = component.shadowRoot?.querySelector(
1094
+ ".files__upload-conflict-rename-input",
1095
+ ) as HTMLInputElement;
1096
+ const form = component.shadowRoot?.querySelector(
1097
+ ".files__upload-conflict-form",
1098
+ ) as HTMLFormElement;
1099
+
1100
+ expect(input.value).toBe("existing.txt");
1101
+
1102
+ // 1. Try empty name
1103
+ input.value = " ";
1104
+ form.dispatchEvent(new Event("submit"));
1105
+ expect(showWarning).toHaveBeenCalledWith("Please enter a name", 3000);
1106
+
1107
+ // 2. Try path name
1108
+ input.value = "folder/name.txt";
1109
+ form.dispatchEvent(new Event("submit"));
1110
+ expect(showWarning).toHaveBeenCalledWith(
1111
+ "Use only a name, not a path",
1112
+ 3500,
1113
+ );
1114
+
1115
+ // 3. Try already taken name
1116
+ input.value = "taken.txt";
1117
+ form.dispatchEvent(new Event("submit"));
1118
+ expect(showWarning).toHaveBeenCalledWith(
1119
+ '"taken.txt" already exists. Please choose a different name.',
1120
+ 4000,
1121
+ );
1122
+
1123
+ // 4. Valid new name
1124
+ input.value = "brand-new.txt";
1125
+ form.dispatchEvent(new Event("submit"));
1126
+
1127
+ const result = await conflictPromise;
1128
+ expect(result).toEqual({ action: "rename", name: "brand-new.txt" });
1129
+
1130
+ document.body.removeChild(component);
1131
+ });
913
1132
  });