titles: smart truncation — the cut snaps to word boundaries

Owner round (2026-10-06, release polish #3): first messages are always
long, and the auto-title's hard 72-char cut landed mid-word. The cut now
snaps to the last word boundary at or under TITLE_MAX_CHARS (72, exported
and tunable); whitespace collapses first so boundaries are single-space.
The one special case (owner-specified): a single word longer than the cap
— rare but real: identifiers, URLs — splits at the cap. A boundary landing
exactly at the cap is a clean cut. Seam unchanged: titleFromPrompt
(core/contracts/graph.ts), applied once server-side at chat creation.
Pinned in tests/title-from-prompt.test.ts (6).
This commit is contained in:
amosblomqvist
2026-10-06 07:49:38 +03:00
parent e6f52d3ad1
commit 8a6eab0b6a
2 changed files with 82 additions and 1 deletions
+26 -1
View File
@@ -341,9 +341,34 @@ export interface Position {
y: number;
}
/** The auto-title's character cap (owner, 2026-10-06): long enough to
* stay informative in the sidebar and the top bar, short enough that a
* first message never reads as a paragraph. Tunable — one number, one
* place. */
export const TITLE_MAX_CHARS = 72;
/** Derive a short chat title from the learner's first message (pure).
* Smart truncation (owner, 2026-10-06): the cut snaps to a word boundary
* AT OR UNDER the cap — a title never ends mid-word. The one exception
* is a single word longer than the cap (rare, but real: identifiers,
* URLs): it cuts at the cap, splitting the word. Whitespace collapses to
* single spaces first, so boundaries are always single-space boundaries.
* Used on chat creation; deterministic so tests can pin it. */
export function titleFromPrompt(prompt: string): string {
const oneLine = prompt.replace(/\s+/g, " ").trim();
return oneLine.length > 72 ? `${oneLine.slice(0, 72)}…` : oneLine;
if (oneLine.length <= TITLE_MAX_CHARS) return oneLine;
// Last space among the first TITLE_MAX_CHARS characters; the cut sits
// before it so no trailing space rides into the title.
let cut = -1;
for (let i = TITLE_MAX_CHARS; i > 0; i--) {
if (oneLine[i - 1] === " ") {
cut = i - 1;
break;
}
}
// One long word: no boundary exists under the cap — split it (owner's
// special case). This also lands correctly when the first word exactly
// fills the cap: cutting at the cap IS the boundary.
if (cut < 0) cut = TITLE_MAX_CHARS;
return `${oneLine.slice(0, cut)}…`;
}