Skip to content

Commit 66ffff4

Browse files
authored
Merge pull request #6799 from remix-project-org/debugger_v3
Debugger part 4
2 parents 68fc571 + 6d35e45 commit 66ffff4

File tree

11 files changed

+523
-136
lines changed

11 files changed

+523
-136
lines changed

apps/remix-ide-e2e/src/tests/mcp_debugging_tools.test.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,100 @@ module.exports = {
876876
}
877877
browser.assert.ok(data.success || data.error, 'Decode state variable should execute');
878878
});
879+
},
880+
881+
'Should test get_stack_at tool': function (browser: NightwatchBrowser) {
882+
browser
883+
.executeAsync(function (done) {
884+
const aiPlugin = (window as any).getRemixAIPlugin;
885+
886+
if (!aiPlugin?.remixMCPServer) {
887+
done({ error: 'RemixMCPServer not available' });
888+
return;
889+
}
890+
891+
aiPlugin.remixMCPServer.handleMessage({
892+
method: 'tools/call',
893+
params: {
894+
name: 'get_stack_at',
895+
arguments: {
896+
step: 100
897+
}
898+
},
899+
id: 'test-get-stack-at'
900+
}).then(function (result) {
901+
if (result.error) {
902+
done({
903+
success: false,
904+
error: result.error.message || JSON.stringify(result.error)
905+
});
906+
return;
907+
}
908+
const resultData = JSON.parse(result.result?.content?.[0]?.text || '{}');
909+
done({
910+
success: !result.error,
911+
hasStack: !!resultData?.stack,
912+
step: resultData?.step,
913+
stackDepth: resultData?.metadata?.stackDepth
914+
});
915+
}).catch(function (error) {
916+
done({ error: error.message });
917+
});
918+
}, [], function (result) {
919+
const data = result.value as any;
920+
if (!data || data.error) {
921+
console.error('Get stack at error:', data?.error || 'No data returned');
922+
return;
923+
}
924+
browser.assert.ok(data.success || data.error, 'Get stack at should execute');
925+
});
926+
},
927+
928+
'Should test get_scopes_with_root tool': function (browser: NightwatchBrowser) {
929+
browser
930+
.executeAsync(function (done) {
931+
const aiPlugin = (window as any).getRemixAIPlugin;
932+
933+
if (!aiPlugin?.remixMCPServer) {
934+
done({ error: 'RemixMCPServer not available' });
935+
return;
936+
}
937+
938+
aiPlugin.remixMCPServer.handleMessage({
939+
method: 'tools/call',
940+
params: {
941+
name: 'get_scopes_with_root',
942+
arguments: {
943+
rootScopeId: '1'
944+
}
945+
},
946+
id: 'test-get-scopes-with-root'
947+
}).then(function (result) {
948+
if (result.error) {
949+
done({
950+
success: false,
951+
error: result.error.message || JSON.stringify(result.error)
952+
});
953+
return;
954+
}
955+
const resultData = JSON.parse(result.result?.content?.[0]?.text || '{}');
956+
done({
957+
success: !result.error,
958+
hasScopes: !!resultData?.scopes,
959+
rootScopeId: resultData?.rootScopeId,
960+
totalScopes: resultData?.metadata?.totalScopes,
961+
hasDepthLimit: !!resultData?.metadata?.depthLimit
962+
});
963+
}).catch(function (error) {
964+
done({ error: error.message });
965+
});
966+
}, [], function (result) {
967+
const data = result.value as any;
968+
if (!data || data.error) {
969+
console.error('Get scopes with root error:', data?.error || 'No data returned');
970+
return;
971+
}
972+
browser.assert.ok(data.success || data.error, 'Get scopes with root should execute');
973+
});
879974
}
880975
};

apps/remix-ide/src/app/tabs/debugger-tab.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as packageJson from '../../../../../package.json'
55
import React from 'react' // eslint-disable-line
66
import { bleach } from '@remix-ui/helper'
77
import { compilationFinishedToastMsg, compilingToastMsg, notFoundToastMsg, sourceVerificationNotAvailableToastMsg } from '@remix-ui/helper'
8+
import { ScopeFilterMode } from '@remix-project/remix-debug'
89

910
const css = require('./styles/debugger-tab-styles') // eslint-disable-line
1011

@@ -27,7 +28,9 @@ const profile = {
2728
'jumpTo',
2829
'getCallTreeScopes',
2930
'getAllDebugCache',
30-
'getCurrentSourceLocation'
31+
'getCurrentSourceLocation',
32+
'getScopesAsNestedJSON',
33+
'getStackAt'
3134
],
3235
events: [],
3336
icon: 'assets/img/debuggerLogo.webp',
@@ -277,6 +280,11 @@ export default class DebuggerTab extends DebuggerApiMixin(ViewPlugin) {
277280
return this.debuggerBackend.debugger.callTree.getScopes()
278281
}
279282

283+
getScopesAsNestedJSON (filterMode: ScopeFilterMode, rootScopeId?: string) {
284+
if (!this.debuggerBackend) return null
285+
return this.debuggerBackend.debugger.callTree.getScopesAsNestedJSON(filterMode, rootScopeId)
286+
}
287+
280288
/**
281289
* Retrieves all trace cache data accumulated during transaction execution debugging.
282290
* The trace cache stores comprehensive metadata about the execution trace including calls, storage changes, memory changes, and more.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Scope Processing Helper - Provides utilities for processing debugging scope data
3+
*/
4+
5+
import { NestedScope, traceHelper, StepDetail } from '@remix-project/remix-debug';
6+
7+
export interface ProcessedScope {
8+
scopeId: string;
9+
functionName?: string;
10+
variableCount: number;
11+
variableNames: string[];
12+
stepRange: { first: number; last: number };
13+
gasCost?: number;
14+
isCreation?: boolean;
15+
isExternalCall?: boolean;
16+
reverted?: {
17+
step: StepDetail
18+
line?: number
19+
}
20+
opcode?: string;
21+
children: ProcessedScope[] | string | null;
22+
childCount: number;
23+
totalDescendants: number;
24+
message?: string;
25+
}
26+
27+
/**
28+
* Process a scope with depth limiting to prevent context overflow
29+
* @param scope - The nested scope to process
30+
* @param depth - Current depth (default: 0)
31+
* @param maxDepth - Maximum depth to process (default: 3)
32+
* @returns Processed scope with depth-limited children
33+
*/
34+
export function processScope(
35+
scope: NestedScope,
36+
depth: number = 0,
37+
maxDepth: number = 3
38+
): ProcessedScope {
39+
const processed: ProcessedScope = {
40+
scopeId: scope.scopeId,
41+
functionName: scope.functionDefinition ? scope.functionDefinition.name : undefined,
42+
variableCount: scope.locals ? Object.keys(scope.locals).length : 0,
43+
variableNames: scope.locals ? Object.keys(scope.locals) : [],
44+
stepRange: { first: scope.firstStep, last: scope.lastStep },
45+
gasCost: scope.gasCost,
46+
isCreation: scope.isCreation,
47+
isExternalCall: scope.isCreation || traceHelper.isCallInstruction(scope.opcodeInfo),
48+
reverted: scope.reverted,
49+
opcode: scope.opcodeInfo?.op,
50+
children: null,
51+
childCount: 0,
52+
totalDescendants: 0
53+
};
54+
55+
// Process children with depth limit
56+
if (scope.children && scope.children.length > 0) {
57+
processed.childCount = scope.children.length;
58+
59+
if (depth < maxDepth) {
60+
// Recursively process children if under depth limit
61+
processed.children = scope.children.map(child => processScope(child, depth + 1, maxDepth));
62+
processed.totalDescendants = scope.children.reduce((total, child) => {
63+
const childProcessed = processScope(child, depth + 1, maxDepth);
64+
return total + 1 + (childProcessed.totalDescendants || 0);
65+
}, 0);
66+
} else {
67+
// At depth limit, provide guidance to use get_scopes_with_root tool
68+
processed.children = null;
69+
processed.message = `descending the tree can be done using the tool get_scopes_with_root. scope ids: (${scope.children.map(el => el.scopeId).join(' , ')})`;
70+
processed.totalDescendants = scope.children.length; // Just count direct children
71+
}
72+
}
73+
74+
return processed;
75+
}
76+
77+
/**
78+
* Process an array of scopes with depth limiting
79+
* @param scopes - Array of nested scopes to process
80+
* @param maxDepth - Maximum depth to process (default: 3)
81+
* @returns Array of processed scopes
82+
*/
83+
export function processScopes(
84+
scopes: NestedScope[],
85+
maxDepth: number = 3
86+
): ProcessedScope[] {
87+
return scopes.map(scope => processScope(scope, 0, maxDepth));
88+
}
89+
90+
/**
91+
* Count all scopes including nested ones
92+
* @param scopes - Array of processed scopes
93+
* @returns Total count of all scopes
94+
*/
95+
export function countAllScopes(scopes: ProcessedScope[]): number {
96+
return scopes.reduce((total, scope) => {
97+
const childCount = Array.isArray(scope.children) ? countAllScopes(scope.children) : 0;
98+
return total + 1 + childCount;
99+
}, 0);
100+
}
101+
102+
/**
103+
* Count all variables across all scopes
104+
* @param scopes - Array of processed scopes
105+
* @returns Total count of all variables
106+
*/
107+
export function countAllVariables(scopes: ProcessedScope[]): number {
108+
return scopes.reduce((total, scope) => {
109+
const scopeVars = scope.variableCount || 0;
110+
const childVars = Array.isArray(scope.children) ? countAllVariables(scope.children) : 0;
111+
return total + scopeVars + childVars;
112+
}, 0);
113+
}
114+
115+
/**
116+
* Get function summary across all scopes
117+
* @param scopes - Array of processed scopes
118+
* @returns Array of function summaries
119+
*/
120+
export function getFunctionSummary(scopes: ProcessedScope[]): Array<{
121+
name?: string;
122+
scopeId: string;
123+
variableCount: number;
124+
variableNames: string[];
125+
childCount: number;
126+
stepRange: { first: number; last: number };
127+
}> {
128+
const functions: Array<{
129+
name?: string;
130+
scopeId: string;
131+
variableCount: number;
132+
variableNames: string[];
133+
childCount: number;
134+
stepRange: { first: number; last: number };
135+
}> = [];
136+
137+
const collectFunctions = (scopeList: ProcessedScope[]) => {
138+
for (const scope of scopeList) {
139+
if (scope.functionName) {
140+
functions.push({
141+
name: scope.functionName,
142+
scopeId: scope.scopeId,
143+
variableCount: scope.variableCount,
144+
variableNames: scope.variableNames,
145+
childCount: scope.childCount,
146+
stepRange: scope.stepRange
147+
});
148+
}
149+
if (Array.isArray(scope.children)) {
150+
collectFunctions(scope.children);
151+
}
152+
}
153+
};
154+
155+
collectFunctions(scopes);
156+
return functions;
157+
}

libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -902,8 +902,10 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer {
902902
this._resources.register(tutorialsProvider);
903903

904904
// Register Amp resource provider
905-
const ampProvider = new AmpResourceProvider(this._plugin);
906-
this._resources.register(ampProvider);
905+
/*
906+
const ampProvider = new AmpResourceProvider(this._plugin);
907+
this._resources.register(ampProvider);
908+
*/
907909

908910
// Register debugging resource provider
909911
const debuggingProvider = new DebuggingResourceProvider(this._plugin);

0 commit comments

Comments
 (0)