mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Preserve composer command popovers after drag-drop support
Drag-drop support made the composer panel clip overflow so the stateful slash and file popovers still opened but were hidden above the panel. Restore visible overflow on the drop target while keeping the drop overlay and attachment flow intact, then lock both active and empty composer surfaces with regression coverage. Constraint: Drag-drop attachments must continue using the existing path-only composer pipeline Rejected: Move slash and file menus outside the composer tree | broader portal refactor not needed for this regression Confidence: high Scope-risk: narrow Directive: Do not reintroduce overflow clipping on composer panels without proving bottom-full popovers still render Tested: cd desktop && bun run test -- --run src/components/chat/ChatInput.test.tsx src/pages/EmptySession.test.tsx Tested: cd desktop && bun run lint Tested: Browser smoke at http://127.0.0.1:1421/?serverUrl=http%3A%2F%2F127.0.0.1%3A3456 for / and @src popovers Tested: bun run check:desktop
This commit is contained in:
parent
b6fcd9e422
commit
5c5c5933f0
@ -748,6 +748,42 @@ describe('ChatInput file mentions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps slash and @ popovers outside the drop target clipping context', async () => {
|
||||
mocks.search.mockResolvedValueOnce({
|
||||
currentPath: '/repo',
|
||||
parentPath: null,
|
||||
query: '',
|
||||
entries: [
|
||||
{ name: 'README.md', path: '/repo/README.md', isDirectory: false },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChatInput compact />)
|
||||
|
||||
const panel = screen.getByTestId('chat-input-panel')
|
||||
const input = screen.getByRole('textbox') as HTMLTextAreaElement
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '/',
|
||||
selectionStart: 1,
|
||||
},
|
||||
})
|
||||
expect(await screen.findByText('/mcp')).toBeInTheDocument()
|
||||
expect(panel).toHaveClass('overflow-visible')
|
||||
expect(panel).not.toHaveClass('overflow-hidden')
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '@readme',
|
||||
selectionStart: 7,
|
||||
},
|
||||
})
|
||||
expect(await screen.findByText('README.md')).toBeInTheDocument()
|
||||
expect(panel).toHaveClass('overflow-visible')
|
||||
expect(panel).not.toHaveClass('overflow-hidden')
|
||||
})
|
||||
|
||||
it('uses larger icon-only mobile action buttons for browser H5 access', async () => {
|
||||
viewportMocks.isMobile = true
|
||||
mocks.search.mockResolvedValueOnce({
|
||||
|
||||
@ -809,10 +809,10 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
ref={panelRef}
|
||||
data-testid="chat-input-panel"
|
||||
className={isHeroComposer
|
||||
? `glass-panel relative flex flex-col gap-3 overflow-hidden ${embedLaunchControlsInHero ? 'rounded-xl' : 'rounded-t-xl rounded-b-none'} p-4 transition-colors ${isDragActive ? 'composer-drop-target-active' : ''}`
|
||||
? `glass-panel relative flex flex-col gap-3 overflow-visible ${embedLaunchControlsInHero ? 'rounded-xl' : 'rounded-t-xl rounded-b-none'} p-4 transition-colors ${isDragActive ? 'composer-drop-target-active' : ''}`
|
||||
: compact
|
||||
? `glass-panel relative overflow-hidden p-3 transition-colors ${isMobileComposer ? 'rounded-2xl shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl'} ${isDragActive ? 'composer-drop-target-active' : ''}`
|
||||
: `glass-panel relative overflow-hidden transition-colors ${isMobileComposer ? 'rounded-2xl p-3 shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl p-4'} ${isDragActive ? 'composer-drop-target-active' : ''}`}
|
||||
? `glass-panel relative overflow-visible p-3 transition-colors ${isMobileComposer ? 'rounded-2xl shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl'} ${isDragActive ? 'composer-drop-target-active' : ''}`
|
||||
: `glass-panel relative overflow-visible transition-colors ${isMobileComposer ? 'rounded-2xl p-3 shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl p-4'} ${isDragActive ? 'composer-drop-target-active' : ''}`}
|
||||
{...dragHandlers}
|
||||
>
|
||||
{isDragActive && (
|
||||
|
||||
@ -970,7 +970,7 @@ describe('WorkspacePanel', () => {
|
||||
expect(view.getByTestId('workspace-code').textContent).toContain('const line2300 = 2300')
|
||||
})
|
||||
expect(view.getByRole('button', { name: 'Collapse preview' })).toBeTruthy()
|
||||
})
|
||||
}, 10_000)
|
||||
|
||||
it('renders image previews from workspace files', async () => {
|
||||
await setWorkspaceState((state) => ({
|
||||
|
||||
@ -9,6 +9,8 @@ const mocks = vi.hoisted(() => ({
|
||||
getMessages: vi.fn(),
|
||||
getSlashCommands: vi.fn(),
|
||||
listSkills: vi.fn(),
|
||||
search: vi.fn(),
|
||||
browse: vi.fn(),
|
||||
getTasksForList: vi.fn(),
|
||||
resetTaskList: vi.fn(),
|
||||
wsClearHandlers: vi.fn(),
|
||||
@ -39,6 +41,13 @@ vi.mock('../api/skills', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../api/filesystem', () => ({
|
||||
filesystemApi: {
|
||||
search: mocks.search,
|
||||
browse: mocks.browse,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../api/cliTasks', () => ({
|
||||
cliTasksApi: {
|
||||
getTasksForList: mocks.getTasksForList,
|
||||
@ -191,6 +200,12 @@ describe('EmptySession', () => {
|
||||
mocks.getMessages.mockResolvedValue({ messages: [] })
|
||||
mocks.getSlashCommands.mockResolvedValue({ commands: [] })
|
||||
mocks.listSkills.mockResolvedValue({ skills: [] })
|
||||
mocks.search.mockResolvedValue({
|
||||
currentPath: '/workspace/project',
|
||||
parentPath: null,
|
||||
query: '',
|
||||
entries: [],
|
||||
})
|
||||
mocks.getTasksForList.mockResolvedValue({ tasks: [] })
|
||||
mocks.resetTaskList.mockResolvedValue(undefined)
|
||||
})
|
||||
@ -489,6 +504,42 @@ describe('EmptySession', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps slash and @ popovers visible above the empty-session drop target', async () => {
|
||||
mocks.search.mockResolvedValueOnce({
|
||||
currentPath: '/workspace/project',
|
||||
parentPath: null,
|
||||
query: '',
|
||||
entries: [
|
||||
{ name: 'README.md', path: '/workspace/project/README.md', isDirectory: false },
|
||||
],
|
||||
})
|
||||
|
||||
render(<EmptySession />)
|
||||
|
||||
const panel = screen.getByTestId('empty-session-composer-panel')
|
||||
const input = screen.getByRole('textbox') as HTMLTextAreaElement
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '/',
|
||||
selectionStart: 1,
|
||||
},
|
||||
})
|
||||
expect(await screen.findByText('/mcp')).toBeInTheDocument()
|
||||
expect(panel).toHaveClass('overflow-visible')
|
||||
expect(panel).not.toHaveClass('overflow-hidden')
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '@readme',
|
||||
selectionStart: 7,
|
||||
},
|
||||
})
|
||||
expect(await screen.findByText('README.md')).toBeInTheDocument()
|
||||
expect(panel).toHaveClass('overflow-visible')
|
||||
expect(panel).not.toHaveClass('overflow-hidden')
|
||||
})
|
||||
|
||||
it('starts in a selected non-Git project without showing a repository warning', async () => {
|
||||
mocks.getRepositoryContext.mockResolvedValueOnce(notGitRepositoryContext())
|
||||
|
||||
|
||||
@ -566,7 +566,7 @@ export function EmptySession() {
|
||||
<div
|
||||
ref={panelRef}
|
||||
data-testid="empty-session-composer-panel"
|
||||
className={`glass-panel relative flex flex-col gap-3 overflow-hidden ${
|
||||
className={`glass-panel relative flex flex-col gap-3 overflow-visible ${
|
||||
isMobileComposer ? 'rounded-2xl p-3 shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl p-0'
|
||||
} ${isDragActive ? 'composer-drop-target-active' : ''}`}
|
||||
{...dragHandlers}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user