diff --git a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx index 61ac069b1c..fd612990ce 100644 --- a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx +++ b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx @@ -32,6 +32,7 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) { summary={firstLine(text)} body={text} state={running ? 'running' : 'ok'} + expandOnRowClick /> ) } diff --git a/packages/client/ui-conversation/src/client/chat/ToolRow.tsx b/packages/client/ui-conversation/src/client/chat/ToolRow.tsx index 0bb92e9f66..f1a5ce7440 100644 --- a/packages/client/ui-conversation/src/client/chat/ToolRow.tsx +++ b/packages/client/ui-conversation/src/client/chat/ToolRow.tsx @@ -4,7 +4,7 @@ // no inline output (full results live in the details panel). Expand state is // component-local view state; row click hands the selection off to the owner. -import { useState, type ReactNode } from 'react' +import { useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react' import clsx from 'clsx' import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives' import { IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives' @@ -20,6 +20,8 @@ export interface ToolRowProps { /** Expanded-body text; null = not expandable (leading slot never toggles). */ body: string | null state: ToolRowState + /** Makes the row itself the expand control instead of only its leading icon. */ + expandOnRowClick?: boolean | undefined /** Selection handoff (row click), already bound to this call by the owner. */ onOpenDetails?: (() => void) | undefined } @@ -35,31 +37,56 @@ function leadingFor(state: ToolRowState, icon: ReactNode): ReactNode { } } -export function ToolRow({ variant, icon, title, summary, body, state, onOpenDetails }: ToolRowProps) { +export function ToolRow({ + variant, + icon, + title, + summary, + body, + state, + expandOnRowClick = false, + onOpenDetails, +}: ToolRowProps) { const [expanded, setExpanded] = useState(false) const expandable = body !== null const open = expanded && expandable + const rowExpands = expandable && expandOnRowClick + const toggleExpand = () => { + setExpanded((v) => !v) + } + const toggleFromLeading = (event: MouseEvent) => { + event.stopPropagation() + toggleExpand() + } + const toggleFromKeyboard = (event: KeyboardEvent) => { + if (!rowExpands || (event.key !== 'Enter' && event.key !== ' ')) return + event.preventDefault() + toggleExpand() + } return (
- {expandable ? ( + {expandable && !rowExpands ? ( ) : ( - {leadingFor(state, icon)} + + {open ? : leadingFor(state, icon)} + )} {title} {!open && ( diff --git a/packages/client/ui-conversation/tests/chat-tool-row.spec.tsx b/packages/client/ui-conversation/tests/chat-tool-row.spec.tsx index 1a381adda8..b977cf003d 100644 --- a/packages/client/ui-conversation/tests/chat-tool-row.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-tool-row.spec.tsx @@ -6,6 +6,7 @@ afterEach(cleanup) import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import type { UseSession } from '@deepseek-ai/dsh-client-ui-slots' import { classifyTool, toolRowModel } from '../src/client/contract/tool-call-model.ts' +import { AssistantMarkdown } from '../src/client/chat/AssistantMarkdown.tsx' import { ToolRow } from '../src/client/chat/ToolRow.tsx' import { GenericToolCard } from '../src/client/chat/GenericToolCard.tsx' import type { ToolViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client' @@ -114,6 +115,25 @@ describe('ToolRow', () => { }) }) +describe('ThinkRow', () => { + it('expands from either Think or the reasoning summary', () => { + const view = render( + , + ) + const row = view.getByRole('button') + + fireEvent.click(view.getByText('Inspect the session')) + expect(row.getAttribute('aria-expanded')).toBe('true') + expect(view.getByText(/Check persistence/)).toBeTruthy() + + fireEvent.click(view.getByText('Think')) + expect(row.getAttribute('aria-expanded')).toBe('false') + }) +}) + describe('GenericToolCard', () => { const props = (toolName: string, block: RunningToolCall | ToolResultNode): ToolViewProps => ({ callId: 'c1', toolName, block,