Skip to content

Commit 5b5ccb0

Browse files
fix(ui): avoid toSorted in cron suggestions (openclaw#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 <[email protected]>
1 parent 0743463 commit 5b5ccb0

File tree

4 files changed

+55
-6
lines changed

4 files changed

+55
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Docs: https://docs.openclaw.ai
8181
- 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.
8282
- 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)
8383
- 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.
84+
- 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.
8485
- 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.
8586
- 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.
8687
- 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.

ui/src/ui/app-render.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ import {
6666
import { buildExternalLinkRel, EXTERNAL_LINK_TARGET } from "./external-link.ts";
6767
import { icons } from "./icons.ts";
6868
import { normalizeBasePath, TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts";
69-
import { resolveConfiguredCronModelSuggestions } from "./views/agents-utils.ts";
69+
import { resolveConfiguredCronModelSuggestions, sortLocaleStrings } from "./views/agents-utils.ts";
7070
import { renderAgents } from "./views/agents.ts";
7171
import { renderChannels } from "./views/channels.ts";
7272
import { renderChat } from "./views/chat.ts";
@@ -166,7 +166,7 @@ export function renderApp(state: AppViewState) {
166166
state.agentsList?.defaultId ??
167167
state.agentsList?.agents?.[0]?.id ??
168168
null;
169-
const cronAgentSuggestions = Array.from(
169+
const cronAgentSuggestions = sortLocaleStrings(
170170
new Set(
171171
[
172172
...(state.agentsList?.agents?.map((entry) => entry.id.trim()) ?? []),
@@ -175,8 +175,8 @@ export function renderApp(state: AppViewState) {
175175
.filter(Boolean),
176176
].filter(Boolean),
177177
),
178-
).toSorted((a, b) => a.localeCompare(b));
179-
const cronModelSuggestions = Array.from(
178+
);
179+
const cronModelSuggestions = sortLocaleStrings(
180180
new Set(
181181
[
182182
...state.cronModelSuggestions,
@@ -191,7 +191,7 @@ export function renderApp(state: AppViewState) {
191191
.filter(Boolean),
192192
].filter(Boolean),
193193
),
194-
).toSorted((a, b) => a.localeCompare(b));
194+
);
195195
const visibleCronJobs = getVisibleCronJobs(state);
196196
const selectedDeliveryChannel =
197197
state.cronForm.deliveryChannel && state.cronForm.deliveryChannel.trim()

ui/src/ui/views/agents-utils.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
resolveConfiguredCronModelSuggestions,
44
resolveEffectiveModelFallbacks,
5+
sortLocaleStrings,
56
} from "./agents-utils.ts";
67

78
describe("resolveEffectiveModelFallbacks", () => {
@@ -87,3 +88,13 @@ describe("resolveConfiguredCronModelSuggestions", () => {
8788
);
8889
});
8990
});
91+
92+
describe("sortLocaleStrings", () => {
93+
it("sorts values using localeCompare without relying on Array.prototype.toSorted", () => {
94+
expect(sortLocaleStrings(["z", "b", "a"])).toEqual(["a", "b", "z"]);
95+
});
96+
97+
it("accepts any iterable input, including sets", () => {
98+
expect(sortLocaleStrings(new Set(["beta", "alpha"]))).toEqual(["alpha", "beta"]);
99+
});
100+
});

ui/src/ui/views/agents-utils.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,43 @@ function addModelConfigIds(target: Set<string>, modelConfig: unknown) {
288288
}
289289
}
290290

291+
export function sortLocaleStrings(values: Iterable<string>): string[] {
292+
const sorted = Array.from(values);
293+
const buffer = new Array<string>(sorted.length);
294+
295+
const merge = (left: number, middle: number, right: number): void => {
296+
let i = left;
297+
let j = middle;
298+
let k = left;
299+
while (i < middle && j < right) {
300+
buffer[k++] = sorted[i].localeCompare(sorted[j]) <= 0 ? sorted[i++] : sorted[j++];
301+
}
302+
while (i < middle) {
303+
buffer[k++] = sorted[i++];
304+
}
305+
while (j < right) {
306+
buffer[k++] = sorted[j++];
307+
}
308+
for (let idx = left; idx < right; idx += 1) {
309+
sorted[idx] = buffer[idx];
310+
}
311+
};
312+
313+
const sortRange = (left: number, right: number): void => {
314+
if (right - left <= 1) {
315+
return;
316+
}
317+
318+
const middle = (left + right) >>> 1;
319+
sortRange(left, middle);
320+
sortRange(middle, right);
321+
merge(left, middle, right);
322+
};
323+
324+
sortRange(0, sorted.length);
325+
return sorted;
326+
}
327+
291328
export function resolveConfiguredCronModelSuggestions(
292329
configForm: Record<string, unknown> | null,
293330
): string[] {
@@ -319,7 +356,7 @@ export function resolveConfiguredCronModelSuggestions(
319356
addModelConfigIds(out, (entry as Record<string, unknown>).model);
320357
}
321358
}
322-
return [...out].toSorted((a, b) => a.localeCompare(b));
359+
return sortLocaleStrings(out);
323360
}
324361

325362
export function parseFallbackList(value: string): string[] {

0 commit comments

Comments
 (0)