google-gemini / google-gemini/gemini-cli

sanitizeForDisplay can drop an emoji when its truncation limit lands inside a surrogate pair

Open Beginner friendly
#29,301 2 comments 0 reactions 0 assignees View on GitHub
area/core effort/small status/bot-triaged
Dominant language
TypeScript
Stars
107k
Forks
14.6k
Avg merge
2d 3h
Merged PRs (30d)
45

Description

### What happened?

`sanitizeForDisplay` truncates by UTF-16 code units, so when its limit lands inside a surrogate pair it emits an unpaired surrogate and the rendered emoji is silently dropped. This is the same failure class as #29296 (which covers `ExpandableText`'s own slicing), but on the sibling code path used for suggestion descriptions, help text, command-loader descriptions, and terminal notifications.

In `packages/cli/src/ui/utils/textUtils.ts` (lines 158-160 at revision `9c1b0a610`):

```ts
if (maxLength && sanitized.length > maxLength) {
sanitized = sanitized.substring(0, maxLength - 3) + '...';
}
```

`length` and `substring` are UTF-16 based. Pure-function repro:

```js
const s = 'a'.repeat(96) + '😀' + 'b'.repeat(50);
const cut = s.substring(0, 97); // what maxLength 100 produces
cut.charCodeAt(96).toString(16); // 'd83d' — an unpaired high surrogate
```

Component-level confirmation (rendering `SuggestionsDisplay` in slash mode with a suggestion whose `description` is `'a'.repeat(96) + '😀' + 'b'.repeat(50)`): the rendered frame contains the 96 `a`s and the `...` marker, but no `😀` at all — the TUI silently omits the emoji, exactly the symptom described in #29296. A short description like `hello 😀 world` (no truncation) renders the emoji fine, so the loss is caused by the truncation cut, not by the sanitizer's character stripping.

Affected call sites that pass a `maxLength`:

- `packages/cli/src/ui/components/SuggestionsDisplay.tsx` — description column, `maxLength` 100
- `packages/cli/src/ui/components/Help.tsx` — two call sites, `maxLength` 100
- `packages/cli/src/services/FileCommandLoader.ts` — command descriptions, `maxLength` 100
- `packages/cli/src/utils/terminalNotifications.ts` — notification title/subtitle/body limits

Call sites without `maxLength` are unaffected.

### What did you expect to happen?

Truncation should stop at a code-point boundary: the cut should back off so a surrogate pair is never split, and the function should never emit an unpaired surrogate to Ink. Ideally the complete emoji is retained before the `...` marker, as requested for `ExpandableText` in #29296.

### Client information

Component-level reproduction; no Gemini API request, login session, or account is involved. Verified against a working tree based on main revision `9c1b0a610534d6f8120964cf2672c07807d8fc90`; the files cited above are unmodified from that revision. Package version reports `0.61.0-nightly.20260908.gc647533d6`.

### Login information

Not applicable. No provider, API key, Google account, or network request is used by the reproducer.

### Anything else we need to know?

The same module already exports code-point helpers (`cpLen`, `cpSlice`), so a minimal fix is:

```ts
if (maxLength && cpLen(sanitized) > maxLength) {
sanitized = cpSlice(sanitized, 0, maxLength - 3) + '...';
}
```

This is separate from #29296, which is fixed in the `ExpandableText` label path only; `sanitizeForDisplay` is untouched by that change.

Contributor guide

Open the contributing guide

Research direction

Start in packages/cli/src/ui/utils/textUtils.ts at sanitizeForDisplay and review the existing cpLen and cpSlice helpers. Check the affected call sites in SuggestionsDisplay.tsx, Help.tsx, FileCommandLoader.ts, and terminalNotifications.ts; done means truncation never splits a surrogate pair and the reproduced emoji remains before the ellipsis.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.