Files
openclaw/ui/src/app/assistant-identity.ts
T
Peter SteinbergerandGitHub 096cb91175 feat(tooling): enforce noUncheckedIndexedAccess in core and ui lanes (NUIA phase 3c) (#104981)
* fix(ui): make indexed access explicit across the Control UI

Burns down all 322 ui-lane noUncheckedIndexedAccess errors: untrusted
markdown/diff/patch parsing gets miss-tolerant guards (renderer rules
degrade to empty output instead of throwing mid-render), accumulators
use canonical sparse initialization, DOM lookups stay null-guarded, and
length-checked constructions carry named invariants.

* feat(tooling): enforce noUncheckedIndexedAccess in the core and ui lanes

Flips the flag on for tsconfig.core.json and tsconfig.ui.json (covering
src, all packages including the two deferred ones, and ui) and retires
the strict-ratchet lane wholesale: config, script, projects reference,
check/CI wiring, changed-lane routing, and sync test. The assertion-ban
oxlint override stays as an independent surface.
2026-07-12 06:36:36 +01:00

95 lines
3.3 KiB
TypeScript

import type { GatewayBrowserClient } from "../api/gateway.ts";
import { normalizeAssistantIdentity, type AssistantIdentity } from "../lib/assistant-identity.ts";
import { normalizeOptionalString } from "../lib/string-coerce.ts";
import { getSafeLocalStorage } from "../local-storage.ts";
const LOCAL_ASSISTANT_IDENTITY_KEY = "openclaw.control.assistant.v1";
type LocalAssistantIdentity = { avatar: string | null; agentId?: string | null };
type PersistedLocalAssistantIdentities = {
avatars?: Record<string, unknown>;
avatar?: unknown;
agentId?: unknown;
};
function parseLocalAssistantAvatarMap(raw: string): {
avatars: Record<string, string>;
legacyAvatar: string | null;
} {
const parsed = JSON.parse(raw) as PersistedLocalAssistantIdentities;
const avatars = Object.create(null) as Record<string, string>;
if (parsed.avatars && typeof parsed.avatars === "object" && !Array.isArray(parsed.avatars)) {
for (const [agentId, avatar] of Object.entries(parsed.avatars)) {
const normalizedAgentId = normalizeOptionalString(agentId);
const normalizedAvatar = normalizeOptionalString(avatar);
if (normalizedAgentId && normalizedAvatar) {
avatars[normalizedAgentId] = normalizedAvatar;
}
}
}
const legacyAvatar = normalizeOptionalString(parsed.avatar);
const legacyAgentId = normalizeOptionalString(parsed.agentId);
if (legacyAvatar && legacyAgentId && !Object.hasOwn(avatars, legacyAgentId)) {
avatars[legacyAgentId] = legacyAvatar;
}
return { avatars, legacyAvatar: legacyAgentId ? null : (legacyAvatar ?? null) };
}
function persistLocalAssistantAvatarMap(storage: Storage | null, avatars: Record<string, string>) {
if (Object.keys(avatars).length === 0) {
storage?.removeItem(LOCAL_ASSISTANT_IDENTITY_KEY);
return;
}
storage?.setItem(LOCAL_ASSISTANT_IDENTITY_KEY, JSON.stringify({ avatars }));
}
export function loadLocalAssistantIdentity(opts?: {
agentId?: string | null;
}): LocalAssistantIdentity {
const agentId = normalizeOptionalString(opts?.agentId);
if (!agentId) {
return { avatar: null };
}
const storage = getSafeLocalStorage();
try {
const raw = storage?.getItem(LOCAL_ASSISTANT_IDENTITY_KEY);
if (!raw) {
return { avatar: null };
}
const { avatars, legacyAvatar } = parseLocalAssistantAvatarMap(raw);
if (!Object.hasOwn(avatars, agentId) && legacyAvatar) {
// Assign the old global override to the first concrete agent that loads it.
avatars[agentId] = legacyAvatar;
persistLocalAssistantAvatarMap(storage, avatars);
}
return { avatar: Object.hasOwn(avatars, agentId) ? (avatars[agentId] ?? null) : null, agentId };
} catch {
return { avatar: null };
}
}
export async function fetchAssistantIdentity(
client: GatewayBrowserClient,
sessionKey?: string,
): Promise<AssistantIdentity | null> {
const result = await client.request<Partial<AssistantIdentity>>(
"agent.identity.get",
sessionKey?.trim() ? { sessionKey: sessionKey.trim() } : {},
);
if (!result) {
return null;
}
const identity = normalizeAssistantIdentity(result);
const localAvatar = loadLocalAssistantIdentity({ agentId: identity.agentId }).avatar;
return localAvatar
? {
...identity,
avatar: localAvatar,
avatarSource: localAvatar,
avatarStatus: "data",
avatarReason: null,
}
: identity;
}