@@ -85,6 +85,52 @@ function TraceDrawerContent(): JSX.Element {
8585 return extractVariables ( instruction . debug . context ) ;
8686 } , [ trace , currentStep , pcToInstruction ] ) ;
8787
88+ // Extract call info from current instruction context
89+ const currentCallInfo = useMemo ( ( ) => {
90+ if ( trace . length === 0 || currentStep >= trace . length ) {
91+ return undefined ;
92+ }
93+
94+ const step = trace [ currentStep ] ;
95+ const instruction = pcToInstruction . get ( step . pc ) ;
96+ if ( ! instruction ?. debug ?. context ) return undefined ;
97+
98+ return extractCallInfo ( instruction . debug . context ) ;
99+ } , [ trace , currentStep , pcToInstruction ] ) ;
100+
101+ // Build call stack by scanning invoke/return/revert up to
102+ // current step
103+ const callStack = useMemo ( ( ) => {
104+ const frames : Array < {
105+ identifier ?: string ;
106+ stepIndex : number ;
107+ callType ?: string ;
108+ } > = [ ] ;
109+
110+ for ( let i = 0 ; i <= currentStep && i < trace . length ; i ++ ) {
111+ const step = trace [ i ] ;
112+ const instruction = pcToInstruction . get ( step . pc ) ;
113+ if ( ! instruction ?. debug ?. context ) continue ;
114+
115+ const info = extractCallInfo ( instruction . debug . context ) ;
116+ if ( ! info ) continue ;
117+
118+ if ( info . kind === "invoke" ) {
119+ frames . push ( {
120+ identifier : info . identifier ,
121+ stepIndex : i ,
122+ callType : info . callType ,
123+ } ) ;
124+ } else if ( info . kind === "return" || info . kind === "revert" ) {
125+ if ( frames . length > 0 ) {
126+ frames . pop ( ) ;
127+ }
128+ }
129+ }
130+
131+ return frames ;
132+ } , [ trace , currentStep , pcToInstruction ] ) ;
133+
88134 // Compile source and run trace in one shot.
89135 // Takes source directly to avoid stale-state issues.
90136 const compileAndTrace = useCallback ( async ( sourceCode : string ) => {
@@ -298,6 +344,33 @@ function TraceDrawerContent(): JSX.Element {
298344 </ button >
299345 </ div >
300346
347+ { callStack . length > 0 && (
348+ < div className = "call-stack-bar" >
349+ { callStack . map ( ( frame , i ) => (
350+ < React . Fragment key = { frame . stepIndex } >
351+ { i > 0 && (
352+ < span className = "call-stack-sep" > ›</ span >
353+ ) }
354+ < button
355+ className = "call-stack-frame-btn"
356+ onClick = { ( ) => setCurrentStep ( frame . stepIndex ) }
357+ type = "button"
358+ >
359+ { frame . identifier || "(anonymous)" }
360+ </ button >
361+ </ React . Fragment >
362+ ) ) }
363+ </ div >
364+ ) }
365+
366+ { currentCallInfo && (
367+ < div
368+ className = { `call-info-bar call-info-${ currentCallInfo . kind } ` }
369+ >
370+ { formatCallBanner ( currentCallInfo ) }
371+ </ div >
372+ ) }
373+
301374 < div className = "trace-panels" >
302375 < div className = "trace-panel opcodes-panel" >
303376 < div className = "panel-header" > Instructions</ div >
@@ -468,6 +541,90 @@ function VariablesDisplay({ variables }: VariablesDisplayProps): JSX.Element {
468541 ) ;
469542}
470543
544+ /**
545+ * Info about a call context (invoke/return/revert).
546+ */
547+ interface CallInfoResult {
548+ kind : "invoke" | "return" | "revert" ;
549+ identifier ?: string ;
550+ callType ?: string ;
551+ }
552+
553+ /**
554+ * Extract call info from an ethdebug format context object.
555+ */
556+ function extractCallInfo ( context : unknown ) : CallInfoResult | undefined {
557+ if ( ! context || typeof context !== "object" ) {
558+ return undefined ;
559+ }
560+
561+ const ctx = context as Record < string , unknown > ;
562+
563+ if ( "invoke" in ctx && ctx . invoke ) {
564+ const inv = ctx . invoke as Record < string , unknown > ;
565+ let callType : string | undefined ;
566+ if ( "jump" in inv ) callType = "internal" ;
567+ else if ( "message" in inv ) callType = "external" ;
568+ else if ( "create" in inv ) callType = "create" ;
569+
570+ return {
571+ kind : "invoke" ,
572+ identifier : inv . identifier as string | undefined ,
573+ callType,
574+ } ;
575+ }
576+
577+ if ( "return" in ctx && ctx . return ) {
578+ const ret = ctx . return as Record < string , unknown > ;
579+ return {
580+ kind : "return" ,
581+ identifier : ret . identifier as string | undefined ,
582+ } ;
583+ }
584+
585+ if ( "revert" in ctx && ctx . revert ) {
586+ const rev = ctx . revert as Record < string , unknown > ;
587+ return {
588+ kind : "revert" ,
589+ identifier : rev . identifier as string | undefined ,
590+ } ;
591+ }
592+
593+ // Walk gather/pick
594+ if ( "gather" in ctx && Array . isArray ( ctx . gather ) ) {
595+ for ( const sub of ctx . gather ) {
596+ const info = extractCallInfo ( sub ) ;
597+ if ( info ) return info ;
598+ }
599+ }
600+
601+ if ( "pick" in ctx && Array . isArray ( ctx . pick ) ) {
602+ for ( const sub of ctx . pick ) {
603+ const info = extractCallInfo ( sub ) ;
604+ if ( info ) return info ;
605+ }
606+ }
607+
608+ return undefined ;
609+ }
610+
611+ /**
612+ * Format a call info banner string.
613+ */
614+ function formatCallBanner ( info : CallInfoResult ) : string {
615+ const name = info . identifier || "(anonymous)" ;
616+ switch ( info . kind ) {
617+ case "invoke" : {
618+ const prefix = info . callType === "create" ? "Creating" : "Calling" ;
619+ return `${ prefix } ${ name } ()` ;
620+ }
621+ case "return" :
622+ return `Returned from ${ name } ()` ;
623+ case "revert" :
624+ return `Reverted in ${ name } ()` ;
625+ }
626+ }
627+
471628/**
472629 * Extract variables from an ethdebug format context object.
473630 */
0 commit comments