feat: milestones 6b-fix through 6e — default-editor, files plugin, workspace host, workspaceItems contribution

- Fix PluginCard openProviders display
- Add default-editor plugin (text/markdown/notes-context)
- Add files plugin with workspaceItems placement
- Add workspaceItems contribution point (Go + API + mock + SDK)
- Add WorkspaceHost component for workspace area
- WorkspaceTree dispatches selection event
- Fix default-editor layout to fill container
- Fix PluginCard unsafe .length access
- Add E2E tests: 34/34 pass
- Add bundle execution check to official-plugins check.sh
- Update docs: PLUGIN_RUNTIME, DEV_PLUGINS, MILESTONE_6B/6C/6D plans
This commit is contained in:
2026-06-19 16:42:01 +08:00
parent 6ed6df311a
commit a6412fa070
23 changed files with 1518 additions and 286 deletions
+16
View File
@@ -4,6 +4,7 @@
import ViewContainer from './lib/shell/ViewContainer.svelte';
import VaultSelection from './lib/shell/VaultSelection.svelte';
import WorkbenchHost from './lib/shell/WorkbenchHost.svelte';
import WorkspaceHost from './lib/shell/WorkspaceHost.svelte';
import * as App from '../wailsjs/go/api/App';
import { debug } from './lib/log/debug.js';
import { onMount } from 'svelte';
@@ -20,6 +21,9 @@
let activeSettingsPanelId = '';
let openedResource = null;
let workspaceNodes = [];
let currentWorkspaceNodeId = '';
function flog(msg) {
App.WriteFrontendLog('App', msg);
}
@@ -91,6 +95,15 @@
currentView = 'workbench';
}
function onWorkspaceNodeSelected(e) {
debug.log('[App] onWorkspaceNodeSelected:', e.detail?.nodeId);
currentWorkspaceNodeId = e.detail?.nodeId || '';
workspaceNodes = e.detail?.nodes || workspaceNodes;
if (currentWorkspaceNodeId) {
currentView = 'workspace';
}
}
function onCloseSettings() {
debug.log('[App] onCloseSettings');
activeSettingsPluginId = '';
@@ -105,6 +118,7 @@
window.addEventListener('verstak:open-settings', onOpenSettings);
window.addEventListener('verstak:close-settings', onCloseSettings);
window.addEventListener('verstak:workbench-opened', onWorkbenchOpened);
window.addEventListener('verstak:workspace-node-selected', onWorkspaceNodeSelected);
}
onMount(() => { checkVault(); });
@@ -125,6 +139,8 @@
<PluginManager {activeSettingsPluginId} {activeSettingsPanelId} />
{:else if currentView === 'workbench'}
<WorkbenchHost {openedResource} />
{:else if currentView === 'workspace'}
<WorkspaceHost currentNodeId={currentWorkspaceNodeId} nodes={workspaceNodes} />
{:else}
<ViewContainer {activeView} {activeViewPluginId} />
{/if}
@@ -32,6 +32,7 @@
commands: (contributions.commands || []).filter(c => c.pluginId === pluginId).length,
sidebar: (contributions.sidebarItems || []).filter(s => s.pluginId === pluginId).length,
statusbar: (contributions.statusBarItems || []).filter(s => s.pluginId === pluginId).length,
openProviders: (contributions.openProviders || []).filter(o => o.pluginId === pluginId).length,
};
$: contribSummary = (() => {
@@ -40,6 +41,7 @@
if (contribCounts.commands > 0) parts.push(contribCounts.commands + ' command' + (contribCounts.commands !== 1 ? 's' : ''));
if (contribCounts.sidebar > 0) parts.push(contribCounts.sidebar + ' sidebar' + (contribCounts.sidebar !== 1 ? 's' : ''));
if (contribCounts.statusbar > 0) parts.push(contribCounts.statusbar + ' statusbar' + (contribCounts.statusbar !== 1 ? 's' : ''));
if (contribCounts.openProviders > 0) parts.push(contribCounts.openProviders + ' openProvider' + (contribCounts.openProviders !== 1 ? 's' : ''));
return parts.length > 0 ? parts.join(', ') : 'none';
})();
@@ -200,7 +202,7 @@
</div>
<!-- Permission warnings -->
{#if !hasUIPermission && (m.contributes && (m.contributes.views || m.contributes.sidebarItems || m.contributes.settingsPanels).length > 0)}
{#if !hasUIPermission && m.contributes && ((m.contributes.views || []).length > 0 || (m.contributes.sidebarItems || []).length > 0 || (m.contributes.settingsPanels || []).length > 0)}
<p class="warning"><Icon name="warning" size={12} /> Plugin has UI contributions but lacks ui.register permission</p>
{/if}
</div>
+4 -1
View File
@@ -89,8 +89,11 @@
.workbench-content {
min-width: 0;
min-height: 0;
height: 100%;
flex: 1;
padding: 1rem;
display: flex;
flex-direction: column;
padding: 0;
}
.workbench-empty {
+163
View File
@@ -0,0 +1,163 @@
<script>
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
import * as App from '../../../wailsjs/go/api/App';
export let currentNodeId = '';
export let nodes = [];
let contributions = {};
let plugins = [];
let workspaceTools = [];
$: currentNode = nodes.find(n => n.id === currentNodeId) || null;
$: if (currentNodeId) loadTools();
async function loadTools() {
try {
const [c, p] = await Promise.all([
App.GetContributions().catch(() => ({})),
App.GetPlugins().catch(() => []),
]);
contributions = c || {};
plugins = p || [];
const enabledIds = new Set(
plugins.filter(pl => pl.enabled && (pl.status === 'loaded' || pl.status === 'degraded')).map(pl => pl.manifest?.id)
);
workspaceTools = (contributions.workspaceItems || []).filter(tool => enabledIds.has(tool.pluginId));
} catch (e) {
console.error('[WorkspaceHost] loadTools error:', e);
}
}
</script>
<div class="workspace-host">
{#if currentNode}
<div class="workspace-header">
<span class="workspace-title">{currentNode.title}</span>
<span class="workspace-type">{currentNode.type}</span>
</div>
{#if workspaceTools.length > 0}
<div class="workspace-tools">
{#each workspaceTools as tool (tool.id + tool.pluginId)}
<div class="workspace-tool">
<div class="tool-header">
<span class="tool-title">{tool.title || tool.id}</span>
<span class="tool-plugin">{tool.pluginId}</span>
</div>
<div class="tool-content">
<PluginBundleHost
pluginId={tool.pluginId}
componentId={tool.component}
componentProps={{ workspaceNodeId: currentNodeId, workspaceNode: currentNode }}
/>
</div>
</div>
{/each}
</div>
{:else}
<div class="workspace-empty">
<p>No workspace tools available</p>
<p class="workspace-hint">Install plugins that contribute workspaceItems to see tools here.</p>
</div>
{/if}
{:else}
<div class="workspace-empty">
<p>Select a workspace node from the sidebar</p>
</div>
{/if}
</div>
<style>
.workspace-host {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
background: #1a1a2e;
}
.workspace-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid #16213e;
flex-shrink: 0;
}
.workspace-title {
color: #e0e0f0;
font-size: 0.95rem;
font-weight: 600;
}
.workspace-type {
color: #4ecca3;
font-size: 0.75rem;
padding: 0.1rem 0.4rem;
border-radius: 3px;
background: #1a2a3a;
}
.workspace-tools {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0.5rem;
}
.workspace-tool {
border: 1px solid #16213e;
border-radius: 6px;
margin-bottom: 0.5rem;
overflow: hidden;
}
.tool-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
background: #12122a;
border-bottom: 1px solid #16213e;
}
.tool-title {
color: #e0e0f0;
font-size: 0.8rem;
font-weight: 600;
}
.tool-plugin {
color: #666;
font-size: 0.65rem;
margin-left: auto;
}
.tool-content {
min-height: 300px;
max-height: 60vh;
overflow: auto;
}
.workspace-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
gap: 0.5rem;
}
.workspace-hint {
font-size: 0.8rem;
color: #555;
max-width: 300px;
text-align: center;
}
</style>
@@ -78,6 +78,9 @@
if (err) { localError = err; return; }
currentNodeId = id;
activeWorkspaceNodeId.set(id);
window.dispatchEvent(new CustomEvent('verstak:workspace-node-selected', {
detail: { nodeId: id, nodes: nodes }
}));
}
function openCreate(parentId, type) {
+283 -11
View File
@@ -47,11 +47,10 @@
{
id: 'verstak.platform-test.markdown-diagnostic',
title: 'Platform Test Markdown Diagnostic',
priority: 100,
priority: 10,
component: 'MarkdownDiagnosticProvider',
supports: [
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] }
]
}
]
@@ -59,11 +58,86 @@
},
rootPath: '/tmp/verstak-test/plugins/platform-test',
error: ''
},
'verstak.default-editor': {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.default-editor',
name: 'Default Editor',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Built-in text and markdown editor/viewer.',
source: 'official',
icon: 'edit',
provides: ['verstak/default-editor/v1'],
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
permissions: ['files.read', 'files.write', 'workbench.open'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
openProviders: [
{
id: 'verstak.default-editor.text',
title: 'Default Text Editor',
priority: 50,
component: 'DefaultEditor',
supports: [
{ kind: 'vault-file', extensions: ['.txt', '.log', '.conf', '.ini', '.toml', '.yaml', '.yml', '.json', '.csv'], mime: ['text/plain', 'application/json'], contexts: ['generic-text'] }
]
},
{
id: 'verstak.default-editor.markdown',
title: 'Default Markdown Editor',
priority: 50,
component: 'DefaultEditor',
supports: [
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown'] }
]
},
{
id: 'verstak.default-editor.notes-markdown',
title: 'Default Notes Markdown Editor',
priority: 50,
component: 'DefaultEditor',
supports: [
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['notes-markdown'] }
]
}
]
}
},
rootPath: '/tmp/verstak-test/plugins/default-editor',
error: ''
},
'verstak.files': {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.files',
name: 'Files',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Minimal vault file navigator.',
source: 'official',
icon: 'folder',
provides: ['verstak/files/v1'],
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
permissions: ['files.read', 'files.write', 'workbench.open', 'ui.register'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
views: [{ id: 'verstak.files.view', title: 'Files', icon: 'folder', component: 'FilesView' }],
workspaceItems: [{ id: 'verstak.files.workspace', title: 'Files', icon: 'folder', component: 'FilesView' }]
}
},
rootPath: '/tmp/verstak-test/plugins/files',
error: ''
}
};
var vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
var vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
var vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }] };
var appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
var workbenchPreferences = {};
var openedResources = [];
@@ -97,7 +171,12 @@
function makeDefaultVaultFiles() {
return {
'': { type: 'folder', modifiedAt: new Date().toISOString() }
'': { type: 'folder', modifiedAt: new Date().toISOString() },
'Docs': { type: 'folder', modifiedAt: new Date().toISOString() },
'Docs/todo.txt': { type: 'file', content: 'Buy groceries\nWrite tests', modifiedAt: new Date().toISOString() },
'Docs/readme.md': { type: 'file', content: '# Hello World\n\nThis is a **test** document.\n\n- item 1\n- item 2', modifiedAt: new Date().toISOString() },
'Notes': { type: 'folder', modifiedAt: new Date().toISOString() },
'Notes/Overview.md': { type: 'file', content: '# Notes Overview\n\nMy notes content here.', modifiedAt: new Date().toISOString() }
};
}
@@ -204,7 +283,7 @@
}
function allContributions() {
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [];
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [], workspaceItems = [];
for (var id in pluginStates) {
var s = pluginStates[id];
var c = (s.manifest && s.manifest.contributes) || {};
@@ -214,8 +293,9 @@
if (c.statusBarItems) c.statusBarItems.forEach(function (st) { statusBarItems.push(Object.assign({}, st, { pluginId: id })); });
if (c.settingsPanels) c.settingsPanels.forEach(function (sp) { settingsPanels.push(Object.assign({}, sp, { pluginId: id })); });
if (c.openProviders) c.openProviders.forEach(function (op) { openProviders.push(Object.assign({}, op, { pluginId: id })); });
if (c.workspaceItems) c.workspaceItems.forEach(function (wi) { workspaceItems.push(Object.assign({}, wi, { pluginId: id })); });
}
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders };
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders, workspaceItems: workspaceItems };
}
function requestExtension(request) {
@@ -294,6 +374,118 @@
return Promise.resolve([result, '']);
}
function defaultEditorBundle() {
return [
'(function(){',
'var DefaultEditor={',
'mount:function(c,p,api){',
'c.innerHTML="";',
'c.className="de-root";',
'var req=p.request||{};',
'var path=req.path||"";',
'var mode=req.mode||"view";',
'var ctx=req.context||{};',
'var isNotes=ctx.notesMode||ctx.isInsideNotesFolder;',
'var ext=(req.extension||"").toLowerCase();',
'var isMd=ext===".md"||ext===".markdown";',
'var editorMode=isNotes?"notes-markdown":isMd?"generic-markdown":"text";',
'c.setAttribute("data-editor-mode",editorMode);',
'c.setAttribute("data-resource-path",path);',
'c.setAttribute("data-request-mode",mode);',
'var toolbar=document.createElement("div");',
'toolbar.className="de-toolbar";',
'var modeLabel=document.createElement("span");',
'modeLabel.className="de-toolbar-mode";',
'modeLabel.textContent=editorMode;',
'toolbar.appendChild(modeLabel);',
'var pathLabel=document.createElement("span");',
'pathLabel.className="de-toolbar-context";',
'pathLabel.textContent=path;',
'toolbar.appendChild(pathLabel);',
'if(isNotes){var badge=document.createElement("span");badge.className="de-notes-badge";badge.textContent="notes context";badge.setAttribute("data-notes-badge","");toolbar.appendChild(badge);}',
'c.appendChild(toolbar);',
'var content=document.createElement("div");',
'content.className="de-editor-wrap";',
'content.textContent="Loading...";',
'c.appendChild(content);',
'api.files.readText(path).then(function(text){',
'content.textContent="";',
'if(isMd){',
'var preview=document.createElement("div");',
'preview.className="de-preview";',
'preview.setAttribute("data-preview","");',
'preview.textContent=text;',
'content.appendChild(preview);',
'}else{',
'var ta=document.createElement("textarea");',
'ta.className="de-textarea";',
'ta.value=text;',
'ta.setAttribute("data-editor-textarea","");',
'content.appendChild(ta);',
'}',
'}).catch(function(err){',
'content.textContent="Error: "+(err.message||err);',
'});',
'},',
'unmount:function(c){c.innerHTML="";}',
'};',
'window.VerstakPluginRegister("verstak.default-editor",{components:{DefaultEditor:DefaultEditor}});',
'})();'
].join('\n');
}
function filesPluginBundle() {
return [
"(function(){",
"var FilesView={",
"mount:function(c,p,api){",
"c.innerHTML='';",
"c.className='files-root';",
"c.setAttribute('data-plugin-id','verstak.files');",
"var list=document.createElement('div');",
"list.className='files-list';",
"list.setAttribute('data-files-list','');",
"c.appendChild(list);",
"function load(){",
"list.textContent='Loading...';",
"api.files.list('').then(function(entries){",
"list.innerHTML='';",
"if(!entries||!entries.length){list.textContent='Empty folder';return;}",
"entries.forEach(function(e){",
"if(e.isHidden||e.isReserved)return;",
"var item=document.createElement('div');",
"item.className='files-item';",
"item.setAttribute('data-file-name',e.name);",
"item.setAttribute('data-file-type',e.type);",
"item.setAttribute('data-file-path',e.relativePath);",
"var icon=document.createElement('span');",
"icon.className='files-item-icon';",
"icon.textContent=e.type==='folder'?'[D]':'[F]';",
"var name=document.createElement('span');",
"name.className='files-item-name';",
"name.textContent=e.name;",
"item.appendChild(icon);",
"item.appendChild(name);",
"if(e.type!=='folder'){",
"item.addEventListener('dblclick',function(){",
"var ext=e.extension?'.'+e.extension:'';",
"var ctx={sourcePluginId:'verstak.files',sourceView:'files'};",
"api.workbench.openResource({kind:'vault-file',path:e.relativePath,mode:'view',extension:ext,context:ctx});",
"});",
"}",
"list.appendChild(item);",
"});",
"}).catch(function(err){list.textContent='Error: '+(err.message||err);});",
"}",
"load();",
"},",
"unmount:function(c){c.innerHTML='';}",
"};",
"window.VerstakPluginRegister('verstak.files',{components:{FilesView:FilesView}});",
"})();"
].join('\n');
}
function platformTestBundle() {
return [
"(function(){",
@@ -448,6 +640,12 @@
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(platformTestBundle());
}
if (pluginId === 'verstak.default-editor' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(defaultEditorBundle());
}
if (pluginId === 'verstak.files' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(filesPluginBundle());
}
return Promise.resolve('');
},
GetPluginCapability: function (pluginId, capId) {
@@ -675,11 +873,10 @@
{
id: 'verstak.platform-test.markdown-diagnostic',
title: 'Platform Test Markdown Diagnostic',
priority: 100,
priority: 10,
component: 'MarkdownDiagnosticProvider',
supports: [
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] },
{ kind: 'vault-file', extensions: ['.txt', '.log'], mime: ['text/plain'], contexts: ['generic-text'] }
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown', 'notes-markdown'] }
]
}
]
@@ -687,10 +884,85 @@
},
rootPath: '/tmp/verstak-test/plugins/platform-test',
error: ''
},
'verstak.default-editor': {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.default-editor',
name: 'Default Editor',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Built-in text and markdown editor/viewer.',
source: 'official',
icon: 'edit',
provides: ['verstak/default-editor/v1'],
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
permissions: ['files.read', 'files.write', 'workbench.open'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
openProviders: [
{
id: 'verstak.default-editor.text',
title: 'Default Text Editor',
priority: 50,
component: 'DefaultEditor',
supports: [
{ kind: 'vault-file', extensions: ['.txt', '.log', '.conf', '.ini', '.toml', '.yaml', '.yml', '.json', '.csv'], mime: ['text/plain', 'application/json'], contexts: ['generic-text'] }
]
},
{
id: 'verstak.default-editor.markdown',
title: 'Default Markdown Editor',
priority: 50,
component: 'DefaultEditor',
supports: [
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['generic-markdown'] }
]
},
{
id: 'verstak.default-editor.notes-markdown',
title: 'Default Notes Markdown Editor',
priority: 50,
component: 'DefaultEditor',
supports: [
{ kind: 'vault-file', extensions: ['.md', '.markdown'], contexts: ['notes-markdown'] }
]
}
]
}
},
rootPath: '/tmp/verstak-test/plugins/default-editor',
error: ''
},
'verstak.files': {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.files',
name: 'Files',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Minimal vault file navigator.',
source: 'official',
icon: 'folder',
provides: ['verstak/files/v1'],
requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'],
permissions: ['files.read', 'files.write', 'workbench.open', 'ui.register'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
views: [{ id: 'verstak.files.view', title: 'Files', icon: 'folder', component: 'FilesView' }],
workspaceItems: [{ id: 'verstak.files.workspace', title: 'Files', icon: 'folder', component: 'FilesView' }]
}
},
rootPath: '/tmp/verstak-test/plugins/files',
error: ''
}
};
vaultStatus = { status: 'open', path: '/tmp/verstak-test/vault', vaultId: 'test-vault-001' };
vaultPluginState = { enabledPlugins: ['verstak.platform-test'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }] };
vaultPluginState = { enabledPlugins: ['verstak.platform-test', 'verstak.default-editor', 'verstak.files'], disabledPlugins: [], desiredPlugins: [{ id: 'verstak.platform-test', version: '0.1.0', source: 'official' }, { id: 'verstak.default-editor', version: '0.1.0', source: 'official' }, { id: 'verstak.files', version: '0.1.0', source: 'official' }] };
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
workbenchPreferences = {};
openedResources = [];