From 578ce2f2a5b56cbac6254fae25faf61f849c3bfc Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 30 Jun 2026 22:43:09 +0800 Subject: [PATCH] Fix global search result navigation --- frontend/e2e/ux-followup.spec.js | 41 ++++++++ frontend/src/lib/shell/GlobalSearch.svelte | 48 ++++++++- frontend/src/lib/test/wails-mock.js | 109 ++++++++++++++++++++- 3 files changed, 191 insertions(+), 7 deletions(-) diff --git a/frontend/e2e/ux-followup.spec.js b/frontend/e2e/ux-followup.spec.js index e846ba6..10636ac 100644 --- a/frontend/e2e/ux-followup.spec.js +++ b/frontend/e2e/ux-followup.spec.js @@ -46,6 +46,47 @@ test.describe('UX follow-up fixes', () => { await expect(page.locator('.files-item')).toContainText('Overview.md'); }); + test('global search opens indexed browser inbox results', async ({ page }) => { + await page.evaluate(async () => { + await window.go.api.App.WritePluginSettings('verstak.browser-inbox', { + 'captures:workspace:Project': [{ + captureId: 'capture-search-1', + capturedAt: '2026-06-30T08:00:00.000Z', + kind: 'page', + url: 'https://example.com/research', + title: 'Research Search Result', + domain: 'example.com', + text: 'Searchable captured browser text', + workspaceRootPath: 'Project', + browserName: 'Firefox', + }], + }); + }); + + const search = page.locator('[data-global-search-input]'); + await search.fill('Research Search Result'); + const result = page.locator('[data-global-search-result-type="Browser Inbox"]').filter({ hasText: 'Research Search Result' }); + await expect(result).toBeVisible({ timeout: 10000 }); + await result.click(); + + await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.browser-inbox-detail-title')).toHaveText('Research Search Result'); + }); + + test('workspace Search input keeps focus while typing', async ({ page }) => { + await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); + await page.getByRole('tab', { name: 'Search' }).click(); + const searchInput = page.locator('[data-search-input="query"]'); + await expect(searchInput).toBeVisible({ timeout: 10000 }); + + await searchInput.click(); + await page.keyboard.press('p'); + await expect(searchInput).toBeFocused(); + await page.keyboard.press('r'); + await expect(searchInput).toBeFocused(); + await expect(searchInput).toHaveValue('pr'); + }); + test('plugin settings modal gives complex panels enough space', async ({ page }) => { await openPluginManager(page); await page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).getByRole('button', { name: 'Settings' }).click(); diff --git a/frontend/src/lib/shell/GlobalSearch.svelte b/frontend/src/lib/shell/GlobalSearch.svelte index c50db5a..863761c 100644 --- a/frontend/src/lib/shell/GlobalSearch.svelte +++ b/frontend/src/lib/shell/GlobalSearch.svelte @@ -15,6 +15,7 @@ let focused = false; let loading = true; let searchTimer = null; + let buildSeq = 0; $: scheduleSearch(query); @@ -114,7 +115,14 @@ return String(text || '').slice(0, 900); } - async function indexPluginSettings(pluginId, label, rank) { + function pluginToolKind(pluginId, label) { + if (pluginId === 'verstak.browser-inbox') return 'browser-inbox'; + if (pluginId === 'verstak.activity') return 'activity'; + if (pluginId === 'verstak.journal') return 'journal'; + return String(label || pluginId || '').toLowerCase(); + } + + async function indexPluginSettings(pluginId, label, rank, view, nodes) { const settings = await resultOrEmpty(App.ReadPluginSettings(pluginId), {}); const items = []; Object.keys(settings || {}).forEach(key => { @@ -123,12 +131,19 @@ rows.forEach(row => { if (!row || typeof row !== 'object') return; const title = row.title || row.summary || row.url || row.captureId || row.activityId || row.entryId || label; + const workspaceName = row.workspaceRootPath || row.workspaceName || ''; items.push({ type: label, title, subtitle: row.url || row.summary || row.workspaceRootPath || key, keywords: JSON.stringify(row), rank, + action: workspaceName ? 'workspace-tool' : (view ? 'view' : ''), + viewId: view?.id || '', + pluginId, + workspaceName, + toolKind: pluginToolKind(pluginId, label), + nodes, }); }); }); @@ -136,6 +151,7 @@ } async function buildIndex() { + const seq = ++buildSeq; loading = true; const next = []; @@ -155,6 +171,10 @@ }); const contributions = await resultOrEmpty(App.GetContributions(), {}); + const viewByPluginId = new Map(); + (contributions.views || []).forEach(view => { + if (view.pluginId && !viewByPluginId.has(view.pluginId)) viewByPluginId.set(view.pluginId, view); + }); (contributions.sidebarItems || []).forEach(item => { next.push({ type: 'Tool', @@ -185,16 +205,22 @@ } const pluginItems = await Promise.all([ - indexPluginSettings('verstak.journal', 'Journal', 50), - indexPluginSettings('verstak.browser-inbox', 'Browser Inbox', 55), - indexPluginSettings('verstak.activity', 'Activity', 60), + indexPluginSettings('verstak.journal', 'Journal', 50, viewByPluginId.get('verstak.journal'), nodes), + indexPluginSettings('verstak.browser-inbox', 'Browser Inbox', 55, viewByPluginId.get('verstak.browser-inbox'), nodes), + indexPluginSettings('verstak.activity', 'Activity', 60, viewByPluginId.get('verstak.activity'), nodes), ]); + if (seq !== buildSeq) return; index = next.concat(pluginItems.flat()); loading = false; runSearch(query); } + function handleFocus() { + focused = true; + buildIndex(); + } + async function openResult(item) { query = ''; results = []; @@ -210,6 +236,18 @@ })); return; } + if (item.action === 'workspace-tool') { + const workspaceName = item.workspaceName || ''; + if (workspaceName) { + window.dispatchEvent(new CustomEvent('verstak:workspace-selected', { + detail: { workspaceName, nodes: item.nodes || [] } + })); + window.dispatchEvent(new CustomEvent('verstak:workspace-open-tool', { + detail: { kind: item.toolKind || item.type || '' } + })); + } + return; + } if (item.action === 'file-folder') { const parts = String(item.path || '').split('/').filter(Boolean); const workspaceName = parts[0] || ''; @@ -252,7 +290,7 @@ focused = true} + on:focus={handleFocus} on:blur={() => setTimeout(() => focused = false, 120)} type="search" placeholder={loading ? 'Индексируем...' : 'Поиск'} diff --git a/frontend/src/lib/test/wails-mock.js b/frontend/src/lib/test/wails-mock.js index 63514be..b604f73 100644 --- a/frontend/src/lib/test/wails-mock.js +++ b/frontend/src/lib/test/wails-mock.js @@ -220,8 +220,11 @@ icon: 'search', provides: ['verstak/search/v1', 'search.provider'], requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'], - permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register'], + permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register', 'events.subscribe', 'commands.register'], + frontend: { entry: 'frontend/dist/index.js' }, contributes: { + workspaceItems: [{ id: 'verstak.search.workspace', title: 'Search', icon: 'search', component: 'SearchView' }], + commands: [{ id: 'verstak.search.searchVaultText', title: 'Search Vault Text', handler: 'verstak.search.searchVaultText' }], searchProviders: [{ id: 'verstak.search.vault-text', label: 'Vault Text Search', handler: 'verstak.search.searchVaultText' }] } }, @@ -1422,6 +1425,102 @@ }.toString() + ')();'; } + function searchPluginBundle() { + return '(' + function () { + function el(tag, attrs, children) { + var node = document.createElement(tag); + attrs = attrs || {}; + Object.keys(attrs).forEach(function (key) { + if (attrs[key] == null) return; + if (key === 'className') node.className = attrs[key]; + else if (key.indexOf('on') === 0) node.addEventListener(key.slice(2).toLowerCase(), attrs[key]); + else if (key === 'textContent') node.textContent = attrs[key]; + else node.setAttribute(key, attrs[key]); + }); + (children || []).forEach(function (child) { + if (child == null) return; + node.appendChild(typeof child === 'string' ? document.createTextNode(child) : child); + }); + return node; + } + function clean(path) { return String(path || '').split('/').filter(Boolean).join('/'); } + function SearchView(containerEl, props, api) { + if (!document.getElementById('mock-search-style')) { + var style = document.createElement('style'); + style.id = 'mock-search-style'; + style.textContent = '.search-root{height:100%;min-height:0;display:flex;flex-direction:column;background:#0d0d1a;color:#e0e0e0}.search-toolbar{display:flex;gap:.5rem;padding:.55rem .75rem;border-bottom:1px solid #16213e;background:#12122a}.search-input{flex:1;min-width:180px;font-size:.86rem;padding:.42rem .55rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}.search-input:focus{border-color:#4ecca3}.search-btn{font-size:.8rem;padding:.42rem .7rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ddd}.search-scope,.search-status{font-size:.78rem;color:#8b8ba8}.search-status{padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}.search-results{flex:1;min-height:0;overflow:auto}.search-empty{padding:2rem;color:#666;text-align:center}.search-result{padding:.7rem .85rem;border-bottom:1px solid rgba(22,33,62,.55)}.search-path{color:#4ecca3}.search-snippet{margin-top:.25rem;color:#cfcfe0;font-size:.8rem}'; + document.head.appendChild(style); + } + containerEl.innerHTML = ''; + containerEl.className = 'search-root'; + containerEl.setAttribute('data-plugin-id', 'verstak.search'); + var rootPath = clean(props && (props.workspaceRootPath || props.workspaceName)); + var query = ''; + var timer = null; + var results = []; + var input = el('input', { className: 'search-input', type: 'search', placeholder: 'Search files, folders, text', 'data-search-input': 'query' }); + var button = el('button', { className: 'search-btn', 'data-search-action': 'run', textContent: 'Search' }); + var status = el('div', { className: 'search-status', textContent: 'Enter at least 2 characters.' }); + var list = el('div', { className: 'search-results' }); + containerEl.appendChild(el('div', { className: 'search-toolbar' }, [ + input, + button, + el('span', { className: 'search-scope', title: rootPath || 'Vault' }, [rootPath || 'Vault']) + ])); + containerEl.appendChild(status); + containerEl.appendChild(list); + function render() { + list.innerHTML = ''; + if (!results.length) { + list.appendChild(el('div', { className: 'search-empty' }, [query.length < 2 ? 'Enter at least 2 characters.' : 'No results'])); + return; + } + results.forEach(function (item) { + list.appendChild(el('div', { className: 'search-result' }, [ + el('div', { className: 'search-path', textContent: item.relativePath }), + el('div', { className: 'search-snippet', textContent: item.name }) + ])); + }); + } + async function run() { + query = input.value.trim(); + if (query.length < 2) { + results = []; + status.textContent = 'Enter at least 2 characters.'; + render(); + return; + } + var entries = await api.files.list(rootPath); + var needle = query.toLowerCase(); + results = (Array.isArray(entries) ? entries : []).filter(function (item) { + return String(item.name || item.relativePath || '').toLowerCase().indexOf(needle) !== -1; + }); + status.textContent = results.length + ' result' + (results.length === 1 ? '' : 's'); + render(); + } + function schedule() { + if (timer) clearTimeout(timer); + timer = setTimeout(run, 100); + } + input.addEventListener('input', schedule); + button.addEventListener('click', run); + render(); + containerEl.__searchMockCleanup = function () { if (timer) clearTimeout(timer); }; + } + window.VerstakPluginRegister('verstak.search', { + components: { + SearchView: { + mount: SearchView, + unmount: function (containerEl) { + if (containerEl.__searchMockCleanup) containerEl.__searchMockCleanup(); + containerEl.innerHTML = ''; + } + } + } + }); + }.toString() + ')();'; + } + function platformTestBundle() { return [ "(function(){", @@ -1665,6 +1764,9 @@ if (pluginId === 'verstak.browser-inbox' && assetPath === 'frontend/dist/index.js') { return Promise.resolve(browserInboxBundle()); } + if (pluginId === 'verstak.search' && assetPath === 'frontend/dist/index.js') { + return Promise.resolve(searchPluginBundle()); + } return Promise.resolve(''); }, GetPluginCapability: function (pluginId, capId) { @@ -2226,8 +2328,11 @@ icon: 'search', provides: ['verstak/search/v1', 'search.provider'], requires: ['verstak/core/files/v1', 'verstak/core/workbench/v1'], - permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register'], + permissions: ['files.read', 'workbench.open', 'storage.namespace', 'ui.register', 'events.subscribe', 'commands.register'], + frontend: { entry: 'frontend/dist/index.js' }, contributes: { + workspaceItems: [{ id: 'verstak.search.workspace', title: 'Search', icon: 'search', component: 'SearchView' }], + commands: [{ id: 'verstak.search.searchVaultText', title: 'Search Vault Text', handler: 'verstak.search.searchVaultText' }], searchProviders: [{ id: 'verstak.search.vault-text', label: 'Vault Text Search', handler: 'verstak.search.searchVaultText' }] } },