diff --git a/src/features/chat/ui/AgentModelPicker.tsx b/src/features/chat/ui/AgentModelPicker.tsx index 2adfbb91d..e76afafd1 100644 --- a/src/features/chat/ui/AgentModelPicker.tsx +++ b/src/features/chat/ui/AgentModelPicker.tsx @@ -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 { @@ -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. @@ -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); @@ -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 ? ( diff --git a/src/features/chat/ui/AgentModelPickerLists.tsx b/src/features/chat/ui/AgentModelPickerLists.tsx index 7ad2902bd..22a492462 100644 --- a/src/features/chat/ui/AgentModelPickerLists.tsx +++ b/src/features/chat/ui/AgentModelPickerLists.tsx @@ -399,7 +399,10 @@ export const RecommendedModelList = forwardRef< {providerIcon} ) : null} -
+
{getModelDisplayName(model)}
diff --git a/src/features/chat/ui/__tests__/AgentModelPicker.test.tsx b/src/features/chat/ui/__tests__/AgentModelPicker.test.tsx index 056f71e6f..5c9c980aa 100644 --- a/src/features/chat/ui/__tests__/AgentModelPicker.test.tsx +++ b/src/features/chat/ui/__tests__/AgentModelPicker.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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(); @@ -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( + , + ); + + 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" }] }); diff --git a/src/features/providers/curatedProviders.ts b/src/features/providers/curatedProviders.ts index 7b4806910..edc480f39 100644 --- a/src/features/providers/curatedProviders.ts +++ b/src/features/providers/curatedProviders.ts @@ -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", diff --git a/src/features/providers/hooks/useProviderModels.test.tsx b/src/features/providers/hooks/useProviderModels.test.tsx index f0a1da316..06ead6080 100644 --- a/src/features/providers/hooks/useProviderModels.test.tsx +++ b/src/features/providers/hooks/useProviderModels.test.tsx @@ -127,6 +127,7 @@ describe("useProviderModels", () => { "codex-acp", "copilot-acp", "cursor-agent", + "pi-acp", ]); act(() => { @@ -151,6 +152,7 @@ describe("useProviderModels", () => { "codex-acp", "copilot-acp", "cursor-agent", + "pi-acp", ]); act(() => { diff --git a/src/features/providers/providerCatalog.test.ts b/src/features/providers/providerCatalog.test.ts index 1214f3f7d..65426d45e 100644 --- a/src/features/providers/providerCatalog.test.ts +++ b/src/features/providers/providerCatalog.test.ts @@ -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", diff --git a/src/shared/api/__tests__/acp.test.ts b/src/shared/api/__tests__/acp.test.ts index 8bed6a1d4..9e36ddf81 100644 --- a/src/shared/api/__tests__/acp.test.ts +++ b/src/shared/api/__tests__/acp.test.ts @@ -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}` }); diff --git a/src/shared/ui/icons/ProviderIcons.tsx b/src/shared/ui/icons/ProviderIcons.tsx index c3e70aaba..dfce90d13 100644 --- a/src/shared/ui/icons/ProviderIcons.tsx +++ b/src/shared/ui/icons/ProviderIcons.tsx @@ -420,6 +420,27 @@ export function CopilotIcon({ className = "" }: { className?: string }) { ); } +export function PiIcon({ className = "" }: { className?: string }) { + return ( + + ); +} + const PROVIDER_ICON_MAP: Record ReactNode> = { goose: (className) => ( @@ -451,6 +472,8 @@ const PROVIDER_ICON_MAP: Record ReactNode> = { snowflake: (className) => , xai: (className) => , lmstudio: (className) => , + "pi-acp": (className) => , + pi: (className) => , }; function normalizeProviderId(providerId: string) {