refactor: dedupe ui lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 20:20:33 +01:00
parent abf81ff1ed
commit f665da8dbc
43 changed files with 226 additions and 135 deletions

View File

@@ -10,6 +10,7 @@ import { loadSessions } from "./controllers/sessions.ts";
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
import { normalizeBasePath } from "./navigation.ts";
import { parseAgentSessionKey } from "./session-key.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import type { ChatModelOverride, ModelCatalogEntry } from "./types.ts";
import type { SessionsListResult } from "./types.ts";
import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts";
@@ -51,7 +52,7 @@ export function isChatStopCommand(text: string) {
if (!trimmed) {
return false;
}
const normalized = trimmed.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
if (normalized === "/stop") {
return true;
}
@@ -69,7 +70,7 @@ function isChatResetCommand(text: string) {
if (!trimmed) {
return false;
}
const normalized = trimmed.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(trimmed);
if (normalized === "/new" || normalized === "/reset") {
return true;
}

View File

@@ -16,6 +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 type { ThemeMode } from "./theme.ts";
import {
listThinkingLevelLabels,
@@ -607,7 +608,7 @@ function buildThinkingOptions(
if (!trimmed) {
return;
}
const key = trimmed.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(trimmed);
if (seen.has(key)) {
return;
}
@@ -624,7 +625,7 @@ function buildThinkingOptions(
};
for (const label of listThinkingLevelLabels(provider)) {
const normalized = normalizeThinkLevel(label) ?? label.trim().toLowerCase();
const normalized = normalizeThinkLevel(label) ?? normalizeLowercaseStringOrEmpty(label);
addOption(normalized);
}
if (currentOverride) {
@@ -807,7 +808,7 @@ function capitalize(s: string): string {
* fallback display name. Exported for testing.
*/
export function parseSessionKey(key: string): SessionKeyInfo {
const normalized = key.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(key);
// ── Main session ─────────────────────────────────
if (key === "main" || key === "agent:main:main") {
@@ -878,7 +879,7 @@ export function resolveSessionDisplayName(
}
export function isCronSessionKey(key: string): boolean {
const normalized = key.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(key);
if (!normalized) {
return false;
}
@@ -946,7 +947,7 @@ export function resolveSessionOptionGroups(
const parsed = parseAgentSessionKey(key);
const group = parsed
? ensureGroup(
`agent:${parsed.agentId.toLowerCase()}`,
`agent:${normalizeLowercaseStringOrEmpty(parsed.agentId)}`,
resolveAgentGroupLabel(state, parsed.agentId),
)
: ensureGroup("other", "Other Sessions");
@@ -1060,9 +1061,9 @@ function countHiddenCronSessions(sessionKey: string, sessions: SessionsListResul
}
function resolveAgentGroupLabel(state: AppViewState, agentIdRaw: string): string {
const normalized = agentIdRaw.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(agentIdRaw);
const agent = (state.agentsList?.agents ?? []).find(
(entry) => entry.id.trim().toLowerCase() === normalized,
(entry) => normalizeLowercaseStringOrEmpty(entry.id) === normalized,
);
const name = agent?.identity?.name?.trim() || agent?.name?.trim() || "";
return name && name !== agentIdRaw ? `${name} (${agentIdRaw})` : agentIdRaw;

View File

@@ -101,14 +101,15 @@ import {
updateSkillEnabled,
} from "./controllers/skills.ts";
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
import "./components/dashboard-header.ts";
import { icons } from "./icons.ts";
import "./components/dashboard-header.ts";
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
import {
buildAgentMainSessionKey,
parseAgentSessionKey,
resolveAgentIdFromSessionKey,
} from "./session-key.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import { agentLogoUrl } from "./views/agents-utils.ts";
import {
resolveAgentConfig,
@@ -218,7 +219,7 @@ function uniquePreserveOrder(values: string[]): string[] {
if (!normalized) {
continue;
}
const key = normalized.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(normalized);
if (seen.has(key)) {
continue;
}

View File

@@ -1,4 +1,5 @@
import { formatUnknownText, truncateText } from "./format.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
const TOOL_STREAM_LIMIT = 50;
const TOOL_STREAM_THROTTLE_MS = 80;
@@ -53,7 +54,11 @@ function resolveModelLabel(provider: unknown, model: unknown): string | null {
const providerValue = toTrimmedString(provider);
if (providerValue) {
const prefix = `${providerValue}/`;
if (modelValue.toLowerCase().startsWith(prefix.toLowerCase())) {
if (
normalizeLowercaseStringOrEmpty(modelValue).startsWith(
normalizeLowercaseStringOrEmpty(prefix),
)
) {
const trimmedModel = modelValue.slice(prefix.length).trim();
if (trimmedModel) {
return `${providerValue}/${trimmedModel}`;

View File

@@ -72,6 +72,7 @@ import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway.ts";
import type { Tab } from "./navigation.ts";
import { resolveAgentIdFromSessionKey } from "./session-key.ts";
import { loadSettings, type UiSettings } from "./storage.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import { VALID_THEME_NAMES, type ResolvedTheme, type ThemeMode, type ThemeName } from "./theme.ts";
import type {
AgentsListResult,
@@ -118,7 +119,7 @@ function resolveOnboardingMode(): boolean {
if (!raw) {
return false;
}
const normalized = raw.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(raw);
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
}

View File

@@ -1,4 +1,5 @@
import type { ChatEventPayload } from "./controllers/chat.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
export function shouldReloadHistoryForFinalEvent(payload?: ChatEventPayload): boolean {
if (!payload || payload.state !== "final") {
@@ -8,7 +9,7 @@ export function shouldReloadHistoryForFinalEvent(payload?: ChatEventPayload): bo
return true;
}
const message = payload.message as Record<string, unknown>;
const role = typeof message.role === "string" ? message.role.toLowerCase() : "";
const role = normalizeLowercaseStringOrEmpty(message.role);
if (role && role !== "assistant") {
return true;
}

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import type { ModelCatalogEntry } from "./types.ts";
export type ChatModelOverride =
@@ -68,8 +69,9 @@ export function resolveChatModelOverride(
}
let matchedValue = "";
const normalizedTrimmed = normalizeLowercaseStringOrEmpty(trimmed);
for (const entry of catalog) {
if (entry.id.trim().toLowerCase() !== trimmed.toLowerCase()) {
if (normalizeLowercaseStringOrEmpty(entry.id) !== normalizedTrimmed) {
continue;
}
const candidate = buildQualifiedChatModelValue(entry.id, entry.provider);
@@ -77,7 +79,9 @@ export function resolveChatModelOverride(
matchedValue = candidate;
continue;
}
if (matchedValue.toLowerCase() !== candidate.toLowerCase()) {
if (
normalizeLowercaseStringOrEmpty(matchedValue) !== normalizeLowercaseStringOrEmpty(candidate)
) {
return { value: trimmed, source: "raw", reason: "ambiguous" };
}
}

View File

@@ -5,6 +5,7 @@ import {
normalizeChatModelOverrideValue,
resolvePreferredServerChatModelValue,
} from "./chat-model-ref.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import type { ModelCatalogEntry } from "./types.ts";
type ChatModelSelectStateInput = Pick<
@@ -66,7 +67,7 @@ function buildChatModelOptions(
if (!trimmed) {
return;
}
const key = trimmed.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(trimmed);
if (seen.has(key)) {
return;
}

View File

@@ -5,6 +5,7 @@ import type { AssistantIdentity } from "../assistant-identity.ts";
import { icons } from "../icons.ts";
import { toSanitizedMarkdownHtml } from "../markdown.ts";
import { openExternalUrlSafe } from "../open-external-url.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { detectTextDirection } from "../text-direction.ts";
import type { MessageGroup, ToolCard } from "../types/chat-types.ts";
import { agentLogoUrl } from "../views/agents-utils.ts";
@@ -726,10 +727,11 @@ function renderGroupedMessage(
const m = message as Record<string, unknown>;
const role = typeof m.role === "string" ? m.role : "unknown";
const normalizedRole = normalizeRoleForGrouping(role);
const normalizedRawRole = normalizeLowercaseStringOrEmpty(role);
const isToolResult =
isToolResultMessage(message) ||
role.toLowerCase() === "toolresult" ||
role.toLowerCase() === "tool_result" ||
normalizedRawRole === "toolresult" ||
normalizedRawRole === "tool_result" ||
typeof m.toolCallId === "string" ||
typeof m.tool_call_id === "string";
@@ -752,7 +754,12 @@ function renderGroupedMessage(
// Detect pure-JSON messages and render as collapsible block
const jsonResult = markdown && !opts.isStreaming ? detectJson(markdown) : null;
const bubbleClasses = ["chat-bubble", opts.isStreaming ? "streaming" : "", "fade-in", canCopyMarkdown ? "has-copy" : ""]
const bubbleClasses = [
"chat-bubble",
opts.isStreaming ? "streaming" : "",
"fade-in",
canCopyMarkdown ? "has-copy" : "",
]
.filter(Boolean)
.join(" ");

View File

@@ -2,12 +2,13 @@ import { stripInboundMetadata } from "../../../../src/auto-reply/reply/strip-inb
import { stripEnvelope } from "../../../../src/shared/chat-envelope.js";
import { extractAssistantVisibleText as extractSharedAssistantVisibleText } from "../../../../src/shared/chat-message-content.js";
import { stripThinkingTags } from "../format.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
const textCache = new WeakMap<object, string | null>();
const thinkingCache = new WeakMap<object, string | null>();
function processMessageText(text: string, role: string): string {
const shouldStripInboundMetadata = role.toLowerCase() === "user";
const shouldStripInboundMetadata = normalizeLowercaseStringOrEmpty(role) === "user";
if (role === "assistant") {
return stripThinkingTags(text);
}

View File

@@ -8,6 +8,7 @@ import {
isToolResultContentType,
resolveToolBlockArgs,
} from "../../../../src/chat/tool-content.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { NormalizedMessage, MessageContentItem } from "../types/chat-types.ts";
/**
@@ -74,7 +75,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
* Normalize role for grouping purposes.
*/
export function normalizeRoleForGrouping(role: string): string {
const lower = role.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(role);
// Preserve original casing when it's already a core role.
if (role === "user" || role === "User") {
return role;
@@ -102,6 +103,6 @@ export function normalizeRoleForGrouping(role: string): string {
*/
export function isToolResultMessage(message: unknown): boolean {
const m = message as Record<string, unknown>;
const role = typeof m.role === "string" ? m.role.toLowerCase() : "";
const role = normalizeLowercaseStringOrEmpty(m.role);
return role === "toolresult" || role === "tool_result";
}

View File

@@ -1,10 +1,11 @@
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { extractTextCached } from "./message-extract.ts";
export function messageMatchesSearchQuery(message: unknown, query: string): boolean {
const normalizedQuery = query.trim().toLowerCase();
const normalizedQuery = normalizeLowercaseStringOrEmpty(query);
if (!normalizedQuery) {
return true;
}
const text = (extractTextCached(message) ?? "").toLowerCase();
const text = normalizeLowercaseStringOrEmpty(extractTextCached(message));
return text.includes(normalizedQuery);
}

View File

@@ -11,6 +11,10 @@ import {
isSubagentSessionKey,
parseAgentSessionKey,
} from "../session-key.ts";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../string-coerce.ts";
import {
formatThinkingLevels,
normalizeThinkLevel,
@@ -60,7 +64,7 @@ function normalizeVerboseLevel(raw?: string | null): "off" | "on" | "full" | und
if (!raw) {
return undefined;
}
const key = raw.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(raw);
if (["off", "false", "no", "0"].includes(key)) {
return "off";
}
@@ -313,7 +317,7 @@ async function executeFast(
sessionKey: string,
args: string,
): Promise<SlashCommandResult> {
const rawMode = args.trim().toLowerCase();
const rawMode = normalizeLowercaseStringOrEmpty(args);
if (!rawMode || rawMode === "status") {
try {
@@ -406,6 +410,7 @@ async function executeKill(
args: string,
): Promise<SlashCommandResult> {
const target = args.trim();
const normalizedTarget = normalizeLowercaseStringOrEmpty(target);
if (!target) {
return { content: "Usage: `/kill <id|all>`" };
}
@@ -415,7 +420,7 @@ async function executeKill(
if (matched.length === 0) {
return {
content:
target.toLowerCase() === "all"
normalizedTarget === "all"
? "No active sub-agent sessions found."
: `No matching sub-agent sessions found for \`${target}\`.`,
};
@@ -435,7 +440,7 @@ async function executeKill(
if (rejected.length === 0) {
return {
content:
target.toLowerCase() === "all"
normalizedTarget === "all"
? "No active sub-agent runs to abort."
: `No active runs matched \`${target}\`.`,
};
@@ -443,7 +448,7 @@ async function executeKill(
throw rejected[0]?.reason ?? new Error("abort failed");
}
if (target.toLowerCase() === "all") {
if (normalizedTarget === "all") {
return {
content:
successCount === matched.length
@@ -468,13 +473,13 @@ function resolveKillTargets(
currentSessionKey: string,
target: string,
): string[] {
const normalizedTarget = target.trim().toLowerCase();
const normalizedTarget = normalizeLowercaseStringOrEmpty(target);
if (!normalizedTarget) {
return [];
}
const keys = new Set<string>();
const normalizedCurrentSessionKey = currentSessionKey.trim().toLowerCase();
const normalizedCurrentSessionKey = normalizeLowercaseStringOrEmpty(currentSessionKey);
const currentParsed = parseAgentSessionKey(normalizedCurrentSessionKey);
const currentAgentId =
currentParsed?.agentId ??
@@ -485,7 +490,7 @@ function resolveKillTargets(
if (!key || !isSubagentSessionKey(key)) {
continue;
}
const normalizedKey = key.toLowerCase();
const normalizedKey = normalizeLowercaseStringOrEmpty(key);
const parsed = parseAgentSessionKey(normalizedKey);
const belongsToCurrentSession = isWithinCurrentSessionSubtree(
normalizedKey,
@@ -550,8 +555,7 @@ function buildSessionIndex(sessions: GatewaySessionRow[]): Map<string, GatewaySe
}
function normalizeSessionKey(key?: string | null): string | undefined {
const normalized = key?.trim().toLowerCase();
return normalized || undefined;
return normalizeOptionalLowercaseString(key);
}
function resolveEquivalentSessionKeys(
@@ -658,11 +662,11 @@ function resolveSteerSubagent(
currentSessionKey: string,
target: string,
): string[] {
const normalizedTarget = target.trim().toLowerCase();
const normalizedTarget = normalizeLowercaseStringOrEmpty(target);
if (!normalizedTarget) {
return [];
}
const normalizedCurrentSessionKey = currentSessionKey.trim().toLowerCase();
const normalizedCurrentSessionKey = normalizeLowercaseStringOrEmpty(currentSessionKey);
const currentParsed = parseAgentSessionKey(normalizedCurrentSessionKey);
const currentAgentId =
currentParsed?.agentId ??
@@ -675,7 +679,7 @@ function resolveSteerSubagent(
if (!key || !isSubagentSessionKey(key)) {
continue;
}
const normalizedKey = key.toLowerCase();
const normalizedKey = normalizeLowercaseStringOrEmpty(key);
const parsed = parseAgentSessionKey(normalizedKey);
const belongsToCurrentSession = isWithinCurrentSessionSubtree(
normalizedKey,
@@ -692,7 +696,7 @@ function resolveSteerSubagent(
normalizedKey === normalizedTarget ||
normalizedKey.endsWith(`:subagent:${normalizedTarget}`) ||
normalizedKey === `subagent:${normalizedTarget}` ||
(session.label ?? "").toLowerCase() === normalizedTarget;
normalizeLowercaseStringOrEmpty(session.label) === normalizedTarget;
if (isMatch) {
keys.add(key);
}
@@ -728,7 +732,7 @@ async function resolveSteerTarget(
const rest = trimmed.slice(spaceIdx + 1).trim();
// Skip "all" — resolveKillTargets treats it as a wildcard, but steer/redirect
// target a single session, so "all good now" should not match subagents.
if (rest && maybeTarget.toLowerCase() !== "all") {
if (rest && normalizeLowercaseStringOrEmpty(maybeTarget) !== "all") {
const sessions =
context.sessionsResult ?? (await client.request<SessionsListResult>("sessions.list", {}));
const matched = resolveSteerSubagent(sessions?.sessions ?? [], sessionKey, maybeTarget);

View File

@@ -4,6 +4,7 @@ import type {
CommandArgChoice,
} from "../../../../src/auto-reply/commands-registry.types.js";
import type { IconName } from "../icons.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
export type SlashCommandCategory = "session" | "model" | "agents" | "tools";
@@ -216,13 +217,13 @@ export const CATEGORY_LABELS: Record<SlashCommandCategory, string> = {
};
export function getSlashCommandCompletions(filter: string): SlashCommandDef[] {
const lower = filter.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(filter);
const commands = lower
? SLASH_COMMANDS.filter(
(cmd) =>
cmd.name.startsWith(lower) ||
cmd.aliases?.some((alias) => alias.toLowerCase().startsWith(lower)) ||
cmd.description.toLowerCase().includes(lower),
cmd.aliases?.some((alias) => normalizeLowercaseStringOrEmpty(alias).startsWith(lower)) ||
normalizeLowercaseStringOrEmpty(cmd.description).includes(lower),
)
: SLASH_COMMANDS;
return commands.toSorted((a, b) => {
@@ -266,11 +267,11 @@ export function parseSlashCommand(text: string): ParsedSlashCommand | null {
return null;
}
const normalizedName = name.toLowerCase();
const normalizedName = normalizeLowercaseStringOrEmpty(name);
const command = SLASH_COMMANDS.find(
(cmd) =>
cmd.name === normalizedName ||
cmd.aliases?.some((alias) => alias.toLowerCase() === normalizedName),
cmd.aliases?.some((alias) => normalizeLowercaseStringOrEmpty(alias) === normalizedName),
);
if (!command) {
return null;

View File

@@ -1,5 +1,6 @@
import { ConnectErrorDetailCodes } from "../../../src/gateway/protocol/connect-error-details.js";
import { resolveGatewayErrorDetailCode } from "./gateway.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
type ErrorWithMessageAndDetails = {
message?: unknown;
@@ -39,7 +40,7 @@ function formatErrorFromMessageAndDetails(error: ErrorWithMessageAndDetails): st
break;
}
const normalized = message.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(message);
if (
normalized === "fetch failed" ||
normalized === "failed to fetch" ||

View File

@@ -2,6 +2,7 @@ import { resetToolStream } from "../app-tool-stream.ts";
import { extractText } from "../chat/message-extract.ts";
import { formatConnectError } from "../connect-error.ts";
import type { GatewayBrowserClient } from "../gateway.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { ChatAttachment } from "../ui-types.ts";
import { generateUUID } from "../uuid.ts";
import {
@@ -20,7 +21,7 @@ function isAssistantSilentReply(message: unknown): boolean {
return false;
}
const entry = message as Record<string, unknown>;
const role = typeof entry.role === "string" ? entry.role.toLowerCase() : "";
const role = normalizeLowercaseStringOrEmpty(entry.role);
if (role !== "assistant") {
return false;
}
@@ -128,7 +129,7 @@ function normalizeAssistantMessage(
const candidate = message as Record<string, unknown>;
const roleValue = candidate.role;
if (typeof roleValue === "string") {
const role = options.roleCaseSensitive ? roleValue : roleValue.toLowerCase();
const role = options.roleCaseSensitive ? roleValue : normalizeLowercaseStringOrEmpty(roleValue);
if (role !== "assistant") {
return null;
}

View File

@@ -2,6 +2,7 @@ import { t } from "../../i18n/index.ts";
import { DEFAULT_CRON_FORM } from "../app-defaults.ts";
import { toNumber } from "../format.ts";
import type { GatewayBrowserClient } from "../gateway.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type {
CronJob,
CronDeliveryStatus,
@@ -900,13 +901,13 @@ export function startCronEdit(state: CronState, job: CronJob) {
function buildCloneName(name: string, existingNames: Set<string>) {
const base = name.trim() || "Job";
const first = `${base} copy`;
if (!existingNames.has(first.toLowerCase())) {
if (!existingNames.has(normalizeLowercaseStringOrEmpty(first))) {
return first;
}
let index = 2;
while (index < 1000) {
const next = `${base} copy ${index}`;
if (!existingNames.has(next.toLowerCase())) {
if (!existingNames.has(normalizeLowercaseStringOrEmpty(next))) {
return next;
}
index += 1;
@@ -917,7 +918,9 @@ function buildCloneName(name: string, existingNames: Set<string>) {
export function startCronClone(state: CronState, job: CronJob) {
clearCronEditState(state);
state.cronRunsJobId = job.id;
const existingNames = new Set(state.cronJobs.map((entry) => entry.name.trim().toLowerCase()));
const existingNames = new Set(
state.cronJobs.map((entry) => normalizeLowercaseStringOrEmpty(entry.name)),
);
const cloned = jobToForm(job, state.cronForm);
cloned.name = buildCloneName(job.name, existingNames);
state.cronForm = cloned;

View File

@@ -1,4 +1,5 @@
import type { GatewayBrowserClient } from "../gateway.ts";
import { normalizeOptionalLowercaseString } from "../string-coerce.ts";
import type { ConfigSnapshot } from "../types.ts";
export type DreamingPhaseId = "light" | "deep" | "rem";
@@ -118,7 +119,7 @@ function normalizeFiniteScore(value: unknown, fallback = 0): number {
}
function normalizeStorageMode(value: unknown): DreamingStatus["storageMode"] {
const normalized = normalizeTrimmedString(value)?.toLowerCase();
const normalized = normalizeOptionalLowercaseString(normalizeTrimmedString(value));
if (normalized === "inline" || normalized === "separate" || normalized === "both") {
return normalized;
}
@@ -144,7 +145,7 @@ function resolveDreamingPluginId(configValue: Record<string, unknown> | null): s
const plugins = asRecord(configValue?.plugins);
const slots = asRecord(plugins?.slots);
const configuredSlot = normalizeTrimmedString(slots?.memory);
if (configuredSlot && configuredSlot.toLowerCase() !== "none") {
if (configuredSlot && normalizeOptionalLowercaseString(configuredSlot) !== "none") {
return configuredSlot;
}
return DEFAULT_DREAMING_PLUGIN_ID;

View File

@@ -1,4 +1,5 @@
import type { GatewayBrowserClient } from "../gateway.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { LogEntry, LogLevel } from "../types.ts";
import {
formatMissingOperatorReadScopeMessage,
@@ -45,7 +46,7 @@ function normalizeLevel(value: unknown): LogLevel | null {
if (typeof value !== "string") {
return null;
}
const lowered = value.toLowerCase() as LogLevel;
const lowered = normalizeLowercaseStringOrEmpty(value) as LogLevel;
return LEVELS.has(lowered) ? lowered : null;
}

View File

@@ -1,5 +1,6 @@
import { getSafeLocalStorage } from "../../local-storage.ts";
import type { GatewayBrowserClient } from "../gateway.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { SessionsUsageResult, CostUsageSummary, SessionUsageTimeSeries } from "../types.ts";
import type { SessionLogEntry } from "../views/usage.ts";
import {
@@ -102,9 +103,9 @@ function normalizeGatewayCompatibilityKey(gatewayUrl?: string): string {
try {
const parsed = new URL(trimmed);
const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
return `${parsed.protocol}//${parsed.host}${pathname}`.toLowerCase();
return normalizeLowercaseStringOrEmpty(`${parsed.protocol}//${parsed.host}${pathname}`);
} catch {
return trimmed.toLowerCase();
return normalizeLowercaseStringOrEmpty(trimmed);
}
}

View File

@@ -1,3 +1,5 @@
import { normalizeOptionalLowercaseString } from "./string-coerce.ts";
const REQUIRED_EXTERNAL_REL_TOKENS = ["noopener", "noreferrer"] as const;
export const EXTERNAL_LINK_TARGET = "_blank";
@@ -7,7 +9,7 @@ export function buildExternalLinkRel(currentRel?: string): string {
const seen = new Set<string>(REQUIRED_EXTERNAL_REL_TOKENS);
for (const rawToken of (currentRel ?? "").split(/\s+/)) {
const token = rawToken.trim().toLowerCase();
const token = normalizeOptionalLowercaseString(rawToken);
if (!token || seen.has(token)) {
continue;
}

View File

@@ -12,6 +12,7 @@ import {
} from "../../../src/gateway/protocol/connect-error-details.js";
import { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken } from "./device-auth.ts";
import { loadOrCreateDeviceIdentity, signDevicePayload } from "./device-identity.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
import { generateUUID } from "./uuid.ts";
export type GatewayEventFrame = {
@@ -82,7 +83,7 @@ export function isNonRecoverableAuthError(error: GatewayErrorInfo | undefined):
function isTrustedRetryEndpoint(url: string): boolean {
try {
const gatewayUrl = new URL(url, window.location.href);
const host = gatewayUrl.hostname.trim().toLowerCase();
const host = normalizeLowercaseStringOrEmpty(gatewayUrl.hostname);
const isLoopbackHost =
host === "localhost" || host === "::1" || host === "[::1]" || host === "127.0.0.1";
const isLoopbackIPv4 = host.startsWith("127.");

View File

@@ -1,6 +1,7 @@
import DOMPurify from "dompurify";
import { marked } from "marked";
import { truncateText } from "./format.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
const allowedTags = [
"a",
@@ -116,7 +117,7 @@ function installHooks() {
node.setAttribute("rel", "noreferrer noopener");
node.setAttribute("target", "_blank");
if (href.toLowerCase().includes("tail")) {
if (normalizeLowercaseStringOrEmpty(href).includes("tail")) {
node.classList.add(TAIL_LINK_BLUR_CLASS);
}
});

View File

@@ -1,5 +1,6 @@
import { t } from "../i18n/index.ts";
import type { IconName } from "./icons.js";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
export const TAB_GROUPS = [
{ label: "chat", tabs: ["chat"] },
@@ -122,7 +123,7 @@ export function tabFromPath(pathname: string, basePath = ""): Tab | null {
path = path.slice(base.length);
}
}
let normalized = normalizePath(path).toLowerCase();
let normalized = normalizeLowercaseStringOrEmpty(normalizePath(path));
if (normalized.endsWith("/index.html")) {
normalized = "/";
}
@@ -145,7 +146,7 @@ export function inferBasePathFromPathname(pathname: string): string {
return "";
}
for (let i = 0; i < segments.length; i++) {
const candidate = `/${segments.slice(i).join("/")}`.toLowerCase();
const candidate = normalizeLowercaseStringOrEmpty(`/${segments.slice(i).join("/")}`);
if (PATH_TO_TAB.has(candidate)) {
const prefix = segments.slice(0, i);
return prefix.length ? `/${prefix.join("/")}` : "";

View File

@@ -1,9 +1,11 @@
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
const DATA_URL_PREFIX = "data:";
const ALLOWED_EXTERNAL_PROTOCOLS = new Set(["http:", "https:", "blob:"]);
const BLOCKED_DATA_IMAGE_MIME_TYPES = new Set(["image/svg+xml"]);
function isAllowedDataImageUrl(url: string): boolean {
if (!url.toLowerCase().startsWith(DATA_URL_PREFIX)) {
if (!normalizeLowercaseStringOrEmpty(url).startsWith(DATA_URL_PREFIX)) {
return false;
}
@@ -13,7 +15,7 @@ function isAllowedDataImageUrl(url: string): boolean {
}
const metadata = url.slice(DATA_URL_PREFIX.length, commaIndex);
const mimeType = metadata.split(";")[0]?.trim().toLowerCase() ?? "";
const mimeType = normalizeLowercaseStringOrEmpty(metadata.split(";")[0]);
if (!mimeType.startsWith("image/")) {
return false;
}
@@ -39,13 +41,15 @@ export function resolveSafeExternalUrl(
return candidate;
}
if (candidate.toLowerCase().startsWith(DATA_URL_PREFIX)) {
if (normalizeLowercaseStringOrEmpty(candidate).startsWith(DATA_URL_PREFIX)) {
return null;
}
try {
const parsed = new URL(candidate, baseHref);
return ALLOWED_EXTERNAL_PROTOCOLS.has(parsed.protocol.toLowerCase()) ? parsed.toString() : null;
return ALLOWED_EXTERNAL_PROTOCOLS.has(normalizeLowercaseStringOrEmpty(parsed.protocol))
? parsed.toString()
: null;
} catch {
return null;
}

View File

@@ -1,3 +1,8 @@
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "./string-coerce.ts";
export type ParsedAgentSessionKey = {
agentId: string;
rest: string;
@@ -14,7 +19,7 @@ const TRAILING_DASH_RE = /-+$/;
export function parseAgentSessionKey(
sessionKey: string | undefined | null,
): ParsedAgentSessionKey | null {
const raw = (sessionKey ?? "").trim().toLowerCase();
const raw = normalizeLowercaseStringOrEmpty(sessionKey);
if (!raw) {
return null;
}
@@ -31,8 +36,7 @@ export function parseAgentSessionKey(
}
export function normalizeMainKey(value: string | undefined | null): string {
const trimmed = (value ?? "").trim();
return trimmed ? trimmed.toLowerCase() : DEFAULT_MAIN_KEY;
return normalizeOptionalLowercaseString(value) ?? DEFAULT_MAIN_KEY;
}
export function normalizeAgentId(value: string | undefined | null): string {
@@ -41,11 +45,10 @@ export function normalizeAgentId(value: string | undefined | null): string {
return DEFAULT_AGENT_ID;
}
if (VALID_ID_RE.test(trimmed)) {
return trimmed.toLowerCase();
return normalizeLowercaseStringOrEmpty(trimmed);
}
return (
trimmed
.toLowerCase()
normalizeLowercaseStringOrEmpty(trimmed)
.replace(INVALID_CHARS_RE, "-")
.replace(LEADING_DASH_RE, "")
.replace(TRAILING_DASH_RE, "")
@@ -72,9 +75,9 @@ export function isSubagentSessionKey(sessionKey: string | undefined | null): boo
if (!raw) {
return false;
}
if (raw.toLowerCase().startsWith("subagent:")) {
if (normalizeLowercaseStringOrEmpty(raw).startsWith("subagent:")) {
return true;
}
const parsed = parseAgentSessionKey(raw);
return Boolean((parsed?.rest ?? "").toLowerCase().startsWith("subagent:"));
return normalizeLowercaseStringOrEmpty(parsed?.rest).startsWith("subagent:");
}

View File

@@ -0,0 +1,15 @@
export function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}
export function normalizeOptionalLowercaseString(value: unknown): string | undefined {
return normalizeOptionalString(value)?.toLowerCase();
}
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
return normalizeOptionalLowercaseString(value) ?? "";
}

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
export type ThinkingCatalogEntry = {
provider: string;
id: string;
@@ -13,7 +15,7 @@ export function normalizeThinkingProviderId(provider?: string | null): string {
if (!provider) {
return "";
}
const normalized = provider.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(provider);
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}
@@ -31,7 +33,7 @@ export function normalizeThinkLevel(raw?: string | null): string | undefined {
if (!raw) {
return undefined;
}
const key = raw.trim().toLowerCase();
const key = normalizeLowercaseStringOrEmpty(raw);
const collapsed = key.replace(/[\s_-]+/g, "");
if (collapsed === "adaptive" || collapsed === "auto") {
return "adaptive";

View File

@@ -7,6 +7,7 @@ import {
type ToolDisplaySpec as ToolDisplaySpecBase,
} from "../../../src/agents/tool-display-common.js";
import type { IconName } from "./icons.ts";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";
type ToolDisplaySpec = ToolDisplaySpecBase & {
icon?: string;
@@ -120,7 +121,7 @@ export function resolveToolDisplay(params: {
meta?: string;
}): ToolDisplay {
const name = normalizeToolName(params.name);
const key = name.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(name);
const spec = TOOL_MAP[key];
const icon = (spec?.icon ?? FALLBACK.icon ?? "puzzle") as IconName;
const title = spec?.title ?? defaultTitle(name);

View File

@@ -51,7 +51,7 @@ const QUERY_KEYS = new Set([
"maxmessages",
]);
const normalizeQueryText = (value: string): string => value.trim().toLowerCase();
const normalizeQueryText = (value: string): string => normalizeLowercaseStringOrEmpty(value);
const globToRegex = (pattern: string): RegExp => {
const escaped = pattern
@@ -62,7 +62,7 @@ const globToRegex = (pattern: string): RegExp => {
};
const parseQueryNumber = (value: string): number | null => {
let raw = value.trim().toLowerCase();
let raw = normalizeLowercaseStringOrEmpty(value);
if (!raw) {
return null;
}
@@ -101,23 +101,25 @@ export const extractQueryTerms = (query: string): UsageQueryTerm[] => {
const getSessionText = (session: UsageSessionQueryTarget): string[] => {
const items: Array<string | undefined> = [session.label, session.key, session.sessionId];
return items.filter((item): item is string => Boolean(item)).map((item) => item.toLowerCase());
return items
.filter((item): item is string => Boolean(item))
.map((item) => normalizeLowercaseStringOrEmpty(item));
};
const getSessionProviders = (session: UsageSessionQueryTarget): string[] => {
const providers = new Set<string>();
if (session.modelProvider) {
providers.add(session.modelProvider.toLowerCase());
providers.add(normalizeLowercaseStringOrEmpty(session.modelProvider));
}
if (session.providerOverride) {
providers.add(session.providerOverride.toLowerCase());
providers.add(normalizeLowercaseStringOrEmpty(session.providerOverride));
}
if (session.origin?.provider) {
providers.add(session.origin.provider.toLowerCase());
providers.add(normalizeLowercaseStringOrEmpty(session.origin.provider));
}
for (const entry of session.usage?.modelUsage ?? []) {
if (entry.provider) {
providers.add(entry.provider.toLowerCase());
providers.add(normalizeLowercaseStringOrEmpty(entry.provider));
}
}
return Array.from(providers);
@@ -126,18 +128,18 @@ const getSessionProviders = (session: UsageSessionQueryTarget): string[] => {
const getSessionModels = (session: UsageSessionQueryTarget): string[] => {
const models = new Set<string>();
if (session.model) {
models.add(session.model.toLowerCase());
models.add(normalizeLowercaseStringOrEmpty(session.model));
}
for (const entry of session.usage?.modelUsage ?? []) {
if (entry.model) {
models.add(entry.model.toLowerCase());
models.add(normalizeLowercaseStringOrEmpty(entry.model));
}
}
return Array.from(models);
};
const getSessionTools = (session: UsageSessionQueryTarget): string[] =>
(session.usage?.toolUsage?.tools ?? []).map((tool) => tool.name.toLowerCase());
(session.usage?.toolUsage?.tools ?? []).map((tool) => normalizeLowercaseStringOrEmpty(tool.name));
const matchesUsageQuery = (session: UsageSessionQueryTarget, term: UsageQueryTerm): boolean => {
const value = normalizeQueryText(term.value ?? "");
@@ -151,11 +153,11 @@ const matchesUsageQuery = (session: UsageSessionQueryTarget, term: UsageQueryTer
const key = normalizeQueryText(term.key);
switch (key) {
case "agent":
return session.agentId?.toLowerCase().includes(value) ?? false;
return normalizeLowercaseStringOrEmpty(session.agentId).includes(value);
case "channel":
return session.channel?.toLowerCase().includes(value) ?? false;
return normalizeLowercaseStringOrEmpty(session.channel).includes(value);
case "chat":
return session.chatType?.toLowerCase().includes(value) ?? false;
return normalizeLowercaseStringOrEmpty(session.chatType).includes(value);
case "provider":
return getSessionProviders(session).some((provider) => provider.includes(value));
case "model":
@@ -163,7 +165,7 @@ const matchesUsageQuery = (session: UsageSessionQueryTarget, term: UsageQueryTer
case "tool":
return getSessionTools(session).some((tool) => tool.includes(value));
case "label":
return session.label?.toLowerCase().includes(value) ?? false;
return normalizeLowercaseStringOrEmpty(session.label).includes(value);
case "key":
case "session":
case "id":
@@ -174,8 +176,8 @@ const matchesUsageQuery = (session: UsageSessionQueryTarget, term: UsageQueryTer
);
}
return (
session.key.toLowerCase().includes(value) ||
(session.sessionId?.toLowerCase().includes(value) ?? false)
normalizeLowercaseStringOrEmpty(session.key).includes(value) ||
normalizeLowercaseStringOrEmpty(session.sessionId).includes(value)
);
case "has":
switch (value) {
@@ -316,3 +318,4 @@ export function parseToolSummary(content: string) {
cleanContent: nonToolLines.join("\n").trim(),
};
}
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.ts";

View File

@@ -1,6 +1,7 @@
import { html, nothing } from "lit";
import { normalizeToolName } from "../../../../src/agents/tool-policy-shared.js";
import { t } from "../../i18n/index.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type {
SkillStatusEntry,
SkillStatusReport,
@@ -416,10 +417,12 @@ export function renderAgentSkills(params: {
const usingAllowlist = allowlist !== undefined;
const reportReady = Boolean(params.report && params.activeAgentId === params.agentId);
const rawSkills = reportReady ? (params.report?.skills ?? []) : [];
const filter = params.filter.trim().toLowerCase();
const filter = normalizeLowercaseStringOrEmpty(params.filter);
const filtered = filter
? rawSkills.filter((skill) =>
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
normalizeLowercaseStringOrEmpty(
[skill.name, skill.description, skill.source].join(" "),
).includes(filter),
)
: rawSkills;
const groups = groupSkills(filtered);

View File

@@ -4,6 +4,7 @@ import {
normalizeToolName,
resolveToolProfilePolicy,
} from "../../../../src/agents/tool-policy-shared.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type {
AgentIdentityResult,
AgentsFilesListResult,
@@ -586,7 +587,7 @@ export function buildModelOptions(
const seen = new Set<string>();
const options: ConfiguredModelOption[] = [];
const addOption = (value: string, label: string) => {
const key = value.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(value);
if (seen.has(key)) {
return;
}
@@ -607,7 +608,7 @@ export function buildModelOptions(
}
}
if (current && !seen.has(current.toLowerCase())) {
if (current && !seen.has(normalizeLowercaseStringOrEmpty(current))) {
options.unshift({ value: current, label: `Current (${current})` });
}

View File

@@ -31,6 +31,7 @@ import {
} from "../chat/slash-commands.ts";
import { isSttSupported, startStt, stopStt } from "../chat/speech.ts";
import { icons } from "../icons.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { detectTextDirection } from "../text-direction.ts";
import type { GatewaySessionRow, SessionsListResult } from "../types.ts";
import type { ChatItem, MessageGroup } from "../types/chat-types.ts";
@@ -495,12 +496,12 @@ function updateSlashMenu(value: string, requestUpdate: () => void): void {
// Arg mode: /command <partial-arg>
const argMatch = value.match(/^\/(\S+)\s(.*)$/);
if (argMatch) {
const cmdName = argMatch[1].toLowerCase();
const argFilter = argMatch[2].toLowerCase();
const cmdName = normalizeLowercaseStringOrEmpty(argMatch[1]);
const argFilter = normalizeLowercaseStringOrEmpty(argMatch[2]);
const cmd = SLASH_COMMANDS.find((c) => c.name === cmdName);
if (cmd?.argOptions?.length) {
const filtered = argFilter
? cmd.argOptions.filter((opt) => opt.toLowerCase().startsWith(argFilter))
? cmd.argOptions.filter((opt) => normalizeLowercaseStringOrEmpty(opt).startsWith(argFilter))
: cmd.argOptions;
if (filtered.length > 0) {
vs.slashMenuMode = "args";
@@ -1421,13 +1422,14 @@ function groupMessages(items: ChatItem[]): Array<ChatItem | MessageGroup> {
const normalized = normalizeMessage(item.message);
const role = normalizeRoleForGrouping(normalized.role);
const senderLabel = role.toLowerCase() === "user" ? (normalized.senderLabel ?? null) : null;
const senderLabel =
normalizeLowercaseStringOrEmpty(role) === "user" ? (normalized.senderLabel ?? null) : null;
const timestamp = normalized.timestamp || Date.now();
if (
!currentGroup ||
currentGroup.role !== role ||
(role.toLowerCase() === "user" && currentGroup.senderLabel !== senderLabel)
(normalizeLowercaseStringOrEmpty(role) === "user" && currentGroup.senderLabel !== senderLabel)
) {
if (currentGroup) {
result.push(currentGroup);
@@ -1486,7 +1488,7 @@ function buildChatItems(props: ChatProps): Array<ChatItem | MessageGroup> {
continue;
}
if (!props.showToolCalls && normalized.role.toLowerCase() === "toolresult") {
if (!props.showToolCalls && normalizeLowercaseStringOrEmpty(normalized.role) === "toolresult") {
continue;
}

View File

@@ -3,6 +3,7 @@ import { ref } from "lit/directives/ref.js";
import { t } from "../../i18n/index.ts";
import { SLASH_COMMANDS } from "../chat/slash-commands.ts";
import { icons, type IconName } from "../icons.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
type PaletteItem = {
id: string;
@@ -97,11 +98,11 @@ function filteredItems(query: string): PaletteItem[] {
if (!query) {
return PALETTE_ITEMS;
}
const q = query.toLowerCase();
const q = normalizeLowercaseStringOrEmpty(query);
return PALETTE_ITEMS.filter(
(item) =>
item.label.toLowerCase().includes(q) ||
(item.description?.toLowerCase().includes(q) ?? false),
normalizeLowercaseStringOrEmpty(item.label).includes(q) ||
normalizeLowercaseStringOrEmpty(item.description).includes(q),
);
}

View File

@@ -1,6 +1,10 @@
import { html, nothing, type TemplateResult } from "lit";
import { formatUnknownText } from "../format.ts";
import { icons as sharedIcons } from "../icons.ts";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "../string-coerce.ts";
import type { ConfigUiHints } from "../types.ts";
import {
defaultValue,
@@ -218,7 +222,7 @@ export function parseConfigSearchQuery(query: string): ConfigSearchCriteria {
const seen = new Set<string>();
const raw = query.trim();
const stripped = raw.replace(/(^|\s)tag:([^\s]+)/gi, (_, leading: string, token: string) => {
const normalized = token.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(token);
if (normalized && !seen.has(normalized)) {
seen.add(normalized);
tags.push(normalized);
@@ -226,7 +230,7 @@ export function parseConfigSearchQuery(query: string): ConfigSearchCriteria {
return leading;
});
return {
text: stripped.trim().toLowerCase(),
text: normalizeLowercaseStringOrEmpty(stripped),
tags,
};
}
@@ -245,7 +249,7 @@ function normalizeTags(raw: unknown): string[] {
if (!tag) {
continue;
}
const key = tag.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(tag);
if (seen.has(key)) {
continue;
}
@@ -277,7 +281,7 @@ function matchesText(text: string, candidates: Array<string | undefined>): boole
return true;
}
for (const candidate of candidates) {
if (candidate && candidate.toLowerCase().includes(text)) {
if (normalizeOptionalLowercaseString(candidate)?.includes(text)) {
return true;
}
}
@@ -288,7 +292,7 @@ function matchesTags(filterTags: string[], fieldTags: string[]): boolean {
if (filterTags.length === 0) {
return true;
}
const normalized = new Set(fieldTags.map((tag) => tag.toLowerCase()));
const normalized = new Set(fieldTags.map((tag) => normalizeLowercaseStringOrEmpty(tag)));
return filterTags.every((tag) => normalized.has(tag));
}

View File

@@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { icons } from "../icons.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { ConfigUiHints } from "../types.ts";
import { matchesNodeSearch, parseConfigSearchQuery, renderNode } from "./config-form.node.ts";
import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared.ts";
@@ -342,9 +343,9 @@ function matchesSearch(params: {
const meta = SECTION_META[params.key];
const sectionMetaMatches =
q &&
(params.key.toLowerCase().includes(q) ||
(meta?.label ? meta.label.toLowerCase().includes(q) : false) ||
(meta?.description ? meta.description.toLowerCase().includes(q) : false));
(normalizeLowercaseStringOrEmpty(params.key).includes(q) ||
(meta?.label ? normalizeLowercaseStringOrEmpty(meta.label).includes(q) : false) ||
(meta?.description ? normalizeLowercaseStringOrEmpty(meta.description).includes(q) : false));
if (sectionMetaMatches && criteria.tags.length === 0) {
return true;

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { ConfigUiHint, ConfigUiHints } from "../types.ts";
export type JsonSchema = {
@@ -125,7 +126,7 @@ function isEnvVarPlaceholder(value: string): boolean {
}
export function isSensitiveConfigPath(path: string): boolean {
const lowerPath = path.toLowerCase();
const lowerPath = normalizeLowercaseStringOrEmpty(path);
const whitelisted = SENSITIVE_KEY_WHITELIST_SUFFIXES.some((suffix) => lowerPath.endsWith(suffix));
return !whitelisted && SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
}

View File

@@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n/index.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { LogEntry, LogLevel } from "../types.ts";
const LEVELS: LogLevel[] = ["trace", "debug", "info", "warn", "error", "fatal"];
@@ -36,15 +37,14 @@ function matchesFilter(entry: LogEntry, needle: string) {
if (!needle) {
return true;
}
const haystack = [entry.message, entry.subsystem, entry.raw]
.filter(Boolean)
.join(" ")
.toLowerCase();
const haystack = normalizeLowercaseStringOrEmpty(
[entry.message, entry.subsystem, entry.raw].filter(Boolean).join(" "),
);
return haystack.includes(needle);
}
export function renderLogs(props: LogsProps) {
const needle = props.filterText.trim().toLowerCase();
const needle = normalizeLowercaseStringOrEmpty(props.filterText);
const levelFiltered = LEVELS.some((level) => !props.levelFilters[level]);
const filtered = props.entries.filter((entry) => {
if (entry.level && !props.levelFilters[entry.level]) {

View File

@@ -1,4 +1,5 @@
import { ConnectErrorDetailCodes } from "../../../../src/gateway/protocol/connect-error-details.js";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
const AUTH_REQUIRED_CODES = new Set<string>([
ConnectErrorDetailCodes.AUTH_REQUIRED,
@@ -40,7 +41,7 @@ export function shouldShowPairingHint(
if (lastErrorCode === ConnectErrorDetailCodes.PAIRING_REQUIRED) {
return true;
}
return lastError.toLowerCase().includes("pairing required");
return normalizeLowercaseStringOrEmpty(lastError).includes("pairing required");
}
/**
@@ -66,7 +67,7 @@ export function resolveAuthHintKind(params: {
return AUTH_REQUIRED_CODES.has(params.lastErrorCode) ? "required" : "failed";
}
const lower = params.lastError.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(params.lastError);
if (!lower.includes("unauthorized")) {
return null;
}
@@ -84,6 +85,6 @@ export function shouldShowInsecureContextHint(
if (lastErrorCode) {
return INSECURE_CONTEXT_CODES.has(lastErrorCode);
}
const lower = lastError.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(lastError);
return lower.includes("secure context") || lower.includes("device identity required");
}

View File

@@ -6,6 +6,7 @@ import { formatRelativeTimestamp, formatDurationHuman } from "../format.ts";
import type { GatewayHelloOk } from "../gateway.ts";
import { icons } from "../icons.ts";
import type { UiSettings } from "../storage.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type {
AttentionItem,
CronJob,
@@ -196,7 +197,7 @@ export function renderOverview(props: OverviewProps) {
if (props.connected || !props.lastError || !props.warnQueryToken) {
return null;
}
const lower = props.lastError.toLowerCase();
const lower = normalizeLowercaseStringOrEmpty(props.lastError);
const authFailed = lower.includes("unauthorized") || lower.includes("device identity required");
if (!authFailed) {
return null;

View File

@@ -4,6 +4,7 @@ import { formatRelativeTimestamp } from "../format.ts";
import { icons } from "../icons.ts";
import { pathForTab } from "../navigation.ts";
import { formatSessionTokens } from "../presenter.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type {
GatewaySessionRow,
SessionCompactionCheckpoint,
@@ -82,7 +83,7 @@ function normalizeProviderId(provider?: string | null): string {
if (!provider) {
return "";
}
const normalized = provider.trim().toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(provider);
if (normalized === "z.ai" || normalized === "z-ai") {
return "zai";
}
@@ -144,15 +145,15 @@ function resolveThinkLevelPatchValue(value: string, isBinary: boolean): string |
}
function filterRows(rows: GatewaySessionRow[], query: string): GatewaySessionRow[] {
const q = query.trim().toLowerCase();
const q = normalizeLowercaseStringOrEmpty(query);
if (!q) {
return rows;
}
return rows.filter((row) => {
const key = (row.key ?? "").toLowerCase();
const label = (row.label ?? "").toLowerCase();
const kind = (row.kind ?? "").toLowerCase();
const displayName = (row.displayName ?? "").toLowerCase();
const key = normalizeLowercaseStringOrEmpty(row.key);
const label = normalizeLowercaseStringOrEmpty(row.label);
const kind = normalizeLowercaseStringOrEmpty(row.kind);
const displayName = normalizeLowercaseStringOrEmpty(row.displayName);
return key.includes(q) || label.includes(q) || kind.includes(q) || displayName.includes(q);
});
}

View File

@@ -8,6 +8,7 @@ import type {
} from "../controllers/skills.ts";
import { clampText } from "../format.ts";
import { resolveSafeExternalUrl } from "../open-external-url.ts";
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import type { SkillStatusEntry, SkillStatusReport } from "../types.ts";
import { groupSkills } from "./skills-grouping.ts";
import {
@@ -114,10 +115,12 @@ export function renderSkills(props: SkillsProps) {
? skills
: skills.filter((s) => skillMatchesStatus(s, props.statusFilter));
const filter = props.filter.trim().toLowerCase();
const filter = normalizeLowercaseStringOrEmpty(props.filter);
const filtered = filter
? afterStatus.filter((skill) =>
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
normalizeLowercaseStringOrEmpty(
[skill.name, skill.description, skill.source].join(" "),
).includes(filter),
)
: afterStatus;
const groups = groupSkills(filtered);

View File

@@ -1,3 +1,4 @@
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
import { extractQueryTerms } from "../usage-helpers.ts";
import { CostDailyEntry, UsageAggregates, UsageSessionEntry } from "./usageTypes.ts";
@@ -138,8 +139,8 @@ const buildQuerySuggestions = (
? [lastToken.slice(0, lastToken.indexOf(":")), lastToken.slice(lastToken.indexOf(":") + 1)]
: ["", ""];
const key = rawKey.toLowerCase();
const value = rawValue.toLowerCase();
const key = normalizeLowercaseStringOrEmpty(rawKey);
const value = normalizeLowercaseStringOrEmpty(rawValue);
const unique = (items: Array<string | undefined>): string[] => {
const set = new Set<string>();
@@ -181,7 +182,7 @@ const buildQuerySuggestions = (
const suggestions: QuerySuggestion[] = [];
const addValues = (prefix: string, values: string[]) => {
for (const val of values) {
if (!value || val.toLowerCase().includes(value)) {
if (!value || normalizeLowercaseStringOrEmpty(val).includes(value)) {
suggestions.push({ label: `${prefix}:${val}`, value: `${prefix}:${val}` });
}
}
@@ -227,7 +228,7 @@ const applySuggestionToQuery = (query: string, suggestion: string): string => {
return `${tokens.join(" ")} `;
};
const normalizeQueryText = (value: string): string => value.trim().toLowerCase();
const normalizeQueryText = (value: string): string => normalizeLowercaseStringOrEmpty(value);
const addQueryToken = (query: string, token: string): string => {
const trimmed = query.trim();