feat: add capability playground to discover
This commit is contained in:
@@ -1202,6 +1202,332 @@ tbody tr:hover {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* The selected capability is an operator receipt: facts stay visible, while
|
||||
contract payloads and execution evidence open only when the operator asks. */
|
||||
.capability-playground {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.capability-playground__header,
|
||||
.capability-playground__receipt-heading,
|
||||
.capability-playground__try-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.capability-playground__header h2,
|
||||
.capability-playground__try-heading h3,
|
||||
.capability-playground__receipt-heading h3 {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.capability-playground__header h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.capability-playground__source,
|
||||
.capability-playground__receipt-label {
|
||||
color: var(--color-slate);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.capability-playground__tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.9rem;
|
||||
padding-bottom: 0.35rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.capability-playground__tab {
|
||||
min-width: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-color: var(--color-border);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
}
|
||||
|
||||
.capability-playground__tab:hover:not(:disabled),
|
||||
.capability-playground__tab[aria-selected="true"] {
|
||||
border-color: var(--color-ink);
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
}
|
||||
|
||||
.capability-playground__tab:focus-visible,
|
||||
.capability-playground__panel:focus-visible,
|
||||
.capability-playground__schema-block summary:focus-visible {
|
||||
outline: 2px solid var(--color-signal-green);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.capability-playground__panel {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0.95rem 0 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.capability-playground__panel > h3,
|
||||
.capability-playground__try-heading h3,
|
||||
.capability-playground__receipt-heading h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.capability-playground__facts,
|
||||
.capability-playground__receipt-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.55rem;
|
||||
margin: 0 0 0.9rem;
|
||||
}
|
||||
|
||||
.capability-playground__facts div,
|
||||
.capability-playground__receipt-facts div {
|
||||
min-width: 0;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-paper);
|
||||
}
|
||||
|
||||
.capability-playground__facts dt,
|
||||
.capability-playground__receipt-facts dt {
|
||||
color: var(--color-slate);
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.capability-playground__facts dd,
|
||||
.capability-playground__receipt-facts dd {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.3rem;
|
||||
margin: 0.15rem 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.capability-playground__description,
|
||||
.capability-playground__try-heading p {
|
||||
color: var(--color-slate);
|
||||
}
|
||||
|
||||
.capability-playground__schemas {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
|
||||
.capability-playground__schema-block h3 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.capability-playground__schema-block details {
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-paper);
|
||||
}
|
||||
|
||||
.capability-playground__schema-block summary {
|
||||
padding: 0.5rem 0.65rem;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.capability-playground__schema-block pre,
|
||||
.capability-playground__receipt pre {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 0.7rem;
|
||||
background: var(--color-ink);
|
||||
color: var(--color-paper);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.capability-playground__try-heading {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.capability-playground__try-heading p {
|
||||
margin: 0;
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
.capability-playground__warning,
|
||||
.capability-playground__disabled,
|
||||
.capability-playground__local-error,
|
||||
.capability-playground__runtime-note {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
margin: 0.85rem 0;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.capability-playground__warning {
|
||||
border-color: var(--color-amber);
|
||||
background: rgba(184, 134, 11, 0.08);
|
||||
}
|
||||
|
||||
.capability-playground__warning svg {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-amber);
|
||||
}
|
||||
|
||||
.capability-playground__warning p,
|
||||
.capability-playground__disabled,
|
||||
.capability-playground__local-error,
|
||||
.capability-playground__runtime-note {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.capability-playground__disabled,
|
||||
.capability-playground__muted {
|
||||
color: var(--color-slate);
|
||||
}
|
||||
|
||||
.capability-playground__deployment {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.capability-playground__deployment input[type="text"] {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.capability-playground__deployment small {
|
||||
color: var(--color-slate);
|
||||
}
|
||||
|
||||
.capability-playground__acknowledgement {
|
||||
display: flex;
|
||||
flex-basis: auto;
|
||||
gap: 0.45rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.capability-playground__acknowledgement input {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 0.25rem;
|
||||
accent-color: var(--color-signal-green);
|
||||
}
|
||||
|
||||
.capability-playground__form-fieldset {
|
||||
min-width: 0;
|
||||
margin: 0.85rem 0 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.capability-playground__local-error {
|
||||
border-color: var(--color-red);
|
||||
background: rgba(192, 57, 43, 0.08);
|
||||
}
|
||||
|
||||
.capability-playground__receipt {
|
||||
min-width: 0;
|
||||
margin: 1rem 0 0;
|
||||
padding: 0.85rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-paper);
|
||||
}
|
||||
|
||||
.capability-playground__receipt-label {
|
||||
margin: 0 0 0.25rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.capability-playground__receipt-heading h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.capability-playground__outcome {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
color: var(--color-signal-green);
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.capability-playground__outcome[data-outcome="runtime-error"] {
|
||||
color: var(--color-amber);
|
||||
}
|
||||
|
||||
.capability-playground__runtime-note {
|
||||
border-color: var(--color-amber);
|
||||
background: rgba(184, 134, 11, 0.08);
|
||||
}
|
||||
|
||||
.capability-playground__receipt-section {
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.capability-playground__receipt-section h4 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-family: var(--font-heading);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.capability-playground__diagnostics {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
}
|
||||
|
||||
.capability-playground__diagnostics li {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.capability-playground__diagnostic-meta {
|
||||
color: var(--color-slate);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.capability-playground__diagnostics small {
|
||||
color: var(--color-slate);
|
||||
}
|
||||
|
||||
.evidence-ledger .evidence-list {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1704,6 +2030,10 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
|
||||
position: static;
|
||||
}
|
||||
|
||||
.capability-playground {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.capability-discovery__filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1818,6 +2148,16 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.capability-playground__facts,
|
||||
.capability-playground__receipt-facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.capability-playground__receipt-heading,
|
||||
.capability-playground__try-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DraftDiagnostic, DraftWorkspace } from "../domain/draft-workspace-
|
||||
import type { CapabilityDetail, CapabilitySummary } from "../domain/capability-models.js";
|
||||
import { projectAuthoringGraph, type WorkbenchSelection } from "./authoring-graph.js";
|
||||
import { withDiagnosticKeys } from "./diagnostic-key.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import { CapabilityNodeForm } from "./CapabilityNodeForm.js";
|
||||
import { SelectedCapabilityInspector } from "./SelectedCapabilityInspector.js";
|
||||
import { RouteForm } from "./RouteForm.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type SchemaField,
|
||||
} from "../schema-form/schema-field.js";
|
||||
import { serializeSchemaValues, type FieldSources } from "../schema-form/schema-values.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import { displayGraphInputPath, displayLocalInputPath } from "./input-binding-paths.js";
|
||||
import {
|
||||
capabilityLocalPathSuggestions,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
OutputBinding,
|
||||
StatePath,
|
||||
} from "../domain/draft-workspace-models.js";
|
||||
import { formatBoundedJson } from "./format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import {
|
||||
capabilityLocalPathSuggestions,
|
||||
inferredStateSchemaPreview,
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { initialState } from "../../app/state.js";
|
||||
import { useConsoleWorkspace } from "../context.js";
|
||||
import type { CapabilityCallResult, CapabilityDetail } from "../domain/capability-models.js";
|
||||
import type { ConsoleWriteExecutor } from "../domain/write-executor.js";
|
||||
import type {
|
||||
CapabilityPlaygroundController,
|
||||
} from "./useCapabilityPlayground.js";
|
||||
import { useCapabilityPlayground } from "./useCapabilityPlayground.js";
|
||||
import { CapabilityPlayground } from "./CapabilityPlayground.js";
|
||||
|
||||
vi.mock("./useCapabilityPlayground.js", () => ({
|
||||
useCapabilityPlayground: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../context.js", () => ({
|
||||
useConsoleWorkspace: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockedUseCapabilityPlayground = vi.mocked(useCapabilityPlayground);
|
||||
const mockedUseConsoleWorkspace = vi.mocked(useConsoleWorkspace);
|
||||
|
||||
const writeExecutor = {} as ConsoleWriteExecutor;
|
||||
|
||||
const nodeCapability: CapabilityDetail = {
|
||||
kind: "node_spec",
|
||||
name: "local.documents.read",
|
||||
sourceId: "local.documents",
|
||||
description: "Read project documents.",
|
||||
isAsync: false,
|
||||
outcomes: ["ok", "error"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", title: "Query" },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
outputSchema: { type: "object", properties: { documents: { type: "array" } } },
|
||||
wrapperHints: { input: "query", output: "documents" },
|
||||
acceptsContext: false,
|
||||
};
|
||||
|
||||
const wrapperCapability: CapabilityDetail = {
|
||||
kind: "wrapper_artifact",
|
||||
name: "local.documents.wrapper",
|
||||
sourceId: "local.documents",
|
||||
description: "Call the document wrapper.",
|
||||
isAsync: false,
|
||||
outcomes: ["ok", "runtime_error"],
|
||||
inputSchema: nodeCapability.inputSchema,
|
||||
outputSchema: nodeCapability.outputSchema,
|
||||
wrapperHints: { input: "query", output: "documents" },
|
||||
artifactId: "documents-wrapper",
|
||||
title: "Documents wrapper",
|
||||
version: 1,
|
||||
requiredCapabilities: {},
|
||||
};
|
||||
|
||||
const callResult = (overrides: Partial<CapabilityCallResult> = {}): CapabilityCallResult => ({
|
||||
qualifiedName: nodeCapability.name,
|
||||
sourceId: nodeCapability.sourceId,
|
||||
kind: nodeCapability.kind,
|
||||
deploymentId: null,
|
||||
outcome: "ok",
|
||||
output: { documents: ["README.md"] },
|
||||
diagnostics: [
|
||||
{
|
||||
boundSource: "local.documents",
|
||||
code: "dependency_checked",
|
||||
logicalRef: "documents",
|
||||
message: "Document source was checked.",
|
||||
repairHint: null,
|
||||
severity: "info",
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const controller = (
|
||||
overrides: Partial<CapabilityPlaygroundController> = {},
|
||||
): CapabilityPlaygroundController => ({
|
||||
phase: "idle",
|
||||
result: null,
|
||||
message: null,
|
||||
acknowledged: false,
|
||||
deploymentId: "",
|
||||
setAcknowledged: vi.fn(),
|
||||
setDeploymentId: vi.fn(),
|
||||
call: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const renderPlayground = (capability: CapabilityDetail = nodeCapability) =>
|
||||
render(
|
||||
<CapabilityPlayground
|
||||
capability={capability}
|
||||
executor={writeExecutor}
|
||||
target="http://workflow.example/rpc"
|
||||
/>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockedUseConsoleWorkspace.mockReturnValue({
|
||||
connection: {
|
||||
...initialState(),
|
||||
evidence: [
|
||||
{
|
||||
id: "call-1",
|
||||
target: "http://workflow.example/rpc",
|
||||
operation: "workflow.capabilities.call",
|
||||
label: "Call capability",
|
||||
equivalentCli: "wf capability call",
|
||||
request: {},
|
||||
response: {},
|
||||
durationMs: 18,
|
||||
},
|
||||
],
|
||||
},
|
||||
connectedTarget: "http://workflow.example/rpc",
|
||||
recordEvidence: vi.fn(),
|
||||
readExecutor: null,
|
||||
writeExecutor,
|
||||
});
|
||||
mockedUseCapabilityPlayground.mockReturnValue(controller());
|
||||
});
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("CapabilityPlayground", () => {
|
||||
it("starts on an accessible Contract view with facts, schemas, and draft action", () => {
|
||||
renderPlayground();
|
||||
|
||||
expect(screen.getByRole("tab", { name: "Contract" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "Try capability" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: nodeCapability.name })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Input schema" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Output schema" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Wrapper hints" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Add to draft" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the literal-only form and immediate execution warning in Try", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPlayground();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
|
||||
expect(screen.getByText("This calls the capability immediately against the connected workflow server.")).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox", { name: "Query" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("group", { name: "Value source" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("checkbox", { name: /I understand/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Call capability" })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("gates the call behind acknowledgement and never calls for render, tab selection, or edits", async () => {
|
||||
const user = userEvent.setup();
|
||||
const call = vi.fn();
|
||||
mockedUseCapabilityPlayground.mockReturnValue(
|
||||
controller({ acknowledged: true, call }),
|
||||
);
|
||||
renderPlayground();
|
||||
|
||||
expect(call).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
expect(call).not.toHaveBeenCalled();
|
||||
await user.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
|
||||
expect(call).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes only literal object values to the controller after acknowledgement", async () => {
|
||||
const user = userEvent.setup();
|
||||
const call = vi.fn();
|
||||
const setAcknowledged = vi.fn();
|
||||
mockedUseCapabilityPlayground.mockReturnValue(
|
||||
controller({ acknowledged: true, call, setAcknowledged }),
|
||||
);
|
||||
renderPlayground();
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
await user.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
|
||||
await user.click(screen.getByRole("button", { name: "Call capability" }));
|
||||
|
||||
expect(call).toHaveBeenCalledWith({ query: "README.md" });
|
||||
});
|
||||
|
||||
it("shows deployment input only for wrapper capabilities", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { unmount } = renderPlayground();
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
expect(screen.queryByRole("textbox", { name: "Wrapper deployment ID" })).not.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
renderPlayground(wrapperCapability);
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
expect(screen.getByRole("textbox", { name: "Wrapper deployment ID" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["calling", controller({ phase: "calling", acknowledged: true }), "Calling capability..."],
|
||||
["rejected", controller({ phase: "error", message: "Operation rejected by policy" }), "Operation rejected by policy"],
|
||||
] as const)("renders the %s state inline", async (_label, state, textContent) => {
|
||||
const user = userEvent.setup();
|
||||
mockedUseCapabilityPlayground.mockReturnValue(state);
|
||||
renderPlayground();
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
|
||||
if (state.phase === "calling") {
|
||||
expect(screen.getByRole("button", { name: textContent })).toBeDisabled();
|
||||
} else {
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(textContent);
|
||||
}
|
||||
});
|
||||
|
||||
it("renders outcome, evidence provenance, diagnostics, and bounded output in the receipt", async () => {
|
||||
mockedUseCapabilityPlayground.mockReturnValue(
|
||||
controller({ phase: "result", acknowledged: true, result: callResult() }),
|
||||
);
|
||||
renderPlayground();
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
|
||||
expect(screen.getByText("Completed")).toBeInTheDocument();
|
||||
expect(screen.getByText("18 ms")).toBeInTheDocument();
|
||||
expect(screen.getByText("workflow.capabilities.call")).toBeInTheDocument();
|
||||
expect(screen.getByText("Document source was checked.")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Capability output")).toHaveTextContent("README.md");
|
||||
});
|
||||
|
||||
it("labels runtime_error as completed without claiming a workflow run or trace", async () => {
|
||||
mockedUseCapabilityPlayground.mockReturnValue(
|
||||
controller({
|
||||
phase: "result",
|
||||
acknowledged: true,
|
||||
result: callResult({ outcome: "runtime_error", output: null, diagnostics: [] }),
|
||||
}),
|
||||
);
|
||||
renderPlayground();
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
|
||||
expect(screen.getByText("Completed with runtime error")).toBeInTheDocument();
|
||||
expect(screen.getByText("No workflow run or trace was created.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("rejects a non-object serialized root locally", async () => {
|
||||
const user = userEvent.setup();
|
||||
const call = vi.fn();
|
||||
mockedUseCapabilityPlayground.mockReturnValue(
|
||||
controller({ acknowledged: true, call }),
|
||||
);
|
||||
renderPlayground({
|
||||
...nodeCapability,
|
||||
name: "local.documents.invalid-root",
|
||||
inputSchema: { type: "array", items: { type: "string" } },
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
await user.click(screen.getByRole("button", { name: "Call capability" }));
|
||||
|
||||
expect(call).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||
"Capability inputs must serialize to an object.",
|
||||
);
|
||||
});
|
||||
|
||||
it("clears the receipt and acknowledgement when the capability changes", async () => {
|
||||
const firstState = controller({
|
||||
phase: "result",
|
||||
acknowledged: true,
|
||||
result: callResult(),
|
||||
});
|
||||
const secondState = controller();
|
||||
mockedUseCapabilityPlayground.mockImplementation((qualifiedName: string | null) =>
|
||||
qualifiedName === nodeCapability.name ? firstState : secondState,
|
||||
);
|
||||
const { rerender } = render(
|
||||
<CapabilityPlayground
|
||||
capability={nodeCapability}
|
||||
executor={writeExecutor}
|
||||
target="http://workflow.example/rpc"
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
expect(screen.getByText("Completed")).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<CapabilityPlayground
|
||||
capability={{ ...nodeCapability, name: "local.documents.write" }}
|
||||
executor={writeExecutor}
|
||||
target="http://workflow.example/rpc"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Completed")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("checkbox", { name: /I understand/i })).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,380 @@
|
||||
import { useState } from "react";
|
||||
import { AlertCircle, CheckCircle2, Clock3, Play } from "lucide-react";
|
||||
import type { EvidenceRecord } from "../../app/state.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
import type {
|
||||
CapabilityCallResult,
|
||||
CapabilityDetail,
|
||||
} from "../domain/capability-models.js";
|
||||
import type { ConsoleWriteExecutor } from "../domain/write-executor.js";
|
||||
import { useConsoleWorkspace } from "../context.js";
|
||||
import { SchemaForm } from "../schema-form/SchemaForm.js";
|
||||
import type { SchemaSerializationResult } from "../schema-form/schema-values.js";
|
||||
import { useCapabilityPlayground } from "./useCapabilityPlayground.js";
|
||||
|
||||
export type CapabilityPlaygroundProps = {
|
||||
readonly capability: CapabilityDetail;
|
||||
readonly target: string | null;
|
||||
readonly executor: ConsoleWriteExecutor | null;
|
||||
readonly onAddToDraft?: () => void;
|
||||
};
|
||||
|
||||
type PlaygroundTab = "contract" | "try";
|
||||
|
||||
const formatKind = (kind: CapabilityDetail["kind"]): string =>
|
||||
kind === "node_spec" ? "Node spec" : "Wrapper artifact";
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const latestCallEvidence = (
|
||||
evidence: ReadonlyArray<EvidenceRecord>,
|
||||
target: string | null,
|
||||
): EvidenceRecord | null => {
|
||||
for (let index = evidence.length - 1; index >= 0; index -= 1) {
|
||||
const record = evidence[index];
|
||||
if (record === undefined) continue;
|
||||
if (
|
||||
record.operation === "workflow.capabilities.call" &&
|
||||
(target === null || record.target === target)
|
||||
) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const schemaText = (value: Record<string, unknown>): string =>
|
||||
formatBoundedJson(value, 6_000);
|
||||
|
||||
const SchemaBlock = ({
|
||||
heading,
|
||||
value,
|
||||
}: {
|
||||
readonly heading: string;
|
||||
readonly value: Record<string, unknown>;
|
||||
}) => (
|
||||
<div className="capability-playground__schema-block">
|
||||
<h3>{heading}</h3>
|
||||
<details>
|
||||
<summary>View {heading.toLowerCase()}</summary>
|
||||
<pre aria-label={`${heading} JSON`}>{schemaText(value)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ContractView = ({
|
||||
capability,
|
||||
onAddToDraft,
|
||||
}: {
|
||||
readonly capability: CapabilityDetail;
|
||||
readonly onAddToDraft: (() => void) | undefined;
|
||||
}) => (
|
||||
<section
|
||||
aria-labelledby="capability-playground-contract-heading"
|
||||
className="capability-playground__panel"
|
||||
id="capability-playground-contract-panel"
|
||||
role="tabpanel"
|
||||
tabIndex={0}
|
||||
>
|
||||
<h3 id="capability-playground-contract-heading">Current contract</h3>
|
||||
<dl className="capability-playground__facts">
|
||||
<div>
|
||||
<dt>Kind</dt>
|
||||
<dd>{formatKind(capability.kind)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Source</dt>
|
||||
<dd>{capability.sourceId}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Async</dt>
|
||||
<dd>{capability.isAsync ? "yes" : "no"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Outcomes</dt>
|
||||
<dd>{capability.outcomes.join(", ") || "none"}</dd>
|
||||
</div>
|
||||
{capability.kind === "wrapper_artifact" && (
|
||||
<>
|
||||
<div>
|
||||
<dt>Artifact</dt>
|
||||
<dd>{capability.artifactId}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Version</dt>
|
||||
<dd>{capability.version}</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
{capability.description && <p className="capability-playground__description">{capability.description}</p>}
|
||||
<div className="capability-playground__schemas">
|
||||
<SchemaBlock heading="Input schema" value={capability.inputSchema} />
|
||||
<SchemaBlock heading="Output schema" value={capability.outputSchema} />
|
||||
<SchemaBlock heading="Wrapper hints" value={capability.wrapperHints} />
|
||||
</div>
|
||||
<button onClick={onAddToDraft} type="button">
|
||||
Add to draft
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
|
||||
const formatSerializationIssues = (
|
||||
result: SchemaSerializationResult,
|
||||
): string => result.issues.map((issue) => issue.message).join(" ");
|
||||
|
||||
const outcomeLabel = (outcome: string): string =>
|
||||
outcome === "runtime_error" ? "Completed with runtime error" : "Completed";
|
||||
|
||||
const ResultReceipt = ({
|
||||
result,
|
||||
evidence,
|
||||
}: {
|
||||
readonly result: CapabilityCallResult;
|
||||
readonly evidence: EvidenceRecord | null;
|
||||
}) => (
|
||||
<section
|
||||
aria-labelledby="capability-playground-result-heading"
|
||||
className="capability-playground__receipt"
|
||||
>
|
||||
<div className="capability-playground__receipt-heading">
|
||||
<div>
|
||||
<p className="capability-playground__receipt-label">Result receipt</p>
|
||||
<h3 id="capability-playground-result-heading">{result.qualifiedName}</h3>
|
||||
</div>
|
||||
<p
|
||||
className="capability-playground__outcome"
|
||||
data-outcome={result.outcome === "runtime_error" ? "runtime-error" : "completed"}
|
||||
>
|
||||
<CheckCircle2 aria-hidden="true" size={17} strokeWidth={2} />
|
||||
<span>{outcomeLabel(result.outcome)}</span>
|
||||
</p>
|
||||
</div>
|
||||
<dl className="capability-playground__receipt-facts">
|
||||
<div>
|
||||
<dt>Outcome</dt>
|
||||
<dd>{result.outcome}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Deployment</dt>
|
||||
<dd>{result.deploymentId ?? "default"}</dd>
|
||||
</div>
|
||||
{evidence && (
|
||||
<>
|
||||
<div>
|
||||
<dt>Evidence operation</dt>
|
||||
<dd>{evidence.operation}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Duration</dt>
|
||||
<dd>
|
||||
<Clock3 aria-hidden="true" size={14} strokeWidth={1.8} />
|
||||
{evidence.durationMs} ms
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Target</dt>
|
||||
<dd>{evidence.target}</dd>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
{result.outcome === "runtime_error" && (
|
||||
<p className="capability-playground__runtime-note">
|
||||
No workflow run or trace was created.
|
||||
</p>
|
||||
)}
|
||||
<div className="capability-playground__receipt-section">
|
||||
<h4>Diagnostics</h4>
|
||||
{result.diagnostics.length > 0 ? (
|
||||
<ul className="capability-playground__diagnostics">
|
||||
{result.diagnostics.map((diagnostic, index) => (
|
||||
<li key={`${diagnostic.code}-${index}`}>
|
||||
<span className="capability-playground__diagnostic-meta">
|
||||
{diagnostic.severity} / {diagnostic.code}
|
||||
</span>
|
||||
<span>{diagnostic.message}</span>
|
||||
{diagnostic.repairHint && (
|
||||
<small>Next: {diagnostic.repairHint}</small>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="capability-playground__muted">No diagnostics returned.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="capability-playground__receipt-section">
|
||||
<h4>Output</h4>
|
||||
{result.output === null ? (
|
||||
<p className="capability-playground__muted">No output returned.</p>
|
||||
) : (
|
||||
<pre aria-label="Capability output">
|
||||
{formatBoundedJson(result.output)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
const TryView = ({
|
||||
capability,
|
||||
target,
|
||||
executor,
|
||||
}: {
|
||||
readonly capability: CapabilityDetail;
|
||||
readonly target: string | null;
|
||||
readonly executor: ConsoleWriteExecutor | null;
|
||||
}) => {
|
||||
const controller = useCapabilityPlayground(capability.name);
|
||||
const { connection } = useConsoleWorkspace();
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const evidence =
|
||||
controller.phase === "result"
|
||||
? latestCallEvidence(connection.evidence, target)
|
||||
: null;
|
||||
const operationAvailable = executor !== null && target !== null && controller.phase !== "disconnected";
|
||||
|
||||
const handleSubmit = (result: SchemaSerializationResult): void => {
|
||||
setLocalError(null);
|
||||
if (result.issues.length > 0) {
|
||||
setLocalError(formatSerializationIssues(result));
|
||||
return;
|
||||
}
|
||||
if (!isRecord(result.value)) {
|
||||
setLocalError("Capability inputs must serialize to an object.");
|
||||
return;
|
||||
}
|
||||
controller.call(result.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="capability-playground-try-heading"
|
||||
className="capability-playground__panel capability-playground__panel--try"
|
||||
id="capability-playground-try-panel"
|
||||
role="tabpanel"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="capability-playground__try-heading">
|
||||
<div>
|
||||
<h3 id="capability-playground-try-heading">Try capability</h3>
|
||||
<p>
|
||||
Use literal values from this form. Nothing runs until you acknowledge the immediate call.
|
||||
</p>
|
||||
</div>
|
||||
<Play aria-hidden="true" size={20} strokeWidth={1.8} />
|
||||
</div>
|
||||
<div className="capability-playground__warning" role="note">
|
||||
<AlertCircle aria-hidden="true" size={18} strokeWidth={1.8} />
|
||||
<p>This calls the capability immediately against the connected workflow server.</p>
|
||||
</div>
|
||||
{!operationAvailable ? (
|
||||
<p className="capability-playground__disabled" role="status">
|
||||
Capability calls are unavailable until a workflow server is connected.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{capability.kind === "wrapper_artifact" && (
|
||||
<div className="capability-playground__deployment">
|
||||
<label htmlFor="capability-wrapper-deployment">Wrapper deployment ID</label>
|
||||
<input
|
||||
id="capability-wrapper-deployment"
|
||||
onChange={(event) => controller.setDeploymentId(event.target.value)}
|
||||
type="text"
|
||||
value={controller.deploymentId}
|
||||
/>
|
||||
<small>Optional. Leave blank to use the server default.</small>
|
||||
</div>
|
||||
)}
|
||||
<label className="capability-playground__acknowledgement">
|
||||
<input
|
||||
checked={controller.acknowledged}
|
||||
onChange={(event) => controller.setAcknowledged(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>I understand this executes the capability now.</span>
|
||||
</label>
|
||||
<fieldset
|
||||
className="capability-playground__form-fieldset"
|
||||
disabled={!controller.acknowledged || controller.phase === "calling"}
|
||||
>
|
||||
<legend className="visually-hidden">Literal capability inputs</legend>
|
||||
<SchemaForm
|
||||
schema={capability.inputSchema}
|
||||
onValueChange={() => setLocalError(null)}
|
||||
onSubmit={handleSubmit}
|
||||
showSourceControls={false}
|
||||
submitLabel={controller.phase === "calling" ? "Calling capability..." : "Call capability"}
|
||||
/>
|
||||
</fieldset>
|
||||
</>
|
||||
)}
|
||||
{localError && (
|
||||
<p className="capability-playground__local-error" role="alert">
|
||||
{localError}
|
||||
</p>
|
||||
)}
|
||||
{controller.phase === "error" && controller.message && (
|
||||
<p className="capability-playground__local-error" role="alert">
|
||||
{controller.message}
|
||||
</p>
|
||||
)}
|
||||
{controller.phase === "result" && controller.result && (
|
||||
<ResultReceipt result={controller.result} evidence={evidence} />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export const CapabilityPlayground = ({
|
||||
capability,
|
||||
target,
|
||||
executor,
|
||||
onAddToDraft,
|
||||
}: CapabilityPlaygroundProps) => {
|
||||
const [activeTab, setActiveTab] = useState<PlaygroundTab>("contract");
|
||||
const tabId = (tab: PlaygroundTab): string => `capability-playground-tab-${tab}`;
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-labelledby="capability-playground-heading"
|
||||
className="capability-playground"
|
||||
id="capability-detail"
|
||||
>
|
||||
<div className="capability-playground__header">
|
||||
<h2 id="capability-playground-heading">{capability.name}</h2>
|
||||
<span className="capability-playground__source">{capability.sourceId}</span>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Capability detail views"
|
||||
className="capability-playground__tabs"
|
||||
role="tablist"
|
||||
>
|
||||
{([
|
||||
["contract", "Contract"],
|
||||
["try", "Try capability"],
|
||||
] as const).map(([tab, label]) => (
|
||||
<button
|
||||
aria-controls={`capability-playground-${tab}-panel`}
|
||||
aria-selected={activeTab === tab}
|
||||
className="capability-playground__tab"
|
||||
id={tabId(tab)}
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
role="tab"
|
||||
type="button"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{activeTab === "contract" ? (
|
||||
<ContractView capability={capability} onAddToDraft={onAddToDraft} />
|
||||
) : (
|
||||
<TryView capability={capability} executor={executor} target={target} />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -183,6 +183,11 @@ describe("DiscoverRoute", () => {
|
||||
);
|
||||
renderRoute();
|
||||
|
||||
expect(screen.getByRole("tab", { name: "Contract" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByRole("tab", { name: "Try capability" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Input schema" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Output schema" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Wrapper hints" })).toBeInTheDocument();
|
||||
@@ -190,6 +195,36 @@ describe("DiscoverRoute", () => {
|
||||
expect(screen.getByRole("button", { name: "Add to draft" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps a disconnected operation state inline with the selected capability", async () => {
|
||||
mockedUseCapabilityDiscovery.mockReturnValue(
|
||||
controller({
|
||||
selected: {
|
||||
...summary,
|
||||
isAsync: false,
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
outputSchema: {},
|
||||
wrapperHints: {},
|
||||
acceptsContext: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
mockedUseConsoleWorkspace.mockReturnValue({
|
||||
connection: initialState(),
|
||||
connectedTarget: null,
|
||||
recordEvidence: vi.fn(),
|
||||
readExecutor: null,
|
||||
writeExecutor: null,
|
||||
});
|
||||
renderRoute();
|
||||
|
||||
await userEvent.click(screen.getByRole("tab", { name: "Try capability" }));
|
||||
|
||||
expect(
|
||||
screen.getByText("Capability calls are unavailable until a workflow server is connected."),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Capabilities" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("exposes selected row state and associates the result with its detail", () => {
|
||||
mockedUseCapabilityDiscovery.mockReturnValue(
|
||||
controller({
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { Boxes, PackageOpen } from "lucide-react";
|
||||
import type {
|
||||
CapabilityDetail,
|
||||
CapabilitySummary,
|
||||
} from "../domain/capability-models.js";
|
||||
import type { CapabilitySummary } from "../domain/capability-models.js";
|
||||
import { CreateDraftDialog } from "../authoring/CreateDraftDialog.js";
|
||||
import { useConsoleWorkspace } from "../context.js";
|
||||
import { CapabilityPlayground } from "./CapabilityPlayground.js";
|
||||
import { useCapabilityDiscovery } from "./useCapabilityDiscovery.js";
|
||||
|
||||
const formatKind = (kind: CapabilitySummary["kind"]): string =>
|
||||
@@ -15,19 +14,6 @@ const KindIcon = ({ kind }: { readonly kind: CapabilitySummary["kind"] }) => {
|
||||
return <Icon aria-hidden="true" size={16} strokeWidth={1.8} />;
|
||||
};
|
||||
|
||||
const SchemaBlock = ({
|
||||
heading,
|
||||
value,
|
||||
}: {
|
||||
readonly heading: string;
|
||||
readonly value: Record<string, unknown>;
|
||||
}) => (
|
||||
<div className="capability-discovery__schema-block">
|
||||
<h3>{heading}</h3>
|
||||
<pre>{JSON.stringify(value, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CapabilityRow = ({
|
||||
item,
|
||||
selected,
|
||||
@@ -64,33 +50,9 @@ const CapabilityRow = ({
|
||||
</li>
|
||||
);
|
||||
|
||||
const DetailView = ({
|
||||
detail,
|
||||
onAddToDraft,
|
||||
}: {
|
||||
readonly detail: CapabilityDetail;
|
||||
readonly onAddToDraft: () => void;
|
||||
}) => (
|
||||
<section aria-labelledby="capability-detail-heading" className="capability-discovery__detail" id="capability-detail">
|
||||
<p className="workspace-route-pending__eyebrow">Selected contract</p>
|
||||
<h2 id="capability-detail-heading">{detail.name}</h2>
|
||||
<dl className="capability-discovery__facts">
|
||||
<div><dt>Kind</dt><dd>{formatKind(detail.kind)}</dd></div>
|
||||
<div><dt>Source</dt><dd>{detail.sourceId}</dd></div>
|
||||
<div><dt>Async</dt><dd>{detail.isAsync ? "yes" : "no"}</dd></div>
|
||||
<div><dt>Outcomes</dt><dd>{detail.outcomes.join(", ") || "none"}</dd></div>
|
||||
</dl>
|
||||
<div className="capability-discovery__schemas">
|
||||
<SchemaBlock heading="Input schema" value={detail.inputSchema} />
|
||||
<SchemaBlock heading="Output schema" value={detail.outputSchema} />
|
||||
<SchemaBlock heading="Wrapper hints" value={detail.wrapperHints} />
|
||||
</div>
|
||||
<button onClick={onAddToDraft} type="button">Add to draft</button>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const DiscoverRoute = () => {
|
||||
const discovery = useCapabilityDiscovery();
|
||||
const { connectedTarget, writeExecutor } = useConsoleWorkspace();
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const isReady = discovery.phase === "ready";
|
||||
|
||||
@@ -174,9 +136,12 @@ export const DiscoverRoute = () => {
|
||||
</section>
|
||||
|
||||
{discovery.selected ? (
|
||||
<DetailView
|
||||
detail={discovery.selected}
|
||||
<CapabilityPlayground
|
||||
capability={discovery.selected}
|
||||
executor={writeExecutor}
|
||||
key={discovery.selected.name}
|
||||
onAddToDraft={() => setCreateDialogOpen(true)}
|
||||
target={connectedTarget}
|
||||
/>
|
||||
) : (
|
||||
<section aria-labelledby="capability-detail-empty-heading" className="capability-discovery__detail capability-discovery__detail--empty" id="capability-detail">
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { DraftWorkspace } from "../domain/draft-workspace-models.js";
|
||||
import type { DraftWorkspaceController } from "./useDraftWorkspace.js";
|
||||
import { useDraftWorkspace } from "./useDraftWorkspace.js";
|
||||
import { DraftDetailRoute } from "./DraftDetailRoute.js";
|
||||
import { formatBoundedJson } from "../authoring/format-bounded-json.js";
|
||||
import { formatBoundedJson } from "../domain/format-bounded-json.js";
|
||||
|
||||
const globalStyles = readFileSync(
|
||||
"src/styles/global.css",
|
||||
|
||||
Reference in New Issue
Block a user