mirror of
https://github.com/zhukunpenglinyutong/jetbrains-cc-gui.git
synced 2026-09-19 01:43:54 +08:00
fix(claude): flush buffered deltas before block-reset clears the throttlers
Frequent mid-response BLOCK_RESET markers (one per content-block edge) were silently discarding deltas still sitting in the 33ms throttle window, so thinking text stopped streaming and only appeared via updateMessages snapshots. Flush both throttlers to the frontend before resetting, with a regression test covering the ordering.
This commit is contained in:
@@ -343,7 +343,12 @@ public class SessionCallbackAdapter implements ClaudeSession.SessionCallback {
|
||||
if (isInactive()) {
|
||||
return;
|
||||
}
|
||||
// Reset throttlers for the new turn's deltas
|
||||
// Flush BEFORE resetting: block boundaries now fire mid-response (one per
|
||||
// content-block edge, not just per tool-loop turn), so deltas buffered in
|
||||
// the throttlers belong to the ending block. reset() alone would silently
|
||||
// drop them and force the frontend to fall back to updateMessages snapshots.
|
||||
contentDeltaThrottler.flushNow();
|
||||
thinkingDeltaThrottler.flushNow();
|
||||
contentDeltaThrottler.reset();
|
||||
thinkingDeltaThrottler.reset();
|
||||
ApplicationManager.getApplication().invokeLater(() -> {
|
||||
|
||||
+72
@@ -1,7 +1,11 @@
|
||||
package com.github.claudecodegui.session;
|
||||
|
||||
import com.intellij.openapi.application.Application;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -176,5 +180,73 @@ public class SessionCallbackAdapterStreamEndTest {
|
||||
assertEquals(1, jsTarget.calls.size());
|
||||
assertEquals("onStreamEnd:77", jsTarget.calls.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: block boundaries now fire mid-response (one per content-block
|
||||
* edge), so deltas still buffered in the delta throttlers when onBlockReset
|
||||
* runs belong to the ending block. The adapter must flush them to the
|
||||
* frontend BEFORE resetting; a bare reset() silently drops the buffered tail
|
||||
* and forces the frontend onto updateMessages snapshot rendering (visible as
|
||||
* thinking text jumping in chunks instead of streaming).
|
||||
*
|
||||
* <p>Exercised through the real SessionCallbackAdapter with test-friendly
|
||||
* collaborators; the throttlers' default constructor flushes synchronously
|
||||
* via flushNow(), so no IntelliJ Application is needed — the headless
|
||||
* invokeLater in onBlockReset happens after the ordering under test.
|
||||
*/
|
||||
@Test
|
||||
public void blockResetFlushesBufferedDeltasBeforeClearing() throws Exception {
|
||||
RecordingJsTarget jsTarget = new RecordingJsTarget();
|
||||
SessionCallbackAdapter adapter = new SessionCallbackAdapter(
|
||||
null,
|
||||
jsTarget,
|
||||
null,
|
||||
() -> true,
|
||||
null
|
||||
);
|
||||
|
||||
// Deltas arrive and sit in the throttlers' 33ms window...
|
||||
adapter.onContentDelta("text-tail");
|
||||
adapter.onThinkingDelta("thinking-tail");
|
||||
|
||||
// onBlockReset dispatches its JS notification via invokeLater, which has
|
||||
// no Application in headless tests. The flush-before-reset ordering under
|
||||
// test completes before that call, so a benign proxy stub suffices.
|
||||
// Not restored afterwards: setApplication(null, ...) is rejected by the
|
||||
// platform's @NotNull contract, and each Gradle test fork owns its JVM.
|
||||
ApplicationManager.setApplication(invokeLaterInlineApplication());
|
||||
adapter.onBlockReset();
|
||||
|
||||
assertTrue(jsTarget.calls.contains("onContentDelta:text-tail"));
|
||||
assertTrue(jsTarget.calls.contains("onThinkingDelta:thinking-tail"));
|
||||
adapter.deactivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Headless Application stub whose invokeLater runs the runnable inline — the
|
||||
* adapter only needs a non-null application for its EDT dispatch.
|
||||
*/
|
||||
private static @NotNull Application invokeLaterInlineApplication() {
|
||||
return (Application) Proxy.newProxyInstance(
|
||||
Application.class.getClassLoader(),
|
||||
new Class<?>[] { Application.class },
|
||||
(proxy, method, args) -> {
|
||||
if ("invokeLater".equals(method.getName()) && args != null && args.length >= 1) {
|
||||
((Runnable) args[0]).run();
|
||||
return null;
|
||||
}
|
||||
if (method.getName().equals("isDispatchThread")) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
// Unimplemented platform calls are irrelevant to this test;
|
||||
// returning defaults keeps the stub minimal.
|
||||
Class<?> type = method.getReturnType();
|
||||
if (type == boolean.class) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user