fix(web): open tool details from inline previews (#725)

* fix(web): open tool details from inline previews

* fix(web): ignore nested controls in tool previews
This commit is contained in:
Ananovo
2026-05-29 10:22:54 +08:00
committed by GitHub
parent 27e58570c3
commit c78785a0b2
4 changed files with 50 additions and 6 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

+4 -1
View File
@@ -78,7 +78,10 @@ export function CodeBlock(props: {
{showCopyButton ? ( {showCopyButton ? (
<button <button
type="button" type="button"
onClick={() => copy(props.code)} onClick={(event) => {
event.stopPropagation()
copy(props.code)
}}
className="shrink-0 rounded-md p-1 text-[var(--app-code-header-fg)] transition-colors hover:bg-[var(--app-code-copy-hover-bg)] hover:text-[var(--app-fg)]" className="shrink-0 rounded-md p-1 text-[var(--app-code-header-fg)] transition-colors hover:bg-[var(--app-code-copy-hover-bg)] hover:text-[var(--app-fg)]"
title={t('code.copy')} title={t('code.copy')}
> >
+46 -5
View File
@@ -1,7 +1,7 @@
import type { ToolCallBlock } from '@/chat/types' import type { ToolCallBlock } from '@/chat/types'
import type { ApiClient } from '@/api/client' import type { ApiClient } from '@/api/client'
import type { SessionMetadataSummary } from '@/types/api' import type { SessionMetadataSummary } from '@/types/api'
import { memo, useEffect, useMemo, useState, type ReactNode } from 'react' import { memo, useEffect, useMemo, useState, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'
import { isObject, safeStringify } from '@hapi/protocol' import { isObject, safeStringify } from '@hapi/protocol'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { CodeBlock } from '@/components/CodeBlock' import { CodeBlock } from '@/components/CodeBlock'
@@ -187,6 +187,16 @@ function DetailsIcon() {
) )
} }
const INLINE_PREVIEW_INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, summary, [role="button"], [contenteditable="true"]'
function isNestedInteractiveElement(event: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>): boolean {
if (event.target === event.currentTarget) return false
if (!(event.target instanceof Element)) return false
const interactive = event.target.closest(INLINE_PREVIEW_INTERACTIVE_SELECTOR)
return interactive !== null && interactive !== event.currentTarget
}
type ToolCardProps = { type ToolCardProps = {
api: ApiClient api: ApiClient
sessionId: string sessionId: string
@@ -238,6 +248,7 @@ export function ToolDetailDialogContent(props: {
function ToolCardInner(props: ToolCardProps) { function ToolCardInner(props: ToolCardProps) {
const { t } = useTranslation() const { t } = useTranslation()
const [detailsOpen, setDetailsOpen] = useState(false)
const presentation = useMemo(() => getToolPresentation({ const presentation = useMemo(() => getToolPresentation({
toolName: props.block.tool.name, toolName: props.block.tool.name,
input: props.block.tool.input, input: props.block.tool.input,
@@ -276,6 +287,18 @@ function ToolCardInner(props: ToolCardProps) {
const hasBody = showInline || taskSummary !== null || showsPermissionFooter const hasBody = showInline || taskSummary !== null || showsPermissionFooter
const stateColor = toolStatusColorClass(props.block.tool.state) const stateColor = toolStatusColorClass(props.block.tool.state)
const { suppressFocusRing, onTriggerPointerDown, onTriggerKeyDown, onTriggerBlur } = usePointerFocusRing() const { suppressFocusRing, onTriggerPointerDown, onTriggerKeyDown, onTriggerBlur } = usePointerFocusRing()
const openDetails = () => setDetailsOpen(true)
const openDetailsFromInlinePreview = (event: MouseEvent<HTMLElement>) => {
if (isNestedInteractiveElement(event)) return
openDetails()
}
const openDetailsFromInlinePreviewKeyDown = (event: KeyboardEvent<HTMLElement>) => {
if (isNestedInteractiveElement(event)) return
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
openDetails()
}
}
const header = ( const header = (
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@@ -320,7 +343,7 @@ function ToolCardInner(props: ToolCardProps) {
return ( return (
<Card className="overflow-hidden rounded-[20px] bg-[var(--app-tool-card-bg)] shadow-none"> <Card className="overflow-hidden rounded-[20px] bg-[var(--app-tool-card-bg)] shadow-none">
<CardHeader className={cn('space-y-0 p-3', subtitle ? 'pb-2' : null)}> <CardHeader className={cn('space-y-0 p-3', subtitle ? 'pb-2' : null)}>
<Dialog> <Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<button <button
type="button" type="button"
@@ -354,16 +377,34 @@ function ToolCardInner(props: ToolCardProps) {
{showInline ? ( {showInline ? (
CompactToolView ? ( CompactToolView ? (
<div className="mt-3"> <div
className="mt-3 cursor-pointer rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
role="button"
tabIndex={0}
onClick={openDetailsFromInlinePreview}
onKeyDown={openDetailsFromInlinePreviewKeyDown}
>
<CompactToolView block={props.block} metadata={props.metadata} surface="inline" /> <CompactToolView block={props.block} metadata={props.metadata} surface="inline" />
</div> </div>
) : ( ) : (
<div className="mt-3 flex flex-col gap-3"> <div className="mt-3 flex flex-col gap-3">
<div> <div
className="cursor-pointer rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
role="button"
tabIndex={0}
onClick={openDetailsFromInlinePreview}
onKeyDown={openDetailsFromInlinePreviewKeyDown}
>
<div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.input')}</div> <div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.input')}</div>
{renderToolInput(props.block, 'inline')} {renderToolInput(props.block, 'inline')}
</div> </div>
<div> <div
className="cursor-pointer rounded-xl focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--app-link)]"
role="button"
tabIndex={0}
onClick={openDetailsFromInlinePreview}
onKeyDown={openDetailsFromInlinePreviewKeyDown}
>
<div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.result')}</div> <div className="mb-1 text-xs font-medium text-[var(--app-hint)]">{t('tool.result')}</div>
<ResultToolView block={props.block} metadata={props.metadata} surface="inline" /> <ResultToolView block={props.block} metadata={props.metadata} surface="inline" />
</div> </div>