mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
* fix(control-ui): restore userAvatar wiring in Quick Settings (#107292) Between 2026.6.11 and 2026.7.1 the Quick Settings parent renderer stopped passing userAvatar / onUserAvatarChange down to the avatar editor, while the editor still calls onUserAvatarChange?.(...). As a result, choosing, clearing, or entering an emoji/text avatar under Settings \u2192 Simple silently no-ops and never persists. Restore the wiring: ConfigPage now reads the browser-local user identity via loadLocalUserIdentity(), tracks the avatar on a @state field, and persists changes through applyLocalUserIdentity() (added to ui/src/app/settings.ts). No behavior change vs 2026.6.11. * chore: drop changelog edit (release-generation owns CHANGELOG) * ci: pick up periphery workflow fix * ci: pick up pinned xcodegen installer * chore: restore merge-base CHANGELOG (release-owned file) --------- Co-authored-by: SymbolStar <SymbolStar@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
loadSettings,
|
||||
persistSessionToken,
|
||||
resolvePageGatewaySettings,
|
||||
saveLocalUserIdentity,
|
||||
saveSettings,
|
||||
type UiSettings,
|
||||
} from "./settings.ts";
|
||||
@@ -908,6 +909,20 @@ describe("loadSettings default gateway URL derivation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists and clears normalized local user identity", () => {
|
||||
expect(saveLocalUserIdentity({ name: " Buns ", avatar: " 🦞 " })).toEqual({
|
||||
name: "Buns",
|
||||
avatar: "🦞",
|
||||
});
|
||||
expect(loadLocalUserIdentity()).toEqual({ name: "Buns", avatar: "🦞" });
|
||||
|
||||
expect(saveLocalUserIdentity({ name: null, avatar: null })).toEqual({
|
||||
name: null,
|
||||
avatar: null,
|
||||
});
|
||||
expect(localStorage.getItem("openclaw.control.user.v1")).toBeNull();
|
||||
});
|
||||
|
||||
it("normalizes invalid local user identity values on load", () => {
|
||||
localStorage.setItem(
|
||||
"openclaw.control.user.v1",
|
||||
|
||||
@@ -458,6 +458,22 @@ export function loadLocalUserIdentity(): LocalUserIdentity {
|
||||
}
|
||||
}
|
||||
|
||||
export function saveLocalUserIdentity(next: LocalUserIdentity): LocalUserIdentity {
|
||||
const storage = getSafeLocalStorage();
|
||||
const normalized = normalizeLocalUserIdentity(next);
|
||||
try {
|
||||
if (normalized.name === null && normalized.avatar === null) {
|
||||
storage?.removeItem(LOCAL_USER_IDENTITY_KEY);
|
||||
} else {
|
||||
storage?.setItem(LOCAL_USER_IDENTITY_KEY, JSON.stringify(normalized));
|
||||
}
|
||||
} catch {
|
||||
// best-effort — quota exceeded or security restrictions should not
|
||||
// prevent in-memory identity updates from being applied
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function persistSettings(next: UiSettings, options: { selectGateway?: boolean } = {}) {
|
||||
persistSessionToken(next.gatewayUrl, next.token);
|
||||
const storage = getSafeLocalStorage();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, type ReactiveController } from "lit";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SystemInfoResult } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type {
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
ApplicationGateway,
|
||||
ApplicationGatewaySnapshot,
|
||||
} from "../../app/context.ts";
|
||||
import { loadLocalUserIdentity } from "../../app/settings.ts";
|
||||
import { createStorageMock } from "../../test-helpers/storage.ts";
|
||||
import { ConfigPage, configSelectionFromSearch, supportsSystemInfo } from "./config-page.ts";
|
||||
import type { ConfigViewState } from "./view.ts";
|
||||
|
||||
@@ -20,9 +22,36 @@ function deferred<T>() {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
let localStorageMock: Storage;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock = createStorageMock();
|
||||
vi.stubGlobal("localStorage", localStorageMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("ConfigPage local user identity", () => {
|
||||
it("persists avatar selections while preserving the local display name", () => {
|
||||
localStorageMock.setItem(
|
||||
"openclaw.control.user.v1",
|
||||
JSON.stringify({ name: "Buns", avatar: "old" }),
|
||||
);
|
||||
const page = new ConfigPage();
|
||||
const state = page as unknown as {
|
||||
userAvatar: string | null;
|
||||
setLocalUserAvatar: (avatar: string | null) => void;
|
||||
};
|
||||
|
||||
state.setLocalUserAvatar("🦞");
|
||||
|
||||
expect(state.userAvatar).toBe("🦞");
|
||||
expect(loadLocalUserIdentity()).toEqual({ name: "Buns", avatar: "🦞" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("configSelectionFromSearch", () => {
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
import { importCustomThemeFromUrl } from "../../app/custom-theme.ts";
|
||||
import { hasOperatorAdminAccess } from "../../app/operator-access.ts";
|
||||
import {
|
||||
loadLocalUserIdentity,
|
||||
loadSettings,
|
||||
normalizeCatalogOpenTarget,
|
||||
normalizeTextScale,
|
||||
normalizeChatFollowUpMode,
|
||||
normalizeChatSendShortcut,
|
||||
patchSettings,
|
||||
saveLocalUserIdentity,
|
||||
type UiSettings,
|
||||
} from "../../app/settings.ts";
|
||||
import { startThemeTransition } from "../../app/theme-transition.ts";
|
||||
@@ -240,6 +242,7 @@ export class ConfigPage extends OpenClawLightDomElement {
|
||||
@property({ attribute: false }) routeData: ConfigRouteData | null = null;
|
||||
|
||||
@state() private settings = loadSettings();
|
||||
@state() private userAvatar: string | null = loadLocalUserIdentity().avatar;
|
||||
@state() private settingsMode: "quick" | "advanced" = "quick";
|
||||
@state() private systemInfo: SystemInfoResult | null = null;
|
||||
@state() private systemInfoUnavailable = false;
|
||||
@@ -321,9 +324,18 @@ export class ConfigPage extends OpenClawLightDomElement {
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.settings = loadSettings();
|
||||
this.userAvatar = loadLocalUserIdentity().avatar;
|
||||
this.syncRouteData();
|
||||
}
|
||||
|
||||
private setLocalUserAvatar(avatar: string | null) {
|
||||
const identity = saveLocalUserIdentity({
|
||||
...loadLocalUserIdentity(),
|
||||
avatar,
|
||||
});
|
||||
this.userAvatar = identity.avatar;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
this.systemInfoPolling.stop();
|
||||
this.invalidateSystemInfoRequest();
|
||||
@@ -927,6 +939,8 @@ export class ConfigPage extends OpenClawLightDomElement {
|
||||
assistantAvatarStatus: appConfig.assistantIdentity.avatarStatus,
|
||||
assistantAvatarReason: appConfig.assistantIdentity.avatarReason,
|
||||
assistantAvatarOverride: null,
|
||||
userAvatar: this.userAvatar,
|
||||
onUserAvatarChange: (avatar) => this.setLocalUserAvatar(avatar),
|
||||
basePath: this.context.basePath,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user