| ... | @@ -194,13 +194,20 @@ const MARKDOWN_FENCE_PATTERN = /^( *)(`{3,}|~{3,})/; | ... | @@ -194,13 +194,20 @@ const MARKDOWN_FENCE_PATTERN = /^( *)(`{3,}|~{3,})/; |
| 194 | // CommonMark blank lines hold only spaces and tabs. Other whitespace, such as | 194 | // CommonMark blank lines hold only spaces and tabs. Other whitespace, such as |
| 195 | // a no-break space, is paragraph content. | 195 | // a no-break space, is paragraph content. |
| 196 | const BLANK_LINE_PATTERN = /^[ \t]*$/; | 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 | 203 | * Splits buffered assistant text at the last blank line, closing code fence, |
| 200 | * that is not inside an open fenced code block. `ready` is safe to deliver now | 204 | * or list item start that is not inside an open fenced code block. `ready` is |
| 201 | * because the markdown before it will not change shape as more text arrives. | 205 | * safe to deliver now because the markdown before it will not change shape as |
| 202 | * `rest` stays buffered until the next boundary or completion. Only fully | 206 | * more text arrives. `rest` stays buffered until the next boundary or |
| 203 | * terminated lines count, so a trailing partial line never leaks. | 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 | export function splitBufferedAssistantText(text: string): { ready: string; rest: string } { | 212 | export function splitBufferedAssistantText(text: string): { ready: string; rest: string } { |
| 206 | let openFence: { marker: string; indent: number } | null = null; | 213 | let openFence: { marker: string; indent: number } | null = null; |
| ... | @@ -208,10 +215,15 @@ export function splitBufferedAssistantText(text: string): { ready: string; rest: | ... | @@ -208,10 +215,15 @@ export function splitBufferedAssistantText(text: string): { ready: string; rest: |
| 208 | let lineStart = 0; | 215 | let lineStart = 0; |
| 209 | for (;;) { | 216 | for (;;) { |
| 210 | const newline = text.indexOf("\n", lineStart); | 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 | if (newline === -1) { | 224 | if (newline === -1) { |
| 212 | break; | 225 | break; |
| 213 | } | 226 | } |
| 214 | const line = text.slice(lineStart, newline).replace(/[ \t\r]+$/, ""); | | |
| 215 | const fenceMatch = MARKDOWN_FENCE_PATTERN.exec(line); | 227 | const fenceMatch = MARKDOWN_FENCE_PATTERN.exec(line); |
| 216 | if (fenceMatch) { | 228 | if (fenceMatch) { |
| 217 | const indent = fenceMatch[1]!.length; | 229 | const indent = fenceMatch[1]!.length; |