fix(web): remove design file mentions with chips (#3204)

This commit is contained in:
mehmet turac 2026-05-30 07:50:50 +03:00 committed by GitHub
parent 7a9dcf38d7
commit 259295419a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 141 additions and 0 deletions

View file

@ -1234,6 +1234,7 @@ export const ChatComposer = forwardRef<ChatComposerHandle, Props>(
function removeStaged(p: string) {
setStaged((s) => s.filter((a) => a.path !== p));
setStagedVisualComments((current) => current.filter((attachment) => attachment.screenshotPath !== p));
setDraft((current) => stripInlineMentionToken(current, p));
}
function removeCommentAttachment(id: string) {
@ -2893,6 +2894,14 @@ function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function stripInlineMentionToken(text: string, label: string): string {
const token = inlineMentionToken(label);
return text.replace(
new RegExp(`(^|[\\s([{"'])${escapeRegExp(token)}(?=$|\\s|[.,;:!?)}\\]"'])([^\\S\\r\\n])?`, 'g'),
'$1',
);
}
function loadComposerDraft(key?: string): string | null {
if (!key || typeof window === 'undefined') return null;
try {

View file

@ -328,6 +328,138 @@ describe('ChatComposer context pickers', () => {
expect(screen.getByTestId('chat-composer-mention-overlay').textContent).toContain('@My Export');
});
it('removes the inline design file token when its staged chip is removed', async () => {
renderComposer({
projectFiles: [
{
path: 'designs/landing.html',
name: 'landing.html',
kind: 'html',
mime: 'text/html',
mtime: 1,
size: 128,
},
],
});
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
fireEvent.change(input, {
target: { value: 'Use @landing', selectionStart: 12 },
});
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
fireEvent.click(screen.getByText('designs/landing.html'));
expect(input.value).toBe('Use @designs/landing.html ');
expect(screen.getByTestId('staged-attachments').textContent).toContain('landing.html');
fireEvent.click(screen.getByLabelText('Remove landing.html'));
expect(input.value).toBe('Use ');
expect(screen.queryByTestId('staged-attachments')).toBeNull();
});
it('preserves surrounding draft formatting when removing a design file token', async () => {
renderComposer({
projectFiles: [
{
path: 'designs/landing.html',
name: 'landing.html',
kind: 'html',
mime: 'text/html',
mtime: 1,
size: 128,
},
],
});
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
const draft = 'Plan:\n\n@landing\n\nKeep spacing';
fireEvent.change(input, {
target: { value: draft, selectionStart: 'Plan:\n\n@landing'.length },
});
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
fireEvent.click(screen.getByText('designs/landing.html'));
expect(input.value).toBe('Plan:\n\n@designs/landing.html \n\nKeep spacing');
fireEvent.click(screen.getByLabelText('Remove landing.html'));
expect(input.value).toBe('Plan:\n\n\n\nKeep spacing');
expect(screen.queryByTestId('staged-attachments')).toBeNull();
});
it('removes a design file token when punctuation follows it', async () => {
renderComposer({
projectFiles: [
{
path: 'designs/landing.html',
name: 'landing.html',
kind: 'html',
mime: 'text/html',
mtime: 1,
size: 128,
},
],
});
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
fireEvent.change(input, {
target: { value: 'Use @landing', selectionStart: 12 },
});
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
fireEvent.click(screen.getByText('designs/landing.html'));
fireEvent.change(input, {
target: {
value: 'Use @designs/landing.html, please',
selectionStart: 'Use @designs/landing.html, please'.length,
},
});
fireEvent.click(screen.getByLabelText('Remove landing.html'));
expect(input.value).toBe('Use , please');
expect(screen.queryByTestId('staged-attachments')).toBeNull();
});
it('removes a quoted design file token when its chip is removed', async () => {
renderComposer({
projectFiles: [
{
path: 'designs/landing.html',
name: 'landing.html',
kind: 'html',
mime: 'text/html',
mtime: 1,
size: 128,
},
],
});
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
fireEvent.change(input, {
target: { value: '@landing', selectionStart: 8 },
});
await waitFor(() => expect(screen.getByText('designs/landing.html')).toBeTruthy());
fireEvent.click(screen.getByText('designs/landing.html'));
fireEvent.change(input, {
target: {
value: '"@designs/landing.html"',
selectionStart: '"@designs/landing.html"'.length,
},
});
fireEvent.click(screen.getByLabelText('Remove landing.html'));
expect(input.value).toBe('""');
expect(screen.queryByTestId('staged-attachments')).toBeNull();
});
it('lets the tools panel switch between Official and My plugins', async () => {
renderComposer();
fireEvent.click(screen.getByLabelText('Open CLI and model settings'));