mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
// Package-internal clipboard write, shared by every copy control in this
|
|
// package (CodeBlock's code copy, TerminalBlock's output copy). Not part of the
|
|
// public surface: consumers get the components, not the host detection.
|
|
|
|
/**
|
|
* Write text to the host clipboard, preferring the async Clipboard API and
|
|
* falling back to `execCommand('copy')` on hosts (jsdom, insecure contexts)
|
|
* that omit it.
|
|
* @param text - the exact text to place on the clipboard.
|
|
* @returns true only when the host accepted the write.
|
|
*/
|
|
export async function writeClipboard(text: string): Promise<boolean> {
|
|
// lib.dom types clipboard non-optional, but insecure contexts omit it —
|
|
// that runtime gap is exactly what this guard detects.
|
|
/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
|
|
if (navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
return true
|
|
} catch {
|
|
// Denied permissions / iframe policy — do not claim success.
|
|
return false
|
|
}
|
|
}
|
|
// jsdom and older hosts: best-effort execCommand path when present.
|
|
// execCommand('copy') is the only clipboard fallback where the async API
|
|
// is missing; deprecated but deliberately retained.
|
|
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
const exec = typeof document.execCommand === 'function'
|
|
? document.execCommand.bind(document)
|
|
: undefined
|
|
if (exec === undefined) return false
|
|
const el = document.createElement('textarea')
|
|
el.value = text
|
|
el.setAttribute('readonly', '')
|
|
el.style.position = 'fixed'
|
|
el.style.left = '-9999px'
|
|
document.body.appendChild(el)
|
|
el.select()
|
|
try {
|
|
return exec('copy')
|
|
} catch {
|
|
return false
|
|
} finally {
|
|
el.remove()
|
|
}
|
|
/* eslint-enable @typescript-eslint/no-deprecated */
|
|
}
|