refactor(ui-primitives): extract shared head/tail cap and copy-feedback helpers

The tail-header cap fix pushed SearchBlock's head/tail slicing arithmetic and
its copy-feedback hook over the duplication gate's threshold against the
byte-identical logic in TerminalBlock. Extract both into head-tail-cap.ts
(headTailCap) and use-copy-feedback.ts (useCopyFeedback) and consume them from
both blocks, deleting the clone rather than nudging it under the limit.
This commit is contained in:
Chinesezjc
2026-07-30 23:09:12 +08:00
parent 4a4ec6fd4d
commit e1408c2a44
4 changed files with 80 additions and 37 deletions

View File

@@ -10,7 +10,8 @@
import { useCallback, useState, type ReactNode } from 'react'
import clsx from 'clsx'
import { writeClipboard } from './clipboard.ts'
import { headTailCap } from './head-tail-cap.ts'
import { useCopyFeedback } from './use-copy-feedback.ts'
import css from './SearchBlock.module.css'
/**
@@ -173,23 +174,13 @@ export function SearchBlock(props: SearchBlockProps) {
const { truncated, total, maxLines = DEFAULT_SEARCH_MAX_LINES, className } = props
const [expanded, setExpanded] = useState(false)
const [collapsed, setCollapsed] = useState<ReadonlySet<number>>(() => new Set())
const [copied, setCopied] = useState(false)
// `props` is a fresh object each render, so memoizing on it never hits; the
// flatten is cheap, so it runs inline keyed on the collapse set instead.
const rows = toRows(props, collapsed)
const shown = shownCount(props)
const empty = rows.length === 0
const text = copyText(props)
const onCopy = useCallback(() => {
if (copied) return
void writeClipboard(text).then((ok) => {
if (!ok) return
setCopied(true)
window.setTimeout(() => { setCopied(false) }, 1000)
})
}, [copied, text])
const { copied, onCopy } = useCopyFeedback(copyText(props))
const onToggle = useCallback(() => { setExpanded(value => !value) }, [])
@@ -202,12 +193,7 @@ export function SearchBlock(props: SearchBlockProps) {
})
}, [])
const hidden = rows.length - maxLines
const capped = hidden > 0 && !expanded
// Same split arithmetic as TerminalBlock (and the TUI transcript's collapsed
// tool card), so a long result's head and tail slices agree across surfaces.
const headLines = Math.ceil(maxLines / 2)
const tailLines = maxLines - headLines
const { hidden, capped, headLines, tailLines } = headTailCap(rows.length, maxLines, expanded)
const head = capped ? rows.slice(0, headLines) : rows
const naturalTail = capped ? rows.slice(rows.length - tailLines) : []
// When the tail slice begins inside a file's matches, its own header sits