feat: tighten scene 11-12 factual evidence
This commit is contained in:
@@ -73,6 +73,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
|
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "interrupt-approval");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "interrupt-approval");
|
||||||
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-approval-focus", "decision");
|
||||||
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
|
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
|
||||||
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
||||||
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
|
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
|
||||||
@@ -94,6 +95,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
|
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "resume-output");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "resume-output");
|
||||||
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-continuation-focus", "output");
|
||||||
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
|
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,6 +119,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
|
|
||||||
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("region", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument();
|
expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument();
|
||||||
expect(screen.getAllByText(/lda.chat Thesis And Project Readiness Report/i).length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText(/lda.chat Thesis And Project Readiness Report/i).length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
@@ -191,6 +194,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
expect(screen.queryByText("Output")).not.toBeInTheDocument();
|
expect(screen.queryByText("Output")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("Trace frames")).not.toBeInTheDocument();
|
expect(screen.queryByText("Trace frames")).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelector(".interrupt-decision-form__report-preview")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resume makes output primary and operation/resume payload supporting", () => {
|
it("resume makes output primary and operation/resume payload supporting", () => {
|
||||||
@@ -260,9 +264,11 @@ describe("GuidedProductMoment", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-support-surface", "output-summary");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-support-surface", "output-summary");
|
||||||
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-continuation-focus", "trace");
|
||||||
expect(screen.getByRole("region", { name: /workflow trace proof/i })).toBeInTheDocument();
|
expect(screen.getByRole("region", { name: /workflow trace proof/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("region", { name: /workflow output summary/i })).toHaveAttribute("data-output-priority", "summary");
|
expect(screen.getByRole("region", { name: /workflow output summary/i })).toHaveAttribute("data-output-priority", "summary");
|
||||||
expect(screen.queryByText("No trace frames captured.")).not.toBeInTheDocument();
|
expect(screen.queryByText("No trace frames captured.")).not.toBeInTheDocument();
|
||||||
|
expect(document.querySelectorAll(".run-trace-frame")).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("trace beat shows trace frames instead of the empty fallback after trace is primed", () => {
|
it("trace beat shows trace frames instead of the empty fallback after trace is primed", () => {
|
||||||
|
|||||||
@@ -75,6 +75,8 @@ export const GuidedProductMoment = ({
|
|||||||
data-moment={moment}
|
data-moment={moment}
|
||||||
data-primary-surface={hierarchy.primary}
|
data-primary-surface={hierarchy.primary}
|
||||||
data-support-surface={hierarchy.support}
|
data-support-surface={hierarchy.support}
|
||||||
|
data-approval-focus={moment === "approval" ? "decision" : undefined}
|
||||||
|
data-continuation-focus={moment === "resume" ? "output" : moment === "trace" ? "trace" : undefined}
|
||||||
>
|
>
|
||||||
<header className="guided-product-moment__header">
|
<header className="guided-product-moment__header">
|
||||||
<span>{lens.eyebrow}</span>
|
<span>{lens.eyebrow}</span>
|
||||||
@@ -89,14 +91,17 @@ export const GuidedProductMoment = ({
|
|||||||
<RunInputFacts facts={facts} density="compact" />
|
<RunInputFacts facts={facts} density="compact" />
|
||||||
</aside>
|
</aside>
|
||||||
<InterruptPayloadFacts facts={facts} priority="primary" />
|
<InterruptPayloadFacts facts={facts} priority="primary" />
|
||||||
<InterruptDecisionForm
|
<div className="guided-product-moment__decision-column" role="region" aria-label="operator resume decision">
|
||||||
interrupt={facts.interrupt}
|
<InterruptDecisionForm
|
||||||
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
|
interrupt={facts.interrupt}
|
||||||
onSubmit={approvalActions?.canSubmit ? (ids, comment) => approvalActions.submit(ids, comment) : undefined}
|
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
|
||||||
onCancel={approvalActions?.canCancel ? () => approvalActions.cancel() : undefined}
|
onSubmit={approvalActions?.canSubmit ? (ids, comment) => approvalActions.submit(ids, comment) : undefined}
|
||||||
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
|
onCancel={approvalActions?.canCancel ? () => approvalActions.cancel() : undefined}
|
||||||
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
|
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
|
||||||
/>
|
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
|
||||||
|
showReportPreview={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{moment === "resume" && runResume ? (
|
{moment === "resume" && runResume ? (
|
||||||
|
|||||||
@@ -127,6 +127,21 @@ describe("InterruptDecisionForm", () => {
|
|||||||
expect(screen.queryByRole("button", { name: /cancel/i })).toBeFalsy();
|
expect(screen.queryByRole("button", { name: /cancel/i })).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("can leave report evidence to the surrounding interrupt panel", () => {
|
||||||
|
render(
|
||||||
|
<InterruptDecisionForm
|
||||||
|
interrupt={interrupt}
|
||||||
|
runId="run_recorded_lda_report"
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
showReportPreview={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText(/The workflow substrate is ready/i)).not.toBeInTheDocument();
|
||||||
|
expect(screen.getAllByRole("checkbox")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows terminal outcome label when state is cancelled", () => {
|
it("shows terminal outcome label when state is cancelled", () => {
|
||||||
render(
|
render(
|
||||||
<InterruptDecisionForm
|
<InterruptDecisionForm
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ type InterruptDecisionFormProps = {
|
|||||||
readonly onSubmit?: ((selectedIssueIds: ReadonlyArray<string>, comment: string) => void) | undefined;
|
readonly onSubmit?: ((selectedIssueIds: ReadonlyArray<string>, comment: string) => void) | undefined;
|
||||||
readonly onCancel?: (() => void) | undefined;
|
readonly onCancel?: (() => void) | undefined;
|
||||||
readonly terminalOutcome?: "submitted" | "cancelled" | undefined;
|
readonly terminalOutcome?: "submitted" | "cancelled" | undefined;
|
||||||
|
readonly showReportPreview?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InterruptDecisionForm = ({
|
export const InterruptDecisionForm = ({
|
||||||
@@ -15,6 +16,7 @@ export const InterruptDecisionForm = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
terminalOutcome,
|
terminalOutcome,
|
||||||
|
showReportPreview = true,
|
||||||
}: InterruptDecisionFormProps) => {
|
}: InterruptDecisionFormProps) => {
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(() =>
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(() =>
|
||||||
new Set(interrupt.proposedIssues.map((issue) => issue.id)),
|
new Set(interrupt.proposedIssues.map((issue) => issue.id)),
|
||||||
@@ -71,7 +73,7 @@ export const InterruptDecisionForm = ({
|
|||||||
<dd>{interrupt.outcomes.join(", ")}</dd>
|
<dd>{interrupt.outcomes.join(", ")}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{interrupt.reportMarkdownPreview && (
|
{showReportPreview && interrupt.reportMarkdownPreview && (
|
||||||
<pre className="interrupt-decision-form__report-preview">
|
<pre className="interrupt-decision-form__report-preview">
|
||||||
{interrupt.reportMarkdownPreview}
|
{interrupt.reportMarkdownPreview}
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
import { callOperation } from "../connection/api.js";
|
||||||
|
|
||||||
vi.mock("../connection/api.js", () => ({
|
vi.mock("../connection/api.js", () => ({
|
||||||
callOperation: vi.fn().mockResolvedValue({
|
callOperation: vi.fn().mockResolvedValue({
|
||||||
@@ -20,6 +21,21 @@ class MockResizeObserver {
|
|||||||
disconnect() {}
|
disconnect() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mockedCallOperation = vi.mocked(callOperation);
|
||||||
|
const healthyResponse = {
|
||||||
|
ok: true as const,
|
||||||
|
operation: "workflow.health" as const,
|
||||||
|
label: "Health",
|
||||||
|
interpreted: { status: "ok" },
|
||||||
|
exchange: { request: {}, response: {} },
|
||||||
|
equivalentCli: "uv run wf status",
|
||||||
|
durationMs: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockedCallOperation.mockResolvedValue(healthyResponse);
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
|
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
|
||||||
globalThis.DOMRect = {
|
globalThis.DOMRect = {
|
||||||
@@ -50,6 +66,8 @@ const graphNodeByLabel = (label: RegExp): HTMLElement => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
mockedCallOperation.mockReset();
|
||||||
|
mockedCallOperation.mockResolvedValue(healthyResponse);
|
||||||
window.sessionStorage.clear();
|
window.sessionStorage.clear();
|
||||||
window.location.hash = "";
|
window.location.hash = "";
|
||||||
});
|
});
|
||||||
@@ -218,6 +236,21 @@ describe("PresentationRoute", () => {
|
|||||||
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("switches to replay evidence when a configured target fails health", async () => {
|
||||||
|
mockedCallOperation.mockResolvedValue({
|
||||||
|
ok: false as const,
|
||||||
|
error: { code: "upstream_unreachable", message: "connection refused" },
|
||||||
|
exchange: { request: {}, response: {} },
|
||||||
|
});
|
||||||
|
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8766/rpc");
|
||||||
|
window.location.hash = "#scene/resume-output-evidence/resume";
|
||||||
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
|
render(<PresentationRoute />);
|
||||||
|
|
||||||
|
expect(await screen.findByText(/Replay fallback/i)).toBeInTheDocument();
|
||||||
|
expect(await screen.findByRole("region", { name: /workflow output report/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("updates the chat intro after the live health probe succeeds", async () => {
|
it("updates the chat intro after the live health probe succeeds", async () => {
|
||||||
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
||||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
|
|||||||
@@ -97,9 +97,12 @@ export const PresentationRoute = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (demo.state.phase !== "ready") return;
|
if (demo.state.phase !== "ready") return;
|
||||||
// The resolved target owns the initial mode: a healthy loopback target must
|
// The resolved target owns the initial mode: a healthy loopback target must
|
||||||
// remain live, while an invalid target starts from the offline recording.
|
// remain live, while an invalid or unreachable target starts from the
|
||||||
if (demo.state.mode !== presentationTarget.mode) demo.setMode(presentationTarget.mode);
|
// offline recording. The health hook is the point where an HTTP target
|
||||||
}, [demo.setMode, demo.state.mode, demo.state.phase, presentationTarget.mode]);
|
// becomes known to be unreachable; URL shape alone is not enough.
|
||||||
|
const desiredMode = targetStatus.kind === "failed" ? "replay" : presentationTarget.mode;
|
||||||
|
if (demo.state.mode !== desiredMode) demo.setMode(desiredMode);
|
||||||
|
}, [demo.setMode, demo.state.mode, demo.state.phase, presentationTarget.mode, targetStatus.kind]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (demo.state.phase === "ready" && demo.state.mode === "replay") {
|
if (demo.state.phase === "ready" && demo.state.mode === "replay") {
|
||||||
|
|||||||
@@ -234,6 +234,11 @@ describe("RunTraceFacts", () => {
|
|||||||
expect(screen.getByText("review_issues")).toBeDefined();
|
expect(screen.getByText("review_issues")).toBeDefined();
|
||||||
expect(screen.getByText("2 captured")).toBeInTheDocument();
|
expect(screen.getByText("2 captured")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("captured as empty object").length).toBeGreaterThanOrEqual(2);
|
expect(screen.getAllByText("captured as empty object").length).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(screen.getAllByText("Resolved input").length).toBe(2);
|
||||||
|
expect(screen.getAllByText("captured as empty object")[0]!.closest(".run-trace-frame__fact")).toHaveAttribute(
|
||||||
|
"data-value-kind",
|
||||||
|
"empty-object",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders trace frames inside a scrollable list", () => {
|
it("renders trace frames inside a scrollable list", () => {
|
||||||
@@ -244,4 +249,18 @@ describe("RunTraceFacts", () => {
|
|||||||
expect(screen.getByRole("region", { name: /workflow trace frames/i })).toHaveClass("run-facts-scroll-region");
|
expect(screen.getByRole("region", { name: /workflow trace frames/i })).toHaveClass("run-facts-scroll-region");
|
||||||
expect(screen.getByText("node-7")).toBeInTheDocument();
|
expect(screen.getByText("node-7")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps repeated node IDs as separate trace frames", () => {
|
||||||
|
const traceFacts = makeTraceFacts(2).trace;
|
||||||
|
const repeatedTraceFacts: DemoRunFacts = {
|
||||||
|
...baseFacts,
|
||||||
|
trace: {
|
||||||
|
frames: traceFacts.frames.map((frame) => ({ ...frame, nodeId: "review_issues" })),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<RunTraceFacts facts={repeatedTraceFacts} />);
|
||||||
|
|
||||||
|
expect(document.querySelectorAll(".run-trace-frame")).toHaveLength(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import type { DemoRunFacts } from "./demo-run-facts.js";
|
import { factValueKind, type DemoRunFacts } from "./demo-run-facts.js";
|
||||||
|
|
||||||
type RunInputFactsProps = {
|
type RunInputFactsProps = {
|
||||||
readonly facts: DemoRunFacts;
|
readonly facts: DemoRunFacts;
|
||||||
@@ -138,6 +138,13 @@ type RunTraceFactsProps = {
|
|||||||
readonly facts: DemoRunFacts;
|
readonly facts: DemoRunFacts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TraceFact = ({ label, value }: { readonly label: string; readonly value: string }) => (
|
||||||
|
<div className="run-trace-frame__fact" data-value-kind={factValueKind(value)}>
|
||||||
|
<dt>{label}</dt>
|
||||||
|
<dd><code>{value}</code></dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const RunTraceFacts = ({ facts }: RunTraceFactsProps) => (
|
export const RunTraceFacts = ({ facts }: RunTraceFactsProps) => (
|
||||||
<div className="run-facts-card run-trace-facts" role="region" aria-label="workflow trace proof">
|
<div className="run-facts-card run-trace-facts" role="region" aria-label="workflow trace proof">
|
||||||
<h3>
|
<h3>
|
||||||
@@ -148,18 +155,17 @@ export const RunTraceFacts = ({ facts }: RunTraceFactsProps) => (
|
|||||||
) : (
|
) : (
|
||||||
<div className="run-facts-scroll-region run-facts-scroll-region--trace" role="region" aria-label="workflow trace frames">
|
<div className="run-facts-scroll-region run-facts-scroll-region--trace" role="region" aria-label="workflow trace frames">
|
||||||
<ul className="run-facts-list">
|
<ul className="run-facts-list">
|
||||||
{facts.trace.frames.map((frame) => (
|
{facts.trace.frames.map((frame, index) => (
|
||||||
<li key={frame.nodeId} className="run-trace-frame">
|
<li key={`${frame.nodeId}-${index}`} className="run-trace-frame" data-trace-node={frame.nodeId}>
|
||||||
<strong>{frame.nodeId}</strong>
|
<header className="run-trace-frame__header">
|
||||||
<span className="run-trace-step-type">{frame.stepType}</span>
|
<strong>{frame.nodeId}</strong>
|
||||||
<span className="run-trace-outcome">{frame.outcome}</span>
|
<span className="run-trace-step-type">{frame.stepType}</span>
|
||||||
<dl className="run-facts-dl">
|
<span className="run-trace-outcome">{frame.outcome}</span>
|
||||||
<dt>Resolved input</dt>
|
</header>
|
||||||
<dd>{frame.resolvedInputLabel}</dd>
|
<dl className="run-trace-frame__facts">
|
||||||
<dt>Output</dt>
|
<TraceFact label="Resolved input" value={frame.resolvedInputLabel} />
|
||||||
<dd>{frame.outputLabel}</dd>
|
<TraceFact label="Output" value={frame.outputLabel} />
|
||||||
<dt>State changes</dt>
|
<TraceFact label="State changes" value={frame.stateChangesLabel} />
|
||||||
<dd>{frame.stateChangesLabel}</dd>
|
|
||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||||
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
|
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
|
||||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||||
import { formatFactValue, projectDemoRunFacts } from "./demo-run-facts.js";
|
import { factValueKind, formatFactValue, projectDemoRunFacts } from "./demo-run-facts.js";
|
||||||
|
|
||||||
const controller = (overrides: Partial<DemoTimelineController> = {}): DemoTimelineController => {
|
const controller = (overrides: Partial<DemoTimelineController> = {}): DemoTimelineController => {
|
||||||
const recording = loadCanonicalDemoRecording();
|
const recording = loadCanonicalDemoRecording();
|
||||||
@@ -189,4 +189,10 @@ describe("demo-run-facts", () => {
|
|||||||
expect(formatFactValue({}, "not captured in this recording")).toBe("captured as empty object");
|
expect(formatFactValue({}, "not captured in this recording")).toBe("captured as empty object");
|
||||||
expect(formatFactValue(undefined, "not captured in this recording")).toBe("not captured in this recording");
|
expect(formatFactValue(undefined, "not captured in this recording")).toBe("not captured in this recording");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("classifies captured values for compact trace rendering", () => {
|
||||||
|
expect(factValueKind("captured as empty object")).toBe("empty-object");
|
||||||
|
expect(factValueKind("not captured in this recording")).toBe("missing");
|
||||||
|
expect(factValueKind('{"documents":3}')).toBe("value");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export type RunFactsTrace = {
|
|||||||
readonly frames: ReadonlyArray<RunFactsTraceFrame>;
|
readonly frames: ReadonlyArray<RunFactsTraceFrame>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FactValueKind = "value" | "empty-object" | "missing";
|
||||||
|
|
||||||
export type DemoRunFacts = {
|
export type DemoRunFacts = {
|
||||||
readonly input: RunFactsInput;
|
readonly input: RunFactsInput;
|
||||||
readonly interrupt: RunFactsInterrupt;
|
readonly interrupt: RunFactsInterrupt;
|
||||||
@@ -109,6 +111,12 @@ export const formatFactValue = (value: unknown, absentLabel: string): string =>
|
|||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const factValueKind = (value: string): FactValueKind => {
|
||||||
|
if (value === EMPTY_OBJECT_LABEL) return "empty-object";
|
||||||
|
if (value === "not captured in this recording") return "missing";
|
||||||
|
return "value";
|
||||||
|
};
|
||||||
|
|
||||||
const findEvent = (
|
const findEvent = (
|
||||||
events: ReadonlyArray<DemoEvent>,
|
events: ReadonlyArray<DemoEvent>,
|
||||||
stage: DemoEventStage,
|
stage: DemoEventStage,
|
||||||
|
|||||||
@@ -2514,6 +2514,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
min-width: 0;
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2526,6 +2527,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.interrupt-decision-form__run-id {
|
.interrupt-decision-form__run-id {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
font: 0.82rem/1.4 var(--font-mono);
|
font: 0.82rem/1.4 var(--font-mono);
|
||||||
color: color-mix(in srgb, var(--text-primary), white 20%);
|
color: color-mix(in srgb, var(--text-primary), white 20%);
|
||||||
}
|
}
|
||||||
@@ -2733,7 +2738,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__meta {
|
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__meta {
|
||||||
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
gap: 0.15rem 0.45rem;
|
gap: 0.15rem 0.45rem;
|
||||||
margin-bottom: 0.45rem;
|
margin-bottom: 0.45rem;
|
||||||
font-size: 0.74rem;
|
font-size: 0.74rem;
|
||||||
|
|||||||
@@ -1286,16 +1286,64 @@
|
|||||||
|
|
||||||
.guided-product-moment__approval-grid {
|
.guided-product-moment__approval-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(12.5rem, 0.42fr) minmax(0, 1.25fr) minmax(20rem, 0.62fr);
|
grid-template-columns: 14rem minmax(0, 1.45fr) minmax(18rem, 0.78fr);
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guided-product-moment__approval-grid > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.guided-product-moment__input-rail {
|
.guided-product-moment__input-rail {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guided-product-moment__decision-column {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .guided-product-moment__decision-column {
|
||||||
|
border-left: 1px solid color-mix(in srgb, var(--accent-cyan) 36%, transparent);
|
||||||
|
padding-left: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form > * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__issues {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__header,
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__meta,
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__comment-label,
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__issue-title {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.run-facts-card[data-density="compact"] {
|
.run-facts-card[data-density="compact"] {
|
||||||
padding: 0.8rem;
|
padding: 0.8rem;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
@@ -1384,6 +1432,11 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guided-product-moment[data-continuation-focus="output"] .run-facts-card[data-output-priority="report"] {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-cyan) 62%, var(--stage-line));
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-cyan) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.guided-product-moment__resume-support .operation-command {
|
.guided-product-moment__resume-support .operation-command {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
@@ -1427,13 +1480,75 @@
|
|||||||
|
|
||||||
.run-trace-frame {
|
.run-trace-frame {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(8rem, 0.26fr) auto auto minmax(0, 1fr);
|
gap: 0.65rem;
|
||||||
gap: 0.5rem 0.75rem;
|
padding: 0.7rem 0.8rem;
|
||||||
align-items: start;
|
border: 1px solid var(--stage-line);
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
background: color-mix(in srgb, var(--stage-surface) 84%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-trace-frame .run-facts-dl {
|
.guided-product-moment[data-continuation-focus="trace"] .run-trace-facts {
|
||||||
grid-column: 4;
|
border-color: color-mix(in srgb, var(--accent-cyan) 62%, var(--stage-line));
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-cyan) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__header strong {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: 700 0.82rem/1.2 var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__facts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.45rem;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__fact {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.42rem 0.5rem;
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: color-mix(in srgb, var(--stage-inset) 72%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__fact dt {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__fact dd {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__fact code {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: 600 0.67rem/1.35 var(--font-mono, monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__fact[data-value-kind="empty-object"] code {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-trace-frame__fact[data-value-kind="missing"] code {
|
||||||
|
color: var(--accent-amber);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guided-product-moment[data-moment="trace"] .run-facts-card[data-output-priority="summary"] {
|
.guided-product-moment[data-moment="trace"] .run-facts-card[data-output-priority="summary"] {
|
||||||
@@ -1447,6 +1562,12 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container presentation-canvas (max-width: 820px) {
|
||||||
|
.run-trace-frame__facts {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@container presentation-canvas (max-width: 1050px) {
|
@container presentation-canvas (max-width: 1050px) {
|
||||||
.guided-product-moment__approval-grid,
|
.guided-product-moment__approval-grid,
|
||||||
.guided-product-moment__resume-grid,
|
.guided-product-moment__resume-grid,
|
||||||
|
|||||||
Reference in New Issue
Block a user