Skip to content

Latest commit

 

History

History
791 lines (605 loc) · 23.5 KB

.cursor-tasks.md

File metadata and controls

791 lines (605 loc) · 23.5 KB

Implementation Checklist

Story 1: Project Setup and Basic Electron App

Goal:

Initialize the project, set up Electron, and confirm that a basic window can be displayed.

  • 1.2 Initialize Node.js Project

    • In a terminal, navigate to the project directory.
    • Run npm init -y to create a package.json file with default settings.
  • 1.3 Install Electron

    • Run npm install electron --save-dev (or --save) to add Electron as a dev dependency.
  • 1.4 Create Main Process File

    • Create a new file named main.js in the project root directory.
  • 1.5 Add Basic Electron Boilerplate to main.js

    • Import app and BrowserWindow from electron:

      const { app, BrowserWindow } = require("electron");
    • Create a function createWindow() that instantiates a BrowserWindow:

      function createWindow() {
        const mainWindow = new BrowserWindow({
          width: 800,
          height: 600,
          webPreferences: {
            nodeIntegration: true, // or false if using a more secure setup
            contextIsolation: false, // depends on security preference
          },
        });
      
        mainWindow.loadFile("index.html");
      }
    • In app.whenReady(), call createWindow():

      app.whenReady().then(() => {
        createWindow();
      
        app.on("activate", () => {
          if (BrowserWindow.getAllWindows().length === 0) createWindow();
        });
      });
    • Handle window-all-closed to quit on platforms other than macOS:

      app.on("window-all-closed", () => {
        if (process.platform !== "darwin") {
          app.quit();
        }
      });
  • 1.6 Create Renderer Process File - index.html

    • In the project root, create index.html.
  • 1.7 Basic HTML Structure

    • Add the minimal tags to index.html:
      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="UTF-8" />
          <title>Repo String Clone</title>
        </head>
        <body>
          <h1>Repo String Clone</h1>
          <!-- Additional UI elements will be added later -->
        </body>
      </html>
  • 1.8 Load HTML in Main Process

    • Confirm main.js calls mainWindow.loadFile('index.html') inside createWindow(). (Already in step 1.5)
  • 1.9 Add Start Script in package.json

    • Open package.json.
    • Add a script named "start" with the command "electron .":
      "scripts": {
        "start": "electron ."
      }
  • 1.10 Test Basic App

    • Run npm start.
    • Ensure the Electron app launches with a blank (or basic) window titled "Repo String Clone".

Story 2: Folder Selection

Goal:

Allow the user to open a folder dialog, select a directory, and store/return the path.

  • 2.1 Add "Open Folder" Button (HTML)

    • In index.html, in the <body>, add:
      <button id="open-folder-button">Open Folder</button>
      <div id="selected-folder-display"></div>
  • 2.2 Create renderer.js (Renderer Script)

    • Create a file named renderer.js in the project root (or a src/ directory).
    • Link it in index.html by adding:
      <script src="./renderer.js"></script>
  • 2.3 Import IPC in Renderer

    • In renderer.js, add:
      const { ipcRenderer } = require("electron");
  • 2.4 Button Click Handler

    • Still in renderer.js, get a reference to the button and attach an event listener:
      const openFolderButton = document.getElementById("open-folder-button");
      openFolderButton.addEventListener("click", () => {
        ipcRenderer.send("open-folder");
      });
  • 2.5 Handle IPC in Main Process

    • In main.js, import ipcMain and dialog:

      const { ipcMain, dialog } = require("electron");
    • Listen for the 'open-folder' message:

      ipcMain.on("open-folder", async (event) => {
        const result = await dialog.showOpenDialog({
          properties: ["openDirectory"],
        });
      
        if (!result.canceled && result.filePaths && result.filePaths.length > 0) {
          const selectedPath = result.filePaths[0];
          event.sender.send("folder-selected", selectedPath);
        }
      });
  • 2.6 Receive Path in Renderer

    • In renderer.js, listen for 'folder-selected':

      ipcRenderer.on("folder-selected", (event, selectedPath) => {
        // Store or display the selected path
        const selectedFolderDisplay = document.getElementById(
          "selected-folder-display",
        );
        selectedFolderDisplay.textContent = `Selected Folder: ${selectedPath}`;
      
        // Additional logic to request file listing, etc., can go here
      });
  • 2.7 Verify Folder Selection Flow

    • Launch the app (npm start).
    • Click Open Folder, choose a directory, and ensure the path appears in selected-folder-display.

Story 3: File Listing (Basic)

Goal:

Display a list of files (non-recursive) from the selected folder.

Note: We will soon switch to a recursive listing in Story 4. For now, a single-level read is fine.

  • 3.1 Create File List Container (HTML)

    • In index.html, add a container for file entries:
      <ul id="file-list"></ul>
  • 3.2 Extend IPC Flow for File List Request

    • In renderer.js, when folder is selected ('folder-selected' event), also send a message to the main process requesting file data:

      ipcRenderer.on("folder-selected", (event, selectedPath) => {
        // Display selected path
        document.getElementById(
          "selected-folder-display",
        ).textContent = `Selected Folder: ${selectedPath}`;
      
        // Request file list data
        ipcRenderer.send("request-file-list", selectedPath);
      });
  • 3.3 In Main Process, Listen for request-file-list

    • In main.js, add:

      const fs = require("fs");
      const path = require("path");
      
      ipcMain.on("request-file-list", (event, folderPath) => {
        try {
          const dirents = fs.readdirSync(folderPath, { withFileTypes: true });
          // Filter for files
          const files = dirents
            .filter((dirent) => dirent.isFile())
            .map((dirent) => {
              return {
                name: dirent.name,
                path: path.join(folderPath, dirent.name),
              };
            });
      
          event.sender.send("file-list-data", files);
        } catch (err) {
          console.error("Error reading directory:", err);
          event.sender.send("file-list-data", []); // or send an error message
        }
      });
  • 3.4 Render File List in Renderer

    • In renderer.js, add:

      ipcRenderer.on("file-list-data", (event, files) => {
        const fileList = document.getElementById("file-list");
        // Clear existing list
        fileList.innerHTML = "";
      
        files.forEach((file) => {
          const li = document.createElement("li");
          li.textContent = file.name;
          fileList.appendChild(li);
        });
      });
  • 3.5 Test Basic File Listing

    • Launch the app, open a folder with a few files, and ensure file names appear.
    • Confirm no subfolder files are listed yet (that's expected at this stage).

Story 4: Recursive File Listing

Goal:

Recursively list files from the selected folder and all subfolders.

  • 4.1 Create Recursive Function in Main Process

    • In main.js, define a function (e.g., readFilesRecursively) that accepts a directory path and returns an array of file info objects.
      function readFilesRecursively(dir) {
        let results = [];
        const dirents = fs.readdirSync(dir, { withFileTypes: true });
        dirents.forEach((dirent) => {
          const fullPath = path.join(dir, dirent.name);
          if (dirent.isDirectory()) {
            // Recursively read subdirectory
            results = results.concat(readFilesRecursively(fullPath));
          } else if (dirent.isFile()) {
            // Add file info
            results.push({
              name: dirent.name,
              path: fullPath,
            });
          }
        });
        return results;
      }
  • 4.2 Integrate Recursive Function into IPC

    • Replace the previous fs.readdirSync logic in ipcMain.on('request-file-list', ...) with the recursive version:
      ipcMain.on("request-file-list", (event, folderPath) => {
        try {
          const files = readFilesRecursively(folderPath);
          event.sender.send("file-list-data", files);
        } catch (err) {
          console.error("Error reading directory:", err);
          event.sender.send("file-list-data", []);
        }
      });
  • 4.3 Test Recursive Listing

    • Open a directory with nested subfolders and confirm that all files from subfolders appear in the list.

Story 5: File Selection (Checkboxes)

Goal:

Allow the user to select multiple files via checkboxes, with "Select All" and "Deselect All" functionality.

  • 5.1 Add Checkboxes to File List

    • In the 'file-list-data' handler in renderer.js, modify the forEach to create a checkbox:

      files.forEach((file) => {
        const li = document.createElement("li");
      
        const checkbox = document.createElement("input");
        checkbox.type = "checkbox";
        checkbox.value = file.path; // or use dataset
        checkbox.addEventListener("change", handleCheckboxChange);
      
        const label = document.createElement("span");
        label.textContent = file.name;
      
        li.appendChild(checkbox);
        li.appendChild(label);
        fileList.appendChild(li);
      });
  • 5.2 Maintain a selectedFiles Array in renderer.js

    • Create a top-level array:
      let selectedFiles = [];
    • Create a handler function:
      function handleCheckboxChange(event) {
        const filePath = event.target.value;
        if (event.target.checked) {
          if (!selectedFiles.includes(filePath)) {
            selectedFiles.push(filePath);
          }
        } else {
          selectedFiles = selectedFiles.filter((path) => path !== filePath);
        }
      }
  • 5.3 Add "Select All" / "Deselect All" Buttons (HTML)

    • In index.html, add:
      <button id="select-all-button">Select All</button>
      <button id="deselect-all-button">Deselect All</button>
  • 5.4 Implement "Select All"

    • In renderer.js, reference the buttons:
      const selectAllButton = document.getElementById("select-all-button");
      const deselectAllButton = document.getElementById("deselect-all-button");
    • Add event listener for select all:
      selectAllButton.addEventListener("click", () => {
        const checkboxes = document.querySelectorAll(
          '#file-list input[type="checkbox"]',
        );
        selectedFiles = [];
        checkboxes.forEach((checkbox) => {
          checkbox.checked = true;
          selectedFiles.push(checkbox.value);
        });
      });
  • 5.5 Implement "Deselect All"

    • Add event listener for deselect all:
      deselectAllButton.addEventListener("click", () => {
        const checkboxes = document.querySelectorAll(
          '#file-list input[type="checkbox"]',
        );
        checkboxes.forEach((checkbox) => {
          checkbox.checked = false;
        });
        selectedFiles = [];
      });
  • 5.6 Test File Selection

    • Check/uncheck individual boxes and confirm selectedFiles updates.
    • Click "Select All" / "Deselect All" and confirm the UI and selectedFiles state matches.

Story 6: Token Estimation (Basic)

Goal:

Estimate tokens per file using a tokenization library (e.g., gpt-3-encoder), store the token count in file data, and display it.

  • 6.1 Choose and Install Token Estimator

    • Run npm install gpt-3-encoder (or your chosen library).
  • 6.2 Read File Content in Main Process

    • In readFilesRecursively, after identifying a file, read its content:
      const fileContent = fs.readFileSync(fullPath, "utf8");
    • Store fileContent in a local variable (e.g., content).
  • 6.3 Estimate Tokens

    • Import encode from gpt-3-encoder at the top of main.js:
      const { encode } = require("gpt-3-encoder");
    • Calculate token length:
      const encoded = encode(fileContent);
      const tokenCount = encoded.length;
    • Attach tokenCount to the results array's item object (including the file path and content):
      results.push({
        name: dirent.name,
        path: fullPath,
        content: fileContent,
        tokenCount: tokenCount,
      });
  • 6.4 Send Token Count to Renderer

    • Ensure the final array includes tokenCount for each file.
  • 6.5 Display Token Count in Renderer

    • In ipcRenderer.on('file-list-data', ...), when creating the list item, also show tokenCount:
      const tokenCountSpan = document.createElement("span");
      tokenCountSpan.textContent = ` (Tokens: ${file.tokenCount})`;
      // ...
      li.appendChild(tokenCountSpan);
  • 6.6 Test Token Estimation

    • Open a folder with some files, confirm tokens appear.
    • Validate approximate accuracy with a known sample (e.g., small text file).

Story 7: Total Token Count Display

Goal:

Show the total tokens of selected files in the UI, updating as selection changes.

  • 7.1 Add "Total Tokens" Display (HTML)

    • In index.html, add:
      <div id="total-tokens">Total Tokens: 0</div>
  • 7.2 Create a calculateTotalTokens Function (Renderer)

    • In renderer.js, define:
      function calculateTotalTokens() {
        // files is the full array of file data
        let total = 0;
        selectedFiles.forEach((selectedPath) => {
          const fileData = allFiles.find((f) => f.path === selectedPath);
          if (fileData) {
            total += fileData.tokenCount;
          }
        });
        return total;
      }
  • 7.3 Integrate Calculation into File List Rendering

    • Modify the 'file-list-data' handler to store the retrieved files in a global variable (e.g. allFiles) so we can reference them later:
      let allFiles = [];
      ipcRenderer.on("file-list-data", (event, files) => {
        allFiles = files;
        // Render the list...
      });
    • After rendering the list, call a function updateTotalTokens() that sets:
      function updateTotalTokens() {
        const totalTokens = calculateTotalTokens(allFiles);
        document.getElementById(
          "total-tokens",
        ).textContent = `Total Tokens: ${totalTokens}`;
      }
  • 7.4 Update Total Tokens on Checkbox Change

    • In handleCheckboxChange, after modifying selectedFiles, call updateTotalTokens().
  • 7.5 Update Total Tokens on "Select All" / "Deselect All"

    • In the "Select All" button click handler, after setting selectedFiles, call updateTotalTokens().
    • In the "Deselect All" button click handler, after clearing selectedFiles, call updateTotalTokens().
  • 7.6 Test Total Tokens

    • Select various files, observe total token count changes.
    • Confirm correctness with small test files.

Story 8: File Sorting

Goal:

Allow sorting of files by name, size, or token count, in ascending/descending order.

  • 8.1 Get File Size in Main Process

    • In readFilesRecursively, after reading the file content and before pushing the object, get file stats:
      const stats = fs.statSync(fullPath);
      const fileSize = stats.size; // in bytes
    • Add size: fileSize to the file object.
  • 8.2 Add Sort Controls (HTML)

    • In index.html, add a dropdown or buttons. Example (dropdown):
      <select id="sort-dropdown">
        <option value="name-asc">Name (A-Z)</option>
        <option value="name-desc">Name (Z-A)</option>
        <option value="tokens-asc">Tokens (Low-High)</option>
        <option value="tokens-desc">Tokens (High-Low)</option>
        <option value="size-asc">Size (Small-Large)</option>
        <option value="size-desc">Size (Large-Small)</option>
      </select>
  • 8.3 Sort State and Function (Renderer)

    • In renderer.js, maintain a variable currentSort = 'name-asc'.

    • Define a function sortFiles(files, sortValue):

      function sortFiles(files, sortValue) {
        // sortValue might be something like 'name-asc', 'tokens-desc', etc.
        const [sortKey, sortDir] = sortValue.split("-"); // e.g. 'name', 'asc'
      
        files.sort((a, b) => {
          let comparison = 0;
      
          if (sortKey === "name") {
            // compare by name
            comparison = a.name.localeCompare(b.name);
          } else if (sortKey === "tokens") {
            comparison = a.tokenCount - b.tokenCount;
          } else if (sortKey === "size") {
            comparison = a.size - b.size;
          }
      
          return sortDir === "asc" ? comparison : -comparison;
        });
      
        return files;
      }
  • 8.4 Re-render File List After Sorting

    • Modify the 'file-list-data' handler:
      ipcRenderer.on("file-list-data", (event, files) => {
        allFiles = files;
        allFiles = sortFiles(allFiles, currentSort);
        renderFileList(allFiles); // a separate function that builds the UI
        updateTotalTokens();
      });
    • Where renderFileList() is your logic that creates <li> elements, etc.
  • 8.5 Sort Trigger

    • In renderer.js, reference the sort dropdown:
      const sortDropdown = document.getElementById("sort-dropdown");
      sortDropdown.addEventListener("change", (event) => {
        currentSort = event.target.value;
        allFiles = sortFiles(allFiles, currentSort);
        renderFileList(allFiles);
        updateTotalTokens();
      });
  • 8.6 Test Sorting

    • Confirm the list re-sorts properly by name, token count, and size.
    • Check ascending vs. descending.

Story 9: Concatenation and Copy to Clipboard

Goal:

Concatenate the contents of selected files in the sorted order, and let the user copy it to the clipboard.

  • 9.1 Add "Copy to Clipboard" Button (HTML)

    • In index.html, add:
      <button id="copy-button">Copy to Clipboard</button>
  • 9.2 Concatenate Files in Renderer

    • Define a function concatenateSelectedFiles() in renderer.js:

      function concatenateSelectedFiles() {
        // ensure we use the sorted, current allFiles array
        let concatenatedString = "";
      
        // We only want to concatenate the files that are both in `selectedFiles` and `allFiles` (in the correct order).
        allFiles.forEach((file) => {
          if (selectedFiles.includes(file.path)) {
            // optional separator
            concatenatedString += `\n\n// ---- File: ${file.name} ----\n\n`;
            concatenatedString += file.content;
          }
        });
      
        return concatenatedString;
      }
  • 9.3 Implement Copy to Clipboard

    • In renderer.js, get a reference to copy-button and add a click handler:
      const copyButton = document.getElementById("copy-button");
      copyButton.addEventListener("click", async () => {
        const finalString = concatenateSelectedFiles();
        try {
          await navigator.clipboard.writeText(finalString);
          // Provide feedback
          copyButton.textContent = "Copied!";
          setTimeout(() => {
            copyButton.textContent = "Copy to Clipboard";
          }, 2000);
        } catch (err) {
          console.error("Failed to copy:", err);
        }
      });
  • 9.4 Test Copy Functionality

    • Select some files, click "Copy to Clipboard".
    • Paste into a text editor to confirm the correct concatenated content.

Story 10: File Filtering

Goal:

Allow users to filter the file list by name/path. The displayed list (and selection states) should reflect the filtered results.

  • 10.1 Add a Filter/Search Input (HTML)

    • In index.html, add:
      <input
        type="text"
        id="filter-input"
        placeholder="Filter files by name..."
      />
  • 10.2 Maintain Original File Data

    • In renderer.js, use two arrays:
      • allFiles — the full, unfiltered set of files.
      • displayedFiles — the files currently filtered and sorted.
  • 10.3 Filter Logic

    • Define a function filterFiles(files, filterText):
      function filterFiles(files, filterText) {
        const lowerFilter = filterText.toLowerCase();
        return files.filter((file) => {
          // filter by name (or path)
          return (
            file.name.toLowerCase().includes(lowerFilter) ||
            file.path.toLowerCase().includes(lowerFilter)
          );
        });
      }
  • 10.4 Handle Filter Input

    • In renderer.js, get reference to the input and add listener:
      const filterInput = document.getElementById("filter-input");
      filterInput.addEventListener("input", () => {
        const filterText = filterInput.value;
        displayedFiles = filterFiles(allFiles, filterText);
        displayedFiles = sortFiles(displayedFiles, currentSort);
        renderFileList(displayedFiles);
        updateTotalTokens();
      });
    • Where renderFileList(displayedFiles) only displays those files.
  • 10.5 Update Selections After Filtering

    • In renderFileList(files), re-create checkboxes. Some selected files may not appear if they're filtered out, but remain in selectedFiles.
    • Optionally, you can remove from selectedFiles any file not in files. For example:
      selectedFiles = selectedFiles.filter((selectedPath) =>
        files.some((file) => file.path === selectedPath),
      );
    • After doing that filtering step, call updateTotalTokens() to ensure the total token count is correct.
  • 10.6 Test Filtering

    • Type partial filenames to see the list update in real time.
    • Confirm "Select All"/"Deselect All" still works with filtered results.
    • Confirm total tokens is correct under different filter conditions.

Final Notes

  1. Security Considerations

    • If you enable nodeIntegration and contextIsolation: false, ensure you're aware of potential security risks. A more secure approach might rely on a preload script or a carefully restricted environment.
  2. Performance Considerations

    • Reading large directories and large files might be slow. Consider asynchronous fs.readFile if necessary.
    • Displaying thousands of files in the UI can also affect performance—virtual scrolling or pagination might be needed for huge repos.
  3. UI Libraries

    • If the project grows, consider using React, Vue, or Svelte for better state management and reactivity. The above plan works fine for a small to medium application in plain DOM manipulation.
  4. Further Enhancements

    • Multiple workspace memory
    • Different tokenization modes (e.g., GPT-4, GPT-3.5, etc.)
    • Handling binary files (exclude them or show warnings)
    • Enhanced styling with CSS frameworks

Congratulations! By following this meticulous checklist, you or an autonomous AI Coding Agent should be able to fully implement an open-source, cross-platform Electron application that mirrors (and improves upon) the core functionality of RepoPrompt—providing file listing, selection, token estimation, sorting, filtering, concatenation, and a simple one-click copy to clipboard feature.