Skip to content

feat: native AI-assisted Magic Rewrite for character, persona, and lorebook fields#1184

Closed
kevin-ho wants to merge 2 commits into
Pasta-Devs:mainfrom
kevin-ho:magic-rewrite
Closed

feat: native AI-assisted Magic Rewrite for character, persona, and lorebook fields#1184
kevin-ho wants to merge 2 commits into
Pasta-Devs:mainfrom
kevin-ho:magic-rewrite

Conversation

@kevin-ho
Copy link
Copy Markdown
Contributor

@kevin-ho kevin-ho commented May 25, 2026

Linked issue

Closes #1183

Why this change

Writing and maintaining long-form text entries — character descriptions, persona definitions, lorebook entries, system prompts — is one of the more tedious parts of setting up a roleplay session. Other RP platforms offer AI-assisted rewriting that lets users iteratively refine these fields with natural language instructions: "add more detail about their childhood," "convert to XML format," "make this more token-efficient."

Currently Marinara has no built-in mechanism for this. A Marinara app extension (magic-rewrite-extension) exists as a proof of concept, but it requires users to manually enter API keys because it cannot access the LLM connections already configured in the app. This PR ships the feature natively, reusing Marinara's existing connection infrastructure so users can leverage their configured endpoints with no additional credential management.

What changed

  • packages/shared/src/types/connection.ts + packages/shared/src/schemas/connection.schema.ts: add defaultForRewrite boolean field to the shared connection type and Zod create schema.
  • packages/server/src/db/schema/connections.ts + packages/server/src/db/migrate.ts: add default_for_rewrite text column to the api_connections table with a migration entry.
  • packages/server/src/services/storage/connections.storage.ts: add getDefaultForRewrite() method and mutual-exclusion enforcement in create/update — only one non-image-generation connection can hold the rewrite default at a time.
  • packages/server/src/routes/magic-rewrite.routes.ts: new POST /api/magic-rewrite/generate route. Accepts text, instruction, and optional context (character, lorebook, chat history). Resolves connection via rewrite-specific → default chat → agent default chain. Uses createLLMProvider() and chatComplete() with a focused system prompt that returns only rewritten text.
  • packages/client/src/components/ui/MagicRewritePanel.tsx: new panel component with instruction textarea, context selectors (character card, lorebook entries, chat history with configurable message count up to 250), generate button with loading spinner, and a word-level diff preview showing before/after with changed spans highlighted. Supports persistent instruction recall via localStorage.
  • packages/client/src/components/lorebooks/LorebookFormFields.tsx: adds ✨ Rewrite button in the lorebook description editor modal (bottom-right, left of Done). Clicking it enters rewrite mode with Back/Apply controls.
  • packages/client/src/components/ui/ExpandedTextarea.tsx: adds ✨ Rewrite button in the shared full-screen editor header. This automatically makes the feature available in the character editor, persona editor, and chat settings drawer — all of which use this shared component.
  • packages/client/src/components/connections/ConnectionEditor.tsx: adds "Default for Rewrite" toggle beneath the existing "Default for Agents" toggle, with tooltip explaining the fallback behavior. Hidden for image-generation provider connections.

Validation

  • pnpm check passes locally
  • Container (Docker / Podman) built and ran without issue
  • Ran the app, clicked through the changes manually
  • Checked edge cases (light + dark mode, mobile viewport, empty states, error paths)
  • Above manual verification completed (describe below)
  • Read and followed CONTRIBUTING.md

Manual verification notes

  • Lorebook description editor: ✨ Rewrite button appears, generates rewrite using the default chat connection, diff preview renders correctly with line breaks in both before/after panes, Apply saves rewritten text back to the editor.
  • Character editor: ✨ Rewrite button appears in the expand-textarea header for all text fields.
  • Persona editor: ✨ Rewrite button appears in the expand-textarea header for all text fields.
  • Connection settings: "Default for Rewrite" toggle works, mutual exclusion enforced (only one non-image connection can hold the flag).
  • Fallback chain: with no rewrite-specific connection set, correctly falls through to the default chat connection, then agent default.
  • Chat history context: messages slider configurable up to 250, context included in the generation request.
  • Instruction persistence: rewrite instructions saved to and recalled from localStorage.
  • Escape key does not close the modal while Magic Rewrite mode is active.

Docs and release impact

  • No docs changes needed
  • Updated docs (README / CONTRIBUTING / android/README / CHANGELOG) as needed
  • Version/release files updated (only if this PR includes a version bump)

UI evidence (if applicable)

Screenshot 2026-05-24 222848
Screenshot 2026-05-24 223102 ---- Screenshot 2026-05-24 223043

…rebook fields (Pasta-Devs#1183)

- Add POST /api/magic-rewrite/generate endpoint using configured LLM connections
- Connection resolution: rewrite-specific → default chat → agent default
- New 'Default for Rewrite' per-connection toggle in Connection Settings
- MagicRewritePanel component with instructions, context selectors, diff preview
- Integrated into LorebookFormFields and shared ExpandedTextarea component
- Covers character editor, persona editor, chat settings, and lorebook entries
- DB migration adds default_for_rewrite column with mutual-exclusion enforcement
@github-actions github-actions Bot added feature New feature or enhancement client server shared labels May 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

Adds an end-to-end Magic Rewrite feature: a new DB flag and schema, storage enforcement for a single default rewrite connection, a server route POST /api/magic-rewrite/generate that calls the selected LLM provider, a client-side MagicRewritePanel component, and editor integrations to run/apply rewrites.

Changes

Magic Rewrite

Layer / File(s) Summary
Shared connection types and schemas
packages/shared/src/types/connection.ts, packages/shared/src/schemas/connection.schema.ts
Adds defaultForRewrite: boolean to APIConnection and createConnectionSchema.
Database schema and migration
packages/server/src/db/schema/connections.ts, packages/server/src/db/migrate.ts
Adds default_for_rewrite column to api_connections and an idempotent migration to add it when missing.
Connection storage for rewrite defaults
packages/server/src/services/storage/connections.storage.ts
Adds getDefaultForRewrite(); persists defaultForRewrite on create/update; clears the flag on other non-image-generation rows; duplicated rows start with default false.
Magic Rewrite generation endpoint
packages/server/src/routes/index.ts, packages/server/src/routes/magic-rewrite.routes.ts
Registers magicRewriteRoutes and implements POST /api/magic-rewrite/generate: validates input, resolves a rewrite-capable connection (with fallbacks), assembles prompt/context, invokes provider.chatComplete, and returns the rewritten text with metadata.
Connection editor rewrite toggle
packages/client/src/components/connections/ConnectionEditor.tsx
Adds localDefaultForRewrite state and a conditional "Default for Rewrite" checkbox; includes the flag in save payload (forced false for image providers).
MagicRewritePanel component
packages/client/src/components/ui/MagicRewritePanel.tsx
New panel: instruction textarea (localStorage persisted), optional context selectors (character, lorebook, chat), generate action (POST /magic-rewrite/generate), loading/error states, and before/after diff viewer with large-input skip.
ExpandedTextarea and modal rewrite integration
packages/client/src/components/ui/ExpandedTextarea.tsx, packages/client/src/components/lorebooks/LorebookFormFields.tsx
Adds local editable state, rewrite mode toggles and handlers, Escape handling, conditional rendering of textarea vs. MagicRewritePanel, apply/back actions, and wider modal layout for the panel.

Sequence Diagram

sequenceDiagram
  participant User
  participant ExpandedTextarea
  participant MagicRewritePanel
  participant MagicRewriteAPI
  participant Storage
  participant LLMProvider

  User->>ExpandedTextarea: Open editor
  ExpandedTextarea->>ExpandedTextarea: Sync value to local state
  User->>ExpandedTextarea: Enter ✨ Magic Rewrite mode
  ExpandedTextarea->>MagicRewritePanel: Show panel with current text
  User->>MagicRewritePanel: Enter instruction & select context
  User->>MagicRewritePanel: Click Generate
  MagicRewritePanel->>MagicRewriteAPI: POST /api/magic-rewrite/generate (text,instruction,context)
  MagicRewriteAPI->>Storage: getDefaultForRewrite()
  Storage-->>MagicRewriteAPI: return connection (with apiKey)
  MagicRewriteAPI->>LLMProvider: chatComplete(system+messages, params)
  LLMProvider-->>MagicRewriteAPI: rewritten text
  MagicRewriteAPI-->>MagicRewritePanel: { text, finishReason, usage }
  MagicRewritePanel->>MagicRewritePanel: compute diff / show before-after
  User->>MagicRewritePanel: Click Apply
  MagicRewritePanel->>ExpandedTextarea: update local text & exit rewrite mode
  ExpandedTextarea->>User: commit final text via onChange when closed
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

"Ah, the contraption hums—sparkles, scribbles, then a finer script;
Cards and lore fed into the crucible of instruction,
The machine exhales a polished line, the editor accepts with a click,
Bravo, bravo—science, art, and a pinch of theatrical flair!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main feature—native Magic Rewrite support—and specifies the affected text field types (character, persona, lorebook).
Linked Issues check ✅ Passed The PR fully implements all objectives from issue #1183: server endpoint with context support, connection resolution chain, per-connection rewrite toggle, UI integration across editors, and context awareness for character/lorebook/chat history.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the Magic Rewrite feature—database schema, storage layer, API route, and client UI components—with no unrelated modifications detected.
Description check ✅ Passed PR description comprehensively addresses all required template sections with detailed justification, implementation scope, validation evidence, and UI screenshots.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/client/src/components/lorebooks/LorebookFormFields.tsx (1)

247-251: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Block Escape-to-close while Magic Rewrite mode is active.

Line 247–251 closes immediately even during rewrite mode, which bypasses the Back/Apply workflow advertised in Line 308 and can drop generated rewrite output.

Suggested fix
-      if (e.key === "Escape") {
+      if (e.key === "Escape" && !magicRewriteMode) {
         onChange(local);
         onCommit?.();
         onClose();
       }
...
-  }, [onClose, onChange, onCommit, local]);
+  }, [onClose, onChange, onCommit, local, magicRewriteMode]);

Also applies to: 308-308

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client/src/components/lorebooks/LorebookFormFields.tsx` around lines
247 - 251, The Escape key handler currently always calls onChange(local),
onCommit and onClose which closes the form even when Magic Rewrite mode is
active; update the handler to check the Magic Rewrite mode flag (e.g.,
isMagicRewrite or isRewriteMode in component state/props) and only perform the
close/commit sequence when that flag is false. Concretely, modify the block
around the Escape check (the code using e.key === "Escape" that calls
onChange(local), onCommit?.(), onClose()) to early-return or ignore Escape when
the Magic Rewrite boolean is true so the Back/Apply workflow (the rewrite flow)
is not bypassed.
🧹 Nitpick comments (1)
packages/server/src/services/storage/connections.storage.ts (1)

224-260: ⚡ Quick win

Ah, a most curious omission in your duplication apparatus!

The duplicate() method explicitly sets defaultForAgents: "false" on line 240 to prevent inheritance of privileged status, yet the new defaultForRewrite field receives no such explicit handling. While the database default shall suffice functionally, this inconsistency diminishes the elegance of the experimental design!

For clarity and to match the established pattern, explicitly set this field alongside its sibling.

🔬 Proposed refinement to maintain consistency
        isDefault: "false",
        useForRandom: source.useForRandom,
        defaultForAgents: "false",
+       defaultForRewrite: "false",
        enableCaching: source.enableCaching,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/services/storage/connections.storage.ts` around lines 224
- 260, The duplicate(id: string) method currently omits explicitly setting the
defaultForRewrite field when inserting the copied connection; update the
db.insert in connections.storage.ts inside duplicate() to include
defaultForRewrite (set to "false" to match defaultForAgents handling) so the new
record explicitly mirrors the intended non-default rewrite flag instead of
relying on DB defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client/src/components/connections/ConnectionEditor.tsx`:
- Around line 1736-1739: The tooltip/help text for the "Default for Rewrite"
control in ConnectionEditor.tsx currently stops at "default chat connection" but
omits the final fallback to the agent default; update the help string used for
the field labeled "Default for Rewrite" (the help prop in the ConnectionEditor
component) to explicitly describe the full fallback chain: use the
rewrite-specific connection, otherwise the default chat connection, and if that
is not set fall through to the agent default (and mention any further global
default if applicable).
- Around line 417-418: The bug is that defaultForRewrite
(localDefaultForRewrite) is still included when saving an image_generation
provider connection; update the object assembly in ConnectionEditor so
defaultForRewrite is only added for providers that support rewrites. Concretely,
when building the payload that currently sets defaultForAgents:
localDefaultForAgents, defaultForRewrite: localDefaultForRewrite, guard adding
defaultForRewrite (or set it to false/omit it) when the connection/provider type
equals 'image_generation' (or otherwise lacks rewrite capability) so that
defaultForRewrite is not persisted for image generation providers.

In `@packages/client/src/components/ui/ExpandedTextarea.tsx`:
- Around line 22-24: ExpandedTextarea currently keeps magicRewriteMode and
magicRewriteResult across open/close cycles; update the component to clear these
when the overlay is closed by calling setMagicRewriteMode(false) and
setMagicRewriteResult("") in the overlay close path (e.g., the component's
onClose handler or a useEffect that watches the overlay's open state), and
ensure the same reset is applied wherever the duplicate state hooks at lines
~26-28 are used so reopening always starts with a clean rewrite state.

In `@packages/client/src/components/ui/MagicRewritePanel.tsx`:
- Line 93: LocalStorage access in MagicRewritePanel (the initial state using
PROMPT_KEY and later reads/writes around setInstruction) must be guarded to
avoid throws in restricted browsers; update the useState initializer and any
subsequent window.localStorage.getItem/setItem calls to first check for
availability (typeof window !== "undefined" && window.localStorage) and wrap
accesses in try/catch, falling back to an empty string or no-op when errors
occur, so all reads (the initializer using PROMPT_KEY) and writes (the update
logic near lines 105-106) safely handle blocked storage without throwing.

In `@packages/server/src/routes/magic-rewrite.routes.ts`:
- Around line 92-94: The handler currently logs the full error via app.log.error
but returns error.message to clients; change it to keep the full error in server
logs (use app.log.error({ err: error }, "Magic Rewrite generation failed")) and
instead return a stable, generic message in reply.status(500).send (e.g., {
error: "Magic Rewrite generation failed" }) so no raw provider/internal error
text is exposed; ensure you remove or replace any use of error.message in the
response while preserving the existing log call.
- Line 34: Replace the direct call to magicRewriteSchema.parse(req.body) (which
can throw) with a non-throwing validation using
magicRewriteSchema.safeParse(req.body); check the result object (result.success)
and if false respond with res.status(400).json(...) containing the validation
errors and return early so the handler doesn't proceed; if success, use
result.data as the input variable for the existing logic. This change touches
the usage of magicRewriteSchema and the input variable in the route handler.
- Line 92: Replace the inline use of app.log.error with the shared Pino logger:
import the default logger from ../lib/logger.js (if not already imported) and
change the call in the Magic Rewrite generation error path to
logger.error(error, "Magic Rewrite generation failed") so the error object is
passed first and the project-wide logging contract is followed; also remove or
stop using app.log in this module to keep logging consistent.

---

Outside diff comments:
In `@packages/client/src/components/lorebooks/LorebookFormFields.tsx`:
- Around line 247-251: The Escape key handler currently always calls
onChange(local), onCommit and onClose which closes the form even when Magic
Rewrite mode is active; update the handler to check the Magic Rewrite mode flag
(e.g., isMagicRewrite or isRewriteMode in component state/props) and only
perform the close/commit sequence when that flag is false. Concretely, modify
the block around the Escape check (the code using e.key === "Escape" that calls
onChange(local), onCommit?.(), onClose()) to early-return or ignore Escape when
the Magic Rewrite boolean is true so the Back/Apply workflow (the rewrite flow)
is not bypassed.

---

Nitpick comments:
In `@packages/server/src/services/storage/connections.storage.ts`:
- Around line 224-260: The duplicate(id: string) method currently omits
explicitly setting the defaultForRewrite field when inserting the copied
connection; update the db.insert in connections.storage.ts inside duplicate() to
include defaultForRewrite (set to "false" to match defaultForAgents handling) so
the new record explicitly mirrors the intended non-default rewrite flag instead
of relying on DB defaults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 96baea19-32c4-4522-babb-7108eead7d48

📥 Commits

Reviewing files that changed from the base of the PR and between 77a30fa and ecd2816.

📒 Files selected for processing (11)
  • packages/client/src/components/connections/ConnectionEditor.tsx
  • packages/client/src/components/lorebooks/LorebookFormFields.tsx
  • packages/client/src/components/ui/ExpandedTextarea.tsx
  • packages/client/src/components/ui/MagicRewritePanel.tsx
  • packages/server/src/db/migrate.ts
  • packages/server/src/db/schema/connections.ts
  • packages/server/src/routes/index.ts
  • packages/server/src/routes/magic-rewrite.routes.ts
  • packages/server/src/services/storage/connections.storage.ts
  • packages/shared/src/schemas/connection.schema.ts
  • packages/shared/src/types/connection.ts

Comment thread packages/client/src/components/connections/ConnectionEditor.tsx Outdated
Comment thread packages/client/src/components/connections/ConnectionEditor.tsx
Comment thread packages/client/src/components/ui/ExpandedTextarea.tsx
Comment thread packages/client/src/components/ui/MagicRewritePanel.tsx Outdated
Comment thread packages/server/src/routes/magic-rewrite.routes.ts Outdated
Comment thread packages/server/src/routes/magic-rewrite.routes.ts Outdated
Comment thread packages/server/src/routes/magic-rewrite.routes.ts Outdated
- Block Escape-to-close during Magic Rewrite mode in LorebookFormFields
- Add defaultForRewrite: 'false' to duplicate() method for consistency
- Reset magicRewriteMode/magicRewriteResult when ExpandedTextarea closes
- Guard localStorage access with try/catch in MagicRewritePanel
- Use safeParse instead of parse for request validation
- Use shared Pino logger instead of app.log; return generic error to client
- Exclude defaultForRewrite from image_generation provider save payload
- Update tooltip to describe full fallback chain
@cha1latte
Copy link
Copy Markdown
Collaborator

@kevin-ho Hi! Thanks for putting this together. I think the overall direction is a good fit, native Magic Rewrite supports the core authoring workflows around characters, personas, prompts, and lorebooks.

I do think the first version should be a little narrower before we merge it. This PR includes the core rewrite flow, but it also makes a few longer-term product decisions at the same time like a rewrite-specific default connection, a DB migration, new connection settings UI, richer context selection, chat-history context, and broad integration through shared editor surfaces.

For V1, I’d like to keep the merge focused on the smallest useful native rewrite workflow. Specifically, rewrite the current editor text from user instructions using the existing default chat/agent connection fallback, with preview/apply/back behavior. Then we can handle rewrite-specific defaults, chat-history context, and richer context controls as follow-up PRs once the basic workflow has landed.

Would you be able to submit a new PR that does just those things for now?

@kevin-ho
Copy link
Copy Markdown
Contributor Author

Superseded by #1238 — slimmed-down V1 per the feedback here. Closes this out.

@kevin-ho
Copy link
Copy Markdown
Contributor Author

Closing in favor of the scoped V1 PR → #1238

@kevin-ho kevin-ho closed this May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Native AI-assisted text rewriting for character, persona, and lorebook fields

3 participants