From b4794608049e6493232cb68bf97da33f78236701 Mon Sep 17 00:00:00 2001 From: Severian Date: Fri, 11 Sep 2026 05:07:19 +0800 Subject: [PATCH] Harden the parsing, and mark unread changelog entries Stress-testing the beta with request shapes we had not tried turned up two more ways to hit the same class of bug. Roles were compared exactly, so a client sending "Assistant" instead of "assistant" fell straight into the dingus error. And content was assumed to be a string, so OpenAI's multipart array form did the same. Roles are now compared case-insensitively and content is flattened to text first; messages with no usable text are skipped rather than mistaken for a slot. The slots are plain strings now, so no caller can reach past them to a raw field again. Separately: the Changelog button carries a dot when the current release has not been opened yet, on the desktop button, the mobile menu item, and the hamburger that hides it. The marker is the version in localStorage, so every release brings it back on its own and reading it once clears it. The version is a single constant now rather than being written in two places. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa --- README.md | 2 +- src/app/api/proxy/route.ts | 10 +++--- src/app/page.tsx | 53 +++++++++++++++++++++++++--- src/lib/messages.ts | 72 ++++++++++++++++++++++++++++---------- tests/messages.test.ts | 61 +++++++++++++++++++++++++------- 5 files changed, 155 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index faeae86..42ddb80 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ You can also build and run Sucker as a Docker container with `docker compose bui ### Changelog -- 2.5: Fixed extraction for users whose Janitor sends a request without the blank turn before the first message — sucker read the parts by position rather than by role, so for them it read the wrong ones and errored. Now resolved by role, with tests for both shapes. +- 2.5: Unread dot on the Changelog button. Fixed extraction for users whose Janitor sends a request without the blank turn before the first message — sucker read the parts by position rather than by role, so for them it read the wrong ones and errored. Now resolved by role, with tests for both shapes. - 2.4: CSS editor for Janitor profiles. New banner links to the live-preview editor at tools.from-avalon.com/janitor-profile-maker. - 2.3: ECHO. Put ECHO: anywhere in a message and sucker repeats whatever comes after it in the reply — for triggering char-side script and lorebook keywords. - 2.2: Added support for directly inputting webp file names or character image links when creating PNG character cards diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index 77607ed..5f76ed9 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -121,16 +121,16 @@ function removePersonaTags(content: string): string { function extractCardData(messages: Message[]): ExtractedCard { const { system, greeting, nameSlot } = selectMessageSlots(messages); - if (!system?.content) { + if (!system) { throw new Error("No system message: this request carries no character card."); } - if (!greeting?.content) { + if (!greeting) { throw new Error("No assistant message: this chat has no greeting to read."); } - const first_mes = greeting.content.replace(/{user}/g, "{{user}}"); + const first_mes = greeting.replace(/{user}/g, "{{user}}"); - const nameContent = nameSlot?.content ?? ""; + const nameContent = nameSlot ?? ""; // If the name slot is actually a token, ignore it for naming purposes const tokenInNameSlot = parseConversationToken(nameContent); const lastColonIndex = nameContent.lastIndexOf(": "); @@ -140,7 +140,7 @@ function extractCardData(messages: Message[]): ExtractedCard { ? nameContent.substring(lastColonIndex + 2).trim() : ""; - let content = system.content.replace(/{user}/g, "{{user}}"); + let content = system.replace(/{user}/g, "{{user}}"); const inferredName = extractPersonaName(content); content = removePersonaTags(content); diff --git a/src/app/page.tsx b/src/app/page.tsx index 6781b56..e4d2b69 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -67,9 +67,36 @@ interface Card { initialVersion?: CardDataV2; } +/** Bumped once per release; the changelog dot keys off it. */ +const APP_VERSION = "2.5"; +const CHANGELOG_SEEN_KEY = "sucker:changelog-seen"; + export default function Home() { const [cards, setCards] = useState([]); + // The changelog dot. Starts hidden so the server and the first client render + // agree; the effect below turns it on for anyone who has not opened the + // dialog since this version shipped. + const [changelogSeen, setChangelogSeen] = useState(true); const [dialogOpen, setDialogOpen] = useState(false); + useEffect(() => { + try { + setChangelogSeen(localStorage.getItem(CHANGELOG_SEEN_KEY) === APP_VERSION); + } catch { + // Private mode or blocked storage: just leave the dot off. + } + }, []); + + /** Open the changelog and remember that this version has been read. */ + const openChangelog = () => { + setChangelogDialogOpen(true); + setChangelogSeen(true); + try { + localStorage.setItem(CHANGELOG_SEEN_KEY, APP_VERSION); + } catch { + // Nothing to do; the dot simply comes back next time. + } + }; + const [selectedCardIndex, setSelectedCardIndex] = useState( null ); @@ -373,15 +400,21 @@ export default function Home() { {/* Row 1: Title on left, Hamburger on right */}
-

Sucker v2.5

+

Sucker v{APP_VERSION}

New: a live CSS editor for your profile.

- @@ -391,8 +424,11 @@ export default function Home() { setMirrorsDialogOpen(true)}> Mirrors - setChangelogDialogOpen(true)}> + Changelog + {!changelogSeen && ( + + )} @@ -411,7 +447,7 @@ export default function Home() { {/* Desktop Layout */}
-

Sucker v2.5

+

Sucker v{APP_VERSION}

New: a live CSS editor for your profile.

@@ -430,10 +466,17 @@ export default function Home() { Mirrors