Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -286,6 +288,46 @@ 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(
<ClaudeCodeToolCallPart
addResult={vi.fn()}
args={{ command: 'pnpm --filter nemo-studio-ui test', description: 'run Studio tests' }}
argsText='{"command":"pnpm --filter nemo-studio-ui test","description":"run Studio tests"}'
resume={vi.fn()}
status={{ type: 'complete' }}
toolCallId="toolu_bash"
toolName="Bash"
type="tool-call"
/>
);

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'
);
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' }));

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();

Expand Down Expand Up @@ -365,10 +407,58 @@ 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();
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 only the file name', () => {
it('renders Read as summary-only subtle text', () => {
render(
<ClaudeCodeToolCallPart
addResult={vi.fn()}
Expand All @@ -386,7 +476,8 @@ describe('ClaudeCodeToolCallPart', () => {
expect(readBlock).toHaveTextContent('Read App.tsx');
expect(readBlock.tagName).toBe('DIV');
expectSubtleToolBlock(readBlock);
expect(screen.queryByText('web/packages/studio/src/App.tsx')).not.toBeInTheDocument();
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -179,6 +180,7 @@ const ClaudeCodeToolCallPartContent = ({
{
detail: getSubtleToolDetail(toolName, args, subtleMessage),
Icon: getSubtleToolIcon(toolName),
invocation: getToolInvocation(toolName, args),
message: subtleMessage,
toolCallId: toolName,
toolName,
Expand Down Expand Up @@ -207,6 +209,7 @@ const ClaudeCodeToolCallPartContent = ({
{
detail: getSubtleToolDetail(toolName, args, fallbackMessage),
Icon: getSubtleToolIcon(toolName),
invocation: getToolInvocation(toolName, args),
message: fallbackMessage,
toolCallId: toolName,
toolName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,84 @@
// 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';
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) => (
<Flex
direction="col"
Comment thread
dmariali marked this conversation as resolved.
Outdated
className="w-full min-w-0 max-w-full overflow-hidden"
data-testid="claude-code-tool-call-invocation"
id={`${toolCallId}-invocation-${invocationIndex}`}
>
<Flex direction="col" className="relative w-full min-w-0 max-w-full">
<button
aria-label={`Copy invocation${invocationCount === 1 ? '' : ` ${invocationIndex + 1}`}`}
className="absolute right-density-xs top-density-xs z-10 rounded p-0.5 hover:bg-surface-sunken"
onClick={() => copyInvocation(invocation)}
title="Copy invocation"
type="button"
>
<Copy aria-hidden className="size-3.5" />
</button>
<pre
className={`max-h-72 w-full min-w-0 max-w-full overflow-auto whitespace-pre-wrap break-words rounded ${CODE_BLOCK_SURFACE_CLASS} p-density-sm pr-density-xl text-xs leading-relaxed text-secondary`}
data-testid="claude-code-tool-call-invocation-surface"
>
<code data-testid="claude-code-tool-call-invocation-content">{invocation}</code>
</pre>
</Flex>
</Flex>
);

export const SubtleToolCallRow = ({ actions }: SubtleToolCallRowProps) => (
<Text asChild kind="body/regular/sm">
<div
className="my-density-xs flex max-w-full flex-wrap items-center gap-x-density-sm gap-y-density-xs rounded border border-base border-l-2 border-l-[var(--border-color-accent-blue)] bg-[color-mix(in_srgb,var(--background-color-accent-blue-subtle)_38%,var(--background-color-surface-base))] px-density-sm py-density-xs text-secondary"
className="my-density-xs flex w-full max-w-full flex-wrap items-center gap-x-density-sm gap-y-density-xs overflow-hidden rounded border border-base border-l-2 border-l-[var(--border-color-accent-blue)] bg-[color-mix(in_srgb,var(--background-color-accent-blue-subtle)_38%,var(--background-color-surface-base))] px-density-sm py-density-xs text-secondary"
data-testid="claude-code-tool-call-subtle"
title={actions.map((action) => action.title ?? action.message).join(' | ')}
>
{summarizeRepeatedSubtleToolActions(actions).map((action, index) => {
const Icon = action.Icon;
const key = `${action.toolCallId}-${index}`;

if (action.details?.length) {
const invocations = action.invocations ?? [action.invocation];
const isExpandable =
Boolean(action.details?.length) || (action.toolName !== 'Read' && invocations.length > 0);

if (isExpandable) {
return (
<details
key={key}
className="group/subtle max-w-full basis-full"
className="group/subtle w-full min-w-0 max-w-full basis-full overflow-hidden"
data-testid="claude-code-tool-call-subtle-details"
>
<summary
className="inline-flex cursor-pointer list-none items-center gap-density-xs marker:hidden"
className="inline-flex w-full min-w-0 max-w-full basis-full cursor-pointer list-none items-center gap-density-xs overflow-hidden marker:hidden"
data-testid="claude-code-tool-call-subtle-action"
>
<ChevronRight
Expand All @@ -43,29 +92,77 @@ export const SubtleToolCallRow = ({ actions }: SubtleToolCallRowProps) => (
/>
<span className="min-w-0 truncate">{action.message}</span>
</summary>
<ul
className="mt-0.5 max-w-full space-y-0.5 pl-7"
data-testid="claude-code-tool-call-subtle-detail-list"
>
{action.details.map((detail, detailIndex) => (
<li
key={`${action.toolCallId}-${detailIndex}`}
className="min-w-0 truncate"
data-testid="claude-code-tool-call-subtle-detail-item"
title={detail}
>
{detail}
</li>
))}
</ul>
{action.details?.length ? (
<Flex
direction="col"
className="mt-0.5 w-full min-w-0 max-w-full space-y-0.5 overflow-hidden pl-7"
data-testid="claude-code-tool-call-subtle-detail-list"
>
{action.toolName === 'Read'
? action.details.map((detail, detailIndex) => (
<span
key={`${action.toolCallId}-${detailIndex}`}
className="block min-w-0 max-w-full truncate py-0.5"
data-testid="claude-code-tool-call-subtle-detail-item"
title={detail}
>
{detail}
</span>
))
: action.details.map((detail, detailIndex) => {
const invocation = invocations[detailIndex];
if (!invocation) return null;

return (
<details
key={`${action.toolCallId}-${detailIndex}`}
className="group/invocation w-full min-w-0 max-w-full overflow-hidden"
data-testid="claude-code-tool-call-nested-invocation"
>
<summary
className="flex w-full min-w-0 max-w-full cursor-pointer list-none items-center gap-density-xs overflow-hidden py-0.5 marker:hidden"
data-testid="claude-code-tool-call-subtle-detail-item"
title={detail}
>
<span className="min-w-0 truncate">{detail}</span>
<ChevronRight
aria-hidden
className="size-3 shrink-0 transition-transform group-open/invocation:rotate-90"
/>
</summary>
<Flex
direction="col"
className="mt-density-xs w-full min-w-0 max-w-full pl-density-sm"
>
<InvocationPanel
invocation={invocation}
invocationCount={invocations.length}
invocationIndex={detailIndex}
toolCallId={action.toolCallId}
/>
</Flex>
</details>
);
})}
</Flex>
) : (
<Flex direction="col" className="mt-density-xs w-full min-w-0 max-w-full pl-7">
<InvocationPanel
invocation={invocations[0]!}
invocationCount={1}
invocationIndex={0}
toolCallId={action.toolCallId}
/>
</Flex>
)}
</details>
);
}

return (
<span
key={key}
className="inline-flex min-w-0 max-w-full basis-full items-center gap-density-xs"
className="inline-flex w-full min-w-0 max-w-full basis-full items-center gap-density-xs overflow-hidden"
data-testid="claude-code-tool-call-subtle-action"
>
<Icon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] => {
Expand All @@ -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,
Expand Down Expand Up @@ -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}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading