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
86 changes: 86 additions & 0 deletions apps/portal/src/pages/SubmitRun.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { useEffect, type ReactNode } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { http, HttpResponse } from "msw";
import { expect, userEvent } from "storybook/test";

import { useProjectContext } from "@/contexts/ProjectContext";
import type { CodingAgent, ProfileWithVersion } from "@/types";
import { SubmitRun } from "./SubmitRun";

const agent: CodingAgent = {
_id: "coder-acp-copilot",
name: "GitHub Copilot CLI",
supportedModels: ["gpt-5.6-sol"],
defaultModel: "gpt-5.6-sol",
available: true,
versions: [{
agentVersion: "1.0.0",
workerVersion: "1.0.0",
components: {},
gitCommit: "abc123",
buildTime: "2026-01-01T00:00:00.000Z",
imageTag: "demo",
queueName: "queue-coder-acp-copilot",
status: "active",
createdAt: "2026-01-01T00:00:00.000Z",
}],
createdAt: "2026-01-01T00:00:00.000Z",
};

const profiles: ProfileWithVersion[] = ["Baseline", "Variation One", "Variation Two"].map((name, index) => ({
_id: `profile-${index + 1}`,
name,
latestVersion: 1,
createdAt: "2026-01-01T00:00:00.000Z",
version: {
_id: `version-${index + 1}`,
profileId: `profile-${index + 1}`,
version: 1,
workerType: agent._id,
model: "gpt-5.6-sol",
createdAt: "2026-01-01T00:00:00.000Z",
},
}));

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

const meta = {
component: SubmitRun,
decorators: [(Story) => <WithDemoProject><Story /></WithDemoProject>],
parameters: {
msw: { handlers: [
http.get("*/api/v1/agents", () => HttpResponse.json([agent])),
http.get("*/api/v1/profiles", () => HttpResponse.json(profiles)),
http.get("*/api/v1/profiles/:profileId/versions", ({ params }) =>
HttpResponse.json(profiles.filter((profile) => profile._id === params.profileId).map((profile) => profile.version))),
http.get("*/api/v1/models", () => HttpResponse.json([{ modelId: "gpt-5.6-sol" }])),
http.get("*/api/v1/mcp/servers", () => HttpResponse.json([])),
http.get("*/api/v1/requests", () => HttpResponse.json({ data: [] })),
http.get("*/api/v1/version", () => HttpResponse.json({ commit: "demo", buildTime: "2026-01-01T00:00:00.000Z" })),
] },
},
} satisfies Meta<typeof SubmitRun>;

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

export const SearchVariations: Story = {
play: async ({ canvas }) => {
await userEvent.click(await canvas.findByPlaceholderText("Search existing profiles…"));
await userEvent.click(canvas.getByText("Baseline", { selector: "li p" }));
await userEvent.click(canvas.getByRole("button", { name: /Add after Baseline/ }));

const variationPicker = canvas.getByPlaceholderText("Variation One");
await userEvent.type(variationPicker, "Variation Two");
await userEvent.click(canvas.getByText("Variation Two", { selector: "li p" }));

await expect(canvas.getByPlaceholderText("Variation Two")).toBeVisible();
},
};
30 changes: 10 additions & 20 deletions apps/portal/src/pages/SubmitRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1610,32 +1610,22 @@ export function SubmitRun() {
<div className="flex items-end gap-2">
<div className="flex-1 space-y-1">
<Label className="text-xs">Variation #{index + 1}</Label>
<Select
value={draft.profileId || undefined}
onValueChange={(value) => {
<ProfilePicker
profiles={availableProfiles}
selectedProfileId={draft.profileId || null}
placeholder={selectedVariationProfile
? `${selectedVariationProfile.name}${selectedVariationIsUnavailable ? " (unavailable)" : ""}`
: "Search profiles…"}
workerNameById={agentNameById}
onSelect={(value) => {
if (!value) return;
const selectedProfile = profileList.find((p) => p._id === value);
updateVariationDraft(index, {
profileId: value,
profileVersion: selectedProfile?.latestVersion,
});
}}
>
<SelectTrigger>
<SelectValue placeholder="Select profile" />
</SelectTrigger>
<SelectContent>
{selectedVariationIsUnavailable && selectedVariationProfile && (
<SelectItem value={selectedVariationProfile._id} disabled>
{selectedVariationProfile.name} (unavailable)
</SelectItem>
)}
{availableProfiles.map((p) => (
<SelectItem key={p._id} value={p._id}>
{p.name}
</SelectItem>
))}
</SelectContent>
</Select>
/>
{!draft.profileId && hasAvailableProfiles && (
<p className="text-xs text-muted-foreground">Select a profile for this variation.</p>
)}
Expand Down
5 changes: 5 additions & 0 deletions docs/architecture/app-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,11 @@ 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

On the Portal submit form, base and variation profiles use the same searchable
picker. Variation choices exclude the base and profiles already used by another
variation; changing a variation updates its selected version to that profile's
latest version.

## Runs List Query API

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