feat(webui): add first-run AI setup flow

This commit is contained in:
Xubin Ren
2026-09-02 16:25:07 +08:00
parent 042f96f6ba
commit b47f0980f1
15 changed files with 401 additions and 39 deletions
+37 -9
View File
@@ -681,7 +681,7 @@ describe("ThreadShell", () => {
);
expect(await screen.findByTitle("fast · gpt-5.5 · OpenAI Codex")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Model not configured" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Choose your AI" })).not.toBeInTheDocument();
});
it("switches through every named preset while preserving call-order priority", async () => {
@@ -763,7 +763,7 @@ describe("ThreadShell", () => {
);
expect(await screen.findByTitle("fast · gpt-4 · Company Proxy")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Model not configured" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Choose your AI" })).not.toBeInTheDocument();
});
it("shows the effective fallback model in the composer badge", async () => {
@@ -835,7 +835,7 @@ describe("ThreadShell", () => {
expect(screen.getByText("Default")).toBeInTheDocument();
});
it("opens model settings from the unconfigured model badge", async () => {
it("opens first-run model setup without clearing the draft", async () => {
const client = makeClient();
const settings = modelSettings("openai-codex/gpt-5.1-codex", "openai_codex");
settings.agent.has_api_key = false;
@@ -844,6 +844,20 @@ describe("ThreadShell", () => {
? { ...provider, auth_type: "oauth", configured: false }
: provider,
);
settings.providers.push(
{
name: "xai_grok",
label: "xAI Grok",
auth_type: "oauth",
configured: true,
},
{
name: "ollama",
label: "Ollama",
configured: true,
api_base: "http://127.0.0.1:11434",
},
);
const onOpenModelSettings = vi.fn();
render(
@@ -860,17 +874,31 @@ describe("ThreadShell", () => {
),
);
const badge = await screen.findByRole("button", { name: "Model not configured" });
expect(screen.getByTestId("composer-model-setup-icon")).toBeInTheDocument();
const badge = await screen.findByRole("button", { name: "Choose your AI" });
const setupIcon = screen.getByTestId("composer-model-setup-icon");
expect(setupIcon).toBeInTheDocument();
expect(setupIcon.parentElement).not.toHaveClass("border-amber-500/35");
expect(screen.queryByTestId("composer-model-logo-openai_codex")).not.toBeInTheDocument();
fireEvent.click(badge);
expect(onOpenModelSettings).toHaveBeenCalledTimes(1);
expect(await screen.findByRole("dialog", { name: "Choose your AI" })).toBeInTheDocument();
expect(screen.getAllByText("Ready")).toHaveLength(3);
expect(onOpenModelSettings).not.toHaveBeenCalled();
fireEvent.change(screen.getByRole("textbox", { name: "Message input" }), {
fireEvent.click(screen.getByRole("button", { name: "Close" }));
const input = screen.getByRole("textbox", { name: "Message input" });
fireEvent.change(input, {
target: { value: "hello" },
});
fireEvent.click(screen.getByRole("button", { name: "Configure model" }));
expect(onOpenModelSettings).toHaveBeenCalledTimes(2);
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
expect(await screen.findByRole("dialog", { name: "Choose your AI" })).toBeInTheDocument();
expect(input).toHaveValue("hello");
fireEvent.click(screen.getByRole("button", { name: "Use an API key" }));
expect(onOpenModelSettings).toHaveBeenCalledTimes(1);
expect(input).toHaveValue("hello");
await waitFor(() => expect(input).toHaveFocus());
expect(client.sendMessage).not.toHaveBeenCalled();
});