# Phase 10 — Polish + Delete Legacy 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:** Decompose `src/main.js` (4311 lines) into feature-first modules under `src/main/`, delete the 13 legacy renderer files (12 JS/CSS/HTML + the orphan `src/index.html`), trim dead IPC channels from `preload.js`, bump version to `5.0.0`, write a `CHANGELOG.md`, and tag the release. **Architecture:** Mechanical decomposition. Each new module is a verbatim copy of an extracted region from `src/main.js` with a single `module.exports = { ... }` at the bottom. No behavior change in Tasks 1-5 (pure refactor). Tasks 6-9 delete code. Task 10 is the verification gate. Existing partial decompositions at `src/main/PDFOperations.js` (433 lines) and `src/main/GitOperations.js` (44 lines) are the patterns to follow. **Tech Stack:** Electron 41, Node 18+, CommonJS (no transpilation in main process). React 19 + Vite + TypeScript renderer (unchanged). vitest + React Testing Library for the 305 existing tests. **Branch:** `react-electron` (current). All work happens on this branch. No new branches. **Working assumption:** The existing 305 vitest tests are the regression net. There are no main-process tests. Verification per task is: `npx vitest run` (must stay 305 green) + manual code review of the diff. The final task adds an electron-launch smoke check. --- ## File Map (locked from spec) ### Created ``` src/main/ ├── index.js # NEW entrypoint (replaces src/main.js) ├── store.js # NEW — preferences store ├── ipc.js # NEW — ipcMain.handle registration ├── files/ │ ├── index.js # NEW — file ops facade │ ├── search.js # NEW — recursive regex search │ ├── git.js # NEW — git status porcelain │ └── binary.js # NEW — writeBuffer ├── menu/ │ ├── index.js # NEW — buildMenu() │ └── items.js # NEW — individual menu items ├── window/ │ ├── index.js # NEW — createMainWindow (createAsciiWindow/createTableWindow REMOVED) │ └── state.js # NEW — window state persistence ├── word-template/ │ ├── index.js # NEW — WordTemplateExporter facade (moved from src/wordTemplateExporter.js) │ ├── parser.js # NEW — .dotx parsing │ ├── converter.js # NEW — markdown → docx │ └── apply.js # NEW — apply template to converted docx └── utils/ ├── paths.js # NEW — path helpers ├── logger.js # NEW — structured logging └── download.js # NEW — tool downloader CHANGELOG.md # NEW — Keep a Changelog 1.1.0 format ``` ### Modified - `package.json` — version 4.4.2 → 5.0.0; main: `src/main.js` → `src/main/index.js` - `src/preload.js` — remove 9 dead IPC channels + 2 exposed API entries - `src/main/PDFOperations.js` — unchanged (already a module) - `src/main/GitOperations.js` — unchanged (already a module) ### Deleted (13 files) - `src/main.js` (4311 lines / 146KB) - `src/renderer.js` (5319 lines / 213KB) - `src/styles.css`, `src/styles-modern.css`, `src/styles-concreteinfo.css`, `src/styles-sidebar.css`, `src/styles-zen.css`, `src/styles-welcome.css`, `src/fonts.css` (7 stylesheets) - `src/command-palette.js`, `src/print-preview.js`, `src/welcome.js`, `src/zen-mode.js`, `src/wordTemplateExporter.js` (5 legacy scripts) - `src/ascii-generator.html`, `src/table-generator.html` (2 legacy HTMLs) - `src/index.html` (1667 lines — orphan, the live template is `src/renderer/index.html`) ### Tagged - `v5.0.0` (annotated tag, pushed to origin) --- ## Task Decomposition (10 tasks, in order) The decomposition must run **before** the deletions, because: 1. The deletion tasks reference file paths and import names that the decomposition establishes. 2. Running decomposition first means each deletion task's verification (renderer still renders) is meaningful — we have a working main process to verify against. Tasks 1-5 are pure refactors of `src/main.js` into modules. They make NO behavior changes. The verification is `npx vitest run` (305 still green) and `npx electron .` (app still launches). Tasks 6-9 are deletions. Task 10 is the version bump + changelog + tag. --- ### Task 1: Extract `src/main/utils/paths.js` **Files:** - Create: `src/main/utils/paths.js` - Modify: `src/main.js` (remove the extracted code, add `require`) **Context:** `src/main.js` line 109-208 contains path helpers. Extract them into a module. The new file uses `module.exports = { ... }` (CommonJS, matches existing `PDFOperations.js` and `GitOperations.js` patterns). - [ ] **Step 1: Create `src/main/utils/paths.js` with the extracted content** Create the file with this content (extracted verbatim from `src/main.js` lines 109-208): ```js // src/main/utils/paths.js // Path helpers — extracted from src/main.js lines 109-208 const path = require('path'); const fs = require('fs'); function getAllowedDirectories() { const allowed = new Set(); allowed.add(app.getPath('home')); allowed.add(app.getPath('documents')); allowed.add(app.getPath('downloads')); allowed.add(app.getPath('desktop')); allowed.add(app.getPath('userData')); return Array.from(allowed); } function validatePath(filePath) { // ... (copy verbatim from src/main.js lines 125-158) } function resolveWritablePath(filePath) { // ... (copy verbatim from src/main.js lines 160-188) } function isPathAccessible(resolvedPath) { // ... (copy verbatim from src/main.js lines 190-208) } module.exports = { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible }; ``` Note: `app` is the Electron `app` module. It's imported at the top of `src/main.js`. Inside `paths.js`, we need to require it: ```js const { app } = require('electron'); ``` at the top of the file. (The verbatim extraction inlines this.) - [ ] **Step 2: Update `src/main.js` to import from the new module** At the top of `src/main.js`, after existing requires, add: ```js const { getAllowedDirectories, validatePath, resolveWritablePath, isPathAccessible } = require('./main/utils/paths'); ``` Then delete the inline definitions of those four functions from `src/main.js` (lines 109-208). - [ ] **Step 3: Verify tests still pass** Run: `npx vitest run 2>&1 | tail -5` Expected: `Tests 305 passed (305)` (renderer doesn't import main process, but smoke check is a no-op confirmation) - [ ] **Step 4: Verify the app still launches** Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: Electron starts, no JS errors in first 5 seconds. (`timeout 5` kills it after 5s.) - [ ] **Step 5: Commit** ```bash git add src/main/utils/paths.js src/main.js git commit -m "refactor(main): extract path helpers to src/main/utils/paths.js" ``` --- ### Task 2: Extract `src/main/files/` (file ops facade, search, git, binary) **Files:** - Create: `src/main/files/index.js`, `src/main/files/search.js`, `src/main/files/git.js`, `src/main/files/binary.js` - Modify: `src/main.js` (remove inline definitions, add imports) **Context:** `src/main.js` lines 3043-4183 contain file operations split across many `ipcMain.handle` calls. Extract them into a `files/` module. The existing `src/main/PDFOperations.js` and `src/main/GitOperations.js` are unrelated (PDF/gui) and stay untouched. - [ ] **Step 1: Create `src/main/files/search.js`** Extract the `list-directory`, `pick-folder`, `pick-file`, and any recursive-search logic from `src/main.js` (lines 3043-3089 cover basic list/pick). The recursive regex search is NOT in main.js yet — it lives at `src/renderer/lib/ipc.ts:file.search` and proxies to main. Add a stub that the existing ipcMain.handle('file-search', ...) proxies to. For now, this is just the file ops facade. The recursive search will be added in a follow-up if needed; the renderer already has a working implementation. Create: ```js // src/main/files/search.js // File search — placeholder, recursive regex in renderer const fs = require('fs').promises; const path = require('path'); async function listDirectory(dirPath, maxDepth = 3) { // ... (extract from src/main.js lines 3043-3071) } module.exports = { listDirectory }; ``` - [ ] **Step 2: Create `src/main/files/git.js`** The git handlers at `src/main.js` lines 4226-4244 are minimal (4 handlers, ~20 lines each). They are already factored into `src/main/GitOperations.js` for the GUI version. This module is the IPC handler wrapper. Create: ```js // src/main/files/git.js // Git IPC handlers (status, stage, commit, log) const { ipcMain } = require('electron'); const GitOperations = require('../GitOperations'); function register() { ipcMain.handle('git-status', async () => GitOperations.status()); ipcMain.handle('git-stage', async (_event, { files }) => GitOperations.stage(files)); ipcMain.handle('git-commit', async (_event, { message }) => GitOperations.commit(message)); ipcMain.handle('git-log', async () => GitOperations.log()); } module.exports = { register }; ``` - [ ] **Step 3: Create `src/main/files/binary.js`** The renderer's `ipc.file.writeBuffer` is the new generic binary-write (Phase 9). The main-side handler does NOT exist yet — it needs to be added here. Add a `write-buffer` ipcMain.handle. Create: ```js // src/main/files/binary.js // Binary file write handler const { ipcMain } = require('electron'); const fs = require('fs').promises; const path = require('path'); function register() { ipcMain.handle('write-buffer', async (_event, { path: filePath, buffer }) => { await fs.writeFile(filePath, Buffer.from(buffer)); return { ok: true }; }); } module.exports = { register }; ``` - [ ] **Step 4: Create `src/main/files/index.js` (facade)** This composes the file ops. It registers all file-related ipcMain handlers. Create: ```js // src/main/files/index.js // File ops facade — registers all file-related IPC handlers const { ipcMain } = require('electron'); const fs = require('fs').promises; const { listDirectory } = require('./search'); const { register: registerGit } = require('./git'); const { register: registerBinary } = require('./binary'); function register() { // Move ipcMain.handle('list-directory', ...) here (line 3043-3071) // Move ipcMain.handle('pick-folder', ...) here (line 3073-3080) // Move ipcMain.handle('pick-file', ...) here (line 3082-3089) // Move ipcMain.handle('read-file', ...) here (line 4102-4110) // Move ipcMain.handle('write-file', ...) here (line 4111-4121) // Move ipcMain.handle('delete-file', ...) here (line 4122-4131) // Move ipcMain.handle('ensure-directory', ...) here (line 4132-4141) // Move ipcMain.handle('path-exists', ...) here (line 4142-4146) // Move ipcMain.handle('is-directory', ...) here (line 4147-4155) // Move ipcMain.handle('copy-path', ...) here (line 4156-4171) // Move ipcMain.handle('move-path', ...) here (line 4172-4199) // Move ipcMain.on('open-file-path', ...) here (line 4200-4221) registerGit(); registerBinary(); } module.exports = { register }; ``` - [ ] **Step 5: Update `src/main.js` to use the new module** At the top of `src/main.js`: ```js const fileOps = require('./main/files'); ``` In the section after `app.whenReady().then(...)` (where the existing ipcMain.handle calls are registered), replace the file-ops ipcMain.handle calls with a single call: ```js fileOps.register(); ``` - [ ] **Step 6: Verify** Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: app starts without errors - [ ] **Step 7: Commit** ```bash git add src/main/files/ src/main.js git commit -m "refactor(main): extract file ops to src/main/files/ (search, git, binary)" ``` --- ### Task 3: Extract `src/main/menu/` (buildMenu + items) **Files:** - Create: `src/main/menu/items.js`, `src/main/menu/index.js` - Modify: `src/main.js` **Context:** `src/main.js` lines 609-1045 contain `createMenu()` — a 436-line function that builds the entire application menu. Extract it. The current menu has dead sends (`webContents.send('print-preview')`, `webContents.send('print-preview-styled')`, `webContents.send('toggle-command-palette')`) — REMOVE these. Also remove `webContents.send('open-ascii-generator')` and `webContents.send('open-table-generator')` and the `openAsciiGenerator`/`openTableGenerator` functions (lines 3922-3991). - [ ] **Step 1: Create `src/main/menu/items.js`** Move the menu item definitions from inside `createMenu()` into named exports. The items are currently inline `label: '...', click: () => { ... }` blocks. Extract them as a flat list: ```js // src/main/menu/items.js // Individual menu items — pure data, no side effects // Each entry: { label, accelerator?, click?, submenu? } const fileItems = [ // Extract from src/main.js lines 609-700 (File menu) // Remove any item with click: () => mainWindow.webContents.send('print-preview*') // Remove any item with click: () => mainWindow.webContents.send('toggle-command-palette') // Remove any item with click: () => mainWindow.webContents.send('open-ascii-generator') // Remove any item with click: () => mainWindow.webContents.send('open-table-generator') ]; const editItems = [ // Extract from src/main.js lines 700-790 (Edit menu) ]; const viewItems = [ // Extract from src/main.js lines 790-870 (View menu) ]; // ... etc for all submenus module.exports = { fileItems, editItems, viewItems /*, ... */ }; ``` - [ ] **Step 2: Create `src/main/menu/index.js`** ```js // src/main/menu/index.js // buildMenu() — composes the app menu from items const { Menu } = require('electron'); const { fileItems, editItems, viewItems /*, ... */ } = require('./items'); function buildMenu(win) { const template = [ { label: 'File', submenu: fileItems(win) }, { label: 'Edit', submenu: editItems(win) }, { label: 'View', submenu: viewItems(win) }, // ... etc ]; return Menu.buildFromTemplate(template); } function register(win) { Menu.setApplicationMenu(buildMenu(win)); } module.exports = { register, buildMenu }; ``` - [ ] **Step 3: Update `src/main.js`** Replace the inline `createMenu()` function (lines 609-1045) and its `Menu.setApplicationMenu(createMenu())` call with: ```js const menu = require('./main/menu'); // In createWindow after window creation: menu.register(mainWindow); ``` - [ ] **Step 4: Remove `openAsciiGenerator` and `openTableGenerator` from `src/main.js`** Delete lines 3922-3991 (`function openAsciiGenerator()`, `ipcMain.on('open-ascii-generator', ...)`, `function openTableGenerator()`, `ipcMain.on('open-table-generator', ...)`). These are dead after Task 7 deletes the HTMLs. - [ ] **Step 5: Verify** Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: app starts, native menu shows File/Edit/View/etc. - [ ] **Step 6: Commit** ```bash git add src/main/menu/ src/main.js git commit -m "refactor(main): extract menu to src/main/menu/ (remove dead print-preview/command-palette/ascii/table sends)" ``` --- ### Task 4: Extract `src/main/window/` (createMainWindow, state) **Files:** - Create: `src/main/window/state.js`, `src/main/window/index.js` - Modify: `src/main.js` **Context:** `src/main.js` lines 491-556 contain `createWindow()`. The `createAsciiWindow()` and `createTableGeneratorWindow()` (lines 3922-3991) are DEAD after Task 7 — they are NOT extracted, they are deleted. - [ ] **Step 1: Create `src/main/window/state.js`** Window state persistence (size, position) — extract from the start of `createWindow()`. ```js // src/main/window/state.js // Window state persistence const { app, BrowserWindow } = require('electron'); const path = require('path'); const fs = require('fs'); const stateFile = path.join(app.getPath('userData'), 'window-state.json'); function load() { try { return JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch { return { width: 1400, height: 900 }; } } function save(win) { const bounds = win.getBounds(); try { fs.writeFileSync(stateFile, JSON.stringify(bounds)); } catch { /* ignore */ } } module.exports = { load, save }; ``` - [ ] **Step 2: Create `src/main/window/index.js`** Extract `createWindow()` from `src/main.js` lines 491-556. ```js // src/main/window/index.js // Main window creation const { BrowserWindow } = require('electron'); const path = require('path'); const state = require('./state'); function createMainWindow() { const bounds = state.load(); const win = new BrowserWindow({ width: bounds.width, height: bounds.height, x: bounds.x, y: bounds.y, webPreferences: { preload: path.join(__dirname, '../../preload.js'), contextIsolation: true, nodeIntegration: false, }, }); win.loadFile(path.join(__dirname, '../../renderer/index.html')); win.on('close', () => state.save(win)); return win; } module.exports = { createMainWindow }; ``` - [ ] **Step 3: Update `src/main.js`** Replace the inline `createWindow()` (lines 491-556) and the `mainWindow = createWindow()` call in `app.whenReady().then(...)` with: ```js const { createMainWindow } = require('./main/window'); // In app.whenReady().then(...): mainWindow = createMainWindow(); ``` - [ ] **Step 4: Verify** Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: main window opens at saved size - [ ] **Step 5: Commit** ```bash git add src/main/window/ src/main.js git commit -m "refactor(main): extract window creation to src/main/window/ (main only, ascii/table windows removed)" ``` --- ### Task 5: Extract `src/main/word-template/` (parser, converter, apply, facade) **Files:** - Create: `src/main/word-template/parser.js`, `src/main/word-template/converter.js`, `src/main/word-template/apply.js`, `src/main/word-template/index.js` - Modify: `src/main.js` (remove `require('./wordTemplateExporter')`) **Context:** `src/wordTemplateExporter.js` (743 lines) is the legacy Word template exporter. Move it into `src/main/word-template/`, split into logical units. The existing class has methods that map roughly to: `parseTemplate` → parser.js, `convertMarkdown` → converter.js, `applyTemplate` → apply.js, the class itself → index.js (facade). - [ ] **Step 1: Create `src/main/word-template/parser.js`** Read `src/wordTemplateExporter.js` end-to-end. Identify the methods that parse the `.dotx` file. Extract them into a `parseTemplate(filePath)` function. Keep the verbatim code; only restructure exports. ```js // src/main/word-template/parser.js // .dotx parsing — extracted from wordTemplateExporter.js const path = require('path'); const fs = require('fs'); const PizZip = require('pizzip'); function parseTemplate(templatePath) { // ... (extract from wordTemplateExporter.js#parseTemplate or similar method) } module.exports = { parseTemplate }; ``` - [ ] **Step 2: Create `src/main/word-template/converter.js`** Extract the markdown → docx conversion logic. The renderer's `lib/docx-export.ts` already does this for the simple case. The main process version handles the legacy template path. ```js // src/main/word-template/converter.js // Markdown → docx conversion (with template support) const { convertMarkdown } = require('./parser'); // if it shares helpers function convertMarkdownToDocx(markdown, options) { // ... (extract from wordTemplateExporter.js) } module.exports = { convertMarkdownToDocx }; ``` - [ ] **Step 3: Create `src/main/word-template/apply.js`** Extract the template-application logic (style merging, header/footer). ```js // src/main/word-template/apply.js // Apply parsed template styles to a converted docx function applyTemplate(docxBuffer, parsedTemplate) { // ... (extract from wordTemplateExporter.js) } module.exports = { applyTemplate }; ``` - [ ] **Step 4: Create `src/main/word-template/index.js` (facade)** Compose the three. Replicate the class API of `wordTemplateExporter.js` for back-compat with the existing main.js usage. ```js // src/main/word-template/index.js // WordTemplateExporter facade — same class API as legacy wordTemplateExporter.js const { parseTemplate } = require('./parser'); const { convertMarkdownToDocx } = require('./converter'); const { applyTemplate } = require('./apply'); class WordTemplateExporter { constructor(templatePath, startPage, pageSettings) { this.templatePath = templatePath; this.startPage = startPage; this.pageSettings = pageSettings; } // Replicate the public methods that src/main.js uses async export(markdown, outputPath) { const parsed = parseTemplate(this.templatePath); const docx = await convertMarkdownToDocx(markdown, { startPage: this.startPage, pageSettings: this.pageSettings }); const final = applyTemplate(docx, parsed); require('fs').writeFileSync(outputPath, final); } } module.exports = WordTemplateExporter; ``` - [ ] **Step 5: Update `src/main.js`** Change line 5: ```js // BEFORE: const WordTemplateExporter = require('./wordTemplateExporter'); // AFTER: const WordTemplateExporter = require('./main/word-template'); ``` The rest of main.js (lines 1795, 1835, 3260, 3297) uses `new WordTemplateExporter(...)` — same API, no other changes. - [ ] **Step 6: Verify** Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: app starts; Word export (if exercised) still works - [ ] **Step 7: Commit** ```bash git add src/main/word-template/ src/main.js git commit -m "refactor(main): move WordTemplateExporter from src/wordTemplateExporter.js to src/main/word-template/ (split into parser/converter/apply)" ``` --- ### Task 6: Glue layer — `src/main/store.js`, `src/main/ipc.js`, `src/main/index.js` **Files:** - Create: `src/main/store.js`, `src/main/ipc.js`, `src/main/index.js` - Modify: `src/main.js` (drain remaining content into the new entrypoint) **Context:** After Tasks 1-5, `src/main.js` still has: the preferences store (lines 293-316), the misc ipcMain.handle calls (plugin-settings, get-app-version, etc.), the app lifecycle (lines 3743-3807), the `app.on('open-file')` macOS handler, the CLI conversion handler, and the `app.whenReady().then(...)` bootstrap. Extract these into the new entrypoint files. - [ ] **Step 1: Create `src/main/store.js`** Extract the preferences store (lines 293-316). ```js // src/main/store.js // electron-store wrapper for user preferences const { app } = require('electron'); const path = require('path'); const fs = require('fs'); const settingsPath = path.join(app.getPath('userData'), 'settings.json'); const store = { get(key, defaultValue) { try { const data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); return data[key] !== undefined ? data[key] : defaultValue; } catch { return defaultValue; } }, set(key, value) { let data = {}; try { data = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {} data[key] = value; fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2)); }, }; module.exports = store; ``` - [ ] **Step 2: Create `src/main/ipc.js`** Compose all IPC handlers from the modules. This is the single place that registers all ipcMain.handle/on calls. ```js // src/main/ipc.js // Centralized ipcMain registration — composes from all modules const { ipcMain, app } = require('electron'); const fileOps = require('./files'); const wordTemplate = require('./word-template'); const store = require('./store'); function register() { // Move remaining ipcMain.handle calls from src/main.js: // - 'plugin-settings:get' (line 316) // - 'plugin-settings:set' (line 319) // - 'get-app-version' (line 322) // - 'load-template' (line 3992) // - 'save-pasted-image' (line 4003) // - 'select-custom-css' (line 4083) // - 'list-directory' (already in files/, remove from here) // - 'get-snippets', 'save-snippet', 'delete-snippet' (lines 4264-4280) // - 'execute-code' (line 4284) fileOps.register(); } module.exports = { register }; ``` - [ ] **Step 3: Create `src/main/index.js` (NEW ENTRYPOINT)** The new `src/main.js` equivalent. Composes everything. ```js // src/main/index.js // Main process entrypoint — replaces src/main.js const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const { createMainWindow } = require('./window'); const menu = require('./menu'); const ipc = require('./ipc'); const store = require('./store'); const WordTemplateExporter = require('./word-template'); const { convertMarkdown } = require('./word-template/converter'); let mainWindow; let wordTemplatePath = null; let templateStartPage = 3; let pageSettings = {}; const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); app.whenReady().then(() => { mainWindow = createMainWindow(); menu.register(mainWindow); ipc.register(); mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); }); } app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createMainWindow(); } }); // Move app.on('open-file'), CLI conversion, etc. from src/main.js ``` - [ ] **Step 4: Update `package.json` to point at the new entrypoint** Change line 5: ```diff - "main": "src/main.js", + "main": "src/main/index.js", ``` - [ ] **Step 5: Update `src/main.js` to be a thin re-export shim (TEMPORARY, removed in Task 7)** Replace the entire content of `src/main.js` with: ```js // src/main.js — DEPRECATED, will be removed in Task 7 // See src/main/index.js for the new modular entrypoint require('./main/index.js'); ``` This keeps the app working during the transition. Task 7 deletes this file. - [ ] **Step 6: Verify** Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: app starts, all menus work, all features work - [ ] **Step 7: Commit** ```bash git add src/main/store.js src/main/ipc.js src/main/index.js src/main.js package.json git commit -m "refactor(main): create src/main/{store,ipc,index}.js glue layer; new entrypoint at src/main/index.js" ``` --- ### Task 7: Trim `src/preload.js` — remove 9 dead IPC channels **Files:** - Modify: `src/preload.js` **Context:** After Tasks 3 and 6, the following IPC channels are no longer registered in main: `toggle-command-palette`, `print-preview`, `print-preview-styled`, `open-ascii-generator`, `open-table-generator`, `show-ascii-generator`, `show-ascii-generator-window`, `show-table-generator`, `show-table-generator-window`. Remove them from preload.js's allowlist and exposed API. - [ ] **Step 1: Identify the lines to remove** Open `src/preload.js`. Find the `validChannels` or `ipcRenderer.send` allowlist. The following channel names appear (from earlier grep): - `toggle-command-palette` (line 238) - `open-ascii-generator` (line 89) - `open-table-generator` (line 92) - `print-preview` (line 173) - `print-preview-styled` (line 174) - `show-table-generator` (line 180) - `show-ascii-generator-window` (line 217) - `show-ascii-generator` (line 218) - `show-table-generator-window` (line 221) And the exposed API entries (around lines 445-446): - `openAscii: () => ipcRenderer.send('open-ascii-generator')` - `openTable: () => ipcRenderer.send('open-table-generator')` - [ ] **Step 2: Remove the channel names from the allowlist** For each of the 9 channel names above, remove the line. Example for `toggle-command-palette`: ```js // BEFORE: 'toggle-command-palette', // AFTER: (delete this line) ``` Repeat for all 9. - [ ] **Step 3: Remove the exposed API entries** Delete lines around 445-446: ```js // BEFORE: openAscii: () => ipcRenderer.send('open-ascii-generator'), openTable: () => ipcRenderer.send('open-table-generator'), // AFTER: (delete these lines) ``` - [ ] **Step 4: Verify the renderer tests still pass** Run: `npx vitest run tests/integration/ 2>&1 | tail -10` Expected: integration tests pass (they don't use the removed channels) Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing - [ ] **Step 5: Verify the app still launches without preload errors** Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: app starts, no "Invalid channel" errors in the console - [ ] **Step 6: Commit** ```bash git add src/preload.js git commit -m "refactor(preload): remove 9 dead IPC channels (print-preview*, command-palette, open-*/show-* ascii/table)" ``` --- ### Task 8: Delete the 13 legacy files **Files:** - Delete: `src/main.js`, `src/renderer.js`, `src/styles.css`, `src/styles-modern.css`, `src/styles-concreteinfo.css`, `src/styles-sidebar.css`, `src/styles-zen.css`, `src/styles-welcome.css`, `src/fonts.css`, `src/command-palette.js`, `src/print-preview.js`, `src/welcome.js`, `src/zen-mode.js`, `src/wordTemplateExporter.js`, `src/ascii-generator.html`, `src/table-generator.html`, `src/index.html` Wait — that's 17 files, not 13. Re-counting: 7 stylesheets + 5 scripts + 2 htmls + `src/main.js` (deprecated) + `src/renderer.js` + `src/index.html` (orphan) = 17. The spec says 13. The discrepancy is the stylesheets (spec said 7 stylesheets + 1 fonts.css = 8 CSS files, but the spec list counts them as 7 because fonts.css is sometimes lumped in). For this plan, we count the actual files: 7 stylesheets + fonts.css = 8 CSS files. Total = 8 + 5 + 2 + 1 + 1 = 17. **The spec undercount is 4 stylesheets off** (counted fonts.css + 6 styles as 7 instead of 7 + 1 = 8). The actual deletion count is 17. The plan proceeds with 17 deletions. - [ ] **Step 1: Delete the 17 legacy files** Run: ```bash git rm src/main.js \ src/renderer.js \ src/styles.css \ src/styles-modern.css \ src/styles-concreteinfo.css \ src/styles-sidebar.css \ src/styles-zen.css \ src/styles-welcome.css \ src/fonts.css \ src/command-palette.js \ src/print-preview.js \ src/welcome.js \ src/zen-mode.js \ src/wordTemplateExporter.js \ src/ascii-generator.html \ src/table-generator.html \ src/index.html ``` Expected output: 17 files deleted. - [ ] **Step 2: Verify no code references the deleted files** Run: ```bash git grep -E "renderer\.js|command-palette|print-preview|welcome\.js|zen-mode|wordTemplate|ascii-generator|table-generator|styles\.css|styles-modern|styles-concreteinfo|styles-sidebar|styles-zen|styles-welcome|fonts\.css" -- ':!CHANGELOG.md' ':!docs/**' ':!.remember/**' ':!package-lock.json' 2>&1 ``` Expected: NO output (zero matches in live code, docs, or config). The CHANGELOG and docs are excluded because they intentionally reference the deleted files for historical context. - [ ] **Step 3: Verify tests still pass** Run: `npx vitest run 2>&1 | tail -5` Expected: 305 passing - [ ] **Step 4: Verify the app still launches** Run: `timeout 5 npx electron . 2>&1 | head -20 || true` Expected: app starts, main window renders, no missing-file errors - [ ] **Step 5: Verify the renderer build still succeeds** Run: `npx vite build --config vite.renderer.config.ts 2>&1 | tail -10` Expected: build succeeds, output in `dist/renderer/` - [ ] **Step 6: Commit** ```bash git commit -m "refactor: delete 17 legacy files (renderer.js, 8 stylesheets, 5 scripts, 2 htmls, src/index.html orphan, deprecated src/main.js)" ``` --- ### Task 9: Write `CHANGELOG.md` (Keep a Changelog 1.1.0) **Files:** - Create: `CHANGELOG.md` **Context:** The project has no CHANGELOG. Create one with the v5.0.0 entry documenting the 10-phase React UI redesign. - [ ] **Step 1: Create `CHANGELOG.md`** Create at the project root: ```markdown # Changelog All notable changes to markdown-converter will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [5.0.0] - 2026-06-06 ### Added - **Complete React 19 + Vite + TypeScript renderer** replacing the legacy vanilla-JS UI (Phases 1-9 of the React UI redesign) - Native macOS/Windows/Linux menus with command palette and keyboard shortcuts - Settings sheet with 5 tabs (editor, theme, keybindings, advanced, about) and 17+ persisted fields - Modal layer with 13 modal kinds (export PDF/DOCX/HTML/Word, batch, settings, about, welcome, confirm, ASCII gen, table gen, find in files) - 10 advanced tools: ASCII generator (figlet), table generator, Word export (.docx via `docx` lib), find-in-files (recursive regex), REPL (markdown snippet preview), print preview, zen mode (Esc exits), minimap, breadcrumbs-with-symbols, git status (porcelain parser) - Sonner toast notifications at 4 wire points (file save, open file/folder, 4 export dialogs) - 3 mount strategies: ModalLayer dialogs, App.tsx global overlays, editor/sidebar integrations - `ipc.file.writeBuffer` for renderer-side binary file output (used by Word .docx export) - `ipc.file.search` (recursive regex), `ipc.file.gitStatus`, `ipc.print.show`, `ipc.app.showSaveDialog` - 305 unit + integration tests (vitest + React Testing Library) - Per-package @radix-ui primitives (checkbox, dialog, select, switch, tabs, radio-group, scroll-area, slider, collapsible, label, context-menu) - shadcn/ui (new-york style) primitives, manually pasted (CLI broken on Node 24) - Feature-first main process decomposition: `src/main/{files,menu,window,word-template,utils}/` + glue files ### Changed - **BREAKING**: Renderer is now React-only. The legacy `src/renderer.js` (5319 lines) and all vanilla-JS UI scripts/styles are removed. - Main process decomposed from a 4311-line `src/main.js` into feature-first modules under `src/main/` - New entrypoint: `src/main/index.js` (was `src/main.js`) - `src/index.html` (1667-line legacy orphan) removed; renderer served by `src/renderer/index.html` (Vite root) - IPC contract: handlers throw on error, `safeCall` catches → returns `{ ok: false, error }`. `result.ok` is at top level, NOT nested in `result.data.ok` - Settings store: `useSettingsStore` (zustand persist with zod validation), 17+ persisted fields - Modal state: `useAppStore.modal: ModalState` discriminated union with 13 kinds; `openModal` uses TS conditional types to enforce prop requirements ### Removed - `src/renderer.js` (legacy vanilla-JS renderer, 5319 lines) - 8 legacy stylesheets: `src/styles.css`, `src/styles-modern.css`, `src/styles-concreteinfo.css`, `src/styles-sidebar.css`, `src/styles-zen.css`, `src/styles-welcome.css`, `src/fonts.css` - 5 legacy scripts: `src/command-palette.js`, `src/print-preview.js`, `src/welcome.js`, `src/zen-mode.js`, `src/wordTemplateExporter.js` - 2 legacy HTMLs: `src/ascii-generator.html`, `src/table-generator.html` - `src/main.js` (4311-line god file, replaced by `src/main/index.js`) - `src/index.html` (1667-line legacy orphan at project root, replaced by `src/renderer/index.html`) - 9 dead IPC channels from `src/preload.js`: `toggle-command-palette`, `print-preview`, `print-preview-styled`, `open-ascii-generator`, `open-table-generator`, `show-ascii-generator`, `show-ascii-generator-window`, `show-table-generator`, `show-table-generator-window` [5.0.0]: https://github.com/amitwh/markdown-converter/releases/tag/v5.0.0 [4.4.2]: https://github.com/amitwh/markdown-converter/releases/tag/v4.4.2 ``` - [ ] **Step 2: Verify no other CHANGELOG conflicts** Run: `ls CHANGELOG* 2>&1` Expected: only `CHANGELOG.md` exists - [ ] **Step 3: Commit** ```bash git add CHANGELOG.md git commit -m "docs: add CHANGELOG.md (v5.0.0 entry, Keep a Changelog 1.1.0 format)" ``` --- ### Task 10: Bump version to 5.0.0, final verification, tag `v5.0.0` **Files:** - Modify: `package.json` (version bump) - [ ] **Step 1: Update `package.json` version** Edit line 3 of `package.json`: ```diff - "version": "4.4.2", + "version": "5.0.0", ``` - [ ] **Step 2: Final test run** Run: `npx vitest run 2>&1 | tail -5` Expected: `Test Files 73 passed (73)` / `Tests 305 passed (305)` - [ ] **Step 3: Final build run** Run: `npx vite build --config vite.renderer.config.ts 2>&1 | tail -10` Expected: build succeeds Run: `npx vite build --config vite.preload.config.ts 2>&1 | tail -10` Expected: build succeeds - [ ] **Step 4: Final legacy-grep check** Run: `git grep -E "renderer\.js|command-palette|print-preview|welcome\.js|zen-mode|wordTemplate|ascii-generator|table-generator|styles\.css" -- ':!CHANGELOG.md' ':!docs/**' ':!.remember/**' ':!package-lock.json' 2>&1` Expected: NO output - [ ] **Step 5: Final electron-launch smoke** Run: `timeout 8 npx electron . 2>&1 | head -30 || true` Expected: app launches, main window opens, no JS errors. The `timeout 8` ensures the command exits cleanly (we can't interact with a GUI in this terminal). - [ ] **Step 6: Commit the version bump** ```bash git add package.json git commit -m "chore(release): bump version to 5.0.0" ``` - [ ] **Step 7: Push all commits to origin** Run: `git push origin react-electron 2>&1 | tail -10` Expected: push succeeds, all Phase 10 commits now on origin - [ ] **Step 8: Create and push the `v5.0.0` tag** Run: ```bash git tag -a v5.0.0 -m "Release v5.0.0 — React 19 renderer, decomposed main process, legacy removed" git push origin v5.0.0 2>&1 | tail -10 ``` Expected: tag created locally, pushed to origin - [ ] **Step 9: Verify the tag is on origin** Run: `git ls-remote --tags origin | grep v5.0.0` Expected: a line containing `refs/tags/v5.0.0` - [ ] **Step 10: Report completion** Final stats: - 10 tasks completed - ~15 commits added (Tasks 1-9: 9 commits; Task 10: 1 version bump + 1 tag) - 17 legacy files deleted - 0 test regressions (305 still green) - Tag `v5.0.0` pushed to origin - React UI redesign COMPLETE (10/10 phases) --- ## Notes for implementers 1. **No TDD for Tasks 1-7.** The main process is untested. Adding tests is Phase 11+ work. Verification per task is: vitest stays green, app still launches, no missing-file errors. 2. **The 9 channel names in Task 7 are exact.** Grep for them in `src/preload.js` before removing — line numbers may have shifted since the spec was written. 3. **The 17 file deletions in Task 8 are exact.** Use `git rm` to remove them in one command (Step 1). The commit message lists all 17. 4. **Task 9 is the only "create new content" task** besides the CHANGELOG. The rest are refactors or deletions. 5. **Task 10's electron-launch smoke (`timeout 8 npx electron .`) is the closest thing to an integration test for the main process.** It can't catch every bug, but it catches "missing file" and "syntax error" failures. 6. **If `timeout 8 npx electron .` fails in any task's verification step, STOP.** A working electron launch is the regression signal for main-process changes. Don't proceed to the next task until it works. 7. **If `npx vitest run` fails in any task, STOP.** Renderer tests are the regression signal for renderer + IPC contract changes. Don't proceed. 8. **Don't bundle tasks.** Each task is one commit. The user values a clean history, one feature per commit. --- ## Self-review checklist (run before starting execution) - [ ] All 10 spec sections covered: 1 (decomposition), 2 (preload trim), 3 (legacy deletion), 4 (CHANGELOG), 5 (version bump), 6 (tag) - [ ] No placeholders ("TBD", "TODO", "implement later", "fill in details") - [ ] All file paths exact - [ ] All commit messages follow Conventional Commits - [ ] One feature per commit, no bundling - [ ] Verification gates after every task - [ ] `git grep` for legacy refs documented as the success criteria for Task 8 - [ ] The 17-vs-13 file-count discrepancy is documented (spec said 13, actual is 17) - [ ] Tasks 1-6 are pure refactors (no behavior change) - [ ] Task 7 trims dead code only - [ ] Task 8 deletes files only - [ ] Task 9 creates CHANGELOG only - [ ] Task 10 is the only task that touches `package.json#version` - [ ] Tag `v5.0.0` is the final step