Skip to content

feat(chat): stamp each user message with the persona that sent it - #384

Merged
hydall merged 2 commits into
nightlyfrom
claude/chat-message-persona-1605pw
Sep 3, 2026
Merged

feat(chat): stamp each user message with the persona that sent it#384
hydall merged 2 commits into
nightlyfrom
claude/chat-message-persona-1605pw

Conversation

@hydall

@hydall hydall commented Sep 3, 2026

Copy link
Copy Markdown
Owner

A user message now records the persona it was sent as — the id and the name — and the chat renders that persona instead of whichever one happens to be active. Switching persona no longer re-faces the whole history, and a message from a deleted persona keeps its name while its avatar drops to the initial letter.

Chat rendering

  • ChatNotifier._sendMessage stamps the effective persona (personaId + personaName) onto every sent user message. Both fields already existed on ChatMessage; nothing wrote them.
  • ChatMessageMapper.toMap resolves the stored personaId against a live roster (ChatMessageMapperContext.personasById): a persona that still exists contributes its current name and avatar, so renaming a persona renames the messages it sent; one that is gone (or has no picture) yields avatarFallback: true, and the message falls back to the name stored on it plus a letter avatar.
  • ChatBridgeController.setPersonaRoster builds that roster once per change, resolving each avatar path to a WebView URL there rather than per message.
  • message_renderer.js stamps data-avatar-pinned on a message that names its own persona, and _refreshIdentityDom (bridge/chat_bridge_controller.js and the bridge.legacy.js snapshot) skips pinned avatars — an identity push must not hand a message somebody else's face.
  • ChatWebViewBuildListeners._listenPersonaRoster re-pushes the roster and re-renders (scroll preserved) when a persona is renamed, deleted, or given a new avatar; ChatWebViewInitializer seeds the roster before the first setMessages so a chat never opens with letter avatars first.
  • Messages written before this change carry no persona: they stay unpinned and keep following the active identity.

Chat export

  • exportChatAsJsonl writes msg.personaName into the SillyTavern name field of a user message, falling back to the caller's userName only for messages that carry none.
  • ChatActionsService.exportChat passes the chat's effective persona as that fallback, so legacy messages are no longer all named the literal User.

Docs

  • docs/rules/message-rendering.md gains the rule the pin exists to protect, so a later refactor does not re-clobber avatars on identity refresh.

Verification

Run with Flutter 3.44.9 (SDK installed in the session):

  • flutter analyze --no-fatal-infos --no-fatal-warnings — no errors; the 7 remaining infos/warnings are pre-existing and untouched by this diff.
  • flutter test — 3782 tests, all passing (after generating locale_keys.g.dart the way CI does).
  • cd test/webview_js && npx playwright test — 73 passing in headless Chromium, including the 4 new cases in specs/message_persona.spec.js.

New tests: test/chat_message_persona_test.dart (mapper: live persona, rename, deletion, avatar-less persona, legacy message, assistant message), test/chat_export_persona_name_test.dart (export naming), test/webview_js/specs/message_persona.spec.js (rendered DOM), plus two contract assertions in test/webview_assets_test.dart.

Not run: the app itself — no runtime/hot-reload check was possible in this environment.

🤖 Generated with Claude Code

https://claude.ai/code/session_018SXpk5JW6Ext697GbCmZvr


Generated by Claude Code

A user message now records `personaId` + `personaName` at send time, and the
chat renders the persona it was sent as instead of whichever one is active:

- `ChatNotifier._sendMessage` stamps the effective persona onto the message.
- `ChatMessageMapper` resolves the stored id against a live roster
  (`ChatBridgeController.setPersonaRoster`, fed from `personaListProvider`):
  an existing persona contributes its current name and avatar, so renaming it
  renames its own past messages; a deleted one leaves the message with the name
  stored on it and `avatarFallback`, which drops the avatar to its letter.
- The renderer stamps `data-avatar-pinned` on such a message and `setIdentity`
  skips pinned avatars, so switching persona no longer re-faces the history.
- Messages sent before this change carry no persona and keep following the
  active identity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SXpk5JW6Ext697GbCmZvr
- `exportChatAsJsonl` writes `msg.personaName` into the SillyTavern `name`
  field of a user message, falling back to the caller's `userName` only for
  messages that carry no persona.
- `ChatActionsService.exportChat` passes the chat's effective persona as that
  fallback, so legacy messages are no longer all named the literal "User".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SXpk5JW6Ext697GbCmZvr
@hydall
hydall merged commit cfe5b07 into nightly Sep 3, 2026
3 checks passed
@hydall
hydall deleted the claude/chat-message-persona-1605pw branch September 6, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants