Ship Assistent 0.11.6: opt-in vision, negative pass-through, generate from context.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+236
-18
@@ -16,6 +16,8 @@
|
||||
const LS_AUTO_APPLY = 'swarm_assistent_auto_apply';
|
||||
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
||||
const LS_AUTO_CRITIQUE = 'swarm_assistent_auto_critique';
|
||||
/** One-shot: turn off auto look_at / auto-critique (vision is button / /look / model look_at). */
|
||||
const LS_VISION_OPTIN_MIG = 'swarm_assistent_vision_optin_v1';
|
||||
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
|
||||
/** When '1', unload chat LLM before Generate (frees VRAM; VL reload can take 1–2 min). Default off. */
|
||||
const LS_PARK_LLM = 'swarm_assistent_park_llm';
|
||||
@@ -89,7 +91,7 @@
|
||||
/debug ask — то же + короткий ответ модели
|
||||
/why — сразу /debug ask
|
||||
/gen — Generate сейчас
|
||||
/look generate|refN — прикрепить окно к vision
|
||||
/look generate|refN — показать кадр модели (vision)
|
||||
/init /mask /clear — Init / Mask / Clear Init
|
||||
/interrupt — остановить генерацию
|
||||
/aspect 16:9 — размер из таблицы 1K
|
||||
@@ -656,6 +658,43 @@
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
function liveNegativePrompt() {
|
||||
return String(val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '').trim();
|
||||
}
|
||||
|
||||
function exactDefaultNegative() {
|
||||
const { exact } = resolveExactBundle();
|
||||
const n = exact?.generation?.negative ?? exact?.negative;
|
||||
return n != null ? String(n).trim() : '';
|
||||
}
|
||||
|
||||
function setNegativePrompt(text) {
|
||||
const s = text != null ? String(text) : '';
|
||||
if (document.getElementById('input_negativeprompt')) {
|
||||
setVal('input_negativeprompt', s);
|
||||
}
|
||||
if (document.getElementById('alt_negativeprompt_textbox')) {
|
||||
setVal('alt_negativeprompt_textbox', s);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create / keep / pass-through negative so Generate never drops the Swarm box. */
|
||||
function ensureNegativeForGenerate(patch) {
|
||||
let neg = '';
|
||||
if (patch && patch.negative != null && String(patch.negative).trim() !== '') {
|
||||
neg = String(patch.negative).trim();
|
||||
} else {
|
||||
neg = liveNegativePrompt() || exactDefaultNegative();
|
||||
}
|
||||
if (neg) {
|
||||
setNegativePrompt(neg);
|
||||
if (patch && (patch.negative == null || String(patch.negative).trim() === '')) {
|
||||
patch.negative = neg;
|
||||
}
|
||||
}
|
||||
return neg;
|
||||
}
|
||||
|
||||
function isEmptyParamField(raw, { treatZeroEmpty = false } = {}) {
|
||||
if (raw == null) {
|
||||
return true;
|
||||
@@ -875,7 +914,7 @@
|
||||
/** When the model wrote ### JSON Patch with no fence — build Apply+Generate from prose / last patch. */
|
||||
function synthesizePatchAfterEmptyFence(reply, userText, opts = {}) {
|
||||
const wants = !!(opts.userWantsGenerate || state.pendingSilentGen
|
||||
|| userAsksGenerate(userText) || userAsksContinue(userText));
|
||||
|| userAsksGenerate(userText) || userAsksContinue(userText) || userImpliesGenerate(userText));
|
||||
const missing = replyMissingJsonPatch(reply);
|
||||
if (!wants && !missing) {
|
||||
return null;
|
||||
@@ -930,6 +969,14 @@
|
||||
if (patch.aspect) {
|
||||
keep.aspect = patch.aspect;
|
||||
}
|
||||
if (patch.negative != null && String(patch.negative).trim() !== '') {
|
||||
keep.negative = patch.negative;
|
||||
} else {
|
||||
const liveNeg = liveNegativePrompt() || exactDefaultNegative();
|
||||
if (liveNeg) {
|
||||
keep.negative = liveNeg;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(patch.loras)) {
|
||||
keep.loras = patch.loras;
|
||||
}
|
||||
@@ -947,9 +994,10 @@
|
||||
+ '- Structure & front-load: subject → pose/action → body/wardrobe → setting → materials/textures → camera/framing → lighting → medium/mood.\n'
|
||||
+ '- Expand thin ideas; fix anti-patterns; one coherent scene.\n'
|
||||
+ '- Keep LoRA trigger phrases in English near the subject they affect.\n'
|
||||
+ '- Prefer positives over negatives; preserve meaning and NSFW level from SOURCE.\n'
|
||||
+ '- Always include JSON "negative": keep/supplement SOURCE+live negative, or use Exact default if empty. Never drop it.\n'
|
||||
+ '- Put “no blur / empty street” ideas as positives in prompt, not as a huge negative dump.\n'
|
||||
+ '- One short ack in the user language max, then ONE fenced JSON merging these keys: '
|
||||
+ `${JSON.stringify(keep)} plus the new English "prompt".\n`
|
||||
+ `${JSON.stringify(keep)} plus the new English "prompt" and "negative".\n`
|
||||
+ '- Include actions:["generate"] when an image was requested.\n\n'
|
||||
+ `SOURCE:\n${patch.prompt}`
|
||||
);
|
||||
@@ -965,6 +1013,9 @@
|
||||
...base,
|
||||
...effective,
|
||||
prompt: effective.prompt || base.prompt,
|
||||
negative: (effective.negative != null && String(effective.negative).trim() !== '')
|
||||
? effective.negative
|
||||
: (base.negative || liveNegativePrompt() || exactDefaultNegative() || undefined),
|
||||
actions: (Array.isArray(effective.actions) && effective.actions.length)
|
||||
? effective.actions
|
||||
: (base.actions || ['generate']),
|
||||
@@ -990,6 +1041,137 @@
|
||||
).test(t);
|
||||
}
|
||||
|
||||
/** Button / /look / «посмотри на кадр» — not casual «смотри какая». */
|
||||
function userAsksLook(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
return false;
|
||||
}
|
||||
if (/\b(look\s+at|critique|criticize|describe\s+(this|the|ref|image)|what\s+do\s+you\s+see)\b/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (cyrTokenRe('критик[а-яё]*|что\\s+не\\s+так|разбери').test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (cyrTokenRe('опиши\\s+(это|эту|реф|изображ[а-яё]*|картинк[а-яё]*|кадр|результат|референс)').test(t)) {
|
||||
return true;
|
||||
}
|
||||
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene / edit / «ещё» — Generate without the magic word «генерируй».
|
||||
* Bare «давай» and trivia questions stay false.
|
||||
*/
|
||||
function userImpliesGenerate(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || userAsksNoGenerate(t)) {
|
||||
return false;
|
||||
}
|
||||
if (userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t)) {
|
||||
return true;
|
||||
}
|
||||
if (t.length < 8) {
|
||||
return false;
|
||||
}
|
||||
const wantsLook = userAsksLook(t);
|
||||
const wantsRedraw = cyrTokenRe('поправь|исправь|перегенерир|перерисуй|улучши|переделай').test(t)
|
||||
|| /\b(fix|redo|redraw|improve)\b/i.test(t);
|
||||
if (wantsLook && !wantsRedraw && !userAsksGenerate(t)) {
|
||||
return false;
|
||||
}
|
||||
if (cyrTokenRe(
|
||||
'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|'
|
||||
+ 'список\\s+лор|где\\s+настрой|что\\s+значит',
|
||||
).test(t) && !cyrTokenRe('нарису|сгенер|картинк').test(t)) {
|
||||
return false;
|
||||
}
|
||||
if (cyrTokenRe(
|
||||
'нарису|сгенер|перерису|картинк|изображен|'
|
||||
+ 'хочу\\s+(увидеть|видеть)|покажи\\s+(её|ее|его|как)|'
|
||||
+ 'сделай\\s+(её|ее|его|мне)|пусть\\s+будет|'
|
||||
+ 'давай\\s+(её|ее|его|с\\s|в\\s|на\\s)|'
|
||||
+ 'в\\s+(студии|лесу|интерьер|постел)|'
|
||||
+ 'другой\\s+(ракурс|свет|наряд|поза)|'
|
||||
+ 'поменяй\\s+(позу|свет|одежд|фон)|добавь\\s+(свет|детал)|'
|
||||
+ 'ещё\\s+одн|еще\\s+одн|вариант',
|
||||
).test(t)) {
|
||||
return true;
|
||||
}
|
||||
if (/\b(draw|paint|render|make her|make him|another one|new frame|in the (studio|forest))\b/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
const isQuestion = /[??]\s*$/.test(t);
|
||||
if (isQuestion) {
|
||||
return cyrTokenRe('нарису|сгенер|можешь\\s+(сделать|нарисовать)|можно\\s+(картинк|сгенер)').test(t);
|
||||
}
|
||||
return t.length >= 40 && cyrTokenRe(
|
||||
'девушк|женщин|парн|мужчин|стоит|сидит|лежит|обнаж|поза|интерьер|студи',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
function packBlocksAutoGenerate(pack) {
|
||||
const p = String(pack || '');
|
||||
return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona';
|
||||
}
|
||||
|
||||
/** If the turn is a frame, put actions:["generate"] on the patch even when the model omitted it. */
|
||||
function ensureGenerateAction(patch, userText) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return patch;
|
||||
}
|
||||
if (userAsksNoGenerate(userText)) {
|
||||
return patch;
|
||||
}
|
||||
if (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate')) {
|
||||
return patch;
|
||||
}
|
||||
const pack = $('sa_pack')?.value || '';
|
||||
if (packBlocksAutoGenerate(pack) && !userAsksGenerate(userText) && !userAsksContinue(userText)) {
|
||||
return patch;
|
||||
}
|
||||
const hasFrame = patch.prompt != null
|
||||
|| (Array.isArray(patch.variants) && patch.variants.length > 0);
|
||||
const implied = userImpliesGenerate(userText);
|
||||
if (!hasFrame && !implied) {
|
||||
return patch;
|
||||
}
|
||||
const out = { ...patch };
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String).filter((a) => a && a !== 'generate') : [];
|
||||
acts.push('generate');
|
||||
out.actions = acts;
|
||||
return out;
|
||||
}
|
||||
|
||||
function packWantsVision(pack) {
|
||||
const p = String(pack || '');
|
||||
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
|
||||
}
|
||||
|
||||
/** Honor model look_at only when asked, pack needs it, or look-only (no Generate this turn). */
|
||||
function shouldHonorLookAt(patch, opts = {}) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (patch.look_at == null && patch.vision_from == null && patch.vision_slots == null) {
|
||||
return false;
|
||||
}
|
||||
if (opts.fromAutoCritique || opts.fromVisionHop) {
|
||||
return true;
|
||||
}
|
||||
if (userAsksLook(opts.userText || '')) {
|
||||
return true;
|
||||
}
|
||||
if (packWantsVision($('sa_pack')?.value)) {
|
||||
return true;
|
||||
}
|
||||
// look_at + generate on a write turn stares at the old frame and delays the new one.
|
||||
if (patchHasGenTrigger(patch)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function stripGenerateAction(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return patch;
|
||||
@@ -1204,6 +1386,12 @@
|
||||
setVal(batchId, String(batch));
|
||||
}
|
||||
}
|
||||
if (!liveNegativePrompt()) {
|
||||
const neg = exactDefaultNegative();
|
||||
if (neg) {
|
||||
setNegativePrompt(neg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldSkipSessionRollback(key, patchValue) {
|
||||
@@ -2274,6 +2462,9 @@
|
||||
promptBox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
setVal('input_negativeprompt', params.negative != null ? String(params.negative) : '');
|
||||
if (document.getElementById('alt_negativeprompt_textbox')) {
|
||||
setVal('alt_negativeprompt_textbox', params.negative != null ? String(params.negative) : '');
|
||||
}
|
||||
// Force-write numerics when present so prior chat values cannot stick.
|
||||
if (params.width != null) {
|
||||
setVal('input_width', String(params.width));
|
||||
@@ -3965,9 +4156,6 @@
|
||||
box.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
box.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
if (patch.negative != null) {
|
||||
setVal('input_negativeprompt', patch.negative);
|
||||
}
|
||||
if (Array.isArray(patch.loras)) {
|
||||
for (const l of patch.loras) {
|
||||
const triggers = l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []);
|
||||
@@ -3980,6 +4168,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doPrompt) {
|
||||
if (patch.negative != null) {
|
||||
setNegativePrompt(patch.negative);
|
||||
} else if (patchHasGenTrigger(patch)) {
|
||||
ensureNegativeForGenerate(patch);
|
||||
}
|
||||
}
|
||||
|
||||
if (doLoras && Array.isArray(patch.loras) && typeof loraHelper !== 'undefined' && loraHelper) {
|
||||
try {
|
||||
@@ -4611,6 +4806,7 @@
|
||||
await applyPatch(job.patch, 'all');
|
||||
syncLiveParamsBar();
|
||||
}
|
||||
ensureNegativeForGenerate(job.patch);
|
||||
|
||||
if (epoch !== state.chatEpoch) {
|
||||
break;
|
||||
@@ -5332,6 +5528,18 @@
|
||||
}
|
||||
localStorage.setItem(LS_PACK_ORDINARY_MIG, '1');
|
||||
}
|
||||
// One-shot: stop staring at every Generate. Vision = button / /look / model look_at.
|
||||
if (!localStorage.getItem(LS_VISION_OPTIN_MIG)) {
|
||||
localStorage.setItem(LS_AUTO_VISION, '0');
|
||||
localStorage.setItem(LS_AUTO_CRITIQUE, '0');
|
||||
localStorage.setItem(LS_VISION_OPTIN_MIG, '1');
|
||||
if ($('sa_auto_vision')) {
|
||||
$('sa_auto_vision').checked = false;
|
||||
}
|
||||
if ($('sa_auto_critique')) {
|
||||
$('sa_auto_critique').checked = false;
|
||||
}
|
||||
}
|
||||
const base = localStorage.getItem(LS_BASE);
|
||||
const model = localStorage.getItem(LS_MODEL);
|
||||
const pack = localStorage.getItem(LS_PACK);
|
||||
@@ -7865,19 +8073,18 @@
|
||||
}
|
||||
if (!effective && !fromVisionHop && !fromAutoCritique && !fromDebug
|
||||
&& !opts.fromEmptyPatchRetry
|
||||
&& replyMissingJsonPatch(reply)
|
||||
&& (opts.userWantsGenerate || state.pendingSilentGen || userAsksContinue(opts.userText || ''))) {
|
||||
appendSystemNote('Патч пустой — прошу модель дописать JSON.');
|
||||
&& userImpliesGenerate(opts.userText || '')) {
|
||||
appendSystemNote('Нужен кадр — прошу JSON с prompt + generate.');
|
||||
await sendChat({
|
||||
skipSlash: true,
|
||||
skipAutoPack: true,
|
||||
fromEmptyPatchRetry: true,
|
||||
userWantsGenerate: true,
|
||||
forcedUserText:
|
||||
'Ты написал «JSON Patch» без fenced ```json```. '
|
||||
+ 'Сейчас ответь ТОЛЬКО одним fenced JSON объектом: '
|
||||
+ '{"prompt":"<English Krea prompt for Qwen3-VL>","actions":["generate"]}. '
|
||||
+ 'prompt — только английский (skill prompting). Без прозы, без ### заголовков.',
|
||||
'Пользователь уже просит кадр (это следует из сообщения, даже без слова «генерируй»). '
|
||||
+ 'Ответь ТОЛЬКО одним fenced JSON: '
|
||||
+ '{"prompt":"<English Krea prompt>","negative":"<echo or short quality neg>","actions":["generate"]}. '
|
||||
+ 'prompt — английский. Без прозы, без «скажи сгенерируй».',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -7905,7 +8112,17 @@
|
||||
rememberLastPatch(effective);
|
||||
}
|
||||
}
|
||||
if (effective && !fromVisionHop && !fromAutoCritique && !suppressGen) {
|
||||
if (effective && !fromDebug && !suppressGen) {
|
||||
effective = ensureGenerateAction(effective, opts.userText || '');
|
||||
rememberLastPatch(effective);
|
||||
}
|
||||
if (effective && !fromVisionHop && !fromAutoCritique && !fromDebug && !shouldHonorLookAt(effective, opts)) {
|
||||
effective = stripLookAt(effective);
|
||||
if (effective) {
|
||||
rememberLastPatch(effective);
|
||||
}
|
||||
}
|
||||
if (effective && !fromVisionHop && !fromAutoCritique && !suppressGen && shouldHonorLookAt(effective, opts)) {
|
||||
const hopped = await maybeVisionHop(effective, opts.attachedSlotIds || []);
|
||||
if (hopped) {
|
||||
return;
|
||||
@@ -7917,6 +8134,7 @@
|
||||
return;
|
||||
}
|
||||
const wantsGen = !suppressGen && !!(opts.userWantsGenerate || state.pendingSilentGen
|
||||
|| userImpliesGenerate(opts.userText || '')
|
||||
|| (Array.isArray(effective?.actions) && effective.actions.map(String).includes('generate')));
|
||||
const willGen = !!(effective && !fromAutoCritique && !suppressGen
|
||||
&& (wantsGen || $('sa_auto_generate')?.checked));
|
||||
@@ -8385,7 +8603,7 @@
|
||||
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromDebug) {
|
||||
state.lastUserParamIntent = userTextMentionsParams(text);
|
||||
state.lastUserControlIntent = userTextMentionsControls(text);
|
||||
state.pendingSilentGen = userAsksGenerate(text) || userAsksContinue(text) || isSameButAspectRequest(text);
|
||||
state.pendingSilentGen = userImpliesGenerate(text);
|
||||
if (userAsksNoGenerate(text)) {
|
||||
state.pendingSilentGen = false;
|
||||
}
|
||||
@@ -8602,12 +8820,12 @@
|
||||
const prose = extractPatch(reply).prose || reply;
|
||||
state.history.push({ role: 'assistant', content: prose, persona, pack });
|
||||
persistHistory();
|
||||
setBusyPhase(state.pendingSilentGen || userAsksGenerate(text) || userAsksContinue(text) ? 'silent_gen' : 'thinking');
|
||||
setBusyPhase(state.pendingSilentGen || userImpliesGenerate(text) ? 'silent_gen' : 'thinking');
|
||||
try {
|
||||
await handleReplySideEffects(reply, civitaiResults, {
|
||||
...opts,
|
||||
userText: text,
|
||||
userWantsGenerate: !!state.pendingSilentGen || userAsksGenerate(text) || userAsksContinue(text),
|
||||
userWantsGenerate: !!state.pendingSilentGen || userImpliesGenerate(text),
|
||||
attachedSlotIds: visionSlots.map((s) => s.id),
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -6,7 +6,7 @@ window.SA = window.SA || {};
|
||||
|
||||
(function () {
|
||||
const PATCH_KEYS = [
|
||||
'prompt', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler',
|
||||
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler',
|
||||
'actions', 'search_query', 'civitai_query',
|
||||
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||
@@ -31,8 +31,8 @@ window.SA = window.SA || {};
|
||||
return false;
|
||||
}
|
||||
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
||||
const genish = !!(obj.prompt != null || obj.loras || obj.actions || obj.width || obj.height
|
||||
|| obj.steps || obj.cfg || obj.aspect || obj.seed != null
|
||||
const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions
|
||||
|| obj.width || obj.height || obj.steps || obj.cfg || obj.aspect || obj.seed != null
|
||||
|| obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
|
||||
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user