Files
markdown-converter/docs/superpowers/plans/2026-06-06-phase-10-polish.md

40 KiB

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.jssrc/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):

// 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:

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:

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
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:

// 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:

// 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:

// 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:

// 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:

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:

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
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:

// 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
// 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:

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
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().

// 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.

// 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:

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
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.

// 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.

// 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).

// 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.

// 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:

// 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
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).

// 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.

// 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.

// 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:

-  "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:

// 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
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:

// BEFORE:
'toggle-command-palette',
// AFTER: (delete this line)

Repeat for all 9.

  • Step 3: Remove the exposed API entries

Delete lines around 445-446:

// 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
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:

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:

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
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:

# 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
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:

-  "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
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:

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