feat(consult): type out streamed text at a capped pace and write the tail out on settle instead of one frame (T3, BUG-1075)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199rbQDTsUbCVw84wc8BTFe
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
86d99115c6
commit
3f8b817261
@@ -489,7 +489,9 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
if (pendingConsultation.current?.requestId !== requestId) return;
|
||||
pendingConsultation.current = null;
|
||||
if (consultationReplayStarted.current === requestId) consultationReplayStarted.current = null;
|
||||
setStreamingReply(null);
|
||||
// A reply still being written out at the typing pace keeps the display
|
||||
// (BUG-1075); its last paced frame clears it. Everything else is done now.
|
||||
setStreamingReply((current) => (current?.settling ? current : null));
|
||||
setPendingSessionId(null);
|
||||
setPendingRequestId(null);
|
||||
setConsultationPhase(null);
|
||||
@@ -766,6 +768,11 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
let streamedThinking = "";
|
||||
let timelineState = emptyConsultationTimeline();
|
||||
let currentActivity: AgentActivityView | undefined;
|
||||
// True once the stream has ended and the buffer is writing out what is
|
||||
// left at the typing pace (BUG-1075). From then on the display belongs to
|
||||
// this request only while the streaming reply still carries its id: a new
|
||||
// send replaces the reply, and the old buffer's frames fall through.
|
||||
let settleRequested = false;
|
||||
// Every stream event lands in `frames`; it commits at most once per animation
|
||||
// frame and releases text at a steady pace. Nothing below calls
|
||||
// setStreamingReply directly while the response body is being read.
|
||||
@@ -775,7 +782,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
const partialReply = parseAgentReply(frame.answer).text;
|
||||
latestPartialReply = partialReply;
|
||||
thinkingSections = applyThinkingSectionProgress(thinkingSections, partialReply);
|
||||
setStreamingReply({
|
||||
const next: StreamingReply = {
|
||||
sessionId,
|
||||
text: partialReply,
|
||||
responseKind,
|
||||
@@ -783,12 +790,33 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
thinkingSections: thinkingSections.length ? thinkingSections : undefined,
|
||||
timeline: timelineState.rows,
|
||||
activity: currentActivity,
|
||||
});
|
||||
...(settleRequested ? { settling: requestId } : {}),
|
||||
};
|
||||
if (settleRequested) {
|
||||
setStreamingReply((current) => (
|
||||
current?.settling !== requestId ? current : frame.settled ? null : next
|
||||
));
|
||||
} else {
|
||||
setStreamingReply(next);
|
||||
}
|
||||
if (partialReply && pendingConsultation.current?.requestId === requestId) {
|
||||
pendingConsultation.current = { ...pendingConsultation.current, partialReply };
|
||||
}
|
||||
},
|
||||
});
|
||||
// The stream has ended: write out the rest at the typing pace unless the
|
||||
// run was stopped or failed, in which case what arrived shows at once.
|
||||
const settleFrames = (immediate: boolean) => {
|
||||
if (immediate) {
|
||||
void frames.settle({ immediate: true });
|
||||
return;
|
||||
}
|
||||
settleRequested = true;
|
||||
setStreamingReply((current) => (
|
||||
current && current.sessionId === sessionId ? { ...current, settling: requestId } : current
|
||||
));
|
||||
void frames.settle();
|
||||
};
|
||||
try {
|
||||
const response = await fetch("/api/consult", {
|
||||
method: "POST",
|
||||
@@ -949,7 +977,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
parser.push(decoder.decode(value, { stream: true }));
|
||||
}
|
||||
parser.finish(decoder.decode());
|
||||
frames.settle();
|
||||
settleFrames(controller.signal.aborted || Boolean(truncatedFailure));
|
||||
if (truncatedFailure) {
|
||||
const reply = parseAgentReply(answer);
|
||||
if (!reply.text) throw new ConsultationResponseError(502, truncatedFailure.message);
|
||||
@@ -992,7 +1020,7 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
}
|
||||
answer += decoder.decode();
|
||||
frames.setAnswer(answer);
|
||||
frames.settle();
|
||||
settleFrames(controller.signal.aborted);
|
||||
}
|
||||
if (controller.signal.aborted) return Boolean(latestPartialReply);
|
||||
const reply = parseAgentReply(answer);
|
||||
@@ -1043,7 +1071,8 @@ export function useConsultationRun(params: ConsultationRunParams) {
|
||||
} catch (caught) {
|
||||
// Whatever arrived before the failure is what gets kept, not just the
|
||||
// part the pacing had released so far.
|
||||
frames.settle();
|
||||
settleRequested = false;
|
||||
void frames.settle({ immediate: true });
|
||||
const cancelled = controller.signal.aborted;
|
||||
const ownsInterface = pendingConsultation.current?.requestId === requestId;
|
||||
const partialReply = latestPartialReply;
|
||||
|
||||
Reference in New Issue
Block a user