Add personas and model Cards (0.5.1): tone presets, stem.assistent.json, wanted queue.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 21:25:01 +03:00
co-authored by Cursor
parent b8145c6455
commit 0b429542b5
10 changed files with 1573 additions and 84 deletions
+145
View File
@@ -770,6 +770,142 @@
100% { box-shadow: 0 0 0 0 transparent; }
}
.sa-subtabs {
display: inline-flex;
gap: 0.2rem;
margin-left: 0.55rem;
vertical-align: middle;
}
.sa-subtab {
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: transparent;
color: inherit;
border-radius: 999px;
padding: 0.12rem 0.55rem;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
opacity: 0.7;
}
.sa-subtab-active {
opacity: 1;
background: color-mix(in srgb, currentColor 12%, transparent);
}
.sa-view {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.sa-cards-layout {
flex: 1;
display: flex;
min-height: 0;
gap: 0;
}
.sa-cards-list-pane {
flex: 0 0 38%;
min-width: 10rem;
border-right: 1px solid color-mix(in srgb, currentColor 18%, transparent);
display: flex;
flex-direction: column;
min-height: 0;
}
.sa-cards-filter {
display: flex;
gap: 0.35rem;
padding: 0.45rem 0.55rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}
.sa-cards-list {
flex: 1;
overflow: auto;
padding: 0.35rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sa-card-row {
text-align: left;
padding: 0.45rem 0.55rem;
border-radius: 0.4rem;
border: 1px solid transparent;
background: color-mix(in srgb, currentColor 4%, transparent);
cursor: pointer;
font-size: 0.85rem;
}
.sa-card-row:hover {
border-color: color-mix(in srgb, currentColor 22%, transparent);
}
.sa-card-row.sa-selected {
border-color: color-mix(in srgb, currentColor 45%, transparent);
background: color-mix(in srgb, currentColor 10%, transparent);
}
.sa-card-row-kind {
font-size: 0.68rem;
text-transform: uppercase;
opacity: 0.55;
letter-spacing: 0.04em;
}
.sa-card-row-meta {
font-size: 0.75rem;
opacity: 0.7;
}
.sa-cards-editor-pane {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
padding: 0.55rem 0.65rem 0.7rem;
gap: 0.45rem;
}
.sa-cards-editor-head {
display: flex;
align-items: center;
gap: 0.5rem;
}
.sa-card-badge {
font-size: 0.72rem;
padding: 0.1rem 0.4rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
opacity: 0.85;
}
#sa_card_json {
flex: 1;
min-height: 12rem;
width: 100%;
box-sizing: border-box;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.82rem;
border-radius: 0.4rem;
padding: 0.5rem;
resize: vertical;
}
.sa-cards-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
align-items: center;
}
@media (max-width: 900px) {
.sa-layout {
flex-direction: column;
@@ -786,4 +922,13 @@
.sa-splitter {
display: none;
}
.sa-cards-layout {
flex-direction: column;
}
.sa-cards-list-pane {
flex: 0 0 auto;
max-height: 40%;
border-right: none;
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
}
}
+576 -23
View File
@@ -1,11 +1,13 @@
/**
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
* v0.5.0: Krea knowledge packs, aspect/vary/lock_seed patches, slash commands, chips.
* v0.5.3: live inventory+cards in context, taste memory, post-download cards, wanted YAML fix.
*/
(function () {
const LS_BASE = 'swarm_assistent_base_url';
const LS_MODEL = 'swarm_assistent_model';
const LS_PACK = 'swarm_assistent_pack';
const LS_PERSONA = 'swarm_assistent_persona';
const LS_VIEW = 'swarm_assistent_view';
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
const LS_AUTO_APPLY = 'swarm_assistent_auto_apply';
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
@@ -13,6 +15,7 @@
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
const LS_WELCOMED = 'swarm_assistent_welcomed';
const LS_TASTE = 'swarm_assistent_taste';
const TAB_BUTTON_ID = 'maintab_assistent';
const GEN_ID = 'generate';
const MAX_REF_SLOTS = 4;
@@ -41,6 +44,9 @@
inpaint_edit: 'inpaint_edit',
describe: 'describe_ref',
describe_ref: 'describe_ref',
card: 'catalog_card',
catalog: 'catalog_card',
catalog_card: 'catalog_card',
};
const WELCOME_HTML = `
@@ -65,6 +71,7 @@
/vary — новый seed, тот же промпт
/pack write|critique|compose|params|inpaint|describe
/civitai <query> — поиск LoRA (Confirm в чате)
/inventory — rescan моделей + обновить список LoRA
Чипсы над полем ввода делают то же для aspect / seed / vary.`;
@@ -77,6 +84,8 @@
preferredModel: null,
dragDepth: 0,
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
inventoryFetchedAt: 0,
taste: { styles: [], likes: [], avoid: [], notes: '', updated: 0 },
streamEl: null,
critiqueHopUsed: false,
visionHopUsed: false,
@@ -88,6 +97,11 @@
selectedSlotId: 'ref1',
refSeq: 1,
packUserTouched: false,
view: 'chat',
personas: [],
modelCards: {},
cardsSelection: null,
cardsBusy: false,
};
function $(id) {
@@ -1100,14 +1114,18 @@
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
prompt_image_count: countPromptImages(),
selected_loras: [],
available_loras: (inv.loras || []).slice(0, 100),
available_loras: slimInventoryLoras(inv.loras || [], 100),
available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 40),
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
inventory_at: inv.inventory_at || null,
has_vision_image: attachableSlots().length > 0,
image_slots: slotCatalog(),
attached_slot_ids: attachableSlots().map((s) => s.id),
has_civitai_key: !!inv.has_civitai_key,
auto_apply: !!$('sa_auto_apply')?.checked,
auto_generate: !!$('sa_auto_generate')?.checked,
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
model_cards: [],
...initCtx,
};
@@ -1131,6 +1149,23 @@
}
} catch (e) { /* ignore */ }
// Recommendation cards for current checkpoint + selected LoRAs only.
const cardKeys = [];
if (ctx.checkpoint?.name) {
cardKeys.push({ kind: 'checkpoint', name: ctx.checkpoint.name });
}
for (const l of ctx.selected_loras || []) {
if (l?.name) {
cardKeys.push({ kind: 'lora', name: l.name });
}
}
for (const k of cardKeys) {
const cached = state.modelCards[`${k.kind}:${k.name}`];
if (cached) {
ctx.model_cards.push(cached);
}
}
// Fallback if inventory empty
if (!ctx.available_loras.length) {
try {
@@ -1162,6 +1197,58 @@
return ctx;
}
function slimInventoryLoras(list, limit) {
const selected = new Set();
try {
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
for (const l of loraHelper.selected) {
selected.add(String(l.name || l || '').toLowerCase());
}
}
} catch (e) { /* ignore */ }
const rows = (list || []).map((l) => ({
name: l.name,
title: l.title || l.name,
trigger_phrase: l.trigger_phrase || null,
triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined,
architecture: l.architecture || null,
compat_class: l.compat_class || null,
has_card: !!l.has_card,
krea_likely: !!l.krea_likely,
blurb: l.blurb || l.usage_hint || null,
default_weight: l.default_weight || undefined,
tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined,
_sel: selected.has(String(l.name || '').toLowerCase()),
}));
rows.sort((a, b) => (b._sel - a._sel) || (b.krea_likely - a.krea_likely) || String(a.name).localeCompare(String(b.name)));
return rows.slice(0, limit).map(({ _sel, ...rest }) => {
const out = {};
for (const [k, v] of Object.entries(rest)) {
if (v != null && v !== '' && !(Array.isArray(v) && !v.length)) {
out[k] = v;
}
}
return out;
});
}
function slimInventoryCheckpoints(list, limit) {
return (list || []).slice(0, limit).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;
}
return out;
});
}
function isPatchObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
@@ -1205,10 +1292,50 @@
obj.movement != null ||
obj.clear_prompt_images != null ||
obj.slot_to_prompt_image != null ||
obj.pack != null
obj.pack != null ||
obj.triggers != null ||
obj.when != null ||
obj.prompt_hint != null ||
obj.notes != null
);
}
function isCardObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
return (
(obj.kind || obj.triggers || obj.when || obj.prompt_hint || obj.notes) &&
(obj.name || obj.triggers || obj.when)
);
}
function extractCardJson(text) {
if (!text) {
return null;
}
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
let match;
let last = null;
while ((match = re.exec(text)) !== null) {
try {
const obj = JSON.parse(match[1].trim());
if (isCardObject(obj)) {
last = obj;
}
} catch (e) { /* ignore */ }
}
if (last) {
return last;
}
try {
const obj = JSON.parse(text.trim());
return isCardObject(obj) ? obj : null;
} catch (e) {
return null;
}
}
function extractPatch(text) {
if (!text) {
return { prose: text || '', patch: null };
@@ -1845,7 +1972,7 @@
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
}
sendChat({ fromDownload: true });
});
}, { rescan: true });
} else {
setStatus(msg || 'Download failed');
if (btn) {
@@ -1866,7 +1993,8 @@
}
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
if (data.success || data.overall_percent >= 0.99) {
onDone(true);
// Swarm docs: download does not always refresh model list — force both.
triggerSwarmModelRefresh(() => onDone(true));
} else if (data.current_percent != null) {
setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
}
@@ -2051,6 +2179,8 @@
const base = localStorage.getItem(LS_BASE);
const model = localStorage.getItem(LS_MODEL);
const pack = localStorage.getItem(LS_PACK);
const persona = localStorage.getItem(LS_PERSONA);
const view = localStorage.getItem(LS_VIEW);
const auto = localStorage.getItem(LS_AUTO_VISION);
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
@@ -2063,6 +2193,9 @@
if (pack && $('sa_pack')) {
$('sa_pack').value = pack;
}
if (persona && $('sa_persona')) {
$('sa_persona').value = persona;
}
if (auto != null && $('sa_auto_vision')) {
$('sa_auto_vision').checked = auto === '1';
}
@@ -2084,12 +2217,17 @@
if (paneW) {
document.documentElement.style.setProperty('--sa-image-width', paneW);
}
if (view === 'cards' || view === 'chat') {
state.view = view;
}
}
function saveSettings() {
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral');
localStorage.setItem(LS_VIEW, state.view || 'chat');
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
@@ -2163,7 +2301,367 @@
);
}
function refreshInventory(done) {
function refreshInventory(done, opts = {}) {
if (typeof genericRequest !== 'function') {
if (done) {
done();
}
return;
}
const rescan = !!opts.rescan;
genericRequest(
'AssistentListInventory',
{ rescan },
(data) => {
state.inventory = {
loras: data.loras || [],
checkpoints: data.checkpoints || [],
wildcards: data.wildcards || [],
has_civitai_key: !!data.has_civitai_key,
inventory_at: data.inventory_at || Math.floor(Date.now() / 1000),
rescanned: !!data.rescanned,
};
state.inventoryFetchedAt = Date.now();
const n = state.inventory.loras.length;
const ck = state.inventory.checkpoints.length;
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
prefetchActiveModelCards();
if (state.view === 'cards') {
renderCardsList();
}
if (done) {
done(state.inventory);
}
},
0,
(err) => {
console.warn('Assistent inventory', err);
if (done) {
done(null);
}
},
);
}
function refreshInventoryAsync(opts = {}) {
return new Promise((resolve) => refreshInventory(resolve, opts));
}
function setCardStatus(msg) {
const el = $('sa_card_status');
if (el) {
el.textContent = msg || '';
}
}
function setView(view) {
state.view = view === 'cards' ? 'cards' : 'chat';
const chat = $('sa_view_chat');
const cards = $('sa_view_cards');
if (chat) {
chat.hidden = state.view !== 'chat';
}
if (cards) {
cards.hidden = state.view !== 'cards';
}
$('sa_tab_chat')?.classList.toggle('sa-subtab-active', state.view === 'chat');
$('sa_tab_cards')?.classList.toggle('sa-subtab-active', state.view === 'cards');
saveSettings();
if (state.view === 'cards') {
renderCardsList();
}
}
function refreshPersonas() {
if (typeof genericRequest !== 'function') {
return;
}
genericRequest(
'AssistentListPersonas',
{},
(data) => {
const list = data.personas || [];
state.personas = list;
const sel = $('sa_persona');
if (!sel) {
return;
}
const prefer = localStorage.getItem(LS_PERSONA) || data.default || 'neutral';
sel.innerHTML = '';
for (const p of list) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.title || p.id;
sel.appendChild(opt);
}
if ([...sel.options].some((o) => o.value === prefer)) {
sel.value = prefer;
} else if (data.default) {
sel.value = data.default;
}
},
0,
(err) => console.warn('Assistent personas', err),
);
}
function prefetchCard(kind, name) {
return new Promise((resolve) => {
if (!kind || !name || typeof genericRequest !== 'function') {
resolve(null);
return;
}
const key = `${kind}:${name}`;
genericRequest(
'AssistentGetCard',
{ kind, name },
(data) => {
if (data?.card) {
state.modelCards[key] = data.card;
}
resolve(data?.card || null);
},
0,
() => resolve(null),
);
});
}
async function prefetchActiveModelCards() {
const keys = [];
try {
const ck = resolveCurrentCheckpoint();
if (ck?.name) {
keys.push({ kind: 'checkpoint', name: ck.name });
}
} catch (e) { /* ignore */ }
try {
if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
for (const l of loraHelper.selected) {
const name = l?.name || l;
if (name) {
keys.push({ kind: 'lora', name });
}
}
}
} catch (e) { /* ignore */ }
await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
}
function cardsCatalog() {
const kind = $('sa_cards_kind')?.value || 'all';
const inv = state.inventory || {};
const rows = [];
if (kind === 'all' || kind === 'checkpoint') {
for (const c of inv.checkpoints || []) {
rows.push({ kind: 'checkpoint', name: c.name, title: c.title || c.name, has_card: !!c.has_card });
}
}
if (kind === 'all' || kind === 'lora') {
for (const l of inv.loras || []) {
rows.push({ kind: 'lora', name: l.name, title: l.title || l.name, has_card: !!l.has_card, trigger: l.trigger_phrase });
}
}
return rows;
}
function renderCardsList() {
const root = $('sa_cards_list');
if (!root) {
return;
}
root.innerHTML = '';
const rows = cardsCatalog();
if (!rows.length) {
root.innerHTML = '<div class="sa-chat-empty-hint">Inventory empty — Refresh.</div>';
return;
}
for (const row of rows) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sa-card-row';
if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) {
btn.classList.add('sa-selected');
}
btn.innerHTML = `<div class="sa-card-row-kind">${row.kind}</div><div>${escapeHtml(row.title || row.name)}</div><div class="sa-card-row-meta">${row.has_card ? 'card ✓' : 'no card'}${row.trigger ? ' · ' + escapeHtml(String(row.trigger).slice(0, 40)) : ''}</div>`;
btn.addEventListener('click', () => selectCardModel(row));
root.appendChild(btn);
}
}
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function selectCardModel(row) {
state.cardsSelection = row;
renderCardsList();
if ($('sa_card_title')) {
$('sa_card_title').textContent = row.title || row.name;
}
const badge = $('sa_card_badge');
if (badge) {
badge.hidden = false;
badge.textContent = row.has_card ? 'has card' : 'missing card';
}
setCardStatus('Loading…');
genericRequest(
'AssistentGetCardMeta',
{ kind: row.kind, name: row.name },
async (data) => {
const card = data.card || {
kind: row.kind,
name: row.name,
triggers: data.trigger_phrase ? [data.trigger_phrase] : [],
weight: row.kind === 'lora' ? 0.8 : 1,
when: '',
avoid: '',
prompt_hint: '',
notes: '',
civitai_url: '',
version_id: data.version_id || null,
};
if ($('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
}
state.modelCards[`${row.kind}:${row.name}`] = card;
// Attach 12 Civitai examples to board as Ref vision.
const urls = (data.example_urls || []).slice(0, 2);
for (const url of urls) {
try {
await addRefFromUrl(url);
} catch (e) { /* ignore */ }
}
setCardStatus(data.has_card ? 'Loaded card' : 'No card yet — Generate or edit JSON');
},
0,
(err) => setCardStatus(String(err || 'Load failed')),
);
}
async function addRefFromUrl(url) {
if (!url) {
return;
}
addRefSlot({ src: url, select: false });
}
function readCardDraft() {
const raw = $('sa_card_json')?.value || '';
try {
return JSON.parse(raw);
} catch (e) {
setCardStatus('Invalid JSON');
return null;
}
}
function saveCurrentCard({ enqueue } = {}) {
const sel = state.cardsSelection;
if (!sel) {
setCardStatus('Select a model');
return;
}
const card = readCardDraft();
if (!card) {
return;
}
card.kind = card.kind || sel.kind;
card.name = card.name || sel.name;
setCardStatus('Saving…');
genericRequest(
'AssistentSaveCard',
{ kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue },
(data) => {
if (data.error) {
setCardStatus(data.error);
return;
}
state.modelCards[`${sel.kind}:${sel.name}`] = card;
setCardStatus(data.installed ? `Saved ${data.path}` : `Draft + wanted → ${data.path}`);
refreshInventory();
},
0,
(err) => setCardStatus(String(err || 'Save failed')),
);
}
function enqueueWantedOnly() {
const sel = state.cardsSelection;
const card = readCardDraft() || {};
if (!sel && !card.civitai_url) {
setCardStatus('Need model or civitai_url');
return;
}
genericRequest(
'AssistentEnqueueWanted',
{
kind: (card.kind || sel?.kind || 'lora'),
url: card.civitai_url || '',
version_id: card.version_id || 0,
title: card.name || sel?.name || '',
card,
},
(data) => {
setCardStatus(data.already ? 'Already in wanted queue' : `Wanted → ${data.path}`);
},
0,
(err) => setCardStatus(String(err || 'Enqueue failed')),
);
}
async function generateCardWithAssistent() {
const sel = state.cardsSelection;
if (!sel) {
setCardStatus('Select a model');
return;
}
if (state.busy) {
setCardStatus('Chat busy');
return;
}
setPackValue('catalog_card', { flash: true });
setView('chat');
const meta = await new Promise((resolve) => {
genericRequest(
'AssistentGetCardMeta',
{ kind: sel.kind, name: sel.name },
(data) => resolve(data),
0,
() => resolve(null),
);
});
const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`;
await sendChat({
forcedUserText: forced,
skipSlash: true,
skipAutoPack: true,
fromCards: true,
cardTarget: {
kind: sel.kind,
name: sel.name,
meta,
},
});
}
function inventoryIsStale(maxAgeMs = 20000) {
if (!state.inventoryFetchedAt) {
return true;
}
return (Date.now() - state.inventoryFetchedAt) > maxAgeMs;
}
async function ensureFreshInventory({ forceRescan } = {}) {
const rescan = forceRescan || inventoryIsStale(20000);
await refreshInventoryAsync({ rescan });
}
function triggerSwarmModelRefresh(done) {
if (typeof genericRequest !== 'function') {
if (done) {
done();
@@ -2171,23 +2669,15 @@
return;
}
genericRequest(
'AssistentListInventory',
{},
(data) => {
state.inventory = {
loras: data.loras || [],
checkpoints: data.checkpoints || [],
wildcards: data.wildcards || [],
has_civitai_key: !!data.has_civitai_key,
};
setStatus(`Inventory: ${state.inventory.loras.length} LoRAs, ${state.inventory.wildcards.length} wildcards`);
'TriggerRefresh',
{ strong: true },
() => {
if (done) {
done();
}
},
0,
(err) => {
console.warn('Assistent inventory', err);
() => {
if (done) {
done();
}
@@ -2196,7 +2686,16 @@
}
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
const { fromAutoCritique, fromVisionHop } = opts;
const { fromAutoCritique, fromVisionHop, fromCards } = opts;
if (fromCards) {
const card = extractCardJson(reply);
if (card && $('sa_card_json')) {
$('sa_card_json').value = JSON.stringify(card, null, 2);
setView('cards');
setCardStatus('Draft from Assistent — review & Save');
}
return;
}
const { patch } = extractPatch(reply);
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
doInterruptNow();
@@ -2389,6 +2888,17 @@
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)');
return true;
}
if (cmd === 'inventory' || cmd === 'inv') {
setStatus('Rescanning models…');
triggerSwarmModelRefresh(async () => {
await refreshInventoryAsync({ rescan: true });
const n = state.inventory?.loras?.length || 0;
const ck = state.inventory?.checkpoints?.length || 0;
appendSystemNote(`Inventory refreshed: ${n} LoRAs, ${ck} checkpoints.`);
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts (rescanned)`);
});
return true;
}
if (cmd === 'pack') {
if (!setPackValue(arg, { flash: true, user: true })) {
setStatus('Pack: write|critique|compose|params|inpaint|describe');
@@ -2483,14 +2993,20 @@
return;
}
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromCards) {
const guessed = autoSelectPack(text);
if (guessed) {
setPackValue(guessed, { flash: true });
}
}
// Cards mode must not be overridden by auto-pack; keep catalog_card.
if (opts.fromCards || state.view === 'cards') {
setPackValue('catalog_card', { flash: false });
}
const pack = $('sa_pack')?.value || 'write_prompt';
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
const model = $('sa_model')?.value;
if (!model) {
setStatus('Pick an Ollama model in ⚙');
@@ -2498,6 +3014,11 @@
return;
}
// Always pull latest LoRA/checkpoint list before the LLM sees context
// (rescans disk when inventory is older than ~20s or after downloads).
setStatus('Refreshing inventory…');
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
state.critiqueHopUsed = false;
state.visionHopUsed = false;
@@ -2534,6 +3055,18 @@
const context = collectLiveContext();
context.has_vision_image = !!images;
context.attached_slot_ids = visionSlots.map((s) => s.id);
context.persona = persona;
if (opts.cardTarget) {
context.card_target = opts.cardTarget;
}
if (opts.fromCards || pack === 'catalog_card') {
context.auto_apply = false;
context.auto_generate = false;
}
await prefetchActiveModelCards();
// Refresh cards into context after prefetch
const refreshed = collectLiveContext();
context.model_cards = refreshed.model_cards;
const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content }));
if (images && messages.length) {
messages[messages.length - 1].images = images;
@@ -2550,6 +3083,7 @@
baseUrl,
model,
pack,
persona,
includeBase: true,
messages,
context_json: JSON.stringify(context),
@@ -2557,6 +3091,7 @@
messages,
context_json: JSON.stringify(context),
pack,
persona,
base_url: baseUrl,
model,
},
@@ -2815,6 +3350,7 @@
}
window.__swarmAssistentWired = true;
loadSettings();
setView(state.view || 'chat');
updateGate();
ensureBoard();
renderBoard();
@@ -2824,11 +3360,26 @@
}
maybeWelcome();
refreshModels();
refreshPersonas();
refreshInventory();
wireDropZone();
wireSplitter();
registerSendButton();
$('sa_tab_chat')?.addEventListener('click', () => setView('chat'));
$('sa_tab_cards')?.addEventListener('click', () => setView('cards'));
$('sa_persona')?.addEventListener('change', saveSettings);
$('sa_cards_kind')?.addEventListener('change', renderCardsList);
$('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
$('sa_btn_card_meta')?.addEventListener('click', () => {
if (state.cardsSelection) {
selectCardModel(state.cardsSelection);
}
});
$('sa_btn_card_generate')?.addEventListener('click', () => generateCardWithAssistent());
$('sa_btn_card_save')?.addEventListener('click', () => saveCurrentCard());
$('sa_btn_card_wanted')?.addEventListener('click', () => enqueueWantedOnly());
$('sa_btn_settings')?.addEventListener('click', () => {
const s = $('sa_settings');
if (s) {
@@ -2839,7 +3390,7 @@
saveSettings();
refreshModels();
});
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory(null, { rescan: true }));
$('sa_btn_add_ref')?.addEventListener('click', () => addRefSlot({ select: true }));
$('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef());
$('sa_btn_as_init')?.addEventListener('click', async () => {
@@ -2951,9 +3502,11 @@
setInterval(syncGenerateSlot, 700);
setInterval(() => {
if (!state.busy) {
refreshInventory();
const tabOn = !!document.getElementById(TAB_BUTTON_ID)?.classList.contains('tab-button-selected')
|| !!document.getElementById('swarm_assistent_root')?.offsetParent;
refreshInventory(null, { rescan: inventoryIsStale(tabOn ? 45000 : 120000) });
}
}, 120000);
}, 30000);
window.swarmAssistent = {
setImageFromSrc,