Skip to content

Commit ef303a2

Browse files
committed
fix(ui): keep session/tool/card label truncation UTF-16 safe
Replace raw .slice(0, N) with truncateUtf16Safe in six UI label truncation paths that may contain emoji or CJK: - view-overview: session label @20 - view-details: session label @50 - tool-cards: preview text @120 - workboard: card title markup + segment sanitize @96 - custom-theme: theme name @80
1 parent 7c0a7c8 commit ef303a2

5 files changed

Lines changed: 52 additions & 19 deletions

File tree

ui/src/app/custom-theme.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
12
// Control UI module implements custom theme behavior.
23
import { z } from "zod";
34
import { normalizeOptionalString } from "../lib/string-coerce.ts";
@@ -429,7 +430,7 @@ function describeThemeLabel(value: string | undefined) {
429430
if (!normalized) {
430431
return "Custom";
431432
}
432-
return normalized.slice(0, 80);
433+
return truncateUtf16Safe(normalized, 80);
433434
}
434435

435436
export function normalizeTweakcnThemeUrl(input: string): TweakcnThemeResolution {

ui/src/lib/chat/tool-cards.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
12
// Control UI chat domain owns pure tool-card extraction rules.
23
import { extractCanvasFromText } from "../../../../src/chat/canvas-render.js";
34
import {
@@ -238,7 +239,7 @@ export function formatCollapsedToolPreviewText(value: string | undefined): strin
238239
if (!normalized) {
239240
return undefined;
240241
}
241-
return normalized.slice(0, 120);
242+
return truncateUtf16Safe(normalized, 120);
242243
}
243244

244245
function findFirstUnmatchedCard(

ui/src/lib/workboard/index.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
12
import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gateway.ts";
23
import type { GatewaySessionRow } from "../../api/types.ts";
34
import { requestSessionCreate } from "../sessions/index.ts";
@@ -2192,7 +2193,9 @@ async function loadWorkboardInternal(
21922193
taskRefreshError = confirmationResult.error;
21932194
}
21942195
nextUnfilteredCursor = pollResult?.nextUnfilteredCursor;
2195-
applyTaskSummariesToState(taskLinkState, taskSummaries, { missingTaskIds });
2196+
applyTaskSummariesToState(taskLinkState, taskSummaries, {
2197+
missingTaskIds,
2198+
});
21962199
preserveLifecycleTaskRefreshFailure =
21972200
params.taskRefresh === "linked" &&
21982201
state.lifecycleTaskRefreshFailed &&
@@ -3167,7 +3170,9 @@ async function refreshWorkboardLifecycleTasks(
31673170
}
31683171
resetWorkboardLifecycleTaskConfirmations(state, { host: params.host });
31693172
const recoveredTaskRefreshError = state.lifecycleTaskRefreshError;
3170-
setWorkboardLifecycleTaskRefreshFailed(state, false, { host: params.host });
3173+
setWorkboardLifecycleTaskRefreshFailed(state, false, {
3174+
host: params.host,
3175+
});
31713176
state.lifecycleTaskRefreshError = null;
31723177
if (
31733178
recoveredTaskRefreshError !== null &&
@@ -3584,7 +3589,9 @@ export async function deleteWorkboardCard(params: {
35843589
state.error = null;
35853590
params.requestUpdate?.();
35863591
try {
3587-
await params.client.request("workboard.cards.delete", { id: params.cardId });
3592+
await params.client.request("workboard.cards.delete", {
3593+
id: params.cardId,
3594+
});
35883595
state.cards = removeCardAndReferences(state.cards, params.cardId);
35893596
} catch (error) {
35903597
state.error = formatError(error);
@@ -3658,7 +3665,9 @@ export async function dispatchWorkboard(params: {
36583665
resetWorkboardLifecycleTaskConfirmations(state, { host: params.host });
36593666
try {
36603667
applyTaskSummariesToState(state, await listWorkboardTasks(params.client));
3661-
setWorkboardLifecycleTaskRefreshFailed(state, false, { host: params.host });
3668+
setWorkboardLifecycleTaskRefreshFailed(state, false, {
3669+
host: params.host,
3670+
});
36623671
state.lifecycleTaskRefreshError = null;
36633672
state.lastRefreshError = null;
36643673
} catch (error) {
@@ -3712,7 +3721,7 @@ function buildCardSessionLabel(card: WorkboardCard): string {
37123721
return `${title}${suffixText}`;
37133722
}
37143723
const titleMax = WORKBOARD_SESSION_LABEL_MAX_CHARS - suffixText.length;
3715-
return `${title.slice(0, titleMax - 3).trimEnd()}...${suffixText}`;
3724+
return `${truncateUtf16Safe(title, titleMax - 3).trimEnd()}...${suffixText}`;
37163725
}
37173726

37183727
function sanitizeSessionSegment(value: string | undefined, fallback: string): string {
@@ -3721,7 +3730,7 @@ function sanitizeSessionSegment(value: string | undefined, fallback: string): st
37213730
.replace(/[^a-zA-Z0-9_-]/g, "-")
37223731
.replace(/-+/g, "-")
37233732
.replace(/^-|-$/g, "");
3724-
return (sanitized || fallback).slice(0, 96);
3733+
return truncateUtf16Safe(sanitized || fallback, 96);
37253734
}
37263735

37273736
function buildCardTaskSessionKey(card: WorkboardCard): string {
@@ -3839,7 +3848,11 @@ function taskIsActive(task: WorkboardTaskSummary | undefined): task is Workboard
38393848
async function cancelWorkboardTaskRun(params: {
38403849
client: GatewayBrowserClient;
38413850
taskId: string;
3842-
}): Promise<{ cancelled: boolean; missing: boolean; task: WorkboardTaskSummary | null }> {
3851+
}): Promise<{
3852+
cancelled: boolean;
3853+
missing: boolean;
3854+
task: WorkboardTaskSummary | null;
3855+
}> {
38433856
const result = await params.client.request("tasks.cancel", {
38443857
taskId: params.taskId,
38453858
reason: "Stopped from Workboard.",

ui/src/pages/usage/view-details.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
12
// Control UI view renders usage render details screen content.
23
import { html, svg, nothing } from "lit";
34
import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts";
4-
import { t } from "../../i18n/index.ts";
55
import "../../components/tooltip.ts";
6+
import { t } from "../../i18n/index.ts";
67
import { formatDateTimeMs, formatMs, formatTimeMs } from "../../lib/format.ts";
78
import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts";
89
import { parseToolSummary } from "./helpers.ts";
@@ -257,7 +258,7 @@ function renderSessionDetailPanel(
257258
onClose: () => void,
258259
) {
259260
const label = session.label || session.key;
260-
const displayLabel = label.length > 50 ? label.slice(0, 50) + "…" : label;
261+
const displayLabel = label.length > 50 ? truncateUtf16Safe(label, 50) + "…" : label;
261262
const usage = session.usage;
262263

263264
const hasRange = timeSeriesCursorStart !== null && timeSeriesCursorEnd !== null;
@@ -266,8 +267,14 @@ function renderSessionDetailPanel(
266267
? computeFilteredUsage(usage, timeSeries.points, timeSeriesCursorStart, timeSeriesCursorEnd)
267268
: undefined;
268269
const headerStats = filteredUsage
269-
? { totalTokens: filteredUsage.totalTokens, totalCost: filteredUsage.totalCost }
270-
: { totalTokens: usage?.totalTokens ?? 0, totalCost: usage?.totalCost ?? 0 };
270+
? {
271+
totalTokens: filteredUsage.totalTokens,
272+
totalCost: filteredUsage.totalCost,
273+
}
274+
: {
275+
totalTokens: usage?.totalTokens ?? 0,
276+
totalCost: usage?.totalCost ?? 0,
277+
};
271278
const cursorIndicator = filteredUsage ? t("usage.details.filtered") : "";
272279

273280
return html`

ui/src/pages/usage/view-overview.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
12
// Control UI view renders usage render overview screen content.
23
import { html, nothing } from "lit";
34
import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts";
4-
import { t } from "../../i18n/index.ts";
55
import "../../components/tooltip.ts";
6+
import { t } from "../../i18n/index.ts";
67
import { normalizeLowercaseStringOrEmpty } from "../../lib/string-coerce.ts";
78
import {
89
buildUsageCostWindows,
@@ -96,11 +97,13 @@ function renderFilterChips(
9697
const selectedSession =
9798
selectedSessions.length === 1 ? sessions.find((s) => s.key === selectedSessions[0]) : null;
9899
const sessionsLabel = selectedSession
99-
? (selectedSession.label || selectedSession.key).slice(0, 20) +
100+
? truncateUtf16Safe(selectedSession.label || selectedSession.key, 20) +
100101
((selectedSession.label || selectedSession.key).length > 20 ? "…" : "")
101102
: selectedSessions.length === 1
102103
? selectedSessions[0].slice(0, 8) + "…"
103-
: t("usage.filters.sessionsCount", { count: String(selectedSessions.length) });
104+
: t("usage.filters.sessionsCount", {
105+
count: String(selectedSessions.length),
106+
});
104107
const sessionsFullName = selectedSession
105108
? selectedSession.label || selectedSession.key
106109
: selectedSessions.length === 1
@@ -196,7 +199,11 @@ function renderCostWindowComparison(
196199
return t("usage.costWindows.lastDays", { count: String(days) });
197200
};
198201
const cards = [
199-
{ label: t("usage.costWindows.selectedRange"), summary: range, range: true },
202+
{
203+
label: t("usage.costWindows.selectedRange"),
204+
summary: range,
205+
range: true,
206+
},
200207
...windows.map((summary) => ({
201208
label: labelForWindow(summary.days, summary.endDate),
202209
summary,
@@ -687,7 +694,9 @@ function renderUsageInsights(
687694

688695
const costShare = (cost: number) =>
689696
showCostShares && totals.totalCost > 0
690-
? t("usage.overview.costShare", { percent: ((cost / totals.totalCost) * 100).toFixed(1) })
697+
? t("usage.overview.costShare", {
698+
percent: ((cost / totals.totalCost) * 100).toFixed(1),
699+
})
691700
: null;
692701
const costAttributionSub = (cost: number, tokens: number, messageCount?: number) =>
693702
[
@@ -787,7 +796,9 @@ function renderUsageInsights(
787796
title: t("usage.overview.sessions"),
788797
hint: t("usage.overview.sessionsHint"),
789798
value: sessionCount,
790-
sub: t("usage.overview.sessionsInRange", { count: String(totalSessions) }),
799+
sub: t("usage.overview.sessionsInRange", {
800+
count: String(totalSessions),
801+
}),
791802
className: "usage-summary-card--compact",
792803
})}
793804
${renderSummaryStat({

0 commit comments

Comments
 (0)