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.
- package/bin/assert-version-bump.mjs +60 -38
- package/bin/assert-version-bump.test.mjs +74 -11
- package/dist/public/agent.worker.js +6 -6
- package/dist/public/{app-routes-B0ePT49v.js → app-routes-Bs-riIvr.js} +1 -1
- package/dist/public/components/settings/shadow-claw-storage/shadow-claw-storage.html +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.css +1 -1
- package/dist/public/components/shadow-claw-files/shadow-claw-files.html +1 -1
- package/dist/public/config-BYAuxx3j.js +1 -0
- package/dist/public/{configurePeerJs-B7kNU8AQ.js → configurePeerJs-B4Ym9VMj.js} +1 -1
- package/dist/public/{connections-xd_66YVU.js → connections-BEn6JYdu.js} +1 -1
- package/dist/public/{custom-element-security-CZ92anB8.js → custom-element-security-DdYlUozq.js} +1 -1
- package/dist/public/docs/architecture/storage.md +5 -2
- package/dist/public/docs/decisions/indexeddb-and-opfs-storage.md +5 -2
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/publishing/index.html +19 -76
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/{downloadGroupFile-BABv_1mJ.js → downloadGroupFile-Drdvg1Ev.js} +1 -1
- package/dist/public/{e2e-bridge-DkS5xFGX.js → e2e-bridge-BQ4KzUt5.js} +1 -1
- package/dist/public/{file-viewer-CCSABMsE.js → file-viewer-CcZYw-PS.js} +1 -1
- package/dist/public/files/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/{git-BkjrsRXX.js → git-DEUvQTUA.js} +2 -2
- package/dist/public/{iframe-sanitizer-L6Jyu__Z.js → iframe-sanitizer-0NS4XLOd.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/{initChatSplitResize-BslPXAXS.js → initChatSplitResize-llGkSJXg.js} +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{markdown-6MBrIo-S.js → markdown-CyYR4App.js} +1 -1
- package/dist/public/mcp-reconnect-CSx4aMwy.js +1 -0
- package/dist/public/{memoryStorage-CjGseKI1.js → memoryStorage-Dwg8NhAZ.js} +1 -1
- package/dist/public/{orchestrator-B9KmIj8Q.js → orchestrator-DRfEudXk.js} +18 -18
- package/dist/public/orchestrator-o9Fj67BN.js +15 -0
- package/dist/public/pages/main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/{peerjs-Dv8JNm7v.js → peerjs-CurBuKvk.js} +1 -1
- package/dist/public/{push-client-Dqq06wbB.js → push-client-Dii-vPEh.js} +1 -1
- package/dist/public/service-worker/fetch-proxy.js +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-Bd1rfuVC.js → shadow-claw-5IHaDDMJ.js} +2 -2
- package/dist/public/{shadow-claw-accounts-DL2l3oM0.js → shadow-claw-accounts-CZvz4iQz.js} +3 -3
- package/dist/public/{shadow-claw-channels-BGXDsQ38.js → shadow-claw-channels-BKXFyRS3.js} +1 -1
- package/dist/public/{shadow-claw-chat-NXg6zz_l.js → shadow-claw-chat-CCLuhWdI.js} +3 -3
- package/dist/public/{shadow-claw-conversations-C8eqGpP9.js → shadow-claw-conversations-DV-87UUh.js} +1 -1
- package/dist/public/shadow-claw-file-viewer-Dk2dbAUd.js +577 -0
- package/dist/public/shadow-claw-files-BpfpJIuP.js +840 -0
- package/dist/public/{shadow-claw-git-C0ntz7aX.js → shadow-claw-git-DRA7_qck.js} +3 -3
- package/dist/public/{shadow-claw-integrations-DCXV2r69.js → shadow-claw-integrations-1Xu5snuV.js} +1 -1
- package/dist/public/{shadow-claw-llm-CudgFQsS.js → shadow-claw-llm-6Aci1BDP.js} +2 -2
- package/dist/public/{shadow-claw-mcp-remote-B8RkpX28.js → shadow-claw-mcp-remote-BUjAeVXC.js} +1 -1
- package/dist/public/{shadow-claw-networking-BLiRsrsK.js → shadow-claw-networking-FQe4nCp3.js} +1 -1
- package/dist/public/{shadow-claw-notifications-CQhQcjtL.js → shadow-claw-notifications-DV2wibL3.js} +1 -1
- package/dist/public/{shadow-claw-page-header-NeRD0tKU.js → shadow-claw-page-header-B63S4HTU.js} +1 -1
- package/dist/public/{shadow-claw-pages-DCHnFh6r.js → shadow-claw-pages-DE3jWj5K.js} +3 -3
- package/dist/public/{shadow-claw-settings-VAfjKFQg.js → shadow-claw-settings-S6DAKfYx.js} +3 -3
- package/dist/public/shadow-claw-storage-DaacA3_K.js +219 -0
- package/dist/public/{shadow-claw-task-server-Dk33N_1G.js → shadow-claw-task-server-IjKX042p.js} +1 -1
- package/dist/public/{shadow-claw-tasks-xA1VfmWN.js → shadow-claw-tasks-Cy0JYmGU.js} +4 -4
- package/dist/public/{shadow-claw-terminal-Du2r0ciq.js → shadow-claw-terminal-znAt7HyP.js} +1 -1
- package/dist/public/{shadow-claw-tools-CZj4rq7l.js → shadow-claw-tools-Cutv9Si5.js} +2 -2
- package/dist/public/{shadow-claw-webvm-JYUkWICG.js → shadow-claw-webvm-BH8mPDEu.js} +1 -1
- package/dist/public/static-main/~/docs/publishing-to-github-pages.md +17 -82
- package/dist/public/static-main-manifest.json +1 -1
- package/dist/public/{syncWebMcpRegistration-DfpvASnh.js → syncWebMcpRegistration-BqJxaMGR.js} +1 -1
- package/package.json +1 -1
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.html +17 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.test.ts +44 -0
- package/src/components/settings/shadow-claw-storage/shadow-claw-storage.ts +56 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.test.ts +105 -0
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +11 -9
- package/src/components/shadow-claw-files/shadow-claw-files.css +41 -17
- package/src/components/shadow-claw-files/shadow-claw-files.html +35 -0
- package/src/components/shadow-claw-files/shadow-claw-files.test.ts +219 -0
- package/src/components/shadow-claw-files/shadow-claw-files.ts +170 -6
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.test.ts +31 -0
- package/src/components/shadow-claw-pages/utils/resolveWorkspaceLinkPath.ts +2 -1
- package/src/components/shadow-claw-pages/utils/rewriteWorkspacePreviewHtml.test.ts +15 -0
- package/src/config/config.test.ts +9 -0
- package/src/config/config.ts +5 -0
- package/src/core/app-routes.test.ts +56 -0
- package/src/core/app-routes.ts +8 -1
- package/src/db/openDatabase.test.ts +10 -1
- package/src/db/openDatabase.ts +3 -1
- package/src/storage/migrateLegacyOpfs.test.ts +327 -0
- package/src/storage/migrateLegacyOpfs.ts +117 -0
- package/dist/public/config-AL77QAx4.js +0 -1
- package/dist/public/mcp-reconnect-DoicazQd.js +0 -1
- package/dist/public/orchestrator-4su7iyWj.js +0 -15
- package/dist/public/shadow-claw-file-viewer-9Qkwc30T.js +0 -577
- package/dist/public/shadow-claw-files-aDyq6Hv7.js +0 -781
- package/dist/public/shadow-claw-storage-Cf31np81.js +0 -202
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { CONFIG_KEYS } from "../../config/config.js";
|
|
1
2
|
import { effect } from "../../core/effect.js";
|
|
2
3
|
import { getDb } from "../../db/db.js";
|
|
4
|
+
import { getConfig } from "../../db/getConfig.js";
|
|
5
|
+
import { isTruthyConfigValue } from "../../common/utils/config-value.mjs";
|
|
3
6
|
|
|
4
7
|
import { setSanitizedHtml } from "../../security/trusted-types.js";
|
|
5
8
|
import { copyGroupEntry } from "../../storage/copyGroupEntry.js";
|
|
@@ -1575,34 +1578,195 @@ export class ShadowClawFiles extends ShadowClawElement {
|
|
|
1575
1578
|
return false;
|
|
1576
1579
|
}
|
|
1577
1580
|
|
|
1581
|
+
async promptUploadConflict(
|
|
1582
|
+
fileName: string,
|
|
1583
|
+
existingNames: Set<string>,
|
|
1584
|
+
): Promise<
|
|
1585
|
+
{ action: "overwrite" } | { action: "rename"; name: string } | null
|
|
1586
|
+
> {
|
|
1587
|
+
const root = this.shadowRoot;
|
|
1588
|
+
if (!root) {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const dialog = root.querySelector(".files__upload-conflict-dialog");
|
|
1593
|
+
const messageEl = root.querySelector(".files__upload-conflict-message");
|
|
1594
|
+
const renameInput = root.querySelector(
|
|
1595
|
+
".files__upload-conflict-rename-input",
|
|
1596
|
+
);
|
|
1597
|
+
const form = root.querySelector(".files__upload-conflict-form");
|
|
1598
|
+
const cancelBtn = root.querySelector(".files__upload-conflict-cancel");
|
|
1599
|
+
const overwriteBtn = root.querySelector(
|
|
1600
|
+
".files__upload-conflict-overwrite",
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
if (
|
|
1604
|
+
!(dialog instanceof HTMLDialogElement) ||
|
|
1605
|
+
!(form instanceof HTMLFormElement) ||
|
|
1606
|
+
!(renameInput instanceof HTMLInputElement)
|
|
1607
|
+
) {
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const targetDirName =
|
|
1612
|
+
orchestratorStore.currentPath === "."
|
|
1613
|
+
? "Root"
|
|
1614
|
+
: orchestratorStore.currentPath;
|
|
1615
|
+
|
|
1616
|
+
if (messageEl instanceof HTMLElement) {
|
|
1617
|
+
messageEl.textContent = `"${fileName}" already exists in "${targetDirName}". Please rename or choose to overwrite.`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
renameInput.value = fileName;
|
|
1621
|
+
|
|
1622
|
+
return new Promise((resolve) => {
|
|
1623
|
+
let resolved = false;
|
|
1624
|
+
|
|
1625
|
+
const cleanup = () => {
|
|
1626
|
+
form.removeEventListener("submit", handleSubmit);
|
|
1627
|
+
cancelBtn?.removeEventListener("click", handleCancel);
|
|
1628
|
+
overwriteBtn?.removeEventListener("click", handleOverwrite);
|
|
1629
|
+
dialog.removeEventListener("cancel", handleCancel);
|
|
1630
|
+
dialog.removeEventListener("close", handleClose);
|
|
1631
|
+
};
|
|
1632
|
+
|
|
1633
|
+
const finish = (
|
|
1634
|
+
result:
|
|
1635
|
+
| { action: "overwrite" }
|
|
1636
|
+
| { action: "rename"; name: string }
|
|
1637
|
+
| null,
|
|
1638
|
+
) => {
|
|
1639
|
+
if (resolved) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
resolved = true;
|
|
1643
|
+
cleanup();
|
|
1644
|
+
if (typeof dialog.close === "function") {
|
|
1645
|
+
dialog.close();
|
|
1646
|
+
} else {
|
|
1647
|
+
dialog.removeAttribute("open");
|
|
1648
|
+
}
|
|
1649
|
+
resolve(result);
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
const handleSubmit = (event: Event) => {
|
|
1653
|
+
event.preventDefault();
|
|
1654
|
+
const nextName = renameInput.value.trim();
|
|
1655
|
+
if (!nextName) {
|
|
1656
|
+
showWarning("Please enter a name", 3000);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (nextName.includes("/") || nextName.includes("\\")) {
|
|
1661
|
+
showWarning("Use only a name, not a path", 3500);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
if (existingNames.has(nextName) || existingNames.has(`${nextName}/`)) {
|
|
1666
|
+
showWarning(
|
|
1667
|
+
`"${nextName}" already exists. Please choose a different name.`,
|
|
1668
|
+
4000,
|
|
1669
|
+
);
|
|
1670
|
+
renameInput.select();
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
finish({ action: "rename", name: nextName });
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
const handleOverwrite = (event: Event) => {
|
|
1678
|
+
event.preventDefault();
|
|
1679
|
+
finish({ action: "overwrite" });
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
const handleCancel = (event: Event) => {
|
|
1683
|
+
event.preventDefault();
|
|
1684
|
+
finish(null);
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
const handleClose = () => {
|
|
1688
|
+
finish(null);
|
|
1689
|
+
};
|
|
1690
|
+
|
|
1691
|
+
form.addEventListener("submit", handleSubmit);
|
|
1692
|
+
cancelBtn?.addEventListener("click", handleCancel);
|
|
1693
|
+
overwriteBtn?.addEventListener("click", handleOverwrite);
|
|
1694
|
+
dialog.addEventListener("cancel", handleCancel);
|
|
1695
|
+
dialog.addEventListener("close", handleClose);
|
|
1696
|
+
|
|
1697
|
+
if (typeof dialog.showModal === "function") {
|
|
1698
|
+
dialog.showModal();
|
|
1699
|
+
} else {
|
|
1700
|
+
dialog.setAttribute("open", "");
|
|
1701
|
+
}
|
|
1702
|
+
renameInput.select();
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1578
1706
|
async uploadFileList(db: ShadowClawDatabase, files: FileList | File[]) {
|
|
1579
1707
|
const fileList: File[] = Array.from(files) as File[];
|
|
1580
1708
|
if (fileList.length === 0) {
|
|
1581
1709
|
return;
|
|
1582
1710
|
}
|
|
1583
1711
|
|
|
1712
|
+
const appendUlid = await getConfig(db, CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID)
|
|
1713
|
+
.then((val) => isTruthyConfigValue(val, false))
|
|
1714
|
+
.catch(() => false);
|
|
1715
|
+
|
|
1584
1716
|
const groupId = orchestratorStore.activeGroupId;
|
|
1585
1717
|
const currentPath = orchestratorStore.currentPath;
|
|
1586
1718
|
const count = fileList.length;
|
|
1587
1719
|
|
|
1588
1720
|
filesUiStore.startUpload(count);
|
|
1589
1721
|
|
|
1722
|
+
const existingNames = new Set(orchestratorStore.files);
|
|
1723
|
+
let uploadedCount = 0;
|
|
1724
|
+
|
|
1590
1725
|
try {
|
|
1591
1726
|
for (let i = 0; i < count; i++) {
|
|
1592
1727
|
const file = fileList[i];
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1728
|
+
let filename = appendUlid ? `${ulid()}-${file.name}` : file.name;
|
|
1729
|
+
|
|
1730
|
+
const hasConflict =
|
|
1731
|
+
existingNames.has(filename) || existingNames.has(`${filename}/`);
|
|
1732
|
+
|
|
1733
|
+
if (hasConflict) {
|
|
1734
|
+
const decision = await this.promptUploadConflict(
|
|
1735
|
+
filename,
|
|
1736
|
+
existingNames,
|
|
1737
|
+
);
|
|
1738
|
+
if (!decision) {
|
|
1739
|
+
filesUiStore.setUploadCompleted(i + 1);
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1597
1742
|
|
|
1598
|
-
|
|
1743
|
+
if (decision.action === "rename") {
|
|
1744
|
+
filename = decision.name;
|
|
1745
|
+
} else if (decision.action === "overwrite") {
|
|
1746
|
+
const targetPath =
|
|
1747
|
+
currentPath === "." ? filename : `${currentPath}/${filename}`;
|
|
1748
|
+
await this.removePageAssignmentIfNeeded(db, targetPath);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
const fullPath =
|
|
1753
|
+
currentPath === "." ? filename : `${currentPath}/${filename}`;
|
|
1754
|
+
|
|
1755
|
+
await uploadGroupFile(db, groupId, fullPath, file);
|
|
1756
|
+
existingNames.add(filename);
|
|
1757
|
+
uploadedCount++;
|
|
1599
1758
|
filesUiStore.setUploadCompleted(i + 1);
|
|
1600
1759
|
}
|
|
1601
1760
|
|
|
1602
1761
|
// Reload files
|
|
1603
1762
|
await orchestratorStore.loadFiles(db);
|
|
1604
1763
|
|
|
1605
|
-
|
|
1764
|
+
if (uploadedCount > 0) {
|
|
1765
|
+
showSuccess(
|
|
1766
|
+
`Uploaded ${uploadedCount} file${uploadedCount === 1 ? "" : "s"}`,
|
|
1767
|
+
3000,
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1606
1770
|
} catch (err) {
|
|
1607
1771
|
const message = err instanceof Error ? err.message : String(err);
|
|
1608
1772
|
|
|
@@ -36,6 +36,37 @@ describe("resolveWorkspaceLinkPath", () => {
|
|
|
36
36
|
expect(result).toBe("docs/guide.md");
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
it("resolves relative image link in file directory", () => {
|
|
40
|
+
const result = resolveWorkspaceLinkPath(
|
|
41
|
+
"01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
42
|
+
"index.md",
|
|
43
|
+
groupId,
|
|
44
|
+
origin,
|
|
45
|
+
);
|
|
46
|
+
expect(result).toBe("01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("resolves relative links when subpath base tag is active", () => {
|
|
50
|
+
document.querySelectorAll("base").forEach((el) => el.remove());
|
|
51
|
+
const baseEl = document.createElement("base");
|
|
52
|
+
baseEl.setAttribute("href", "/shadow-claw/");
|
|
53
|
+
document.head.appendChild(baseEl);
|
|
54
|
+
(globalThis as any).__applyBasePathCacheReset?.();
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const result = resolveWorkspaceLinkPath(
|
|
58
|
+
"01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
59
|
+
"index.md",
|
|
60
|
+
groupId,
|
|
61
|
+
origin,
|
|
62
|
+
);
|
|
63
|
+
expect(result).toBe("01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg");
|
|
64
|
+
} finally {
|
|
65
|
+
baseEl.remove();
|
|
66
|
+
(globalThis as any).__applyBasePathCacheReset?.();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
39
70
|
it("returns null if link points to external origin", () => {
|
|
40
71
|
const result = resolveWorkspaceLinkPath(
|
|
41
72
|
"https://example.com/page",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
applyBasePath,
|
|
2
3
|
getFileRouteDirPath,
|
|
3
4
|
getWorkspaceRouteRequestPath,
|
|
4
5
|
resolveHrefAgainstRoute,
|
|
@@ -43,7 +44,7 @@ export function resolveWorkspaceLinkPath(
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
const routeDir = getFileRouteDirPath(groupId, filePath);
|
|
47
|
+
const routeDir = applyBasePath(getFileRouteDirPath(groupId, filePath));
|
|
47
48
|
const resolved = resolveHrefAgainstRoute(
|
|
48
49
|
trimmed,
|
|
49
50
|
routeDir,
|
|
@@ -42,4 +42,19 @@ describe("rewriteWorkspacePreviewHtml", () => {
|
|
|
42
42
|
);
|
|
43
43
|
expect(rewritten).toContain('href="https://example.com/docs"');
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
it("rewrites relative markdown links to subpath-prefixed workspace routes", () => {
|
|
47
|
+
const html =
|
|
48
|
+
'<p><a href="01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg">01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg</a></p>';
|
|
49
|
+
const rewritten = rewriteWorkspacePreviewHtml(
|
|
50
|
+
html,
|
|
51
|
+
"index.md",
|
|
52
|
+
"/shadow-claw/files/br:main/",
|
|
53
|
+
groupId,
|
|
54
|
+
origin,
|
|
55
|
+
);
|
|
56
|
+
expect(rewritten).toContain(
|
|
57
|
+
'href="/shadow-claw/files/br:main/01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg"',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
45
60
|
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DEFAULT_DEV_HOST,
|
|
13
13
|
DEFAULT_DEV_IP,
|
|
14
14
|
DEFAULT_DEV_PORT,
|
|
15
|
+
DEFAULT_FILES_UPLOAD_APPEND_ULID,
|
|
15
16
|
DEFAULT_GROUP_ID,
|
|
16
17
|
DEFAULT_MAX_TOKENS,
|
|
17
18
|
DEFAULT_PROMPT_API_BACKEND,
|
|
@@ -149,6 +150,10 @@ describe("config.js", () => {
|
|
|
149
150
|
expect(DEFAULT_TRANSFORMERS_JS_BACKEND).toBe("auto");
|
|
150
151
|
expect(DEFAULT_TRANSFORMERS_JS_DTYPE_STRATEGY).toBe("auto");
|
|
151
152
|
});
|
|
153
|
+
|
|
154
|
+
it("should have valid DEFAULT_FILES_UPLOAD_APPEND_ULID", () => {
|
|
155
|
+
expect(DEFAULT_FILES_UPLOAD_APPEND_ULID).toBe(false);
|
|
156
|
+
});
|
|
152
157
|
});
|
|
153
158
|
|
|
154
159
|
describe("buildTriggerPattern", () => {
|
|
@@ -425,6 +430,10 @@ describe("config.js", () => {
|
|
|
425
430
|
);
|
|
426
431
|
expect(CONFIG_KEYS.CHANNEL_ENABLED_PREFIX).toBe("channel_enabled:");
|
|
427
432
|
expect(CONFIG_KEYS.CHAT_INPUT_AREA_HEIGHT).toBe("chat_input_area_height");
|
|
433
|
+
expect(CONFIG_KEYS.ENABLED_TOOLS).toBe("enabled_tools");
|
|
434
|
+
expect(CONFIG_KEYS.FILES_UPLOAD_APPEND_ULID).toBe(
|
|
435
|
+
"files_upload_append_ulid",
|
|
436
|
+
);
|
|
428
437
|
expect(CONFIG_KEYS.GIT_AUTHOR_EMAIL).toBe("git_author_email");
|
|
429
438
|
expect(CONFIG_KEYS.GIT_AUTHOR_NAME).toBe("git_author_name");
|
|
430
439
|
expect(CONFIG_KEYS.GIT_CORS_PROXY).toBe("git_cors_proxy");
|
package/src/config/config.ts
CHANGED
|
@@ -1042,6 +1042,7 @@ export const CONFIG_KEYS = {
|
|
|
1042
1042
|
DEFAULT_PINNED_PAGE: "default_pinned_page",
|
|
1043
1043
|
DIRECT_TOOL_COMMAND_POLICY: "direct_tool_command_policy",
|
|
1044
1044
|
ENABLED_TOOLS: "enabled_tools",
|
|
1045
|
+
FILES_UPLOAD_APPEND_ULID: "files_upload_append_ulid",
|
|
1045
1046
|
GIT_ACCOUNTS: "git_accounts",
|
|
1046
1047
|
GIT_AUTHOR_EMAIL: "git_author_email",
|
|
1047
1048
|
GIT_AUTHOR_NAME: "git_author_name",
|
|
@@ -1073,6 +1074,7 @@ export const CONFIG_KEYS = {
|
|
|
1073
1074
|
MESH_LLM_HOST: "mesh_llm_host",
|
|
1074
1075
|
MODEL: "model",
|
|
1075
1076
|
OAUTH_PROVIDER_DEFINITIONS: "oauth_provider_definitions",
|
|
1077
|
+
OPFS_MIGRATED_FROM_LEGACY: "opfs_migrated_from_legacy",
|
|
1076
1078
|
OVERRIDE_PRERENDER_SKELETON: "override_prerender_skeleton",
|
|
1077
1079
|
PAGES_AUTO_REFRESH_INTERVAL: "pages_auto_refresh_interval",
|
|
1078
1080
|
PAGES_LIST: "pages_list",
|
|
@@ -1180,3 +1182,6 @@ export const DEFAULT_TASK_SERVER_URL = "http://localhost:8888/schedule";
|
|
|
1180
1182
|
|
|
1181
1183
|
/** Default Task Server Enabled */
|
|
1182
1184
|
export const DEFAULT_TASK_SERVER_ENABLED = false;
|
|
1185
|
+
|
|
1186
|
+
/** Default Files Upload Append ULID */
|
|
1187
|
+
export const DEFAULT_FILES_UPLOAD_APPEND_ULID = false;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getDeploymentNamespace,
|
|
6
6
|
getFileRouteDirPath,
|
|
7
7
|
getWorkspaceRouteRequestPath,
|
|
8
|
+
isPossibleAppRoute,
|
|
8
9
|
parseRouteFromUrl,
|
|
9
10
|
resolveHrefAgainstRoute,
|
|
10
11
|
} from "./app-routes.js";
|
|
@@ -148,11 +149,17 @@ describe("app-routes", () => {
|
|
|
148
149
|
describe("applyBasePath", () => {
|
|
149
150
|
// Reset cached base path between tests
|
|
150
151
|
beforeEach(() => {
|
|
152
|
+
document.querySelectorAll("base").forEach((el) => el.remove());
|
|
151
153
|
// Force-reset the module-level cache so each sub-test starts fresh.
|
|
152
154
|
// We do this by temporarily reassigning window.location via jsdom.
|
|
153
155
|
(globalThis as any).__applyBasePathCacheReset?.();
|
|
154
156
|
});
|
|
155
157
|
|
|
158
|
+
afterEach(() => {
|
|
159
|
+
document.querySelectorAll("base").forEach((el) => el.remove());
|
|
160
|
+
(globalThis as any).__applyBasePathCacheReset?.();
|
|
161
|
+
});
|
|
162
|
+
|
|
156
163
|
it("returns path unchanged when base is root", () => {
|
|
157
164
|
// In jsdom the default base path resolves to "/"
|
|
158
165
|
expect(applyBasePath("/chat")).toBe("/chat");
|
|
@@ -191,6 +198,55 @@ describe("app-routes", () => {
|
|
|
191
198
|
anchor: undefined,
|
|
192
199
|
});
|
|
193
200
|
});
|
|
201
|
+
|
|
202
|
+
it("handles explicit subpath <base href> tag correctly for all workspace routes", () => {
|
|
203
|
+
document.querySelectorAll("base").forEach((el) => el.remove());
|
|
204
|
+
const baseEl = document.createElement("base");
|
|
205
|
+
baseEl.setAttribute("href", "/shadow-claw/");
|
|
206
|
+
document.head.appendChild(baseEl);
|
|
207
|
+
(globalThis as any).__applyBasePathCacheReset?.();
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
expect(getAppBasePath()).toBe("/shadow-claw/");
|
|
211
|
+
|
|
212
|
+
// Workspace file route extraction with subpath prefix
|
|
213
|
+
expect(
|
|
214
|
+
getWorkspaceRouteRequestPath(
|
|
215
|
+
"/shadow-claw/files/main/01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
216
|
+
),
|
|
217
|
+
).toEqual({
|
|
218
|
+
groupId: "br:main",
|
|
219
|
+
path: "01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// isPossibleAppRoute validation
|
|
223
|
+
expect(
|
|
224
|
+
isPossibleAppRoute(
|
|
225
|
+
"/shadow-claw/files/main/01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
226
|
+
),
|
|
227
|
+
).toBe(true);
|
|
228
|
+
|
|
229
|
+
// Path without base prefix should NOT match app routes
|
|
230
|
+
expect(
|
|
231
|
+
isPossibleAppRoute(
|
|
232
|
+
"/files/main/01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
233
|
+
),
|
|
234
|
+
).toBe(false);
|
|
235
|
+
|
|
236
|
+
// resolveHrefAgainstRoute with subpath routeDir
|
|
237
|
+
const resolved = resolveHrefAgainstRoute(
|
|
238
|
+
"01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
239
|
+
applyBasePath(getFileRouteDirPath("br:main", "index.md")),
|
|
240
|
+
"http://localhost",
|
|
241
|
+
);
|
|
242
|
+
expect(resolved?.pathname).toBe(
|
|
243
|
+
"/shadow-claw/files/main/01M1A1R4Y708YJNPGV2NHQKYF7-IMG_0233.jpeg",
|
|
244
|
+
);
|
|
245
|
+
} finally {
|
|
246
|
+
baseEl.remove();
|
|
247
|
+
(globalThis as any).__applyBasePathCacheReset?.();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
194
250
|
});
|
|
195
251
|
|
|
196
252
|
describe("pretty path routing", () => {
|
package/src/core/app-routes.ts
CHANGED
|
@@ -551,7 +551,14 @@ export function getFileRouteDirPath(groupId: string, filePath: string): string {
|
|
|
551
551
|
export function getWorkspaceRouteRequestPath(
|
|
552
552
|
pathname: string,
|
|
553
553
|
): { groupId: string; path: string } | null {
|
|
554
|
-
|
|
554
|
+
let relativePath = pathname;
|
|
555
|
+
const basePath = getAppBasePath();
|
|
556
|
+
|
|
557
|
+
if (basePath !== "/" && relativePath.startsWith(basePath)) {
|
|
558
|
+
relativePath = "/" + relativePath.slice(basePath.length);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const parts = relativePath
|
|
555
562
|
.split("/")
|
|
556
563
|
.filter(Boolean)
|
|
557
564
|
.map((part) => decodePathSegment(part));
|
|
@@ -2,8 +2,15 @@ import { jest } from "@jest/globals";
|
|
|
2
2
|
|
|
3
3
|
const setDB = jest.fn();
|
|
4
4
|
|
|
5
|
+
const mockMigrateLegacyDatabase = jest.fn<any>().mockResolvedValue(undefined);
|
|
6
|
+
const mockMigrateLegacyOpfs = jest.fn<any>().mockResolvedValue(undefined);
|
|
7
|
+
|
|
5
8
|
jest.unstable_mockModule("./migrateLegacyDatabase.js", () => ({
|
|
6
|
-
migrateLegacyDatabase:
|
|
9
|
+
migrateLegacyDatabase: mockMigrateLegacyDatabase,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.unstable_mockModule("../storage/migrateLegacyOpfs.js", () => ({
|
|
13
|
+
migrateLegacyOpfs: mockMigrateLegacyOpfs,
|
|
7
14
|
}));
|
|
8
15
|
|
|
9
16
|
jest.unstable_mockModule("./db.js", () => ({
|
|
@@ -110,6 +117,8 @@ describe("openDatabase", () => {
|
|
|
110
117
|
);
|
|
111
118
|
|
|
112
119
|
expect(setDB).toHaveBeenCalledWith(database);
|
|
120
|
+
expect(mockMigrateLegacyDatabase).toHaveBeenCalledWith(database);
|
|
121
|
+
expect(mockMigrateLegacyOpfs).toHaveBeenCalledWith(database);
|
|
113
122
|
});
|
|
114
123
|
|
|
115
124
|
it("skips creating stores that already exist", async () => {
|
package/src/db/openDatabase.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DB_VERSION, getDbName } from "../config/config.js";
|
|
2
2
|
import { setDB } from "./db.js";
|
|
3
3
|
import { migrateLegacyDatabase } from "./migrateLegacyDatabase.js";
|
|
4
|
+
import { migrateLegacyOpfs } from "../storage/migrateLegacyOpfs.js";
|
|
4
5
|
import type { ShadowClawDatabase } from "./types.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -59,8 +60,9 @@ export function openDatabase(): Promise<ShadowClawDatabase> {
|
|
|
59
60
|
setDB(db);
|
|
60
61
|
try {
|
|
61
62
|
await migrateLegacyDatabase(db);
|
|
63
|
+
await migrateLegacyOpfs(db);
|
|
62
64
|
} catch (e) {
|
|
63
|
-
console.warn("Error running
|
|
65
|
+
console.warn("Error running legacy migration:", e);
|
|
64
66
|
}
|
|
65
67
|
resolve(db);
|
|
66
68
|
};
|