Two pre-existing Windows-specific failures blocked the CI release
matrix from publishing the Windows build:
- tests/main/DailyNotes.test.js: 'joins dir + YYYY-MM-DD.md' used
a hard-coded '/tmp/notes/2026-09-13.md' expected value. On
POSIX path.join returns '/' and the test passes; on Windows
path.join returns '\' and the test fails. Fix: build the
expected value via path.join so it matches the platform.
- tests/main/PDFBatchOperations.test.js: 'watermarks every PDF
including subfolders' used the default 5s Jest timeout. Windows
CI runners are slower at pdf-lib / pdfjs-dist cold-start and the
test regularly exceeded 5s. Fix: jest.setTimeout(30000) at the
start of the watermark describe block.
Neither is a regression — both predate this session. The fix gets
the Windows job green so the release matrix completes 3/3.
Amit Haridas
Pure CommonJS orchestrator that unifies hand-coded fonts, figlet adapter,
and templates behind a single public API. Wired to main.js via ipcMain.handle.
Public API:
- generate({ text, font, options }) - resolves 'template:<name>' /
'figlet:<font>' / bare id, falls back to standard for unknown fonts.
- listFonts() - hand-coded first (17), then figlet, then templates (19).
- getFontMeta(id) - null for unknown id, full meta for hand-coded,
{ kind } for figlet/templates.
Amit Haridas
Per Task 3 of the 2026-09-14 ASCII art upgrade plan.
- New module: src/main/AsciiArt.templates.js exporting ASCII_TEMPLATES
(19 entries) and getTemplate(name) accessor (returns '' for unknown).
- New test: tests/main/ascii-art.templates.test.js with 19 per-template
snapshots + unknown-name + keys-match assertions (21 tests total).
- 17 templates verbatim from src/ascii-generator.html:596-626
(arrow-right, arrow-down, decision, process, flowchart, sequence,
network, hierarchy, header, note, warning, info, divider, separator,
banner, checklist + the brief's own TEMPLATE_NAMES order).
- 2 templates verbatim from src/renderer.js:6542-6697
(progress-bar, table-simple).
- 1 newly authored: arrow-up (completes the arrow triplet; does not
exist in either source).
Amit Haridas
Widen height bound to 3-12 so the spec-named figlet fonts fit:
- Isometric1, Isometric2, Isometric3, Isometric4 (h=11)
- Calvin S (h=3)
Previously these were substituted with height-compliant alternatives
(Small Isometric1, Banner3-D, Henry 3D, Small Poison, Modular). Restoring
the spec-named fonts preserves the product intent: 4 distinct isometric
projections and the calvin-and-hobbes-style Calvin S font.
Amit Haridas
Adds src/main/themeMenuBuilder.js — a pure module that consumes
ThemeRegistry.list() + ThemeRegistry.categories() and the injected
setTheme/getCurrentThemeId callbacks to produce the View → Theme
submenu's MenuItemTemplate[] in the same shape as the previous
hardcoded block in src/main.js:1137-1245.
- Radio-style items with checked=true on the active theme id
- Grouped by category in registry order with separators between
non-empty categories
- No Electron / electron-store imports — keeps the module pure and
unit-testable under @jest-environment node
- Tests use jest.resetModules() + per-test require to avoid the
module-cache leakage the bootstrap test surfaced in T2
Amit Haridas
Registers all 37 editor themes at startup via ThemeRegistry.bootstrap.js.
Side-effect module: requiring it populates the registry from a static
THEMES array (25 existing menu themes refactored into the registry +
12 new: Catppuccin x4, one-light, tokyo-night-storm, synthwave-84,
outrun, winter-is-coming x2, solarized-dark-hc, spring-light).
Categories split: 13 light / 22 dark / 1 high-contrast / 1 seasonal
(spring-light per spec).
Snapshot test asserts exact id order, shape validity, and category
counts. Tests use jest.resetModules() + per-test requires so the
bootstrap module re-evaluates its registration loop each run.
Amit Haridas
- 8 exports: register/unregister/list/get/categories/lightThemes/darkThemes/clear
- kebab-case id validator; category whitelist (light/dark/high-contrast/seasonal)
- duplicate-id and shape errors with descriptive messages
- pure CommonJS, no IO, no Electron deps — T2 bootstrap will register all 37 themes
- 10 Jest tests covering register/unregister/get/categories/light+dark filters
- full suite: 77 suites, 924 tests passing; lint + Prettier clean
Amit Haridas
Two more features from the deferred menu:
Daily-note template gallery:
- src/main/DailyNotesTemplates.js — pure module: listTemplates() /
saveTemplate() / deleteTemplate() / labelFor() with injectable IO.
- src/main/DailyNotes.js — openOrCreate() now accepts seedContent so a
non-default template can seed a NEW note (existing notes never get
clobbered).
- src/main.js — IPC channels daily-templates:list / save / delete /
apply. apply renders the chosen template (with {date}/{weekday}
substitution) and pipes through DailyNotes.openOrCreate.
- src/sidebar/daily-templates-panel.js — gallery UI: list, +New
(prompt for name + content), Use (applies to today's note),
delete (refuses to remove the last template so the default survives).
- src/renderer.js — registers the panel.
- src/index.html — icon (already added).
Pluggable DocQA engine (semantic search hook):
- src/main/SemanticEngine.js — engine interface with defaultEngine() (TF-idF,
always available) and neuralEngine() (lazy @xenova/transformers,
falls back gracefully when the dep is missing). getEngine(name)
resolves either.
- src/main/DocQA.js — ask() is now async and accepts an engine arg.
TF-idF path unchanged; neural path calls engine.rank(question, chunks)
directly. The chunk corpus is built up front regardless of engine so
ranking is consistent.
- src/main.js — doc-qa:ask IPC resolves the engine via SemanticEngine.getEngine(name)
before calling DocQA.ask. The renderer can pass {engine: 'transformers'}
to opt in once @xenova/transformers is installed.
Tests (51 new across this batch):
- tests/main/DailyNotesTemplates.test.js (18): labelFor separators /
edge cases / non-string safety, listTemplates empty / present / sort,
saveTemplate nested dir + .md extension + validation + null content,
deleteTemplate success / missing / validation.
- tests/daily-templates-panel.test.js (12): mount + empty state + list +
XSS safety, Use button (apply + error path), Delete button (success +
last-template guard), New template (save + cancel), refresh.
- tests/main/SemanticEngine.test.js (8): default engine shape + rank
matches WorkspaceSearch, getEngine for tf-idf / unknown / transformers
(graceful fallback when @xenova/transformers missing), parity check.
- DocQA: 5 new tests for engine arg (custom engine.rank called, default
fallback, neural hit shape translation); existing tests updated to
await the now-async ask().
Full suite: 76 suites, 914 tests, lint+format clean.
Activation for the neural engine:
npm install @xenova/transformers
(heavy; ~50 MiB with deps) — then 'transformers' is selectable in
doc-qa:ask. Until then, all calls use TF-idF transparently.
Amit Haridas
- src/main/UrlTitle.js — fetch a URL, return its <title>. Pure module
with injectable fetch for tests. Decodes named + numeric + hex entities
(AT&T, Café, —), strips the <title> tags, collapses
whitespace, caps the label at 200 chars. 5s timeout via AbortController,
2 MiB body cap to avoid OOM on big downloads, streaming reader with
overflow cancellation. Rejects non-http(s) URLs up front.
- src/main.js — IPC url-title:fetch proxies to fetchTitle.
- src/editor/smart-paste.js — CodeMirror 6 extension that detects
URL-only pastes (one URL, surrounded only by whitespace), inserts the
URL immediately, then async-rewrites the insertion range to
[Title](url) once the title arrives. Multi-line / prose pastes pass
through untouched.
- src/editor/codemirror-setup.js — accepts smartPasteFetcher option and
pushes the extension when provided.
- src/renderer.js — passes ipcRenderer.invoke('url-title:fetch') as
the fetcher.
- src/preload.js — url-title:fetch added to ALLOWED_SEND_CHANNELS.
- eslint.config.js — AbortController / TextDecoder / TextEncoder added
to globals (available in Node 20+ and Chromium).
Tests (34 new):
- tests/main/UrlTitle.test.js (24): isHttpUrl scheme filter, decodeTitle
named/numeric/hex entities + whitespace + non-string, extractTitleFromHtml
first match + case-insensitive + null fallback, fetchTitle success +
long-title cap + streaming body, all failure paths (non-http,
no-fetch, non-OK, wrong content-type, no <title>, network error,
body over maxBytes — including streaming overflow cancellation).
- tests/smart-paste.test.js (10): URL_ONLY_RE detection (bare URL,
path/query/fragment, whitespace padding, scheme rejection, embedded
rejection, empty/malformed), replacement format ([T](url), brackets
in title survive, query strings preserved).
Full suite: 71 suites, 831 tests, lint+format clean.
Amit Haridas
Three more features from the brainstorm menu, built on a shared search
algorithm so the codebase stays small.
- src/main/DailyNotes.js — Zettelkasten-style helper. One YYYY-MM-DD.md
per local date under <userData>/notes/daily/; loads skeleton from
<userData>/notes/templates/daily.md when present (built-in default
otherwise). openOrCreate never clobbers existing content.
- src/main/WorkspaceSearch.js — tag/wikilink-aware content search.
Pure module, injectable-IO tested. Query grammar: bare words,
#tag, @wikilink, "quoted phrases". Facets weight +3 each; prose
terms +1/occurrence capped at 5; edits within 7 days get a recency
nudge. Returns ranked results with snippets.
- src/main/DocQA.js — chunk-level Q&A wrapper over WorkspaceSearch.
cleanQuestion strips question words (what/how/why/...) and verb
noise (write/read/show/tell/...) so they don't drown the ranking.
Returns top-K passages instead of whole-file hits — multiple chunks
from the same file can appear in the answer.
- src/main.js — IPC: daily-notes:open-today, daily-notes:list,
workspace-search:query, doc-qa:ask. Path validation through the
existing validatePath gate; a global Ctrl+Alt+D shortcut creates
today's daily note from anywhere.
- src/preload.js — all four channels added to ALLOWED_SEND_CHANNELS.
Tests (56 new across the three modules):
- tests/main/DailyNotes.test.js (15): dateKey formatting, pathFor,
template load + {date}/{weekday} substitution, openOrCreate +
no-clobber, nested-dir creation, listExisting filtering,
isValidDir rejects NUL/non-string.
- tests/main/WorkspaceSearch.test.js (25): parseQuery grammar,
hasTag/hasWikilink word boundaries, scoreDocument scoring,
per-term spam cap, recency nudge, search ranking + limit +
empty-query short-circuit, bad-input safety.
- tests/main/DocQA.test.js (16): cleanQuestion stripping + facet
preservation, chunkDocument paragraph + hard-split, ask()
top-K, recency tiebreaker, missing-files fallback.
Full suite: 748 tests pass, 66 suites, lint+format clean.
Amit Haridas
VersionHistory snapshots the previous content on every explicit save — an
unsaved buffer is still lost on crash. AutosaveBuffer fills that gap.
- src/main/AutosaveBuffer.js — pure module mirroring VersionHistory's
injectable-IO pattern; one blob per doc path under
<userData>/autosave/by-path/<sha1>/recovery.md + meta.json. No history
(VersionHistory owns that) — just the latest dirty buffer.
- src/main.js — IPC channels autosave:write/read/clear/list; real paths
go through validatePath, synthetic 'untitled-tab-<id>' keys skip it.
- src/preload.js — added the four channels to ALLOWED_SEND_CHANNELS.
- src/renderer/autosave-client.js — debounced (2s) flush per tab +
periodic safety net (10s max age) + dirty-write retry on failure.
- src/renderer.js — register on tab create, unregister on close,
notifyChange piggybacks on performAutoSave's existing dirty-check,
clearForDocPath after a successful save, showAutosaveRecoveryBanner
on startup listing pending recoveries with Restore/Dismiss.
Tests (39 new):
- tests/main/AutosaveBuffer.test.js (19): round-trip, overwrite, isolation,
unicode/emoji, empty content, null coercion, ENOENT vs corrupt meta,
list ordering, corrupt-sibling skip, input validation, sha1 storage.
- tests/autosave-client.test.js (11): debounce, flushNow bypass,
no-path skip, clearForDocPath, list proxy, IPC error fallback,
unregister tear-down, failure-retry, periodic flush, idempotency.
Full suite: 692 tests pass, 63 suites, lint+format clean.
Amit Haridas
@ cantoo/pdf-lib 2.9.1 was already the dep in use; the encryption plumbing
and probe were already in place from the prior hardening pass. This commit:
- adds a test confirming executeOperation('permissions', ...) routes through
pdfSetPermissions and produces an encrypted PDF unlocked by the owner
password (mirrors the existing 'encrypt' route coverage)
- updates docs/superpowers/plans/2026-08-23-security-assessment-summary.md
to mark deferred risk D1 as resolved (the honest-failure message remains
as a fail-closed net for any future library regression)
35 PDF ops tests pass; lint/format clean.
Amit Haridas
Export themes:
- Six presets in the export dialog (basic + advanced modes) for PDF/DOCX:
Default, Modern, Classic, Sepia, Minimal, Elegant
- PDF: LaTeX header (xcolor/titlesec) recolors headings, adds section
rules and colored links — core-TeX packages only, hex-literal only
(no injection surface)
- DOCX: styles.xml surgery recolors Heading1-6/Title/Subtitle/Hyperlink
and swaps heading/body fonts; verified end-to-end against a real
pandoc-produced docx
- Themes ride along in export presets (unknown ids fall back to Default)
Windows CI fixes:
- pdfjs standardFontDataUrl now a file:// URL (backslash paths failed
pdfjs's trailing-slash validation, breaking extractText/extractImages)
- sharp temp cleanup EPERM retries; path-separator assertions; pdfjs
test timeouts raised; batch suite testTimeout 30s
648/648 tests green; 4.7.1 linux+win artifacts rebuilt.
pdfjs validates standardFontDataUrl as a URL ending in a forward slash —
our raw path with a trailing path.sep is invalid on Windows (C:\...\),
failing extractText/extractImages (and every test that verifies through
them) with 'Invalid factory url: must include trailing slash'. Linux and
macOS passed only because / is also a valid URL slash. Convert with
pathToFileURL() so every platform sends file:///.../standard_fonts/.
Also escape path.sep in PDFBatchOperations' sanitizer test regex — a bare
backslash made new RegExp() a syntax error on Windows.
Bundle (v4.7.0):
- MarkItDown frozen to a single ~75MB per-platform binary (PyInstaller via
npm run bundle:markitdown; ML extras excluded) — built and verified
locally: HTML/XLSX/PDF conversions pass through the bundled binary, and
the app resolves bin/linux/markitdown first at runtime
- Packaging copies bundled markitdown alongside Pandoc for win/mac/linux;
FFmpeg/sharp/KaTeX/fonts were already bundled
Legal artifacts:
- THIRD-PARTY-NOTICES.md: complete license inventory of everything
distributed (binaries, npm runtime deps, fonts, embedded Python packages)
- SOURCES.md: GPL §3(b) written source offers for Pandoc 3.9.0.2,
ffmpeg-static's GPL build, and the PyInstaller bootloader; LGPL relinking
note for libvips
- third-party-licenses/: canonical GPL-2.0, LGPL-2.1, MPL-2.0, Apache-2.0,
OFL-1.1, PSF-Python texts
- Help > Third-Party Notices & Licenses: in-app viewer for both documents
- README: 'Bundled Dependencies, Legal Notices & Credits' section
Hardening:
- download-tools.js now SHA-256 pins every artifact, verifies after
download AND against the cache on every run, and hard-fails on mismatch
(closes security finding D6)
Large tools intentionally not bundled (documented): LibreOffice,
MiKTeX/TeX Live, ImageMagick, PlantUML+JRE, Calibre.
637/637 tests green; lint clean; clean boot; bundled binary verified.
- File → Import with MarkItDown (Any Format)…: PDF, DOCX, PPTX, XLSX,
Outlook .msg/.eml, EPUB, images, CSV/JSON/XML, ZIP (audio/OCR via the
[all] extras) — verified live against HTML, XLSX (our own exporter's
output), and PDF fixtures
- Command auto-resolution with caching: markitdown binary → python -m
markitdown → python3 -m markitdown
- SEC-1 argv discipline (execFile only, user paths never through a shell),
50MB cap, 120s timeout, sanitized errors that surface markitdown's own
"pip install 'markitdown[pdf]'" hints for missing format extras
- Output lands next to the source as <name>.md (numeric suffix, never
overwrites) and opens in a new tab; markitdown:available/convert IPC
allowlisted for renderer flows
- Help → Dependencies lists MarkItDown; README/UPDATES updated (v4.6.1)
12 new tests (629 green); lint clean; clean app boot
Anthropic-compatible provider:
- New 'anthropic-compatible' option for any base URL speaking the Anthropic
messages schema (LiteLLM proxies, Bedrock gateways, local servers)
- Sends x-api-key AND Bearer auth when a key is set (gateway-friendly,
harmless for the official API); keyless proxies supported
- Tolerates base URLs with or without a trailing /v1 segment
- Settings modal, manifest, and provider docs updated
Runtime bug fixes found by booting the app (run-to-verify pass):
- PDF editor: File > Open PDF sends operation=null which matched no switch
case and crashed on getElementById(undefined); now defaults to the merge
section
- backlinks-panel: wrong require depth (../../utils -> ../utils) threw at
panel registration time
- writing-studio stack was written against a synchronous settings backend but
the real one is IPC-backed: GoalTracker/SnapshotManager/ProjectManager and
all four panels now await; JSON.parse(Promise) crashes eliminated
- manuscript panel used window.prompt (unavailable in Electron); replaced
with an inline dialog
- collaboration comment-store/save-load made async to match its IPC IO
617/617 tests green; 4 consecutive clean app boots (no uncaught errors)
- AI Assistant plugin: multi-provider chat (OpenAI/Anthropic/Ollama/LM Studio),
summarize/improve/translate commands, proofread via ai:analyze; calls
proxied through main so API keys stay out of the renderer
- Collaboration plugin: anchor-based comments in .comments/ sidecars with
drift detection and F8 navigation
- Local knowledge base: [[wiki-links]] with click-to-create + Backlinks panel
- Crash recovery: debounced session snapshots with restore prompt on launch
- Version history: pre-save snapshots, History panel with restore/diff/delete
- Real PDF encryption: swap pdf-lib for @cantoo/pdf-lib (probe-driven UI)
- XLSX export (native workbooks via JSZip), ODT headers/footers + page size
- Offline KaTeX (bundled CSS+fonts), local-first PlantUML rendering
- Editor: vim mode toggle, snippet Tab-expansion, zen word-goal setter,
writing heatmap, writing-studio panels wired with rail icons
- Quick Note global scratchpad (Ctrl+Alt+Q), markdownconverter:// deep links,
REPL first-run confirmation
- Fix: Ctrl+Shift+P collision, pandoc converter availability check, CLI
dangling --css/--reference-doc flags, dead converter button
8 new test suites; 613 tests green; lint clean
A missing/pruned @img/sharp-* binding made the top-level require('sharp')
crash src/main.js at startup, killing the packaged app before any window.
Load sharp through a cached lazy getter instead; when the native module
cannot load, executeOperation resolves the honest failure shape
{ success: false, error: 'Image operations unavailable: <sanitized>' }
(free of absolute paths), mirroring PDFOperations' Task-27 precedent.
Amit Haridas
The interval split mode looped for (i = 0; i < totalPages; i += interval),
which spins forever when interval <= 0. Both the single-file dialog and the
batch dialog can reach it (the batch dialog's validateOperationData only
checks truthiness, so -1 passes). Guard at the source in the main process:
reject non-positive or non-integer intervals before the loop, protecting
both paths and any future caller.
Amit Haridas
Adds pdfGetFormFields (lists AcroForm fields with name/type/value) and
pdfFillForm (fills text fields by name, optionally flattens) to
PDFOperations.js, dispatched via 'formFields'/'fillForm' in
executeOperation. pdfFillForm skips unknown/non-text fields per-field
(logs + continues) rather than failing the whole batch, matching the
partial-success precedent set by pdfExtractImages.
Wires a "Fill Form" entry into the PDF editor dialog: selecting a PDF
fetches its fields via a new get-pdf-form-fields/pdf-form-fields IPC
round trip and renders one text input per field, plus a flatten
checkbox, following the same structure as the crop/pageNumbers dialogs.
Amit Haridas
Adds four new PDFOperations: pdfExtractText (pdfjs-dist getTextContent),
pdfAddPageNumbers (reuses pdfWatermark's position-mapping logic, extracted
into a shared resolvePosition helper), pdfCrop (page.setCropBox against the
existing MediaBox), and pdfExtractImages (pdfjs-dist operator list +
paintImageXObject + sharp). Wired into executeOperation's switch and the PDF
editor dialog UI (4 new sections/toolbar buttons/menu items) with no new IPC
channel needed.
pdfjs-dist v5 is ESM-only, so it's loaded via dynamic import() of its
Node-friendly legacy build; Jest needs --experimental-vm-modules to support
that, so the test scripts now set NODE_OPTIONS accordingly via cross-env.
Amit Haridas
Extends GitOperations.js with diff/branches/checkoutBranch/push/pull,
wires the 5 new IPC handlers in main.js (reusing the existing dir
resolution), whitelists the new channels in preload.js, and fixes the
Git sidebar panel's previously dead _gitDiff callback by wiring up a
diff view, branch list/create/checkout UI, and push/pull buttons.
Resolves Task 5, which deferred this work to this task.
Amit Haridas
Reviewer follow-up on Task 12: the task's own title/brief called for
batch support and no later task in the plan picks it up, so this closes
that gap. Adds a "Single File" / "Batch Folder" mode toggle to the
existing media-operations-dialog.js; batch mode swaps the per-file
input/output fields for an Input Folder + "Include subfolders" +
Output Folder trio while keeping every other parameter (width/height/
quality/angle/startTime/duration/crf/fps/format/fit) applied uniformly
to every matching file. Disabled for audio "Merge", which combines many
inputs into one output and doesn't fit a per-file batch model.
main.js: adds collectFilesByExtension() (src/main/collectFilesByExtension.js,
unit tested), a generalization of the inline collectFiles() closure inside
ipcMain.on('universal-convert-batch', ...) to match a set of extensions
instead of one format. runMediaBatchOperation() loops
ImageOperations/AudioOperations/VideoOperations.executeOperation() over
the matched files, reporting per-file progress via new
'media-batch-progress' events and a final 'media-batch-complete' event,
then shows a "Batch Conversion Complete" dialog.showMessageBox with
completed/failed counts, mirroring performBatchConversion()'s pattern.
Wired via three new ipcMain.on handlers: batch-image-operation,
batch-audio-operation, batch-video-operation.
preload.js: whitelists the three new send channels and the two new
receive channels (media-batch-progress, media-batch-complete).
Add src/main/VideoOperations.js with pure argument-builder functions
(buildConvertArgs, buildCompressArgs, buildTrimArgs, buildFramesArgs,
buildGifArgs) and a single executeOperation entry point that spawns
ffmpeg via dependency-injected execFileFn, mirroring AudioOperations.js.
Wire ipcMain.handle('process-video-operation', ...) in main.js using
getFFmpegPath() and sanitizeErrorMessage(). Update preload.js's
ALLOWED_SEND_CHANNELS: remove 6 stale video-* channel names, add
process-video-operation.
Amit Haridas
Adds AudioOperations.js with pure argument builders (convert/trim/extract/merge)
plus one executeOperation that spawns ffmpeg via a dependency-injected execFileFn,
so tests never invoke a real binary. Wires process-audio-operation in main.js and
updates preload.js's ALLOWED_SEND_CHANNELS to replace the 5 stale audio-* entries.
Amit Haridas
Three lines in tests/main/ImageOperations.test.js (copied verbatim
from the task brief's sample) exceeded the project's 100-char width,
failing npm run format:check. Ran npm run format to auto-fix; no
behavioral change.
Amit Haridas
Add src/main/ImageOperations.js (convert/resize/compress/rotate via
sharp), modeled on PDFOperations.js's executeOperation dispatcher.
Wire ipcMain.handle('process-image-operation', ...) in main.js using
sanitizeErrorMessage() on error paths, and replace the 5 stale/unused
image-* channel names in preload.js's ALLOWED_SEND_CHANNELS with
process-image-operation + select-image-folder (mirroring
select-pdf-folder for a later batch-UI task).
Amit Haridas