Implement milestone 6b workbench routing skeleton

This commit is contained in:
2026-06-19 07:51:57 +08:00
parent a100f5a441
commit 6ed6df311a
53 changed files with 7592 additions and 335 deletions
+54
View File
@@ -0,0 +1,54 @@
/**
* Shared helpers for Verstak E2E tests.
*/
import { expect } from '@playwright/test';
/** Wait for the app to finish loading (loading screen disappears) */
export async function waitForAppReady(page) {
// App shows "Loading Verstak..." initially, then renders the main layout
await page.waitForSelector('main', { state: 'visible', timeout: 15000 });
// Wait a bit for all async data to load
await page.waitForTimeout(1000);
}
/** Collect all console errors since last reset */
export function setupConsoleCollector(page) {
const errors = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
page.on('pageerror', (err) => {
errors.push(err.message);
});
return {
getErrors: () => errors,
assertNoErrors: () => {
if (errors.length > 0) {
throw new Error(`Console errors detected:\n${errors.join('\n')}`);
}
},
};
}
/** Reset mock state before each test */
export async function resetMockState(page) {
await page.evaluate(() => {
if (window.__wailsMock) {
window.__wailsMock.reset();
}
});
}
/** Set plugin status in mock */
export async function setPluginStatus(page, pluginId, status, enabled) {
await page.evaluate(
({ id, st, en }) => {
if (window.__wailsMock) {
window.__wailsMock.setPluginStatus(id, st, en);
}
},
{ id: pluginId, st: status, en: enabled }
);
}
+125
View File
@@ -0,0 +1,125 @@
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
test.describe('D: Plugin API bridge', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
consoleCollector = setupConsoleCollector(page);
await resetMockState(page);
await page.goto('/');
await waitForAppReady(page);
});
test.afterEach(async () => {
consoleCollector.assertNoErrors();
});
test('platform-test reads and writes settings through scoped API after reload', async ({ page }) => {
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
const saved = page.locator('.pt-saved-setting');
await expect(saved).toHaveText('Saved setting: initial value', { timeout: 10000 });
const input = page.locator('.pt-setting-input');
await input.fill('persisted through bridge');
await page.locator('.pt-save-setting').click();
await expect(saved).toHaveText('Saved setting: persisted through bridge', { timeout: 10000 });
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0);
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0);
await page.locator('button.reload-btn').click();
await expect(page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).locator('.status-badge')).toHaveText('loaded', { timeout: 10000 });
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
await expect(page.locator('.pt-saved-setting')).toHaveText('Saved setting: persisted through bridge', { timeout: 10000 });
await expect(page.locator('.pt-badge')).toHaveAttribute('data-command-status', 'handled');
await expect(page.locator('.pt-badge')).toContainText('capability available');
await expect(page.locator('.pt-command-result')).toContainText('Command: handled 0.1.0 from bundled-frontend');
await expect(page.locator('.pt-event-result')).toHaveAttribute('data-event-status', 'received');
await expect(page.locator('.pt-event-result')).toContainText('Event: received hello-event');
await expect(page.locator('.pt-files-result')).toHaveAttribute('data-files-status', 'ok');
await expect(page.locator('.pt-files-result')).toContainText('Files: wrote/read/listed/moved/trashed');
await expect(page.locator('.pt-files-error-result')).toHaveAttribute('data-files-error-status', 'expected');
await expect(page.locator('.pt-files-error-result')).toContainText('Files error path: rejected reserved-path');
await page.locator('.pt-open-workbench-notes').click();
await expect(page.locator('.pt-workbench-result')).toHaveAttribute('data-workbench-status', 'ok');
await expect(page.locator('.pt-workbench-result')).toContainText('Workbench: opened Notes/Overview.md with verstak.platform-test.markdown-diagnostic');
await expect(page.locator('.pt-workbench-result')).toHaveAttribute('data-resource-context', 'notes-markdown');
});
test('platform-test diagnostic provider routes text, markdown, and notes markdown contexts', async ({ page }) => {
async function openFromDiagnostics(buttonClass, path, context) {
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
await page.locator(buttonClass).click();
const result = page.locator('.pt-workbench-result');
await expect(result).toHaveAttribute('data-workbench-status', 'ok', { timeout: 10000 });
await expect(result).toHaveAttribute('data-resource-path', path);
await expect(result).toHaveAttribute('data-resource-mode', 'edit');
await expect(result).toHaveAttribute('data-resource-context', context);
await expect(result).toContainText('verstak.platform-test.markdown-diagnostic');
}
await openFromDiagnostics('.pt-open-workbench-text', 'Docs/todo.txt', 'generic-text');
await openFromDiagnostics('.pt-open-workbench-markdown', 'Docs/readme.md', 'generic-markdown');
await openFromDiagnostics('.pt-open-workbench-notes', 'Notes/Overview.md', 'notes-markdown');
});
test('workbench shows no-provider fallback when no provider matches', async ({ page }) => {
await page.evaluate(async () => {
const [result, err] = await window.go.api.App.OpenWorkbenchResource('verstak.platform-test', {
kind: 'vault-file',
path: 'Images/logo.png',
extension: '.png',
context: { sourceView: 'files' },
});
if (err) throw new Error(err);
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
});
await expect(page.locator('[data-workbench-status="no-provider"]')).toBeVisible();
await expect(page.locator('[data-workbench-status="no-provider"]')).toContainText('No viewer/editor available');
});
test('platform-test command and event handlers are cleaned up after leaving plugin view', async ({ page }) => {
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
await expect(page.locator('.pt-event-result')).toHaveAttribute('data-event-status', 'received', { timeout: 10000 });
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(1);
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(1);
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0);
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0);
});
test('platform-test cleanup remains empty after disable reload flow', async ({ page }) => {
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 });
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
await pluginCard.locator('button.btn-disable').click();
await expect(pluginCard.locator('button.btn-enable')).toBeVisible({ timeout: 10000 });
await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0);
await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0);
});
test('platform-test settings panel loads bundle content returned as raw string', async ({ page }) => {
await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click();
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
await pluginCard.locator('button.btn-settings').click();
const modal = page.locator('.modal[aria-label="Plugin Settings"]');
await expect(modal).toBeVisible();
await expect(modal).toContainText('Platform Test Settings');
await expect(modal.locator('.host-state.error')).toHaveCount(0);
});
});
@@ -0,0 +1,121 @@
/**
* Acceptance Test A: Plugin Manager Disable/Enable refresh
*
* Scenario:
* 1. Open Plugin Manager
* 2. See Platform Test as loaded/enabled
* 3. Click Disable
* 4. Verify Enable button appears
* 5. Verify plugin sidebar item disappears
* 6. Click Enable
* 7. Verify Disable button appears
* 8. Verify plugin sidebar item returns
*/
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
test.describe('A: Plugin Manager Disable/Enable refresh', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
consoleCollector = setupConsoleCollector(page);
await resetMockState(page);
await page.goto('/');
await waitForAppReady(page);
});
test.afterEach(async () => {
consoleCollector.assertNoErrors();
});
test('Platform Test plugin is initially visible and enabled', async ({ page }) => {
// Plugin Manager should show Platform Test plugin card
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
await expect(pluginCard).toBeVisible();
// Status should show "loaded"
const statusBadge = pluginCard.locator('.status-badge');
await expect(statusBadge).toHaveText('loaded');
// Disable button should be visible (not Enable)
const disableBtn = pluginCard.locator('button.btn-disable');
await expect(disableBtn).toBeVisible();
// Sidebar should have Platform Test item
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).toBeVisible();
});
test('Disable plugin: button changes to Enable, sidebar item disappears', async ({ page }) => {
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
// Click Disable
const disableBtn = pluginCard.locator('button.btn-disable');
await expect(disableBtn).toBeVisible();
await disableBtn.click();
// Wait for UI to update after disable
await page.waitForTimeout(500);
// After disable: Enable button should appear
const enableBtn = pluginCard.locator('button.btn-enable');
await expect(enableBtn).toBeVisible({ timeout: 10000 });
// After disable: sidebar item for this plugin should disappear
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).not.toBeVisible();
// Status should show "disabled"
const statusBadge = pluginCard.locator('.status-badge');
await expect(statusBadge).toHaveText('disabled');
});
test('Re-enable plugin: button changes to Disable, sidebar item returns', async ({ page }) => {
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
// First disable
await pluginCard.locator('button.btn-disable').click();
await page.waitForTimeout(500);
// Wait for Enable button
const enableBtn = pluginCard.locator('button.btn-enable');
await expect(enableBtn).toBeVisible({ timeout: 10000 });
// Click Enable
await enableBtn.click();
await page.waitForTimeout(500);
// After re-enable: Disable button should appear
const disableBtn = pluginCard.locator('button.btn-disable');
await expect(disableBtn).toBeVisible({ timeout: 10000 });
// Sidebar item should return
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).toBeVisible();
// Status should show "loaded"
const statusBadge = pluginCard.locator('.status-badge');
await expect(statusBadge).toHaveText('loaded');
});
test('Disable → Enable full flow in sequence', async ({ page }) => {
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
const sidebar = page.locator('.sidebar');
// Initial state: enabled
await expect(pluginCard.locator('button.btn-disable')).toBeVisible();
await expect(sidebar.locator('.plugin-item').filter({ hasText: 'Platform Test' })).toBeVisible();
// Disable
await pluginCard.locator('button.btn-disable').click();
await page.waitForTimeout(500);
await expect(pluginCard.locator('button.btn-enable')).toBeVisible({ timeout: 10000 });
await expect(sidebar.locator('.plugin-item').filter({ hasText: 'Platform Test' })).not.toBeVisible();
// Enable
await pluginCard.locator('button.btn-enable').click();
await page.waitForTimeout(500);
await expect(pluginCard.locator('button.btn-disable')).toBeVisible({ timeout: 10000 });
await expect(sidebar.locator('.plugin-item').filter({ hasText: 'Platform Test' })).toBeVisible();
});
});
@@ -0,0 +1,84 @@
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
test.describe('E: Plugin Manager layout', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
consoleCollector = setupConsoleCollector(page);
await resetMockState(page);
await page.goto('/');
await waitForAppReady(page);
});
test.afterEach(async () => {
consoleCollector.assertNoErrors();
});
test('plugin list scrolls through the global main scroll surface and stays responsive', async ({ page }) => {
await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(18));
await page.locator('button.reload-btn').click();
await expect(page.locator('.plugin-card')).toHaveCount(19, { timeout: 10000 });
const manager = page.locator('.plugin-manager');
const scrollSurface = page.locator('.content.scroll-surface');
await expect(manager).toBeVisible();
await expect(scrollSurface).toBeVisible();
const desktopMetrics = await scrollSurface.evaluate((node) => ({
clientHeight: node.clientHeight,
scrollHeight: node.scrollHeight,
overflowY: getComputedStyle(node).overflowY,
}));
expect(desktopMetrics.overflowY).toBe('auto');
expect(desktopMetrics.scrollHeight).toBeGreaterThan(desktopMetrics.clientHeight);
const scrolledTop = await scrollSurface.evaluate((node) => {
node.scrollTop = node.scrollHeight;
return node.scrollTop;
});
expect(scrolledTop).toBeGreaterThan(0);
await page.setViewportSize({ width: 720, height: 640 });
await expect(manager).toBeVisible();
const narrowMetrics = await scrollSurface.evaluate((node) => ({
clientWidth: node.clientWidth,
scrollWidth: node.scrollWidth,
scrollTop: node.scrollTop,
}));
expect(narrowMetrics.scrollWidth).toBeLessThanOrEqual(narrowMetrics.clientWidth + 1);
expect(narrowMetrics.scrollTop).toBeGreaterThan(0);
});
test('platform-test buttons use the global button contract', async ({ page }) => {
await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click();
const saveButton = page.locator('.pt-save-setting');
await expect(saveButton).toBeVisible({ timeout: 10000 });
await expect(saveButton).toHaveClass(/btn-primary/);
const buttonStyle = await saveButton.evaluate((node) => {
const style = getComputedStyle(node);
return {
display: style.display,
backgroundColor: style.backgroundColor,
borderRadius: style.borderRadius,
};
});
expect(buttonStyle.display).toBe('inline-flex');
expect(buttonStyle.backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
expect(buttonStyle.borderRadius).toBe('6px');
});
test('workspace selection keeps exactly one active node', async ({ page }) => {
const selected = page.locator('.wt-node.selected .wt-label');
await expect(selected).toHaveCount(1);
await expect(selected).toHaveText('Alpha Case');
await page.locator('.wt-label').filter({ hasText: 'Beta Case' }).click();
await expect(selected).toHaveCount(1);
await expect(selected).toHaveText('Beta Case');
});
});
+101
View File
@@ -0,0 +1,101 @@
/**
* Acceptance Test C: Reload updates UI state
*
* Scenario:
* 1. Change mocked plugin state (e.g. disable a plugin in mock)
* 2. Click Reload button
* 3. Verify UI reflects the updated state
*/
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState, setPluginStatus } from './helpers.js';
test.describe('C: Reload updates UI state', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
consoleCollector = setupConsoleCollector(page);
await resetMockState(page);
await page.goto('/');
await waitForAppReady(page);
});
test.afterEach(async () => {
consoleCollector.assertNoErrors();
});
test('Reload after mock state change reflects new plugin status', async ({ page }) => {
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
// Initial state: loaded/enabled
await expect(pluginCard.locator('.status-badge')).toHaveText('loaded');
await expect(pluginCard.locator('button.btn-disable')).toBeVisible();
// Change mock state to disabled (simulating external state change)
await setPluginStatus(page, 'verstak.platform-test', 'disabled', false);
await page.waitForTimeout(200);
// Click Reload button in Plugin Manager header
const reloadBtn = page.locator('button.reload-btn');
await expect(reloadBtn).toBeVisible();
await reloadBtn.click();
// Wait for reload to complete and UI to update
await page.waitForTimeout(1000);
// After reload: status should reflect the disabled state
await expect(pluginCard.locator('.status-badge')).toHaveText('disabled', { timeout: 10000 });
// Enable button should appear (since plugin is now disabled)
await expect(pluginCard.locator('button.btn-enable')).toBeVisible({ timeout: 10000 });
// Sidebar item should be gone (disabled plugins are filtered from sidebar)
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).not.toBeVisible();
});
test('Reload restores plugin after re-enabling in mock', async ({ page }) => {
const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' });
// Disable in mock, reload
await setPluginStatus(page, 'verstak.platform-test', 'disabled', false);
await page.waitForTimeout(200);
await page.locator('button.reload-btn').click();
await page.waitForTimeout(1000);
// Verify disabled
await expect(pluginCard.locator('.status-badge')).toHaveText('disabled', { timeout: 10000 });
await expect(pluginCard.locator('button.btn-enable')).toBeVisible();
// Re-enable in mock
await setPluginStatus(page, 'verstak.platform-test', 'loaded', true);
await page.waitForTimeout(200);
// Reload again
await page.locator('button.reload-btn').click();
await page.waitForTimeout(1000);
// After reload: should be loaded again
await expect(pluginCard.locator('.status-badge')).toHaveText('loaded', { timeout: 10000 });
await expect(pluginCard.locator('button.btn-disable')).toBeVisible();
// Sidebar item should return
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).toBeVisible();
});
test('Reload button is not disabled during normal operation', async ({ page }) => {
const reloadBtn = page.locator('button.reload-btn');
await expect(reloadBtn).toBeVisible();
await expect(reloadBtn).not.toBeDisabled();
});
test('Reload handles raw Wails count result without falling into error state', async ({ page }) => {
await page.evaluate(() => window.__wailsMock.setReloadResponseMode('raw-count'));
const reloadBtn = page.locator('button.reload-btn');
await reloadBtn.click();
await expect(page.locator('.error-state')).toHaveCount(0);
await expect(page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' })).toBeVisible({ timeout: 10000 });
});
});
+75
View File
@@ -0,0 +1,75 @@
/**
* Acceptance Test B: Sidebar opens plugin view by item.view, not item.id
*
* Data:
* - sidebar item id = verstak.platform-test.sidebar
* - sidebar item view = verstak.platform-test.diagnostics
*
* Scenario:
* 1. Click sidebar item "Platform Test"
* 2. Verify diagnostics view is opened (verstak.platform-test.diagnostics)
* 3. Verify NOT opened empty container by sidebar id
*/
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
test.describe('B: Sidebar opens plugin view by item.view', () => {
let consoleCollector;
test.beforeEach(async ({ page }) => {
consoleCollector = setupConsoleCollector(page);
await resetMockState(page);
await page.goto('/');
await waitForAppReady(page);
});
test.afterEach(async () => {
consoleCollector.assertNoErrors();
});
test('Sidebar item exists with correct label', async ({ page }) => {
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).toBeVisible();
});
test('Click sidebar item opens diagnostics view by view ID, not sidebar ID', async ({ page }) => {
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await expect(sidebarItem).toBeVisible();
// Click the sidebar item
await sidebarItem.click();
await page.waitForTimeout(500);
// View container should be visible
const viewContainer = page.locator('.view-container');
await expect(viewContainer).toBeVisible();
// The view header should show "Platform Diagnostics" (from view contribution title)
// This proves the view was opened by item.view = "verstak.platform-test.diagnostics"
// NOT by item.id = "verstak.platform-test.sidebar"
const viewHeader = viewContainer.locator('.view-header h2');
await expect(viewHeader).toHaveText('Platform Diagnostics', { timeout: 10000 });
// The view should NOT show "View ... not found" error
// (which would happen if it tried to open by sidebar item id)
await expect(viewContainer).not.toHaveText(/not found/);
// The view should NOT show an empty container message
const emptyView = viewContainer.locator('.empty');
await expect(emptyView).not.toBeVisible();
});
test('View header shows correct title from view contribution', async ({ page }) => {
const sidebarItem = page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' });
await sidebarItem.click();
await page.waitForTimeout(500);
// Verify the view title comes from the view contribution (item.view)
// NOT from the sidebar item (item.id)
const viewHeader = page.locator('.view-container .view-header h2');
await expect(viewHeader).toHaveText('Platform Diagnostics', { timeout: 10000 });
// Should NOT show sidebar item id as view title
await expect(viewHeader).not.toHaveText('verstak.platform-test.sidebar');
});
});
+10 -1
View File
@@ -16,6 +16,15 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module">
// In test mode (no Wails runtime), load mock bridge first
if (!window['go'] || !window['go']['api']) {
import('/src/lib/test/wails-mock.js').then(function() {
import('/src/main.js');
});
} else {
import('/src/main.js');
}
</script>
</body>
</html>
+60
View File
@@ -8,6 +8,7 @@
"name": "verstak-desktop-frontend",
"version": "0.1.0",
"devDependencies": {
"@playwright/test": "^1.61.0",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"svelte": "^4.2.0",
"vite": "^5.4.0"
@@ -429,6 +430,21 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@playwright/test": {
"version": "1.61.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz",
"integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==",
"dev": true,
"dependencies": {
"playwright": "1.61.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
@@ -1022,6 +1038,50 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/playwright": {
"version": "1.61.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.61.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+5 -1
View File
@@ -6,9 +6,13 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test:e2e": "npx playwright test --config playwright.config.js",
"test:e2e:ui": "npx playwright test --config playwright.config.js --ui",
"test:e2e:headed": "npx playwright test --config playwright.config.js --headed"
},
"devDependencies": {
"@playwright/test": "^1.61.0",
"@sveltejs/vite-plugin-svelte": "^3.1.0",
"svelte": "^4.2.0",
"vite": "^5.4.0"
+1 -1
View File
@@ -1 +1 @@
ef6849d398e9d32c4ae5afdea904eb49
43be2fbdf6ba6ca9504a7c4b0ac32ae0
+59
View File
@@ -0,0 +1,59 @@
import { defineConfig, devices } from '@playwright/test';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const FRONTEND_PORT = 5174;
const FRONTEND_URL = `http://localhost:${FRONTEND_PORT}`;
const LOOPBACK_NO_PROXY = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = process.env.NO_PROXY
? `${process.env.NO_PROXY},${LOOPBACK_NO_PROXY}`
: LOOPBACK_NO_PROXY;
process.env.no_proxy = process.env.no_proxy
? `${process.env.no_proxy},${LOOPBACK_NO_PROXY}`
: LOOPBACK_NO_PROXY;
export default defineConfig({
testDir: resolve(__dirname, 'e2e'),
testMatch: '**/*.spec.js',
timeout: 30000,
expect: { timeout: 10000 },
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: [
['list'],
['json', { outputFile: resolve(__dirname, 'e2e-results/test-results.json') }],
],
outputDir: resolve(__dirname, 'e2e-results'),
use: {
baseURL: FRONTEND_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
headless: true,
viewport: { width: 1280, height: 720 },
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: `npx vite --mode test --port ${FRONTEND_PORT}`,
url: FRONTEND_URL,
reuseExistingServer: !process.env.CI,
timeout: 60000,
env: {
NO_PROXY: process.env.NO_PROXY,
no_proxy: process.env.no_proxy,
},
stdout: 'pipe',
stderr: 'pipe',
},
});
+167 -4
View File
@@ -3,7 +3,11 @@
import Sidebar from './lib/shell/Sidebar.svelte';
import ViewContainer from './lib/shell/ViewContainer.svelte';
import VaultSelection from './lib/shell/VaultSelection.svelte';
import WorkbenchHost from './lib/shell/WorkbenchHost.svelte';
import * as App from '../wailsjs/go/api/App';
import { debug } from './lib/log/debug.js';
import { onMount } from 'svelte';
import { tick } from 'svelte';
let currentView = 'plugin-manager';
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
@@ -14,47 +18,81 @@
let activeViewPluginId = '';
let activeSettingsPluginId = '';
let activeSettingsPanelId = '';
let openedResource = null;
function flog(msg) {
App.WriteFrontendLog('App', msg);
}
async function checkVault() {
debug.log('[App] checkVault: START');
flog('checkVault: START');
loading = true;
try {
debug.log('[App] checkVault: calling GetAppSettings...');
const settings = await App.GetAppSettings();
debug.log('[App] checkVault: GetAppSettings returned', settings);
flog('checkVault: GetAppSettings returned');
debug.log('[App] checkVault: calling GetVaultStatus...');
vaultStatus = await App.GetVaultStatus() || { status: 'unknown', path: '', vaultId: '' };
debug.log('[App] checkVault: GetVaultStatus returned', vaultStatus);
flog('checkVault: vaultStatus=' + vaultStatus.status);
if (!settings.currentVaultPath || vaultStatus.status !== 'open') {
debug.log('[App] checkVault: vault not open, needsVaultSelection=true');
flog('checkVault: needsVaultSelection=true');
needsVaultSelection = true;
} else {
debug.log('[App] checkVault: vault open, needsVaultSelection=false');
flog('checkVault: needsVaultSelection=false');
needsVaultSelection = false;
}
} catch (e) {
debug.log('[App] checkVault: ERROR', String(e));
flog('checkVault: ERROR: ' + String(e));
console.error('[App] startup check failed:', e);
needsVaultSelection = true;
}
loading = false;
await tick();
debug.log('[App] checkVault: END, loading=false');
flog('checkVault: END, loading=false');
}
function onVaultOpened() {
debug.log('[App] onVaultOpened');
needsVaultSelection = false;
vaultStatus = { status: 'open', path: '', vaultId: '' };
}
function onNav(e) {
debug.log('[App] onNav:', e.detail.viewId);
currentView = e.detail.viewId;
}
function onOpenView(e) {
debug.log('[App] onOpenView:', e.detail.viewId, 'plugin:', e.detail.pluginId);
activeView = e.detail.viewId;
activeViewPluginId = e.detail.pluginId || '';
currentView = 'plugin-view';
}
function onOpenSettings(e) {
debug.log('[App] onOpenSettings:', e.detail.pluginId, e.detail.panelId);
activeSettingsPluginId = e.detail.pluginId;
activeSettingsPanelId = e.detail.panelId || '';
currentView = 'plugin-manager';
}
function onWorkbenchOpened(e) {
debug.log('[App] onWorkbenchOpened:', e.detail?.request?.path, e.detail?.providerId);
openedResource = e.detail;
currentView = 'workbench';
}
function onCloseSettings() {
debug.log('[App] onCloseSettings');
activeSettingsPluginId = '';
activeSettingsPanelId = '';
}
@@ -66,9 +104,10 @@
window.addEventListener('verstak:open-view', onOpenView);
window.addEventListener('verstak:open-settings', onOpenSettings);
window.addEventListener('verstak:close-settings', onCloseSettings);
window.addEventListener('verstak:workbench-opened', onWorkbenchOpened);
}
checkVault();
onMount(() => { checkVault(); });
</script>
{#if loading}
@@ -81,9 +120,11 @@
<main>
<Sidebar />
<section class="content">
<section class="content scroll-surface">
{#if currentView === 'plugin-manager'}
<PluginManager {activeSettingsPluginId} {activeSettingsPanelId} />
{:else if currentView === 'workbench'}
<WorkbenchHost {openedResource} />
{:else}
<ViewContainer {activeView} {activeViewPluginId} />
{/if}
@@ -98,6 +139,13 @@
box-sizing: border-box;
}
:global(html),
:global(body),
:global(#app) {
width: 100%;
height: 100%;
}
:global(body) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a1a2e;
@@ -105,6 +153,118 @@
overflow: hidden;
}
:global(button) {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
min-height: 2rem;
padding: 0.4rem 0.85rem;
border: 1px solid #1a3a5c;
border-radius: 6px;
background: #0f3460;
color: #e0e0f0;
font: inherit;
font-size: 0.85rem;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
}
:global(button:hover:not(:disabled)) {
background: #1a3a5c;
border-color: #4ecca3;
color: #ffffff;
}
:global(button:focus-visible) {
outline: 2px solid #4ecca3;
outline-offset: 2px;
}
:global(button:disabled) {
opacity: 0.55;
cursor: not-allowed;
}
:global(.btn-primary) {
background: #4ecca3;
border-color: #4ecca3;
color: #101827;
}
:global(.btn-primary:hover:not(:disabled)) {
background: #63d9b3;
border-color: #63d9b3;
color: #101827;
}
:global(.btn-secondary) {
background: #0f3460;
border-color: #533483;
color: #e0e0f0;
}
:global(.btn-danger) {
background: #e94560;
border-color: #e94560;
color: #ffffff;
}
:global(.btn-danger:hover:not(:disabled)) {
background: #ff5b73;
border-color: #ff5b73;
}
:global(.btn-ghost) {
background: transparent;
border-color: transparent;
color: #a0a0b8;
}
:global(.btn-ghost:hover:not(:disabled)) {
background: rgba(15, 52, 96, 0.55);
border-color: #0f3460;
color: #e0e0f0;
}
:global(.btn-icon) {
width: 2rem;
min-width: 2rem;
padding: 0;
}
:global(.scroll-surface) {
min-width: 0;
min-height: 0;
overflow: auto;
scrollbar-gutter: stable;
}
:global(*) {
scrollbar-width: thin;
scrollbar-color: #0f3460 #1a1a2e;
}
:global(*::-webkit-scrollbar) {
width: 8px;
height: 8px;
}
:global(*::-webkit-scrollbar-track) {
background: #1a1a2e;
}
:global(*::-webkit-scrollbar-thumb) {
background: #0f3460;
border-radius: 4px;
}
:global(*::-webkit-scrollbar-thumb:hover) {
background: #1a4a7a;
}
.app-loading {
display: flex;
align-items: center;
@@ -118,14 +278,17 @@
main {
display: flex;
height: 100vh;
width: 100%;
background: #1a1a2e;
overflow: hidden;
}
.content {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 1.5rem;
padding: clamp(1rem, 2vw, 1.5rem);
}
</style>
+150
View File
@@ -0,0 +1,150 @@
/**
* Frontend debug logger.
*
* Enabled when:
* 1. URL has ?debug query param, OR
* 2. localStorage has verstak-debug = "true"
*
* Writes to:
* - console (always, with [debug] prefix)
* - localStorage buffer (last 1000 entries, key: verstak-debug-log)
*
* Usage:
* import { debug } from '../log/debug.js';
* debug.log('[ComponentName]', 'message', data);
* debug.logf('[ComponentName]', 'format %s', arg);
*
* To enable: open app with ?debug or run in console:
* localStorage.setItem('verstak-debug', 'true')
*
* To export log: run in console:
* copy(JSON.parse(localStorage.getItem('verstak-debug-log')))
*/
var ENABLED = false;
var BUFFER_KEY = 'verstak-debug-log';
var MAX_ENTRIES = 1000;
// Check enable conditions
function checkEnabled() {
try {
if (window.location && window.location.search && window.location.search.indexOf('debug') !== -1) {
return true;
}
if (typeof localStorage !== 'undefined' && localStorage.getItem('verstak-debug') === 'true') {
return true;
}
} catch (e) {
// localStorage not available
}
return false;
}
ENABLED = checkEnabled();
function getTimestamp() {
return new Date().toISOString();
}
function formatMessage(args) {
var parts = [];
for (var i = 0; i < args.length; i++) {
var a = args[i];
if (typeof a === 'object') {
try {
parts.push(JSON.stringify(a));
} catch (e) {
parts.push(String(a));
}
} else {
parts.push(String(a));
}
}
return parts.join(' ');
}
function writeToBuffer(entry) {
try {
if (typeof localStorage === 'undefined') return;
var raw = localStorage.getItem(BUFFER_KEY);
var log = raw ? JSON.parse(raw) : [];
log.push(entry);
if (log.length > MAX_ENTRIES) {
log = log.slice(log.length - MAX_ENTRIES);
}
localStorage.setItem(BUFFER_KEY, JSON.stringify(log));
} catch (e) {
// Ignore quota errors
}
}
function log() {
if (!ENABLED) return;
var msg = formatMessage(Array.prototype.slice.call(arguments));
var entry = { ts: getTimestamp(), msg: msg };
writeToBuffer(entry);
console.log('[debug]', msg);
}
function logf() {
if (!ENABLED) return;
var args = Array.prototype.slice.call(arguments);
var format = args.shift();
var i = 0;
var msg = format.replace(/%[sdfo]/g, function () {
return i < args.length ? String(args[i++]) : '';
});
var entry = { ts: getTimestamp(), msg: msg };
writeToBuffer(entry);
console.log('[debug]', msg);
}
function getLog() {
try {
if (typeof localStorage === 'undefined') return [];
var raw = localStorage.getItem(BUFFER_KEY);
return raw ? JSON.parse(raw) : [];
} catch (e) {
return [];
}
}
function clearLog() {
try {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(BUFFER_KEY);
}
} catch (e) {}
}
function exportLog() {
var entries = getLog();
return entries.map(function (e) { return e.ts + ' ' + e.msg; }).join('\n');
}
// Named export for Svelte/Vite
export var debug = {
log: log,
logf: logf,
isEnabled: function () { return ENABLED; },
enable: function () {
ENABLED = true;
try { localStorage.setItem('verstak-debug', 'true'); } catch (e) {}
},
disable: function () {
ENABLED = false;
try { localStorage.removeItem('verstak-debug'); } catch (e) {}
},
getLog: getLog,
clearLog: clearLog,
exportLog: exportLog
};
// Also expose globally for console access
if (typeof window !== 'undefined') {
window.__verstakDebug = debug;
}
if (ENABLED) {
console.log('[debug] frontend debug logger enabled');
}
@@ -3,12 +3,12 @@
import * as App from '../../../wailsjs/go/api/App';
import Icon from '../ui/Icon.svelte';
// Import the VerstakPluginAPI contract
import './VerstakPluginAPI.js';
import { createPluginAPI } from './VerstakPluginAPI.js';
export let pluginId = null;
export let componentId = null;
export let viewPluginId = null;
export let componentProps = {};
let loadState = 'idle'; // idle | loading | loaded | error
let pluginInfo = null;
@@ -16,6 +16,7 @@
let mountContainer = null;
let currentPluginId = null;
let currentComponent = null;
let currentAPI = null;
$: activePluginId = pluginId || viewPluginId;
$: activeComponent = componentId;
@@ -33,6 +34,13 @@
});
function cleanup() {
if (currentAPI && typeof currentAPI.dispose === 'function') {
try {
currentAPI.dispose();
} catch (e) {
console.error('[PluginBundleHost] API dispose error:', e);
}
}
const reg = window.__VERSTAK_PLUGIN_REGISTRY__;
if (currentPluginId && currentComponent && reg && reg[currentPluginId]) {
const comp = reg[currentPluginId][currentComponent];
@@ -49,6 +57,14 @@
}
currentPluginId = null;
currentComponent = null;
currentAPI = null;
}
function unpackBackendResult(result) {
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
return { value: result[0], error: result[1] || '' };
}
return { value: result, error: '' };
}
async function loadAndMount(pId, compId) {
@@ -82,10 +98,11 @@
const reg = window.__VERSTAK_PLUGIN_REGISTRY__ || {};
if (!reg[pId]) {
// Load the bundle JS content via backend API
const [content, err] = await App.GetPluginAssetContent(pId, info.entry);
if (err || !content) {
const assetResult = unpackBackendResult(await App.GetPluginAssetContent(pId, info.entry));
const content = assetResult.value;
if (assetResult.error || !content) {
loadState = 'error';
errorText = 'Failed to load bundle: ' + (err || 'empty content');
errorText = 'Failed to load bundle: ' + (assetResult.error || 'empty content');
return;
}
@@ -120,7 +137,8 @@
}
// Create API
const api = window.VerstakPluginAPI(pId);
const api = createPluginAPI(pId);
currentAPI = api;
// Mount component
if (!mountContainer) {
@@ -129,7 +147,7 @@
}
if (mountContainer) {
try {
comp.mount(mountContainer, { componentId: compId }, api);
comp.mount(mountContainer, Object.assign({ componentId: compId }, componentProps || {}), api);
loadState = 'loaded';
errorText = '';
} catch (e) {
@@ -161,12 +179,6 @@
<p>Select a plugin view from the sidebar</p>
</div>
{:else if loadState === 'loading'}
<div class="host-state loading">
<div class="spinner"></div>
<p>Loading plugin bundle...</p>
</div>
{:else if loadState === 'error'}
<div class="host-state error">
<Icon name="warning" size={24} className="error-icon" />
@@ -184,9 +196,16 @@
</div>
</div>
{:else if loadState === 'loaded'}
{:else}
{#if loadState === 'loading'}
<div class="host-state loading">
<div class="spinner"></div>
<p>Loading plugin bundle...</p>
</div>
{/if}
<div
class="plugin-mount-container"
class:mount-hidden={loadState !== 'loaded'}
bind:this={mountContainer}
data-plugin-id={currentPluginId}
data-component={currentComponent}
@@ -197,10 +216,10 @@
<style>
.plugin-bundle-host {
width: 100%;
height: 100%;
min-height: 200px;
display: flex;
flex-direction: column;
min-width: 0;
}
.host-state {
@@ -286,8 +305,14 @@
}
.plugin-mount-container {
flex: 1;
overflow: auto;
min-width: 0;
position: relative;
}
.plugin-mount-container.mount-hidden {
height: 0;
min-height: 0;
overflow: hidden;
visibility: hidden;
}
</style>
+283 -48
View File
@@ -1,18 +1,10 @@
// VerstakPluginAPI is the restricted API passed to plugin frontend bundles.
// Plugins do NOT get direct access to Wails bridge — only what's exposed here.
// All methods are stubs or limited implementations.
import * as App from '../../../wailsjs/go/api/App';
(function() {
// Store registered components per plugin
window.__VERSTAK_PLUGIN_REGISTRY__ = window.__VERSTAK_PLUGIN_REGISTRY__ || {};
// Original register function
const origRegister = window.VerstakPluginRegister;
if (origRegister) {
// Already defined — don't override
return;
}
window.__VERSTAK_PLUGIN_REGISTRY__ = window.__VERSTAK_PLUGIN_REGISTRY__ || {};
window.__VERSTAK_EVENT_HANDLERS__ = window.__VERSTAK_EVENT_HANDLERS__ || {};
window.__VERSTAK_COMMAND_HANDLERS__ = window.__VERSTAK_COMMAND_HANDLERS__ || {};
if (!window.VerstakPluginRegister) {
window.VerstakPluginRegister = function(pluginId, bundle) {
if (!pluginId || !bundle || !bundle.components) {
console.error('[VerstakPluginRegister] invalid registration:', pluginId);
@@ -21,48 +13,291 @@
console.log('[VerstakPluginRegister] registered:', pluginId, Object.keys(bundle.components));
window.__VERSTAK_PLUGIN_REGISTRY__[pluginId] = bundle.components;
};
}
// Create the restricted API object for a plugin host context
window.VerstakPluginAPI = function(pluginId) {
return {
pluginId: pluginId,
function unpack(result) {
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
return [result[0], result[1] || ''];
}
return [result, ''];
}
capabilities: {
has: function(capId) {
// planned: query backend cap registry
console.log('[plugin:' + pluginId + '] capabilities.has(' + capId + ') — stub');
return false;
}
async function callBackend(pluginId, label, fn) {
try {
const [value, err] = unpack(await fn());
if (err) {
throw new Error(err);
}
return value;
} catch (e) {
const message = e && e.message ? e.message : String(e);
throw new Error('[plugin:' + pluginId + '] ' + label + ' failed: ' + message);
}
}
async function callBackendErrorString(pluginId, label, fn) {
try {
const err = await fn();
if (err) {
throw new Error(err);
}
} catch (e) {
const message = e && e.message ? e.message : String(e);
throw new Error('[plugin:' + pluginId + '] ' + label + ' failed: ' + message);
}
}
function getEventHandlers(eventName) {
if (!window.__VERSTAK_EVENT_HANDLERS__[eventName]) {
window.__VERSTAK_EVENT_HANDLERS__[eventName] = [];
}
return window.__VERSTAK_EVENT_HANDLERS__[eventName];
}
function dispatchLocalEvent(pluginId, eventName, payload) {
const event = {
name: eventName,
pluginId: pluginId,
payload: payload || {},
timestamp: new Date().toISOString()
};
const handlers = getEventHandlers(eventName).slice();
handlers.forEach(function(handler) {
try {
handler(event);
} catch (e) {
console.error('[VerstakPluginAPI] event handler error:', e);
}
});
}
function commandKey(pluginId, commandId) {
return pluginId + ':' + commandId;
}
export function createPluginAPI(pluginId) {
if (!pluginId) {
throw new Error('createPluginAPI requires pluginId');
}
const cleanups = [];
let disposed = false;
function assertActive(label) {
if (disposed) {
throw new Error('[plugin:' + pluginId + '] ' + label + ' failed: API disposed');
}
}
function trackCleanup(fn) {
cleanups.push(fn);
return function untrackAndRun() {
const idx = cleanups.indexOf(fn);
if (idx !== -1) {
cleanups.splice(idx, 1);
}
fn();
};
}
return {
pluginId: pluginId,
capabilities: {
has: async function(capId) {
const info = await callBackend(pluginId, 'capabilities.has(' + capId + ')', function() {
return App.GetPluginCapability(pluginId, capId);
});
return !!(info && info.available);
},
events: {
publish: function(type, payload) {
console.log('[plugin:' + pluginId + '] event publish:', type, payload);
// planned: actual event bus bridge
},
subscribe: function(type, handler) {
console.log('[plugin:' + pluginId + '] event subscribe:', type, '(stub)');
// planned: actual event bus bridge
}
get: function(capId) {
return callBackend(pluginId, 'capabilities.get(' + capId + ')', function() {
return App.GetPluginCapability(pluginId, capId);
});
},
list: function() {
return callBackend(pluginId, 'capabilities.list', function() {
return App.ListPluginCapabilities(pluginId);
});
}
},
settings: {
read: function(key) {
console.log('[plugin:' + pluginId + '] settings.read(' + key + ') — stub');
return null;
},
write: function(key, value) {
console.log('[plugin:' + pluginId + '] settings.write(' + key + ',', value, ') — stub');
// planned: backend storage namespace
}
events: {
publish: async function(type, payload) {
await callBackendErrorString(pluginId, 'events.publish(' + type + ')', function() {
return App.PublishPluginEvent(pluginId, type, payload || {});
});
dispatchLocalEvent(pluginId, type, payload || {});
},
subscribe: function(type, handler) {
assertActive('events.subscribe(' + type + ')');
if (typeof handler !== 'function') {
throw new Error('events.subscribe requires a handler function');
}
return callBackendErrorString(pluginId, 'events.subscribe(' + type + ')', function() {
return App.SubscribePluginEvent(pluginId, type);
}).then(function() {
const handlers = getEventHandlers(type);
handlers.push(handler);
return trackCleanup(function unsubscribe() {
const current = getEventHandlers(type);
window.__VERSTAK_EVENT_HANDLERS__[type] = current.filter(function(item) {
return item !== handler;
});
});
});
}
},
commands: {
execute: function(cmdId, args) {
console.log('[plugin:' + pluginId + '] commands.execute(' + cmdId + ') — stub');
// planned: command execution
settings: {
read: async function(key) {
assertActive('settings.read');
const settings = await callBackend(pluginId, 'settings.read', function() {
return App.ReadPluginSettings(pluginId);
});
if (!key) {
return settings || {};
}
return settings ? settings[key] : undefined;
},
write: async function(key, value) {
assertActive('settings.write(' + key + ')');
if (!key) {
throw new Error('settings.write requires a key');
}
const settings = await this.read();
settings[key] = value;
await callBackendErrorString(pluginId, 'settings.write(' + key + ')', function() {
return App.WritePluginSettings(pluginId, settings);
});
return settings;
},
writeAll: function(settings) {
assertActive('settings.writeAll');
return callBackendErrorString(pluginId, 'settings.writeAll', function() {
return App.WritePluginSettings(pluginId, settings || {});
});
}
},
files: {
list: function(relativeDir) {
assertActive('files.list');
return callBackend(pluginId, 'files.list(' + (relativeDir || '') + ')', function() {
return App.ListVaultFiles(pluginId, relativeDir || '');
});
},
metadata: function(relativePath) {
assertActive('files.metadata(' + relativePath + ')');
return callBackend(pluginId, 'files.metadata(' + relativePath + ')', function() {
return App.GetVaultFileMetadata(pluginId, relativePath);
});
},
readText: function(relativePath) {
assertActive('files.readText(' + relativePath + ')');
return callBackend(pluginId, 'files.readText(' + relativePath + ')', function() {
return App.ReadVaultTextFile(pluginId, relativePath);
});
},
writeText: function(relativePath, content, options) {
assertActive('files.writeText(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.writeText(' + relativePath + ')', function() {
return App.WriteVaultTextFile(pluginId, relativePath, String(content == null ? '' : content), options || {});
});
},
createFolder: function(relativePath) {
assertActive('files.createFolder(' + relativePath + ')');
return callBackendErrorString(pluginId, 'files.createFolder(' + relativePath + ')', function() {
return App.CreateVaultFolder(pluginId, relativePath);
});
},
move: function(fromRelativePath, toRelativePath, options) {
assertActive('files.move(' + fromRelativePath + ')');
return callBackendErrorString(pluginId, 'files.move(' + fromRelativePath + ')', function() {
return App.MoveVaultPath(pluginId, fromRelativePath, toRelativePath, options || {});
});
},
trash: function(relativePath) {
assertActive('files.trash(' + relativePath + ')');
return callBackend(pluginId, 'files.trash(' + relativePath + ')', function() {
return App.TrashVaultPath(pluginId, relativePath);
});
}
},
workbench: {
openResource: async function(request) {
assertActive('workbench.openResource');
const result = await callBackend(pluginId, 'workbench.openResource', function() {
return App.OpenWorkbenchResource(pluginId, request || {});
});
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
return result;
},
editResource: async function(request) {
assertActive('workbench.editResource');
const result = await callBackend(pluginId, 'workbench.editResource', function() {
return App.EditWorkbenchResource(pluginId, request || {});
});
window.dispatchEvent(new CustomEvent('verstak:workbench-opened', { detail: result }));
return result;
}
},
commands: {
register: function(cmdId, handler) {
assertActive('commands.register(' + cmdId + ')');
if (!cmdId) {
throw new Error('commands.register requires a command id');
}
if (typeof handler !== 'function') {
throw new Error('commands.register requires a handler function');
}
return callBackend(pluginId, 'commands.register(' + cmdId + ')', function() {
return App.ExecutePluginCommand(pluginId, cmdId, { validateOnly: true });
}).then(function() {
const key = commandKey(pluginId, cmdId);
window.__VERSTAK_COMMAND_HANDLERS__[key] = handler;
return trackCleanup(function unregisterCommand() {
if (window.__VERSTAK_COMMAND_HANDLERS__[key] === handler) {
delete window.__VERSTAK_COMMAND_HANDLERS__[key];
}
});
});
},
execute: async function(cmdId, args) {
assertActive('commands.execute(' + cmdId + ')');
const declared = await callBackend(pluginId, 'commands.execute(' + cmdId + ')', function() {
return App.ExecutePluginCommand(pluginId, cmdId, args || {});
});
const handler = window.__VERSTAK_COMMAND_HANDLERS__[commandKey(pluginId, cmdId)];
if (!handler) {
throw new Error('[plugin:' + pluginId + '] commands.execute(' + cmdId + ') failed: declared-but-unhandled');
}
const result = await handler(args || {}, declared);
return {
status: 'handled',
pluginId: pluginId,
commandId: cmdId,
result: result
};
}
},
dispose: function() {
if (disposed) return;
disposed = true;
while (cleanups.length > 0) {
const cleanup = cleanups.pop();
try {
cleanup();
} catch (e) {
console.error('[VerstakPluginAPI] cleanup error:', e);
}
}
};
}
};
})();
}
window.createPluginAPI = createPluginAPI;
window.VerstakPluginAPI = createPluginAPI;
@@ -211,6 +211,7 @@
border: 1px solid #0f3460;
border-radius: 8px;
padding: 1rem;
min-width: 0;
}
.plugin-card.disabled {
@@ -225,6 +226,8 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
@@ -232,6 +235,12 @@
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
flex-wrap: wrap;
}
.plugin-id strong {
overflow-wrap: anywhere;
}
.status-dot {
@@ -268,7 +277,7 @@
.card-meta {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.3rem;
margin-bottom: 0.75rem;
font-size: 0.8rem;
@@ -277,6 +286,7 @@
.meta-row {
display: flex;
gap: 0.5rem;
min-width: 0;
}
.label {
@@ -288,6 +298,8 @@
font-family: monospace;
font-size: 0.75rem;
color: #a0a0b8;
min-width: 0;
overflow-wrap: anywhere;
}
.section {
@@ -316,6 +328,8 @@
font-size: 0.75rem;
font-family: monospace;
color: #e0e0e0;
max-width: 100%;
overflow-wrap: anywhere;
}
.tag.provides {
@@ -375,6 +389,7 @@
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.75rem;
padding-top: 0.5rem;
border-top: 1px solid #0f3460;
@@ -432,4 +447,19 @@
font-size: 0.75rem;
font-style: italic;
}
@media (max-width: 760px) {
.card-meta {
grid-template-columns: 1fr;
}
.meta-row {
flex-direction: column;
gap: 0.15rem;
}
.label {
min-width: 0;
}
}
</style>
@@ -2,8 +2,9 @@
import Icon from '../ui/Icon.svelte';
import PluginCard from './PluginCard.svelte';
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
import { onMount } from 'svelte';
import { GetPlugins, GetCapabilities, GetPermissions, GetContributions, ReloadPlugins, GetVaultStatus, GetVaultPluginState, EnablePlugin, DisablePlugin, ReadPluginSettings, WritePluginSettings, GetPluginFrontendInfo } from '../../../wailsjs/go/api/App';
import { onMount, tick } from 'svelte';
import { GetPlugins, GetCapabilities, GetPermissions, GetContributions, ReloadPlugins, GetVaultStatus, GetVaultPluginState, EnablePlugin, DisablePlugin, ReadPluginSettings, WritePluginSettings, GetPluginFrontendInfo, WriteFrontendLog } from '../../../wailsjs/go/api/App';
import { debug } from '../log/debug.js';
let plugins = [];
let capabilities = [];
@@ -45,6 +46,28 @@
}, 4000);
}
function notifyPluginsChanged() {
window.dispatchEvent(new CustomEvent('verstak:plugins-changed'));
}
function unpackBackendResult(result) {
if (Array.isArray(result) && result.length === 2 && (typeof result[1] === 'string' || result[1] == null)) {
return { value: result[0], error: result[1] || '' };
}
return { value: result, error: '' };
}
function unpackReloadResult(result) {
if (Array.isArray(result)) {
return {
count: Number(result[0] || 0),
summary: result[1] || `Reloaded ${Number(result[0] || 0)} plugin(s).`,
};
}
const count = Number(result || 0);
return { count, summary: `Reloaded ${count} plugin(s).` };
}
async function openSettingsFromProps(pluginId, panelId) {
const panel = (contributions.settingsPanels || []).find(sp => sp.pluginId === pluginId && (!panelId || sp.id === panelId));
if (panel) {
@@ -55,8 +78,14 @@
const info = await GetPluginFrontendInfo(pluginId);
settingsPluginInfo = info;
} catch { settingsPluginInfo = null; }
ReadPluginSettings(pluginId).then(data => {
settingsData = data || {};
ReadPluginSettings(pluginId).then(result => {
const unpacked = unpackBackendResult(result);
if (unpacked.error) {
settingsError = unpacked.error;
settingsData = {};
return;
}
settingsData = unpacked.value || {};
}).catch(() => { settingsData = {}; });
} else {
settingsError = `Settings panel not found for plugin "${pluginId}". Check that the plugin is enabled and has settingsPanels in its manifest.`;
@@ -73,18 +102,27 @@
}
async function loadAll() {
debug.log('[PluginManager] loadAll: START');
error = '';
loading = true;
try {
debug.log('[PluginManager] loadAll: calling GetPlugins...');
const p = await GetPlugins();
plugins = p || [];
debug.log('[PluginManager] loadAll: GetPlugins returned', plugins.length, 'plugins');
for (var i = 0; i < plugins.length; i++) {
debug.log('[PluginManager] loadAll: plugin[' + i + ']:', plugins[i].manifest?.id, 'status:', plugins[i].status, 'enabled:', plugins[i].enabled);
}
} catch (e) {
debug.log('[PluginManager] loadAll: GetPlugins ERROR:', String(e));
WriteFrontendLog('PluginManager', 'loadAll: GetPlugins ERROR: ' + String(e));
error = 'GetPlugins: ' + String(e);
loading = false;
return;
}
// Collect all async loads but await them so loading stays true until all are done
try {
debug.log('[PluginManager] loadAll: loading vault/capabilities/permissions/contributions...');
const [v, caps, perms, contribs] = await Promise.all([
GetVaultStatus().catch(() => ({ status: 'unknown', path: '', vaultId: '' })),
GetCapabilities().catch(() => []),
@@ -95,66 +133,93 @@
capabilities = caps || [];
permissions = perms || [];
contributions = contribs || {};
debug.log('[PluginManager] loadAll: vault=' + vaultStatus.status + ' caps=' + capabilities.length + ' perms=' + permissions.length);
WriteFrontendLog('PluginManager', 'loadAll: vault=' + vaultStatus.status + ' caps=' + capabilities.length + ' perms=' + permissions.length);
} catch (e) {
// Non-critical — log but don't fail
debug.log('[PluginManager] loadAll: non-critical load ERROR:', String(e));
WriteFrontendLog('PluginManager', 'loadAll: non-critical ERROR: ' + String(e));
console.error('[PluginManager] non-critical load error:', e);
}
if (vaultStatus.status === 'open') {
try {
debug.log('[PluginManager] loadAll: calling GetVaultPluginState...');
vaultPluginState = await GetVaultPluginState() || { enabledPlugins: [], disabledPlugins: [], desiredPlugins: [] };
} catch { /* non-critical */ }
WriteFrontendLog('PluginManager', 'loadAll: GetVaultPluginState returned');
} catch (e) {
WriteFrontendLog('PluginManager', 'loadAll: GetVaultPluginState ERROR: ' + String(e));
}
}
loading = false;
await tick();
debug.log('[PluginManager] loadAll: END, loading=false');
WriteFrontendLog('PluginManager', 'loadAll: END, loading=false');
}
onMount(() => { loadAll(); });
async function reload() {
debug.log('[PluginManager] reload: START');
reloading = true;
error = '';
let resultMsg = '';
try {
const [count, summary] = await ReloadPlugins();
debug.log('[PluginManager] reload: calling ReloadPlugins...');
const { count, summary } = unpackReloadResult(await ReloadPlugins());
debug.log('[PluginManager] reload: ReloadPlugins returned count=' + count + ' summary=' + summary);
resultMsg = `Reloaded ${count} plugin(s). ${summary}`;
} catch (e) {
debug.log('[PluginManager] reload: ReloadPlugins ERROR:', String(e));
error = 'Reload: ' + String(e);
reloading = false;
return;
}
debug.log('[PluginManager] reload: calling loadAll after reload...');
await loadAll();
notifyPluginsChanged();
reloading = false;
debug.log('[PluginManager] reload: END');
showToast(resultMsg, 'success');
}
async function enablePlugin(pluginId) {
debug.log('[PluginManager] enablePlugin:', pluginId);
actionFeedback = { ...actionFeedback, [pluginId]: 'enabling' };
error = '';
const err = await EnablePlugin(pluginId);
if (err) {
debug.log('[PluginManager] enablePlugin: ERROR:', err);
actionFeedback = { ...actionFeedback, [pluginId]: null };
error = 'Enable: ' + err;
return;
}
debug.log('[PluginManager] enablePlugin: success, reloading...');
// Reload to get updated state
try { await ReloadPlugins(); } catch (e) { /* ignore */ }
await loadAll();
notifyPluginsChanged();
actionFeedback = { ...actionFeedback, [pluginId]: null };
debug.log('[PluginManager] enablePlugin: done');
showToast(`Plugin "${pluginId}" enabled`, 'success');
}
async function disablePlugin(pluginId) {
debug.log('[PluginManager] disablePlugin:', pluginId);
actionFeedback = { ...actionFeedback, [pluginId]: 'disabling' };
error = '';
const err = await DisablePlugin(pluginId);
if (err) {
debug.log('[PluginManager] disablePlugin: ERROR:', err);
actionFeedback = { ...actionFeedback, [pluginId]: null };
error = 'Disable: ' + err;
return;
}
debug.log('[PluginManager] disablePlugin: success, reloading...');
// Reload to get updated state
try { await ReloadPlugins(); } catch (e) { /* ignore */ }
await loadAll();
notifyPluginsChanged();
actionFeedback = { ...actionFeedback, [pluginId]: null };
debug.log('[PluginManager] disablePlugin: done');
showToast(`Plugin "${pluginId}" disabled`, 'info');
}
@@ -327,14 +392,18 @@
<style>
.plugin-manager {
max-width: 900px;
padding-top: 0.5rem;
flex: 1;
width: min(100%, 1100px);
min-height: 0;
padding: 0.5rem 0.5rem 1.5rem 0;
position: relative;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #0f3460;
@@ -343,14 +412,20 @@
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
flex-wrap: wrap;
}
h2 { color: #e0e0e0; font-size: 1.3rem; margin: 0; }
.vault-badge {
max-width: 100%;
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 12px;
font-weight: 600;
border: 1px solid;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vault-open { background: rgba(78, 204, 163, 0.15); color: #4ecca3; border-color: #4ecca3; }
.vault-not-created { background: rgba(255, 200, 87, 0.15); color: #ffc857; border-color: #ffc857; }
@@ -404,7 +479,7 @@
.hint-list { list-style: none; padding: 0; margin: 0.5rem 0; font-size: 0.8rem; opacity: 0.7; }
.hint-list li { margin: 0.25rem 0; }
.hint code { background: #0f3460; padding: 0.1rem 0.3rem; border-radius: 3px; }
.plugin-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; }
.plugin-list { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.5rem; min-width: 0; }
.missing-section { margin-bottom: 1.5rem; }
.missing-section h3 { color: #e94560; font-size: 1rem; margin: 0 0 0.25rem; }
@@ -450,4 +525,24 @@
.modal-body { padding: 1rem; overflow-y: auto; }
.settings-hint { color: #666; font-size: 0.8rem; margin: 0.25rem 0; }
.settings-hint code { color: #4ecca3; }
@media (max-width: 760px) {
.plugin-manager {
width: 100%;
padding-right: 0;
}
header {
align-items: flex-start;
}
.reload-btn {
width: 100%;
}
.modal {
width: min(480px, calc(100vw - 2rem));
max-height: calc(100vh - 2rem);
}
}
</style>
+31 -2
View File
@@ -1,8 +1,13 @@
<script>
import { onMount } from 'svelte';
import { onDestroy, onMount } from 'svelte';
import * as App from '../../../wailsjs/go/api/App';
import WorkspaceTree from './WorkspaceTree.svelte';
import Icon from '../ui/Icon.svelte';
import { debug } from '../log/debug.js';
function flog(msg) {
App.WriteFrontendLog('Sidebar', msg);
}
let plugins = [];
let vaultStatus = { status: 'unknown', path: '', vaultId: '' };
@@ -15,9 +20,13 @@
$: vaultOpen = vaultStatus.status === 'open';
onMount(async () => {
async function loadSidebar() {
debug.log('[Sidebar] onMount: START');
flog('onMount: START');
let contribErr = false;
try {
debug.log('[Sidebar] onMount: loading plugins/vault/contributions...');
flog('onMount: loading plugins/vault/contributions...');
const [p, v, contribs] = await Promise.all([
App.GetPlugins().catch(() => []),
App.GetVaultStatus().catch(() => ({ status: 'unknown', path: '', vaultId: '' })),
@@ -25,6 +34,8 @@
]);
plugins = p || [];
vaultStatus = v;
debug.log('[Sidebar] onMount: plugins=' + plugins.length + ' vault=' + vaultStatus.status);
flog('onMount: plugins=' + plugins.length + ' vault=' + vaultStatus.status);
if (contribErr) {
errorMessage = 'Failed to load plugin contributions';
}
@@ -34,17 +45,34 @@
return plugin.status !== 'disabled' && plugin.status !== 'failed' && plugin.status !== 'incompatible' && plugin.status !== 'missing-required-capability';
});
sidebarItems.sort((a, b) => (a.position || 100) - (b.position || 100));
debug.log('[Sidebar] onMount: sidebarItems=' + sidebarItems.length);
flog('onMount: sidebarItems=' + sidebarItems.length);
} catch (e) {
debug.log('[Sidebar] onMount: ERROR:', String(e));
flog('onMount: ERROR: ' + String(e));
console.error('[Sidebar] load error:', e);
errorMessage = 'Failed to load sidebar';
}
debug.log('[Sidebar] onMount: END');
flog('onMount: END');
}
onMount(() => {
loadSidebar();
window.addEventListener('verstak:plugins-changed', loadSidebar);
});
onDestroy(() => {
window.removeEventListener('verstak:plugins-changed', loadSidebar);
});
function handleNav(id) {
debug.log('[Sidebar] handleNav:', id);
window.dispatchEvent(new CustomEvent('verstak:nav', { detail: { viewId: id } }));
}
function handleSidebarItem(item) {
debug.log('[Sidebar] handleSidebarItem:', item.id, '-> view:', item.view);
// Use item.view (the view contribution ID) if available, fall back to item.id
const viewId = item.view || item.id;
window.dispatchEvent(new CustomEvent('verstak:open-view', { detail: { viewId, pluginId: item.pluginId } }));
@@ -171,6 +199,7 @@
.nav-item {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.6rem;
padding: 0.45rem 0.75rem;
background: none;
+3 -2
View File
@@ -101,13 +101,14 @@
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
background: #1a1a2e;
}
.view {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
padding: 1.5rem;
}
.view.degraded {
@@ -155,7 +156,7 @@
}
.view-content {
flex: 1;
overflow: auto;
min-width: 0;
}
.placeholder {
color: #666;
+114
View File
@@ -0,0 +1,114 @@
<script>
import PluginBundleHost from '../plugin-host/PluginBundleHost.svelte';
export let openedResource = null;
$: providerPluginId = openedResource?.providerPluginId || '';
$: providerComponent = openedResource?.providerComponent || '';
$: resourcePath = openedResource?.request?.path || '';
$: providerId = openedResource?.providerId || '';
$: requestMode = openedResource?.request?.mode || 'view';
$: requestContext = openedResource?.request?.context?.notesMode || openedResource?.request?.context?.isInsideNotesFolder
? 'notes-markdown'
: ((openedResource?.request?.extension === '.md' || openedResource?.request?.extension === '.markdown') ? 'generic-markdown' : 'generic-text');
$: componentProps = openedResource || {};
$: mountKey = [
providerPluginId,
providerComponent,
resourcePath,
requestMode,
requestContext,
].join(':');
</script>
<div class="workbench-host">
{#if openedResource?.status === 'no-provider'}
<div class="workbench-header">
<span class="workbench-title">{resourcePath}</span>
<span class="workbench-provider">no-provider</span>
</div>
<div class="workbench-empty no-provider" data-workbench-status="no-provider">
<p>No viewer/editor available</p>
<p class="workbench-meta">{requestMode} · {requestContext}</p>
</div>
{:else if openedResource}
<div class="workbench-header">
<span class="workbench-title">{resourcePath}</span>
<span class="workbench-provider">{providerId}</span>
</div>
<div class="workbench-content">
{#key mountKey}
<PluginBundleHost
pluginId={providerPluginId}
componentId={providerComponent}
{componentProps}
/>
{/key}
</div>
{:else}
<div class="workbench-empty">
<p>No resource opened</p>
</div>
{/if}
</div>
<style>
.workbench-host {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
background: #1a1a2e;
}
.workbench-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid #16213e;
flex-shrink: 0;
}
.workbench-title {
color: #e0e0f0;
font-size: 0.95rem;
font-weight: 600;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.workbench-provider {
color: #4ecca3;
font-size: 0.75rem;
margin-left: auto;
}
.workbench-content {
min-width: 0;
min-height: 0;
flex: 1;
padding: 1rem;
}
.workbench-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #666;
}
.workbench-empty.no-provider {
flex-direction: column;
gap: 0.35rem;
}
.workbench-meta {
margin: 0;
color: #8b8ba8;
font-size: 0.8rem;
}
</style>
+14 -5
View File
@@ -1,8 +1,15 @@
<script context="module">
import { writable } from 'svelte/store';
const activeWorkspaceNodeId = writable('');
</script>
<script>
import { onMount } from 'svelte';
import * as App from '../../../wailsjs/go/api/App';
export let nodes = [];
export let node = null;
export let currentNodeId = '';
export let expandedNodes = {};
export let depth = 0;
@@ -32,6 +39,7 @@
} else {
nodes = result.nodes || [];
currentNodeId = result.currentNodeId || '';
activeWorkspaceNodeId.set(currentNodeId);
const root = nodes.find(n => !n.parentId);
if (root) expandedNodes[root.id] = true;
}
@@ -69,6 +77,7 @@
const err = await App.SetCurrentWorkspaceNode(id);
if (err) { localError = err; return; }
currentNodeId = id;
activeWorkspaceNodeId.set(id);
}
function openCreate(parentId, type) {
@@ -128,7 +137,7 @@
{/if}
</div>
{:else}
<div class="wt-node" class:selected={node.id === currentNodeId} class:archived={node.status === 'archived'} class:sleeping={node.status === 'sleeping'}>
<div class="wt-node" class:selected={node.id === $activeWorkspaceNodeId} class:archived={node.status === 'archived'} class:sleeping={node.status === 'sleeping'}>
<div class="wt-row" style="padding-left: {depth * 1.0 + 0.4}rem;">
{#if hasKids(node.id)}
<button class="wt-expand" on:click={() => toggle(node.id)} type="button">{expandedNodes[node.id] ? '\u25BE' : '\u25B8'}</button>
@@ -153,7 +162,7 @@
.wt { display: flex; flex-direction: column; flex: 1; overflow: hidden; position: relative; }
.wt-header { display: flex; align-items: center; justify-content: space-between; padding: 0.4rem 0.6rem; border-bottom: 1px solid #0f3460; flex-shrink: 0; }
.wt-title { color: #a0a0b8; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.wt-btn { background: none; border: none; color: #666; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.3rem; border-radius: 3px; }
.wt-btn { min-height: 0; background: none; border: none; color: #666; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.3rem; border-radius: 3px; }
.wt-btn:hover { color: #4ecca3; background: rgba(78,204,163,0.1); }
.wt-btn-small { font-size: 0.7rem; opacity: 0; }
.wt-row:hover .wt-btn-small { opacity: 1; }
@@ -162,12 +171,12 @@
.wt-node { }
.wt-row { display: flex; align-items: center; gap: 0.2rem; padding: 0.15rem 0; }
.wt-row:hover { background: rgba(15,52,96,0.4); }
.wt-row.selected { background: rgba(78,204,163,0.1); }
.wt-expand { width: 1rem; height: 1rem; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; color: #666; background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; }
.wt-node.selected > .wt-row { background: rgba(78,204,163,0.1); }
.wt-expand { width: 1rem; height: 1rem; min-height: 0; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; color: #666; background: none; border: none; cursor: pointer; padding: 0; flex-shrink: 0; }
.wt-expand:hover { color: #e0e0f0; }
.wt-expand-spacer { width: 1rem; flex-shrink: 0; }
.wt-icon { font-size: 0.8rem; flex-shrink: 0; }
.wt-label { flex: 1; background: none; border: none; color: #e0e0f0; font-size: 0.78rem; text-align: left; cursor: pointer; padding: 0.1rem 0.2rem; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wt-label { flex: 1; min-height: 0; justify-content: flex-start; background: none; border: none; color: #e0e0f0; font-size: 0.78rem; text-align: left; cursor: pointer; padding: 0.1rem 0.2rem; border-radius: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wt-label:hover { color: #4ecca3; }
.wt-node.archived .wt-label { text-decoration: line-through; opacity: 0.5; }
.wt-node.sleeping .wt-label { opacity: 0.6; }
+756
View File
@@ -0,0 +1,756 @@
/**
* Wails Mock Bridge эмулирует window['go']['api']['App'] для тестового окружения.
*
* Каждый метод возвращает Promise с данными, совместимыми с Wails-контрактом.
* Состояние мутабельно тесты могут менять его между сценариями.
*/
(function () {
if (window.__wailsMockReady) return;
// ── Mutable state ──────────────────────────────────────────────────
var pluginStates = {
'verstak.platform-test': {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.platform-test',
name: 'Platform Test',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Runtime test plugin for verifying the Verstak platform.',
source: 'official',
icon: '🧪',
provides: ['verstak/platform-test/v1', 'verstak/diagnostics/v1'],
requires: ['verstak/core/plugin-manager/v1', 'verstak/core/capability-registry/v1'],
optionalRequires: ['verstak/core/vault/v1', 'verstak/core/sync/v1', 'verstak/core/files/v1', 'verstak/core/workbench/v1'],
permissions: ['vault.read', 'events.publish', 'events.subscribe', 'ui.register', 'commands.register', 'storage.namespace', 'files.read', 'files.write', 'files.delete', 'workbench.open'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
views: [
{ id: 'verstak.platform-test.diagnostics', title: 'Platform Diagnostics', icon: '🧪', component: 'DiagnosticsPanel' }
],
commands: [
{ id: 'verstak.platform-test.run-tests', title: 'Run Platform Tests', handler: 'runAllTests' },
{ id: 'verstak.platform-test.show-version', title: 'Show Version Info', handler: 'showVersion' }
],
sidebarItems: [
{ id: 'verstak.platform-test.sidebar', title: 'Platform Test', icon: '🧪', view: 'verstak.platform-test.diagnostics', position: 100 }
],
statusBarItems: [
{ id: 'verstak.platform-test.status', label: '🧪 All Tests Pass', position: 'right', handler: 'openDiagnostics' }
],
settingsPanels: [
{ id: 'verstak.platform-test.settings', title: 'Platform Test Settings', icon: '🧪', component: 'PlatformTestSettings' }
],
openProviders: [
{
id: 'verstak.platform-test.markdown-diagnostic',
title: 'Platform Test Markdown Diagnostic',
priority: 100,
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'] }
]
}
]
}
},
rootPath: '/tmp/verstak-test/plugins/platform-test',
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 appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
var workbenchPreferences = {};
var openedResources = [];
var pluginSettings = {
'verstak.platform-test': { savedText: 'initial value' }
};
var vaultFiles = makeDefaultVaultFiles();
var workspaceTree = makeDefaultWorkspaceTree();
var reloadResponseMode = 'tuple';
// ── Helpers ────────────────────────────────────────────────────────
function makeDefaultWorkspaceTree() {
return {
status: 'initialized',
currentNodeId: 'case-alpha',
nodes: [
{ id: 'space-main', parentId: '', type: 'space', title: 'Main Space', status: 'active', order: 1 },
{ id: 'case-alpha', parentId: 'space-main', type: 'case', title: 'Alpha Case', status: 'active', order: 1 },
{ id: 'case-beta', parentId: 'space-main', type: 'case', title: 'Beta Case', status: 'active', order: 2 }
]
};
}
function cloneWorkspaceTree() {
return {
status: workspaceTree.status,
currentNodeId: workspaceTree.currentNodeId,
nodes: workspaceTree.nodes.map(function (n) { return Object.assign({}, n); })
};
}
function makeDefaultVaultFiles() {
return {
'': { type: 'folder', modifiedAt: new Date().toISOString() }
};
}
function normalizeVaultPath(relativePath, allowRoot) {
var p = String(relativePath || '');
if (p.indexOf('\x00') !== -1) return { error: 'invalid-path: null-byte' };
if (p.indexOf('\\') !== -1) return { error: 'invalid-path: backslash not allowed' };
if (p.indexOf('./') === 0) p = p.slice(2);
if (!allowRoot && !p) return { error: 'invalid-path: empty path' };
if (p.charAt(0) === '/' || /^[A-Za-z]:/.test(p)) return { error: 'invalid-path: absolute path rejected' };
var parts = p.split('/').filter(Boolean);
if (parts.indexOf('..') !== -1) return { error: 'invalid-path: path-traversal' };
if (parts[0] && parts[0].toLowerCase() === '.verstak') return { error: 'reserved-path: .verstak is internal' };
return { path: parts.join('/') };
}
function parentPath(path) {
var idx = path.lastIndexOf('/');
return idx === -1 ? '' : path.slice(0, idx);
}
function baseName(path) {
var idx = path.lastIndexOf('/');
return idx === -1 ? path : path.slice(idx + 1);
}
function fileEntry(path, node) {
var name = path ? baseName(path) : '';
var ext = '';
var dot = name.lastIndexOf('.');
if (dot > 0) ext = name.slice(dot + 1);
return {
name: name,
relativePath: path,
type: node.type,
size: node.type === 'file' ? (node.content || '').length : 0,
modifiedAt: node.modifiedAt || new Date().toISOString(),
extension: ext,
isHidden: name.charAt(0) === '.',
isReserved: false,
canRead: node.type === 'file' || node.type === 'folder',
canWrite: node.type === 'file' || node.type === 'folder'
};
}
function requirePluginPermission(pluginId, permission) {
var s = pluginStates[pluginId];
if (!s || !s.enabled || (s.status !== 'loaded' && s.status !== 'degraded')) {
return 'plugin not enabled and loaded';
}
if (!s.manifest.permissions || s.manifest.permissions.indexOf(permission) === -1) {
return 'plugin lacks required permission ' + permission;
}
if (vaultStatus.status !== 'open') return 'vault-not-open';
return '';
}
function makePlugin(id) {
var s = pluginStates[id];
if (!s) return null;
return {
manifest: s.manifest,
status: s.status,
enabled: s.enabled,
rootPath: s.rootPath,
error: s.error
};
}
function allPlugins() {
return Object.keys(pluginStates).map(makePlugin).filter(Boolean);
}
function allCapabilities() {
var caps = [];
caps.push({ name: 'verstak/core/plugin-manager/v1', description: 'Plugin management', pluginId: 'verstak-desktop', status: 'stable' });
caps.push({ name: 'verstak/core/capability-registry/v1', description: 'Capability registry', pluginId: 'verstak-desktop', status: 'stable' });
caps.push({ name: 'verstak/core/files/v1', description: 'Files API', pluginId: 'verstak-desktop', status: 'stable' });
caps.push({ name: 'verstak/core/workbench/v1', description: 'Workbench routing', pluginId: 'verstak-desktop', status: 'stable' });
for (var id in pluginStates) {
var s = pluginStates[id];
if (s.status === 'loaded' && s.enabled && s.manifest && s.manifest.provides) {
s.manifest.provides.forEach(function (p) {
caps.push({ name: p, description: '', pluginId: id, status: 'stable' });
});
}
}
return caps;
}
function allPermissions() {
return [
{ name: 'vault.read', description: 'Read vault data', dangerous: false },
{ name: 'events.publish', description: 'Publish events', dangerous: false },
{ name: 'events.subscribe', description: 'Subscribe to events', dangerous: false },
{ name: 'ui.register', description: 'Register UI contributions', dangerous: false },
{ name: 'commands.register', description: 'Register commands', dangerous: false },
{ name: 'storage.namespace', description: 'Access plugin storage', dangerous: false },
{ name: 'files.read', description: 'Read vault files', dangerous: false },
{ name: 'files.write', description: 'Write vault files', dangerous: true },
{ name: 'files.delete', description: 'Trash vault files', dangerous: true },
{ name: 'workbench.open', description: 'Request Workbench open/edit routing', dangerous: false }
];
}
function allContributions() {
var views = [], commands = [], sidebarItems = [], statusBarItems = [], settingsPanels = [], openProviders = [];
for (var id in pluginStates) {
var s = pluginStates[id];
var c = (s.manifest && s.manifest.contributes) || {};
if (c.views) c.views.forEach(function (v) { views.push(Object.assign({}, v, { pluginId: id })); });
if (c.commands) c.commands.forEach(function (cmd) { commands.push(Object.assign({}, cmd, { pluginId: id })); });
if (c.sidebarItems) c.sidebarItems.forEach(function (sb) { sidebarItems.push(Object.assign({}, sb, { pluginId: id })); });
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 })); });
}
return { views: views, commands: commands, sidebarItems: sidebarItems, statusBarItems: statusBarItems, settingsPanels: settingsPanels, openProviders: openProviders };
}
function requestExtension(request) {
if (request && request.extension) {
var explicit = String(request.extension).toLowerCase();
return explicit.charAt(0) === '.' ? explicit : '.' + explicit;
}
var p = String((request && request.path) || '').toLowerCase();
var slash = p.lastIndexOf('/');
var name = slash === -1 ? p : p.slice(slash + 1);
var dot = name.lastIndexOf('.');
return dot > 0 ? name.slice(dot) : '';
}
function requestContextName(request) {
var ctx = (request && request.context) || {};
if (ctx.notesMode || ctx.isInsideNotesFolder || ctx.sourceView === 'notes') return 'notes-markdown';
var ext = requestExtension(request);
if (ext === '.md' || ext === '.markdown') return 'generic-markdown';
return 'generic-text';
}
function providerSupports(provider, request) {
var ext = requestExtension(request);
var contextName = requestContextName(request);
return (provider.supports || []).some(function (support) {
if (support.kind && support.kind !== request.kind) return false;
if (support.extensions && support.extensions.length && support.extensions.map(function (e) { return String(e).toLowerCase(); }).indexOf(ext) === -1) return false;
if (support.contexts && support.contexts.length && support.contexts.indexOf(contextName) === -1) return false;
return true;
});
}
function selectOpenProvider(request) {
var providers = allContributions().openProviders.filter(function (provider) {
var s = pluginStates[provider.pluginId];
return s && s.enabled && (s.status === 'loaded' || s.status === 'degraded') && providerSupports(provider, request);
});
providers.sort(function (a, b) {
var byPriority = (b.priority || 0) - (a.priority || 0);
if (byPriority) return byPriority;
return String(a.id).localeCompare(String(b.id));
});
return providers[0] || null;
}
function openWorkbenchResource(pluginId, request, forcedMode) {
var s = pluginStates[pluginId];
if (!s || !s.enabled || (s.status !== 'loaded' && s.status !== 'degraded')) {
return Promise.resolve([{}, 'plugin not enabled and loaded']);
}
if (!s.manifest.permissions || s.manifest.permissions.indexOf('workbench.open') === -1) {
return Promise.resolve([{}, 'plugin lacks required permission workbench.open']);
}
var normalized = Object.assign({}, request || {});
normalized.kind = normalized.kind || 'vault-file';
normalized.mode = forcedMode || normalized.mode || 'view';
normalized.extension = requestExtension(normalized);
normalized.context = Object.assign({}, normalized.context || {}, { sourcePluginId: pluginId });
var provider = selectOpenProvider(normalized);
if (!provider) {
return Promise.resolve([{
status: 'no-provider',
request: normalized,
message: 'no open provider for resource'
}, '']);
}
var result = {
status: 'opened',
providerId: provider.id,
providerPluginId: provider.pluginId,
providerComponent: provider.component,
request: normalized
};
openedResources.push(Object.assign({ id: provider.id + ':' + openedResources.length, openedAt: new Date().toISOString() }, result));
return Promise.resolve([result, '']);
}
function platformTestBundle() {
return [
"(function(){",
"var DiagnosticsPanel={",
"mount:function(containerEl,props,api){",
"containerEl.innerHTML='';",
"containerEl.__ptCleanup=[];",
"function track(fn){if(typeof fn==='function')containerEl.__ptCleanup.push(fn);}",
"var root=document.createElement('div');",
"root.className='pt-root';",
"var title=document.createElement('h2');",
"title.className='pt-plugin-name';",
"title.textContent='Platform Diagnostics';",
"var pluginId=document.createElement('p');",
"pluginId.className='pt-plugin-id';",
"pluginId.textContent=api.pluginId;",
"var status=document.createElement('div');",
"status.className='pt-badge pt-badge-success';",
"status.textContent='Frontend Bundle Loaded';",
"var saved=document.createElement('div');",
"saved.className='pt-card pt-saved-setting';",
"saved.textContent='Saved setting: loading...';",
"var cap=document.createElement('div');",
"cap.className='pt-capability-result';",
"cap.textContent='Capabilities: loading...';",
"api.capabilities.list().then(function(caps){cap.textContent='Capabilities: '+caps.length+' available';});",
"api.settings.read('savedText').then(function(value){saved.textContent='Saved setting: '+(value||'');});",
"var input=document.createElement('input');",
"input.className='pt-setting-input';",
"input.setAttribute('aria-label','Saved setting');",
"input.value='changed value';",
"var button=document.createElement('button');",
"button.className='btn btn-primary pt-save-setting';",
"button.textContent='Save Setting';",
"button.addEventListener('click',function(){api.settings.write('savedText',input.value).then(function(){saved.textContent='Saved setting: '+input.value;});});",
"api.capabilities.has('verstak/platform-test/v1').then(function(ok){status.textContent='Frontend Bundle Loaded | capability '+(ok?'available':'missing');});",
"var command=document.createElement('div');",
"command.className='pt-command-result';",
"command.textContent='Command: registering...';",
"api.commands.register('verstak.platform-test.show-version',function(){return {version:'0.1.0',source:'bundled-frontend'};}).then(function(unregister){track(unregister);return api.commands.execute('verstak.platform-test.show-version',{});}).then(function(result){status.setAttribute('data-command-status',result.status||'');command.textContent='Command: '+result.status+' '+result.result.version+' from '+result.result.source;});",
"var eventResult=document.createElement('div');",
"eventResult.className='pt-event-result';",
"eventResult.textContent='Event: subscribing...';",
"api.events.subscribe('verstak.platform-test.echo',function(event){eventResult.textContent='Event: received '+event.payload.message;eventResult.setAttribute('data-event-status','received');}).then(function(unsubscribe){track(unsubscribe);return api.events.publish('verstak.platform-test.echo',{message:'hello-event'});});",
"var filesResult=document.createElement('div');",
"filesResult.className='pt-files-result';",
"filesResult.textContent='Files: running...';",
"var filesError=document.createElement('div');",
"filesError.className='pt-files-error-result';",
"filesError.textContent='Files error path: checking...';",
"var workbenchResult=document.createElement('div');",
"workbenchResult.className='pt-workbench-result';",
"workbenchResult.textContent='Workbench: ready';",
"function makeWorkbenchButton(cls,label,request){var b=document.createElement('button');b.className='btn btn-primary '+cls;b.textContent=label;b.addEventListener('click',function(){workbenchResult.textContent='Workbench: opening...';api.workbench.editResource(request).then(function(result){workbenchResult.textContent='Workbench: opened '+result.request.path+' with '+(result.providerId||'no-provider');workbenchResult.setAttribute('data-workbench-status',result.status==='opened'?'ok':result.status);}).catch(function(err){workbenchResult.textContent='Workbench error: '+(err&&err.message?err.message:String(err));workbenchResult.setAttribute('data-workbench-status','error');});});return b;}",
"var textWorkbenchButton=makeWorkbenchButton('pt-open-workbench-text','Open Text Diagnostic',{kind:'vault-file',path:'Docs/todo.txt',extension:'.txt',mime:'text/plain',context:{sourceView:'files'}});",
"var markdownWorkbenchButton=makeWorkbenchButton('pt-open-workbench-markdown','Open Markdown Diagnostic',{kind:'vault-file',path:'Docs/readme.md',extension:'.md',context:{sourceView:'files'}});",
"var notesWorkbenchButton=makeWorkbenchButton('pt-open-workbench-notes','Open Notes Diagnostic',{kind:'vault-file',path:'Notes/Overview.md',extension:'.md',context:{sourceView:'notes',isInsideNotesFolder:true,notesMode:true}});",
"api.files.createFolder('PlatformTest').catch(function(e){if(String(e).indexOf('conflict')===-1)throw e;}).then(function(){return api.files.writeText('PlatformTest/files-api.txt','hello files',{createIfMissing:true,overwrite:true});}).then(function(){return api.files.readText('PlatformTest/files-api.txt');}).then(function(text){if(text!=='hello files')throw new Error('read mismatch');return api.files.list('PlatformTest');}).then(function(entries){if(!entries.some(function(e){return e.relativePath==='PlatformTest/files-api.txt';}))throw new Error('list missing file');return api.files.move('PlatformTest/files-api.txt','PlatformTest/files-api-moved.txt',{overwrite:true});}).then(function(){return api.files.trash('PlatformTest/files-api-moved.txt');}).then(function(){filesResult.textContent='Files: wrote/read/listed/moved/trashed';filesResult.setAttribute('data-files-status','ok');}).catch(function(err){filesResult.textContent='Files error: '+(err&&err.message?err.message:String(err));filesResult.setAttribute('data-files-status','error');});",
"api.files.readText('.verstak/vault.json').then(function(){filesError.textContent='Files error path: unexpectedly allowed';filesError.setAttribute('data-files-error-status','error');}).catch(function(err){var message=err&&err.message?err.message:String(err);if(message.indexOf('reserved-path')===-1&&message.indexOf('.verstak')===-1){filesError.textContent='Files error path: wrong error '+message;filesError.setAttribute('data-files-error-status','error');return;}filesError.textContent='Files error path: rejected reserved-path';filesError.setAttribute('data-files-error-status','expected');});",
"root.appendChild(title);",
"root.appendChild(pluginId);",
"root.appendChild(status);",
"root.appendChild(saved);",
"root.appendChild(input);",
"root.appendChild(button);",
"root.appendChild(cap);",
"root.appendChild(command);",
"root.appendChild(eventResult);",
"root.appendChild(filesResult);",
"root.appendChild(filesError);",
"root.appendChild(textWorkbenchButton);",
"root.appendChild(markdownWorkbenchButton);",
"root.appendChild(notesWorkbenchButton);",
"root.appendChild(workbenchResult);",
"containerEl.appendChild(root);",
"},",
"unmount:function(containerEl){while(containerEl.__ptCleanup&&containerEl.__ptCleanup.length){containerEl.__ptCleanup.pop()();}containerEl.innerHTML='';}",
"};",
"var MarkdownDiagnosticProvider={",
"mount:function(containerEl,props,api){",
"containerEl.innerHTML='';",
"var root=document.createElement('div');",
"root.className='pt-root pt-workbench-result';",
"root.setAttribute('data-workbench-status','ok');",
"var req=(props&&props.request)||{};",
"var ctx=(req.context&&req.context.notesMode)||false?'notes-markdown':((req.extension==='.md'||req.extension==='.markdown')?'generic-markdown':'generic-text');",
"root.setAttribute('data-resource-path',req.path||'');",
"root.setAttribute('data-resource-mode',req.mode||'');",
"root.setAttribute('data-resource-context',ctx);",
"root.textContent='Workbench: opened '+(req.path||'')+' with '+((props&&props.providerId)||'')+' mode='+(req.mode||'')+' context='+ctx;",
"containerEl.appendChild(root);",
"},",
"unmount:function(containerEl){containerEl.innerHTML='';}",
"};",
"var PlatformTestSettings={",
"mount:function(containerEl,props,api){",
"containerEl.innerHTML='<div class=\"pt-root\"><h2>Platform Test Settings</h2><p>'+api.pluginId+'</p></div>';",
"},",
"unmount:function(containerEl){containerEl.innerHTML='';}",
"};",
"window.VerstakPluginRegister('verstak.platform-test',{components:{DiagnosticsPanel:DiagnosticsPanel,PlatformTestSettings:PlatformTestSettings,MarkdownDiagnosticProvider:MarkdownDiagnosticProvider}});",
"})();"
].join('');
}
// ── Mock API ───────────────────────────────────────────────────────
var mock = {
GetPlugins: function () { return Promise.resolve(allPlugins()); },
GetCapabilities: function () { return Promise.resolve(allCapabilities()); },
GetPermissions: function () { return Promise.resolve(allPermissions()); },
GetContributions: function () { return Promise.resolve(allContributions()); },
GetVaultStatus: function () { return Promise.resolve(vaultStatus); },
GetVaultPluginState: function () { return Promise.resolve(vaultPluginState); },
GetAppSettings: function () { return Promise.resolve(appSettings); },
GetPluginFrontendInfo: function (pluginId) {
var s = pluginStates[pluginId];
if (s && s.manifest && s.manifest.frontend) {
return Promise.resolve({ entry: s.manifest.frontend.entry });
}
return Promise.resolve({});
},
ReadPluginSettings: function (pluginId) {
return Promise.resolve([Object.assign({}, pluginSettings[pluginId] || {}), '']);
},
WritePluginSettings: function (pluginId, settings) {
pluginSettings[pluginId] = Object.assign({}, settings || {});
return Promise.resolve('');
},
ReadPluginSetting: function () { return Promise.resolve(null); },
WritePluginSetting: function () { return Promise.resolve(null); },
ReadPluginDataJSON: function () { return Promise.resolve({}); },
WritePluginDataJSON: function () { return Promise.resolve(null); },
OpenWorkbenchResource: function (pluginId, request) {
return openWorkbenchResource(pluginId, request || {}, '');
},
EditWorkbenchResource: function (pluginId, request) {
return openWorkbenchResource(pluginId, request || {}, 'edit');
},
GetWorkbenchOpenedResources: function () {
return Promise.resolve(openedResources.map(function (resource) {
return Object.assign({}, resource, { request: Object.assign({}, resource.request || {}) });
}));
},
GetWorkbenchPreferences: function () {
return Promise.resolve(Object.assign({}, workbenchPreferences));
},
UpdateWorkbenchPreferences: function (preferences) {
workbenchPreferences = Object.assign({}, workbenchPreferences, preferences || {});
return Promise.resolve('');
},
GetPluginAssetContent: function (pluginId, assetPath) {
if (pluginId === 'verstak.platform-test' && assetPath === 'frontend/dist/index.js') {
return Promise.resolve(platformTestBundle());
}
return Promise.resolve('');
},
GetPluginCapability: function (pluginId, capId) {
var caps = allCapabilities();
var found = caps.find(function (cap) { return cap.name === capId; });
return Promise.resolve([found ? Object.assign({ available: true }, found) : { available: false, name: capId }, '']);
},
ListPluginCapabilities: function () { return Promise.resolve([allCapabilities(), '']); },
ExecutePluginCommand: function (pluginId, commandId, args) {
var s = pluginStates[pluginId];
var commands = ((s && s.manifest && s.manifest.contributes && s.manifest.contributes.commands) || []);
var found = commands.find(function (cmd) { return cmd.id === commandId; });
if (!found) return Promise.resolve([{}, 'command not declared']);
return Promise.resolve([{ status: 'declared', pluginId: pluginId, commandId: commandId, handler: found.handler, args: args || {} }, '']);
},
PublishPluginEvent: function () { return Promise.resolve(''); },
SubscribePluginEvent: function (pluginId, eventName) {
var s = pluginStates[pluginId];
if (!s || !s.enabled || s.status !== 'loaded') return Promise.resolve('plugin not enabled and loaded');
if (!eventName) return Promise.resolve('event name is empty');
if (!s.manifest.permissions || s.manifest.permissions.indexOf('events.subscribe') === -1) {
return Promise.resolve('plugin lacks required permission events.subscribe');
}
return Promise.resolve('');
},
ListVaultFiles: function (pluginId, relativeDir) {
var err = requirePluginPermission(pluginId, 'files.read');
if (err) return Promise.resolve([[], err]);
var norm = normalizeVaultPath(relativeDir, true);
if (norm.error) return Promise.resolve([[], norm.error]);
var dir = norm.path;
if (!vaultFiles[dir] || vaultFiles[dir].type !== 'folder') return Promise.resolve([[], 'not-found: ' + dir]);
var prefix = dir ? dir + '/' : '';
var entries = [];
Object.keys(vaultFiles).forEach(function (path) {
if (path === dir || path.indexOf(prefix) !== 0) return;
var rest = path.slice(prefix.length);
if (!rest || rest.indexOf('/') !== -1) return;
entries.push(fileEntry(path, vaultFiles[path]));
});
return Promise.resolve([entries, '']);
},
GetVaultFileMetadata: function (pluginId, relativePath) {
var err = requirePluginPermission(pluginId, 'files.read');
if (err) return Promise.resolve([{}, err]);
var norm = normalizeVaultPath(relativePath, false);
if (norm.error) return Promise.resolve([{}, norm.error]);
var node = vaultFiles[norm.path];
if (!node) return Promise.resolve([{}, 'not-found: ' + norm.path]);
return Promise.resolve([fileEntry(norm.path, node), '']);
},
ReadVaultTextFile: function (pluginId, relativePath) {
var err = requirePluginPermission(pluginId, 'files.read');
if (err) return Promise.resolve(['', err]);
var norm = normalizeVaultPath(relativePath, false);
if (norm.error) return Promise.resolve(['', norm.error]);
var node = vaultFiles[norm.path];
if (!node) return Promise.resolve(['', 'not-found: ' + norm.path]);
if (node.type !== 'file') return Promise.resolve(['', 'not-regular-file: ' + norm.path]);
return Promise.resolve([node.content || '', '']);
},
WriteVaultTextFile: function (pluginId, relativePath, content, options) {
var err = requirePluginPermission(pluginId, 'files.write');
if (err) return Promise.resolve(err);
var norm = normalizeVaultPath(relativePath, false);
if (norm.error) return Promise.resolve(norm.error);
options = options || {};
var existing = vaultFiles[norm.path];
if (existing && existing.type !== 'file') return Promise.resolve('not-regular-file: ' + norm.path);
if (existing && !options.overwrite) return Promise.resolve('conflict: ' + norm.path);
if (!existing && !options.createIfMissing) return Promise.resolve('not-found: ' + norm.path);
var parent = parentPath(norm.path);
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
vaultFiles[norm.path] = { type: 'file', content: String(content == null ? '' : content), modifiedAt: new Date().toISOString() };
return Promise.resolve('');
},
CreateVaultFolder: function (pluginId, relativePath) {
var err = requirePluginPermission(pluginId, 'files.write');
if (err) return Promise.resolve(err);
var norm = normalizeVaultPath(relativePath, false);
if (norm.error) return Promise.resolve(norm.error);
if (vaultFiles[norm.path]) return Promise.resolve('conflict: ' + norm.path);
var parent = parentPath(norm.path);
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
vaultFiles[norm.path] = { type: 'folder', modifiedAt: new Date().toISOString() };
return Promise.resolve('');
},
MoveVaultPath: function (pluginId, fromRelativePath, toRelativePath, options) {
var err = requirePluginPermission(pluginId, 'files.write');
if (err) return Promise.resolve(err);
var from = normalizeVaultPath(fromRelativePath, false);
var to = normalizeVaultPath(toRelativePath, false);
if (from.error) return Promise.resolve(from.error);
if (to.error) return Promise.resolve(to.error);
options = options || {};
if (!vaultFiles[from.path]) return Promise.resolve('not-found: ' + from.path);
if (vaultFiles[from.path].type === 'folder' && (to.path === from.path || to.path.indexOf(from.path + '/') === 0)) {
return Promise.resolve('move-into-self: ' + from.path + ' -> ' + to.path);
}
if (vaultFiles[to.path] && !options.overwrite) return Promise.resolve('conflict: ' + to.path);
var parent = parentPath(to.path);
if (!vaultFiles[parent] || vaultFiles[parent].type !== 'folder') return Promise.resolve('parent-not-found: ' + parent);
var moving = Object.keys(vaultFiles).filter(function (path) { return path === from.path || path.indexOf(from.path + '/') === 0; });
moving.forEach(function (path) {
var suffix = path.slice(from.path.length);
vaultFiles[to.path + suffix] = vaultFiles[path];
delete vaultFiles[path];
});
return Promise.resolve('');
},
TrashVaultPath: function (pluginId, relativePath) {
var err = requirePluginPermission(pluginId, 'files.delete');
if (err) return Promise.resolve([{}, err]);
var norm = normalizeVaultPath(relativePath, false);
if (norm.error) return Promise.resolve([{}, norm.error]);
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 moving = Object.keys(vaultFiles).filter(function (path) { return path === norm.path || path.indexOf(norm.path + '/') === 0; });
moving.forEach(function (path) { delete vaultFiles[path]; });
return Promise.resolve([{ originalPath: norm.path, trashPath: trashPath, trashId: trashId, deletedAt: new Date().toISOString() }, '']);
},
GetCurrentWorkspaceNode: function () { return Promise.resolve(null); },
GetWorkspaceTree: function () { return Promise.resolve(cloneWorkspaceTree()); },
ArchiveWorkspaceNode: function () { return Promise.resolve(''); },
CreateWorkspaceNode: function () { return Promise.resolve({}); },
MoveWorkspaceNode: function () { return Promise.resolve(''); },
RenameWorkspaceNode: function () { return Promise.resolve(''); },
SetCurrentWorkspaceNode: function (id) {
var found = workspaceTree.nodes.some(function (n) { return n.id === id; });
if (!found) return Promise.resolve('workspace node not found: ' + id);
workspaceTree.currentNodeId = id;
return Promise.resolve('');
},
SelectDirectory: function () { return Promise.resolve(''); },
SelectVaultForOpen: function () { return Promise.resolve(''); },
CreateVault: function () { return Promise.resolve(null); },
OpenVault: function () { return Promise.resolve(null); },
CloseVault: function () { return Promise.resolve(null); },
SetCurrentVault: function () { return Promise.resolve(''); },
UpdateAppSettings: function () { return Promise.resolve(''); },
RecordDesiredPlugin: function () { return Promise.resolve(''); },
WriteFrontendLog: function () { return Promise.resolve(); },
EnablePlugin: function (pluginId) {
if (pluginStates[pluginId]) {
pluginStates[pluginId].status = 'loaded';
pluginStates[pluginId].enabled = true;
if (vaultPluginState.disabledPlugins.indexOf(pluginId) !== -1) {
vaultPluginState.disabledPlugins = vaultPluginState.disabledPlugins.filter(function (id) { return id !== pluginId; });
}
if (vaultPluginState.enabledPlugins.indexOf(pluginId) === -1) {
vaultPluginState.enabledPlugins.push(pluginId);
}
}
return Promise.resolve(null);
},
DisablePlugin: function (pluginId) {
if (pluginStates[pluginId]) {
pluginStates[pluginId].status = 'disabled';
pluginStates[pluginId].enabled = false;
if (vaultPluginState.enabledPlugins.indexOf(pluginId) !== -1) {
vaultPluginState.enabledPlugins = vaultPluginState.enabledPlugins.filter(function (id) { return id !== pluginId; });
}
if (vaultPluginState.disabledPlugins.indexOf(pluginId) === -1) {
vaultPluginState.disabledPlugins.push(pluginId);
}
}
return Promise.resolve(null);
},
ReloadPlugins: function () {
if (reloadResponseMode === 'raw-count') {
return Promise.resolve(Object.keys(pluginStates).length);
}
return Promise.resolve([Object.keys(pluginStates).length, 'Reloaded ' + Object.keys(pluginStates).length + ' plugin(s).']);
}
};
// ── Install bridge ─────────────────────────────────────────────────
if (!window['go']) window['go'] = {};
if (!window['go']['api']) window['go']['api'] = {};
window['go']['api']['App'] = mock;
// ── Test helpers (exposed for Playwright) ──────────────────────────
window.__wailsMock = {
reset: function () {
pluginStates = {
'verstak.platform-test': {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: 'verstak.platform-test',
name: 'Platform Test',
version: '0.1.0',
apiVersion: '0.1.0',
description: 'Runtime test plugin for verifying the Verstak platform.',
source: 'official',
icon: '🧪',
provides: ['verstak/platform-test/v1', 'verstak/diagnostics/v1'],
requires: ['verstak/core/plugin-manager/v1', 'verstak/core/capability-registry/v1'],
optionalRequires: ['verstak/core/vault/v1', 'verstak/core/sync/v1', 'verstak/core/files/v1', 'verstak/core/workbench/v1'],
permissions: ['vault.read', 'events.publish', 'events.subscribe', 'ui.register', 'commands.register', 'storage.namespace', 'files.read', 'files.write', 'files.delete', 'workbench.open'],
frontend: { entry: 'frontend/dist/index.js' },
contributes: {
views: [
{ id: 'verstak.platform-test.diagnostics', title: 'Platform Diagnostics', icon: '🧪', component: 'DiagnosticsPanel' }
],
commands: [
{ id: 'verstak.platform-test.run-tests', title: 'Run Platform Tests', handler: 'runAllTests' },
{ id: 'verstak.platform-test.show-version', title: 'Show Version Info', handler: 'showVersion' }
],
sidebarItems: [
{ id: 'verstak.platform-test.sidebar', title: 'Platform Test', icon: '🧪', view: 'verstak.platform-test.diagnostics', position: 100 }
],
statusBarItems: [
{ id: 'verstak.platform-test.status', label: '🧪 All Tests Pass', position: 'right', handler: 'openDiagnostics' }
],
settingsPanels: [
{ id: 'verstak.platform-test.settings', title: 'Platform Test Settings', icon: '🧪', component: 'PlatformTestSettings' }
],
openProviders: [
{
id: 'verstak.platform-test.markdown-diagnostic',
title: 'Platform Test Markdown Diagnostic',
priority: 100,
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'] }
]
}
]
}
},
rootPath: '/tmp/verstak-test/plugins/platform-test',
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' }] };
appSettings = { currentVaultPath: '/tmp/verstak-test/vault', recentVaults: [] };
workbenchPreferences = {};
openedResources = [];
pluginSettings = { 'verstak.platform-test': { savedText: 'initial value' } };
vaultFiles = makeDefaultVaultFiles();
workspaceTree = makeDefaultWorkspaceTree();
reloadResponseMode = 'tuple';
},
setPluginStatus: function (pluginId, status, enabled) {
if (pluginStates[pluginId]) {
pluginStates[pluginId].status = status;
pluginStates[pluginId].enabled = enabled;
}
},
getPluginState: function (pluginId) {
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
},
addSyntheticPlugins: function (count) {
var total = Number(count || 0);
for (var i = 1; i <= total; i++) {
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
pluginStates[id] = {
status: 'loaded',
enabled: true,
manifest: {
schemaVersion: 1,
id: id,
name: 'Synthetic Layout Plugin ' + i,
version: '0.0.' + i,
apiVersion: '0.1.0',
description: 'Synthetic plugin used by frontend layout tests.',
source: 'test',
provides: ['verstak/synthetic-layout-' + i + '/v1'],
requires: [],
optionalRequires: [],
permissions: [],
contributes: {
views: [],
commands: [],
sidebarItems: [],
statusBarItems: [],
settingsPanels: []
}
},
rootPath: '/tmp/verstak-test/plugins/synthetic-layout-' + i + '/with/a/long/path/for/responsive-checks',
error: ''
};
if (vaultPluginState.enabledPlugins.indexOf(id) === -1) {
vaultPluginState.enabledPlugins.push(id);
}
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: 'test' });
}
}
},
setVaultStatus: function (status) { vaultStatus = status; },
setVaultPluginState: function (state) { vaultPluginState = state; },
setReloadResponseMode: function (mode) { reloadResponseMode = mode || 'tuple'; }
};
window.__wailsMockReady = true;
console.log('[wails-mock] bridge installed');
})();
+14 -10
View File
@@ -1,14 +1,18 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
server: {
port: 5173,
strictPort: true,
},
export default defineConfig(({ mode }) => {
const isTest = mode === 'test';
return {
plugins: [svelte()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
server: {
port: isTest ? 5174 : 5173,
strictPort: true,
},
};
});
+39 -1
View File
@@ -4,6 +4,8 @@ import {capability} from '../models';
import {api} from '../models';
import {permissions} from '../models';
import {plugin} from '../models';
import {files} from '../models';
import {workbench} from '../models';
export function ArchiveWorkspaceNode(arg1:string):Promise<string>;
@@ -11,12 +13,18 @@ export function CloseVault():Promise<void>;
export function CreateVault(arg1:string):Promise<void>;
export function CreateVaultFolder(arg1:string,arg2:string):Promise<string>;
export function CreateWorkspaceNode(arg1:string,arg2:string,arg3:string):Promise<Record<string, any>>;
export function DisablePlugin(arg1:string):Promise<string>;
export function EnablePlugin(arg1:string):Promise<string>;
export function ExecutePluginCommand(arg1:string,arg2:string,arg3:Record<string, any>):Promise<Record<string, any>|string>;
export function EditWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
export function GetAppSettings():Promise<Record<string, any>>;
export function GetCapabilities():Promise<Array<capability.Entry>>;
@@ -29,25 +37,45 @@ export function GetPermissions():Promise<Array<permissions.Entry>>;
export function GetPluginAssetContent(arg1:string,arg2:string):Promise<string|string>;
export function GetPluginCapability(arg1:string,arg2:string):Promise<Record<string, any>|string>;
export function GetPluginFrontendInfo(arg1:string):Promise<Record<string, any>>;
export function GetPlugins():Promise<Array<plugin.Plugin>>;
export function GetVaultFileMetadata(arg1:string,arg2:string):Promise<files.FileMetadata|string>;
export function GetVaultPluginState():Promise<Record<string, any>>;
export function GetVaultStatus():Promise<Record<string, string>>;
export function GetWorkspaceTree():Promise<Record<string, any>>;
export function GetWorkbenchOpenedResources():Promise<Array<workbench.OpenedResource>>;
export function GetWorkbenchPreferences():Promise<workbench.Preferences>;
export function ListPluginCapabilities(arg1:string):Promise<Array<capability.Entry>|string>;
export function ListVaultFiles(arg1:string,arg2:string):Promise<Array<files.FileEntry>|string>;
export function MoveVaultPath(arg1:string,arg2:string,arg3:string,arg4:files.MoveOptions):Promise<string>;
export function MoveWorkspaceNode(arg1:string,arg2:string):Promise<string>;
export function OpenVault(arg1:string):Promise<void>;
export function OpenWorkbenchResource(arg1:string,arg2:Record<string, any>):Promise<workbench.OpenResourceResult|string>;
export function PublishPluginEvent(arg1:string,arg2:string,arg3:Record<string, any>):Promise<string>;
export function ReadPluginDataJSON(arg1:string,arg2:string):Promise<Record<string, any>>;
export function ReadPluginSetting(arg1:string,arg2:string):Promise<any>;
export function ReadPluginSettings(arg1:string):Promise<Record<string, any>>;
export function ReadPluginSettings(arg1:string):Promise<Record<string, any>|string>;
export function ReadVaultTextFile(arg1:string,arg2:string):Promise<string|string>;
export function RecordDesiredPlugin(arg1:string,arg2:string,arg3:string):Promise<string>;
@@ -63,10 +91,20 @@ export function SetCurrentVault(arg1:string):Promise<string>;
export function SetCurrentWorkspaceNode(arg1:string):Promise<string>;
export function SubscribePluginEvent(arg1:string,arg2:string):Promise<string>;
export function TrashVaultPath(arg1:string,arg2:string):Promise<files.TrashResult|string>;
export function UpdateAppSettings(arg1:Record<string, any>):Promise<string>;
export function UpdateWorkbenchPreferences(arg1:workbench.Preferences):Promise<string>;
export function WriteFrontendLog(arg1:string,arg2:string):Promise<void>;
export function WritePluginDataJSON(arg1:string,arg2:string,arg3:Record<string, any>):Promise<string>;
export function WritePluginSetting(arg1:string,arg2:string,arg3:any):Promise<string>;
export function WritePluginSettings(arg1:string,arg2:Record<string, any>):Promise<string>;
export function WriteVaultTextFile(arg1:string,arg2:string,arg3:string,arg4:files.WriteOptions):Promise<string>;
+72
View File
@@ -14,6 +14,10 @@ export function CreateVault(arg1) {
return window['go']['api']['App']['CreateVault'](arg1);
}
export function CreateVaultFolder(arg1, arg2) {
return window['go']['api']['App']['CreateVaultFolder'](arg1, arg2);
}
export function CreateWorkspaceNode(arg1, arg2, arg3) {
return window['go']['api']['App']['CreateWorkspaceNode'](arg1, arg2, arg3);
}
@@ -26,6 +30,14 @@ export function EnablePlugin(arg1) {
return window['go']['api']['App']['EnablePlugin'](arg1);
}
export function ExecutePluginCommand(arg1, arg2, arg3) {
return window['go']['api']['App']['ExecutePluginCommand'](arg1, arg2, arg3);
}
export function EditWorkbenchResource(arg1, arg2) {
return window['go']['api']['App']['EditWorkbenchResource'](arg1, arg2);
}
export function GetAppSettings() {
return window['go']['api']['App']['GetAppSettings']();
}
@@ -50,6 +62,10 @@ export function GetPluginAssetContent(arg1, arg2) {
return window['go']['api']['App']['GetPluginAssetContent'](arg1, arg2);
}
export function GetPluginCapability(arg1, arg2) {
return window['go']['api']['App']['GetPluginCapability'](arg1, arg2);
}
export function GetPluginFrontendInfo(arg1) {
return window['go']['api']['App']['GetPluginFrontendInfo'](arg1);
}
@@ -58,6 +74,10 @@ export function GetPlugins() {
return window['go']['api']['App']['GetPlugins']();
}
export function GetVaultFileMetadata(arg1, arg2) {
return window['go']['api']['App']['GetVaultFileMetadata'](arg1, arg2);
}
export function GetVaultPluginState() {
return window['go']['api']['App']['GetVaultPluginState']();
}
@@ -70,6 +90,26 @@ export function GetWorkspaceTree() {
return window['go']['api']['App']['GetWorkspaceTree']();
}
export function GetWorkbenchOpenedResources() {
return window['go']['api']['App']['GetWorkbenchOpenedResources']();
}
export function GetWorkbenchPreferences() {
return window['go']['api']['App']['GetWorkbenchPreferences']();
}
export function ListPluginCapabilities(arg1) {
return window['go']['api']['App']['ListPluginCapabilities'](arg1);
}
export function ListVaultFiles(arg1, arg2) {
return window['go']['api']['App']['ListVaultFiles'](arg1, arg2);
}
export function MoveVaultPath(arg1, arg2, arg3, arg4) {
return window['go']['api']['App']['MoveVaultPath'](arg1, arg2, arg3, arg4);
}
export function MoveWorkspaceNode(arg1, arg2) {
return window['go']['api']['App']['MoveWorkspaceNode'](arg1, arg2);
}
@@ -78,6 +118,14 @@ export function OpenVault(arg1) {
return window['go']['api']['App']['OpenVault'](arg1);
}
export function OpenWorkbenchResource(arg1, arg2) {
return window['go']['api']['App']['OpenWorkbenchResource'](arg1, arg2);
}
export function PublishPluginEvent(arg1, arg2, arg3) {
return window['go']['api']['App']['PublishPluginEvent'](arg1, arg2, arg3);
}
export function ReadPluginDataJSON(arg1, arg2) {
return window['go']['api']['App']['ReadPluginDataJSON'](arg1, arg2);
}
@@ -90,6 +138,10 @@ export function ReadPluginSettings(arg1) {
return window['go']['api']['App']['ReadPluginSettings'](arg1);
}
export function ReadVaultTextFile(arg1, arg2) {
return window['go']['api']['App']['ReadVaultTextFile'](arg1, arg2);
}
export function RecordDesiredPlugin(arg1, arg2, arg3) {
return window['go']['api']['App']['RecordDesiredPlugin'](arg1, arg2, arg3);
}
@@ -118,10 +170,26 @@ export function SetCurrentWorkspaceNode(arg1) {
return window['go']['api']['App']['SetCurrentWorkspaceNode'](arg1);
}
export function SubscribePluginEvent(arg1, arg2) {
return window['go']['api']['App']['SubscribePluginEvent'](arg1, arg2);
}
export function TrashVaultPath(arg1, arg2) {
return window['go']['api']['App']['TrashVaultPath'](arg1, arg2);
}
export function UpdateAppSettings(arg1) {
return window['go']['api']['App']['UpdateAppSettings'](arg1);
}
export function UpdateWorkbenchPreferences(arg1) {
return window['go']['api']['App']['UpdateWorkbenchPreferences'](arg1);
}
export function WriteFrontendLog(arg1, arg2) {
return window['go']['api']['App']['WriteFrontendLog'](arg1, arg2);
}
export function WritePluginDataJSON(arg1, arg2, arg3) {
return window['go']['api']['App']['WritePluginDataJSON'](arg1, arg2, arg3);
}
@@ -133,3 +201,7 @@ export function WritePluginSetting(arg1, arg2, arg3) {
export function WritePluginSettings(arg1, arg2) {
return window['go']['api']['App']['WritePluginSettings'](arg1, arg2);
}
export function WriteVaultTextFile(arg1, arg2, arg3, arg4) {
return window['go']['api']['App']['WriteVaultTextFile'](arg1, arg2, arg3, arg4);
}
+466 -133
View File
@@ -1,105 +1,45 @@
export namespace api {
export class FlatSidebarItem {
pluginId: string;
id: string;
title: string;
icon?: string;
view: string;
position?: number;
export class FlatOpenProviderSupport {
kind: string;
mime?: string[];
extensions?: string[];
contexts?: string[];
static createFrom(source: any = {}) {
return new FlatSidebarItem(source);
return new FlatOpenProviderSupport(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.view = source["view"];
this.position = source["position"];
this.kind = source["kind"];
this.mime = source["mime"];
this.extensions = source["extensions"];
this.contexts = source["contexts"];
}
}
export class FlatSettingsPanel {
export class FlatOpenProvider {
pluginId: string;
id: string;
title: string;
icon?: string;
priority?: number;
component: string;
supports: FlatOpenProviderSupport[];
static createFrom(source: any = {}) {
return new FlatSettingsPanel(source);
return new FlatOpenProvider(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.priority = source["priority"];
this.component = source["component"];
this.supports = this.convertValues(source["supports"], FlatOpenProviderSupport);
}
}
export class FlatCommand {
pluginId: string;
id: string;
title: string;
icon?: string;
handler?: string;
static createFrom(source: any = {}) {
return new FlatCommand(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.handler = source["handler"];
}
}
export class FlatView {
pluginId: string;
id: string;
title: string;
icon?: string;
component: string;
static createFrom(source: any = {}) {
return new FlatView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.component = source["component"];
}
}
export class ContributionSummary {
views: FlatView[];
commands: FlatCommand[];
settingsPanels: FlatSettingsPanel[];
sidebarItems: FlatSidebarItem[];
static createFrom(source: any = {}) {
return new ContributionSummary(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.views = this.convertValues(source["views"], FlatView);
this.commands = this.convertValues(source["commands"], FlatCommand);
this.settingsPanels = this.convertValues(source["settingsPanels"], FlatSettingsPanel);
this.sidebarItems = this.convertValues(source["sidebarItems"], FlatSidebarItem);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
@@ -118,24 +58,305 @@ export namespace api {
return a;
}
}
export class FlatSidebarItem {
pluginId: string;
id: string;
title: string;
icon?: string;
view: string;
position?: number;
static createFrom(source: any = {}) {
return new FlatSidebarItem(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.view = source["view"];
this.position = source["position"];
}
}
export class FlatSettingsPanel {
pluginId: string;
id: string;
title: string;
icon?: string;
component: string;
static createFrom(source: any = {}) {
return new FlatSettingsPanel(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.component = source["component"];
}
}
export class FlatCommand {
pluginId: string;
id: string;
title: string;
icon?: string;
handler?: string;
static createFrom(source: any = {}) {
return new FlatCommand(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.handler = source["handler"];
}
}
export class FlatView {
pluginId: string;
id: string;
title: string;
icon?: string;
component: string;
static createFrom(source: any = {}) {
return new FlatView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.pluginId = source["pluginId"];
this.id = source["id"];
this.title = source["title"];
this.icon = source["icon"];
this.component = source["component"];
}
}
export class ContributionSummary {
views: FlatView[];
commands: FlatCommand[];
settingsPanels: FlatSettingsPanel[];
sidebarItems: FlatSidebarItem[];
openProviders: FlatOpenProvider[];
static createFrom(source: any = {}) {
return new ContributionSummary(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.views = this.convertValues(source["views"], FlatView);
this.commands = this.convertValues(source["commands"], FlatCommand);
this.settingsPanels = this.convertValues(source["settingsPanels"], FlatSettingsPanel);
this.sidebarItems = this.convertValues(source["sidebarItems"], FlatSidebarItem);
this.openProviders = this.convertValues(source["openProviders"], FlatOpenProvider);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}
export namespace workbench {
export class OpenResourceContext {
sourcePluginId?: string;
sourceView?: string;
isInsideNotesFolder?: boolean;
notesScopePath?: string;
notesMode?: boolean;
static createFrom(source: any = {}) {
return new OpenResourceContext(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.sourcePluginId = source["sourcePluginId"];
this.sourceView = source["sourceView"];
this.isInsideNotesFolder = source["isInsideNotesFolder"];
this.notesScopePath = source["notesScopePath"];
this.notesMode = source["notesMode"];
}
}
export class OpenResourceRequest {
kind: string;
path: string;
mode?: string;
mime?: string;
extension?: string;
context?: OpenResourceContext;
static createFrom(source: any = {}) {
return new OpenResourceRequest(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.kind = source["kind"];
this.path = source["path"];
this.mode = source["mode"];
this.mime = source["mime"];
this.extension = source["extension"];
this.context = this.convertValues(source["context"], OpenResourceContext);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class OpenResourceResult {
status: string;
providerId?: string;
providerPluginId?: string;
providerComponent?: string;
request: OpenResourceRequest;
message?: string;
static createFrom(source: any = {}) {
return new OpenResourceResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.status = source["status"];
this.providerId = source["providerId"];
this.providerPluginId = source["providerPluginId"];
this.providerComponent = source["providerComponent"];
this.request = this.convertValues(source["request"], OpenResourceRequest);
this.message = source["message"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class OpenedResource {
id: string;
providerId: string;
providerPluginId: string;
providerComponent: string;
request: OpenResourceRequest;
openedAt: string;
static createFrom(source: any = {}) {
return new OpenedResource(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.providerId = source["providerId"];
this.providerPluginId = source["providerPluginId"];
this.providerComponent = source["providerComponent"];
this.request = this.convertValues(source["request"], OpenResourceRequest);
this.openedAt = source["openedAt"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class Preferences {
defaultTextEditorProvider?: string;
defaultMarkdownEditorProvider?: string;
defaultNotesMarkdownEditorProvider?: string;
static createFrom(source: any = {}) {
return new Preferences(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.defaultTextEditorProvider = source["defaultTextEditorProvider"];
this.defaultMarkdownEditorProvider = source["defaultMarkdownEditorProvider"];
this.defaultNotesMarkdownEditorProvider = source["defaultNotesMarkdownEditorProvider"];
}
}
}
export namespace capability {
export class Entry {
name: string;
description?: string;
pluginId: string;
status: string;
static createFrom(source: any = {}) {
return new Entry(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
@@ -147,17 +368,130 @@ export namespace capability {
}
export namespace files {
export class FileEntry {
name: string;
relativePath: string;
type: string;
size: number;
modifiedAt: string;
extension: string;
isHidden: boolean;
isReserved: boolean;
canRead: boolean;
canWrite: boolean;
static createFrom(source: any = {}) {
return new FileEntry(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
this.relativePath = source["relativePath"];
this.type = source["type"];
this.size = source["size"];
this.modifiedAt = source["modifiedAt"];
this.extension = source["extension"];
this.isHidden = source["isHidden"];
this.isReserved = source["isReserved"];
this.canRead = source["canRead"];
this.canWrite = source["canWrite"];
}
}
export class FileMetadata {
relativePath: string;
type: string;
size: number;
modifiedAt: string;
createdAt?: string;
extension: string;
mimeHint: string;
isText: boolean;
isHidden: boolean;
isReserved: boolean;
canRead: boolean;
canWrite: boolean;
static createFrom(source: any = {}) {
return new FileMetadata(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.relativePath = source["relativePath"];
this.type = source["type"];
this.size = source["size"];
this.modifiedAt = source["modifiedAt"];
this.createdAt = source["createdAt"];
this.extension = source["extension"];
this.mimeHint = source["mimeHint"];
this.isText = source["isText"];
this.isHidden = source["isHidden"];
this.isReserved = source["isReserved"];
this.canRead = source["canRead"];
this.canWrite = source["canWrite"];
}
}
export class MoveOptions {
overwrite: boolean;
static createFrom(source: any = {}) {
return new MoveOptions(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.overwrite = source["overwrite"];
}
}
export class TrashResult {
originalPath: string;
trashPath: string;
trashId: string;
deletedAt: string;
static createFrom(source: any = {}) {
return new TrashResult(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.originalPath = source["originalPath"];
this.trashPath = source["trashPath"];
this.trashId = source["trashId"];
this.deletedAt = source["deletedAt"];
}
}
export class WriteOptions {
createIfMissing: boolean;
overwrite: boolean;
static createFrom(source: any = {}) {
return new WriteOptions(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.createIfMissing = source["createIfMissing"];
this.overwrite = source["overwrite"];
}
}
}
export namespace permissions {
export class Entry {
name: string;
description: string;
dangerous: boolean;
static createFrom(source: any = {}) {
return new Entry(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.name = source["name"];
@@ -169,15 +503,15 @@ export namespace permissions {
}
export namespace plugin {
export class HealthCheckConfig {
type?: string;
timeout?: number;
static createFrom(source: any = {}) {
return new HealthCheckConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.type = source["type"];
@@ -188,18 +522,18 @@ export namespace plugin {
type: string;
entry: Record<string, string>;
healthCheck?: HealthCheckConfig;
static createFrom(source: any = {}) {
return new BackendConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.type = source["type"];
this.entry = source["entry"];
this.healthCheck = this.convertValues(source["healthCheck"], HealthCheckConfig);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
@@ -224,11 +558,11 @@ export namespace plugin {
icon?: string;
capability?: string;
handler?: string;
static createFrom(source: any = {}) {
return new ContributionAction(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -242,11 +576,11 @@ export namespace plugin {
id: string;
events?: string[];
handler: string;
static createFrom(source: any = {}) {
return new ContributionActivityProvider(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -260,11 +594,11 @@ export namespace plugin {
keybinding?: string;
icon?: string;
handler?: string;
static createFrom(source: any = {}) {
return new ContributionCommand(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -281,11 +615,11 @@ export namespace plugin {
group?: string;
capability?: string;
handler?: string;
static createFrom(source: any = {}) {
return new ContributionContextMenuEntry(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -300,11 +634,11 @@ export namespace plugin {
id: string;
label: string;
handler: string;
static createFrom(source: any = {}) {
return new ContributionSearchProvider(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -317,11 +651,11 @@ export namespace plugin {
title: string;
component: string;
icon?: string;
static createFrom(source: any = {}) {
return new ContributionSettingsPanel(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -336,11 +670,11 @@ export namespace plugin {
icon?: string;
view: string;
position?: number;
static createFrom(source: any = {}) {
return new ContributionSidebarItem(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -355,11 +689,11 @@ export namespace plugin {
label: string;
position?: string;
handler?: string;
static createFrom(source: any = {}) {
return new ContributionStatusBarItem(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -373,11 +707,11 @@ export namespace plugin {
title: string;
icon?: string;
component: string;
static createFrom(source: any = {}) {
return new ContributionView(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
@@ -397,11 +731,11 @@ export namespace plugin {
searchProviders?: ContributionSearchProvider[];
activityProviders?: ContributionActivityProvider[];
statusBarItems?: ContributionStatusBarItem[];
static createFrom(source: any = {}) {
return new Contributions(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.views = this.convertValues(source["views"], ContributionView);
@@ -415,7 +749,7 @@ export namespace plugin {
this.activityProviders = this.convertValues(source["activityProviders"], ContributionActivityProvider);
this.statusBarItems = this.convertValues(source["statusBarItems"], ContributionStatusBarItem);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
@@ -437,26 +771,26 @@ export namespace plugin {
export class FrontendConfig {
entry: string;
style?: string;
static createFrom(source: any = {}) {
return new FrontendConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.entry = source["entry"];
this.style = source["style"];
}
}
export class SyncConfig {
namespaces?: string[];
participate?: boolean;
static createFrom(source: any = {}) {
return new SyncConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.namespaces = source["namespaces"];
@@ -465,11 +799,11 @@ export namespace plugin {
}
export class MigrationConfig {
path?: string;
static createFrom(source: any = {}) {
return new MigrationConfig(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.path = source["path"];
@@ -493,11 +827,11 @@ export namespace plugin {
migrations?: MigrationConfig;
contributes?: Contributions;
sync?: SyncConfig;
static createFrom(source: any = {}) {
return new Manifest(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.schemaVersion = source["schemaVersion"];
@@ -518,7 +852,7 @@ export namespace plugin {
this.contributes = this.convertValues(source["contributes"], Contributions);
this.sync = this.convertValues(source["sync"], SyncConfig);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
@@ -537,18 +871,18 @@ export namespace plugin {
return a;
}
}
export class Plugin {
manifest: Manifest;
status: string;
error?: string;
enabled: boolean;
rootPath: string;
static createFrom(source: any = {}) {
return new Plugin(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.manifest = this.convertValues(source["manifest"], Manifest);
@@ -557,7 +891,7 @@ export namespace plugin {
this.enabled = source["enabled"];
this.rootPath = source["rootPath"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
@@ -578,4 +912,3 @@ export namespace plugin {
}
}