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({