diff --git a/AUDIO_TRANSCRIPTION_UX_IMPROVEMENT.md b/AUDIO_TRANSCRIPTION_UX_IMPROVEMENT.md new file mode 100644 index 00000000000..4d6ea28eab8 --- /dev/null +++ b/AUDIO_TRANSCRIPTION_UX_IMPROVEMENT.md @@ -0,0 +1,169 @@ +# Audio Transcription UX Improvement + +## Summary + +Modified the audio transcription behavior in the Remix AI Assistant to append transcribed text to the input box instead of immediately executing it as a prompt. This allows users to review and edit the transcription before sending. + +## Changes Made + +### File Modified +- `libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx` + +### Previous Behavior +When audio transcription completed, the transcribed text was immediately sent as a prompt to the AI assistant: + +```typescript +onTranscriptionComplete: async (text) => { + if (sendPromptRef.current) { + await sendPromptRef.current(text) + trackMatomoEvent({ category: 'ai', action: 'SpeechToTextPrompt', name: 'SpeechToTextPrompt', isClick: true }) + } +} +``` + +### New Behavior +When audio transcription completes, the behavior depends on whether the transcription ends with "run": + +**If transcription ends with "run":** The prompt is automatically executed (with "run" removed) +**If transcription does NOT end with "run":** The text is appended to the input box for review + +```typescript +onTranscriptionComplete: async (text) => { + // Check if transcription ends with "run" (case-insensitive) + const trimmedText = text.trim() + const endsWithRun = /\brun\b\s*$/i.test(trimmedText) + + if (endsWithRun) { + // Remove "run" from the end and execute the prompt + const promptText = trimmedText.replace(/\brun\b\s*$/i, '').trim() + if (promptText) { + await sendPrompt(promptText) + trackMatomoEvent({ category: 'ai', action: 'SpeechToTextPromptAutoRun', name: 'SpeechToTextPromptAutoRun', isClick: true }) + } + } else { + // Append transcription to the input box for user review + setInput(prev => prev ? `${prev} ${text}`.trim() : text) + // Focus the textarea so user can review/edit + if (textareaRef.current) { + textareaRef.current.focus() + } + trackMatomoEvent({ category: 'ai', action: 'SpeechToTextPrompt', name: 'SpeechToTextPrompt', isClick: true }) + } +} +``` + +### Code Cleanup +Removed unused code that was only needed for the previous immediate execution behavior: + +1. **Removed ref declaration:** + ```typescript + // Ref to hold the sendPrompt function for audio transcription callback + const sendPromptRef = useRef<((prompt: string) => Promise) | null>(null) + ``` + +2. **Removed useEffect that updated the ref:** + ```typescript + // Update ref for audio transcription callback + useEffect(() => { + sendPromptRef.current = sendPrompt + }, [sendPrompt]) + ``` + +## Benefits + +1. **User Control:** Users can now review and edit the transcription before sending +2. **Error Correction:** If the speech-to-text makes mistakes, users can fix them +3. **Better UX:** Users can append multiple transcriptions or combine voice with typing +4. **Flexibility:** Transcriptions can be modified to add context or clarification +5. **Voice Command Execution:** Users can say "run" at the end to immediately execute the prompt +6. **Hands-free Operation:** The "run" command enables completely hands-free prompt execution + +## User Flow + +### Standard Flow (Without "run") +1. User clicks the microphone button to start recording +2. User speaks their prompt (e.g., "Explain how to create an ERC-20 token") +3. User clicks the microphone button again to stop recording +4. **Transcribing status** is shown while processing +5. **Transcribed text appears in the input box** (NEW) +6. Input textarea is automatically focused (NEW) +7. User can review, edit, or append to the transcription (NEW) +8. User clicks send button or presses Enter to submit the prompt + +### Auto-Execute Flow (With "run") +1. User clicks the microphone button to start recording +2. User speaks their prompt ending with "run" (e.g., "Explain how to create an ERC-20 token run") +3. User clicks the microphone button again to stop recording +4. **Transcribing status** is shown while processing +5. **Prompt is automatically executed** with "run" removed (NEW) +6. AI response begins streaming immediately (hands-free execution) + +## Implementation Details + +### "Run" Detection Logic +The implementation uses a word-boundary regex to detect if the transcription ends with "run": + +```typescript +const endsWithRun = /\brun\b\s*$/i.test(trimmedText) +``` + +Key features: +- **Case-insensitive:** Matches "run", "Run", "RUN", etc. +- **Word boundary:** Only matches "run" as a complete word, not as part of another word +- **Trailing whitespace:** Ignores any spaces after "run" + +Examples: +- ✅ "Explain ERC-20 tokens run" → Auto-executes +- ✅ "Help me debug this run" → Auto-executes +- ✅ "Create a contract RUN" → Auto-executes (case-insensitive) +- ❌ "Explain running contracts" → Does NOT auto-execute (word boundary) +- ❌ "Tell me about runtime" → Does NOT auto-execute (word boundary) + +### Smart Text Appending +The implementation intelligently handles existing input (when NOT auto-executing): +- If input is empty: Sets the transcription as the input +- If input exists: Appends the transcription with a space separator +- Always trims whitespace for clean formatting + +```typescript +setInput(prev => prev ? `${prev} ${text}`.trim() : text) +``` + +### Auto-focus +After transcription (when NOT auto-executing), the textarea is automatically focused so the user can immediately start editing: + +```typescript +if (textareaRef.current) { + textareaRef.current.focus() +} +``` + +## Testing Recommendations + +### Standard Transcription (Without "run") +1. Test basic transcription flow - text appears in input box +2. Test appending multiple transcriptions +3. Test transcription with existing text in input +4. Test keyboard navigation after transcription +5. Test error handling (transcription failures) +6. Verify textarea focus behavior + +### Auto-Execute with "run" +1. Test transcription ending with "run" - should auto-execute +2. Test case-insensitivity - "run", "Run", "RUN" should all work +3. Test word boundary - "running" or "runtime" should NOT trigger auto-execute +4. Test "run" removal - verify the word "run" is removed from the prompt +5. Test empty prompt after "run" removal - should not execute +6. Verify prompt execution starts immediately after transcription + +### Edge Cases +1. Test "run" with trailing spaces - "prompt run " should work +2. Test "run" as the only word - should not execute (empty prompt) +3. Test transcription with "run" in the middle - "run a test" should NOT auto-execute +4. Test multiple spaces before "run" - "prompt run" should work + +## Related Files + +- Main component: `libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx` +- Transcription hook: `libs/remix-ui/remix-ai-assistant/src/hooks/useAudioTranscription.tsx` +- Prompt input area: `libs/remix-ui/remix-ai-assistant/src/components/prompt.tsx` diff --git a/FOUNDRY_HARDHAT_COMMAND_IMPLEMENTATION.md b/FOUNDRY_HARDHAT_COMMAND_IMPLEMENTATION.md new file mode 100644 index 00000000000..8f3f0ffa887 --- /dev/null +++ b/FOUNDRY_HARDHAT_COMMAND_IMPLEMENTATION.md @@ -0,0 +1,286 @@ +# Foundry and Hardhat Command Execution Implementation + +## Summary + +This implementation adds comprehensive support for executing Foundry and Hardhat commands through the Remix MCP Server, allowing AI agents to interact with these development frameworks. + +## Changes Made + +### 1. Plugin Extensions + +#### Foundry Plugin (`apps/remixdesktop/src/plugins/foundryPlugin.ts`) +- **Added method:** `runCommand(commandArgs: string)` + - Executes any Foundry command (forge, cast, anvil) + - Validates commands to ensure they start with allowed tools + - Captures stdout and stderr + - Logs output to Remix terminal + - Returns exit code and output + +- **Updated methods list:** Added `'runCommand'` to the profile + +#### Hardhat Plugin (`apps/remixdesktop/src/plugins/hardhatPlugin.ts`) +- **Added method:** `runCommand(commandArgs: string)` + - Executes any Hardhat command + - Validates commands to ensure they are Hardhat commands + - Captures stdout and stderr + - Logs output to Remix terminal + - Returns exit code and output + +- **Updated methods list:** Added `'runCommand'` to the profile + +### 2. MCP Server Handlers + +#### New Handlers in `FoundryHardhatHandler.ts` + +**FoundryRunCommandHandler** +- Tool name: `foundry_run_command` +- Executes any Foundry command (forge, cast, anvil) +- Validates command format +- Returns execution results including stdout, stderr, and exit code + +**HardhatRunCommandHandler** +- Tool name: `hardhat_run_command` +- Executes any Hardhat command +- Validates command format (must be hardhat or npx hardhat) +- Returns execution results including stdout, stderr, and exit code + +#### Updated Handlers + +**GetFoundryHardhatInfoHandler** +- Added comprehensive information about command execution tools +- Included example commands for various operations: + - Testing (forge test, npx hardhat test) + - Deployment scripts (forge script, npx hardhat run) + - Contract interaction (cast commands) + - Local nodes (anvil, npx hardhat node) + +### 3. Tool Registration + +Updated `createFoundryHardhatTools()` to include: +- `foundry_run_command` - Execute any Foundry command +- `hardhat_run_command` - Execute any Hardhat command + +Total tools: 7 (up from 5) + +## Available Commands + +### Foundry Commands + +**Testing:** +- `forge test` - Run all tests +- `forge test -vvv` - Verbose test output +- `forge test --match-test testName` - Run specific test +- `forge test --match-contract ContractName` - Run tests for specific contract + +**Scripts:** +- `forge script scripts/Deploy.s.sol` - Run deployment script +- `forge script scripts/Deploy.s.sol --rpc-url $URL --broadcast` - Deploy and broadcast + +**Contract Interaction (Cast):** +- `cast call
"method(args)" ` - Call contract method +- `cast send
"method(args)" ` - Send transaction +- `cast balance
` - Check balance + +**Development:** +- `anvil` - Start local Ethereum node +- `forge build` - Build contracts +- `forge clean` - Clean build artifacts + +### Hardhat Commands + +**Testing:** +- `npx hardhat test` - Run all tests +- `npx hardhat test --grep "pattern"` - Run specific tests +- `npx hardhat coverage` - Generate coverage report + +**Deployment:** +- `npx hardhat run scripts/deploy.js` - Run deployment script +- `npx hardhat run scripts/deploy.js --network ` - Deploy to specific network + +**Verification:** +- `npx hardhat verify --network
` - Verify contract + +**Development:** +- `npx hardhat node` - Start local Hardhat node +- `npx hardhat console` - Interactive console +- `npx hardhat accounts` - List accounts +- `npx hardhat compile` - Compile contracts +- `npx hardhat clean` - Clean artifacts + +## Security Features + +### Command Validation + +**Foundry:** +- Commands MUST start with: `forge`, `cast`, or `anvil` +- Validation occurs at both handler and plugin level +- Invalid commands are rejected with clear error messages + +**Hardhat:** +- Commands MUST be Hardhat commands +- Must start with `hardhat` or `npx hardhat` +- Validation occurs at both handler and plugin level +- Invalid commands are rejected with clear error messages + +### Execution Security + +- Commands execute in the current working directory only +- stdout and stderr are captured and logged +- Exit codes are returned for error handling +- No arbitrary shell commands allowed +- Shell execution is scoped to validated framework commands + +## Usage Examples + +### Running Tests + +**Foundry:** +```json +{ + "name": "foundry_run_command", + "arguments": { + "command": "forge test -vvv" + } +} +``` + +**Hardhat:** +```json +{ + "name": "hardhat_run_command", + "arguments": { + "command": "npx hardhat test --grep 'MyTest'" + } +} +``` + +### Running Deployment Scripts + +**Foundry:** +```json +{ + "name": "foundry_run_command", + "arguments": { + "command": "forge script scripts/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast" + } +} +``` + +**Hardhat:** +```json +{ + "name": "hardhat_run_command", + "arguments": { + "command": "npx hardhat run scripts/deploy.js --network localhost" + } +} +``` + +### Contract Interaction (Foundry Cast) + +```json +{ + "name": "foundry_run_command", + "arguments": { + "command": "cast call 0x123... \"balanceOf(address)\" 0xabc..." + } +} +``` + +### Starting Local Nodes + +**Foundry (Anvil):** +```json +{ + "name": "foundry_run_command", + "arguments": { + "command": "anvil" + } +} +``` + +**Hardhat:** +```json +{ + "name": "hardhat_run_command", + "arguments": { + "command": "npx hardhat node" + } +} +``` + +## Response Format + +Success response includes: +- `success`: true/false +- `message`: Description of what was executed +- `command`: The command that was run +- `exitCode`: Exit code from the command +- `stdout`: Standard output +- `stderr`: Standard error output + +Example: +```json +{ + "success": true, + "message": "Foundry command executed successfully: forge test", + "command": "forge test", + "exitCode": 0, + "stdout": "Running 10 tests...\nAll tests passed!", + "stderr": "" +} +``` + +## Files Modified/Created + +### Created: +- `libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts` (initial creation in previous PR) +- `libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.README.md` +- `FOUNDRY_HARDHAT_COMMAND_IMPLEMENTATION.md` (this file) + +### Modified: +- `apps/remixdesktop/src/plugins/foundryPlugin.ts` + - Added `runCommand` method + - Updated profile methods list + +- `apps/remixdesktop/src/plugins/hardhatPlugin.ts` + - Added `runCommand` method + - Updated profile methods list + +- `libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts` + - Added `FoundryRunCommandHandler` class + - Added `HardhatRunCommandHandler` class + - Updated `GetFoundryHardhatInfoHandler` to include command execution info + - Updated `createFoundryHardhatTools()` to register new handlers + +- `libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.README.md` + - Added documentation for new command handlers + - Added security section + - Added usage examples + +## Benefits + +1. **Flexibility:** AI agents can now execute any Foundry or Hardhat command, not just compile +2. **Testing:** Full test suite execution with custom flags and filters +3. **Deployment:** Run deployment scripts with network configurations +4. **Contract Interaction:** Use Cast to interact with deployed contracts +5. **Development:** Start local nodes, run console, and more +6. **Security:** Command validation prevents arbitrary code execution +7. **Observability:** All output is logged to Remix terminal for visibility + +## Integration with Remix IDE + +The implementation seamlessly integrates with Remix IDE: +- Terminal output shows command execution in real-time +- File watchers sync compilation artifacts automatically +- Working directory context maintained across commands +- Plugin architecture ensures clean separation of concerns + +## Future Enhancements + +Potential improvements: +- Add timeout configuration for long-running commands +- Support for command cancellation +- Better handling of interactive commands +- Command history and replay functionality +- Preset command templates for common operations diff --git a/apps/remix-ide/src/app/files/foundry-handle.js b/apps/remix-ide/src/app/files/foundry-handle.js index 94efd7b04b0..69b2a060a75 100644 --- a/apps/remix-ide/src/app/files/foundry-handle.js +++ b/apps/remix-ide/src/app/files/foundry-handle.js @@ -5,7 +5,7 @@ const profile = { name: 'foundry', displayName: 'Foundry', url: 'ws://127.0.0.1:65525', - methods: ['sync'], + methods: ['compile', 'sync'], description: 'Using Remixd daemon, allow to access foundry API', kind: 'other', version: packageJson.version diff --git a/apps/remix-ide/src/app/tabs/locales/en/solidity.json b/apps/remix-ide/src/app/tabs/locales/en/solidity.json index 54ac22b886a..63d87bedc48 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/solidity.json +++ b/apps/remix-ide/src/app/tabs/locales/en/solidity.json @@ -10,9 +10,11 @@ "solidity.downloadedCompilers": "Show downloaded only", "solidity.autoCompile": "Auto compile", "solidity.hideWarnings": "Hide warnings", - "solidity.enableHardhat": "Enable Hardhat Compilation", + "solidity.enableHardhat": "Compile with Hardhat", "solidity.learnHardhat": "Learn how to use Hardhat Compilation", - "solidity.enableTruffle": "Enable Truffle Compilation", + "solidity.learnFoundry": "Learn how to use Foundry Compilation", + "solidity.enableTruffle": "Compile with Truffle", + "solidity.enableFoundry": "Compile with Foundry", "solidity.learnTruffle": "Learn how to use Truffle Compilation", "solidity.advancedConfigurations": "Advanced Configurations", "solidity.compilerConfiguration": "Compiler configuration", diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 71a24c58b8c..50dc9020270 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -25,7 +25,7 @@ const clientProfile: Profile = { name: 'foundry', displayName: 'electron foundry', description: 'electron foundry', - methods: ['sync', 'compile'] + methods: ['sync', 'compile', 'runCommand'] } @@ -40,9 +40,7 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { async onActivation(): Promise { console.log('Foundry plugin activated') - this.call('terminal', 'log', { type: 'log', value: 'Foundry plugin activated' }) this.on('fs' as any, 'workingDirChanged', async (path: string) => { - console.log('workingDirChanged foundry', path) this.currentSharedFolder = path this.startListening() }) @@ -53,120 +51,84 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { startListening() { this.buildPath = utils.absolutePath('out', this.currentSharedFolder) this.cachePath = utils.absolutePath('cache', this.currentSharedFolder) - console.log('Foundry plugin checking for', this.buildPath, this.cachePath) - if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { - this.listenOnFoundryCompilation() - } else { - this.listenOnFoundryFolder() - } + this.on('fileManager', 'currentFileChanged', async (currentFile: string) => { + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + this.emitContract(basename(currentFile), cache) + }) + this.listenOnFoundryCompilation() } - listenOnFoundryFolder() { - console.log('Foundry out folder doesn\'t exist... waiting for the compilation.') + listenOnFoundryCompilation() { try { if (this.watcher) this.watcher.close() - this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true }) - // watch for new folders - this.watcher.on('addDir', (path: string) => { - console.log('add dir foundry', path) - if (fs.existsSync(this.buildPath) && fs.existsSync(this.cachePath)) { - this.listenOnFoundryCompilation() - } + this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) + this.watcher.on('change', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + this.emitContract(basename(currentFile), cache) + }) + this.watcher.on('add', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + this.emitContract(basename(currentFile), cache) }) } catch (e) { console.log(e) } } - + compile() { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const cmd = `forge build` + this.call('terminal', 'log', { type: 'log', value: `running ${cmd}` }) const options = { cwd: this.currentSharedFolder, shell: true } const child = spawn(cmd, options) - let result = '' let error = '' - child.stdout.on('data', (data) => { - const msg = `[Foundry Compilation]: ${data.toString()}` - console.log('\x1b[32m%s\x1b[0m', msg) - result += msg + '\n' + child.stdout.on('data', async (data) => { + if (data.toString().includes('Error')) { + this.call('terminal', 'log', { type: 'error', value: `${data.toString()}` }) + } else { + const msg = `${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + this.call('terminal', 'log', { type: 'log', value: msg }) + } }) child.stderr.on('data', (err) => { - error += `[Foundry Compilation]: ${err.toString()} \n` + error += err.toString() + '\n' + this.call('terminal', 'log', { type: 'error', value: `${err.toString()}` }) }) - child.on('close', () => { - if (error && result) resolve(error + result) - else if (error) reject(error) - else resolve(result) + child.on('close', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + this.emitContract(basename(currentFile), cache) + resolve('') }) }) } - - checkPath() { - if (!fs.existsSync(this.buildPath) || !fs.existsSync(this.cachePath)) { - this.listenOnFoundryFolder() - return false - } - if (!fs.existsSync(join(this.cachePath, 'solidity-files-cache.json'))) return false - return true - } - - private async processArtifact() { - if (!this.checkPath()) return - const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder + + private async emitContract(file: string, cache) { try { - const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) - // name of folders are file names - for (const file of folderFiles) { - const path = join(this.buildPath, file) // out/Counter.sol/ - const compilationResult = { - input: {}, - output: { - contracts: {}, - sources: {} - }, - inputSources: { sources: {}, target: '' }, - solcVersion: null, - compilationTarget: null - } - compilationResult.inputSources.target = file - await this.readContract(path, compilationResult, cache) - this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) + const path = join(this.buildPath, file) // out/Counter.sol/ + const compilationResult = { + input: {}, + output: { + contracts: {}, + sources: {} + }, + inputSources: { sources: {}, target: '' }, + solcVersion: null, + compilationTarget: null } - - clearTimeout(this.logTimeout) - this.logTimeout = setTimeout(() => { - // @ts-ignore - this.call('terminal', 'log', { type: 'log', value: `receiving compilation result from Foundry. Select a file to populate the contract interaction interface.` }) - console.log('Syncing compilation result from Foundry') - }, 1000) - + compilationResult.inputSources.target = file + if (!fs.existsSync(path)) return + await this.readContract(path, compilationResult, cache) + this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) } catch (e) { - console.log(e) + console.log('Error emitting contract', e) } } - async triggerProcessArtifact() { - // prevent multiple calls - clearTimeout(this.processingTimeout) - this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) - } - - listenOnFoundryCompilation() { - try { - console.log('Foundry out folder exists... processing the artifact.') - if (this.watcher) this.watcher.close() - this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) - this.watcher.on('change', async () => await this.triggerProcessArtifact()) - this.watcher.on('add', async () => await this.triggerProcessArtifact()) - this.watcher.on('unlink', async () => await this.triggerProcessArtifact()) - // process the artifact on activation - this.triggerProcessArtifact() - } catch (e) { - console.log(e) - } - } - - async readContract(contractFolder, compilationResultPart, cache) { + async readContract(contractFolder, compilationResultPart, cache) { const files = await fs.promises.readdir(contractFolder) for (const file of files) { const path = join(contractFolder, file) @@ -235,13 +197,61 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { bytecode: contentJSON.bytecode, deployedBytecode: contentJSON.deployedBytecode, methodIdentifiers: contentJSON.methodIdentifiers - } + }, + metadata: contentJSON.metadata } } async sync() { console.log('syncing Foundry with Remix...') - this.processArtifact() + const currentFile = await this.call('fileManager', 'getCurrentFile') + const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + this.emitContract(basename(currentFile), cache) + } + + runCommand(commandArgs: string) { + return new Promise((resolve, reject) => { + // Validate that the command starts with allowed Foundry commands + const allowedCommands = ['forge', 'cast', 'anvil'] + const commandParts = commandArgs.trim().split(' ') + const baseCommand = commandParts[0] + + if (!allowedCommands.includes(baseCommand)) { + reject(new Error(`Command must start with one of: ${allowedCommands.join(', ')}`)) + return + } + + const cmd = commandArgs + this.call('terminal', 'log', { type: 'log', value: `running ${cmd}` }) + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let stdout = '' + let stderr = '' + + child.stdout.on('data', (data) => { + const output = data.toString() + stdout += output + this.call('terminal', 'log', { type: 'log', value: output }) + }) + + child.stderr.on('data', (err) => { + const output = err.toString() + stderr += output + this.call('terminal', 'log', { type: 'error', value: output }) + }) + + child.on('close', (code) => { + if (code === 0) { + resolve({ stdout, stderr, exitCode: code }) + } else { + reject(new Error(`Command failed with exit code ${code}: ${stderr}`)) + } + }) + + child.on('error', (err) => { + reject(err) + }) + }) } } diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index ffef08e1c76..313ba7c80f3 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -25,7 +25,7 @@ const clientProfile: Profile = { name: 'hardhat', displayName: 'electron hardhat', description: 'electron hardhat', - methods: ['sync', 'compile'] + methods: ['sync', 'compile', 'runCommand'] } @@ -38,11 +38,7 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { processingTimeout: NodeJS.Timeout async onActivation(): Promise { - console.log('Hardhat plugin activated') - this.call('terminal', 'log', { type: 'log', value: 'Hardhat plugin activated' }) - this.on('fs' as any, 'workingDirChanged', async (path: string) => { - console.log('workingDirChanged hardhat', path) this.currentSharedFolder = path this.startListening() }) @@ -51,170 +47,168 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { } startListening() { - this.buildPath = utils.absolutePath('artifacts/contracts', this.currentSharedFolder) - if (fs.existsSync(this.buildPath)) { - this.listenOnHardhatCompilation() - } else { - console.log('If you are using Hardhat, run `npx hardhat compile` or run the compilation with `Enable Hardhat Compilation` checked from the Remix IDE.') - this.listenOnHardHatFolder() - } - } - - compile(configPath: string) { - return new Promise((resolve, reject) => { - const cmd = `npx hardhat compile --config ${utils.normalizePath(configPath)}` - const options = { cwd: this.currentSharedFolder, shell: true } - const child = spawn(cmd, options) - let result = '' - let error = '' - child.stdout.on('data', (data) => { - const msg = `[Hardhat Compilation]: ${data.toString()}` - console.log('\x1b[32m%s\x1b[0m', msg) - result += msg + '\n' - }) - child.stderr.on('data', (err) => { - error += `[Hardhat Compilation]: ${err.toString()} \n` + this.buildPath = utils.absolutePath('artifacts/contracts', this.currentSharedFolder) + this.cachePath = utils.absolutePath('cache', this.currentSharedFolder) + this.on('fileManager', 'currentFileChanged', async (currentFile: string) => { + this.emitContract(basename(currentFile)) + }) + this.listenOnHardhatCompilation() + } + + listenOnHardhatCompilation() { + try { + if (this.watcher) this.watcher.close() + this.watcher = chokidar.watch(this.cachePath, { depth: 0, ignorePermissionErrors: true, ignoreInitial: true }) + this.watcher.on('change', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) }) - child.on('close', () => { - if (error && result) resolve(error + result) - else if (error) reject(error) - else resolve(result) + this.watcher.on('add', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) }) - }) - } - - checkPath() { - if (!fs.existsSync(this.buildPath)) { - this.listenOnHardHatFolder() - return false + } catch (e) { + console.log('listenOnHardhatCompilation', e) } - return true } - private async processArtifact() { - console.log('processing artifact') - if (!this.checkPath()) return - // resolving the files - const folderFiles = await fs.promises.readdir(this.buildPath) - const targetsSynced = [] - // name of folders are file names - for (const file of folderFiles) { // ["artifacts/contracts/Greeter.sol/"] - const contractFilePath = join(this.buildPath, file) - const stat = await fs.promises.stat(contractFilePath) - if (!stat.isDirectory()) continue - const files = await fs.promises.readdir(contractFilePath) - const compilationResult = { - input: {}, - output: { - contracts: {}, - sources: {} - }, - solcVersion: null, - target: null - } - for (const file of files) { - if (file.endsWith('.dbg.json')) { // "artifacts/contracts/Greeter.sol/Greeter.dbg.json" - const stdFile = file.replace('.dbg.json', '.json') - const contentStd = await fs.promises.readFile(join(contractFilePath, stdFile), { encoding: 'utf-8' }) - const contentDbg = await fs.promises.readFile(join(contractFilePath, file), { encoding: 'utf-8' }) - const jsonDbg = JSON.parse(contentDbg) - const jsonStd = JSON.parse(contentStd) - compilationResult.target = jsonStd.sourceName - - targetsSynced.push(compilationResult.target) - const path = join(contractFilePath, jsonDbg.buildInfo) - const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) - - await this.feedContractArtifactFile(content, compilationResult) + compile() { + return new Promise((resolve, reject) => { + const cmd = `npx hardhat compile` + this.call('terminal', 'log', { type: 'log', value: `running ${cmd}` }) + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let error = '' + child.stdout.on('data', (data) => { + if (data.toString().includes('Error')) { + this.call('terminal', 'log', { type: 'error', value: `${data.toString()}` }) + } else { + const msg = `${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + this.call('terminal', 'log', { type: 'log', value: msg }) } - if (compilationResult.target) { - // we are only interested in the contracts that are in the target of the compilation - compilationResult.output = { - ...compilationResult.output, - contracts: { [compilationResult.target]: compilationResult.output.contracts[compilationResult.target] } - } - this.emit('compilationFinished', compilationResult.target, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) - } - } + }) + child.stderr.on('data', (err) => { + error += err.toString() + '\n' + this.call('terminal', 'log', { type: 'error', value: `${err.toString()}` }) + }) + child.on('close', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) + resolve('') + }) + }) + } + + private async emitContract(file: string) { + const contractFilePath = join(this.buildPath, file) + if (!fs.existsSync(contractFilePath)) return + const stat = await fs.promises.stat(contractFilePath) + if (!stat.isDirectory()) return + const files = await fs.promises.readdir(contractFilePath) + const compilationResult = { + input: {}, + output: { + contracts: {}, + sources: {} + }, + solcVersion: null, + target: null + } + for (const file of files) { + if (file.endsWith('.dbg.json')) { // "artifacts/contracts/Greeter.sol/Greeter.dbg.json" + const stdFile = file.replace('.dbg.json', '.json') + const contentStd = await fs.promises.readFile(join(contractFilePath, stdFile), { encoding: 'utf-8' }) + const contentDbg = await fs.promises.readFile(join(contractFilePath, file), { encoding: 'utf-8' }) + const jsonDbg = JSON.parse(contentDbg) + const jsonStd = JSON.parse(contentStd) + compilationResult.target = jsonStd.sourceName + + const path = join(contractFilePath, jsonDbg.buildInfo) + const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) + await this.feedContractArtifactFile(content, compilationResult) } - - clearTimeout(this.logTimeout) - this.logTimeout = setTimeout(() => { - this.call('terminal', 'log', { value: 'receiving compilation result from Hardhat. Select a file to populate the contract interaction interface.', type: 'log' }) - if (targetsSynced.length) { - console.log(`Processing artifacts for files: ${[...new Set(targetsSynced)].join(', ')}`) - // @ts-ignore - this.call('terminal', 'log', { type: 'log', value: `synced with Hardhat: ${[...new Set(targetsSynced)].join(', ')}` }) - } else { - console.log('No artifacts to process') - // @ts-ignore - this.call('terminal', 'log', { type: 'log', value: 'No artifacts from Hardhat to process' }) + if (compilationResult.target) { + // we are only interested in the contracts that are in the target of the compilation + compilationResult.output = { + ...compilationResult.output, + contracts: { [compilationResult.target]: compilationResult.output.contracts[compilationResult.target] } } - }, 1000) - - } - - listenOnHardHatFolder() { - console.log('Hardhat artifacts folder doesn\'t exist... waiting for the compilation.') - try { - if (this.watcher) this.watcher.close() - this.watcher = chokidar.watch(this.currentSharedFolder, { depth: 2, ignorePermissionErrors: true, ignoreInitial: true }) - // watch for new folders - this.watcher.on('addDir', (path: string) => { - console.log('add dir hardhat', path) - if (fs.existsSync(this.buildPath)) { - this.listenOnHardhatCompilation() - } - }) - } catch (e) { - console.log('listenOnHardHatFolder', e) + this.emit('compilationFinished', compilationResult.target, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) } } - - async triggerProcessArtifact() { - console.log('triggerProcessArtifact') - // prevent multiple calls - clearTimeout(this.processingTimeout) - this.processingTimeout = setTimeout(async () => await this.processArtifact(), 1000) - } - - listenOnHardhatCompilation() { - try { - console.log('listening on Hardhat compilation...', this.buildPath) - if (this.watcher) this.watcher.close() - this.watcher = chokidar.watch(this.buildPath, { depth: 1, ignorePermissionErrors: true, ignoreInitial: true }) - this.watcher.on('change', async () => await this.triggerProcessArtifact()) - this.watcher.on('add', async () => await this.triggerProcessArtifact()) - this.watcher.on('unlink', async () => await this.triggerProcessArtifact()) - // process the artifact on activation - this.processArtifact() - } catch (e) { - console.log('listenOnHardhatCompilation', e) + } + + async sync() { + console.log('syncing Hardhet with Remix...') + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) + } + + runCommand(commandArgs: string) { + return new Promise((resolve, reject) => { + // Validate that the command is a Hardhat command + const commandParts = commandArgs.trim().split(' ') + + // Allow 'npx hardhat' or 'hardhat' commands + if (commandParts[0] === 'npx' && commandParts[1] !== 'hardhat') { + reject(new Error('Command must be an npx hardhat command')) + return + } else if (commandParts[0] !== 'npx' && commandParts[0] !== 'hardhat') { + reject(new Error('Command must be a hardhat command (use "npx hardhat" or "hardhat")')) + return } - } - - async sync() { - console.log('syncing from Hardhat') - this.processArtifact() - } - - async feedContractArtifactFile(artifactContent, compilationResultPart) { - const contentJSON = JSON.parse(artifactContent) - compilationResultPart.solcVersion = contentJSON.solcVersion - for (const file in contentJSON.input.sources) { - const source = contentJSON.input.sources[file] - const absPath = join(this.currentSharedFolder, file) - if (fs.existsSync(absPath)) { // if not that is a lib - const contentOnDisk = await fs.promises.readFile(absPath, { encoding: 'utf-8' }) - if (contentOnDisk === source.content) { - compilationResultPart.input[file] = source - compilationResultPart.output['sources'][file] = contentJSON.output.sources[file] - compilationResultPart.output['contracts'][file] = contentJSON.output.contracts[file] - if (contentJSON.output.errors && contentJSON.output.errors.length) { - compilationResultPart.output['errors'] = contentJSON.output.errors.filter(error => error.sourceLocation.file === file) - } + + const cmd = commandArgs + this.call('terminal', 'log', { type: 'log', value: `running ${cmd}` }) + const options = { cwd: this.currentSharedFolder, shell: true } + const child = spawn(cmd, options) + let stdout = '' + let stderr = '' + + child.stdout.on('data', (data) => { + const output = data.toString() + stdout += output + this.call('terminal', 'log', { type: 'log', value: output }) + }) + + child.stderr.on('data', (err) => { + const output = err.toString() + stderr += output + this.call('terminal', 'log', { type: 'error', value: output }) + }) + + child.on('close', (code) => { + if (code === 0) { + resolve({ stdout, stderr, exitCode: code }) + } else { + reject(new Error(`Command failed with exit code ${code}: ${stderr}`)) + } + }) + + child.on('error', (err) => { + reject(err) + }) + }) + } + + async feedContractArtifactFile(artifactContent, compilationResultPart) { + const contentJSON = JSON.parse(artifactContent) + compilationResultPart.solcVersion = contentJSON.solcVersion + for (const file in contentJSON.input.sources) { + const source = contentJSON.input.sources[file] + const absPath = join(this.currentSharedFolder, file) + if (fs.existsSync(absPath)) { // if not that is a lib + const contentOnDisk = await fs.promises.readFile(absPath, { encoding: 'utf-8' }) + if (contentOnDisk === source.content) { + compilationResultPart.input[file] = source + compilationResultPart.output['sources'][file] = contentJSON.output.sources[file] + compilationResultPart.output['contracts'][file] = contentJSON.output.contracts[file] + if (contentJSON.output.errors && contentJSON.output.errors.length) { + compilationResultPart.output['errors'] = contentJSON.output.errors.filter(error => error.sourceLocation.file === file) } } } } + } } \ No newline at end of file diff --git a/libs/endpoints-helper/src/index.ts b/libs/endpoints-helper/src/index.ts index ae128dc2ddc..8d0b19caee2 100644 --- a/libs/endpoints-helper/src/index.ts +++ b/libs/endpoints-helper/src/index.ts @@ -66,9 +66,16 @@ const resolvedUrls: EndpointUrls = prefix ) as EndpointUrls : defaultUrls; -resolvedUrls.solidityScanWebSocket = resolvedUrls.solidityScan.replace( - 'http://', - 'ws://' -); +if (resolvedUrls.solidityScan.startsWith('https://')) { + resolvedUrls.solidityScanWebSocket = resolvedUrls.solidityScan.replace( + 'https://', + 'wss://' + ); +} else if (resolvedUrls.solidityScan.startsWith('http://')) { + resolvedUrls.solidityScanWebSocket = resolvedUrls.solidityScan.replace( + 'http://', + 'ws://' + ); +} export const endpointUrls = resolvedUrls; diff --git a/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts b/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts index 87898448fe8..06a5d0836ce 100644 --- a/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts +++ b/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts @@ -32,6 +32,7 @@ import { createDeploymentTools } from './handlers/DeploymentHandler'; import { createDebuggingTools } from './handlers/DebuggingHandler'; import { createCodeAnalysisTools } from './handlers/CodeAnalysisHandler'; import { createTutorialsTools } from './handlers/TutorialsHandler'; +import { createFoundryHardhatTools } from './handlers/FoundryHardhatHandler'; // Import resource providers import { ProjectResourceProvider } from './providers/ProjectResourceProvider'; @@ -458,6 +459,10 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { const tutorialTools = createTutorialsTools(); this._tools.registerBatch(tutorialTools); + // Register Foundry and Hardhat tools + const foundryHardhatTools = createFoundryHardhatTools(); + this._tools.registerBatch(foundryHardhatTools); + const totalTools = this._tools.list().length; } catch (error) { diff --git a/libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.README.md b/libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.README.md new file mode 100644 index 00000000000..f3297b288d6 --- /dev/null +++ b/libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.README.md @@ -0,0 +1,308 @@ +# Foundry and Hardhat Handler for Remix MCP Server + +This handler provides AI agents with the ability to interact with Foundry and Hardhat development frameworks through the Remix IDE MCP Server. + +## Overview + +The `FoundryHardhatHandler.ts` file contains handlers that enable AI agents to: +- Compile Solidity contracts using Foundry (`forge build`) +- Compile Solidity contracts using Hardhat (`npx hardhat compile`) +- Sync compilation artifacts with Remix IDE +- Get information about Foundry and Hardhat usage + +## Available Tools + +### 1. `foundry_compile` +Compiles Solidity contracts using Foundry's `forge build` command. + +**Description:** Compile Solidity contracts using Foundry (forge build). This command builds your Foundry project, compiling all contracts in the src directory according to foundry.toml configuration. + +**Input Schema:** +```json +{ + "sync": { + "type": "boolean", + "description": "Whether to sync the compilation result with Remix IDE after compilation", + "default": true + } +} +``` + +**Permissions:** `foundry:compile` + +**Underlying Command:** Calls `foundryPlugin.compile()` which runs `forge build` + +### 2. `foundry_sync` +Syncs Foundry compilation artifacts with Remix IDE. + +**Description:** Sync Foundry compilation artifacts with Remix IDE. This updates Remix with the latest Foundry build artifacts from the out/ and cache/ directories. + +**Input Schema:** No parameters required + +**Permissions:** `foundry:sync` + +**Underlying Command:** Calls `foundryPlugin.sync()` + +### 3. `hardhat_compile` +Compiles Solidity contracts using Hardhat's `npx hardhat compile` command. + +**Description:** Compile Solidity contracts using Hardhat (npx hardhat compile). This command builds your Hardhat project, compiling all contracts in the contracts directory according to hardhat.config.js configuration. + +**Input Schema:** +```json +{ + "sync": { + "type": "boolean", + "description": "Whether to sync the compilation result with Remix IDE after compilation", + "default": true + } +} +``` + +**Permissions:** `hardhat:compile` + +**Underlying Command:** Calls `hardhatPlugin.compile()` which runs `npx hardhat compile` + +### 4. `hardhat_sync` +Syncs Hardhat compilation artifacts with Remix IDE. + +**Description:** Sync Hardhat compilation artifacts with Remix IDE. This updates Remix with the latest Hardhat build artifacts from the artifacts/ and cache/ directories. + +**Input Schema:** No parameters required + +**Permissions:** `hardhat:sync` + +**Underlying Command:** Calls `hardhatPlugin.sync()` + +### 5. `foundry_run_command` +Executes any Foundry command (forge, cast, anvil) through the foundryPlugin. + +**Description:** Run any Foundry command (forge, cast, or anvil) in the current working directory. Examples: "forge test", "forge script", "cast call", "anvil". + +**Input Schema:** +```json +{ + "command": { + "type": "string", + "description": "The Foundry command to execute. Must start with 'forge', 'cast', or 'anvil'.", + "required": true + } +} +``` + +**Permissions:** `foundry:command` + +**Underlying Command:** Calls `foundryPlugin.runCommand(command)` which executes the command in the working directory + +**Example Commands:** +- `forge test` - Run all tests +- `forge test -vvv` - Run tests with verbose output +- `forge test --match-test testMyFunction` - Run specific test +- `forge script scripts/Deploy.s.sol` - Run deployment script +- `forge script scripts/Deploy.s.sol --rpc-url $RPC_URL --broadcast` - Deploy with broadcasting +- `cast call
"balanceOf(address)"
` - Call contract method +- `cast send
"transfer(address,uint256)" ` - Send transaction +- `anvil` - Start local Ethereum node + +### 6. `hardhat_run_command` +Executes any Hardhat command through the hardhatPlugin. + +**Description:** Run any Hardhat command in the current working directory. Examples: "npx hardhat test", "npx hardhat run scripts/deploy.js", "npx hardhat node". + +**Input Schema:** +```json +{ + "command": { + "type": "string", + "description": "The Hardhat command to execute. Must be a hardhat command, typically prefixed with 'npx hardhat'.", + "required": true + } +} +``` + +**Permissions:** `hardhat:command` + +**Underlying Command:** Calls `hardhatPlugin.runCommand(command)` which executes the command in the working directory + +**Example Commands:** +- `npx hardhat test` - Run all tests +- `npx hardhat test --grep "MyTest"` - Run specific tests +- `npx hardhat run scripts/deploy.js` - Run deployment script +- `npx hardhat run scripts/deploy.js --network localhost` - Deploy to specific network +- `npx hardhat node` - Start local Hardhat node +- `npx hardhat verify --network mainnet
` - Verify contract on network +- `npx hardhat accounts` - List available accounts +- `npx hardhat console` - Open interactive console + +### 7. `get_foundry_hardhat_info` +Provides comprehensive information about using Foundry and Hardhat in Remix IDE. + +**Description:** Get information about using Foundry and Hardhat in Remix IDE, including available commands and usage patterns. + +**Input Schema:** +```json +{ + "framework": { + "type": "string", + "enum": ["foundry", "hardhat", "both"], + "description": "Which framework to get info about", + "default": "both" + } +} +``` + +**Permissions:** `foundry:info`, `hardhat:info` + +**Returns:** Comprehensive information including: +- Available commands and their descriptions (including command execution tools) +- Project structure +- Setup instructions +- Example commands for running tests, scripts, and more +- Comparison between frameworks (when framework="both") + +## Implementation Details + +### Plugin Integration + +The handlers call the respective plugins: +- **Foundry:** Calls `foundryPlugin` methods which are implemented in `apps/remixdesktop/src/plugins/foundryPlugin.ts` +- **Hardhat:** Calls `hardhatPlugin` methods which are implemented in `apps/remixdesktop/src/plugins/hardhatPlugin.ts` + +Both plugins: +1. Execute the compilation command using `spawn` +2. Log output to the Remix terminal +3. Watch for file changes in cache directories +4. Emit compilation results to Remix IDE + +### Registration + +The tools are registered in the Remix MCP Server through: +1. Export function `createFoundryHardhatTools()` in `FoundryHardhatHandler.ts` +2. Import and registration in `RemixMCPServer.ts` via `initializeDefaultTools()` +3. Export in `index.ts` for external use + +## Usage Examples + +### For AI Agents + +When an AI agent wants to compile a Foundry project: +```json +{ + "name": "foundry_compile", + "arguments": {} +} +``` + +When an AI agent wants to run Foundry tests: +```json +{ + "name": "foundry_run_command", + "arguments": { + "command": "forge test -vvv" + } +} +``` + +When an AI agent wants to run a Foundry script: +```json +{ + "name": "foundry_run_command", + "arguments": { + "command": "forge script scripts/Deploy.s.sol --rpc-url http://localhost:8545 --broadcast" + } +} +``` + +When an AI agent wants to run Hardhat tests: +```json +{ + "name": "hardhat_run_command", + "arguments": { + "command": "npx hardhat test --grep 'MyContract'" + } +} +``` + +When an AI agent wants to run a Hardhat deployment script: +```json +{ + "name": "hardhat_run_command", + "arguments": { + "command": "npx hardhat run scripts/deploy.js --network localhost" + } +} +``` + +When an AI agent wants to get information about both frameworks: +```json +{ + "name": "get_foundry_hardhat_info", + "arguments": { + "framework": "both" + } +} +``` + +### Response Format + +Success response: +```json +{ + "content": [{ + "type": "text", + "text": "{\"success\":true,\"message\":\"Foundry compilation completed successfully...\"}" + }], + "isError": false +} +``` + +Error response: +```json +{ + "content": [{ + "type": "text", + "text": "Error: Foundry compilation failed: ..." + }], + "isError": true +} +``` + +## Security + +The command execution handlers include security validation to prevent arbitrary command execution: + +### Foundry Command Validation +- Commands must start with one of: `forge`, `cast`, or `anvil` +- Any command not starting with these tools will be rejected +- Validation happens both in the handler and in the plugin + +### Hardhat Command Validation +- Commands must be Hardhat commands (starting with `hardhat` or `npx hardhat`) +- Any non-Hardhat commands will be rejected +- Validation happens both in the handler and in the plugin + +### Command Execution +- All commands are executed in the current working directory +- Commands are executed with shell: true for proper argument parsing +- stdout and stderr are captured and logged to the Remix terminal +- Exit codes are returned to indicate success/failure + +## File Locations + +- **Handler:** `libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts` +- **Foundry Plugin:** `apps/remixdesktop/src/plugins/foundryPlugin.ts` +- **Hardhat Plugin:** `apps/remixdesktop/src/plugins/hardhatPlugin.ts` +- **Registration:** `libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts` +- **Export:** `libs/remix-ai-core/src/remix-mcp-server/index.ts` + +## Dependencies + +The handlers depend on: +- `BaseToolHandler` from `RemixToolRegistry` +- `IMCPToolResult` from MCP types +- `Plugin` from `@remixproject/engine` +- The respective Foundry and Hardhat plugins being available in the Remix IDE + +## Category + +All tools are registered under `ToolCategory.COMPILATION` diff --git a/libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts b/libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts new file mode 100644 index 00000000000..ff165009b01 --- /dev/null +++ b/libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts @@ -0,0 +1,533 @@ +/** + * Foundry and Hardhat Tool Handlers for Remix MCP Server + * + * These handlers enable AI agents to interact with Foundry and Hardhat frameworks + * through their respective Remix plugins, executing compilation and sync operations. + */ + +import { IMCPToolResult } from '../../types/mcp'; +import { BaseToolHandler } from '../registry/RemixToolRegistry'; +import { + ToolCategory, + RemixToolDefinition, +} from '../types/mcpTools'; +import { Plugin } from '@remixproject/engine'; + +/** + * Foundry Compile Tool Handler + * + * Executes Foundry compilation by calling the foundryPlugin. + * This runs `forge build` in the current working directory. + */ +export class FoundryCompileHandler extends BaseToolHandler { + name = 'foundry_compile'; + description = 'Compile Solidity contracts using Foundry (forge build). This command builds your Foundry project, compiling all contracts in the src directory according to foundry.toml configuration.'; + inputSchema = { + type: 'object', + properties: { + sync: { + type: 'boolean', + description: 'Whether to sync the compilation result with Remix IDE after compilation', + default: true + } + } + }; + + getPermissions(): string[] { + return ['foundry:compile']; + } + + validate(args: { sync?: boolean }): boolean | string { + if (args.sync !== undefined) { + const types = this.validateTypes(args, { sync: 'boolean' }); + if (types !== true) return types; + } + return true; + } + + async execute(_args: { sync?: boolean }, plugin: Plugin): Promise { + try { + // Call the foundry plugin's compile method + await plugin.call('foundry' as any, 'compile'); + + return this.createSuccessResult({ + success: true, + message: 'Foundry compilation completed successfully. Contracts were compiled using forge build.', + framework: 'foundry', + command: 'forge build' + }); + } catch (error) { + return this.createErrorResult(`Foundry compilation failed: ${error.message}`); + } + } +} + +/** + * Foundry Sync Tool Handler + * + * Syncs Foundry compilation artifacts with Remix IDE. + * This reads the cache and emits compilation results for the current file. + */ +export class FoundrySyncHandler extends BaseToolHandler { + name = 'foundry_sync'; + description = 'Sync Foundry compilation artifacts with Remix IDE. This updates Remix with the latest Foundry build artifacts from the out/ and cache/ directories.'; + inputSchema = { + type: 'object', + properties: {} + }; + + getPermissions(): string[] { + return ['foundry:sync']; + } + + async execute(_args: any, plugin: Plugin): Promise { + try { + // Call the foundry plugin's sync method + await plugin.call('foundry' as any, 'sync'); + + return this.createSuccessResult({ + success: true, + message: 'Foundry artifacts synced successfully with Remix IDE', + framework: 'foundry' + }); + } catch (error) { + return this.createErrorResult(`Foundry sync failed: ${error.message}`); + } + } +} + +/** + * Hardhat Compile Tool Handler + * + * Executes Hardhat compilation by calling the hardhatPlugin. + * This runs `npx hardhat compile` in the current working directory. + */ +export class HardhatCompileHandler extends BaseToolHandler { + name = 'hardhat_compile'; + description = 'Compile Solidity contracts using Hardhat (npx hardhat compile). This command builds your Hardhat project, compiling all contracts in the contracts directory according to hardhat.config.js configuration.'; + inputSchema = { + type: 'object', + properties: { + sync: { + type: 'boolean', + description: 'Whether to sync the compilation result with Remix IDE after compilation', + default: true + } + } + }; + + getPermissions(): string[] { + return ['hardhat:compile']; + } + + validate(args: { sync?: boolean }): boolean | string { + if (args.sync !== undefined) { + const types = this.validateTypes(args, { sync: 'boolean' }); + if (types !== true) return types; + } + return true; + } + + async execute(_args: { sync?: boolean }, plugin: Plugin): Promise { + try { + // Call the hardhat plugin's compile method + await plugin.call('hardhat' as any, 'compile'); + + return this.createSuccessResult({ + success: true, + message: 'Hardhat compilation completed successfully. Contracts were compiled using npx hardhat compile.', + framework: 'hardhat', + command: 'npx hardhat compile' + }); + } catch (error) { + return this.createErrorResult(`Hardhat compilation failed: ${error.message}`); + } + } +} + +/** + * Hardhat Sync Tool Handler + * + * Syncs Hardhat compilation artifacts with Remix IDE. + * This reads the artifacts and emits compilation results for the current file. + */ +export class HardhatSyncHandler extends BaseToolHandler { + name = 'hardhat_sync'; + description = 'Sync Hardhat compilation artifacts with Remix IDE. This updates Remix with the latest Hardhat build artifacts from the artifacts/ and cache/ directories.'; + inputSchema = { + type: 'object', + properties: {} + }; + + getPermissions(): string[] { + return ['hardhat:sync']; + } + + async execute(_args: any, plugin: Plugin): Promise { + try { + // Call the hardhat plugin's sync method + await plugin.call('hardhat' as any, 'sync'); + + return this.createSuccessResult({ + success: true, + message: 'Hardhat artifacts synced successfully with Remix IDE', + framework: 'hardhat' + }); + } catch (error) { + return this.createErrorResult(`Hardhat sync failed: ${error.message}`); + } + } +} + +/** + * Foundry Run Command Tool Handler + * + * Executes any Foundry command (forge, cast, anvil) through the foundryPlugin. + */ +export class FoundryRunCommandHandler extends BaseToolHandler { + name = 'foundry_run_command'; + description = 'Run any Foundry command (forge, cast, or anvil) in the current working directory. Examples: "forge test", "forge script", "cast call", "anvil".'; + inputSchema = { + type: 'object', + properties: { + command: { + type: 'string', + description: 'The Foundry command to execute. Must start with "forge", "cast", or "anvil". Example: "forge test -vvv" or "forge script scripts/Deploy.s.sol"' + } + }, + required: ['command'] + }; + + getPermissions(): string[] { + return ['foundry:command']; + } + + validate(args: { command: string }): boolean | string { + const required = this.validateRequired(args, ['command']); + if (required !== true) return required; + + const types = this.validateTypes(args, { command: 'string' }); + if (types !== true) return types; + + // Validate command starts with allowed Foundry commands + const allowedCommands = ['forge', 'cast', 'anvil']; + const commandParts = args.command.trim().split(' '); + const baseCommand = commandParts[0]; + + if (!allowedCommands.includes(baseCommand)) { + return `Command must start with one of: ${allowedCommands.join(', ')}`; + } + + return true; + } + + async execute(args: { command: string }, plugin: Plugin): Promise { + try { + // Call the foundry plugin's runCommand method + const result: any = await plugin.call('foundry' as any, 'runCommand', args.command); + + return this.createSuccessResult({ + success: true, + message: `Foundry command executed successfully: ${args.command}`, + command: args.command, + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr + }); + } catch (error) { + return this.createErrorResult(`Foundry command failed: ${error.message}`); + } + } +} + +/** + * Hardhat Run Command Tool Handler + * + * Executes any Hardhat command through the hardhatPlugin. + */ +export class HardhatRunCommandHandler extends BaseToolHandler { + name = 'hardhat_run_command'; + description = 'Run any Hardhat command in the current working directory. Examples: "npx hardhat test", "npx hardhat run scripts/deploy.js", "npx hardhat node".'; + inputSchema = { + type: 'object', + properties: { + command: { + type: 'string', + description: 'The Hardhat command to execute. Must be a hardhat command, typically prefixed with "npx hardhat". Example: "npx hardhat test" or "npx hardhat run scripts/deploy.js --network localhost"' + } + }, + required: ['command'] + }; + + getPermissions(): string[] { + return ['hardhat:command']; + } + + validate(args: { command: string }): boolean | string { + const required = this.validateRequired(args, ['command']); + if (required !== true) return required; + + const types = this.validateTypes(args, { command: 'string' }); + if (types !== true) return types; + + // Validate command is a Hardhat command + const commandParts = args.command.trim().split(' '); + + if (commandParts[0] === 'npx' && commandParts[1] !== 'hardhat') { + return 'Command must be an npx hardhat command'; + } else if (commandParts[0] !== 'npx' && commandParts[0] !== 'hardhat') { + return 'Command must be a hardhat command (use "npx hardhat" or "hardhat")'; + } + + return true; + } + + async execute(args: { command: string }, plugin: Plugin): Promise { + try { + // Call the hardhat plugin's runCommand method + const result: any = await plugin.call('hardhat' as any, 'runCommand', args.command); + + return this.createSuccessResult({ + success: true, + message: `Hardhat command executed successfully: ${args.command}`, + command: args.command, + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr + }); + } catch (error) { + return this.createErrorResult(`Hardhat command failed: ${error.message}`); + } + } +} + +/** + * Get Foundry/Hardhat Info Tool Handler + * + * Provides information about how to use Foundry and Hardhat commands in Remix. + */ +export class GetFoundryHardhatInfoHandler extends BaseToolHandler { + name = 'get_foundry_hardhat_info'; + description = 'Get information about using Foundry and Hardhat in Remix IDE, including available commands and usage patterns.'; + inputSchema = { + type: 'object', + properties: { + framework: { + type: 'string', + enum: ['foundry', 'hardhat', 'both'], + description: 'Which framework to get info about', + default: 'both' + } + } + }; + + getPermissions(): string[] { + return ['foundry:info', 'hardhat:info']; + } + + validate(args: { framework?: string }): boolean | string { + if (args.framework && !['foundry', 'hardhat', 'both'].includes(args.framework)) { + return 'Framework must be one of: foundry, hardhat, both'; + } + return true; + } + + async execute(args: { framework?: string }, _plugin: Plugin): Promise { + const framework = args.framework || 'both'; + + const foundryInfo = { + name: 'Foundry', + description: 'A blazing fast, portable and modular toolkit for Ethereum application development written in Rust.', + commands: { + compile: { + tool: 'foundry_compile', + description: 'Compiles all contracts in your Foundry project using forge build', + underlyingCommand: 'forge build', + outputDirectory: 'out/', + cacheDirectory: 'cache/', + configFile: 'foundry.toml' + }, + sync: { + tool: 'foundry_sync', + description: 'Syncs Foundry compilation artifacts with Remix IDE', + usage: 'Use after external compilation or to refresh artifacts' + }, + runCommand: { + tool: 'foundry_run_command', + description: 'Execute any Foundry command (forge, cast, anvil)', + usage: 'Pass any valid Foundry command as a string', + examples: [ + 'forge test', + 'forge test -vvv', + 'forge test --match-test testMyFunction', + 'forge script scripts/Deploy.s.sol', + 'forge script scripts/Deploy.s.sol --rpc-url $RPC_URL --broadcast', + 'cast call "balanceOf(address)"
', + 'cast send "transfer(address,uint256)" ', + 'anvil' + ], + supportedTools: ['forge', 'cast', 'anvil'] + } + }, + projectStructure: { + src: 'Source contracts directory', + test: 'Test files directory', + script: 'Deployment scripts directory', + out: 'Compiled artifacts output', + cache: 'Compilation cache', + lib: 'Dependencies directory' + }, + setupInstructions: [ + 'Ensure Foundry is installed (foundryup)', + 'Initialize a Foundry project with: forge init', + 'Place contracts in the src/ directory', + 'Configure foundry.toml as needed', + 'Use foundry_compile to build your contracts' + ] + }; + + const hardhatInfo = { + name: 'Hardhat', + description: 'A development environment to compile, deploy, test, and debug Ethereum software.', + commands: { + compile: { + tool: 'hardhat_compile', + description: 'Compiles all contracts in your Hardhat project using npx hardhat compile', + underlyingCommand: 'npx hardhat compile', + outputDirectory: 'artifacts/', + cacheDirectory: 'cache/', + configFile: 'hardhat.config.js or hardhat.config.ts' + }, + sync: { + tool: 'hardhat_sync', + description: 'Syncs Hardhat compilation artifacts with Remix IDE', + usage: 'Use after external compilation or to refresh artifacts' + }, + runCommand: { + tool: 'hardhat_run_command', + description: 'Execute any Hardhat command', + usage: 'Pass any valid Hardhat command as a string (typically prefixed with "npx hardhat")', + examples: [ + 'npx hardhat test', + 'npx hardhat test --grep "MyTest"', + 'npx hardhat run scripts/deploy.js', + 'npx hardhat run scripts/deploy.js --network localhost', + 'npx hardhat node', + 'npx hardhat verify --network mainnet ', + 'npx hardhat accounts', + 'npx hardhat console' + ] + } + }, + projectStructure: { + contracts: 'Source contracts directory', + test: 'Test files directory', + scripts: 'Deployment scripts directory', + artifacts: 'Compiled artifacts output', + cache: 'Compilation cache', + node_modules: 'Dependencies directory' + }, + setupInstructions: [ + 'Ensure Node.js and npm are installed', + 'Initialize a Hardhat project with: npx hardhat', + 'Place contracts in the contracts/ directory', + 'Configure hardhat.config.js as needed', + 'Install dependencies with: npm install', + 'Use hardhat_compile to build your contracts' + ] + }; + + let result: any = {}; + + if (framework === 'foundry' || framework === 'both') { + result.foundry = foundryInfo; + } + + if (framework === 'hardhat' || framework === 'both') { + result.hardhat = hardhatInfo; + } + + if (framework === 'both') { + result.comparison = { + foundry: { + pros: ['Very fast compilation', 'Written in Rust', 'Built-in fuzzing', 'Gas-efficient testing'], + useCases: ['Performance-critical projects', 'Advanced testing needs', 'Rust ecosystem integration'] + }, + hardhat: { + pros: ['JavaScript/TypeScript ecosystem', 'Large plugin ecosystem', 'Mature tooling', 'Easy debugging'], + useCases: ['JavaScript-based teams', 'Complex deployment scripts', 'Extensive plugin requirements'] + } + }; + } + + return this.createSuccessResult({ + success: true, + framework: framework, + info: result + }); + } +} + +/** + * Create Foundry and Hardhat tool definitions + */ +export function createFoundryHardhatTools(): RemixToolDefinition[] { + return [ + { + name: 'foundry_compile', + description: 'Compile Solidity contracts using Foundry (forge build)', + inputSchema: new FoundryCompileHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['foundry:compile'], + handler: new FoundryCompileHandler() + }, + { + name: 'foundry_sync', + description: 'Sync Foundry compilation artifacts with Remix IDE', + inputSchema: new FoundrySyncHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['foundry:sync'], + handler: new FoundrySyncHandler() + }, + { + name: 'foundry_run_command', + description: 'Run any Foundry command (forge, cast, or anvil)', + inputSchema: new FoundryRunCommandHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['foundry:command'], + handler: new FoundryRunCommandHandler() + }, + { + name: 'hardhat_compile', + description: 'Compile Solidity contracts using Hardhat (npx hardhat compile)', + inputSchema: new HardhatCompileHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['hardhat:compile'], + handler: new HardhatCompileHandler() + }, + { + name: 'hardhat_sync', + description: 'Sync Hardhat compilation artifacts with Remix IDE', + inputSchema: new HardhatSyncHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['hardhat:sync'], + handler: new HardhatSyncHandler() + }, + { + name: 'hardhat_run_command', + description: 'Run any Hardhat command', + inputSchema: new HardhatRunCommandHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['hardhat:command'], + handler: new HardhatRunCommandHandler() + }, + { + name: 'get_foundry_hardhat_info', + description: 'Get information about using Foundry and Hardhat in Remix IDE', + inputSchema: new GetFoundryHardhatInfoHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['foundry:info', 'hardhat:info'], + handler: new GetFoundryHardhatInfoHandler() + } + ]; +} diff --git a/libs/remix-ai-core/src/remix-mcp-server/index.ts b/libs/remix-ai-core/src/remix-mcp-server/index.ts index 8e0efd957eb..401d47590ca 100644 --- a/libs/remix-ai-core/src/remix-mcp-server/index.ts +++ b/libs/remix-ai-core/src/remix-mcp-server/index.ts @@ -17,6 +17,7 @@ export { createCompilationTools } from './handlers/CompilationHandler'; export { createDeploymentTools } from './handlers/DeploymentHandler'; export { createDebuggingTools } from './handlers/DebuggingHandler'; export { createCodeAnalysisTools } from './handlers/CodeAnalysisHandler'; +export { createFoundryHardhatTools } from './handlers/FoundryHardhatHandler'; // Resource Providers export { ProjectResourceProvider } from './providers/ProjectResourceProvider'; diff --git a/libs/remix-core-plugin/src/lib/compiler-metadata.ts b/libs/remix-core-plugin/src/lib/compiler-metadata.ts index 17a63e2dbae..11ea5fac3bc 100644 --- a/libs/remix-core-plugin/src/lib/compiler-metadata.ts +++ b/libs/remix-core-plugin/src/lib/compiler-metadata.ts @@ -58,6 +58,7 @@ export class CompilerMetadata extends Plugin { const allBuildFiles = await this.call('fileManager', 'fileList', buildDir) const currentInputFileNames = Object.keys(currentInput.sources) for (const fileName of allBuildFiles) { + if (!await this.call('fileManager', 'exists', fileName)) continue let fileContent = await this.call('fileManager', 'readFile', fileName) fileContent = JSON.parse(fileContent) const inputFiles = Object.keys(fileContent.input.sources) @@ -121,7 +122,8 @@ export class CompilerMetadata extends Plugin { let parsedMetadata try { - parsedMetadata = contract.object && contract.object.metadata ? JSON.parse(contract.object.metadata) : null + parsedMetadata = contract.object && contract.object.metadata && typeof(contract.object.metadata) === 'string' ? JSON.parse(contract.object.metadata) : null + if (!parsedMetadata) parsedMetadata = contract.object.metadata } catch (e) { console.log(e) } diff --git a/libs/remix-lib/src/types/ICompilerApi.ts b/libs/remix-lib/src/types/ICompilerApi.ts index cfb4ce832f3..62a574ebdd0 100644 --- a/libs/remix-lib/src/types/ICompilerApi.ts +++ b/libs/remix-lib/src/types/ICompilerApi.ts @@ -43,8 +43,9 @@ export interface ICompilerApi { logToTerminal: (log: terminalLog) => void - compileWithHardhat: (configPath: string) => Promise - compileWithTruffle: (configPath: string) => Promise + compileWithFoundry: () => Promise + compileWithHardhat: () => Promise + compileWithTruffle: () => Promise statusChanged: (data: { key: string, title?: string, type?: string }) => void, emit?: (key: string, ...payload: any) => void diff --git a/libs/remix-ui/helper/src/lib/solidity-scan.tsx b/libs/remix-ui/helper/src/lib/solidity-scan.tsx index 8c6ef9b286f..6e1873de09b 100644 --- a/libs/remix-ui/helper/src/lib/solidity-scan.tsx +++ b/libs/remix-ui/helper/src/lib/solidity-scan.tsx @@ -9,13 +9,18 @@ export const handleSolidityScan = async (api: any, compiledFileName: string) => await api.call('notification', 'toast', 'Processing data to scan...') await trackMatomoEvent(api, { category: 'solidityCompiler', action: 'solidityScan', name: 'initiateScan', isClick: false }) - const workspace = await api.call('filePanel', 'getCurrentWorkspace') - const fileName = `${workspace.name}/${compiledFileName}` - const filePath = `.workspaces/${fileName}` + let filePath + if (await api.call('fileManager', 'exists', compiledFileName)) { + filePath = compiledFileName + } else { + const workspace = await api.call('filePanel', 'getCurrentWorkspace') + const fileName = `${workspace.name}/${compiledFileName}` + filePath = `.workspaces/${fileName}` + } const file = await api.call('fileManager', 'readFile', filePath) try { - const urlResponse = await axios.post(`${endpointUrls.solidityScan}/uploadFile`, { file, fileName }) + const urlResponse = await axios.post(`${endpointUrls.solidityScan}/uploadFile`, { file, fileName: compiledFileName }) if (urlResponse.data.status === 'success') { const ws = new WebSocket(`${endpointUrls.solidityScanWebSocket}/solidityscan`) @@ -64,7 +69,7 @@ export const handleSolidityScan = async (api: any, compiledFileName: string) => template.positions = JSON.stringify(positions) } } - await api.call('terminal', 'logHtml', ) + await api.call('terminal', 'logHtml', ) } else { await api.call('notification', 'modal', { id: 'SolidityScanError', diff --git a/libs/remix-ui/panel/src/lib/plugins/panel.css b/libs/remix-ui/panel/src/lib/plugins/panel.css index 51cc99530af..c26c354a5a1 100644 --- a/libs/remix-ui/panel/src/lib/plugins/panel.css +++ b/libs/remix-ui/panel/src/lib/plugins/panel.css @@ -66,7 +66,6 @@ iframe { } .plugItIn>div { - overflow-y: scroll; overflow-x: hidden; height: 100%; width: 100%; diff --git a/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx b/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx index d1ef0f757d7..31d3e853d80 100644 --- a/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx +++ b/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx @@ -78,20 +78,33 @@ export const RemixUiRemixAiAssistant = React.forwardRef< const userHasScrolledRef = useRef(false) const lastMessageCountRef = useRef(0) - // Ref to hold the sendPrompt function for audio transcription callback - const sendPromptRef = useRef<((prompt: string) => Promise) | null>(null) - // Audio transcription hook const { isRecording, isTranscribing, - error: transcriptionError, + error: _transcriptionError, // Handled in onError callback toggleRecording } = useAudioTranscription({ model: 'whisper-v3', onTranscriptionComplete: async (text) => { - if (sendPromptRef.current) { - await sendPromptRef.current(text) + // Check if transcription ends with "run" (case-insensitive) + const trimmedText = text.trim() + const endsWithRun = /\brun\b\s*$/i.test(trimmedText) + + if (endsWithRun) { + // Remove "run" from the end and execute the prompt + const promptText = trimmedText.replace(/\brun\b\s*$/i, '').trim() + if (promptText) { + await sendPrompt(promptText) + trackMatomoEvent({ category: 'ai', action: 'SpeechToTextPrompt', name: 'SpeechToTextPrompt', isClick: true }) + } + } else { + // Append transcription to the input box for user review + setInput(prev => prev ? `${prev} ${text}`.trim() : text) + // Focus the textarea so user can review/edit + if (textareaRef.current) { + textareaRef.current.focus() + } trackMatomoEvent({ category: 'ai', action: 'SpeechToTextPrompt', name: 'SpeechToTextPrompt', isClick: true }) } }, @@ -524,11 +537,6 @@ export const RemixUiRemixAiAssistant = React.forwardRef< [isStreaming, props.plugin] ) - // Update ref for audio transcription callback - useEffect(() => { - sendPromptRef.current = sendPrompt - }, [sendPrompt]) - const handleGenerateWorkspaceWithPrompt = useCallback(async (prompt: string) => { dispatchActivity('button', 'generateWorkspace') if (prompt && prompt.trim()) { diff --git a/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts b/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts index 80258ad5854..176b73d14bf 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts @@ -1,7 +1,7 @@ import React from 'react'; import { compile, helper, Source, CompilerInputOptions, compilerInputFactory, CompilerInput } from '@remix-project/remix-solidity' import { CompileTabLogic, parseContracts } from '@remix-ui/solidity-compiler' // eslint-disable-line -import type { ConfigurationSettings, iSolJsonBinData } from '@remix-project/remix-lib' +import { ConfigurationSettings, iSolJsonBinData, execution } from '@remix-project/remix-lib' export const CompilerApiMixin = (Base) => class extends Base { currentFile: string @@ -106,12 +106,16 @@ export const CompilerApiMixin = (Base) => class extends Base { this.call('compileAndRun', 'runScriptAfterCompilation', fileName) } - compileWithHardhat (configFile) { - return this.call('hardhat', 'compile', configFile) + compileWithHardhat () { + return this.call('hardhat', 'compile') } - compileWithTruffle (configFile) { - return this.call('truffle', 'compile', configFile) + compileWithFoundry () { + return this.call('foundry', 'compile') + } + + compileWithTruffle () { + return this.call('truffle', 'compile') } logToTerminal (content) { @@ -256,6 +260,11 @@ export const CompilerApiMixin = (Base) => class extends Base { if (this.onSetWorkspace) this.onSetWorkspace(workspace.isLocalhost, workspace.name) }) + this.on('fs', 'workingDirChanged', (path) => { + this.resetResults() + if (this.onSetWorkspace) this.onSetWorkspace(true, 'localhost') + }) + this.on('fileManager', 'fileRemoved', (path) => { if (this.onFileRemoved) this.onFileRemoved(path) }) @@ -350,6 +359,18 @@ export const CompilerApiMixin = (Base) => class extends Base { } this.compiler.event.register('compilationFinished', this.data.eventHandlers.onCompilationFinished) + this.on('foundry', 'compilationFinished', (target, sources, lang, output, version) => { + const contract = output.contracts[target][Object.keys(output.contracts[target])[0]] + sources.target = target + this.data.eventHandlers.onCompilationFinished(true, output, sources, JSON.stringify(contract.metadata), version) + }) + + this.on('hardhat', 'compilationFinished', (target, sources, lang, output, version) => { + const contract = output.contracts[target][Object.keys(output.contracts[target])[0]] + sources.target = target + this.data.eventHandlers.onCompilationFinished(true, output, sources, JSON.stringify(contract.metadata), version) + }) + this.data.eventHandlers.onThemeChanged = (theme) => { const invert = theme.quality === 'dark' ? 1 : 0 const img = document.getElementById('swarmLogo') @@ -367,7 +388,8 @@ export const CompilerApiMixin = (Base) => class extends Base { if (this.currentFile && (this.currentFile.endsWith('.sol') || this.currentFile.endsWith('.yul'))) { if (await this.getAppParameter('hardhat-compilation')) this.compileTabLogic.runCompiler('hardhat') else if (await this.getAppParameter('truffle-compilation')) this.compileTabLogic.runCompiler('truffle') - else this.compileTabLogic.runCompiler(undefined).catch((error) => { + else if (await this.getAppParameter('foundry-compilation')) this.compileTabLogic.runCompiler('foundry') + else this.compileTabLogic.runCompiler('remix').catch((error) => { this.call('notification', 'toast', error.message) }) } else if (this.currentFile && this.currentFile.endsWith('.circom')) { @@ -391,7 +413,8 @@ export const CompilerApiMixin = (Base) => class extends Base { } const contractMap = {} const contractsDetails = {} - this.compiler.visitContracts((contract) => { + + execution.txHelper.visitContracts(data.contracts, (contract) => { contractMap[contract.name] = contract contractsDetails[contract.name] = parseContracts( contract.name, diff --git a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx index 9685625e477..6e7677e19e9 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -69,9 +69,17 @@ export const CompilerContainer = (props: CompilerContainerProps) => { const [disableCompileButton, setDisableCompileButton] = useState(false) const compileIcon = useRef(null) const promptMessageInput = useRef(null) - const [hhCompilation, sethhCompilation] = useState(false) + const [foundryCompilation, setFoundryCompilation] = useState(isFoundryProject) + const [hhCompilation, sethhCompilation] = useState(isHardhatProject) const [truffleCompilation, setTruffleCompilation] = useState(false) const [compilerContainer, dispatch] = useReducer(compilerReducer, compilerInitialState) + useEffect(() => { + setFoundryCompilation(isFoundryProject) + }, [isFoundryProject]) + + useEffect(() => { + sethhCompilation(isHardhatProject) + }, [isHardhatProject]) const intl = useIntl() @@ -436,9 +444,10 @@ export const CompilerContainer = (props: CompilerContainerProps) => { if (state.useFileConfiguration) await createNewConfigFile() _setCompilerVersionFromPragma(currentFile) - let externalCompType + let externalCompType = 'remix' if (hhCompilation) externalCompType = 'hardhat' else if (truffleCompilation) externalCompType = 'truffle' + else if (foundryCompilation) externalCompType = 'foundry' compileTabLogic.runCompiler(externalCompType).catch((error) => { tooltip(error.message) compileIcon.current.classList.remove('remixui_bouncingIcon') @@ -447,6 +456,8 @@ export const CompilerContainer = (props: CompilerContainerProps) => { props.setCompileErrors({ [currentFile]: { error: error.message } }) // @ts-ignore props.setBadgeStatus({ [currentFile]: { key: 1, title: error.message, type: 'error' } }) + }).then(() => { + compileIcon.current.classList.remove('remixui_bouncingIcon') }) } @@ -471,6 +482,8 @@ export const CompilerContainer = (props: CompilerContainerProps) => { props.setCompileErrors({ [currentFile]: { error: error.message } }) // @ts-ignore props.setBadgeStatus({ [currentFile]: { key: 1, title: error.message, type: 'error' } }) + }).then(() => { + compileIcon.current.classList.remove('remixui_bouncingIcon') }) } @@ -711,14 +724,21 @@ export const CompilerContainer = (props: CompilerContainerProps) => { const updatehhCompilation = (event) => { const checked = event.target.checked - if (checked) setTruffleCompilation(false) // wayaround to reset the variable + if (checked) sethhCompilation(false) // wayaround to reset the variable sethhCompilation(checked) api.setAppParameter('hardhat-compilation', checked) } + const updateFoundryCompilation = (event) => { + const checked = event.target.checked + if (checked) setFoundryCompilation(false) // wayaround to reset the variable + setFoundryCompilation(checked) + api.setAppParameter('foundry-compilation', checked) + } + const updateTruffleCompilation = (event) => { const checked = event.target.checked - if (checked) sethhCompilation(false) // wayaround to reset the variable + if (checked) setTruffleCompilation(false) // wayaround to reset the variable setTruffleCompilation(checked) api.setAppParameter('truffle-compilation', checked) } @@ -759,7 +779,14 @@ export const CompilerContainer = (props: CompilerContainerProps) => { tooltipClasses="text-nowrap" tooltipText={} > - promptCompiler()}> + !(hhCompilation || foundryCompilation) && promptCompiler()} + style={{ + cursor: (hhCompilation || foundryCompilation) ? 'not-allowed' : 'pointer', + opacity: (hhCompilation || foundryCompilation) ? 0.5 : 1 + }} + > { tooltipClasses="text-nowrap" tooltipText={} > - showCompilerLicense()}> + !(hhCompilation || foundryCompilation) && showCompilerLicense()} + style={{ + cursor: (hhCompilation || foundryCompilation) ? 'not-allowed' : 'pointer', + opacity: (hhCompilation || foundryCompilation) ? 0.5 : 1 + }} + > { solJsonBinData && solJsonBinData.selectorList && solJsonBinData.selectorList.length > 0 ? ( - ):null} +
+ +
+ ):null} -
+
- +
{platform === appPlatformTypes.desktop ? -
- +
+
:null}
{
+ {isFoundryProject && ( + + )} {isHardhatProject && (
{
)}
-
{ - // Track advanced configuration toggle - trackMatomoEvent({ category: 'compilerContainer', action: 'advancedConfigToggle', name: !toggleExpander ? 'expanded' : 'collapsed', isClick: true }) - toggleConfigurations() - }}> +
{ + if (hhCompilation || foundryCompilation) return + // Track advanced configuration toggle + trackMatomoEvent({ category: 'compilerContainer', action: 'advancedConfigToggle', name: !toggleExpander ? 'expanded' : 'collapsed', isClick: true }) + toggleConfigurations() + }} + style={{ + cursor: (hhCompilation || foundryCompilation) ? 'not-allowed' : 'pointer', + opacity: (hhCompilation || foundryCompilation) ? 0.5 : 1, + pointerEvents: (hhCompilation || foundryCompilation) ? 'none' : 'auto' + }} + >
{ + if (hhCompilation || foundryCompilation) return // Track advanced configuration toggle trackMatomoEvent({ category: 'compilerContainer', action: 'advancedConfigToggle', name: !toggleExpander ? 'expanded' : 'collapsed', isClick: true }) toggleConfigurations() diff --git a/libs/remix-ui/solidity-compiler/src/lib/components/compiler-dropdown.tsx b/libs/remix-ui/solidity-compiler/src/lib/components/compiler-dropdown.tsx index 7540ac28122..7c1a304f1a9 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/components/compiler-dropdown.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/components/compiler-dropdown.tsx @@ -19,6 +19,7 @@ interface compilerDropdownProps { handleLoadVersion: (url: string) => void, _shouldBeAdded: (version: string) => boolean, onlyDownloaded: boolean + disabled: boolean } export const CompilerDropdown = (props: compilerDropdownProps) => { @@ -27,9 +28,20 @@ export const CompilerDropdown = (props: compilerDropdownProps) => { const { customVersions, selectedVersion, defaultVersion, allversions, handleLoadVersion, _shouldBeAdded, onlyDownloaded } = props return ( - +
-
+
{customVersions.map((url, i) => { if (selectedVersion === url) return (custom) })} diff --git a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts index 3a3fc086d21..8181c1c6de7 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts @@ -179,10 +179,13 @@ export class CompileTabLogic { } else return false } - runCompiler (externalCompType) { + async runCompiler (externalCompType) { try { + this.api.saveCurrentFile() if (this.api.getFileManagerMode() === 'localhost' || this.api.isDesktop()) { if (externalCompType === 'hardhat') { + /* + => let the framework to use it's default config file const { currentVersion, optimize, runs } = this.compiler.state if (currentVersion) { const fileContent = `module.exports = { @@ -197,16 +200,18 @@ export class CompileTabLogic { ` const configFilePath = 'remix-compiler.config.js' this.api.writeFile(configFilePath, fileContent) - if (window._matomoManagerInstance) { - window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithHardhat') - } - this.api.compileWithHardhat(configFilePath).then((result) => { - this.api.logToTerminal({ type: 'log', value: result }) - }).catch((error) => { - this.api.logToTerminal({ type: 'error', value: error }) - }) + */ + if (window._matomoManagerInstance) { + window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithHardhat') } + this.api.compileWithHardhat().then((result) => { + }).catch((error) => { + this.api.logToTerminal({ type: 'error', value: error }) + }) + // } } else if (externalCompType === 'truffle') { + /* + => let the framework to use it's default config file const { currentVersion, optimize, runs, evmVersion } = this.compiler.state if (currentVersion) { const fileContent = `module.exports = { @@ -225,21 +230,47 @@ export class CompileTabLogic { }` const configFilePath = 'remix-compiler.config.js' this.api.writeFile(configFilePath, fileContent) - if (window._matomoManagerInstance) { - window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithTruffle') + */ + if (window._matomoManagerInstance) { + window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithTruffle') + } + this.api.compileWithTruffle().then((result) => { + }).catch((error) => { + this.api.logToTerminal({ type: 'error', value: error }) + }) + // } + } else if (externalCompType === 'foundry') { + /* + => let the framework to use it's default config file + const { currentVersion, optimize, runs } = this.compiler.state + if (currentVersion) { + const fileContent = `module.exports = { + solidity: '${currentVersion.substring(0, currentVersion.indexOf('+commit'))}', + settings: { + optimizer: { + enabled: ${optimize}, + runs: ${runs} + } + } } - this.api.compileWithTruffle(configFilePath).then((result) => { - this.api.logToTerminal({ type: 'log', value: result }) - }).catch((error) => { - this.api.logToTerminal({ type: 'error', value: error }) - }) + ` + const configFilePath = 'remix-compiler.config.js' + this.api.writeFile(configFilePath, fileContent) + */ + if (window._matomoManagerInstance) { + window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithFoundry') } + this.api.compileWithFoundry().then((result) => { + }).catch((error) => { + this.api.logToTerminal({ type: 'error', value: error }) + }) + // } } } - // TODO readd saving current file - this.api.saveCurrentFile() - const currentFile = this.api.currentFile - return this.compileFile(currentFile) + if (externalCompType === 'remix' || !externalCompType) { + const currentFile = this.api.currentFile + return this.compileFile(currentFile) + } } catch (err) { console.error(err) } diff --git a/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx b/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx index 17d69bbd205..1515a722e7e 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/solidity-compiler.tsx @@ -82,11 +82,12 @@ export const SolidityCompiler = (props: SolidityCompilerProps) => { api.onSetWorkspace = async (isLocalhost: boolean, workspaceName: string) => { const isDesktop = platform === appPlatformTypes.desktop - + const isHardhat = (isLocalhost || isDesktop) && (await compileTabLogic.isHardhatProject()) const isTruffle = (isLocalhost || isDesktop) && (await compileTabLogic.isTruffleProject()) const isFoundry = (isLocalhost || isDesktop) && (await compileTabLogic.isFoundryProject()) - + console.log('Solidity compiler detected workspace change', { isLocalhost, workspaceName, isDesktop, isFoundry }) + setState((prevState) => { return { ...prevState, diff --git a/libs/remix-ui/vertical-icons-panel/src/lib/remix-ui-vertical-icons-panel.tsx b/libs/remix-ui/vertical-icons-panel/src/lib/remix-ui-vertical-icons-panel.tsx index 0d2323978e5..3c686ce5533 100644 --- a/libs/remix-ui/vertical-icons-panel/src/lib/remix-ui-vertical-icons-panel.tsx +++ b/libs/remix-ui/vertical-icons-panel/src/lib/remix-ui-vertical-icons-panel.tsx @@ -98,10 +98,10 @@ const RemixUiVerticalIconsPanel = ({ verticalIconsPlugin, icons }: RemixUiVertic verticalIconsPlugin={verticalIconsPlugin} itemContextAction={itemContextAction} /> + p.profile.name === 'settings' || p.profile.name === 'pluginManager')} verticalIconsPlugin={verticalIconsPlugin} itemContextAction={itemContextAction} />
-
+
{scrollableRef.current && scrollableRef.current.scrollHeight > scrollableRef.current.clientHeight ? : null} - p.profile.name === 'settings' || p.profile.name === 'pluginManager')} verticalIconsPlugin={verticalIconsPlugin} itemContextAction={itemContextAction} /> {Registry.getInstance().get('platform').api.isDesktop() ? ( online ? ( diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index 8c5e923e42e..733110cc4b6 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1041,6 +1041,7 @@ export const checkoutRemoteBranch = async (branch: branch) => { export const openElectronFolder = async (path: string) => { await plugin.call('fs', 'openFolderInSameWindow', path) + } export const getElectronRecentFolders = async () => { diff --git a/libs/remixd/src/services/foundryClient.ts b/libs/remixd/src/services/foundryClient.ts index cf1447e0394..10aa577c441 100644 --- a/libs/remixd/src/services/foundryClient.ts +++ b/libs/remixd/src/services/foundryClient.ts @@ -23,7 +23,6 @@ export class FoundryClient extends PluginClient { this.methods = ['compile', 'sync'] this.onActivation = () => { console.log('Foundry plugin activated') - this.call('terminal', 'log', { type: 'log', value: 'Foundry plugin activated' }) this.startListening() } } diff --git a/libs/remixd/src/services/hardhatClient.ts b/libs/remixd/src/services/hardhatClient.ts index ccf02acda1e..01a9ea805bd 100644 --- a/libs/remixd/src/services/hardhatClient.ts +++ b/libs/remixd/src/services/hardhatClient.ts @@ -21,7 +21,6 @@ export class HardhatClient extends PluginClient { this.methods = ['compile', 'sync'] this.onActivation = () => { console.log('Hardhat plugin activated') - this.call('terminal', 'log', { type: 'log', value: 'Hardhat plugin activated' }) this.startListening() } }