Files
deepseek-harness/packages/client/ui-primitives/src/Pill.tsx
T
Chinesezjc 5081697aaf feat(web): render bash tool output as a terminal card
The bash tool already declares the `card: 'terminal'` render intent for
both its call and its result, and host/connection/runtime already deliver
it to the browser as callView/resultView. The Web client ignored it:
rows derived from raw args, and the details panel flattened every tool's
content into one soft-wrapping `<pre>`. Column-aligned output folded into
a paragraph and a long listing stretched the panel without bound.

`TerminalBlock` (ui-primitives) renders a command as a terminal surface:
a shortened-cwd prompt line, output at `white-space: pre` in a
horizontally scrolling box, a head/tail height cap with an expand
control, an exit-code/signal status pill, and a copy control for the raw
output. ANSI SGR runs are parsed with `anser` and resolved onto `--dsw-*`
theme tokens, with literal rgb kept for values the design system has no
token for. Geometry and fonts mirror CodeBlock; the clipboard write both
need moved into a package-internal `clipboard.ts`.

Both Web render sites for a bash call consume the intent through one
derivation (`terminal-card-model.ts`), so they cannot disagree about a
command, its cwd, or its exit status: the keyed BashRow carries the card
resident below its summary row, and the render-site fallback row keeps it
behind its existing expand control. Rows cap at 8 lines against the
panel's 16.

Inline output in the chat row reverses this package's stated
no-inline-output convention, on the owner's explicit decision; the Agent
Note records the reversal and its bound.

Tests: TerminalBlock/ansi/clipboard unit specs, ui-conversation wiring
specs at every render site, a built-client-graph snapshot covering both
chat-row shapes, and a real-browser e2e asserting the no-wrap layout and
the page's own Clipboard API.
2026-07-28 14:58:06 +08:00

34 lines
1.1 KiB
TypeScript

// Pill: small rounded label chip (view switcher tabs, filters, badges).
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import clsx from 'clsx'
import css from './Pill.module.css'
/**
* Render a pill chip. Interactive when onClick is supplied (renders a button);
* otherwise a static span.
* @param props.active - selected/active visual state.
* @returns pill element.
*/
export function Pill({ active = false, className, children, onClick, ...rest }: {
active?: boolean
// `| undefined` so a caller can forward an optional class straight through
// under exactOptionalPropertyTypes (a CSS-module lookup is string|undefined).
className?: string | undefined
children?: ReactNode
} & ButtonHTMLAttributes<HTMLButtonElement>) {
if (!onClick) {
return <span className={clsx(css.pill, active && css.active, className)}>{children}</span>
}
return (
<button
type="button"
className={clsx(css.pill, css.interactive, active && css.active, className)}
onClick={onClick}
{...rest}
>
{children}
</button>
)
}