Skip to content
Merged
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
226 changes: 119 additions & 107 deletions src/features/chat/ui/AgentModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function PickerItem({
onClick={onClick}
disabled={disabled}
className={cn(
"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors",
"flex min-w-0 w-full items-center gap-2 overflow-hidden rounded-sm px-2 py-1.5 text-left text-sm transition-colors",
"hover:bg-muted focus-visible:bg-muted focus:outline-none",
"disabled:pointer-events-none disabled:opacity-50",
selected && "bg-muted/60",
Expand Down Expand Up @@ -201,7 +201,7 @@ export function AgentModelPicker({
</PopoverTrigger>
<PopoverContent
align="start"
className="w-96 max-h-[min(24rem,50vh)] p-1"
className="h-[min(24rem,50vh)] w-96 overflow-hidden p-1"
onKeyDown={(e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
Expand Down Expand Up @@ -251,121 +251,133 @@ export function AgentModelPicker({
}
}}
>
<div className="grid grid-cols-2 gap-1 max-h-[inherit]">
<div data-col="agent" className="flex min-h-0 flex-col p-1">
<div className="shrink-0 px-2 py-1.5 text-sm font-semibold">
{t("toolbar.agent")}
</div>
<ScrollArea className="min-h-0 flex-1">
<div className="p-1 space-y-0.5">
{agents.map((agent) => {
const isSelected = agent.id === selectedAgentId;

return (
<PickerItem
key={agent.id}
onClick={() => handleAgentSelect(agent.id)}
selected={isSelected}
>
<span className="shrink-0">
{getProviderIcon(agent.id, "size-4")}
</span>
<span className="min-w-0 flex-1 truncate">
{agent.label}
</span>
{isSelected ? (
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
) : null}
</PickerItem>
);
})}
<div className="grid h-full grid-cols-[minmax(0,1fr)_minmax(0,1fr)] gap-1 overflow-hidden">
<div
data-col="agent"
className="flex min-h-0 min-w-0 overflow-hidden p-1"
>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="shrink-0 px-2 py-1.5 text-sm font-semibold">
{t("toolbar.agent")}
</div>
</ScrollArea>
</div>
<div data-col="model" className="flex min-h-0 flex-col p-1">
<div className="shrink-0 px-2 py-1.5 text-sm font-semibold">
{t("toolbar.model")}
</div>
{groupedModels.length > 0 ? (
<ScrollArea className="min-h-0 flex-1">
<ScrollArea className="min-h-0 min-w-0 flex-1">
<div className="p-1 space-y-0.5">
{groupedModels.map((group) => {
const expanded = isGroupExpanded(group);
{agents.map((agent) => {
const isSelected = agent.id === selectedAgentId;

return (
<div key={group.provider}>
<button
type="button"
onClick={() => toggleGroup(group.provider)}
className={cn(
"flex w-full items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm font-medium transition-colors",
"hover:bg-muted focus:bg-muted focus:outline-none",
)}
>
<IconChevronRight
className={cn(
"size-3.5 shrink-0 text-muted-foreground transition-transform",
expanded && "rotate-90",
)}
/>
<span className="flex-1">{group.provider}</span>
<span className="text-xs text-muted-foreground">
{group.models.length}
</span>
</button>
{expanded ? (
<div className="pb-1">
{group.models.map((model) => {
const modelName = getModelDisplayName(model);

return (
<PickerItem
key={model.id}
onClick={() => handleModelSelect(model.id)}
selected={model.id === currentModelId}
className="justify-between pl-6"
>
<div className="min-w-0 flex-1 truncate">
{modelName}
</div>
{model.id === currentModelId ? (
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
) : null}
</PickerItem>
);
})}
</div>
) : group.hasSelectedModel ? (
<div className="pb-1">
{group.models
.filter((m) => m.id === currentModelId)
.map((model) => (
<PickerItem
key={model.id}
onClick={() => handleModelSelect(model.id)}
selected
className="justify-between pl-6"
>
<div className="min-w-0 flex-1 truncate">
{getModelDisplayName(model)}
</div>
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
</PickerItem>
))}
</div>
<PickerItem
key={agent.id}
onClick={() => handleAgentSelect(agent.id)}
selected={isSelected}
>
<span className="shrink-0">
{getProviderIcon(agent.id, "size-4")}
</span>
<span className="min-w-0 flex-1 truncate">
{agent.label}
</span>
{isSelected ? (
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
) : null}
</div>
</PickerItem>
);
})}
</div>
</ScrollArea>
) : (
<div className="px-2 py-2">
<div className="text-sm text-muted-foreground">
{currentModelName ? currentModelName : t("toolbar.loading")}
</div>
</div>
</div>
<div
data-col="model"
className="flex min-h-0 min-w-0 overflow-hidden p-1"
>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="shrink-0 px-2 py-1.5 text-sm font-semibold">
{t("toolbar.model")}
</div>
)}
{groupedModels.length > 0 ? (
<ScrollArea className="min-h-0 min-w-0 flex-1">
<div className="p-1 space-y-0.5">
{groupedModels.map((group) => {
const expanded = isGroupExpanded(group);

return (
<div key={group.provider}>
<button
type="button"
onClick={() => toggleGroup(group.provider)}
className={cn(
"flex min-w-0 w-full items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm font-medium transition-colors",
"hover:bg-muted focus:bg-muted focus:outline-none",
)}
>
<IconChevronRight
className={cn(
"size-3.5 shrink-0 text-muted-foreground transition-transform",
expanded && "rotate-90",
)}
/>
<span className="min-w-0 flex-1 truncate">
{group.provider}
</span>
<span className="text-xs text-muted-foreground">
{group.models.length}
</span>
</button>
{expanded ? (
<div className="overflow-hidden pb-1">
{group.models.map((model) => {
const modelName = getModelDisplayName(model);

return (
<PickerItem
key={model.id}
onClick={() => handleModelSelect(model.id)}
selected={model.id === currentModelId}
className="justify-between pl-6"
>
<div className="min-w-0 flex-1 truncate">
{modelName}
</div>
{model.id === currentModelId ? (
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
) : null}
</PickerItem>
);
})}
</div>
) : group.hasSelectedModel ? (
<div className="overflow-hidden pb-1">
{group.models
.filter((m) => m.id === currentModelId)
.map((model) => (
<PickerItem
key={model.id}
onClick={() => handleModelSelect(model.id)}
selected
className="justify-between pl-6"
>
<div className="min-w-0 flex-1 truncate">
{getModelDisplayName(model)}
</div>
<IconCheck className="size-4 shrink-0 text-muted-foreground" />
</PickerItem>
))}
</div>
) : null}
</div>
);
})}
</div>
</ScrollArea>
) : (
<div className="px-2 py-2">
<div className="text-sm text-muted-foreground">
{currentModelName ? currentModelName : t("toolbar.loading")}
</div>
</div>
)}
</div>
</div>
</div>
</PopoverContent>
Expand Down
38 changes: 38 additions & 0 deletions src/features/chat/ui/__tests__/AgentModelPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,44 @@ describe("AgentModelPicker", () => {
).toBeInTheDocument();
});

it("keeps long model names in constrained rows", async () => {
const user = userEvent.setup();

render(
<AgentModelPicker
agents={AGENTS}
selectedAgentId="goose"
onAgentChange={vi.fn()}
currentModelId="databricks-gpt-5-4-mini"
currentModelName="databricks-gpt-5-4-mini"
availableModels={[
{
id: "databricks-gpt-5-4-mini",
name: "databricks-gpt-5-4-mini",
provider: "OpenAI",
},
{
id: "databricks-gpt-5-4-nano-preview-super-long",
name: "databricks-gpt-5-4-nano-preview-super-long",
provider: "OpenAI",
},
]}
onModelChange={vi.fn()}
/>,
);

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

const longModelButton = screen.getByRole("button", {
name: "databricks-gpt-5-4-mini",
});

expect(longModelButton).toHaveClass("min-w-0");
expect(longModelButton).toHaveClass("overflow-hidden");
});

it("shows only agent name when no model info is available", () => {
render(
<AgentModelPicker
Expand Down
6 changes: 3 additions & 3 deletions src/features/sidebar/ui/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export function Sidebar({
)}
style={{ width: collapsed ? 54 : width }}
>
<div className="flex flex-col h-full overflow-hidden bg-background border border-border rounded-xl">
<div className="flex h-full flex-col overflow-hidden rounded-xl border border-border bg-background [--muted-foreground:var(--text-subtle)]">
<div
className={cn(
"flex-shrink-0 pt-3",
Expand Down Expand Up @@ -449,7 +449,7 @@ export function Sidebar({
"flex items-center w-full text-[13px] transition-colors duration-200 rounded-md",
"gap-2.5 p-3",
activeView === "home"
? "text-foreground"
? "font-medium text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
Expand Down Expand Up @@ -508,7 +508,7 @@ export function Sidebar({
"flex items-center w-full text-[13px] transition-colors duration-200 rounded-md",
"gap-2.5 p-3",
isActive
? "text-foreground"
? "font-medium text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
aria-current={isActive ? "page" : undefined}
Expand Down
2 changes: 1 addition & 1 deletion src/features/sidebar/ui/SidebarChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { SessionActivityIndicator } from "@/shared/ui/SessionActivityIndicator";
const INACTIVE_CHAT_ROW_CLASS =
"text-muted-foreground hover:bg-transparent hover:text-foreground group-hover:text-foreground";
const ACTIVE_CHAT_ROW_CLASS =
"text-foreground hover:bg-transparent hover:text-foreground";
"font-medium text-foreground hover:bg-transparent hover:text-foreground";

interface SidebarChatRowProps {
id: string;
Expand Down
2 changes: 1 addition & 1 deletion src/features/sidebar/ui/SidebarProjectsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function ProjectSection({
className={cn(
"flex-1 min-w-0 justify-start gap-2 rounded-md px-3 py-2 text-[13px] font-light",
activeProjectId === project.id
? "text-foreground hover:bg-transparent hover:text-foreground group-hover:text-foreground"
? "font-medium text-foreground hover:bg-transparent hover:text-foreground group-hover:text-foreground"
: PROJECT_ROW_TEXT_CLASS,
)}
>
Expand Down
2 changes: 1 addition & 1 deletion src/shared/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function ScrollArea({
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
Expand Down
Loading