Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions shell/src/app/chat/chat-cleaner/chat-cleaner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import {TestBed} from '@angular/core/testing';
import {describe, it, expect, beforeEach} from 'vitest';
import {ChatCleaner} from './chat-cleaner';
import {MessageRole} from '../llm-client/llm-client';

describe('ChatCleaner', () => {
let service: ChatCleaner;
Expand Down Expand Up @@ -197,6 +198,73 @@ describe('ChatCleaner', () => {
});
});

describe('isUserLayoutSnapshot', () => {
it('returns true for unprompted USER messages with isSnapshot: true or JSON layout content', () => {
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: '[{"version":"v0.9"}]',
isSnapshot: true,
}),
).toBe(true);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: '[{"version":"v0.9"}]',
}),
).toBe(true);
});

it('returns false for undefined, non-USER, prompted, attachment-bearing, or prose-prefixed messages', () => {
expect(service.isUserLayoutSnapshot(undefined)).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.MODEL,
content: '[{"version":"v0.9"}]',
}),
).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: '[{"version":"v0.9"}]',
promptId: 'p1',
}),
).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: '[{"version":"v0.9"}]',
attachments: [{name: 'img.png', mimeType: 'image/png', data: 'abc'}],
}),
).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: '[{"version":"v0.9"}]',
isSnapshot: false,
}),
).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: 'Fix this: [{"version":"v0.9"}]',
}),
).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: undefined as unknown as string,
}),
).toBe(false);
expect(
service.isUserLayoutSnapshot({
role: MessageRole.USER,
content: null as unknown as string,
}),
).toBe(false);
});
});

describe('Nullish and empty input resilience', () => {
it('returns safe defaults when calling stripPulse on empty or nullish values', () => {
expect(service.stripPulse('')).toBe('');
Expand Down
16 changes: 16 additions & 0 deletions shell/src/app/chat/chat-cleaner/chat-cleaner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import {Injectable} from '@angular/core';
import {tryParseJsonArray} from '../../utils/json';
import {LlmMessage, MessageRole} from '../llm-client/llm-client';

const MD_FENCE_REGEX = /```(?:jsonl?|jsonlines|a2ui|html|xml)?\s*([\s\S]*?)\s*```/gi;
const TAG_REGEX = /<(thought|thinking|reasoning)>([\s\S]*?)(?:<\/\1>|$)/gi;
Expand Down Expand Up @@ -164,4 +165,19 @@ export class ChatCleaner {
const parsedArray = tryParseJsonArray(trimmed);
return parsedArray.success && isLayoutArray(parsedArray.data);
}

/**
* Determines if a message is an unprompted USER canvas layout snapshot
* rather than a human prompt turn or a message with attachments.
*/
isUserLayoutSnapshot(msg?: LlmMessage): boolean {
if (!msg || msg.role !== MessageRole.USER || msg.promptId || msg.attachments?.length) {
return false;
}
if (msg.isSnapshot !== undefined) {
return msg.isSnapshot;
}
const trimmed = typeof msg.content === 'string' ? msg.content.trim() : '';
return (trimmed.startsWith('[') || trimmed.startsWith('{')) && this.isLayoutSnapshot(trimmed);
}
}
Loading
Loading