| ... | ... | @@ -194,13 +194,20 @@ const MARKDOWN_FENCE_PATTERN = /^( *)(`{3,}|~{3,})/; |
| 194 | 194 | // CommonMark blank lines hold only spaces and tabs. Other whitespace, such as |
| 195 | 195 | // a no-break space, is paragraph content. |
| 196 | 196 | const BLANK_LINE_PATTERN = /^[ \t]*$/; |
| 197 | // A bullet or ordered marker followed by whitespace, at any indentation so |
| 198 | // nested items count. The trailing space is required, so a partial `-` or |
| 199 | // `1.` never matches before the model finishes the marker. |
| 200 | const LIST_ITEM_START_PATTERN = /^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]/; |
| 197 | 201 | |
| 198 | 202 | /** |
| 199 | | * Splits buffered assistant text at the last blank line or closing code fence |
| 200 | | * that is not inside an open fenced code block. `ready` is safe to deliver now |
| 201 | | * because the markdown before it will not change shape as more text arrives. |
| 202 | | * `rest` stays buffered until the next boundary or completion. Only fully |
| 203 | | * terminated lines count, so a trailing partial line never leaks. |
| 203 | * Splits buffered assistant text at the last blank line, closing code fence, |
| 204 | * or list item start that is not inside an open fenced code block. `ready` is |
| 205 | * safe to deliver now because the markdown before it will not change shape as |
| 206 | * more text arrives. `rest` stays buffered until the next boundary or |
| 207 | * completion. Only fully terminated lines count, so a trailing partial line |
| 208 | * never leaks; a list item start is the one lookahead that may sit on the |
| 209 | * partial line, since tight lists have no blank lines between items and would |
| 210 | * otherwise land all at once. |
| 204 | 211 | */ |
| 205 | 212 | export function splitBufferedAssistantText(text: string): { ready: string; rest: string } { |
| 206 | 213 | let openFence: { marker: string; indent: number } | null = null; |
| ... | ... | @@ -208,10 +215,15 @@ export function splitBufferedAssistantText(text: string): { ready: string; rest: |
| 208 | 215 | let lineStart = 0; |
| 209 | 216 | for (;;) { |
| 210 | 217 | const newline = text.indexOf("\n", lineStart); |
| 218 | const line = text |
| 219 | .slice(lineStart, newline === -1 ? text.length : newline) |
| 220 | .replace(/[ \t\r]+$/, ""); |
| 221 | if (openFence === null && lineStart > 0 && LIST_ITEM_START_PATTERN.test(line)) { |
| 222 | boundary = lineStart; |
| 223 | } |
| 211 | 224 | if (newline === -1) { |
| 212 | 225 | break; |
| 213 | 226 | } |
| 214 | | const line = text.slice(lineStart, newline).replace(/[ \t\r]+$/, ""); |
| 215 | 227 | const fenceMatch = MARKDOWN_FENCE_PATTERN.exec(line); |
| 216 | 228 | if (fenceMatch) { |
| 217 | 229 | const indent = fenceMatch[1]!.length; |