Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
654df84
fix(web): context chips drop their pill and stay readable on every theme
flamboh Oct 25, 2026
ca348e5
fix(web): context chips reach 5:1 on the standard light bubble and wh…
flamboh Oct 25, 2026
5b7a175
fix(web): context chips hold 5:1 on painted surfaces and neutral chip…
flamboh Oct 25, 2026
82b52c7
fix(web): context chips sit on the text baseline at body size
flamboh Oct 26, 2026
e22dde5
fix(web): document chips keep their type colour at 5:1 contrast
flamboh Oct 26, 2026
f0ad3d1
fix(web): long composer chips truncate instead of overflowing the line
flamboh Oct 26, 2026
452c871
fix(web): context chips underline on hover and hug punctuation like l…
flamboh Oct 26, 2026
d7ec1ca
fix(web): document icons in chips use the chip's solved colour
flamboh Oct 26, 2026
cdea5c4
fix(web): document icons in chips follow the chip colour past their i…
flamboh Oct 26, 2026
64f44f5
refactor(web): bubble chip colour moves from global CSS to the bubble…
flamboh Oct 26, 2026
101916d
fix(web): focusable span chips underline on keyboard focus
flamboh Oct 26, 2026
e50cd63
fix(web): context chips keep their existing keyboard focus outline
flamboh Oct 26, 2026
ffca6ec
fix(web): context chips use the app's default focus outline
flamboh Oct 26, 2026
4ed3cf4
fix(web): chip labels truncate without poking out of the chip
flamboh Oct 26, 2026
eab6cdd
Merge remote-tracking branch 'github/main' into maria/pr-25066-head
maria-rcks Oct 26, 2026
1d73574
fix(web): preserve chip contrast and playback cues
maria-rcks Oct 26, 2026
2cd8376
fix(web): keep chip focus outlines inside their bounds
maria-rcks Oct 26, 2026
f4cb49e
fix(web): chip focus outlines follow the app's rounded focus corners
flamboh Oct 26, 2026
c0bc12b
fix(web): chip focus outlines are as tall as markdown link outlines
flamboh Oct 26, 2026
c0db0f2
Merge remote-tracking branch 'github/main' into maria/pr-25066-head
maria-rcks Oct 27, 2026
b506460
fix(web): keep chip contrast on redesigned dark bubbles
maria-rcks Oct 27, 2026
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
11 changes: 4 additions & 7 deletions apps/web/src/components/ComposerPromptEditorTiptap.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { documentIconColorForFileName } from "~/pierre-icons";
import { documentChipKindForFileName } from "~/pierre-icons";
import { isProviderSkillUserInvocable } from "@t3tools/client-runtime/providerSkills";
import { formatProviderSkillDisplayName } from "@t3tools/shared/inlineSkills";
import {
Expand Down Expand Up @@ -46,7 +46,6 @@ import {
useLayoutEffect,
useRef,
useState,
type CSSProperties,
type KeyboardEvent as ReactKeyboardEvent,
type RefObject,
} from "react";
Expand Down Expand Up @@ -232,7 +231,7 @@ function resolvedThemeFromDocument(): "light" | "dark" {
}

const CHIP_NODE_SELECTION_CLASS_NAME =
"relative inline-flex select-none items-center align-middle leading-none data-[composer-chip-selected]:after:pointer-events-none data-[composer-chip-selected]:after:absolute data-[composer-chip-selected]:after:inset-0 data-[composer-chip-selected]:after:rounded-sm data-[composer-chip-selected]:after:bg-[Highlight] data-[composer-chip-selected]:after:opacity-30 data-[composer-chip-selected]:after:content-['']";
"relative inline-flex min-w-0 max-w-full select-none items-baseline data-[composer-chip-selected]:after:pointer-events-none data-[composer-chip-selected]:after:absolute data-[composer-chip-selected]:after:inset-0 data-[composer-chip-selected]:after:rounded-sm data-[composer-chip-selected]:after:bg-[Highlight] data-[composer-chip-selected]:after:opacity-30 data-[composer-chip-selected]:after:content-['']";

const ComposerMentionExtension = Node.create({
name: "composer-mention",
Expand Down Expand Up @@ -260,11 +259,9 @@ const ComposerMentionExtension = Node.create({
function ComposerMentionNodeView({ node }: NodeViewProps) {
const actions = use(ComposerContextActionsContext);
const path = (node.attrs.path as string) ?? "";
const accent = documentIconColorForFileName(path);
const chip = (
<ContextChip
kind="mention"
style={accent ? ({ "--context-chip-accent": accent } as CSSProperties) : undefined}
kind={documentChipKindForFileName(path) ?? "mention"}
dir="ltr"
render={<button type="button" />}
onClick={() => actions.openMention(path)}
Expand Down Expand Up @@ -442,7 +439,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {
return (
<NodeViewWrapper
as="span"
className={cn(CHIP_NODE_SELECTION_CLASS_NAME, "min-w-0 max-w-full")}
className={CHIP_NODE_SELECTION_CLASS_NAME}
contentEditable={false}
spellCheck={false}
data-composer-citation-chip="true"
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/components/ContextChip.browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FileIcon } from "lucide-react";
import { describe, expect, it } from "vite-plus/test";

import "~/index.css";
import { render } from "../../test/render";
import { ContextChip, ContextChipLabel } from "./ContextChip";

// An empty inline-block's bottom edge sits on the baseline of the line it is in.
function BaselineMarker(props: { id: string }) {
return <span data-baseline={props.id} className="inline-block size-0" />;
}

describe("ContextChip", () => {
it("sets its label on the sentence baseline at the sentence size without growing the line", async () => {
await using screen = await render(
<p className="w-[60ch] text-sm leading-relaxed">
Open
<BaselineMarker id="text" /> the{" "}
<ContextChip kind="mention" render={<button type="button" />}>
<FileIcon />
<ContextChipLabel>
src/
<BaselineMarker id="chip" />
Typography.tsx
</ContextChipLabel>
</ContextChip>{" "}
file
</p>,
);
const paragraph = screen.container.querySelector("p")!;
const label = screen.container.querySelector<HTMLElement>('[data-slot="context-chip-label"]')!;
const baseline = (id: string) =>
screen.container.querySelector(`[data-baseline="${id}"]`)!.getBoundingClientRect().bottom;

expect(getComputedStyle(label).fontSize).toBe(getComputedStyle(paragraph).fontSize);
expect(Math.abs(baseline("chip") - baseline("text"))).toBeLessThanOrEqual(0.5);
expect(paragraph.getBoundingClientRect().height).toBeCloseTo(
Number.parseFloat(getComputedStyle(paragraph).lineHeight),
0,
);
});
});
79 changes: 31 additions & 48 deletions apps/web/src/components/ContextChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,40 @@ import type * as React from "react";
import { cn } from "~/lib/utils";

const contextChipVariants = cva(
"inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] border px-[0.5em] align-middle font-medium text-[0.86em] leading-none [&_svg]:block [&_svg]:size-[1.17em] [&_svg]:shrink-0 [&_svg]:self-center [button&,a&,[data-popup-open]&]:cursor-pointer [button&,a&]:transition-colors [button&,a&]:motion-reduce:transition-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-foreground disabled:cursor-default",
"inline-flex max-w-full items-center gap-[0.2em] font-medium leading-[normal] text-(--context-chip-ink) [--context-chip-ink:var(--context-chip-accent)] [--document-icon-stroke:var(--context-chip-ink)] [&_svg]:block [&_svg]:size-[1em] [&_svg]:shrink-0 [&_svg]:self-center [button&,a&,[data-popup-open]&]:cursor-pointer focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current rounded-sm disabled:cursor-default",
{
defaultVariants: { kind: "neutral" },
variants: {
kind: {
neutral: "border-border/70 bg-accent/40 text-foreground",
image: "[--context-chip-accent:oklch(0.62_0.16_16)]",
video: "[--context-chip-accent:oklch(0.62_0.16_48)]",
file: "[--context-chip-accent:oklch(0.62_0.136_237)]",
mention: "[--context-chip-accent:oklch(0.62_0.11_215)]",
terminal: "[--context-chip-accent:oklch(0.62_0.134_163)]",
element: "[--context-chip-accent:oklch(0.62_0.134_70)]",
"preview-annotation": "[--context-chip-accent:oklch(0.62_0.134_70)]",
"review-comment": "[--context-chip-accent:oklch(0.62_0.16_292)]",
"pull-request": "[--context-chip-accent:oklch(0.62_0.16_277)]",
"pr-open": "[--context-chip-accent:oklch(0.62_0.134_163)]",
"pr-draft": "[--context-chip-accent:oklch(0.62_0.02_259)]",
"pr-merged": "[--context-chip-accent:oklch(0.62_0.16_292)]",
"pr-closed": "[--context-chip-accent:oklch(0.62_0.16_16)]",
skill: "[--context-chip-accent:oklch(0.62_0.16_322)]",
thread: "[--context-chip-accent:oklch(0.62_0.12_190)]",
citation: "[--context-chip-accent:oklch(0.62_0.16_259)]",
neutral: "[--context-chip-accent:currentColor]",
image: "[--context-chip-accent:var(--context-chip-image)]",
video: "[--context-chip-accent:var(--context-chip-video)]",
file: "[--context-chip-accent:var(--context-chip-file)]",
mention: "[--context-chip-accent:var(--context-chip-mention)]",
terminal: "[--context-chip-accent:var(--context-chip-terminal)]",
element: "[--context-chip-accent:var(--context-chip-element)]",
"preview-annotation": "[--context-chip-accent:var(--context-chip-preview-annotation)]",
"review-comment": "[--context-chip-accent:var(--context-chip-review-comment)]",
"pull-request": "[--context-chip-accent:var(--context-chip-pull-request)]",
"pr-open": "[--context-chip-accent:var(--context-chip-pr-open)]",
"pr-draft": "[--context-chip-accent:var(--context-chip-pr-draft)]",
"pr-merged": "[--context-chip-accent:var(--context-chip-pr-merged)]",
"pr-closed": "[--context-chip-accent:var(--context-chip-pr-closed)]",
skill: "[--context-chip-accent:var(--context-chip-skill)]",
thread: "[--context-chip-accent:var(--context-chip-thread)]",
citation: "[--context-chip-accent:var(--context-chip-citation)]",
"doc-pdf": "[--context-chip-accent:var(--context-chip-doc-pdf)]",
"doc-word": "[--context-chip-accent:var(--context-chip-doc-word)]",
"doc-spreadsheet": "[--context-chip-accent:var(--context-chip-doc-spreadsheet)]",
"doc-slides": "[--context-chip-accent:var(--context-chip-doc-slides)]",
"doc-epub": "[--context-chip-accent:var(--context-chip-doc-epub)]",
},
state: {
unresolved: "border-dashed",
invalid: "",
unresolved: "data-[state=unresolved]:[--context-chip-accent:currentColor]",
invalid:
"data-[state=invalid]:[--context-chip-accent:var(--context-chip-invalid,var(--destructive-foreground))]",
},
},
compoundVariants: [
{
kind: [
"image",
"video",
"file",
"mention",
"terminal",
"element",
"preview-annotation",
"review-comment",
"pull-request",
"pr-open",
"pr-draft",
"pr-merged",
"pr-closed",
"skill",
"thread",
"citation",
],
className:
"[--context-chip-border:color-mix(in_oklab,var(--context-chip-accent)_34%,var(--contrast-border))] [--context-chip-border-hover:color-mix(in_oklab,var(--context-chip-accent)_48%,var(--contrast-border))] [--context-chip-foreground:color-mix(in_oklab,var(--context-chip-accent)_22%,var(--contrast-foreground))] border-(--context-chip-border) bg-(--context-chip-accent)/11 text-(--context-chip-foreground) [button:enabled&,a&]:hover:border-(--context-chip-border-hover) [button:enabled&,a&]:hover:bg-(--context-chip-accent)/17",
},
{ state: "unresolved", className: "text-foreground" },
{ state: "invalid", className: "border-destructive/35 bg-destructive/8 text-destructive" },
],
},
);

Expand All @@ -86,7 +66,10 @@ function ContextChip({ className, kind, state, render, ...props }: ContextChipPr
function ContextChipLabel({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn("block min-w-0 self-center truncate leading-tight", className)}
className={cn(
"block min-w-0 self-baseline overflow-x-clip text-ellipsis whitespace-nowrap underline decoration-transparent decoration-1 underline-offset-[0.2em] transition-colors motion-reduce:transition-none [:is(button:enabled,a):hover>&]:decoration-current",
className,
)}
data-slot="context-chip-label"
{...props}
/>
Expand All @@ -96,7 +79,7 @@ function ContextChipLabel({ className, ...props }: React.ComponentProps<"span">)
function ContextChipAction({ className, render, ...props }: useRender.ComponentProps<"button">) {
const defaultProps = {
className: cn(
"ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-(--context-chip-accent,var(--color-foreground))/17 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]",
"ml-[0.17em] inline-flex size-[1em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-(--context-chip-accent,var(--color-foreground))/17 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]",
className,
),
"data-slot": "context-chip-action",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ThreadContextChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function ThreadContextChip(props: {
className="no-underline"
>
{project ? (
<ProjectFavicon project={project} className="size-[1.17em] self-center" />
<ProjectFavicon project={project} className="size-[1em] self-center" />
) : (
<MessagesSquareIcon />
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/AssistantCitationChip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export function AssistantCitationChip({
const composerSourceLink = (
<Link
{...sourceLinkProps}
className="inline-flex h-full min-w-0 items-center gap-[0.33em] rounded-sm text-inherit no-underline focus-visible:outline-2 focus-visible:outline-foreground"
className="inline-flex min-w-0 items-center gap-[0.2em] self-baseline text-inherit no-underline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-current rounded-sm"
aria-label={`View cited assistant text: ${label}`}
>
<QuoteIcon aria-hidden="true" />
Expand Down
13 changes: 5 additions & 8 deletions apps/web/src/components/chat/MarkdownFileLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
type AtomCommandResult,
} from "@t3tools/client-runtime/state/runtime";
import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts";
import { memo, type CSSProperties, type MouseEvent as ReactMouseEvent } from "react";
import { memo, type MouseEvent as ReactMouseEvent } from "react";

import { readLocalApi } from "../../localApi";
import { cn } from "../../lib/utils";
import { shouldOpenMarkdownFileLinkInEditor } from "../../markdown-links";
import type { markdownFileCopyPaths } from "../../markdownFileHostPath";
import { documentIconColorForFileName } from "../../pierre-icons";
import { documentChipKindForFileName } from "../../pierre-icons";
import { ContextChip } from "../ContextChip";
import { stackedThreadToast, toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand Down Expand Up @@ -321,20 +321,18 @@ export const MarkdownFileLink = memo(function MarkdownFileLink({
canOpenInBrowser,
canOpenInPanel,
});
const accent = documentIconColorForFileName(iconPath);
const chipStyle = accent ? ({ "--context-chip-accent": accent } as CSSProperties) : undefined;
const chipKind = documentChipKindForFileName(iconPath) ?? "mention";

return (
<Tooltip>
<TooltipTrigger
render={
hasPrimaryAction ? (
<ContextChip
kind="mention"
kind={chipKind}
dir="ltr"
render={<a href={href} />}
className={MARKDOWN_FILE_LINK_CLASS_NAME}
style={chipStyle}
data-markdown-copy={copyMarkdown}
onClick={(event) => {
event.preventDefault();
Expand All @@ -360,13 +358,12 @@ export const MarkdownFileLink = memo(function MarkdownFileLink({
</ContextChip>
) : (
<ContextChip
kind="mention"
kind={chipKind}
dir="ltr"
render={<button type="button" />}
aria-label={`File options for ${label}`}
aria-haspopup="menu"
className={cn(MARKDOWN_FILE_LINK_CLASS_NAME, "select-text")}
style={chipStyle}
data-markdown-copy={copyMarkdown}
onClick={handleContextMenu}
onContextMenu={handleContextMenu}
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/chat/TimelineUserMessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ import {
import { UserMessageContextReferenceChip } from "./UserMessageContextChips";
import { resolvePreviewAnnotationImage } from "./TimelineUserMessageRow.logic";

const BUBBLE_CHIP_CLASS_NAME =
"[&_[data-slot=context-chip]]:[--context-chip-ink:var(--context-chip-message,var(--context-chip-accent))]";

function UserVideoAttachment({ file }: { readonly file: ChatFileAttachment }) {
const ctx = use(TimelineRowCtx);
// Row updates rebuild attachment objects, so the asset follows only the
Expand Down Expand Up @@ -165,6 +168,7 @@ function AutomationMessageRow({ row }: { row: Extract<TimelineRow, { kind: "mess
<div
className={cn(
"min-w-0 max-w-[80%] rounded-2xl bg-message text-message-foreground",
BUBBLE_CHIP_CLASS_NAME,
expanded && "w-full",
)}
>
Expand Down Expand Up @@ -454,6 +458,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
<div
className={cn(
"relative min-w-0 max-w-[80%] rounded-2xl text-message-foreground",
BUBBLE_CHIP_CLASS_NAME,
userMessage.attribution === "guest" ? guestBubbleClassName(false) : "bg-message",
!folded && "p-3",
folded && expanded && "w-full",
Expand Down
8 changes: 3 additions & 5 deletions apps/web/src/components/chat/UserMessageContextChips.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { documentIconColorForFileName } from "~/pierre-icons";
import { documentChipKindForFileName } from "~/pierre-icons";
import { isSvgAttachment } from "@t3tools/shared/image";
import { useRenderedAppearance } from "../../hooks/useTheme";
import { PullRequestGlyph } from "../pullRequest/pullRequestIcons";
import { ReadOnlySourcePreview } from "../files/AttachmentFilePreview";
import { useRightPanelStore } from "~/rightPanelStore";
import { COMPOSER_CONTEXT_KINDS } from "@t3tools/contracts";
import { formatAttachmentSize } from "@t3tools/client-runtime/state/attachments";
import { use, type CSSProperties, type ReactNode } from "react";
import { use, type ReactNode } from "react";
import { FileDiff } from "@pierre/diffs/react";
import { DiffWorkerPoolProvider } from "../DiffWorkerPoolProvider";
import {
Expand Down Expand Up @@ -65,14 +65,12 @@ function UserMessageMentionChip(props: {
copyMarkdown: string;
}) {
const ctx = use(TimelineRowCtx);
const accent = documentIconColorForFileName(props.record.path);
return (
<Tooltip>
<TooltipTrigger
render={
<ContextChip
kind="mention"
style={accent ? ({ "--context-chip-accent": accent } as CSSProperties) : undefined}
kind={documentChipKindForFileName(props.record.path) ?? "mention"}
render={<button type="button" />}
aria-label={`Preview ${props.record.path}`}
data-markdown-copy={props.copyMarkdown}
Expand Down
Loading
Loading