diff --git a/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx b/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx index 91d113f15e0..bc0f47f2260 100644 --- a/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx +++ b/src/renderer/src/components/native-chat/NativeChatDiffCard.tsx @@ -152,13 +152,13 @@ export function NativeChatDiffCard({ type="button" onClick={() => hasBody && setExpanded(!expanded)} className={cn( - 'group flex w-full items-center gap-1.5 px-2 py-1 text-left', + 'group/diff-card flex w-full items-center gap-1.5 px-2 py-1 text-left', hasBody ? 'cursor-pointer hover:bg-accent/30' : 'cursor-default' )} aria-expanded={hasBody ? expanded : undefined} > - + {verbLabel(file)} {hasBody ? ( diff --git a/src/renderer/src/components/native-chat/NativeChatSubagentRun.tsx b/src/renderer/src/components/native-chat/NativeChatSubagentRun.tsx index cbb25e624cd..1ea4852ab15 100644 --- a/src/renderer/src/components/native-chat/NativeChatSubagentRun.tsx +++ b/src/renderer/src/components/native-chat/NativeChatSubagentRun.tsx @@ -208,7 +208,7 @@ export function NativeChatSubagentRun({ diff --git a/src/renderer/src/components/native-chat/NativeChatToolLine.tsx b/src/renderer/src/components/native-chat/NativeChatToolLine.tsx index 883244d8f91..4984853268d 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolLine.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolLine.tsx @@ -72,7 +72,7 @@ export function NativeChatToolLine({ type="button" onClick={() => hasDetail && setExpanded(!expanded)} className={cn( - 'group flex w-full items-center gap-1.5 py-0.5 text-left', + 'group/tool-line flex w-full items-center gap-1.5 py-0.5 text-left', hasDetail ? 'cursor-pointer' : 'cursor-default' )} aria-expanded={hasDetail ? expanded : undefined} @@ -89,12 +89,12 @@ export function NativeChatToolLine({ category to read from it. The empty slot keeps rows aligned. */ )} - + {isCall ? : name} {preview ? ( {preview} @@ -102,11 +102,12 @@ export function NativeChatToolLine({ ) : null} {isCall ? : null} {hasDetail ? ( - // Chevron stays hidden until this row is expanded. + // Hover reveal is keyed to this row's own named group: a bare `group` + // also answers to the message row's, lighting every chevron at once. ) : null} diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx index 6c043b92374..aee0d2a94e4 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx @@ -774,6 +774,64 @@ describe('NativeChatToolRun', () => { expect(container.querySelector('.lucide-folder')).toBeInTheDocument() expect(screen.getByTitle('ls')).toHaveTextContent('ls') }) + + // A bare `group-hover:` matches a hover on ANY ancestor carrying `.group`, and + // the assistant message row around this run is one. Hovering a single tool line + // therefore revealed every chevron in the message at once. + describe('hover reveal', () => { + const run: NativeChatBlock[] = [ + { type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' }, + { type: 'tool-result', output: 'a\nb' }, + { type: 'tool-call', name: 'Bash', input: { command: 'pwd' }, state: 'completed' }, + { type: 'tool-result', output: '/tmp' } + ] + + /** Every node's class list, read through the attribute because an SVG's + * `className` is an `SVGAnimatedString` rather than a string. */ + function hoverRevealed(container: HTMLElement): { element: Element; classes: string }[] { + return [...container.querySelectorAll('[class*="group-hover"]')].map((element) => ({ + element, + classes: element.getAttribute('class') ?? '' + })) + } + + it('scopes a row’s reveal to that row rather than the whole message', () => { + // Open run, collapsed children — the state the turn caret leaves behind. + const { container } = render( + + ) + + const revealed = hoverRevealed(container) + expect(revealed.length).toBeGreaterThan(0) + revealed.forEach(({ classes }) => { + expect(classes).not.toMatch(/(^|\s)group-hover:/) + }) + }) + + it('puts every reveal inside the row button that governs it', () => { + const { container } = render( + + ) + + hoverRevealed(container).forEach(({ element, classes }) => { + const scope = /group-hover\/([a-z-]+):/.exec(classes)?.[1] + expect(scope).toBeDefined() + expect(element.closest(`.group\\/${scope}`)).not.toBeNull() + }) + }) + + it('hides a collapsed chevron on every row until its own row is hovered', () => { + const { container } = render( + + ) + + const chevrons = [...container.querySelectorAll('.pl-4 button svg.lucide-chevron-right')] + expect(chevrons.length).toBeGreaterThan(1) + chevrons.forEach((chevron) => { + expect(chevron.getAttribute('class')).toContain('group-hover/tool-line:opacity-100') + }) + }) + }) }) describe('NativeChatToolRun task lists', () => { diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.tsx index 2f8eb76b8cf..6a6e26ebd37 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.tsx @@ -214,7 +214,7 @@ export function NativeChatToolRun({