fix(ui): avoid toSorted in cron suggestions (#31775)
* Control UI: avoid toSorted in cron suggestions * Control UI: make sortLocaleStrings legacy-safe * fix(ui): use sort fallback in locale string helper * fix(ui): remove toSorted from locale helper * fix(ui): remove toSorted from locale helper * fix(ui): remove toSorted from locale helper * fix(ui): remove toSorted from locale helper * fix(ui): remove toSorted from locale helper * fix(ui): avoid sort in locale helper for browser compatibility * ui: avoid unnecessary assertions in locale sort * changelog: credit browser-compat cron fix PR * fix(ui): use native locale sort in compatibility helper * ui: use compat merge-sort for locale strings * style: format locale sort helper * style: fix oxfmt ordering in agents utils --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Subagents `sessions_spawn`: reject malformed `agentId` inputs before normalization (for example error-message/path-like strings) to prevent unintended synthetic agent IDs and ghost workspace/session paths; includes strict validation regression coverage. (#31381) Thanks @openperf.
|
||||
- Webchat/Feishu session continuation: preserve routable `OriginatingChannel`/`OriginatingTo` metadata from session delivery context in `chat.send`, and prefer provider-normalized channel when deciding cross-channel route dispatch so Webchat replies continue on the selected Feishu session instead of falling back to main/internal session routing. (#31573)
|
||||
- Pairing/AllowFrom account fallback: handle omitted `accountId` values in `readChannelAllowFromStore` and `readChannelAllowFromStoreSync` as `default`, while preserving legacy unscoped allowFrom merges for default-account flows. Thanks @Sid-Qin and @vincentkoc.
|
||||
- Control UI/Legacy browser compatibility: replace `toSorted`-dependent cron suggestion sorting in `app-render` with a compatibility helper so older browsers without `Array.prototype.toSorted` no longer white-screen. (#31775) Thanks @liuxiaopai-ai.
|
||||
- Agents/Sandbox workdir mapping: map container workdir paths (for example `/workspace`) back to the host workspace before sandbox path validation so exec requests keep the intended directory in containerized runs instead of falling back to an unavailable host path. (#31841) Thanks @liuxiaopai-ai.
|
||||
- Agents/Subagent announce cleanup: keep completion-message runs pending while descendants settle, add a 30 minute hard-expiry backstop to avoid indefinite pending state, and keep retry bookkeeping resumable across deferred wakes. (#23970) Thanks @tyler6204.
|
||||
- Gateway/Control UI method guard: allow POST requests to non-UI routes to fall through when no base path is configured, and add POST regression coverage for fallthrough and base-path 405 behavior. (#23970) Thanks @tyler6204.
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
|
||||
import { resolveConfiguredCronModelSuggestions } from "./views/agents-utils.ts";
|
||||
import { resolveConfiguredCronModelSuggestions, sortLocaleStrings } from "./views/agents-utils.ts";
|
||||
import { renderAgents } from "./views/agents.ts";
|
||||
import { renderChannels } from "./views/channels.ts";
|
||||
import { renderChat } from "./views/chat.ts";
|
||||
@@ -166,7 +166,7 @@ export function renderApp(state: AppViewState) {
|
||||
state.agentsList?.defaultId ??
|
||||
state.agentsList?.agents?.[0]?.id ??
|
||||
null;
|
||||
const cronAgentSuggestions = Array.from(
|
||||
const cronAgentSuggestions = sortLocaleStrings(
|
||||
new Set(
|
||||
[
|
||||
...(state.agentsList?.agents?.map((entry) => entry.id.trim()) ?? []),
|
||||
@@ -175,8 +175,8 @@ export function renderApp(state: AppViewState) {
|
||||
.filter(Boolean),
|
||||
].filter(Boolean),
|
||||
),
|
||||
).toSorted((a, b) => a.localeCompare(b));
|
||||
const cronModelSuggestions = Array.from(
|
||||
);
|
||||
const cronModelSuggestions = sortLocaleStrings(
|
||||
new Set(
|
||||
[
|
||||
...state.cronModelSuggestions,
|
||||
@@ -191,7 +191,7 @@ export function renderApp(state: AppViewState) {
|
||||
.filter(Boolean),
|
||||
].filter(Boolean),
|
||||
),
|
||||
).toSorted((a, b) => a.localeCompare(b));
|
||||
);
|
||||
const visibleCronJobs = getVisibleCronJobs(state);
|
||||
const selectedDeliveryChannel =
|
||||
state.cronForm.deliveryChannel && state.cronForm.deliveryChannel.trim()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveConfiguredCronModelSuggestions,
|
||||
resolveEffectiveModelFallbacks,
|
||||
sortLocaleStrings,
|
||||
} from "./agents-utils.ts";
|
||||
|
||||
describe("resolveEffectiveModelFallbacks", () => {
|
||||
@@ -87,3 +88,13 @@ describe("resolveConfiguredCronModelSuggestions", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortLocaleStrings", () => {
|
||||
it("sorts values using localeCompare without relying on Array.prototype.toSorted", () => {
|
||||
expect(sortLocaleStrings(["z", "b", "a"])).toEqual(["a", "b", "z"]);
|
||||
});
|
||||
|
||||
it("accepts any iterable input, including sets", () => {
|
||||
expect(sortLocaleStrings(new Set(["beta", "alpha"]))).toEqual(["alpha", "beta"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -288,6 +288,43 @@ function addModelConfigIds(target: Set<string>, modelConfig: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export function sortLocaleStrings(values: Iterable<string>): string[] {
|
||||
const sorted = Array.from(values);
|
||||
const buffer = new Array<string>(sorted.length);
|
||||
|
||||
const merge = (left: number, middle: number, right: number): void => {
|
||||
let i = left;
|
||||
let j = middle;
|
||||
let k = left;
|
||||
while (i < middle && j < right) {
|
||||
buffer[k++] = sorted[i].localeCompare(sorted[j]) <= 0 ? sorted[i++] : sorted[j++];
|
||||
}
|
||||
while (i < middle) {
|
||||
buffer[k++] = sorted[i++];
|
||||
}
|
||||
while (j < right) {
|
||||
buffer[k++] = sorted[j++];
|
||||
}
|
||||
for (let idx = left; idx < right; idx += 1) {
|
||||
sorted[idx] = buffer[idx];
|
||||
}
|
||||
};
|
||||
|
||||
const sortRange = (left: number, right: number): void => {
|
||||
if (right - left <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const middle = (left + right) >>> 1;
|
||||
sortRange(left, middle);
|
||||
sortRange(middle, right);
|
||||
merge(left, middle, right);
|
||||
};
|
||||
|
||||
sortRange(0, sorted.length);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export function resolveConfiguredCronModelSuggestions(
|
||||
configForm: Record<string, unknown> | null,
|
||||
): string[] {
|
||||
@@ -319,7 +356,7 @@ export function resolveConfiguredCronModelSuggestions(
|
||||
addModelConfigIds(out, (entry as Record<string, unknown>).model);
|
||||
}
|
||||
}
|
||||
return [...out].toSorted((a, b) => a.localeCompare(b));
|
||||
return sortLocaleStrings(out);
|
||||
}
|
||||
|
||||
export function parseFallbackList(value: string): string[] {
|
||||
|
||||
Reference in New Issue
Block a user