Merge remote-tracking branch 'origin/feat/search-presenter' into feat/web-search-card

# Conflicts:
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/src/client/chat/GenericToolCard.tsx
#	packages/client/ui-conversation/src/client/chat/ToolRow.module.css
#	packages/client/ui-conversation/src/client/chat/ToolRow.tsx
#	packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx
#	packages/client/ui-primitives/README.i18n.yaml
#	packages/client/ui-primitives/src/index.ts
This commit is contained in:
Chinesezjc
2026-07-31 12:27:30 +08:00
948 changed files with 28959 additions and 4629 deletions

View File

@@ -21,6 +21,54 @@ import css from './TerminalBlock.module.css'
*/
export const DEFAULT_TERMINAL_MAX_LINES = 16
/**
* Display copy for the terminal surface; the owner passes localized labels
* (this package is cordis-free, so copy arrives via props). Every field
* defaults to the current built-in value, so existing consumers render
* unchanged.
*/
export interface TerminalBlockLabels {
/** Status pill text for a signal-terminated command. */
signal: (signal: string) => string
/** Status pill text for a non-zero exit code. */
exitCode: (exitCode: number) => string
/** Run-state text while the command is still running. */
running: string
/** Run-state text for a signal or non-zero-exit settle. */
failed: string
/** Run-state text for a clean settle. */
done: string
/** Copy-button idle label. */
copy: string
/** Copy-button label during the post-copy confirmation window. */
copied: string
/** Placeholder when a settled command produced no visible output. */
noOutput: string
/** Collapse-toggle aria label while expanded. */
collapseAria: string
/** Collapse-toggle text while expanded. */
collapse: string
/** Expand-toggle aria label while capped, given the hidden line count. */
expandAria: (hidden: number) => string
/** Expand-toggle text while capped, given the hidden line count. */
expand: (hidden: number) => string
}
const DEFAULT_LABELS: TerminalBlockLabels = {
signal: signal => `信号 ${signal}`,
exitCode: exitCode => `退出码 ${exitCode}`,
running: '运行中',
failed: '失败',
done: '已完成',
copy: '复制',
copied: '复制成功',
noOutput: '无输出',
collapseAria: '收起输出',
collapse: '收起',
expandAria: hidden => `展开其余 ${hidden} 行输出`,
expand: hidden => `… 其余 ${hidden} 行`,
}
export interface TerminalBlockProps {
/** The command line, rendered verbatim after the prompt label. */
command: string
@@ -36,10 +84,12 @@ export interface TerminalBlockProps {
signal?: string | undefined
/** The command is still running: the block shows the prompt line alone. */
running?: boolean | undefined
/** Height cap in output lines before the middle collapses (default {@link DEFAULT_TERMINAL_MAX_LINES}). */
/** Height cap in output lines before the middle collapses (default {@link DEFAULT_TERMINAL_MAX_LINES}); Infinity disables the cap. */
maxLines?: number | undefined
/** Extra class merged onto the wrapper (callers position; this component draws). */
className?: string | undefined
/** Localized display copy; omitted fields keep the built-in defaults. */
labels?: Partial<TerminalBlockLabels> | undefined
}
/**
@@ -64,11 +114,16 @@ function promptLabel(cwd: string, home: string | undefined): string {
* distinction the bash tool's own exit-status markers draw.
* @param exitCode - settled exit code, when known.
* @param signal - settled terminating signal name, when known.
* @param labels - display copy for the pill text.
* @returns the pill text, or undefined for a clean exit.
*/
function statusText(exitCode: number | undefined, signal: string | undefined): string | undefined {
if (signal !== undefined) return `信号 ${signal}`
if (exitCode !== undefined && exitCode !== 0) return `退出码 ${exitCode}`
function statusText(
exitCode: number | undefined,
signal: string | undefined,
labels: TerminalBlockLabels,
): string | undefined {
if (signal !== undefined) return labels.signal(signal)
if (exitCode !== undefined && exitCode !== 0) return labels.exitCode(exitCode)
return undefined
}
@@ -85,16 +140,18 @@ function statusText(exitCode: number | undefined, signal: string | undefined): s
* @param running - the command has not settled.
* @param exitCode - settled exit code, when known.
* @param signal - settled terminating signal name, when known.
* @param labels - display copy for the text label.
* @returns the dot's state and its text label, since the dot is aria-hidden.
*/
function runState(
running: boolean,
exitCode: number | undefined,
signal: string | undefined,
labels: TerminalBlockLabels,
): { state: StateDotState; label: string } {
if (running) return { state: 'ongoing', label: '运行中' }
if (statusText(exitCode, signal) !== undefined) return { state: 'error', label: '失败' }
return { state: 'done', label: '已完成' }
if (running) return { state: 'ongoing', label: labels.running }
if (statusText(exitCode, signal, labels) !== undefined) return { state: 'error', label: labels.failed }
return { state: 'done', label: labels.done }
}
/**
@@ -124,7 +181,12 @@ export function TerminalBlock({
running = false,
maxLines = DEFAULT_TERMINAL_MAX_LINES,
className,
labels,
}: TerminalBlockProps) {
const copy = useMemo<TerminalBlockLabels>(
() => (labels === undefined ? DEFAULT_LABELS : { ...DEFAULT_LABELS, ...labels }),
[labels],
)
const text = output ?? ''
// A command's output ends with a newline; that terminator is not an extra
// blank line to draw or to count against the height cap. The check runs on the
@@ -147,8 +209,8 @@ export function TerminalBlock({
const onToggle = useCallback(() => { setExpanded(value => !value) }, [])
const status = statusText(exitCode, signal)
const state = runState(running, exitCode, signal)
const status = statusText(exitCode, signal, copy)
const state = runState(running, exitCode, signal, copy)
// A multi-line command gets one prompt row per line, so a two-command shell
// snippet reads as the two commands it is instead of collapsing into one
// ellipsized row. A trailing newline is a terminator, not an empty command.
@@ -192,12 +254,12 @@ export function TerminalBlock({
{status !== undefined && <Pill className={css.status}>{status}</Pill>}
{!running && !empty && (
<button type="button" className={css.copyButton} onClick={onCopy}>
{copied ? '复制成功' : '复制'}
{copied ? copy.copied : copy.copy}
</button>
)}
</div>
{!running && (empty
? <div className={css.empty}>无输出</div>
? <div className={css.empty}>{copy.noOutput}</div>
: (
<div className={css.output}>
{(capped ? lines.slice(0, headLines) : lines).map((line, index) => (
@@ -208,10 +270,10 @@ export function TerminalBlock({
type="button"
className={css.expand}
aria-expanded={expanded}
aria-label={expanded ? '收起输出' : `展开其余 ${hidden} 行输出`}
aria-label={expanded ? copy.collapseAria : copy.expandAria(hidden)}
onClick={onToggle}
>
{expanded ? '收起' : `… 其余 ${hidden} 行`}
{expanded ? copy.collapse : copy.expand(hidden)}
</button>
)}
{capped && lines.slice(lines.length - tailLines).map((line, index) => (