feat: render factual run state panels
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user