feat: render factual run state panels

This commit is contained in:
lda
2026-07-09 18:21:48 +07:00 Verified
parent bc7d6c0265
commit 7fd3e9d123
3 changed files with 298 additions and 0 deletions
@@ -0,0 +1,118 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import type { DemoRunFacts } from "./demo-run-facts.js";
import {
RunInputFacts,
RunOutputFacts,
RunTraceFacts,
} from "./RunFactsPanel.js";
const baseFacts: DemoRunFacts = {
input: {
selectedDocuments: ["project-brief.md", "architecture-notes.md"],
boardPath: "issue-board.json",
},
interrupt: {
kind: "issue_review",
typed: true,
outcomes: ["submitted", "cancelled"],
proposedIssues: [],
reportMarkdownPreview: "",
},
resume: { outcome: null, payload: null },
output: { state: "not-created", message: "Output not created yet" },
trace: { frames: [] },
};
describe("RunInputFacts", () => {
it("renders workflow input facts with selected documents and board path", () => {
render(<RunInputFacts facts={baseFacts} />);
expect(screen.getByText("Workflow input")).toBeDefined();
expect(screen.getByText("project-brief.md")).toBeDefined();
expect(screen.getByText("architecture-notes.md")).toBeDefined();
expect(screen.getByText("issue-board.json")).toBeDefined();
});
});
describe("RunOutputFacts", () => {
it("renders output not created yet before resume", () => {
render(<RunOutputFacts facts={baseFacts} />);
expect(screen.getByText("Output not created yet")).toBeDefined();
});
it("renders created output facts with issue details", () => {
const createdFacts: DemoRunFacts = {
...baseFacts,
output: {
state: "created",
output: {
approved: true,
markdown: "# Report",
created_issues: [
{ id: "ISSUE-001", title: "Prepare defense", url: "local://issue-board/ISSUE-001" },
],
selected_issue_ids: ["risk-1"],
comment: "Create the selected issue.",
},
createdIssues: [
{ id: "ISSUE-001", title: "Prepare defense", url: "local://issue-board/ISSUE-001" },
],
markdownPreview: "# Report",
},
};
render(<RunOutputFacts facts={createdFacts} />);
expect(screen.getByText("ISSUE-001")).toBeDefined();
expect(screen.getByText("local://issue-board/ISSUE-001")).toBeDefined();
expect(screen.getByText("Create the selected issue.")).toBeDefined();
});
});
describe("RunTraceFacts", () => {
it("renders trace frame node IDs and empty object labels", () => {
const traceFacts: DemoRunFacts = {
...baseFacts,
output: {
state: "created",
output: {
approved: true,
markdown: "# Report",
created_issues: [],
selected_issue_ids: [],
comment: null,
},
createdIssues: [],
markdownPreview: "# Report",
},
trace: {
frames: [
{
nodeId: "list_documents",
stepType: "node",
outcome: "ok",
resolvedInputLabel: "captured as empty object",
outputLabel: "captured as empty object",
stateChangesLabel: "captured as empty object",
},
{
nodeId: "review_issues",
stepType: "interrupt",
outcome: "submitted",
resolvedInputLabel: "captured as empty object",
outputLabel: "captured as empty object",
stateChangesLabel: "captured as empty object",
},
],
},
};
render(<RunTraceFacts facts={traceFacts} />);
expect(screen.getByText("list_documents")).toBeDefined();
expect(screen.getByText("review_issues")).toBeDefined();
expect(screen.getAllByText("captured as empty object").length).toBeGreaterThanOrEqual(2);
});
});
@@ -0,0 +1,90 @@
import type { DemoRunFacts } from "./demo-run-facts.js";
type RunInputFactsProps = {
readonly facts: DemoRunFacts;
};
export const RunInputFacts = ({ facts }: RunInputFactsProps) => (
<div className="run-facts-card">
<h3>Workflow input</h3>
<dl className="run-facts-dl">
<dt>Selected documents</dt>
<dd>
<ul className="run-facts-list">
{facts.input.selectedDocuments.map((doc) => (
<li key={doc}>{doc}</li>
))}
</ul>
</dd>
<dt>Board path</dt>
<dd>{facts.input.boardPath}</dd>
</dl>
</div>
);
type RunOutputFactsProps = {
readonly facts: DemoRunFacts;
};
export const RunOutputFacts = ({ facts }: RunOutputFactsProps) => (
<div className="run-facts-card">
<h3>Output</h3>
{facts.output.state === "not-created" ? (
<p>{facts.output.message}</p>
) : (
<dl className="run-facts-dl">
<dt>Created issues</dt>
<dd>
<ul className="run-facts-list">
{facts.output.createdIssues.map((issue) => (
<li key={issue.id}>
<strong>{issue.id}</strong> {issue.title}
<br />
<span className="run-facts-url">{issue.url}</span>
</li>
))}
</ul>
</dd>
<dt>Selected issue IDs</dt>
<dd>{facts.output.output.selected_issue_ids.join(", ")}</dd>
<dt>Comment</dt>
<dd>{facts.output.output.comment ?? "none"}</dd>
<dt>Markdown preview</dt>
<dd>
<pre className="run-facts-markdown-preview">{facts.output.markdownPreview}</pre>
</dd>
</dl>
)}
</div>
);
type RunTraceFactsProps = {
readonly facts: DemoRunFacts;
};
export const RunTraceFacts = ({ facts }: RunTraceFactsProps) => (
<div className="run-facts-card run-trace-facts">
<h3>Trace frames</h3>
{facts.trace.frames.length === 0 ? (
<p>No trace frames captured.</p>
) : (
<ul className="run-facts-list">
{facts.trace.frames.map((frame) => (
<li key={frame.nodeId} className="run-trace-frame">
<strong>{frame.nodeId}</strong>
<span className="run-trace-step-type">{frame.stepType}</span>
<span className="run-trace-outcome">{frame.outcome}</span>
<dl className="run-facts-dl">
<dt>Resolved input</dt>
<dd>{frame.resolvedInputLabel}</dd>
<dt>Output</dt>
<dd>{frame.outputLabel}</dd>
<dt>State changes</dt>
<dd>{frame.stateChangesLabel}</dd>
</dl>
</li>
))}
</ul>
)}
</div>
);
@@ -1410,3 +1410,93 @@
color: var(--text-primary);
font-weight: 800;
}
/* Run facts panels */
.run-facts-card {
padding: 1.2rem 1.4rem;
border: 1px solid var(--stage-line);
border-radius: 8px;
background: var(--stage-surface);
}
.run-facts-card h3 {
margin: 0 0 0.8rem;
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--accent-cyan);
}
.run-facts-dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.35rem 0.8rem;
margin: 0;
font-size: 0.92rem;
line-height: 1.55;
}
.run-facts-dl dt {
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
}
.run-facts-dl dd {
margin: 0;
color: color-mix(in srgb, var(--text-primary), white 12%);
}
.run-facts-list {
list-style: none;
margin: 0;
padding: 0;
}
.run-facts-list li {
padding: 0.25rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--stage-line), transparent 50%);
font-size: 0.88rem;
}
.run-facts-list li:last-child {
border-bottom: none;
}
.run-facts-url {
font: 0.78rem/1.4 var(--font-mono);
color: color-mix(in srgb, var(--accent-cyan), white 20%);
}
.run-facts-markdown-preview {
margin: 0.4rem 0 0;
padding: 0.6rem 0.8rem;
border-radius: 6px;
background: color-mix(in srgb, var(--stage-surface), black 20%);
font: 0.82rem/1.5 var(--font-mono);
white-space: pre-wrap;
word-break: break-word;
color: color-mix(in srgb, var(--text-primary), white 8%);
}
.run-trace-facts .run-trace-frame {
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.8rem;
align-items: baseline;
}
.run-trace-step-type,
.run-trace-outcome {
font: 0.78rem/1.4 var(--font-mono);
padding: 0.15rem 0.45rem;
border-radius: 4px;
background: color-mix(in srgb, var(--accent-cyan), transparent 85%);
color: var(--accent-cyan);
}
.run-trace-outcome {
background: color-mix(in srgb, var(--accent-amber), transparent 85%);
color: var(--accent-amber);
}