From 3f27ba79a4747fbda4d51236862eb29d7e1a4e77 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Fri, 10 Jul 2026 08:35:46 +0800 Subject: [PATCH] feat: bridge permanent trash deletion to plugins --- docs/DEV_PLUGINS.md | 2 +- frontend/e2e/plugin-api-bridge.spec.js | 23 +++++++++++++++++++ .../src/lib/plugin-host/VerstakPluginAPI.js | 6 +++++ frontend/src/lib/test/wails-mock.js | 15 ++++++++++-- frontend/wailsjs/go/api/App.d.ts | 10 ++++---- frontend/wailsjs/go/api/App.js | 20 +++++++++------- frontend/wailsjs/go/models.ts | 5 +++- 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/docs/DEV_PLUGINS.md b/docs/DEV_PLUGINS.md index bba00fa..e54eb6d 100644 --- a/docs/DEV_PLUGINS.md +++ b/docs/DEV_PLUGINS.md @@ -85,7 +85,7 @@ Frontend bundles are mounted with a plugin-scoped API created by - `capabilities.list/get/has` - `commands.register/execute` for handlers declared in `contributes.commands` - `events.publish/subscribe` using the bundled frontend event bus -- `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/openExternal/showInFolder` +- `files.list/metadata/readText/readBytes/writeText/createFolder/move/trash/listTrash/restoreTrash/deleteTrash/openExternal/showInFolder` for canonical vault-relative slash paths guarded by `files.read`, `files.write`, `files.delete`, and `files.openExternal`. Backslashes, Windows absolute paths, UNC paths, traversal, `.verstak` variants, and diff --git a/frontend/e2e/plugin-api-bridge.spec.js b/frontend/e2e/plugin-api-bridge.spec.js index 99273ea..2c637e5 100644 --- a/frontend/e2e/plugin-api-bridge.spec.js +++ b/frontend/e2e/plugin-api-bridge.spec.js @@ -125,6 +125,29 @@ test.describe('D: Plugin API bridge', () => { expect(result.disconnected.statusLabel).toBe('disabled'); }); + test('files API permanently deletes an existing trash entry through the bridge', async ({ page }) => { + const result = await page.evaluate(async () => { + const api = window.createPluginAPI('verstak.files'); + await api.files.writeText('Project/bridge-trash.txt', 'remove me', { createIfMissing: true }); + const trash = await api.files.trash('Project/bridge-trash.txt'); + const before = await api.files.listTrash(); + await api.files.deleteTrash(trash.trashId); + const after = await api.files.listTrash(); + let readError = ''; + try { + await api.files.readText('Project/bridge-trash.txt'); + } catch (error) { + readError = String(error && error.message || error); + } + api.dispose(); + return { before, after, readError }; + }); + + expect(result.before).toHaveLength(1); + expect(result.after).toEqual([]); + expect(result.readError).toContain('not-found: Project/bridge-trash.txt'); + }); + test('backend plugin events are dispatched to subscribed frontend handlers', async ({ page }) => { const result = await page.evaluate(async () => { const api = window.createPluginAPI('verstak.platform-test'); diff --git a/frontend/src/lib/plugin-host/VerstakPluginAPI.js b/frontend/src/lib/plugin-host/VerstakPluginAPI.js index e242f9b..c646515 100644 --- a/frontend/src/lib/plugin-host/VerstakPluginAPI.js +++ b/frontend/src/lib/plugin-host/VerstakPluginAPI.js @@ -380,6 +380,12 @@ export function createPluginAPI(pluginId) { return App.RestoreVaultTrash(pluginId, trashId, options || {}); }); }, + deleteTrash: function(trashId) { + assertActive('files.deleteTrash(' + trashId + ')'); + return callBackendErrorString(pluginId, 'files.deleteTrash(' + trashId + ')', function() { + return App.DeleteVaultTrash(pluginId, trashId); + }); + }, openExternal: function(relativePath) { assertActive('files.openExternal(' + relativePath + ')'); return callBackendErrorString(pluginId, 'files.openExternal(' + relativePath + ')', function() { diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js index f58a987..7b759a0 100644 --- a/frontend/src/lib/test/wails-mock.js +++ b/frontend/src/lib/test/wails-mock.js @@ -2608,13 +2608,15 @@ if (!vaultFiles[norm.path]) return Promise.resolve([{}, 'not-found: ' + norm.path]); var trashId = 'mock-' + Date.now() + '-' + Math.random().toString(16).slice(2); var trashPath = '.verstak/trash/files/' + trashId + '/' + baseName(norm.path); - var originalType = vaultFiles[norm.path].type || 'file'; + var originalNode = vaultFiles[norm.path]; + var originalType = originalNode.type || 'file'; + var originalSize = originalType === 'file' ? String(originalNode.content || '').length : 0; var moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; }); trashPayloads[trashId] = moving.map(function (path) { return { suffix: path.slice(norm.path.length), entry: Object.assign({}, vaultFiles[path]) }; }); moving.forEach(function (path) { delete vaultFiles[path]; }); - var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path) }; + var entry = { originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString(), originalType: originalType, basename: baseName(norm.path), size: originalSize }; trashEntries.unshift(entry); return Promise.resolve([entry, '']); }, @@ -2646,6 +2648,15 @@ trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; }); return Promise.resolve([target.path, '']); }, + DeleteVaultTrash: function (pluginId, trashId) { + var err = requirePluginPermission(pluginId, 'files.delete'); + if (err) return Promise.resolve(err); + var entry = trashEntries.find(function (item) { return item.trashId === trashId; }); + if (!entry) return Promise.resolve('not-found: trash entry ' + trashId); + delete trashPayloads[trashId]; + trashEntries = trashEntries.filter(function (item) { return item.trashId !== trashId; }); + return Promise.resolve(''); + }, OpenVaultPathExternal: function (pluginId, relativePath) { var err = requirePluginPermission(pluginId, 'files.openExternal'); if (err) return Promise.resolve(err); diff --git a/frontend/wailsjs/go/api/App.d.ts b/frontend/wailsjs/go/api/App.d.ts index 32398a5..6090e2f 100755 --- a/frontend/wailsjs/go/api/App.d.ts +++ b/frontend/wailsjs/go/api/App.d.ts @@ -20,6 +20,8 @@ export function CreateWorkspace(arg1:string,arg2:string):Promise>; +export function DeleteVaultTrash(arg1:string,arg2:string):Promise; + export function DisablePlugin(arg1:string):Promise; export function EditWorkbenchResource(arg1:string,arg2:Record):Promise; @@ -80,6 +82,10 @@ export function OpenVaultPathExternal(arg1:string,arg2:string):Promise; export function OpenWorkbenchResource(arg1:string,arg2:Record):Promise; +export function PluginBrowserReceiverPairing(arg1:string):Promise|string>; + +export function PluginRotateBrowserReceiverToken(arg1:string):Promise|string>; + export function PluginSecretsCopyLink(arg1:string,arg2:string):Promise; export function PluginSecretsDelete(arg1:string,arg2:string):Promise; @@ -94,10 +100,6 @@ export function PluginSecretsUnlock(arg1:string,arg2:string):Promise; export function PluginSecretsWrite(arg1:string,arg2:Record):Promise|string>; -export function PluginBrowserReceiverPairing(arg1:string):Promise|string>; - -export function PluginRotateBrowserReceiverToken(arg1:string):Promise|string>; - export function PluginSyncConfigure(arg1:string,arg2:string,arg3:string,arg4:string):Promise; export function PluginSyncDisconnect(arg1:string):Promise; diff --git a/frontend/wailsjs/go/api/App.js b/frontend/wailsjs/go/api/App.js index 6d50289..4bdea3b 100755 --- a/frontend/wailsjs/go/api/App.js +++ b/frontend/wailsjs/go/api/App.js @@ -26,6 +26,10 @@ export function CreateWorkspaceNode(arg1, arg2, arg3) { return window['go']['api']['App']['CreateWorkspaceNode'](arg1, arg2, arg3); } +export function DeleteVaultTrash(arg1, arg2) { + return window['go']['api']['App']['DeleteVaultTrash'](arg1, arg2); +} + export function DisablePlugin(arg1) { return window['go']['api']['App']['DisablePlugin'](arg1); } @@ -146,6 +150,14 @@ export function OpenWorkbenchResource(arg1, arg2) { return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2); } +export function PluginBrowserReceiverPairing(arg1) { + return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1); +} + +export function PluginRotateBrowserReceiverToken(arg1) { + return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1); +} + export function PluginSecretsCopyLink(arg1, arg2) { return window['go']['api']['App']['PluginSecretsCopyLink'](arg1, arg2); } @@ -174,14 +186,6 @@ export function PluginSecretsWrite(arg1, arg2) { return window['go']['api']['App']['PluginSecretsWrite'](arg1, arg2); } -export function PluginBrowserReceiverPairing(arg1) { - return window['go']['api']['App']['PluginBrowserReceiverPairing'](arg1); -} - -export function PluginRotateBrowserReceiverToken(arg1) { - return window['go']['api']['App']['PluginRotateBrowserReceiverToken'](arg1); -} - export function PluginSyncConfigure(arg1, arg2, arg3, arg4) { return window['go']['api']['App']['PluginSyncConfigure'](arg1, arg2, arg3, arg4); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index cc8cf2b..07c6aa6 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -484,6 +484,7 @@ export namespace files { deletedAt: string; originalType: string; basename: string; + size: number; static createFrom(source: any = {}) { return new TrashEntry(source); @@ -497,6 +498,7 @@ export namespace files { this.deletedAt = source["deletedAt"]; this.originalType = source["originalType"]; this.basename = source["basename"]; + this.size = source["size"]; } } export class TrashResult { @@ -504,6 +506,7 @@ export namespace files { trashPath: string; trashId: string; deletedAt: string; + size: number; static createFrom(source: any = {}) { return new TrashResult(source); @@ -515,6 +518,7 @@ export namespace files { this.trashPath = source["trashPath"]; this.trashId = source["trashId"]; this.deletedAt = source["deletedAt"]; + this.size = source["size"]; } } export class WriteOptions { @@ -1315,4 +1319,3 @@ export namespace workspace { } } -