Ship Assistent 0.10.2: lean context packing with quality-first vision flags.
Cut always-on system tokens (slim core, inventory, identity shelves, hops) and fix has_vision_image vs images_in_request so look_at gates correctly; enrich rich LoRAs and expose system_chars in debug. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+169
-81
@@ -27,6 +27,8 @@
|
||||
let MAX_REF_SLOTS = 4;
|
||||
let CONTEXT_PROMPT_MAX = 2000;
|
||||
let HISTORY_KEEP_TURNS = 4;
|
||||
let INVENTORY_PROMPT_RICH = 12;
|
||||
let INVENTORY_PROMPT_NAMES = 24;
|
||||
|
||||
let ASPECT_TABLE = {
|
||||
'1:1': [1024, 1024],
|
||||
@@ -136,6 +138,8 @@
|
||||
streamMeta: null,
|
||||
critiqueHopUsed: false,
|
||||
visionHopUsed: false,
|
||||
lastSystemChars: 0,
|
||||
lastContextChars: 0,
|
||||
busyPhase: 'idle',
|
||||
busyStarted: 0,
|
||||
gotDelta: false,
|
||||
@@ -784,6 +788,12 @@
|
||||
return state.slots.filter((s) => s.attach && s.src);
|
||||
}
|
||||
|
||||
/** Real board frames (not model previews). Used for has_vision_image even when JPEG is not sent. */
|
||||
function visionReadySlots() {
|
||||
ensureBoard();
|
||||
return state.slots.filter((s) => s && s.src && !looksLikeModelPreview(s.src));
|
||||
}
|
||||
|
||||
function setSlotSrc(id, src, { select = true, attach = null, note = null, switchTab = false, allowPreview = false } = {}) {
|
||||
const slot = slotById(id);
|
||||
if (!slot) {
|
||||
@@ -2374,11 +2384,11 @@
|
||||
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
|
||||
prompt_image_count: countPromptImages(),
|
||||
selected_loras: [],
|
||||
available_loras: slimInventoryLoras(inv.loras || [], 100),
|
||||
available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 40),
|
||||
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
|
||||
available_loras: [],
|
||||
available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 8),
|
||||
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 20),
|
||||
inventory_at: inv.inventory_at || null,
|
||||
has_vision_image: attachableSlots().length > 0,
|
||||
has_vision_image: visionReadySlots().length > 0,
|
||||
image_slots: slotCatalog(),
|
||||
attached_slot_ids: attachableSlots().map((s) => s.id),
|
||||
has_civitai_key: !!inv.has_civitai_key,
|
||||
@@ -2389,6 +2399,14 @@
|
||||
user_prefs_count: 0,
|
||||
...initCtx,
|
||||
};
|
||||
{
|
||||
const slim = slimInventoryLoras(inv.loras || [], INVENTORY_PROMPT_NAMES);
|
||||
ctx.available_loras = slim;
|
||||
if ((inv.loras || []).length > slim.length) {
|
||||
ctx.available_loras_truncated = true;
|
||||
ctx.available_loras_total = (inv.loras || []).length;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const model = resolveCurrentCheckpoint();
|
||||
@@ -2403,14 +2421,35 @@
|
||||
|
||||
try {
|
||||
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
|
||||
ctx.selected_loras = loraHelper.selected.map((l) => ({
|
||||
name: l.name || l,
|
||||
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
|
||||
}));
|
||||
const byName = new Map();
|
||||
for (const l of inv.loras || []) {
|
||||
if (l?.name) {
|
||||
byName.set(String(l.name).toLowerCase(), l);
|
||||
}
|
||||
}
|
||||
ctx.selected_loras = loraHelper.selected.map((l) => {
|
||||
const name = l.name || l;
|
||||
const invRow = byName.get(String(name).toLowerCase()) || {};
|
||||
const out = {
|
||||
name,
|
||||
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[name]) || invRow.default_weight || 1,
|
||||
};
|
||||
if (invRow.trigger_phrase) {
|
||||
out.trigger_phrase = invRow.trigger_phrase;
|
||||
}
|
||||
if (Array.isArray(invRow.triggers) && invRow.triggers.length) {
|
||||
out.triggers = invRow.triggers.slice(0, 8);
|
||||
}
|
||||
if (invRow.blurb) {
|
||||
out.blurb = invRow.blurb;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
ctx.enabled_loras = ctx.selected_loras;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// Recommendation cards: checkpoint + selected LoRAs + other has_card entries (capped).
|
||||
// Recommendation cards: checkpoint + selected LoRAs only (no has_card sweep).
|
||||
const cardKeys = [];
|
||||
const seenCard = new Set();
|
||||
const addKey = (kind, name) => {
|
||||
@@ -2432,22 +2471,6 @@
|
||||
addKey('lora', l.name);
|
||||
}
|
||||
}
|
||||
for (const l of inv.loras || []) {
|
||||
if (l?.has_card && l?.name) {
|
||||
addKey('lora', l.name);
|
||||
}
|
||||
if (cardKeys.length >= 14) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const c of inv.checkpoints || []) {
|
||||
if (c?.has_card && c?.name) {
|
||||
addKey('checkpoint', c.name);
|
||||
}
|
||||
if (cardKeys.length >= 16) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const k of cardKeys) {
|
||||
const cached = state.modelCards[`${k.kind}:${k.name}`];
|
||||
if (cached) {
|
||||
@@ -2477,8 +2500,8 @@
|
||||
architecture: m.architecture || null,
|
||||
});
|
||||
}
|
||||
if (ctx.available_loras.length > 80) {
|
||||
ctx.available_loras = ctx.available_loras.slice(0, 80);
|
||||
if (ctx.available_loras.length > INVENTORY_PROMPT_NAMES) {
|
||||
ctx.available_loras = ctx.available_loras.slice(0, INVENTORY_PROMPT_NAMES);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
@@ -2491,28 +2514,33 @@
|
||||
const profile = hasRaw && !hasTurbo ? 'raw' : 'turbo';
|
||||
ctx.krea_profile = profile;
|
||||
const defaults = mergedGenerationDefaults(profile);
|
||||
ctx.recommended_params = {
|
||||
// Only send recommended_params when live UI differs from Exact-backed defaults
|
||||
// (Exact itself is already in the system prompt).
|
||||
const rec = {
|
||||
steps: defaults.steps ?? 8,
|
||||
cfg: defaults.cfg ?? 1,
|
||||
sigma_shift: defaults.sigma_shift ?? 1.15,
|
||||
};
|
||||
if (defaults.aspect) {
|
||||
ctx.recommended_params.aspect = defaults.aspect;
|
||||
rec.aspect = defaults.aspect;
|
||||
}
|
||||
const liveDiffers = (ctx.steps != null && ctx.steps !== rec.steps)
|
||||
|| (ctx.cfg != null && ctx.cfg !== rec.cfg)
|
||||
|| (ctx.sigma_shift != null && ctx.sigma_shift !== rec.sigma_shift);
|
||||
if (liveDiffers) {
|
||||
ctx.recommended_params = rec;
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.krea_profile = 'turbo';
|
||||
const defaults = mergedGenerationDefaults('turbo');
|
||||
ctx.recommended_params = {
|
||||
steps: defaults.steps ?? 8,
|
||||
cfg: defaults.cfg ?? 1,
|
||||
sigma_shift: defaults.sigma_shift ?? 1.15,
|
||||
};
|
||||
}
|
||||
|
||||
ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length
|
||||
? { ...state.sessionExact }
|
||||
: {};
|
||||
: undefined;
|
||||
// Exact KV is already in the system prompt — do not duplicate the full blob into live context.
|
||||
if (!ctx.session_exact) {
|
||||
delete ctx.session_exact;
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
@@ -2562,7 +2590,8 @@
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
const softCap = Math.min(limit || 100, 80);
|
||||
const namesCap = Math.min(limit || INVENTORY_PROMPT_NAMES, INVENTORY_PROMPT_NAMES);
|
||||
const richCap = Math.max(4, Math.min(INVENTORY_PROMPT_RICH, namesCap));
|
||||
const rows = (list || []).map((l) => {
|
||||
const sel = selected.has(String(l.name || '').toLowerCase());
|
||||
const hasCard = !!l.has_card;
|
||||
@@ -2584,58 +2613,72 @@
|
||||
};
|
||||
});
|
||||
rows.sort((a, b) => b._score - a._score || String(a.name).localeCompare(String(b.name)));
|
||||
// Full detail for top tier; name+trigger only for the rest within softCap.
|
||||
const fullDetail = 36;
|
||||
return rows.slice(0, softCap).map((row, idx) => {
|
||||
const out = { name: row.name, title: row.title };
|
||||
if (row.trigger_phrase) {
|
||||
out.trigger_phrase = row.trigger_phrase;
|
||||
// Rich: enabled + top krea/card (triggers/blurb). Rest: name (+ krea_likely) only.
|
||||
let richUsed = 0;
|
||||
const out = [];
|
||||
for (const row of rows) {
|
||||
if (out.length >= namesCap) {
|
||||
break;
|
||||
}
|
||||
if (row.triggers) {
|
||||
out.triggers = row.triggers;
|
||||
const sel = selected.has(String(row.name || '').toLowerCase());
|
||||
let wantRich = sel;
|
||||
if (!wantRich && richUsed < richCap && (row.krea_likely || row.has_card || row.blurb)) {
|
||||
wantRich = true;
|
||||
}
|
||||
if (row.krea_likely) {
|
||||
out.krea_likely = true;
|
||||
}
|
||||
if (row.has_card) {
|
||||
out.has_card = true;
|
||||
}
|
||||
const rich = idx < fullDetail || row._score >= 200;
|
||||
if (rich) {
|
||||
if (row.architecture) {
|
||||
out.architecture = row.architecture;
|
||||
if (wantRich) {
|
||||
const rich = { name: row.name, title: row.title };
|
||||
if (row.trigger_phrase) {
|
||||
rich.trigger_phrase = row.trigger_phrase;
|
||||
}
|
||||
if (row.compat_class) {
|
||||
out.compat_class = row.compat_class;
|
||||
if (row.triggers) {
|
||||
rich.triggers = row.triggers;
|
||||
}
|
||||
if (row.krea_likely) {
|
||||
rich.krea_likely = true;
|
||||
}
|
||||
if (row.has_card) {
|
||||
rich.has_card = true;
|
||||
}
|
||||
if (row.blurb) {
|
||||
out.blurb = row.blurb;
|
||||
rich.blurb = row.blurb;
|
||||
}
|
||||
if (row.default_weight) {
|
||||
out.default_weight = row.default_weight;
|
||||
rich.default_weight = row.default_weight;
|
||||
}
|
||||
if (row.tags) {
|
||||
out.tags = row.tags;
|
||||
if (row.architecture) {
|
||||
rich.architecture = row.architecture;
|
||||
}
|
||||
out.push(rich);
|
||||
if (!sel) {
|
||||
richUsed++;
|
||||
}
|
||||
} else {
|
||||
const nameOnly = { name: row.name };
|
||||
if (row.krea_likely) {
|
||||
nameOnly.krea_likely = true;
|
||||
}
|
||||
out.push(nameOnly);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function slimInventoryCheckpoints(list, limit) {
|
||||
const rows = (list || []).slice();
|
||||
rows.sort((a, b) => ((b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0)) || ((b.has_card ? 1 : 0) - (a.has_card ? 1 : 0)) || String(a.name).localeCompare(String(b.name)));
|
||||
return rows.slice(0, limit || 40).map((c) => {
|
||||
return rows.slice(0, limit || 8).map((c) => {
|
||||
const out = {
|
||||
name: c.name,
|
||||
title: c.title || c.name,
|
||||
architecture: c.architecture || null,
|
||||
compat_class: c.compat_class || null,
|
||||
has_card: !!c.has_card,
|
||||
krea_likely: !!c.krea_likely,
|
||||
};
|
||||
if (c.blurb) {
|
||||
out.blurb = c.blurb;
|
||||
if (c.architecture) {
|
||||
out.architecture = c.architecture;
|
||||
}
|
||||
if (c.krea_likely) {
|
||||
out.krea_likely = true;
|
||||
}
|
||||
if (c.has_card) {
|
||||
out.has_card = true;
|
||||
}
|
||||
return out;
|
||||
});
|
||||
@@ -3566,6 +3609,30 @@
|
||||
await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] });
|
||||
}
|
||||
|
||||
/** After Generate: send look_at with JPEG when sa_auto_vision is on (skipped if auto-critique already attaches vision). */
|
||||
async function maybeAutoVisionLook(imageSrc) {
|
||||
if (!wantsAutoVision() || $('sa_auto_critique')?.checked || state.visionHopUsed || state.busy) {
|
||||
return;
|
||||
}
|
||||
const src = await resolveFinishedGenerateSrc(imageSrc);
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
const gen = generateSlot();
|
||||
if (gen) {
|
||||
gen.src = src;
|
||||
gen.attach = true;
|
||||
renderBoard();
|
||||
}
|
||||
state.visionHopUsed = true;
|
||||
setPackValue('critique_image', { flash: true });
|
||||
if ($('sa_input')) {
|
||||
$('sa_input').value = 'Look at the Generate result and briefly say what worked and what to fix next.';
|
||||
}
|
||||
setStatus('Auto look_at…');
|
||||
await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true });
|
||||
}
|
||||
|
||||
/** Board action: attach the finished Generate frame and ask for a verdict. */
|
||||
async function askLookAtResult() {
|
||||
if (state.busy || state.generating) {
|
||||
@@ -4309,6 +4376,12 @@
|
||||
if (asst.context_prompt_max != null) {
|
||||
CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2000);
|
||||
}
|
||||
if (asst.inventory_prompt_rich != null) {
|
||||
INVENTORY_PROMPT_RICH = Math.max(4, Number(asst.inventory_prompt_rich) || 12);
|
||||
}
|
||||
if (asst.inventory_prompt_names != null) {
|
||||
INVENTORY_PROMPT_NAMES = Math.max(INVENTORY_PROMPT_RICH, Number(asst.inventory_prompt_names) || 24);
|
||||
}
|
||||
fillKnobsFromConfig(data);
|
||||
if (applyDefaults || data.exact) {
|
||||
fillEmptyParamsFromExact();
|
||||
@@ -6375,6 +6448,7 @@
|
||||
);
|
||||
if (src) {
|
||||
await maybeAutoCritique(src);
|
||||
await maybeAutoVisionLook(src);
|
||||
}
|
||||
} else if (!state.generating) {
|
||||
stopBusyUi(wantsGen ? 'Применено' : '');
|
||||
@@ -6494,9 +6568,12 @@
|
||||
` ckpt=${ctx.checkpoint?.name || '—'} · krea_profile=${ctx.krea_profile || profile}`,
|
||||
` ${ctx.width || '?'}×${ctx.height || '?'} (${aspect}) · steps=${ctx.steps ?? '—'} · cfg=${ctx.cfg ?? '—'} · sigma=${ctx.sigma_shift ?? '—'} · seed=${ctx.seed ?? '—'} · batch=${ctx.batch ?? '—'}`,
|
||||
` loras: ${formatDebugLoras(ctx.selected_loras || ctx.enabled_loras)}`,
|
||||
` available_loras=${(ctx.available_loras || []).length}${ctx.available_loras_truncated ? ` truncated/${ctx.available_loras_total || '?'}` : ''}`,
|
||||
` prompt: ${clipDebug(ctx.prompt, 220)}`,
|
||||
` negative: ${clipDebug(ctx.negative, 120)}`,
|
||||
` init=${!!ctx.has_init_image} mask=${!!ctx.has_mask_image} prompt_images=${ctx.prompt_image_count || 0}`,
|
||||
` has_vision_image=${!!ctx.has_vision_image} · images_in_request=${!!ctx.images_in_request} · vision_ready=${visionReadySlots().length}`,
|
||||
` context_json_chars≈${JSON.stringify(ctx).length} · last_system_chars=${state.lastSystemChars || '—'} · last_context_chars=${state.lastContextChars || '—'}`,
|
||||
'',
|
||||
'Exact defaults (merged):',
|
||||
` generation=${JSON.stringify(exactGen)}`,
|
||||
@@ -6853,11 +6930,10 @@
|
||||
}
|
||||
|
||||
let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean);
|
||||
if (!wantedIds.length) {
|
||||
wantedIds = attachableSlots().map((s) => s.id);
|
||||
}
|
||||
if (wantsAutoVision() && generateSlot()?.src && !looksLikeModelPreview(generateSlot().src) && !wantedIds.includes(GEN_ID)) {
|
||||
wantedIds.push(GEN_ID);
|
||||
// JPEG only on look_at / vision hop — never auto-attach every board ref to ordinary chat.
|
||||
const sendVision = !!(opts.fromVisionHop || (wantedIds.length && opts.forceSlotIds));
|
||||
if (!sendVision) {
|
||||
wantedIds = [];
|
||||
}
|
||||
const visionSlots = wantedIds.map((id) => slotById(id)).filter((s) => s && s.src && !looksLikeModelPreview(s.src));
|
||||
let images = null;
|
||||
@@ -6897,8 +6973,12 @@
|
||||
persistHistory();
|
||||
|
||||
const context = collectLiveContext();
|
||||
context.has_vision_image = !!images;
|
||||
context.attached_slot_ids = visionSlots.map((s) => s.id);
|
||||
// has_vision_image = board has a real frame (even when JPEG is not in this request).
|
||||
// images_in_request = JPEG bytes are attached to the last user message this turn.
|
||||
context.has_vision_image = visionReadySlots().length > 0;
|
||||
context.images_in_request = !!(images && images.length);
|
||||
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
||||
context.vision_slot_ids = visionSlots.map((s) => s.id);
|
||||
context.persona = persona;
|
||||
if (opts.cardTarget) {
|
||||
context.card_target = opts.cardTarget;
|
||||
@@ -6935,10 +7015,18 @@
|
||||
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
|
||||
};
|
||||
|
||||
const finishOk = async (reply, civitaiResults) => {
|
||||
const finishOk = async (reply, civitaiResults, meta = {}) => {
|
||||
if (chatEpoch !== state.chatEpoch) {
|
||||
return;
|
||||
}
|
||||
if (meta.system_chars != null) {
|
||||
state.lastSystemChars = Number(meta.system_chars) || 0;
|
||||
}
|
||||
try {
|
||||
state.lastContextChars = (context && JSON.stringify(context).length) || 0;
|
||||
} catch (e) {
|
||||
state.lastContextChars = 0;
|
||||
}
|
||||
const prose = extractPatch(reply).prose || reply;
|
||||
state.history.push({ role: 'assistant', content: prose, persona, pack });
|
||||
persistHistory();
|
||||
@@ -7018,7 +7106,7 @@
|
||||
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
|
||||
const civitai = data.civitai_results || [];
|
||||
finalizeStreamMessage(reply, civitai);
|
||||
finishOk(reply, civitai);
|
||||
finishOk(reply, civitai, { system_chars: data.system_chars });
|
||||
}
|
||||
},
|
||||
0,
|
||||
@@ -7046,7 +7134,7 @@
|
||||
}
|
||||
const reply = data.reply || '';
|
||||
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
|
||||
finishOk(reply, data.civitai_results || []);
|
||||
finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars });
|
||||
},
|
||||
0,
|
||||
(err2) => finishErr(String(err2 || err || 'Chat failed')),
|
||||
@@ -7069,7 +7157,7 @@
|
||||
}
|
||||
const reply = data.reply || '';
|
||||
appendMessage('assistant', reply, null, data.civitai_results || [], msgMeta);
|
||||
finishOk(reply, data.civitai_results || []);
|
||||
finishOk(reply, data.civitai_results || [], { system_chars: data.system_chars });
|
||||
},
|
||||
0,
|
||||
(err) => finishErr(String(err || 'Chat failed')),
|
||||
|
||||
Reference in New Issue
Block a user