feat(search): sidebar panel + Ask mode for workspace Q&A

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 <script>, <img>, <unsafe> are escaped
  - Escape clears, Clear button resets, host API (setDir/focus/clear)

Full suite: 67 suites, 766 tests, lint+format clean.

Amit Haridas
This commit is contained in:
2026-09-14 08:53:27 +05:30
parent 3f856bc857
commit da97369b44
4 changed files with 551 additions and 0 deletions
+26
View File
@@ -2197,6 +2197,32 @@ document.addEventListener('DOMContentLoaded', async () => {
currentDir: explorerCurrentDir,
}),
});
// Search + Q&A panel — wires the workspace-search:query / doc-qa:ask IPC
// channels that the WorkspaceSearch + DocQA main-process modules back. The
// panel defaults its folder to whatever folder the Explorer last loaded;
// the user can override the input.
sidebarManager.registerPanel('search', {
title: 'Search',
icon:
'<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M10 2a8 8 0 1 1-5.3 14L1 19.7 2.3 21l3.7-3.7A8 8 0 0 1 10 2zm0 2a6 6 0 1 0 0 12A6 6 0 0 0 10 4z"/></svg>',
render: (container) =>
require('./sidebar/search-panel').renderSearchPanel(container, {
search: ({ query, dir, limit }) =>
ipcRenderer.invoke('workspace-search:query', { query, dir, limit }),
ask: ({ question, dir, topK }) =>
ipcRenderer.invoke('doc-qa:ask', { question, dir, topK }),
getCurrentDir: () => {
// Re-read the explorer's path input each time the user opens the
// search panel; survives the explorer being remounted.
const el = document.getElementById('explorer-path');
return el ? el.value.trim() || null : null;
},
onOpenFile: (filePath /* , offset */) => {
ipcRenderer.send('open-file-path', filePath);
},
}),
});
sidebarManager.registerPanel('git', {
title: 'Git',
render: (container) =>