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
132 changes: 84 additions & 48 deletions desktop/electron/renderer/src/components/StacksPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,66 +95,95 @@ export default function StacksPage({ onUnavailable, sessionId }) {
}

function StackCall({ stack }) {
const request = stack.request ?? {};
const response = stack.response ?? {};
const tokens = tokenSummary(response.tokenUsage);

return (
<li className={`stack-call stack-call-${stack.status}`}>
<header className="stack-call-header">
<div>
<h2>Call {stack.sequence + 1}</h2>
<p>
{stack.status} - {formatDuration(stack.durationMs)} -{" "}
{stack.status} · {formatDuration(stack.durationMs)} ·{" "}
{formatTime(stack.startedAtMs)}
</p>
</div>
<span className="stack-run-id" title={stack.runId}>
{shortId(stack.runId)}
</span>
<div className="stack-call-meta">
{request.model ? (
<span className="stack-call-model">{request.model}</span>
) : null}
{Number.isFinite(response.statusCode) ? (
<span className="stack-call-status-code">
HTTP {response.statusCode}
</span>
) : null}
{tokens ? <span className="stack-call-tokens">{tokens}</span> : null}
<span className="stack-run-id" title={stack.runId}>
{shortId(stack.runId)}
</span>
</div>
</header>

<ol className="stack-layer-list">
{stack.layers.map((layer, index) => (
<StackLayer
index={index}
key={`${stack.id}-${layer.kind}`}
layer={layer}
/>
))}
</ol>
<RequestSection request={request} />
<ResponseSection response={response} />
</li>
);
}

function StackLayer({ index, layer }) {
function RequestSection({ request }) {
// Serialize the body only once the user opens the section: a session can hold
// hundreds of calls, each carrying the full conversation context, so eagerly
// pretty-printing every body on first paint is wasted work.
const [expanded, setExpanded] = useState(false);
const hasBody = request.body !== undefined && request.body !== null;
const meta = [request.api, request.url].filter(Boolean).join(" · ");

return (
<li className={`stack-layer stack-layer-${layer.status}`}>
<details open={defaultOpen(layer)}>
<summary>
<span className="stack-layer-index">
{String(index + 1).padStart(2, "0")}
</span>
<span className="stack-layer-title">{layer.title}</span>
<span className="stack-layer-status">{layer.status}</span>
</summary>
<p className="stack-layer-summary">{layer.summary}</p>
{layer.entries?.length ? <EntryGrid entries={layer.entries} /> : null}
{layer.text ? <pre className="stack-text">{layer.text}</pre> : null}
{layer.json !== undefined ? (
<pre className="stack-json">{stringifyJson(layer.json)}</pre>
) : null}
</details>
</li>
<details
className="stack-section stack-section-request"
onToggle={(event) => setExpanded(event.currentTarget.open)}
>
<summary>
<span className="stack-section-title">Request</span>
{meta ? <span className="stack-section-meta">{meta}</span> : null}
</summary>
{expanded && hasBody ? (
<pre className="stack-json">{stringifyJson(request.body)}</pre>
) : null}
{!hasBody ? (
<p className="stack-section-empty">No request body captured.</p>
) : null}
</details>
);
}

function EntryGrid({ entries }) {
function ResponseSection({ response }) {
// Defer body serialization until the section is opened (see RequestSection).
const [expanded, setExpanded] = useState(false);
const hasBody = response.body !== undefined && response.body !== null;

return (
<dl className="stack-entry-grid">
{entries.map((entry) => (
<div key={`${entry.label}:${entry.value}`} className="stack-entry">
<dt>{entry.label}</dt>
<dd>{entry.value}</dd>
</div>
))}
</dl>
<details
className="stack-section stack-section-response"
onToggle={(event) => setExpanded(event.currentTarget.open)}
>
<summary>
<span className="stack-section-title">Response</span>
{Number.isFinite(response.statusCode) ? (
<span className="stack-section-meta">HTTP {response.statusCode}</span>
) : null}
</summary>
{response.error ? (
<pre className="stack-error-body">{response.error}</pre>
) : null}
{expanded && hasBody ? (
<pre className="stack-json">{stringifyJson(response.body)}</pre>
) : null}
{!hasBody && !response.error ? (
<p className="stack-section-empty">No response body captured.</p>
) : null}
</details>
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand All @@ -175,14 +204,21 @@ function emptyStackText(reason) {
}
}

function defaultOpen(layer) {
return [
"system_prompt",
"session_history",
"provider_payload",
"normalized_response",
"carried_forward",
].includes(layer.kind);
function tokenSummary(usage) {
if (!usage || typeof usage !== "object") {
return "";
}
const parts = [];
if (Number.isFinite(usage.input)) {
parts.push(`${usage.input} in`);
}
if (Number.isFinite(usage.output)) {
parts.push(`${usage.output} out`);
}
if (Number.isFinite(usage.total)) {
parts.push(`${usage.total} total`);
}
return parts.join(" / ");
}

function stringifyJson(value) {
Expand Down
135 changes: 57 additions & 78 deletions desktop/electron/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,17 @@ h1 {
line-height: 1.4;
}

.stack-call-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}

.stack-call-model,
.stack-call-status-code,
.stack-call-tokens,
.stack-run-id {
flex: 0 0 auto;
padding: 3px 6px;
Expand All @@ -615,133 +626,101 @@ h1 {
line-height: 1.2;
}

.stack-layer-list {
display: grid;
margin: 0;
padding: 0;
list-style: none;
.stack-call-model {
color: var(--ink-dim);
font-weight: 700;
}

.stack-layer {
border-bottom: 1px solid var(--border);
.stack-call-tokens {
font-variant-numeric: tabular-nums;
}

.stack-layer:last-child {
border-bottom: 0;
.stack-call-failed .stack-call-status-code {
border-color: var(--danger);
background: var(--danger-tint);
color: var(--danger);
}

.stack-layer details {
padding: 0;
.stack-section {
border-bottom: 1px solid var(--border);
}

.stack-layer summary {
display: grid;
grid-template-columns: 36px minmax(0, 1fr) auto;
align-items: center;
.stack-section:last-child {
border-bottom: 0;
}

.stack-section > summary {
display: flex;
align-items: baseline;
gap: 12px;
min-height: 46px;
min-height: 44px;
padding: 10px 16px;
cursor: pointer;
list-style: none;
}

.stack-layer summary::-webkit-details-marker {
.stack-section > summary::-webkit-details-marker {
display: none;
}

.stack-layer summary:hover {
.stack-section > summary:hover {
background: var(--surface-muted);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

.stack-layer-index {
color: var(--ink-faint);
font-size: 11px;
line-height: 1;
font-variant-numeric: tabular-nums;
.stack-section > summary:focus-visible {
outline: none;
background: var(--surface-muted);
box-shadow: inset 0 0 0 2px var(--active);
}

.stack-layer-title {
min-width: 0;
overflow: hidden;
.stack-section-title {
flex: 0 0 auto;
color: var(--ink-dim);
font-size: 13px;
font-weight: 700;
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}

.stack-layer-status {
.stack-section-meta {
min-width: 0;
overflow: hidden;
color: var(--ink-faint);
font-family: var(--font-mono);
font-size: 11px;
line-height: 1;
}

.stack-layer-error .stack-layer-status,
.stack-layer-error .stack-layer-title {
color: var(--danger);
line-height: 1.3;
text-overflow: ellipsis;
white-space: nowrap;
}

.stack-layer-summary {
margin: 0;
padding: 0 16px 12px 64px;
.stack-section-empty {
margin: 0 16px 14px;
color: var(--ink-faint);
font-size: 12px;
line-height: 1.45;
}

.stack-entry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1px;
margin: 0 16px 12px 64px;
padding: 1px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--border);
overflow: hidden;
}

.stack-entry {
min-width: 0;
padding: 8px 10px;
background: var(--surface-raised);
}

.stack-entry dt {
margin: 0 0 4px;
color: var(--ink-faint);
font-size: 10.5px;
line-height: 1.25;
text-transform: uppercase;
}

.stack-entry dd {
margin: 0;
overflow-wrap: anywhere;
color: var(--ink-dim);
font-size: 12px;
line-height: 1.4;
}

.stack-text,
.stack-error-body,
.stack-json {
max-height: 360px;
margin: 0 16px 14px 64px;
max-height: 420px;
margin: 0 16px 14px;
padding: 12px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface-muted);
color: var(--ink-dim);
color: var(--ink);
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
overflow: auto;
white-space: pre;
}

.stack-json {
color: var(--ink);
.stack-error-body {
border-color: var(--danger);
background: var(--danger-tint);
color: var(--danger);
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.message-list {
Expand Down
21 changes: 12 additions & 9 deletions desktop/electron/renderer/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,18 @@ declare global {
status: string;
startedAtMs: number;
durationMs: number;
layers: Array<{
kind: string;
title: string;
status: string;
summary: string;
entries: Array<{ label: string; value: string }>;
text?: string;
json?: unknown;
}>;
request: {
api: string;
url: string;
model: string;
body?: unknown;
};
response: {
statusCode?: number;
body?: unknown;
error?: string;
tokenUsage?: unknown;
};
}>;
unavailableReason?: string;
}>;
Expand Down
Loading