From da97369b4432dbf8febe4da56630dcc5a24d3d77 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Mon, 14 Sep 2026 08:53:27 +0530 Subject: [PATCH] feat(search): sidebar panel + Ask mode for workspace Q&A MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace-search:query and doc-qa:ask IPC channels were reachable from the renderer but had no UI. This commit wires them into a sidebar panel that consumes both backends through a single input. - src/sidebar/search-panel.js — one input, two modes: Search (default): routes to workspace-search:query, returns file hits Ask: routes to doc-qa:ask, returns chunk-level passages with offsets Click a result → open the file (offset passed through for Q&A hits). All dynamic content is escaped before innerHTML — hostile filenames or snippets stay as text instead of becoming script/img nodes. - src/renderer.js — registers the panel; reads the explorer's folder input on each open so the search dir stays in sync. - src/index.html — search sidebar icon (magnifier) next to the others. Tests (18 new, tests/search-panel.test.js): - mount + DOM structure - Enter / click run → search() with query + dir - result rendering (filePath, snippet, tag facet) - onOpenFile receives (filePath, offset) - Ask tab switches placeholder + routes to ask() - Ask chunks carry +offset in the meta line - empty query, no folder, search error → handled - XSS: filename/snippet/tag with .md', + snippet: '', + score: 1, + matchedTerms: [], + matchedTags: [''], + }, + ]); + const { container } = mountPanel({ search }); + container.querySelector('#search-input').value = 'x'; + container.querySelector('#search-run').click(); + await flush(); + + // No executable elements should have been built — escapeHtml() prevents + // a hostile filename or snippet from running script/img in the panel. + expect(container.querySelector('script')).toBeNull(); + expect(container.querySelector('img')).toBeNull(); + // The literal text still appears (decoded by the browser); the safety + // is that it stays as text instead of becoming a node. + expect(container.querySelector('.search-result').textContent).toContain('