Skip to content

Commit 35e0712

Browse files
bloveclaude
andauthored
fix(chat,langgraph): jank — stable @for tracking + empty-array guard (#174)
- chat-message-list: `track message.id` (was `track $index`) so the @for doesn't tear down DOM when the messages array briefly shrinks/refills during streaming. Loses streaming-md renderer state on every teardown → visible flash + missed tokens. - stream-manager: skip publishing an empty `messages` array during streaming (both messages-event and values-event paths). An empty payload mid-stream shouldn't wipe the UI; this was the upstream cause of the transient empty-array we saw via MutationObserver instrumentation. - Bumps: @ngaf/chat 0.0.8 → 0.0.9, @ngaf/langgraph 0.0.2 → 0.0.3. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ccd2790 commit 35e0712

4 files changed

Lines changed: 10 additions & 4 deletions

File tree

libs/chat/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngaf/chat",
3-
"version": "0.0.8",
3+
"version": "0.0.9",
44
"exports": {
55
".": {
66
"types": "./index.d.ts",

libs/chat/src/lib/primitives/chat-message-list/chat-message-list.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function getMessageType(message: Message): MessageTemplateType {
3939
changeDetection: ChangeDetectionStrategy.OnPush,
4040
styles: [CHAT_HOST_TOKENS, CHAT_MESSAGE_LIST_STYLES],
4141
template: `
42-
@for (message of messages(); track $index) {
42+
@for (message of messages(); track message.id) {
4343
@let template = findTemplate(getMessageType(message));
4444
@if (template) {
4545
<ng-container

libs/langgraph/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngaf/langgraph",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"peerDependencies": {
55
"@ngaf/chat": "*",
66
"@ngaf/licensing": "*",

libs/langgraph/src/lib/internals/stream-manager.bridge.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ export function createStreamManagerBridge<T, ResolvedBag extends BagTemplate = B
322322
// so optimistic human messages and earlier tool messages are preserved.
323323
if (event.type === 'messages/partial' || event.messageMetadata) {
324324
subjects.messages$.next(mergeMessages(subjects.messages$.value, normalized));
325+
} else if (normalized.length === 0) {
326+
// Defensive: skip empty replacements during streaming. An empty
327+
// batch shouldn't tear down the entire UI (causes message DOM
328+
// teardown + streaming renderer reset = visible jank).
325329
} else {
326330
subjects.messages$.next(normalized);
327331
}
@@ -346,7 +350,9 @@ export function createStreamManagerBridge<T, ResolvedBag extends BagTemplate = B
346350
// Also sync messages$ from the values state so the full message
347351
// history (including human messages) is available to consumers.
348352
const stateMessages = (vals as Record<string, unknown>)['messages'];
349-
if (Array.isArray(stateMessages)) {
353+
if (Array.isArray(stateMessages) && stateMessages.length > 0) {
354+
// Defensive: only sync when state carries messages. An empty
355+
// values payload shouldn't wipe the UI mid-stream.
350356
if (options.toMessage) {
351357
subjects.messages$.next(stateMessages.map(options.toMessage));
352358
} else {

0 commit comments

Comments
 (0)