diff --git a/frontend/e2e/activity.spec.js b/frontend/e2e/activity.spec.js
new file mode 100644
index 0000000..a43e0bf
--- /dev/null
+++ b/frontend/e2e/activity.spec.js
@@ -0,0 +1,66 @@
+import { test, expect } from '@playwright/test';
+import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
+
+test.describe('Activity workflow', () => {
+ let consoleCollector;
+
+ test.beforeEach(async ({ page }) => {
+ consoleCollector = setupConsoleCollector(page);
+ await resetMockState(page);
+ await page.goto('/');
+ await waitForAppReady(page);
+ });
+
+ test.afterEach(async () => {
+ consoleCollector.assertNoErrors();
+ });
+
+ test('workspace activity explains empty event flow', async ({ page }) => {
+ await page.getByRole('tab', { name: 'Activity' }).click();
+
+ const activity = page.locator('.activity-root');
+ await expect(activity).toBeVisible({ timeout: 10000 });
+ await expect(activity.locator('.activity-title')).toContainText('Activity');
+ await expect(activity.locator('.activity-count')).toHaveText('0 events');
+ await expect(activity.locator('.activity-empty')).toContainText('No activity events yet');
+ await expect(activity.locator('.activity-empty')).toContainText('File changes, browser captures, and conversions will appear here');
+ await expect(activity.locator('[data-activity-action="clear"]')).toBeDisabled();
+ });
+
+ test('workspace activity renders stored events and worklog suggestions', async ({ page }) => {
+ await page.evaluate(async () => {
+ await window.go.api.App.WritePluginSettings('verstak.activity', {
+ 'events:workspace:Project': [
+ {
+ activityId: 'activity-e2e-capture',
+ occurredAt: '2026-06-30T08:00:00.000Z',
+ type: 'browser.capture.selection',
+ title: 'Research Capture',
+ summary: 'Selected text from the article',
+ sourcePluginId: 'verstak.browser-inbox',
+ workspaceRootPath: 'Project',
+ },
+ {
+ activityId: 'activity-e2e-note',
+ occurredAt: '2026-06-30T08:25:00.000Z',
+ type: 'note.saved',
+ title: 'Saved note',
+ summary: 'Project/Notes/Research Capture.md',
+ sourcePluginId: 'verstak.files',
+ workspaceRootPath: 'Project',
+ },
+ ],
+ });
+ });
+
+ await page.getByRole('tab', { name: 'Activity' }).click();
+
+ const activity = page.locator('.activity-root');
+ await expect(activity.locator('.activity-count')).toHaveText('2 events');
+ await expect(activity.locator('[data-activity-id="activity-e2e-capture"]')).toContainText('Research Capture');
+ await expect(activity.locator('[data-activity-id="activity-e2e-note"]')).toContainText('Saved note');
+ await expect(activity.locator('[data-activity-section="worklog-suggestions"]')).toContainText('Worklog suggestions');
+ await expect(activity.locator('[data-worklog-suggestion="worklog:Project:2026-06-30"]')).toContainText('Project work on 2026-06-30');
+ await expect(activity.locator('[data-worklog-suggestion="worklog:Project:2026-06-30"]')).toContainText('50 min');
+ });
+});
diff --git a/frontend/e2e/browser-inbox.spec.js b/frontend/e2e/browser-inbox.spec.js
new file mode 100644
index 0000000..ceaf054
--- /dev/null
+++ b/frontend/e2e/browser-inbox.spec.js
@@ -0,0 +1,61 @@
+import { test, expect } from '@playwright/test';
+import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
+
+test.describe('Browser Inbox workflow', () => {
+ let consoleCollector;
+
+ test.beforeEach(async ({ page }) => {
+ consoleCollector = setupConsoleCollector(page);
+ await resetMockState(page);
+ await page.goto('/');
+ await waitForAppReady(page);
+ });
+
+ test.afterEach(async () => {
+ consoleCollector.assertNoErrors();
+ });
+
+ test('workspace inbox explains empty capture flow and primary actions', async ({ page }) => {
+ await page.getByRole('tab', { name: 'Browser Inbox' }).click();
+
+ const inbox = page.locator('.browser-inbox-root');
+ await expect(inbox).toBeVisible({ timeout: 10000 });
+ await expect(inbox.locator('.browser-inbox-title')).toContainText('Browser Inbox');
+ await expect(inbox.locator('.browser-inbox-count')).toHaveText('0 items');
+ await expect(inbox.locator('.browser-inbox-empty')).toContainText('No browser captures yet');
+ await expect(inbox.locator('.browser-inbox-empty')).toContainText('send a page, selection, link, or file from the browser extension');
+ await expect(inbox.locator('[data-browser-inbox-action="clear"]')).toBeDisabled();
+ });
+
+ test('workspace inbox renders stored captures with conversion actions', async ({ page }) => {
+ await page.evaluate(async () => {
+ await window.go.api.App.WritePluginSettings('verstak.browser-inbox', {
+ 'captures:workspace:Project': [{
+ captureId: 'capture-e2e-1',
+ capturedAt: '2026-06-30T08:00:00.000Z',
+ kind: 'file',
+ url: 'https://example.com/report',
+ title: 'Research Report',
+ domain: 'example.com',
+ text: 'Selected page text',
+ fileName: 'report.txt',
+ fileText: 'Report file contents',
+ workspaceRootPath: 'Project',
+ browserName: 'Firefox',
+ }],
+ });
+ });
+ await page.getByRole('tab', { name: 'Browser Inbox' }).click();
+
+ const inbox = page.locator('.browser-inbox-root');
+ await expect(inbox.locator('.browser-inbox-count')).toHaveText('1 item');
+ await expect(inbox.locator('[data-browser-capture-id="capture-e2e-1"]')).toContainText('Research Report');
+ await expect(inbox.locator('.browser-inbox-detail-title')).toHaveText('Research Report');
+ await expect(inbox.locator('.browser-inbox-meta')).toContainText('example.com');
+ await expect(inbox.locator('.browser-inbox-text').first()).toContainText('Selected page text');
+ await expect(inbox.locator('[data-browser-inbox-action="create-note"]')).toBeVisible();
+ await expect(inbox.locator('[data-browser-inbox-action="create-link"]')).toBeVisible();
+ await expect(inbox.locator('[data-browser-inbox-action="create-file"]')).toBeVisible();
+ await expect(inbox.locator('[data-browser-inbox-action="remove"]')).toBeVisible();
+ });
+});
diff --git a/frontend/e2e/command-palette.spec.js b/frontend/e2e/command-palette.spec.js
index c91fe7d..9d6a3d7 100644
--- a/frontend/e2e/command-palette.spec.js
+++ b/frontend/e2e/command-palette.spec.js
@@ -45,4 +45,83 @@ test.describe('Command Palette', () => {
await expect(page.locator('.command-palette')).not.toBeVisible();
await expect(page.locator('.workspace-host')).toBeVisible();
});
+
+ test('shows user workflow commands before diagnostics and opens workspace tools', async ({ page }) => {
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
+
+ const palette = page.locator('.command-palette');
+ await expect(palette).toBeVisible();
+ const items = palette.locator('.command-palette-item');
+ await expect(items.nth(0)).toHaveAttribute('data-command-id', 'verstak.shell.open-today');
+ await expect(items.nth(1)).toHaveAttribute('data-command-id', 'verstak.shell.open-files');
+ await expect(items.nth(2)).toHaveAttribute('data-command-id', 'verstak.shell.open-activity');
+ await expect(items.nth(3)).toHaveAttribute('data-command-id', 'verstak.shell.open-browser-inbox');
+
+ await palette.locator('[data-command-palette-input]').fill('activity');
+ await expect(palette.locator('[data-command-id="verstak.shell.open-activity"]')).toBeVisible();
+ await expect(palette.locator('[data-command-id="verstak.platform-test.run-tests"]')).not.toBeVisible();
+
+ await page.keyboard.press('Enter');
+
+ await expect(palette).not.toBeVisible();
+ await expect(page.getByRole('tab', { name: 'Activity' })).toHaveAttribute('aria-selected', 'true');
+ await expect(page.locator('.activity-root')).toBeVisible({ timeout: 10000 });
+ });
+
+ test('starts file creation workflows from user commands', async ({ page }) => {
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
+
+ let palette = page.locator('.command-palette');
+ await palette.locator('[data-command-palette-input]').fill('markdown');
+ await expect(palette.locator('[data-command-id="verstak.shell.create-markdown"]')).toBeVisible();
+ await page.keyboard.press('Enter');
+
+ await expect(palette).not.toBeVisible();
+ await expect(page.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true');
+ await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('[data-files-create-input]')).toBeVisible();
+
+ await page.locator('[data-files-create-input]').fill('Palette Note.md');
+ await page.locator('[data-files-create-confirm]').click();
+ await expect(page.locator('[data-file-name="Palette Note.md"]')).toBeVisible();
+
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
+ palette = page.locator('.command-palette');
+ await palette.locator('[data-command-palette-input]').fill('text file');
+ await expect(palette.locator('[data-command-id="verstak.shell.create-text"]')).toBeVisible();
+ await page.keyboard.press('Enter');
+
+ await expect(page.locator('[data-files-create-input]')).toBeVisible();
+ await page.locator('[data-files-create-input]').fill('Palette Text.txt');
+ await page.locator('[data-files-create-confirm]').click();
+ await expect(page.locator('[data-file-name="Palette Text.txt"]')).toBeVisible();
+ });
+
+ test('runs sync workflow commands', async ({ page }) => {
+ await page.evaluate(async () => {
+ const err = await window.go.api.App.PluginSyncConfigure('verstak.sync', 'https://sync.example.test', 'alice', 'secret');
+ if (err) throw new Error(err);
+ });
+
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
+
+ let palette = page.locator('.command-palette');
+ await palette.locator('[data-command-palette-input]').fill('sync now');
+ await expect(palette.locator('[data-command-id="verstak.shell.sync-now"]')).toBeVisible();
+ await page.keyboard.press('Enter');
+
+ await expect(palette).not.toBeVisible();
+ await expect(page.locator('[data-command-palette-status="success"]')).toContainText('Sync Now');
+ await expect(page.locator('[data-command-palette-status="success"]')).toContainText('handled');
+
+ await page.keyboard.press(process.platform === 'darwin' ? 'Meta+K' : 'Control+K');
+ palette = page.locator('.command-palette');
+ await palette.locator('[data-command-palette-input]').fill('sync settings');
+ await expect(palette.locator('[data-command-id="verstak.shell.open-sync-settings"]')).toBeVisible();
+ await page.keyboard.press('Enter');
+
+ await expect(page.locator('.plugin-manager')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('.modal[aria-label="Plugin Settings"]')).toBeVisible({ timeout: 10000 });
+ await expect(page.locator('.modal-header h3')).toContainText('Sync');
+ });
});
diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js
index 1c75e5d..b0fd5d2 100644
--- a/frontend/e2e/files-plugin.spec.js
+++ b/frontend/e2e/files-plugin.spec.js
@@ -1,6 +1,11 @@
import { test, expect } from '@playwright/test';
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
+async function openFilesTool(page) {
+ await page.getByRole('tab', { name: 'Files' }).click();
+ await expect(page.locator('.files-root')).toBeVisible({ timeout: 10000 });
+}
+
test.describe('G: Files Plugin', () => {
let consoleCollector;
@@ -29,6 +34,7 @@ test.describe('G: Files Plugin', () => {
test('workspace Files view is scoped to selected workspace folder', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.files-item-name').filter({ hasText: 'project-only.txt' })).toBeVisible({ timeout: 10000 });
@@ -42,6 +48,7 @@ test.describe('G: Files Plugin', () => {
test('files explorer supports create navigate rename filter sort open and trash', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
await page.locator('[data-files-action="new-folder"]').click();
@@ -90,6 +97,7 @@ test.describe('G: Files Plugin', () => {
test('files explorer uses labeled controls and no row New Here action', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
for (const action of ['back', 'forward', 'up', 'refresh', 'new-folder', 'new-markdown', 'new-text', 'open', 'rename', 'trash', 'cut', 'copy', 'paste']) {
@@ -108,6 +116,7 @@ test.describe('G: Files Plugin', () => {
test('files explorer supports empty-space context paste after cutting a folder', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
await page.locator('[data-files-action="new-folder"]').click();
@@ -132,6 +141,7 @@ test.describe('G: Files Plugin', () => {
test('files explorer supports multiselect and internal drag/drop move', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
await page.locator('[data-files-action="new-folder"]').click();
@@ -166,6 +176,7 @@ test.describe('G: Files Plugin', () => {
test('files history persists in workspace context and handles mouse back forward buttons', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
await page.locator('[data-file-name="Notes"]').dblclick();
@@ -191,6 +202,7 @@ test.describe('G: Files Plugin', () => {
test('workbench close and mouse back return from editor to the previous Files folder', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await openFilesTool(page);
await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 });
await page.locator('[data-file-name="Notes"]').dblclick();
diff --git a/frontend/e2e/plugin-manager-layout.spec.js b/frontend/e2e/plugin-manager-layout.spec.js
index f9132da..69106b3 100644
--- a/frontend/e2e/plugin-manager-layout.spec.js
+++ b/frontend/e2e/plugin-manager-layout.spec.js
@@ -84,15 +84,21 @@ test.describe('E: Plugin Manager layout', () => {
await expect(selected).toHaveText('Test');
});
- test('workspace tools render as tabs with Files as one tab', async ({ page }) => {
+ test('workspace tools render Today first with Files as one tab', async ({ page }) => {
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
const tabs = page.locator('.workspace-tabs');
await expect(tabs).toBeVisible({ timeout: 10000 });
+ const todayTab = tabs.locator('[role="tab"]').filter({ hasText: 'Today' });
const filesTab = tabs.locator('[role="tab"]').filter({ hasText: 'Files' });
+ await expect(todayTab).toBeVisible();
+ await expect(todayTab).toHaveAttribute('aria-selected', 'true');
await expect(filesTab).toBeVisible();
- await expect(filesTab).toHaveAttribute('aria-selected', 'true');
+ await expect(filesTab).toHaveAttribute('aria-selected', 'false');
await expect(page.locator('.workspace-tool')).toHaveCount(0);
+ await expect(page.locator('.today-root')).toBeVisible();
+
+ await filesTab.click();
await expect(page.locator('.files-root')).toBeVisible();
});
diff --git a/frontend/e2e/ux-p0.spec.js b/frontend/e2e/ux-p0.spec.js
index 579c68e..103237c 100644
--- a/frontend/e2e/ux-p0.spec.js
+++ b/frontend/e2e/ux-p0.spec.js
@@ -58,6 +58,7 @@ test.describe('UX quick wins', () => {
await page.goto('/');
await waitForAppReady(page);
await page.locator('.wt-label').filter({ hasText: 'Project' }).click();
+ await page.getByRole('tab', { name: 'Files' }).click();
const files = page.locator('.files-root');
await expect(files).toBeVisible({ timeout: 10000 });
diff --git a/frontend/e2e/ux-today.spec.js b/frontend/e2e/ux-today.spec.js
new file mode 100644
index 0000000..cd2e753
--- /dev/null
+++ b/frontend/e2e/ux-today.spec.js
@@ -0,0 +1,42 @@
+import { test, expect } from '@playwright/test';
+import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js';
+
+test.describe('UX Today workspace flow', () => {
+ let consoleCollector;
+
+ test.beforeEach(async ({ page }) => {
+ consoleCollector = setupConsoleCollector(page);
+ await resetMockState(page);
+ await page.goto('/');
+ await waitForAppReady(page);
+ });
+
+ test.afterEach(async () => {
+ consoleCollector.assertNoErrors();
+ });
+
+ test('workspace opens with Today before plugin tools', async ({ page }) => {
+ await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 });
+
+ const tabs = page.getByRole('tab');
+ await expect(tabs.nth(0)).toHaveText('Today');
+ await expect(tabs.nth(1)).toHaveText('Files');
+ await expect(page.getByRole('tab', { name: 'Today' })).toHaveAttribute('aria-selected', 'true');
+
+ const today = page.locator('.today-root');
+ await expect(today).toBeVisible();
+ await expect(today.locator('[data-today-section="captured"]')).toContainText('Captured');
+ await expect(today.locator('[data-today-section="activity"]')).toContainText('Recent Activity');
+ await expect(today.locator('[data-today-section="worklog"]')).toContainText('Worklog Suggestions');
+ await expect(today.locator('[data-today-section="quick-actions"]')).toContainText('Quick Actions');
+ await expect(today).toContainText('No browser captures yet');
+ await expect(today).toContainText('No activity events yet');
+ });
+
+ test('Today quick action opens Browser Inbox workspace tool', async ({ page }) => {
+ await page.locator('[data-today-action="browser-inbox"]').click();
+
+ await expect(page.getByRole('tab', { name: 'Browser Inbox' })).toHaveAttribute('aria-selected', 'true');
+ await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 });
+ });
+});
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index 3d7ce0a..c87abd9 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -25,6 +25,7 @@
let workspaceNodes = [];
let selectedWorkspaceName = '';
+ let activeWorkspaceToolKey = '';
let navigationStack = [];
let navigationIndex = -1;
let applyingNavigation = false;
@@ -113,6 +114,7 @@
activeSettingsPanelId,
openedResource,
selectedWorkspaceName,
+ activeWorkspaceToolKey,
};
}
@@ -139,6 +141,7 @@
activeSettingsPanelId = snapshot.activeSettingsPanelId;
openedResource = snapshot.openedResource;
selectedWorkspaceName = snapshot.selectedWorkspaceName;
+ activeWorkspaceToolKey = snapshot.activeWorkspaceToolKey || '';
emitWorkspaceActive(currentView === 'workspace' ? selectedWorkspaceName : '');
applyingNavigation = false;
}
@@ -272,11 +275,17 @@
function onWorkbenchOpened(e) {
debug.log('[App] onWorkbenchOpened:', e.detail?.request?.path, e.detail?.providerId);
+ if (currentView === 'workspace') pushNavigation();
openedResource = e.detail;
currentView = 'workbench';
pushNavigation();
}
+ function onWorkspaceToolSelected(e) {
+ activeWorkspaceToolKey = e.detail?.toolKey || '';
+ if (currentView === 'workspace') pushNavigation();
+ }
+
function onWorkspaceSelected(e) {
debug.log('[App] onWorkspaceSelected:', e.detail?.workspaceName);
selectedWorkspaceName = e.detail?.workspaceName || '';
@@ -351,6 +360,7 @@
window.addEventListener('verstak:close-settings', onCloseSettings);
window.addEventListener('verstak:workbench-opened', onWorkbenchOpened);
window.addEventListener('verstak:workspace-selected', onWorkspaceSelected);
+ window.addEventListener('verstak:workspace-tool-selected', onWorkspaceToolSelected);
window.addEventListener('verstak:navigate-back', onNavigateBack);
window.addEventListener('verstak:navigate-forward', onNavigateForward);
window.addEventListener('verstak:close-workbench', onCloseWorkbench);
@@ -385,7 +395,11 @@
{:else if currentView === 'workbench'}
{:else if currentView === 'workspace' || currentView === 'workspace-empty'}
-
+
{:else}
{/if}
diff --git a/frontend/src/lib/shell/CommandPalette.svelte b/frontend/src/lib/shell/CommandPalette.svelte
index 7e17838..4262ee4 100644
--- a/frontend/src/lib/shell/CommandPalette.svelte
+++ b/frontend/src/lib/shell/CommandPalette.svelte
@@ -12,6 +12,17 @@
let statusType = '';
const inactiveStatuses = new Set(['disabled', 'failed', 'incompatible', 'missing-required-capability']);
+ const shellCommands = [
+ { id: 'verstak.shell.open-today', title: 'Open Today', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 10, shellAction: 'today' },
+ { id: 'verstak.shell.open-files', title: 'Open Files', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 20, shellAction: 'files' },
+ { id: 'verstak.shell.open-activity', title: 'Open Activity', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 30, shellAction: 'activity' },
+ { id: 'verstak.shell.open-browser-inbox', title: 'Open Browser Inbox', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 40, shellAction: 'browser-inbox' },
+ { id: 'verstak.shell.create-markdown', title: 'Create Markdown File', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 50, shellAction: 'create-markdown' },
+ { id: 'verstak.shell.create-text', title: 'Create Text File', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 60, shellAction: 'create-text' },
+ { id: 'verstak.shell.sync-now', title: 'Sync Now', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 70, shellAction: 'sync-now' },
+ { id: 'verstak.shell.open-sync-settings', title: 'Open Sync Settings', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 80, shellAction: 'sync-settings' },
+ { id: 'verstak.shell.open-plugin-manager', title: 'Open Plugin Manager', pluginId: 'verstak.shell', pluginName: 'Verstak', priority: 90, shellAction: 'plugin-manager' },
+ ];
$: normalizedQuery = query.trim().toLowerCase();
$: filteredCommands = commands.filter((command) => {
@@ -33,7 +44,7 @@
App.GetContributions().catch(() => ({})),
]);
const pluginById = new Map((plugins || []).map((plugin) => [plugin.manifest?.id, plugin]));
- commands = (contributions.commands || [])
+ const pluginCommands = (contributions.commands || [])
.filter((command) => {
const plugin = pluginById.get(command.pluginId);
if (!plugin) return false;
@@ -44,13 +55,16 @@
return {
...command,
pluginName: plugin?.manifest?.name || command.pluginId,
+ priority: 1000,
};
- })
- .sort((a, b) => {
- const title = String(a.title || a.id).localeCompare(String(b.title || b.id));
- if (title) return title;
- return String(a.pluginId).localeCompare(String(b.pluginId));
});
+ commands = [...shellCommands, ...pluginCommands].sort((a, b) => {
+ const priority = (a.priority || 1000) - (b.priority || 1000);
+ if (priority) return priority;
+ const title = String(a.title || a.id).localeCompare(String(b.title || b.id));
+ if (title) return title;
+ return String(a.pluginId).localeCompare(String(b.pluginId));
+ });
}
async function openPalette() {
@@ -78,9 +92,87 @@
}, 4000);
}
+ function clickWorkspaceTool(label) {
+ const tabs = Array.from(document.querySelectorAll('.workspace-tabs [role="tab"]'));
+ const tab = tabs.find((node) => String(node.textContent || '').trim().toLowerCase() === label);
+ if (tab) {
+ tab.click();
+ return true;
+ }
+ return false;
+ }
+
+ async function openWorkspaceTool(label) {
+ if (clickWorkspaceTool(label)) return;
+ const selectedWorkspace = document.querySelector('.wt-node.selected .wt-label');
+ const firstWorkspace = document.querySelector('.wt-label');
+ const workspaceButton = selectedWorkspace || firstWorkspace;
+ if (workspaceButton) {
+ workspaceButton.click();
+ await tick();
+ await new Promise((resolve) => requestAnimationFrame(resolve));
+ }
+ clickWorkspaceTool(label);
+ }
+
+ async function startFilesCreate(action) {
+ await openWorkspaceTool('files');
+ await tick();
+ await new Promise((resolve) => requestAnimationFrame(resolve));
+ const button = document.querySelector(`[data-files-action="${action}"]`);
+ if (!button) {
+ throw new Error(`Files action not available: ${action}`);
+ }
+ button.click();
+ }
+
+ async function runShellCommand(command) {
+ if (command.shellAction === 'plugin-manager') {
+ window.dispatchEvent(new CustomEvent('verstak:open-settings', { detail: {} }));
+ return;
+ }
+ if (command.shellAction === 'sync-settings') {
+ window.dispatchEvent(new CustomEvent('verstak:open-settings', {
+ detail: { pluginId: 'verstak.sync', panelId: 'verstak.sync.settings' }
+ }));
+ return;
+ }
+ if (command.shellAction === 'sync-now') {
+ const result = await App.PluginSyncNow('verstak.sync');
+ if (typeof result === 'string' && result) {
+ throw new Error(result);
+ }
+ if (Array.isArray(result) && result[1]) {
+ throw new Error(result[1]);
+ }
+ return;
+ }
+ if (command.shellAction === 'create-markdown') {
+ await startFilesCreate('new-markdown');
+ return;
+ }
+ if (command.shellAction === 'create-text') {
+ await startFilesCreate('new-text');
+ return;
+ }
+ const actionToTab = {
+ today: 'today',
+ files: 'files',
+ activity: 'activity',
+ 'browser-inbox': 'browser inbox',
+ };
+ await openWorkspaceTool(actionToTab[command.shellAction] || command.shellAction);
+ }
+
async function runCommand(command) {
if (!command) return;
try {
+ if (command.shellAction) {
+ await runShellCommand(command);
+ closePalette();
+ setStatus('success', `${command.title || command.id} handled`);
+ return;
+ }
const result = await executePluginCommand(command.pluginId, command.id, {
source: 'command-palette',
});
diff --git a/frontend/src/lib/shell/TodaySurface.svelte b/frontend/src/lib/shell/TodaySurface.svelte
new file mode 100644
index 0000000..9de76af
--- /dev/null
+++ b/frontend/src/lib/shell/TodaySurface.svelte
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
Captured
+
+
+ {#if loading}
+ Loading captures...
+ {:else if captures.length}
+
+ {#each captures as capture}
+
+ {captureTitle(capture)}
+ {capture.url || capture.domain || capture.kind || 'Browser capture'}
+
+ {/each}
+
+ {:else}
+ No browser captures yet. Send a page, selection, link, or file from the browser extension.
+ {/if}
+
+
+
+
+
Recent Activity
+ {#if hasActivity}
+
+ {/if}
+
+ {#if loading}
+ Loading activity...
+ {:else if activity.length}
+
+ {#each activity as item}
+
+ {activityTitle(item)}
+ {item.occurredAt || item.receivedAt || item.type || 'Activity event'}
+
+ {/each}
+
+ {:else}
+ No activity events yet. File changes, captures, and conversions will appear here.
+ {/if}
+
+
+
+
+
Worklog Suggestions
+
+ {#if loading}
+ Loading worklog suggestions...
+ {:else if worklogSuggestions.length}
+
+ {#each worklogSuggestions as item}
+
+ {worklogTitle(item)}
+ {item.minutes ? item.minutes + ' min' : item.date || 'Suggested worklog'}
+
+ {/each}
+
+ {:else}
+ No worklog suggestions yet. Activity will be grouped into suggestions once work starts.
+ {/if}
+
+
+
+
+
Quick Actions
+
+
+ {#if hasFiles}
+
+ {/if}
+
+ {#if hasActivity}
+
+ {/if}
+
+
+
+
+
+
diff --git a/frontend/src/lib/shell/WorkspaceHost.svelte b/frontend/src/lib/shell/WorkspaceHost.svelte
index a824f5c..889bb95 100644
--- a/frontend/src/lib/shell/WorkspaceHost.svelte
+++ b/frontend/src/lib/shell/WorkspaceHost.svelte
@@ -1,14 +1,17 @@
@@ -78,16 +106,16 @@
- {#if workspaceTools.length > 0}
+ {#if displayedTools.length > 0}
- {#each workspaceTools as tool (tool.id + tool.pluginId)}
+ {#each displayedTools as tool (tool.id + tool.pluginId)}
@@ -95,11 +123,20 @@
{#if activeTool}
-
+ {#if activeTool.shell}
+
+ {:else}
+
+ {/if}
{/if}
{:else}
diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js
index f4ea14d..63514be 100644
--- a/frontend/src/lib/test/wails-mock.js
+++ b/frontend/src/lib/test/wails-mock.js
@@ -1002,21 +1002,424 @@
}
function activityBundle() {
- return [
- "(function(){",
- "var ActivityView={mount:function(containerEl){containerEl.innerHTML='';var root=document.createElement('div');root.className='activity-root';root.setAttribute('data-plugin-id','verstak.activity');var title=document.createElement('h2');title.textContent='Activity';var body=document.createElement('div');body.textContent='Global activity feed';root.appendChild(title);root.appendChild(body);containerEl.appendChild(root);},unmount:function(containerEl){containerEl.innerHTML='';}};",
- "window.VerstakPluginRegister('verstak.activity',{components:{ActivityView:ActivityView}});",
- "})();"
- ].join('');
+ return '(' + function () {
+ var PLUGIN_ID = 'verstak.activity';
+
+ function injectStyles() {
+ if (document.getElementById('mock-activity-style')) return;
+ var style = document.createElement('style');
+ style.id = 'mock-activity-style';
+ style.textContent = [
+ '.activity-root{height:100%;min-height:0;display:flex;flex-direction:column;background:#0d0d1a;color:#e0e0f0}',
+ '.activity-toolbar{display:flex;align-items:center;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid #16213e;background:#12122a;flex-wrap:wrap}',
+ '.activity-title{font-size:.86rem;font-weight:600;color:#f0f0ff}.activity-count,.activity-status{font-size:.74rem;color:#8b8ba8}.activity-spacer{flex:1}',
+ '.activity-btn{min-height:1.85rem;padding:.3rem .65rem;border:1px solid #1a3a5c;border-radius:4px;background:#0f3460;color:#e0e0f0;font-size:.76rem;cursor:pointer}.activity-btn:hover{background:#1a4a7a}.activity-btn:disabled{opacity:.45;cursor:default}.activity-btn.danger{border-color:#633;color:#ffb0b0}',
+ '.activity-suggestions{display:grid;gap:.5rem;padding:.65rem .75rem;border-bottom:1px solid rgba(22,33,62,.75);background:#111126}.activity-suggestions-title{font-size:.76rem;font-weight:600;color:#8b8ba8;text-transform:uppercase}.activity-suggestion{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.65rem;padding:.55rem .65rem;border:1px solid rgba(78,204,163,.24);border-radius:4px;background:#14142c}.activity-suggestion-title{font-size:.84rem;font-weight:600;color:#f4f7fb}.activity-suggestion-summary{margin-top:.22rem;color:#aaa;font-size:.76rem;line-height:1.4}.activity-suggestion-minutes{color:#4ecca3;font-size:.76rem;white-space:nowrap}',
+ '.activity-list{flex:1;min-height:0;overflow:auto;background:#101020}.activity-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:1.5rem;color:#8b8ba8;font-size:.84rem;line-height:1.45;text-align:center}.activity-row{display:grid;grid-template-columns:9.5rem minmax(0,1fr);gap:.75rem;padding:.72rem .85rem;border-bottom:1px solid rgba(22,33,62,.7)}.activity-time{font-size:.72rem;color:#777;white-space:nowrap}.activity-main{min-width:0}.activity-row-head{display:flex;align-items:center;gap:.45rem;min-width:0}.activity-type{font-size:.68rem;color:#4ecca3;text-transform:uppercase;letter-spacing:.04em}.activity-title-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e0e0f0;font-size:.86rem}.activity-summary{margin-top:.25rem;color:#aaa;font-size:.78rem;line-height:1.4}.activity-source{margin-top:.25rem;color:#777;font-size:.72rem}',
+ '@media(max-width:760px){.activity-row,.activity-suggestion{grid-template-columns:1fr;gap:.25rem}.activity-status{width:100%}}'
+ ].join('');
+ document.head.appendChild(style);
+ }
+
+ function el(tag, attrs, children) {
+ var node = document.createElement(tag);
+ attrs = attrs || {};
+ Object.keys(attrs).forEach(function (key) {
+ if (key === 'textContent') node.textContent = attrs[key];
+ else if (key === 'className') node.className = attrs[key];
+ else if (key === 'disabled') node.disabled = !!attrs[key];
+ else if (key.indexOf('data-') === 0) node.setAttribute(key, attrs[key]);
+ else if (key === 'onClick') node.addEventListener('click', attrs[key]);
+ else node[key] = attrs[key];
+ });
+ (children || []).forEach(function (child) {
+ if (child) node.appendChild(child);
+ });
+ return node;
+ }
+
+ function workspaceRoot(props) {
+ return String(
+ props.workspaceRootPath ||
+ (props.workspaceNode && (props.workspaceNode.rootPath || props.workspaceNode.name || props.workspaceNode.id)) ||
+ props.workspaceName ||
+ ''
+ ).trim();
+ }
+
+ function workspaceKey(root) {
+ return 'events:workspace:' + encodeURIComponent(root || '');
+ }
+
+ function rows(value) {
+ return Array.isArray(value) ? value.filter(function (item) { return item && typeof item === 'object'; }) : [];
+ }
+
+ function eventTime(value) {
+ var date = new Date(value || 0);
+ return isNaN(date.getTime()) ? 0 : date.getTime();
+ }
+
+ function eventDay(activity) {
+ var value = activity.occurredAt || activity.receivedAt || new Date().toISOString();
+ return String(value).slice(0, 10);
+ }
+
+ function formatDate(value) {
+ var date = new Date(value || 0);
+ if (isNaN(date.getTime())) return '-';
+ return date.toLocaleString(undefined, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' });
+ }
+
+ function title(activity) {
+ return activity.title || activity.summary || activity.type || activity.activityId || 'Activity event';
+ }
+
+ function summarize(group) {
+ return group.map(title).filter(Boolean).slice(0, 3).join('; ') || group.length + ' activity events';
+ }
+
+ function suggestions(events, root) {
+ var groups = {};
+ events.forEach(function (activity) {
+ var workspace = activity.workspaceRootPath || root || 'Global';
+ var day = eventDay(activity);
+ var key = workspace + '|' + day;
+ groups[key] = groups[key] || { workspaceRootPath: workspace, date: day, events: [] };
+ groups[key].events.push(activity);
+ });
+ return Object.keys(groups).map(function (key) {
+ var group = groups[key];
+ var ordered = group.events.slice().sort(function (a, b) { return eventTime(a.occurredAt || a.receivedAt) - eventTime(b.occurredAt || b.receivedAt); });
+ return {
+ suggestionId: 'worklog:' + group.workspaceRootPath + ':' + group.date,
+ title: group.workspaceRootPath + ' work on ' + group.date,
+ summary: summarize(ordered),
+ minutes: Math.max(25, ordered.length * 25)
+ };
+ }).sort(function (a, b) { return b.suggestionId.localeCompare(a.suggestionId); });
+ }
+
+ function renderActivity(containerEl, props, api) {
+ injectStyles();
+ var rootPath = workspaceRoot(props || {});
+ var key = workspaceKey(rootPath);
+ var events = [];
+ var statusText = 'Listening for workspace activity';
+
+ containerEl.innerHTML = '';
+ var root = el('div', { className: 'activity-root', 'data-plugin-id': PLUGIN_ID });
+ var toolbar = el('div', { className: 'activity-toolbar' });
+ var titleEl = el('span', { className: 'activity-title', textContent: rootPath ? 'Activity · ' + rootPath : 'Activity' });
+ var countEl = el('span', { className: 'activity-count' });
+ var statusEl = el('span', { className: 'activity-status' });
+ var clearBtn = el('button', {
+ className: 'activity-btn danger',
+ 'data-activity-action': 'clear',
+ textContent: 'Clear',
+ onClick: function () {
+ events = [];
+ persist().then(render);
+ }
+ });
+ var suggestionsEl = el('div', { className: 'activity-suggestions', 'data-activity-section': 'worklog-suggestions' });
+ var listEl = el('div', { className: 'activity-list' });
+
+ toolbar.appendChild(titleEl);
+ toolbar.appendChild(countEl);
+ toolbar.appendChild(el('span', { className: 'activity-spacer' }));
+ toolbar.appendChild(statusEl);
+ toolbar.appendChild(clearBtn);
+ root.appendChild(toolbar);
+ root.appendChild(suggestionsEl);
+ root.appendChild(listEl);
+ containerEl.appendChild(root);
+
+ function readSettings() {
+ if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve({});
+ return api.settings.read().then(function (settings) { return settings || {}; }).catch(function () { return {}; });
+ }
+
+ function persist() {
+ if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
+ return api.settings.write(key, events.slice()).catch(function (err) {
+ statusText = 'Could not save activity: ' + (err && err.message ? err.message : String(err));
+ });
+ }
+
+ function renderSuggestions() {
+ suggestionsEl.innerHTML = '';
+ var items = suggestions(events, rootPath);
+ if (!items.length) return;
+ suggestionsEl.appendChild(el('div', { className: 'activity-suggestions-title', textContent: 'Worklog suggestions' }));
+ items.forEach(function (item) {
+ suggestionsEl.appendChild(el('div', { className: 'activity-suggestion', 'data-worklog-suggestion': item.suggestionId }, [
+ el('div', {}, [
+ el('div', { className: 'activity-suggestion-title', textContent: item.title }),
+ el('div', { className: 'activity-suggestion-summary', textContent: item.summary })
+ ]),
+ el('div', { className: 'activity-suggestion-minutes', textContent: item.minutes + ' min' })
+ ]));
+ });
+ }
+
+ function renderList() {
+ listEl.innerHTML = '';
+ if (!events.length) {
+ listEl.appendChild(el('div', {
+ className: 'activity-empty',
+ textContent: 'No activity events yet. File changes, browser captures, and conversions will appear here.'
+ }));
+ return;
+ }
+ events.slice().sort(function (a, b) { return eventTime(b.occurredAt || b.receivedAt) - eventTime(a.occurredAt || a.receivedAt); }).forEach(function (activity) {
+ listEl.appendChild(el('div', { className: 'activity-row', 'data-activity-id': activity.activityId }, [
+ el('div', { className: 'activity-time', textContent: formatDate(activity.occurredAt || activity.receivedAt) }),
+ el('div', { className: 'activity-main' }, [
+ el('div', { className: 'activity-row-head' }, [
+ el('span', { className: 'activity-type', textContent: activity.type || 'activity.event' }),
+ el('span', { className: 'activity-title-text', textContent: title(activity) })
+ ]),
+ activity.summary ? el('div', { className: 'activity-summary', textContent: activity.summary }) : null,
+ activity.sourcePluginId ? el('div', { className: 'activity-source', textContent: activity.sourcePluginId }) : null
+ ])
+ ]));
+ });
+ }
+
+ function render() {
+ countEl.textContent = events.length + ' event' + (events.length === 1 ? '' : 's');
+ clearBtn.disabled = events.length === 0;
+ statusEl.textContent = statusText;
+ renderSuggestions();
+ renderList();
+ }
+
+ readSettings().then(function (settings) {
+ events = rows(settings[key]);
+ render();
+ });
+ render();
+ }
+
+ var ActivityView = {
+ mount: renderActivity,
+ unmount: function (containerEl) { containerEl.innerHTML = ''; }
+ };
+ window.VerstakPluginRegister('verstak.activity', { components: { ActivityView: ActivityView } });
+ }.toString() + ')();';
}
function browserInboxBundle() {
- return [
- "(function(){",
- "var BrowserInboxView={mount:function(containerEl){containerEl.innerHTML='';var root=document.createElement('div');root.className='browser-inbox-root';root.setAttribute('data-plugin-id','verstak.browser-inbox');var title=document.createElement('h2');title.textContent='Browser Inbox';var body=document.createElement('div');body.textContent='Global browser inbox';root.appendChild(title);root.appendChild(body);containerEl.appendChild(root);},unmount:function(containerEl){containerEl.innerHTML='';}};",
- "window.VerstakPluginRegister('verstak.browser-inbox',{components:{BrowserInboxView:BrowserInboxView}});",
- "})();"
- ].join('');
+ return '(' + function () {
+ var PLUGIN_ID = 'verstak.browser-inbox';
+
+ function injectStyles() {
+ if (document.getElementById('mock-browser-inbox-style')) return;
+ var style = document.createElement('style');
+ style.id = 'mock-browser-inbox-style';
+ style.textContent = [
+ '.browser-inbox-root{height:100%;min-height:0;display:flex;flex-direction:column;background:#0d0d1a;color:#e0e0f0}',
+ '.browser-inbox-toolbar{display:flex;align-items:center;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid #16213e;background:#12122a;flex-wrap:wrap}',
+ '.browser-inbox-title{font-size:.86rem;font-weight:600;color:#f0f0ff}.browser-inbox-count,.browser-inbox-status{font-size:.74rem;color:#8b8ba8}.browser-inbox-spacer{flex:1}',
+ '.browser-inbox-btn{min-height:1.85rem;padding:.3rem .65rem;border:1px solid #1a3a5c;border-radius:4px;background:#0f3460;color:#e0e0f0;font-size:.76rem;cursor:pointer}.browser-inbox-btn:hover{background:#1a4a7a}.browser-inbox-btn:disabled{opacity:.45;cursor:default}.browser-inbox-btn.danger{border-color:#633;color:#ffb0b0}',
+ '.browser-inbox-body{flex:1;min-height:0;display:grid;grid-template-columns:minmax(260px,360px) minmax(0,1fr)}.browser-inbox-list{min-height:0;overflow:auto;border-right:1px solid #16213e;background:#101020}.browser-inbox-detail{min-width:0;min-height:0;overflow:auto;padding:1rem;display:flex;flex-direction:column;gap:.75rem}',
+ '.browser-inbox-empty,.browser-inbox-detail-empty{height:100%;display:flex;align-items:center;justify-content:center;padding:1.5rem;color:#8b8ba8;font-size:.84rem;line-height:1.45;text-align:center}.browser-inbox-detail-empty{height:auto;margin:auto}',
+ '.browser-inbox-row{display:grid;gap:.25rem;padding:.65rem .75rem;border-bottom:1px solid rgba(22,33,62,.75);cursor:pointer}.browser-inbox-row:hover{background:#17172d}.browser-inbox-row.selected{background:#1a2a3a}.browser-inbox-row-head{display:flex;align-items:center;gap:.45rem;min-width:0}.browser-inbox-kind{color:#4ecca3;font-size:.68rem;text-transform:uppercase}.browser-inbox-row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.86rem}.browser-inbox-row-url,.browser-inbox-row-text{min-width:0;overflow:hidden;text-overflow:ellipsis;color:#8b8ba8;font-size:.74rem}',
+ '.browser-inbox-detail-title{font-size:1rem;font-weight:600;color:#f4f7fb;overflow-wrap:anywhere}.browser-inbox-meta{display:grid;grid-template-columns:7rem minmax(0,1fr);gap:.35rem .75rem;font-size:.78rem}.browser-inbox-meta-label{color:#777}.browser-inbox-meta-value{color:#ccc;overflow-wrap:anywhere}.browser-inbox-text{padding:.75rem;border:1px solid #24304f;border-radius:6px;background:#101020;color:#ddd;font-size:.84rem;line-height:1.5;white-space:pre-wrap}.browser-inbox-detail-actions{display:flex;gap:.5rem;flex-wrap:wrap}',
+ '@media(max-width:760px){.browser-inbox-body{grid-template-columns:1fr}.browser-inbox-list{border-right:0;border-bottom:1px solid #16213e;max-height:45vh}.browser-inbox-meta{grid-template-columns:1fr}}'
+ ].join('');
+ document.head.appendChild(style);
+ }
+
+ function el(tag, attrs, children) {
+ var node = document.createElement(tag);
+ attrs = attrs || {};
+ Object.keys(attrs).forEach(function (key) {
+ if (key === 'textContent') node.textContent = attrs[key];
+ else if (key === 'className') node.className = attrs[key];
+ else if (key === 'disabled') node.disabled = !!attrs[key];
+ else if (key.indexOf('data-') === 0) node.setAttribute(key, attrs[key]);
+ else if (key === 'onClick') node.addEventListener('click', attrs[key]);
+ else node[key] = attrs[key];
+ });
+ (children || []).forEach(function (child) {
+ if (child) node.appendChild(child);
+ });
+ return node;
+ }
+
+ function workspaceRoot(props) {
+ return String(
+ props.workspaceRootPath ||
+ (props.workspaceNode && (props.workspaceNode.rootPath || props.workspaceNode.name || props.workspaceNode.id)) ||
+ props.workspaceName ||
+ ''
+ ).trim();
+ }
+
+ function workspaceKey(root) {
+ return 'captures:workspace:' + encodeURIComponent(root || '');
+ }
+
+ function rows(value) {
+ return Array.isArray(value) ? value.filter(function (item) { return item && typeof item === 'object'; }) : [];
+ }
+
+ function title(capture) {
+ return capture.title || capture.fileName || capture.url || capture.captureId || 'Untitled capture';
+ }
+
+ function renderBrowserInbox(containerEl, props, api) {
+ injectStyles();
+ var rootPath = workspaceRoot(props || {});
+ var key = workspaceKey(rootPath);
+ var captures = [];
+ var selectedId = '';
+ var statusText = 'Ready for browser captures';
+
+ containerEl.innerHTML = '';
+ var root = el('div', { className: 'browser-inbox-root', 'data-plugin-id': PLUGIN_ID });
+ var toolbar = el('div', { className: 'browser-inbox-toolbar' });
+ var titleEl = el('span', { className: 'browser-inbox-title', textContent: rootPath ? 'Browser Inbox · ' + rootPath : 'Browser Inbox' });
+ var countEl = el('span', { className: 'browser-inbox-count' });
+ var statusEl = el('span', { className: 'browser-inbox-status' });
+ var clearBtn = el('button', {
+ className: 'browser-inbox-btn danger',
+ 'data-browser-inbox-action': 'clear',
+ textContent: 'Clear',
+ onClick: function () {
+ captures = [];
+ selectedId = '';
+ persist().then(render);
+ }
+ });
+ var body = el('div', { className: 'browser-inbox-body' });
+ var listEl = el('div', { className: 'browser-inbox-list' });
+ var detailEl = el('div', { className: 'browser-inbox-detail' });
+
+ toolbar.appendChild(titleEl);
+ toolbar.appendChild(countEl);
+ toolbar.appendChild(el('span', { className: 'browser-inbox-spacer' }));
+ toolbar.appendChild(statusEl);
+ toolbar.appendChild(clearBtn);
+ body.appendChild(listEl);
+ body.appendChild(detailEl);
+ root.appendChild(toolbar);
+ root.appendChild(body);
+ containerEl.appendChild(root);
+
+ function readSettings() {
+ if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve({});
+ return api.settings.read().then(function (settings) { return settings || {}; }).catch(function () { return {}; });
+ }
+
+ function persist() {
+ if (!api || !api.settings || typeof api.settings.write !== 'function') return Promise.resolve();
+ return api.settings.write(key, captures.slice()).catch(function (err) {
+ statusText = 'Could not save inbox: ' + (err && err.message ? err.message : String(err));
+ });
+ }
+
+ function selectedCapture() {
+ for (var i = 0; i < captures.length; i += 1) {
+ if (captures[i].captureId === selectedId) return captures[i];
+ }
+ return captures[0] || null;
+ }
+
+ function removeCapture(captureId) {
+ captures = captures.filter(function (item) { return item.captureId !== captureId; });
+ selectedId = captures[0] ? captures[0].captureId : '';
+ statusText = 'Capture removed';
+ return persist().then(render);
+ }
+
+ function conversionAction(kind, capture) {
+ statusText = 'Ready to create ' + kind + ': ' + title(capture);
+ render();
+ }
+
+ function renderList() {
+ listEl.innerHTML = '';
+ if (captures.length === 0) {
+ listEl.appendChild(el('div', {
+ className: 'browser-inbox-empty',
+ textContent: 'No browser captures yet. Keep this view open, then send a page, selection, link, or file from the browser extension.'
+ }));
+ return;
+ }
+ captures.forEach(function (capture) {
+ var row = el('div', {
+ className: 'browser-inbox-row' + (capture.captureId === selectedId ? ' selected' : ''),
+ 'data-browser-capture-id': capture.captureId,
+ onClick: function () {
+ selectedId = capture.captureId;
+ render();
+ }
+ }, [
+ el('div', { className: 'browser-inbox-row-head' }, [
+ el('span', { className: 'browser-inbox-kind', textContent: capture.kind || 'capture' }),
+ el('span', { className: 'browser-inbox-row-title', textContent: title(capture) })
+ ]),
+ el('div', { className: 'browser-inbox-row-url', textContent: capture.url || capture.domain || capture.captureId || '' })
+ ]);
+ if (capture.text) row.appendChild(el('div', { className: 'browser-inbox-row-text', textContent: capture.text }));
+ listEl.appendChild(row);
+ });
+ }
+
+ function renderDetail() {
+ detailEl.innerHTML = '';
+ var capture = selectedCapture();
+ if (!capture) {
+ detailEl.appendChild(el('div', { className: 'browser-inbox-detail-empty', textContent: 'Select a capture to inspect it.' }));
+ return;
+ }
+ selectedId = capture.captureId;
+ detailEl.appendChild(el('div', { className: 'browser-inbox-detail-title', textContent: title(capture) }));
+ detailEl.appendChild(el('div', { className: 'browser-inbox-meta' }, [
+ el('div', { className: 'browser-inbox-meta-label', textContent: 'Kind' }),
+ el('div', { className: 'browser-inbox-meta-value', textContent: capture.kind || '-' }),
+ el('div', { className: 'browser-inbox-meta-label', textContent: 'URL' }),
+ el('div', { className: 'browser-inbox-meta-value', textContent: capture.url || '-' }),
+ el('div', { className: 'browser-inbox-meta-label', textContent: 'Domain' }),
+ el('div', { className: 'browser-inbox-meta-value', textContent: capture.domain || '-' }),
+ el('div', { className: 'browser-inbox-meta-label', textContent: 'Browser' }),
+ el('div', { className: 'browser-inbox-meta-value', textContent: capture.browserName || capture.source || '-' })
+ ]));
+ if (capture.text) detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.text }));
+ if (capture.fileText) detailEl.appendChild(el('div', { className: 'browser-inbox-text', textContent: capture.fileText }));
+ detailEl.appendChild(el('div', { className: 'browser-inbox-detail-actions' }, [
+ el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'create-note', textContent: 'Create Note', onClick: function () { conversionAction('note', capture); } }),
+ el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'create-link', textContent: 'Create Link', onClick: function () { conversionAction('link', capture); } }),
+ el('button', { className: 'browser-inbox-btn', 'data-browser-inbox-action': 'create-file', textContent: 'Create File', onClick: function () { conversionAction('file', capture); } }),
+ el('button', { className: 'browser-inbox-btn danger', 'data-browser-inbox-action': 'remove', textContent: 'Remove', onClick: function () { removeCapture(capture.captureId); } })
+ ]));
+ }
+
+ function render() {
+ countEl.textContent = captures.length + ' item' + (captures.length === 1 ? '' : 's');
+ clearBtn.disabled = captures.length === 0;
+ statusEl.textContent = statusText;
+ renderList();
+ renderDetail();
+ }
+
+ readSettings().then(function (settings) {
+ captures = rows(settings[key]);
+ selectedId = captures[0] ? captures[0].captureId : '';
+ render();
+ });
+ render();
+ }
+
+ var BrowserInboxView = {
+ mount: renderBrowserInbox,
+ unmount: function (containerEl) { containerEl.innerHTML = ''; }
+ };
+ window.VerstakPluginRegister('verstak.browser-inbox', { components: { BrowserInboxView: BrowserInboxView } });
+ }.toString() + ')();';
}
function platformTestBundle() {