fix(app): resolve plugin provider icons after the snapshot arrives (#5720)

Subscribe icon consumers to snapshot SVG metadata so compiler-cached fallbacks update when provider discovery completes.
This commit is contained in:
Mohamed Boudra
2026-09-30 13:46:43 +01:00
committed by GitHub
parent b0c9de0c90
commit ad56659a89
23 changed files with 165 additions and 140 deletions
@@ -13,7 +13,7 @@ import {
PlanModeIcon,
ThinkingIcon,
} from "@/agent-controls/icons";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcons } from "@/components/provider-icons";
import type { ProviderSelectorProvider } from "@/provider-selection/provider-selection";
import {
buildAgentControlContributions,
@@ -88,6 +88,7 @@ export function useAgentControlCommandCenterActions(input: {
const { t } = useTranslation();
const { controls } = input;
const { features, models, modes, thinking } = controls;
const getProviderIcon = useProviderIcons(controls.serverId);
const actions = useMemo(
() =>
buildAgentControlContributions({
@@ -96,8 +97,7 @@ export function useAgentControlCommandCenterActions(input: {
provider: controls.provider ?? null,
labels: buildAgentControlContributionLabels(t),
icons: {
provider: (provider) =>
getCommandCenterIcon(getProviderIcon(provider, controls.serverId)),
provider: (provider) => getCommandCenterIcon(getProviderIcon(provider)),
thinking: getCommandCenterIcon(ThinkingIcon),
planMode: getCommandCenterIcon(PlanModeIcon),
mode: (modeId) =>
@@ -129,6 +129,7 @@ export function useAgentControlCommandCenterActions(input: {
},
}),
[
getProviderIcon,
controls.ownerKey,
controls.provider,
controls.providerDefinitions,
+2 -2
View File
@@ -18,7 +18,7 @@ import { useTimeAgo } from "@/hooks/use-time-ago";
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
import { Archive, ChevronRight } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { navigateToAgent } from "@/utils/navigate-to-agent";
import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { HighlightedText } from "@/components/ui/highlighted-text";
@@ -186,7 +186,7 @@ function SessionRow({
const projectName = agent.projectPlacement?.projectName ?? "";
const branch = agent.projectPlacement?.checkout.currentBranch ?? "";
const workspaceName = agent.projectPlacement?.workspaceName ?? "";
const ProviderIcon = getProviderIcon(agent.provider, agent.serverId);
const ProviderIcon = useProviderIcon(agent.provider, agent.serverId);
const pendingPermissionCount = agent.pendingPermissionCount ?? 0;
const ranges = useMemo(
() => ({
@@ -63,7 +63,8 @@ vi.mock("@/constants/layout", () => ({
}));
vi.mock("@/components/provider-icons", () => ({
getProviderIcon: () => () => null,
useProviderIcon: () => () => null,
useProviderIcons: () => () => () => null,
}));
vi.mock("lucide-react-native", () => {
@@ -13,7 +13,7 @@ import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-moda
import { Button } from "@/components/ui/button";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon, useProviderIcons } from "@/components/provider-icons";
import { useTimeAgo } from "@/hooks/use-time-ago";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
@@ -309,7 +309,7 @@ function ImportSessionSheetRow({
const { t } = useTranslation();
const title = getSessionTitle(entry);
const promptPreview = getPromptPreview(entry);
const ProviderIcon = getProviderIcon(entry.providerId, serverId);
const ProviderIcon = useProviderIcon(entry.providerId, serverId);
const accessibilityState = useMemo(
() => (disabled ? DISABLED_ACCESSIBILITY_STATE : undefined),
[disabled],
@@ -559,15 +559,17 @@ export function ImportSessionSheet({
setIsFilterOpen(false);
}, []);
const getProviderIcon = useProviderIcons(serverId);
const SelectedProviderIcon = useProviderIcon(selectedProvider, serverId);
const filterOptionIcons = useMemo(() => {
const map = new Map<string, React.ReactNode>();
map.set(ALL_FILTER_VALUE, <Layers size={14} color={theme.colors.foregroundMuted} />);
for (const provider of filterProviders) {
const ProviderIcon = getProviderIcon(provider, serverId);
const ProviderIcon = getProviderIcon(provider);
map.set(provider, <ProviderIcon size={14} color={theme.colors.foregroundMuted} />);
}
return map;
}, [filterProviders, serverId, theme.colors.foregroundMuted]);
}, [filterProviders, getProviderIcon, theme.colors.foregroundMuted]);
const renderFilterOption = useCallback(
({
@@ -746,10 +748,7 @@ export function ImportSessionSheet({
{selectedProvider === ALL_FILTER_VALUE ? (
<Layers size={14} color={theme.colors.foregroundMuted} />
) : (
(() => {
const ProviderIcon = getProviderIcon(selectedProvider, serverId);
return <ProviderIcon size={14} color={theme.colors.foregroundMuted} />;
})()
<SelectedProviderIcon size={14} color={theme.colors.foregroundMuted} />
)}
<Text style={styles.filterTriggerText} numberOfLines={1}>
{selectedProviderLabel}
@@ -39,7 +39,7 @@ import type { SheetHeader } from "@/components/adaptive-modal-sheet";
import { Button } from "@/components/ui/button";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative, isWeb } from "@/constants/platform";
import {
@@ -223,7 +223,7 @@ export function ModelProviderGlyph({
size: number;
tone?: ProviderGlyphTone;
}) {
const Icon = getProviderIcon(provider, serverId);
const Icon = useProviderIcon(provider, serverId);
const color =
tone === "foreground" ? styles.providerIconForeground.color : styles.providerIconMuted.color;
return <Icon size={size} color={color} />;
@@ -5,7 +5,7 @@ import {
TERMINAL_PROFILE_ICON_NAMES,
} from "@getpaseo/protocol/provider-icon-names";
import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog";
import { replaceProviderSnapshotIcons, resolveProviderIconName } from "./provider-icon-name";
import { resolveProviderIconName } from "./provider-icon-name";
describe("resolveProviderIconName", () => {
it("returns the built-in identifier for known provider ids", () => {
@@ -29,43 +29,7 @@ describe("resolveProviderIconName", () => {
it("resolves a snapshot SVG for a custom provider", () => {
const svg = '<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" /></svg>';
replaceProviderSnapshotIcons("server-1", [{ provider: "snapshot-provider", iconSvg: svg }]);
expect(resolveProviderIconName("snapshot-provider", "server-1")).toEqual({ kind: "svg", svg });
});
it("replaces each host snapshot without leaking icons across hosts", () => {
const secondSvg = "<svg id='second' />";
replaceProviderSnapshotIcons("server-1", [
{ provider: "removed-provider", iconSvg: "<svg />" },
{ provider: "snapshot-provider", iconSvg: "<svg />" },
{ provider: "claude", iconSvg: "<svg />" },
]);
replaceProviderSnapshotIcons("server-2", [
{ provider: "snapshot-provider", iconSvg: secondSvg },
]);
expect(resolveProviderIconName("snapshot-provider", "server-1")).toEqual({
kind: "svg",
svg: "<svg />",
});
expect(resolveProviderIconName("snapshot-provider", "server-2")).toEqual({
kind: "svg",
svg: secondSvg,
});
expect(resolveProviderIconName("claude", "server-1")).toEqual({
kind: "builtin",
id: "claude",
});
replaceProviderSnapshotIcons("server-1", [{ provider: "snapshot-provider" }]);
expect(resolveProviderIconName("removed-provider", "server-1")).toEqual({ kind: "bot" });
expect(resolveProviderIconName("snapshot-provider", "server-1")).toEqual({ kind: "bot" });
expect(resolveProviderIconName("snapshot-provider", "server-2")).toEqual({
kind: "svg",
svg: secondSvg,
});
expect(resolveProviderIconName("snapshot-provider", svg)).toEqual({ kind: "svg", svg });
});
});
@@ -2,7 +2,6 @@ import {
BUILTIN_PROVIDER_ICON_NAMES,
KNOWN_PROVIDER_ICON_NAMES,
} from "@getpaseo/protocol/provider-icon-names";
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
export type ProviderIconName =
| { kind: "builtin"; id: string }
@@ -12,31 +11,10 @@ export type ProviderIconName =
const BUILTIN_PROVIDER_IDS = new Set(BUILTIN_PROVIDER_ICON_NAMES);
const KNOWN_PROVIDER_IDS = new Set(KNOWN_PROVIDER_ICON_NAMES);
const providerSnapshotIconSvgsByServer = new Map<string, ReadonlyMap<string, string>>();
export function replaceProviderSnapshotIcons(
serverId: string,
entries: readonly Pick<ProviderSnapshotEntry, "provider" | "iconSvg">[],
): void {
const icons = new Map<string, string>();
for (const entry of entries) {
if (entry.iconSvg) {
icons.set(entry.provider, entry.iconSvg);
}
}
providerSnapshotIconSvgsByServer.set(serverId, icons);
}
export function resolveProviderIconName(
provider: string,
serverId?: string | null,
): ProviderIconName {
export function resolveProviderIconName(provider: string, iconSvg?: string): ProviderIconName {
if (BUILTIN_PROVIDER_IDS.has(provider)) {
return { kind: "builtin", id: provider };
}
const iconSvg = serverId
? providerSnapshotIconSvgsByServer.get(serverId)?.get(provider)
: undefined;
if (iconSvg) {
return { kind: "svg", svg: iconSvg };
}
@@ -1,8 +1,12 @@
// @vitest-environment jsdom
import { act, cleanup, renderHook } from "@testing-library/react";
import { Bot } from "lucide-react-native";
import { SvgXml } from "react-native-svg";
import { describe, expect, it } from "vitest";
import { replaceProviderSnapshotIcons } from "./provider-icon-name";
import { getProviderIcon, type ProviderIconComponent } from "./provider-icons";
import { afterEach, describe, expect, it } from "vitest";
import { replaceProviderSnapshotIcons } from "@/data/provider-icons";
import { useProviderIcon, useProviderIcons, type ProviderIconComponent } from "./provider-icons";
afterEach(cleanup);
function renderIcon(Component: ProviderIconComponent) {
if (typeof Component !== "function") throw new Error("Expected a function component");
@@ -12,12 +16,14 @@ function renderIcon(Component: ProviderIconComponent) {
});
}
describe("getProviderIcon", () => {
describe("useProviderIcon", () => {
it("renders registered snapshot SVG metadata with the requested size and color", () => {
const svg = '<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" /></svg>';
replaceProviderSnapshotIcons("server-1", [{ provider: "rendered-provider", iconSvg: svg }]);
const rendered = renderIcon(getProviderIcon("rendered-provider", "server-1"));
const rendered = renderIcon(
renderHook(() => useProviderIcon("rendered-provider", "server-1")).result.current,
);
expect(rendered).toMatchObject({
type: SvgXml,
@@ -28,6 +34,53 @@ describe("getProviderIcon", () => {
it("uses the normal Bot fallback without snapshot SVG metadata", () => {
replaceProviderSnapshotIcons("server-1", [{ provider: "plain-provider" }]);
expect(getProviderIcon("plain-provider", "server-1")).toBe(Bot);
expect(renderHook(() => useProviderIcon("plain-provider", "server-1")).result.current).toBe(
Bot,
);
});
});
it("updates a mounted consumer when the plugin snapshot arrives after mount", () => {
const serverId = "cold-reload-host";
const provider = "plugin-provider";
const svg = '<svg viewBox="0 0 24 24"><path d="M4 4h16v16H4z" /></svg>';
replaceProviderSnapshotIcons(serverId, []);
const { result, unmount } = renderHook(() => useProviderIcon(provider, serverId));
expect(result.current).toBe(Bot);
act(() => replaceProviderSnapshotIcons(serverId, [{ provider, iconSvg: svg }]));
expect(renderIcon(result.current)).toMatchObject({
type: SvgXml,
props: { xml: svg, width: 18, height: 18, color: "#123456" },
});
unmount();
});
it("keeps host SVGs isolated, updates replacements, and clears removed icons", () => {
const provider = "shared-plugin-provider";
const firstSvg = '<svg id="first" />';
const secondSvg = '<svg id="second" />';
replaceProviderSnapshotIcons("first-host", [{ provider, iconSvg: firstSvg }]);
replaceProviderSnapshotIcons("second-host", [{ provider, iconSvg: secondSvg }]);
const first = renderHook(() => useProviderIcon(provider, "first-host"));
const second = renderHook(() => useProviderIcon(provider, "second-host"));
const firstIcon = first.result.current;
expect(renderIcon(firstIcon)).toMatchObject({ props: { xml: firstSvg } });
expect(renderIcon(second.result.current)).toMatchObject({ props: { xml: secondSvg } });
act(() => replaceProviderSnapshotIcons("second-host", [{ provider, iconSvg: firstSvg }]));
expect(renderIcon(second.result.current)).toMatchObject({ props: { xml: firstSvg } });
expect(first.result.current).toBe(firstIcon);
act(() => replaceProviderSnapshotIcons("second-host", []));
expect(second.result.current).toBe(Bot);
expect(first.result.current).toBe(firstIcon);
});
it("updates collection consumers when their host snapshot arrives", () => {
const serverId = "collection-host";
const provider = "collection-plugin-provider";
const svg = '<svg id="collection" />';
replaceProviderSnapshotIcons(serverId, []);
const { result } = renderHook(() => useProviderIcons(serverId));
expect(result.current(provider)).toBe(Bot);
act(() => replaceProviderSnapshotIcons(serverId, [{ provider, iconSvg: svg }]));
expect(renderIcon(result.current(provider))).toMatchObject({ props: { xml: svg } });
});
+28 -8
View File
@@ -1,5 +1,7 @@
import { useStore } from "zustand";
import { providerSnapshotIcons } from "@/data/provider-icons";
import { Bot, PackagePlus } from "lucide-react-native";
import { createElement, type ComponentType } from "react";
import { createElement, useCallback, type ComponentType } from "react";
import { SvgXml } from "react-native-svg";
import { ClaudeIcon } from "@/components/icons/claude-icon";
import { CodexIcon } from "@/components/icons/codex-icon";
@@ -34,7 +36,7 @@ const CATALOG_ICON_SVGS = new Map(
);
const catalogIconComponents = new Map<string, ProviderIconComponent>();
const snapshotIconComponents = new Map<string, { svg: string; component: ProviderIconComponent }>();
const snapshotIconComponents = new Map<string, ProviderIconComponent>();
/** Renders a sanitized SVG string from a host catalog, tinted through `currentColor`. */
export function SvgIcon({ svg, size, color }: ProviderIconProps & { svg: string }) {
@@ -63,15 +65,15 @@ function getCatalogProviderIcon(provider: string): ProviderIconComponent {
}
function getSnapshotProviderIcon(provider: string, svg: string): ProviderIconComponent {
const cached = snapshotIconComponents.get(provider);
if (cached?.svg === svg) return cached.component;
const cached = snapshotIconComponents.get(svg);
if (cached) return cached;
const component = createSvgIcon(provider, svg);
snapshotIconComponents.set(provider, { svg, component });
snapshotIconComponents.set(svg, component);
return component;
}
export function getProviderIcon(provider: string, serverId?: string | null): ProviderIconComponent {
const name = resolveProviderIconName(provider, serverId);
function resolveProviderIcon(provider: string, svg?: string): ProviderIconComponent {
const name = resolveProviderIconName(provider, svg);
if (name.kind === "builtin") {
return BUILTIN_PROVIDER_ICONS[name.id];
}
@@ -79,7 +81,25 @@ export function getProviderIcon(provider: string, serverId?: string | null): Pro
return getCatalogProviderIcon(name.id);
}
if (name.kind === "svg") {
return getSnapshotProviderIcon(`${serverId}:${provider}`, name.svg);
return getSnapshotProviderIcon(provider, name.svg);
}
return Bot;
}
export function useProviderIcon(provider: string, serverId?: string | null): ProviderIconComponent {
const svg = useStore(providerSnapshotIcons, (state) =>
serverId ? state.get(serverId)?.get(provider) : undefined,
);
return resolveProviderIcon(provider, svg);
}
/** Subscribe once for consumers that render a collection of providers. */
export function useProviderIcons(serverId?: string | null) {
const icons = useStore(providerSnapshotIcons, (state) =>
serverId ? state.get(serverId) : undefined,
);
return useCallback(
(provider: string) => resolveProviderIcon(provider, icons?.get(provider)),
[icons],
);
}
@@ -24,7 +24,7 @@ import { HostStatusDotSlot } from "@/components/hosts/host-picker";
import { createControlGeometry, type FieldControlSize } from "@/components/ui/control-geometry";
import { Field, FormTextInput } from "@/components/ui/form-field";
import { Switch } from "@/components/ui/switch";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { CadenceEditor } from "@/components/schedules/cadence-editor";
import {
SelectField,
@@ -1026,10 +1026,10 @@ function ProviderGlyph({
provider: string | null;
serverId: string | null;
}): ReactElement | null {
const Icon = useProviderIcon(provider ?? "", serverId);
if (!provider) {
return null;
}
const Icon = getProviderIcon(provider, serverId);
return <Icon size={16} color={styles.providerIcon.color} />;
}
@@ -10,7 +10,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { StatusBadge } from "@/components/ui/status-badge";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { isNative } from "@/constants/platform";
import { useIsCompactFormFactor } from "@/constants/layout";
import { settingsStyles } from "@/styles/settings";
@@ -144,7 +144,7 @@ function ScheduleMeta({
}
/** Small provider glyph. Reads the icon color off a StyleSheet object so the
* dynamic component (getProviderIcon) stays compliant without useUnistyles. */
* dynamic component (useProviderIcon) stays compliant without useUnistyles. */
function ProviderGlyph({
provider,
serverId,
@@ -152,10 +152,10 @@ function ProviderGlyph({
provider: string | null;
serverId: string;
}): ReactElement | null {
const Icon = useProviderIcon(provider ?? "", serverId);
if (!provider) {
return null;
}
const Icon = getProviderIcon(provider, serverId);
return <Icon size={PROVIDER_ICON_SIZE} color={styles.providerIcon.color} />;
}
@@ -1,7 +1,7 @@
import type { ReactElement } from "react";
import { SquareTerminal } from "lucide-react-native";
import { withUnistyles } from "react-native-unistyles";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import type { Theme } from "@/styles/theme";
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
@@ -17,7 +17,7 @@ function ProviderProfileIcon({
size: number;
color?: string;
}) {
const Icon = getProviderIcon(iconKey);
const Icon = useProviderIcon(iconKey);
return <Icon size={size} color={color} />;
}
@@ -2,12 +2,11 @@ import { useCallback, useMemo, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { Keyboard, ScrollView, Text, View, type PressableStateCallbackType } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { Bot } from "lucide-react-native";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { AgentProfilePicker, AgentProfileSeed } from "@/agent-profiles";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { ComboboxTrigger } from "@/components/ui/combobox-trigger";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { ModelBrowser, useModelBrowser } from "@/components/model-browser";
import { resolveModelBrowserScrolling } from "@/components/model-browser-view";
import { AgentControlTrigger } from "@/composer/agent-controls/control";
@@ -130,9 +129,8 @@ export function CompactModelSheet({
profiles,
serverId,
});
const ProviderIcon =
selectedProvider.trim().length > 0 ? getProviderIcon(selectedProvider, serverId) : null;
const ModelIcon = ProviderIcon ?? Bot;
const ModelIcon = useProviderIcon(selectedProvider, serverId);
const hasSelectedProvider = selectedProvider.trim().length > 0;
const rootHeader = useMemo(
() => ({
...rootBrowser.header,
@@ -277,9 +275,9 @@ export function CompactModelSheet({
testID="combined-model-selector"
chevron={null}
>
{ProviderIcon ? (
{hasSelectedProvider ? (
<ComposerToolbarGlyph size={glyphSize}>
<ProviderIcon size={glyphSize} color={styles.providerIcon.color} />
<ModelIcon size={glyphSize} color={styles.providerIcon.color} />
</ComposerToolbarGlyph>
) : null}
<View style={styles.triggerLabels}>
+17
View File
@@ -0,0 +1,17 @@
import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types";
import { createStore } from "zustand/vanilla";
export const providerSnapshotIcons = createStore<ReadonlyMap<string, ReadonlyMap<string, string>>>(
() => new Map(),
);
export function replaceProviderSnapshotIcons(
serverId: string,
entries: readonly Pick<ProviderSnapshotEntry, "provider" | "iconSvg">[],
): void {
const icons = new Map<string, string>();
for (const entry of entries) {
if (entry.iconSvg) icons.set(entry.provider, entry.iconSvg);
}
providerSnapshotIcons.setState((previous) => new Map(previous).set(serverId, icons), true);
}
@@ -5,10 +5,7 @@ import type { GetProvidersSnapshotResponseMessage } from "@getpaseo/protocol/mes
import { compactProviderSnapshot } from "@getpaseo/protocol/provider-snapshot-codec";
import { applyProvidersSnapshotUpdate } from "./push-router";
import { createProviderSnapshotCache } from "./provider-snapshot-cache";
import {
resolveProviderIconName,
replaceProviderSnapshotIcons,
} from "@/components/provider-icon-name";
import { providerSnapshotIcons, replaceProviderSnapshotIcons } from "@/data/provider-icons";
import { describe, expect, it } from "vitest";
import {
fetchProvidersSnapshot,
@@ -143,10 +140,9 @@ describe("provider fetch with runtime abort signals", () => {
expect(result.entries).toEqual(entries);
expect(queryClient.getQueryData(queryKey)).toEqual(result);
expect((await cache.read(serverId, snapshot.cwd))?.entries).toEqual(entries);
expect(resolveProviderIconName("test-provider", serverId)).toEqual({
kind: "svg",
svg: entries[0].iconSvg,
});
expect(providerSnapshotIcons.getState().get(serverId)?.get("test-provider")).toEqual(
entries[0].iconSvg,
);
} finally {
queryClient.clear();
replaceProviderSnapshotIcons(serverId, []);
@@ -203,10 +199,9 @@ describe.each([
).rejects.toBeInstanceOf(CancelledError);
expect(controller.signal.aborted).toBe(true);
expect(queryClient.getQueryData(queryKey)).toEqual(previous);
expect(resolveProviderIconName("test-provider", serverId)).toEqual({
kind: "svg",
svg: "previous-icon",
});
expect(providerSnapshotIcons.getState().get(serverId)?.get("test-provider")).toEqual(
"previous-icon",
);
expect((await cache.readHash(serverId, "test-hash"))?.entries ?? null).toEqual(
hasBody ? entries : null,
);
@@ -287,10 +282,9 @@ it("keeps a replacement query when the cancelled native fetch finishes late", as
expect(queryClient.getQueryData(queryKey)).toEqual(replacement);
expect(replacement.entries).toEqual(replacementEntries);
expect((await cache.read(serverId, snapshot.cwd))?.entries).toEqual(replacementEntries);
expect(resolveProviderIconName("test-provider", serverId)).toEqual({
kind: "svg",
svg: "replacement-icon",
});
expect(providerSnapshotIcons.getState().get(serverId)?.get("test-provider")).toEqual(
"replacement-icon",
);
} finally {
finishResponse();
queryClient.clear();
+1 -1
View File
@@ -7,7 +7,7 @@ import {
type ProviderSnapshotCache,
} from "./provider-snapshot-cache";
import { queryClient as singletonQueryClient } from "./query-client";
import { replaceProviderSnapshotIcons } from "@/components/provider-icon-name";
import { replaceProviderSnapshotIcons } from "@/data/provider-icons";
import { agentCommandsQueryRoot } from "@/hooks/agent-commands-query";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import { normalizeWorkspacePath } from "@/utils/workspace-identity";
@@ -9,7 +9,7 @@ import {
} from "@getpaseo/protocol/provider-snapshot-codec";
import type { CachedProviderSnapshot, ProviderSnapshotCache } from "@/data/provider-snapshot-cache";
import { draftAgentCommandsQueryKey } from "@/hooks/agent-commands-query";
import { resolveProviderIconName } from "@/components/provider-icon-name";
import { providerSnapshotIcons } from "@/data/provider-icons";
import { applyProvidersSnapshotUpdate, type ProvidersSnapshotUpdate } from "@/data/push-router";
import {
fetchProvidersSnapshot,
@@ -141,10 +141,9 @@ describe("fetchProvidersSnapshot", () => {
await fetchProvidersSnapshot({ client, serverId, cwd: null, cache: createCache() });
expect(resolveProviderIconName("snapshot-icon-provider", serverId)).toEqual({
kind: "svg",
expect(providerSnapshotIcons.getState().get(serverId)?.get("snapshot-icon-provider")).toEqual(
svg,
});
);
});
it("sends no cwd for the home scope", async () => {
+2 -2
View File
@@ -34,7 +34,7 @@ import {
} from "@/composer/pill-styles";
import { getActiveMessageSubmissions } from "@/composer/submission/model";
import { RewindComposerRestoreProvider } from "@/components/rewind/composer-restore";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { useToastHost, type ToastApi, type ToastState } from "@/components/toast-host";
import type { WorkspaceComposerAttachment } from "@/attachments/types";
import { useWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
@@ -343,7 +343,7 @@ function useAgentPanelDescriptor(
);
const provider = descriptorState.provider;
const label = resolveWorkspaceAgentTabLabel(descriptorState.title);
const icon = getProviderIcon(provider, context.serverId);
const icon = useProviderIcon(provider, context.serverId);
return {
label: label ?? "",
@@ -4,7 +4,7 @@ import { StyleSheet } from "react-native-unistyles";
import invariant from "tiny-invariant";
import { useShallow } from "zustand/react/shallow";
import { AgentStreamView } from "@/agent-stream/view";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import {
resolveComposerTrackControlClearance,
resolveComposerTrackTailClearance,
@@ -97,7 +97,7 @@ function useProviderSubagentDescriptor(
subagentType && subagentType !== label ? `${subagentType} · ${providerLabel}` : providerLabel,
tooltip: label,
titleState: descriptor ? "ready" : "loading",
icon: getProviderIcon(provider, context.serverId),
icon: useProviderIcon(provider, context.serverId),
statusBucket: descriptor
? deriveSidebarStateBucket({
status: providerSubagentLifecycleStatus(descriptor.status),
@@ -63,7 +63,7 @@ import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons
import { formatLatency } from "@/utils/latency";
import { ICON_SIZE } from "@/styles/theme";
import type { Theme } from "@/styles/theme";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { BrowserToolsOptInCard } from "./browser-tools-card";
import { restartDaemonFromSettings, updateDaemonFromSettings } from "./daemon-lifecycle";
@@ -90,7 +90,7 @@ interface DynamicProviderIconProps {
}
function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) {
const Icon = getProviderIcon(iconKey);
const Icon = useProviderIcon(iconKey);
return <Icon size={size} color={color} />;
}
@@ -233,7 +233,7 @@ vi.mock("@/components/ui/dropdown-menu", () => ({
}));
vi.mock("@/components/provider-icons", () => ({
getProviderIcon: (provider: string) => () =>
useProviderIcon: (provider: string) => () =>
React.createElement("span", { "data-icon": `provider-${provider}` }),
}));
@@ -22,7 +22,7 @@ import {
type AcpProviderCatalogItem,
} from "@/hooks/use-acp-provider-catalog";
import { ProviderCatalogList } from "@/components/provider-catalog-list";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { Switch } from "@/components/ui/switch";
import {
@@ -183,7 +183,7 @@ function ProviderRow({
const { t } = useTranslation();
const { theme } = useUnistyles();
const isCompact = useIsCompactFormFactor();
const ProviderIcon = getProviderIcon(def.id, serverId);
const ProviderIcon = useProviderIcon(def.id, serverId);
const providerError =
enabled &&
entry.status === "error" &&
+6 -5
View File
@@ -1,9 +1,9 @@
import { useCallback, useMemo, type ReactElement } from "react";
import { useCallback, type ReactElement } from "react";
import { Pressable, Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { Archive, Unlink } from "lucide-react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { getProviderIcon } from "@/components/provider-icons";
import { useProviderIcon } from "@/components/provider-icons";
import { ComposerTrackActions, ComposerTrackPill, ComposerTrackRow } from "@/composer/tracks";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useIsCompactFormFactor } from "@/constants/layout";
@@ -45,13 +45,14 @@ const IDLE_ARCHIVE_FINISHED_STATUS: ArchiveFinishedStatus = { kind: "idle" };
/** Leading and action glyphs share one size so rows keep a single icon column. */
const ROW_ICON_SIZE = 14;
function buildRowPresentation(row: SubagentRow, serverId: string): WorkspaceTabPresentation {
function useRowPresentation(row: SubagentRow, serverId: string): WorkspaceTabPresentation {
const icon = useProviderIcon(row.provider, serverId);
const data = buildSubagentRowPresentationData(row);
return {
...data,
tooltip: data.label,
modified: false,
icon: getProviderIcon(row.provider, serverId),
icon,
};
}
@@ -185,7 +186,7 @@ function SubagentsTrackRow({
}: SubagentsTrackRowProps): ReactElement {
const { t } = useTranslation();
const isCompact = useIsCompactFormFactor();
const presentation = useMemo(() => buildRowPresentation(row, serverId), [row, serverId]);
const presentation = useRowPresentation(row, serverId);
const displayLabel =
presentation.titleState === "loading" ? t("common.states.loading") : presentation.label;
const handlePress = useCallback(() => {