mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:50:45 +00:00
fix(ui): keep assistant avatar overrides authoritative
Summary: - Make browser-local assistant avatar overrides win over stale missing IDENTITY.md avatar metadata. - Show the selected assistant image in Personal settings and chat instead of a false File not found state. - Add focused Control UI coverage for assistant avatar override and clear behavior. Validation: - pnpm test ui/src/ui/app-render.assistant-avatar.test.ts ui/src/ui/views/config-quick.test.ts ui/src/ui/controllers/assistant-identity.test.ts -- --reporter=verbose - pnpm tsgo:core:test - pnpm deadcode:dependencies - pnpm deadcode:unused-files - CI green on PR #74260
This commit is contained in:
230
ui/src/ui/app-render.assistant-avatar.test.ts
Normal file
230
ui/src/ui/app-render.assistant-avatar.test.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { html } from "lit";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { AppViewState } from "./app-view-state.ts";
|
||||||
|
import type { QuickSettingsProps } from "./views/config-quick.ts";
|
||||||
|
|
||||||
|
const quickSettingsProps = vi.hoisted(() => ({
|
||||||
|
current: null as QuickSettingsProps | null,
|
||||||
|
}));
|
||||||
|
const localStorageValues = vi.hoisted(() => new Map<string, string>());
|
||||||
|
|
||||||
|
vi.mock("../local-storage.ts", () => ({
|
||||||
|
getSafeLocalStorage: () => ({
|
||||||
|
getItem: (key: string) => localStorageValues.get(key) ?? null,
|
||||||
|
removeItem: (key: string) => localStorageValues.delete(key),
|
||||||
|
setItem: (key: string, value: string) => localStorageValues.set(key, value),
|
||||||
|
}),
|
||||||
|
getSafeSessionStorage: () => null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./views/config-quick.ts", () => ({
|
||||||
|
renderQuickSettings: (props: QuickSettingsProps) => {
|
||||||
|
quickSettingsProps.current = props;
|
||||||
|
return html`<div data-testid="quick-settings"></div>`;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./views/chat.ts", () => ({
|
||||||
|
renderChat: () => html`<div data-testid="chat"></div>`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./icons.ts", () => ({
|
||||||
|
icons: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { renderApp } from "./app-render.ts";
|
||||||
|
import { saveLocalAssistantIdentity } from "./storage.ts";
|
||||||
|
|
||||||
|
function createState(overrides: Partial<AppViewState> = {}): AppViewState {
|
||||||
|
return {
|
||||||
|
settings: {
|
||||||
|
gatewayUrl: "ws://localhost:18789",
|
||||||
|
token: "",
|
||||||
|
locale: "en",
|
||||||
|
sessionKey: "main",
|
||||||
|
lastActiveSessionKey: "main",
|
||||||
|
theme: "claw",
|
||||||
|
themeMode: "dark",
|
||||||
|
splitRatio: 0.6,
|
||||||
|
navWidth: 280,
|
||||||
|
navCollapsed: false,
|
||||||
|
navGroupsCollapsed: {},
|
||||||
|
borderRadius: 50,
|
||||||
|
chatFocusMode: false,
|
||||||
|
chatShowThinking: false,
|
||||||
|
chatShowToolCalls: true,
|
||||||
|
},
|
||||||
|
password: "",
|
||||||
|
loginShowGatewayToken: false,
|
||||||
|
loginShowGatewayPassword: false,
|
||||||
|
tab: "config",
|
||||||
|
onboarding: false,
|
||||||
|
basePath: "",
|
||||||
|
connected: true,
|
||||||
|
theme: "claw",
|
||||||
|
themeMode: "dark",
|
||||||
|
themeResolved: "dark",
|
||||||
|
themeOrder: ["claw", "knot", "dash"],
|
||||||
|
customThemeImportUrl: "",
|
||||||
|
customThemeImportBusy: false,
|
||||||
|
customThemeImportMessage: null,
|
||||||
|
customThemeImportExpanded: false,
|
||||||
|
customThemeImportFocusToken: 0,
|
||||||
|
hello: null,
|
||||||
|
lastError: null,
|
||||||
|
lastErrorCode: null,
|
||||||
|
eventLog: [],
|
||||||
|
assistantName: "Nova",
|
||||||
|
assistantAvatar: "/avatar/main",
|
||||||
|
assistantAvatarSource: "avatars/missing.png",
|
||||||
|
assistantAvatarStatus: "none",
|
||||||
|
assistantAvatarReason: "missing",
|
||||||
|
assistantAvatarUploadBusy: false,
|
||||||
|
assistantAvatarUploadError: null,
|
||||||
|
assistantAgentId: "main",
|
||||||
|
userName: null,
|
||||||
|
userAvatar: null,
|
||||||
|
localMediaPreviewRoots: [],
|
||||||
|
embedSandboxMode: "scripts",
|
||||||
|
allowExternalEmbedUrls: false,
|
||||||
|
sessionKey: "main",
|
||||||
|
chatLoading: false,
|
||||||
|
chatSending: false,
|
||||||
|
chatMessage: "",
|
||||||
|
chatAttachments: [],
|
||||||
|
chatMessages: [],
|
||||||
|
chatToolMessages: [],
|
||||||
|
chatStreamSegments: [],
|
||||||
|
chatStream: null,
|
||||||
|
chatStreamStartedAt: null,
|
||||||
|
chatRunId: null,
|
||||||
|
chatSideResult: null,
|
||||||
|
chatSideResultTerminalRuns: new Set(),
|
||||||
|
compactionStatus: null,
|
||||||
|
fallbackStatus: null,
|
||||||
|
chatAvatarUrl: null,
|
||||||
|
chatAvatarSource: null,
|
||||||
|
chatAvatarStatus: null,
|
||||||
|
chatAvatarReason: null,
|
||||||
|
chatThinkingLevel: null,
|
||||||
|
chatModelOverrides: {},
|
||||||
|
chatModelsLoading: false,
|
||||||
|
chatModelCatalog: [],
|
||||||
|
chatQueue: [],
|
||||||
|
chatQueueBySession: {},
|
||||||
|
chatLocalInputHistoryBySession: {},
|
||||||
|
chatInputHistorySessionKey: null,
|
||||||
|
chatInputHistoryItems: null,
|
||||||
|
chatInputHistoryIndex: -1,
|
||||||
|
chatDraftBeforeHistory: null,
|
||||||
|
realtimeTalkActive: false,
|
||||||
|
realtimeTalkStatus: "idle",
|
||||||
|
realtimeTalkDetail: null,
|
||||||
|
realtimeTalkTranscript: null,
|
||||||
|
chatManualRefreshInFlight: false,
|
||||||
|
nodesLoading: false,
|
||||||
|
nodes: [],
|
||||||
|
chatNewMessagesBelow: false,
|
||||||
|
navDrawerOpen: false,
|
||||||
|
sidebarOpen: false,
|
||||||
|
sidebarContent: null,
|
||||||
|
sidebarError: null,
|
||||||
|
splitRatio: 0.6,
|
||||||
|
scrollToBottom: vi.fn(),
|
||||||
|
presenceEntries: [],
|
||||||
|
sessionsResult: null,
|
||||||
|
cronStatus: null,
|
||||||
|
configSettingsMode: "quick",
|
||||||
|
configForm: {},
|
||||||
|
configSnapshot: { config: {}, hash: "hash" } as AppViewState["configSnapshot"],
|
||||||
|
configFormDirty: false,
|
||||||
|
configSaving: false,
|
||||||
|
configApplying: false,
|
||||||
|
cronJobs: [],
|
||||||
|
skillsReport: {
|
||||||
|
skills: [],
|
||||||
|
workspaceDir: "",
|
||||||
|
managedSkillsDir: "",
|
||||||
|
} as AppViewState["skillsReport"],
|
||||||
|
configActiveSection: null,
|
||||||
|
configActiveSubsection: null,
|
||||||
|
communicationsActiveSection: null,
|
||||||
|
communicationsActiveSubsection: null,
|
||||||
|
appearanceActiveSection: null,
|
||||||
|
appearanceActiveSubsection: null,
|
||||||
|
appearanceFormMode: "form",
|
||||||
|
appearanceSearchQuery: "",
|
||||||
|
automationActiveSection: null,
|
||||||
|
automationActiveSubsection: null,
|
||||||
|
infrastructureActiveSection: null,
|
||||||
|
infrastructureActiveSubsection: null,
|
||||||
|
aiAgentsActiveSection: null,
|
||||||
|
aiAgentsActiveSubsection: null,
|
||||||
|
configReady: true,
|
||||||
|
configRaw: "",
|
||||||
|
configRawOriginal: "",
|
||||||
|
configValid: true,
|
||||||
|
configIssues: [],
|
||||||
|
configLoading: false,
|
||||||
|
configSchema: null,
|
||||||
|
configSchemaLoading: false,
|
||||||
|
configUiHints: null,
|
||||||
|
configFormOriginal: {},
|
||||||
|
updateRunning: false,
|
||||||
|
agentsList: null,
|
||||||
|
agentsSelectedId: null,
|
||||||
|
cronModelSuggestions: [],
|
||||||
|
cronForm: { deliveryChannel: "", deliveryMode: "last" },
|
||||||
|
cronFieldErrors: {},
|
||||||
|
cronError: null,
|
||||||
|
cronQuickCreateOpen: false,
|
||||||
|
cronQuickCreateStep: "what",
|
||||||
|
cronQuickCreateDraft: null,
|
||||||
|
cronEditingJobId: null,
|
||||||
|
channelsSnapshot: null,
|
||||||
|
execApprovalQueue: [],
|
||||||
|
dreamingRestartConfirmOpen: false,
|
||||||
|
dreamingRestartConfirmLoading: false,
|
||||||
|
dreamingStatusError: null,
|
||||||
|
client: null,
|
||||||
|
refreshSessionsAfterChat: new Set(),
|
||||||
|
connect: vi.fn(),
|
||||||
|
setTab: vi.fn(),
|
||||||
|
setTheme: vi.fn(),
|
||||||
|
setThemeMode: vi.fn(),
|
||||||
|
setCustomThemeImportUrl: vi.fn(),
|
||||||
|
openCustomThemeImport: vi.fn(),
|
||||||
|
importCustomTheme: vi.fn(),
|
||||||
|
clearCustomTheme: vi.fn(),
|
||||||
|
setBorderRadius: vi.fn(),
|
||||||
|
applySettings: vi.fn(),
|
||||||
|
applyLocalUserIdentity: vi.fn(),
|
||||||
|
loadOverview: vi.fn(),
|
||||||
|
loadAssistantIdentity: vi.fn(),
|
||||||
|
loadCron: vi.fn(),
|
||||||
|
...overrides,
|
||||||
|
} as unknown as AppViewState;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorageValues.clear();
|
||||||
|
quickSettingsProps.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("renderApp assistant avatar routing", () => {
|
||||||
|
it("passes the browser-local assistant override to Quick Settings ahead of stale identity metadata", () => {
|
||||||
|
const dataUrl = "data:image/png;base64,bG9jYWwtYXNzaXN0YW50";
|
||||||
|
saveLocalAssistantIdentity({ avatar: dataUrl });
|
||||||
|
|
||||||
|
renderApp(createState());
|
||||||
|
|
||||||
|
expect(quickSettingsProps.current?.assistantAvatar).toBe(dataUrl);
|
||||||
|
expect(quickSettingsProps.current?.assistantAvatarUrl).toBe(dataUrl);
|
||||||
|
expect(quickSettingsProps.current?.assistantAvatarSource).toBe(dataUrl);
|
||||||
|
expect(quickSettingsProps.current?.assistantAvatarStatus).toBe("data");
|
||||||
|
expect(quickSettingsProps.current?.assistantAvatarReason).toBeNull();
|
||||||
|
expect(quickSettingsProps.current?.assistantAvatarOverride).toBe(dataUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -118,13 +118,14 @@ import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
|
|||||||
import { icons } from "./icons.ts";
|
import { icons } from "./icons.ts";
|
||||||
import { createLazyView, renderLazyView } from "./lazy-view.ts";
|
import { createLazyView, renderLazyView } from "./lazy-view.ts";
|
||||||
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
|
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
|
||||||
import "./components/dashboard-header.ts";
|
|
||||||
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
|
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
|
||||||
|
import "./components/dashboard-header.ts";
|
||||||
import {
|
import {
|
||||||
buildAgentMainSessionKey,
|
buildAgentMainSessionKey,
|
||||||
parseAgentSessionKey,
|
parseAgentSessionKey,
|
||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
} from "./session-key.ts";
|
} from "./session-key.ts";
|
||||||
|
import { loadLocalAssistantIdentity } from "./storage.ts";
|
||||||
import { normalizeOptionalString } from "./string-coerce.ts";
|
import { normalizeOptionalString } from "./string-coerce.ts";
|
||||||
import { isRenderableControlUiAvatarUrl } from "./views/agents-utils.ts";
|
import { isRenderableControlUiAvatarUrl } from "./views/agents-utils.ts";
|
||||||
import { agentLogoUrl } from "./views/agents-utils.ts";
|
import { agentLogoUrl } from "./views/agents-utils.ts";
|
||||||
@@ -637,28 +638,44 @@ export function renderApp(state: AppViewState) {
|
|||||||
const navCollapsed = state.settings.navCollapsed && !navDrawerOpen;
|
const navCollapsed = state.settings.navCollapsed && !navDrawerOpen;
|
||||||
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
||||||
const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls;
|
const showToolCalls = state.onboarding ? true : state.settings.chatShowToolCalls;
|
||||||
|
const localAssistantAvatarOverride =
|
||||||
|
normalizeOptionalString(loadLocalAssistantIdentity().avatar) ?? null;
|
||||||
const assistantAvatarUrl = resolveAssistantAvatarUrl(state);
|
const assistantAvatarUrl = resolveAssistantAvatarUrl(state);
|
||||||
const chatAssistantAvatarStatus = state.chatAvatarStatus ?? state.assistantAvatarStatus ?? null;
|
const chatAssistantAvatarStatus = localAssistantAvatarOverride
|
||||||
const chatAssistantAvatarReason = state.chatAvatarReason ?? state.assistantAvatarReason ?? null;
|
? "data"
|
||||||
|
: (state.chatAvatarStatus ?? state.assistantAvatarStatus ?? null);
|
||||||
|
const chatAssistantAvatarReason = localAssistantAvatarOverride
|
||||||
|
? null
|
||||||
|
: (state.chatAvatarReason ?? state.assistantAvatarReason ?? null);
|
||||||
const chatAssistantAvatarMissing =
|
const chatAssistantAvatarMissing =
|
||||||
chatAssistantAvatarStatus === "none" && chatAssistantAvatarReason === "missing";
|
chatAssistantAvatarStatus === "none" && chatAssistantAvatarReason === "missing";
|
||||||
const effectiveAssistantAvatar = chatAssistantAvatarMissing ? null : state.assistantAvatar;
|
const effectiveAssistantAvatar =
|
||||||
|
localAssistantAvatarOverride ?? (chatAssistantAvatarMissing ? null : state.assistantAvatar);
|
||||||
const chatAvatarUrl =
|
const chatAvatarUrl =
|
||||||
state.chatAvatarUrl ?? (chatAssistantAvatarMissing ? null : (assistantAvatarUrl ?? null));
|
localAssistantAvatarOverride ??
|
||||||
const configAssistantAvatarStatus = state.assistantAvatarStatus ?? state.chatAvatarStatus ?? null;
|
state.chatAvatarUrl ??
|
||||||
const configAssistantAvatarReason = state.assistantAvatarReason ?? state.chatAvatarReason ?? null;
|
(chatAssistantAvatarMissing ? null : (assistantAvatarUrl ?? null));
|
||||||
const configAssistantAvatarSource = state.assistantAvatarSource ?? state.chatAvatarSource ?? null;
|
const configAssistantAvatarStatus = localAssistantAvatarOverride
|
||||||
|
? "data"
|
||||||
|
: (state.assistantAvatarStatus ?? state.chatAvatarStatus ?? null);
|
||||||
|
const configAssistantAvatarReason = localAssistantAvatarOverride
|
||||||
|
? null
|
||||||
|
: (state.assistantAvatarReason ?? state.chatAvatarReason ?? null);
|
||||||
|
const configAssistantAvatarSource =
|
||||||
|
localAssistantAvatarOverride ?? state.assistantAvatarSource ?? state.chatAvatarSource ?? null;
|
||||||
const configAssistantAvatarMissing =
|
const configAssistantAvatarMissing =
|
||||||
configAssistantAvatarStatus === "none" && configAssistantAvatarReason === "missing";
|
configAssistantAvatarStatus === "none" && configAssistantAvatarReason === "missing";
|
||||||
const configAssistantAvatar =
|
const configAssistantAvatar =
|
||||||
configAssistantAvatarMissing || configAssistantAvatarStatus === "local"
|
localAssistantAvatarOverride ??
|
||||||
|
(configAssistantAvatarMissing || configAssistantAvatarStatus === "local"
|
||||||
? null
|
? null
|
||||||
: state.assistantAvatar;
|
: state.assistantAvatar);
|
||||||
const configAssistantAvatarUrl =
|
const configAssistantAvatarUrl =
|
||||||
configAssistantAvatarStatus === "local" && state.assistantAgentId
|
localAssistantAvatarOverride ??
|
||||||
|
(configAssistantAvatarStatus === "local" && state.assistantAgentId
|
||||||
? buildAssistantAvatarRoute(state.basePath, state.assistantAgentId)
|
? buildAssistantAvatarRoute(state.basePath, state.assistantAgentId)
|
||||||
: (state.chatAvatarUrl ??
|
: (state.chatAvatarUrl ??
|
||||||
(configAssistantAvatarMissing ? null : (assistantAvatarUrl ?? null)));
|
(configAssistantAvatarMissing ? null : (assistantAvatarUrl ?? null))));
|
||||||
const configValue =
|
const configValue =
|
||||||
state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
|
state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
|
||||||
const configuredDreaming = resolveConfiguredDreaming(configValue);
|
const configuredDreaming = resolveConfiguredDreaming(configValue);
|
||||||
@@ -956,7 +973,8 @@ export function renderApp(state: AppViewState) {
|
|||||||
// Quick Settings mode — opinionated card layout
|
// Quick Settings mode — opinionated card layout
|
||||||
if (state.configSettingsMode === "quick") {
|
if (state.configSettingsMode === "quick") {
|
||||||
const configObj = state.configForm ?? state.configSnapshot?.config ?? {};
|
const configObj = state.configForm ?? state.configSnapshot?.config ?? {};
|
||||||
const assistantAvatarOverride = resolveAssistantAvatarOverride(configObj);
|
const assistantAvatarOverride =
|
||||||
|
localAssistantAvatarOverride ?? resolveAssistantAvatarOverride(configObj);
|
||||||
const agentsDefaults = ((configObj.agents as Record<string, unknown> | undefined)
|
const agentsDefaults = ((configObj.agents as Record<string, unknown> | undefined)
|
||||||
?.defaults ?? {}) as Record<string, unknown>;
|
?.defaults ?? {}) as Record<string, unknown>;
|
||||||
const activeSession = resolveQuickSettingsSessionRow(state);
|
const activeSession = resolveQuickSettingsSessionRow(state);
|
||||||
@@ -1069,6 +1087,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
state.chatAvatarStatus = null;
|
state.chatAvatarStatus = null;
|
||||||
state.chatAvatarReason = null;
|
state.chatAvatarReason = null;
|
||||||
state.assistantAvatarUploadError = null;
|
state.assistantAvatarUploadError = null;
|
||||||
|
void state.loadAssistantIdentity?.().finally(() => requestHostUpdate?.());
|
||||||
requestHostUpdate?.();
|
requestHostUpdate?.();
|
||||||
},
|
},
|
||||||
basePath: state.basePath ?? "",
|
basePath: state.basePath ?? "",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ describe("setAssistantAvatarOverride", () => {
|
|||||||
|
|
||||||
setAssistantAvatarOverride(state, null);
|
setAssistantAvatarOverride(state, null);
|
||||||
|
|
||||||
|
expect(state.assistantAvatar).toBeNull();
|
||||||
expect(state.assistantAvatarSource).toBeNull();
|
expect(state.assistantAvatarSource).toBeNull();
|
||||||
expect(state.assistantAvatarStatus).toBeNull();
|
expect(state.assistantAvatarStatus).toBeNull();
|
||||||
expect(state.assistantAvatarReason).toBeNull();
|
expect(state.assistantAvatarReason).toBeNull();
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export function setAssistantAvatarOverride(
|
|||||||
state.assistantAvatarStatus = "data";
|
state.assistantAvatarStatus = "data";
|
||||||
state.assistantAvatarReason = null;
|
state.assistantAvatarReason = null;
|
||||||
} else {
|
} else {
|
||||||
|
state.assistantAvatar = null;
|
||||||
state.assistantAvatarSource = null;
|
state.assistantAvatarSource = null;
|
||||||
state.assistantAvatarStatus = null;
|
state.assistantAvatarStatus = null;
|
||||||
state.assistantAvatarReason = null;
|
state.assistantAvatarReason = null;
|
||||||
|
|||||||
@@ -253,6 +253,42 @@ describe("renderQuickSettings", () => {
|
|||||||
expect(onAssistantAvatarClearOverride).toHaveBeenCalledTimes(1);
|
expect(onAssistantAvatarClearOverride).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets the browser-local assistant avatar override stale missing IDENTITY.md metadata", () => {
|
||||||
|
const dataUrl = "data:image/png;base64,bG9jYWwtYXNzaXN0YW50";
|
||||||
|
const container = document.createElement("div");
|
||||||
|
|
||||||
|
render(
|
||||||
|
renderQuickSettings(
|
||||||
|
createProps({
|
||||||
|
assistantName: "Nova",
|
||||||
|
assistantAvatar: "/avatar/main",
|
||||||
|
assistantAvatarUrl: null,
|
||||||
|
assistantAvatarSource: "avatars/missing.png",
|
||||||
|
assistantAvatarStatus: "none",
|
||||||
|
assistantAvatarReason: "missing",
|
||||||
|
assistantAvatarOverride: dataUrl,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container.querySelector(".qs-assistant-avatar")?.getAttribute("src")).toBe(dataUrl);
|
||||||
|
expect(container.querySelector(".qs-identity-card__source")?.textContent).toContain(
|
||||||
|
"UI override",
|
||||||
|
);
|
||||||
|
expect(container.querySelector(".qs-identity-card__issue")).toBeNull();
|
||||||
|
expect(
|
||||||
|
Array.from(container.querySelectorAll("label.btn")).some(
|
||||||
|
(label) => label.textContent?.trim() === "Replace image",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
Array.from(container.querySelectorAll("button")).some(
|
||||||
|
(button) => button.textContent?.trim() === "Clear override",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects oversized avatar uploads before reading them", () => {
|
it("rejects oversized avatar uploads before reading them", () => {
|
||||||
const onUserAvatarChange = vi.fn();
|
const onUserAvatarChange = vi.fn();
|
||||||
const fileReader = vi.fn();
|
const fileReader = vi.fn();
|
||||||
|
|||||||
@@ -171,6 +171,15 @@ function renderLocalUserAvatarPreview(avatar: string | null | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveAssistantPreviewAvatarUrl(props: QuickSettingsProps): string | null {
|
function resolveAssistantPreviewAvatarUrl(props: QuickSettingsProps): string | null {
|
||||||
|
const override = normalizeOptionalString(props.assistantAvatarOverride);
|
||||||
|
if (override) {
|
||||||
|
return resolveChatAvatarRenderUrl(override, {
|
||||||
|
identity: {
|
||||||
|
avatar: override,
|
||||||
|
avatarUrl: override,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (props.assistantAvatarStatus === "none" && props.assistantAvatarReason === "missing") {
|
if (props.assistantAvatarStatus === "none" && props.assistantAvatarReason === "missing") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -198,7 +207,11 @@ function formatAssistantAvatarIssue(
|
|||||||
status: QuickSettingsProps["assistantAvatarStatus"],
|
status: QuickSettingsProps["assistantAvatarStatus"],
|
||||||
reason: string | null | undefined,
|
reason: string | null | undefined,
|
||||||
_rendered: boolean,
|
_rendered: boolean,
|
||||||
|
hasOverride = false,
|
||||||
): string | null {
|
): string | null {
|
||||||
|
if (hasOverride) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (status === "remote") {
|
if (status === "remote") {
|
||||||
return "Remote URLs are blocked by Control UI image policy";
|
return "Remote URLs are blocked by Control UI image policy";
|
||||||
}
|
}
|
||||||
@@ -219,11 +232,14 @@ function formatAssistantAvatarIssue(
|
|||||||
|
|
||||||
function renderAssistantAvatarPreview(props: QuickSettingsProps) {
|
function renderAssistantAvatarPreview(props: QuickSettingsProps) {
|
||||||
const assistantName = normalizeOptionalString(props.assistantName) ?? "Assistant";
|
const assistantName = normalizeOptionalString(props.assistantName) ?? "Assistant";
|
||||||
|
const assistantAvatarOverride = normalizeOptionalString(props.assistantAvatarOverride);
|
||||||
const assistantAvatarUrl = resolveAssistantPreviewAvatarUrl(props);
|
const assistantAvatarUrl = resolveAssistantPreviewAvatarUrl(props);
|
||||||
if (assistantAvatarUrl) {
|
if (assistantAvatarUrl) {
|
||||||
return html`<img class="qs-assistant-avatar" src=${assistantAvatarUrl} alt=${assistantName} />`;
|
return html`<img class="qs-assistant-avatar" src=${assistantAvatarUrl} alt=${assistantName} />`;
|
||||||
}
|
}
|
||||||
const assistantAvatarText = resolveAssistantTextAvatar(props.assistantAvatar);
|
const assistantAvatarText = resolveAssistantTextAvatar(
|
||||||
|
assistantAvatarOverride ?? props.assistantAvatar,
|
||||||
|
);
|
||||||
if (assistantAvatarText) {
|
if (assistantAvatarText) {
|
||||||
return html`<div
|
return html`<div
|
||||||
class="qs-assistant-avatar qs-assistant-avatar--text"
|
class="qs-assistant-avatar qs-assistant-avatar--text"
|
||||||
@@ -616,15 +632,19 @@ function renderPersonalCard(props: QuickSettingsProps) {
|
|||||||
const assistantName = normalizeOptionalString(props.assistantName) ?? "Assistant";
|
const assistantName = normalizeOptionalString(props.assistantName) ?? "Assistant";
|
||||||
const assistantAvatarUrl = resolveAssistantPreviewAvatarUrl(props);
|
const assistantAvatarUrl = resolveAssistantPreviewAvatarUrl(props);
|
||||||
const assistantAvatarRendered = Boolean(
|
const assistantAvatarRendered = Boolean(
|
||||||
assistantAvatarUrl || resolveAssistantTextAvatar(props.assistantAvatar),
|
assistantAvatarUrl ||
|
||||||
|
resolveAssistantTextAvatar(props.assistantAvatarOverride ?? props.assistantAvatar),
|
||||||
|
);
|
||||||
|
const assistantAvatarOverride = normalizeOptionalString(props.assistantAvatarOverride);
|
||||||
|
const assistantAvatarSource = formatAssistantAvatarSource(
|
||||||
|
assistantAvatarOverride ?? props.assistantAvatarSource,
|
||||||
);
|
);
|
||||||
const assistantAvatarSource = formatAssistantAvatarSource(props.assistantAvatarSource);
|
|
||||||
const assistantAvatarIssue = formatAssistantAvatarIssue(
|
const assistantAvatarIssue = formatAssistantAvatarIssue(
|
||||||
props.assistantAvatarStatus ?? null,
|
props.assistantAvatarStatus ?? null,
|
||||||
props.assistantAvatarReason,
|
props.assistantAvatarReason,
|
||||||
assistantAvatarRendered,
|
assistantAvatarRendered,
|
||||||
|
Boolean(assistantAvatarOverride),
|
||||||
);
|
);
|
||||||
const assistantAvatarOverride = normalizeOptionalString(props.assistantAvatarOverride);
|
|
||||||
const assistantAvatarSourceLabel = assistantAvatarOverride ? "UI override" : "IDENTITY.md";
|
const assistantAvatarSourceLabel = assistantAvatarOverride ? "UI override" : "IDENTITY.md";
|
||||||
const canOverrideAssistantAvatar = Boolean(props.onAssistantAvatarOverrideChange);
|
const canOverrideAssistantAvatar = Boolean(props.onAssistantAvatarOverrideChange);
|
||||||
const assistantAvatarSubtitle = assistantAvatarOverride
|
const assistantAvatarSubtitle = assistantAvatarOverride
|
||||||
|
|||||||
Reference in New Issue
Block a user