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 @@ + + +
+
+
+

Today

+

{workspaceTitle || workspaceRootPath || 'Workspace'} overview

+
+ +
+ +
+
+
+

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() {