diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 330ca35..3ae149d 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -335,6 +335,13 @@ return full.indexOf(workspaceRoot + '/') === 0 ? full.slice(workspaceRoot.length + 1) : full; } + function isWorkspaceEvent(event) { + var payload = (event && event.payload) || {}; + var path = cleanPath(payload.path || ''); + if (!path) return true; + return !workspaceRoot || path === workspaceRoot || path.indexOf(workspaceRoot + '/') === 0; + } + var toolbar = el('div', { className: 'files-toolbar' }); var breadcrumb = el('div', { className: 'files-breadcrumb' }); var backBtn = iconButton('back', 'Back', 'back', goBack); @@ -1412,8 +1419,22 @@ loadContributionActions(); loadEntries(); + var fileChangedUnsubscribe = null; + if (api.events && typeof api.events.subscribe === 'function') { + api.events.subscribe('file.changed', function (event) { + if (disposed || !isWorkspaceEvent(event)) return; + if (showingTrash) loadTrashEntries(); + else loadEntries(); + }).then(function (unsubscribe) { + fileChangedUnsubscribe = unsubscribe; + }).catch(function (err) { + console.error('[files] file.changed subscription:', err); + }); + } + containerEl.__filesCleanup = function () { disposed = true; + if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe(); document.removeEventListener('click', onDocClick); document.removeEventListener('keydown', onDocKeydown); window.removeEventListener('mousedown', handleMouseHistory, true); diff --git a/plugins/files/plugin.json b/plugins/files/plugin.json index 387661c..f6dc4d9 100644 --- a/plugins/files/plugin.json +++ b/plugins/files/plugin.json @@ -19,6 +19,7 @@ "files.write", "files.delete", "files.openExternal", + "events.subscribe", "workbench.open", "ui.register" ], diff --git a/plugins/notes/frontend/src/index.js b/plugins/notes/frontend/src/index.js index a934a1b..83db706 100644 --- a/plugins/notes/frontend/src/index.js +++ b/plugins/notes/frontend/src/index.js @@ -345,6 +345,14 @@ }); } + function isNotesEvent(event) { + var payload = (event && event.payload) || {}; + var changedPath = cleanPath(payload.path || ''); + if (!changedPath) return true; + var parent = notesParent(); + return changedPath === parent || changedPath.indexOf(parent + '/') === 0; + } + function actionInitial(action) { var label = String(action && action.label || '').trim(); return label ? label.charAt(0).toUpperCase() : '+'; @@ -640,8 +648,21 @@ loadContributionActions(); loadNotes(); + var fileChangedUnsubscribe = null; + if (api.events && typeof api.events.subscribe === 'function') { + api.events.subscribe('file.changed', function (event) { + if (disposed || !isNotesEvent(event)) return; + loadNotes(); + }).then(function (unsubscribe) { + fileChangedUnsubscribe = unsubscribe; + }).catch(function (err) { + console.error('[notes] file.changed subscription:', err); + }); + } + containerEl.__notesCleanup = function () { disposed = true; + if (typeof fileChangedUnsubscribe === 'function') fileChangedUnsubscribe(); }; }, diff --git a/plugins/notes/plugin.json b/plugins/notes/plugin.json index 4c2d3ff..dc3b381 100644 --- a/plugins/notes/plugin.json +++ b/plugins/notes/plugin.json @@ -18,6 +18,7 @@ "files.read", "files.write", "files.delete", + "events.subscribe", "workbench.open", "ui.register" ], diff --git a/scripts/smoke-files-plugin.js b/scripts/smoke-files-plugin.js index 7ed64ed..b820257 100755 --- a/scripts/smoke-files-plugin.js +++ b/scripts/smoke-files-plugin.js @@ -198,7 +198,9 @@ function loadFilesComponent(document) { function makeApi() { const externalCalls = []; const contributionCalls = []; + const eventHandlers = {}; let restored = false; + let externalVisible = false; let trashEntries = [{ originalPath: 'Docs/deleted.md', trashPath: '.verstak/trash/files/mock/deleted.md', @@ -210,6 +212,13 @@ function makeApi() { return { externalCalls, contributionCalls, + emitFileChanged(payload) { + (eventHandlers['file.changed'] || []).forEach((handler) => handler({ + name: 'file.changed', + payload, + timestamp: new Date().toISOString(), + })); + }, files: { list: async () => { const entries = [{ @@ -230,6 +239,16 @@ function makeApi() { modifiedAt: '2026-06-27T01:03:00Z', }); } + if (externalVisible) { + entries.push({ + name: 'external.md', + relativePath: 'Docs/external.md', + type: 'file', + extension: 'md', + size: 9, + modifiedAt: '2026-06-27T01:04:00Z', + }); + } return entries; }, metadata: async () => { throw new Error('not-found'); }, @@ -279,6 +298,18 @@ function makeApi() { return { status: 'handled' }; }, }, + events: { + subscribe: async (eventName, handler) => { + eventHandlers[eventName] = eventHandlers[eventName] || []; + eventHandlers[eventName].push(handler); + return () => { + eventHandlers[eventName] = (eventHandlers[eventName] || []).filter((candidate) => candidate !== handler); + }; + }, + }, + showExternalFile() { + externalVisible = true; + }, }; } @@ -351,6 +382,13 @@ async function flush() { if (!restoredRow) { throw new Error(`restored file row not rendered after restore: ${container.textContent}`); } + api.showExternalFile(); + api.emitFileChanged({ path: 'Docs/external.md', operation: 'external.create', type: 'file' }); + await flush(); + const externalRow = walk(container, (node) => node.getAttribute && node.getAttribute('data-file-path') === 'Docs/external.md'); + if (!externalRow) { + throw new Error(`external file row not rendered after file.changed: ${container.textContent}`); + } console.log('files frontend smoke passed'); })().catch((err) => { diff --git a/scripts/smoke-notes-plugin.js b/scripts/smoke-notes-plugin.js index 44e8337..e92520a 100755 --- a/scripts/smoke-notes-plugin.js +++ b/scripts/smoke-notes-plugin.js @@ -137,10 +137,18 @@ function makeApi(options = {}) { const entries = new Map(); const opened = []; const contributionCalls = []; + const eventHandlers = {}; return { entries, opened, contributionCalls, + emitFileChanged(payload) { + (eventHandlers['file.changed'] || []).forEach((handler) => handler({ + name: 'file.changed', + payload, + timestamp: new Date().toISOString(), + })); + }, files: { list: async (relativeDir) => { const prefix = relativeDir ? `${relativeDir}/` : ''; @@ -212,6 +220,15 @@ function makeApi(options = {}) { return { status: 'handled' }; }, }, + events: { + subscribe: async (eventName, handler) => { + eventHandlers[eventName] = eventHandlers[eventName] || []; + eventHandlers[eventName].push(handler); + return () => { + eventHandlers[eventName] = (eventHandlers[eventName] || []).filter((candidate) => candidate !== handler); + }; + }, + }, }; } @@ -292,6 +309,13 @@ async function mountNotes(api) { sortSelect.value = 'title-asc'; sortSelect.dispatchEvent('change'); + createApi.entries.set('Project/Notes/Third_Note.md', { type: 'file', content: '# Third Note\n' }); + createApi.emitFileChanged({ path: 'Project/Notes/Third_Note.md', operation: 'external.create', type: 'file' }); + await flush(); + if (!container.textContent.includes('Third Note')) { + throw new Error(`notes list did not refresh after file.changed: ${container.textContent}`); + } + const renameButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'rename'); if (!renameButton) throw new Error('rename note button not found'); renameButton.click();