Skip to content

Commit 1ec3cc6

Browse files
committed
🤖 fix: stabilize agent fallback accents and cache hydration
1 parent 8f12201 commit 1ec3cc6

6 files changed

Lines changed: 715 additions & 35 deletions

File tree

src/browser/components/AgentModePicker.test.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,4 +283,87 @@ describe("AgentModePicker", () => {
283283
expect(getByTestId("agentId").textContent).toBe("exec");
284284
});
285285
});
286+
287+
test("keeps trigger border and icon colors in sync", () => {
288+
const customColor = "rgb(12, 34, 56)";
289+
290+
function Harness() {
291+
const [agentId, setAgentId] = React.useState("exec");
292+
return (
293+
<AgentProvider
294+
value={{
295+
agentId,
296+
setAgentId,
297+
agents: [
298+
{
299+
...BUILT_INS[0],
300+
uiColor: customColor,
301+
},
302+
],
303+
loaded: true,
304+
loadFailed: false,
305+
refresh: () => Promise.resolve(),
306+
refreshing: false,
307+
...defaultContextProps,
308+
}}
309+
>
310+
<TooltipProvider>
311+
<AgentModePicker />
312+
</TooltipProvider>
313+
</AgentProvider>
314+
);
315+
}
316+
317+
const { getByLabelText } = render(<Harness />);
318+
const triggerButton = getByLabelText("Select agent");
319+
const triggerIcon = triggerButton.querySelector("svg");
320+
321+
expect(triggerIcon).toBeTruthy();
322+
expect(triggerButton.style.borderColor).toBe(customColor);
323+
expect(triggerIcon?.style.color).toBe(customColor);
324+
});
325+
326+
test("uses built-in accent colors before agent metadata loads", () => {
327+
const expectedAccents: ReadonlyArray<[string, string]> = [
328+
["ask", "var(--color-ask-mode)"],
329+
["plan", "var(--color-plan-mode)"],
330+
["exec", "var(--color-exec-mode)"],
331+
["orchestrator", "var(--color-exec-mode)"],
332+
["auto", "var(--color-auto-mode)"],
333+
];
334+
335+
for (const [agentId, expectedAccent] of expectedAccents) {
336+
function Harness() {
337+
const [currentAgentId, setAgentId] = React.useState(agentId);
338+
return (
339+
<AgentProvider
340+
value={{
341+
agentId: currentAgentId,
342+
setAgentId,
343+
agents: [],
344+
loaded: false,
345+
loadFailed: false,
346+
refresh: () => Promise.resolve(),
347+
refreshing: false,
348+
...defaultContextProps,
349+
}}
350+
>
351+
<TooltipProvider>
352+
<AgentModePicker />
353+
</TooltipProvider>
354+
</AgentProvider>
355+
);
356+
}
357+
358+
const { getByLabelText, unmount } = render(<Harness />);
359+
const triggerButton = getByLabelText("Select agent");
360+
const triggerIcon = triggerButton.querySelector("svg");
361+
362+
expect(triggerIcon).toBeTruthy();
363+
expect(triggerButton.style.borderColor).toBe(expectedAccent);
364+
expect(triggerIcon?.style.color).toBe(expectedAccent);
365+
366+
unmount();
367+
}
368+
});
286369
});

src/browser/components/AgentModePicker.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
KEYBINDS,
3030
matchNumberedKeybind,
3131
} from "@/browser/utils/ui/keybinds";
32-
import { sortAgentsStable } from "@/browser/utils/agents";
32+
import { resolveAgentAccentColor, sortAgentsStable } from "@/browser/utils/agents";
3333
import { stopKeyboardPropagation } from "@/browser/utils/events";
3434

3535
interface AgentModePickerProps {
@@ -56,7 +56,7 @@ interface AgentOption {
5656
subagentRunnable: boolean;
5757
}
5858

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

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

349349
return (
350350
<div ref={containerRef} className={cn("relative flex items-center gap-1.5", props.className)}>
@@ -366,13 +366,12 @@ export const AgentModePicker: React.FC<AgentModePickerProps> = (props) => {
366366
}}
367367
style={activeStyle}
368368
className={cn(
369-
"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",
370-
activeClassName
369+
"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"
371370
)}
372371
>
373372
<TriggerIcon
374-
className="h-3 w-3 shrink-0"
375-
style={activeOption?.uiColor ? { color: activeOption.uiColor } : undefined}
373+
className="h-2 w-2 shrink-0 transition-colors duration-150"
374+
style={{ color: activeAccentColor }}
376375
/>
377376
<span className="max-w-[clamp(4.5rem,30vw,130px)] truncate">{activeDisplayName}</span>
378377
<ChevronDown

src/browser/components/ChatInput/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import {
8080
isEditableElement,
8181
} from "@/browser/utils/ui/keybinds";
8282
import { stopKeyboardPropagation } from "@/browser/utils/events";
83+
import { resolveAgentAccentColor } from "@/browser/utils/agents";
8384
import { ModelSelector, type ModelSelectorRef } from "../ModelSelector";
8485
import { useModelsFromSettings } from "@/browser/hooks/useModelsFromSettings";
8586
import { SendHorizontal } from "lucide-react";
@@ -456,8 +457,9 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
456457
const autoAvailable = agents.some((entry) => entry.uiSelectable && entry.id === "auto");
457458
const isAutoAgent = normalizedAgentId === "auto" && autoAvailable;
458459

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

0 commit comments

Comments
 (0)