Implement milestone 6b workbench routing skeleton
This commit is contained in:
@@ -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 }
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Generated
+60
@@ -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",
|
||||
|
||||
@@ -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 @@
|
||||
ef6849d398e9d32c4ae5afdea904eb49
|
||||
43be2fbdf6ba6ca9504a7c4b0ac32ae0
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Vendored
+39
-1
@@ -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>;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user