/**
* Chat naming — pure helpers.
*
* When the plugin drives the prediction loop, LM Studio no longer auto-names
* the conversation from the model's first reply, so it stays "New Chat". The
* handler fixes this by calling `ctl.suggestName(...)` once (gated by
* `ctl.needsNaming()`); this module builds the title-generation prompt and
* sanitizes the model's answer into a short, clean title.
*
* Kept pure (no `@lmstudio/sdk`) so it can be unit-tested in
* `scripts/smoke.mjs`; the single model call lives in the handler (mirrors how
* `memory/` keeps `buildSummaryPrompt` pure and the call in the handler).
*/
/**
* Build the prompt that asks the model for a short story title from the opening
* scene. Low-stakes, single-shot; the handler runs it non-streamed at low
* temperature with a tight token cap.
*/
export function buildTitlePrompt(
opening: string,
maxWords: number,
): { system: string; user: string } {
const n = Math.max(2, Math.floor(maxWords));
const system =
"You name interactive role-play stories. Read the opening scene and reply " +
`with ONLY a short, evocative title of at most ${n} words. No quotation ` +
"marks, no surrounding punctuation, no explanation, no preamble — just the " +
"title itself.";
const user = `Opening scene:\n${opening.trim()}\n\nTitle:`;
return { system, user };
}
/**
* Clean a model-returned (or user-supplied) title into something fit for a chat
* tab: first line only, stripped of quotes / markdown / a leading "Title:" /
* trailing punctuation, whitespace collapsed, capped to `maxWords` words and
* `maxChars` characters (cut on a word boundary). Returns "" if nothing usable
* remains, so the caller can fall back.
*/
export function cleanTitle(raw: string, maxWords: number, maxChars: number): string {
if (typeof raw !== "string") return "";
// First non-empty line — models sometimes add a blank line or a follow-up.
let title =
raw
.split(/\r?\n/)
.map((l) => l.trim())
.find((l) => l.length > 0) ?? "";
// Drop a leading label like "Title:", "Titre :", "Name -".
title = title.replace(/^\s*(title|titre|name|nom)\s*[:\-–—]\s*/i, "");
// Strip surrounding quotes / markdown emphasis / backticks, repeatedly.
let prev: string;
do {
prev = title;
title = title
.replace(/^["'“”‘’`*_]+/, "")
.replace(/["'“”‘’`*_]+$/, "")
.trim();
} while (title !== prev);
// Collapse internal whitespace.
title = title.replace(/\s+/g, " ").trim();
// Cap words.
const words = title.split(" ").filter(Boolean);
const cap = Math.max(1, Math.floor(maxWords));
if (words.length > cap) title = words.slice(0, cap).join(" ");
// Cap characters on a word boundary.
const max = Math.max(8, Math.floor(maxChars));
if (title.length > max) {
const cut = title.slice(0, max);
const lastSpace = cut.lastIndexOf(" ");
title = (lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trim();
}
// Drop trailing sentence punctuation a title shouldn't carry.
title = title.replace(/[.,;:!?\-–—]+$/, "").trim();
return title;
}