diff --git a/CHANGELOG.md b/CHANGELOG.md index f49f2b6471e..dc1e8be6293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 7bf0665de79..97b2271b1bf 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -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() diff --git a/ui/src/ui/views/agents-utils.test.ts b/ui/src/ui/views/agents-utils.test.ts index 56f2cf6ef73..eea9bec03c8 100644 --- a/ui/src/ui/views/agents-utils.test.ts +++ b/ui/src/ui/views/agents-utils.test.ts @@ -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"]); + }); +}); diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts index 9c3f18c355d..436c5c337c1 100644 --- a/ui/src/ui/views/agents-utils.ts +++ b/ui/src/ui/views/agents-utils.ts @@ -288,6 +288,43 @@ function addModelConfigIds(target: Set, modelConfig: unknown) { } } +export function sortLocaleStrings(values: Iterable): string[] { + const sorted = Array.from(values); + const buffer = new Array(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 | null, ): string[] { @@ -319,7 +356,7 @@ export function resolveConfiguredCronModelSuggestions( addModelConfigIds(out, (entry as Record).model); } } - return [...out].toSorted((a, b) => a.localeCompare(b)); + return sortLocaleStrings(out); } export function parseFallbackList(value: string): string[] {