fix(gui): expand thinking rows on click
This commit is contained in:
@@ -32,6 +32,7 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
|
||||
summary={firstLine(text)}
|
||||
body={text}
|
||||
state={running ? 'running' : 'ok'}
|
||||
expandOnRowClick
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
event.stopPropagation()
|
||||
toggleExpand()
|
||||
}
|
||||
const toggleFromKeyboard = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!rowExpands || (event.key !== 'Enter' && event.key !== ' ')) return
|
||||
event.preventDefault()
|
||||
toggleExpand()
|
||||
}
|
||||
return (
|
||||
<div className={css.root} data-variant={variant} data-state={state}>
|
||||
<div
|
||||
className={css.row}
|
||||
data-clickable={onOpenDetails !== undefined || undefined}
|
||||
onClick={onOpenDetails}
|
||||
data-clickable={rowExpands || onOpenDetails !== undefined || undefined}
|
||||
role={rowExpands ? 'button' : undefined}
|
||||
tabIndex={rowExpands ? 0 : undefined}
|
||||
aria-expanded={rowExpands ? open : undefined}
|
||||
onClick={rowExpands ? toggleExpand : onOpenDetails}
|
||||
onKeyDown={rowExpands ? toggleFromKeyboard : undefined}
|
||||
>
|
||||
{expandable ? (
|
||||
{expandable && !rowExpands ? (
|
||||
<button
|
||||
type="button"
|
||||
className={css.leading}
|
||||
aria-expanded={open}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setExpanded((v) => !v)
|
||||
}}
|
||||
onClick={toggleFromLeading}
|
||||
>
|
||||
{open ? <IconChevronDownOutline14 className={clsx(css.chevron)} /> : leadingFor(state, icon)}
|
||||
</button>
|
||||
) : (
|
||||
<span className={css.leading}>{leadingFor(state, icon)}</span>
|
||||
<span className={css.leading}>
|
||||
{open ? <IconChevronDownOutline14 className={clsx(css.chevron)} /> : leadingFor(state, icon)}
|
||||
</span>
|
||||
)}
|
||||
<span className={css.title}>{title}</span>
|
||||
{!open && (
|
||||
|
||||
@@ -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(
|
||||
<AssistantMarkdown
|
||||
blocks={[{ kind: 'reasoning', text: 'Inspect the session\nCheck persistence' }]}
|
||||
streaming={false}
|
||||
/>,
|
||||
)
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user