feat(workspace,settings): model menu chip in the composer, compact settings panels

The workspace home's composer takes ONE Model chip (ModelMenu: model list
plus Effort) and moves AI Router / Permission / Local-New worktree into the
row under it; Settings drops the General-page cost card and export row,
PricingSettings and the empty states are compacted for settings panels.
This commit is contained in:
2026-10-02 17:49:05 +07:00
parent da1b17778c
commit fadecaddd0
23 changed files with 925 additions and 640 deletions
@@ -5,7 +5,9 @@
* open, `/` only where slash commands apply), Shift/Alt+Enter newlines, a
* pasted image shown as a thumbnail and sent as its saved path, and `ModelPicker` — a tool's own
* list from the server, a fixed list (an AI Router's models) when given one,
* and Custom…, which is a sentinel, never a model id.
* and Custom…, which is a sentinel, never a model id — and `ModelMenu`, the
* same list as one chip: aliases up front, pinned versions under More models,
* plus the effort slider.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -14,7 +16,13 @@ import { useState } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "i18next";
import { ModelPicker, PromptEditor, promptWithImages, type PromptImage } from "../RunSetup";
import {
ModelMenu,
ModelPicker,
PromptEditor,
promptWithImages,
type PromptImage,
} from "../RunSetup";
import { api } from "../../../lib/api";
vi.mock("../../../lib/api", async (importOriginal) => {
@@ -217,3 +225,24 @@ describe("ModelPicker", () => {
expect(screen.queryByText("Sonnet (latest · 5.5)")).toBeNull();
});
});
describe("ModelMenu", () => {
it("puts the aliases up front, pinned versions under More models, and sets the effort", async () => {
const onChange = vi.fn();
const onEffort = vi.fn();
render(
<ModelMenu value="" onChange={onChange} tool="claude" effort="" onEffortChange={onEffort} />
);
fireEvent.click(screen.getByTitle(i18n.t("run:fields.model")));
await screen.findByText("Sonnet (latest · 5.5)");
expect(screen.queryByText("Claude Sonnet 5.5")).toBeNull();
// Low, Medium, High, … — index 2 is High.
fireEvent.change(screen.getByRole("slider", { name: i18n.t("run:fields.effort") }), {
target: { value: "2" },
});
expect(onEffort).toHaveBeenCalledWith("high");
fireEvent.click(screen.getByText(i18n.t("run:fields.moreModels")));
fireEvent.click(screen.getByText("Claude Sonnet 5.5"));
expect(onChange).toHaveBeenCalledWith("claude-sonnet-5-5");
});
});