refactor: dedupe core lowercase helpers

This commit is contained in:
Peter Steinberger
2026-04-07 20:45:35 +01:00
parent dffa88f396
commit bfff74fb11
19 changed files with 103 additions and 63 deletions

View File

@@ -2,6 +2,7 @@ import type { SlashCommand } from "@mariozechner/pi-tui";
import { listChatCommands, listChatCommandsForConfig } from "../auto-reply/commands-registry.js";
import { formatThinkingLevels, listThinkingLevelLabels } from "../auto-reply/thinking.js";
import type { OpenClawConfig } from "../config/types.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
const VERBOSE_LEVELS = ["on", "off"];
const FAST_LEVELS = ["status", "on", "off"];
@@ -31,7 +32,7 @@ function createLevelCompletion(
): NonNullable<SlashCommand["getArgumentCompletions"]> {
return (prefix) =>
levels
.filter((value) => value.startsWith(prefix.toLowerCase()))
.filter((value) => value.startsWith(normalizeLowercaseStringOrEmpty(prefix)))
.map((value) => ({
value,
label: value,
@@ -44,7 +45,7 @@ export function parseCommand(input: string): ParsedCommand {
return { name: "", args: "" };
}
const [name, ...rest] = trimmed.split(/\s+/);
const normalized = name.toLowerCase();
const normalized = normalizeLowercaseStringOrEmpty(name);
return {
name: COMMAND_ALIASES[normalized] ?? normalized,
args: rest.join(" ").trim(),
@@ -77,7 +78,7 @@ export function getSlashCommands(options: SlashCommandOptions = {}): SlashComman
description: "Set thinking level",
getArgumentCompletions: (prefix) =>
thinkLevels
.filter((v) => v.startsWith(prefix.toLowerCase()))
.filter((v) => v.startsWith(normalizeLowercaseStringOrEmpty(prefix)))
.map((value) => ({ value, label: value })),
},
{

View File

@@ -7,6 +7,7 @@ import {
type SelectListTheme,
} from "@mariozechner/pi-tui";
import chalk from "chalk";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { fuzzyFilterLower, prepareSearchItems } from "./fuzzy-filter.js";
export interface FilterableSelectItem extends SelectItem {
@@ -44,7 +45,7 @@ export class FilterableSelectList implements Component {
}
private applyFilter(): void {
const queryLower = this.filterText.toLowerCase();
const queryLower = normalizeLowercaseStringOrEmpty(this.filterText);
if (!queryLower.trim()) {
this.selectList = new SelectList(this.allItems, this.maxVisible, this.theme);
return;

View File

@@ -2,6 +2,8 @@
* Shared fuzzy filtering utilities for select list components.
*/
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
/**
* Word boundary characters for matching.
*/
@@ -22,8 +24,8 @@ export function findWordBoundaryIndex(text: string, query: string): number | nul
if (!query) {
return null;
}
const textLower = text.toLowerCase();
const queryLower = query.toLowerCase();
const textLower = normalizeLowercaseStringOrEmpty(text);
const queryLower = normalizeLowercaseStringOrEmpty(query);
const maxIndex = textLower.length - queryLower.length;
if (maxIndex < 0) {
return null;
@@ -133,6 +135,6 @@ export function prepareSearchItems<
if (item.searchText) {
parts.push(item.searchText);
}
return { ...item, searchTextLower: parts.join(" ").toLowerCase() };
return { ...item, searchTextLower: normalizeLowercaseStringOrEmpty(parts.join(" ")) };
});
}

View File

@@ -7,6 +7,7 @@ import {
type SelectListTheme,
truncateToWidth,
} from "@mariozechner/pi-tui";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { stripAnsi, visibleWidth } from "../../terminal/ansi.js";
import { findWordBoundaryIndex, fuzzyFilterLower } from "./fuzzy-filter.js";
@@ -80,7 +81,7 @@ export class SearchableSelectList implements Component {
* 4. Fuzzy match (lowest priority)
*/
private smartFilter(query: string): SelectItem[] {
const q = query.toLowerCase();
const q = normalizeLowercaseStringOrEmpty(query);
type ScoredItem = { item: SelectItem; tier: number; score: number };
type FuzzyCandidate = { item: SelectItem; searchTextLower: string };
const scoredItems: ScoredItem[] = [];
@@ -89,8 +90,8 @@ export class SearchableSelectList implements Component {
for (const item of this.items) {
const rawLabel = this.getItemLabel(item);
const rawDesc = item.description ?? "";
const label = stripAnsi(rawLabel).toLowerCase();
const desc = stripAnsi(rawDesc).toLowerCase();
const label = normalizeLowercaseStringOrEmpty(stripAnsi(rawLabel));
const desc = normalizeLowercaseStringOrEmpty(stripAnsi(rawDesc));
// Tier 1: Exact substring in label
const labelIndex = label.indexOf(q);
@@ -114,11 +115,12 @@ export class SearchableSelectList implements Component {
const searchText = (item as { searchText?: string }).searchText ?? "";
fuzzyCandidates.push({
item,
searchTextLower: [rawLabel, rawDesc, searchText]
.map((value) => stripAnsi(value))
.filter(Boolean)
.join(" ")
.toLowerCase(),
searchTextLower: normalizeLowercaseStringOrEmpty(
[rawLabel, rawDesc, searchText]
.map((value) => stripAnsi(value))
.filter(Boolean)
.join(" "),
),
});
}
@@ -171,7 +173,7 @@ export class SearchableSelectList implements Component {
const tokens = query
.trim()
.split(/\s+/)
.map((token) => token.toLowerCase())
.map((token) => normalizeLowercaseStringOrEmpty(token))
.filter((token) => token.length > 0);
if (tokens.length === 0) {
return text;

View File

@@ -1,3 +1,5 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export function createEditorSubmitHandler(params: {
editor: {
setText: (value: string) => void;
@@ -44,7 +46,7 @@ export function shouldEnableWindowsGitBashPasteFallback(params?: {
}): boolean {
const platform = params?.platform ?? process.platform;
const env = params?.env ?? process.env;
const termProgram = (env.TERM_PROGRAM ?? "").toLowerCase();
const termProgram = normalizeLowercaseStringOrEmpty(env.TERM_PROGRAM);
// Some macOS terminals emit multiline paste as rapid single-line submits.
// Enable burst coalescing so pasted blocks stay as one user message.
@@ -64,7 +66,7 @@ export function shouldEnableWindowsGitBashPasteFallback(params?: {
if (msystem.startsWith("MINGW") || msystem.startsWith("MSYS")) {
return true;
}
if (shell.toLowerCase().includes("bash")) {
if (normalizeLowercaseStringOrEmpty(shell).includes("bash")) {
return true;
}
return termProgram.includes("mintty");

View File

@@ -16,6 +16,7 @@ import {
normalizeMainKey,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { getSlashCommands } from "./commands.js";
import { ChatLog } from "./components/chat-log.js";
import { CustomEditor } from "./components/custom-editor.js";
@@ -69,9 +70,9 @@ export function resolveTuiSessionKey(params: {
return trimmed;
}
if (trimmed.startsWith("agent:")) {
return trimmed.toLowerCase();
return normalizeLowercaseStringOrEmpty(trimmed);
}
return `agent:${params.currentAgentId}:${trimmed.toLowerCase()}`;
return `agent:${params.currentAgentId}:${normalizeLowercaseStringOrEmpty(trimmed)}`;
}
export function resolveInitialTuiAgentId(params: {