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);
|
color: var(--text-primary);
|
||||||
font-weight: 800;
|
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