fix(web): compact terminal tool cards by default (#601)

This commit is contained in:
junes
2026-05-09 09:00:18 +08:00
committed by GitHub
parent 3eac3456b4
commit 3a6574f2b9
12 changed files with 278 additions and 7 deletions
@@ -10,6 +10,7 @@ import { HappyUserMessage } from '@/components/AssistantChat/messages/UserMessag
import { HappySystemMessage } from '@/components/AssistantChat/messages/SystemMessage'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/Spinner'
import { useTerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
import { useTranslation } from '@/lib/use-translation'
import { CloseIcon } from '@/components/icons'
@@ -264,6 +265,7 @@ export function HappyThread(props: {
onOutlineItemClick?: (item: ConversationOutlineItem) => void
}) {
const { t } = useTranslation()
const { terminalToolDisplayMode } = useTerminalToolDisplayMode()
const viewportRef = useRef<HTMLDivElement | null>(null)
const contentRef = useRef<HTMLDivElement | null>(null)
const topSentinelRef = useRef<HTMLDivElement | null>(null)
@@ -676,6 +678,7 @@ export function HappyThread(props: {
api: props.api,
sessionId: props.sessionId,
metadata: props.metadata,
terminalToolDisplayMode,
disabled: props.disabled,
onRefresh: props.onRefresh,
onRetryMessage: props.onRetryMessage
@@ -1,12 +1,14 @@
import type { ReactNode } from 'react'
import { createContext, useContext } from 'react'
import type { ApiClient } from '@/api/client'
import type { TerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
import type { SessionMetadataSummary } from '@/types/api'
export type HappyChatContextValue = {
api: ApiClient
sessionId: string
metadata: SessionMetadataSummary | null
terminalToolDisplayMode: TerminalToolDisplayMode
disabled: boolean
onRefresh: () => void
onRetryMessage?: (localId: string) => void
@@ -120,6 +120,7 @@ function HappyNestedBlockList(props: {
api={ctx.api}
sessionId={ctx.sessionId}
metadata={ctx.metadata}
terminalToolDisplayMode={ctx.terminalToolDisplayMode}
disabled={ctx.disabled}
onDone={ctx.onRefresh}
block={block}
@@ -211,6 +212,7 @@ export function HappyToolMessage(props: ToolCallMessagePartProps) {
api={ctx.api}
sessionId={ctx.sessionId}
metadata={ctx.metadata}
terminalToolDisplayMode={ctx.terminalToolDisplayMode}
disabled={ctx.disabled}
onDone={ctx.onRefresh}
block={block}
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import { shouldShowInlineToolCardBody, shouldUseCompactTerminalToolCard } from '@/components/ToolCard/ToolCard'
describe('ToolCard terminal display mode helpers', () => {
it('treats terminal-related cards as compact by default', () => {
expect(shouldUseCompactTerminalToolCard('CodexBash', 'compact')).toBe(true)
expect(shouldUseCompactTerminalToolCard('shell_command', 'compact')).toBe(true)
expect(shouldUseCompactTerminalToolCard('run_shell_command', 'compact')).toBe(true)
expect(shouldUseCompactTerminalToolCard('Read', 'compact')).toBe(false)
})
it('hides inline terminal previews in compact mode', () => {
expect(shouldShowInlineToolCardBody('CodexBash', false, 'compact')).toBe(false)
})
it('keeps inline terminal previews in detailed mode', () => {
expect(shouldShowInlineToolCardBody('CodexBash', false, 'detailed')).toBe(true)
expect(shouldShowInlineToolCardBody('Bash', true, 'detailed')).toBe(true)
expect(shouldShowInlineToolCardBody('shell_command', true, 'detailed')).toBe(true)
expect(shouldShowInlineToolCardBody('run_shell_command', true, 'detailed')).toBe(true)
})
it('still hides inline bodies for minimal and Task/Agent subagent cards', () => {
expect(shouldShowInlineToolCardBody('Task', false, 'detailed')).toBe(false)
expect(shouldShowInlineToolCardBody('Agent', false, 'detailed')).toBe(false)
expect(shouldShowInlineToolCardBody('Read', true, 'detailed')).toBe(false)
})
})
+23 -3
View File
@@ -17,6 +17,7 @@ import { getToolPresentation } from '@/components/ToolCard/knownTools'
import { getToolFullViewComponent, getToolViewComponent } from '@/components/ToolCard/views/_all'
import { getToolResultViewComponent } from '@/components/ToolCard/views/_results'
import { formatTaskChildLabel, TaskStateIcon } from '@/components/ToolCard/helpers'
import type { TerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
import { usePointerFocusRing } from '@/hooks/usePointerFocusRing'
import { getInputString, getInputStringAny, truncate } from '@/lib/toolInputUtils'
import { cn } from '@/lib/utils'
@@ -25,6 +26,23 @@ import { TraceSection } from '@/components/ToolCard/trace'
import { isSubagentToolName } from '@/chat/subagentTool'
const ELAPSED_INTERVAL_MS = 1000
const TERMINAL_RELATED_TOOL_NAMES = new Set(['Bash', 'CodexBash', 'shell_command', 'run_shell_command'])
export function shouldUseCompactTerminalToolCard(toolName: string, terminalToolDisplayMode: TerminalToolDisplayMode): boolean {
return TERMINAL_RELATED_TOOL_NAMES.has(toolName) && terminalToolDisplayMode === 'compact'
}
export function shouldShowInlineToolCardBody(
toolName: string,
presentationMinimal: boolean,
terminalToolDisplayMode: TerminalToolDisplayMode
): boolean {
if (isSubagentToolName(toolName)) return false
if (TERMINAL_RELATED_TOOL_NAMES.has(toolName)) {
return terminalToolDisplayMode === 'detailed'
}
return !presentationMinimal
}
function ElapsedView(props: { from: number; active: boolean }) {
const [now, setNow] = useState(() => Date.now())
@@ -260,6 +278,7 @@ type ToolCardProps = {
api: ApiClient
sessionId: string
metadata: SessionMetadataSummary | null
terminalToolDisplayMode: TerminalToolDisplayMode
disabled: boolean
onDone: () => void
block: ToolCallBlock
@@ -289,7 +308,9 @@ function ToolCardInner(props: ToolCardProps) {
const subtitle = presentation.subtitle ?? props.block.tool.description
const taskSummary = renderTaskSummary(props.block, props.metadata, t)
const runningFrom = props.block.tool.startedAt ?? props.block.tool.createdAt
const showInline = !presentation.minimal && !isSubagentToolName(toolName)
const isCodexAgentCard = toolName === 'CodexAgent'
const useCompactTerminalCard = shouldUseCompactTerminalToolCard(toolName, props.terminalToolDisplayMode)
const showInline = shouldShowInlineToolCardBody(toolName, presentation.minimal, props.terminalToolDisplayMode)
const CompactToolView = showInline ? getToolViewComponent(toolName) : null
const FullToolView = getToolFullViewComponent(toolName)
const ResultToolView = getToolResultViewComponent(toolName)
@@ -297,7 +318,6 @@ function ToolCardInner(props: ToolCardProps) {
const isAskUserQuestion = isAskUserQuestionToolName(toolName)
const isRequestUserInput = isRequestUserInputToolName(toolName)
const isQuestionTool = isAskUserQuestion || isRequestUserInput
const isCodexAgentCard = toolName === 'CodexAgent'
const showsPermissionFooter = Boolean(permission && (
permission.status === 'pending'
|| ((permission.status === 'denied' || permission.status === 'canceled') && Boolean(permission.reason))
@@ -324,7 +344,7 @@ function ToolCardInner(props: ToolCardProps) {
{subtitle ? (
<CardDescription className={cn(
'font-mono text-xs text-[var(--app-tool-card-subtitle)]',
isCodexAgentCard ? 'truncate whitespace-nowrap' : 'break-all'
isCodexAgentCard || useCompactTerminalCard ? 'truncate whitespace-nowrap' : 'break-all'
)}>
{truncate(subtitle, 160)}
</CardDescription>