feat(desktop): strip the header chrome off markdown code blocks

A fenced block carried a bordered card with a "Code · <lang>" title row and a
pinned copy button, which read as an attached artifact next to the reply. Drop
the border and the header: the block is now a tinted slab on --ui-bg-editor
with syntax highlighting and a copy button that reveals on hover. The streaming
glow moves entirely to box-shadow since there is no border left to animate.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 21:28:00 -05:00
parent cfd79e5b1b
commit b7fc36cf3c
3 changed files with 20 additions and 69 deletions

View file

@ -4,15 +4,16 @@ import { Codicon, type CodiconProps } from '@/components/ui/codicon'
import { cn } from '@/lib/utils'
/**
* Rounded-card shell for fenced code (and any equivalent: diffs, raw payloads,
* etc.) sized for the conversation column. Mirrors the expanded tool-row
* pattern so code blocks read as the same family of artifact.
* Rounded surface for fenced code (and any equivalent: diffs, raw payloads,
* etc.) sized for the conversation column. Background only no border, no
* header, no language label so a code block reads as a tinted slab of the
* reply rather than an attached artifact.
*/
function CodeCard({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
className={cn(
'min-w-0 max-w-full overflow-hidden rounded-[0.625rem] border border-border text-[length:var(--conversation-tool-font-size)] text-muted-foreground',
'group/code relative min-w-0 max-w-full overflow-hidden rounded-[0.625rem] bg-(--ui-bg-editor) [--expandable-fade-from:var(--ui-bg-editor)] text-[length:var(--conversation-tool-font-size)] text-muted-foreground',
className
)}
data-slot="code-card"
@ -21,31 +22,6 @@ function CodeCard({ className, ...props }: React.ComponentProps<'div'>) {
)
}
function CodeCardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
className={cn('flex items-center justify-between gap-2 border-b border-border px-2 py-1.5', className)}
data-slot="code-card-header"
{...props}
/>
)
}
function CodeCardTitle({ className, children, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
'flex min-w-0 items-center gap-1.5 truncate text-[length:var(--conversation-tool-font-size)] font-medium leading-(--conversation-line-height) text-foreground/80',
className
)}
data-slot="code-card-title"
{...props}
>
{children}
</span>
)
}
function CodeCardIcon({ className, ...props }: CodiconProps) {
return (
<Codicon
@ -56,12 +32,6 @@ function CodeCardIcon({ className, ...props }: CodiconProps) {
)
}
function CodeCardSubtitle({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span className={cn('font-normal text-muted-foreground', className)} data-slot="code-card-subtitle" {...props} />
)
}
function CodeCardBody({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
@ -75,4 +45,4 @@ function CodeCardBody({ className, ...props }: React.ComponentProps<'div'>) {
)
}
export { CodeCard, CodeCardBody, CodeCardHeader, CodeCardIcon, CodeCardSubtitle, CodeCardTitle }
export { CodeCard, CodeCardBody, CodeCardIcon }

View file

@ -4,24 +4,18 @@ import type { SyntaxHighlighterProps } from '@assistant-ui/react-streamdown'
import { type ComponentProps, type FC, lazy, Suspense, useMemo } from 'react'
import type ShikiHighlighter from 'react-shiki'
import {
CodeCard,
CodeCardBody,
CodeCardHeader,
CodeCardIcon,
CodeCardSubtitle,
CodeCardTitle
} from '@/components/chat/code-card'
import { CodeCard, CodeCardBody } from '@/components/chat/code-card'
import { ExpandableBlock } from '@/components/chat/expandable-block'
import { CopyButton } from '@/components/ui/copy-button'
import { useI18n } from '@/i18n'
import { codiconForLanguage, isLikelyProseCodeBlock, sanitizeLanguageTag } from '@/lib/markdown-code'
import { isLikelyProseCodeBlock } from '@/lib/markdown-code'
/**
* Streamdown's code adapter renders header + body as inline siblings, so we
* own the wrapping `<CodeCard>` here and neutralize the upstream
* `data-streamdown="code-block"` chrome from styles.css. Anything that wants
* a card-shaped code surface should compose `CodeCard*` directly.
* `data-streamdown="code-block"` chrome from styles.css. The card is
* background-only no header row, no language label so a fence reads as a
* tinted slab of the reply; copy is a hover-reveal control in the corner.
*
* `react-shiki` full bundle so all `bundledLanguages` work; theme switches
* follow the document `color-scheme` via `defaultColor="light-dark()"`.
@ -148,28 +142,19 @@ export const SyntaxHighlighter: FC<HermesSyntaxHighlighterProps> = ({
return <div className="aui-prose-fence whitespace-pre-wrap wrap-anywhere text-foreground">{trimmed}</div>
}
const cleanLanguage = sanitizeLanguageTag(language || '')
const label = cleanLanguage && cleanLanguage !== 'unknown' ? cleanLanguage : ''
const plain = defer || exceedsHighlightBudget(trimmed)
return (
<CodeCard data-streaming={defer ? 'true' : undefined}>
<CodeCardHeader>
<CodeCardTitle>
<CodeCardIcon name={codiconForLanguage(label)} />
{t.assistant.tool.code}
{label && <CodeCardSubtitle> · {label}</CodeCardSubtitle>}
</CodeCardTitle>
<CopyButton
appearance="inline"
className="-my-1 -mr-1 h-5 px-1 opacity-55 hover:opacity-100"
iconClassName="size-2.5"
label={t.assistant.tool.copyCode}
showLabel={false}
text={trimmed}
/>
</CodeCardHeader>
<CodeCardBody>
<CopyButton
appearance="inline"
className="absolute right-1.5 top-1.5 z-10 h-5 gap-0 rounded-md px-1 opacity-0 transition-opacity group-hover/code:opacity-100 focus-visible:opacity-100"
iconClassName="size-2.5"
label={t.assistant.tool.copyCode}
showLabel={false}
text={trimmed}
/>
<CodeCardBody className="[&_pre]:px-3 [&_pre]:py-2.5">
<ExpandableBlock>
<Pre className="aui-shiki m-0 overflow-hidden bg-transparent p-0">
{plain ? (

View file

@ -1356,7 +1356,6 @@ text-* variant utilities. */ .btn-arc {
margin-block: var(--paragraph-gap) 0;
position: relative;
transition:
border-color 180ms ease-out,
box-shadow 180ms ease-out,
background-color 180ms ease-out;
}
@ -1365,7 +1364,6 @@ text-* variant utilities. */ .btn-arc {
animation:
code-card-stream-enter 180ms cubic-bezier(0.16, 1, 0.3, 1) both,
code-card-stream-glow 1.8s ease-in-out 180ms infinite alternate;
border-color: color-mix(in srgb, var(--dt-ring) 24%, var(--ui-stroke-tertiary));
box-shadow:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 10%, transparent),
0 0.625rem 1.75rem color-mix(in srgb, var(--dt-ring) 8%, transparent);
@ -1678,14 +1676,12 @@ text-* variant utilities. */ .btn-arc {
@keyframes code-card-stream-glow {
from {
border-color: color-mix(in srgb, var(--dt-ring) 18%, var(--ui-stroke-tertiary));
box-shadow:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 6%, transparent),
0 0.5rem 1.5rem color-mix(in srgb, var(--dt-ring) 5%, transparent);
}
to {
border-color: color-mix(in srgb, var(--dt-ring) 32%, var(--ui-stroke-tertiary));
box-shadow:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 12%, transparent),
0 0.75rem 2rem color-mix(in srgb, var(--dt-ring) 10%, transparent);