refactor: dedupe provider ui trimmed readers

This commit is contained in:
Peter Steinberger
2026-04-08 01:13:25 +01:00
parent bf03babd2b
commit e0b4f3b995
18 changed files with 238 additions and 166 deletions

View File

@@ -16,7 +16,7 @@ import { loadSessions } from "./controllers/sessions.ts";
import { icons } from "./icons.ts";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts";
import { parseAgentSessionKey } from "./session-key.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts";
import type { ThemeMode } from "./theme.ts";
import {
listThinkingLevelLabels,
@@ -34,11 +34,11 @@ function resolveSidebarChatSessionKey(state: AppViewState): string {
const snapshot = state.hello?.snapshot as
| { sessionDefaults?: SessionDefaultsSnapshot }
| undefined;
const mainSessionKey = snapshot?.sessionDefaults?.mainSessionKey?.trim();
const mainSessionKey = normalizeOptionalString(snapshot?.sessionDefaults?.mainSessionKey);
if (mainSessionKey) {
return mainSessionKey;
}
const mainKey = snapshot?.sessionDefaults?.mainKey?.trim();
const mainKey = normalizeOptionalString(snapshot?.sessionDefaults?.mainKey);
if (mainKey) {
return mainKey;
}
@@ -857,8 +857,8 @@ export function resolveSessionDisplayName(
key: string,
row?: SessionsListResult["sessions"][number],
): string {
const label = row?.label?.trim() || "";
const displayName = row?.displayName?.trim() || "";
const label = normalizeOptionalString(row?.label) ?? "";
const displayName = normalizeOptionalString(row?.displayName) ?? "";
const { prefix, fallbackName } = parseSessionKey(key);
const applyTypedPrefix = (name: string): string => {
@@ -951,7 +951,7 @@ export function resolveSessionOptionGroups(
resolveAgentGroupLabel(state, parsed.agentId),
)
: ensureGroup("other", "Other Sessions");
const scopeLabel = parsed?.rest?.trim() || key;
const scopeLabel = normalizeOptionalString(parsed?.rest) ?? key;
const label = resolveSessionScopedOptionLabel(key, row, parsed?.rest);
group.options.push({
key,
@@ -1065,7 +1065,8 @@ function resolveAgentGroupLabel(state: AppViewState, agentIdRaw: string): string
const agent = (state.agentsList?.agents ?? []).find(
(entry) => normalizeLowercaseStringOrEmpty(entry.id) === normalized,
);
const name = agent?.identity?.name?.trim() || agent?.name?.trim() || "";
const name =
normalizeOptionalString(agent?.identity?.name) ?? normalizeOptionalString(agent?.name) ?? "";
return name && name !== agentIdRaw ? `${name} (${agentIdRaw})` : agentIdRaw;
}
@@ -1074,13 +1075,13 @@ function resolveSessionScopedOptionLabel(
row?: SessionsListResult["sessions"][number],
rest?: string,
) {
const base = rest?.trim() || key;
const base = normalizeOptionalString(rest) ?? key;
if (!row) {
return base;
}
const label = row.label?.trim() || "";
const displayName = row.displayName?.trim() || "";
const label = normalizeOptionalString(row.label) ?? "";
const displayName = normalizeOptionalString(row.displayName) ?? "";
if ((label && label !== key) || (displayName && displayName !== key)) {
return resolveSessionDisplayName(key, row);
}

View File

@@ -109,7 +109,11 @@ import {
parseAgentSessionKey,
resolveAgentIdFromSessionKey,
} from "./session-key.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
normalizeStringifiedOptionalString,
} from "./string-coerce.ts";
import { agentLogoUrl } from "./views/agents-utils.ts";
import {
resolveAgentConfig,
@@ -208,7 +212,7 @@ function isHttpUrl(value: string): boolean {
}
function normalizeSuggestionValue(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
return normalizeOptionalString(value) ?? "";
}
function uniquePreserveOrder(values: string[]): string[] {
@@ -408,9 +412,7 @@ export function renderApp(state: AppViewState) {
new Set(
[
...(state.agentsList?.agents?.map((entry) => entry.id.trim()) ?? []),
...state.cronJobs
.map((job) => (typeof job.agentId === "string" ? job.agentId.trim() : ""))
.filter(Boolean),
...state.cronJobs.map((job) => normalizeOptionalString(job.agentId) ?? "").filter(Boolean),
].filter(Boolean),
),
);
@@ -424,7 +426,7 @@ export function renderApp(state: AppViewState) {
if (job.payload.kind !== "agentTurn" || typeof job.payload.model !== "string") {
return "";
}
return job.payload.model.trim();
return normalizeOptionalString(job.payload.model) ?? "";
})
.filter(Boolean),
].filter(Boolean),
@@ -1289,7 +1291,7 @@ export function renderApp(state: AppViewState) {
const entry = Array.isArray(list)
? (list[index] as { skills?: unknown })
: undefined;
const normalizedSkill = skillName.trim();
const normalizedSkill = normalizeOptionalString(skillName) ?? "";
if (!normalizedSkill) {
return;
}
@@ -1297,7 +1299,9 @@ export function renderApp(state: AppViewState) {
state.agentSkillsReport?.skills?.map((skill) => skill.name).filter(Boolean) ??
[];
const existing = Array.isArray(entry?.skills)
? entry.skills.map((name) => String(name).trim()).filter(Boolean)
? entry.skills
.map((name) => normalizeStringifiedOptionalString(name) ?? "")
.filter(Boolean)
: undefined;
const base = existing ?? allSkills;
const next = new Set(base);

View File

@@ -6,6 +6,16 @@ export function normalizeOptionalString(value: unknown): string | undefined {
return trimmed ? trimmed : undefined;
}
export function normalizeStringifiedOptionalString(value: unknown): string | undefined {
if (typeof value === "string") {
return normalizeOptionalString(value);
}
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
return normalizeOptionalString(String(value));
}
return undefined;
}
export function normalizeOptionalLowercaseString(value: unknown): string | undefined {
return normalizeOptionalString(value)?.toLowerCase();
}

View File

@@ -4,7 +4,7 @@ import {
normalizeToolName,
resolveToolProfilePolicy,
} from "../../../../src/agents/tool-policy-shared.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts";
import type {
AgentIdentityResult,
AgentsFilesListResult,
@@ -193,7 +193,9 @@ export function normalizeAgentLabel(agent: {
name?: string;
identity?: { name?: string };
}) {
return agent.name?.trim() || agent.identity?.name?.trim() || agent.id;
return (
normalizeOptionalString(agent.name) ?? normalizeOptionalString(agent.identity?.name) ?? agent.id
);
}
const AVATAR_URL_RE = /^(https?:\/\/|data:image\/|\/)/i;
@@ -203,9 +205,9 @@ export function resolveAgentAvatarUrl(
agentIdentity?: AgentIdentityResult | null,
): string | null {
const candidates = [
agentIdentity?.avatar?.trim(),
agent.identity?.avatarUrl?.trim(),
agent.identity?.avatar?.trim(),
normalizeOptionalString(agentIdentity?.avatar),
normalizeOptionalString(agent.identity?.avatarUrl),
normalizeOptionalString(agent.identity?.avatar),
];
for (const candidate of candidates) {
if (!candidate) {
@@ -219,7 +221,7 @@ export function resolveAgentAvatarUrl(
}
export function agentLogoUrl(basePath: string): string {
const base = basePath?.trim() ? basePath.replace(/\/$/, "") : "";
const base = normalizeOptionalString(basePath)?.replace(/\/$/, "") ?? "";
return base ? `${base}/favicon.svg` : "favicon.svg";
}
@@ -251,19 +253,19 @@ export function resolveAgentEmoji(
agent: { identity?: { emoji?: string; avatar?: string } },
agentIdentity?: AgentIdentityResult | null,
) {
const identityEmoji = agentIdentity?.emoji?.trim();
const identityEmoji = normalizeOptionalString(agentIdentity?.emoji);
if (identityEmoji && isLikelyEmoji(identityEmoji)) {
return identityEmoji;
}
const agentEmoji = agent.identity?.emoji?.trim();
const agentEmoji = normalizeOptionalString(agent.identity?.emoji);
if (agentEmoji && isLikelyEmoji(agentEmoji)) {
return agentEmoji;
}
const identityAvatar = agentIdentity?.avatar?.trim();
const identityAvatar = normalizeOptionalString(agentIdentity?.avatar);
if (identityAvatar && isLikelyEmoji(identityAvatar)) {
return identityAvatar;
}
const avatar = agent.identity?.avatar?.trim();
const avatar = normalizeOptionalString(agent.identity?.avatar);
if (avatar && isLikelyEmoji(avatar)) {
return avatar;
}
@@ -341,9 +343,9 @@ export function buildAgentContext(
? resolveModelLabel(config.defaults?.model)
: resolveModelLabel(agent.model);
const identityName =
agentIdentity?.name?.trim() ||
agent.identity?.name?.trim() ||
agent.name?.trim() ||
normalizeOptionalString(agentIdentity?.name) ||
normalizeOptionalString(agent.identity?.name) ||
normalizeOptionalString(agent.name) ||
config.entry?.name ||
agent.id;
const identityAvatar = resolveAgentAvatarUrl(agent, agentIdentity) ? "custom" : "—";
@@ -364,11 +366,11 @@ export function resolveModelLabel(model?: unknown): string {
return "-";
}
if (typeof model === "string") {
return model.trim() || "-";
return normalizeOptionalString(model) || "-";
}
if (typeof model === "object" && model) {
const record = model as { primary?: string; fallbacks?: string[] };
const primary = record.primary?.trim();
const primary = normalizeOptionalString(record.primary);
if (primary) {
const fallbackCount = Array.isArray(record.fallbacks) ? record.fallbacks.length : 0;
return fallbackCount > 0 ? `${primary} (+${fallbackCount} fallback)` : primary;
@@ -387,7 +389,7 @@ export function resolveModelPrimary(model?: unknown): string | null {
return null;
}
if (typeof model === "string") {
const trimmed = model.trim();
const trimmed = normalizeOptionalString(model);
return trimmed || null;
}
if (typeof model === "object" && model) {
@@ -402,7 +404,7 @@ export function resolveModelPrimary(model?: unknown): string | null {
: typeof record.value === "string"
? record.value
: null;
const primary = candidate?.trim();
const primary = normalizeOptionalString(candidate);
return primary || null;
}
return null;

View File

@@ -8,6 +8,7 @@ import type {
} from "../controllers/devices.ts";
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "../controllers/exec-approvals.ts";
import { formatRelativeTimestamp, formatList } from "../format.ts";
import { normalizeOptionalString } from "../string-coerce.ts";
import { renderExecApprovals, resolveExecApprovalsState } from "./nodes-exec-approvals.ts";
import { resolveConfigAgents, resolveNodeTargets, type NodeTargetOption } from "./nodes-shared.ts";
export type NodesProps = {
@@ -111,9 +112,9 @@ function renderDevices(props: NodesProps) {
}
function renderPendingDevice(req: PendingDevice, props: NodesProps) {
const name = req.displayName?.trim() || req.deviceId;
const name = normalizeOptionalString(req.displayName) || req.deviceId;
const age = typeof req.ts === "number" ? formatRelativeTimestamp(req.ts) : t("common.na");
const roleValue = req.role?.trim() || formatList(req.roles);
const roleValue = normalizeOptionalString(req.role) || formatList(req.roles);
const scopesValue = formatList(req.scopes);
const repair = req.isRepair ? " · repair" : "";
const ip = req.remoteIp ? ` · ${req.remoteIp}` : "";
@@ -141,7 +142,7 @@ function renderPendingDevice(req: PendingDevice, props: NodesProps) {
}
function renderPairedDevice(device: PairedDevice, props: NodesProps) {
const name = device.displayName?.trim() || device.deviceId;
const name = normalizeOptionalString(device.displayName) || device.deviceId;
const ip = device.remoteIp ? ` · ${device.remoteIp}` : "";
const roles = `roles: ${formatList(device.roles)}`;
const scopes = `scopes: ${formatList(device.scopes)}`;