Ship Assistent 0.15.2: apply closed generate patches immediately, recover stalled streams, and skip repeat greetings when a chat already has replies.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+196
-95
@@ -79,8 +79,8 @@
|
||||
|
||||
let HELP_TEXT = `Slash-команды (без LLM):
|
||||
/help — этот список
|
||||
/new — новый чат (текущий сохранится в Историю)
|
||||
/history — открыть список чатов
|
||||
/new — новый чат (текущий сохранится в списке)
|
||||
/history — открыть или скрыть панель чатов
|
||||
/compress — сжать старые ходы в саммари
|
||||
/debug — сводка UI/Exact (без LLM)
|
||||
/debug ask — то же + короткий ответ модели
|
||||
@@ -96,12 +96,12 @@
|
||||
/inventory — rescan моделей + обновить список LoRA
|
||||
|
||||
Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.
|
||||
При старте всегда новый чат; смена чата в Истории восстанавливает параметры.`;
|
||||
При старте всегда новый чат; смена чата в панели восстанавливает параметры.`;
|
||||
|
||||
let SLASH_COMMANDS = [
|
||||
{ cmd: '/help', hint: 'список команд' },
|
||||
{ cmd: '/new', hint: 'новый чат' },
|
||||
{ cmd: '/history', hint: 'история чатов' },
|
||||
{ cmd: '/history', hint: 'панель чатов' },
|
||||
{ cmd: '/compress', hint: 'сжать старые ходы' },
|
||||
{ cmd: '/debug', hint: 'сводка · ask = с LLM' },
|
||||
{ cmd: '/why', hint: 'debug + пояснение LLM' },
|
||||
@@ -234,7 +234,7 @@
|
||||
|
||||
// ---- Turn lifecycle --------------------------------------------------
|
||||
// A turn is one user utterance. It can fan out into nested LLM hops: Krea
|
||||
// prompt prep, empty-patch retry, vision, auto-critique. They share one
|
||||
// prompt prep, ask:settings/inventory, vision, auto-critique. They share one
|
||||
// budget so a turn always terminates, and the text they carry is written by
|
||||
// the client, not the user — intent heuristics must never read it.
|
||||
|
||||
@@ -1091,9 +1091,18 @@
|
||||
function mergePromptEnRewrite(effective) {
|
||||
const base = state.pendingPromptEnMerge;
|
||||
state.pendingPromptEnMerge = null;
|
||||
if (!base || !effective) {
|
||||
if (!base) {
|
||||
return effective;
|
||||
}
|
||||
if (!effective) {
|
||||
return {
|
||||
...base,
|
||||
generate: true,
|
||||
actions: (Array.isArray(base.actions) && base.actions.length)
|
||||
? base.actions
|
||||
: ['generate'],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
...effective,
|
||||
@@ -1141,6 +1150,22 @@
|
||||
}
|
||||
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
||||
}
|
||||
function userAsksGenerate(text) {
|
||||
if (window.SA && typeof SA.userAsksGenerate === 'function') {
|
||||
return SA.userAsksGenerate(text);
|
||||
}
|
||||
const t = String(text || '').trim();
|
||||
if (!t || userAsksNoGenerate(t)) {
|
||||
return false;
|
||||
}
|
||||
if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
return cyrTokenRe(
|
||||
'сгенер[а-яё]*|нарисуй|нарисуйте|'
|
||||
+ 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
function packWantsVision(pack) {
|
||||
const p = String(pack || '');
|
||||
@@ -1149,12 +1174,20 @@
|
||||
function resolveTurnIntent(patch, userText, opts = {}) {
|
||||
const S = window.SA && window.SA.session;
|
||||
if (S && typeof S.resolveTurnIntent === 'function') {
|
||||
return S.resolveTurnIntent(patch, userText, { vetoFn: userAsksNoGenerate });
|
||||
return S.resolveTurnIntent(patch, userText, {
|
||||
vetoFn: userAsksNoGenerate,
|
||||
askGenerateFn: userAsksGenerate,
|
||||
fromAutoCritique: !!opts.fromAutoCritique,
|
||||
});
|
||||
}
|
||||
// generate:true / actions / user «сгенерируй» when a prompt already exists.
|
||||
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
||||
const modelAsked = !!(patch && (patch.generate === true
|
||||
const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1
|
||||
|| (typeof patch.generate === 'string' && /^(true|1|yes|on)$/i.test(patch.generate))
|
||||
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))));
|
||||
const generate = !vetoed && !opts.fromAutoCritique && modelAsked;
|
||||
const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText)
|
||||
&& !!(modelAsked || String(patch?.prompt || '').trim());
|
||||
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
||||
const hasLook = !!patch
|
||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||
const look = !!(hasLook && !vetoed && !generate);
|
||||
@@ -2435,6 +2468,7 @@
|
||||
genBtn.textContent = 'Сгенерировать';
|
||||
genBtn.addEventListener('click', async () => {
|
||||
if (isGenerateUnavailable()) {
|
||||
setStatus('Generate недоступен — подожди или нажми Стоп');
|
||||
return;
|
||||
}
|
||||
startBusyUi('silent_gen');
|
||||
@@ -3294,7 +3328,7 @@
|
||||
empty.className = 'sa-chat-empty';
|
||||
empty.id = 'sa_chat_empty';
|
||||
empty.innerHTML = emptyHint
|
||||
|| '<div class="sa-chat-empty-title">Новый чат</div><div class="sa-chat-empty-hint">Параметры Generate остаются как сейчас.<br><strong>+</strong> — ещё один чат · <strong>История</strong> — вернуться к прошлому (с его параметрами).</div>';
|
||||
|| '<div class="sa-chat-empty-title">Новый чат</div><div class="sa-chat-empty-hint">Параметры Generate остаются как сейчас.<br><strong>+</strong> — ещё один чат · кнопка панели слева — прошлые диалоги.</div>';
|
||||
box.appendChild(empty);
|
||||
}
|
||||
|
||||
@@ -3357,23 +3391,40 @@
|
||||
}
|
||||
const chat = findChat(state.activeChatId);
|
||||
el.textContent = chat?.title || 'Новый чат';
|
||||
el.title = (chat?.title || 'Новый чат') + ' — клик: История';
|
||||
el.title = chat?.title || 'Новый чат';
|
||||
}
|
||||
|
||||
function chatHasTranscript(c) {
|
||||
return ((c?.messages || []).length > 0) || ((c?.messages_count || 0) > 0);
|
||||
}
|
||||
|
||||
function savedChatsCount() {
|
||||
return (state.chats || []).filter((c) => (c.messages || []).length > 0).length;
|
||||
return (state.chats || []).filter(chatHasTranscript).length;
|
||||
}
|
||||
|
||||
function isBlankActiveChat() {
|
||||
if ((state.history || []).length) {
|
||||
return false;
|
||||
}
|
||||
const chat = findChat(state.activeChatId);
|
||||
return !chat || !chatHasTranscript(chat);
|
||||
}
|
||||
|
||||
function syncHistoryBadge() {
|
||||
const btn = $('sa_btn_chats');
|
||||
if (!btn) {
|
||||
return;
|
||||
const countEl = $('sa_chats_count');
|
||||
const n = (state.chats || []).filter(chatHasTranscript).length;
|
||||
const open = !!state.chatsPanelOpen;
|
||||
if (btn) {
|
||||
btn.title = open
|
||||
? (n ? `Скрыть чаты (${n})` : 'Скрыть чаты')
|
||||
: (n ? `Показать чаты (${n})` : 'Показать чаты');
|
||||
btn.setAttribute('aria-label', btn.title);
|
||||
}
|
||||
if (countEl) {
|
||||
countEl.hidden = n < 1;
|
||||
countEl.textContent = n > 99 ? '99+' : String(n);
|
||||
}
|
||||
const n = savedChatsCount();
|
||||
btn.textContent = n > 0 ? `История (${n})` : 'История';
|
||||
btn.title = n > 0
|
||||
? `Сохранённых чатов: ${n}. Переключение восстанавливает параметры.`
|
||||
: 'История чатов (пока пусто)';
|
||||
}
|
||||
|
||||
function formatChatWhen(ts) {
|
||||
@@ -3450,7 +3501,7 @@
|
||||
let chats = (state.chats || [])
|
||||
.slice()
|
||||
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
|
||||
.filter((c) => (c.messages || []).length > 0 || (c.messages_count || 0) > 0);
|
||||
.filter((c) => c.id === state.activeChatId || chatHasTranscript(c));
|
||||
if (q) {
|
||||
const local = chats.filter((c) => chatMatchesQuery(c, q));
|
||||
const seen = new Set(local.map((c) => c.id));
|
||||
@@ -3491,6 +3542,7 @@
|
||||
btn?.classList.toggle('sa-sessions-toggle-active', state.chatsPanelOpen);
|
||||
root?.classList.toggle('sa-drawer-open', state.chatsPanelOpen);
|
||||
localStorage.setItem(LS_CHATS_DRAWER, state.chatsPanelOpen ? '1' : '0');
|
||||
syncHistoryBadge();
|
||||
if (state.chatsPanelOpen) {
|
||||
saveActiveChatToStore();
|
||||
const search = $('sa_chats_search');
|
||||
@@ -3503,14 +3555,17 @@
|
||||
saveUiStateToDisk();
|
||||
}
|
||||
|
||||
async function startNewChat({ saveCurrent = true, force = false } = {}) {
|
||||
if (!force && (state.busy || state.generating)) {
|
||||
setStatus('Занято — дождись конца ответа или Стоп');
|
||||
async function startNewChat({ saveCurrent = true, force = false, openDrawer = false } = {}) {
|
||||
if (!force && isBlankActiveChat()) {
|
||||
setStatus('Уже новый чат');
|
||||
if (openDrawer) {
|
||||
setChatsPanelOpen(true);
|
||||
}
|
||||
$('sa_input')?.focus();
|
||||
return;
|
||||
}
|
||||
if (force) {
|
||||
// Drop in-flight reply so it cannot land in the new chat.
|
||||
abortInFlightWork({ status: '' });
|
||||
if (state.busy || state.generating) {
|
||||
abortInFlightWork({ status: '', interruptSwarm: !!state.generating });
|
||||
}
|
||||
if (saveCurrent) {
|
||||
saveActiveChatToStore({ dropEmpty: true });
|
||||
@@ -3549,6 +3604,12 @@
|
||||
updateCtxChip();
|
||||
setStatus('Новый чат — параметры Generate как сейчас');
|
||||
maybeWelcome();
|
||||
if (openDrawer) {
|
||||
setChatsPanelOpen(true);
|
||||
}
|
||||
if (openDrawer || !force) {
|
||||
$('sa_input')?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
async function switchToChat(id) {
|
||||
@@ -3692,7 +3753,7 @@
|
||||
syncBuildGenButton();
|
||||
clearPersistedHistory();
|
||||
resetContextMemory({ persist: false });
|
||||
resetMessagesUi('<div class="sa-chat-empty-title">Чат очищен</div><div class="sa-chat-empty-hint">Сообщения сброшены. Параметры Generate на месте. <strong>+</strong> — новый чат в Историю, <strong>История</strong> — прошлые диалоги.</div>');
|
||||
resetMessagesUi('<div class="sa-chat-empty-title">Чат очищен</div><div class="sa-chat-empty-hint">Сообщения сброшены. Параметры Generate на месте. <strong>+</strong> — новый чат, кнопка панели слева — прошлые диалоги.</div>');
|
||||
setStatus('Чат очищен');
|
||||
updateSessionLabel();
|
||||
syncHistoryBadge();
|
||||
@@ -4841,7 +4902,7 @@
|
||||
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
const waitMs = state.streamFenceDone ? 4000 : 15000;
|
||||
const waitMs = state.streamFenceDone ? 800 : (state.gotDelta ? 2200 : 15000);
|
||||
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
||||
return;
|
||||
}
|
||||
@@ -5477,23 +5538,32 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
||||
return div;
|
||||
}
|
||||
|
||||
function parseTerminalPatchObject(raw) {
|
||||
try {
|
||||
const obj = JSON.parse(String(raw || '').trim());
|
||||
const terminal = (window.SA && typeof SA.isTerminalStreamPatch === 'function')
|
||||
? SA.isTerminalStreamPatch(obj)
|
||||
: (typeof isPatchObject === 'function' && isPatchObject(obj));
|
||||
return terminal ? obj : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function streamHasClosedPatchFence(text) {
|
||||
const t = String(text || '');
|
||||
if (!/```[\s\S]*```/.test(t)) {
|
||||
return false;
|
||||
}
|
||||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
let match;
|
||||
while ((match = re.exec(t)) !== null) {
|
||||
try {
|
||||
const obj = JSON.parse(match[1].trim());
|
||||
const terminal = (window.SA && typeof SA.isTerminalStreamPatch === 'function')
|
||||
? SA.isTerminalStreamPatch(obj)
|
||||
: (typeof isPatchObject === 'function' && isPatchObject(obj));
|
||||
if (terminal) {
|
||||
if (/```[\s\S]*```/.test(t)) {
|
||||
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
let match;
|
||||
while ((match = re.exec(t)) !== null) {
|
||||
if (parseTerminalPatchObject(match[1])) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
const brace = t.lastIndexOf('{');
|
||||
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -5504,17 +5574,18 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
||||
let match;
|
||||
let lastEnd = -1;
|
||||
while ((match = re.exec(t)) !== null) {
|
||||
try {
|
||||
const obj = JSON.parse(match[1].trim());
|
||||
const terminal = (window.SA && typeof SA.isTerminalStreamPatch === 'function')
|
||||
? SA.isTerminalStreamPatch(obj)
|
||||
: (typeof isPatchObject === 'function' && isPatchObject(obj));
|
||||
if (terminal) {
|
||||
lastEnd = match.index + match[0].length;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
if (parseTerminalPatchObject(match[1])) {
|
||||
lastEnd = match.index + match[0].length;
|
||||
}
|
||||
}
|
||||
return lastEnd > 0 ? t.slice(0, lastEnd).trimEnd() : t;
|
||||
if (lastEnd > 0) {
|
||||
return t.slice(0, lastEnd).trimEnd();
|
||||
}
|
||||
const brace = t.lastIndexOf('{');
|
||||
if (brace >= 0 && parseTerminalPatchObject(t.slice(brace))) {
|
||||
return t.trimEnd();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function appendStreamDelta(delta) {
|
||||
@@ -5540,6 +5611,18 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
||||
if (streamHasClosedPatchFence(state.streamText)) {
|
||||
state.streamText = trimToClosedPatchFence(state.streamText);
|
||||
state.streamFenceDone = true;
|
||||
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||||
scrollMessagesToBottom();
|
||||
const fn = state.onClosedTerminalFence;
|
||||
state.onClosedTerminalFence = null;
|
||||
if (typeof fn === 'function') {
|
||||
try {
|
||||
fn(state.streamText);
|
||||
} catch (e) {
|
||||
console.warn('Assistent closed-fence finalize', e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
||||
scrollMessagesToBottom();
|
||||
@@ -5562,19 +5645,12 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
||||
}
|
||||
el.classList.remove('sa-streaming', 'sa-typing');
|
||||
mountAssistantMeta(el, meta || undefined);
|
||||
const { prose, patch } = extractPatch(fullReply);
|
||||
const { prose, patch } = extractPatch(fullReply);
|
||||
setAssistantBody(el, prose || fullReply || '');
|
||||
el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove());
|
||||
if (patch && !(card && !patch.prompt && !patch.actions && !patch.loras)) {
|
||||
if (patch) {
|
||||
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
|
||||
mountPatchBlock(el, patch, { silent });
|
||||
} else if (card) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'sa-patch sa-card-json-preview';
|
||||
const pre = document.createElement('pre');
|
||||
pre.textContent = JSON.stringify(card, null, 2);
|
||||
wrap.appendChild(pre);
|
||||
el.appendChild(wrap);
|
||||
}
|
||||
if (!(meta && meta.historical)) {
|
||||
mountCurateButtons(el, meta);
|
||||
@@ -6754,10 +6830,6 @@ if (!(meta && meta.historical)) {
|
||||
const n = state.inventory.loras.length;
|
||||
const ck = state.inventory.checkpoints.length;
|
||||
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
|
||||
|
||||
if (state.view === 'cards') {
|
||||
|
||||
}
|
||||
if (done) {
|
||||
done(state.inventory);
|
||||
}
|
||||
@@ -7731,7 +7803,8 @@ if (!(meta && meta.historical)) {
|
||||
}
|
||||
const extracted = typeof extractPatch === 'function' ? extractPatch(reply) : { patch: null };
|
||||
let effective = extracted && extracted.patch ? extracted.patch : null;
|
||||
if (opts.fromPromptEnRetry && effective && typeof mergePromptEnRewrite === 'function') {
|
||||
if (opts.fromPromptEnRetry && typeof mergePromptEnRewrite === 'function'
|
||||
&& (effective || state.pendingPromptEnMerge)) {
|
||||
effective = mergePromptEnRewrite(effective);
|
||||
}
|
||||
const S = window.SA && window.SA.session;
|
||||
@@ -7739,11 +7812,12 @@ if (!(meta && meta.historical)) {
|
||||
if (effective && act && typeof act.noteModelCommands === 'function') {
|
||||
act.noteModelCommands(effective);
|
||||
}
|
||||
const promptChanged = !!(effective && String(effective.prompt || '').trim());
|
||||
if (effective && S) {
|
||||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||
activityDone('delta', {
|
||||
kind: 'delta',
|
||||
label: 'Обновил сессию',
|
||||
label: promptChanged ? 'Промпт обновлён' : 'Обновил сессию',
|
||||
detail: Object.keys(effective).filter((k) => effective[k] != null
|
||||
&& !['actions', 'notes'].includes(k)).slice(0, 10).join(', '),
|
||||
});
|
||||
@@ -7759,7 +7833,10 @@ if (!(meta && meta.historical)) {
|
||||
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
|
||||
if (typeof doInterruptNow === 'function') doInterruptNow();
|
||||
}
|
||||
const intent = resolveTurnIntent(effective, opts.userText || '', opts);
|
||||
let intent = resolveTurnIntent(effective, opts.userText || '', opts);
|
||||
if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) {
|
||||
intent = { ...intent, generate: true };
|
||||
}
|
||||
if (effective) {
|
||||
if (intent.generate) {
|
||||
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
||||
@@ -7835,7 +7912,8 @@ if (!(meta && meta.historical)) {
|
||||
}
|
||||
if (intent.generate && effective?.prompt
|
||||
&& typeof promptNeedsKreaPrep === 'function' && promptNeedsKreaPrep(effective.prompt)
|
||||
&& !fromVisionHop && typeof claimTurnHop === 'function' && claimTurnHop('krea_prep')) {
|
||||
&& !fromVisionHop
|
||||
&& typeof claimTurnHop === 'function' && claimTurnHop('krea_prep')) {
|
||||
state.pendingPromptEnMerge = { ...effective };
|
||||
activityStep('prep', {
|
||||
kind: 'prep',
|
||||
@@ -7870,6 +7948,10 @@ if (!(meta && meta.historical)) {
|
||||
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||
await pushSessionToSwarm(state.chatSession);
|
||||
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
||||
if (typeof appendSystemNote === 'function') {
|
||||
appendSystemNote(promptChanged ? 'Промпт обновлён · Generate' : 'Запускаю Generate');
|
||||
}
|
||||
setStatus(promptChanged ? 'Промпт обновлён · Generate' : 'Generate');
|
||||
const srcOut = await runGenerateFromPatch(
|
||||
{ ...(effective || {}), actions: ['generate'], generate: true },
|
||||
{ force: true, fromSession: true },
|
||||
@@ -7879,12 +7961,20 @@ if (!(meta && meta.historical)) {
|
||||
if (typeof maybeAutoCritique === 'function') await maybeAutoCritique(srcOut);
|
||||
if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut);
|
||||
}
|
||||
} else if (effective && $('sa_auto_apply')?.checked) {
|
||||
} else if (effective) {
|
||||
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||
await pushSessionToSwarm(state.chatSession);
|
||||
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
||||
if (promptChanged || $('sa_auto_apply')?.checked) {
|
||||
await pushSessionToSwarm(state.chatSession);
|
||||
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
||||
if (promptChanged && typeof appendSystemNote === 'function') {
|
||||
appendSystemNote('Промпт обновлён');
|
||||
}
|
||||
if (promptChanged) {
|
||||
setStatus('Промпт обновлён');
|
||||
}
|
||||
}
|
||||
if (!state.generating && typeof stopBusyUi === 'function') {
|
||||
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : '');
|
||||
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : (promptChanged ? 'Промпт обновлён' : ''));
|
||||
}
|
||||
}
|
||||
state.pendingSilentGen = false;
|
||||
@@ -8058,12 +8148,12 @@ if (!(meta && meta.historical)) {
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'new' || cmd === 'newchat') {
|
||||
await startNewChat({ saveCurrent: true });
|
||||
await startNewChat({ saveCurrent: true, openDrawer: true });
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'history' || cmd === 'chats' || cmd === 'sessions') {
|
||||
setChatsPanelOpen(true);
|
||||
setStatus('/history');
|
||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||
setStatus(state.chatsPanelOpen ? '/history' : 'Чаты скрыты');
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'compress' || cmd === 'compact' || cmd === 'сжать') {
|
||||
@@ -8501,6 +8591,8 @@ if (!(meta && meta.historical)) {
|
||||
const context = collectLiveContext();
|
||||
// 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.prior_assistant_turns = (state.history || []).filter((m) => m && m.role === 'assistant' && !m.systemish).length;
|
||||
context.do_not_greet = context.prior_assistant_turns > 0;
|
||||
context.has_vision_image = visionReadySlots().length > 0;
|
||||
context.images_in_request = !!(images && images.length);
|
||||
context.attached_slot_ids = attachableSlots().map((s) => s.id);
|
||||
@@ -8544,6 +8636,7 @@ if (!(meta && meta.historical)) {
|
||||
return;
|
||||
}
|
||||
state.turnSettled = true;
|
||||
state.onClosedTerminalFence = null;
|
||||
clearStreamStall();
|
||||
if (meta.system_chars != null) {
|
||||
state.lastSystemChars = Number(meta.system_chars) || 0;
|
||||
@@ -8601,6 +8694,7 @@ if (!(meta && meta.historical)) {
|
||||
return;
|
||||
}
|
||||
state.turnSettled = true;
|
||||
state.onClosedTerminalFence = null;
|
||||
clearStreamStall();
|
||||
state.busy = false;
|
||||
setInterruptVisible(state.generating);
|
||||
@@ -8618,14 +8712,20 @@ if (!(meta && meta.historical)) {
|
||||
|
||||
if (typeof makeWSRequest === 'function') {
|
||||
beginStreamMessage(msgMeta);
|
||||
armStreamStall(chatEpoch, (reply) => {
|
||||
const settleStreamReply = (reply) => {
|
||||
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||||
return;
|
||||
}
|
||||
const raw = String(reply || state.streamText || '').trim()
|
||||
|| (state.streamEl?.querySelector('.sa-msg-body')?.textContent || '');
|
||||
if (state.streamEl) {
|
||||
finalizeStreamMessage(reply, []);
|
||||
finalizeStreamMessage(raw, []);
|
||||
}
|
||||
finishOk(reply, [], {});
|
||||
finishOk(raw, [], {});
|
||||
};
|
||||
state.onClosedTerminalFence = (text) => settleStreamReply(text);
|
||||
armStreamStall(chatEpoch, (reply) => {
|
||||
settleStreamReply(reply);
|
||||
});
|
||||
makeWSRequest(
|
||||
'AssistentChatWS',
|
||||
@@ -8661,12 +8761,14 @@ if (!(meta && meta.historical)) {
|
||||
if (data.delta) {
|
||||
appendStreamDelta(data.delta);
|
||||
}
|
||||
if (data.done || data.reply != null) {
|
||||
if (data.done || (typeof data.reply === 'string' && data.reply.length > 0 && !data.delta)) {
|
||||
if (state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
|
||||
const reply = String(state.streamText || data.reply || '').trim()
|
||||
|| (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
|
||||
const civitai = data.civitai_results || [];
|
||||
state.onClosedTerminalFence = null;
|
||||
if (state.streamEl) {
|
||||
finalizeStreamMessage(reply, civitai);
|
||||
}
|
||||
@@ -8944,7 +9046,14 @@ if (!(meta && meta.historical)) {
|
||||
wireSlashInput();
|
||||
|
||||
|
||||
$('sa_btn_new_chat')?.addEventListener('click', () => startNewChat({ saveCurrent: true }));
|
||||
$('sa_btn_new_chat')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
startNewChat({ saveCurrent: true, openDrawer: true });
|
||||
});
|
||||
$('sa_btn_new_chat_bar')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
startNewChat({ saveCurrent: true, openDrawer: true });
|
||||
});
|
||||
$('sa_ctx_chip')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleCtxPanel();
|
||||
@@ -8979,16 +9088,6 @@ if (!(meta && meta.historical)) {
|
||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||
});
|
||||
$('sa_btn_chats_close')?.addEventListener('click', () => setChatsPanelOpen(false));
|
||||
$('sa_session_label')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||
});
|
||||
$('sa_session_label')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setChatsPanelOpen(!state.chatsPanelOpen);
|
||||
}
|
||||
});
|
||||
$('sa_chats_panel')?.addEventListener('click', (e) => e.stopPropagation());
|
||||
$('sa_chats_list')?.addEventListener('click', (e) => {
|
||||
const row = e.target.closest('.sa-chat-row');
|
||||
@@ -9003,9 +9102,7 @@ if (!(meta && meta.historical)) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('[data-open]')) {
|
||||
switchToChat(id);
|
||||
}
|
||||
switchToChat(id);
|
||||
});
|
||||
let chatsSearchTimer = null;
|
||||
$('sa_chats_search')?.addEventListener('input', () => {
|
||||
@@ -9248,7 +9345,11 @@ if (!(meta && meta.historical)) {
|
||||
});
|
||||
$('sa_chips')?.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.sa-chip');
|
||||
if (!btn || state.busy || state.generating) {
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
if (state.busy || state.generating) {
|
||||
setStatus('Занято — подожди или нажми Стоп');
|
||||
return;
|
||||
}
|
||||
const aspect = btn.getAttribute('data-aspect');
|
||||
|
||||
+34
-5
@@ -1,4 +1,4 @@
|
||||
/** Turn intent — veto only; generate comes from model `generate: true` (or legacy actions). */
|
||||
/** Turn intent — veto + model generate; user «сгенерируй» also counts when a prompt/delta exists. */
|
||||
|
||||
export function cyrTokenRe(alts) {
|
||||
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||
@@ -23,6 +23,20 @@ export function userAsksNoGenerate(text) {
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function userAsksGenerate(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t || userAsksNoGenerate(t)) {
|
||||
return false;
|
||||
}
|
||||
if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) {
|
||||
return true;
|
||||
}
|
||||
return cyrTokenRe(
|
||||
'сгенер[а-яё]*|нарисуй|нарисуйте|'
|
||||
+ 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
||||
).test(t);
|
||||
}
|
||||
|
||||
export function userAsksLook(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) {
|
||||
@@ -56,12 +70,27 @@ export function packWantsVision(pack) {
|
||||
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
|
||||
}
|
||||
|
||||
/** Model generate + explicit veto. No RU imply/command heuristics. */
|
||||
function generateFlagOn(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const g = patch.generate;
|
||||
if (g === true || g === 1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof g === 'string' && /^(true|1|yes|on)$/i.test(g.trim())) {
|
||||
return true;
|
||||
}
|
||||
return Array.isArray(patch.actions) && patch.actions.map(String).includes('generate');
|
||||
}
|
||||
|
||||
/** Model generate flag, or user «сгенерируй» when a prompt/delta already exists. */
|
||||
export function resolveTurnIntent(patch, userText, opts = {}) {
|
||||
const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
|
||||
const modelAsked = patch?.generate === true
|
||||
|| (Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate'));
|
||||
const generate = !vetoed && !opts.fromAutoCritique && !!modelAsked;
|
||||
const modelAsked = generateFlagOn(patch);
|
||||
const userAsked = !opts.machineTurn && userAsksGenerate(userText)
|
||||
&& !!(modelAsked || String(patch?.prompt || '').trim());
|
||||
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
||||
const hasLook = !!patch
|
||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||
const look = !!(hasLook && !vetoed && !generate);
|
||||
|
||||
+66
-18
@@ -2,7 +2,7 @@
|
||||
|
||||
const DEFAULT_PATCH_KEYS = [
|
||||
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'scheduler',
|
||||
'actions', 'generate', 'ask',
|
||||
'actions', 'generate', 'ask', 'checkpoint',
|
||||
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||
'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask',
|
||||
@@ -30,6 +30,22 @@ function has(obj, key) {
|
||||
return obj[key] !== undefined && obj[key] !== null;
|
||||
}
|
||||
|
||||
/** true / "true" / 1 / actions:["generate"] — models sometimes stringify the flag. */
|
||||
export function generateFlagOn(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const g = obj.generate;
|
||||
if (g === true || g === 1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof g === 'string' && /^(true|1|yes|on)$/i.test(g.trim())) {
|
||||
return true;
|
||||
}
|
||||
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
|
||||
return acts.includes('generate');
|
||||
}
|
||||
|
||||
export function isPatchObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
@@ -42,7 +58,7 @@ export function normalizePatch(patch) {
|
||||
return patch;
|
||||
}
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
if (patch.generate === true || acts.includes('generate')) {
|
||||
if (generateFlagOn(patch) || acts.includes('generate')) {
|
||||
patch.generate = true;
|
||||
}
|
||||
if (typeof patch.ask === 'string') {
|
||||
@@ -61,31 +77,65 @@ export function normalizePatch(patch) {
|
||||
return patch;
|
||||
}
|
||||
|
||||
function tryParsePatchJson(raw) {
|
||||
try {
|
||||
const obj = JSON.parse(String(raw || '').trim());
|
||||
if (isPatchObject(obj)) {
|
||||
return normalizePatch(obj);
|
||||
}
|
||||
} catch (e) { /* not json */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractPatch(text) {
|
||||
if (!text) {
|
||||
return { prose: text || '', patch: null };
|
||||
}
|
||||
const re = new RegExp(FENCE_RE.source, 'gi');
|
||||
let match;
|
||||
let lastPatch = null;
|
||||
let prose = text;
|
||||
let lastAny = null;
|
||||
let lastAnyIndex = -1;
|
||||
let lastAnyLen = 0;
|
||||
let lastTerminal = null;
|
||||
let lastTermIndex = -1;
|
||||
let lastTermLen = 0;
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
try {
|
||||
const obj = JSON.parse(match[1].trim());
|
||||
if (isPatchObject(obj)) {
|
||||
lastPatch = normalizePatch(obj);
|
||||
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
|
||||
}
|
||||
} catch (e) { /* not json */ }
|
||||
const parsed = tryParsePatchJson(match[1]);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
lastAny = parsed;
|
||||
lastAnyIndex = match.index;
|
||||
lastAnyLen = match[0].length;
|
||||
if (isTerminalStreamPatch(parsed)) {
|
||||
lastTerminal = parsed;
|
||||
lastTermIndex = match.index;
|
||||
lastTermLen = match[0].length;
|
||||
}
|
||||
}
|
||||
return { prose, patch: lastPatch };
|
||||
const chosen = lastTerminal || lastAny;
|
||||
if (chosen) {
|
||||
const idx = lastTerminal ? lastTermIndex : lastAnyIndex;
|
||||
const len = lastTerminal ? lastTermLen : lastAnyLen;
|
||||
const prose = (text.slice(0, idx) + text.slice(idx + len)).trim();
|
||||
return { prose, patch: chosen };
|
||||
}
|
||||
// Unfenced trailing object — some turns emit raw {prompt, generate:true}.
|
||||
const brace = text.lastIndexOf('{');
|
||||
if (brace >= 0) {
|
||||
const parsed = tryParsePatchJson(text.slice(brace));
|
||||
if (parsed) {
|
||||
return { prose: text.slice(0, brace).trim(), patch: parsed };
|
||||
}
|
||||
}
|
||||
return { prose: text, patch: null };
|
||||
}
|
||||
|
||||
export function isTerminalStreamPatch(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (obj.generate === true) {
|
||||
if (generateFlagOn(obj)) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(obj.ask) && obj.ask.length) {
|
||||
@@ -100,16 +150,13 @@ export function isTerminalStreamPatch(obj) {
|
||||
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
||||
return true;
|
||||
}
|
||||
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
|
||||
if (acts.includes('generate')) {
|
||||
return true;
|
||||
}
|
||||
if (String(obj.prompt || '').trim().length >= 48) {
|
||||
return true;
|
||||
}
|
||||
if (obj.loras != null || obj.aspect != null || obj.steps != null
|
||||
|| obj.width != null || obj.height != null || obj.cfg != null
|
||||
|| obj.seed != null || obj.controls != null) {
|
||||
|| obj.seed != null || obj.controls != null || obj.checkpoint != null
|
||||
|| obj.negative != null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -122,4 +169,5 @@ export function attachPatch(SA) {
|
||||
SA.isTerminalStreamPatch = isTerminalStreamPatch;
|
||||
SA.normalizePatch = normalizePatch;
|
||||
SA.extractPatch = extractPatch;
|
||||
SA.generateFlagOn = generateFlagOn;
|
||||
}
|
||||
|
||||
+27
-18
@@ -59,7 +59,11 @@ export function normalizeDelta(raw) {
|
||||
}
|
||||
const delta = { ...raw };
|
||||
const acts = Array.isArray(delta.actions) ? delta.actions.map(String) : [];
|
||||
if (delta.generate === true || acts.includes('generate')) {
|
||||
const g = delta.generate;
|
||||
const generateOn = g === true || g === 1
|
||||
|| (typeof g === 'string' && /^(true|1|yes|on)$/i.test(g.trim()))
|
||||
|| acts.includes('generate');
|
||||
if (generateOn) {
|
||||
delta.generate = true;
|
||||
}
|
||||
if (typeof delta.ask === 'string') {
|
||||
@@ -77,7 +81,8 @@ export function patchWantsGenerate(patch) {
|
||||
if (!patch || typeof patch !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if (patch.generate === true) {
|
||||
const n = normalizeDelta(patch);
|
||||
if (n?.generate === true) {
|
||||
return true;
|
||||
}
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
@@ -340,10 +345,14 @@ export function fullSettingsDump(session, extras = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
|
||||
/** Model generate/actions, or askGenerateFn when a prompt/delta exists. vetoFn / fromAutoCritique cancel. */
|
||||
export function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) {
|
||||
const delta = normalizeDelta(patch) || {};
|
||||
const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
|
||||
const generate = !vetoed && patchWantsGenerate(delta);
|
||||
const modelAsked = patchWantsGenerate(delta);
|
||||
const userAsked = typeof askGenerateFn === 'function' && !!askGenerateFn(userText)
|
||||
&& !!(modelAsked || String(delta.prompt || '').trim());
|
||||
const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked);
|
||||
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||||
const look = !!(hasLook && !generate && !vetoed);
|
||||
const ask = patchAskList(delta);
|
||||
@@ -390,20 +399,20 @@ export function mergeExactParamsForGenerate(patch, {
|
||||
const defaults = resolveExactProfileDefaults({ exact, profiles, profileName });
|
||||
const out = { ...patch };
|
||||
const clearSessionKeys = [];
|
||||
for (const key of EXACT_GENERATE_PARAM_KEYS) {
|
||||
if (out[key] != null) {
|
||||
// Sparse LLM may echo live leftovers (20/7). Without user intent, force Exact.
|
||||
if (
|
||||
!userParamIntent
|
||||
&& defaults[key] != null
|
||||
&& String(out[key]) !== String(defaults[key])
|
||||
) {
|
||||
out[key] = defaults[key];
|
||||
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
|
||||
clearSessionKeys.push(key);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
for (const key of EXACT_GENERATE_PARAM_KEYS) {
|
||||
if (out[key] != null) {
|
||||
// Sparse LLM may echo live leftovers (20/7). Without user intent, force Exact.
|
||||
if (
|
||||
!userParamIntent
|
||||
&& defaults[key] != null
|
||||
&& String(out[key]) !== String(defaults[key])
|
||||
) {
|
||||
out[key] = defaults[key];
|
||||
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
|
||||
clearSessionKeys.push(key);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (userParamIntent && sessionExact?.[key] != null) {
|
||||
out[key] = sessionExact[key];
|
||||
|
||||
Reference in New Issue
Block a user