fix: streaming flicker aggressive static promotion (#608)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-20 19:52:46 -08:00
committed by GitHub
parent 3bf43d7eb9
commit 271289a00b
5 changed files with 220 additions and 10 deletions

View File

@@ -1236,6 +1236,11 @@ export default function App({
// Track whether we've already backfilled history (should only happen once)
const hasBackfilledRef = useRef(false);
// Keep buffers in sync with tokenStreamingEnabled state for aggressive static promotion
useEffect(() => {
buffersRef.current.tokenStreamingEnabled = tokenStreamingEnabled;
}, [tokenStreamingEnabled]);
// Cache precomputed diffs from approval dialogs for tool return rendering
// Key: toolCallId or "toolCallId:filePath" for Patch operations
const precomputedDiffsRef = useRef<Map<string, AdvancedDiffSuccess>>(

View File

@@ -16,6 +16,7 @@ type AssistantLine = {
id: string;
text: string;
phase: "streaming" | "finished";
isContinuation?: boolean;
};
/**
@@ -23,7 +24,7 @@ type AssistantLine = {
* This is a direct port from the old letta-code codebase to preserve the exact styling
*
* Features:
* - Left column (2 chars wide) with bullet point marker
* - Left column (2 chars wide) with bullet point marker (unless continuation)
* - Right column with wrapped text content
* - Proper text normalization
* - Support for markdown rendering (when MarkdownDisplay is available)
@@ -37,7 +38,7 @@ export const AssistantMessage = memo(({ line }: { line: AssistantLine }) => {
return (
<Box flexDirection="row">
<Box width={2} flexShrink={0}>
<Text></Text>
<Text>{line.isContinuation ? " " : "●"}</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<MarkdownDisplay text={normalizedText} hangingIndent={0} />

View File

@@ -16,6 +16,7 @@ type ReasoningLine = {
id: string;
text: string;
phase: "streaming" | "finished";
isContinuation?: boolean;
};
/**
@@ -23,7 +24,7 @@ type ReasoningLine = {
* This is a direct port from the old letta-code codebase to preserve the exact styling
*
* Features:
* - Header row with "✻" symbol and "Thinking…" text
* - Header row with "✻" symbol and "Thinking…" text (unless continuation)
* - Reasoning content indented with 2 spaces
* - Full markdown rendering with dimmed colors
* - Proper text normalization
@@ -34,6 +35,20 @@ export const ReasoningMessage = memo(({ line }: { line: ReasoningLine }) => {
const normalizedText = normalize(line.text);
// Continuation lines skip the header, just show content
if (line.isContinuation) {
return (
<Box flexDirection="row">
<Box width={2} flexShrink={0}>
<Text> </Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<MarkdownDisplay text={normalizedText} dimColor={true} />
</Box>
</Box>
);
}
return (
<Box flexDirection="column">
<Box flexDirection="row">

View File

@@ -6,6 +6,7 @@
import type { LettaStreamingResponse } from "@letta-ai/letta-client/resources/agents/messages";
import { INTERRUPTED_BY_USER } from "../../constants";
import { findLastSafeSplitPoint } from "./markdownSplit";
import { isShellTool } from "./toolNameMapping";
// Constants for streaming output
@@ -104,12 +105,14 @@ export type Line =
id: string;
text: string;
phase: "streaming" | "finished";
isContinuation?: boolean; // true for split continuation lines (no header)
}
| {
kind: "assistant";
id: string;
text: string;
phase: "streaming" | "finished";
isContinuation?: boolean; // true for split continuation lines (no bullet)
}
| {
kind: "tool_call";
@@ -174,6 +177,9 @@ export type Buffers = {
reasoningTokens: number;
stepCount: number;
};
// Aggressive static promotion: split streaming content at paragraph boundaries
tokenStreamingEnabled?: boolean;
splitCounters: Map<string, number>; // tracks split count per original otid
};
export function createBuffers(): Buffers {
@@ -194,6 +200,8 @@ export function createBuffers(): Buffers {
reasoningTokens: 0,
stepCount: 0,
},
tokenStreamingEnabled: false,
splitCounters: new Map(),
};
}
@@ -337,6 +345,63 @@ function extractTextPart(v: unknown): string {
return "";
}
/**
* Attempts to split content at a paragraph boundary for aggressive static promotion.
* If split found, creates a committed line for "before" and updates original with "after".
* Returns true if split occurred, false otherwise.
*/
function trySplitContent(
b: Buffers,
id: string,
kind: "assistant" | "reasoning",
newText: string,
): boolean {
if (!b.tokenStreamingEnabled) return false;
const splitPoint = findLastSafeSplitPoint(newText);
if (splitPoint >= newText.length) return false; // No safe split point
const beforeText = newText.substring(0, splitPoint);
const afterText = newText.substring(splitPoint);
// Get or initialize split counter for this original ID
const counter = b.splitCounters.get(id) ?? 0;
b.splitCounters.set(id, counter + 1);
// Create committed line for "before" content
// Only the first split (counter=0) shows the bullet/header; subsequent splits are continuations
const commitId = `${id}-split-${counter}`;
const committedLine = {
kind,
id: commitId,
text: beforeText,
phase: "finished" as const,
isContinuation: counter > 0, // First split shows bullet, subsequent don't
};
b.byId.set(commitId, committedLine);
// Insert committed line BEFORE the original in order array
const originalIndex = b.order.indexOf(id);
if (originalIndex !== -1) {
b.order.splice(originalIndex, 0, commitId);
} else {
// Should not happen, but handle gracefully
b.order.push(commitId);
}
// Update original line with just the "after" content (keep streaming)
// Mark it as a continuation so it doesn't show bullet/header
const originalLine = b.byId.get(id);
if (
originalLine &&
(originalLine.kind === "assistant" || originalLine.kind === "reasoning")
) {
b.byId.set(id, { ...originalLine, text: afterText, isContinuation: true });
}
return true;
}
// Feed one SDK chunk; mutate buffers in place.
export function onChunk(b: Buffers, chunk: LettaStreamingResponse) {
// Skip processing if stream was interrupted mid-turn. handleInterrupt already
@@ -371,11 +436,15 @@ export function onChunk(b: Buffers, chunk: LettaStreamingResponse) {
phase: "streaming",
}));
if (delta) {
// Immutable update: create new object with updated text
const updatedLine = { ...line, text: line.text + delta };
b.byId.set(id, updatedLine);
const newText = line.text + delta;
b.tokenCount += delta.length;
// console.log(`[REASONING] Updated ${id}, phase=${updatedLine.phase}, textLen=${updatedLine.text.length}`);
// Try to split at paragraph boundary (only if streaming enabled)
if (!trySplitContent(b, id, "reasoning", newText)) {
// No split - normal accumulation
b.byId.set(id, { ...line, text: newText });
}
// console.log(`[REASONING] Updated ${id}, textLen=${newText.length}`);
}
break;
}
@@ -395,10 +464,14 @@ export function onChunk(b: Buffers, chunk: LettaStreamingResponse) {
phase: "streaming",
}));
if (delta) {
// Immutable update: create new object with updated text
const updatedLine = { ...line, text: line.text + delta };
b.byId.set(id, updatedLine);
const newText = line.text + delta;
b.tokenCount += delta.length;
// Try to split at paragraph boundary (only if streaming enabled)
if (!trySplitContent(b, id, "assistant", newText)) {
// No split - normal accumulation
b.byId.set(id, { ...line, text: newText });
}
}
break;
}

View File

@@ -0,0 +1,116 @@
// src/cli/helpers/markdownSplit.ts
// Markdown-aware content splitting for aggressive static promotion.
// Ported from Gemini CLI: packages/cli/src/ui/utils/markdownUtilities.ts
/**
* Checks if a given character index is inside a fenced code block (```).
* Counts fence markers before the index - odd count means inside a block.
* Only counts ``` at the start of a line (real markdown fences), not ones
* embedded in code like: content.indexOf("```")
*/
function isIndexInsideCodeBlock(content: string, indexToTest: number): boolean {
let fenceCount = 0;
let searchPos = 0;
while (searchPos < content.length) {
const nextFence = content.indexOf("```", searchPos);
if (nextFence === -1 || nextFence >= indexToTest) {
break;
}
// Only count as fence if at start of content or after a newline
if (nextFence === 0 || content[nextFence - 1] === "\n") {
fenceCount++;
}
searchPos = nextFence + 3;
}
return fenceCount % 2 === 1;
}
/**
* Finds the next fence marker (``` at start of line) starting from pos.
* Returns -1 if not found.
*/
function findNextLineFence(content: string, startPos: number): number {
let pos = startPos;
while (pos < content.length) {
const nextFence = content.indexOf("```", pos);
if (nextFence === -1) return -1;
// Only count as fence if at start of content or after a newline
if (nextFence === 0 || content[nextFence - 1] === "\n") {
return nextFence;
}
pos = nextFence + 3;
}
return -1;
}
/**
* Finds the starting index of the code block that encloses the given index.
* Returns -1 if the index is not inside a code block.
*/
function findEnclosingCodeBlockStart(content: string, index: number): number {
if (!isIndexInsideCodeBlock(content, index)) {
return -1;
}
let currentSearchPos = 0;
while (currentSearchPos < index) {
const blockStartIndex = findNextLineFence(content, currentSearchPos);
if (blockStartIndex === -1 || blockStartIndex >= index) {
break;
}
const blockEndIndex = findNextLineFence(content, blockStartIndex + 3);
if (blockStartIndex < index) {
if (blockEndIndex === -1 || index < blockEndIndex + 3) {
return blockStartIndex;
}
}
if (blockEndIndex === -1) break;
currentSearchPos = blockEndIndex + 3;
}
return -1;
}
// Minimum content length before we consider splitting
// This prevents creating many tiny chunks which causes spacing issues
// Higher value = fewer splits = cleaner output but more content re-rendering
const MIN_SPLIT_LENGTH = 1500;
/**
* Finds the last safe split point in content (paragraph boundary not inside code block).
* Returns content.length if no safe split point found (meaning don't split).
*
* Used for aggressive static promotion during streaming - completed paragraphs
* can be committed to Ink's <Static> component to reduce flicker.
*/
export function findLastSafeSplitPoint(content: string): number {
// Don't split if content is too short - prevents excessive chunking
if (content.length < MIN_SPLIT_LENGTH) {
return content.length;
}
// If end of content is inside a code block, split before that block
const enclosingBlockStart = findEnclosingCodeBlockStart(
content,
content.length,
);
if (enclosingBlockStart !== -1) {
return enclosingBlockStart;
}
// Search for the last double newline (\n\n) not in a code block
let searchStartIndex = content.length;
while (searchStartIndex >= 0) {
const dnlIndex = content.lastIndexOf("\n\n", searchStartIndex);
if (dnlIndex === -1) {
break;
}
const potentialSplitPoint = dnlIndex + 2; // Split AFTER the \n\n
if (!isIndexInsideCodeBlock(content, potentialSplitPoint)) {
return potentialSplitPoint;
}
searchStartIndex = dnlIndex - 1;
}
// No safe split point found - don't split
return content.length;
}