Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/desktop/src/app/chat/sidebar/session-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
11 changes: 4 additions & 7 deletions apps/desktop/src/components/assistant-ui/markdown-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
>
Expand Down
53 changes: 40 additions & 13 deletions apps/desktop/src/components/assistant-ui/tool/fallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down
11 changes: 3 additions & 8 deletions apps/desktop/src/components/chat/compact-markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
24 changes: 15 additions & 9 deletions apps/desktop/src/components/ui/pane-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,17 +72,17 @@ 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
className={cn(
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}
Expand Down
15 changes: 14 additions & 1 deletion apps/desktop/src/lib/external-link.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/lib/external-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
59 changes: 56 additions & 3 deletions apps/desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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%;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
Loading