diff --git a/src/App.tsx b/src/App.tsx index a04c3cd9..c1f3719c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -916,6 +916,7 @@ export default function App({ onLogout }: AppProps) { chatPanelRef.current?.addWorkspacePath(path, kind)} lastChangedEvent={lastChangedEvent} revealRequest={revealRequest} onRemapOpenPaths={remapOpenPaths} @@ -942,6 +943,7 @@ export default function App({ onLogout }: AppProps) { chatPanelRef.current?.addWorkspacePath(path, kind)} lastChangedEvent={lastChangedEvent} revealRequest={revealRequest} onRemapOpenPaths={remapOpenPaths} diff --git a/src/features/chat/ChatPanel.tsx b/src/features/chat/ChatPanel.tsx index 65e65018..380fb9d1 100644 --- a/src/features/chat/ChatPanel.tsx +++ b/src/features/chat/ChatPanel.tsx @@ -49,6 +49,7 @@ interface ChatPanelProps { export interface ChatPanelHandle { focusInput: () => void; + addWorkspacePath: (path: string, kind: 'file' | 'directory') => Promise; } /** Main chat panel with message list, infinite scroll, search, and input bar. */ @@ -119,7 +120,10 @@ export const ChatPanel = forwardRef(function Ch // Expose focusInput to parent useImperativeHandle(ref, () => ({ - focusInput: () => inputBarRef.current?.focus() + focusInput: () => inputBarRef.current?.focus(), + addWorkspacePath: async (path: string, kind: 'file' | 'directory') => { + await inputBarRef.current?.addWorkspacePath(path, kind); + }, }), []); // Clean up stale messageRefs when messages change diff --git a/src/features/file-browser/FileTreePanel.test.tsx b/src/features/file-browser/FileTreePanel.test.tsx index 5841740d..8e514c8f 100644 --- a/src/features/file-browser/FileTreePanel.test.tsx +++ b/src/features/file-browser/FileTreePanel.test.tsx @@ -53,6 +53,7 @@ function createDeferred() { } const mockOnOpenFile = vi.fn(); +const mockOnAddToChat = vi.fn(); const mockOnRemapOpenPaths = vi.fn(); const mockOnCloseOpenPaths = vi.fn(); @@ -193,6 +194,50 @@ describe('FileTreePanel', () => { }); }); + describe('context menu add to chat', () => { + it('shows "Add to chat" for files and calls the callback', async () => { + render( + + ); + + fireEvent.contextMenu(screen.getByText('package.json'), new MouseEvent('contextmenu', { bubbles: true })); + + await waitFor(() => { + expect(screen.getByText('Add to chat')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Add to chat')); + + await waitFor(() => { + expect(mockOnAddToChat).toHaveBeenCalledWith('package.json', 'file'); + }); + }); + + it('does not show "Add to chat" for directories', async () => { + render( + + ); + + fireEvent.contextMenu(screen.getByText('src'), new MouseEvent('contextmenu', { bubbles: true })); + + await waitFor(() => { + expect(screen.queryByText('Add to chat')).not.toBeInTheDocument(); + }); + }); + }); + describe('context menu for deletion', () => { it('shows "Move to Trash" for default workspace', async () => { render( diff --git a/src/features/file-browser/FileTreePanel.tsx b/src/features/file-browser/FileTreePanel.tsx index e3661147..efcfe40a 100644 --- a/src/features/file-browser/FileTreePanel.tsx +++ b/src/features/file-browser/FileTreePanel.tsx @@ -6,7 +6,7 @@ */ import { useRef, useState, useCallback, useEffect } from 'react'; -import { PanelLeftClose, RefreshCw, Pencil, Trash2, RotateCcw, X } from 'lucide-react'; +import { PanelLeftClose, RefreshCw, Pencil, Trash2, RotateCcw, X, Paperclip } from 'lucide-react'; import { FileTreeNode } from './FileTreeNode'; import { useFileTree } from './hooks/useFileTree'; import { ConfirmDialog } from '../../components/ConfirmDialog'; @@ -58,6 +58,7 @@ export interface FileTreeChangeEvent { interface FileTreePanelProps { workspaceAgentId: string; onOpenFile: (path: string) => void; + onAddToChat?: (path: string, kind: 'file' | 'directory') => Promise | void; onRemapOpenPaths?: (fromPath: string, toPath: string, targetAgentId?: string) => void; onCloseOpenPaths?: (pathPrefix: string, targetAgentId?: string) => void; /** Called externally when a file changes (SSE) — refreshes affected directory. */ @@ -99,6 +100,7 @@ function isSameScopedSession(current: T | null, ta export function FileTreePanel({ workspaceAgentId = 'main', onOpenFile, + onAddToChat, onRemapOpenPaths, onCloseOpenPaths, lastChangedEvent, @@ -667,6 +669,7 @@ export function FileTreePanel({ const menuPath = menuEntry?.path || ''; const menuInTrash = isTrashItemPath(menuPath); const showRestore = menuInTrash; + const showAddToChat = Boolean(onAddToChat && menuEntry?.type === 'file' && !menuPath.startsWith('.trash') && menuPath !== '.trash'); const showRename = Boolean(menuEntry && menuPath !== '.trash'); const showTrashAction = Boolean(menuEntry && !menuPath.startsWith('.trash') && menuPath !== '.trash'); @@ -791,6 +794,19 @@ export function FileTreePanel({ )} + {showAddToChat && ( + + )} + {showRename && (