Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,7 @@ export default function App({ onLogout }: AppProps) {
<FileTreePanel
workspaceAgentId={workspaceAgentId}
onOpenFile={openFile}
onAddToChat={(path, kind) => chatPanelRef.current?.addWorkspacePath(path, kind)}
lastChangedEvent={lastChangedEvent}
revealRequest={revealRequest}
onRemapOpenPaths={remapOpenPaths}
Expand All @@ -942,6 +943,7 @@ export default function App({ onLogout }: AppProps) {
<FileTreePanel
workspaceAgentId={workspaceAgentId}
onOpenFile={openFile}
onAddToChat={(path, kind) => chatPanelRef.current?.addWorkspacePath(path, kind)}
lastChangedEvent={lastChangedEvent}
revealRequest={revealRequest}
onRemapOpenPaths={remapOpenPaths}
Expand Down
6 changes: 5 additions & 1 deletion src/features/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ interface ChatPanelProps {

export interface ChatPanelHandle {
focusInput: () => void;
addWorkspacePath: (path: string, kind: 'file' | 'directory') => Promise<void>;
}

/** Main chat panel with message list, infinite scroll, search, and input bar. */
Expand Down Expand Up @@ -119,7 +120,10 @@ export const ChatPanel = forwardRef<ChatPanelHandle, ChatPanelProps>(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
Expand Down
45 changes: 45 additions & 0 deletions src/features/file-browser/FileTreePanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function createDeferred<T>() {
}

const mockOnOpenFile = vi.fn();
const mockOnAddToChat = vi.fn();
const mockOnRemapOpenPaths = vi.fn();
const mockOnCloseOpenPaths = vi.fn();

Expand Down Expand Up @@ -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(
<FileTreePanel
onOpenFile={mockOnOpenFile}
onAddToChat={mockOnAddToChat}
onRemapOpenPaths={mockOnRemapOpenPaths}
onCloseOpenPaths={mockOnCloseOpenPaths}
collapsed={false}
/>
);

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(
<FileTreePanel
onOpenFile={mockOnOpenFile}
onAddToChat={mockOnAddToChat}
onRemapOpenPaths={mockOnRemapOpenPaths}
onCloseOpenPaths={mockOnCloseOpenPaths}
collapsed={false}
/>
);

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(
Expand Down
20 changes: 18 additions & 2 deletions src/features/file-browser/FileTreePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -58,6 +58,7 @@ export interface FileTreeChangeEvent {
interface FileTreePanelProps {
workspaceAgentId: string;
onOpenFile: (path: string) => void;
onAddToChat?: (path: string, kind: 'file' | 'directory') => Promise<void> | 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. */
Expand Down Expand Up @@ -99,6 +100,7 @@ function isSameScopedSession<T extends ScopedSessionState>(current: T | null, ta
export function FileTreePanel({
workspaceAgentId = 'main',
onOpenFile,
onAddToChat,
onRemapOpenPaths,
onCloseOpenPaths,
lastChangedEvent,
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -791,6 +794,19 @@ export function FileTreePanel({
</button>
)}

{showAddToChat && (
<button
className="w-full px-3 py-1.5 text-left text-xs text-foreground hover:bg-muted/60 flex items-center gap-2"
onClick={() => {
setContextMenu(null);
void onAddToChat?.(menuEntry.path, 'file');
}}
Comment on lines +800 to +803
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle async failures from onAddToChat instead of dropping them.

Line 802 intentionally discards the returned promise. If staging fails, this can become an unhandled rejection and a silent UX failure.

Proposed fix
-              onClick={() => {
-                setContextMenu(null);
-                void onAddToChat?.(menuEntry.path, 'file');
-              }}
+              onClick={async () => {
+                setContextMenu(null);
+                try {
+                  await onAddToChat?.(menuEntry.path, 'file');
+                } catch (err) {
+                  const message = err instanceof Error ? err.message : 'Failed to add file to chat';
+                  showToastForAgent(workspaceAgentId, { type: 'error', message }, 4500);
+                }
+              }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/file-browser/FileTreePanel.tsx` around lines 800 - 803, The
onClick handler currently drops the promise returned by onAddToChat (called in
FileTreePanel's onClick) which can lead to unhandled rejections; wrap the call
to onAddToChat in an async flow and handle failures (either await in an async
function or attach .catch) to surface errors and avoid unhandled rejections —
e.g., call an async wrapper that calls await onAddToChat(menuEntry.path, 'file')
inside try/catch and handle/report the error (e.g., show a toast or log and
restore UI state) after clearing setContextMenu(null).

>
<Paperclip size={12} />
Add to chat
</button>
)}

{showRename && (
<button
className="w-full px-3 py-1.5 text-left text-xs text-foreground hover:bg-muted/60 flex items-center gap-2"
Expand All @@ -811,7 +827,7 @@ export function FileTreePanel({
</button>
)}

{!showRestore && !showRename && !showTrashAction && (
{!showRestore && !showAddToChat && !showRename && !showTrashAction && (
<div className="px-3 py-1.5 text-xs text-muted-foreground">
No actions
</div>
Expand Down
Loading