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
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

## What's new in 4.0.28

### Features

- **RovoDev**: Added file picker autocomplete in chat prompt using `#` trigger - users can now type `#` to get autocomplete suggestions for workspace files and automatically attach them as context. Supports searching by filename (e.g., `#analytics`) or folder path (e.g., `#src/` to see all files in src folder)

### Improvements

- **RovoDev**: Generalized MCP tool parsing in chat UI to support any MCP toolset via regex matching (`mcp__<name>__invoke_tool` / `mcp__<name>__get_tool_schema`) instead of hardcoded tool names


## What's new in 4.0.27

### Improvements
Expand Down Expand Up @@ -44,7 +47,6 @@

- **RovoDev**: Added copy code button within the Rovo Dev chat for code blocks in the chat.


## What's new in 4.0.23

### Features
Expand Down
27 changes: 25 additions & 2 deletions src/rovo-dev/rovoDevChatContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ export class RovoDevChatContextProvider {
await this.addContextItem(picked);
}

private async searchWorkspaceFiles(query?: string): Promise<FileContextQuickPickItem[]> {
try {
const pattern = query ?? '**/*';
return await this.fetchFileContextItems(pattern, 100);
} catch (error) {
console.error('Failed to search workspace files:', error);
return [];
}
}

public async handleFetchWorkspaceFiles(query?: string): Promise<{ path: string; name: string }[] | undefined> {
try {
const items = await this.searchWorkspaceFiles(`**/*${query}*`);
return items.map((item) => ({
path: item.relativePath,
name: item.name,
}));
} catch (error) {
console.error('Failed to fetch workspace files:', error);
return undefined;
}
}

public async processDragDropData(dragDropData: string[]) {
// search for a Jira work item
if (dragDropData.find((x) => x.includes('atlascode.views.jira.assignedWorkItemsTreeView'))) {
Expand Down Expand Up @@ -143,7 +166,7 @@ export class RovoDevChatContextProvider {
}

private async selectContextItem(): Promise<RovoDevContextItem | undefined> {
const initItems = await this.fetchFileContextItems('**/*', 100);
const initItems = await this.searchWorkspaceFiles();

if (!initItems.length) {
window.showInformationMessage('No files found in workspace to add as context.');
Expand All @@ -157,7 +180,7 @@ export class RovoDevChatContextProvider {

const debouncedFetch = debounce(async (value: string) => {
contextQuickPick.busy = true;
const filteredItems = await this.fetchFileContextItems(`**/*${value}*`, 100);
const filteredItems = await this.searchWorkspaceFiles(`**/*${value}*`);

contextQuickPick.items = filteredItems;
contextQuickPick.busy = false;
Expand Down
8 changes: 8 additions & 0 deletions src/rovo-dev/rovoDevWebviewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,14 @@ export class RovoDevWebviewProvider extends Disposable implements WebviewViewPro

break;

case RovoDevViewResponseType.FetchWorkspaceFiles:
const workspaceFiles = await this._chatContextprovider.handleFetchWorkspaceFiles(e.query);
await webview.postMessage({
type: RovoDevProviderMessageType.UpdateWorkspaceFiles,
files: workspaceFiles,
});
break;

default:
// @ts-expect-error ts(2339) - e here should be 'never'
this.processError(new Error(`Unknown message type: ${e.type}`));
Expand Down
5 changes: 5 additions & 0 deletions src/rovo-dev/rovoDevWebviewProviderMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const enum RovoDevProviderMessageType {
GetCurrentAgentModeComplete = 'getCurrentAgentModeComplete',
SetAgentModeComplete = 'setAgentModeComplete',
UpdateSavedPrompts = 'updateSavedPrompts',
UpdateWorkspaceFiles = 'updateWorkspaceFiles',
ShowDeferredAskUserQuestions = 'showDeferredAskUserQuestions',
ShowDeferredExitPlanMode = 'showDeferredExitPlanMode',
UpdateAgentModels = 'updateAgentModels',
Expand Down Expand Up @@ -144,6 +145,10 @@ export type RovoDevProviderMessage =
RovoDevProviderMessageType.UpdateSavedPrompts,
{ savedPrompts: { name: string; description: string; content_file: string }[] | undefined }
>
| ReducerAction<
RovoDevProviderMessageType.UpdateWorkspaceFiles,
{ files: { path: string; name: string }[] | undefined }
>
| ReducerAction<
RovoDevProviderMessageType.ShowDeferredAskUserQuestions,
{ toolCallId: string; args: RovoDevAskUserQuestionsToolArgs }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ const editor = {
dispose: jest.fn(),
setValue: jest.fn(),
getValue: jest.fn(),
onDidChangeModelContent: jest.fn(),
onDidChangeModelContent: jest.fn(() => ({
dispose: jest.fn(),
})),
onDidContentSizeChange: jest.fn(),
getContentHeight: jest.fn(() => 100),
getContainerDomNode: jest.fn(() => ({ style: { height: '' } })),
getModel: jest.fn(),
getPosition: jest.fn(),
focus: jest.fn(),
layout: jest.fn(),
updateOptions: jest.fn(),
Expand Down Expand Up @@ -100,7 +103,6 @@ describe('PromptInputBox', () => {

it('calls onSend when Send button is clicked', () => {
jest.spyOn(editor, 'getValue').mockReturnValue('text prompt');
jest.spyOn(editor, 'onDidChangeModelContent').mockImplementation((cb) => cb());
render(<PromptInputBox {...defaultProps} />);
fireEvent.click(screen.getByLabelText('Send prompt'));
expect(defaultProps.onSend).toHaveBeenCalledWith('text prompt');
Expand Down
86 changes: 86 additions & 0 deletions src/rovo-dev/ui/prompt-box/prompt-input/PromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import PromptContextPopup from '../prompt-context-popup/PromptContextPopup';
import { getAgentModeIcon } from '../prompt-settings-popup/AgentModeSection';
import PromptSettingsPopup from '../prompt-settings-popup/PromptSettingsPopup';
import {
createFileCompletionProvider,
createMonacoPromptEditor,
createPromptCompletionProvider,
createSlashCommandProvider,
Expand All @@ -28,6 +29,7 @@ import {
setupAutoResize,
setupMonacoCommands,
setupPromptKeyBindings,
WorkspaceFile,
} from './utils';

type NonDisabledState = Exclude<State, DisabledState>;
Expand All @@ -54,6 +56,7 @@ interface PromptInputBoxProps {
onSend: (text: string) => boolean;
onCancel: () => void;
onAddContext: () => void;
onFileSelected?: (filePath: string) => void;
onCopy: () => void;
handleMcpConfigurationCommand: () => void;
handleMemoryCommand: () => void;
Expand All @@ -63,6 +66,9 @@ interface PromptInputBoxProps {
onPromptTextSet?: () => void;
handleFetchSavedPrompts?: () => Promise<SavedPrompt[]>;
canFetchSavedPrompts?: boolean;
handleFetchWorkspaceFiles?: (query?: string) => Promise<WorkspaceFile[]>;
canFetchWorkspaceFiles?: boolean;
workspacePath?: string;
}

const TextAreaMessages: Record<NonDisabledState['state'], string> = {
Expand Down Expand Up @@ -127,6 +133,7 @@ export const PromptInputBox: React.FC<PromptInputBoxProps> = ({
onSend,
onCancel,
onAddContext,
onFileSelected,
onCopy,
handleMcpConfigurationCommand,
handleMemoryCommand,
Expand All @@ -136,14 +143,39 @@ export const PromptInputBox: React.FC<PromptInputBoxProps> = ({
onPromptTextSet,
handleFetchSavedPrompts,
canFetchSavedPrompts = false,
handleFetchWorkspaceFiles,
canFetchWorkspaceFiles = false,
workspacePath,
}) => {
const [editor, setEditor] = React.useState<ReturnType<typeof createEditor>>(undefined);
const [isEmpty, setIsEmpty] = React.useState(true);
const promptCompletionProviderRef = React.useRef<monaco.IDisposable | null>(null);
const fileCompletionProviderRef = React.useRef<monaco.IDisposable | null>(null);
const contentChangeListenerRef = React.useRef<monaco.IDisposable | null>(null);
const autoResizeRef = React.useRef<monaco.IDisposable | null>(null);
const commandsRef = React.useRef<monaco.IDisposable | null>(null);

const handleFileSelected = React.useCallback(
(filePath: string) => {
if (!editor) {
return;
}

const model = editor.getModel();
if (model) {
const value = editor.getValue();
const match = value.match(/(?:^|\s)#[\w\-\.\/]*$/);
if (match) {
const newValue = value.substring(0, value.length - match[0].length);
editor.setValue(newValue);
}
}

onFileSelected?.(filePath);
},
[editor, onFileSelected],
);

// create the editor only once - use onAddContext hook to retry
React.useEffect(() => setEditor((prev) => prev ?? createEditor(setIsEmpty)), [onAddContext]);

Expand Down Expand Up @@ -200,6 +232,58 @@ export const PromptInputBox: React.FC<PromptInputBoxProps> = ({
};
}, [canFetchSavedPrompts, editor, handleFetchSavedPrompts]);

React.useEffect(() => {
if (!editor) {
return;
}

// Register file completion provider
if (handleFetchWorkspaceFiles) {
if (fileCompletionProviderRef.current) {
fileCompletionProviderRef.current.dispose();
}

fileCompletionProviderRef.current = monaco.languages.registerCompletionItemProvider(
'plaintext',
createFileCompletionProvider(handleFetchWorkspaceFiles, canFetchWorkspaceFiles, handleFileSelected),
);
}

// Re-trigger autocomplete when editing within # context
const contentChangeDisposable = editor.onDidChangeModelContent(() => {
const position = editor.getPosition();
if (!position) {
return;
}

const model = editor.getModel();
if (!model) {
return;
}

const textUntilPosition = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});

if (/(?:^|\s)#[\w\-\.\/]*$/.test(textUntilPosition)) {
editor.trigger('keyboard', 'editor.action.triggerSuggest', {});
}
});

return () => {
if (fileCompletionProviderRef.current) {
fileCompletionProviderRef.current.dispose();
fileCompletionProviderRef.current = null;
}
if (contentChangeDisposable) {
contentChangeDisposable.dispose();
}
};
}, [canFetchWorkspaceFiles, editor, handleFetchWorkspaceFiles, handleFileSelected]);

React.useEffect(() => {
// Remove Monaco's color stylesheet
removeMonacoStyles();
Expand Down Expand Up @@ -232,6 +316,7 @@ export const PromptInputBox: React.FC<PromptInputBoxProps> = ({
handleTriggerFeedbackCommand,
handleSessionCommand,
onYoloModeToggled,
handleFileSelected,
);

return () => {
Expand All @@ -247,6 +332,7 @@ export const PromptInputBox: React.FC<PromptInputBoxProps> = ({
handleTriggerFeedbackCommand,
onYoloModeToggled,
handleSessionCommand,
handleFileSelected,
]);

// Handle setting prompt text from external source
Expand Down
Loading
Loading