From b1a7a2b4d6ac74fae9ede5ebd43e913ea5db613b Mon Sep 17 00:00:00 2001 From: Danielle Ali <44468613+dmariali@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:35:03 -0400 Subject: [PATCH 1/4] feat(studio): show additional details for coding agent tool calls Signed-off-by: Danielle Ali <44468613+dmariali@users.noreply.github.com> --- .../ClaudeCodeToolCallPart.test.tsx | 78 +++++++++++- .../ClaudeCodeToolCallPart.tsx | 3 + .../toolCall/SubtleToolCallRow.tsx | 114 +++++++++++++++--- .../ClaudeCodeChatRoute/toolCall/helpers.ts | 11 ++ .../ClaudeCodeChatRoute/toolCall/types.ts | 2 + 5 files changed, 188 insertions(+), 20 deletions(-) diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx index af83318fcd..bdf3d3530a 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx @@ -286,6 +286,39 @@ describe('ClaudeCodeToolCallPart', () => { expect(screen.queryByTestId('claude-code-tool-call')).not.toBeInTheDocument(); }); + it('reveals and copies the exact Bash command instead of its description', async () => { + const user = userEvent.setup(); + + render( + + ); + + const disclosure = screen.getByTestId('claude-code-tool-call-subtle-details'); + const summary = screen.getByTestId('claude-code-tool-call-subtle-action'); + expect(summary).toHaveTextContent('Ran run Studio tests'); + expect(disclosure).not.toHaveAttribute('open'); + + await user.click(summary); + + expect(disclosure).toHaveAttribute('open'); + expect(screen.getByTestId('claude-code-tool-call-invocation')).toHaveTextContent( + 'pnpm --filter nemo-studio-ui test' + ); + + await user.click(screen.getByRole('button', { name: 'Copy invocation' })); + + expect(await navigator.clipboard.readText()).toBe('pnpm --filter nemo-studio-ui test'); + }); + it('summarizes repeated grouped tool actions with expandable details', async () => { const user = userEvent.setup(); @@ -365,10 +398,43 @@ describe('ClaudeCodeToolCallPart', () => { 'run tests', 'pnpm typecheck', ]); + expect( + within(commandDetails) + .getAllByTestId('claude-code-tool-call-invocation-content') + .map((item) => item.textContent) + ).toEqual(['pwd', 'ls', 'git status', 'pnpm test', 'pnpm typecheck']); + const nestedInvocations = within(commandDetails).getAllByTestId( + 'claude-code-tool-call-nested-invocation' + ); + expect(nestedInvocations).toHaveLength(5); + for (const nestedInvocation of nestedInvocations) { + expect(nestedInvocation).not.toHaveAttribute('open'); + expect( + within(nestedInvocation).getByTestId('claude-code-tool-call-invocation') + ).not.toBeVisible(); + } + + await user.click( + within(nestedInvocations[1]!).getByTestId('claude-code-tool-call-subtle-detail-item') + ); + + expect(nestedInvocations[1]).toHaveAttribute('open'); + expect(within(nestedInvocations[1]!).queryByText('Invocation 2')).not.toBeInTheDocument(); + expect( + within(nestedInvocations[1]!).getByRole('button', { name: 'Copy invocation 2' }) + ).toBeVisible(); + expect( + within(nestedInvocations[1]!).getByTestId('claude-code-tool-call-invocation-content') + ).toHaveTextContent('ls'); + expect( + within(nestedInvocations[0]!).getByTestId('claude-code-tool-call-invocation') + ).not.toBeVisible(); expect(screen.getAllByTestId('claude-code-tool-call-subtle-detail-item')).toHaveLength(7); }); - it('renders Read as subtle text with only the file name', () => { + it('renders Read as subtle text with the full invocation behind a disclosure', async () => { + const user = userEvent.setup(); + render( { expect(readBlock).toHaveTextContent('Read App.tsx'); expect(readBlock.tagName).toBe('DIV'); expectSubtleToolBlock(readBlock); - expect(screen.queryByText('web/packages/studio/src/App.tsx')).not.toBeInTheDocument(); + const disclosure = screen.getByTestId('claude-code-tool-call-subtle-details'); + expect(disclosure).not.toHaveAttribute('open'); + + await user.click(screen.getByText('Read App.tsx')); + + expect(disclosure).toHaveAttribute('open'); + expect(screen.getByTestId('claude-code-tool-call-invocation')).toHaveTextContent( + '"file_path": "web/packages/studio/src/App.tsx"' + ); }); it('renders running subtle tool text without special color or animation', () => { diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.tsx index 039f47509f..dcc38ad7be 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.tsx @@ -17,6 +17,7 @@ import { getSubtleToolGroupActions, getSubtleToolIcon, getSubtleToolMessage, + getToolInvocation, getToolSummary, } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/helpers'; import { SubtleToolCallRow } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow'; @@ -179,6 +180,7 @@ const ClaudeCodeToolCallPartContent = ({ { detail: getSubtleToolDetail(toolName, args, subtleMessage), Icon: getSubtleToolIcon(toolName), + invocation: getToolInvocation(toolName, args), message: subtleMessage, toolCallId: toolName, toolName, @@ -207,6 +209,7 @@ const ClaudeCodeToolCallPartContent = ({ { detail: getSubtleToolDetail(toolName, args, fallbackMessage), Icon: getSubtleToolIcon(toolName), + invocation: getToolInvocation(toolName, args), message: fallbackMessage, toolCallId: toolName, toolName, diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx index b3a5468cdb..b24030c535 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx @@ -2,14 +2,57 @@ // SPDX-License-Identifier: Apache-2.0 import { Text } from '@nvidia/foundations-react-core'; +import { CODE_BLOCK_SURFACE_CLASS } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/constants'; import { summarizeRepeatedSubtleToolActions } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/helpers'; import type { SubtleToolAction } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/types'; -import { ChevronRight } from 'lucide-react'; +import { ChevronRight, Copy } from 'lucide-react'; interface SubtleToolCallRowProps { readonly actions: readonly SubtleToolAction[]; } +interface InvocationPanelProps { + readonly invocation: string; + readonly invocationCount: number; + readonly invocationIndex: number; + readonly toolCallId: string; +} + +const copyInvocation = (invocation: string): void => { + if (!navigator.clipboard) return; + void navigator.clipboard.writeText(invocation).catch(() => undefined); +}; + +const InvocationPanel = ({ + invocation, + invocationCount, + invocationIndex, + toolCallId, +}: InvocationPanelProps) => ( +
+
+ +
+        {invocation}
+      
+
+
+); + export const SubtleToolCallRow = ({ actions }: SubtleToolCallRowProps) => (
( const Icon = action.Icon; const key = `${action.toolCallId}-${index}`; - if (action.details?.length) { + const invocations = action.invocations ?? [action.invocation]; + + if (action.details?.length || invocations.length) { return (
( data-testid="claude-code-tool-call-subtle-details" > ( /> {action.message} -
    - {action.details.map((detail, detailIndex) => ( -
  • - {detail} -
  • - ))} -
+ {action.details?.length ? ( +
+ {action.details.map((detail, detailIndex) => { + const invocation = invocations[detailIndex]; + if (!invocation) return null; + + return ( +
+ + {detail} + + +
+ +
+
+ ); + })} +
+ ) : ( +
+ +
+ )}
); } diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/helpers.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/helpers.ts index 286c56b32f..ff8e2cf607 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/helpers.ts +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/helpers.ts @@ -355,6 +355,15 @@ export const getSubtleToolDetail = ( return compactSubtleDetail(getToolSummary(toolName, args)) ?? message; }; +export const getToolInvocation = (toolName: string, args: ClaudeCodeToolArgs): string => { + if (toolName === 'Bash') { + const command = args.command; + if (typeof command === 'string' && command) return command; + } + + return JSON.stringify(args, null, 2); +}; + export const getSubtleToolGroupActions = ( args: ClaudeCodeToolArgs ): readonly SubtleToolAction[] => { @@ -374,6 +383,7 @@ export const getSubtleToolGroupActions = ( return { detail: getSubtleToolDetail(toolName, actionArgs, message), Icon: getSubtleToolIcon(toolName), + invocation: getToolInvocation(toolName, actionArgs), message, toolCallId: getRawStringArg(action, ['toolCallId'])?.trim() ?? `${toolName}-${index}`, toolName, @@ -403,6 +413,7 @@ export const summarizeRepeatedSubtleToolActions = ( return { ...firstAction, details: group.map((action) => action.detail), + invocations: group.map((action) => action.invocation), message: getRepeatedSubtleToolMessage(firstAction.toolName, group.length), title: group.map((action) => action.message).join(' | '), toolCallId: `${firstAction.toolCallId}-${group.length}`, diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/types.ts b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/types.ts index dc61fabedd..cce3a28d7f 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/types.ts +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/types.ts @@ -7,6 +7,8 @@ export interface SubtleToolAction { readonly detail: string; readonly details?: readonly string[]; readonly Icon: LucideIcon; + readonly invocation: string; + readonly invocations?: readonly string[]; readonly message: string; readonly title?: string; readonly toolCallId: string; From ad23c759dd42f79651fb3a55bea74d7a8bd42fbd Mon Sep 17 00:00:00 2001 From: Danielle Ali <44468613+dmariali@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:17:44 -0400 Subject: [PATCH 2/4] cleanup Signed-off-by: Danielle Ali <44468613+dmariali@users.noreply.github.com> --- .../ClaudeCodeToolCallPart.test.tsx | 41 +++++-- .../toolCall/SubtleToolCallRow.tsx | 111 ++++++++++-------- 2 files changed, 94 insertions(+), 58 deletions(-) diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx index bdf3d3530a..55d5d9d74e 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/ClaudeCodeToolCallPart.test.tsx @@ -68,7 +68,9 @@ const expectSubtleToolBlock = (subtleBlock: HTMLElement) => { expect(subtleBlock).toHaveClass( 'my-density-xs', 'flex', + 'w-full', 'max-w-full', + 'overflow-hidden', 'flex-wrap', 'items-center', 'gap-x-density-sm', @@ -313,6 +315,13 @@ describe('ClaudeCodeToolCallPart', () => { expect(screen.getByTestId('claude-code-tool-call-invocation')).toHaveTextContent( 'pnpm --filter nemo-studio-ui test' ); + expect(screen.getByTestId('claude-code-tool-call-invocation-surface')).toHaveClass( + 'w-full', + 'min-w-0', + 'max-w-full', + 'overflow-auto', + 'break-words' + ); await user.click(screen.getByRole('button', { name: 'Copy invocation' })); @@ -429,12 +438,27 @@ describe('ClaudeCodeToolCallPart', () => { expect( within(nestedInvocations[0]!).getByTestId('claude-code-tool-call-invocation') ).not.toBeVisible(); + const readDetails = screen.getAllByTestId('claude-code-tool-call-subtle-details')[1]!; + expect(readDetails).not.toHaveAttribute('open'); + + await user.click(screen.getByText('Read 2 files')); + + expect(readDetails).toHaveAttribute('open'); + expect( + within(readDetails) + .getAllByTestId('claude-code-tool-call-subtle-detail-item') + .map((item) => item.textContent) + ).toEqual(['README.md', 'package.json']); + expect( + within(readDetails).queryByTestId('claude-code-tool-call-nested-invocation') + ).not.toBeInTheDocument(); + expect( + within(readDetails).queryByTestId('claude-code-tool-call-invocation') + ).not.toBeInTheDocument(); expect(screen.getAllByTestId('claude-code-tool-call-subtle-detail-item')).toHaveLength(7); }); - it('renders Read as subtle text with the full invocation behind a disclosure', async () => { - const user = userEvent.setup(); - + it('renders Read as summary-only subtle text', () => { render( { expect(readBlock).toHaveTextContent('Read App.tsx'); expect(readBlock.tagName).toBe('DIV'); expectSubtleToolBlock(readBlock); - const disclosure = screen.getByTestId('claude-code-tool-call-subtle-details'); - expect(disclosure).not.toHaveAttribute('open'); - - await user.click(screen.getByText('Read App.tsx')); - - expect(disclosure).toHaveAttribute('open'); - expect(screen.getByTestId('claude-code-tool-call-invocation')).toHaveTextContent( - '"file_path": "web/packages/studio/src/App.tsx"' - ); + expect(screen.queryByTestId('claude-code-tool-call-subtle-details')).not.toBeInTheDocument(); + expect(screen.queryByTestId('claude-code-tool-call-invocation')).not.toBeInTheDocument(); }); it('renders running subtle tool text without special color or animation', () => { diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx index b24030c535..19cf7b6d3e 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Text } from '@nvidia/foundations-react-core'; +import { Flex, Text } from '@nvidia/foundations-react-core'; import { CODE_BLOCK_SURFACE_CLASS } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/constants'; import { summarizeRepeatedSubtleToolActions } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/helpers'; import type { SubtleToolAction } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/types'; @@ -29,12 +29,13 @@ const InvocationPanel = ({ invocationIndex, toolCallId, }: InvocationPanelProps) => ( -
-
+
         {invocation}
       
-
-
+ + ); export const SubtleToolCallRow = ({ actions }: SubtleToolCallRowProps) => (
action.title ?? action.message).join(' | ')} > @@ -65,16 +67,18 @@ export const SubtleToolCallRow = ({ actions }: SubtleToolCallRowProps) => ( const key = `${action.toolCallId}-${index}`; const invocations = action.invocations ?? [action.invocation]; + const isExpandable = + Boolean(action.details?.length) || (action.toolName !== 'Read' && invocations.length > 0); - if (action.details?.length || invocations.length) { + if (isExpandable) { return (
( {action.message} {action.details?.length ? ( -
- {action.details.map((detail, detailIndex) => { - const invocation = invocations[detailIndex]; - if (!invocation) return null; - - return ( -
- ( + - {detail} - - -
- -
-
- ); - })} -
+ {detail} + + )) + : action.details.map((detail, detailIndex) => { + const invocation = invocations[detailIndex]; + if (!invocation) return null; + + return ( +
+ + {detail} + + + + + +
+ ); + })} + ) : ( -
+ -
+ )}
); @@ -143,7 +162,7 @@ export const SubtleToolCallRow = ({ actions }: SubtleToolCallRowProps) => ( return ( Date: Tue, 14 Jul 2026 09:09:34 -0400 Subject: [PATCH 3/4] change flex dir='row' to stack Signed-off-by: Danielle Ali <44468613+dmariali@users.noreply.github.com> --- .../toolCall/SubtleToolCallRow.tsx | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx index 19cf7b6d3e..7ebf2f3f9e 100644 --- a/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx +++ b/web/packages/studio/src/routes/agents/ClaudeCodeChatRoute/toolCall/SubtleToolCallRow.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Flex, Text } from '@nvidia/foundations-react-core'; +import { Stack, Text } from '@nvidia/foundations-react-core'; import { CODE_BLOCK_SURFACE_CLASS } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/constants'; import { summarizeRepeatedSubtleToolActions } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/helpers'; import type { SubtleToolAction } from '@studio/routes/agents/ClaudeCodeChatRoute/toolCall/types'; @@ -29,13 +29,12 @@ const InvocationPanel = ({ invocationIndex, toolCallId, }: InvocationPanelProps) => ( - - +