Skip to content

Commit 66b47b9

Browse files
sshtomarShubham Singh Tomarpre-commit-ci[bot]
authored
fix: merge streamed agent message chunks to prevent fragmentation (#6835)
When external agents (e.g., Claude Code) stream responses via WebSocket, each chunk was being rendered as a separate content block, resulting in fragmented display with gaps between chunks. This change merges consecutive text blocks into a single block before rendering, providing a continuous, readable message display. Fixes #6834 Solution Added a mergeConsecutiveTextBlocks() helper function in frontend/src/components/chat/acp/blocks.tsx that: 1. Iterates through all content blocks from streamed chunks 2. Concatenates consecutive text blocks into a single unified text block 3. Preserves non-text blocks (images, audio, resources) in their original positions 4. Returns merged content for continuous display Modified AgentMessagesBlock component to use this function before rendering, ensuring streamed text appears as a single, readable message. Technical Details - File modified: frontend/src/components/chat/acp/blocks.tsx - Lines added: 47 (40 for the helper function + 7 for integration) - Algorithm: Simple text accumulation with flush-on-non-text-block logic - Backward compatible: Does not affect existing functionality or other message types Additional Changes The linter auto-fixed 12 other files for code style consistency (naming conventions, type syntax, numeric separators, copyright headers). These are purely formatting changes with no functional impact. Checklist - I have read the https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md. - For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on https://marimo.io/discord?ref=pr, or the community https://github.com/marimo-team/marimo/discussions (Link: #6834) - I have added tests for the changes made. (Note: This is a UI fix for streaming behavior; manual testing performed with local marimo instance) - I have run the code and verified that it works as expected. - Frontend builds successfully (make fe) - Linting and type checking pass (pnpm lint, pnpm typecheck) - Manual testing with local marimo server --- --------- Co-authored-by: Shubham Singh Tomar <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 1525d7a commit 66b47b9

File tree

12 files changed

+72
-26
lines changed

12 files changed

+72
-26
lines changed

frontend/src/components/chat/acp/blocks.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,46 @@ import {
5757
isUserMessages,
5858
} from "./utils";
5959

60+
/**
61+
* Merges consecutive text blocks into a single text block to prevent
62+
* fragmented display when agent messages are streamed in chunks.
63+
*/
64+
function mergeConsecutiveTextBlocks(
65+
contentBlocks: ContentBlock[],
66+
): ContentBlock[] {
67+
if (contentBlocks.length === 0) {
68+
return contentBlocks;
69+
}
70+
71+
const merged: ContentBlock[] = [];
72+
let currentTextBlock: string | null = null;
73+
74+
for (const block of contentBlocks) {
75+
if (block.type === "text") {
76+
// Accumulate text content
77+
if (currentTextBlock === null) {
78+
currentTextBlock = block.text;
79+
} else {
80+
currentTextBlock += block.text;
81+
}
82+
} else {
83+
// If we have accumulated text, flush it before adding non-text block
84+
if (currentTextBlock !== null) {
85+
merged.push({ type: "text", text: currentTextBlock });
86+
currentTextBlock = null;
87+
}
88+
merged.push(block);
89+
}
90+
}
91+
92+
// Flush any remaining text
93+
if (currentTextBlock !== null) {
94+
merged.push({ type: "text", text: currentTextBlock });
95+
}
96+
97+
return merged;
98+
}
99+
60100
export const ErrorBlock = (props: {
61101
data: ErrorNotificationEvent["data"];
62102
onRetry?: () => void;
@@ -321,9 +361,14 @@ export const UserMessagesBlock = (props: { data: UserNotificationEvent[] }) => {
321361
export const AgentMessagesBlock = (props: {
322362
data: AgentNotificationEvent[];
323363
}) => {
364+
// Merge consecutive text chunks to prevent fragmented display
365+
const mergedContent = mergeConsecutiveTextBlocks(
366+
props.data.map((item) => item.content),
367+
);
368+
324369
return (
325370
<div className="flex flex-col gap-2">
326-
<ContentBlocks data={props.data.map((item) => item.content)} />
371+
<ContentBlocks data={mergedContent} />
327372
</div>
328373
);
329374
};

frontend/src/components/editor/chrome/panels/cache-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ const CachePanel = () => {
3838
});
3939
// Request updated cache info after purge
4040
refetch();
41-
} catch (err) {
41+
} catch (error) {
4242
toast({
4343
title: "Error",
4444
description:
45-
err instanceof Error ? err.message : "Failed to purge cache",
45+
error instanceof Error ? error.message : "Failed to purge cache",
4646
variant: "danger",
4747
});
4848
} finally {

frontend/src/components/editor/code/readonly-diff.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* Copyright 2024 Marimo. All rights reserved. */
12
import { unifiedMergeView } from "@codemirror/merge";
23
import { EditorView } from "@codemirror/view";
34
import CodeMirror from "@uiw/react-codemirror";

frontend/src/components/ui/confirmation-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const ConfirmationButton: React.FC<ConfirmationButtonProps> = ({
6767

6868
return (
6969
<AlertDialog open={open} onOpenChange={setOpen}>
70-
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
70+
<AlertDialogTrigger asChild={true}>{children}</AlertDialogTrigger>
7171
<AlertDialogContent>
7272
<AlertDialogHeader>
7373
<AlertDialogTitle>{title}</AlertDialogTitle>

frontend/src/core/ai/context/providers/tables.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ export class TableContextProvider extends AIContextProvider<TableContextItem> {
4343

4444
// Build shape information
4545
const shape = [
46-
num_rows != null ? `${num_rows} rows` : undefined,
47-
num_columns != null ? `${num_columns} columns` : undefined,
46+
num_rows == null ? undefined : `${num_rows} rows`,
47+
num_columns == null ? undefined : `${num_columns} columns`,
4848
]
4949
.filter(Boolean)
5050
.join(", ");

frontend/src/core/codemirror/language/languages/markdown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { parseLatex } from "../embedded/latex";
2626
import { languageMetadataField } from "../metadata";
2727
import type { LanguageAdapter } from "../types";
2828

29-
export interface MarkdownLanguageAdapterMetadata extends MarkdownMetadata {}
29+
export type MarkdownLanguageAdapterMetadata = MarkdownMetadata;
3030

3131
/**
3232
* Language adapter for Markdown.

frontend/src/core/codemirror/language/languages/sql/sql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export class SQLLanguageAdapter
112112
const metadata = result.metadata as SQLLanguageAdapterMetadata;
113113

114114
if (metadata.engine && metadata.engine !== DUCKDB_ENGINE) {
115-
setLatestEngineSelected(metadata.engine as ConnectionName);
115+
setLatestEngineSelected(metadata.engine);
116116
}
117117

118118
return [result.code, result.offset, metadata];

frontend/src/core/codemirror/lsp/federated-lsp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class FederatedLanguageServerClient implements ILanguageServerClient {
3131
params: LSP.PublishDiagnosticsParams;
3232
}) => void,
3333
): () => boolean {
34-
const callbacks: Array<() => boolean> = [];
34+
const callbacks: (() => boolean)[] = [];
3535
for (const client of this.clients) {
3636
callbacks.push(client.onNotification(listener));
3737
}

frontend/src/core/vscode/is-in-vscode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright 2025 Marimo. All rights reserved. */
1+
/* Copyright 2024 Marimo. All rights reserved. */
22

33
/**
44
* Whether the current environment is in the VSCode extension

frontend/src/plugins/impl/plotly/parse-from-template.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface PlotlyTemplateParser {
2525
export function createParser(hovertemplate: string): PlotlyTemplateParser {
2626
// Regular expression to match the pattern key=%{selector}
2727
// Match any characters except = for the key (non-greedy to avoid capturing previous content)
28-
const regex = /([^=<>]+)=%{([^}]+)}/g;
28+
const regex = /([^<=>]+)=%{([^}]+)}/g;
2929

3030
// Create an object to hold the key-selector pairs
3131
const keySelectorPairs: Record<string, string> = {};

0 commit comments

Comments
 (0)