Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions apps/portal/src/pages/SubmitRun.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { Meta, StoryObj } from "@storybook/react-vite";
import { useEffect } from "react";
import { http, HttpResponse } from "msw";
import { expect, userEvent } from "storybook/test";
import { useProjectContext } from "@/contexts/ProjectContext";
import { SubmitRun } from "./SubmitRun";

function InDemoProject({ children }: { children: React.ReactNode }) {
const { selectedProjectId, setSelectedProjectId } = useProjectContext();
useEffect(() => setSelectedProjectId("demo-project"), [setSelectedProjectId]);
return selectedProjectId === "demo-project" ? children : null;
}

const profileVersion = {
_id: "profile-version-1",
profileId: "profile-1",
version: 1,
workerType: "coder-acp-copilot",
model: "gpt-5.6-sol",
createdAt: "2026-01-01T00:00:00.000Z",
};

const meta = {
title: "pages/SubmitRun",
component: SubmitRun,
decorators: [
(Story) => <InDemoProject><Story /></InDemoProject>,
],
parameters: {
msw: {
handlers: [
http.get("/api/v1/agents", () => HttpResponse.json([{
_id: "coder-acp-copilot",
name: "GitHub Copilot CLI",
supportedModels: ["gpt-5.6-sol"],
available: true,
versions: [{ agentVersion: "1.0.0", status: "active", queueName: "queue-copilot" }],
createdAt: "2026-01-01T00:00:00.000Z",
}])),
http.get("/api/v1/profiles", () => HttpResponse.json([{
_id: "profile-1",
name: "Base profile",
latestVersion: 1,
createdAt: "2026-01-01T00:00:00.000Z",
version: profileVersion,
}])),
http.get("/api/v1/profiles/profile-1/versions", () => HttpResponse.json([profileVersion])),
],
},
},
} satisfies Meta<typeof SubmitRun>;

export default meta;
type Story = StoryObj<typeof meta>;

/** Profile composition uses the selected base and variation rows without a graph. */
export const ProfileVariations: Story = {
play: async ({ canvas }) => {
const picker = await canvas.findByPlaceholderText("Search existing profiles…");
await userEvent.click(picker);
await userEvent.click(await canvas.findByRole("listitem", { name: /Base profile/ }));
await expect(canvas.getByText("Variations")).toBeVisible();
await expect(canvas.queryByLabelText("Profile composition graph")).not.toBeInTheDocument();
},
};
132 changes: 0 additions & 132 deletions apps/portal/src/pages/SubmitRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ type VariationDraft = {
color?: string;
};

type GraphSelection = { kind: "base" } | { kind: "variation"; index: number };

type GateDraft = {
enabled: boolean;
promptText: string;
Expand Down Expand Up @@ -250,7 +248,6 @@ export function SubmitRun() {
const [resourcesOpen, setResourcesOpen] = useState(false);
const [agentsMdOpen, setAgentsMdOpen] = useState(false);
const [extensionsOpen, setExtensionsOpen] = useState(false);
const [graphSelection, setGraphSelection] = useState<GraphSelection>({ kind: "base" });

// AI generation
const [showGenerate, setShowGenerate] = useState(false);
Expand Down Expand Up @@ -750,53 +747,6 @@ export function SubmitRun() {
? profileList.find((p) => p._id === lastVariationDraft.profileId)?.name
: null;

useEffect(() => {
setGraphSelection({ kind: "base" });
}, [selectedProfileId]);

useEffect(() => {
setGraphSelection((previous) => {
if (previous.kind === "variation" && previous.index >= variationDrafts.length) {
return { kind: "base" };
}
return previous;
});
}, [variationDrafts.length]);

const selectedGraphPreview = (() => {
if (!selectedProfileId) return null;

if (graphSelection.kind === "base") {
const versionDocument =
profileVersions.find((version) => version.version === selectedProfileVersion) ??
selectedBaseProfile?.version ??
null;
return {
title: "Base Profile",
name: selectedBaseProfile?.name ?? selectedProfileId,
version: selectedProfileVersion ?? selectedBaseProfile?.latestVersion ?? null,
color: "hsl(var(--primary))",
versionDocument,
};
}

const draft = variationDrafts[graphSelection.index];
if (!draft) return null;
const profile = profileList.find((item) => item._id === draft.profileId);
const versionDocument =
(variationProfileVersions[draft.profileId] ?? []).find((version) => version.version === draft.profileVersion) ??
profile?.version ??
null;

return {
title: `Variation #${graphSelection.index + 1}`,
name: profile?.name ?? draft.profileId,
version: draft.profileVersion ?? profile?.latestVersion ?? null,
color: draft.color ?? VARIATION_COLORS[graphSelection.index % VARIATION_COLORS.length],
versionDocument,
};
})();

useCommandEnter(doSubmit, canSubmit);

// ─── Render helpers ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -1471,88 +1421,6 @@ export function SubmitRun() {

{selectedProfileId && (
<div className="space-y-3 rounded-md border p-3">
<div className="rounded-md border bg-muted/30 p-3">
<svg
className="h-auto w-full"
viewBox={`0 0 320 ${Math.max(70, 50 + variationDrafts.length * 34)}`}
preserveAspectRatio="none"
aria-label="Profile composition graph"
>
{(() => {
const baseName = selectedBaseProfile?.name ?? selectedProfileId ?? "Base profile";
return (
<g
onClick={() => setGraphSelection({ kind: "base" })}
style={{ cursor: "pointer" }}
>
<text
x="34"
y="22"
fontSize="10"
fill="currentColor"
className="text-foreground"
>
{truncate(baseName, 28)}
</text>
</g>
);
})()}
<circle
cx="20"
cy="18"
r={graphSelection.kind === "base" ? 7 : 6}
fill="hsl(var(--primary))"
style={{ cursor: "pointer" }}
onClick={() => setGraphSelection({ kind: "base" })}
/>
{variationDrafts.map((draft, index) => {
const y = 48 + index * 34;
const color = draft.color ?? VARIATION_COLORS[index % VARIATION_COLORS.length];
const variationName = profileList.find((p) => p._id === draft.profileId)?.name ?? "Variation";
return (
<g
key={`variation-link-${index}`}
onClick={() => setGraphSelection({ kind: "variation", index })}
style={{ cursor: "pointer" }}
>
<path
d={`M20 24 V${y} H68`}
stroke={color}
strokeWidth="1.75"
strokeDasharray="4 4"
fill="none"
>
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="0.9s" repeatCount="indefinite" />
</path>
<circle cx="68" cy={y} r={graphSelection.kind === "variation" && graphSelection.index === index ? 6 : 5} fill={color} />
<text x="80" y={y + 4} fontSize="9" fill="currentColor" className="text-foreground">
{truncate(variationName, 28)}
</text>
</g>
);
})}
</svg>
</div>

{selectedGraphPreview && (
<div className="rounded-md border bg-background p-2.5 text-xs">
<p className="font-medium text-foreground">{selectedGraphPreview.title}</p>
<p className="mt-1 truncate" style={{ color: selectedGraphPreview.color }}>
{selectedGraphPreview.name} {selectedGraphPreview.version ? `v${selectedGraphPreview.version}` : ""}
</p>
<p className="mt-1 text-muted-foreground">
{selectedGraphPreview.versionDocument
? (agentNameById.get(selectedGraphPreview.versionDocument.workerType) ?? "Unknown agent")
: "worker: n/a"}{" "}
· {selectedGraphPreview.versionDocument?.model ?? "model: n/a"}
</p>
<p className="mt-1 text-muted-foreground">
{(selectedGraphPreview.versionDocument?.mcpServers?.length ?? 0)} MCP · {(selectedGraphPreview.versionDocument?.skillRevisions?.length ?? 0)} skills · {(selectedGraphPreview.versionDocument?.extensions?.length ?? 0)} extensions
· {(selectedGraphPreview.versionDocument?.resources?.length ?? 0)} resources
</p>
</div>
)}

<div className="flex items-center justify-between">
<div>
<Label className="text-sm">Variations</Label>
Expand Down
3 changes: 3 additions & 0 deletions docs/architecture/app-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,9 @@ Profile fan-out mode is also supported for comparative runs:
3. Each expanded request resolves configuration from its variation profile; unpinned specs resolve to `latestVersion`
4. Expanded requests persist `profileId` and `profileVersionId` on each `RequestDocument` for indexing and traceability

The Portal's Submit Run form shows the base profile and editable variation rows
directly. It omits the former composition graph because it repeated those rows.

## Runs List Query API

`GET /api/v1/requests` powers the Portal **Runs** list and `scope run list`. All
Expand Down