* fix(app): validate persisted client state Persisted replica data could satisfy TypeScript through an assertion while missing fields required by renderers. Treat every AsyncStorage value as untrusted input, clear values that fail strict schemas, and version the replica DTO so incompatible caches cannot hydrate. * fix(app): preserve validated legacy state * fix(app): preserve validated preferences Preserve supported legacy location fields and every shipped theme while keeping unknown persisted fields rejected. Align browser test seeds with the current preference shape so strict validation exercises the intended provider. * fix(app): migrate legacy agent preferences Released clients stored host/location fields and per-provider thinkingOptionId together. Migrate those exact strict bytes into current preferences without admitting unknown fields. * fix(app): preserve drafts with legacy reviews Accept the exact strict shape released for workspace review attachments so migration can discard that obsolete attachment without clearing every persisted draft.
93 lines
2.9 KiB
TypeScript
93 lines
2.9 KiB
TypeScript
import { IDENTITY_COLOR_NAMES, type IdentityColorName } from "@/styles/identity-colors";
|
|
import type { HostProfile } from "@/types/host-connection";
|
|
import { z } from "zod";
|
|
|
|
export type HostColor = "none" | IdentityColorName;
|
|
|
|
export const HOST_COLORS: readonly HostColor[] = ["none", ...IDENTITY_COLOR_NAMES];
|
|
|
|
export type HostBadgeDisplay = "name" | "icon" | "hidden";
|
|
|
|
export const HOST_BADGE_DISPLAYS: readonly HostBadgeDisplay[] = ["name", "icon", "hidden"];
|
|
|
|
/**
|
|
* Per-device host presentation. `badgeDisplay` is null while the user has not chosen,
|
|
* because the default differs by host (local hides, remote shows) and local-ness is only
|
|
* knowable from a desktop-only async query — never at parse time.
|
|
*/
|
|
export interface HostAppearance {
|
|
color: HostColor;
|
|
badgeDisplay: HostBadgeDisplay | null;
|
|
}
|
|
|
|
export const HostAppearanceSchema: z.ZodType<HostAppearance> = z.strictObject({
|
|
color: z.enum(["none", ...IDENTITY_COLOR_NAMES]),
|
|
badgeDisplay: z.enum(["name", "icon", "hidden"]).nullable(),
|
|
});
|
|
|
|
export function defaultHostAppearance(): HostAppearance {
|
|
return { color: "none", badgeDisplay: null };
|
|
}
|
|
|
|
export function normalizeStoredHostAppearance(value: unknown): HostAppearance {
|
|
const result = HostAppearanceSchema.safeParse(value);
|
|
return result.success ? result.data : defaultHostAppearance();
|
|
}
|
|
|
|
export function resolveHostBadgeDisplay(input: {
|
|
appearance: HostAppearance;
|
|
isLocalHost: boolean;
|
|
localHostResolutionPending?: boolean;
|
|
}): HostBadgeDisplay | null {
|
|
if (input.appearance.badgeDisplay) {
|
|
return input.appearance.badgeDisplay;
|
|
}
|
|
if (input.localHostResolutionPending) {
|
|
return null;
|
|
}
|
|
return input.isLocalHost ? "hidden" : "name";
|
|
}
|
|
|
|
export interface HostBadgeModel {
|
|
serverId: string;
|
|
label: string;
|
|
color: HostColor;
|
|
showLabel: boolean;
|
|
}
|
|
|
|
export type HostAppearanceSource = Pick<HostProfile, "serverId" | "label" | "appearance">;
|
|
|
|
/**
|
|
* The sidebar's whole host-badge decision, resolved once per host list. Rows look their
|
|
* badge up by serverId and render whatever they find; a host that should show no badge is
|
|
* simply absent from the map.
|
|
*/
|
|
export function selectHostBadges(input: {
|
|
hosts: readonly HostAppearanceSource[];
|
|
localServerId: string | null;
|
|
localHostResolutionPending?: boolean;
|
|
enabled: boolean;
|
|
}): ReadonlyMap<string, HostBadgeModel> {
|
|
const badges = new Map<string, HostBadgeModel>();
|
|
if (!input.enabled) {
|
|
return badges;
|
|
}
|
|
for (const host of input.hosts) {
|
|
const display = resolveHostBadgeDisplay({
|
|
appearance: host.appearance,
|
|
isLocalHost: host.serverId === input.localServerId,
|
|
localHostResolutionPending: input.localHostResolutionPending,
|
|
});
|
|
if (display === null || display === "hidden") {
|
|
continue;
|
|
}
|
|
badges.set(host.serverId, {
|
|
serverId: host.serverId,
|
|
label: host.label.trim() || host.serverId,
|
|
color: host.appearance.color,
|
|
showLabel: display === "name",
|
|
});
|
|
}
|
|
return badges;
|
|
}
|