@@ -366,13 +366,12 @@ export const AgentModePicker: React.FC
= (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"
)}
>
{activeDisplayName}
= (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,
diff --git a/src/browser/contexts/AgentContext.test.tsx b/src/browser/contexts/AgentContext.test.tsx
index f1e771abc9..791208282f 100644
--- a/src/browser/contexts/AgentContext.test.tsx
+++ b/src/browser/contexts/AgentContext.test.tsx
@@ -3,11 +3,55 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { cleanup, render, waitFor } from "@testing-library/react";
import { GlobalWindow } from "happy-dom";
import { GLOBAL_SCOPE_ID, getAgentIdKey, getProjectScopeId } from "@/common/constants/storage";
+import type { AgentDefinitionDescriptor } from "@/common/types/agentDefinition";
+
+interface AgentsListInput {
+ projectPath?: string;
+ workspaceId?: string;
+ disableWorkspaceAgents?: boolean;
+}
+
+interface Deferred {
+ promise: Promise;
+ resolve: (value: T) => void;
+ reject: (error: unknown) => void;
+}
+
+function createDeferred(): Deferred {
+ let resolve: ((value: T) => void) | undefined;
+ let reject: ((error: unknown) => void) | undefined;
+
+ const promise = new Promise((resolvePromise, rejectPromise) => {
+ resolve = resolvePromise;
+ reject = rejectPromise;
+ });
+
+ if (!resolve || !reject) {
+ throw new Error("failed to create deferred promise");
+ }
+
+ return { promise, resolve, reject };
+}
+
+function createAgent(id: string, name: string): AgentDefinitionDescriptor {
+ return {
+ id,
+ scope: "built-in",
+ name,
+ uiSelectable: true,
+ subagentRunnable: false,
+ };
+}
+
+const listAgentsMock = mock((_input: AgentsListInput) =>
+ Promise.resolve([])
+);
+let currentApiMock: { agents: { list: typeof listAgentsMock } } | null = null;
void mock.module("@/browser/contexts/API", () => ({
useAPI: () => ({
- api: null,
- status: "connecting" as const,
+ api: currentApiMock,
+ status: currentApiMock ? ("connected" as const) : ("connecting" as const),
error: null,
authenticate: () => undefined,
retry: () => undefined,
@@ -44,6 +88,12 @@ describe("AgentContext", () => {
globalThis.window = dom as unknown as Window & typeof globalThis;
globalThis.document = dom.document as unknown as Document;
globalThis.localStorage = dom.localStorage as unknown as Storage;
+
+ listAgentsMock.mockReset();
+ listAgentsMock.mockImplementation((_input: AgentsListInput) =>
+ Promise.resolve([])
+ );
+ currentApiMock = null;
});
afterEach(() => {
@@ -51,6 +101,7 @@ describe("AgentContext", () => {
globalThis.window = originalWindow;
globalThis.document = originalDocument;
globalThis.localStorage = originalLocalStorage;
+ currentApiMock = null;
});
test("project-scoped agent falls back to global default when project preference is unset", async () => {
@@ -90,4 +141,447 @@ describe("AgentContext", () => {
expect(contextValue?.agentId).toBe("plan");
});
});
+
+ test("uses workspace cache when revisiting a workspace while refetch is pending", async () => {
+ const projectPath = "/tmp/project-cache";
+ const latestByWorkspace = new Map([
+ ["ws-a", [createAgent("exec", "Exec")]],
+ ["ws-b", [createAgent("plan", "Plan")]],
+ ]);
+ const pendingByWorkspace = new Map>();
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ const workspaceId = input.workspaceId;
+ if (!workspaceId) {
+ return Promise.resolve([]);
+ }
+
+ const pending = pendingByWorkspace.get(workspaceId);
+ if (pending) {
+ return pending.promise;
+ }
+
+ return Promise.resolve(latestByWorkspace.get(workspaceId) ?? []);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ let contextValue: AgentContextValue | undefined;
+ const { rerender } = render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ });
+
+ rerender(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["plan"]);
+ });
+
+ latestByWorkspace.set("ws-a", [createAgent("ask", "Ask")]);
+ const wsADeferred = createDeferred();
+ pendingByWorkspace.set("ws-a", wsADeferred);
+
+ rerender(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(listAgentsMock.mock.calls.length).toBeGreaterThanOrEqual(3);
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ });
+
+ pendingByWorkspace.delete("ws-a");
+ wsADeferred.resolve(latestByWorkspace.get("ws-a") ?? []);
+
+ await waitFor(() => {
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["ask"]);
+ });
+ });
+
+ test("uses project cache for first render in a sibling workspace", async () => {
+ const projectPath = "/tmp/project-fallback";
+ const latestByWorkspace = new Map([
+ ["ws-a", [createAgent("exec", "Exec")]],
+ ["ws-b", [createAgent("plan", "Plan")]],
+ ]);
+ const pendingByWorkspace = new Map>();
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ const workspaceId = input.workspaceId;
+ if (!workspaceId) {
+ return Promise.resolve([]);
+ }
+
+ const pending = pendingByWorkspace.get(workspaceId);
+ if (pending) {
+ return pending.promise;
+ }
+
+ return Promise.resolve(latestByWorkspace.get(workspaceId) ?? []);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ let contextValue: AgentContextValue | undefined;
+ const { rerender } = render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ });
+
+ const wsBDeferred = createDeferred();
+ pendingByWorkspace.set("ws-b", wsBDeferred);
+
+ rerender(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(
+ listAgentsMock.mock.calls.some(
+ ([input]: [AgentsListInput, ...unknown[]]) => input.workspaceId === "ws-b"
+ )
+ ).toBe(true);
+ expect(contextValue?.loaded).toBe(false);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ expect(contextValue?.agents.every((agent) => !agent.uiSelectable)).toBe(true);
+ });
+
+ pendingByWorkspace.delete("ws-b");
+ wsBDeferred.resolve(latestByWorkspace.get("ws-b") ?? []);
+
+ await waitFor(() => {
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["plan"]);
+ expect(contextValue?.agents.every((agent) => agent.uiSelectable)).toBe(true);
+ });
+ });
+
+ test("does not hydrate project scope from workspace-sourced project cache", async () => {
+ const projectPath = "/tmp/project-scope-cache";
+ const projectScopeKey = "__project__";
+ const pendingByScope = new Map>();
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ const scopeKey = input.workspaceId ?? projectScopeKey;
+ const pending = pendingByScope.get(scopeKey);
+ if (pending) {
+ return pending.promise;
+ }
+
+ if (scopeKey === "ws-a") {
+ return Promise.resolve([createAgent("exec", "Exec")]);
+ }
+
+ return Promise.resolve([createAgent("ask", "Ask")]);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ let contextValue: AgentContextValue | undefined;
+ const { rerender } = render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ });
+
+ const projectDeferred = createDeferred();
+ pendingByScope.set(projectScopeKey, projectDeferred);
+
+ rerender(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(
+ listAgentsMock.mock.calls.some(
+ ([input]: [AgentsListInput, ...unknown[]]) =>
+ input.projectPath === projectPath && input.workspaceId === undefined
+ )
+ ).toBe(true);
+ expect(contextValue?.loaded).toBe(false);
+ expect(contextValue?.agents.length).toBe(0);
+ });
+
+ pendingByScope.delete(projectScopeKey);
+ projectDeferred.resolve([createAgent("ask", "Ask")]);
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["ask"]);
+ });
+ });
+
+ test("keeps project-sourced fallback selectable in workspace scope", async () => {
+ const projectPath = "/tmp/project-fallback-selectable";
+ const projectScopeKey = "__project__";
+ const pendingByScope = new Map>();
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ const scopeKey = input.workspaceId ?? projectScopeKey;
+ const pending = pendingByScope.get(scopeKey);
+ if (pending) {
+ return pending.promise;
+ }
+
+ if (scopeKey === projectScopeKey) {
+ return Promise.resolve([createAgent("exec", "Exec")]);
+ }
+
+ return Promise.resolve([createAgent("plan", "Plan")]);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ let contextValue: AgentContextValue | undefined;
+ const { rerender } = render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ expect(contextValue?.agents.every((agent) => agent.uiSelectable)).toBe(true);
+ });
+
+ const workspaceDeferred = createDeferred();
+ pendingByScope.set("ws-a", workspaceDeferred);
+
+ rerender(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(
+ listAgentsMock.mock.calls.some(
+ ([input]: [AgentsListInput, ...unknown[]]) =>
+ input.projectPath === projectPath && input.workspaceId === "ws-a"
+ )
+ ).toBe(true);
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ expect(contextValue?.agents.every((agent) => agent.uiSelectable)).toBe(true);
+ });
+
+ pendingByScope.delete("ws-a");
+ workspaceDeferred.resolve([createAgent("plan", "Plan")]);
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["plan"]);
+ expect(contextValue?.agents.every((agent) => agent.uiSelectable)).toBe(true);
+ });
+ });
+
+ test("does not reuse workspace cache across projects sharing a workspace id", async () => {
+ const workspaceId = "ws-shared";
+ const projectOne = "/tmp/project-one";
+ const projectTwo = "/tmp/project-two";
+ const latestByScope = new Map([
+ [`${projectOne}|${workspaceId}`, [createAgent("exec", "Exec")]],
+ [`${projectTwo}|${workspaceId}`, [createAgent("plan", "Plan")]],
+ ]);
+ const pendingByScope = new Map>();
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ if (!input.workspaceId) {
+ return Promise.resolve([]);
+ }
+
+ const cacheScopeKey = `${input.projectPath ?? ""}|${input.workspaceId}`;
+ const pending = pendingByScope.get(cacheScopeKey);
+ if (pending) {
+ return pending.promise;
+ }
+
+ return Promise.resolve(latestByScope.get(cacheScopeKey) ?? []);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ let contextValue: AgentContextValue | undefined;
+ const { rerender } = render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["exec"]);
+ });
+
+ const projectTwoScopeKey = `${projectTwo}|${workspaceId}`;
+ const projectTwoDeferred = createDeferred();
+ pendingByScope.set(projectTwoScopeKey, projectTwoDeferred);
+
+ rerender(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(
+ listAgentsMock.mock.calls.some(
+ ([input]: [AgentsListInput, ...unknown[]]) =>
+ input.projectPath === projectTwo && input.workspaceId === workspaceId
+ )
+ ).toBe(true);
+ expect(contextValue?.loaded).toBe(false);
+ expect(contextValue?.agents.length).toBe(0);
+ });
+
+ pendingByScope.delete(projectTwoScopeKey);
+ projectTwoDeferred.resolve(latestByScope.get(projectTwoScopeKey) ?? []);
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agents.map((agent) => agent.id)).toEqual(["plan"]);
+ });
+ });
+
+ test("cycle-agent keybind skips auto and rotates only manual agents", async () => {
+ const projectPath = "/tmp/project-cycle";
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ if (input.workspaceId !== "ws-cycle") {
+ return Promise.resolve([]);
+ }
+
+ return Promise.resolve([
+ createAgent("exec", "Exec"),
+ createAgent("plan", "Plan"),
+ createAgent("auto", "Auto"),
+ ]);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ window.localStorage.setItem(getAgentIdKey("ws-cycle"), JSON.stringify("exec"));
+
+ let contextValue: AgentContextValue | undefined;
+
+ render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agentId).toBe("exec");
+ });
+
+ window.dispatchEvent(new window.KeyboardEvent("keydown", { key: ".", ctrlKey: true }));
+
+ await waitFor(() => {
+ expect(contextValue?.agentId).toBe("plan");
+ });
+
+ window.dispatchEvent(new window.KeyboardEvent("keydown", { key: ".", ctrlKey: true }));
+
+ await waitFor(() => {
+ expect(contextValue?.agentId).toBe("exec");
+ expect(contextValue?.agentId).not.toBe("auto");
+ });
+ });
+
+ test("cycle-agent keybind exits auto when only one manual agent is available", async () => {
+ const projectPath = "/tmp/project-cycle-single";
+
+ listAgentsMock.mockImplementation((input: AgentsListInput) => {
+ if (input.workspaceId !== "ws-cycle-single") {
+ return Promise.resolve([]);
+ }
+
+ return Promise.resolve([
+ createAgent("exec", "Exec"),
+ createAgent("auto", "Auto"),
+ ]);
+ });
+
+ currentApiMock = {
+ agents: {
+ list: listAgentsMock,
+ },
+ };
+
+ window.localStorage.setItem(getAgentIdKey("ws-cycle-single"), JSON.stringify("auto"));
+
+ let contextValue: AgentContextValue | undefined;
+
+ render(
+
+ (contextValue = value)} />
+
+ );
+
+ await waitFor(() => {
+ expect(contextValue?.loaded).toBe(true);
+ expect(contextValue?.agentId).toBe("auto");
+ });
+
+ window.dispatchEvent(new window.KeyboardEvent("keydown", { key: ".", ctrlKey: true }));
+
+ await waitFor(() => {
+ expect(contextValue?.agentId).toBe("exec");
+ });
+ });
});
diff --git a/src/browser/contexts/AgentContext.tsx b/src/browser/contexts/AgentContext.tsx
index d261fc0a2f..f1ec20b643 100644
--- a/src/browser/contexts/AgentContext.tsx
+++ b/src/browser/contexts/AgentContext.tsx
@@ -61,6 +61,38 @@ function coerceAgentId(value: unknown): string {
: WORKSPACE_DEFAULTS.agentId;
}
+type AgentDiscoveryCacheMode = "enabled" | "disabled";
+
+function getAgentDiscoveryCacheMode(disableWorkspaceAgents: boolean): AgentDiscoveryCacheMode {
+ return disableWorkspaceAgents ? "disabled" : "enabled";
+}
+
+function getWorkspaceDiscoveryCacheKey(
+ workspaceId: string | undefined,
+ projectPath: string | undefined,
+ mode: AgentDiscoveryCacheMode
+): string | undefined {
+ if (!workspaceId) {
+ return undefined;
+ }
+
+ return projectPath ? `${projectPath}:${workspaceId}:${mode}` : `${workspaceId}:${mode}`;
+}
+
+function getProjectDiscoveryCacheKey(
+ projectPath: string | undefined,
+ mode: AgentDiscoveryCacheMode
+): string | undefined {
+ return projectPath ? `${projectPath}:${mode}` : undefined;
+}
+
+type ProjectDiscoveryCacheSource = "project" | "workspace";
+
+interface ProjectDiscoveryCacheEntry {
+ agents: AgentDefinitionDescriptor[];
+ source: ProjectDiscoveryCacheSource;
+}
+
export function AgentProvider(props: AgentProviderProps) {
if ("value" in props) {
return {props.children};
@@ -128,6 +160,10 @@ function AgentProviderWithState(props: {
const [loadFailed, setLoadFailed] = useState(false);
const isMountedRef = useRef(true);
+ // Keep recently-discovered agents in memory so workspace switches can render
+ // immediately while the backend refresh completes.
+ const workspaceDiscoveryCacheRef = useRef(new Map());
+ const projectDiscoveryCacheRef = useRef(new Map());
useEffect(() => {
isMountedRef.current = true;
@@ -150,6 +186,10 @@ function AgentProviderWithState(props: {
workspaceId: string | undefined,
workspaceAgentsDisabled: boolean
) => {
+ const cacheMode = getAgentDiscoveryCacheMode(workspaceAgentsDisabled);
+ const workspaceCacheKey = getWorkspaceDiscoveryCacheKey(workspaceId, projectPath, cacheMode);
+ const projectCacheKey = getProjectDiscoveryCacheKey(projectPath, cacheMode);
+
fetchParamsRef.current = {
projectPath,
workspaceId,
@@ -178,6 +218,15 @@ function AgentProviderWithState(props: {
current.disableWorkspaceAgents === workspaceAgentsDisabled &&
isMountedRef.current
) {
+ if (workspaceCacheKey) {
+ workspaceDiscoveryCacheRef.current.set(workspaceCacheKey, result);
+ }
+ if (projectCacheKey) {
+ projectDiscoveryCacheRef.current.set(projectCacheKey, {
+ agents: result,
+ source: workspaceId ? "workspace" : "project",
+ });
+ }
setAgents(result);
setLoadFailed(false);
setLoaded(true);
@@ -200,8 +249,51 @@ function AgentProviderWithState(props: {
);
useEffect(() => {
- setAgents([]);
- setLoaded(false);
+ const cacheMode = getAgentDiscoveryCacheMode(disableWorkspaceAgents);
+ const workspaceCacheKey = getWorkspaceDiscoveryCacheKey(
+ props.workspaceId,
+ props.projectPath,
+ cacheMode
+ );
+ const projectCacheKey = getProjectDiscoveryCacheKey(props.projectPath, cacheMode);
+ const workspaceCachedAgents = workspaceCacheKey
+ ? workspaceDiscoveryCacheRef.current.get(workspaceCacheKey)
+ : undefined;
+ const projectCacheEntry = projectCacheKey
+ ? projectDiscoveryCacheRef.current.get(projectCacheKey)
+ : undefined;
+ // Avoid hydrating project-scoped providers from workspace-sourced cache
+ // entries; those can include workspace-only agent definitions.
+ const projectCachedAgents =
+ projectCacheEntry == null
+ ? undefined
+ : props.workspaceId == null && projectCacheEntry.source === "workspace"
+ ? undefined
+ : projectCacheEntry.agents;
+ const optimisticAgents = workspaceCachedAgents ?? projectCachedAgents;
+
+ if (optimisticAgents !== undefined) {
+ const usingWorkspaceSourcedProjectFallbackForWorkspace =
+ workspaceCachedAgents === undefined &&
+ projectCachedAgents !== undefined &&
+ props.workspaceId !== undefined &&
+ projectCacheEntry?.source === "workspace";
+ // Workspace-sourced project fallback is display-only until workspace
+ // discovery resolves, so users can't persist cross-workspace agent IDs.
+ setAgents(
+ usingWorkspaceSourcedProjectFallbackForWorkspace
+ ? optimisticAgents.map((agent) => ({
+ ...agent,
+ uiSelectable: false,
+ }))
+ : optimisticAgents
+ );
+ // Keep loading state while only display-only fallback data is available.
+ setLoaded(!usingWorkspaceSourcedProjectFallbackForWorkspace);
+ } else {
+ setAgents([]);
+ setLoaded(false);
+ }
setLoadFailed(false);
void fetchAgents(props.projectPath, props.workspaceId, disableWorkspaceAgents);
}, [fetchAgents, props.projectPath, props.workspaceId, disableWorkspaceAgents]);
@@ -220,34 +312,26 @@ function AgentProviderWithState(props: {
}
}, [fetchAgents, props.projectPath, props.workspaceId, disableWorkspaceAgents]);
- const selectableAgents = useMemo(
- () => sortAgentsStable(agents.filter((a) => a.uiSelectable)),
+ const cycleableAgents = useMemo(
+ // Keep keyboard cycling aligned with numbered quick-select behavior: auto is
+ // selectable via explicit toggle only, not via cycle-next shortcuts.
+ () => sortAgentsStable(agents.filter((agent) => agent.uiSelectable && agent.id !== "auto")),
[agents]
);
const cycleToNextAgent = useCallback(() => {
- if (selectableAgents.length < 2) return;
+ if (cycleableAgents.length === 0) return;
const activeAgentId = coerceAgentId(
isProjectScope ? (scopedAgentId ?? globalDefaultAgentId) : scopedAgentId
);
-
- // Auto mode: ignore the cycle shortcut when auto is a live agent
- // (stale persisted "auto" not in list → allow cycling to recover)
- const autoAvailable = selectableAgents.some((a) => a.id === "auto");
- if (activeAgentId === "auto" && autoAvailable) return;
-
- // Never cycle into "auto" — it's toggled explicitly via the picker switch
- const cyclableAgents = selectableAgents.filter((a) => a.id !== "auto");
- if (cyclableAgents.length < 2) return;
-
- const currentIndex = cyclableAgents.findIndex((a) => a.id === activeAgentId);
- const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % cyclableAgents.length;
- const nextAgent = cyclableAgents[nextIndex];
+ const currentIndex = cycleableAgents.findIndex((agent) => agent.id === activeAgentId);
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % cycleableAgents.length;
+ const nextAgent = cycleableAgents[nextIndex];
if (nextAgent) {
setAgentId(nextAgent.id);
}
- }, [globalDefaultAgentId, isProjectScope, scopedAgentId, selectableAgents, setAgentId]);
+ }, [globalDefaultAgentId, isProjectScope, scopedAgentId, cycleableAgents, setAgentId]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
diff --git a/src/browser/utils/agents.ts b/src/browser/utils/agents.ts
index e1d289137c..bc6f3e9e60 100644
--- a/src/browser/utils/agents.ts
+++ b/src/browser/utils/agents.ts
@@ -4,6 +4,14 @@ import type { AgentDefinitionDescriptor } from "@/common/types/agentDefinition";
// Only includes agents that are uiSelectable by default.
const BUILTIN_AGENT_ORDER: readonly string[] = ["exec", "plan", "ask"];
+const BUILTIN_AGENT_ACCENTS: Readonly> = {
+ auto: "var(--color-auto-mode)",
+ ask: "var(--color-ask-mode)",
+ plan: "var(--color-plan-mode)",
+ exec: "var(--color-exec-mode)",
+ orchestrator: "var(--color-exec-mode)",
+};
+
/**
* Sort agents with stable ordering: built-ins first (exec, plan),
* then custom agents alphabetically by name.
@@ -23,3 +31,14 @@ export function sortAgentsStable 0) {
+ return discoveredUiColor;
+ }
+
+ const normalizedAgentId =
+ typeof agentId === "string" && agentId.trim().length > 0 ? agentId.trim().toLowerCase() : "";
+
+ return BUILTIN_AGENT_ACCENTS[normalizedAgentId] ?? "var(--color-border-light)";
+}