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
34 changes: 30 additions & 4 deletions src/features/chat/ui/AgentModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ type PopoverContentAlign = NonNullable<
>;
const REASONING_EFFORT_COLUMN_TRANSITION_MS = 240;
const PICKER_WIDTH_COMPACT_PX = 420;
// Harness agents (pi-acp, codex-acp, etc.) can report long ACP model names
// (e.g. provider/id names), so they get wider pickers than Goose, whose
// curated model names are short. With a reasoning-effort column present,
// the agent (11.75rem) + reasoning (11rem) columns alone consume ~23rem,
// so the picker needs extra width for the model column to breathe.
const PICKER_WIDTH_HARNESS_PX = 560;
const PICKER_WIDTH_HARNESS_EXPANDED_PX = 736;
const PICKER_WIDTH_EXPANDED_PX = 596;

function toSentenceCaseLabel(value: string | undefined): string {
Expand Down Expand Up @@ -395,10 +402,19 @@ export function AgentModelPicker({
!modelBrowsing &&
(agents.length > 1 || hasAgentNeedingSetup);
const showReasoningEffortColumn = showReasoningEffort;
// Harness agents get wider pickers (and flexible model columns) whenever
// their agent column is visible, with or without a reasoning-effort
// column, so long ACP model names are not truncated. Goose is excluded
// because its curated model names are short.
const widenForHarnessAgent = showAgentColumn && selectedAgentId !== "goose";
const isWidePicker = showReasoningEffortColumn && showAgentColumn;
const pickerWidth = isWidePicker
? PICKER_WIDTH_EXPANDED_PX
: PICKER_WIDTH_COMPACT_PX;
? widenForHarnessAgent
? PICKER_WIDTH_HARNESS_EXPANDED_PX
: PICKER_WIDTH_EXPANDED_PX
: widenForHarnessAgent
? PICKER_WIDTH_HARNESS_PX
: PICKER_WIDTH_COMPACT_PX;

// Land keyboard focus in the revealed column, since the reveal button that
// held focus unmounts with it.
Expand Down Expand Up @@ -500,7 +516,13 @@ export function AgentModelPicker({
// gated single-column layout has no dead vertical space below the
// model list.
"flex max-h-[min(24rem,50vh)] flex-col overflow-hidden p-1 transition-[width] duration-[240ms] ease-[cubic-bezier(0.2,0,0,1)]",
isWidePicker ? "w-[37.25rem]" : "w-[26.25rem]",
isWidePicker
? widenForHarnessAgent
? "w-[46rem]"
: "w-[37.25rem]"
: widenForHarnessAgent
? "w-[35rem]"
: "w-[26.25rem]",
)}
onInteractOutside={(event) => {
classifyOutsideInteraction(event.target);
Expand Down Expand Up @@ -672,7 +694,11 @@ export function AgentModelPicker({
data-col="model"
className={cn(
"flex min-h-0 min-w-0 overflow-hidden p-1",
showAgentColumn ? "ml-1 w-56 shrink-0" : "flex-1",
showAgentColumn
? widenForHarnessAgent
? "ml-1 flex-1"
: "ml-1 w-56 shrink-0"
: "flex-1",
)}
>
{modelsLoading ? (
Expand Down
5 changes: 4 additions & 1 deletion src/features/chat/ui/AgentModelPickerLists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,10 @@ export const RecommendedModelList = forwardRef<
{providerIcon}
</span>
) : null}
<div className="min-w-0 flex-1 truncate">
<div
className="min-w-0 flex-1 truncate"
title={getModelDisplayName(model)}
>
{getModelDisplayName(model)}
</div>
</div>
Expand Down
90 changes: 90 additions & 0 deletions src/features/chat/ui/__tests__/AgentModelPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,67 @@ describe("AgentModelPicker", () => {
).toHaveTextContent("GPT-4o");
});

// Harness agents (pi-acp, codex-acp, etc.) get a wider picker in the
// new-chat composer because their ACP model names can be long; Goose keeps
// the compact layout.
it("widens the new-chat picker for a harness agent without reasoning effort", async () => {
const user = userEvent.setup();
render(
<AgentModelPicker
agents={AGENTS}
selectedAgentId="claude-acp"
onAgentChange={vi.fn()}
currentModelId="claude-sonnet-4"
currentModelName="Claude Sonnet 4"
availableModels={[{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }]}
onModelChange={vi.fn()}
/>,
);

await user.click(
screen.getByRole("button", { name: /choose agent and model/i }),
);

expect(screen.getByRole("dialog")).toHaveClass("w-[35rem]");
});

// With a reasoning-effort column present the harness picker grows further,
// because the agent (11.75rem) and reasoning (11rem) columns alone consume
// most of the Goose expanded width and the model column needs room for
// long ACP names (e.g. "databricks / databricks-glm-5-3").
it("widens the expanded picker for a harness agent with reasoning effort", async () => {
const user = userEvent.setup();
render(
<AgentModelPicker
agents={AGENTS}
selectedAgentId="claude-acp"
onAgentChange={vi.fn()}
currentModelId="claude-sonnet-4"
currentModelName="Claude Sonnet 4"
availableModels={[{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }]}
onModelChange={vi.fn()}
reasoningEffort={{
config: {
configId: "thinking_effort",
currentValue: "medium",
options: [
{ id: "low", name: "low" },
{ id: "medium", name: "medium" },
{ id: "high", name: "high" },
],
},
onChange: vi.fn(),
}}
/>,
);

await user.click(
screen.getByRole("button", { name: /choose agent and model/i }),
);

expect(screen.getByRole("dialog")).toHaveClass("w-[46rem]");
});

it("routes not-ready Goose to Providers settings with a connect action", async () => {
const user = userEvent.setup();
const onAgentChange = vi.fn();
Expand Down Expand Up @@ -1418,6 +1479,35 @@ describe("AgentModelPicker", () => {
expect(content).toHaveClass("w-[37.25rem]");
});

// Without a reasoning-effort column the gated reveal widens to the
// harness width so long model names (e.g. pi-acp provider/id names) are
// not truncated. Goose is excluded, so use a harness agent here.
it("widens to the harness width when no reasoning-effort column is present", async () => {
const user = userEvent.setup();

render(
<AgentModelPicker
agents={AGENTS}
selectedAgentId="claude-acp"
onAgentChange={vi.fn()}
currentModelId="claude-sonnet-4"
currentModelName="Claude Sonnet 4"
availableModels={[{ id: "claude-sonnet-4", name: "Claude Sonnet 4" }]}
onModelChange={vi.fn()}
providerColumnMode="gated"
/>,
);

await openPicker(user);

const content = document.querySelector('[data-slot="popover-content"]');
expect(content).toHaveClass("w-[26.25rem]");

await user.click(screen.getByRole("button", { name: /switch agent/i }));

expect(content).toHaveClass("w-[35rem]");
});

it("hides the switch-agent button when the only agent is ready", async () => {
const user = userEvent.setup();
renderGated({ agents: [{ id: "goose", label: "Goose" }] });
Expand Down
29 changes: 13 additions & 16 deletions src/features/providers/curatedProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,19 @@ export const CURATED_PROVIDER_CATALOG: ProviderCatalogEntry[] = [
supportsAuth: true,
supportsAuthStatus: true,
},
// full pi support in a future update
// {
// id: "pi-acp",
// displayName: "Pi",
// category: "agent",
// description: "Pi ACP agent",
// setupMethod: "cli_auth",
// binaryName: "pi-acp",
// group: "default",
// aliases: ["pi-acp", "pi"],
// supportsInstall: false,
// supportsAuth: false,
// supportsAuthStatus: false,
// supportsModelList: false,
// modelSelectionHint: "Use the Pi CLI to configure the model.",
// },
{
id: "pi-acp",
displayName: "Pi",
category: "agent",
description: "Pi coding agent",
setupMethod: "cli_auth",
binaryName: "pi-acp",
group: "default",
aliases: ["pi-acp", "pi"],
supportsInstall: false,
supportsAuth: false,
supportsAuthStatus: false,
},
{
id: "databricks_v2",
displayName: "Databricks AI Gateway",
Expand Down
2 changes: 2 additions & 0 deletions src/features/providers/hooks/useProviderModels.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ describe("useProviderModels", () => {
"codex-acp",
"copilot-acp",
"cursor-agent",
"pi-acp",
]);

act(() => {
Expand All @@ -151,6 +152,7 @@ describe("useProviderModels", () => {
"codex-acp",
"copilot-acp",
"cursor-agent",
"pi-acp",
]);

act(() => {
Expand Down
1 change: 1 addition & 0 deletions src/features/providers/providerCatalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("provider catalog selectors", () => {
"copilot-acp",
"amp-acp",
"cursor-agent",
"pi-acp",
]);
expect(getModelProviders().map((provider) => provider.id)).toEqual([
"databricks_v2",
Expand Down
1 change: 1 addition & 0 deletions src/shared/api/__tests__/acp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,7 @@ describe("acpCreateSession", () => {
"copilot-acp",
"amp-acp",
"cursor-agent",
"pi-acp",
])("keeps the %s harness outside Goose provider policy", async (harnessId) => {
await setRuntimeConfig(managedRuntimeConfig);
mockNewSession.mockResolvedValue({ sessionId: `session-${harnessId}` });
Expand Down
23 changes: 23 additions & 0 deletions src/shared/ui/icons/ProviderIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,27 @@ export function CopilotIcon({ className = "" }: { className?: string }) {
);
}

export function PiIcon({ className = "" }: { className?: string }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 800 800"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden="true"
>
<title>Pi</title>
<path
fill="currentColor"
fillRule="evenodd"
d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"
/>
<path fill="currentColor" d="M517.36 400H634.72V634.72H517.36Z" />
</svg>
);
}

const PROVIDER_ICON_MAP: Record<string, (className: string) => ReactNode> = {
goose: (className) => (
<GooseIcon className={`${className} text-foreground`} />
Expand Down Expand Up @@ -451,6 +472,8 @@ const PROVIDER_ICON_MAP: Record<string, (className: string) => ReactNode> = {
snowflake: (className) => <SnowflakeIcon className={className} />,
xai: (className) => <XAIIcon className={className} />,
lmstudio: (className) => <OllamaIcon className={className} />,
"pi-acp": (className) => <PiIcon className={className} />,
pi: (className) => <PiIcon className={className} />,
};

function normalizeProviderId(providerId: string) {
Expand Down