Skip to content

Commit 9ac5703

Browse files
committed
Add call stack breadcrumb and call info banner to TraceDrawer
The deploy preview uses TraceDrawer (not TraceViewer) for the interactive trace playground. Add call context display directly to TraceDrawer: a breadcrumb bar showing nested call frames with clickable navigation, and a colored banner showing invoke/return/revert status at the current step.
1 parent 3c4dcbd commit 9ac5703

2 files changed

Lines changed: 220 additions & 0 deletions

File tree

packages/web/src/theme/ProgramExample/TraceDrawer.css

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,69 @@
127127
text-align: center;
128128
}
129129

130+
/* Call stack breadcrumb */
131+
.call-stack-bar {
132+
display: flex;
133+
flex-wrap: wrap;
134+
align-items: center;
135+
gap: 2px;
136+
padding: 4px 12px;
137+
font-size: 12px;
138+
background: var(--ifm-background-surface-color);
139+
border-bottom: 1px solid var(--ifm-color-emphasis-200);
140+
flex-shrink: 0;
141+
}
142+
143+
.call-stack-sep {
144+
color: var(--ifm-color-content-secondary);
145+
padding: 0 2px;
146+
user-select: none;
147+
}
148+
149+
.call-stack-frame-btn {
150+
background: none;
151+
border: 1px solid transparent;
152+
border-radius: 3px;
153+
padding: 1px 4px;
154+
cursor: pointer;
155+
font-family: var(--ifm-font-family-monospace);
156+
font-size: 12px;
157+
font-weight: 500;
158+
color: var(--ifm-color-primary);
159+
}
160+
161+
.call-stack-frame-btn:hover {
162+
background: var(--ifm-color-emphasis-100);
163+
border-color: var(--ifm-color-emphasis-300);
164+
}
165+
166+
/* Call info banner */
167+
.call-info-bar {
168+
padding: 4px 12px;
169+
font-size: 12px;
170+
font-weight: 500;
171+
flex-shrink: 0;
172+
border-bottom: 1px solid var(--ifm-color-emphasis-200);
173+
}
174+
175+
.call-info-invoke {
176+
background: var(--ifm-color-info-contrast-background);
177+
color: var(--ifm-color-info-darkest);
178+
border-left: 3px solid var(--ifm-color-info);
179+
}
180+
181+
.call-info-return {
182+
background: var(--ifm-color-success-contrast-background);
183+
color: var(--ifm-color-success-darkest);
184+
border-left: 3px solid var(--ifm-color-success);
185+
}
186+
187+
.call-info-revert {
188+
background: var(--ifm-color-danger-contrast-background);
189+
color: var(--ifm-color-danger-darkest);
190+
border-left: 3px solid var(--ifm-color-danger);
191+
}
192+
130193
/* Trace panels */
131194
.trace-panels {
132195
display: grid;

packages/web/src/theme/ProgramExample/TraceDrawer.tsx

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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">&#x203A;</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

Comments
 (0)