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>
This commit is contained in:
lefarcen
2026-06-09 03:03:15 +00:00
committed by GitHub
co-authored by audit
parent 992824c594
commit b5bd8e3b35
2 changed files with 156 additions and 3 deletions
+13 -3
View File
@@ -162,6 +162,13 @@ interface Props {
type HomeMentionTab = 'all' | 'files' | 'plugins' | 'skills' | 'mcp' | 'connectors';
// In the combined "All" overview, every surface is capped to a handful of top
// matches so no single section floods the picker. The dedicated "Design files"
// tab is exempt: staged files are the user's own finite content, so that tab
// lists every match (the results panel scrolls) and its count reflects the true
// total rather than the truncated preview.
const HOME_MENTION_ALL_TAB_PREVIEW = 6;
interface HomeMentionOption {
id: string;
icon: IconName;
@@ -295,7 +302,6 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
? stagedFiles
.map((file, index) => ({ file, index }))
.filter(({ file }) => fileMatchesQuery(file, mentionQuery))
.slice(0, 6)
: [],
[mentionActive, mentionQuery, stagedFiles],
);
@@ -329,7 +335,11 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
);
const pickerOpen = mentionActive;
const tabs: Array<{ id: HomeMentionTab; label: string; count: number }> = [
{ id: 'all', label: t('common.all'), count: fileMatches.length + pluginMatches.length + skillMatches.length + mcpMatches.length + connectorMatches.length },
// The All overview previews at most HOME_MENTION_ALL_TAB_PREVIEW files, so
// its badge counts the previewed slice — not the full staged total — to keep
// the count aligned with what that tab actually renders. The dedicated files
// tab below lists every match and reports the true total.
{ id: 'all', label: t('common.all'), count: Math.min(fileMatches.length, HOME_MENTION_ALL_TAB_PREVIEW) + pluginMatches.length + skillMatches.length + mcpMatches.length + connectorMatches.length },
{ id: 'files', label: t('chat.mentionTabFiles'), count: fileMatches.length },
{ id: 'plugins', label: t('entry.navPlugins'), count: pluginMatches.length },
{ id: 'skills', label: t('homeHero.skills'), count: skillMatches.length },
@@ -346,7 +356,7 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
? {
id: 'files',
label: t('chat.mentionSectionFiles'),
options: fileMatches.map(({ file, index }) => ({
options: (mentionTab === 'files' ? fileMatches : fileMatches.slice(0, HOME_MENTION_ALL_TAB_PREVIEW)).map(({ file, index }) => ({
id: `file-${index}-${file.name}`,
icon: isImageFile(file) ? 'image' : 'file',
title: file.name,
@@ -0,0 +1,143 @@
// @vitest-environment jsdom
// Red spec for the Home "@" picker capping staged design files at 6.
//
// Repro: a user stages more than six design files, then opens the context
// picker. On `origin/main` the "Design files" surface ran the staged files
// through `.slice(0, 6)` BEFORE both the tab count and the rendered list, so
// the tab badge read "6" and only six files were ever pickable — the 7th+
// upload was unreachable through the picker. The dedicated Design files tab
// must instead list every staged match and report the true total.
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HomeView } from '../../src/components/HomeView';
import { setHomeHeroPrompt } from '../helpers/home-hero-lexical';
async function settle() {
await act(async () => {
await Promise.resolve();
});
}
function stubContextFetch() {
const fetchMock = vi.fn<typeof fetch>(async (url) => {
if (typeof url === 'string' && url === '/api/plugins') {
return new Response(JSON.stringify({ plugins: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
if (typeof url === 'string' && url === '/api/mcp/servers') {
return new Response(JSON.stringify({ servers: [], templates: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
throw new Error(`unexpected fetch ${url}`);
});
vi.stubGlobal('fetch', fetchMock);
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
cb(0);
return 0;
});
}
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
describe('HomeView design-files mention picker', () => {
it('lists every staged design file and counts the true total when more than six are uploaded', async () => {
stubContextFetch();
const files = Array.from(
{ length: 7 },
(_, i) => new File(['x'], `design-${i + 1}.png`, { type: 'image/png' }),
);
render(
<HomeView
projects={[]}
onSubmit={() => undefined}
onOpenProject={() => undefined}
onViewAllProjects={() => undefined}
/>,
);
const input = await screen.findByTestId('home-hero-input');
// Stage all seven uploads in one paste (Lexical's PastePlugin reads
// `clipboardData.files`).
fireEvent.paste(input, {
clipboardData: {
files,
items: files.map((file) => ({ kind: 'file', getAsFile: () => file })),
},
});
await waitFor(() => expect(screen.getByText('design-1.png')).toBeTruthy());
// Open the context picker with a query that matches every staged file.
setHomeHeroPrompt('@design');
await settle();
// The "Design files" tab badge must report the true number staged (7),
// not the truncated preview (6).
const filesTab = await screen.findByRole('tab', { name: /design files/i });
expect(filesTab.textContent).toContain('7');
// Switching to the dedicated Design files tab must surface every match,
// including the 7th upload that the old `.slice(0, 6)` dropped.
fireEvent.click(filesTab);
await settle();
const picker = screen.getByTestId('home-hero-plugin-picker');
for (let i = 1; i <= 7; i += 1) {
expect(within(picker).getByText(`design-${i}.png`)).toBeTruthy();
}
});
it('keeps the All overview count aligned with its preview-sized render', async () => {
stubContextFetch();
const files = Array.from(
{ length: 7 },
(_, i) => new File(['x'], `design-${i + 1}.png`, { type: 'image/png' }),
);
render(
<HomeView
projects={[]}
onSubmit={() => undefined}
onOpenProject={() => undefined}
onViewAllProjects={() => undefined}
/>,
);
const input = await screen.findByTestId('home-hero-input');
fireEvent.paste(input, {
clipboardData: {
files,
items: files.map((file) => ({ kind: 'file', getAsFile: () => file })),
},
});
await waitFor(() => expect(screen.getByText('design-1.png')).toBeTruthy());
setHomeHeroPrompt('@design');
await settle();
// The default All overview previews only the first six files, so its badge
// must read 6 — not the full 7 — and exactly six file options render. This
// prevents the count/content mismatch from relocating to the All tab.
const allTab = await screen.findByRole('tab', { name: /^all/i });
expect(allTab.textContent).toContain('6');
expect(allTab.textContent).not.toContain('7');
const picker = screen.getByTestId('home-hero-plugin-picker');
const shown = Array.from({ length: 7 }, (_, i) => `design-${i + 1}.png`).filter(
(name) => within(picker).queryByText(name) !== null,
);
expect(shown).toHaveLength(6);
});
});