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:
1 parent
eb32a2526a
commit
b1d449b997
5 files changed
+92
-23
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user