Skip to content

Commit b5bd8e3

Browse files
lefarcenaudit
andauthored
fix(web): show every staged design file in the Home @ picker (#3948)
* fix(web): show every staged design file in the Home @ picker The Home context picker ran staged files through .slice(0, 6) before both the Design files tab count and the rendered list, so uploading more than six design files capped the tab badge at 6 and made the 7th+ upload unreachable through the picker. Drop the cap on the file matches so the dedicated Design files tab lists every staged match (the results panel already scrolls) and its count reflects the true total. The combined All overview still previews the first six files per surface so no single section floods the picker. * fix(web): cap All-overview file count to its preview size Address review: dropping the file slice made the All tab badge count the full staged total while the All panel still previews only the first six files, relocating the count/content mismatch from the Design files tab to the All tab. Cap the All badge's file contribution to the preview size so each tab's count matches what it renders; the dedicated Design files tab keeps the true total. Extend the red spec to assert the All overview count stays aligned with its preview-sized render. --------- Co-authored-by: audit <a@b.c>
1 parent 992824c commit b5bd8e3

2 files changed

Lines changed: 156 additions & 3 deletions

File tree

apps/web/src/components/HomeHero.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,13 @@ interface Props {
162162

163163
type HomeMentionTab = 'all' | 'files' | 'plugins' | 'skills' | 'mcp' | 'connectors';
164164

165+
// In the combined "All" overview, every surface is capped to a handful of top
166+
// matches so no single section floods the picker. The dedicated "Design files"
167+
// tab is exempt: staged files are the user's own finite content, so that tab
168+
// lists every match (the results panel scrolls) and its count reflects the true
169+
// total rather than the truncated preview.
170+
const HOME_MENTION_ALL_TAB_PREVIEW = 6;
171+
165172
interface HomeMentionOption {
166173
id: string;
167174
icon: IconName;
@@ -295,7 +302,6 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
295302
? stagedFiles
296303
.map((file, index) => ({ file, index }))
297304
.filter(({ file }) => fileMatchesQuery(file, mentionQuery))
298-
.slice(0, 6)
299305
: [],
300306
[mentionActive, mentionQuery, stagedFiles],
301307
);
@@ -329,7 +335,11 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
329335
);
330336
const pickerOpen = mentionActive;
331337
const tabs: Array<{ id: HomeMentionTab; label: string; count: number }> = [
332-
{ id: 'all', label: t('common.all'), count: fileMatches.length + pluginMatches.length + skillMatches.length + mcpMatches.length + connectorMatches.length },
338+
// The All overview previews at most HOME_MENTION_ALL_TAB_PREVIEW files, so
339+
// its badge counts the previewed slice — not the full staged total — to keep
340+
// the count aligned with what that tab actually renders. The dedicated files
341+
// tab below lists every match and reports the true total.
342+
{ id: 'all', label: t('common.all'), count: Math.min(fileMatches.length, HOME_MENTION_ALL_TAB_PREVIEW) + pluginMatches.length + skillMatches.length + mcpMatches.length + connectorMatches.length },
333343
{ id: 'files', label: t('chat.mentionTabFiles'), count: fileMatches.length },
334344
{ id: 'plugins', label: t('entry.navPlugins'), count: pluginMatches.length },
335345
{ id: 'skills', label: t('homeHero.skills'), count: skillMatches.length },
@@ -346,7 +356,7 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
346356
? {
347357
id: 'files',
348358
label: t('chat.mentionSectionFiles'),
349-
options: fileMatches.map(({ file, index }) => ({
359+
options: (mentionTab === 'files' ? fileMatches : fileMatches.slice(0, HOME_MENTION_ALL_TAB_PREVIEW)).map(({ file, index }) => ({
350360
id: `file-${index}-${file.name}`,
351361
icon: isImageFile(file) ? 'image' : 'file',
352362
title: file.name,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// @vitest-environment jsdom
2+
3+
// Red spec for the Home "@" picker capping staged design files at 6.
4+
//
5+
// Repro: a user stages more than six design files, then opens the context
6+
// picker. On `origin/main` the "Design files" surface ran the staged files
7+
// through `.slice(0, 6)` BEFORE both the tab count and the rendered list, so
8+
// the tab badge read "6" and only six files were ever pickable — the 7th+
9+
// upload was unreachable through the picker. The dedicated Design files tab
10+
// must instead list every staged match and report the true total.
11+
12+
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
13+
import { afterEach, describe, expect, it, vi } from 'vitest';
14+
import { HomeView } from '../../src/components/HomeView';
15+
import { setHomeHeroPrompt } from '../helpers/home-hero-lexical';
16+
17+
async function settle() {
18+
await act(async () => {
19+
await Promise.resolve();
20+
});
21+
}
22+
23+
function stubContextFetch() {
24+
const fetchMock = vi.fn<typeof fetch>(async (url) => {
25+
if (typeof url === 'string' && url === '/api/plugins') {
26+
return new Response(JSON.stringify({ plugins: [] }), {
27+
status: 200,
28+
headers: { 'content-type': 'application/json' },
29+
});
30+
}
31+
if (typeof url === 'string' && url === '/api/mcp/servers') {
32+
return new Response(JSON.stringify({ servers: [], templates: [] }), {
33+
status: 200,
34+
headers: { 'content-type': 'application/json' },
35+
});
36+
}
37+
throw new Error(`unexpected fetch ${url}`);
38+
});
39+
vi.stubGlobal('fetch', fetchMock);
40+
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
41+
cb(0);
42+
return 0;
43+
});
44+
}
45+
46+
afterEach(() => {
47+
cleanup();
48+
vi.unstubAllGlobals();
49+
});
50+
51+
describe('HomeView design-files mention picker', () => {
52+
it('lists every staged design file and counts the true total when more than six are uploaded', async () => {
53+
stubContextFetch();
54+
55+
const files = Array.from(
56+
{ length: 7 },
57+
(_, i) => new File(['x'], `design-${i + 1}.png`, { type: 'image/png' }),
58+
);
59+
60+
render(
61+
<HomeView
62+
projects={[]}
63+
onSubmit={() => undefined}
64+
onOpenProject={() => undefined}
65+
onViewAllProjects={() => undefined}
66+
/>,
67+
);
68+
69+
const input = await screen.findByTestId('home-hero-input');
70+
// Stage all seven uploads in one paste (Lexical's PastePlugin reads
71+
// `clipboardData.files`).
72+
fireEvent.paste(input, {
73+
clipboardData: {
74+
files,
75+
items: files.map((file) => ({ kind: 'file', getAsFile: () => file })),
76+
},
77+
});
78+
79+
await waitFor(() => expect(screen.getByText('design-1.png')).toBeTruthy());
80+
81+
// Open the context picker with a query that matches every staged file.
82+
setHomeHeroPrompt('@design');
83+
await settle();
84+
85+
// The "Design files" tab badge must report the true number staged (7),
86+
// not the truncated preview (6).
87+
const filesTab = await screen.findByRole('tab', { name: /design files/i });
88+
expect(filesTab.textContent).toContain('7');
89+
90+
// Switching to the dedicated Design files tab must surface every match,
91+
// including the 7th upload that the old `.slice(0, 6)` dropped.
92+
fireEvent.click(filesTab);
93+
await settle();
94+
95+
const picker = screen.getByTestId('home-hero-plugin-picker');
96+
for (let i = 1; i <= 7; i += 1) {
97+
expect(within(picker).getByText(`design-${i}.png`)).toBeTruthy();
98+
}
99+
});
100+
101+
it('keeps the All overview count aligned with its preview-sized render', async () => {
102+
stubContextFetch();
103+
104+
const files = Array.from(
105+
{ length: 7 },
106+
(_, i) => new File(['x'], `design-${i + 1}.png`, { type: 'image/png' }),
107+
);
108+
109+
render(
110+
<HomeView
111+
projects={[]}
112+
onSubmit={() => undefined}
113+
onOpenProject={() => undefined}
114+
onViewAllProjects={() => undefined}
115+
/>,
116+
);
117+
118+
const input = await screen.findByTestId('home-hero-input');
119+
fireEvent.paste(input, {
120+
clipboardData: {
121+
files,
122+
items: files.map((file) => ({ kind: 'file', getAsFile: () => file })),
123+
},
124+
});
125+
await waitFor(() => expect(screen.getByText('design-1.png')).toBeTruthy());
126+
127+
setHomeHeroPrompt('@design');
128+
await settle();
129+
130+
// The default All overview previews only the first six files, so its badge
131+
// must read 6 — not the full 7 — and exactly six file options render. This
132+
// prevents the count/content mismatch from relocating to the All tab.
133+
const allTab = await screen.findByRole('tab', { name: /^all/i });
134+
expect(allTab.textContent).toContain('6');
135+
expect(allTab.textContent).not.toContain('7');
136+
137+
const picker = screen.getByTestId('home-hero-plugin-picker');
138+
const shown = Array.from({ length: 7 }, (_, i) => `design-${i + 1}.png`).filter(
139+
(name) => within(picker).queryByText(name) !== null,
140+
);
141+
expect(shown).toHaveLength(6);
142+
});
143+
});

0 commit comments

Comments
 (0)