├── .gitignore
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── applyChange.js
├── background.js
├── content.js
├── contextMenu.js
├── easycode.png
├── fileChips.js
├── files.js
├── interceptSubmit.js
├── manifest.json
├── mixpanel.js
├── options.html
├── options.js
├── platform.js
├── styles.css
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Byte-compiled files
4 | __pycache__/
5 | *.py[cod]
6 | *$py.class
7 |
8 | # Virtual environment directories
9 | venv/
10 | env/
11 |
12 | # Distribution and build directories
13 | build/
14 | dist/
15 |
16 |
17 | .aider*
18 | .env
19 | easycode.ignore
20 |
21 | # ignore python folder
22 | python
23 | reference.md
24 | testA.js
25 |
26 | .DS_Store
27 | *.zip
28 | mixpanel-chrome.js
29 | options.backup.js
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome",
11 | "url": "https://www.chatgpt.com",
12 | "timeout": 20000,
13 | "runtimeArgs": ["--load-extension=${workspaceFolder}"]
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 EasyCode-AI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Codaware - by EasyCode
2 |
3 | ## Chat with Codebase from ChatGPT or Claude
4 |
5 | [](https://youtu.be/WFo2guwa8bY)
6 |
7 | - no more copying & pasting
8 | - don't pay for multiple subscriptions, use chatgpt plus or claude paid plan, and advanced models
9 | - take advantage of web features such as o1-preview, image, web search, artifacts, etc.
10 |
11 | ## Installation
12 |
13 | ### Option 1: Chrome Web Store (Recommended)
14 | You can install Codaware directly from the Chrome Web Store:
15 | [Click here to install Codaware](https://chromewebstore.google.com/detail/codaware-by-easycode/mmelffgafmcppjiecckdlmgbbdcgmkdg?hl=en)
16 |
17 | ### Option 2: Manual Installation (Load Unpacked)
18 | If you prefer to install manually:
19 | 1. Download the extension files
20 | 2. Open Chrome and navigate to `chrome://extensions/`
21 | 3. Enable "Developer mode" in the top right corner
22 | 4. Click "Load unpacked"
23 | 5. Select the downloaded extension directory
24 |
25 | ## Features
26 | - [x] reference files on ChatGPT & Claude.ai
27 | - [x] ability to apply changes directly from ChatGPT and Claude.ai
28 |
29 | ## Bugs
30 | - [x] hitting ENTER sends question without injecting file content [Medium]
31 | - [x] Error loading files sometimes
32 | - [x] socket error sometimes [Tiny]
33 | - [x] prevent duplicated files from being added. [Tiny]
34 |
35 | ## Improvements
36 | - [x] refactor front end into more modular components [Medium]
37 | - [ ] migrate project to react or nextjs [Medium]
38 | - [ ] add bundling
39 | - [x] add a file name place holder after the file is injected [tiny]
40 | - [x] refactor vscode side to its own folder and make it modular as well,
41 | - [x] stop generation doesn't work due to capturing the button submit [Medium?]
42 | - [ ] collapse the codeblocks in the "sent" sections [Tiny/Easy]
43 | - [ ] don't resend file content its already in chat context [Tiny]
44 |
45 | ## Future Feature Ideas
46 | - [ ] ability to @problems inside chrome extension
47 | - [ ] ability to @codebase and RAG the codebase
48 | - [ ] [WIP] add ability to drag a folder and parse the file path, and fetch the files.. [Medium]
49 | - [ ] send file updates from vscode to browser.
50 | - [ ] compare answer with different models such as DeepSeek, Qwen, Llama 3, etc. [Hard]
51 | - [ ] add ability to watch for errors in console, auto suggest it in the web browser [Medium?]
52 |
53 | ## Bugs or Feature Ideas
54 | - Please submit a issue.
--------------------------------------------------------------------------------
/applyChange.js:
--------------------------------------------------------------------------------
1 | const SPINNER_SVG = ``;
5 |
6 | const TIMEOUT_DURATION = 120000; // 2 minutes in milliseconds
7 |
8 |
9 | function findActionButtonContainerFromCodeBlock(codeBlock) {
10 | const platform = getCurrentPlatform();
11 |
12 | if (platform === PLATFORMS.CLAUDE) {
13 | const codeBlockParent = codeBlock.parentElement;
14 | if (codeBlockParent.classList.contains('h-fit') && codeBlockParent.classList.contains('w-fit')) {
15 | // This is an artifact codeblock
16 | const parentContainer = codeBlock.closest('div.h-full.bg-bg-000');
17 | const actionButtonContainer = parentContainer.querySelector('div.justify-end > button').parentElement;
18 | if (actionButtonContainer) {
19 | // console.log(actionButtonContainer);
20 | return actionButtonContainer;
21 | } else {
22 | console.log("no action button container found");
23 | return null;
24 | }
25 | } else {
26 | // this is an in chat codeblock
27 | const parentContainer = codeBlock.closest('pre');
28 | const actionButtonContainer = parentContainer.querySelector('button').parentElement.parentElement;
29 | if (actionButtonContainer) {
30 | //console.log(actionButtonContainer);
31 | return actionButtonContainer;
32 | } else {
33 | console.log("no action button container found");
34 | return null;
35 | }
36 | }
37 | } else if (platform === PLATFORMS.CHATGPT) {
38 | if (codeBlock.tagName === 'CODE') {
39 | // this is a typical in chat code block
40 | const parentContainer = codeBlock.closest('pre');
41 | const actionButtonContainer = parentContainer.querySelector('div.sticky');
42 | if (actionButtonContainer) {
43 | // console.log(actionButtonContainer);
44 | return actionButtonContainer.firstChild;
45 | } else {
46 | console.log("no action button container found");
47 | return null;
48 | }
49 | } else if (!Array.from(codeBlock.classList).some(className => className.includes('preview'))) {
50 | // this is a canvas code block in editor mode
51 | console.log("canvas code block in editor mode");
52 | const parentContainer = codeBlock.closest('section');
53 | const actionButtonContainer = parentContainer.querySelector('header');
54 | if (actionButtonContainer) {
55 | console.log(actionButtonContainer);
56 | return actionButtonContainer.lastElementChild;
57 | } else {
58 | console.log("no action button container found");
59 | return null;
60 | }
61 | }
62 | }
63 | return null;
64 | }
65 |
66 | function addCodeBlockButton(codeBlock) {
67 | // console.log(codeBlock);
68 |
69 | // Check if button was already added using dataset
70 | if (codeBlock.dataset.buttonAdded) {
71 | console.log("skipping adding button, already has it");
72 | return; // Add early return here
73 | }
74 |
75 | const platform = getCurrentPlatform();
76 | if (!platform) return;
77 | const selectors = platform.selectors;
78 |
79 | // For both ChatGPT and Claude
80 | if (codeBlock.matches(selectors.codeBlock)) {
81 | const buttonContainer = findActionButtonContainerFromCodeBlock(codeBlock, platform.selectors.codeActionButtonContainer);
82 |
83 | if (!buttonContainer) {
84 | console.error('Could not find button container');
85 | return;
86 | }
87 |
88 | //let buttonContainerElement = buttonContainer.querySelector(`${platform.buttonStyle.container}`);
89 |
90 | // Check if button already exists in the container
91 | if (buttonContainer) {
92 | const existingApplyButton = Array.from(buttonContainer.querySelectorAll('button')).find(
93 | btn => btn.innerHTML === platform.buttonStyle.icon
94 | );
95 | if (existingApplyButton) {
96 | console.log("Apply button already exists, remove the current one");
97 | codeBlock.dataset.buttonAdded = 'true';
98 | buttonContainer.removeChild(existingApplyButton);
99 | }
100 | } else {
101 | buttonContainer = document.createElement('div');
102 | buttonContainer.className = platform.buttonStyle.container;
103 | codeBlock.appendChild(buttonContainer);
104 | }
105 |
106 | const applyButton = document.createElement('button');
107 | if (platform === PLATFORMS.CHATGPT) {
108 | applyButton.style.cssText = platform.buttonStyle.style;
109 | buttonContainer.prepend(applyButton);
110 | } else if (platform == PLATFORMS.CLAUDE) {
111 | applyButton.className = platform.buttonStyle.classNames;
112 | applyButton.style.cssText = platform.buttonStyle.style;
113 | buttonContainer.prepend(applyButton);
114 | }
115 | applyButton.innerHTML = platform.buttonStyle.icon;
116 |
117 | setupButtonClickHandler(applyButton, codeBlock);
118 | }
119 |
120 | codeBlock.dataset.buttonAdded = 'true';
121 | console.log("adding button for codeblock");
122 | }
123 |
124 | // Helper function for button click handler
125 | function setupButtonClickHandler(button, codeBlock) {
126 | const originalButtonContent = button.innerHTML;
127 | let timeoutId = null;
128 |
129 | const resetButton = () => {
130 | if (timeoutId) {
131 | clearTimeout(timeoutId);
132 | timeoutId = null;
133 | }
134 | button.innerHTML = originalButtonContent;
135 | button.disabled = false;
136 | };
137 |
138 | button.addEventListener('click', async (e) => {
139 | e.preventDefault();
140 | e.stopPropagation();
141 |
142 | // Check WebSocket connection
143 | const isConnected = await isWebSocketConnected();
144 |
145 | if (!isConnected) {
146 | alert("Cannot connect with VS Code, please ensure EasyCode extension is installed");
147 | return true;
148 | }
149 |
150 | const platform = getCurrentPlatform();
151 | const selectors = platform.selectors;
152 |
153 | let code;
154 | if (window.location.hostname.includes('claude.ai')) {
155 | //const containerDiv = button.closest(selectors.codeActionButtonContainer);
156 | //codeblock = containerDiv.querySelector(selectors.codeBlock);
157 | code = codeBlock.textContent
158 | } else {
159 | code = codeBlock.matches(selectors.codeBlock) && codeBlock.querySelector('code')
160 | ? codeBlock.querySelector('code').textContent
161 | : codeBlock.textContent;
162 | }
163 |
164 | console.log('Code block content:', code);
165 |
166 | try {
167 | const similarityScores = predictApplyDestination(code);
168 | const applyDestination = similarityScores.reduce((best, current) =>
169 | current.score > best.score ? current : best
170 | );
171 |
172 | const scoresText = similarityScores
173 | .sort((a, b) => b.score - a.score)
174 | .map(entry => `${entry.fileName}: ${(entry.score * 100).toFixed(1)}%`)
175 | .join('\n');
176 |
177 | const confirmMessage = `Do you want to apply changes to:\n${applyDestination.fileName}\n\nAll matches:\n${scoresText}`;
178 |
179 | if (confirm(confirmMessage)) {
180 | // Show spinner and disable button
181 | button.innerHTML = SPINNER_SVG;
182 | button.disabled = true;
183 |
184 | // Set timeout to reset button after 2 minutes
185 | timeoutId = setTimeout(() => {
186 | resetButton();
187 | }, TIMEOUT_DURATION);
188 |
189 | chrome.runtime.sendMessage({
190 | type: 'APPLY_DIFF',
191 | fileName: applyDestination.fileName,
192 | code: code
193 | }, (response) => {
194 | if (response.error) {
195 | console.error('Error applying changes:', response.error);
196 | alert('Failed to apply changes: ' + response.error);
197 | resetButton();
198 | } else {
199 | console.log('Changes applied successfully:', response.output);
200 | alert('Changes applied successfully');
201 | resetButton();
202 | }
203 | });
204 | }
205 | } catch (e) {
206 | alert("Failed to apply change, please ensure VS Code extension is running and the right project is open");
207 | }
208 | });
209 | }
210 |
211 |
212 | // Rest of the code remains the same
213 | function addButtonsToCodeBlocks() {
214 | const platform = getCurrentPlatform();
215 | if (!platform) return;
216 |
217 | const codeBlocks = document.querySelectorAll(platform.selectors.codeBlock);
218 | codeBlocks.forEach((codeBlock) => {
219 | if (!Array.from(codeBlock.classList).some(className => className.includes('preview'))) {
220 | addCodeBlockButton(codeBlock);
221 | }
222 | });
223 | }
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | import mixpanel from "./mixpanel.js"
2 |
3 | mixpanel.init("885bb3993bb98e37dbb21823f8d1903d");
4 |
5 | chrome.runtime.onInstalled.addListener((details) => {
6 | // tracking extension install
7 | console.log("extension installed");
8 | mixpanel.track('Install');
9 | })
10 |
11 | let websocketPort; // default port
12 |
13 | // Initialize port when extension loads
14 | initializePort();
15 |
16 | function initializePort() {
17 | chrome.storage.local.get({ websocketPort: 49201 }, (items) => {
18 | websocketPort = items.websocketPort;
19 | connectWebSocket();
20 | });
21 | }
22 |
23 | const MAX_RETRIES = 3;
24 | const RETRY_DELAY = 1000; // 1 second between retries
25 |
26 | // Listen for changes to the port setting
27 | chrome.storage.onChanged.addListener((changes, namespace) => {
28 | if (namespace === 'local' && changes.websocketPort) {
29 | websocketPort = changes.websocketPort.newValue;
30 | // Reconnect with new port if socket exists
31 | if (socket) {
32 | socket.close();
33 | connectWebSocket();
34 | }
35 | }
36 | });
37 |
38 | function safeStorageAccess(operation) {
39 | // Check if chrome.storage is available
40 | if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
41 | return operation();
42 | } else {
43 | console.warn('Chrome storage API not available');
44 | // Optionally retry after a short delay
45 | setTimeout(() => {
46 | if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
47 | operation();
48 | }
49 | }, 1000);
50 | }
51 | }
52 |
53 | // Helper function to safely send WebSocket messages
54 | function safeSendWebSocketMessage(message) {
55 | if (socket && socket.readyState === WebSocket.OPEN) {
56 | socket.send(JSON.stringify(message));
57 | return true;
58 | }
59 | console.warn('WebSocket not ready, message not sent:', message);
60 | return false;
61 | }
62 |
63 | chrome.runtime.onInstalled.addListener(() => {
64 | console.log('ChatGPT Mention Extension installed.');
65 | });
66 |
67 | let socket;
68 | let reconnectAttempts = 0;
69 | const MAX_RECONNECT_ATTEMPTS = 3;
70 | const RECONNECT_DELAY = 3000; // 3 seconds
71 |
72 | function connectWebSocket(retry = false) {
73 | try{
74 | socket = new WebSocket(`ws://localhost:${websocketPort}`);
75 | } catch (e) {
76 | console.warn("Failed to establish websocket connect");
77 | }
78 |
79 | socket.onopen = () => {
80 | console.log('Connected to VS Code extension');
81 | reconnectAttempts = 0; // Reset attempts on successful connection
82 |
83 | // Add a small delay to ensure the connection is ready
84 | setTimeout(() => {
85 | safeSendWebSocketMessage({
86 | type: 'REQUEST_FILES'
87 | });
88 | }, 100);
89 | };
90 |
91 | socket.onmessage = (event) => {
92 | const data = JSON.parse(event.data);
93 | if (data.type === 'FILE_LIST') {
94 | console.log(`received file list`);
95 | safeStorageAccess(() => {
96 | chrome.storage.local.set({
97 | filePaths: data.files
98 | });
99 | });
100 | } else if (data.type === 'FILE_CONTENTS') {
101 | console.log("Looking for file content callback for:", data.filePath);
102 | if (fileContentCallbacks[data.filePath]) {
103 | // Ensure content is properly decoded
104 | const content = typeof data.content === 'string'
105 | ? data.content
106 | : new TextDecoder('utf-8').decode(data.content);
107 |
108 | fileContentCallbacks[data.filePath]({
109 | content: content
110 | });
111 | delete fileContentCallbacks[data.filePath];
112 | } else {
113 | console.warn("No file content callback found for:", data.filePath);
114 | }
115 | } else if (data.type === 'DIFF_CLIPBOARD_RESULT') { // Assuming this is the type for diff responses
116 | console.log("Looking for diff callback for:", data.fileName);
117 | if (diffCallbacks[data.fileName]) {
118 | diffCallbacks[data.fileName](data);
119 | delete diffCallbacks[data.fileName];
120 | } else {
121 | console.warn("No diff callback found for:", data.fileName);
122 | }
123 | }
124 | };
125 |
126 | socket.onerror = (error) => {
127 | console.warn('WebSocket error:', error);
128 | };
129 |
130 | socket.onclose = () => {
131 | if (!retry) {
132 | console.log('WebSocket closed');
133 | return;
134 | } else {
135 | // Attempt to reconnect
136 | if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
137 | reconnectAttempts++;
138 | console.log(`Attempting to reconnect (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
139 | setTimeout(connectWebSocket, RECONNECT_DELAY);
140 | } else {
141 | console.warn('Max reconnection attempts reached');
142 | }
143 | }
144 | };
145 | }
146 |
147 | // Add a function to check connection status
148 | async function isSocketConnected() {
149 | let isConnected = socket && socket.readyState === WebSocket.OPEN;
150 | if (!isConnected) {
151 | console.log('Socket not connected. Attempting to reconnect...');
152 | reconnectAttempts = 0;
153 |
154 | // Create a promise that resolves when connection is established or rejects after timeout
155 | const connectionPromise = new Promise((resolve, reject) => {
156 | const originalOnOpen = socket?.onopen;
157 | const originalOnError = socket?.onerror;
158 |
159 | // Set a timeout to reject the promise if connection takes too long
160 | const timeoutId = setTimeout(() => {
161 | reject(new Error('Connection timeout'));
162 | }, 1000); // 5 second timeout
163 |
164 | connectWebSocket();
165 |
166 | socket.onopen = (event) => {
167 | clearTimeout(timeoutId);
168 | if (originalOnOpen) originalOnOpen(event);
169 | else {
170 | setTimeout(() => {
171 | safeSendWebSocketMessage({
172 | type: 'REQUEST_FILES'
173 | });
174 | }, 100);
175 | }
176 | resolve();
177 | };
178 |
179 | socket.onerror = (error) => {
180 | clearTimeout(timeoutId);
181 | if (originalOnError) originalOnError(error);
182 | reject(error);
183 | };
184 | });
185 |
186 | try {
187 | await connectionPromise;
188 | isConnected = socket && socket.readyState === WebSocket.OPEN;
189 | } catch (error) {
190 | console.warn('Failed to establish connection:', error);
191 | isConnected = false;
192 | }
193 | }
194 | return isConnected;
195 | }
196 |
197 | // Separate callback queues
198 | let fileContentCallbacks = {};
199 | let diffCallbacks = {};
200 |
201 | // Add a helper function to handle retries
202 | async function retryOperation(operation, maxRetries = MAX_RETRIES) {
203 | for (let attempt = 0; attempt < maxRetries; attempt++) {
204 | const isConnected = await isSocketConnected();
205 | if (isConnected) {
206 | return await operation();
207 | }
208 |
209 | console.log(`WebSocket not connected, attempt ${attempt + 1}/${maxRetries}`);
210 |
211 | // Try to reconnect
212 | connectWebSocket();
213 |
214 | // Wait for connection
215 | await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
216 | }
217 |
218 | throw new Error('WebSocket connection failed after retries');
219 | }
220 |
221 | // Modify the message listener to use retries
222 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
223 | if (message.type === 'APPLY_DIFF') {
224 | console.log("Background: received request to apply diff for:", message.fileName);
225 |
226 | retryOperation(async () => {
227 | // Store in diff-specific callback queue
228 | diffCallbacks[message.fileName] = (response) => {
229 | console.log("Diff result received:", response);
230 | sendResponse(response);
231 | };
232 |
233 | if (!safeSendWebSocketMessage({
234 | type: 'DIFF_CLIPBOARD',
235 | fileName: message.fileName,
236 | code: `${message.code}\n`
237 | })) {
238 | throw new Error('Failed to send diff request');
239 | }
240 | })
241 | .catch(error => {
242 | console.log('Error applying diff:', error);
243 | sendResponse({
244 | error: 'Failed to apply changes after retries. Please check your connection.'
245 | });
246 | });
247 |
248 | return true;
249 | }
250 | else if (message.type === 'GET_FILE_CONTENTS') {
251 | console.log("Background: received request for file:", message.filePath);
252 |
253 | retryOperation(async () => {
254 | // Store in file-specific callback queue
255 | fileContentCallbacks[message.filePath] = (response) => {
256 | console.log("Executing callback for:", message.filePath, response);
257 | sendResponse(response);
258 | };
259 |
260 | if (!safeSendWebSocketMessage({
261 | type: 'GET_FILE_CONTENTS',
262 | filePath: message.filePath.trim()
263 | })) {
264 | throw new Error('Failed to send file contents request');
265 | }
266 | })
267 | .catch(error => {
268 | console.warn('Error getting file contents:', error);
269 | sendResponse({
270 | error: 'Failed to get file contents after retries. Please check your connection.'
271 | });
272 | });
273 |
274 | return true;
275 | }
276 | else if (message.type === 'CHECK_CONNECTION') {
277 | isSocketConnected().then(connected => {
278 | sendResponse({ connected });
279 | });
280 | return true;
281 | } else if (message.type === "REQUEST_FILES") {
282 | // Create a promise that resolves when files are updated
283 | const filesUpdatePromise = new Promise((resolve) => {
284 | // Store the resolve function in a callback that will be called
285 | // when we receive the FILE_LIST response
286 | const messageCallback = (event) => {
287 | const data = JSON.parse(event.data);
288 | if (data.type === 'FILE_LIST') {
289 | socket.removeEventListener('message', messageCallback);
290 | resolve(data.files);
291 | }
292 | };
293 |
294 | // Add temporary listener for this specific request
295 | socket.addEventListener('message', messageCallback);
296 |
297 | // Send the request
298 | safeSendWebSocketMessage({
299 | type: 'REQUEST_FILES'
300 | });
301 |
302 | // Add timeout to prevent hanging
303 | setTimeout(() => {
304 | socket.removeEventListener('message', messageCallback);
305 | resolve([]); // Resolve with empty array if timeout
306 | }, 5000);
307 | });
308 |
309 | // Wait for files to be updated before sending response
310 | filesUpdatePromise.then((files) => {
311 | sendResponse({ success: true, files });
312 | });
313 |
314 | return true; // Keep message channel open for async response
315 | }
316 | });
317 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
318 | if (request.action === 'openOptions') {
319 | chrome.runtime.openOptionsPage();
320 | }
321 | });
322 |
323 | chrome.tabs.onActivated.addListener(async (activeInfo) => {
324 | const tab = await chrome.tabs.get(activeInfo.tabId);
325 | await handleTabUrl(tab.url);
326 | });
327 |
328 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
329 | if (changeInfo.url) {
330 | await handleTabUrl(changeInfo.url);
331 | }
332 | });
333 |
334 | async function handleTabUrl(url) {
335 | const validUrls = [
336 | 'https://chat.openai.com',
337 | 'https://chatgpt.com',
338 | 'https://claude.ai'
339 | ];
340 |
341 | const shouldConnect = validUrls.some(validUrl => url?.startsWith(validUrl));
342 |
343 | if (shouldConnect) {
344 | const isConnected = await isSocketConnected();
345 | if (!isConnected) {
346 | chrome.storage.local.get({
347 | websocketPort
348 | }, (items) => {
349 | websocketPort = items.websocketPort;
350 | connectWebSocket();
351 | });
352 | }
353 | } else {
354 | // Disconnect if we're on a non-matching page
355 | if (socket) {
356 | socket.close();
357 | socket = null;
358 | }
359 | }
360 | }
361 |
362 | async function checkExistingTabs() {
363 | const tabs = await chrome.tabs.query({active: true});
364 | if (tabs[0]) {
365 | handleTabUrl(tabs[0].url);
366 | }
367 | }
368 |
369 | chrome.runtime.onInstalled.addListener(() => {
370 | checkExistingTabs();
371 | });
--------------------------------------------------------------------------------
/content.js:
--------------------------------------------------------------------------------
1 | // content.js
2 |
3 | async function initializeMentionExtension(inputField) {
4 | inputField.classList.add('mention-extension-enabled');
5 |
6 | // Add single keyboard event listeners that delegate to menu handler
7 | inputField.addEventListener('keydown', (event) => {
8 | handleMenuKeyboardEvents(event, inputField);
9 | }, { capture: true, passive: false });
10 |
11 | inputField.addEventListener('keyup', (event) => {
12 | handleMenuKeyboardEvents(event, inputField);
13 | }, true);
14 |
15 | // Close context menu on click outside
16 | document.addEventListener('click', (event) => {
17 | const menu = document.getElementById('mention-context-menu');
18 | const isAddContextButton = event.target.classList.contains('add-context-btn');
19 |
20 | if (menu && !menu.contains(event.target) && !isAddContextButton) {
21 | removeContextMenu();
22 | }
23 | });
24 | }
25 |
26 | let inputFieldParentContainer;
27 |
28 | // Wrap the observer initialization in a function
29 | function initializeObservers() {
30 | const observer = new MutationObserver(() => {
31 | const selectors = getSelectors();
32 | if (!selectors) return;
33 |
34 | // Add button to input container
35 | const inputFieldContainer = getInputFieldContainer();
36 |
37 | if (inputFieldContainer) {
38 | const chipsContainer = createChipsContainer(inputFieldContainer);
39 | addContextButton(chipsContainer);
40 | addSettingsButton(chipsContainer);
41 | } else {
42 | console.log("no found");
43 | }
44 |
45 | const inputField = document.querySelector(selectors.inputField);
46 | if (inputField && !inputField.classList.contains('mention-extension-enabled')) {
47 | initializeMentionExtension(inputField);
48 | }
49 | });
50 |
51 | // Code block observer
52 | const codeBlockObserver = new MutationObserver((mutations) => {
53 | const platform = getCurrentPlatform();
54 | if (!platform) return;
55 |
56 | const selectors = platform.selectors;
57 | const codeBlockSelector = selectors.codeBlock;
58 |
59 | mutations.forEach(mutation => {
60 | if (mutation.type === 'childList') {
61 | mutation.addedNodes.forEach(node => {
62 | if (node.nodeType === Node.ELEMENT_NODE) {
63 | // Check for code blocks using platform-specific selector
64 | if (node.matches(codeBlockSelector)) {
65 | addCodeBlockButton(
66 | node,
67 | platform === PLATFORMS.CHATGPT ? 'chatgpt' : 'claude'
68 | );
69 | }
70 |
71 | // Also check children using platform-specific selector
72 | const codeBlocks = node.querySelectorAll(codeBlockSelector);
73 | codeBlocks.forEach(codeBlock => {
74 | addCodeBlockButton(
75 | codeBlock,
76 | platform === PLATFORMS.CHATGPT ? 'chatgpt' : 'claude'
77 | );
78 | });
79 | }
80 | });
81 | }
82 | });
83 | });
84 |
85 | // Function to start observers
86 | function startObservers() {
87 | if (document.body) {
88 | observer.observe(document.body, {
89 | childList: true,
90 | subtree: true
91 | });
92 |
93 | codeBlockObserver.observe(document.body, {
94 | childList: true,
95 | subtree: true
96 | });
97 | }
98 | }
99 |
100 | // Check document readiness and initialize accordingly
101 | if (document.readyState === 'loading') {
102 | document.addEventListener('DOMContentLoaded', startObservers);
103 | } else {
104 | startObservers();
105 | }
106 |
107 | // Backup timeout in case DOMContentLoaded already fired
108 | setTimeout(startObservers, 1000);
109 | }
110 |
111 | // Initialize everything
112 | initializeObservers();
113 |
114 | // Initialize code block buttons with proper timing
115 | function initializeCodeBlockButtons() {
116 | //addButtonsToCodeBlocks();
117 | setTimeout(addButtonsToCodeBlocks, 1000);
118 | //setInterval(addButtonsToCodeBlocks, 2000);
119 | }
120 |
121 | // Start the code block button initialization
122 | if (document.readyState === 'loading') {
123 | console.log("page still loading, wait for DOM load");
124 | document.addEventListener('DOMContentLoaded', initializeCodeBlockButtons);
125 | } else {
126 | console.log("page loaded, attempting to add buttons");
127 | initializeCodeBlockButtons();
128 | }
129 |
130 | // Insert mention content
131 | async function insertMentionContent(inputField, suggestion) {
132 | const container = createChipsContainer(inputField);
133 |
134 | // Check if chip already exists for this file
135 | const existingChips = container.getElementsByClassName('file-chip');
136 | let hasChipAlready = false;
137 | for (const chip of existingChips) {
138 | if (chip.getAttribute('data-file') === suggestion.label) {
139 | hasChipAlready = true;
140 | console.log("already has chip");
141 | // Chip already exists, don't add duplicate
142 | } else {
143 | console.log("NOT has chip");
144 | }
145 | }
146 |
147 | if (hasChipAlready) {
148 |
149 | } else {
150 | const chip = createFileChip(suggestion);
151 | container.appendChild(chip);
152 | }
153 |
154 | if (!(suggestion.label in fileContentCache) && suggestion.type !== 'folder') {
155 | getFileContents(suggestion.label)
156 | .then(content => {
157 | fileContentCache[suggestion.label] = content;
158 | })
159 | .catch(error => {
160 | console.error('Error caching file content:', error);
161 | });
162 | }
163 |
164 | // Clean up '>' character and add file name
165 | const currentText = inputField.value || inputField.innerText;
166 | if (currentText.endsWith('>')) {
167 | const newText = currentText.slice(0, -1) + `file: ${suggestion.label} `;
168 | if (inputField.value !== undefined) {
169 | inputField.value = newText;
170 | } else {
171 | inputField.innerText = newText;
172 | }
173 | }
174 |
175 | // Set cursor position
176 | const range = document.createRange();
177 | const selection = window.getSelection();
178 | range.selectNodeContents(inputField);
179 | range.collapse(false);
180 | selection.removeAllRanges();
181 | selection.addRange(range);
182 |
183 | removeContextMenu();
184 | }
185 |
186 | // Get input field
187 | function getInputField() {
188 | const selectors = getSelectors();
189 | return document.querySelector(selectors.inputField);
190 | }
191 |
192 | function addContextButton(inputFieldContainer) {
193 | // Check if button already exists
194 | if (inputFieldContainer.querySelector('.add-context-btn')) {
195 | return;
196 | }
197 |
198 | // Create button
199 | const button = document.createElement('button');
200 | button.className = 'add-context-btn';
201 | button.innerHTML = '+ Add Context';
202 | button.style.cssText = `
203 | top: 10px;
204 | left: 10px;
205 | padding: 6px 12px;
206 | background-color: #333;
207 | color: #fff;
208 | border: 1px solid #555;
209 | border-radius: 15px;
210 | cursor: pointer;
211 | font-size: 12px;
212 | z-index: 1000;
213 | `;
214 |
215 | // Add hover effect
216 | button.addEventListener('mouseover', () => {
217 | button.style.backgroundColor = '#444';
218 | });
219 | button.addEventListener('mouseout', () => {
220 | button.style.backgroundColor = '#333';
221 | });
222 |
223 | // Add click handler
224 | button.addEventListener('click', () => {
225 | const inputField = getInputField();
226 | if (inputField) {
227 | // Simulate typing '>' by creating and showing the file menu
228 |
229 | const selection = window.getSelection();
230 | const range = selection.getRangeAt(0);
231 | showContextMenu(getInputFieldContainer(), range, "");
232 | }
233 | });
234 |
235 | // Insert as first child
236 | inputFieldContainer.prepend(button);
237 | }
238 |
239 | function addSettingsButton(inputFieldContainer) {
240 | // Check if link already exists
241 | if (inputFieldContainer.querySelector('.settings-link')) {
242 | return;
243 | }
244 |
245 | // Create container for right alignment
246 | const linkContainer = document.createElement('div');
247 | linkContainer.style.cssText = `
248 | display: flex;
249 | justify-content: flex-end;
250 | position: absolute;
251 | right: 10px;
252 | z-index: 1000;
253 | `;
254 |
255 | // Create settings link
256 | const link = document.createElement('span');
257 | link.className = 'settings-link';
258 | link.innerHTML = 'EasyCode Settings';
259 | link.style.cssText = `
260 | color: #888;
261 | font-size: 11px;
262 | cursor: pointer;
263 | padding: 4px 8px;
264 | opacity: 1;
265 | transition: opacity 0.2s, color 0.2s;
266 | font-family: Arial, sans-serif;
267 | `;
268 |
269 | // Add hover effect
270 | link.addEventListener('mouseover', () => {
271 | link.style.opacity = '1';
272 | link.style.color = '#aaa';
273 | });
274 | link.addEventListener('mouseout', () => {
275 | link.style.opacity = '1';
276 | link.style.color = '#888';
277 | });
278 |
279 | // Add click handler to open options page
280 | link.addEventListener('click', () => {
281 | chrome.runtime.sendMessage({ action: 'openOptions' });
282 | });
283 |
284 | // Add link to container and container to inputFieldContainer
285 | linkContainer.appendChild(link);
286 | inputFieldContainer.appendChild(linkContainer);
287 |
288 | // Platform-specific adjustments
289 | const platform = getCurrentPlatform();
290 | if (platform === PLATFORMS.CHATGPT) {
291 | linkContainer.style.top = '8px';
292 | linkContainer.style.right = '8px';
293 | } else if (platform === PLATFORMS.CLAUDE) {
294 | linkContainer.style.top = '10px';
295 | linkContainer.style.right = '10px';
296 | }
297 | }
--------------------------------------------------------------------------------
/contextMenu.js:
--------------------------------------------------------------------------------
1 | let currentMenuIndex = 0; // Track the currently highlighted menu item
2 |
3 | function shouldShowContextMenu(text, index) {
4 | // Extract text before and after the '>' character
5 | const textBeforeArrow = text.slice(0, index);
6 | const textAfterArrow = text.slice(index + 1);
7 |
8 | // Check if there's no non-whitespace character after '>'
9 | const noTrailingString = !/\S/.test(textAfterArrow);
10 |
11 | // Check if the '>' is at the start or immediately preceded by a whitespace
12 | const hasWhiteSpaceImmediatelyBefore =
13 | index === 0 || /\s/.test(text[index - 1]);
14 |
15 | // Decide whether to show the context menu
16 | return noTrailingString && hasWhiteSpaceImmediatelyBefore;
17 | }
18 |
19 | // Add this function to handle all menu-related keyboard events
20 | function handleMenuKeyboardEvents(event, inputField) {
21 | const menu = document.getElementById('mention-context-menu');
22 | const inputFieldContainer = getInputFieldContainer();
23 |
24 | // Handle keydown events when menu is open
25 | if (event.type === 'keydown' && menu) {
26 | if (event.key === 'Enter') {
27 | console.log("captured enter inside mention menu");
28 | event.preventDefault();
29 | event.stopPropagation();
30 | event.stopImmediatePropagation();
31 |
32 | const menuItems = document.querySelectorAll('.mention-menu-item');
33 | if (menuItems.length > 0 && currentMenuIndex >= 0) {
34 | const selectedItem = menuItems[currentMenuIndex];
35 | const suggestionLabel = selectedItem.innerText;
36 |
37 | getSuggestions('').then(suggestions => {
38 | const suggestion = suggestions.find(s => s.label === suggestionLabel);
39 | if (selectedItem && suggestion) {
40 | insertMentionContent(inputField, suggestion);
41 | removeContextMenu();
42 | }
43 | });
44 | }
45 | return false;
46 | }
47 | }
48 |
49 | // Handle keyup events
50 | if (event.type === 'keyup') {
51 | // Check if menu is open first for navigation
52 | if (menu && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
53 | console.log(`Navigating menu with key: ${event.key}`);
54 | event.preventDefault();
55 | event.stopPropagation();
56 | navigateMenu(event.key);
57 | return;
58 | }
59 |
60 | if (event.key === ">") {
61 | // Get the current selection
62 | const selection = window.getSelection();
63 | const range = selection.getRangeAt(0);
64 | const currentNode = range.startContainer;
65 |
66 | // Check for context menu trigger
67 | const textContent = currentNode.textContent;
68 | const cursorPosition = range.startOffset;
69 | const textBeforeCursor = textContent.slice(0, cursorPosition);
70 |
71 | const lastIndex = textBeforeCursor.lastIndexOf('>');
72 | const shouldShowFileSuggestions = shouldShowContextMenu(textBeforeCursor, lastIndex);
73 |
74 | if (lastIndex !== -1 && shouldShowFileSuggestions) {
75 | const query = textBeforeCursor.slice(lastIndex + 1);
76 | showContextMenu(getInputFieldContainer(), range, query.trim());
77 | } else {
78 | removeContextMenu();
79 | }
80 | }
81 | }
82 | }
83 |
84 | function navigateMenu(direction, menuItems) {
85 | if (menuItems.length === 0) return;
86 |
87 | // Clear previous highlight
88 | if (currentMenuIndex >= 0 && menuItems[currentMenuIndex]) {
89 | menuItems[currentMenuIndex].classList.remove('highlighted');
90 | }
91 |
92 | // Update the index based on the direction
93 | if (direction === 'ArrowDown') {
94 | currentMenuIndex = (currentMenuIndex + 1) % menuItems.length;
95 | } else if (direction === 'ArrowUp') {
96 | currentMenuIndex = (currentMenuIndex - 1 + menuItems.length) % menuItems.length;
97 | }
98 |
99 | // Highlight the current menu item
100 | const currentItem = menuItems[currentMenuIndex];
101 | currentItem.classList.add('highlighted');
102 |
103 | // Scroll the highlighted item into view
104 | currentItem.scrollIntoView({ block: 'nearest' });
105 | }
106 |
107 | function handleMenuInputKeyDown(event, menuInput, menu) {
108 | const menuItems = menu.querySelectorAll('.mention-menu-item');
109 |
110 | if (event.key === 'ArrowDown') {
111 | event.preventDefault();
112 | navigateMenu('ArrowDown', menuItems);
113 | } else if (event.key === 'ArrowUp') {
114 | event.preventDefault();
115 | navigateMenu('ArrowUp', menuItems);
116 | } else if (event.key === 'Enter') {
117 | console.log("Captured Enter inside FileMenu")
118 | event.preventDefault();
119 | if (currentMenuIndex >= 0 && menuItems[currentMenuIndex]) {
120 | const selectedItem = menuItems[currentMenuIndex];
121 | const suggestionLabel = selectedItem.innerText;
122 | getSuggestions('').then((suggestions) => {
123 | const suggestion = suggestions.find(s => s.label === suggestionLabel);
124 | if (suggestion) {
125 | insertMentionContent(document.querySelector(getSelectors().inputField), suggestion);
126 | //removeContextMenu();
127 | }
128 | });
129 | }
130 | } else if (event.key === 'Escape') {
131 | console.log("Captured Escape inside FileMenu")
132 | removeContextMenu();
133 | }
134 | }
135 |
136 | // Add this helper function to check WebSocket connection
137 | async function isWebSocketConnected() {
138 | // Send a message to background script to check connection status
139 | return new Promise(resolve => {
140 | chrome.runtime.sendMessage({ type: 'CHECK_CONNECTION' }, response => {
141 | resolve(response?.connected || false);
142 | });
143 | });
144 | }
145 |
146 | async function showContextMenu(inputField, range, query) {
147 | removeContextMenu();
148 |
149 | const menu = document.createElement('div');
150 | menu.id = 'mention-context-menu';
151 | document.body.appendChild(menu);
152 |
153 | // Position the menu
154 | const inputRect = inputField.getBoundingClientRect();
155 | const menuTop = inputRect.top;
156 | const menuLeft = inputRect.left;
157 |
158 | menu.style.setProperty('--menu-top', `${menuTop}px`);
159 | menu.style.left = `${menuLeft}px`;
160 | menu.style.width = `${inputRect.width}px`;
161 | menu.style.maxHeight = '200px';
162 | menu.style.overflowY = 'auto';
163 | menu.style.border = '1px solid #ccc';
164 | menu.style.borderRadius = '15px';
165 | menu.style.zIndex = '1000';
166 |
167 | const menuInput = document.createElement('input');
168 | menuInput.type = 'text';
169 | menuInput.id = 'menu-input';
170 | menuInput.value = query;
171 | menuInput.placeholder = 'Search files...';
172 | menuInput.className = 'menu-input';
173 |
174 | // Create a suggestions container
175 | const suggestionsContainer = document.createElement('div');
176 | suggestionsContainer.id = 'suggestions-container';
177 |
178 | // Append both elements to the menu
179 | menu.appendChild(menuInput);
180 | menu.appendChild(suggestionsContainer);
181 |
182 | // Initial suggestions
183 | await updateSuggestions(suggestionsContainer, query);
184 |
185 | // Add event listeners
186 | menuInput.addEventListener('input', async (event) => {
187 | currentMenuIndex = 0;
188 | await updateSuggestions(suggestionsContainer, event.target.value);
189 | });
190 |
191 | menuInput.addEventListener('keydown', (event) => {
192 | handleMenuInputKeyDown(event, menuInput, menu);
193 | });
194 |
195 | // Focus on the menu input
196 | menuInput.focus();
197 |
198 | isWebSocketConnected().then(connected => {
199 | if(!connected) {
200 | // remove
201 | menu.removeChild(menuInput);
202 | menu.removeChild(suggestionsContainer);
203 |
204 | // Create a message container for the disconnected state
205 | const disconnectedMessage = document.createElement('div');
206 | disconnectedMessage.style.cssText = `
207 | height: 100%;
208 | padding: 16px;
209 | background: #f8f9fa;
210 | color: #333;
211 | font-size: 13px;
212 | line-height: 1.5;
213 | `;
214 |
215 | // Create message text
216 | const messageText = document.createElement('p');
217 | messageText.style.margin = '0 0 12px 0';
218 | messageText.innerHTML = `Error: Failed to retrieve files.
219 |
I don't see any files. Make sure VS Code is running, and your project is opened inside VS Code. Try to refresh the file cache first. If that doesn't work, try restarting VS Code. As a last resort, reinstall the chrome extension.
126 |
Nothing happens after I click "Apply Change". Make sure you are logged into EasyCode inside VS Code. This is required to apply changes. It's free to use.
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
Files
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
FAQ
152 |
153 |
Is my code stored or shared? No. The chrome extension simply fetches file content from the file system as specified by the user. No data is stored. The code is open source and can be found here.
154 |
Why do I need to install a VS Code extension? The VS Code extension does 2 things: it watches for changes in the file system and serves the file content to the chrome extension, and it helps apply changes back to the source code. You can install the extension from the VS Code Marketplace.
155 |
Does this work with JetBrains IDEs? Not at the moment, but we are working on this. If you want to try it first, please join the waitlist here.
156 |
Have a bug or feature request? Please open an issue on GitHub.