Ship persona shelves, Exact controls, and overlay author pipeline.

Add Leonid as a shelf-based example with preference_bias slider; support /persona new clone-to-overlay and UI-only delete.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 01:48:02 +03:00
co-authored by Cursor
parent ac8e30c7c8
commit cf89348f85
26 changed files with 1311 additions and 102 deletions
+238 -1
View File
@@ -2150,6 +2150,8 @@
}
}
fillEmptyParamsFromExact();
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
});
}
@@ -3212,6 +3214,34 @@
setStatus('Prompt Images: drop the ref into the Swarm prompt box (no auto helper yet)');
}
}
// Persona Exact controls (model or user patch). Ignore persona_delete.
if (patch.controls && typeof patch.controls === 'object' && !Array.isArray(patch.controls)) {
const schema = state.config?.controls || {};
const next = { ...(state.config?.control_values || state.exact?.controls || {}) };
for (const [k, v] of Object.entries(patch.controls)) {
if (schema[k]) {
next[k] = v;
}
}
savePersonaControls(next);
renderPersonaControls(schema, next);
}
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
const wantSwitch = acts.includes('persona_switch')
|| (patch.persona && typeof patch.persona === 'string')
|| patch._persona_cloned
|| patch._persona_written;
if (wantSwitch) {
const newId = String(patch.persona || patch._persona_cloned || patch._persona_written || '').trim();
if (newId && AssistentConfigSafeIdClient(newId)) {
await refreshPersonasAndSwitch(newId);
} else if (acts.includes('persona_clone') || acts.includes('persona_write') || patch.persona_clone) {
await refreshPersonasAndSwitch(null);
}
}
syncChipHighlight();
syncLiveParamsBar();
syncBuildGenButton();
@@ -3224,10 +3254,46 @@
}
}
if (!state.restoringChat) {
setStatus('Applied patch');
setStatus(patch._persona_error ? `Persona: ${patch._persona_error}` : 'Applied patch');
}
}
function AssistentConfigSafeIdClient(id) {
return /^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$/.test(String(id || ''));
}
async function refreshPersonasAndSwitch(preferId) {
await new Promise((resolve) => {
genericRequest(
'AssistentListPersonas',
{},
async (data) => {
if (Array.isArray(data?.personas)) {
state.personas = data.personas.map((p) => ({
id: p.id,
title: p.title,
accent: p.accent,
source: p.source,
}));
renderPersonaOptions(state.personas, preferId || $('sa_persona')?.value);
}
if (preferId && $('sa_persona')) {
if ([...$('sa_persona').options].some((o) => o.value === preferId)) {
$('sa_persona').value = preferId;
await applyPersonaForChat(preferId, { quiet: true });
}
} else {
loadConfig($('sa_persona')?.value, () => resolve());
return;
}
resolve();
},
0,
() => resolve(),
);
});
}
function triggerGenerate() {
try {
if (typeof mainGenHandler !== 'undefined' && mainGenHandler && typeof mainGenHandler.doGenerate === 'function') {
@@ -4202,6 +4268,151 @@
if (applyDefaults || data.exact) {
fillEmptyParamsFromExact();
}
renderPersonaControls(data.controls || {}, data.control_values || data.exact?.controls || {});
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $('sa_persona')?.value))?.source);
}
function syncPersonaDeleteButton(source) {
const btn = $('sa_persona_delete');
if (!btn) {
return;
}
const src = String(source || '');
const canDelete = src === 'overlay' || src === 'overlay+bundled';
btn.hidden = !canDelete;
btn.disabled = !canDelete;
}
let controlSaveTimer = null;
function renderPersonaControls(schema, values) {
const box = $('sa_persona_controls');
if (!box) {
return;
}
box.innerHTML = '';
const keys = schema && typeof schema === 'object' ? Object.keys(schema) : [];
if (!keys.length) {
box.hidden = true;
return;
}
box.hidden = false;
for (const id of keys) {
const def = schema[id];
if (!def || typeof def !== 'object') {
continue;
}
if (String(def.type || 'slider').toLowerCase() !== 'slider') {
continue;
}
const min = Number(def.min ?? -1);
const max = Number(def.max ?? 1);
const step = Number(def.step ?? 0.05);
const defVal = Number(def.default ?? 0);
let cur = values && values[id] != null ? Number(values[id]) : defVal;
if (Number.isNaN(cur)) {
cur = defVal;
}
const row = document.createElement('div');
row.className = 'sa-control-row';
row.title = def.hint || id;
const lab = document.createElement('label');
lab.textContent = def.label || id;
const input = document.createElement('input');
input.type = 'range';
input.min = String(min);
input.max = String(max);
input.step = String(step);
input.value = String(cur);
input.dataset.controlId = id;
const valEl = document.createElement('span');
valEl.className = 'sa-control-val';
valEl.textContent = cur.toFixed(2);
const onInput = () => {
const v = Number(input.value);
valEl.textContent = v.toFixed(2);
if (controlSaveTimer) {
clearTimeout(controlSaveTimer);
}
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 350);
};
input.addEventListener('input', onInput);
input.addEventListener('change', onInput);
row.appendChild(lab);
row.appendChild(input);
row.appendChild(valEl);
box.appendChild(row);
}
}
function savePersonaControls(partial) {
const persona = $('sa_persona')?.value || 'neutral';
if (typeof genericRequest !== 'function') {
return;
}
genericRequest(
'AssistentSaveControls',
{ persona, controls: partial || {} },
(data) => {
if (data?.error) {
setStatus(data.error);
return;
}
if (data?.control_values && state.config) {
state.config.control_values = data.control_values;
if (state.exact) {
state.exact.controls = data.control_values;
}
}
if (data?.controls) {
renderPersonaControls(data.controls, data.control_values || {});
}
},
0,
() => setStatus('controls save failed'),
);
}
async function deleteCurrentOverlayPersona() {
const id = $('sa_persona')?.value;
if (!id) {
return;
}
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');
return;
}
if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) {
return;
}
await new Promise((resolve) => {
genericRequest(
'AssistentDeletePersona',
{ persona: id },
async (data) => {
if (data?.error) {
setStatus(data.error);
resolve();
return;
}
const next = data?.default_persona || 'neutral';
if (Array.isArray(data?.personas)) {
state.personas = data.personas;
}
renderPersonaOptions(state.personas || [], next);
if ($('sa_persona')) {
$('sa_persona').value = next;
}
await applyPersonaForChat(next, { quiet: false });
setStatus(`Удалено: ${id}`);
resolve();
},
0,
() => { setStatus('delete failed'); resolve(); },
);
});
}
function renderPersonaOptions(personas, selected) {
@@ -4223,6 +4434,8 @@
if ([...sel.options].some((o) => o.value === cur)) {
sel.value = cur;
}
const meta = (personas || []).find((p) => p.id === sel.value);
syncPersonaDeleteButton(meta?.source || state.config?.persona_source);
}
function renderPackOptions(packs, preferred) {
@@ -5883,6 +6096,29 @@
});
return true;
}
if (cmd === 'persona') {
const sub = (parts[1] || 'new').toLowerCase();
const rest = parts.slice(2).join(' ').trim();
setPackValue('author_persona', { flash: true, user: true });
if (sub === 'save') {
await sendChat({
skipAutoPack: true,
forcedUserText:
'Сохрани согласованный черновик личности сейчас (persona_clone / persona_write). Не удаляй личности.',
});
return true;
}
const fromId = sub === 'clone' && rest
? rest.split(/\s+/)[0]
: ($('sa_persona')?.value || 'neutral');
await sendChat({
skipAutoPack: true,
forcedUserText:
`Начни интервью author_persona: клон с источника «${fromId}». ` +
'Спрашивай по полкам группами. Не пиши на диск, пока мало ответов. Не удаляй личности.',
});
return true;
}
appendSystemNote(`Unknown command /${cmd}.\n\n${HELP_TEXT}`);
setStatus(`Unknown /${cmd}`);
@@ -6478,6 +6714,7 @@
$('sa_board_tab_gen')?.addEventListener('click', () => setBoardTab('generate'));
$('sa_board_tab_refs')?.addEventListener('click', () => setBoardTab('refs'));
$('sa_persona')?.addEventListener('change', onPersonaChanged);
$('sa_persona_delete')?.addEventListener('click', () => deleteCurrentOverlayPersona());
$('sa_cards_kind')?.addEventListener('change', renderCardsList);
$('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
$('sa_btn_card_meta')?.addEventListener('click', () => fetchCardMetaLive());