From 34c68a17d77f61026a785681e2e42c83b7d52974 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sun, 2 Aug 2026 08:54:32 +0200 Subject: [PATCH 01/99] bump version to v2.4.1 --- CHANGELOG.md | 6 +++++ README.md | 2 +- android/app/build.gradle | 6 ++--- package.json | 2 +- packages/client/package.json | 2 +- packages/client/public/manifest.json | 2 +- .../src/components/chat/HomeCreditsModal.tsx | 24 ++++++++++--------- packages/server/package.json | 2 +- packages/shared/package.json | 2 +- packages/shared/src/constants/defaults.ts | 2 +- win/installer/install.bat | 4 ++-- win/installer/installer.nsi | 4 ++-- 12 files changed, 33 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c16034d295..0b6f6a0362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ This file is the release-notes source of truth for Marinara Engine. Reuse these ## [Unreleased] +## [2.4.1] + +### Changed + +- Advanced the stable release identity to v2.4.1 across the Engine, PWA manifest, Windows installer, Android bootstrap APK, update checks, Home page, and release references. Android uses `versionName` `2.4.1` with `versionCode` `42` so it updates over every previously published APK. + ## [2.4.0] ### Added diff --git a/README.md b/README.md index b48614c073..12cc9b504b 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ ## Latest Release -Current stable release: **[v2.4.0](https://github.com/Pasta-Devs/Marinara-Engine/releases/tag/v2.4.0)**. +Current stable release: **[v2.4.1](https://github.com/Pasta-Devs/Marinara-Engine/releases/tag/v2.4.1)**. See [CHANGELOG.md](CHANGELOG.md) for detailed release notes. Tagged releases use the `vX.Y.Z` format and are published on the [Releases](https://github.com/Pasta-Devs/Marinara-Engine/releases) page with a Windows installer, Android bootstrap APK, and named versioned source ZIP. Android APKs are Termux bootstrap + WebView shells: they can download Termux from F-Droid, launch Android's installer, start the Termux setup flow after required permission prompts, then open the local Marinara server on the same device. diff --git a/android/app/build.gradle b/android/app/build.gradle index 5d0ed48467..91e1068752 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,10 +22,10 @@ android { applicationId "com.marinara.engine" minSdk 24 targetSdk 34 - versionCode 41 - versionName "2.4.0" + versionCode 42 + versionName "2.4.1" buildConfigField "String", "MARINARA_SERVER_URL", "\"http://127.0.0.1:${marinaraPort}\"" - buildConfigField "String", "MARINARA_RELEASE_TAG", "\"v2.4.0\"" + buildConfigField "String", "MARINARA_RELEASE_TAG", "\"v2.4.1\"" } signingConfigs { diff --git a/package.json b/package.json index d2e2282755..466f9b8293 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marinara-engine", - "version": "2.4.0", + "version": "2.4.1", "private": true, "description": "AI Chat & Roleplay Frontend — Conversation, Roleplay, Game", "author": "Marianna", diff --git a/packages/client/package.json b/packages/client/package.json index 82b66847ae..ff35d4e660 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,7 +1,7 @@ { "name": "@marinara-engine/client", "private": true, - "version": "2.4.0", + "version": "2.4.1", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/client/public/manifest.json b/packages/client/public/manifest.json index 62bbe3de86..6ab68a0117 100644 --- a/packages/client/public/manifest.json +++ b/packages/client/public/manifest.json @@ -2,7 +2,7 @@ "name": "Marinara Engine", "short_name": "Marinara", "description": "AI Chat & Roleplay Frontend — Conversation, Roleplay, Game", - "version": "2.4.0", + "version": "2.4.1", "start_url": "/", "display": "standalone", "background_color": "#0a0a0f", diff --git a/packages/client/src/components/chat/HomeCreditsModal.tsx b/packages/client/src/components/chat/HomeCreditsModal.tsx index 4b53cc5606..2913926992 100644 --- a/packages/client/src/components/chat/HomeCreditsModal.tsx +++ b/packages/client/src/components/chat/HomeCreditsModal.tsx @@ -6,16 +6,16 @@ const FONT_AWESOME_D20_SOURCE_URL = "https://github.com/FortAwesome/Font-Awesome const CC_BY_4_0_LICENSE_URL = "https://creativecommons.org/licenses/by/4.0/"; const CONTRIBUTORS = [ - { login: "SpicyMarinara", url: "https://github.com/SpicyMarinara", contributions: 1663 }, + { login: "SpicyMarinara", url: "https://github.com/SpicyMarinara", contributions: 2152 }, { login: "cha1latte", url: "https://github.com/cha1latte", contributions: 319 }, - { login: "kolacheee", url: "https://github.com/kolacheee", contributions: 214 }, + { login: "thetopham", url: "https://github.com/thetopham", contributions: 260 }, + { login: "kolacheee", url: "https://github.com/kolacheee", contributions: 258 }, + { login: "Gunterlie", url: "https://github.com/Gunterlie", contributions: 245 }, { login: "Romuromylus", url: "https://github.com/Romuromylus", contributions: 202 }, - { login: "thetopham", url: "https://github.com/thetopham", contributions: 125 }, - { login: "Gunterlie", url: "https://github.com/Gunterlie", contributions: 90 }, + { login: "Xelvanis", url: "https://github.com/Xelvanis", contributions: 124 }, { login: "LukaTheHero", url: "https://github.com/LukaTheHero", contributions: 86 }, - { login: "Xelvanis", url: "https://github.com/Xelvanis", contributions: 86 }, { login: "TheLonelyDevil9", url: "https://github.com/TheLonelyDevil9", contributions: 69 }, - { login: "Promansis", url: "https://github.com/Promansis", contributions: 64 }, + { login: "Promansis", url: "https://github.com/Promansis", contributions: 69 }, { login: "coxde", url: "https://github.com/coxde", contributions: 60 }, { login: "munimunigamer", url: "https://github.com/munimunigamer", contributions: 31 }, { login: "Minsklatte", url: "https://github.com/Minsklatte", contributions: 16 }, @@ -27,14 +27,15 @@ const CONTRIBUTORS = [ { login: "NeoKazuya", url: "https://github.com/NeoKazuya", contributions: 7 }, { login: "felorhik", url: "https://github.com/felorhik", contributions: 6 }, { login: "bignast", url: "https://github.com/bignast", contributions: 6 }, - { login: "jake9000", url: "https://github.com/jake9000", contributions: 5 }, - { login: "JurijPietrowicz", url: "https://github.com/JurijPietrowicz", contributions: 5 }, - { login: "mm14141", url: "https://github.com/mm14141", contributions: 5 }, { login: "amauragis", url: "https://github.com/amauragis", contributions: 5 }, + { login: "mm14141", url: "https://github.com/mm14141", contributions: 5 }, + { login: "JurijPietrowicz", url: "https://github.com/JurijPietrowicz", contributions: 5 }, + { login: "jake9000", url: "https://github.com/jake9000", contributions: 5 }, { login: "marysia", url: "https://github.com/marysia", contributions: 4 }, - { login: "myaiexp", url: "https://github.com/myaiexp", contributions: 3 }, - { login: "kh0p", url: "https://github.com/kh0p", contributions: 3 }, { login: "LightD31", url: "https://github.com/LightD31", contributions: 3 }, + { login: "kh0p", url: "https://github.com/kh0p", contributions: 3 }, + { login: "mallang0723", url: "https://github.com/mallang0723", contributions: 3 }, + { login: "myaiexp", url: "https://github.com/myaiexp", contributions: 3 }, { login: "Lochalan", url: "https://github.com/Lochalan", contributions: 2 }, { login: "Lamboozled", url: "https://github.com/Lamboozled", contributions: 2 }, { login: "ailthrim", url: "https://github.com/ailthrim", contributions: 2 }, @@ -52,6 +53,7 @@ const CONTRIBUTORS = [ { login: "Yasyasyasvil", url: "https://github.com/Yasyasyasvil", contributions: 1 }, { login: "vanta-jack", url: "https://github.com/vanta-jack", contributions: 1 }, { login: "pwildani", url: "https://github.com/pwildani", contributions: 1 }, + { login: "olegpro171", url: "https://github.com/olegpro171", contributions: 1 }, { login: "Lemon-will", url: "https://github.com/Lemon-will", contributions: 1 }, { login: "kevin-ho", url: "https://github.com/kevin-ho", contributions: 1 }, { login: "KeKKERUUU", url: "https://github.com/KeKKERUUU", contributions: 1 }, diff --git a/packages/server/package.json b/packages/server/package.json index 1edfe04864..c10b7d1985 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@marinara-engine/server", - "version": "2.4.0", + "version": "2.4.1", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/shared/package.json b/packages/shared/package.json index 6855a2b18f..48f5b5b3fd 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@marinara-engine/shared", - "version": "2.4.0", + "version": "2.4.1", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/shared/src/constants/defaults.ts b/packages/shared/src/constants/defaults.ts index 4a97cb44ff..584393c695 100644 --- a/packages/shared/src/constants/defaults.ts +++ b/packages/shared/src/constants/defaults.ts @@ -4,7 +4,7 @@ import type { GenerationParameters } from "../types/prompt.js"; /** Derived copy of the app version. Root package.json is canonical; this file is regenerated by pnpm version:sync. */ -export const APP_VERSION = "2.4.0"; +export const APP_VERSION = "2.4.1"; /** Stable synthetic connection id for the built-in local llama sidecar. */ export const LOCAL_SIDECAR_CONNECTION_ID = "__local_sidecar__"; diff --git a/win/installer/install.bat b/win/installer/install.bat index 04bb25fa64..6b31d506b0 100644 --- a/win/installer/install.bat +++ b/win/installer/install.bat @@ -11,14 +11,14 @@ set "NODE_DOWNLOAD_URL=https://nodejs.org/dist/v24.15.0/node-v24.15.0-x64.msi" set "NODE_SHA256=feffb8e5cb5ac47f793666636d496ef3e975be82c84c4da5d20e6aa8fa4eb806" set "GIT_DOWNLOAD_URL=https://github.com/git-for-windows/git/releases/download/v2.54.0.windows.1/Git-2.54.0-64-bit.exe" set "GIT_SHA256=2b96e7854f0520f0f6b709c21041d9801b1be44d5e1a0d9fa621b2fbc40f1983" -set "RELEASE_TAG=v2.4.0" +set "RELEASE_TAG=v2.4.1" if not defined MARINARA_RELEASE_COMMIT set "MARINARA_RELEASE_COMMIT=" set "RELEASE_COMMIT=%MARINARA_RELEASE_COMMIT%" echo. echo +==========================================+ echo ^| Marinara Engine - Windows Installer ^| -echo ^| v2.4.0 ^| +echo ^| v2.4.1 ^| echo +==========================================+ echo. diff --git a/win/installer/installer.nsi b/win/installer/installer.nsi index fac46787b6..e4693d8dca 100644 --- a/win/installer/installer.nsi +++ b/win/installer/installer.nsi @@ -13,7 +13,7 @@ ; ── App metadata ── !define APP_NAME "Marinara Engine" -!define APP_VERSION "2.4.0" +!define APP_VERSION "2.4.1" !define APP_PUBLISHER "Pasta-Devs" !define APP_URL "https://github.com/Pasta-Devs/Marinara-Engine" !define REPO_URL "https://github.com/Pasta-Devs/Marinara-Engine.git" @@ -27,7 +27,7 @@ !define NODE_DOWNLOAD_URL "https://nodejs.org/dist/v24.15.0/node-v24.15.0-x64.msi" !define GIT_SHA256 "2b96e7854f0520f0f6b709c21041d9801b1be44d5e1a0d9fa621b2fbc40f1983" !define NODE_SHA256 "feffb8e5cb5ac47f793666636d496ef3e975be82c84c4da5d20e6aa8fa4eb806" -!define RELEASE_TAG "v2.4.0" +!define RELEASE_TAG "v2.4.1" !ifndef RELEASE_COMMIT !define RELEASE_COMMIT "" !endif From 6701a3defb24c428b55df5faaeb1af0416fc6a1f Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sun, 2 Aug 2026 09:22:27 +0200 Subject: [PATCH 02/99] fix v2.4.1 issue sweep --- CHANGELOG.md | 11 + .../components/agents/RegexScriptEditor.tsx | 213 ++++++++++++++---- .../characters/CharacterRegexSection.tsx | 2 + .../src/components/panels/PresetsPanel.tsx | 2 + packages/client/src/hooks/use-apply-regex.ts | 31 ++- .../client/src/hooks/use-regex-scripts.ts | 1 + .../client/src/localization/locales/en.json | 12 +- .../server/src/db/schema/regex-scripts.ts | 2 + packages/server/src/db/seed-regex.ts | 2 + packages/server/src/routes/chats.routes.ts | 1 + .../server/src/routes/connections.routes.ts | 11 +- packages/server/src/routes/generate.routes.ts | 4 + .../src/routes/generate/dry-run-route.ts | 1 + packages/server/src/routes/scene.routes.ts | 8 +- .../services/import/st-character.importer.ts | 1 + .../src/services/mari-db/mari-db.service.ts | 2 +- .../professor-mari/workspace-agent.service.ts | 2 + .../src/services/regex/regex-application.ts | 7 + .../services/storage/chat-presets.storage.ts | 18 +- .../services/storage/regex-scripts.storage.ts | 4 + packages/shared/src/schemas/regex.schema.ts | 1 + packages/shared/src/types/regex.ts | 2 + .../chat-branch-lineage.regression.ts | 62 ++++- scripts/regressions/open-issues.regression.ts | 53 ++++- scripts/regressions/prompt.regression.ts | 33 +++ 25 files changed, 433 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6f6a0362..32ee1e84f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,21 @@ This file is the release-notes source of truth for Marinara Engine. Reuse these ## [2.4.1] +### Added + +- Added optional prompt-preset targeting to regex scripts and refreshed the existing character target picker so scoped regexes follow the selected preset or characters without clipped controls (#4446). + ### Changed - Advanced the stable release identity to v2.4.1 across the Engine, PWA manifest, Windows installer, Android bootstrap APK, update checks, Home page, and release references. Android uses `versionName` `2.4.1` with `versionCode` `42` so it updates over every previously published APK. +### Fixed + +- Removed accumulated duplicate built-in **Default** settings profiles during startup normalization while preserving one stable profile and active selection per chat mode (#4442). +- Kept `/scene` chats and standalone conversions out of Conversation branch groups so original conversations remain visible in the Conversation sidebar (#4443). +- Taught Professor Mari the supported custom `image_prompt` agent configuration, including marker activation and the image-generation capability, so she creates requested image agents instead of falsely refusing them (#4444). +- Kept Google Gemini API keys in the `x-goog-api-key` header when fetching models instead of duplicating them in the URL, preventing compatible proxies from rejecting the query token with HTTP 401 (#4448). + ## [2.4.0] ### Added diff --git a/packages/client/src/components/agents/RegexScriptEditor.tsx b/packages/client/src/components/agents/RegexScriptEditor.tsx index a74c4765ff..a183a03726 100644 --- a/packages/client/src/components/agents/RegexScriptEditor.tsx +++ b/packages/client/src/components/agents/RegexScriptEditor.tsx @@ -1,7 +1,7 @@ // ────────────────────────────────────────────── // Full-Page Regex Script Editor // ────────────────────────────────────────────── -import { useState, useCallback, useEffect, useMemo } from "react"; +import { useState, useCallback, useEffect, useMemo, type ReactNode } from "react"; import { useUIStore } from "../../stores/ui.store"; import { showConfirmDialog } from "../../lib/app-dialogs"; import { @@ -12,6 +12,7 @@ import { type RegexScriptRow, } from "../../hooks/use-regex-scripts"; import { useCharacters } from "../../hooks/use-characters"; +import { usePresets } from "../../hooks/use-presets"; import { ArrowLeft, Save, @@ -25,6 +26,7 @@ import { Plus, Minus, Users, + FileText, Upload, } from "lucide-react"; import { cn } from "../../lib/utils"; @@ -163,6 +165,64 @@ function describeRegexEditorError(error: unknown): string { return "Failed to save regex script"; } +function ScopeTargetPicker({ + icon, + options, + selectedIds, + addLabel, + emptyLabel, + removeLabel, + onAdd, + onRemove, +}: { + icon: ReactNode; + options: Array<{ id: string; name: string }>; + selectedIds: string[]; + addLabel: string; + emptyLabel: string; + removeLabel: (name: string) => string; + onAdd: (id: string) => void; + onRemove: (id: string) => void; +}) { + const selected = selectedIds.map((id) => options.find((option) => option.id === id) ?? { id, name: id }); + const available = options.filter((option) => !selectedIds.includes(option.id)); + + return ( +
+ {selected.map((option) => ( +
+ {icon} + {option.name} + +
+ ))} + +
+ ); +} + // ═══════════════════════════════════════════════ // Main Editor // ═══════════════════════════════════════════════ @@ -177,6 +237,7 @@ export function RegexScriptEditor() { const { data: regexScripts } = useRegexScripts(); const { data: characters } = useCharacters(); + const { data: promptPresets } = usePresets(); const updateScript = useUpdateRegexScript(); const createScript = useCreateRegexScript(); const deleteScript = useDeleteRegexScript(); @@ -200,6 +261,8 @@ export function RegexScriptEditor() { const [localApplyMode, setLocalApplyMode] = useState("display"); const [localCharacterScopeEnabled, setLocalCharacterScopeEnabled] = useState(false); const [localTargetCharacterIds, setLocalTargetCharacterIds] = useState([]); + const [localPromptPresetScopeEnabled, setLocalPromptPresetScopeEnabled] = useState(false); + const [localTargetPromptPresetIds, setLocalTargetPromptPresetIds] = useState([]); const [localOrder, setLocalOrder] = useState(0); const [localMinDepth, setLocalMinDepth] = useState(null); const [localMaxDepth, setLocalMaxDepth] = useState(null); @@ -234,6 +297,13 @@ export function RegexScriptEditor() { .filter((character): character is { id: string; name: string } => character !== null) .sort((a, b) => a.name.localeCompare(b.name)); }, [characters]); + const promptPresetOptions = useMemo( + () => + (promptPresets ?? []) + .map((preset) => ({ id: preset.id, name: preset.name })) + .sort((a, b) => a.name.localeCompare(b.name)), + [promptPresets], + ); // Populate from DB row or defaults for new useEffect(() => { @@ -258,6 +328,9 @@ export function RegexScriptEditor() { const targetCharacterIds = parseStringArray(dbRow.targetCharacterIds); setLocalTargetCharacterIds(targetCharacterIds); setLocalCharacterScopeEnabled(targetCharacterIds.length > 0); + const targetPromptPresetIds = parseStringArray(dbRow.targetPromptPresetIds); + setLocalTargetPromptPresetIds(targetPromptPresetIds); + setLocalPromptPresetScopeEnabled(targetPromptPresetIds.length > 0); setLocalOrder(dbRow.order); setLocalMinDepth(dbRow.minDepth); setLocalMaxDepth(dbRow.maxDepth); @@ -275,6 +348,8 @@ export function RegexScriptEditor() { const defaultScope = regexDetailDefaultCharacterIds ?? []; setLocalTargetCharacterIds(defaultScope); setLocalCharacterScopeEnabled(defaultScope.length > 0); + setLocalTargetPromptPresetIds([]); + setLocalPromptPresetScopeEnabled(false); setLocalOrder(0); setLocalMinDepth(null); setLocalMaxDepth(null); @@ -344,7 +419,11 @@ export function RegexScriptEditor() { if (!regexDetailId) return; setSaveError(null); if (localCharacterScopeEnabled && localTargetCharacterIds.length === 0) { - setSaveError("Choose at least one target character."); + setSaveError(localizeUi("ui.agents.regexscripteditor.chooseAtLeastOneCharacter")); + return; + } + if (localPromptPresetScopeEnabled && localTargetPromptPresetIds.length === 0) { + setSaveError(localizeUi("ui.agents.regexscripteditor.chooseAtLeastOnePromptPreset")); return; } if (blockingRegexError) { @@ -367,6 +446,7 @@ export function RegexScriptEditor() { promptOnly: localApplyMode === "prompt", applyMode: localApplyMode, targetCharacterIds: localCharacterScopeEnabled ? localTargetCharacterIds : [], + targetPromptPresetIds: localPromptPresetScopeEnabled ? localTargetPromptPresetIds : [], minDepth: localMinDepth, maxDepth: localMaxDepth, }; @@ -402,6 +482,8 @@ export function RegexScriptEditor() { localApplyMode, localCharacterScopeEnabled, localTargetCharacterIds, + localPromptPresetScopeEnabled, + localTargetPromptPresetIds, localOrder, localMinDepth, localMaxDepth, @@ -412,6 +494,7 @@ export function RegexScriptEditor() { createScript, openRegexDetail, regexDetailReturn, + localizeUi, ]); const markDirty = useCallback(() => setDirty(true), []); @@ -443,13 +526,6 @@ export function RegexScriptEditor() { markDirty(); }; - const toggleTargetCharacter = (characterId: string) => { - setLocalTargetCharacterIds((prev) => - prev.includes(characterId) ? prev.filter((id) => id !== characterId) : [...prev, characterId], - ); - markDirty(); - }; - const handleExport = () => { downloadJsonFile( { @@ -466,6 +542,7 @@ export function RegexScriptEditor() { promptOnly: localApplyMode === "prompt", applyMode: localApplyMode, targetCharacterIds: localCharacterScopeEnabled ? localTargetCharacterIds : [], + targetPromptPresetIds: localPromptPresetScopeEnabled ? localTargetPromptPresetIds : [], order: localOrder, minDepth: localMinDepth, maxDepth: localMaxDepth, @@ -485,7 +562,13 @@ export function RegexScriptEditor() { const isPending = updateScript.isPending || createScript.isPending; const characterScopeError = - localCharacterScopeEnabled && localTargetCharacterIds.length === 0 ? "Choose at least one character." : null; + localCharacterScopeEnabled && localTargetCharacterIds.length === 0 + ? localizeUi("ui.agents.regexscripteditor.chooseAtLeastOneCharacter") + : null; + const promptPresetScopeError = + localPromptPresetScopeEnabled && localTargetPromptPresetIds.length === 0 + ? localizeUi("ui.agents.regexscripteditor.chooseAtLeastOnePromptPreset") + : null; return (
@@ -523,7 +606,13 @@ export function RegexScriptEditor() { {dirty && !saveError && {localizeUi("ui.agents.agenteditor.unsaved")}}
{localCharacterScopeEnabled && ( -
- {characterOptions.length > 0 ? ( -
- {characterOptions.map((character) => { - const selected = localTargetCharacterIds.includes(character.id); - return ( - - ); - })} -
- ) : ( -
{localizeUi("ui.agents.regexscripteditor.noCharactersFound")}
- )} +
+ } + options={characterOptions} + selectedIds={localTargetCharacterIds} + addLabel={localizeUi("ui.agents.regexscripteditor.addCharacter")} + emptyLabel={localizeUi("ui.agents.regexscripteditor.noCharactersFound")} + removeLabel={(name) => + localizeUi("ui.agents.regexscripteditor.removeValue1", { value1: name }) + } + onAdd={(id) => { + setLocalTargetCharacterIds((previous) => [...previous, id]); + markDirty(); + }} + onRemove={(id) => { + setLocalTargetCharacterIds((previous) => previous.filter((value) => value !== id)); + markDirty(); + }} + /> {characterScopeError && (
{characterScopeError} @@ -756,6 +837,62 @@ export function RegexScriptEditor() {
)}
+
+
+ { + setLocalPromptPresetScopeEnabled(checked); + markDirty(); + }} + className="mt-0.5 shrink-0 p-0 hover:bg-transparent" + /> +
+
+ + {localizeUi("ui.agents.regexscripteditor.specificPromptPresets")} + +
+
+ {localPromptPresetScopeEnabled + ? localizeUi("ui.agents.regexscripteditor.value1Selected", { + value1: localTargetPromptPresetIds.length, + }) + : localizeUi("ui.agents.regexscripteditor.appliesToAllPromptPresets")} +
+
+
+ {localPromptPresetScopeEnabled && ( +
+ } + options={promptPresetOptions} + selectedIds={localTargetPromptPresetIds} + addLabel={localizeUi("ui.agents.regexscripteditor.addPromptPreset")} + emptyLabel={localizeUi("ui.agents.regexscripteditor.noPromptPresetsFound")} + removeLabel={(name) => + localizeUi("ui.agents.regexscripteditor.removeValue1", { value1: name }) + } + onAdd={(id) => { + setLocalTargetPromptPresetIds((previous) => [...previous, id]); + markDirty(); + }} + onRemove={(id) => { + setLocalTargetPromptPresetIds((previous) => previous.filter((value) => value !== id)); + markDirty(); + }} + /> + {promptPresetScopeError && ( +
+ {promptPresetScopeError} +
+ )} +
+ )} +
{/* ── Trim Strings ── */} diff --git a/packages/client/src/components/characters/CharacterRegexSection.tsx b/packages/client/src/components/characters/CharacterRegexSection.tsx index 95781a875b..7e7de42afd 100644 --- a/packages/client/src/components/characters/CharacterRegexSection.tsx +++ b/packages/client/src/components/characters/CharacterRegexSection.tsx @@ -102,6 +102,7 @@ function serializeRegexScript(script: RegexScriptRow) { flags: script.flags, promptOnly: parseBooleanValue(script.promptOnly, false), applyMode: isRegexApplyMode(script.applyMode) ? script.applyMode : readRegexApplyMode(script as unknown as Record), + targetPromptPresetIds: parseStringArray(script.targetPromptPresetIds), order: script.order, minDepth: script.minDepth, maxDepth: script.maxDepth, @@ -169,6 +170,7 @@ function normalizeRegexImportEntry(entry: unknown, fallbackOrder: number) { flags, promptOnly: readRegexApplyMode(entry) === "prompt", applyMode: readRegexApplyMode(entry), + targetPromptPresetIds: parseStringArray(entry.targetPromptPresetIds), order: typeof entry.order === "number" ? fallbackOrder + entry.order : fallbackOrder, minDepth: parseNullableNumber(entry.minDepth), maxDepth: parseNullableNumber(entry.maxDepth), diff --git a/packages/client/src/components/panels/PresetsPanel.tsx b/packages/client/src/components/panels/PresetsPanel.tsx index b5475568aa..6dcffe4029 100644 --- a/packages/client/src/components/panels/PresetsPanel.tsx +++ b/packages/client/src/components/panels/PresetsPanel.tsx @@ -198,6 +198,7 @@ function serializeRegexScript(script: RegexScriptRow) { ? script.applyMode : readRegexApplyMode(script as unknown as Record), targetCharacterIds: parseStringArray(script.targetCharacterIds), + targetPromptPresetIds: parseStringArray(script.targetPromptPresetIds), order: script.order, minDepth: script.minDepth, maxDepth: script.maxDepth, @@ -266,6 +267,7 @@ function normalizeRegexImportEntry(entry: unknown, fallbackOrder: number) { promptOnly: readRegexApplyMode(entry) === "prompt", applyMode: readRegexApplyMode(entry), targetCharacterIds: parseStringArray(entry.targetCharacterIds), + targetPromptPresetIds: parseStringArray(entry.targetPromptPresetIds), order: typeof entry.order === "number" ? fallbackOrder + entry.order : fallbackOrder, minDepth: parseNullableNumber(entry.minDepth), maxDepth: parseNullableNumber(entry.maxDepth), diff --git a/packages/client/src/hooks/use-apply-regex.ts b/packages/client/src/hooks/use-apply-regex.ts index a36935f0ef..e76ba38b6a 100644 --- a/packages/client/src/hooks/use-apply-regex.ts +++ b/packages/client/src/hooks/use-apply-regex.ts @@ -11,6 +11,7 @@ import { type RegexPlacement, } from "@marinara-engine/shared"; import { useUIStore } from "../stores/ui.store"; +import { useChatStore } from "../stores/chat.store"; /** How character-scoped regex scripts apply at display time (mirrors card CSS modes). */ export type ScopedRegexMode = "disabled" | "exclusive" | "chat"; @@ -55,6 +56,14 @@ function parseScript(row: RegexScriptRow) { return []; } })(); + const targetPromptPresetIds: string[] = (() => { + try { + const parsed = JSON.parse(row.targetPromptPresetIds); + return Array.isArray(parsed) ? parsed.filter((entry): entry is string => typeof entry === "string") : []; + } catch { + return []; + } + })(); return { ...row, enabledBool: row.enabled === "true", @@ -62,6 +71,7 @@ function parseScript(row: RegexScriptRow) { placements, trimStrings, targetCharacterIds, + targetPromptPresetIds, }; } @@ -82,6 +92,7 @@ function applyScripts( targetedOnly?: boolean; scopedMode?: ScopedRegexMode; characterId?: string | null; + promptPresetId?: string | null; }, ): string { let result = text; @@ -113,6 +124,9 @@ function applyScripts( } } } + if (script.targetPromptPresetIds.length > 0) { + if (!options?.promptPresetId || !script.targetPromptPresetIds.includes(options.promptPresetId)) continue; + } // Depth range filtering if (options?.depth != null) { @@ -152,6 +166,7 @@ function applyScripts( export function useApplyRegex() { const { data: regexScripts } = useRegexScripts(); const quoteFormat = useUIStore((s) => s.quoteFormat); + const activePromptPresetId = useChatStore((s) => s.activeChat?.promptPresetId ?? null); // Pre-parse all scripts (sorted by order, which is done server-side) const parsedScripts = useMemo(() => { @@ -168,16 +183,24 @@ export function useApplyRegex() { scopedMode?: ScopedRegexMode; characterId?: string | null; }, - ) => formatTextQuotes(applyScripts(text, parsedScripts, "ai_output", options), quoteFormat), - [parsedScripts, quoteFormat], + ) => + formatTextQuotes( + applyScripts(text, parsedScripts, "ai_output", { ...options, promptPresetId: activePromptPresetId }), + quoteFormat, + ), + [activePromptPresetId, parsedScripts, quoteFormat], ); const applyToUserInput = useCallback( ( text: string, options?: { depth?: number; resolveMacros?: (value: string) => string; scopedMode?: ScopedRegexMode }, - ) => formatTextQuotes(applyScripts(text, parsedScripts, "user_input", options), quoteFormat), - [parsedScripts, quoteFormat], + ) => + formatTextQuotes( + applyScripts(text, parsedScripts, "user_input", { ...options, promptPresetId: activePromptPresetId }), + quoteFormat, + ), + [activePromptPresetId, parsedScripts, quoteFormat], ); return { applyToAIOutput, applyToUserInput }; diff --git a/packages/client/src/hooks/use-regex-scripts.ts b/packages/client/src/hooks/use-regex-scripts.ts index f8eaa039cb..035ff22e1d 100644 --- a/packages/client/src/hooks/use-regex-scripts.ts +++ b/packages/client/src/hooks/use-regex-scripts.ts @@ -20,6 +20,7 @@ export interface RegexScriptRow { promptOnly: string; applyMode?: string | null; targetCharacterIds: string; + targetPromptPresetIds: string; order: number; minDepth: number | null; maxDepth: number | null; diff --git a/packages/client/src/localization/locales/en.json b/packages/client/src/localization/locales/en.json index 20b769800f..ce8b68f73c 100644 --- a/packages/client/src/localization/locales/en.json +++ b/packages/client/src/localization/locales/en.json @@ -1322,16 +1322,21 @@ "ui.agents.featureagentdetailhost.value1Value2": "{{value1}} · {{value2}}", "ui.agents.featureagentdetailhost.versionValue1Value2": "Version {{value1}} · {{value2}}", "ui.agents.regexscripteditor.aboutRegexScripts": "About Regex Scripts", + "ui.agents.regexscripteditor.addCharacter": "Add character…", "ui.agents.regexscripteditor.additionalStringsToRemoveFromTheResultAfterThe": "Additional strings to remove from the result after the regex replacement. One per row.", + "ui.agents.regexscripteditor.addPromptPreset": "Add prompt preset…", "ui.agents.regexscripteditor.addTrimString": "Add trim string", "ui.agents.regexscripteditor.advancedOptions": "Advanced Options", "ui.agents.regexscripteditor.appliesToAllCharacters": "Applies to all characters", + "ui.agents.regexscripteditor.appliesToAllPromptPresets": "Applies to all prompt presets", "ui.agents.regexscripteditor.applyMode": "Apply Mode", "ui.agents.regexscripteditor.applyTo": "Apply To", "ui.agents.regexscripteditor.backToRegexScripts": "Back to regex scripts", "ui.agents.regexscripteditor.bbadwordB": "\\bbadword\\b", "ui.agents.regexscripteditor.capitalizeReplacement": "Capitalize replacement:", "ui.agents.regexscripteditor.censorWords": "Censor words:", + "ui.agents.regexscripteditor.chooseAtLeastOneCharacter": "Choose at least one character.", + "ui.agents.regexscripteditor.chooseAtLeastOnePromptPreset": "Choose at least one prompt preset.", "ui.agents.regexscripteditor.deleteRegexScript": "Delete regex script", "ui.agents.regexscripteditor.deleteRegexScript_d694998": "Delete Regex Script", "ui.agents.regexscripteditor.deleteThisRegexScriptThisCannotBeUndone": "Delete this regex script? This cannot be undone.", @@ -1346,6 +1351,7 @@ "ui.agents.regexscripteditor.fineTuneWhenAndHowTheRegexRuns": "Fine-tune when and how the regex runs.", "ui.agents.regexscripteditor.id": "ID:", "ui.agents.regexscripteditor.inTheReplacementToReferenceMatchedGroupsUse": ") in the replacement to reference matched groups. Use", + "ui.agents.regexscripteditor.limitThisScriptToSelectedPromptPresets": "Run this script only while one of the selected prompt presets is active.", "ui.agents.regexscripteditor.limitThisScriptToTheSelectedCharactersPromptOnly": "Limit this script to the selected characters. Prompt-only scripts then run only for those characters' prompts; display scripts apply per the chat's Scoped Regex mode.", "ui.agents.regexscripteditor.liveTest": "Live Test", "ui.agents.regexscripteditor.lowerNumbersRunFirst": "Lower numbers run first", @@ -1353,26 +1359,30 @@ "ui.agents.regexscripteditor.messageDepthEmptyUnlimited": "message depth (empty = unlimited)", "ui.agents.regexscripteditor.min": "Min", "ui.agents.regexscripteditor.noCharactersFound": "No characters found.", + "ui.agents.regexscripteditor.noPromptPresetsFound": "No prompt presets found.", "ui.agents.regexscripteditor.ooc": "\\(OOC:.*?\\)", "ui.agents.regexscripteditor.pasteSampleTextToTest": "Paste sample text to test…", - "ui.agents.regexscripteditor.patternPreviewOnlyPlacementEnabledStateCharacterScopeAnd": "Pattern preview only: placement, enabled state, character scope, and depth are evaluated at runtime.", + "ui.agents.regexscripteditor.patternPreviewOnlyPlacementEnabledStateCharacterScopeAnd": "Pattern preview only: placement, enabled state, character and prompt preset scopes, and depth are evaluated at runtime.", "ui.agents.regexscripteditor.regexFlags": "Regex Flags", "ui.agents.regexscripteditor.regexScriptNotFound": "Regex script not found.", "ui.agents.regexscripteditor.regexScriptsAreAppliedToTextDuringChatEither": "Regex scripts are applied to text during chat — either transforming AI responses before display, or modifying your input before it's sent.", "ui.agents.regexscripteditor.removeAsterisks": "Remove asterisks:", "ui.agents.regexscripteditor.removeOoc": "Remove OOC:", + "ui.agents.regexscripteditor.removeValue1": "Remove {{value1}}", "ui.agents.regexscripteditor.replaceWith": "Replace With", "ui.agents.regexscripteditor.result": "Result:", "ui.agents.regexscripteditor.saveRegexScript": "Save regex script", "ui.agents.regexscripteditor.scriptName": "Script name…", "ui.agents.regexscripteditor.scriptsRunInOrderLowestFirstUseCaptureGroups": "Scripts run in order (lowest first). Use capture groups (", "ui.agents.regexscripteditor.specificCharacters": "Specific Characters", + "ui.agents.regexscripteditor.specificPromptPresets": "Specific Prompt Presets", "ui.agents.regexscripteditor.standardRegexFlagsGGlobalICaseInsensitiveM": "Standard regex flags: g (global), i (case-insensitive), m (multiline), s (dotAll), u (unicode), y (sticky), d (match indices). Duplicate or unsupported flags are rejected.", "ui.agents.regexscripteditor.stringToTrim": "String to trim…", "ui.agents.regexscripteditor.testYourRegexPatternAgainstSampleTextMacrosUse": "Test your regex pattern against sample text. Macros use sample User and Character values here.", "ui.agents.regexscripteditor.theRegularExpressionPatternToSearchForWrittenWithout": "The regular expression pattern to search for. Written without delimiters. Macros resolve with sample values in Live Test and chat values at runtime.", "ui.agents.regexscripteditor.theReplacementStringSupportsCaptureGroups12Named": "The replacement string. Supports capture groups ($1, $2), named groups ($〈name〉), and case transforms before captures like \\u$1, \\U$1\\E, \\l$1, and \\L$1\\E. Literal backslash text such as C:\\Users is preserved.", "ui.agents.regexscripteditor.toCapitalizeTheFirstCharacterOfACaptureOr": "to capitalize the first character of a capture, or", + "ui.agents.regexscripteditor.togglePromptPresetScope": "Toggle prompt preset target scope", "ui.agents.regexscripteditor.toUppercaseACapture": "to uppercase a capture.", "ui.agents.regexscripteditor.trimStrings": "Trim Strings", "ui.agents.regexscripteditor.u1": "\\u$1", diff --git a/packages/server/src/db/schema/regex-scripts.ts b/packages/server/src/db/schema/regex-scripts.ts index 7a8b650bfa..9112a4b280 100644 --- a/packages/server/src/db/schema/regex-scripts.ts +++ b/packages/server/src/db/schema/regex-scripts.ts @@ -23,6 +23,8 @@ export const regexScripts = fileTable("regex_scripts", { applyMode: text("apply_mode"), /** JSON array of target recipient character IDs (empty = all recipients) */ targetCharacterIds: text("target_character_ids").notNull().default("[]"), + /** JSON array of target prompt preset IDs (empty = all presets) */ + targetPromptPresetIds: text("target_prompt_preset_ids").notNull().default("[]"), /** Execution order (lower = first) */ order: integer("order").notNull().default(0), /** Min message depth to apply (null = unlimited) */ diff --git a/packages/server/src/db/seed-regex.ts b/packages/server/src/db/seed-regex.ts index 3cc4c6a86f..5febf0d7a9 100644 --- a/packages/server/src/db/seed-regex.ts +++ b/packages/server/src/db/seed-regex.ts @@ -41,6 +41,7 @@ export async function seedDefaultRegexScripts(db: DB) { promptOnly: "true", applyMode: "prompt", targetCharacterIds: "[]", + targetPromptPresetIds: "[]", order: 0, minDepth: null, maxDepth: null, @@ -59,6 +60,7 @@ export async function seedDefaultRegexScripts(db: DB) { promptOnly: "false", applyMode: "display", targetCharacterIds: "[]", + targetPromptPresetIds: "[]", order: 10, minDepth: null, maxDepth: null, diff --git a/packages/server/src/routes/chats.routes.ts b/packages/server/src/routes/chats.routes.ts index 84bba933a3..654b87f89b 100644 --- a/packages/server/src/routes/chats.routes.ts +++ b/packages/server/src/routes/chats.routes.ts @@ -2493,6 +2493,7 @@ export async function chatsRoutes(app: FastifyInstance) { applyRegexScriptsToPromptMessages(mappedMessages, await regexStore.list(), { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), + targetPromptPresetId: (chat.promptPresetId as string | null) ?? null, }); promptMacroContext.lastInput = [...mappedMessages] .reverse() diff --git a/packages/server/src/routes/connections.routes.ts b/packages/server/src/routes/connections.routes.ts index 09941d616d..979eaf94cb 100644 --- a/packages/server/src/routes/connections.routes.ts +++ b/packages/server/src/routes/connections.routes.ts @@ -191,6 +191,13 @@ function localUrlPolicyForProvider(provider: string, imageSource: string) { }; } +export function buildGoogleModelsPageUrl(baseUrl: string, modelsEndpoint: string, pageToken = ""): string { + return ( + `${baseUrl}${modelsEndpoint}?pageSize=1000` + + (pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "") + ); +} + function normalizeConnectionTestBaseUrl(baseUrl: string, provider: string): string { if (provider !== "image_generation") return baseUrl.replace(/\/+$/, ""); try { @@ -972,9 +979,7 @@ export async function connectionsRoutes(app: FastifyInstance) { const collected: unknown[] = []; let pageToken = ""; for (let page = 0; page < 20; page++) { - const pageUrl = - `${baseUrl}${provider?.modelsEndpoint ?? "/models"}?key=${encodeURIComponent(conn.apiKey)}&pageSize=1000` + - (pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : ""); + const pageUrl = buildGoogleModelsPageUrl(baseUrl, provider?.modelsEndpoint ?? "/models", pageToken); const pageRes = await safeFetch(pageUrl, { headers, policy: { diff --git a/packages/server/src/routes/generate.routes.ts b/packages/server/src/routes/generate.routes.ts index 0a3aa21d99..91d0f8c4d6 100644 --- a/packages/server/src/routes/generate.routes.ts +++ b/packages/server/src/routes/generate.routes.ts @@ -1748,6 +1748,7 @@ export async function generateRoutes(app: FastifyInstance) { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), targetCharacterId: promptTargetCharacterId, + targetPromptPresetId: presetId ?? null, }); if (regenerateUserSourceMessage) { const sourceMessages = [regenerateUserSourceMessage]; @@ -1755,6 +1756,7 @@ export async function generateRoutes(app: FastifyInstance) { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), targetCharacterId: promptTargetCharacterId, + targetPromptPresetId: presetId ?? null, }); } @@ -5215,6 +5217,7 @@ export async function generateRoutes(app: FastifyInstance) { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), targetCharacterId: targetCharId, + targetPromptPresetId: presetId ?? null, targetedOnly: true, }); } @@ -9183,6 +9186,7 @@ export async function generateRoutes(app: FastifyInstance) { applyRegexScriptsToPromptMessages([newMariMsg], await regexScriptsStore.list(), { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), + targetPromptPresetId: presetId ?? null, }); newMariMsg.content = newMariMsg.content.replace(/\n([ \t]*\n){2,}/g, "\n\n"); runningMessagesForFollowUp.push(resolveHistoryMessageMacros([newMariMsg])[0] ?? newMariMsg); diff --git a/packages/server/src/routes/generate/dry-run-route.ts b/packages/server/src/routes/generate/dry-run-route.ts index fc57206e73..337f1711b2 100644 --- a/packages/server/src/routes/generate/dry-run-route.ts +++ b/packages/server/src/routes/generate/dry-run-route.ts @@ -845,6 +845,7 @@ export async function registerDryRunRoute(app: FastifyInstance) { applyRegexScriptsToPromptMessages(mappedMessages, await regexScriptsStore.list(), { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), targetCharacterId: promptTargetCharacterId, + targetPromptPresetId: effectivePresetId, }); for (const msg of mappedMessages) { diff --git a/packages/server/src/routes/scene.routes.ts b/packages/server/src/routes/scene.routes.ts index a337b0fc38..2f9517cb60 100644 --- a/packages/server/src/routes/scene.routes.ts +++ b/packages/server/src/routes/scene.routes.ts @@ -326,7 +326,9 @@ export async function sceneRoutes(app: FastifyInstance) { name: plan.name, mode: "roleplay", characterIds: finalParticipantIds, - groupId: originChat.groupId, + // Scene linkage is represented by connectedChatId. Reusing the origin's + // branch group crosses chat modes and can hide Conversation rows. + groupId: null, personaId: originChat.personaId, // Scene chats use the generated sceneSystemPrompt as their prompt source. // Copying the origin conversation preset can make those instructions clash. @@ -628,6 +630,8 @@ export async function sceneRoutes(app: FastifyInstance) { if (mode === "convert" && !originChatId) { return reply.status(400).send({ error: "convert requires originChatId" }); } + const originGroupId = originChatId ? (await chats.getById(originChatId))?.groupId : null; + const forkGroupId = sceneChat.groupId && sceneChat.groupId !== originGroupId ? sceneChat.groupId : null; // Sort explicitly before validating/slicing `upToMessageId` so "clone from // here" always copies a chronological prefix even if storage ordering changes. @@ -645,7 +649,7 @@ export async function sceneRoutes(app: FastifyInstance) { }`, mode: "roleplay", characterIds: parseCharacterIds(sceneChat.characterIds), - groupId: sceneChat.groupId, + groupId: forkGroupId, personaId: sceneChat.personaId, promptPresetId: sceneChat.promptPresetId, connectionId: sceneChat.connectionId, diff --git a/packages/server/src/services/import/st-character.importer.ts b/packages/server/src/services/import/st-character.importer.ts index 88b9da9b55..efe14286d5 100644 --- a/packages/server/src/services/import/st-character.importer.ts +++ b/packages/server/src/services/import/st-character.importer.ts @@ -168,6 +168,7 @@ function convertStRegexScripts( promptOnly: applyMode === "prompt", applyMode, targetCharacterIds: scope === "global" ? [] : [characterId], + targetPromptPresetIds: [], // Preserve the card's authoring order so multi-script imports keep a stable // execution/list order (all-zero ties leave it undefined). Gaps from skipped // entries are harmless — list() only sorts by ascending order. diff --git a/packages/server/src/services/mari-db/mari-db.service.ts b/packages/server/src/services/mari-db/mari-db.service.ts index d0d8c7346f..e415362eb1 100644 --- a/packages/server/src/services/mari-db/mari-db.service.ts +++ b/packages/server/src/services/mari-db/mari-db.service.ts @@ -331,7 +331,7 @@ const JSON_COLUMNS: Record = { // chat_images, character_images, assets, and custom_themes // have no JSON columns; their former entries named columns that do not exist. game_engine_state: ["state"], - regex_scripts: ["trimStrings", "placement", "targetCharacterIds"], + regex_scripts: ["trimStrings", "placement", "targetCharacterIds", "targetPromptPresetIds"], }; function buildTableMetas() { diff --git a/packages/server/src/services/professor-mari/workspace-agent.service.ts b/packages/server/src/services/professor-mari/workspace-agent.service.ts index c727648503..02d0a43bb6 100644 --- a/packages/server/src/services/professor-mari/workspace-agent.service.ts +++ b/packages/server/src/services/professor-mari/workspace-agent.service.ts @@ -522,6 +522,7 @@ ${MARI_GUIDED_SEQUENCES} - About Me writing: read the target character or persona first, write the bio in their own voice, then put it in \`patch.aboutMe\` on the matching update action with \`apply:true\`. - Lorebook generation: put the complete \`entries\` array inside \`data\` on \`lorebook.create\`. Marinara saves the lorebook and its entries together, so do not create an empty lorebook and promise to fill it later. - For \`preset.create\`, put prompt sections in \`data.sections\` and preset variables in \`data.choiceBlocks\`. Each choice block needs \`variableName\`, \`question\`, and \`options\` with \`label\`/\`value\` pairs. +- Custom image agents are supported by the live runtime. Use \`data.resultType: "image_prompt"\`, enable \`settings.customCapabilities.trigger_image_generation\`, and have the agent return \`shouldGenerate\` plus \`prompt\`. Marker-triggered agents should also set \`activationKeywords\`. Do not claim that only Illustrator can generate image prompts. - Existing-data changes: use \`apply:true\` for requested \`*.update\`, \`lorebook.updateEntry\`, and \`theme.setActive\`. Marinara will save first and show the user an in-chat Keep/Restore review card for reversible changes. - Personal Extensions: create or update the complete draft with \`apply:true\`, verify it with \`personal_extension.get\`, then tell the user the draft remains disabled until they review and run the exact hash and requested capabilities in Settings → Addons. Browser UI should use \`marinara.ui.registerContribution\` for \`button\`, \`menu-item\`, or \`panel\` slots; panel controls are host-rendered and return values through \`onEvent\`. Use \`marinara.context\` for active IDs and request \`read_active_characters\` or \`read_active_persona\` only for bounded active-record reads. Do not offer or invent an approval action, DOM access, direct app-data access, or network access. - Use \`apply:false\` only for explicit preview/dry-run requests or when you need to inspect validation before making a risky change. @@ -537,6 +538,7 @@ Verified lorebook creation sequence (three turns): {"say":"","commands":[{"name":"app_data","arguments":{"action":"lorebook.search","query":"The Glass City"}}],"stop":false} {"say":"Done — I created the lorebook and the verification read found it.","commands":[],"stop":true} {"say":"","commands":[{"name":"app_data","arguments":{"action":"preset.create","data":{"name":"Test preset","sections":[{"name":"Main","content":"You are {{char}}.","role":"system"}],"choiceBlocks":[{"variableName":"tone","question":"Tone","options":[{"label":"Warm","value":"warm"},{"label":"Sharp","value":"sharp"}]}]},"reason":"User requested a preset with variables","apply":true}}],"stop":false} +{"say":"","commands":[{"name":"app_data","arguments":{"action":"agent.create","data":{"name":"Image Marker","description":"Turns IMG_PROMPT markers into image prompts.","resultType":"image_prompt","activationKeywords":["IMG_PROMPT:"],"activationScanDepth":4,"settings":{"customCapabilities":{"trigger_image_generation":true}}},"reason":"User requested a marker-triggered image agent","apply":true}}],"stop":false} {"say":"","commands":[{"name":"app_data","arguments":{"action":"lorebook.updateEntry","entryId":"entry-id","patch":{"content":"new content"},"reason":"Update requested by user","apply":false}}],"stop":false} Available command schemas: diff --git a/packages/server/src/services/regex/regex-application.ts b/packages/server/src/services/regex/regex-application.ts index 8f6e011431..05add847ee 100644 --- a/packages/server/src/services/regex/regex-application.ts +++ b/packages/server/src/services/regex/regex-application.ts @@ -20,6 +20,7 @@ type RegexScriptLike = { promptOnly?: unknown; applyMode?: unknown; targetCharacterIds?: unknown; + targetPromptPresetIds?: unknown; minDepth?: unknown; maxDepth?: unknown; }; @@ -39,6 +40,7 @@ type ApplyRegexScriptOptions = { resolveMacros?: RegexMacroResolver; randomSeed?: string; targetCharacterId?: string | null; + targetPromptPresetId?: string | null; targetedOnly?: boolean; }; @@ -149,6 +151,11 @@ export function applyRegexScriptsToPromptText( const targetCharacterId = options?.targetCharacterId; if (!targetCharacterId || !targetCharacterIds.includes(targetCharacterId)) continue; } + const targetPromptPresetIds = parseStringArray(script.targetPromptPresetIds); + if (targetPromptPresetIds.length > 0) { + const targetPromptPresetId = options?.targetPromptPresetId; + if (!targetPromptPresetId || !targetPromptPresetIds.includes(targetPromptPresetId)) continue; + } const minDepth = depthValue(script.minDepth); const maxDepth = depthValue(script.maxDepth); diff --git a/packages/server/src/services/storage/chat-presets.storage.ts b/packages/server/src/services/storage/chat-presets.storage.ts index 02a4d2d306..36ad85725f 100644 --- a/packages/server/src/services/storage/chat-presets.storage.ts +++ b/packages/server/src/services/storage/chat-presets.storage.ts @@ -340,7 +340,23 @@ export function createChatPresetsStorage(db: DB) { .where( and(eq(chatPresets.mode, mode), eq(chatPresets.isDefault, "true")), )) as ChatPresetRow[]; - let defaultId = defaultRows[0]?.id ?? null; + const canonicalDefault = [...defaultRows].sort( + (left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id), + )[0]; + let defaultId = canonicalDefault?.id ?? null; + if (canonicalDefault) { + if (canonicalDefault.name !== "Default" || canonicalDefault.settings !== "{}") { + await tx + .update(chatPresets) + .set({ name: "Default", settings: JSON.stringify({}) }) + .where(eq(chatPresets.id, canonicalDefault.id)); + } + for (const duplicate of defaultRows) { + if (duplicate.id !== canonicalDefault.id) { + await tx.delete(chatPresets).where(eq(chatPresets.id, duplicate.id)); + } + } + } if (!defaultId) { const id = newId(); const ts = now(); diff --git a/packages/server/src/services/storage/regex-scripts.storage.ts b/packages/server/src/services/storage/regex-scripts.storage.ts index 8c3d5ada07..0368c20ca6 100644 --- a/packages/server/src/services/storage/regex-scripts.storage.ts +++ b/packages/server/src/services/storage/regex-scripts.storage.ts @@ -54,6 +54,7 @@ export function createRegexScriptsStorage(db: DB) { promptOnly: promptOnlyForApplyMode(applyMode), applyMode, targetCharacterIds: JSON.stringify(input.targetCharacterIds ?? []), + targetPromptPresetIds: JSON.stringify(input.targetPromptPresetIds ?? []), order, minDepth: input.minDepth ?? null, maxDepth: input.maxDepth ?? null, @@ -80,6 +81,9 @@ export function createRegexScriptsStorage(db: DB) { if (data.targetCharacterIds !== undefined) { updateFields.targetCharacterIds = JSON.stringify(data.targetCharacterIds); } + if (data.targetPromptPresetIds !== undefined) { + updateFields.targetPromptPresetIds = JSON.stringify(data.targetPromptPresetIds); + } if (data.order !== undefined) updateFields.order = data.order; if (data.minDepth !== undefined) updateFields.minDepth = data.minDepth; if (data.maxDepth !== undefined) updateFields.maxDepth = data.maxDepth; diff --git a/packages/shared/src/schemas/regex.schema.ts b/packages/shared/src/schemas/regex.schema.ts index f6cc372bac..958d4a5942 100644 --- a/packages/shared/src/schemas/regex.schema.ts +++ b/packages/shared/src/schemas/regex.schema.ts @@ -46,6 +46,7 @@ const regexScriptShape = z.object({ promptOnly: z.boolean().default(false), applyMode: regexApplyModeSchema.optional(), targetCharacterIds: z.array(z.string().min(1)).default([]), + targetPromptPresetIds: z.array(z.string().min(1)).default([]), order: z.number().int().optional(), minDepth: z.number().int().nullable().default(null), maxDepth: z.number().int().nullable().default(null), diff --git a/packages/shared/src/types/regex.ts b/packages/shared/src/types/regex.ts index cf2bf08766..16fae5b507 100644 --- a/packages/shared/src/types/regex.ts +++ b/packages/shared/src/types/regex.ts @@ -31,6 +31,8 @@ export interface RegexScript { applyMode?: RegexApplyMode; /** Prompt recipient character IDs this script is limited to (empty = all recipients) */ targetCharacterIds: string[]; + /** Prompt preset IDs this script is limited to (empty = all presets) */ + targetPromptPresetIds: string[]; /** Execution order (lower = runs first) */ order: number; /** Optional minimum message depth to apply (null = no limit) */ diff --git a/scripts/regressions/chat-branch-lineage.regression.ts b/scripts/regressions/chat-branch-lineage.regression.ts index 365baa236b..a56dbaf2ed 100644 --- a/scripts/regressions/chat-branch-lineage.regression.ts +++ b/scripts/regressions/chat-branch-lineage.regression.ts @@ -20,11 +20,11 @@ try { app = await buildApp(); await app.ready(); - const create = async (name: string) => { + const create = async (name: string, mode: "conversation" | "roleplay" = "roleplay") => { const response = await app!.inject({ method: "POST", url: "/api/chats", - payload: { name, mode: "roleplay", characterIds: [] }, + payload: { name, mode, characterIds: [] }, }); assert.equal(response.statusCode, 200); return response.json(); @@ -96,6 +96,64 @@ try { }); assert.deepEqual((await persistence.getChat(branch.id))?.branch, listedBranch?.branch); + const conversation = await create("Conversation scene origin", "conversation"); + await addMessage(conversation.id, "Convert this conversation into a scene"); + const conversationBranchResponse = await app.inject({ + method: "POST", + url: `/api/chats/${conversation.id}/branch`, + payload: {}, + }); + assert.equal(conversationBranchResponse.statusCode, 200); + const groupedConversation = (await app.inject({ method: "GET", url: `/api/chats/${conversation.id}` })).json(); + assert.equal(typeof groupedConversation.groupId, "string"); + + const sceneCreateResponse = await app.inject({ + method: "POST", + url: "/api/scene/create", + payload: { + originChatId: conversation.id, + initiatorCharId: null, + plan: { + name: "Converted roleplay scene", + description: "A quiet laboratory after midnight.", + scenario: "Verify cross-mode lineage remains separate.", + firstMessage: "The instruments hum softly.", + background: null, + characterIds: [], + systemPrompt: "Keep the scene concise.", + rating: "sfw", + relationshipHistory: "A regression fixture.", + participationGuide: "Continue the scene.", + }, + }, + }); + assert.equal(sceneCreateResponse.statusCode, 200); + const sceneChatId = sceneCreateResponse.json().chatId; + const sceneChat = (await app.inject({ method: "GET", url: `/api/chats/${sceneChatId}` })).json(); + assert.equal(sceneChat.mode, "roleplay"); + assert.equal(sceneChat.groupId, null, "A converted scene must not join the conversation branch group"); + + const legacyScenePatch = await app.inject({ + method: "PATCH", + url: `/api/chats/${sceneChat.id}`, + payload: { groupId: groupedConversation.groupId }, + }); + assert.equal(legacyScenePatch.statusCode, 200); + const sceneForkResponse = await app.inject({ + method: "POST", + url: "/api/scene/fork", + payload: { sceneChatId: sceneChat.id, mode: "convert" }, + }); + assert.equal(sceneForkResponse.statusCode, 200); + const forkedSceneChat = ( + await app.inject({ method: "GET", url: `/api/chats/${sceneForkResponse.json().chatId}` }) + ).json(); + assert.equal( + forkedSceneChat.groupId, + null, + "Forking a legacy converted scene must not retain a cross-mode branch group", + ); + const exportResponse = await app.inject({ method: "GET", url: `/api/chats/${branch.id}/export?format=jsonl`, diff --git a/scripts/regressions/open-issues.regression.ts b/scripts/regressions/open-issues.regression.ts index 50b0ec7029..6ed8fabbed 100644 --- a/scripts/regressions/open-issues.regression.ts +++ b/scripts/regressions/open-issues.regression.ts @@ -8,7 +8,7 @@ import type { Chat, ChatMode, Message } from "../../packages/shared/src/types/ch import { chatModeSchema } from "../../packages/shared/src/schemas/chat.schema.js"; import playwrightConfig from "../../playwright.config.js"; import { resolveDevSharedBuildScript } from "../dev-shared-build.mjs"; -import { characterCardVersions, characters, chats, messages } from "../../packages/server/src/db/schema/index.js"; +import { characterCardVersions, characters, chatPresets, chats, messages } from "../../packages/server/src/db/schema/index.js"; import { eq } from "../../packages/server/src/db/file-query.js"; import { parseBuildMeta, resolveBuildBranch } from "../../packages/server/src/config/build-info.js"; @@ -186,6 +186,8 @@ import { createCustomToolsStorage } from "../../packages/server/src/services/sto import { createCharactersStorage } from "../../packages/server/src/services/storage/characters.storage.js"; import { createLorebooksStorage } from "../../packages/server/src/services/storage/lorebooks.storage.js"; import { createNoodleStorage } from "../../packages/server/src/services/storage/noodle.storage.js"; +import { createChatPresetsStorage } from "../../packages/server/src/services/storage/chat-presets.storage.js"; +import { buildGoogleModelsPageUrl } from "../../packages/server/src/routes/connections.routes.js"; import { buildReferencedCharacterContext } from "../../packages/server/src/services/prompt/macro-context.js"; import { resolveRunPodComfyUiTimeoutSeconds } from "../../packages/server/src/services/image/runpod-comfyui.service.js"; import { @@ -650,6 +652,44 @@ try { const characterStorage = createCharactersStorage(db); const lorebookStorage = createLorebooksStorage(db); const noodleStorage = createNoodleStorage(db); + const chatPresetStorage = createChatPresetsStorage(db); + await chatPresetStorage.ensureDefaults(); + const originalConversationDefault = await chatPresetStorage.getDefault("conversation"); + assert.ok(originalConversationDefault, "Conversation mode must start with a Default settings profile"); + await db.insert(chatPresets).values({ + id: "duplicate-conversation-default", + name: "Default Copy", + mode: "conversation", + isDefault: "true", + isActive: "true", + settings: JSON.stringify({ connectionId: "must-be-reset" }), + createdAt: "9999-12-31T23:59:59.000Z", + updatedAt: "9999-12-31T23:59:59.000Z", + }); + await chatPresetStorage.ensureDefaults(); + const normalizedConversationProfiles = await chatPresetStorage.listByMode("conversation"); + assert.equal( + normalizedConversationProfiles.filter((profile) => profile.isDefault).length, + 1, + "Default settings profile repair must remove duplicate built-ins", + ); + assert.equal( + normalizedConversationProfiles.filter((profile) => profile.isActive).length, + 1, + "Default settings profile repair must leave exactly one active profile", + ); + assert.deepEqual(await chatPresetStorage.getDefault("conversation"), { + ...originalConversationDefault, + name: "Default", + isActive: true, + settings: {}, + }); + await chatPresetStorage.ensureDefaults(); + assert.equal( + (await chatPresetStorage.listByMode("conversation")).filter((profile) => profile.isDefault).length, + 1, + "Default settings profile repair must be idempotent", + ); const storageTrimFixture = await characterStorage.create({ ...characterDataSchema.parse({ name: "Storage trim fixture" }), name: " Storage trim fixture ", @@ -1230,6 +1270,17 @@ try { rmSync(characterUpdateStorageRoot, { recursive: true, force: true }); } +const googleModelsPageUrl = buildGoogleModelsPageUrl( + "https://gemini-proxy.example.test/v1beta", + "/models", + "next page/token", +); +assert.equal( + googleModelsPageUrl, + "https://gemini-proxy.example.test/v1beta/models?pageSize=1000&pageToken=next%20page%2Ftoken", +); +assert.equal(new URL(googleModelsPageUrl).searchParams.has("key"), false, "Gemini API keys must stay out of model URLs"); + const professorMariAboutMeCommands = parseCharacterCommands( '[update_character: name="Luna", about_me="fate dealer. tea hoarder. 🔮"]\n' + '[update_persona: name="Alex Storm", about_me=""]', diff --git a/scripts/regressions/prompt.regression.ts b/scripts/regressions/prompt.regression.ts index f6456e0864..7a12acd285 100644 --- a/scripts/regressions/prompt.regression.ts +++ b/scripts/regressions/prompt.regression.ts @@ -1310,6 +1310,9 @@ const cases: RegressionCase[] = [ assert.match(seededMariSource, /\$\{PROFESSOR_MARI_AGENT_CATALOG_KNOWLEDGE\}/u); assert.match(workspaceMariSource, /\$\{PROFESSOR_MARI_AGENT_CATALOG_KNOWLEDGE\}/u); + assert.match(workspaceMariSource, /"resultType":"image_prompt"/u); + assert.match(workspaceMariSource, /"activationKeywords":\["IMG_PROMPT:"\]/u); + assert.match(workspaceMariSource, /"trigger_image_generation":true/u); }, }, { @@ -2906,6 +2909,7 @@ const cases: RegressionCase[] = [ promptOnly: "true", applyMode: "prompt", targetCharacterIds: "[]", + targetPromptPresetIds: "[]", minDepth: null, maxDepth: null, }, @@ -2923,6 +2927,35 @@ const cases: RegressionCase[] = [ assert.match(cleaned, //u); }, }, + { + name: "prompt preset-scoped regexes run only for their selected preset", + run() { + const script = { + id: "preset-scoped-regex", + enabled: "true", + findRegex: "LAB", + replaceString: "PALACE", + placement: '["user_input"]', + flags: "g", + applyMode: "prompt", + targetCharacterIds: "[]", + targetPromptPresetIds: '["preset-laboratory"]', + }; + assert.equal(applyRegexScriptsToPromptText("LAB", [script], "user_input", 0), "LAB"); + assert.equal( + applyRegexScriptsToPromptText("LAB", [script], "user_input", 0, { + targetPromptPresetId: "preset-ballroom", + }), + "LAB", + ); + assert.equal( + applyRegexScriptsToPromptText("LAB", [script], "user_input", 0, { + targetPromptPresetId: "preset-laboratory", + }), + "PALACE", + ); + }, + }, { name: "Clean HTML seed migration only replaces the exact broken built-in pattern", run() { From 38af40654af173b78be36928b4999663bb04e261 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sun, 2 Aug 2026 09:35:41 +0200 Subject: [PATCH 03/99] address CodeRabbit review --- packages/server/src/routes/chats.routes.ts | 2 +- .../services/storage/chat-presets.storage.ts | 28 +++++++++++++++++-- .../chat-branch-lineage.regression.ts | 22 +++++++++++++++ scripts/regressions/open-issues.regression.ts | 25 +++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/server/src/routes/chats.routes.ts b/packages/server/src/routes/chats.routes.ts index 654b87f89b..6839e38a4d 100644 --- a/packages/server/src/routes/chats.routes.ts +++ b/packages/server/src/routes/chats.routes.ts @@ -2493,7 +2493,7 @@ export async function chatsRoutes(app: FastifyInstance) { applyRegexScriptsToPromptMessages(mappedMessages, await regexStore.list(), { resolveMacros: (value, randomSeed) => resolveMacros(value, promptMacroContext, { trimResult: false, randomSeed }), - targetPromptPresetId: (chat.promptPresetId as string | null) ?? null, + targetPromptPresetId: presetId, }); promptMacroContext.lastInput = [...mappedMessages] .reverse() diff --git a/packages/server/src/services/storage/chat-presets.storage.ts b/packages/server/src/services/storage/chat-presets.storage.ts index 36ad85725f..6163901896 100644 --- a/packages/server/src/services/storage/chat-presets.storage.ts +++ b/packages/server/src/services/storage/chat-presets.storage.ts @@ -28,6 +28,17 @@ function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } +function parseStoredChatMetadata(value: unknown): Record | null { + if (isRecord(value)) return value; + if (typeof value !== "string") return null; + try { + const parsed = JSON.parse(value); + return isRecord(parsed) ? parsed : null; + } catch { + return null; + } +} + function sanitizePresetAgentIds(value: unknown) { if (!Array.isArray(value)) return []; return value.flatMap((agentId) => { @@ -351,8 +362,21 @@ export function createChatPresetsStorage(db: DB) { .set({ name: "Default", settings: JSON.stringify({}) }) .where(eq(chatPresets.id, canonicalDefault.id)); } - for (const duplicate of defaultRows) { - if (duplicate.id !== canonicalDefault.id) { + const duplicates = defaultRows.filter((row) => row.id !== canonicalDefault.id); + if (duplicates.length > 0) { + const duplicateIds = new Set(duplicates.map((row) => row.id)); + const modeChats = await tx.select().from(chats).where(eq(chats.mode, mode)); + for (const chat of modeChats) { + const metadata = parseStoredChatMetadata(chat.metadata); + if (!metadata || !duplicateIds.has(String(metadata.appliedChatPresetId ?? ""))) continue; + await tx + .update(chats) + .set({ + metadata: JSON.stringify({ ...metadata, appliedChatPresetId: canonicalDefault.id }), + }) + .where(eq(chats.id, chat.id)); + } + for (const duplicate of duplicates) { await tx.delete(chatPresets).where(eq(chatPresets.id, duplicate.id)); } } diff --git a/scripts/regressions/chat-branch-lineage.regression.ts b/scripts/regressions/chat-branch-lineage.regression.ts index a56dbaf2ed..3f2ff7a064 100644 --- a/scripts/regressions/chat-branch-lineage.regression.ts +++ b/scripts/regressions/chat-branch-lineage.regression.ts @@ -133,6 +133,28 @@ try { assert.equal(sceneChat.mode, "roleplay"); assert.equal(sceneChat.groupId, null, "A converted scene must not join the conversation branch group"); + const distinctSceneGroupId = "scene-owned-branch-group"; + const distinctScenePatch = await app.inject({ + method: "PATCH", + url: `/api/chats/${sceneChat.id}`, + payload: { groupId: distinctSceneGroupId }, + }); + assert.equal(distinctScenePatch.statusCode, 200); + const distinctGroupForkResponse = await app.inject({ + method: "POST", + url: "/api/scene/fork", + payload: { sceneChatId: sceneChat.id, mode: "clone" }, + }); + assert.equal(distinctGroupForkResponse.statusCode, 200); + const distinctGroupFork = ( + await app.inject({ method: "GET", url: `/api/chats/${distinctGroupForkResponse.json().chatId}` }) + ).json(); + assert.equal( + distinctGroupFork.groupId, + distinctSceneGroupId, + "Forking a scene with its own branch group must preserve that group", + ); + const legacyScenePatch = await app.inject({ method: "PATCH", url: `/api/chats/${sceneChat.id}`, diff --git a/scripts/regressions/open-issues.regression.ts b/scripts/regressions/open-issues.regression.ts index 6ed8fabbed..71eb92e467 100644 --- a/scripts/regressions/open-issues.regression.ts +++ b/scripts/regressions/open-issues.regression.ts @@ -656,6 +656,10 @@ try { await chatPresetStorage.ensureDefaults(); const originalConversationDefault = await chatPresetStorage.getDefault("conversation"); assert.ok(originalConversationDefault, "Conversation mode must start with a Default settings profile"); + await db + .update(chatPresets) + .set({ name: "Broken Default", settings: JSON.stringify({ connectionId: "must-be-reset" }) }) + .where(eq(chatPresets.id, originalConversationDefault.id)); await db.insert(chatPresets).values({ id: "duplicate-conversation-default", name: "Default Copy", @@ -666,6 +670,17 @@ try { createdAt: "9999-12-31T23:59:59.000Z", updatedAt: "9999-12-31T23:59:59.000Z", }); + const duplicateDefaultChatId = "duplicate-default-profile-reference"; + await db.insert(chats).values({ + id: duplicateDefaultChatId, + name: "Duplicate Default profile reference", + mode: "conversation", + characterIds: "[]", + metadata: JSON.stringify({ appliedChatPresetId: "duplicate-conversation-default", preserved: true }), + sortOrder: 0, + createdAt: "2026-08-02T06:00:00.000Z", + updatedAt: "2026-08-02T06:00:00.000Z", + }); await chatPresetStorage.ensureDefaults(); const normalizedConversationProfiles = await chatPresetStorage.listByMode("conversation"); assert.equal( @@ -684,6 +699,16 @@ try { isActive: true, settings: {}, }); + const reboundDefaultChat = (await db.select().from(chats).where(eq(chats.id, duplicateDefaultChatId)))[0]; + assert.ok(reboundDefaultChat); + assert.deepEqual(JSON.parse(reboundDefaultChat.metadata), { + appliedChatPresetId: originalConversationDefault.id, + preserved: true, + }); + assert.ok( + await chatPresetStorage.getById(JSON.parse(reboundDefaultChat.metadata).appliedChatPresetId), + "Rebound chat profile references must remain resolvable after duplicate cleanup", + ); await chatPresetStorage.ensureDefaults(); assert.equal( (await chatPresetStorage.listByMode("conversation")).filter((profile) => profile.isDefault).length, From 52023ce478e94de3d32c3056e29d5f6ae61fd9a3 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Sun, 2 Aug 2026 11:07:50 +0200 Subject: [PATCH 04/99] fix clipped sidebar and call settings rows --- CHANGELOG.md | 1 + .../src/components/panels/AgentsPanel.tsx | 8 ++- .../src/components/panels/CharactersPanel.tsx | 14 ++++- .../components/panels/ConnectionsPanel.tsx | 2 +- .../src/components/panels/LorebooksPanel.tsx | 7 ++- .../src/components/panels/PersonasPanel.tsx | 18 ++++-- .../src/components/panels/PresetsPanel.tsx | 2 +- .../src/components/panels/SettingsPanel.tsx | 14 ++--- scripts/regressions/open-issues.regression.ts | 55 +++++++++++++++++++ 9 files changed, 104 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ee1e84f6..bfe6934324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This file is the release-notes source of truth for Marinara Engine. Reuse these ### Fixed +- Let Characters, Personas, Lorebooks, Agents, Presets, and Connections sidebar labels use the full desktop row width beneath hover actions, and made Conversation Call clip-length rows size to their panel instead of clipping labels beside fixed-width fields (#4449). - Removed accumulated duplicate built-in **Default** settings profiles during startup normalization while preserving one stable profile and active selection per chat mode (#4442). - Kept `/scene` chats and standalone conversions out of Conversation branch groups so original conversations remain visible in the Conversation sidebar (#4443). - Taught Professor Mari the supported custom `image_prompt` agent configuration, including marker activation and the image-generation capability, so she creates requested image agents instead of falsely refusing them (#4444). diff --git a/packages/client/src/components/panels/AgentsPanel.tsx b/packages/client/src/components/panels/AgentsPanel.tsx index f859c721ea..d2447cfeac 100644 --- a/packages/client/src/components/panels/AgentsPanel.tsx +++ b/packages/client/src/components/panels/AgentsPanel.tsx @@ -1633,7 +1633,13 @@ function renderAgentCard({ )}
)} -
+
{/* Info */} -
+
)} -
+
{conn.name}
diff --git a/packages/client/src/components/panels/LorebooksPanel.tsx b/packages/client/src/components/panels/LorebooksPanel.tsx index 355cf146af..02e03b28df 100644 --- a/packages/client/src/components/panels/LorebooksPanel.tsx +++ b/packages/client/src/components/panels/LorebooksPanel.tsx @@ -1238,7 +1238,12 @@ function LorebookRow({ )} -
+
{lorebook.name} {!lorebook.enabled && ( diff --git a/packages/client/src/components/panels/PersonasPanel.tsx b/packages/client/src/components/panels/PersonasPanel.tsx index 8b8b90a463..46eb6246c9 100644 --- a/packages/client/src/components/panels/PersonasPanel.tsx +++ b/packages/client/src/components/panels/PersonasPanel.tsx @@ -933,7 +933,7 @@ export function PersonasPanel() { role="button" tabIndex={0} className={cn( - "group group/member flex touch-pan-y cursor-pointer items-center gap-2 rounded-lg p-1.5 text-xs transition-all hover:bg-[var(--sidebar-accent)]", + "group group/member relative flex touch-pan-y cursor-pointer items-center gap-2 rounded-lg p-1.5 text-xs transition-all hover:bg-[var(--sidebar-accent)]", touchSafePersonaDragMode && "select-none", selectionMode && isBulkSelected && @@ -988,7 +988,12 @@ export function PersonasPanel() { )}
-
+
{p.name}
{p.comment && (
@@ -1005,7 +1010,7 @@ export function PersonasPanel() {
{!selectionMode && ( -
+
{/* Info */} -
+
{persona.name}
{persona.comment && (
diff --git a/packages/client/src/components/panels/PresetsPanel.tsx b/packages/client/src/components/panels/PresetsPanel.tsx index 6dcffe4029..d8e48f6c5d 100644 --- a/packages/client/src/components/panels/PresetsPanel.tsx +++ b/packages/client/src/components/panels/PresetsPanel.tsx @@ -902,7 +902,7 @@ export function PresetsPanel() { className={cn( "min-w-0 flex-1", !selectionMode && - "pr-0 transition-[padding] max-md:pr-32 [@media(pointer:coarse)]:pr-32 [@media(pointer:fine)]:group-hover:pr-32", + "pr-0 max-md:pr-32 [@media(pointer:coarse)]:pr-32", )} >
diff --git a/packages/client/src/components/panels/SettingsPanel.tsx b/packages/client/src/components/panels/SettingsPanel.tsx index 8f5f3a3eab..f655709d68 100644 --- a/packages/client/src/components/panels/SettingsPanel.tsx +++ b/packages/client/src/components/panels/SettingsPanel.tsx @@ -3751,22 +3751,22 @@ function VideoGenerationSettings() {
{localizeUi("ui.panels.videogenerationsettings.conversationCallClips")}
-
+
{CONVERSATION_CALL_CHARACTER_VIDEO_CLIP_KINDS.map((kind) => ( ))}
-