mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
refactor: dedupe ui lowercase helpers
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(" ");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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("/")}` : "";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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:");
|
||||
}
|
||||
|
||||
15
ui/src/ui/string-coerce.ts
Normal file
15
ui/src/ui/string-coerce.ts
Normal 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) ?? "";
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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})` });
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user