diff --git a/.changeset/workflows-local-explorer-step-output.md b/.changeset/workflows-local-explorer-step-output.md new file mode 100644 index 0000000000..26e707b2ca --- /dev/null +++ b/.changeset/workflows-local-explorer-step-output.md @@ -0,0 +1,7 @@ +--- +"miniflare": minor +--- + +Add full step output retrieval to the Workflows local explorer + +The Workflows instance details response truncates streamed step outputs to a short preview (or a placeholder for binary streams). Mirroring the production `/step` endpoint, the local explorer now exposes `GET /workflows/{workflow_name}/instances/{instance_id}/step`, which returns a flat `{ status, error, output }` body for a single step. The explorer UI fetches this on demand when a step whose inline preview was truncated is expanded. diff --git a/packages/local-explorer-ui/src/__tests__/workflows/step-helpers.test.ts b/packages/local-explorer-ui/src/__tests__/workflows/step-helpers.test.ts index a62630f894..25f34b5d38 100644 --- a/packages/local-explorer-ui/src/__tests__/workflows/step-helpers.test.ts +++ b/packages/local-explorer-ui/src/__tests__/workflows/step-helpers.test.ts @@ -1,4 +1,5 @@ import { describe, test } from "vitest"; +import { isTruncatedStreamPreview } from "../../components/workflows/helpers"; import { getStepDisplayName, getStepKey, @@ -106,3 +107,31 @@ describe("getRestartFromStepParam", () => { }); }); }); + +describe("isTruncatedStreamPreview", () => { + test("detects a truncated text preview", ({ expect }) => { + expect(isTruncatedStreamPreview("some output[truncated output]")).toBe( + true + ); + }); + + test("detects a binary stream placeholder", ({ expect }) => { + expect( + isTruncatedStreamPreview("[ReadableStream (binary): 2048 bytes]") + ).toBe(true); + }); + + test("detects an incomplete stream placeholder", ({ expect }) => { + expect(isTruncatedStreamPreview("[ReadableStream: 100 bytes]")).toBe(true); + }); + + test("returns false for a complete inline value", ({ expect }) => { + expect(isTruncatedStreamPreview("hello world")).toBe(false); + }); + + test("returns false for non-string values", ({ expect }) => { + expect(isTruncatedStreamPreview({ foo: "bar" })).toBe(false); + expect(isTruncatedStreamPreview(undefined)).toBe(false); + expect(isTruncatedStreamPreview(42)).toBe(false); + }); +}); diff --git a/packages/local-explorer-ui/src/components/workflows/CopyButton.tsx b/packages/local-explorer-ui/src/components/workflows/CopyButton.tsx index df859c69f5..98484ada79 100644 --- a/packages/local-explorer-ui/src/components/workflows/CopyButton.tsx +++ b/packages/local-explorer-ui/src/components/workflows/CopyButton.tsx @@ -4,9 +4,11 @@ import { useState, type JSX } from "react"; export function CopyButton({ text, label = "Copy", + disabled = false, }: { text: string; label?: string; + disabled?: boolean; }): JSX.Element { const [copied, setCopied] = useState(false); @@ -19,7 +21,8 @@ export function CopyButton({ return (