feat: show prepared workflow input files

This commit is contained in:
lda
2026-07-12 13:58:31 +07:00 Verified
parent c9ef10dab6
commit 91e1ae2817
5 changed files with 196 additions and 2 deletions
@@ -131,6 +131,16 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
});
it("renders factual input files on the prepared workflow input beat", () => {
renderBeat("input");
const browser = screen.getByRole("region", { name: /workflow input files/i });
expect(within(browser).getByText("project-brief.md")).toBeInTheDocument();
expect(within(browser).getByText("architecture-notes.md")).toBeInTheDocument();
expect(within(browser).getByRole("group", { name: /workflow output/i })).toHaveTextContent("issue-board.json");
expect(screen.queryByRole("region", { name: /workflow input summary/i })).not.toBeInTheDocument();
});
it("renders the guided interrupt contract via product moment in typed-human-boundary", () => {
renderBeat("interrupt", "typed-human-boundary");
@@ -16,7 +16,7 @@ import { GuidedProductMoment } from "./GuidedProductMoment.js";
import { InterruptContractPreview } from "./InterruptContractPreview.js";
import { NodeSpotlight } from "./NodeSpotlight.js";
import { OperationBlock } from "./OperationBlock.js";
import { RunInputFacts } from "./RunFactsPanel.js";
import { RunInputFileBrowser } from "./RunInputFileBrowser.js";
import { StageCaption } from "./StageCaption.js";
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
import type { PresentationTargetHealth } from "./presentation-target-status.js";
@@ -160,7 +160,10 @@ export const DemoWorkflowScene = ({
{beat.id === "input" && (
<div className="demo-workflow-stage__input-facts">
<RunInputFacts facts={facts} />
<RunInputFileBrowser
selectedDocuments={facts.input.selectedDocuments}
boardPath={facts.input.boardPath}
/>
</div>
)}
@@ -0,0 +1,44 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { RunInputFileBrowser } from "./RunInputFileBrowser.js";
afterEach(() => cleanup());
describe("RunInputFileBrowser", () => {
it("renders the selected docs paths as factual readable files", () => {
render(
<RunInputFileBrowser
selectedDocuments={["docs/project-brief.md", "docs/architecture-notes.md"]}
boardPath="artifacts/issue-board.json"
/>,
);
const browser = screen.getByRole("region", { name: /workflow input files/i });
expect(within(browser).getByRole("heading", { name: "docs/" })).toBeInTheDocument();
const files = within(browser).getByRole("list", { name: /selected for this run/i });
expect(within(files).getAllByRole("listitem")).toHaveLength(2);
for (const path of ["docs/project-brief.md", "docs/architecture-notes.md"]) {
const row = within(files).getByText(path).closest("li");
expect(row).not.toBeNull();
expect(row).toHaveAttribute("data-file-path", path);
expect(row).toHaveTextContent(/selected|read/i);
}
const destination = within(browser).getByRole("group", { name: /workflow output/i });
expect(destination).toHaveTextContent("artifacts/issue-board.json");
});
it("does not invent file metadata or contents", () => {
render(
<RunInputFileBrowser
selectedDocuments={["docs/project-brief.md"]}
boardPath="issue-board.json"
/>,
);
const browser = screen.getByRole("region", { name: /workflow input files/i });
expect(within(browser).queryByText(/\b\d+(?:\.\d+)?\s*(?:kb|mb|bytes)\b/i)).not.toBeInTheDocument();
expect(within(browser).queryByText(/preview|modified|created|content/i)).not.toBeInTheDocument();
});
});
@@ -0,0 +1,29 @@
export type RunInputFileBrowserProps = {
readonly selectedDocuments: ReadonlyArray<string>;
readonly boardPath: string;
};
export const RunInputFileBrowser = ({
selectedDocuments,
boardPath,
}: RunInputFileBrowserProps) => (
<section className="run-input-file-browser" role="region" aria-label="workflow input files">
<header className="run-input-file-browser__header">
<h3>docs/</h3>
<span>selected for this run</span>
</header>
<ul className="run-input-file-browser__list" aria-label="selected for this run">
{selectedDocuments.map((path) => (
<li className="run-input-file-browser__file" data-file-path={path} key={path}>
<span className="run-input-file-browser__icon" aria-hidden="true">file</span>
<code>{path}</code>
<span className="run-input-file-browser__marker">selected / read</span>
</li>
))}
</ul>
<div className="run-input-file-browser__destination" role="group" aria-label="workflow output">
<span>workflow output</span>
<code>{boardPath}</code>
</div>
</section>
);
@@ -806,6 +806,114 @@
overflow: hidden;
}
.demo-workflow-stage__input-facts {
display: grid;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.run-input-file-browser {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
min-width: 0;
min-height: 0;
overflow: hidden;
border: 1px solid color-mix(in oklch, var(--accent-cyan) 28%, var(--stage-line));
border-radius: 0.8rem;
padding: 0.8rem;
background:
linear-gradient(145deg, oklch(0.72 0.17 195 / 0.08), transparent 45%),
oklch(0.095 0.02 250 / 0.96);
}
.run-input-file-browser__header,
.run-input-file-browser__destination {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
min-width: 0;
}
.run-input-file-browser__header {
padding-bottom: 0.65rem;
border-bottom: 1px solid var(--stage-line);
}
.run-input-file-browser__header h3 {
margin: 0;
color: var(--text-primary);
font: 700 1rem/1 var(--font-mono, monospace);
}
.run-input-file-browser__header span,
.run-input-file-browser__destination > span,
.run-input-file-browser__marker {
color: var(--text-muted);
font: 650 0.6rem/1.2 var(--font-mono, monospace);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.run-input-file-browser__list {
display: grid;
align-content: start;
gap: 0.4rem;
min-height: 0;
margin: 0;
padding: 0.7rem 0;
overflow: auto;
list-style: none;
scrollbar-width: thin;
}
.run-input-file-browser__file {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: 0.55rem;
align-items: center;
min-width: 0;
padding: 0.5rem 0.55rem;
border: 1px solid color-mix(in oklch, var(--stage-line) 72%, transparent);
border-radius: 0.55rem;
background: oklch(0.13 0.025 250 / 0.72);
}
.run-input-file-browser__icon {
color: var(--accent-cyan);
font: 650 0.56rem/1 var(--font-mono, monospace);
text-transform: uppercase;
}
.run-input-file-browser__file code,
.run-input-file-browser__destination code {
min-width: 0;
overflow-wrap: anywhere;
color: var(--text-primary);
font: 600 0.75rem/1.35 var(--font-mono, monospace);
}
.run-input-file-browser__marker {
color: var(--accent-cyan);
white-space: nowrap;
}
.run-input-file-browser__destination {
padding-top: 0.7rem;
border-top: 1px solid var(--stage-line);
}
@container presentation-canvas (max-width: 620px) {
.run-input-file-browser__file {
grid-template-columns: auto minmax(0, 1fr);
}
.run-input-file-browser__marker {
grid-column: 2;
}
}
.interrupt-contract-preview__schema,
.interrupt-contract-preview__payload,
.interrupt-contract-preview__issues {