Skip to content

Commit 9cf00f4

Browse files
authored
Add call stack breadcrumb and call info panel (#189)
* Add call stack breadcrumb and call info panel components Surface invoke/return/revert context information in the trace viewer: a breadcrumb showing the current call stack, and a panel showing call details with async-resolved pointer ref values. New components: CallStackDisplay, CallInfoPanel New utilities: extractCallInfoFromInstruction, buildCallStack, buildPcToInstructionMap New types: CallInfo, CallFrame, ResolvedCallInfo, ResolvedPointerRef * Integrate CallStackDisplay and CallInfoPanel into TraceViewer The components were exported from programs-react but never rendered in the web package's TraceViewer. Add them to the layout: call stack breadcrumb in the header, call info panel at the top of the right sidebar. * 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. * Show always-visible call stack and fix duplicate frame bug - Call stack bar now always visible with "(top level)" empty state so users know the feature exists - Fix duplicate call stack frames: compiler emits invoke context on both the caller JUMP and callee entry JUMPDEST, so skip push if top frame already matches the same call - Applied fix to both TraceDrawer and programs-react utility
1 parent 2c36ef8 commit 9cf00f4

File tree

15 files changed

+1060
-0
lines changed

15 files changed

+1060
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
.call-info-panel {
2+
font-size: 0.9em;
3+
}
4+
5+
.call-info-banner {
6+
padding: 6px 10px;
7+
border-radius: 4px;
8+
font-weight: 500;
9+
margin-bottom: 6px;
10+
}
11+
12+
.call-info-banner-invoke {
13+
background: var(--programs-invoke-bg, #e8f4fd);
14+
color: var(--programs-invoke-text, #0969da);
15+
border-left: 3px solid var(--programs-invoke-accent, #0969da);
16+
}
17+
18+
.call-info-banner-return {
19+
background: var(--programs-return-bg, #dafbe1);
20+
color: var(--programs-return-text, #1a7f37);
21+
border-left: 3px solid var(--programs-return-accent, #1a7f37);
22+
}
23+
24+
.call-info-banner-revert {
25+
background: var(--programs-revert-bg, #ffebe9);
26+
color: var(--programs-revert-text, #cf222e);
27+
border-left: 3px solid var(--programs-revert-accent, #cf222e);
28+
}
29+
30+
.call-info-refs {
31+
display: flex;
32+
flex-direction: column;
33+
gap: 4px;
34+
padding: 4px 0;
35+
}
36+
37+
.call-info-ref {
38+
display: flex;
39+
align-items: baseline;
40+
gap: 6px;
41+
padding: 2px 0;
42+
}
43+
44+
.call-info-ref-label {
45+
font-weight: 500;
46+
color: var(--programs-text-muted, #888);
47+
min-width: 80px;
48+
text-align: right;
49+
flex-shrink: 0;
50+
}
51+
52+
.call-info-ref-resolved {
53+
font-family: monospace;
54+
font-size: 0.9em;
55+
word-break: break-all;
56+
}
57+
58+
.call-info-ref-error {
59+
color: var(--programs-error, #cf222e);
60+
font-style: italic;
61+
}
62+
63+
.call-info-ref-pending {
64+
color: var(--programs-text-muted, #888);
65+
font-style: italic;
66+
}
67+
68+
.call-info-ref-pointer {
69+
margin-top: 2px;
70+
}
71+
72+
.call-info-ref-pointer summary {
73+
cursor: pointer;
74+
color: var(--programs-text-muted, #888);
75+
font-size: 0.85em;
76+
}
77+
78+
.call-info-ref-pointer-json {
79+
font-size: 0.8em;
80+
padding: 4px 8px;
81+
background: var(--programs-bg-code, #f6f8fa);
82+
border-radius: 3px;
83+
overflow-x: auto;
84+
max-height: 200px;
85+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Panel showing call context info for the current instruction.
3+
*
4+
* Displays a banner for invoke/return/revert events and
5+
* lists resolved pointer ref values (arguments, return
6+
* data, etc.).
7+
*/
8+
9+
import React from "react";
10+
import {
11+
useTraceContext,
12+
type ResolvedCallInfo,
13+
type ResolvedPointerRef,
14+
} from "./TraceContext.js";
15+
16+
// CSS is expected to be imported by the consuming application
17+
// import "./CallInfoPanel.css";
18+
19+
export interface CallInfoPanelProps {
20+
/** Whether to show raw pointer JSON */
21+
showPointers?: boolean;
22+
/** Custom class name */
23+
className?: string;
24+
}
25+
26+
function formatBanner(info: ResolvedCallInfo): string {
27+
const name = info.identifier || "(anonymous)";
28+
29+
if (info.kind === "invoke") {
30+
const prefix =
31+
info.callType === "external"
32+
? "Calling (external)"
33+
: info.callType === "create"
34+
? "Creating"
35+
: "Calling";
36+
return `${prefix} ${name}()`;
37+
}
38+
39+
if (info.kind === "return") {
40+
return `Returned from ${name}()`;
41+
}
42+
43+
// revert
44+
if (info.panic !== undefined) {
45+
return `Reverted: panic 0x${info.panic.toString(16)}`;
46+
}
47+
return `Reverted in ${name}()`;
48+
}
49+
50+
function bannerClassName(kind: ResolvedCallInfo["kind"]): string {
51+
if (kind === "invoke") {
52+
return "call-info-banner-invoke";
53+
}
54+
if (kind === "return") {
55+
return "call-info-banner-return";
56+
}
57+
return "call-info-banner-revert";
58+
}
59+
60+
/**
61+
* Shows call context info when the current instruction
62+
* has an invoke, return, or revert context.
63+
*/
64+
export function CallInfoPanel({
65+
showPointers = false,
66+
className = "",
67+
}: CallInfoPanelProps): JSX.Element | null {
68+
const { currentCallInfo } = useTraceContext();
69+
70+
if (!currentCallInfo) {
71+
return null;
72+
}
73+
74+
return (
75+
<div className={`call-info-panel ${className}`.trim()}>
76+
<div
77+
className={`call-info-banner ${bannerClassName(currentCallInfo.kind)}`}
78+
>
79+
{formatBanner(currentCallInfo)}
80+
</div>
81+
82+
{currentCallInfo.pointerRefs.length > 0 && (
83+
<div className="call-info-refs">
84+
{currentCallInfo.pointerRefs.map((ref) => (
85+
<PointerRefItem
86+
key={ref.label}
87+
ref_={ref}
88+
showPointer={showPointers}
89+
/>
90+
))}
91+
</div>
92+
)}
93+
</div>
94+
);
95+
}
96+
97+
interface PointerRefItemProps {
98+
ref_: ResolvedPointerRef;
99+
showPointer: boolean;
100+
}
101+
102+
function PointerRefItem({
103+
ref_,
104+
showPointer,
105+
}: PointerRefItemProps): JSX.Element {
106+
return (
107+
<div className="call-info-ref">
108+
<span className="call-info-ref-label">{ref_.label}:</span>
109+
<span className="call-info-ref-value">
110+
{ref_.error ? (
111+
<span className="call-info-ref-error" title={ref_.error}>
112+
Error: {ref_.error}
113+
</span>
114+
) : ref_.value !== undefined ? (
115+
<code className="call-info-ref-resolved">{ref_.value}</code>
116+
) : (
117+
<span className="call-info-ref-pending">(resolving...)</span>
118+
)}
119+
</span>
120+
121+
{showPointer && !!ref_.pointer && (
122+
<details className="call-info-ref-pointer">
123+
<summary>Pointer</summary>
124+
<pre className="call-info-ref-pointer-json">
125+
{JSON.stringify(ref_.pointer, null, 2)}
126+
</pre>
127+
</details>
128+
)}
129+
</div>
130+
);
131+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
.call-stack {
2+
font-size: 0.85em;
3+
padding: 4px 8px;
4+
}
5+
6+
.call-stack-empty {
7+
color: var(--programs-text-muted, #888);
8+
}
9+
10+
.call-stack-breadcrumb {
11+
display: flex;
12+
flex-wrap: wrap;
13+
align-items: center;
14+
gap: 2px;
15+
}
16+
17+
.call-stack-separator {
18+
color: var(--programs-text-muted, #888);
19+
user-select: none;
20+
}
21+
22+
.call-stack-frame {
23+
display: inline-flex;
24+
align-items: center;
25+
background: none;
26+
border: 1px solid transparent;
27+
border-radius: 3px;
28+
padding: 1px 4px;
29+
cursor: pointer;
30+
font-family: inherit;
31+
font-size: inherit;
32+
color: var(--programs-link, #0366d6);
33+
}
34+
35+
.call-stack-frame:hover {
36+
background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05));
37+
border-color: var(--programs-border, #ddd);
38+
}
39+
40+
.call-stack-name {
41+
font-weight: 500;
42+
}
43+
44+
.call-stack-parens {
45+
color: var(--programs-text-muted, #888);
46+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Displays the current call stack as a breadcrumb trail.
3+
*/
4+
5+
import React from "react";
6+
import { useTraceContext } from "./TraceContext.js";
7+
8+
// CSS is expected to be imported by the consuming application
9+
// import "./CallStackDisplay.css";
10+
11+
export interface CallStackDisplayProps {
12+
/** Custom class name */
13+
className?: string;
14+
}
15+
16+
/**
17+
* Renders the call stack as a breadcrumb.
18+
*
19+
* Shows function names separated by arrows, e.g.:
20+
* main() -> transfer() -> _update()
21+
*/
22+
export function CallStackDisplay({
23+
className = "",
24+
}: CallStackDisplayProps): JSX.Element {
25+
const { callStack, jumpToStep } = useTraceContext();
26+
27+
if (callStack.length === 0) {
28+
return (
29+
<div className={`call-stack call-stack-empty ${className}`.trim()}>
30+
<span className="call-stack-empty-text">(top level)</span>
31+
</div>
32+
);
33+
}
34+
35+
return (
36+
<div className={`call-stack ${className}`.trim()}>
37+
<div className="call-stack-breadcrumb">
38+
{callStack.map((frame, index) => (
39+
<React.Fragment key={index}>
40+
{index > 0 && (
41+
<span className="call-stack-separator">{" -> "}</span>
42+
)}
43+
<button
44+
className="call-stack-frame"
45+
onClick={() => jumpToStep(frame.stepIndex)}
46+
title={
47+
`Step ${frame.stepIndex + 1}` +
48+
(frame.callType ? ` (${frame.callType})` : "")
49+
}
50+
type="button"
51+
>
52+
<span className="call-stack-name">
53+
{frame.identifier || "(anonymous)"}
54+
</span>
55+
<span className="call-stack-parens">()</span>
56+
</button>
57+
</React.Fragment>
58+
))}
59+
</div>
60+
</div>
61+
);
62+
}

0 commit comments

Comments
 (0)