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

## What's new in 4.0.31

### Improvements

- **RovoDev**: Show the skill name when the agent loads a skill (e.g. "Loading skill: jira") instead of displaying a generic "opened file" message.

### Bug Fixes

- **RovoDev**: Hid stack traces, stderr, and log details from external users while preserving them for Atlassian users.
Expand Down
1 change: 1 addition & 0 deletions src/rovo-dev/client/responseParserInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export type RovoDevToolName =
| 'mcp__scout__invoke_tool'
| 'update_todo'
| 'configure_live_preview'
| 'get_skill'
| RovoDevDeferredToolCallName;

export type RovoDevToolPemissionScenario = 'ASK' | 'ALLOWED' | 'DENIED';
Expand Down
3 changes: 3 additions & 0 deletions src/rovo-dev/ui/common/DialogMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ const friendlyToolName: Record<RovoDevToolName, string> = {
ask_user_questions: 'Ask user questions',
exit_plan_mode: 'Exit plan mode',
configure_live_preview: 'Configure live preview',
get_skill: 'Load skill',
};

const ToolCall: React.FC<{
Expand Down Expand Up @@ -365,6 +366,8 @@ const ToolCallBody: React.FC<{
);
} else if (toolName === 'grep') {
return <code style={{ maxWidth: '100%' }}>{jsonArgs.content_pattern}</code>;
} else if (toolName === 'get_skill') {
return <code style={{ maxWidth: '100%' }}>{jsonArgs.skill_name_or_path}</code>;
} else if (toolName === 'mcp_invoke_tool') {
return (
<table style={{ border: '0' }}>
Expand Down
2 changes: 1 addition & 1 deletion src/rovo-dev/ui/rovoDevView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ const RovoDevView: React.FC = () => {

const last = messages.at(-1);
if (last?.event_kind === 'tool-call') {
setPendingToolCallMessage(parseToolCallMessage(last.tool_name));
setPendingToolCallMessage(parseToolCallMessage(last.tool_name, last.args));
if (last.tool_name === 'invoke_subagents') {
const args = safeJsonParse<{ subagent_names?: string[]; task_names?: string[] }>(last.args);
const subagentNames: string[] = args?.subagent_names || [];
Expand Down
36 changes: 36 additions & 0 deletions src/rovo-dev/ui/tools/ToolCallItem.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,40 @@ describe('ToolCallItem', () => {
const loadingIcon = document.querySelector('.codicon.codicon-loading.codicon-modifier-spin');
expect(loadingIcon).toBeTruthy();
});

it('renders the correct message for get_skill tool without args', () => {
const toolMessage = parseToolCallMessage('get_skill');
const { getByText } = render(
<ToolCallItem toolMessage={toolMessage} currentState={{ state: 'WaitingForPrompt' }} />,
);

validateMessage('Loading skill', toolMessage, getByText);
});

it('renders the correct message for get_skill tool with skill name', () => {
const toolMessage = parseToolCallMessage('get_skill', JSON.stringify({ skill_name_or_path: 'jira' }));
const { getByText } = render(
<ToolCallItem toolMessage={toolMessage} currentState={{ state: 'WaitingForPrompt' }} />,
);

validateMessage('Loading skill: jira', toolMessage, getByText);
});

it('renders the correct message for get_skill tool with skill path', () => {
const toolMessage = parseToolCallMessage('get_skill', JSON.stringify({ skill_name_or_path: 'agentic-search' }));
const { getByText } = render(
<ToolCallItem toolMessage={toolMessage} currentState={{ state: 'WaitingForPrompt' }} />,
);

validateMessage('Loading skill: agentic-search', toolMessage, getByText);
});

it('renders the correct message for get_skill tool with invalid args', () => {
const toolMessage = parseToolCallMessage('get_skill', 'invalid-json');
const { getByText } = render(
<ToolCallItem toolMessage={toolMessage} currentState={{ state: 'WaitingForPrompt' }} />,
);

validateMessage('Loading skill', toolMessage, getByText);
});
});
18 changes: 17 additions & 1 deletion src/rovo-dev/ui/tools/ToolCallItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const ToolCallItem: React.FC<{
);
};

export function parseToolCallMessage(msgToolName: RovoDevToolName): string {
export function parseToolCallMessage(msgToolName: RovoDevToolName, toolArgs?: string): string {
if (!msgToolName) {
return '';
}
Expand Down Expand Up @@ -87,12 +87,28 @@ export function parseToolCallMessage(msgToolName: RovoDevToolName): string {
return 'Updating todo';
case 'configure_live_preview':
return 'Configuring live preview';
case 'get_skill': {
const skillName = extractSkillName(toolArgs);
return skillName ? `Loading skill: ${skillName}` : 'Loading skill';
}
default:
// @ts-expect-error ts(2339) - msgToolName here should be 'never'
return msgToolName.toString();
}
}

function extractSkillName(toolArgs?: string): string | undefined {
if (!toolArgs) {
return undefined;
}
try {
const parsed = JSON.parse(toolArgs);
return parsed?.skill_name_or_path;
} catch {
return undefined;
}
}

function getInitStatusMessage(state: InitializingState | InitializingDownladingState): string {
switch (state.subState) {
case 'Other':
Expand Down
81 changes: 81 additions & 0 deletions src/rovo-dev/ui/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -853,5 +853,86 @@ describe('parseToolReturnMessage', () => {
'Error parsing ToolReturnMessage for tool bash',
);
});

it('should handle get_skill tool with skill name', () => {
const toolCallMessage: RovoDevToolCallResponse = {
event_kind: 'tool-call',
tool_name: 'get_skill',
args: '{"skill_name_or_path": "jira"}',
tool_call_id: 'id1',
};

const msg: RovoDevToolReturnResponse = {
event_kind: 'tool-return',
tool_name: 'get_skill',
content: 'skill content',
tool_call_id: 'id1',
timestamp: '0',
toolCallMessage,
};

const result = parseToolReturnMessage(msg, mockOnError);

expect(result).toHaveLength(1);
expect(result[0]).toEqual({
content: 'Loaded skill: jira',
type: 'open',
});
expect(mockOnError).not.toHaveBeenCalled();
});

it('should handle get_skill tool with skill path', () => {
const toolCallMessage: RovoDevToolCallResponse = {
event_kind: 'tool-call',
tool_name: 'get_skill',
args: '{"skill_name_or_path": "agentic-search"}',
tool_call_id: 'id1',
};

const msg: RovoDevToolReturnResponse = {
event_kind: 'tool-return',
tool_name: 'get_skill',
content: 'skill content',
tool_call_id: 'id1',
timestamp: '0',
toolCallMessage,
};

const result = parseToolReturnMessage(msg, mockOnError);

expect(result).toHaveLength(1);
expect(result[0]).toEqual({
content: 'Loaded skill: agentic-search',
type: 'open',
});
expect(mockOnError).not.toHaveBeenCalled();
});

it('should handle get_skill tool without skill name in args', () => {
const toolCallMessage: RovoDevToolCallResponse = {
event_kind: 'tool-call',
tool_name: 'get_skill',
args: '{}',
tool_call_id: 'id1',
};

const msg: RovoDevToolReturnResponse = {
event_kind: 'tool-return',
tool_name: 'get_skill',
content: 'skill content',
tool_call_id: 'id1',
timestamp: '0',
toolCallMessage,
};

const result = parseToolReturnMessage(msg, mockOnError);

expect(result).toHaveLength(1);
expect(result[0]).toEqual({
content: 'Loaded skill',
type: 'open',
});
expect(mockOnError).not.toHaveBeenCalled();
});
});
});
12 changes: 12 additions & 0 deletions src/rovo-dev/ui/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ interface SubagentArgs {
task_descriptions?: string[];
}

interface SkillArgs {
skill_name_or_path?: string;
}

/**
* Safely parses JSON string or returns the value if it's already an object.
* @param value - The value to parse (string or already parsed object)
Expand Down Expand Up @@ -306,6 +310,14 @@ export function parseToolReturnMessage(
type: 'modify',
});
break;
case 'get_skill':
const skillArgs = safeJsonParse<SkillArgs>(msg.toolCallMessage.args);
const skillName = skillArgs?.skill_name_or_path;
resp.push({
content: skillName ? `Loaded skill: ${skillName}` : 'Loaded skill',
type: 'open',
});
break;
case 'invoke_subagents':
const subagentArgs = safeJsonParse<SubagentArgs>(msg.toolCallMessage.args);
const subagentNames = subagentArgs?.subagent_names || [];
Expand Down
Loading