Merge pull request #71872 from NousResearch/bb/desktop-ui-polish

Desktop UI polish: link chips, sidebar arc, tab strip rule
This commit is contained in:
brooklyn! 2026-07-26 05:08:08 -05:00 committed by GitHub
commit 02721cc1c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 139 additions and 46 deletions

View file

@ -163,7 +163,9 @@ export function SidebarSessionRow({
style={style}
{...rest}
>
{sessionShowsRunningArc({ isWorking, needsInput }) && <span aria-hidden="true" className="arc-border" />}
{sessionShowsRunningArc({ isWorking, needsInput }) && (
<span aria-hidden="true" className="arc-border arc-row" />
)}
<SidebarRowBody
className={cn('z-0 group-hover:pr-12', branchStem && 'pl-3.5')}
// Middle-click = open in a new tab (browser muscle memory). Swallow

View file

@ -510,7 +510,7 @@ export const SessionRefLink: FC<{
return (
<a
className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
className="link-chip font-semibold wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()

View file

@ -100,7 +100,7 @@ function OpenMediaButton({ kind, path }: { kind: 'audio' | 'video'; path: string
return (
<span className="block">
<button
className="mt-2 bg-transparent text-xs font-medium text-muted-foreground underline underline-offset-4 decoration-current/20 hover:text-foreground"
className="mt-2 link-chip bg-transparent text-xs font-medium text-muted-foreground hover:text-foreground"
onClick={open}
type="button"
>
@ -196,7 +196,7 @@ function MediaAttachment({ path }: { path: string }) {
return (
<span className="wrap-anywhere">
<a
className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
className="link-chip font-semibold wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()
@ -246,10 +246,7 @@ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a
if (!target || !/^https?:\/\//i.test(target)) {
return (
<a
className={cn(
'font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere',
className
)}
className={cn('link-chip font-semibold wrap-anywhere', className)}
href={href}
rel="noopener noreferrer"
target="_blank"
@ -324,7 +321,7 @@ function MarkdownImage({ className, src, alt, ...props }: ComponentProps<'img'>)
<span className="my-2 block text-sm text-muted-foreground">
Couldn&apos;t load {name}.{' '}
<button
className="bg-transparent font-medium text-foreground underline underline-offset-4 decoration-current/20 hover:text-foreground"
className="link-chip bg-transparent font-medium text-foreground hover:text-foreground"
onClick={open}
type="button"
>

View file

@ -26,6 +26,7 @@ import { ZoomableImage } from '@/components/chat/zoomable-image'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { CopyButton } from '@/components/ui/copy-button'
import { DisclosureCaret } from '@/components/ui/disclosure-caret'
import { FadeText } from '@/components/ui/fade-text'
import { FileTypeIcon } from '@/components/ui/file-type-icon'
import { GlyphSpinner } from '@/components/ui/glyph-spinner'
@ -96,6 +97,44 @@ const TOOL_EXPANDED_SHELL_CLASS = 'rounded-[0.3125rem] border border-(--ui-strok
const TOOL_SECTION_PRE_CLASS = cn(TOOL_SECTION_SURFACE_CLASS, 'font-mono text-[0.7rem] leading-relaxed')
// Raw args/result dump — reference material, so a notch smaller than a body.
const TOOL_PAYLOAD_PRE_CLASS = cn(TOOL_SECTION_SURFACE_CLASS, 'font-mono text-[0.65rem] leading-relaxed')
/**
* Technical-mode raw payload, behind a chevron disclosure.
*
* Collapsed by default in technical mode every tool row carries one, and
* expanding them all buries the transcript. Uses `DisclosureCaret` rather than
* a native `<details>`, whose marker is a browser-drawn triangle matching
* nothing else here.
*/
function ToolPayloadDisclosure({ args, result }: { args: unknown; result: unknown }) {
const [open, setOpen] = useState(false)
return (
// `py-0.5` tops up the parent's `p-1.5` to an even block on both edges.
<div className="max-w-full py-0.5">
<button
aria-expanded={open}
className={cn(
TOOL_SECTION_LABEL_CLASS,
'mb-0 flex items-center gap-1 bg-transparent transition-colors hover:text-(--ui-text-secondary)'
)}
onClick={() => setOpen(value => !value)}
type="button"
>
<DisclosureCaret className="text-(--ui-text-tertiary)" open={open} size="0.625rem" />
Tool payload
</button>
{open && (
<pre className={cn(TOOL_PAYLOAD_PRE_CLASS, 'mt-1 whitespace-pre-wrap wrap-anywhere')}>
{technicalTrace(args, result)}
</pre>
)}
</div>
)
}
interface ToolStatusCopy {
statusDone: string
statusError: string
@ -616,19 +655,7 @@ function ToolEntry({ part }: ToolEntryProps) {
)}
</div>
))}
{toolViewMode === 'technical' && !(isFileEdit && view.inlineDiff) && (
<pre className={cn(TOOL_SECTION_PRE_CLASS, 'whitespace-pre-wrap wrap-anywhere')}>
{technicalTrace(part.args, part.result)}
</pre>
)}
{toolViewMode === 'technical' && isFileEdit && view.inlineDiff && (
<details className="max-w-full">
<summary className={cn(TOOL_SECTION_LABEL_CLASS, 'mb-0 cursor-pointer')}>Tool payload</summary>
<pre className={cn(TOOL_SECTION_PRE_CLASS, 'mt-1 whitespace-pre-wrap wrap-anywhere')}>
{technicalTrace(part.args, part.result)}
</pre>
</details>
)}
{toolViewMode === 'technical' && <ToolPayloadDisclosure args={part.args} result={part.result} />}
</div>
)}
</div>

View file

@ -2,7 +2,7 @@ import type { ComponentProps, ElementType, FC } from 'react'
import { memo } from 'react'
import { Streamdown } from 'streamdown'
import { ExternalLink, ExternalLinkIcon } from '@/lib/external-link'
import { ExternalLink } from '@/lib/external-link'
import { cn } from '@/lib/utils'
// Compact markdown renderer for tool detail bodies. Same Streamdown pipeline
@ -42,20 +42,15 @@ function tagged<T extends keyof typeof TAG_CLASSES>(Tag: T) {
function MarkdownAnchor({ children, className, href, ...rest }: ComponentProps<'a'>) {
if (!href || !/^https?:\/\//i.test(href)) {
return (
<a
className={cn('font-medium underline underline-offset-4 decoration-current/20', className)}
href={href}
{...rest}
>
<a className={cn('link-chip font-medium', className)} href={href} {...rest}>
{children}
</a>
)
}
return (
<ExternalLink className={cn('decoration-current/20', className)} href={href} showExternalIcon={false}>
<ExternalLink className={className} href={href}>
{children}
<ExternalLinkIcon />
</ExternalLink>
)
}

View file

@ -96,7 +96,7 @@ export const GeneratedImage: FC<{ aspectRatio?: string; result?: unknown }> = ({
if (failed && image) {
return (
<a
className="mt-2 inline-block font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
className="mt-2 link-chip inline-block font-semibold wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()

View file

@ -12,17 +12,23 @@ export const PANE_TAB_STRIP_LINE_RIGHT = 'shadow-[inset_-1px_0_0_var(--ui-stroke
const TAB =
'group/tab relative flex shrink-0 items-center border-transparent bg-(--tab-bg) text-[0.6875rem] font-medium [-webkit-app-region:no-drag]'
const TAB_HORIZONTAL = 'h-full min-w-0 max-w-48 border-b not-first:border-l not-first:border-l-(--ui-stroke-quaternary)'
// Stop 1px short: the strip's rule is an INSET shadow painted in the
// container's own last pixel row, so a full-height tab covers it and reads as
// overhanging the bar. The container owns the line; nothing redraws it.
const TAB_HORIZONTAL =
'h-[calc(100%-1px)] min-w-0 max-w-48 not-first:border-l not-first:border-l-(--ui-stroke-quaternary)'
const TAB_VERTICAL =
'w-full max-h-48 justify-center not-first:border-t not-first:border-t-(--ui-stroke-quaternary) [writing-mode:vertical-rl]'
const TAB_ACTIVE = 'text-foreground [--tab-bg:var(--pane-tab-active-bg,var(--ui-editor-surface-background))]'
// Full height, so the active tab alone covers that row and cuts the rule.
const TAB_ACTIVE =
'h-full text-foreground [--tab-bg:var(--pane-tab-active-bg,var(--ui-editor-surface-background))]'
// Inactive = gutter. Hover = 4% translucent wash (VS Code/GitHub alpha hover),
// not an opaque recolor — and never touch borders.
// Inactive = gutter. Hover DARKENS: active is the lighter content surface, so a
// lightening wash made the two nearly indistinguishable.
const TAB_IDLE =
'text-(--ui-text-tertiary) [--tab-bg:var(--pane-tab-strip-bg,var(--theme-card-seed))] hover:shadow-[inset_0_0_0_100vmax_color-mix(in_srgb,var(--ui-base)_4%,transparent)] hover:text-(--ui-text-secondary)'
'text-(--ui-text-tertiary) [--tab-bg:var(--pane-tab-strip-bg,var(--theme-card-seed))] hover:shadow-[inset_0_0_0_100vmax_color-mix(in_srgb,#000_var(--ui-tab-hover-darken),transparent)] hover:text-(--ui-text-secondary)'
interface PaneTabProps extends React.ComponentProps<'div'> {
active?: boolean
@ -66,9 +72,9 @@ export const PaneTab = React.forwardRef<HTMLDivElement, PaneTabProps>(function P
},
ref
) {
// Content-facing edge: horizontal cuts the bottom strip line; vertical cuts
// the side that faces the editor (left rail → right edge, right rail → left).
const edge = vertical ? (side === 'right' ? 'border-l' : 'border-r') : 'border-b'
// Vertical rails only. Horizontal tabs draw no bottom border — the strip owns
// that rule, and a per-tab border stacked a second translucent line over it.
const edge = vertical ? (side === 'right' ? 'border-l' : 'border-r') : undefined
return (
<div
@ -76,7 +82,7 @@ export const PaneTab = React.forwardRef<HTMLDivElement, PaneTabProps>(function P
TAB,
vertical ? TAB_VERTICAL : TAB_HORIZONTAL,
edge,
active ? TAB_ACTIVE : cn(TAB_IDLE, `${edge}-(--ui-stroke-tertiary)`),
active ? TAB_ACTIVE : cn(TAB_IDLE, edge && `${edge}-(--ui-stroke-tertiary)`),
className
)}
data-active={active}

View file

@ -112,11 +112,24 @@ describe('external link helpers', () => {
expect(openExternal).toHaveBeenCalledWith('https://example.com/path/to/resource')
})
it('shows a trailing external-link icon', () => {
it('hides the trailing external-link icon by default', () => {
installDesktopBridge()
render(<ExternalLink href="https://example.com/path/to/resource">Example link</ExternalLink>)
const link = screen.getByRole('link', { name: 'Example link' })
expect(link.querySelector('svg')).toBeNull()
})
it('shows a trailing external-link icon when opted in', () => {
installDesktopBridge()
render(
<ExternalLink href="https://example.com/path/to/resource" showExternalIcon>
Example link
</ExternalLink>,
)
const link = screen.getByRole('link', { name: 'Example link' })
expect(link.querySelector('svg')).toBeTruthy()
})

View file

@ -215,14 +215,14 @@ export function ExternalLink({
className,
href,
onClick,
showExternalIcon = true,
showExternalIcon = false,
...rest
}: ExternalLinkProps) {
const target = normalizeExternalUrl(href)
return (
<a
className={cn('font-semibold text-foreground underline underline-offset-4 decoration-current/20', className)}
className={cn('link-chip font-semibold', className)}
href={target}
onClick={event => {
event.stopPropagation()

View file

@ -317,6 +317,10 @@
:root.dark bumps the alpha since a dark card swallows shadow. Removed on
focus (the focus border carries the state). */
--dt-input-inset: inset 0 1px 1px color-mix(in srgb, #000 10%, transparent);
/* Hovered inactive tab: deepen it AWAY from the lighter active surface, so
hover can never be mistaken for selected. Dark mode needs more black
over a dark gutter shows less. */
--ui-tab-hover-darken: 2.5%;
--dt-ring: var(--ui-stroke-primary);
--dt-midground: var(--theme-midground);
--dt-composer-ring: var(--ui-base);
@ -446,6 +450,7 @@
--backdrop-invert-mul: 0;
/* Dark mode: a dark card needs a stronger black inset to show the recess. */
--dt-input-inset: inset 0 1px 1px color-mix(in srgb, #000 38%, transparent);
--ui-tab-hover-darken: 6%;
/* Dark needs a lighter resting border than light mode. */
--dt-input-border: 4%;
@ -514,6 +519,32 @@
background: repeating-conic-gradient(currentColor 0% 25%, transparent 0% 50%) 0 0 / 0.125rem 0.125rem;
}
/* Inline content links: a small tinted chip instead of an underline. Tint is
currentColor-relative (the old `decoration-current/20` idiom), so text and
fill share a hue on every theme from one `color` declaration. */
.link-chip {
--link-chip-tint: 8%;
/* `ch`/`em` so the chip tracks the text at any size. Block padding is
asymmetric because an inline box's content area is ascent+descent equal
padding paints the glyphs high. Keep it small: `padding-block` doesn't grow
the line box, so an over-padded chip creeps into the line above. */
padding: 0.05ch 0.5ch 0.2ch;
border-radius: 0.25rem;
color: var(--dt-primary);
background: color-mix(in srgb, currentColor var(--link-chip-tint), transparent);
/* Explicit: the base layer underlines every `a` (see the :where(a, …) reset). */
text-decoration: none;
/* A wrapped link gets a chip per line fragment, not one ragged box. */
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
transition: background-color 0.12s ease;
}
.link-chip:hover {
--link-chip-tint: 15%;
}
/* Hover-reveal suppression the shared, declarative escape hatch.
A collapsed pane slides in when the pointer dwells on its thin edge trigger.
Controls that sit over that edge gutter would drag the panel in by accident.
@ -610,14 +641,19 @@
--arc-c2: var(--dt-background);
--arc-angle: 160deg;
--arc-width: 0.078125rem;
--arc-inset: -0.125rem;
/* How far the ring stands off its host, per side. */
--arc-standoff: 0.125rem;
/* An outset ring is only concentric when its radius grows by the standoff
(r = r_host + gap); `border-radius: inherit` leaves the corner pinched.
Defaults to the shared `rounded-md`; a host on another radius sets it. */
--arc-radius: var(--radius-md);
--arc-duration: 2.23s;
pointer-events: none;
position: absolute;
overflow: hidden;
border-radius: inherit;
inset: var(--arc-inset);
border-radius: calc(var(--arc-radius) + var(--arc-standoff));
inset: calc(var(--arc-standoff) * -1);
padding: var(--arc-width);
mask:
linear-gradient(#000 0 0) content-box,
@ -719,6 +755,23 @@
--arc-duration: 3.27s;
}
/* Sidebar session row a thin, clipped, single-line host, so it differs twice
from the default ring:
1. Flush, not outset. The sidebar scroller is `overflow-x-hidden` and rows sit
`gap-px` apart, so an outset ring loses its left/right runs to the clip.
2. A visible tail. `--arc-c2` defaults to `--dt-background` invisible
against the sidebar. With c0 a transparent sentinel and c2 invisible, only
the bright c1 stop survives each pass, and the gradient has three of them:
three moving dots instead of an arc. Tying c2 to c1 gives each spike a
falloff. (`arc-nous` already overrides c2, so the hero is unaffected.) */
.arc-border.arc-row,
:root.dark .arc-border.arc-row {
--arc-standoff: 0rem;
--arc-c2: color-mix(in srgb, var(--arc-c1) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.arc-border::before {
animation: none;