diff --git a/CHANGELOG.md b/CHANGELOG.md index 69337d08a..fadc9777f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/rovo-dev/client/responseParserInterfaces.ts b/src/rovo-dev/client/responseParserInterfaces.ts index 2f2de6a4a..9e7695e99 100644 --- a/src/rovo-dev/client/responseParserInterfaces.ts +++ b/src/rovo-dev/client/responseParserInterfaces.ts @@ -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'; diff --git a/src/rovo-dev/ui/common/DialogMessage.tsx b/src/rovo-dev/ui/common/DialogMessage.tsx index 9d15a28a8..abf595fca 100644 --- a/src/rovo-dev/ui/common/DialogMessage.tsx +++ b/src/rovo-dev/ui/common/DialogMessage.tsx @@ -326,6 +326,7 @@ const friendlyToolName: Record = { 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<{ @@ -365,6 +366,8 @@ const ToolCallBody: React.FC<{ ); } else if (toolName === 'grep') { return {jsonArgs.content_pattern}; + } else if (toolName === 'get_skill') { + return {jsonArgs.skill_name_or_path}; } else if (toolName === 'mcp_invoke_tool') { return ( diff --git a/src/rovo-dev/ui/rovoDevView.tsx b/src/rovo-dev/ui/rovoDevView.tsx index 13aca8717..682f60862 100644 --- a/src/rovo-dev/ui/rovoDevView.tsx +++ b/src/rovo-dev/ui/rovoDevView.tsx @@ -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 || []; diff --git a/src/rovo-dev/ui/tools/ToolCallItem.test.tsx b/src/rovo-dev/ui/tools/ToolCallItem.test.tsx index c6a91e313..aa52ff347 100644 --- a/src/rovo-dev/ui/tools/ToolCallItem.test.tsx +++ b/src/rovo-dev/ui/tools/ToolCallItem.test.tsx @@ -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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + validateMessage('Loading skill', toolMessage, getByText); + }); }); diff --git a/src/rovo-dev/ui/tools/ToolCallItem.tsx b/src/rovo-dev/ui/tools/ToolCallItem.tsx index 998fdd5b1..b3243dd8f 100644 --- a/src/rovo-dev/ui/tools/ToolCallItem.tsx +++ b/src/rovo-dev/ui/tools/ToolCallItem.tsx @@ -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 ''; } @@ -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': diff --git a/src/rovo-dev/ui/utils.test.tsx b/src/rovo-dev/ui/utils.test.tsx index 0c0d5c7e2..7c46a0306 100644 --- a/src/rovo-dev/ui/utils.test.tsx +++ b/src/rovo-dev/ui/utils.test.tsx @@ -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(); + }); }); }); diff --git a/src/rovo-dev/ui/utils.tsx b/src/rovo-dev/ui/utils.tsx index 89c964af6..9ebbcd1f3 100644 --- a/src/rovo-dev/ui/utils.tsx +++ b/src/rovo-dev/ui/utils.tsx @@ -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) @@ -306,6 +310,14 @@ export function parseToolReturnMessage( type: 'modify', }); break; + case 'get_skill': + const skillArgs = safeJsonParse(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(msg.toolCallMessage.args); const subagentNames = subagentArgs?.subagent_names || [];