Ship Assistent 0.15.1: keep composer writable during hung Writing streams.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+120
-18
@@ -155,6 +155,10 @@
|
||||
busyStarted: 0,
|
||||
gotDelta: false,
|
||||
busyTimer: null,
|
||||
lastDeltaAt: 0,
|
||||
streamStallTimer: null,
|
||||
turnSettled: false,
|
||||
lastBusyPhaseShown: '',
|
||||
slots: [],
|
||||
selectedSlotId: 'ref1',
|
||||
refSeq: 1,
|
||||
@@ -379,17 +383,22 @@
|
||||
warming: 'warm', parking: 'park', encoding: 'look', generating: 'generate',
|
||||
applying: 'merge', silent_gen: 'generate', refining: 'prep', compressing: 'compress',
|
||||
};
|
||||
activityStep(`phase:${state.busyPhase}`, {
|
||||
kind: phaseKind[state.busyPhase] || 'think',
|
||||
label: text,
|
||||
status: 'running',
|
||||
});
|
||||
// Mark prior phase:* steps done when switching phase
|
||||
const a = getActivity();
|
||||
if (a && Array.isArray(a.steps)) {
|
||||
for (const s of a.steps) {
|
||||
if (s.id.startsWith('phase:') && s.id !== `phase:${state.busyPhase}` && s.status === 'running') {
|
||||
a.done(s.id);
|
||||
if (state.lastBusyPhaseShown !== state.busyPhase) {
|
||||
state.lastBusyPhaseShown = state.busyPhase;
|
||||
if (state.busyPhase === 'streaming' || state.busyPhase === 'refining') {
|
||||
activityDone('think');
|
||||
}
|
||||
activityStep(`phase:${state.busyPhase}`, {
|
||||
kind: phaseKind[state.busyPhase] || 'think',
|
||||
label: text,
|
||||
status: 'running',
|
||||
});
|
||||
const a = getActivity();
|
||||
if (a && Array.isArray(a.steps)) {
|
||||
for (const s of a.steps) {
|
||||
if (s.id.startsWith('phase:') && s.id !== `phase:${state.busyPhase}` && s.status === 'running') {
|
||||
a.done(s.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -414,12 +423,16 @@
|
||||
state.busyPhase = phase || 'thinking';
|
||||
$('swarm_assistent_root')?.classList.add('sa-is-busy');
|
||||
$('sa_composer')?.classList.add('sa-composer-busy');
|
||||
state.lastBusyPhaseShown = '';
|
||||
// Keep composer typed/sendable: Enter or Отправить interrupts the hung stream.
|
||||
const send = $('sa_btn_send');
|
||||
if (send) {
|
||||
send.disabled = true;
|
||||
send.disabled = false;
|
||||
}
|
||||
const input = $('sa_input');
|
||||
if (input) {
|
||||
input.readOnly = false;
|
||||
input.disabled = false;
|
||||
input.classList.add('sa-input-busy');
|
||||
}
|
||||
const bar = $('sa_livebar');
|
||||
@@ -444,8 +457,10 @@
|
||||
clearInterval(state.busyTimer);
|
||||
state.busyTimer = null;
|
||||
}
|
||||
clearStreamStall();
|
||||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||||
state.busyPhase = 'idle';
|
||||
state.lastBusyPhaseShown = '';
|
||||
activityFinish(finalStatus || 'Готово');
|
||||
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
|
||||
$('sa_composer')?.classList.remove('sa-composer-busy');
|
||||
@@ -4807,6 +4822,60 @@
|
||||
return state.chatEpoch;
|
||||
}
|
||||
|
||||
function clearStreamStall() {
|
||||
if (state.streamStallTimer) {
|
||||
clearInterval(state.streamStallTimer);
|
||||
state.streamStallTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** If Ollama never sends `done` after the last token, unlock the composer. */
|
||||
function armStreamStall(chatEpoch, onStall) {
|
||||
clearStreamStall();
|
||||
state.lastDeltaAt = Date.now();
|
||||
state.streamStallTimer = setInterval(() => {
|
||||
if (chatEpoch !== state.chatEpoch) {
|
||||
clearStreamStall();
|
||||
return;
|
||||
}
|
||||
if (!state.gotDelta || !state.busy || state.generating || state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
const waitMs = state.streamFenceDone ? 4000 : 15000;
|
||||
if (Date.now() - (state.lastDeltaAt || 0) < waitMs) {
|
||||
return;
|
||||
}
|
||||
clearStreamStall();
|
||||
const reply = state.streamText
|
||||
|| state.streamEl?.querySelector('.sa-msg-body')?.textContent
|
||||
|| '';
|
||||
try {
|
||||
onStall(String(reply || ''));
|
||||
} catch (e) {
|
||||
console.warn('Assistent stream stall', e);
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
/** Keep a visible partial reply when Stop / new send cuts the stream. */
|
||||
function settlePartialStream() {
|
||||
const text = String(state.streamText || '').trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
|
||||
const pack = $('sa_pack')?.value || defaultPackId();
|
||||
const prose = (typeof extractPatch === 'function' ? (extractPatch(text).prose || text) : text);
|
||||
if (state.streamEl) {
|
||||
finalizeStreamMessage(text, []);
|
||||
} else {
|
||||
appendMessage('assistant', prose);
|
||||
}
|
||||
state.history.push({ role: 'assistant', content: prose, persona, pack });
|
||||
persistHistory();
|
||||
state.turnSettled = true;
|
||||
}
|
||||
|
||||
function clearInFlightUi({ status } = {}) {
|
||||
state.busy = false;
|
||||
state.generating = false;
|
||||
@@ -4828,6 +4897,7 @@
|
||||
|
||||
/** Invalidate in-flight Assistent WS/wait; optionally also interrupt Swarm Generate. */
|
||||
function abortInFlightWork({ status, interruptSwarm = false } = {}) {
|
||||
settlePartialStream();
|
||||
bumpChatEpoch();
|
||||
cancelWaitForNewImage();
|
||||
if (interruptSwarm) {
|
||||
@@ -4843,6 +4913,7 @@
|
||||
}
|
||||
|
||||
function doInterruptNow() {
|
||||
settlePartialStream();
|
||||
bumpChatEpoch();
|
||||
cancelWaitForNewImage();
|
||||
try {
|
||||
@@ -5460,6 +5531,7 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
||||
setAssistantBody(state.streamEl, '', { live: true });
|
||||
}
|
||||
state.gotDelta = true;
|
||||
state.lastDeltaAt = Date.now();
|
||||
state.expectColdLoad = false;
|
||||
if (state.busyPhase !== 'refining') {
|
||||
setBusyPhase('streaming');
|
||||
@@ -5482,6 +5554,9 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
||||
state.streamText = '';
|
||||
state.streamFenceDone = false;
|
||||
if (!el) {
|
||||
if (state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
appendMessage('assistant', fullReply, null, civitaiResults, meta || undefined);
|
||||
return;
|
||||
}
|
||||
@@ -8226,9 +8301,6 @@ if (!(meta && meta.historical)) {
|
||||
async function sendChat(opts = {}) {
|
||||
// Continuations run inside the parent turn, which still holds `busy`
|
||||
// (finishOk only clears it after handleReplySideEffects returns).
|
||||
if ((state.busy || state.generating) && !isContinuationTurn(opts)) {
|
||||
return;
|
||||
}
|
||||
if (isTrainingLocked() && !isContinuationTurn(opts)) {
|
||||
setStatus('Идёт тренировка — чат заблокирован');
|
||||
return;
|
||||
@@ -8238,6 +8310,13 @@ if (!(meta && meta.historical)) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
if (state.generating && !isContinuationTurn(opts)) {
|
||||
setStatus('Идёт Generate — нажми Стоп, потом отправь');
|
||||
return;
|
||||
}
|
||||
if (state.busy && !isContinuationTurn(opts)) {
|
||||
abortInFlightWork({ status: 'Новое сообщение', interruptSwarm: false });
|
||||
}
|
||||
if (!isMachineTurn(opts)) {
|
||||
state.lastUserParamIntent = userTextMentionsParams(text);
|
||||
state.lastUserControlIntent = userTextMentionsControls(text);
|
||||
@@ -8306,6 +8385,7 @@ if (!(meta && meta.historical)) {
|
||||
|
||||
const chatEpoch = bumpChatEpoch();
|
||||
state.busy = true;
|
||||
state.turnSettled = false;
|
||||
// If Krea just ran, expectColdLoad stays true until warm / first token — don't clear it here.
|
||||
state.llmParked = false;
|
||||
setInterruptVisible(true);
|
||||
@@ -8460,6 +8540,11 @@ if (!(meta && meta.historical)) {
|
||||
if (chatEpoch !== state.chatEpoch) {
|
||||
return;
|
||||
}
|
||||
if (state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
state.turnSettled = true;
|
||||
clearStreamStall();
|
||||
if (meta.system_chars != null) {
|
||||
state.lastSystemChars = Number(meta.system_chars) || 0;
|
||||
}
|
||||
@@ -8512,6 +8597,11 @@ if (!(meta && meta.historical)) {
|
||||
if (chatEpoch !== state.chatEpoch) {
|
||||
return;
|
||||
}
|
||||
if (state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
state.turnSettled = true;
|
||||
clearStreamStall();
|
||||
state.busy = false;
|
||||
setInterruptVisible(state.generating);
|
||||
stopBusyUi(msg);
|
||||
@@ -8528,6 +8618,15 @@ if (!(meta && meta.historical)) {
|
||||
|
||||
if (typeof makeWSRequest === 'function') {
|
||||
beginStreamMessage(msgMeta);
|
||||
armStreamStall(chatEpoch, (reply) => {
|
||||
if (state.turnSettled || chatEpoch !== state.chatEpoch) {
|
||||
return;
|
||||
}
|
||||
if (state.streamEl) {
|
||||
finalizeStreamMessage(reply, []);
|
||||
}
|
||||
finishOk(reply, [], {});
|
||||
});
|
||||
makeWSRequest(
|
||||
'AssistentChatWS',
|
||||
payload,
|
||||
@@ -8561,12 +8660,16 @@ if (!(meta && meta.historical)) {
|
||||
}
|
||||
if (data.delta) {
|
||||
appendStreamDelta(data.delta);
|
||||
return;
|
||||
}
|
||||
if (data.done || data.reply != null) {
|
||||
if (state.turnSettled) {
|
||||
return;
|
||||
}
|
||||
const reply = data.reply || (state.streamEl?.querySelector('.sa-msg-body')?.textContent) || '';
|
||||
const civitai = data.civitai_results || [];
|
||||
finalizeStreamMessage(reply, civitai);
|
||||
if (state.streamEl) {
|
||||
finalizeStreamMessage(reply, civitai);
|
||||
}
|
||||
finishOk(reply, civitai, {
|
||||
system_chars: data.system_chars,
|
||||
system_layers: data.system_layers,
|
||||
@@ -9095,7 +9198,6 @@ if (!(meta && meta.historical)) {
|
||||
$('sa_btn_build_gen')?.addEventListener('click', () => buildCurrentAndGenerate());
|
||||
$('sa_btn_interrupt')?.addEventListener('click', () => {
|
||||
doInterruptNow();
|
||||
clearInFlightUi({ status: 'Прервано' });
|
||||
});
|
||||
$('sa_btn_clear')?.addEventListener('click', () => {
|
||||
if (window.confirm('Очистить весь чат Assistent?')) {
|
||||
|
||||
Reference in New Issue
Block a user