diff --git a/web/src/components/ToolCard/ToolCard.tsx b/web/src/components/ToolCard/ToolCard.tsx index 6e2b561b..c5518b05 100644 --- a/web/src/components/ToolCard/ToolCard.tsx +++ b/web/src/components/ToolCard/ToolCard.tsx @@ -435,6 +435,11 @@ function ToolCardInner(props: ToolCardProps) { || ((permission.status === 'denied' || permission.status === 'canceled') && Boolean(permission.reason)) )) const hasBody = showInline || taskSummary !== null || showsPermissionFooter + // Header/content padding already supplies 12-16px below timing; add only + // the remainder needed to match the detail dialog's 16px section gap. + const inlineBodySpacing = props.block.tool.state === 'pending' + ? 'mt-3' + : (subtitle ? 'mt-1' : 'mt-0') const stateColor = toolStatusColorClass(props.block.tool.state) const { suppressFocusRing, onTriggerPointerDown, onTriggerKeyDown, onTriggerBlur } = usePointerFocusRing() const openDetails = () => setDetailsOpen(true) @@ -516,8 +521,8 @@ function ToolCardInner(props: ToolCardProps) { {header} - - + + {toolTitle} @@ -536,7 +541,10 @@ function ToolCardInner(props: ToolCardProps) { {showInline ? ( CompactToolView ? (
) : ( -
+
{ expect(screen.getAllByText('src/a.ts')[0]).toBeInTheDocument() expect(within(dialog).getAllByText('Input').length).toBeGreaterThan(0) expect(within(dialog).getAllByText('Result').length).toBeGreaterThan(0) + expect(within(dialog).getByRole('heading').parentElement).toHaveClass('text-left') + expect(within(dialog).getByRole('button', { name: 'Close' })).toHaveClass('top-2') }) it('shows structured Codex exploration actions by default without a generic action count', () => { diff --git a/web/src/components/ToolCard/ToolGroupCard.tsx b/web/src/components/ToolCard/ToolGroupCard.tsx index fb57eb62..1a808f3e 100644 --- a/web/src/components/ToolCard/ToolGroupCard.tsx +++ b/web/src/components/ToolCard/ToolGroupCard.tsx @@ -440,10 +440,10 @@ export function ToolGroupCard(props: { setSelectedToolId(null) } }}> - + {selectedTool && selectedPresentation ? ( <> - + {selectedPresentation.title} diff --git a/web/src/components/ToolCard/knownTools.test.tsx b/web/src/components/ToolCard/knownTools.test.tsx index 774fa50a..98251ace 100644 --- a/web/src/components/ToolCard/knownTools.test.tsx +++ b/web/src/components/ToolCard/knownTools.test.tsx @@ -1,3 +1,4 @@ +import { render } from '@testing-library/react' import { describe, expect, it } from 'vitest' import { formatTerminalCommandTitle, getToolPresentation } from '@/components/ToolCard/knownTools' @@ -127,6 +128,8 @@ describe('getToolPresentation — unknown tool semantic title + subtitle dedup', expect(presentation.title).toBe('Tool') expect(presentation.subtitle).toBe('Tool 1') + const icon = render(<>{presentation.icon}).container.querySelector('svg') + expect(icon).toHaveClass('translate-y-px') }) it('returns null subtitle when no recognized input field is present', () => { diff --git a/web/src/components/ToolCard/knownTools.tsx b/web/src/components/ToolCard/knownTools.tsx index fbe21f3a..ab36fa8f 100644 --- a/web/src/components/ToolCard/knownTools.tsx +++ b/web/src/components/ToolCard/knownTools.tsx @@ -675,7 +675,7 @@ export function getToolPresentation( } return { - icon: , + icon: , title, subtitle: subtitle && subtitle !== title ? truncate(subtitle, 80) : null, minimal: true diff --git a/web/src/components/ToolCard/toolCardSpacing.test.tsx b/web/src/components/ToolCard/toolCardSpacing.test.tsx new file mode 100644 index 00000000..2fab744d --- /dev/null +++ b/web/src/components/ToolCard/toolCardSpacing.test.tsx @@ -0,0 +1,83 @@ +import { fireEvent, render, screen, within } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import type { ApiClient } from '@/api/client' +import type { ToolCallBlock } from '@/chat/types' +import { ToolCard } from '@/components/ToolCard/ToolCard' +import { I18nProvider } from '@/lib/i18n-context' + +function renderDetailedBash(command: string, state: 'pending' | 'completed' = 'completed') { + const completed = state === 'completed' + const block: ToolCallBlock = { + kind: 'tool-call', + id: 'tool-1', + localId: null, + createdAt: 1_000, + tool: { + id: 'tool-1', + name: 'Bash', + state, + input: { command }, + createdAt: 1_000, + startedAt: completed ? 1_000 : null, + completedAt: completed ? 1_500 : null, + execStartedAt: null, + execCompletedAt: null, + description: null, + result: completed ? 'ok' : undefined, + }, + children: [], + } + + render( + + {}} + block={block} + /> + + ) + + return screen.getByText('Input').parentElement?.parentElement +} + +describe('ToolCard spacing', () => { + it('matches the dialog gap when the timing header has a subtitle', () => { + const inlineBody = renderDetailedBash('echo hello && pwd') + + expect(inlineBody).toHaveClass('mt-1') + expect(inlineBody).toHaveClass('gap-4') + expect(inlineBody).not.toHaveClass('mt-3') + expect(inlineBody).not.toHaveClass('gap-3') + }) + + it('matches the dialog gap when the timing header has no subtitle', () => { + const inlineBody = renderDetailedBash('pwd') + + expect(inlineBody).toHaveClass('mt-0') + expect(inlineBody).not.toHaveClass('mt-3') + }) + + it('keeps the original body spacing when pending tools have no timing summary', () => { + const inlineBody = renderDetailedBash('pwd', 'pending') + + expect(inlineBody).toHaveClass('mt-3') + }) +}) + +describe('ToolCard detail dialog', () => { + it('keeps the tool detail title left-aligned on mobile', () => { + renderDetailedBash('pwd') + + fireEvent.click(screen.getByRole('button', { expanded: false })) + + const dialog = screen.getByRole('dialog') + const title = within(dialog).getByRole('heading') + expect(title.parentElement).toHaveClass('text-left') + expect(within(dialog).getByRole('button', { name: 'Close' })).toHaveClass('top-2') + }) +}) diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx index f74cc10f..8871189d 100644 --- a/web/src/components/ui/dialog.tsx +++ b/web/src/components/ui/dialog.tsx @@ -7,10 +7,14 @@ import { useTranslation } from '@/lib/use-translation' export const Dialog = DialogPrimitive.Root export const DialogTrigger = DialogPrimitive.Trigger +type DialogContentProps = React.ComponentPropsWithoutRef & { + closeButtonClassName?: string +} + export const DialogContent = React.forwardRef< HTMLDivElement, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => { + DialogContentProps +>(({ className, closeButtonClassName, children, ...props }, ref) => { const { t } = useTranslation() return ( @@ -25,7 +29,10 @@ export const DialogContent = React.forwardRef< > {children}