Doppel-Klick Hard-Stop + ReadLineAsync Cancellation-Fix

Stop-Button: Zwei Stufen statt einer:
- 1. Klick: Graceful Stop (workCt) - laufende API-Calls/Tools abbrechen
- 2. Klick: Hard Stop (hardCt) - sofortiger Abbruch aus while-Loop

StreamingTracker: Zwei separate CancellationTokenSources (StopCts + HardCts),
Cancel() gibt 'graceful'/'hard'/null zurück je nach Stufe.

ReadLineAsync Cancellation-Fix: StreamReader.ReadLineAsync propagiert den
CancellationToken nicht zuverlässig an den underlying NetworkStream.
Bei extended thinking blockiert ReadLineAsync minutenlang ohne Reaktion
auf Cancellation. Fix: workCt.Register() disposed den HTTP-Stream,
damit ReadLineAsync sofort mit Exception abbricht.

Bug-Fix: OperationCanceledException in RunAgentChat/ChatNonStreaming wurde
vom generischen catch(Exception) geschluckt - Agent lief trotz Stop weiter.
Jetzt: catch(OperationCanceledException) { throw; } vor catch(Exception).

Frontend: Stop-Button zeigt Hard-Stop visuell (rot + Pulse-Animation),
SSE-Stream wird erst beim 2. Klick clientseitig abgebrochen.
This commit is contained in:
Meleki committed 2026-03-10 10:53:23 +01:00
1 parent eb32a2526a
commit b1d449b997
5 files changed
+92 -23

No files matched your search

+12
View File
@@ -440,6 +440,18 @@
background: rgb(from var(--dangerous) r g b / 0.15);
}
.stop-btn.stop-hard {
background: var(--dangerous);
color: var(--primary-text);
animation: pulse-hard 0.6s ease-in-out;
}
@keyframes pulse-hard {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
.input-area textarea {
flex: 1;
min-height: 0;
+18 -3
View File
@@ -170,11 +170,23 @@ async function clearChat() {
showArchiveLoadButton();
}
let _stopClickCount = 0;
function stopStreaming() {
if (_abortController) {
_abortController.abort();
_stopClickCount++;
if (_stopClickCount >= 2) {
// Hard-Stop: Sofortiger Abbruch (Doppel-Klick)
var btn = $('#stop');
if (btn) {
btn.classList.add('stop-hard');
btn.title = 'Hard-Stop gesendet!';
}
// SSE-Stream clientseitig sofort abbrechen
if (_abortController) _abortController.abort();
}
// Backend explizit stoppen (kills laufende Tools/API-Calls)
// Backend explizit stoppen (graceful beim 1. Klick, hard beim 2.)
var sid = getCurrentSessionId();
if (sid) apiStopChat(sid).catch(function() {});
}
@@ -193,6 +205,7 @@ async function _runStream(fetchPromise) {
_abortController = new AbortController();
_currentToolBox = null;
_chatAbandoned = false;
_stopClickCount = 0;
setMetaHtml('<span><i class="fa fa-hourglass-half"></i> Streaming...</span>');
try {
@@ -244,6 +257,8 @@ async function _runStream(fetchPromise) {
sendBtn.disabled = false;
sendBtn.hidden = false;
stopBtn.hidden = true;
stopBtn.classList.remove('stop-hard');
stopBtn.title = 'Antwort abbrechen';
isStreaming = false;
_abortController = null;
}