From 3f2c797ddea27f9b894d4446f8975794d2795c1a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 13 Jul 2026 10:09:25 -0400 Subject: [PATCH 1/3] web: rework trace drawer layout and add instruction-object panel Fix the trace playground drawer so it fills the drawer height instead of collapsing to content height and leaving dead space below. The layout column now uses an explicit full-height grid track, and the instructions/state row grows while its header stays fixed. Render the full instruction list (removing the '...N above/below' elision) with the list scrolling internally within its panel; the active step is kept scrolled into view as the trace advances. Add a footer panel that shows the selected step's full ethdebug/format/instruction object (offset, operation, context) as formatted JSON, gated by a toggle that defaults on. --- .../src/theme/ProgramExample/TraceDrawer.css | 83 +++++++++++++++- .../src/theme/ProgramExample/TraceDrawer.tsx | 97 ++++++++++++++++--- 2 files changed, 164 insertions(+), 16 deletions(-) diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index 9f36d24c0..36f21f4f0 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -10,6 +10,9 @@ .trace-drawer-layout { display: grid; grid-template-columns: 1fr 1fr; + /* Fill the drawer height so both columns stretch full-height instead + * of collapsing to content height (which left dead space below). */ + grid-template-rows: minmax(0, 1fr); height: 100%; overflow: hidden; } @@ -204,11 +207,12 @@ border-left: 3px solid var(--ifm-color-danger); } -/* Trace panels */ +/* Trace panels - this row grows to fill the drawer's height */ .trace-panels { display: grid; grid-template-columns: 1fr 1fr; - flex: 1; + flex: 1 1 auto; + min-height: 0; overflow: hidden; gap: 1px; background: var(--ifm-color-emphasis-200); @@ -217,6 +221,24 @@ .trace-panel { background: var(--ifm-background-color); overflow: auto; + min-height: 0; +} + +/* Instructions panel: fixed header, internally scrolling list */ +.opcodes-panel { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.opcodes-panel .panel-header { + flex-shrink: 0; +} + +.opcodes-panel .opcode-list { + flex: 1 1 auto; + min-height: 0; + overflow: auto; } .panel-header { @@ -297,11 +319,62 @@ color: var(--ifm-color-content); } -.opcode-ellipsis { - padding: 4px 12px; +/* Instruction object footer - constant height, scrolls internally */ +.instruction-object-panel { + flex-shrink: 0; + display: flex; + flex-direction: column; + border-top: 1px solid var(--ifm-color-emphasis-200); + background: var(--ifm-background-color); +} + +.instruction-object-header { + position: static; + text-transform: none; + letter-spacing: normal; +} + +.instruction-object-toggle { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + cursor: pointer; + user-select: none; +} + +.instruction-object-toggle input { + cursor: pointer; + margin: 0; +} + +.instruction-object-toggle code { font-size: 11px; - color: var(--ifm-color-content-secondary); +} + +.instruction-object-body { + flex-shrink: 0; + min-height: 0; + max-height: 180px; + overflow: auto; +} + +.instruction-object-json { + margin: 0; + padding: 10px 12px; + font-family: var(--ifm-font-family-monospace); + font-size: 11px; + line-height: 1.5; + white-space: pre; + color: var(--ifm-color-content); + background: transparent; +} + +.instruction-object-empty { + padding: 10px 12px; + font-size: 12px; font-style: italic; + color: var(--ifm-color-content-secondary); } /* Stack display */ diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index cd9b37416..4382c78e7 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -56,6 +56,7 @@ function TraceDrawerContent(): JSX.Element { const [isTracing, setIsTracing] = useState(false); const [traceError, setTraceError] = useState(null); const [storage, setStorage] = useState>({}); + const [showInstructionObject, setShowInstructionObject] = useState(true); // Build PC -> instruction map for source highlighting const pcToInstruction = useMemo(() => { @@ -105,6 +106,17 @@ function TraceDrawerContent(): JSX.Element { return extractCallInfo(instruction.debug.context); }, [trace, currentStep, pcToInstruction]); + // Build the ethdebug/format instruction object for the current step + const currentFormatInstruction = useMemo(() => { + if (trace.length === 0 || currentStep >= trace.length) return undefined; + + const step = trace[currentStep]; + const instruction = pcToInstruction.get(step.pc); + if (!instruction) return undefined; + + return toFormatInstruction(instruction, step.pc); + }, [trace, currentStep, pcToInstruction]); + // Build call stack by scanning invoke/return/revert up to // current step const callStack = useMemo(() => { @@ -576,6 +588,36 @@ function TraceDrawerContent(): JSX.Element { )} + +
+
+ +
+ {showInstructionObject && ( +
+ {currentFormatInstruction ? ( +
+                        {JSON.stringify(currentFormatInstruction, null, 2)}
+                      
+ ) : ( +
+ No instruction data for this step. +
+ )} +
+ )} +
)} @@ -595,21 +637,22 @@ function OpcodeList({ currentStep, onStepClick, }: OpcodeListProps): JSX.Element { - // Show a window around the current step - const windowSize = 8; - const start = Math.max(0, currentStep - windowSize); - const end = Math.min(trace.length, currentStep + windowSize + 1); - const visibleSteps = trace.slice(start, end); + // Render the full instruction list; the panel scrolls internally. + // Keep the active step scrolled into view as the trace advances. + const activeRef = useRef(null); + + useEffect(() => { + activeRef.current?.scrollIntoView({ block: "nearest" }); + }, [currentStep]); return (
- {start > 0 &&
... {start} above
} - {visibleSteps.map((step, i) => { - const index = start + i; + {trace.map((step, index) => { const isActive = index === currentStep; return (
onStepClick(index)} > @@ -621,9 +664,6 @@ function OpcodeList({
); })} - {end < trace.length && ( -
... {trace.length - end} below
- )}
); } @@ -673,6 +713,41 @@ function StorageDisplay({ storage }: StorageDisplayProps): JSX.Element { ); } +/** + * Convert an Evm.Instruction into an ethdebug/format/instruction object + * for display (offset, operation, context). Mirrors the compiler's + * program-builder so the drawer shows the canonical format shape. + */ +function toFormatInstruction( + instruction: Evm.Instruction, + offset: number, +): Record { + const result: Record = { offset }; + + if (instruction.mnemonic) { + const operation: Record = { + mnemonic: instruction.mnemonic, + }; + + if (instruction.immediates && instruction.immediates.length > 0) { + operation.arguments = [ + "0x" + + instruction.immediates + .map((b) => b.toString(16).padStart(2, "0")) + .join(""), + ]; + } + + result.operation = operation; + } + + if (instruction.debug?.context) { + result.context = instruction.debug.context; + } + + return result; +} + function formatBigInt(value: bigint): string { const hex = value.toString(16); if (hex.length <= 8) { From ccdcabc4afe5ebd0aa22a790ee9ed7a18c60b33f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 13 Jul 2026 10:19:29 -0400 Subject: [PATCH 2/3] web: fill instructions panel height; default instruction-object off The instructions/state row still rendered at content height because .trace-panels is itself a grid with no explicit row track: its single implicit row was auto-sized, so the panels never stretched to the grid's flex-grown height. Give .trace-panels (and its mobile variant) an explicit minmax(0, 1fr) row so the panels fill the drawer and the instructions list scrolls internally. Default the ethdebug/format/instruction footer toggle to off. --- packages/web/src/theme/ProgramExample/TraceDrawer.css | 7 ++++++- packages/web/src/theme/ProgramExample/TraceDrawer.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index 36f21f4f0..3ab0c6676 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -211,6 +211,11 @@ .trace-panels { display: grid; grid-template-columns: 1fr 1fr; + /* Explicit full-height row: without this the single implicit grid row + * is auto-sized to content, so the panels stay at content height (the + * instructions list looked "fixed height" with dead space below) even + * though .trace-panels itself flex-grows. The row must fill the grid. */ + grid-template-rows: minmax(0, 1fr); flex: 1 1 auto; min-height: 0; overflow: hidden; @@ -482,6 +487,6 @@ .trace-panels { grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); } } diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 4382c78e7..488ff6850 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -56,7 +56,7 @@ function TraceDrawerContent(): JSX.Element { const [isTracing, setIsTracing] = useState(false); const [traceError, setTraceError] = useState(null); const [storage, setStorage] = useState>({}); - const [showInstructionObject, setShowInstructionObject] = useState(true); + const [showInstructionObject, setShowInstructionObject] = useState(false); // Build PC -> instruction map for source highlighting const pcToInstruction = useMemo(() => { From e34b7dd8ca3bfeda38826c2dbfa852e0a219cd9f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Mon, 13 Jul 2026 10:27:29 -0400 Subject: [PATCH 3/3] web: stop TraceViewer's max-height from pinning the drawer opcodes panel The instructions panel stayed fixed-height because the class names .opcodes-panel / .opcode-list are shared with the sibling TraceViewer component, whose global (non-module-scoped) stylesheet caps them with min-height: 200px; max-height: 300px. Those caps leaked onto the drawer. Scope the drawer's rules as .trace-panel.opcodes-panel to outrank the leaked single-class rule and reset max-height: none / min-height: 0 so the panel grows to fill its grid cell. --- .../src/theme/ProgramExample/TraceDrawer.css | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index 3ab0c6676..b3d017cde 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -229,20 +229,33 @@ min-height: 0; } -/* Instructions panel: fixed header, internally scrolling list */ -.opcodes-panel { +/* Instructions panel: fixed header, internally scrolling list. + * + * These selectors are scoped with `.trace-panel` on purpose. The class + * names `.opcodes-panel` / `.opcode-list` are also used by the sibling + * TraceViewer component, whose global stylesheet caps them with + * `min-height: 200px; max-height: 300px`. Docusaurus theme CSS is not + * module-scoped, so those caps leak onto this drawer and pin the + * instructions list to a fixed height. The `.trace-panel.opcodes-panel` + * selector (specificity 0,2,0) outranks the leaked `.opcodes-panel` + * (0,1,0), and `max-height: none` / `min-height: 0` remove the cap so + * the panel grows to fill its grid cell with the list scrolling inside. */ +.trace-panel.opcodes-panel { display: flex; flex-direction: column; overflow: hidden; + min-height: 0; + max-height: none; } -.opcodes-panel .panel-header { +.trace-panel.opcodes-panel .panel-header { flex-shrink: 0; } -.opcodes-panel .opcode-list { +.trace-panel.opcodes-panel .opcode-list { flex: 1 1 auto; min-height: 0; + max-height: none; overflow: auto; }