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 apackage.json
file with default settings.
-
1.3 Install Electron
- Run
npm install electron --save-dev
(or--save
) to add Electron as a dev dependency.
- Run
-
1.4 Create Main Process File
- Create a new file named
main.js
in the project root directory.
- Create a new file named
-
1.5 Add Basic Electron Boilerplate to
main.js
-
Import
app
andBrowserWindow
fromelectron
:const { app, BrowserWindow } = require("electron");
-
Create a function
createWindow()
that instantiates aBrowserWindow
: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()
, callcreateWindow()
: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
.
- In the project root, create
-
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>
- Add the minimal tags to
-
1.8 Load HTML in Main Process
- Confirm
main.js
callsmainWindow.loadFile('index.html')
insidecreateWindow()
. (Already in step 1.5)
- Confirm
-
1.9 Add Start Script in
package.json
- Open
package.json
. - Add a script named
"start"
with the command"electron ."
:"scripts": { "start": "electron ." }
- Open
-
1.10 Test Basic App
- Run
npm start
. - Ensure the Electron app launches with a blank (or basic) window titled "Repo String Clone".
- Run
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>
- In
-
2.2 Create
renderer.js
(Renderer Script)- Create a file named
renderer.js
in the project root (or asrc/
directory). - Link it in
index.html
by adding:<script src="./renderer.js"></script>
- Create a file named
-
2.3 Import IPC in Renderer
- In
renderer.js
, add:const { ipcRenderer } = require("electron");
- In
-
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"); });
- Still in
-
2.5 Handle IPC in Main Process
-
In
main.js
, importipcMain
anddialog
: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
.
- Launch the app (
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>
- In
-
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).
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; }
- In
-
4.2 Integrate Recursive Function into IPC
- Replace the previous
fs.readdirSync
logic inipcMain.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", []); } });
- Replace the previous
-
4.3 Test Recursive Listing
- Open a directory with nested subfolders and confirm that all files from subfolders appear in the list.
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 inrenderer.js
, modify theforEach
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 inrenderer.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); } }
- Create a top-level array:
-
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>
- In
-
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); }); });
- In
-
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 = []; });
- Add event listener for deselect all:
-
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.
- Check/uncheck individual boxes and confirm
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).
- Run
-
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
).
- In
-
6.3 Estimate Tokens
- Import
encode
fromgpt-3-encoder
at the top ofmain.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, });
- Import
-
6.4 Send Token Count to Renderer
- Ensure the final array includes
tokenCount
for each file.
- Ensure the final array includes
-
6.5 Display Token Count in Renderer
- In
ipcRenderer.on('file-list-data', ...)
, when creating the list item, also showtokenCount
:const tokenCountSpan = document.createElement("span"); tokenCountSpan.textContent = ` (Tokens: ${file.tokenCount})`; // ... li.appendChild(tokenCountSpan);
- In
-
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).
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>
- In
-
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; }
- In
-
7.3 Integrate Calculation into File List Rendering
- Modify the
'file-list-data'
handler to store the retrievedfiles
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}`; }
- Modify the
-
7.4 Update Total Tokens on Checkbox Change
- In
handleCheckboxChange
, after modifyingselectedFiles
, callupdateTotalTokens()
.
- In
-
7.5 Update Total Tokens on "Select All" / "Deselect All"
- In the "Select All" button click handler, after setting
selectedFiles
, callupdateTotalTokens()
. - In the "Deselect All" button click handler, after clearing
selectedFiles
, callupdateTotalTokens()
.
- In the "Select All" button click handler, after setting
-
7.6 Test Total Tokens
- Select various files, observe total token count changes.
- Confirm correctness with small test files.
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.
- In
-
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>
- In
-
8.3 Sort State and Function (Renderer)
-
In
renderer.js
, maintain a variablecurrentSort = '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.
- Modify the
-
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(); });
- In
-
8.6 Test Sorting
- Confirm the list re-sorts properly by name, token count, and size.
- Check ascending vs. descending.
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>
- In
-
9.2 Concatenate Files in Renderer
-
Define a function
concatenateSelectedFiles()
inrenderer.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 tocopy-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); } });
- In
-
9.4 Test Copy Functionality
- Select some files, click "Copy to Clipboard".
- Paste into a text editor to confirm the correct concatenated content.
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..." />
- In
-
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.
- In
-
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) ); }); }
- Define a function
-
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.
- In
-
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 inselectedFiles
. - Optionally, you can remove from
selectedFiles
any file not infiles
. 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.
- In
-
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.
-
Security Considerations
- If you enable
nodeIntegration
andcontextIsolation: false
, ensure you're aware of potential security risks. A more secure approach might rely on a preload script or a carefully restricted environment.
- If you enable
-
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.
- Reading large directories and large files might be slow. Consider asynchronous
-
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.
-
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.