Cut a roster line between graphemes, not between code points - #455
Merged
Conversation
`oneLine` promises in its own docstring that "an emoji is never split", and
cuts with `Array.from`, which walks code points. That holds only for an emoji
that is one code point. Measured on `main`:
oneLine("<KR flag> report", 2) -> a lone regional indicator, boxed "K"
oneLine("<family> hello", 3) -> the man, then a dangling zero-width joiner
oneLine("<thumb+tone> ok", 2) -> the thumb, its skin tone dropped
oneLine("<keycap 1> first", 2) -> a bare "1"
The line is what the sidebar draws under a channel, what the generated title is
trimmed to, and what the titler is shown as an excerpt, so a message opening
with a flag rendered as a letter in a box in all three.
Cut between grapheme clusters instead, via `Intl.Segmenter`. A string cannot
hold more clusters than UTF-16 units, so a line short enough to be under the cap
returns without segmenting at all, which is nearly every line a roster draws.
The three caller constants said `CODE_POINTS`; the unit changed, so the names
did.
New tests: 5 of 9 fail against `main`. The other four are the guard against
over-correcting -- short text untouched, control characters flattened, plain
text cut in the same place, a single-code-point emoji kept whole -- and they
pass both before and after.
kevin9327
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso and
tylerslaton
as code owners
September 8, 2026 22:32
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
# Conflicts: # CHANGELOG.md
davidmckayv
approved these changes
Sep 9, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
Deep-reviewed against live code (correctness, governance, no vendor/secret/scale issues). Composed build+tests green. CHANGELOG/format rebase on CI-validated substance.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is wrong
oneLineinserver/src/channels/text.tssays in its own docstring that it cuts "on code points soan emoji is never split". That holds only for an emoji that is one code point. Most modern emoji
are not:
oneLine(input, cap)onmain🇰🇷 report, cap 2"\u{1F1F0}…"👨👩👧 hello, cap 3"\u{1F468}\u{200D}…"👍🏽 ok, cap 2"\u{1F44D}…"1️⃣ first, cap 2"1…"Every one of those was measured against
main.That line is not incidental. It is:
previewOf,MAX_ACTIVITY_CODE_POINTS),summariseOne,MAX_SUMMARY_CODE_POINTS),openingOf,MAX_EXCERPT_CODE_POINTS).So a message that opens with a flag renders as a letter in a box in all three places.
The change
Segment with
Intl.Segmenter(undefined, { granularity: "grapheme" })and cut between clusters, sowhat a person sees as one character is kept or dropped whole.
A string can never hold more grapheme clusters than UTF-16 units, so a line short enough to be under
the cap by
.lengthis under it by clusters too and returns without segmenting at all. That isnearly every line a roster draws, which keeps the expensive part off the common path.
The three caller constants were named
..._CODE_POINTS. The unit changed, so the names did — thatis the whole of the diff outside
text.ts.Verification
bun test server/tests/channel-one-line.test.tsserver/src/channels/text.tsreverted tomainand only the new test applied: 5 fail, 4 pass.The four that pass before and after are the guard against over-correcting: short text returned
untouched, control characters flattened and whitespace collapsed as before, plain text cut in exactly
the same place with the same ellipsis, and a single-code-point emoji kept whole. If this change had
moved the cut for ordinary text, those four would say so.
The emoji cases do not assert one hand-picked boundary. They sweep every cap from 1 past the end of
the line and assert that what survives is a whole number of the input's own clusters taken from the
front — a property no half-emoji can satisfy.
bun run --filter server typecheckandbunx biome checkon all four touched files are clean.bun test server/tests/channel-routes.test.ts server/tests/channel-titler.test.ts server/tests/channel-activity-input.test.tsgives 58 pass / 29 fail both onmainand on this branch — identical. The 29 are Postgres connection failures on a machine with no database, not a regression.Note
The
CHANGELOG.mdentry lands at the top of## Unreleased, the same anchor as #450 and #452, sowhichever merges last needs a one-line rebase. Happy to do it on request.