From 63c2da2d3864bc821773e981de1285ae6bb8f39a Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Thu, 10 Sep 2026 01:11:39 -0700 Subject: [PATCH] refactor(ai-vault): move the surrogate-safe slice to shared, one implementation `sliceAtCodeUnitLimit` lived in src/main/ai-vault, and src/shared never imports src/main, so a shared consumer could not reach it. Rather than add a second copy, it moves to src/shared and ai-vault imports and re-exports it, leaving every existing importer of that module untouched. Separated from the feature that needs it: this is the only change here to a subsystem the rest of the branch does not touch. --- .../session-scanner-text-normalization.ts | 13 ++++--------- src/shared/surrogate-safe-text-slice.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 src/shared/surrogate-safe-text-slice.ts diff --git a/src/main/ai-vault/session-scanner-text-normalization.ts b/src/main/ai-vault/session-scanner-text-normalization.ts index 99f73a93d02..50fc54ab7aa 100644 --- a/src/main/ai-vault/session-scanner-text-normalization.ts +++ b/src/main/ai-vault/session-scanner-text-normalization.ts @@ -1,3 +1,7 @@ +import { sliceAtCodeUnitLimit } from '../../shared/surrogate-safe-text-slice' + +export { sliceAtCodeUnitLimit } + const SESSION_TITLE_TEXT_LIMIT = 96 const SESSION_PREVIEW_TEXT_LIMIT = 220 const ELLIPSIS = '...' @@ -42,15 +46,6 @@ export function normalizePreviewText(value: string): string | null { return finalizeNormalizedText(normalizeStringText(value, SESSION_PREVIEW_TEXT_LIMIT)) } -/** Cut to `limit` UTF-16 code units without splitting a trailing surrogate pair. */ -export function sliceAtCodeUnitLimit(value: string, limit: number): string { - if (value.length <= limit) { - return value - } - const end = limit > 0 && isHighSurrogate(value.charCodeAt(limit - 1)) ? limit - 1 : limit - return value.slice(0, end) -} - function normalizeContentText(value: unknown, limit: number): string | null { if (typeof value === 'string') { return finalizeNormalizedText(normalizeStringText(value, limit)) diff --git a/src/shared/surrogate-safe-text-slice.ts b/src/shared/surrogate-safe-text-slice.ts new file mode 100644 index 00000000000..6e9e2d8d030 --- /dev/null +++ b/src/shared/surrogate-safe-text-slice.ts @@ -0,0 +1,19 @@ +// Cutting text to a length bound without splitting a character in half. +// +// JavaScript string length counts UTF-16 code units, so a raw `slice` at a +// bound can land between the two halves of an astral character — an emoji, or +// most CJK extension characters — and leave a lone surrogate that every surface +// renders as U+FFFD. + +/** Cut to `limit` UTF-16 code units without splitting a trailing surrogate pair. */ +export function sliceAtCodeUnitLimit(value: string, limit: number): string { + if (value.length <= limit) { + return value + } + const end = limit > 0 && isHighSurrogate(value.charCodeAt(limit - 1)) ? limit - 1 : limit + return value.slice(0, end) +} + +function isHighSurrogate(code: number): boolean { + return code >= 0xd800 && code <= 0xdbff +}