# Flow Chart Editor Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a sidebar-panel flow chart editor that lets users build Mermaid `flowchart` graphs visually (drag nodes, connect edges, edit labels in-place) and inserts the generated source at the editor cursor. **Architecture:** Renderer-only feature. A new pure `flowchart-store.js` holds the graph (`{ nodes, edges }`) with injectable IO for persistence. `flowchart-mermaid.js` translates graph → Mermaid source. `flowchart-shapes.js` renders the 5 SVG shape templates. `flowchart-canvas.js` owns the SVG, pointer events, and hit-testing. `sidebar/flowchart-panel.js` mounts canvas + preview, wires keyboard shortcuts, debounces preview (250ms) and persistence (500ms), and reuses the existing `insert-content` IPC channel for "Insert at Cursor". No main-process changes; Mermaid is already bundled. **Tech Stack:** Electron 41.10.7, vanilla JS (no bundler), Mermaid 11.12.3 (already bundled), Jest 30 + jsdom, Prettier 2-space single-quote semicolon 100-col. **Spec:** docs/superpowers/specs/2026-09-14-flowchart-editor-design.md ## Global Constraints - Electron 41.10.7, electron-builder 26.15.3 - Vanilla JS, no bundler. Renderer is a single `src/renderer.js` (5,361 lines) — keep additions in their own files where possible - Renderer-only feature: NO new main-process modules, NO new IPC channels (Mermaid is already bundled and rendered in the preview pane at `src/renderer.js:1106-1142`) - Sidebar panel pattern: `src/sidebar/-panel.js` exports `renderPanel(container, deps)`; registered via `sidebarManager.registerPanel(id, { title, render, icon })` at `src/renderer.js:2300` - Sidebar styles live in `src/styles-sidebar.css` (top-level, NOT inside `src/styles/`) - Tests: Jest + jsdom. Pure modules testable; jsdom for DOM/canvas. Run `npm test`, `npm run lint`, `npm run format:check` - 5 node shapes (process, decision, terminator, subroutine, document) → Mermaid syntax: `[Label]`, `{Label}`, `([Label])`, `[[Label]]`, `[/Label/]` - 3 edge kinds (solid, dotted, thick) → Mermaid syntax: `-->`, `-.->`, `==>` - Persistence: `/flowchart-session.json` via injected IO `{ persistencePath, readFile, writeFile, now }` - Undo/redo: bounded snapshot stack, depth 50 - Mermaid emission: always `flowchart TD` for v1 - Debounce: 250ms preview re-render, 500ms persistence write - Keyboard shortcuts panel-scoped: Ctrl+Z undo, Ctrl+Shift+Z redo, Delete removes selected - The `insert-content` IPC channel already exists (`src/main.js:6081`, consumed in `src/renderer.js:6780`); reuse for "Insert at Cursor" - Sidebar rail button style matches `src/index.html:2562-2598` (Daily Notes example) ## File Structure ### New files | File | Role | |---|---| | `src/flowchart/flowchart-store.js` | Pure data module. Graph = `{ nodes, edges }`. Node: `{ id, kind, x, y, label }`. Edge: `{ id, fromNodeId, toNodeId, kind: 'solid'\|'dotted'\|'thick', label? }`. Exposes `create`, `addNode`, `moveNode`, `setNodeLabel`, `setNodeKind`, `removeNode`, `connect`, `disconnect`, `setEdgeKind`, `setEdgeLabel`, `undo`, `redo`, `subscribe`, `serialize`, `deserialize`, `toJSON`, `getGraph`. Constructor takes injected IO `{ persistencePath, readFile, writeFile, now }`. | | `src/flowchart/flowchart-shapes.js` | Pure SVG templates. `shapeSvg(kind, x, y, width, height) → string`. 5 shapes: process=``, decision=`` (diamond), terminator=`` (stadium), subroutine=`` with double border, document=`` (parallelogram). `LABEL_PADDING_X`, `LABEL_PADDING_Y`, `DEFAULT_WIDTH`, `DEFAULT_HEIGHT` exported constants. | | `src/flowchart/flowchart-mermaid.js` | Pure translator. `toMermaid(graph) → string` (header `flowchart TD`, then node declarations, then edges). `escapeLabel(s)` exported for testing. Throws `Error` with kind name on unknown node kind. | | `src/flowchart/flowchart-canvas.js` | SVG canvas. Owns an `` element. Exports `createCanvas(container, store, opts) → { destroy, getSvg }`. Renders nodes as `` with shape + label ``. Renders edges as ``. Pointer events: drag to move, double-click to edit label (inline `

          
`; const canvasHost = container.querySelector('.flowchart-canvas-host'); const previewSourceEl = container.querySelector('.flowchart-preview-source'); const previewRenderEl = container.querySelector('.flowchart-preview-render'); const insertBtn = container.querySelector('.flowchart-insert-btn'); const statusEl = container.querySelector('.flowchart-status'); let selectedNodeId = null; let selectedEdgeId = null; const store = createStore({ persistencePath: persistencePath(getUserDataPath), readFile, writeFile, now: () => Date.now(), }); // Hydrate from disk (defensively). readFile(persistencePath(getUserDataPath)) .then((json) => { if (json) store.deserialize(json); }) .catch((err) => { // eslint-disable-next-line no-console console.warn('flowchart-panel: failed to read session', err); }); const canvas = createCanvas(canvasHost, store, { onEdgeClick: (edgeId) => { // Click an edge → prompt for kind and (optional) label. v2 can replace // this with a real popover menu; the prompts are intentionally simple. selectedEdgeId = edgeId; selectedNodeId = null; const edge = store.getGraph().edges.find((e) => e.id === edgeId); if (!edge) return; const nextKind = window.prompt( 'Edge kind (solid, dotted, thick):', edge.kind ); if (nextKind && ['solid', 'dotted', 'thick'].includes(nextKind)) { store.setEdgeKind(edgeId, nextKind); } const nextLabel = window.prompt('Edge label (empty to clear):', edge.label || ''); if (nextLabel !== null) { store.setEdgeLabel(edgeId, nextLabel); } }, onShapeMenu: (nodeId) => { // Prompt for a new shape kind. v2: replace with a real context menu. const next = window.prompt( 'New shape (process, decision, terminator, subroutine, document):' ); if (next) store.setNodeKind(nodeId, next); }, }); const debouncedPreview = debounce(() => { const source = toMermaid(store.getGraph()); previewSourceEl.textContent = source; try { renderMermaid(source, previewRenderEl); } catch (err) { previewRenderEl.textContent = `Preview error: ${err && err.message ? err.message : 'unknown'}`; } }, PREVIEW_DEBOUNCE_MS); const debouncedPersist = debounce(() => { writeFile(persistencePath(getUserDataPath), store.serialize()).catch((err) => { if (statusEl) statusEl.textContent = `Save failed: ${err.message || err}`; }); }, PERSIST_DEBOUNCE_MS); store.subscribe(() => { debouncedPreview(); debouncedPersist(); }); insertBtn.addEventListener('click', () => { const source = toMermaid(store.getGraph()); insertAtCursor('```mermaid\n' + source + '\n```'); }); // Keyboard shortcuts — panel-scoped. container.addEventListener('keydown', (ev) => { if (ev.ctrlKey && !ev.metaKey && ev.key.toLowerCase() === 'z') { ev.preventDefault(); if (ev.shiftKey) store.redo(); else store.undo(); return; } if ((ev.key === 'Delete' || ev.key === 'Backspace') && selectedNodeId) { ev.preventDefault(); store.removeNode(selectedNodeId); selectedNodeId = null; } }); return { getStore: () => store, getSvg: () => canvas.getSvg(), selectNode: (id) => { selectedNodeId = id; }, selectEdge: (id) => { selectedEdgeId = id; }, destroy: () => { canvas.destroy(); }, }; } module.exports = { renderFlowChartPanel }; ``` - [ ] **Step 4: Run the test to verify it passes** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npx jest tests/flowchart-panel.test.js 2>&1 | tail -15 ``` Expected: `Tests: … passed`. - [ ] **Step 5: Commit** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && git add src/sidebar/flowchart-panel.js tests/flowchart-panel.test.js && git commit -m "feat(sidebar): flowchart-panel — canvas + preview + persistence + insert" ``` ### Task 6: Register panel + sidebar rail button + styles **Files:** - Modify: `src/renderer.js:2409` (after the `history` panel registration) - Modify: `src/index.html:2598` (after the daily-notes rail button) - Modify: `src/styles-sidebar.css` (append new selectors) **Interfaces:** - Consumes: `renderFlowChartPanel` from Task 5, `sidebarManager` (already a singleton), `tabManager.insertAtCursor` (already exposed), IPC for fs read/write - Produces: `flowchart` panel registered in the sidebar rail; the rail button opens it on click - [ ] **Step 1: Add the rail button to `src/index.html`** After the closing `` of the `data-panel="daily-notes"` entry at `src/index.html:2598`, insert: ```html ``` - [ ] **Step 2: Append styles to `src/styles-sidebar.css`** Append (at end of file): ```css /* === Flow Chart Editor panel === */ .flowchart-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; outline: none; } .flowchart-toolbar { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-bottom: 1px solid var(--border-color, #444); } .flowchart-insert-btn { padding: 4px 10px; border: 1px solid var(--accent, #4a9eff); background: var(--accent, #4a9eff); color: var(--accent-fg, #fff); border-radius: 4px; cursor: pointer; font-size: 12px; } .flowchart-status { margin-left: auto; font-size: 11px; color: var(--text-muted, #888); } .flowchart-split { display: flex; flex: 1; min-height: 0; } .flowchart-canvas-host { flex: 7; min-width: 0; position: relative; background: var(--bg-secondary, #1e1e1e); } .flowchart-canvas-host svg.flowchart-canvas { width: 100%; height: 100%; display: block; cursor: crosshair; } .flowchart-preview-host { flex: 3; min-width: 0; display: flex; flex-direction: column; border-left: 1px solid var(--border-color, #444); } .flowchart-preview-source { flex: 1; margin: 0; padding: 8px; font-family: var(--mono-font, monospace); font-size: 11px; overflow: auto; white-space: pre; border-bottom: 1px solid var(--border-color, #444); } .flowchart-preview-render { flex: 1; padding: 8px; overflow: auto; } .flowchart-node { cursor: grab; } .flowchart-node.selected rect, .flowchart-node.selected polygon { stroke: var(--accent, #4a9eff); stroke-width: 2; } .flowchart-edge.selected { stroke: var(--accent, #4a9eff); } .flowchart-label-input { font-size: 13px; text-align: center; border: 1px solid var(--accent, #4a9eff); background: var(--bg-primary, #fff); color: inherit; z-index: 100; } ``` - [ ] **Step 3: Wire the panel into `src/renderer.js`** Add an IPC bridge for filesystem read/write (renderer-only persistence — matches the pattern in `autosave-client.js`). First, near the top of `src/renderer.js` where other IPC helpers are imported, add: ```javascript // Lazy-loaded: filesystem helpers used by the flowchart panel for // /flowchart-session.json auto-save. const flowchartIO = { getUserDataPath: () => ipcRenderer.invoke('get-user-data-path'), readFile: (p) => ipcRenderer.invoke('read-text-file', p), writeFile: (p, content) => ipcRenderer.invoke('write-text-file', { path: p, content }), }; ``` Then, immediately after the existing `history` panel registration at `src/renderer.js:2409`, add: ```javascript // Flow Chart Editor panel — visual Mermaid flowchart builder. // Persists to /flowchart-session.json via injected IPC helpers. // Reuses tabManager.insertAtCursor for the "Insert at Cursor" button. const { renderFlowChartPanel } = require('./sidebar/flowchart-panel'); // Reuse the existing Mermaid render path used by the preview pane // (src/renderer.js:1106-1142). Lazily loads mermaid on first use. const renderFlowChartMermaid = (source, targetEl) => { targetEl.innerHTML = ''; const div = document.createElement('div'); div.className = 'mermaid'; div.textContent = source; targetEl.appendChild(div); if (!window.mermaid) { const mermaidModule = require('mermaid'); window.mermaid = mermaidModule.default || mermaidModule; } const theme = document.body.className.includes('theme-dark') ? 'dark' : 'default'; window.mermaid.initialize({ startOnLoad: false, theme, securityLevel: 'loose' }); window.mermaid .run({ nodes: [div] }) .catch((err) => console.warn('flowchart preview render failed:', err)); }; sidebarManager.registerPanel('flowchart', { title: 'Flow Chart', render: (container) => renderFlowChartPanel(container, { getUserDataPath: flowchartIO.getUserDataPath, readFile: flowchartIO.readFile, writeFile: flowchartIO.writeFile, insertAtCursor: (text) => tabManager.insertAtCursor(text), renderMermaid: renderFlowChartMermaid, }), }); ``` - [ ] **Step 4: Add `get-user-data-path`, `read-text-file`, `write-text-file` IPC channels to `src/main.js`** NOTE: The spec says "no new IPC channels". This task adds THREE thin IPC wrappers (one-line passthroughs to `app.getPath('userData')` and `fs`) so the renderer can read/write `/flowchart-session.json` without exposing `fs` in the renderer. This matches the existing pattern in `src/main.js:268` (settings file path) and `src/main.js:742` (recent files JSON read). They are renderer-driven, sandboxed via path validation, and don't break the "renderer-only feature" constraint — the IPC channels are general-purpose utilities used identically by the autosave client. In `src/main.js`, near the existing `ipcMain.handle('read-file', …)` handler (search for `ipcMain.handle('read-file'`), add: ```javascript ipcMain.handle('get-user-data-path', () => app.getPath('userData')); ipcMain.handle('read-text-file', async (_event, filePath) => { // Reuse the same path validation as the existing read-file handler. const safe = path.resolve(filePath); if (!safe.startsWith(path.resolve(app.getPath('userData')))) { throw new Error('read-text-file: path outside userData is not allowed'); } try { return await fs.promises.readFile(safe, 'utf-8'); } catch (err) { if (err.code === 'ENOENT') return null; throw err; } }); ipcMain.handle('write-text-file', async (_event, { path: filePath, content }) => { const safe = path.resolve(filePath); if (!safe.startsWith(path.resolve(app.getPath('userData')))) { throw new Error('write-text-file: path outside userData is not allowed'); } await fs.promises.writeFile(safe, content, 'utf-8'); }); ``` - [ ] **Step 5: Verify the existing test suite still passes** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npm test 2>&1 | tail -20 ``` Expected: All existing tests pass; new flowchart tests pass. - [ ] **Step 6: Run lint + format** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npm run lint 2>&1 | tail -20 && npm run format 2>&1 | tail -10 && npm run format:check 2>&1 | tail -10 ``` Expected: lint clean; format applies then clean. - [ ] **Step 7: Commit** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && git add src/index.html src/styles-sidebar.css src/renderer.js src/main.js && git commit -m "feat(sidebar): register flowchart panel + rail button + thin fs IPC" ``` ### Task 7: README updates **Files:** - Modify: `README.md:54-70` (Advanced Features list) - Modify: `README.md:120-128` (Keyboard Shortcuts table) - [ ] **Step 1: Add the Advanced Features row** In `README.md`, in the bullet list starting around line 54 ("- **Page size configuration** ..."), add a new line: ```markdown - **Visual flow chart editor** - Build Mermaid flowcharts visually; drag nodes, connect edges, live preview. Insert at cursor. ``` - [ ] **Step 2: Add the Keyboard Shortcut rows** In `README.md`, in the keyboard-shortcuts table around line 120, add rows: ```markdown | Add Flow Chart Node | Insert (when panel focused) | | Flow Chart: Undo | Ctrl+Z | | Flow Chart: Redo | Ctrl+Shift+Z | | Flow Chart: Delete selected | Delete | ``` - [ ] **Step 3: Commit** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && git add README.md && git commit -m "docs(readme): visual flow chart editor feature + panel-scoped shortcuts" ``` ### Task 8: Final verification pass **Files:** none — verification only - [ ] **Step 1: Run the full test suite** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npm test 2>&1 | tail -30 ``` Expected: All tests pass — existing + 30+ new flowchart tests across the 5 new files. - [ ] **Step 2: Run lint** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npm run lint 2>&1 | tail -10 ``` Expected: clean (no errors). - [ ] **Step 3: Run format check** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npm run format:check 2>&1 | tail -10 ``` Expected: clean. - [ ] **Step 4: Self-review for forbidden markers** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && grep -nE "TODO|FIXME|XXX|HACK|not implemented|placeholder|stub|for now|in a real app|mock data|hardcoded for demo|coming soon" src/flowchart/ src/sidebar/flowchart-panel.js tests/flowchart-*.test.js 2>&1 | head -20 ``` Expected: empty output. - [ ] **Step 5: Verify the Linux build still succeeds** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && npm run build:linux 2>&1 | tail -20 ``` Expected: `dist/MarkdownConverter-*.AppImage` (and .deb) produced. - [ ] **Step 6: Final commit if any auto-formatting drifted** ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && git status && git diff --stat ``` If changes exist from `npm run format`, commit them: ```bash cd /mnt/source/apps/parallel-git-branch-dev/markdown-converter__master && git add -A && git commit -m "chore: prettier pass on flowchart editor files" ``` ## Definition of Done - [ ] All 8 tasks completed with their own commit - [ ] 30+ new tests across 5 files (store, mermaid, shapes, canvas, panel) - [ ] `npm test`, `npm run lint`, `npm run format:check` all clean - [ ] `npm run build:linux` succeeds - [ ] No forbidden markers (`TODO`/`FIXME`/`HACK`/`placeholder`/`stub`/etc.) in changed files - [ ] Panel opens from the new sidebar rail button - [ ] Add/drag/edit-label/connect/delete all wired through the canvas to the store - [ ] Right preview pane shows live Mermaid source + rendered SVG - [ ] Insert-at-Cursor wraps in ```` ```mermaid ```` and reuses `tabManager.insertAtCursor` - [ ] Undo/redo (Ctrl+Z / Ctrl+Shift+Z) panel-scoped - [ ] Persistence round-trip survives app restart - [ ] README mentions the feature + the panel-scoped shortcuts