fix(control-ui): persist assistant avatar override locally#71639
Conversation
Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]>
…istant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]>
… via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping.
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Unrestricted assistant avatar URLs allow SVG data URLs and arbitrary same-origin paths (XSS/CSRF risk)
DescriptionThe UI normalizes and preserves assistant avatar values that match
This creates risk that a crafted avatar value can:
Vulnerable code paths: // ui/src/ui/assistant-identity.ts
const RENDERABLE_AVATAR_URL_RE = /^(data:image\/|\/(?!\/))/i;
...
if (RENDERABLE_AVATAR_URL_RE.test(trimmed)) {
return trimmed;
}
// ui/src/ui/chat/chat-avatar.ts
return html`<img class="chat-avatar ${className}" src="${assistantAvatar}" alt="${assistantName}" />`;RecommendationConstrain assistant avatar URLs to a safe subset and reuse the existing URL-safety logic.
Example hardening: // ui/src/ui/assistant-identity.ts
const ALLOWED_DATA_IMAGE_MIME = new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"]);
function isAllowedDataImage(url: string): boolean {
const trimmed = url.trim();
if (!trimmed.toLowerCase().startsWith("data:")) return false;
const comma = trimmed.indexOf(",");
if (comma < 5) return false;
const meta = trimmed.slice(5, comma);
const mime = meta.split(";")[0].toLowerCase();
return ALLOWED_DATA_IMAGE_MIME.has(mime);
}
function isAllowedSameOriginAvatarPath(p: string): boolean {
return p.startsWith("/avatar/");
}
function normalizeAssistantAvatar(value: string | null | undefined): string | null {
const trimmed = coerceIdentityValue(value ?? undefined, MAX_ASSISTANT_IMAGE_AVATAR);
if (!trimmed) return null;
if (trimmed.startsWith("data:")) return isAllowedDataImage(trimmed) ? trimmed : null;
if (trimmed.startsWith("/")) return isAllowedSameOriginAvatarPath(trimmed) ? trimmed : null;
...
}This prevents SVG-based payloads and avoids turning the avatar field into a general-purpose same-origin request gadget. 2. 🟡 Unbounded assistant avatar persisted in localStorage can cause persistent client-side DoS
DescriptionThe Control UI persists an assistant avatar override directly to
Vulnerable code: const parsed = JSON.parse(raw) as Partial<LocalAssistantIdentity>;
return { avatar: typeof parsed.avatar === "string" ? parsed.avatar : null };
...
storage?.setItem(LOCAL_ASSISTANT_IDENTITY_KEY, JSON.stringify({ avatar: next.avatar }));RecommendationApply strict validation and size limits before saving and after loading local assistant identity, similar to Suggested approach:
Example: import { normalizeAssistantIdentity } from "./assistant-identity";
export function loadLocalAssistantIdentity(): LocalAssistantIdentity {
const storage = getSafeLocalStorage();
try {
const raw = storage?.getItem(LOCAL_ASSISTANT_IDENTITY_KEY);
if (!raw) return { avatar: null };
// Hard cap raw size to avoid parsing huge blobs repeatedly
if (raw.length > 2_100_000) {
storage?.removeItem(LOCAL_ASSISTANT_IDENTITY_KEY);
return { avatar: null };
}
const parsed = JSON.parse(raw) as Partial<LocalAssistantIdentity>;
const normalized = normalizeAssistantIdentity({ avatar: parsed.avatar ?? null });
return { avatar: normalized.avatar ?? null };
} catch {
return { avatar: null };
}
}
export function saveLocalAssistantIdentity(next: LocalAssistantIdentity) {
const storage = getSafeLocalStorage();
try {
const normalized = normalizeAssistantIdentity({ avatar: next.avatar ?? null });
if (!normalized.avatar) {
storage?.removeItem(LOCAL_ASSISTANT_IDENTITY_KEY);
return;
}
storage?.setItem(
LOCAL_ASSISTANT_IDENTITY_KEY,
JSON.stringify({ avatar: normalized.avatar })
);
} catch {
// best-effort
}
}Analyzed PR: #71639 at commit Last updated on: 2026-04-25T15:45:39Z |
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
Greptile SummaryThis PR fixes the 200-character config validation error for assistant avatar uploads by persisting overrides to
Confidence Score: 3/5Two P1 regressions make this unsafe to merge as-is: the 'Clear override' button is unreachable and the avatar is not visually cleared after a clear action. Two independent P1 bugs: stale assistantAvatar on clear, and assistantAvatarOverride always null for new localStorage-based uploads. Together they mean the clear flow is both visually broken and unreachable from the UI. ui/src/ui/controllers/assistant-identity.ts (missing state.assistantAvatar = null in clear branch) and ui/src/ui/app-render.ts (resolveAssistantAvatarOverride needs to check localStorage).
|
| } else { | ||
| state.assistantAvatarSource = null; | ||
| state.assistantAvatarStatus = null; | ||
| state.assistantAvatarReason = null; | ||
| } |
There was a problem hiding this comment.
state.assistantAvatar not cleared on override clear
When avatar is null, the else branch clears assistantAvatarSource, assistantAvatarStatus, and assistantAvatarReason but leaves state.assistantAvatar untouched. After clearing, configAssistantAvatar in app-render.ts is computed as state.assistantAvatar (when status is not "local" and not missing), so the old data URL is still served to the Quick Settings panel and the effectiveAssistantAvatar used for the chat header — the avatar visually persists until a page reload.
| } else { | |
| state.assistantAvatarSource = null; | |
| state.assistantAvatarStatus = null; | |
| state.assistantAvatarReason = null; | |
| } | |
| } else { | |
| state.assistantAvatar = null; | |
| state.assistantAvatarSource = null; | |
| state.assistantAvatarStatus = null; | |
| state.assistantAvatarReason = null; | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/controllers/assistant-identity.ts
Line: 68-72
Comment:
**`state.assistantAvatar` not cleared on override clear**
When `avatar` is `null`, the `else` branch clears `assistantAvatarSource`, `assistantAvatarStatus`, and `assistantAvatarReason` but leaves `state.assistantAvatar` untouched. After clearing, `configAssistantAvatar` in `app-render.ts` is computed as `state.assistantAvatar` (when status is not `"local"` and not missing), so the old data URL is still served to the Quick Settings panel and the `effectiveAssistantAvatar` used for the chat header — the avatar visually persists until a page reload.
```suggestion
} else {
state.assistantAvatar = null;
state.assistantAvatarSource = null;
state.assistantAvatarStatus = null;
state.assistantAvatarReason = null;
}
```
How can I resolve this? If you propose a fix, please make it concise.Val Alexander's c65aa1d (#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's c65aa1d (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's 9ea8cbb (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's 8862644 (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's 8f694b0 (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's df4c69a (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's ceaa14c (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
…71639) * fix(control-ui): rebalance quick settings into stable 3-col bento Pair Appearance with Automations and let Channels stand alone in the middle column so all three top-row columns reach similar heights. Promote Personal to a full-width row with a horizontal body (identity tiles | emoji + actions) so the avatar block stops fighting for half-width space. Drops the unused .qs-stack--wide hook. Co-Authored-By: Claude Opus 4.7 <[email protected]> * refactor(control-ui): rebalance Personal card with symmetric User↔Assistant identity pair Restructure Personal card layout to present User and Assistant as 2 balanced identity cards instead of separate User tile + form controls. Mirrors the visual hierarchy and UI pattern across both identities. Changes: - Move User avatar text input into User identity card's .__repair section (mirroring Assistant's structure) - Inline "Choose image" and "Clear avatar" buttons as flex-wrapped action group - Remove .qs-personal-body and .qs-personal-form wrapper divs - Update Personal card's .qs-identity-grid to 2-column layout with balanced spacing - Responsive collapse to 1-column at ≤760px Tests: - config-quick.test.ts updated to expect 2 stacks (no longer wrapping Personal in form) - config-quick.test.ts validates identity card layout now has symmetric User↔Assistant structure - All 10 quick settings view tests passing - All 20 schema regression tests passing Co-Authored-By: Claude Haiku 4.5 <[email protected]> * chore: ignore .vmux worktree paths * fix(control-ui): persist assistant avatar override locally instead of via gateway config Mirrors the user-avatar pattern: assistant avatar uploads now go to localStorage and overlay the gateway-resolved identity at bootstrap and on agent.identity.get refreshes. Sidesteps the ui.assistant.avatar zod cap that rejected uploaded data URLs as 'Too big: expected string to have <=200 characters', removes one config.patch RPC from the avatar path, and collapses the upload handler from a 44-line async/loadConfig dance into a plain synchronous setter. Also lifts the gateway-side ui.assistant.avatar schema cap from 200 to 2,000,000 to match the user-avatar size budget for non-UI clients writing the field directly, and adds a content-aware text/image normalizer in ui/src/ui/assistant-identity.ts so short-text avatars stay short while data URLs survive round-tripping. --------- Co-authored-by: Claude Opus 4.7 <[email protected]>
Val Alexander's 239a436 (openclaw#71639) changed assistant avatar uploads from gateway config persistence to localStorage, mirroring the existing user-avatar pattern. CHANGELOG covered it but docs/web/control-ui.md 'Personal identity (browser-local)' section only documented the user identity. Add a paragraph noting the assistant avatar override follows the same browser-local pattern, while keeping the ui.assistant.avatar config field reachable for non-UI clients writing the field directly.
Summary
localStorageinstead of routing throughconfig.patch→ui.assistant.avatar. The Quick Settings uploader stops failing withinvalid config: ui.assistant.avatar: Too big: expected string to have <=200 characterswhen the data URL exceeds the schema cap.control-ui-bootstrap.ts) andloadAssistantIdentityoverlay the local override on top of the gateway-resolved identity so the override always wins — same precedence rule the chat already uses for the user avatar.ui.assistant.avatarzod cap from200→2_000_000to match the user-avatar size budget for non-Control-UI clients (agents.list identity etc.) writing the field directly. UI assistant-identity normalizer split into a 64-char text branch and a 2 MB image-URL/path branch, mirroringuser-identity.ts.app-render.tsfrom a 44-line async/loadConfig/loadAssistantIdentitydance into a synchronous setter..gitignore: ignore.vmux*worktree paths.### FixeswithThanks @BunsDevattribution.Security review
<input type=file>→FileReader.readAsDataURL→<img src=data:…>). SVG-as-image is sandboxed by browsers; no script execution path. No new vector.try/catchcoversQuotaExceededError. Same as user avatar.config.patchpath writing to gateway config.Test plan
pnpm test:changed(4,737 tests across gateway / runtime-config / ui lanes — green)pnpm tsgo:coreandpnpm check:test-typesclean on touched filespnpm lint:corecleanpnpm buildclean (UI bundle + gateway artifacts regenerated)ui/src/ui/assistant-identity.test.ts— data-URL passthrough, path passthrough, short-text accept, long-text reject, newline rejectui/src/ui/controllers/assistant-identity.test.ts— local persistence + clear-overridesrc/gateway/assistant-identity.test.ts— 50 KB+ data URL passthrough throughcoerceIdentityValue🤖 Generated with Claude Code