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
83 changes: 83 additions & 0 deletions src/browser/components/AgentModePicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,87 @@ describe("AgentModePicker", () => {
expect(getByTestId("agentId").textContent).toBe("exec");
});
});

test("keeps trigger border and icon colors in sync", () => {
const customColor = "rgb(12, 34, 56)";

function Harness() {
const [agentId, setAgentId] = React.useState("exec");
return (
<AgentProvider
value={{
agentId,
setAgentId,
agents: [
{
...BUILT_INS[0],
uiColor: customColor,
},
],
loaded: true,
loadFailed: false,
refresh: () => Promise.resolve(),
refreshing: false,
...defaultContextProps,
}}
>
<TooltipProvider>
<AgentModePicker />
</TooltipProvider>
</AgentProvider>
);
}

const { getByLabelText } = render(<Harness />);
const triggerButton = getByLabelText("Select agent");
const triggerIcon = triggerButton.querySelector("svg");

expect(triggerIcon).toBeTruthy();
expect(triggerButton.style.borderColor).toBe(customColor);
expect(triggerIcon?.style.color).toBe(customColor);
});

test("uses built-in accent colors before agent metadata loads", () => {
const expectedAccents: ReadonlyArray<[string, string]> = [
["ask", "var(--color-ask-mode)"],
["plan", "var(--color-plan-mode)"],
["exec", "var(--color-exec-mode)"],
["orchestrator", "var(--color-exec-mode)"],
["auto", "var(--color-auto-mode)"],
];

for (const [agentId, expectedAccent] of expectedAccents) {
function Harness() {
const [currentAgentId, setAgentId] = React.useState(agentId);
return (
<AgentProvider
value={{
agentId: currentAgentId,
setAgentId,
agents: [],
loaded: false,
loadFailed: false,
refresh: () => Promise.resolve(),
refreshing: false,
...defaultContextProps,
}}
>
<TooltipProvider>
<AgentModePicker />
</TooltipProvider>
</AgentProvider>
);
}

const { getByLabelText, unmount } = render(<Harness />);
const triggerButton = getByLabelText("Select agent");
const triggerIcon = triggerButton.querySelector("svg");

expect(triggerIcon).toBeTruthy();
expect(triggerButton.style.borderColor).toBe(expectedAccent);
expect(triggerIcon?.style.color).toBe(expectedAccent);

unmount();
}
});
});
23 changes: 11 additions & 12 deletions src/browser/components/AgentModePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
KEYBINDS,
matchNumberedKeybind,
} from "@/browser/utils/ui/keybinds";
import { sortAgentsStable } from "@/browser/utils/agents";
import { resolveAgentAccentColor, sortAgentsStable } from "@/browser/utils/agents";
import { stopKeyboardPropagation } from "@/browser/utils/events";

interface AgentModePickerProps {
Expand All @@ -56,7 +56,7 @@ interface AgentOption {
subagentRunnable: boolean;
}

/** Maps well-known agent IDs to lucide icons for the dropdown */
/** Maps well-known agent IDs to lucide icons for the expanded dropdown list. */
const AGENT_ICONS: Record<string, LucideIcon> = {
ask: MessageCircleQuestionMark,
plan: Route,
Expand Down Expand Up @@ -338,13 +338,13 @@ export const AgentModePicker: React.FC<AgentModePickerProps> = (props) => {
}
};

// Resolve display properties for the trigger pill
const activeDisplayName = activeOption?.name ?? formatAgentIdLabel(normalizedAgentId);
const activeStyle: React.CSSProperties | undefined = activeOption?.uiColor
? { borderColor: activeOption.uiColor }
: undefined;
const activeClassName = activeOption?.uiColor ? "" : "border-exec-mode";
// Resolve display properties for the trigger pill.
const TriggerIcon = getAgentIcon(normalizedAgentId);
const activeDisplayName = activeOption?.name ?? formatAgentIdLabel(normalizedAgentId);
// Keep icon + border colors on the same source value so they can't desync while
// agent metadata is loading.
const activeAccentColor = resolveAgentAccentColor(normalizedAgentId, activeOption?.uiColor);
const activeStyle: React.CSSProperties = { borderColor: activeAccentColor };

return (
<div ref={containerRef} className={cn("relative flex items-center gap-1.5", props.className)}>
Expand All @@ -366,13 +366,12 @@ export const AgentModePicker: React.FC<AgentModePickerProps> = (props) => {
}}
style={activeStyle}
className={cn(
"text-foreground hover:bg-hover flex items-center gap-1.5 rounded-sm border-[0.5px] px-1.5 py-0.5 text-[11px] font-medium transition-[background-color] duration-150",
activeClassName
"text-foreground hover:bg-hover flex items-center gap-1.5 rounded-sm border-[0.5px] px-1.5 py-0.5 text-[11px] font-medium transition-colors duration-150"
)}
>
<TriggerIcon
className="h-3 w-3 shrink-0"
style={activeOption?.uiColor ? { color: activeOption.uiColor } : undefined}
className="h-2 w-2 shrink-0 transition-colors duration-150"
style={{ color: activeAccentColor }}
/>
<span className="max-w-[clamp(4.5rem,30vw,130px)] truncate">{activeDisplayName}</span>
<ChevronDown
Expand Down
6 changes: 4 additions & 2 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import {
isEditableElement,
} from "@/browser/utils/ui/keybinds";
import { stopKeyboardPropagation } from "@/browser/utils/events";
import { resolveAgentAccentColor } from "@/browser/utils/agents";
import { ModelSelector, type ModelSelectorRef } from "../ModelSelector";
import { useModelsFromSettings } from "@/browser/hooks/useModelsFromSettings";
import { SendHorizontal } from "lucide-react";
Expand Down Expand Up @@ -456,8 +457,9 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
const autoAvailable = agents.some((entry) => entry.uiSelectable && entry.id === "auto");
const isAutoAgent = normalizedAgentId === "auto" && autoAvailable;

// Use current agent's uiColor, or neutral border until agents load
const focusBorderColor = currentAgent?.uiColor ?? "var(--color-border-light)";
// Resolve border accent from discovered metadata, with built-in fallback while
// agent descriptors are still loading during workspace switches.
const focusBorderColor = resolveAgentAccentColor(agentId, currentAgent?.uiColor);
const {
models,
hiddenModelsForSelector,
Expand Down
Loading
Loading