From 0c36cc4cbb7dff122dea6af33e63e6494db069ef Mon Sep 17 00:00:00 2001 From: ci-bot Date: Mon, 10 Nov 2025 21:50:32 +0100 Subject: [PATCH 01/22] foundry compile --- .../remix-ide/src/app/files/foundry-handle.js | 2 +- .../src/app/tabs/locales/en/solidity.json | 2 + .../remixdesktop/src/plugins/foundryPlugin.ts | 44 ++++++++++------ libs/remix-lib/src/types/ICompilerApi.ts | 1 + .../src/lib/api/compiler-api.ts | 10 ++++ .../src/lib/compiler-container.tsx | 51 +++++++++++++++++-- .../src/lib/logic/compileTabLogic.ts | 24 +++++++++ .../src/lib/solidity-compiler.tsx | 5 +- .../workspace/src/lib/actions/workspace.ts | 1 + 9 files changed, 118 insertions(+), 22 deletions(-) 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..fdf93cc050e 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/solidity.json +++ b/apps/remix-ide/src/app/tabs/locales/en/solidity.json @@ -12,7 +12,9 @@ "solidity.hideWarnings": "Hide warnings", "solidity.enableHardhat": "Enable Hardhat Compilation", "solidity.learnHardhat": "Learn how to use Hardhat Compilation", + "solidity.learnFoundry": "Learn how to use Foundry Compilation", "solidity.enableTruffle": "Enable Truffle Compilation", + "solidity.enableFoundry": "Enable Foundry Compilation", "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..693ebf5a3e9 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -110,33 +110,45 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { return true } + private async emitContract(file: string, cache) { + 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) + } + private async processArtifact() { if (!this.checkPath()) return const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder + console.log('Foundry compilation detected, processing artifact...', folderFiles) try { const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) + const currentFile = await this.call('fileManager', 'getCurrentFile') // 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) + if (file !== basename(currentFile)) { + await this.emitContract(file, cache) + } } - + if (folderFiles.includes(basename(currentFile))) { + console.log('emitting current file', currentFile, basename(currentFile)) + await this.emitContract(basename(currentFile), cache) // we emit the current file at the end to make sure it is the last one + } + 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.` }) + // 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) diff --git a/libs/remix-lib/src/types/ICompilerApi.ts b/libs/remix-lib/src/types/ICompilerApi.ts index cfb4ce832f3..c3c0afd3d4c 100644 --- a/libs/remix-lib/src/types/ICompilerApi.ts +++ b/libs/remix-lib/src/types/ICompilerApi.ts @@ -43,6 +43,7 @@ export interface ICompilerApi { logToTerminal: (log: terminalLog) => void + compileWithFoundry: (configPath: string) => Promise compileWithHardhat: (configPath: string) => Promise compileWithTruffle: (configPath: string) => Promise statusChanged: (data: { key: string, title?: string, type?: string }) => void, 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..e40c2db5e9f 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 @@ -110,6 +110,10 @@ export const CompilerApiMixin = (Base) => class extends Base { return this.call('hardhat', 'compile', configFile) } + compileWithFoundry (configFile) { + return this.call('foundry', 'compile', configFile) + } + compileWithTruffle (configFile) { return this.call('truffle', 'compile', configFile) } @@ -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) }) @@ -367,6 +376,7 @@ 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 if (await this.getAppParameter('foundry-compilation')) this.compileTabLogic.runCompiler('foundry') else this.compileTabLogic.runCompiler(undefined).catch((error) => { this.call('notification', 'toast', error.message) }) 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..7627cbabc4e 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() @@ -439,6 +447,7 @@ export const CompilerContainer = (props: CompilerContainerProps) => { let externalCompType 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') @@ -711,14 +720,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) } @@ -820,6 +836,35 @@ export const CompilerContainer = (props: CompilerContainerProps) => { + {isFoundryProject && ( + + )} {isHardhatProject && (
{ + this.api.logToTerminal({ type: 'log', value: result }) + }).catch((error) => { + this.api.logToTerminal({ type: 'error', value: error }) + }) + } } } // TODO readd saving current file 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/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 () => { From 615d685752d8694debcc4beb46f4b14f027d5ac3 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 25 Nov 2025 12:41:21 +0100 Subject: [PATCH 02/22] remove logs --- apps/remixdesktop/src/plugins/foundryPlugin.ts | 1 - apps/remixdesktop/src/plugins/hardhatPlugin.ts | 1 - libs/remixd/src/services/foundryClient.ts | 1 - libs/remixd/src/services/hardhatClient.ts | 1 - 4 files changed, 4 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 693ebf5a3e9..3ebda0a5fa0 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -40,7 +40,6 @@ 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 diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index ffef08e1c76..9af4bd32df8 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -39,7 +39,6 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { 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) 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() } } From 40c5f69deee15e10e173e787550cbd24f0df03b0 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 25 Nov 2025 12:41:41 +0100 Subject: [PATCH 03/22] update plugin manager postion and fix layout --- libs/remix-ui/app/src/lib/remix-app/style/remix-app.css | 1 - .../src/lib/remix-ui-vertical-icons-panel.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css b/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css index 2b7675e3a4e..6045d0ca147 100644 --- a/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css +++ b/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css @@ -9,7 +9,6 @@ pre { } .remixIDE { width : 100%; - height : 95vh; overflow : hidden; flex-direction : row; display : flex; 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 ? ( From 2706487c24ff8638b6c93d2e3de7ae8c5e15fad2 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 25 Nov 2025 12:57:01 +0100 Subject: [PATCH 04/22] fix layout --- libs/remix-ui/panel/src/lib/plugins/panel.css | 1 - 1 file changed, 1 deletion(-) 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%; From e0056bcac66ec56da8ba0cded2c7357e2aca264a Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 25 Nov 2025 22:25:32 +0100 Subject: [PATCH 05/22] let the framework to use default settings --- apps/remixdesktop/src/plugins/hardhatPlugin.ts | 4 ++-- libs/remix-lib/src/types/ICompilerApi.ts | 6 +++--- .../app/src/lib/remix-app/style/remix-app.css | 1 + .../solidity-compiler/src/lib/api/compiler-api.ts | 12 ++++++------ .../src/lib/logic/compileTabLogic.ts | 12 +++++++++--- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 9af4bd32df8..0763573ce3c 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -59,9 +59,9 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { } } - compile(configPath: string) { + compile() { return new Promise((resolve, reject) => { - const cmd = `npx hardhat compile --config ${utils.normalizePath(configPath)}` + const cmd = `npx hardhat compile` const options = { cwd: this.currentSharedFolder, shell: true } const child = spawn(cmd, options) let result = '' diff --git a/libs/remix-lib/src/types/ICompilerApi.ts b/libs/remix-lib/src/types/ICompilerApi.ts index c3c0afd3d4c..62a574ebdd0 100644 --- a/libs/remix-lib/src/types/ICompilerApi.ts +++ b/libs/remix-lib/src/types/ICompilerApi.ts @@ -43,9 +43,9 @@ export interface ICompilerApi { logToTerminal: (log: terminalLog) => void - compileWithFoundry: (configPath: string) => Promise - 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/app/src/lib/remix-app/style/remix-app.css b/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css index 6045d0ca147..2b7675e3a4e 100644 --- a/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css +++ b/libs/remix-ui/app/src/lib/remix-app/style/remix-app.css @@ -9,6 +9,7 @@ pre { } .remixIDE { width : 100%; + height : 95vh; overflow : hidden; flex-direction : row; display : flex; 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 e40c2db5e9f..100f0a60657 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 @@ -106,16 +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') } - compileWithFoundry (configFile) { - return this.call('foundry', 'compile', configFile) + compileWithFoundry () { + return this.call('foundry', 'compile') } - compileWithTruffle (configFile) { - return this.call('truffle', 'compile', configFile) + compileWithTruffle () { + return this.call('truffle', 'compile') } logToTerminal (content) { 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 82c462d9e10..30e1cfa3022 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts @@ -183,6 +183,7 @@ export class CompileTabLogic { try { if (this.api.getFileManagerMode() === 'localhost' || this.api.isDesktop()) { if (externalCompType === 'hardhat') { + /* const { currentVersion, optimize, runs } = this.compiler.state if (currentVersion) { const fileContent = `module.exports = { @@ -197,16 +198,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.compileWithHardhat().then((result) => { this.api.logToTerminal({ type: 'log', value: result }) }).catch((error) => { this.api.logToTerminal({ type: 'error', value: error }) }) } } else if (externalCompType === 'truffle') { + /* const { currentVersion, optimize, runs, evmVersion } = this.compiler.state if (currentVersion) { const fileContent = `module.exports = { @@ -225,16 +228,18 @@ export class CompileTabLogic { }` const configFilePath = 'remix-compiler.config.js' this.api.writeFile(configFilePath, fileContent) + */ if (window._matomoManagerInstance) { window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithTruffle') } - this.api.compileWithTruffle(configFilePath).then((result) => { + this.api.compileWithTruffle().then((result) => { this.api.logToTerminal({ type: 'log', value: result }) }).catch((error) => { this.api.logToTerminal({ type: 'error', value: error }) }) } } else if (externalCompType === 'foundry') { + /* const { currentVersion, optimize, runs } = this.compiler.state if (currentVersion) { const fileContent = `module.exports = { @@ -249,10 +254,11 @@ export class CompileTabLogic { ` const configFilePath = 'remix-compiler.config.js' this.api.writeFile(configFilePath, fileContent) + */ if (window._matomoManagerInstance) { window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithFoundry') } - this.api.compileWithFoundry(configFilePath).then((result) => { + this.api.compileWithFoundry().then((result) => { this.api.logToTerminal({ type: 'log', value: result }) }).catch((error) => { this.api.logToTerminal({ type: 'error', value: error }) From 20dded89f5a112ea65b658f6822f5fcbb6103e1b Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 25 Nov 2025 22:59:10 +0100 Subject: [PATCH 06/22] fix logging and do not compile with remix when external compiler are set --- .../remixdesktop/src/plugins/foundryPlugin.ts | 18 +++--- .../remixdesktop/src/plugins/hardhatPlugin.ts | 18 +++--- .../src/lib/api/compiler-api.ts | 2 +- .../src/lib/compiler-container.tsx | 2 +- .../src/lib/logic/compileTabLogic.ts | 57 ++++++++++--------- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 3ebda0a5fa0..6bdeb7ab98e 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -82,20 +82,22 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { const cmd = `forge build` 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' + if (data.toString().includes('Error')) { + this.call('terminal', 'log', { type: 'error', value: `[Foundry] ${data.toString()}` }) + } else { + const msg = `[Foundry] ${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: `[Foundry] ${err.toString()}` }) }) child.on('close', () => { - if (error && result) resolve(error + result) - else if (error) reject(error) - else resolve(result) + resolve('') }) }) } diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 0763573ce3c..8c8e69bc0cb 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -64,20 +64,22 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { const cmd = `npx hardhat compile` 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' + if (data.toString().includes('Error')) { + this.call('terminal', 'log', { type: 'error', value: `[Hardhat] ${data.toString()}` }) + } else { + const msg = `[Hardhat] ${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + this.call('terminal', 'log', { type: 'log', value: msg }) + } }) child.stderr.on('data', (err) => { - error += `[Hardhat Compilation]: ${err.toString()} \n` + error += err.toString() + '\n' + this.call('terminal', 'log', { type: 'error', value: `[Hardhat] ${err.toString()}` }) }) child.on('close', () => { - if (error && result) resolve(error + result) - else if (error) reject(error) - else resolve(result) + resolve('') }) }) } 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 100f0a60657..7878073ef98 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 @@ -377,7 +377,7 @@ export const CompilerApiMixin = (Base) => class extends Base { if (await this.getAppParameter('hardhat-compilation')) this.compileTabLogic.runCompiler('hardhat') else if (await this.getAppParameter('truffle-compilation')) this.compileTabLogic.runCompiler('truffle') else if (await this.getAppParameter('foundry-compilation')) this.compileTabLogic.runCompiler('foundry') - else this.compileTabLogic.runCompiler(undefined).catch((error) => { + else this.compileTabLogic.runCompiler('remix').catch((error) => { this.call('notification', 'toast', error.message) }) } else if (this.currentFile && this.currentFile.endsWith('.circom')) { 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 7627cbabc4e..e1065fee9fc 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -444,7 +444,7 @@ 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' 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 30e1cfa3022..74239d90f14 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts +++ b/libs/remix-ui/solidity-compiler/src/lib/logic/compileTabLogic.ts @@ -181,9 +181,11 @@ export class CompileTabLogic { 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 = { @@ -199,17 +201,17 @@ 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().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 = { @@ -229,17 +231,17 @@ export class CompileTabLogic { const configFilePath = 'remix-compiler.config.js' this.api.writeFile(configFilePath, fileContent) */ - if (window._matomoManagerInstance) { - window._matomoManagerInstance.trackEvent('compiler', 'runCompile', 'compileWithTruffle') - } - this.api.compileWithTruffle().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', '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 = { @@ -255,21 +257,20 @@ export class CompileTabLogic { 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) => { - this.api.logToTerminal({ type: 'log', value: result }) - }).catch((error) => { - this.api.logToTerminal({ type: 'error', value: error }) - }) + 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) } From 030ea63d71590ed277d46e9562f2970420b58551 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 27 Nov 2025 11:59:38 +0100 Subject: [PATCH 07/22] remove listening.. --- .../remixdesktop/src/plugins/foundryPlugin.ts | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 6bdeb7ab98e..e6bb96df4ab 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -54,12 +54,16 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { 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() + // this.listenOnFoundryCompilation() } else { - this.listenOnFoundryFolder() + // 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) + }) } - + /* listenOnFoundryFolder() { console.log('Foundry out folder doesn\'t exist... waiting for the compilation.') try { @@ -76,6 +80,7 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { console.log(e) } } + */ compile() { return new Promise((resolve, reject) => { @@ -96,12 +101,15 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { error += err.toString() + '\n' this.call('terminal', 'log', { type: 'error', value: `[Foundry] ${err.toString()}` }) }) - child.on('close', () => { + 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() @@ -109,25 +117,32 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { } if (!fs.existsSync(join(this.cachePath, 'solidity-files-cache.json'))) return false return true - } + }*/ private async emitContract(file: string, cache) { - const path = join(this.buildPath, file) // out/Counter.sol/ - const compilationResult = { - input: {}, - output: { - contracts: {}, - sources: {} - }, - inputSources: { sources: {}, target: '' }, - solcVersion: null, - compilationTarget: null + try { + console.log('emitContract', file, this.buildPath, this.cachePath) + 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) + console.log('Foundry compilation detected, emitting contract', file, compilationResult) + this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) + } catch (e) { + console.log('Error emitting contract', e) } - compilationResult.inputSources.target = file - await this.readContract(path, compilationResult, cache) - this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) } + /* private async processArtifact() { if (!this.checkPath()) return const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder @@ -157,15 +172,19 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { console.log(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 }) @@ -174,6 +193,7 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { this.watcher.on('unlink', async () => await this.triggerProcessArtifact()) // process the artifact on activation this.triggerProcessArtifact() + */ } catch (e) { console.log(e) } @@ -254,7 +274,9 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { 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) } } From 5bed4524ddd0715b251d2253428f784ec71d958a Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 27 Nov 2025 12:16:18 +0100 Subject: [PATCH 08/22] fix emitting new compiled contracts --- apps/remixdesktop/src/plugins/foundryPlugin.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index e6bb96df4ab..8aae7cbecde 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -88,13 +88,18 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { const options = { cwd: this.currentSharedFolder, shell: true } const child = spawn(cmd, options) let error = '' - child.stdout.on('data', (data) => { + child.stdout.on('data', async (data) => { if (data.toString().includes('Error')) { this.call('terminal', 'log', { type: 'error', value: `[Foundry] ${data.toString()}` }) } else { const msg = `[Foundry] ${data.toString()}` console.log('\x1b[32m%s\x1b[0m', msg) this.call('terminal', 'log', { type: 'log', value: msg }) + if (data.toString().includes('No files changed, compilation skipped')) { + 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) + } } }) child.stderr.on('data', (err) => { @@ -268,7 +273,8 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { bytecode: contentJSON.bytecode, deployedBytecode: contentJSON.deployedBytecode, methodIdentifiers: contentJSON.methodIdentifiers - } + }, + metadata: contentJSON.metadata } } From 98f7c15d5ac0dc0755889d75ca54a5f80b845988 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 27 Nov 2025 12:42:10 +0100 Subject: [PATCH 09/22] update label && fix foundry comp --- apps/remix-ide/src/app/tabs/locales/en/solidity.json | 6 +++--- libs/remix-core-plugin/src/lib/compiler-metadata.ts | 4 +++- .../solidity-compiler/src/lib/api/compiler-api.ts | 10 ++++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) 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 fdf93cc050e..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,11 +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.learnFoundry": "Learn how to use Foundry Compilation", - "solidity.enableTruffle": "Enable Truffle Compilation", - "solidity.enableFoundry": "Enable 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/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-ui/solidity-compiler/src/lib/api/compiler-api.ts b/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts index 7878073ef98..43bef5b3cda 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 @@ -359,6 +359,11 @@ 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.data.eventHandlers.onThemeChanged = (theme) => { const invert = theme.quality === 'dark' ? 1 : 0 const img = document.getElementById('swarmLogo') @@ -401,7 +406,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, From 7058d472c6e737aa801787c03084da97fd9aa05e Mon Sep 17 00:00:00 2001 From: ci-bot Date: Thu, 27 Nov 2025 13:11:46 +0100 Subject: [PATCH 10/22] fix solidity scan --- libs/remix-ui/helper/src/lib/solidity-scan.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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', From 1cae548e86c990c3774be67d482838e59f2cd557 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Fri, 28 Nov 2025 08:41:48 +0100 Subject: [PATCH 11/22] remove code --- .../remixdesktop/src/plugins/foundryPlugin.ts | 97 +------------------ 1 file changed, 2 insertions(+), 95 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 8aae7cbecde..c93119410f7 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -41,7 +41,6 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { async onActivation(): Promise { console.log('Foundry plugin activated') this.on('fs' as any, 'workingDirChanged', async (path: string) => { - console.log('workingDirChanged foundry', path) this.currentSharedFolder = path this.startListening() }) @@ -52,36 +51,12 @@ 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) }) } - /* - listenOnFoundryFolder() { - console.log('Foundry out folder doesn\'t exist... waiting for the compilation.') - 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() - } - }) - } catch (e) { - console.log(e) - } - } - */ - + compile() { return new Promise((resolve, reject) => { const cmd = `forge build` @@ -114,19 +89,9 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { }) }) } - /* - 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 emitContract(file: string, cache) { try { - console.log('emitContract', file, this.buildPath, this.cachePath) const path = join(this.buildPath, file) // out/Counter.sol/ const compilationResult = { input: {}, @@ -140,70 +105,12 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { } compilationResult.inputSources.target = file await this.readContract(path, compilationResult, cache) - console.log('Foundry compilation detected, emitting contract', file, compilationResult) this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) } catch (e) { console.log('Error emitting contract', e) } } - /* - private async processArtifact() { - if (!this.checkPath()) return - const folderFiles = await fs.promises.readdir(this.buildPath) // "out" folder - console.log('Foundry compilation detected, processing artifact...', folderFiles) - try { - const cache = JSON.parse(await fs.promises.readFile(join(this.cachePath, 'solidity-files-cache.json'), { encoding: 'utf-8' })) - const currentFile = await this.call('fileManager', 'getCurrentFile') - // name of folders are file names - for (const file of folderFiles) { - if (file !== basename(currentFile)) { - await this.emitContract(file, cache) - } - } - if (folderFiles.includes(basename(currentFile))) { - console.log('emitting current file', currentFile, basename(currentFile)) - await this.emitContract(basename(currentFile), cache) // we emit the current file at the end to make sure it is the last one - } - - 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) - - } catch (e) { - console.log(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) { const files = await fs.promises.readdir(contractFolder) for (const file of files) { From d4ac52a50b5173a44290527f752b358d776230c1 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Fri, 28 Nov 2025 09:11:25 +0100 Subject: [PATCH 12/22] fix hardhat comp --- .../remixdesktop/src/plugins/foundryPlugin.ts | 5 - .../remixdesktop/src/plugins/hardhatPlugin.ts | 249 +++++++----------- .../src/lib/api/compiler-api.ts | 7 + 3 files changed, 96 insertions(+), 165 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index c93119410f7..e417187aa63 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -70,11 +70,6 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { const msg = `[Foundry] ${data.toString()}` console.log('\x1b[32m%s\x1b[0m', msg) this.call('terminal', 'log', { type: 'log', value: msg }) - if (data.toString().includes('No files changed, compilation skipped')) { - 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) - } } }) child.stderr.on('data', (err) => { diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 8c8e69bc0cb..14948dd3267 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -38,10 +38,7 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { processingTimeout: NodeJS.Timeout async onActivation(): Promise { - console.log('Hardhat plugin activated') - this.on('fs' as any, 'workingDirChanged', async (path: string) => { - console.log('workingDirChanged hardhat', path) this.currentSharedFolder = path this.startListening() }) @@ -50,172 +47,104 @@ 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() - } - } + this.buildPath = utils.absolutePath('artifacts/contracts', this.currentSharedFolder) + this.on('fileManager', 'currentFileChanged', async (currentFile: string) => { + this.emitContract(basename(currentFile)) + }) + } - compile() { - return new Promise((resolve, reject) => { - const cmd = `npx hardhat compile` - 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: `[Hardhat] ${data.toString()}` }) - } else { - const msg = `[Hardhat] ${data.toString()}` - console.log('\x1b[32m%s\x1b[0m', msg) - this.call('terminal', 'log', { type: 'log', value: msg }) - } - }) - child.stderr.on('data', (err) => { - error += err.toString() + '\n' - this.call('terminal', 'log', { type: 'error', value: `[Hardhat] ${err.toString()}` }) - }) - child.on('close', () => { - resolve('') - }) + compile() { + return new Promise((resolve, reject) => { + const cmd = `npx hardhat compile` + 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: `[Hardhat] ${data.toString()}` }) + } else { + const msg = `[Hardhat] ${data.toString()}` + console.log('\x1b[32m%s\x1b[0m', msg) + this.call('terminal', 'log', { type: 'log', value: msg }) + } }) + child.stderr.on('data', (err) => { + error += err.toString() + '\n' + this.call('terminal', 'log', { type: 'error', value: `[Hardhat] ${err.toString()}` }) + }) + child.on('close', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) + resolve('') + }) + }) + } + + private async emitContract(file: string) { + console.log('emitContract', file, this.buildPath) + const contractFilePath = join(this.buildPath, file) + 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 } - - checkPath() { - if (!fs.existsSync(this.buildPath)) { - this.listenOnHardHatFolder() - return false - } - 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) - } - 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) - } - } + for (const file of files) { + console.log('Foundry emitContract checking file', file) + 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' }) + console.log('Hardhat compilation detected, feeding artifact file', path, content) + 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) + console.log('Hardhat compilation detected, emitting contract', compilationResult.target, compilationResult) + 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 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) - } + } + + async sync() { + console.log('syncing Hardhet with Remix...') + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) + } + + 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/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts b/libs/remix-ui/solidity-compiler/src/lib/api/compiler-api.ts index 43bef5b3cda..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 @@ -364,6 +364,13 @@ export const CompilerApiMixin = (Base) => class extends Base { 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') From 22019cddacf0c156b24464e9e8c2f41e7fe9236e Mon Sep 17 00:00:00 2001 From: ci-bot Date: Mon, 1 Dec 2025 14:28:11 +0100 Subject: [PATCH 13/22] listen on build done --- .../remixdesktop/src/plugins/foundryPlugin.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index e417187aa63..544bde9ee0e 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -55,6 +55,26 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { 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() + } + + listenOnFoundryCompilation() { + 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') + 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() { From 652ad9fc92fec764d31c678d526ba4d99d27f70b Mon Sep 17 00:00:00 2001 From: ci-bot Date: Mon, 1 Dec 2025 14:46:50 +0100 Subject: [PATCH 14/22] add listening to hardhat change --- .../remixdesktop/src/plugins/hardhatPlugin.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 14948dd3267..359b88c6008 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -48,10 +48,29 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { startListening() { 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)) + }) + this.watcher.on('add', async () => { + const currentFile = await this.call('fileManager', 'getCurrentFile') + this.emitContract(basename(currentFile)) + }) + } catch (e) { + console.log('listenOnHardhatCompilation', e) + } + } compile() { return new Promise((resolve, reject) => { From 3f8bd99b8d6c468ca7c7caeaedc1f0fea30815c4 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Mon, 1 Dec 2025 16:13:37 +0100 Subject: [PATCH 15/22] disable remix compiler if external framework --- .../src/lib/compiler-container.tsx | 92 +++++++++++++++---- .../src/lib/components/compiler-dropdown.tsx | 16 +++- 2 files changed, 86 insertions(+), 22 deletions(-) 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 e1065fee9fc..a838b035123 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -775,7 +775,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}
{
)}
-
{ - // 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) })} From 58fd5bca324cc53258256d158191019afa2b8b0e Mon Sep 17 00:00:00 2001 From: ci-bot Date: Mon, 1 Dec 2025 16:25:16 +0100 Subject: [PATCH 16/22] fix solidity scan call in desktop --- libs/endpoints-helper/src/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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; From 9138e39e126e6986857c511c5aebb8cad218912a Mon Sep 17 00:00:00 2001 From: ci-bot Date: Mon, 1 Dec 2025 16:31:18 +0100 Subject: [PATCH 17/22] rm log --- apps/remixdesktop/src/plugins/hardhatPlugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 359b88c6008..8f61776060e 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -115,7 +115,6 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { target: null } for (const file of files) { - console.log('Foundry emitContract checking file', file) 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' }) From b6470ed92ed1cac19d31657b08ad48e862ad2845 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 2 Dec 2025 09:48:48 +0100 Subject: [PATCH 18/22] rm logs --- apps/remixdesktop/src/plugins/foundryPlugin.ts | 6 +++--- apps/remixdesktop/src/plugins/hardhatPlugin.ts | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 544bde9ee0e..055e8558e41 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -85,16 +85,16 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { let error = '' child.stdout.on('data', async (data) => { if (data.toString().includes('Error')) { - this.call('terminal', 'log', { type: 'error', value: `[Foundry] ${data.toString()}` }) + this.call('terminal', 'log', { type: 'error', value: `${data.toString()}` }) } else { - const msg = `[Foundry] ${data.toString()}` + 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 += err.toString() + '\n' - this.call('terminal', 'log', { type: 'error', value: `[Foundry] ${err.toString()}` }) + this.call('terminal', 'log', { type: 'error', value: `${err.toString()}` }) }) child.on('close', async () => { const currentFile = await this.call('fileManager', 'getCurrentFile') diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 8f61776060e..e25b29747f9 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -80,16 +80,16 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { let error = '' child.stdout.on('data', (data) => { if (data.toString().includes('Error')) { - this.call('terminal', 'log', { type: 'error', value: `[Hardhat] ${data.toString()}` }) + this.call('terminal', 'log', { type: 'error', value: `${data.toString()}` }) } else { - const msg = `[Hardhat] ${data.toString()}` + 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 += err.toString() + '\n' - this.call('terminal', 'log', { type: 'error', value: `[Hardhat] ${err.toString()}` }) + this.call('terminal', 'log', { type: 'error', value: `${err.toString()}` }) }) child.on('close', async () => { const currentFile = await this.call('fileManager', 'getCurrentFile') @@ -100,7 +100,6 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { } private async emitContract(file: string) { - console.log('emitContract', file, this.buildPath) const contractFilePath = join(this.buildPath, file) const stat = await fs.promises.stat(contractFilePath) if (!stat.isDirectory()) return @@ -125,7 +124,6 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { const path = join(contractFilePath, jsonDbg.buildInfo) const content = await fs.promises.readFile(path, { encoding: 'utf-8' }) - console.log('Hardhat compilation detected, feeding artifact file', path, content) await this.feedContractArtifactFile(content, compilationResult) } if (compilationResult.target) { @@ -134,7 +132,6 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { ...compilationResult.output, contracts: { [compilationResult.target]: compilationResult.output.contracts[compilationResult.target] } } - console.log('Hardhat compilation detected, emitting contract', compilationResult.target, compilationResult) this.emit('compilationFinished', compilationResult.target, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) } } From 373016abfd2e6f46b4d625735813bff498dc36b4 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 2 Dec 2025 10:14:47 +0100 Subject: [PATCH 19/22] check file exist before emitting --- apps/remixdesktop/src/plugins/foundryPlugin.ts | 4 +++- apps/remixdesktop/src/plugins/hardhatPlugin.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 055e8558e41..3a4650694a4 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -119,6 +119,8 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { compilationTarget: null } compilationResult.inputSources.target = file + console.log('Foundry compilation detected, emitting contract', file, path) + 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) { @@ -126,7 +128,7 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { } } - 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) diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index e25b29747f9..04f75809f07 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -101,6 +101,7 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { 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) From eb664b0e7ffddc89893a84f263155abe1a7e2785 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 2 Dec 2025 11:34:22 +0100 Subject: [PATCH 20/22] log and fix ui --- apps/remixdesktop/src/plugins/foundryPlugin.ts | 4 ++-- apps/remixdesktop/src/plugins/hardhatPlugin.ts | 1 + .../remix-ui/solidity-compiler/src/lib/compiler-container.tsx | 4 ++++ .../solidity-compiler/src/lib/logic/compileTabLogic.ts | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 3a4650694a4..2cad788fc44 100644 --- a/apps/remixdesktop/src/plugins/foundryPlugin.ts +++ b/apps/remixdesktop/src/plugins/foundryPlugin.ts @@ -78,8 +78,9 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { } 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 error = '' @@ -119,7 +120,6 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { compilationTarget: null } compilationResult.inputSources.target = file - console.log('Foundry compilation detected, emitting contract', file, path) if (!fs.existsSync(path)) return await this.readContract(path, compilationResult, cache) this.emit('compilationFinished', compilationResult.compilationTarget, { sources: compilationResult.input }, 'soljson', compilationResult.output, compilationResult.solcVersion) diff --git a/apps/remixdesktop/src/plugins/hardhatPlugin.ts b/apps/remixdesktop/src/plugins/hardhatPlugin.ts index 04f75809f07..d3147d2d022 100644 --- a/apps/remixdesktop/src/plugins/hardhatPlugin.ts +++ b/apps/remixdesktop/src/plugins/hardhatPlugin.ts @@ -75,6 +75,7 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { 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 = '' 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 a838b035123..6e7677e19e9 100644 --- a/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx +++ b/libs/remix-ui/solidity-compiler/src/lib/compiler-container.tsx @@ -456,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') }) } @@ -480,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') }) } 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 74239d90f14..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,7 +179,7 @@ export class CompileTabLogic { } else return false } - runCompiler (externalCompType) { + async runCompiler (externalCompType) { try { this.api.saveCurrentFile() if (this.api.getFileManagerMode() === 'localhost' || this.api.isDesktop()) { From 954d98b70ace6748435e965341bd50c9ae8407c1 Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 2 Dec 2025 11:49:59 +0100 Subject: [PATCH 21/22] foundry hardhat mcp handler --- FOUNDRY_HARDHAT_COMMAND_IMPLEMENTATION.md | 286 ++++++++++ .../remixdesktop/src/plugins/foundryPlugin.ts | 47 +- .../remixdesktop/src/plugins/hardhatPlugin.ts | 49 +- .../src/remix-mcp-server/RemixMCPServer.ts | 5 + .../handlers/FoundryHardhatHandler.README.md | 308 ++++++++++ .../handlers/FoundryHardhatHandler.ts | 533 ++++++++++++++++++ .../src/remix-mcp-server/index.ts | 1 + 7 files changed, 1227 insertions(+), 2 deletions(-) create mode 100644 FOUNDRY_HARDHAT_COMMAND_IMPLEMENTATION.md create mode 100644 libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.README.md create mode 100644 libs/remix-ai-core/src/remix-mcp-server/handlers/FoundryHardhatHandler.ts 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/remixdesktop/src/plugins/foundryPlugin.ts b/apps/remixdesktop/src/plugins/foundryPlugin.ts index 2cad788fc44..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'] } @@ -208,6 +208,51 @@ class FoundryPluginClient extends ElectronBasePluginRemixdClient { 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 d3147d2d022..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'] } @@ -145,6 +145,53 @@ class HardhatPluginClient extends ElectronBasePluginRemixdClient { 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 + } + + 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 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'; From fdf6cdbed994b3447ccf76e0c1924cc42d78827f Mon Sep 17 00:00:00 2001 From: ci-bot Date: Tue, 2 Dec 2025 12:06:59 +0100 Subject: [PATCH 22/22] when transcribing, append to the chat box --- AUDIO_TRANSCRIPTION_UX_IMPROVEMENT.md | 169 ++++++++++++++++++ .../remix-ui-remix-ai-assistant.tsx | 30 ++-- 2 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 AUDIO_TRANSCRIPTION_UX_IMPROVEMENT.md 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/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()) {