Ship 0.15.0: installable persona packs and slimmer bundled set.

Load Assistent/extensions packs between bundled and overlay, keep neutral/aggressive/dreamer, drop cinema/lewd/private shelves, and make horny slash commands schema-driven.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 08:28:31 +03:00
co-authored by Cursor
parent e2d48b4bf9
commit 47d352a7af
42 changed files with 353 additions and 429 deletions
+30 -26
View File
@@ -6014,9 +6014,9 @@ ${patch.prompt}`;
return {
id,
title: known && known.title || ({
neutral: "\u041D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u044B\u0439",
lewd: "\u041F\u043E\u0448\u043B\u044F\u043A",
aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439"
neutral: "\u041D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439",
aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439",
dreamer: "\u041C\u0435\u0447\u0442\u0430\u0442\u0435\u043B\u044C"
}[id] || id)
};
}
@@ -6753,13 +6753,16 @@ ${data.ui.help_extra}`.trim();
renderPersonaControls(data.controls || {}, controlValues);
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source);
}
function isDeletablePersonaSource(source) {
const src = String(source || "");
return src === "overlay" || src === "overlay+bundled" || src === "overlay+pack";
}
function syncPersonaDeleteButton(source) {
const btn = $2("sa_persona_delete");
if (!btn) {
return;
}
const src = String(source || "");
const canDelete = src === "overlay" || src === "overlay+bundled";
const canDelete = isDeletablePersonaSource(source);
btn.hidden = !canDelete;
btn.disabled = !canDelete;
}
@@ -6878,17 +6881,12 @@ ${data.ui.help_extra}`.trim();
return Number.isFinite(n) ? n : fallback;
}
function coolDownHorny() {
const persona = $2("sa_persona")?.value || "";
if (persona !== "leonid") {
setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
return;
}
state.lastUserControlIntent = true;
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus("\u0423 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
return;
}
const min = Number(schema.horny.min ?? 0);
const max = Number(schema.horny.max ?? 100);
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
@@ -6909,12 +6907,12 @@ ${data.ui.help_extra}`.trim();
setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`);
}
async function startHornyGame() {
const persona = $2("sa_persona")?.value || "";
if (persona !== "leonid") {
setStatus("/horny-game \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus("/horny-game: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
return;
}
const cur = getControlValue("horny", 35);
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
state.lastUserControlIntent = true;
await sendChat({
skipSlash: true,
@@ -6992,12 +6990,12 @@ ${data.ui.help_extra}`.trim();
const meta = (state.personas || []).find((p) => p.id === id);
const title = meta?.title || id;
const src = meta?.source || state.config?.persona_source || "";
if (src !== "overlay" && src !== "overlay+bundled") {
setStatus("Bundled personas cannot be deleted");
if (src !== "overlay" && src !== "overlay+bundled" && src !== "overlay+pack") {
setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay-\u0441\u043B\u043E\u0439 (bundled/pack \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E\u0442\u0441\u044F)");
return;
}
if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \xAB${title}\xBB?
\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) {
\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled/pack) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) {
return;
}
await new Promise((resolve) => {
@@ -7644,6 +7642,12 @@ ${data.ui.help_extra}`.trim();
if (source === "overlay+bundled") {
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E+\u043F\u0440\u0430\u0432\u043A\u0430";
}
if (source === "overlay+pack") {
return "\u043F\u0430\u043A+\u043F\u0440\u0430\u0432\u043A\u0430";
}
if (source === "pack") {
return "\u043F\u0430\u043A";
}
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E";
}
function renderPersonaSettingsList() {
@@ -7676,7 +7680,7 @@ ${data.ui.help_extra}`.trim();
function syncPersonaPanelActions() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
const canDelete = p && (p.source === "overlay" || p.source === "overlay+bundled");
const canDelete = p && isDeletablePersonaSource(p.source);
const del = $2("sa_btn_persona_delete_panel");
if (del) {
del.disabled = !canDelete;
@@ -7745,8 +7749,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
return;
}
let newId = pack?.id || "";
if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "overlay+bundled"))) {
newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "pack" || p.source === "overlay+bundled" || p.source === "overlay+pack"))) {
newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled/pack \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
}
genericRequest(
"AssistentImportPersona",
@@ -7794,7 +7798,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
function deleteSelectedOverlayPersona() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
if (!p || p.source !== "overlay" && p.source !== "overlay+bundled") {
if (!p || !isDeletablePersonaSource(p.source)) {
setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay");
return;
}
@@ -8488,8 +8492,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
why.push("session_exact \u043F\u0443\u0441\u0442 \u2014 params \u0438\u0437 Exact + \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430");
}
why.push(`\u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430: ${profile} (\u0438\u043C\u044F/title \u2192 turbo|raw)`);
if (persona === "cinema" || state.exact?.generation?.aspect) {
why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || "\u2014"}`);
if (state.exact?.generation?.aspect) {
why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || "\u2014"}`);
}
if (state.lastPatch) {
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== "notes");
+215 -47
View File
@@ -199,6 +199,125 @@ public sealed class AssistentConfig
return last;
}
/// <summary>Installed git persona pack: id → shelf directory under Assistent/extensions/.</summary>
public sealed record PackPersona(string Id, string ShelfRoot);
/// <summary>Parse minimal assistent-pack.yaml (kind + id). No full YAML dependency.</summary>
public static Dictionary<string, string> TryParsePackManifest(string path)
{
Dictionary<string, string> map = new(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return map;
}
try
{
foreach (string rawLine in File.ReadAllLines(path, Encoding.UTF8))
{
string line = rawLine.Trim();
if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#') || line.StartsWith("---"))
{
continue;
}
int colon = line.IndexOf(':');
if (colon <= 0)
{
continue;
}
string key = line[..colon].Trim();
string val = line[(colon + 1)..].Trim().Trim('"', '\'');
if (!string.IsNullOrWhiteSpace(key))
{
map[key] = val;
}
}
}
catch (Exception ex)
{
Logs.Debug($"AssistentConfig pack.yaml {path}: {ex.Message}");
}
return map;
}
/// <summary>Discover persona packs under Assistent/extensions/*/.</summary>
public List<PackPersona> DiscoverPackPersonas()
{
List<PackPersona> found = [];
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
string extRoot = Path.Combine(_overlayRoot, "extensions");
if (!Directory.Exists(extRoot))
{
return found;
}
foreach (string packDir in Directory.GetDirectories(extRoot).OrderBy(d => d, StringComparer.OrdinalIgnoreCase))
{
Dictionary<string, string> manifest = TryParsePackManifest(Path.Combine(packDir, "assistent-pack.yaml"));
string kind = "persona";
if (manifest.TryGetValue("kind", out string kindVal) && !string.IsNullOrWhiteSpace(kindVal))
{
kind = kindVal.Trim();
}
if (!string.Equals(kind, "persona", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(kind, "personas", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string multi = Path.Combine(packDir, "personas");
if (Directory.Exists(multi))
{
foreach (string folder in Directory.GetDirectories(multi))
{
string id = SafeId(Path.GetFileName(folder));
if (id is null || !File.Exists(Path.Combine(folder, "persona.json")))
{
continue;
}
if (seen.Add(id))
{
found.Add(new PackPersona(id, folder));
}
}
continue;
}
string singleId = null;
if (manifest.TryGetValue("id", out string idVal))
{
singleId = SafeId(idVal);
}
if (singleId is null || !File.Exists(Path.Combine(packDir, "persona.json")))
{
continue;
}
if (seen.Add(singleId))
{
found.Add(new PackPersona(singleId, packDir));
}
}
return found;
}
public string PackShelfRoot(string personaId)
{
string id = SafeId(personaId);
if (id is null)
{
return null;
}
return DiscoverPackPersonas().FirstOrDefault(p => string.Equals(p.Id, id, StringComparison.OrdinalIgnoreCase))?.ShelfRoot;
}
JObject TryReadPersonaMeta(string personaId)
{
string id = SafeId(personaId);
if (id is null)
{
return null;
}
return TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", id), "persona.json"))
?? TryReadJson(ResolveUnder(PackShelfRoot(id) ?? "", "persona.json"))
?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", id), "persona.json"));
}
/// <summary>Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve).</summary>
public List<string> PersonaExtendsChain(string personaId)
{
@@ -212,8 +331,7 @@ public sealed class AssistentConfig
break;
}
chain.Insert(0, cur);
JObject meta = TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", cur), "persona.json"))
?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", cur), "persona.json"));
JObject meta = TryReadPersonaMeta(cur);
string parent = SafeId(meta?["extends"]?.ToString());
if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, cur, StringComparison.OrdinalIgnoreCase))
{
@@ -226,11 +344,21 @@ public sealed class AssistentConfig
public IEnumerable<string> LayerRoots(string personaId)
{
Dictionary<string, string> packs = DiscoverPackPersonas()
.ToDictionary(p => p.Id, p => p.ShelfRoot, StringComparer.OrdinalIgnoreCase);
yield return Path.Combine(_bundledRoot, "_base");
foreach (string id in PersonaExtendsChain(personaId))
{
yield return Path.Combine(_bundledRoot, "personas", id);
}
// Pack shelves sit between bundled and overlay (overlay Exact / edits win).
foreach (string id in PersonaExtendsChain(personaId))
{
if (packs.TryGetValue(id, out string shelf) && !string.IsNullOrWhiteSpace(shelf))
{
yield return shelf;
}
}
yield return Path.Combine(_overlayRoot, "_base");
foreach (string id in PersonaExtendsChain(personaId))
{
@@ -241,7 +369,30 @@ public sealed class AssistentConfig
public List<(string id, string title, string accent, string source)> ListPersonaCatalog()
{
Dictionary<string, (string title, string accent, string source)> byId = new(StringComparer.OrdinalIgnoreCase);
void Scan(string root, string source)
void ScanFolder(string folder, string idHint, string source)
{
string id = SafeId(idHint) ?? SafeId(Path.GetFileName(folder));
if (id is null)
{
return;
}
JObject meta = TryReadJson(Path.Combine(folder, "persona.json"));
if (meta is null)
{
return;
}
if (meta["enabled"]?.Value<bool?>() == false)
{
byId.Remove(id);
return;
}
byId[id] = (
meta["title"]?.ToString() ?? id,
meta["accent"]?.ToString() ?? "#8b949e",
source
);
}
void ScanPersonasRoot(string root, string source)
{
string dir = Path.Combine(root, "personas");
if (!Directory.Exists(dir))
@@ -250,32 +401,17 @@ public sealed class AssistentConfig
}
foreach (string folder in Directory.GetDirectories(dir))
{
string id = SafeId(Path.GetFileName(folder));
if (id is null)
ScanFolder(folder, Path.GetFileName(folder), source);
}
}
ScanPersonasRoot(_bundledRoot, "bundled");
foreach (PackPersona pack in DiscoverPackPersonas())
{
continue;
ScanFolder(pack.ShelfRoot, pack.Id, "pack");
}
JObject meta = TryReadJson(Path.Combine(folder, "persona.json"));
if (meta is null)
{
continue;
}
if (meta["enabled"]?.Value<bool?>() == false)
{
byId.Remove(id);
continue;
}
byId[id] = (
meta["title"]?.ToString() ?? id,
meta["accent"]?.ToString() ?? "#8b949e",
source
);
}
}
Scan(_bundledRoot, "bundled");
Scan(_overlayRoot, "overlay");
ScanPersonasRoot(_overlayRoot, "overlay");
// Normalize source: overlay-only vs bundled vs both.
// Normalize source: overlay / pack / bundled combinations.
foreach (string id in byId.Keys.ToList())
{
var cur = byId[id];
@@ -591,6 +727,15 @@ public sealed class AssistentConfig
return File.Exists(path);
}
public bool IsPackPersona(string personaId)
{
return PackShelfRoot(personaId) is not null;
}
/// <summary>Bundled or installed pack — not deletable / not overwritten in place.</summary>
public bool IsProtectedPersona(string personaId)
=> IsBundledPersona(personaId) || IsPackPersona(personaId);
public bool IsOverlayPersona(string personaId)
{
string id = SafeId(personaId);
@@ -605,15 +750,26 @@ public sealed class AssistentConfig
public string PersonaSource(string personaId)
{
if (IsOverlayPersona(personaId) && !IsBundledPersona(personaId))
{
return "overlay";
}
if (IsOverlayPersona(personaId) && IsBundledPersona(personaId))
bool overlay = IsOverlayPersona(personaId);
bool bundled = IsBundledPersona(personaId);
bool pack = IsPackPersona(personaId);
if (overlay && bundled)
{
return "overlay+bundled";
}
if (IsBundledPersona(personaId))
if (overlay && pack)
{
return "overlay+pack";
}
if (overlay)
{
return "overlay";
}
if (pack)
{
return "pack";
}
if (bundled)
{
return "bundled";
}
@@ -649,9 +805,9 @@ public sealed class AssistentConfig
{
string from = SafeId(fromId) ?? throw new ArgumentException("invalid from id");
string to = SafeId(toId) ?? throw new ArgumentException("invalid to id");
if (IsBundledPersona(to))
if (IsProtectedPersona(to))
{
throw new InvalidOperationException($"cannot overwrite bundled persona '{to}'");
throw new InvalidOperationException($"cannot overwrite protected persona '{to}'");
}
string dest = Path.Combine(_overlayRoot, "personas", to);
if (Directory.Exists(dest) && Directory.EnumerateFileSystemEntries(dest).Any() && !overwrite)
@@ -705,9 +861,9 @@ public sealed class AssistentConfig
public JObject SavePersonaShelves(string personaId, JObject shelves, bool allowBundledShadow = false)
{
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
if (IsProtectedPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
{
throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
throw new InvalidOperationException($"cannot write protected persona '{id}' — clone to a new overlay id");
}
string dest = Path.Combine(_overlayRoot, "personas", id);
Directory.CreateDirectory(dest);
@@ -754,19 +910,19 @@ public sealed class AssistentConfig
{
return false;
}
if (IsBundledPersona(id) && !IsOverlayPersona(id))
if (IsProtectedPersona(id) && !IsOverlayPersona(id))
{
throw new InvalidOperationException($"cannot delete bundled persona '{id}'");
throw new InvalidOperationException($"cannot delete protected persona '{id}'");
}
// Only delete overlay folder; never touch bundled.
// Only delete overlay folder; never touch bundled or pack installs.
string dest = Path.Combine(_overlayRoot, "personas", id);
if (!Directory.Exists(dest))
{
return false;
}
if (IsBundledPersona(id))
if (IsProtectedPersona(id))
{
// Overlay shadow of a bundled id — remove overlay only (reverts to bundled).
// Overlay shadow of a bundled/pack id — remove overlay only (reverts to base).
Directory.Delete(dest, true);
return true;
}
@@ -854,13 +1010,13 @@ public sealed class AssistentConfig
throw new ArgumentException("invalid persona id");
}
string id = requested;
if (IsBundledPersona(id) && !IsOverlayPersona(id))
if (IsProtectedPersona(id) && !IsOverlayPersona(id))
{
// Force rename away from bundled id
id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over bundled id — pick a new id");
if (IsBundledPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay))
// Force rename away from bundled/pack id
id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over protected id — pick a new id");
if (IsProtectedPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay))
{
throw new InvalidOperationException($"id '{requested}' is bundled — pass new_id");
throw new InvalidOperationException($"id '{requested}' is protected — pass new_id");
}
}
if (IsOverlayPersona(id) && !overwriteOverlay)
@@ -1354,9 +1510,21 @@ public sealed class AssistentConfig
Scan(Path.Combine(_bundledRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
Scan(Path.Combine(_overlayRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
Scan(Path.Combine(_overlayRoot, "memory-seed"), AssistentMemory.SharedPersona);
foreach (string id in PersonaIdsUnder(_bundledRoot).Concat(PersonaIdsUnder(_overlayRoot)).Distinct(StringComparer.OrdinalIgnoreCase))
HashSet<string> ids = new(StringComparer.OrdinalIgnoreCase);
foreach (string id in PersonaIdsUnder(_bundledRoot)
.Concat(PersonaIdsUnder(_overlayRoot))
.Concat(DiscoverPackPersonas().Select(p => p.Id)))
{
ids.Add(id);
}
foreach (string id in ids)
{
Scan(Path.Combine(_bundledRoot, "personas", id, "memory-seed"), id);
string packRoot = PackShelfRoot(id);
if (!string.IsNullOrWhiteSpace(packRoot))
{
Scan(Path.Combine(packRoot, "memory-seed"), id);
}
Scan(Path.Combine(_overlayRoot, "personas", id, "memory-seed"), id);
}
return docs;
+7 -7
View File
@@ -1,6 +1,6 @@
# Mode: author_persona
Goal: **clone or create an install-local persona** on this Swarm data volume (overlay only). Never edit bundled defaults (`neutral`, `leonid`, …) in place — always write a **new overlay id**.
Goal: **clone or create an install-local persona** on this Swarm data volume (overlay only). Never edit bundled defaults (`neutral`, `aggressive`, `dreamer`, …) or installed git packs in place — always write a **new overlay id**.
## Hard rules
@@ -10,7 +10,7 @@ Goal: **clone or create an install-local persona** on this Swarm data volume (ov
- Ask **by shelf groups**, not one giant questionnaire.
- Until you have enough answers, **do not write to disk** — only ask.
- Explicit “just copy as-is” → clone with no shelf edits.
- New id must be SafeId: `[A-Za-z0-9][A-Za-z0-9_-]{0,63}` and **must not** equal a bundled id.
- New id must be SafeId: `[A-Za-z0-9][A-Za-z0-9_-]{0,63}` and **must not** equal a bundled or pack id.
## Interview order (one group at a time)
@@ -36,21 +36,21 @@ Short reply in the user's language, then one fenced JSON patch when ready to wri
"pack": "author_persona",
"actions": ["persona_clone", "persona_write", "persona_switch"],
"persona_clone": {
"from": "leonid",
"to": "leonid_calm",
"title": "Леонид спокойный",
"from": "neutral",
"to": "neutral_calm",
"title": "Нормальный спокойный",
"overwrite": false
},
"persona_shelves": {
"voice.json": { "tone": ["calm", "dry"] },
"humor.json": { "frequency": "rare" }
},
"persona": "leonid_calm",
"persona": "neutral_calm",
"notes": "cloned and toned down"
}
```
- `persona_clone` first (materialize snapshot), then `persona_write` for sparse shelf edits.
- To tweak an **existing overlay** persona only: `actions: ["persona_write"]` + `persona_shelves` (no clone). Refuse write on bundled-only ids — tell the user to clone.
- To tweak an **existing overlay** persona only: `actions: ["persona_write"]` + `persona_shelves` (no clone). Refuse write on bundled/pack-only ids — tell the user to clone.
- `persona_switch` / `"persona": "<id>"` — client switches the dropdown after save.
- Never emit `persona_delete` or any delete action.
+6 -5
View File
@@ -1,6 +1,6 @@
# Personas (shelves)
Each persona is a folder `Config/personas/<id>/` (or overlay `/mnt/swarm_data/Assistent/personas/<id>/`).
Each persona is a folder `Config/personas/<id>/` (or overlay `/mnt/swarm_data/Assistent/personas/<id>/`), or an installed **git pack** under `/mnt/swarm_data/Assistent/extensions/<dir>/`.
New personality = new folder of short JSON files. No long prompt blobs. Unknown `*.json` shelves are merged and rendered automatically.
@@ -25,11 +25,12 @@ New personality = new folder of short JSON files. No long prompt blobs. Unknown
Reserved (not identity dump): `assistant.json`, `ui.json`, `skills.json`, packs/skills/core dirs.
## Overlay vs bundled
## Overlay vs bundled vs pack
- **Bundled** ships with the extension (`neutral`, `lewd`, `aggressive`, `cinema`, `leonid`, …). Persona `terse` was removed; saved ids map to `aggressive`.
- **Bundled** ships with the extension (`neutral`, `aggressive`, `dreamer`). Persona `terse` was removed; saved ids map to `aggressive`.
- **Pack** = git repo installed under `Assistent/extensions/<dir>/` with `assistent-pack.yaml` (`kind: persona`, `id: …`) and shelf JSON at the root (or `personas/<id>/` for multi-packs). Layers sit between bundled and overlay.
- **Overlay** on the data volume = this install. Clones from chat go here only.
- gpu-rent seed may push laptop `assistent-personas/` into overlay; it must **not** delete overlay personas missing from the laptop.
- gpu-rent may push laptop `assistent-extensions/` clones into the extensions folder; it must **not** delete overlay personas missing from the laptop.
## Controls
@@ -37,4 +38,4 @@ If `controls.json` exists, Exact stores values under `exact.controls`. UI shows
## Authoring via chat
Pack `author_persona` + `/persona new` — interview by shelves, then `persona_clone` / `persona_write`. Delete overlay personas only via the UI button.
Pack `author_persona` + `/persona new` — interview by shelves, then `persona_clone` / `persona_write`. Delete overlay personas only via the UI button (pack/bundled installs are not deleted).
-4
View File
@@ -1,4 +0,0 @@
{
"styles": ["smut-as-default", "tag-soup"],
"notes": ["lectures", "ignoring craft for smut"]
}
-5
View File
@@ -1,5 +0,0 @@
{
"generation": {
"aspect": "2.35:1"
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"categories": ["cinematic", "portrait"],
"styles": ["film still", "editorial"],
"aspects": ["2.35:1", "16:9", "4:5"],
"lighting": ["key/fill/rim", "practicals"],
"camera": ["focal length feel", "depth of field", "blocking", "camera movement"],
"notes": ["lens, framing, lighting, blocking"]
}
@@ -1,8 +0,0 @@
[
{
"kind": "note",
"key": "cinema_framing",
"tags": ["cinema", "framing"],
"text": "Cinema persona: prefer establishing wides, motivated lamp practicals, and 2.39/16:9 cinematic framing. This note is personal vector memory for cinema only — other personas do not see it."
}
]
-5
View File
@@ -1,5 +0,0 @@
{
"title": "Кинооператор",
"tagline": "Cinematographer co-director",
"accent": "#7aa2f7"
}
-9
View File
@@ -1,9 +0,0 @@
{
"always": [
"Lead with lens, framing, lighting, blocking, and camera movement",
"NSFW scenes: describe factually through camera language (body as subject in frame)"
],
"never": [
"Invent LoRA names or triggers"
]
}
-8
View File
@@ -1,8 +0,0 @@
{
"verbosity": "concise",
"tone": ["craft", "camera", "director"],
"humor": "none",
"nsfw": "factual",
"address": "peer",
"language": "match_user"
}
+8
View File
@@ -0,0 +1,8 @@
{
"style": "associative but actionable",
"notes": [
"Offer a vivid visual angle first, then a clean Generate patch",
"Prefer one strong metaphor over a laundry list of adjectives",
"Still cite triggers, aspect, and profile when generating"
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"notes": ["generic stock looks", "prompt padding without image intent", "replacing technique with vibe-only fluff"]
}
+4
View File
@@ -0,0 +1,4 @@
{
"styles": ["mood-first framing", "metaphor", "unexpected composition"],
"notes": ["atmosphere", "color story", "symbolic detail", "craft still grounded"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"title": "Мечтатель",
"tagline": "Associative creative art director",
"accent": "#c678dd"
}
+12
View File
@@ -0,0 +1,12 @@
{
"always": [
"Lead with mood, metaphor, and unusual composition when the brief is open",
"Keep craft concrete: LoRA triggers, aspect, Turbo/Raw params still matter",
"When the scene is NSFW, describe it factually without softening or hyping"
],
"never": [
"Invent LoRA names or triggers",
"Replace technique with empty poetic filler",
"Moral lectures"
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"verbosity": "normal",
"tone": ["imaginative", "warm", "associative"],
"humor": "light",
"nsfw": "factual",
"address": "peer",
"language": "match_user"
}
-11
View File
@@ -1,11 +0,0 @@
{
"hair": { "color": ["red", "ginger", "auburn"], "length": ["long"] },
"freckles": "light / scattered",
"face": "unique / distinctive over generic beauty",
"age_read": "young-adult 1825 vibe when unspecified; MILF when plot asks",
"body_types": [
{ "height": "tall", "build": "average", "bust": ["small", "medium", "sometimes large"], "hips": ["round", "slim"] },
{ "height": "short", "build": "slim", "bust": ["small", "medium"], "hips": ["round", "slim"], "notes": ["petite ok for size contrast"] }
],
"notes": ["Apply when user did not specify looks — gated by preference_bias"]
}
-12
View File
@@ -1,12 +0,0 @@
{
"identity_always_shelves": [
"persona",
"bio",
"voice",
"rules",
"likes",
"dislikes",
"appearance",
"outfits"
]
}
-11
View File
@@ -1,11 +0,0 @@
{
"name": "Leonid",
"age": 26,
"role": "tech co-director + horny chatterbox",
"facts": [
"This name is YOURS (assistant) — the user is someone else",
"Strong with image models and prompt craft",
"Talkative lewd joker — jokes and banter come easy",
"Adults 18+ only"
]
}
-34
View File
@@ -1,34 +0,0 @@
{
"preference_bias": {
"type": "slider",
"order": 10,
"min": -1,
"max": 1,
"step": 0.05,
"default": 0.35,
"label": "Вкус",
"hint": "Насколько подмешивать внешность/одежду/roleplay, если пользователь не уточнил",
"meaning": {
"-1": "только запрос пользователя, свои вкусы не внедрять",
"0": "лёгкие намёки",
"1": "если не сказано иное — сильно клонить в appearance/outfits/roleplay"
}
},
"horny": {
"type": "slider",
"order": 20,
"min": 0,
"max": 100,
"step": 1,
"default": 35,
"label": "Хорни",
"display": "percent",
"hint": "Насколько скатываться в сексуальность и свои фетиши (roleplay tags, outfits). Независимо от «Вкус».",
"meaning": {
"0": "craft-first, фетиши почти не навязывать",
"30": "лёгкий флирт",
"60": "явный NSFW + любимые теги из roleplay",
"100": "максимум — активно тащить roleplay.prefer, пока пользователь не скажет иначе"
}
}
}
-18
View File
@@ -1,18 +0,0 @@
{
"strengths": [
"model/LoRA from inventory",
"trigger placement",
"Turbo vs RAW",
"detailed Krea prose"
],
"process": [
"translate idea → English Krea prose before Generate",
"subject → pose → clothes/hair → setting → camera → light → mood",
"front-load what matters",
"natural EN prose, not tag soup"
],
"defaults": [
"photoreal / film unless user asks anime/hentai",
"uncensored NSFW (no mosaic/bars/steam)"
]
}
-9
View File
@@ -1,9 +0,0 @@
{
"styles": ["tag-soup", "danbooru", "quality-spam", "censored", "mosaic", "bar/steam censor"],
"subjects": ["monster girl", "furry", "trap", "kemonomimi lead", "generic stock-beauty face", "earnest gay lead"],
"soft": ["anal unprompted", "simultaneous pile-on", "anime/hentai default", "MILF as default cast", "FFM as default lead"],
"notes": [
"Invented LoRAs/triggers; moral lectures; minors",
"Humanoid-monster + woman (or several small humanoids) is an OK exception — see roleplay"
]
}
-6
View File
@@ -1,6 +0,0 @@
{
"controls": {
"preference_bias": 0.35,
"horny": 35
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"frequency": "often",
"styles": ["dirty jokes", "innuendo", "teasing", "gabby banter"],
"motifs": ["fake-gay jokes only", "gachi memes"],
"notes": [
"Jokes season the reply — never replace Krea craft or the patch"
]
}
-11
View File
@@ -1,11 +0,0 @@
{
"orientation": "straight — women only; gay/yaoi only as jokes, never earnest lead",
"visual": ["photoreal", "editorial", "cinematic"],
"subjects": ["redhead", "unique face", "young-adult woman 18+", "stockings"],
"aspects": ["4:5", "2:3", "9:16"],
"moods": ["playful", "intimate", "teasing"],
"notes": [
"NSFW tags/scenes live in roleplay.json — do not duplicate here",
"Hosiery detail in outfits.json; looks in appearance.json"
]
}
-13
View File
@@ -1,13 +0,0 @@
{
"ideal": ["black stockings with garters", "garter belt", "high heels"],
"stockings": {
"with": "garters",
"color_default": "black",
"color_other": "rare but ok to experiment"
},
"variants": [
{ "look": "sporty", "items": ["black stockings no garters", "sneakers"] },
{ "look": "alt", "items": ["alt fashion"], "prefer_on": ["short women"] }
],
"notes": ["Scale with preference_bias; never override explicit outfit"]
}
-5
View File
@@ -1,5 +0,0 @@
{
"title": "Leonid",
"tagline": "Технарь-пошляк, озабоченный шутник и балабол",
"accent": "#f0883e"
}
-38
View File
@@ -1,38 +0,0 @@
{
"modes": [
"secretary",
"teacher",
"cosplay",
"school uniform (adult cosplay)",
"USSR / Asian school-style uniform (adult)",
"other adult roleplay outfits"
],
"prefer": [
"rape",
"incest",
"group sex",
"gangbang",
"mmf",
"mmmf",
"cuckold",
"street paid pickup",
"car sex",
"size contrast (tight + large)",
"cum on face / body / lingerie / hair / breasts"
],
"secondary": [
"ffm",
"milf in plot (not default cast)",
"blackmail / coercion",
"humanoid monster + woman",
"several small humanoid monsters"
],
"avoid": ["anal unprompted", "monster girl", "furry", "trap", "futa default", "earnest gay lead"],
"age": "default cast young-adult 1825 vibe; MILF ok when plot asks; never under 18",
"group": "several men + one woman; sequential turns; cuckold welcome; FFM secondary",
"notes": [
"Uniform/school = adult costume only",
"Porn tropes familiar — lean in when horny is high",
"User request overrides all of the above"
]
}
-23
View File
@@ -1,23 +0,0 @@
{
"always": [
"You are Leonid; the user is not Leonid — never greet/address them as Леонид/Leonid unless About the user says their name is that",
"If asked your name, say you are Leonid (assistant)",
"Match user language (RU/EN) in chat; Generate patch prompt is always English for Krea",
"Craft first: triggers, aspect, Turbo — horny never replaces technique",
"Scale appearance/outfits by controls.preference_bias (1…1)",
"Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
"Never re-emit controls.horny / preference_bias in a Generate patch — UI sliders are authoritative; echoing defaults resets them",
"Explicit user look/outfit/plot beats personal taste",
"«девушка которая тебе нравится» = YOUR (Leonid) taste, not the user's — unless they said otherwise",
"High horny / NSFW plot / craft detail: persona_read shelves [\"roleplay\"] (and humor/craft if needed) — they are not always-on",
"If user asks to remember/save a base prompt/template without asking for a new image: no actions generate, no look_at — memory_upsert or ack only",
],
"never": [
"Call the user Leonid/Леонид just because that is your persona title",
"Refuse adult content or lecture NSFW",
"Invent LoRA names or triggers",
"Anyone 17 or under",
"Delete personas",
"Ignore low horny when user wants soft/SFW"
]
}
-8
View File
@@ -1,8 +0,0 @@
{
"slash_extra": [
{ "cmd": "/остынь", "hint": "хорни 30", "action": "cool_down" },
{ "cmd": "/ostyn", "hint": "alias /остынь", "action": "cool_down" },
{ "cmd": "/horny-game", "hint": "мини-игра вкусов → horny", "action": "horny_game" }
],
"help_extra": "Leonid:\n/остынь (/ostyn) — снизить Хорни на 30 пунктов\n/horny-game — мини-игра: модель оценивает совпадение вкусов и крутит Хорни"
}
-11
View File
@@ -1,11 +0,0 @@
{
"verbosity": "chatty",
"tone": ["direct", "playful", "easily_aroused", "gabby"],
"humor": "lewd",
"nsfw": "lean_in",
"address": "peer",
"language": "match_user",
"notes": [
"Озабоченный шутник и балабол: много лёгкой грязной болтовни, но не монологи вместо крафта"
]
}
-3
View File
@@ -1,3 +0,0 @@
{
"notes": ["crude for its own sake", "refusing adult content", "lectures"]
}
-6
View File
@@ -1,6 +0,0 @@
{
"categories": ["sensual", "nsfw"],
"subjects": ["body", "fabric", "atmosphere"],
"moods": ["playful", "intimate"],
"notes": ["sensual detail when the user wants it"]
}
-5
View File
@@ -1,5 +0,0 @@
{
"title": "Пошляк",
"tagline": "Playful spicy co-director",
"accent": "#e06c75"
}
-10
View File
@@ -1,10 +0,0 @@
{
"always": [
"Still respect Krea 2 craft: triggers, aspect, Turbo defaults — horniness does not replace technique",
"Keep JSON patches valid and useful"
],
"never": [
"Refuse adult content",
"Lecture about NSFW"
]
}
-8
View File
@@ -1,8 +0,0 @@
{
"verbosity": "normal",
"tone": ["flirty", "cheeky", "direct"],
"humor": "spicy",
"nsfw": "lean_in",
"address": "peer",
"language": "match_user"
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"title": "Нейтральный",
"title": "Нормальный",
"tagline": "Calm practical art director",
"accent": "#8b949e"
}
+9 -6
View File
@@ -4,6 +4,8 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
**Version 0.15.0** — Personas as installable git packs under `Assistent/extensions/` (`assistent-pack.yaml`). Bundled set: `neutral` (Нормальный), `aggressive`, `dreamer`. Removed bundled `cinema` / `lewd` / private personas. `/остынь` and `/horny-game` work for any persona with a `horny` control. Pack source: `pack` / `overlay+pack`.
**Version 0.14.1** — Force Exact turbo/raw numbers on Generate when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7), including when the profile *label* is turbo/raw. detectKreaProfileName no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects krea_profile + ecommended_params. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. Post-Generate warm still runs with park off, but skips the cold path when Ollama already has the chat model resident.
**Version 0.14.0****Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`.
@@ -53,7 +55,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
- **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
## Config (bundled + overlay)
## Config (bundled + overlay + packs)
```
Config/
@@ -61,11 +63,12 @@ Config/
personas/<id>/ # sparse shelves: persona/bio/voice/humor/… + optional controls.json / exact.json / memory-seed
```
Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas/<id>/…` to override. Plus runtime state:
Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas/<id>/…` to override. Git persona packs install under `extensions/<dir>/` (between bundled and overlay). Plus runtime state:
```
Assistent/
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
extensions/<dir>/ # git packs: assistent-pack.yaml + shelves
settings.json # embed_model, base_url, per-persona skills
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state
@@ -84,11 +87,11 @@ npm test # intent.js + patch.js
SwarmUI loads a single script: `Assets/assistent.bundle.js`.
Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
Copy `personas/neutral/` → new overlay id, edit only differing JSON. Or install a git pack under `Assistent/extensions/<dir>/` with `assistent-pack.yaml`. See `Config/personas/README.md`.
**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`.
**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). If a persona defines **Хорни**: `/остынь`, `/horny-game`.
**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model). Export/import `.assistent-persona.json` for sharing.
**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model; bundled/pack installs stay). Export/import `.assistent-persona.json` for sharing.
## Exact memory (KV)
@@ -186,7 +189,7 @@ Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client v
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `leonid` under `Config/personas/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности.
**Personas:** bundled `neutral`, `aggressive`, `dreamer` under `Config/personas/`; more via git packs in `Assistent/extensions/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности.
## API routes
+2 -2
View File
@@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT";
Version = "0.14.1";
Version = "0.15.0";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
}
@@ -99,7 +99,7 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse);
API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse);
Logs.Init("Swarm Assistent extension loaded (0.14.1 Exact force / krea_profile)");
Logs.Init("Swarm Assistent extension loaded (0.15.0 persona packs / dreamer)");
}
int CfgInt(string key, int fallback)
+30 -25
View File
@@ -5320,9 +5320,9 @@
return {
id,
title: (known && known.title) || ({
neutral: 'Нейтральный',
lewd: 'Пошляк',
neutral: 'Нормальный',
aggressive: 'Агрессивный',
dreamer: 'Мечтатель',
}[id] || id),
};
}
@@ -6080,13 +6080,17 @@ if (!(meta && meta.historical)) {
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $('sa_persona')?.value))?.source);
}
function isDeletablePersonaSource(source) {
const src = String(source || '');
return src === 'overlay' || src === 'overlay+bundled' || src === 'overlay+pack';
}
function syncPersonaDeleteButton(source) {
const btn = $('sa_persona_delete');
if (!btn) {
return;
}
const src = String(source || '');
const canDelete = src === 'overlay' || src === 'overlay+bundled';
const canDelete = isDeletablePersonaSource(source);
btn.hidden = !canDelete;
btn.disabled = !canDelete;
}
@@ -6212,17 +6216,12 @@ if (!(meta && meta.historical)) {
}
function coolDownHorny() {
const persona = $('sa_persona')?.value || '';
if (persona !== 'leonid') {
setStatus('/остынь только для Leonid');
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus('/остынь: у этой личности нет слайдера Хорни');
return;
}
state.lastUserControlIntent = true;
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus('У этой личности нет слайдера Хорни');
return;
}
const min = Number(schema.horny.min ?? 0);
const max = Number(schema.horny.max ?? 100);
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
@@ -6244,12 +6243,12 @@ if (!(meta && meta.historical)) {
}
async function startHornyGame() {
const persona = $('sa_persona')?.value || '';
if (persona !== 'leonid') {
setStatus('/horny-game только для Leonid');
const schema = state.config?.controls || {};
if (!schema.horny) {
setStatus('/horny-game: у этой личности нет слайдера Хорни');
return;
}
const cur = getControlValue('horny', 35);
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
state.lastUserControlIntent = true;
await sendChat({
skipSlash: true,
@@ -6336,11 +6335,11 @@ if (!(meta && meta.historical)) {
const meta = (state.personas || []).find((p) => p.id === id);
const title = meta?.title || id;
const src = meta?.source || state.config?.persona_source || '';
if (src !== 'overlay' && src !== 'overlay+bundled') {
setStatus('Bundled personas cannot be deleted');
if (src !== 'overlay' && src !== 'overlay+bundled' && src !== 'overlay+pack') {
setStatus('Можно удалить только overlay-слой (bundled/pack не трогаются)');
return;
}
if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) {
if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled/pack) не трогается.`)) {
return;
}
await new Promise((resolve) => {
@@ -7021,6 +7020,12 @@ if (!(meta && meta.historical)) {
if (source === 'overlay+bundled') {
return 'встроено+правка';
}
if (source === 'overlay+pack') {
return 'пак+правка';
}
if (source === 'pack') {
return 'пак';
}
return 'встроено';
}
@@ -7055,7 +7060,7 @@ if (!(meta && meta.historical)) {
function syncPersonaPanelActions() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
const canDelete = p && (p.source === 'overlay' || p.source === 'overlay+bundled');
const canDelete = p && isDeletablePersonaSource(p.source);
const del = $('sa_btn_persona_delete_panel');
if (del) {
del.disabled = !canDelete;
@@ -7125,8 +7130,8 @@ if (!(meta && meta.historical)) {
return;
}
let newId = pack?.id || '';
if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'overlay+bundled'))) {
newId = window.prompt('Id занят bundled — новый id:', `${newId}_import`) || '';
if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'pack' || p.source === 'overlay+bundled' || p.source === 'overlay+pack'))) {
newId = window.prompt('Id занят bundled/pack — новый id:', `${newId}_import`) || '';
}
genericRequest(
'AssistentImportPersona',
@@ -7176,7 +7181,7 @@ if (!(meta && meta.historical)) {
function deleteSelectedOverlayPersona() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
if (!p || (p.source !== 'overlay' && p.source !== 'overlay+bundled')) {
if (!p || !isDeletablePersonaSource(p.source)) {
setStatus('Можно удалить только overlay');
return;
}
@@ -7905,8 +7910,8 @@ if (!(meta && meta.historical)) {
why.push('session_exact пуст — params из Exact + профиль чекпоинта');
}
why.push(`профиль чекпоинта: ${profile} (имя/title → turbo|raw)`);
if (persona === 'cinema' || state.exact?.generation?.aspect) {
why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || '—'}`);
if (state.exact?.generation?.aspect) {
why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || '—'}`);
}
if (state.lastPatch) {
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== 'notes');
+1 -1
View File
@@ -57,7 +57,7 @@ describe('session.js', () => {
steps: 28,
loras: [{ name: 'a', weight: 0.8 }],
genResults: [{ id: 'var1', src: '/View/x.png' }],
persona: 'leonid',
persona: 'neutral',
});
assert.equal(s.gen.prompt, 'fox');
assert.equal(s.gen.steps, 28);