Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions packages/programs-react/src/components/CallInfoPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.call-info-panel {
font-size: 0.9em;
}

.call-info-banner {
padding: 6px 10px;
border-radius: 4px;
font-weight: 500;
margin-bottom: 6px;
}

.call-info-banner-invoke {
background: var(--programs-invoke-bg, #e8f4fd);
color: var(--programs-invoke-text, #0969da);
border-left: 3px solid var(--programs-invoke-accent, #0969da);
}

.call-info-banner-return {
background: var(--programs-return-bg, #dafbe1);
color: var(--programs-return-text, #1a7f37);
border-left: 3px solid var(--programs-return-accent, #1a7f37);
}

.call-info-banner-revert {
background: var(--programs-revert-bg, #ffebe9);
color: var(--programs-revert-text, #cf222e);
border-left: 3px solid var(--programs-revert-accent, #cf222e);
}

.call-info-refs {
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px 0;
}

.call-info-ref {
display: flex;
align-items: baseline;
gap: 6px;
padding: 2px 0;
}

.call-info-ref-label {
font-weight: 500;
color: var(--programs-text-muted, #888);
min-width: 80px;
text-align: right;
flex-shrink: 0;
}

.call-info-ref-resolved {
font-family: monospace;
font-size: 0.9em;
word-break: break-all;
}

.call-info-ref-error {
color: var(--programs-error, #cf222e);
font-style: italic;
}

.call-info-ref-pending {
color: var(--programs-text-muted, #888);
font-style: italic;
}

.call-info-ref-pointer {
margin-top: 2px;
}

.call-info-ref-pointer summary {
cursor: pointer;
color: var(--programs-text-muted, #888);
font-size: 0.85em;
}

.call-info-ref-pointer-json {
font-size: 0.8em;
padding: 4px 8px;
background: var(--programs-bg-code, #f6f8fa);
border-radius: 3px;
overflow-x: auto;
max-height: 200px;
}
131 changes: 131 additions & 0 deletions packages/programs-react/src/components/CallInfoPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Panel showing call context info for the current instruction.
*
* Displays a banner for invoke/return/revert events and
* lists resolved pointer ref values (arguments, return
* data, etc.).
*/

import React from "react";
import {
useTraceContext,
type ResolvedCallInfo,
type ResolvedPointerRef,
} from "./TraceContext.js";

// CSS is expected to be imported by the consuming application
// import "./CallInfoPanel.css";

export interface CallInfoPanelProps {
/** Whether to show raw pointer JSON */
showPointers?: boolean;
/** Custom class name */
className?: string;
}

function formatBanner(info: ResolvedCallInfo): string {
const name = info.identifier || "(anonymous)";

if (info.kind === "invoke") {
const prefix =
info.callType === "external"
? "Calling (external)"
: info.callType === "create"
? "Creating"
: "Calling";
return `${prefix} ${name}()`;
}

if (info.kind === "return") {
return `Returned from ${name}()`;
}

// revert
if (info.panic !== undefined) {
return `Reverted: panic 0x${info.panic.toString(16)}`;
}
return `Reverted in ${name}()`;
}

function bannerClassName(kind: ResolvedCallInfo["kind"]): string {
if (kind === "invoke") {
return "call-info-banner-invoke";
}
if (kind === "return") {
return "call-info-banner-return";
}
return "call-info-banner-revert";
}

/**
* Shows call context info when the current instruction
* has an invoke, return, or revert context.
*/
export function CallInfoPanel({
showPointers = false,
className = "",
}: CallInfoPanelProps): JSX.Element | null {
const { currentCallInfo } = useTraceContext();

if (!currentCallInfo) {
return null;
}

return (
<div className={`call-info-panel ${className}`.trim()}>
<div
className={`call-info-banner ${bannerClassName(currentCallInfo.kind)}`}
>
{formatBanner(currentCallInfo)}
</div>

{currentCallInfo.pointerRefs.length > 0 && (
<div className="call-info-refs">
{currentCallInfo.pointerRefs.map((ref) => (
<PointerRefItem
key={ref.label}
ref_={ref}
showPointer={showPointers}
/>
))}
</div>
)}
</div>
);
}

interface PointerRefItemProps {
ref_: ResolvedPointerRef;
showPointer: boolean;
}

function PointerRefItem({
ref_,
showPointer,
}: PointerRefItemProps): JSX.Element {
return (
<div className="call-info-ref">
<span className="call-info-ref-label">{ref_.label}:</span>
<span className="call-info-ref-value">
{ref_.error ? (
<span className="call-info-ref-error" title={ref_.error}>
Error: {ref_.error}
</span>
) : ref_.value !== undefined ? (
<code className="call-info-ref-resolved">{ref_.value}</code>
) : (
<span className="call-info-ref-pending">(resolving...)</span>
)}
</span>

{showPointer && !!ref_.pointer && (
<details className="call-info-ref-pointer">
<summary>Pointer</summary>
<pre className="call-info-ref-pointer-json">
{JSON.stringify(ref_.pointer, null, 2)}
</pre>
</details>
)}
</div>
);
}
46 changes: 46 additions & 0 deletions packages/programs-react/src/components/CallStackDisplay.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
.call-stack {
font-size: 0.85em;
padding: 4px 8px;
}

.call-stack-empty {
color: var(--programs-text-muted, #888);
}

.call-stack-breadcrumb {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px;
}

.call-stack-separator {
color: var(--programs-text-muted, #888);
user-select: none;
}

.call-stack-frame {
display: inline-flex;
align-items: center;
background: none;
border: 1px solid transparent;
border-radius: 3px;
padding: 1px 4px;
cursor: pointer;
font-family: inherit;
font-size: inherit;
color: var(--programs-link, #0366d6);
}

.call-stack-frame:hover {
background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05));
border-color: var(--programs-border, #ddd);
}

.call-stack-name {
font-weight: 500;
}

.call-stack-parens {
color: var(--programs-text-muted, #888);
}
62 changes: 62 additions & 0 deletions packages/programs-react/src/components/CallStackDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Displays the current call stack as a breadcrumb trail.
*/

import React from "react";
import { useTraceContext } from "./TraceContext.js";

// CSS is expected to be imported by the consuming application
// import "./CallStackDisplay.css";

export interface CallStackDisplayProps {
/** Custom class name */
className?: string;
}

/**
* Renders the call stack as a breadcrumb.
*
* Shows function names separated by arrows, e.g.:
* main() -> transfer() -> _update()
*/
export function CallStackDisplay({
className = "",
}: CallStackDisplayProps): JSX.Element {
const { callStack, jumpToStep } = useTraceContext();

if (callStack.length === 0) {
return (
<div className={`call-stack call-stack-empty ${className}`.trim()}>
<span className="call-stack-empty-text">(top level)</span>
</div>
);
}

return (
<div className={`call-stack ${className}`.trim()}>
<div className="call-stack-breadcrumb">
{callStack.map((frame, index) => (
<React.Fragment key={index}>
{index > 0 && (
<span className="call-stack-separator">{" -> "}</span>
)}
<button
className="call-stack-frame"
onClick={() => jumpToStep(frame.stepIndex)}
title={
`Step ${frame.stepIndex + 1}` +
(frame.callType ? ` (${frame.callType})` : "")
}
type="button"
>
<span className="call-stack-name">
{frame.identifier || "(anonymous)"}
</span>
<span className="call-stack-parens">()</span>
</button>
</React.Fragment>
))}
</div>
</div>
);
}
Loading
Loading