New 'Export Visio' toolbar button sends the current graph to the
main process, which generates an OOXML .vsdx zip with JSZip and
writes it to a user-chosen path. The result is fully editable in
Microsoft Visio, draw.io, Lucidchart, and any other tool that
accepts the format — not an embedded image.
Architecture follows the existing pure-module pattern:
- src/flowchart/flowchart-vsdx-export.js: pure translator that
produces (a) page1.xml describing each node as a Visio Shape
with correct geometry (PinX/PinY/Width/Height in inches, with
the Y-axis flipped to match Visio's bottom-up coords), each
edge as a Connect entry, and (b) the seven static boilerplate
XML files that make a .vsdx zip valid ([Content_Types].xml,
_rels/.rels, visio/document.xml, visio/pages/pages.xml, etc.).
- src/main.js: new 'export-vsdx' IPC handler requires the pure
module + JSZip, builds the zip, writes to disk, returns
{ canceled, path } | { canceled: false, error }.
- src/preload.js: flowchart.exportVsdx(graph) bridge.
- src/renderer/flowchart-bundle.js: btnExportVsdx handler, status
feedback for cancel / save / error paths.
- src/flowchart-generator.html: button next to Export JPG.
Tests: 7 new (1362 total) cover graphBounds (empty / single /
multi-node), XML escaping of label text, Connect emission, the 5
node kinds mapping to 5 distinct master names, and that all
boilerplate files start with the XML declaration.
Amit Haridas
Three new toolbar buttons next to Open from File let the user save
the current canvas to disk in the format their tooling expects:
- Export SVG — straight clone + serialise of the canvas <svg>,
with a minimal stylesheet inlined so the file renders without
the editor's stylesheet. Goes through the existing 'save-text-file'
IPC channel as a UTF-8 string.
- Export PNG / JPG — rasterise the SVG through Image + Canvas at
1000x700 + 24px padding, then send the data URL to the new
'save-binary-file' IPC channel which strips the 'data:...;base64,'
prefix and writes the raw bytes. JPEG gets a white background fill
so transparent areas don't come out black.
New IPC + preload bridge:
- src/main.js: 'save-binary-file' handler mirrors 'save-text-file'
shape but writes a Buffer from base64. Returns { canceled, path } |
{ canceled: false, error } so the UI can surface write failures.
- src/preload.js: flowchart.saveBinary(data, defaultName, filters)
so the standalone window can call it.
Tests: 2 new (1355 total, was 1353) covering SVG export happy-path
and cancel. Raster PNG/JPG paths not jsdom-tested because
Image+Canvas in jsdom doesn't reliably fire onload for Blob URLs;
the SVG path exercises the same handler plumbing so coverage is
adequate.
Amit Haridas
The 6 pre-existing flowchart-controller test failures
('ReferenceError: require is not defined') were caused by
flowchart-canvas.js falling through to require('./flowchart-viewport')
when window.FlowchartViewport was undefined — because flowchart-viewport
was added in C4a but the controller test's script loader was never
updated to include it, and viewport itself had no browser-global
fallback.
Two coordinated fixes:
- Add the same UMD wrapper to flowchart-viewport.js that the other
pure modules already use (CommonJS module.exports + window
global fallback for the standalone window and jsdom). Now the
canvas's 'window.FlowchartViewport || require(...)' short-circuits
in the browser case, matching what shapes / store / mermaid do.
- Load flowchart-viewport.js in the controller test mount helper,
in dependency order between mermaid and store. The canvas now
sees window.FlowchartViewport set and never falls through to
require() under jsdom.
Test suite: 104 suites / 1353 tests passing (was 1347 with 6 failing).
The 6 'require is not defined' failures are gone.
Amit Haridas
v4.13.0 — nodes can be resized by dragging the bottom-right handle.
- New setNodeWidth(id, width) on the store. Clamps to [60, 600] so
shapes stay readable. Throws on unknown id; pushes an undo
snapshot like every other mutator.
- New nodeWidth(node) helper on the canvas uses node.width with a
fallback to DEFAULT_WIDTH for graphs loaded from older JSON
that didn't have the field.
- Canvas render uses the per-node width for shapeSvg + label
positioning, and edge boundaryPoint() uses it too so connection
geometry adapts to the new shape size.
- Selected node renders a bottom-right resize handle (10px square).
Clicking the handle starts a 'resize' dragState that calls
setNodeWidth on every pointermove with the screen-to-SVG delta
applied to the start width.
- The handle is detected before the generic node click handler so
it doesn't accidentally start a move drag.
3 new tests cover setNodeWidth update, clamp to [60, 600], and
unknown-id throw. The canvas change reuses the existing 14 tests
(which check rendering and pointer events) — they still pass.
Amit Haridas
Inverse of flowchart-mermaid.js's toMermaid(). Pure module — no DOM,
no globals — so the parsing logic is unit-tested in isolation.
Recognises the 5 node shapes:
- process [label]
- decision {label}
- terminator ([label])
- subroutine [[label]]
- document [/label/]
Recognises the 3 edge arrows:
- --> solid
- -.-> dotted
- ==> thick
Edge labels may appear before OR after the arrow (Mermaid accepts
both: and ).
Behaviour:
- Auto-creates nodes referenced in edges but not declared explicitly.
- Idempotent: declaring then connecting does not
duplicate the A node.
- Defensive: malformed lines are skipped silently so partial /
hand-edited source still loads whatever it can.
- Unescapes the standard Mermaid escapes (#quot; → ", \n → newline).
Wire-up into the renderer (Open from .mmd file → parse →
store.deserialize) is the next commit; this lands the testable math.
23 new tests cover shape parsing for all 5 kinds, edge parsing for
all 3 arrow types + label placement, unescaping, header skipping,
comment skipping, the subroutine-vs-process regression, auto-node
creation, and the null/non-string inputs.
Amit Haridas
New clipboard serialiser for the flowchart editor. Pure module —
no DOM, no globals — so it's unit-testable in isolation.
- Captures the selected node + its connected edges to a JSON envelope
that survives a paste-into-new-graph round trip. Edge-only selection
is also supported.
- Paste creates a new node offset from the original (default 24px)
and re-attaches every connected edge to the new id. Only edges
whose other endpoint still exists in the graph are recreated, so
deleting a neighbour before pasting doesn't try to reconnect to a
ghost.
- Preserves node color on paste.
Wire-up into the renderer controller (Cmd+C / Cmd+V / Cmd+D handlers
+ canvas-owned selection reading) is the next commit; this one just
lands the testable math.
9 new tests cover selection capture, edge-only selection, paste with
offset, edge re-attachment, dangling-edge handling, color preservation,
and the null-payload no-op.
Amit Haridas
New pure functions for the canvas viewport (v4.13.0):
- zoomAt(view, screenX, screenY, factor) — zoom centred on a screen
point, keeping the world point under cursor fixed. Clamped to
[0.25, 4] scale range.
- panBy(view, dx, dy) — additive pan
- reset() — identity transform
- wheelFactor(deltaY) — multiplicative factor per Ctrl+wheel notch
- snap(value, gridSize) — round to nearest grid (0 disables)
The actual SVG transform wrapper + Ctrl+wheel handler + drag-to-pan
+ snap-on-move is the next canvas-layer commit; this commit lands
the testable math.
14 new tests cover: identity reset, cursor-stable zoom, MIN/MAX
clamping, additive pan, wheel factor reciprocity, snap-to-grid
(positive/negative/zero grid sizes).
Amit Haridas
Three UX/DI wins in one canvas pass:
1. Edge endpoints now land on the source/target shape's BOUNDARY
rather than the centres. Lines no longer cut through nodes. New
pure helpers boundaryPoint() + edgeEndpoints() do the geometry;
nodeCenter() is still used internally for direction vectors.
2. Connect-mode preview line. Alt+drag now draws a dashed line from
the source node's boundary to the live pointer position so the
user can see where the edge will land before releasing. The original
code marked this 'visual feedback deferred to v2'.
3. Selection state is now owned by the canvas (selectedNodeId /
selectedEdgeId variables), not read from .selected DOM classes.
The controller can ask via api.getSelection(). Fixes the fragile
pattern in flowchart-controller.js where Delete/Backspace read
querySelector('.flowchart-node.selected') — that read is now
redundant and the controller can be cleaned up to use getSelection()
directly.
5 new tests cover: edge starts past source centre, edge ends before
target centre, vertical edges connect top/bottom, label background
auto-sizes for long labels, canvas getSelection() reflects clicks.
Amit Haridas
Inline AI first-use confirmation (v4.13.0):
- inline-ai-controller accepts optional confirmFirstUse + getProviderLabel
deps. On the first action of the session, calls confirmFirstUse with
{providerLabel, selectionLength, action}; if it returns false, aborts
and shows error state. Once confirmed, never re-prompts for the
session.
- new 'ai-assistant:confirm-info' invoke handler returns
{provider, model} (no key material) so the renderer can label the
confirm dialog.
- electronAPI.aiAssistant.confirmInfo() exposed via preload.
- renderer.js wires window.confirm() with the provider label as the
v1 prompt UX; can be upgraded to a custom modal later without
touching the controller.
Check-for-Updates menu (v4.13.0):
- Help > Check for Updates now calls ensureAutoUpdater().check()
instead of opening the Releases page in the browser.
- Falls back to the browser page if the check throws (dev runs with
no app-update.yml, network failures, etc.) so the user always has
a way to update manually.
4 new controller tests cover: confirm called once with provider label
and selection length; declining aborts + surfaces error state; confirm
not asked twice in a session; missing confirmFirstUse dep skips the
prompt and streams normally.
Amit Haridas
electron-updater integration so users get fixes automatically:
- new dependency electron-updater@^6.6.2 in package.json
- electron-builder.config.js publishes to GitHub releases
(owner: concreteinfo, repo: markdownconverter)
- new src/main/auto-updater.js wraps autoUpdater: wires the six
lifecycle events to a 'updates:status' IPC channel, exposes
check() and install() helpers, dev-gates via isDev(), and
swallows the 'missing app-update.yml' error that dev runs hit
- main.js: ensureAutoUpdater() forwards status to the renderer
via mainWindow.webContents.send; ipcMain handlers for
'updates:check' (invoke) and 'updates:install' (invoke)
- preload.js: 'updates:check' / 'updates:install' added to
ALLOWED_SEND_CHANNELS; 'updates:status' added to receive
channels; electronAPI.updates.{check, install, onStatus}
exposed
7 new tests cover event registration, dev-gate behaviour,
'not-available' fallback for missing app-update.yml, typed event
payloads, error forwarding, and install() quitAndInstall.
Amit Haridas
The explorer panel's explorerCurrentDir is hardcoded to null, so
the Quick Switcher's workspace toggle was a no-op. Rather than fix
the broken explorer flow, derive the workspace from the active
tab's filePath's parent directory — the most reliable source we
have without restructuring that panel.
deriveWorkspaceDir() handles POSIX and Windows paths (including
mixed separators and trailing separators), returns null for
untitled / empty inputs so the workspace toggle silently stays
off in those cases.
10 new tests cover POSIX, Windows, mixed-separator, trailing
separator, UNC, and invalid input paths.
Amit Haridas
createInlineAiController() glues the popover, the streaming bridge,
and the CodeMirror editor together. Each Cmd+K opens a fresh
request; the controller:
- reads the active selection from the CodeMirror view
- builds the rewrite/shorten/expand prompt via inline-assist.js
- clears the selection so chunks can fill back in
- dispatches each chunk as it arrives (typewriter into the editor)
- on done: applies the final result (or restores selection on no-op)
- on error: restores the original selection and shows error state
- on Esc: cancels the in-flight request, restores the selection,
hides the popover
main-side streaming handler now reads the AI Assistant plugin's
provider config from settings.json under plugins.ai-assistant.* —
keys never cross the IPC boundary.
renderer.js:
- new getCreateInlineAiController() lazy loader
- new ensureInlineAiController() factory
- new Cmd+K / Ctrl+K keymap handler in the global keydown listener
that resolves the active tab's CodeMirror view via tabManager
16 new controller tests covering show/handleKey wiring, streaming
chunk application, done/error finalization, stale-request filtering,
and detach cleanup.
Amit Haridas
createInlineAiPopover() mounts a floating toolbar anchored to the
current text selection. Three actions (Rewrite / Shorten / Expand)
plus loading spinner with cancel, and an error state with retry and
dismiss. Click handlers dispatch via onAction/onCancel/onRetry.
Positioning flips below the line if there is no room above and
clamps horizontally so the popover stays inside the viewport.
16 new tests covering mount/visibility, idle actions dispatching the
selection text, state transitions (idle / loading / error), Retry
and Dismiss callbacks, and positioning edge cases.
Amit Haridas
AiProviders.completeStream() — async iterable over provider chunks.
- OpenAI-style (openai, ollama, lmstudio, *-compatible): parses
SSE data: {choices:[{delta:{content}}]} payloads.
- Anthropic: parses event: content_block_delta with delta.text.
- Honours caller AbortSignal + internal timeout via AbortController.
- shared parseSseStream() helper handles [DONE] sentinel, partial
lines across chunk boundaries, reader.releaseLock() in finally.
main.js IPC:
- 'ai-assist-stream:start' (send) registers a per-requestId entry
in a Map with an AbortController, iterates completeStream, and
forwards each chunk via 'ai-assist-stream:chunk' (send).
- 'ai-assist-stream:cancel' (send) aborts the in-flight request.
- Terminal 'done' or 'error' (with code+message) is sent after
each stream; renderer can correlate by requestId.
preload.js exposes aiAssist.{start, cancel, onChunk, onDone, onError}.
5 new tests covering OpenAI delta parsing, Anthropic content_block_delta
parsing, and three synchronous-error paths (no fetch, no messages,
missing key).
Amit Haridas
buildAssistPrompt(action, selection) returns the {system, messages}
payload for rewrite/shorten/expand. Enforces 8KB selection cap and
typed errors with codes (unknown_action, bad_selection,
empty_selection, selection_too_large) so the IPC layer can map them
to user-friendly messages.
applyAssistResult(original, newText) trims the response and rejects
empty/unchanged payloads so a no-op streaming chunk doesn't churn
the editor.
15 new tests cover action coverage, prompt content, all four error
codes, cap-edge (exactly 8KB OK, 8KB+1 rejected), and trim semantics.
Amit Haridas
createQuickSwitcherOverlay() mounts a centered modal with:
- input box with autocomplete=off
- workspace toggle (off by default — recent + open tabs only)
- keyboard nav: ArrowDown/Up move selection, Enter opens,
Escape and backdrop-click hide
- mouse hover also moves selection
- debounced refresh (default 80ms; 0 in tests)
- destroy() cleanly removes DOM + listeners
Rank/filter logic delegated to fuzzy-matcher.js. Recent and open tabs
are always in the candidate set; workspace files are added only when
the toggle is on. Selection clamps to bounds after every refresh.
23 new tests covering mount/focus/destroy, initial render, typing &
filtering, full keyboard nav, workspace toggle behavior, and the
selected-path escape hatch.
Amit Haridas
listWorkspaceFiles() walks a directory recursively, returning
[{path, name}] for files matching a configurable extension allowlist.
Skips hidden dirs, node_modules, dist/build/coverage, and walks past
permission-denied subdirs instead of throwing. Default allowlist is
markdown variants + .txt; default maxResults caps pathological
workspaces at 2000 entries.
Wired through 'quick-switcher:list-files' invoke channel + a
quickSwitcher.listFiles() convenience method on the preload bridge.
Main-process errors collapse to [] rather than crashing the overlay.
16 new tests covering flat dirs, recursion, skip lists, custom
extensions, maxResults cap, and validation errors.
Amit Haridas
Tiered scoring (exact 1.0 / stem 1.0 / prefix 0.8 / subseq 0.5+bonuses
capped 0.79) with leftmost+rightmost greedy alignment — rightmost wins
for extension/suffix queries ('md' against 'readme.md'), leftmost wins
for prefix queries ('re' against 'readme.md'). Boost-aware rankResults
applies recency x2 and open-tabs x1.5 multiplicatively; empty query
falls back to boosts alone.
Pure module, no DOM/IPC/Node — runs in renderer or main. 22 new tests.
Amit Haridas
User feedback on v4.11.0: the Add Connection form was buried below the
node/edge lists and they couldn't find it; they also asked for per-node
color and Save to File export. v4.12.0 reorganises the #fc-nodelist panel
to put the connect form right after the Add Node buttons, adds a native
<input type="color"> per node row that drives a new store.setNodeColor
mutator (with serialize/deserialize round-trip), and wires a Save to File
button that pops a system save dialog via a new 'save-text-file' IPC
channel.
- src/flowchart-generator.html — panel order is now Add Node /
Add Connection / Nodes / Edges / Export (Insert · Save · Reset).
Removed the legacy top toolbar; status moved into the panel.
Added .fc-help, per-node color-picker CSS, .fc-toolbar-row.
- src/flowchart/flowchart-shapes.js — shapeSvg() accepts an optional
6th 'color' arg; emits fill=... on rects and polygons. Falls back to
#ffffff for empty/null/undefined so old callers keep working.
- src/flowchart/flowchart-store.js — setNodeColor(id, color) mutator
with snapshot/emit, normalizeColor() helper (hex validation),
addNode accepts color, deserialize normalises missing color.
- src/renderer/flowchart-bundle.js — mirrors all of the above inline
(the bundle is loaded as a single <script>), wires fc-btn-save to
api.saveFile, passes node.color to shapeSvg in the canvas render,
and renders <input type='color'> per node row.
- src/main.js — new ipcMain.handle('save-text-file', ...) using a
system Save dialog (mirrors the ascii:save handler shape).
- src/preload.js — added 'save-text-file' to ALLOWED_SEND_CHANNELS
and a saveFile(content, defaultName) helper on the flowchart bridge.
Tests: 92/92 suites, 1165/1165 tests pass on this run
- +9 store tests (color defaults, setNodeColor, undo, hex validation,
round-trip, deserialize normalisation)
- +7 shapes tests (fill color on all 5 kinds, fallback to #ffffff)
- +6 controller tests (color picker wires setNodeColor, canvas rect
reflects color, Save to File calls api.saveFile with the fenced
Mermaid source and 'flowchart.mmd', cancel + error paths surface in
status)
Verification:
- npm run lint clean
- npm run format:check clean
- npm test 92/92 suites, 1165/1165 tests pass
Amit Haridas
Replaced the v4.10.0 floating selection toolbar (which relied on
SVG click hit-testing inside #canvas-host and was unreliable in the
user's Electron runtime) with a button-driven node-list panel
(#fc-nodelist) below the canvas. Every mutation now flows through
explicit controls: 5 Add Node buttons, per-node kind-select +
label-input + delete ×, per-edge kind-select + label-input + delete
×, and a Connect form (From/To selects + Edge button + Refresh).
The canvas is purely visual now: no more #fc-selection-toolbar, no
controller-level _selectedId/_selectedKind state, no keyboard
Delete/Backspace selection handler. Canvas click callbacks
(onNodeClick, onEdgeClick, onShapeMenu) are no-ops. The 5-shape /
3-edge-kind / label-input / delete-button surface is unchanged in
spirit — it just lives in the panel instead of floating over the
canvas.
Files:
- src/flowchart-generator.html: added #fc-nodelist between canvas and
preview (wrapped in new .fc-left column), removed fc-selection-toolbar,
bumped header hint.
- src/renderer/flowchart-bundle.js: removed SHAPE_BUTTONS/EDGE_BUTTONS,
setSelection, renderSelectionToolbar, appendLabelInput,
appendDeleteButton, the keyboard Delete handler, and the
controller-level selection state. Added rerenderNodeList +
shapeLabel/edgeLabel, wired all panel buttons (add-row, per-node
selects/inputs/deletes, per-edge selects/inputs/deletes, connect
form). Subscribe now calls rerenderNodeList instead of the old
renderSelectionToolbar.
- tests/flowchart-controller.test.js: replaced 6 v4.10.0 selection
toolbar tests with 11 v4.11.0 button-driven panel tests.
- package.json: 4.10.0 -> 4.11.0.
- README.md: version line bumped to v4.11.0.
- UPDATES.md: added v4.11.0 changelog entry.
Test summary: 92 suites, 1143 tests pass (was 1138 — +5 net).
Lint + prettier clean.
Amit Haridas
Even after the v4.9.9 inline-modal fix the user kept reporting 'no fix still'
because hidden right-click context menus and window.prompt calls remain
unreliable in Electron renderer contexts. This release ships a *visible*
floating selection toolbar inside the canvas panel so the primary
interactions are not hidden behind modals or context menus.
- New <div id="fc-selection-toolbar"> inside #canvas-host, hidden until a
node or edge is selected. Renders:
* 5 shape buttons (Process / Decision / Terminator / Subroutine /
Document) when a node is selected, calling store.setNodeKind on
click. The active shape is highlighted.
* 3 edge-kind buttons (Solid / Dotted / Thick) when an edge is
selected, calling store.setEdgeKind.
* An always-visible label input that mirrors the selected item's
label and writes back via store.setNodeLabel / store.setEdgeLabel
with a 100ms debounce.
* A red Delete button calling store.removeNode / store.disconnect.
- Selection state now tracked at the controller level (_selectedId +
_selectedKind) and exposed via window.FlowchartController so the
keyboard Delete/Backspace shortcut and the toolbar share one source
of truth.
- Console-log instrumentation on every canvas event (pointerdown with
altKey, pointerup with drag result, dblclick, contextmenu, selection
changes, and every bootstrap phase) so the user can open DevTools
(Ctrl+Shift+I) and verify Alt+drag and double-click actually fire.
- promptInline / confirmInline kept as advanced fallback for the
right-click 'change shape' path; the toolbar is now the primary
interaction surface.
- Preview-render pane now shows a static info card explaining that the
canvas on the left is the rendered chart (previously blank).
- 6 new tests in tests/flowchart-controller.test.js — toolbar hidden
by default, selecting a node populates shape buttons + label input +
Delete, clicking a shape button updates the kind, label input is
debounced, edge selection populates edge-kind buttons, Delete button
removes the selected node.
Total tests: 1127 passing (was 1121; +6 new). The 11 pre-existing
PDFOperations failures are unrelated to this change.
Electron renderer contexts return undefined from window.prompt/window.confirm,
so shape change, edge kind, edge label, and reset confirmation did nothing.
Bundle now ships promptInline/confirmInline (custom DOM overlay modals) and
uses them in onEdgeClick / onShapeMenu / reset handler. Exposed as
window.FlowchartModals for jsdom tests.
Amit Haridas
Each pure module's UMD wrapper assigned window.FlowchartXxx only in the else branch — when 'module' was undefined. But the renderer runs with nodeIntegration:true, so 'module' is always truthy and the else branch never ran. The standalone Flowchart Generator window then aborted with 'Flowchart pure modules not loaded'.
Fix: append 'if (typeof window !== undefined) window.FlowchartXxx = exported;' after the CommonJS branch in all 4 pure modules (store, shapes, mermaid, canvas). Both branches can run now; the legacy sidebar panel still loads them via require() and the renderer unconditionally exposes the global.
Regression guard: 4 new source-grep tests in tests/flowchart-controller.test.js assert each module's source contains the 'window.FlowchartXxx = exported' assignment.
Attribution: Amit Haridas
Five fix rounds (v4.9.1 → v4.9.5) couldn't make the sidebar flowchart
panel feel right — at 280 px sidebar with canvas + preview cramped to
~175 px each, plus the editor-container hide/show dance the
maximize/restore toggle required, the panel kept presenting as
cramped and unreliable at runtime. Strategy pivot: the flowchart
editor now lives in its own BrowserWindow, matching the ASCII Art
Generator pattern.
- src/flowchart-generator.html — new standalone HTML with header,
toolbar (Insert at Cursor / Reset), canvas host, and preview host.
Stylesheet hrefs are src/-relative (no ../). Forced light surface
via background/color !important rules — mirrors the v4.9.5 CSS fix.
- src/renderer/flowchart-controller.js — pure browser IIFE. Hydrates
from <userData>/flowchart-session.json once on mount, persists on
every mutation with 500 ms debounce. Insert at Cursor wraps the
generated Mermaid source in a fenced ```mermaid block and sends
it through the existing insert-content IPC.
- src/main.js — openFlowchartGenerator() launches the standalone
BrowserWindow (1100×720, contextIsolation:true, nodeIntegration:false).
Tools menu entry 'Flowchart Generator' with Cmd/Ctrl+Alt+F.
- src/preload.js — new window.electronAPI.flowchart.* namespace with
getUserDataPath/readFile/writeFile/insertAtCursor, reusing the
existing thin text-file IPC handlers.
- The four pure modules (flowchart-shapes.js, flowchart-mermaid.js,
flowchart-store.js, flowchart-canvas.js) gained a tiny UMD wrapper
so they work both as CommonJS (legacy sidebar) and as browser
globals (standalone window). No behavioural change to the 73
flowchart unit tests in tests/flowchart-*.test.js.
- src/renderer.js — sidebar registerPanel('flowchart', ...) and the
commandPalette entry are now commented out. Legacy panel file
(src/sidebar/flowchart-panel.js) preserved for rollback.
Tests:
- tests/flowchart-controller.test.js — 10 new tests covering
stylesheet paths (no ../ escape), bootstrap wiring
(getUserDataPath once, reads flowchart-session.json), hydration
from a saved session, Insert-at-Cursor fenced block format,
Reset (with and without confirm), and two regression tests that
src/renderer.js no longer has a live sidebar registration.
All 1122 tests pass; lint + format clean.
Amit Haridas
v4.9.4 shipped three interaction bugs in the Flow Chart panel that combined to make it look broken at runtime: (1) .flowchart-preview-render had no min-height, so the Mermaid SVG clipped to 0 when the sidebar flex column shrank; (2) the canvas + preview hosts inherited the body.theme-concreteinfo dark theme, producing dark-on-dark nodes; (3) .flowchart-node.selected only set a 2px stroke on the rect's existing dark fill, which was effectively invisible.
Fix: styles-sidebar.css adds min-height: 120px on .flowchart-preview-render; forces a light background on .flowchart-canvas-host / .flowchart-preview-host with !important so theme inheritance cannot override it; forces explicit white fills and dark strokes on .flowchart-node rect/polygon/text and .flowchart-edge so labels read against any background; selection now changes fill (#e3f0ff) AND bumps stroke-width to 3 on both nodes and edges. renderer.js renderFlowChartMermaid now always initializes Mermaid with theme: 'default' regardless of body class — keeps the Mermaid SVG light to match the CSS-forced canvas surface.
Three new CSS regression tests in tests/flowchart-panel.test.js read the shipped stylesheet and assert the three structural invariants (non-zero min-height on render target, !important light background on canvas+preview hosts, fill + stroke-width >= 3 on .flowchart-node.selected).
Tradeoff: the flowchart surface is now always light, diverging from body theme. Visibility of a working editor is the priority per user direction.
Amit Haridas
- Add opts.onNodeClick + surgical applySelectionHighlight() so node/edge clicks immediately paint .selected and mirror id into panel state (Delete/Backspace works on freshly-clicked node).
- Add Maximize/Restore button to flowchart toolbar: toggles .main-content.flowchart-takeover which hides .editor-container and lets the canvas + preview split the full window width instead of the 280px sidebar.
- destroy() clears the takeover class so the editor stays usable after leaving the panel.
- 8 new tests in tests/flowchart-panel.test.js for selection wiring and takeover.
Amit Haridas
The flowchart preview pane accumulated raw Mermaid source when the user
fired several addNode mutations within the 250ms debounce window —
mermaid.run({nodes:[div]}) is async, so the previous render's
<div class="mermaid"> (still carrying the source text) sat in
.flowchart-preview-render when the next render cleared the target. The
first render's eventual element.innerHTML=svg landed on a detached node,
but the visible preview pane had a stack of stale <div class="mermaid">
elements carrying the source.
Fixed by switching renderFlowChartMermaid (src/renderer.js) to
replaceChildren() and adding a per-target WeakSet in-flight tracker so
the new render always starts from a clean slate and the previous render's
eventual innerHTML=svg is harmless on a detached node.
Added a second regression test that fires 7 mutations inside the debounce
with a renderMermaid mock that mimics mermaid.run's async innerHTML=svg
closure.
Amit Haridas
- src/ascii-generator.html line 7: ../fonts.css -> fonts.css (src/-relative;
fixes silent stylesheet miss — same class as the v4.9.0->v4.9.1 script-tag fix)
- src/renderer/ascii-controller.js: wire all 3 mode tabs, 18 template
buttons, and the 3 box form fields. setMode() toggles .active on tabs
and matching .mode-section. Templates route through the orchestrator's
template:<id> font namespace. Box mode renders text with single/double/
rounded/bold/ascii borders via a pure renderBox() helper. All 11 T9
behaviours preserved.
- tests/ascii-controller.test.js: 6 tests covering stylesheet path,
pure box renderer, mode-tab switching, and template button wiring.
- package.json 4.9.1 -> 4.9.2; README + UPDATES updated.
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
Replace the page-reload IPC handling in the theme-changed listener with an
in-place toggle of the preloaded <link id="theme-*"> tags. The helper scans
for a matching link first and only mutates disabled states when one exists,
so an unknown id (or main-process miss) short-circuits and preserves the
currently active theme. body.className is always set to the requested id so
legacy selectors keep matching.
Add tests/theme-renderer-apply.test.js with three cases covering: switching
to a fresh theme, idempotent re-switch, and the unknown-id short-circuit.
The test's local helper mirrors the renderer implementation so future drift
is caught at test time.
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
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
- src/utils/csv-to-table.js — pure module. Auto-detects the delimiter
(tab wins over comma; comma wins over semicolon). RFC 4180-style
parsing handles quoted fields, escaped quotes (""), and CRLF. Cells
are escaped for markdown tables (\\ for backslashes, \| for pipes,
newlines stripped). Alignment: a column is right-aligned when every
non-empty cell matches a numeric pattern AND there are ≥2 rows or
≥1 multi-character cell (single-char numbers like "1" alone are
too ambiguous to call as numeric — could be labels). Single-row input
produces a header-only table (no fabricated "Column N" labels, no
body).
- src/editor/smart-paste.js — paste handler now tries CSV first (it
needs no async), falls back to the existing URL → title flow.
Tests (30 new, tests/csv-to-table.test.js):
- detectDelimiter: tab > comma > semicolon, empty/non-string safe
- parseRows: simple, RFC 4180 quoted, escaped quotes, CRLF, multi-line
- escapeCell: pipes, backslash-first escaping, newline strip, null/number
- csvToTable: simple CSV, single-row (header-only), TSV, empty input
- alignment: numeric detection (≥2 rows OR ≥1 multi-char cell), currency,
percentages, text columns stay left
- escaping inside cells (pipes, newlines)
- ragged-row padding
- looksLikeCsv: true for tab/comma multi-row, false for single row,
column-count mismatch, prose, very short input
Full suite: 72 suites, 861 tests, lint+format clean.
Amit Haridas