From 933d97bedcabc43dac2aa384fe6281052bf9cb67 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=B4=E9=9D=A9?= <507639721@qq.com>
Date: Thu, 26 Mar 2026 13:56:50 +0800
Subject: [PATCH] fix(plan-notebook): buffer SSE stream lines to preserve order
---
.../src/main/resources/static/index.html | 61 +++++++++++++------
1 file changed, 43 insertions(+), 18 deletions(-)
diff --git a/agentscope-examples/plan-notebook/src/main/resources/static/index.html b/agentscope-examples/plan-notebook/src/main/resources/static/index.html
index 6a2c48cac..66c744550 100644
--- a/agentscope-examples/plan-notebook/src/main/resources/static/index.html
+++ b/agentscope-examples/plan-notebook/src/main/resources/static/index.html
@@ -1103,34 +1103,59 @@
Subtasks
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
+ // SSE can be split across chunks; keep an incremental buffer so we only
+ // process complete lines like `data: xxx`.
+ let buffer = '';
+
+ const handleData = (data) => {
+ if (data === '[PAUSED]') {
+ isPaused = true;
+ stopRequested = false;
+ controlState = 'continue';
+ updateControlUI();
+ if (!fullText) {
+ fullText = '(Plan updated - Review and edit the plan, then Continue)';
+ }
+ } else {
+ fullText += data;
+ }
+ contentDiv.textContent = fullText;
+ };
while (true) {
const { done, value } = await reader.read();
if (done) break;
- const chunk = decoder.decode(value);
- const lines = chunk.split('\n');
+ const chunk = decoder.decode(value, { stream: true });
+ buffer += chunk;
+
+ const lines = buffer.split('\n');
+ // Keep the last (possibly incomplete) line for the next chunk.
+ buffer = lines.pop();
+
for (const line of lines) {
- if (line.startsWith('data:')) {
- const data = line.substring(5).trim();
- if (data) {
- if (data === '[PAUSED]') {
- isPaused = true;
- stopRequested = false;
- controlState = 'continue';
- updateControlUI();
- if (!fullText) {
- fullText = '(Plan updated - Review and edit the plan, then Continue)';
- }
- } else {
- fullText += data;
- }
- contentDiv.textContent = fullText;
- }
+ const lineClean = line.trimEnd();
+ if (!lineClean.startsWith('data:')) continue;
+
+ const data = lineClean.substring(5).trim();
+ if (data) {
+ handleData(data);
}
}
}
+ // Flush any remaining decoded text and process the final buffered line
+ // (in case the stream ends without a trailing newline).
+ const remaining = decoder.decode();
+ if (remaining) buffer += remaining;
+ if (buffer) {
+ const lineClean = buffer.trimEnd();
+ if (lineClean.startsWith('data:')) {
+ const data = lineClean.substring(5).trim();
+ if (data) handleData(data);
+ }
+ }
+
if (!fullText) {
contentDiv.textContent = defaultMessage;
}