=> {
+ setState({ isLoading: true, error: null });
+ try {
+ const folders = useFolderStore.getState().folders;
+ const vault = useVaultStore.getState();
+
+ // Get parent folder state
+ const parentFolder =
+ parentId === 'root' ? getRootFolderState(vault, folders) : folders[parentId];
+
+ if (!parentFolder) {
+ throw new Error('Parent folder not found or vault not initialized');
+ }
+
+ // Replace file in folder metadata
+ const { newSequenceNumber, oldCid } = await folderService.replaceFileInFolder({
+ fileId: fileData.fileId,
+ newCid: fileData.newCid,
+ newFileKeyEncrypted: fileData.newFileKeyEncrypted,
+ newFileIv: fileData.newFileIv,
+ newSize: fileData.newSize,
+ parentFolderState: parentFolder,
+ });
+
+ // Update local state with modified children
+ const updatedChildren = parentFolder.children.map((child) => {
+ if (child.type === 'file' && child.id === fileData.fileId) {
+ return {
+ ...child,
+ cid: fileData.newCid,
+ fileKeyEncrypted: fileData.newFileKeyEncrypted,
+ fileIv: fileData.newFileIv,
+ size: fileData.newSize,
+ modifiedAt: Date.now(),
+ };
+ }
+ return child;
+ });
+
+ const store = useFolderStore.getState();
+ store.updateFolderChildren(parentId, updatedChildren);
+ store.updateFolderSequence(parentId, newSequenceNumber);
+
+ // Unpin old CID fire-and-forget
+ unpinFromIpfs(oldCid).catch(() => {});
+
+ // Refresh quota
+ useQuotaStore.getState().fetchQuota();
+
+ setState({ isLoading: false, error: null });
+ } catch (err) {
+ const error = err instanceof Error ? err.message : 'Failed to update file';
+ setState({ isLoading: false, error });
+ throw err;
+ }
+ },
+ []
+ );
+
/**
* Clear error state.
*/
@@ -526,6 +603,7 @@ export function useFolder() {
deleteItem: handleDelete,
addFile: handleAddFile,
addFiles: handleAddFiles,
+ updateFile: handleUpdateFile,
// Utilities
clearError,
diff --git a/apps/web/src/services/folder.service.ts b/apps/web/src/services/folder.service.ts
index 66dcdab281..f567370a04 100644
--- a/apps/web/src/services/folder.service.ts
+++ b/apps/web/src/services/folder.service.ts
@@ -762,6 +762,63 @@ export async function renameFile(params: {
});
}
+/**
+ * Replace an existing file entry in a folder with updated content.
+ *
+ * Updates the file's CID, wrapped key, IV, and size in-place within the
+ * parent folder's children array, then publishes updated metadata to IPNS.
+ * Returns the old CID so the caller can unpin it.
+ *
+ * @param params.fileId - ID of file to replace
+ * @param params.newCid - New IPFS CID of re-encrypted content
+ * @param params.newFileKeyEncrypted - Hex-encoded ECIES-wrapped new file key
+ * @param params.newFileIv - Hex-encoded new IV
+ * @param params.newSize - New file size in bytes
+ * @param params.parentFolderState - Parent folder containing this file
+ * @returns New sequence number and old CID for unpinning
+ * @throws Error if file not found
+ */
+export async function replaceFileInFolder(params: {
+ fileId: string;
+ newCid: string;
+ newFileKeyEncrypted: string;
+ newFileIv: string;
+ newSize: number;
+ parentFolderState: FolderNode;
+}): Promise<{ newSequenceNumber: bigint; oldCid: string }> {
+ // 1. Find file in parent's children
+ const children = [...params.parentFolderState.children];
+ const fileIndex = children.findIndex((c) => c.type === 'file' && c.id === params.fileId);
+
+ if (fileIndex === -1) throw new Error('File not found');
+
+ // 2. Save old CID for unpinning
+ const file = children[fileIndex] as FileEntry;
+ const oldCid = file.cid;
+
+ // 3. Update file entry with new encrypted content
+ children[fileIndex] = {
+ ...file,
+ cid: params.newCid,
+ fileKeyEncrypted: params.newFileKeyEncrypted,
+ fileIv: params.newFileIv,
+ size: params.newSize,
+ modifiedAt: Date.now(),
+ };
+
+ // 4. Update parent folder metadata and publish IPNS
+ const { newSequenceNumber } = await updateFolderMetadata({
+ folderId: params.parentFolderState.id,
+ children,
+ folderKey: params.parentFolderState.folderKey,
+ ipnsPrivateKey: params.parentFolderState.ipnsPrivateKey,
+ ipnsName: params.parentFolderState.ipnsName,
+ sequenceNumber: params.parentFolderState.sequenceNumber,
+ });
+
+ return { newSequenceNumber, oldCid };
+}
+
/**
* Fetch and decrypt folder metadata from IPFS.
*
diff --git a/apps/web/src/styles/text-editor-dialog.css b/apps/web/src/styles/text-editor-dialog.css
new file mode 100644
index 0000000000..54f1128097
--- /dev/null
+++ b/apps/web/src/styles/text-editor-dialog.css
@@ -0,0 +1,123 @@
+/**
+ * Text Editor Dialog Styles - Terminal Aesthetic
+ *
+ * Wider modal variant for editing text files in-browser.
+ * Monospace textarea with dark background, status line.
+ */
+
+/* ==========================================================================
+ Wider Modal Override
+ ========================================================================== */
+
+.text-editor-modal .modal-container {
+ max-width: 800px;
+}
+
+.text-editor-modal .modal-body {
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+ max-height: calc(90vh - 60px);
+}
+
+/* ==========================================================================
+ Editor Body
+ ========================================================================== */
+
+.text-editor-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+/* ==========================================================================
+ Textarea
+ ========================================================================== */
+
+.text-editor-textarea {
+ flex: 1;
+ width: 100%;
+ min-height: 400px;
+ padding: var(--spacing-md);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ line-height: 1.6;
+ color: var(--color-text-primary);
+ background-color: #0a0a0a;
+ border: none;
+ border-bottom: var(--border-thickness) solid var(--color-border-dim);
+ outline: none;
+ resize: none;
+ tab-size: 2;
+}
+
+.text-editor-textarea:focus {
+ box-shadow: inset 0 0 20px rgba(0, 208, 132, 0.05);
+}
+
+.text-editor-textarea:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.text-editor-textarea::placeholder {
+ color: var(--color-text-secondary);
+}
+
+/* ==========================================================================
+ Status Line
+ ========================================================================== */
+
+.text-editor-status {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ padding: var(--spacing-xs) var(--spacing-md);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ border-bottom: var(--border-thickness) solid var(--color-border-dim);
+ flex-shrink: 0;
+}
+
+.text-editor-status--modified {
+ color: var(--color-warning);
+}
+
+/* ==========================================================================
+ Footer (actions)
+ ========================================================================== */
+
+.text-editor-footer {
+ display: flex;
+ gap: var(--spacing-sm);
+ justify-content: flex-end;
+ padding: var(--spacing-sm) var(--spacing-md);
+ flex-shrink: 0;
+}
+
+/* ==========================================================================
+ Loading State
+ ========================================================================== */
+
+.text-editor-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 400px;
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+}
+
+/* ==========================================================================
+ Error Message
+ ========================================================================== */
+
+.text-editor-error {
+ padding: var(--spacing-xs) var(--spacing-md);
+ font-family: var(--font-family-mono);
+ font-size: var(--font-size-xs);
+ color: var(--color-error);
+}
diff --git a/designs/cipher-box-design.pen b/designs/cipher-box-design.pen
index 1512dc37e2..d151be7794 100644
--- a/designs/cipher-box-design.pen
+++ b/designs/cipher-box-design.pen
@@ -26157,6 +26157,1175 @@
"fontFamily": "JetBrains Mono",
"fontSize": 10,
"fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "GYwvL",
+ "x": 28500,
+ "y": 0,
+ "name": "19. Text Editor Modal",
+ "clip": true,
+ "width": 1440,
+ "height": 900,
+ "fill": "#000000",
+ "layout": "vertical",
+ "children": [
+ {
+ "type": "frame",
+ "id": "N4mif",
+ "name": "header",
+ "width": "fill_container",
+ "height": 55,
+ "fill": "#000000",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 12,
+ 24
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "OhD4Y",
+ "name": "headerLeft",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "nVwmN",
+ "name": "prompt",
+ "fill": "#00D084",
+ "content": ">",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 14,
+ "fontWeight": "700"
+ },
+ {
+ "type": "text",
+ "id": "Bj7Rv",
+ "name": "appName",
+ "fill": "#00D084",
+ "content": "CIPHERBOX",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 14,
+ "fontWeight": "600"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "vsmK1",
+ "name": "headerRight",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "ZU5UC",
+ "name": "userEmail",
+ "fill": "#006644",
+ "content": "test_account_4718@example.com ▼",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "rCuBV",
+ "name": "body",
+ "width": "fill_container",
+ "height": "fill_container",
+ "children": [
+ {
+ "type": "frame",
+ "id": "r5enq",
+ "name": "sidebar",
+ "width": 180,
+ "height": "fill_container",
+ "fill": "#000000",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "right": 1
+ },
+ "fill": "#003322"
+ },
+ "layout": "vertical",
+ "justifyContent": "space_between",
+ "children": [
+ {
+ "type": "frame",
+ "id": "psqti",
+ "name": "sidebarNav",
+ "width": "fill_container",
+ "layout": "vertical",
+ "padding": [
+ 16,
+ 0
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "zoDLU",
+ "name": "filesLink",
+ "width": "fill_container",
+ "fill": "#001a11",
+ "gap": 8,
+ "padding": [
+ 8,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "lZQFY",
+ "name": "filesIcon",
+ "content": "📁",
+ "fontFamily": "Inter",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "601jH",
+ "name": "filesText",
+ "fill": "#00D084",
+ "content": "Files",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "2Q840",
+ "name": "settingsLink",
+ "width": "fill_container",
+ "gap": 8,
+ "padding": [
+ 8,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "pn0qw",
+ "name": "settingsIcon",
+ "fill": "#006644",
+ "content": "⚙",
+ "fontFamily": "Inter",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "BbfRf",
+ "name": "settingsText",
+ "fill": "#006644",
+ "content": "Settings",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "1nszp",
+ "name": "sidebarBottom",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 8,
+ "padding": [
+ 12,
+ 16
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "m0ReL",
+ "name": "quotaBar",
+ "width": "fill_container",
+ "height": 4,
+ "fill": "#003322",
+ "cornerRadius": 2,
+ "children": [
+ {
+ "type": "frame",
+ "id": "fl5jo",
+ "name": "quotaFill",
+ "width": 2,
+ "height": 4,
+ "fill": "#00D084",
+ "cornerRadius": 2
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "id": "xbS4F",
+ "name": "quotaText",
+ "fill": "#006644",
+ "content": "0 B / 500.0 MB",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "NplF8",
+ "name": "mainArea",
+ "width": "fill_container",
+ "height": "fill_container",
+ "fill": "#000000",
+ "layout": "none",
+ "children": [
+ {
+ "type": "frame",
+ "id": "MpEKb",
+ "x": 0,
+ "y": 0,
+ "name": "toolbar",
+ "width": 1260,
+ "padding": [
+ 12,
+ 24
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "69Bha",
+ "name": "breadcrumbs",
+ "gap": 4,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "WXtHW",
+ "name": "tilde",
+ "fill": "#006644",
+ "content": "~/",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "d9FOf",
+ "name": "vaultName",
+ "fill": "#006644",
+ "content": "my vault / workspace",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "YmrcX",
+ "name": "toolbarRight",
+ "gap": 12,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "liKRT",
+ "name": "newFolderBtn",
+ "stroke": {
+ "align": "inside",
+ "thickness": 1,
+ "fill": "#003322"
+ },
+ "padding": [
+ 6,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "FeQiX",
+ "name": "newFolderText",
+ "fill": "#00D084",
+ "content": "+folder",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "xVjkd",
+ "name": "uploadBtn",
+ "stroke": {
+ "align": "inside",
+ "thickness": 1,
+ "fill": "#003322"
+ },
+ "gap": 6,
+ "padding": [
+ 6,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "wDOqN",
+ "name": "uploadIcon",
+ "fill": "#00D084",
+ "content": "⬆",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "L9D57",
+ "name": "uploadText",
+ "fill": "#00D084",
+ "content": "--upload",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "id": "xomH6",
+ "name": "syncIcon",
+ "fill": "#00D084",
+ "content": "✓",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "Yxoce",
+ "x": 0,
+ "y": 52,
+ "name": "fileList",
+ "width": 1260,
+ "height": 761,
+ "fill": "#000000",
+ "layout": "vertical",
+ "children": [
+ {
+ "type": "frame",
+ "id": "I05E6",
+ "name": "fileHeader",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 8,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "uYYME",
+ "name": "colName",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "name",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "hdQZm",
+ "name": "colSize",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "content": "size",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "8cAvx",
+ "name": "colDate",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "modified",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "600"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "mwQdS",
+ "name": "parentRow",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "y44Zh",
+ "name": "parentName",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[DIR] ..",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "46ZLM",
+ "name": "parentSize",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "Lm8wY",
+ "name": "parentDate",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "fUvzp",
+ "name": "folderRow1",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "kpI8u",
+ "name": "r1name",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[DIR] documents",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "I116D",
+ "name": "r1size",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "yMzDV",
+ "name": "r1date",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "2026-02-09 14:32",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "mllLp",
+ "name": "folderRow2",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "wxUOi",
+ "name": "r2name",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[DIR] images",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "SdCmS",
+ "name": "r2size",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "vqyms",
+ "name": "r2date",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "2026-02-09 14:33",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "I7i0j",
+ "name": "folderRow3",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "KNjxl",
+ "name": "r3name",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[DIR] projects",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "nRJ8J",
+ "name": "r3size",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "oxWbq",
+ "name": "r3date",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "2026-02-09 14:34",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "gc3TP",
+ "name": "fileRow1",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "yzbr2",
+ "name": "r4name",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[FILE] readme.txt",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "vpbN2",
+ "name": "r4size",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "content": "1.2 KB",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "XAz8P",
+ "name": "r4date",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "2026-02-09 14:30",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "t5Ovj",
+ "name": "fileRow2",
+ "width": "fill_container",
+ "fill": "#003322",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "OySEx",
+ "name": "r5name",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[FILE] notes.txt",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "ABFgi",
+ "name": "r5size",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "content": "856 B",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "NGlz9",
+ "name": "r5date",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "2026-02-09 14:31",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "g1kcC",
+ "name": "fileRow3",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 10,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "UKcCa",
+ "name": "r6name",
+ "fill": "#00D084",
+ "textGrowth": "fixed-width",
+ "width": 500,
+ "content": "[FILE] config.txt",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "8wmQH",
+ "name": "r6size",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 100,
+ "content": "2.4 KB",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "yamD2",
+ "name": "r6date",
+ "fill": "#006644",
+ "textGrowth": "fixed-width",
+ "width": 200,
+ "content": "2026-02-09 14:31",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "U9DId",
+ "x": 0,
+ "y": 0,
+ "name": "backdrop",
+ "width": 1260,
+ "height": 813,
+ "fill": "#000000CC"
+ },
+ {
+ "type": "frame",
+ "id": "6UL4T",
+ "x": 230,
+ "y": 130,
+ "name": "textEditorModal",
+ "width": 800,
+ "fill": "#000000",
+ "stroke": {
+ "align": "inside",
+ "thickness": 1,
+ "fill": "#003322"
+ },
+ "layout": "vertical",
+ "children": [
+ {
+ "type": "frame",
+ "id": "wtRDA",
+ "name": "modalHeader",
+ "width": "fill_container",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 16,
+ 24
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "4d9rX",
+ "name": "mTitle",
+ "fill": "#00D084",
+ "content": "EDIT: NOTES.TXT",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 1
+ },
+ {
+ "type": "frame",
+ "id": "bEvb0",
+ "name": "closeBtn",
+ "width": 32,
+ "height": 32,
+ "stroke": {
+ "align": "inside",
+ "thickness": 1,
+ "fill": "#003322"
+ },
+ "justifyContent": "center",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "rxkKC",
+ "name": "closeX",
+ "fill": "#006644",
+ "content": "✕",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "Fl0AG",
+ "name": "modalBody",
+ "width": "fill_container",
+ "layout": "vertical",
+ "children": [
+ {
+ "type": "frame",
+ "id": "HUD68",
+ "name": "textareaArea",
+ "width": "fill_container",
+ "height": 380,
+ "fill": "#0A0A0A",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "bottom": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": 16,
+ "children": [
+ {
+ "type": "text",
+ "id": "yqmyY",
+ "name": "textareaContent",
+ "fill": "#00D084",
+ "content": "# My Notes\n\nThis is a sample text file stored in CipherBox.\nAll content is encrypted client-side before upload.\n\n## TODO\n- Review encryption keys\n- Update folder metadata\n- Test IPNS publishing\n\n// end of file",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "ierlv",
+ "name": "statusLine",
+ "width": "fill_container",
+ "padding": [
+ 8,
+ 16
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "XbiFC",
+ "name": "statusText",
+ "fill": "#006644",
+ "content": "// 12 lines | utf-8 | modified ●",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "HXrxw",
+ "name": "modalActions",
+ "width": "fill_container",
+ "gap": 12,
+ "padding": [
+ 16,
+ 24
+ ],
+ "justifyContent": "end",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "kDlUR",
+ "name": "cancelBtn",
+ "stroke": {
+ "align": "inside",
+ "thickness": 1,
+ "fill": "#003322"
+ },
+ "padding": [
+ 8,
+ 20
+ ],
+ "justifyContent": "center",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "FPW8s",
+ "name": "cancelText",
+ "fill": "#006644",
+ "content": "CANCEL",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 1
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "C2Dew",
+ "name": "saveBtn",
+ "fill": "#00D084",
+ "padding": [
+ 8,
+ 24
+ ],
+ "justifyContent": "center",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "xPgBt",
+ "name": "saveText",
+ "fill": "#000000",
+ "content": "--SAVE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 1
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "JfT6v",
+ "name": "footer",
+ "width": "fill_container",
+ "height": 32,
+ "fill": "#000000",
+ "stroke": {
+ "align": "inside",
+ "thickness": {
+ "top": 1
+ },
+ "fill": "#003322"
+ },
+ "padding": [
+ 8,
+ 24
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "Mwl4M",
+ "name": "footerLeft",
+ "fill": "#006644",
+ "content": "(c) 2026 CipherBox",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "ZNE30",
+ "name": "footerCenter",
+ "gap": 16,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "sJE8Z",
+ "name": "fLink1",
+ "fill": "#006644",
+ "content": "[help]",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "ijCdo",
+ "name": "fLink2",
+ "fill": "#006644",
+ "content": "[privacy]",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "RoiB9",
+ "name": "fLink3",
+ "fill": "#006644",
+ "content": "[terms]",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "514gC",
+ "name": "fLink4",
+ "fill": "#006644",
+ "content": "[github]",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "ubsQj",
+ "name": "footerRight",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "ellipse",
+ "id": "hXrCT",
+ "name": "statusDot",
+ "fill": "#00D084",
+ "width": 6,
+ "height": 6
+ },
+ {
+ "type": "text",
+ "id": "hnYoO",
+ "name": "statusText",
+ "fill": "#006644",
+ "content": "[CONNECTED]",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "text",
+ "id": "zuBF0",
+ "x": 28500,
+ "y": -30,
+ "name": "label",
+ "fill": "#00D084",
+ "content": "19. Text Editor Modal",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 14,
+ "fontWeight": "600"
}
]
}
\ No newline at end of file
diff --git a/tests/e2e/page-objects/dialogs/index.ts b/tests/e2e/page-objects/dialogs/index.ts
index 4e45ceb4ca..45db32555c 100644
--- a/tests/e2e/page-objects/dialogs/index.ts
+++ b/tests/e2e/page-objects/dialogs/index.ts
@@ -5,3 +5,4 @@ export { ConfirmDialogPage } from './confirm-dialog.page';
export { CreateFolderDialogPage } from './create-folder-dialog.page';
export { RenameDialogPage } from './rename-dialog.page';
export { MoveDialogPage } from './move-dialog.page';
+export { TextEditorDialogPage } from './text-editor-dialog.page';
diff --git a/tests/e2e/page-objects/dialogs/text-editor-dialog.page.ts b/tests/e2e/page-objects/dialogs/text-editor-dialog.page.ts
new file mode 100644
index 0000000000..e6f075b656
--- /dev/null
+++ b/tests/e2e/page-objects/dialogs/text-editor-dialog.page.ts
@@ -0,0 +1,196 @@
+import { type Page, type Locator } from '@playwright/test';
+
+/**
+ * Page object for TextEditorDialog component.
+ *
+ * Encapsulates the text editor modal: loading state, textarea editing,
+ * status line, save/cancel buttons, and error display.
+ */
+export class TextEditorDialogPage {
+ constructor(private readonly page: Page) {}
+
+ /**
+ * Get the dialog container.
+ * TextEditorDialog renders inside a Modal with .text-editor-modal class.
+ */
+ dialog(): Locator {
+ return this.page.locator('.text-editor-modal .modal-container');
+ }
+
+ /**
+ * Get the dialog title.
+ */
+ title(): Locator {
+ return this.dialog().locator('.modal-title');
+ }
+
+ /**
+ * Get the close button (X).
+ */
+ closeButton(): Locator {
+ return this.dialog().locator('.modal-close');
+ }
+
+ /**
+ * Get the loading indicator.
+ */
+ loadingIndicator(): Locator {
+ return this.dialog().locator('.text-editor-loading');
+ }
+
+ /**
+ * Get the textarea element.
+ */
+ textarea(): Locator {
+ return this.dialog().locator('.text-editor-textarea');
+ }
+
+ /**
+ * Get the status line.
+ */
+ statusLine(): Locator {
+ return this.dialog().locator('.text-editor-status');
+ }
+
+ /**
+ * Get the error message.
+ */
+ errorMessage(): Locator {
+ return this.dialog().locator('.text-editor-error');
+ }
+
+ /**
+ * Get the cancel button.
+ */
+ cancelButton(): Locator {
+ return this.dialog().locator('.dialog-button--secondary', { hasText: 'cancel' });
+ }
+
+ /**
+ * Get the save button.
+ */
+ saveButton(): Locator {
+ return this.dialog().locator('.dialog-button--primary');
+ }
+
+ /**
+ * Check if the dialog is visible.
+ */
+ async isVisible(): Promise {
+ return await this.dialog().isVisible();
+ }
+
+ /**
+ * Wait for the dialog to open.
+ */
+ async waitForOpen(options?: { timeout?: number }): Promise {
+ await this.dialog().waitFor({ state: 'visible', ...options });
+ }
+
+ /**
+ * Wait for the dialog to close.
+ */
+ async waitForClose(options?: { timeout?: number }): Promise {
+ await this.dialog().waitFor({ state: 'hidden', ...options });
+ }
+
+ /**
+ * Wait for file content to finish loading (loading indicator disappears, textarea appears).
+ */
+ async waitForContentLoaded(options?: { timeout?: number }): Promise {
+ await this.textarea().waitFor({ state: 'visible', ...options });
+ }
+
+ /**
+ * Check if the dialog is in loading state.
+ */
+ async isLoading(): Promise {
+ return await this.loadingIndicator().isVisible();
+ }
+
+ /**
+ * Get the dialog title text.
+ */
+ async getTitle(): Promise {
+ return (await this.title().textContent()) ?? '';
+ }
+
+ /**
+ * Get the textarea content.
+ */
+ async getContent(): Promise {
+ return await this.textarea().inputValue();
+ }
+
+ /**
+ * Set the textarea content (clears existing and types new).
+ */
+ async setContent(text: string): Promise {
+ await this.textarea().fill(text);
+ }
+
+ /**
+ * Get the status line text.
+ */
+ async getStatusText(): Promise {
+ return (await this.statusLine().textContent()) ?? '';
+ }
+
+ /**
+ * Check if the status line shows "modified" indicator.
+ */
+ async isModified(): Promise {
+ const text = await this.getStatusText();
+ return text.includes('modified');
+ }
+
+ /**
+ * Check if the save button is disabled.
+ */
+ async isSaveDisabled(): Promise {
+ return await this.saveButton().isDisabled();
+ }
+
+ /**
+ * Get the save button text.
+ */
+ async getSaveButtonText(): Promise {
+ return (await this.saveButton().textContent()) ?? '';
+ }
+
+ /**
+ * Check if there is an error message displayed.
+ */
+ async hasError(): Promise {
+ return await this.errorMessage().isVisible();
+ }
+
+ /**
+ * Get the error message text.
+ */
+ async getErrorText(): Promise {
+ return (await this.errorMessage().textContent()) ?? '';
+ }
+
+ /**
+ * Click the save button.
+ */
+ async clickSave(): Promise {
+ await this.saveButton().click();
+ }
+
+ /**
+ * Click the cancel button.
+ */
+ async clickCancel(): Promise {
+ await this.cancelButton().click();
+ }
+
+ /**
+ * Close the dialog via the X button.
+ */
+ async close(): Promise {
+ await this.closeButton().click();
+ await this.waitForClose();
+ }
+}
diff --git a/tests/e2e/page-objects/file-browser/context-menu.page.ts b/tests/e2e/page-objects/file-browser/context-menu.page.ts
index 341b5a3115..59d013f898 100644
--- a/tests/e2e/page-objects/file-browser/context-menu.page.ts
+++ b/tests/e2e/page-objects/file-browser/context-menu.page.ts
@@ -44,6 +44,13 @@ export class ContextMenuPage {
return this.menu().locator('button[role="menuitem"]', { hasText: 'Move to...' });
}
+ /**
+ * Get the Edit menu option (text files only).
+ */
+ editOption(): Locator {
+ return this.menu().locator('button[role="menuitem"]', { hasText: 'Edit' });
+ }
+
/**
* Get the Details menu option.
*/
@@ -100,6 +107,13 @@ export class ContextMenuPage {
await this.moveOption().click();
}
+ /**
+ * Click the Edit option (text files only).
+ */
+ async clickEdit(): Promise {
+ await this.editOption().click();
+ }
+
/**
* Click the Details option.
*/
diff --git a/tests/e2e/tests/full-workflow.spec.ts b/tests/e2e/tests/full-workflow.spec.ts
index 8045fd40fd..752124d774 100644
--- a/tests/e2e/tests/full-workflow.spec.ts
+++ b/tests/e2e/tests/full-workflow.spec.ts
@@ -11,6 +11,7 @@ import { ConfirmDialogPage } from '../page-objects/dialogs/confirm-dialog.page';
import { CreateFolderDialogPage } from '../page-objects/dialogs/create-folder-dialog.page';
import { MoveDialogPage } from '../page-objects/dialogs/move-dialog.page';
import { DetailsDialogPage } from '../page-objects/dialogs/details-dialog.page';
+import { TextEditorDialogPage } from '../page-objects/dialogs/text-editor-dialog.page';
/**
* Full Workflow E2E Test Suite
@@ -46,6 +47,7 @@ test.describe.serial('Full Workflow', () => {
let createFolderDialog: CreateFolderDialogPage;
let moveDialog: MoveDialogPage;
let detailsDialog: DetailsDialogPage;
+ let textEditorDialog: TextEditorDialogPage;
// Test data - unique names for this test run
const timestamp = Date.now();
@@ -105,6 +107,12 @@ test.describe.serial('Full Workflow', () => {
const editableFileOriginalContent = 'Original content before edit';
const editableFileUpdatedContent = 'Updated content after edit - version 2';
+ // Content for in-place text editor test (Phase 5.5)
+ const textEditorEditedContent = 'Edited in-browser via text editor modal - version 3';
+
+ // Track CID before/after text editor save to verify re-encryption
+ let cidBeforeEdit = '';
+
test.beforeAll(async ({ browser: testBrowser }) => {
browser = testBrowser;
context = await browser.newContext();
@@ -121,6 +129,7 @@ test.describe.serial('Full Workflow', () => {
createFolderDialog = new CreateFolderDialogPage(page);
moveDialog = new MoveDialogPage(page);
detailsDialog = new DetailsDialogPage(page);
+ textEditorDialog = new TextEditorDialogPage(page);
});
test.afterAll(async () => {
@@ -512,6 +521,9 @@ test.describe.serial('Full Workflow', () => {
// key unwrapping). This path is NOT exercised by in-session tests.
test('3.7 Page reload preserves session and reloads root folder', async () => {
+ // Reload + re-auth + IPNS sync chain can take >30s in CI
+ test.setTimeout(90000);
+
// Navigate to root before reload so we start from a clean state
await navigateToRoot();
@@ -783,6 +795,140 @@ test.describe.serial('Full Workflow', () => {
await download2.cancel();
});
+ // ============================================
+ // Phase 5.5: In-Browser Text Editor
+ // ============================================
+ // Exercises the full crypto round-trip: download → decrypt → edit → encrypt
+ // → re-upload → update folder metadata → unpin old CID.
+
+ test('5.5.1 Edit option appears for text files only', async () => {
+ // We're at root from 5.1
+
+ // Right-click the text file — "Edit" should be in context menu
+ expect(await fileList.isItemVisible(editableFileName)).toBe(true);
+ await fileList.rightClickItem(editableFileName);
+ await contextMenu.waitForOpen();
+
+ const options = await contextMenu.getVisibleOptions();
+ expect(options).toContain('Edit');
+ await contextMenu.closeWithEscape();
+
+ // Right-click a folder — "Edit" should NOT be in context menu
+ expect(await fileList.isItemVisible(workspaceFolder)).toBe(true);
+ await fileList.rightClickItem(workspaceFolder);
+ await contextMenu.waitForOpen();
+
+ const folderOptions = await contextMenu.getVisibleOptions();
+ expect(folderOptions).not.toContain('Edit');
+ await contextMenu.closeWithEscape();
+ });
+
+ test('5.5.2 Open text editor, verify content loaded', async () => {
+ // Right-click the editable file and click Edit
+ await fileList.rightClickItem(editableFileName);
+ await contextMenu.waitForOpen();
+ await contextMenu.clickEdit();
+
+ // Dialog should open with loading state
+ await textEditorDialog.waitForOpen({ timeout: 10000 });
+
+ // Title should show the filename
+ const title = await textEditorDialog.getTitle();
+ expect(title).toContain(editableFileName);
+
+ // Wait for content to decrypt and load
+ await textEditorDialog.waitForContentLoaded({ timeout: 30000 });
+
+ // Textarea should contain the file content (uploaded in 5.1 with updated content)
+ const content = await textEditorDialog.getContent();
+ expect(content).toBe(editableFileUpdatedContent);
+
+ // Status line should show line count and utf-8
+ const status = await textEditorDialog.getStatusText();
+ expect(status).toContain('utf-8');
+ expect(status).toMatch(/\d+ line/);
+
+ // Should NOT show modified yet
+ expect(await textEditorDialog.isModified()).toBe(false);
+
+ // Save button should be disabled (no changes)
+ expect(await textEditorDialog.isSaveDisabled()).toBe(true);
+
+ // Close without saving
+ await textEditorDialog.clickCancel();
+ await textEditorDialog.waitForClose();
+ });
+
+ test('5.5.3 Edit content and save (full crypto round-trip)', async () => {
+ // Record the original CID via Details dialog before editing
+ await fileList.rightClickItem(editableFileName);
+ await contextMenu.waitForOpen();
+ await contextMenu.clickDetails();
+ await detailsDialog.waitForOpen();
+ cidBeforeEdit = await detailsDialog.getValueText('Content CID');
+ expect(cidBeforeEdit).toBeTruthy();
+ await detailsDialog.close();
+
+ // Open the text editor
+ await fileList.rightClickItem(editableFileName);
+ await contextMenu.waitForOpen();
+ await contextMenu.clickEdit();
+ await textEditorDialog.waitForOpen({ timeout: 10000 });
+ await textEditorDialog.waitForContentLoaded({ timeout: 30000 });
+
+ // Modify the content
+ await textEditorDialog.setContent(textEditorEditedContent);
+
+ // Status should now show "modified"
+ expect(await textEditorDialog.isModified()).toBe(true);
+
+ // Save button should be enabled
+ expect(await textEditorDialog.isSaveDisabled()).toBe(false);
+
+ // Click save — triggers encrypt → upload → metadata update
+ await textEditorDialog.clickSave();
+
+ // Dialog should close after successful save
+ await textEditorDialog.waitForClose({ timeout: 30000 });
+
+ // File should still be visible in the list
+ expect(await fileList.isItemVisible(editableFileName)).toBe(true);
+ });
+
+ test('5.5.4 Verify CID changed after edit (re-encryption confirmed)', async () => {
+ // Open Details dialog to verify the CID changed
+ await fileList.rightClickItem(editableFileName);
+ await contextMenu.waitForOpen();
+ await contextMenu.clickDetails();
+ await detailsDialog.waitForOpen();
+
+ const newCid = await detailsDialog.getValueText('Content CID');
+ expect(newCid).toBeTruthy();
+
+ // CID must be different from before editing (content was re-encrypted with new key)
+ expect(cidBeforeEdit).toBeTruthy();
+ expect(newCid).not.toBe(cidBeforeEdit);
+
+ await detailsDialog.close();
+ });
+
+ test('5.5.5 Re-open editor to verify saved content persists', async () => {
+ // Re-open the editor to verify the new content was actually saved
+ await fileList.rightClickItem(editableFileName);
+ await contextMenu.waitForOpen();
+ await contextMenu.clickEdit();
+ await textEditorDialog.waitForOpen({ timeout: 10000 });
+ await textEditorDialog.waitForContentLoaded({ timeout: 30000 });
+
+ // Content should be the edited version
+ const content = await textEditorDialog.getContent();
+ expect(content).toBe(textEditorEditedContent);
+
+ // Close without changes
+ await textEditorDialog.clickCancel();
+ await textEditorDialog.waitForClose();
+ });
+
// ============================================
// Phase 6: Rename Operations
// ============================================