feat(sidebar): daily-notes panel + Q&A deep-link to file offset

Daily notes panel:
- src/sidebar/daily-notes-panel.js — new sidebar panel that lists every
  YYYY-MM-DD.md in the daily-notes dir (newest first), with a Today
  button that creates/opens today's entry. Refresh button reloads.
  Clicking a row calls onOpenFile(path). DOM is built with textContent
  for dynamic fields — no XSS surface from a hostile filename.
- src/main.js — daily-notes:list IPC now returns absolute paths (joined
  with the daily-notes dir) so the renderer can pass them straight to
  open-file-path without re-synthesizing.
- src/renderer.js — registers the panel; icon for the daily-notes
  button (calendar icon).
- src/index.html — calendar SVG icon for the daily-notes sidebar entry.

Q&A deep-link:
- src/main.js — open-file-path accepts either a string (legacy) or an
  object {path, offset}. The offset is forwarded to the renderer via
  file-opened.
- src/renderer.js — file-opened handler scrolls the editor to the
  offset using EditorView.scrollIntoView(y: 'center') so the matching
  passage lands in the middle of the visible area.
- search panel now passes {path, offset} to open-file-path so Q&A
  results with chunk offsets jump straight to the passage.

Tests (12 new, tests/daily-notes-panel.test.js):
  - mount, empty state, list rendering
  - Today button → onOpenFile, 'Created today' / 'Today already exists' status
  - error paths (openToday reject, listExisting reject, listExisting missing)
  - click + Enter/Space on a row open the right path
  - refresh() re-fetches, keeps status when keepStatus:true
  - non-md entries filtered out
  - XSS-safe textContent for dynamic data

Full suite: 73 suites, 873 tests, lint+format clean.

Amit Haridas
This commit is contained in:
2026-09-14 11:53:45 +05:30
parent 0cab0b08c7
commit 2aa72738f4
5 changed files with 385 additions and 5 deletions
+35 -2
View File
@@ -22,6 +22,7 @@ const DOMPurify = createDOMPurify(window);
const hljs = require('highlight.js');
const { createEditor } = require('./editor/codemirror-setup');
const { undo, redo } = require('@codemirror/commands');
const { EditorView } = require('@codemirror/view');
const { showMediaOperationsDialog } = require('./renderer/media-operations-dialog');
const { showPdfBatchDialog } = require('./renderer/pdf-batch-dialog');
const { showDocumentCompareDialog } = require('./renderer/document-compare-dialog');
@@ -2241,11 +2242,27 @@ document.addEventListener('DOMContentLoaded', async () => {
const el = document.getElementById('explorer-path');
return el ? el.value.trim() || null : null;
},
onOpenFile: (filePath /* , offset */) => {
ipcRenderer.send('open-file-path', filePath);
onOpenFile: (filePath, offset) => {
ipcRenderer.send('open-file-path', { path: filePath, offset });
},
}),
});
// Daily notes journal browser — lists existing YYYY-MM-DD.md files and
// surfaces a "Today" button that creates/opens today's entry. Renders
// paths as a placeholder string; main rewrites them on click (via the
// file-opened IPC path, which already handles any daily-notes path).
sidebarManager.registerPanel('daily-notes', {
title: 'Daily Notes',
icon:
'<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 4h-1V2h-2v2H8V2H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2z"/></svg>',
render: (container) =>
require('./sidebar/daily-notes-panel').renderDailyNotesPanel(container, {
openToday: ({ date } = {}) => ipcRenderer.invoke('daily-notes:open-today', { date }),
listExisting: () => ipcRenderer.invoke('daily-notes:list'),
onOpenFile: (filePath) => ipcRenderer.send('open-file-path', { path: filePath }),
}),
});
sidebarManager.registerPanel('git', {
title: 'Git',
render: (container) =>
@@ -2900,6 +2917,22 @@ ipcRenderer.on('file-opened', (event, data) => {
if (tabManager) {
tabManager.openFile(data.path, data.content);
// Deep-link from search/Q&A: scroll the editor to the matching offset.
// openFile() loads the doc into the active tab; once it returns the
// CodeMirror view exists and we can dispatch a selection.
if (data && typeof data.offset === 'number' && data.offset > 0) {
const tab = tabManager.tabs.get(tabManager.activeTabId);
const view = tab && tab.editorView;
if (view) {
const docLen = view.state.doc.length;
const pos = Math.min(data.offset, docLen);
view.dispatch({
selection: { anchor: pos, head: pos },
effects: EditorView.scrollIntoView(pos, { y: 'center' }),
});
view.focus();
}
}
} else {
console.error('[RENDERER] tabManager not initialized!');
}