diff --git a/package-lock.json b/package-lock.json index a6cc587..16abf4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "play-sound": "^1.1.6", "replicate": "^0.29.1", "say": "^0.16.0", + "socket.io": "^4.7.5", "wavefile": "^11.0.0" }, "devDependencies": { @@ -374,6 +375,11 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -383,6 +389,19 @@ "node": ">= 6" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -488,6 +507,18 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -696,6 +727,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1049,12 +1088,32 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1081,7 +1140,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1200,6 +1258,34 @@ "once": "^1.4.0" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.16.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", @@ -2539,6 +2625,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-abi": { "version": "3.56.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", @@ -2622,6 +2716,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ollama": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.0.tgz", @@ -3464,6 +3566,44 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/streamx": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", @@ -3810,6 +3950,14 @@ "node": ">=10.12.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/wavefile": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/wavefile/-/wavefile-11.0.0.tgz", @@ -3968,6 +4116,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index cf8f857..a9cc880 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,11 @@ "command": "bizarro-devin.manualPrompt", "title": "Manually Prompt AI Agent", "category": "Bizarro Devin" + }, + { + "command": "bizarro-devin.ignoreErrors", + "title": "Enable error ignoring for the AI Agent", + "category": "Bizarro Devin" } ], "menus": { @@ -101,6 +106,7 @@ "play-sound": "^1.1.6", "replicate": "^0.29.1", "say": "^0.16.0", + "socket.io": "^4.7.5", "wavefile": "^11.0.0" } } diff --git a/src/commands/ignoreErrors.js b/src/commands/ignoreErrors.js new file mode 100644 index 0000000..c6dd5ed --- /dev/null +++ b/src/commands/ignoreErrors.js @@ -0,0 +1,20 @@ +const { getAgent } = require('../lib/agent/Agent'); +const Command = require('../lib/command'); +const vscode = require('vscode'); + +class IgnoreErrorsCommand extends Command { + constructor() { + super('bizarro-devin.ignoreErrors'); + } + + async run() { + const agent = getAgent(); + agent.ignoreErrors = !agent.ignoreErrors; + + vscode.window.showInformationMessage( + 'Error ignoring is now ' + (agent.ignoreErrors ? 'enabled' : 'disabled') + ); + } +} + +module.exports = IgnoreErrorsCommand; diff --git a/src/commands/setupFiles.js b/src/commands/setupFiles.js index 160e515..7006455 100644 --- a/src/commands/setupFiles.js +++ b/src/commands/setupFiles.js @@ -10,6 +10,7 @@ class SetupFilesCommand extends Command { // create index.html await this.createIndexHtml(); // create sketch.js && opening it + await this.createErrorCatcherFile(); const doc = await createFile('sketch.js'); await doc.open(); } @@ -24,6 +25,12 @@ class SetupFilesCommand extends Command { Document + + @@ -31,6 +38,49 @@ class SetupFilesCommand extends Command { ` ); } + + async createErrorCatcherFile() { + await createFile( + 'errorCatcher.js', + `const socket = io('http://127.0.0.1:4025'); + + // Capture all errors, credits to https://github.com/processing/p5.js-web-editor/blob/develop/client%2Futils%2FpreviewEntry.js#L65 + window.onerror = async function (msg, source, lineNumber, columnNo, error) { + let data; + if (!error) { + data = msg; + } else { + data = \`\${error.name}: \${error.message}\`; + // Remove the host from the resolvedLineNo + const line = \` at \${lineNumber}:\${columnNo}\`; + data = data.concat(line); + } + + const errorData = { + msg, + source, + lineNumber, + columnNo, + error, + data, + }; + + await sendMessage(errorData, 'error'); + return false; + }; + + // Send error to backend + async function sendMessage(data, type) { + socket.emit('message', { + type, + data, + }); + } + + socket.on('reload', () => window.location.reload()); + ` + ); + } } module.exports = SetupFilesCommand; diff --git a/src/extension.js b/src/extension.js index 22d26ad..729f4c8 100644 --- a/src/extension.js +++ b/src/extension.js @@ -1,5 +1,7 @@ -const CommandLoader = require('./lib/commandLoader'); +const { getWebserver } = require('./lib/web/webserver'); +const CommandLoader = require('./lib/commandLoader'); +require('./lib/web/webserver'); // This method is called when your extension is activated // Your extension is activated the very first time the command is executed diff --git a/src/lib/agent/Agent.js b/src/lib/agent/Agent.js index 30f1b80..b7c53ed 100644 --- a/src/lib/agent/Agent.js +++ b/src/lib/agent/Agent.js @@ -1,8 +1,8 @@ +const { SocketServer } = require('../web/webserver'); const { typeRealistically } = require('../../util/realisticTyping'); const vscode = require('vscode'); const { getProvider } = require('./providers/providerInstance'); const { speak } = require('../../util/speak'); -const config = require('../../../config'); class Agent { constructor() { @@ -16,6 +16,15 @@ class Agent { this.isNewPrompt = false; this.promptingTemplate = 'Dan says: {prompt}\nCurrent code in the editor:\n```\n{currentCode}\n```'; + this.errorPromptingTemplate = + 'It appears like you have made some errors in the code. Please fix them. The errors are as follows:\n{errors}\nCurrent code in the editor:\n```\n{currentCode}\n```'; + this.isStreaming = false; + + this.receivedErrorList = []; + this.ignoreErrors = false; + + this.webserver = new SocketServer(this); + this.webserver.start(); } /** @@ -23,18 +32,35 @@ class Agent { * @param {string} input The prompt to be processed */ prompt(input) { - const editor = vscode.window.activeTextEditor; + if (this.isStreaming || this.actionsQueue.length > 0) { + return vscode.window.showErrorMessage( + 'Please wait for the current prompt to finish processing before sending another one.' + ); + } - this.isNewPrompt = true; + const editor = vscode.window.activeTextEditor; const prompt = this.promptingTemplate .replace('{prompt}', input) .replace('{currentCode}', editor.document.getText()); - this.provider.queryStream(prompt, (response) => - this.consumeStream(response) - ); + + this.processPrompt(prompt); + } + + processPrompt(prompt) { + this.isNewPrompt = true; + this.isStreaming = true; + this.provider + .queryStream(prompt, (response) => this.consumeStream(response)) + .then((out) => { + this.isStreaming = false; + this.receivedErrorList = []; // Reset the error list + if (out.blocked) { + vscode.window.showErrorMessage(`Prompt blocked: ${out.blockReason}`); + } + }); } - async consumeStream(response) { + consumeStream(response) { const text = response.response; const event = response.event; @@ -153,6 +179,9 @@ class Agent { await this.processAction(step); } this.processingQueue = false; + if (!this.isStreaming) { + this.webserver.broadcastReload(); // Trigger a browser reload for the errors to appear + } } async processAction(step) { @@ -183,6 +212,49 @@ class Agent { await speak(step.content); } } + + async receiveBrowserMessage(message) { + // If the AI is still streaming a response, we want to ignore all incoming messages. + // This is because we cannot be certain the code is in a 'finished' state as it can still be writing code. + if (this.isStreaming || this.actionsQueue.length > 0 || this.ignoreErrors) { + return; + } + + // Add formatted message to the error list + if (message.type === 'error') { + this.receivedErrorList.push( + `${message.data.msg} at line ${message.data.lineNumber} and column ${message.data.columnNo}` + ); + } + + console.log('Received error!'); + // Schedule a timeout if there isn't one, replace the existing timeout if there is one + if (this.errorTimeout) { + console.log('Cleared previous timeout'); + clearTimeout(this.errorTimeout); + } + + this.errorTimeout = setTimeout(() => { + this.triggerErrorResponse(); + }, 1000); + console.log('Set new timeout with id ' + this.errorTimeout); + } + + async triggerErrorResponse() { + // Supply a prompt to the AI to respond to the errors + const prompt = this.errorPromptingTemplate + .replace( + '{errors}', + this.receivedErrorList.map((error) => '- ' + error).join('\n') + ) + .replace( + '{currentCode}', + vscode.window.activeTextEditor.document.getText() + ); + console.log('Triggering error response!', prompt); + + this.processPrompt(prompt); + } } // Singleton instance of the agent diff --git a/src/lib/agent/providers/geminiProvider.js b/src/lib/agent/providers/geminiProvider.js index 044f328..a76a110 100644 --- a/src/lib/agent/providers/geminiProvider.js +++ b/src/lib/agent/providers/geminiProvider.js @@ -47,12 +47,23 @@ class GeminiProvider extends ModelProvider { const result = await this.chat.sendMessageStream(prompt); let fullResponse = ''; for await (const chunk of result.stream) { + if (chunk.promptFeedback?.blockReason) { + // prompt blocked + this.messageHistory.pop(); + return { + blocked: true, + blockReason: chunk.promptFeedback.blockReason, + }; + } const text = chunk.text(); fullResponse += text; await process({ response: text, event: 'output' }); } await process({ response: '', event: 'done' }); this.messageHistory.push({ role: 'assistant', content: fullResponse }); + return { + success: true, + }; } } diff --git a/src/lib/web/webserver.js b/src/lib/web/webserver.js new file mode 100644 index 0000000..fd2f03b --- /dev/null +++ b/src/lib/web/webserver.js @@ -0,0 +1,33 @@ +const { Server } = require('socket.io'); + +class SocketServer { + constructor(agent) { + this.agent = agent; + this.io = new Server({ + cors: { + origin: '*', + }, + }); + } + + start() { + this.io.listen(4025); + this.io.on('connect', (socket) => { + // When socket connects, register event listener + socket.on('message', (message) => this.handleIncomingMessage(message)); + }); + console.log('Socket server started'); + } + + handleIncomingMessage(message) { + this.agent.receiveBrowserMessage(message); + } + + broadcastReload() { + this.io.emit('reload'); + } +} + +module.exports = { + SocketServer, +};