authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-22 21:33:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-22 21:33:01-07:00
log266d70cc4d887b7459ec6557c430ec7bb92bd3fc
tree8ca54b8437862c97047cd42b4599210d7d52729c
parent29931dd3a9a4996a5282f59564a64e6be496d5c6
signature Signed by PGP key B5690EEEBB952194

refactor(web): context chips render through one ContextChip component (#13192)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

14 files changed, 330 insertions(+), 576 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+17-35
......@@ -105,7 +105,7 @@ import { markdownImageGallery, markdownImageItems } from "./chat/markdownImageGa
105105import { MediaVideoPlayer } from "./media/MediaVideoPlayer";
106106import { MediaActions, type MediaActionSource } from "./media/MediaActions";
107107import { resolveProtocolRelativeMediaUrl } from "./media/mediaContent";
108import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
108import { FileTagChipContent } from "./chat/FileTagChip";
109109import { PierreEntryIcon } from "./chat/PierreEntryIcon";
110110import {
111111 revealInFileExplorerLabelForKind,
......@@ -118,6 +118,7 @@ import {
118118import { hasSpecificPierreIconForFileName, syntheticFileNameForLanguageId } from "../pierre-icons";
119119import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
120120import { Button } from "./ui/button";
121import { ContextChip } from "./ContextChip";
121122import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./ui/collapsible";
122123import { ScrollArea } from "./ui/scroll-area";
123124import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu";
......@@ -1156,11 +1157,9 @@ interface MarkdownFileLinkProps {
11561157 /** Platform-specific menu label ("Reveal in Finder", ...); required for the
11571158 reveal item to show. */
11581159 revealLabel?: string | undefined;
1159 className?: string | undefined;
11601160}
11611161
1162const MARKDOWN_FILE_CHIP_CLASS_NAME = "chat-markdown-file-link";
1163const MARKDOWN_FILE_LINK_CLASS_NAME = `${MARKDOWN_FILE_CHIP_CLASS_NAME} cursor-pointer transition-colors hover:bg-accent/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70`;
1162const MARKDOWN_FILE_LINK_CLASS_NAME = "chat-markdown-file-link";
11641163
11651164function pathParentSegments(path: string): string[] {
11661165 const normalized = path.replaceAll("\\", "/");
......@@ -1895,7 +1894,6 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
18951894 onOpenMedia,
18961895 onReveal,
18971896 revealLabel,
1898 className,
18991897}: MarkdownFileLinkProps) {
19001898 const handleOpenInEditor = useCallback(() => {
19011899 if (!onOpen) {
......@@ -2165,13 +2163,10 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
21652163 <TooltipTrigger
21662164 render={
21672165 hasPrimaryAction ? (
2168 <a
2169 href={href}
2170 className={cn(
2171 CHAT_FILE_TAG_CHIP_CLASS_NAME,
2172 MARKDOWN_FILE_LINK_CLASS_NAME,
2173 className,
2174 )}
2166 <ContextChip
2167 kind="mention"
2168 render={<a href={href} />}
2169 className={MARKDOWN_FILE_LINK_CLASS_NAME}
21752170 data-markdown-copy={copyMarkdown}
21762171 onClick={(event) => {
21772172 event.preventDefault();
......@@ -2188,25 +2183,21 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
21882183 }}
21892184 onContextMenu={handleContextMenu}
21902185 >
2191 <FileTagChipContent path={iconPath} label={label} theme={theme} selectable />
2192 </a>
2186 <FileTagChipContent path={iconPath} label={label} theme={theme} />
2187 </ContextChip>
21932188 ) : (
2194 <button
2195 type="button"
2189 <ContextChip
2190 kind="mention"
2191 render={<button type="button" />}
21962192 aria-label={`File options for ${label}`}
21972193 aria-haspopup="menu"
2198 className={cn(
2199 CHAT_FILE_TAG_CHIP_CLASS_NAME,
2200 MARKDOWN_FILE_LINK_CLASS_NAME,
2201 "select-text",
2202 className,
2203 )}
2194 className={cn(MARKDOWN_FILE_LINK_CLASS_NAME, "select-text")}
22042195 data-markdown-copy={copyMarkdown}
22052196 onClick={handleContextMenu}
22062197 onContextMenu={handleContextMenu}
22072198 >
2208 <FileTagChipContent path={iconPath} label={label} theme={theme} selectable />
2209 </button>
2199 <FileTagChipContent path={iconPath} label={label} theme={theme} />
2200 </ContextChip>
22102201 )
22112202 }
22122203 />
......@@ -2242,8 +2233,7 @@ function areMarkdownFileLinkPropsEqual(
22422233 previous.onOpenInBrowser === next.onOpenInBrowser &&
22432234 previous.onOpenMedia === next.onOpenMedia &&
22442235 previous.onReveal === next.onReveal &&
2245 previous.revealLabel === next.revealLabel &&
2246 previous.className === next.className
2236 previous.revealLabel === next.revealLabel
22472237 );
22482238}
22492239
......@@ -2571,12 +2561,7 @@ function useChatMarkdownState({
25712561 [cwd, findWorkspaceBasenameMatch, revealFileInFileManager],
25722562 );
25732563 const fileLinkChip = useCallback(
2574 (
2575 fileLinkMeta: MarkdownFileLinkMeta,
2576 copyMarkdown: string,
2577 className?: string,
2578 mediaSource?: string,
2579 ) => {
2564 (fileLinkMeta: MarkdownFileLinkMeta, copyMarkdown: string, mediaSource?: string) => {
25802565 const parentSuffix = fileLinkParentSuffixByPath.get(
25812566 fileLinkMeta.filePath.replaceAll("\\", "/"),
25822567 );
......@@ -2633,7 +2618,6 @@ function useChatMarkdownState({
26332618 ? () => openMarkdownFileInPreview(fileLinkMeta.filePath)
26342619 : undefined
26352620 }
2636 className={className}
26372621 />
26382622 );
26392623 },
......@@ -3068,7 +3052,6 @@ const CHAT_MARKDOWN_COMPONENTS = {
30683052 return fileLinkChip(
30693053 fileLinkMeta,
30703054 `[${fileLinkMeta.basename}](${normalizedHref})`,
3071 props.className,
30723055 normalizedHref,
30733056 );
30743057 },
......@@ -3085,7 +3068,6 @@ const CHAT_MARKDOWN_COMPONENTS = {
30853068 return fileLinkChip(
30863069 fileLinkMeta,
30873070 `\`${codeText}\``,
3088 undefined,
30893071 inlineCodeFilePathCandidate(codeText) ?? codeText.trim(),
30903072 );
30913073 }
apps/web/src/components/ComposerPromptEditorTiptap.tsx+21-27
......@@ -58,18 +58,13 @@ import {
5858import { collectInlineContextIds } from "~/lib/composerContextReferences";
5959import { cn, isMacPlatform } from "~/lib/utils";
6060import { basenameOfPath } from "~/pierre-icons";
61import {
62 COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME,
63 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
64 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
65 COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME,
66 SKILL_CHIP_ICON_SVG,
67} from "./composerInlineChip";
68import { FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
61import { FileTagChipContent } from "./chat/FileTagChip";
62import { SkillChipIcon } from "./chat/SkillInlineText";
6963import { AssistantCitationChip } from "./chat/AssistantCitationChip";
7064import { getTimelinePageScrollKey } from "./chat/pageScrollController";
7165import { ContextChipPopover } from "./contextChipParts";
7266import { Button } from "./ui/button";
67import { ContextChip } from "./ContextChip";
7368import {
7469 ComposerContextActionsContext,
7570 ComposerContextReferenceChip,
......@@ -190,6 +185,13 @@ function resolvedThemeFromDocument(): "light" | "dark" {
190185
191186// ── Inline atom nodes (chips) ─────────────────────────────────────────────
192187
188/**
189 * Wraps an inline chip node view: keeps the caret and text selection out of the chip and
190 * paints the editor's node selection over it.
191 */
192const CHIP_NODE_SELECTION_CLASS_NAME =
193 "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-[6px] data-[composer-chip-selected]:after:bg-[Highlight] data-[composer-chip-selected]:after:opacity-30 data-[composer-chip-selected]:after:content-['']";
194
193195const ComposerMentionExtension = Node.create({
194196 name: "composer-mention",
195197 group: "inline",
......@@ -217,11 +219,11 @@ function ComposerMentionNodeView({ node }: NodeViewProps) {
217219 const actions = use(ComposerContextActionsContext);
218220 const path = (node.attrs.path as string) ?? "";
219221 const chip = (
220 <Button
221 variant="chip"
222 <ContextChip
223 kind="mention"
224 render={<button type="button" />}
222225 onClick={() => actions.openMention(path)}
223226 aria-label={`Preview ${path}`}
224 className={`${FILE_TAG_CHIP_CLASS_NAME} cursor-pointer focus-visible:outline-2`}
225227 contentEditable={false}
226228 spellCheck={false}
227229 data-composer-mention-chip="true"
......@@ -231,10 +233,10 @@ function ComposerMentionNodeView({ node }: NodeViewProps) {
231233 label={basenameOfPath(path)}
232234 theme={resolvedThemeFromDocument()}
233235 />
234 </Button>
236 </ContextChip>
235237 );
236238 return (
237 <NodeViewWrapper as="span" className={COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME}>
239 <NodeViewWrapper as="span" className={CHIP_NODE_SELECTION_CLASS_NAME}>
238240 <Tooltip>
239241 <TooltipTrigger render={chip} />
240242 <TooltipPopup side="top">{path}</TooltipPopup>
......@@ -275,20 +277,12 @@ function ComposerSkillNodeView({ node }: NodeViewProps) {
275277 const skillDescription = (node.attrs.skillDescription as string | null) ?? null;
276278 const skill = skills.find((candidate) => candidate.name === skillName);
277279 return (
278 <NodeViewWrapper as="span" className={COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME}>
280 <NodeViewWrapper as="span" className={CHIP_NODE_SELECTION_CLASS_NAME}>
279281 <ContextChipPopover
282 kind="skill"
283 icon={<SkillChipIcon />}
284 label={skillLabel}
280285 accessibleLabel={`Skill ${skillLabel}`}
281 triggerClassName={COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME}
282 chip={
283 <>
284 <span
285 aria-hidden="true"
286 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
287 dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
288 />
289 <span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{skillLabel}</span>
290 </>
291 }
292286 >
293287 <div className="space-y-3 p-2 text-sm">
294288 <p className="font-medium">{skillLabel}</p>
......@@ -380,7 +374,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) {
380374 return (
381375 <NodeViewWrapper
382376 as="span"
383 className="inline-flex min-w-0 max-w-full"
377 className="inline-flex min-w-0 max-w-full select-none"
384378 contentEditable={false}
385379 spellCheck={false}
386380 data-composer-citation-chip="true"
......@@ -435,7 +429,7 @@ const ComposerContextReferenceExtension = Node.create({
435429
436430function ComposerContextReferenceNodeView({ node }: NodeViewProps) {
437431 return (
438 <NodeViewWrapper as="span" className={COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME}>
432 <NodeViewWrapper as="span" className={CHIP_NODE_SELECTION_CLASS_NAME}>
439433 <ComposerContextReferenceChip
440434 kind={(node.attrs.kind as string) ?? ""}
441435 contextId={(node.attrs.contextId as string) ?? ""}
apps/web/src/components/ContextChip.tsx created+131
......@@ -0,0 +1,131 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { useRender } from "@base-ui/react/use-render";
3import { cva, type VariantProps } from "class-variance-authority";
4import type * as React from "react";
5
6import { cn } from "~/lib/utils";
7
8/**
9 * The inline pill for one piece of chat context. It lives with the chat components rather than
10 * in components/ui because it is one feature's look, not a generic primitive.
11 *
12 * An inline pill for one piece of context (a file, mention, terminal excerpt, pull request,
13 * skill…) that sits in running text, both in the composer and in sent messages.
14 *
15 * Metrics are in em so the chip scales with the text around it (the composer honors the
16 * prompt font-size preference). Each kind keeps one restrained color identity: every accent
17 * shares a lightness so no kind reads heavier than another, and only hue carries identity.
18 * A consumer may override `--context-chip-accent` through `style` for a color that comes
19 * from content, such as an image's average color.
20 *
21 * Renders a span by default. Render it as a button, link or popover/tooltip trigger to make it
22 * interactive; the hover tint follows from that, not from a prop. Anything focusable (including a
23 * span with tabIndex for a tooltip) gets the focus outline.
24 */
25const contextChipVariants = cva(
26 "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-[var(--contrast-foreground)] disabled:cursor-default",
27 {
28 defaultVariants: { kind: "neutral" },
29 variants: {
30 kind: {
31 neutral: "border-border/70 bg-accent/40 text-foreground",
32 image: "[--context-chip-accent:oklch(0.62_0.16_16)]",
33 video: "[--context-chip-accent:oklch(0.62_0.16_48)]",
34 file: "[--context-chip-accent:oklch(0.62_0.136_237)]",
35 mention: "[--context-chip-accent:oklch(0.62_0.11_215)]",
36 terminal: "[--context-chip-accent:oklch(0.62_0.134_163)]",
37 element: "[--context-chip-accent:oklch(0.62_0.134_70)]",
38 "preview-annotation": "[--context-chip-accent:oklch(0.62_0.134_70)]",
39 "review-comment": "[--context-chip-accent:oklch(0.62_0.16_292)]",
40 "pull-request": "[--context-chip-accent:oklch(0.62_0.16_277)]",
41 "pr-open": "[--context-chip-accent:oklch(0.62_0.134_163)]",
42 "pr-draft": "[--context-chip-accent:oklch(0.62_0.02_259)]",
43 "pr-merged": "[--context-chip-accent:oklch(0.62_0.16_292)]",
44 "pr-closed": "[--context-chip-accent:oklch(0.62_0.16_16)]",
45 skill: "[--context-chip-accent:oklch(0.62_0.16_322)]",
46 citation: "[--context-chip-accent:oklch(0.62_0.16_259)]",
47 },
48 // Colors live in compoundVariants below so they come after the kind colors.
49 state: {
50 unresolved: "border-dashed",
51 invalid: "",
52 },
53 },
54 compoundVariants: [
55 {
56 kind: [
57 "image",
58 "video",
59 "file",
60 "mention",
61 "terminal",
62 "element",
63 "preview-annotation",
64 "review-comment",
65 "pull-request",
66 "pr-open",
67 "pr-draft",
68 "pr-merged",
69 "pr-closed",
70 "skill",
71 "citation",
72 ],
73 className:
74 "border-[color-mix(in_oklab,var(--context-chip-accent)_34%,var(--contrast-border))] bg-[color-mix(in_oklab,var(--context-chip-accent)_11%,transparent)] text-[color-mix(in_oklab,var(--context-chip-accent)_22%,var(--contrast-foreground))] [button:enabled&,a&]:hover:border-[color-mix(in_oklab,var(--context-chip-accent)_48%,var(--contrast-border))] [button:enabled&,a&]:hover:bg-[color-mix(in_oklab,var(--context-chip-accent)_17%,transparent)]",
75 },
76 // State colors win over any kind.
77 { state: "unresolved", className: "text-foreground" },
78 { state: "invalid", className: "border-destructive/35 bg-destructive/8 text-destructive" },
79 ],
80 },
81);
82
83type ContextChipKind = NonNullable<VariantProps<typeof contextChipVariants>["kind"]>;
84
85interface ContextChipProps extends useRender.ComponentProps<"span"> {
86 kind?: ContextChipKind;
87 state?: "unresolved" | "invalid";
88}
89
90function ContextChip({ className, kind, state, render, ...props }: ContextChipProps) {
91 const defaultProps = {
92 className: cn(contextChipVariants({ kind, state }), className),
93 "data-slot": "context-chip",
94 "data-state": state,
95 };
96 return useRender({
97 defaultTagName: "span",
98 props: mergeProps<"span">(defaultProps, props),
99 render,
100 });
101}
102
103/** The chip's text. Truncates to the chip's width. */
104function ContextChipLabel({ className, ...props }: React.ComponentProps<"span">) {
105 return (
106 <span
107 className={cn("block min-w-0 self-center truncate leading-tight", className)}
108 data-slot="context-chip-label"
109 {...props}
110 />
111 );
112}
113
114/** An icon action inside a chip, such as editing a citation's comment. Tints with the chip's kind. */
115function ContextChipAction({ className, render, ...props }: useRender.ComponentProps<"button">) {
116 const defaultProps = {
117 className: cn(
118 "ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-current transition-colors hover:bg-[color-mix(in_oklab,var(--context-chip-accent,var(--color-foreground))_17%,transparent)] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none [&_svg]:size-[0.85em]",
119 className,
120 ),
121 "data-slot": "context-chip-action",
122 type: render ? undefined : ("button" as const),
123 };
124 return useRender({
125 defaultTagName: "button",
126 props: mergeProps<"button">(defaultProps, props),
127 render,
128 });
129}
130
131export { ContextChip, ContextChipAction, ContextChipLabel, type ContextChipKind };
apps/web/src/components/chat/AssistantCitationChip.tsx+10-26
......@@ -13,19 +13,11 @@ import {
1313 findAssistantCitationSourceAnchor,
1414 type AssistantCitationSourceAnchor,
1515} from "~/lib/assistantTextSelection";
16import { cn } from "~/lib/utils";
1716import {
1817 assistantCitationHash,
1918 assistantCitationNavigation,
2019} from "../../lib/assistantCitationNavigation";
21import {
22 CHAT_INLINE_CHIP_CLASS_NAME,
23 COMPOSER_INLINE_CHIP_CLASS_NAME,
24 COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME,
25 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
26 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
27 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
28} from "../composerInlineChip";
20import { ContextChip, ContextChipAction, ContextChipLabel } from "../ContextChip";
2921import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
3022import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
3123import { AssistantCitationCommentEditor } from "./AssistantCitationCommentEditor";
......@@ -33,11 +25,6 @@ import { resolveAssistantCitationCommentDismissal } from "./assistantCitationCom
3325import { observeAssistantCitationCommentSource } from "./AssistantCitationSource";
3426import { composerFloatingLayerProps } from "./composerEventScope";
3527
36const CITATION_ACTION_BUTTON_CLASS_NAME = cn(
37 COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME,
38 "text-current hover:bg-[color-mix(in_oklab,var(--context-chip-accent)_17%,transparent)] hover:text-current",
39);
40
4128export function AssistantCitationChip({
4229 citation,
4330 composer = false,
......@@ -126,8 +113,8 @@ export function AssistantCitationChip({
126113 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-[var(--contrast-foreground)]"
127114 aria-label={`View cited assistant text: ${label}`}
128115 >
129 <QuoteIcon aria-hidden="true" className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} />
130 <span className={cn(COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, "max-w-[16em]")}>{label}</span>
116 <QuoteIcon aria-hidden="true" />
117 <ContextChipLabel className="max-w-[16em]">{label}</ContextChipLabel>
131118 </Link>
132119 );
133120 const chatSourceLink = (
......@@ -136,16 +123,13 @@ export function AssistantCitationChip({
136123 className="inline-flex h-full min-w-0 items-center gap-[0.33em] rounded-sm text-inherit no-underline hover:bg-[color-mix(in_oklab,var(--context-chip-accent)_17%,transparent)] focus-visible:outline-2 focus-visible:outline-[var(--contrast-foreground)]"
137124 aria-label={`View cited assistant text: ${label}`}
138125 >
139 <QuoteIcon aria-hidden="true" className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} />
140 <span className={cn(COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, "max-w-[16em]")}>{label}</span>
126 <QuoteIcon aria-hidden="true" />
127 <ContextChipLabel className="max-w-[16em]">{label}</ContextChipLabel>
141128 </Link>
142129 );
143130 return (
144 <span
145 className={cn(
146 composer ? COMPOSER_INLINE_CHIP_CLASS_NAME : CHAT_INLINE_CHIP_CLASS_NAME,
147 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.citation,
148 )}
131 <ContextChip
132 kind="citation"
149133 contentEditable={false}
150134 data-assistant-citation-chip="true"
151135 data-markdown-copy={serializeAssistantCitation(citation)}
......@@ -171,9 +155,9 @@ export function AssistantCitationChip({
171155 >
172156 <PopoverTrigger
173157 aria-label={citation.comment ? "Edit citation comment" : "Add comment to citation"}
174 className={CITATION_ACTION_BUTTON_CLASS_NAME}
158 render={<ContextChipAction />}
175159 >
176 <PencilIcon aria-hidden="true" className="size-[0.85em]" />
160 <PencilIcon aria-hidden="true" />
177161 </PopoverTrigger>
178162 {commentEditor.open ? (
179163 <PopoverPopup
......@@ -223,6 +207,6 @@ export function AssistantCitationChip({
223207 ) : null}
224208 </Popover>
225209 ) : null}
226 </span>
210 </ContextChip>
227211 );
228212}
apps/web/src/components/chat/FileTagChip.tsx+3-22
......@@ -1,22 +1,12 @@
11import { inferEntryKindFromPath } from "../../pierre-icons";
2import {
3 CHAT_INLINE_CHIP_CLASS_NAME,
4 CHAT_INLINE_CHIP_LABEL_CLASS_NAME,
5 COMPOSER_INLINE_CHIP_CLASS_NAME,
6 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
7 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
8 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
9} from "../composerInlineChip";
2import { ContextChipLabel } from "../ContextChip";
103import { PierreEntryIcon } from "./PierreEntryIcon";
114
12export const FILE_TAG_CHIP_CLASS_NAME = `${COMPOSER_INLINE_CHIP_CLASS_NAME} ${CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.mention}`;
13export const CHAT_FILE_TAG_CHIP_CLASS_NAME = `${CHAT_INLINE_CHIP_CLASS_NAME} ${CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.mention}`;
14
5/** Icon and label for a file mention; render inside `<ContextChip kind="mention">`. */
156export function FileTagChipContent(props: {
167 path: string;
178 label: string;
189 theme: "light" | "dark";
19 selectable?: boolean;
2010}) {
2111 return (
2212 <>
......@@ -24,17 +14,8 @@ export function FileTagChipContent(props: {
2414 pathValue={props.path}
2515 kind={inferEntryKindFromPath(props.path)}
2616 theme={props.theme}
27 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
2817 />
29 <span
30 className={
31 props.selectable
32 ? CHAT_INLINE_CHIP_LABEL_CLASS_NAME
33 : COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME
34 }
35 >
36 {props.label}
37 </span>
18 <ContextChipLabel>{props.label}</ContextChipLabel>
3819 </>
3920 );
4021}
apps/web/src/components/chat/MessagesTimeline.tsx+27-117
......@@ -214,6 +214,7 @@ import {
214214 ContextChipShell,
215215 FileChip,
216216 ImageChipButton,
217 PULL_REQUEST_CHIP_KINDS,
217218 PullRequestChip,
218219 UnresolvedChip,
219220} from "../contextChipParts";
......@@ -236,15 +237,7 @@ import {
236237 encodeComposerContextFragment,
237238} from "@t3tools/shared/composerContextClipboard";
238239import { chatMarkdownClipboardPayload } from "../../markdown-clipboard";
239import {
240 CHAT_INLINE_CHIP_CLASS_NAME,
241 CHAT_INLINE_CHIP_LABEL_CLASS_NAME,
242 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
243 SKILL_CHIP_ICON_SVG,
244 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES,
245 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
246 PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES,
247} from "../composerInlineChip";
240import { ContextChip, ContextChipLabel, type ContextChipKind } from "../ContextChip";
248241import { createContextPresentationRegistry } from "../contextPresentationRegistry";
249242import { useOpenPrLink } from "~/lib/openPullRequestLink";
250243import type { ChatMarkdownContextReference } from "../ChatMarkdown";
......@@ -254,7 +247,7 @@ import { useUiStateStore } from "~/uiStateStore";
254247import { type TimestampFormat } from "@t3tools/contracts/settings";
255248import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat";
256249
257import { SkillInlineText } from "./SkillInlineText";
250import { SkillChipIcon, SkillInlineText } from "./SkillInlineText";
258251import { deriveAgentSpawnSummary } from "./agentSpawnSummary";
259252import { formatWorkspaceRelativePath } from "../../filePathDisplay";
260253import {
......@@ -2559,8 +2552,9 @@ function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnap
25592552 <PopoverTrigger
25602553 render={
25612554 <Button
2562 variant="chip"
2563 className="ml-auto inline-flex h-5 min-w-0 shrink-0 items-center gap-1 rounded-full border border-border/70 px-2 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
2555 variant="ghost-muted"
2556 size="micro"
2557 className="ml-auto min-w-0 shrink-0"
25642558 aria-label={`${scriptName} is still running. Show setup progress.`}
25652559 />
25662560 }
......@@ -3453,14 +3447,10 @@ function UserMessageMentionChip(props: {
34533447 <Tooltip>
34543448 <TooltipTrigger
34553449 render={
3456 <button
3457 type="button"
3450 <ContextChip
3451 kind="mention"
3452 render={<button type="button" />}
34583453 aria-label={`Preview ${props.record.path}`}
3459 className={cn(
3460 CHAT_INLINE_CHIP_CLASS_NAME,
3461 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.mention,
3462 "cursor-pointer focus-visible:outline-2",
3463 )}
34643454 data-markdown-copy={props.copyMarkdown}
34653455 onClick={() => {
34663456 if (ctx.threadRef)
......@@ -3471,10 +3461,9 @@ function UserMessageMentionChip(props: {
34713461 pathValue={props.record.path}
34723462 kind={inferEntryKindFromPath(props.record.path)}
34733463 theme={ctx.resolvedTheme}
3474 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
34753464 />
3476 <span className={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}>{props.record.label}</span>
3477 </button>
3465 <ContextChipLabel>{props.record.label}</ContextChipLabel>
3466 </ContextChip>
34783467 }
34793468 />
34803469 <TooltipPopup>{props.record.path}</TooltipPopup>
......@@ -3488,21 +3477,16 @@ function UserMessageContextChip(props: {
34883477 kindLabel?: string;
34893478 copyMarkdown: string;
34903479 tooltip?: string;
3491 toneClassName?: string;
3492 interactive?: boolean;
3493 unresolved?: boolean;
3480 kind: ContextChipKind;
34943481}) {
34953482 return (
34963483 <ContextChipShell
3484 kind={props.kind}
34973485 icon={props.icon}
34983486 label={props.label}
3499 className={cn(CHAT_INLINE_CHIP_CLASS_NAME, props.toneClassName)}
3500 labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}
35013487 aria-label={props.kindLabel ? `${props.kindLabel}, ${props.label}` : undefined}
35023488 data-markdown-copy={props.copyMarkdown}
35033489 tooltip={props.tooltip}
3504 interactive={props.interactive === true}
3505 unresolved={props.unresolved === true}
35063490 />
35073491 );
35083492}
......@@ -3510,7 +3494,7 @@ function UserMessageContextChip(props: {
35103494function UserMessagePullRequestContextChip(props: {
35113495 record: Extract<KnownComposerContextRecord, { kind: "review-comment" }>;
35123496 copyMarkdown: string;
3513 toneClassName: string;
3497 kind: ContextChipKind;
35143498}) {
35153499 const { activeThreadEnvironmentId, openPullRequest } = use(TimelineRowCtx);
35163500 const metadata = props.record.pullRequest;
......@@ -3521,8 +3505,7 @@ function UserMessagePullRequestContextChip(props: {
35213505 environmentId={activeThreadEnvironmentId}
35223506 label={reviewCommentContextLabel(props.record)}
35233507 kindLabel={pullRequestContextKindLabel(props.record)}
3524 className={cn(CHAT_INLINE_CHIP_CLASS_NAME, props.toneClassName)}
3525 labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}
3508 kind={props.kind}
35263509 copyMarkdown={props.copyMarkdown}
35273510 onOpen={openPullRequest}
35283511 />
......@@ -3675,11 +3658,8 @@ function UnavailableUserMessageContextChip(props: UserMessageContextRenderContex
36753658 return (
36763659 <UnresolvedChip
36773660 label={props.reference.label}
3678 className={CHAT_INLINE_CHIP_CLASS_NAME}
3679 labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}
36803661 copyMarkdown={props.copyMarkdown}
36813662 tooltip="This context is no longer available."
3682 tooltipClassName="max-w-96 whitespace-pre-wrap leading-tight"
36833663 />
36843664 );
36853665}
......@@ -3707,18 +3687,12 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
37073687 render: (record, context) =>
37083688 record.kind === "skill" ? (
37093689 <UserMessageContextChip
3710 icon={
3711 <span
3712 aria-hidden="true"
3713 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
3714 dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
3715 />
3716 }
3690 icon={<SkillChipIcon />}
37173691 label={record.label || record.name}
37183692 kindLabel="Skill"
37193693 tooltip={`$${record.name}`}
37203694 copyMarkdown={context.copyMarkdown}
3721 toneClassName={CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.skill}
3695 kind="skill"
37223696 />
37233697 ) : (
37243698 <UnavailableUserMessageContextChip {...context} />
......@@ -3743,8 +3717,6 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
37433717 <ImageChipButton
37443718 name={record.name}
37453719 previewUrl={attachment.previewUrl}
3746 className={CHAT_INLINE_CHIP_CLASS_NAME}
3747 labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}
37483720 size={formatAttachmentSize(record.sizeBytes)}
37493721 data-markdown-copy={context.copyMarkdown}
37503722 onClick={() => context.onExpandImage(attachment)}
......@@ -3777,8 +3749,6 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
37773749 size={size}
37783750 isVideo={isVideo}
37793751 theme={context.resolvedTheme}
3780 className={CHAT_INLINE_CHIP_CLASS_NAME}
3781 labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}
37823752 disabled={disabled}
37833753 accessibleLabel={`${isVideo ? "Video" : "File"} attachment, ${record.name}, ${size}`}
37843754 copyMarkdown={context.copyMarkdown}
......@@ -3797,7 +3767,6 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
37973767 record.kind === "terminal" ? (
37983768 <span data-markdown-copy={context.copyMarkdown}>
37993769 <TerminalContextInlineChip
3800 surface="transcript"
38013770 label={record.label}
38023771 terminalLabel={record.terminalLabel}
38033772 lineStart={record.lineStart}
......@@ -3818,24 +3787,9 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
38183787 <UserMessageContextPopover
38193788 copyMarkdown={context.copyMarkdown}
38203789 accessibleLabel={`Browser element, ${record.label}`}
3821 chip={
3822 <UserMessageContextChip
3823 icon={
3824 <MousePointerClickIcon
3825 className={cn(
3826 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
3827 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES.element,
3828 "size-3.5",
3829 )}
3830 />
3831 }
3832 label={record.label}
3833 kindLabel="Browser element"
3834 copyMarkdown={context.copyMarkdown}
3835 interactive
3836 toneClassName={CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.element}
3837 />
3838 }
3790 kind="element"
3791 icon={<MousePointerClickIcon />}
3792 label={record.label}
38393793 >
38403794 <UserMessageElementDetails record={record} />
38413795 </UserMessageContextPopover>
......@@ -3859,7 +3813,7 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
38593813 <UserMessagePullRequestContextChip
38603814 record={record}
38613815 copyMarkdown={context.copyMarkdown}
3862 toneClassName={PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES[pullRequestState]}
3816 kind={PULL_REQUEST_CHIP_KINDS[pullRequestState]}
38633817 />
38643818 );
38653819 }
......@@ -3867,38 +3821,9 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
38673821 <UserMessageContextPopover
38683822 copyMarkdown={context.copyMarkdown}
38693823 accessibleLabel={`${kindLabel}, ${label}${record.pullRequest ? `, ${record.pullRequest.title}` : ""}`}
3870 chip={
3871 <UserMessageContextChip
3872 icon={
3873 isPullRequest ? (
3874 <PullRequestGlyph.pullRequest
3875 className={cn(
3876 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
3877 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["pull-request"],
3878 "size-3.5",
3879 )}
3880 />
3881 ) : (
3882 <MessageCircleIcon
3883 className={cn(
3884 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
3885 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["review-comment"],
3886 "size-3.5",
3887 )}
3888 />
3889 )
3890 }
3891 label={label}
3892 kindLabel={kindLabel}
3893 copyMarkdown={context.copyMarkdown}
3894 interactive
3895 toneClassName={
3896 isPullRequest
3897 ? PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES[pullRequestState]
3898 : CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES["review-comment"]
3899 }
3900 />
3901 }
3824 kind={isPullRequest ? PULL_REQUEST_CHIP_KINDS[pullRequestState] : "review-comment"}
3825 icon={isPullRequest ? <PullRequestGlyph.pullRequest /> : <MessageCircleIcon />}
3826 label={label}
39023827 >
39033828 <UserMessageReviewCommentCard
39043829 comment={{
......@@ -3929,24 +3854,9 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry
39293854 <UserMessageContextPopover
39303855 copyMarkdown={context.copyMarkdown}
39313856 accessibleLabel={`Preview annotation, ${record.label}`}
3932 chip={
3933 <UserMessageContextChip
3934 icon={
3935 <MousePointerClickIcon
3936 className={cn(
3937 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
3938 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["preview-annotation"],
3939 "size-3.5",
3940 )}
3941 />
3942 }
3943 label={record.label}
3944 kindLabel="Preview annotation"
3945 copyMarkdown={context.copyMarkdown}
3946 interactive
3947 toneClassName={CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES["preview-annotation"]}
3948 />
3949 }
3857 kind="preview-annotation"
3858 icon={<MousePointerClickIcon />}
3859 label={record.label}
39503860 >
39513861 <UserMessagePreviewAnnotationDetails record={record} image={context.annotationImage} />
39523862 </UserMessageContextPopover>
apps/web/src/components/chat/SkillInlineText.tsx+17-20
......@@ -2,14 +2,8 @@ import { Children, cloneElement, isValidElement, type ReactNode } from "react";
22import type { ServerProviderSkill } from "@t3tools/contracts";
33import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills";
44
5import {
6 CHAT_INLINE_CHIP_CLASS_NAME,
7 CHAT_INLINE_CHIP_LABEL_CLASS_NAME,
8 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
9 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
10 SKILL_CHIP_ICON_SVG,
11} from "../composerInlineChip";
12import { cn } from "~/lib/utils";
5import { SKILL_CHIP_ICON_SVG } from "../composerInlineChip";
6import { ContextChip, ContextChipLabel } from "../ContextChip";
137
148const SKILL_TOKEN_REGEX =
159 /(^|\s)\p{Sc}(?![0-9][0-9_]*(?:[kKmMbBtT]|[eE][0-9]+)?(?:\s|$))(?=[a-zA-Z0-9:_-]*[a-zA-Z])([a-zA-Z0-9][a-zA-Z0-9:_-]*)(?=\s|$)/gu;
......@@ -76,17 +70,20 @@ export function renderSkillInlineMarkdownChildren(
7670
7771function SkillChip(props: { skill: InlineSkill; rawText: string }) {
7872 return (
79 <span className="inline-flex align-middle leading-none" data-markdown-copy={props.rawText}>
80 <span className={cn(CHAT_INLINE_CHIP_CLASS_NAME, CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.skill)}>
81 <span
82 aria-hidden="true"
83 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
84 dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
85 />
86 <span className={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}>
87 {formatProviderSkillDisplayName(props.skill)}
88 </span>
89 </span>
90 </span>
73 <ContextChip kind="skill" data-markdown-copy={props.rawText}>
74 <SkillChipIcon />
75 <ContextChipLabel>{formatProviderSkillDisplayName(props.skill)}</ContextChipLabel>
76 </ContextChip>
77 );
78}
79
80/** The skill glyph; the surrounding chip sizes its svg. */
81export function SkillChipIcon() {
82 return (
83 <span
84 aria-hidden="true"
85 className="contents"
86 dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }}
87 />
9188 );
9289}
apps/web/src/components/chat/TerminalContextInlineChip.tsx+6-44
......@@ -1,16 +1,6 @@
11import { TerminalIcon } from "lucide-react";
22
3import { cn } from "~/lib/utils";
43import type { ContextPresentationCapability } from "../contextPresentationRegistry";
5import {
6 CHAT_INLINE_CHIP_CLASS_NAME,
7 COMPOSER_INLINE_CHIP_CLASS_NAME,
8 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
9 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
10 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES,
11 CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
12 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
13} from "../composerInlineChip";
144import { ContextChipPopover, ContextChipShell } from "../contextChipParts";
155
166interface TerminalContextInlineChipProps {
......@@ -21,41 +11,18 @@ interface TerminalContextInlineChipProps {
2111 text: string;
2212 detailsMode: ContextPresentationCapability["details"];
2313 expired?: boolean;
24 surface?: "composer" | "transcript";
2514}
2615
2716export function TerminalContextInlineChip(props: TerminalContextInlineChipProps) {
2817 const { label, terminalLabel, lineStart, lineEnd, text, detailsMode, expired = false } = props;
29 const chipClassName =
30 props.surface === "transcript" ? CHAT_INLINE_CHIP_CLASS_NAME : COMPOSER_INLINE_CHIP_CLASS_NAME;
31
32 const icon = (
33 <TerminalIcon
34 className={cn(
35 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
36 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES.terminal,
37 "size-3.5",
38 expired && "opacity-100",
39 )}
40 />
41 );
4218
4319 if (!expired && text.length > 0 && detailsMode === "popover") {
4420 return (
4521 <ContextChipPopover
22 kind="terminal"
23 icon={<TerminalIcon />}
24 label={label}
4625 accessibleLabel={`Terminal excerpt, ${label}`}
47 chip={
48 <>
49 {icon}
50 <span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{label}</span>
51 </>
52 }
53 triggerClassName={cn(
54 chipClassName,
55 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.terminal,
56 CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
57 "cursor-pointer",
58 )}
5926 popupClassName="w-[min(40rem,calc(100vw-2rem))]"
6027 viewportClassName="overflow-hidden p-2"
6128 >
......@@ -83,17 +50,12 @@ export function TerminalContextInlineChip(props: TerminalContextInlineChipProps)
8350
8451 return (
8552 <ContextChipShell
86 icon={icon}
53 kind="terminal"
54 {...(expired ? { state: "invalid" as const } : {})}
55 icon={<TerminalIcon />}
8756 label={label}
88 className={cn(
89 chipClassName,
90 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.terminal,
91 expired && "border-destructive/35 bg-destructive/8 text-destructive",
92 )}
93 labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}
9457 aria-label={`Terminal excerpt, ${label}${expired ? ", expired" : ""}`}
9558 data-terminal-context-expired={expired ? "true" : undefined}
96 tooltipClassName="max-w-80 whitespace-pre-wrap leading-tight"
9759 tooltip={
9860 expired
9961 ? `Terminal context expired. Remove and re-add ${label} to include it in your message.`
apps/web/src/components/composerContextPresentation.tsx+24-68
......@@ -14,7 +14,6 @@ import {
1414 formatAttachmentUploadProgress,
1515 type AttachmentUploadState,
1616} from "~/lib/attachmentUploadState";
17import { cn } from "~/lib/utils";
1817import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons";
1918import {
2019 fileContextReference,
......@@ -36,20 +35,14 @@ import {
3635 createContextPresentationRegistry,
3736 type ContextPresentationCapability,
3837} from "./contextPresentationRegistry";
39import {
40 COMPOSER_INLINE_CHIP_CLASS_NAME,
41 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
42 COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME,
43 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES,
44 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
45 PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES,
46} from "./composerInlineChip";
38import type { ContextChipKind } from "./ContextChip";
4739import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
4840import {
4941 ContextChipPopover,
5042 ContextChipShell,
5143 FileChip,
5244 ImageChipButton,
45 PULL_REQUEST_CHIP_KINDS,
5346 PullRequestChip,
5447 UnresolvedChip,
5548} from "./contextChipParts";
......@@ -138,30 +131,31 @@ function ContextChip(props: {
138131 kindLabel: string;
139132 details: ReactNode;
140133 detailsMode: ContextPresentationCapability["details"];
141 toneClassName: string;
134 kind: ContextChipKind;
142135}) {
143 const content = (
136 if (props.detailsMode === "popover") {
137 return (
138 <ContextChipPopover
139 kind={props.kind}
140 icon={props.icon}
141 label={props.label}
142 accessibleLabel={props.kindLabel + ", " + props.label}
143 >
144 {props.details}
145 </ContextChipPopover>
146 );
147 }
148 return (
144149 <ContextChipShell
150 kind={props.kind}
145151 icon={props.icon}
146152 label={props.label}
147 className={cn(COMPOSER_INLINE_CHIP_CLASS_NAME, props.toneClassName)}
148 labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}
149 interactive={props.detailsMode === "popover"}
150 aria-hidden={props.detailsMode === "popover" ? true : undefined}
151153 aria-label={
152154 props.detailsMode === "tooltip" ? `${props.kindLabel}, ${props.label}` : undefined
153155 }
154156 tooltip={props.detailsMode === "tooltip" ? props.details : undefined}
155157 />
156158 );
157 if (props.detailsMode === "popover") {
158 return (
159 <ContextChipPopover accessibleLabel={props.kindLabel + ", " + props.label} chip={content}>
160 {props.details}
161 </ContextChipPopover>
162 );
163 }
164 return content;
165159}
166160
167161function uploadStatusSuffix(upload: AttachmentUploadState | undefined): string | null {
......@@ -191,8 +185,6 @@ function ImageContextChip(props: {
191185 <ImageChipButton
192186 name={props.record.name}
193187 previewUrl={props.record.previewUrl}
194 className={COMPOSER_INLINE_CHIP_CLASS_NAME}
195 labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}
196188 size={formatAttachmentSize(props.record.sizeBytes)}
197189 suffix={uploadStatusSuffix(props.upload)}
198190 onClick={() => actions.expandImage(props.record.id)}
......@@ -222,8 +214,6 @@ function FileContextChip(props: {
222214 size={size}
223215 isVideo={isVideo}
224216 theme={resolvedTheme}
225 className={COMPOSER_INLINE_CHIP_CLASS_NAME}
226 labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}
227217 error={props.upload?.status === "failed"}
228218 unresolved={needsReattach}
229219 suffix={suffix}
......@@ -243,7 +233,7 @@ function FileContextChip(props: {
243233 );
244234}
245235
246function PullRequestContextChip(props: { record: ReviewCommentContext; toneClassName: string }) {
236function PullRequestContextChip(props: { record: ReviewCommentContext; kind: ContextChipKind }) {
247237 const actions = use(ComposerContextActionsContext);
248238 const metadata = props.record.pullRequest;
249239 if (metadata === undefined) return null;
......@@ -253,8 +243,7 @@ function PullRequestContextChip(props: { record: ReviewCommentContext; toneClass
253243 environmentId={actions.environmentId}
254244 label={reviewCommentContextLabel(props.record)}
255245 kindLabel={pullRequestContextKindLabel(props.record)}
256 className={cn(COMPOSER_INLINE_CHIP_CLASS_NAME, props.toneClassName)}
257 labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}
246 kind={props.kind}
258247 onOpen={actions.openPullRequest}
259248 />
260249 );
......@@ -324,10 +313,7 @@ function UnresolvedContextChip(props: { label: string }) {
324313 return (
325314 <UnresolvedChip
326315 label={props.label}
327 className={COMPOSER_INLINE_CHIP_CLASS_NAME}
328 labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}
329316 tooltip="This context is no longer available. Remove it or attach it again."
330 tooltipClassName="max-w-80 leading-tight"
331317 />
332318 );
333319}
......@@ -389,40 +375,18 @@ const composerContextPresentationRegistry = createContextPresentationRegistry<
389375 return (
390376 <PullRequestContextChip
391377 record={entry.record}
392 toneClassName={PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES[pullRequestState]}
378 kind={PULL_REQUEST_CHIP_KINDS[pullRequestState]}
393379 />
394380 );
395381 }
396382 return (
397383 <ContextChip
398 icon={
399 isPullRequest ? (
400 <PullRequestGlyph.pullRequest
401 className={cn(
402 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
403 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["pull-request"],
404 "size-3.5",
405 )}
406 />
407 ) : (
408 <MessageCircleIcon
409 className={cn(
410 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
411 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["review-comment"],
412 "size-3.5",
413 )}
414 />
415 )
416 }
384 icon={isPullRequest ? <PullRequestGlyph.pullRequest /> : <MessageCircleIcon />}
417385 label={reviewCommentContextLabel(entry.record)}
418386 kindLabel={isPullRequest ? pullRequestContextKindLabel(entry.record) : "Review comment"}
419387 details={<ComposerReviewCommentDetails comment={entry.record} />}
420388 detailsMode={definition.capabilities.details}
421 toneClassName={
422 isPullRequest
423 ? PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES[pullRequestState]
424 : CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES["review-comment"]
425 }
389 kind={isPullRequest ? PULL_REQUEST_CHIP_KINDS[pullRequestState] : "review-comment"}
426390 />
427391 );
428392 },
......@@ -433,20 +397,12 @@ const composerContextPresentationRegistry = createContextPresentationRegistry<
433397 render: (entry, context, definition) =>
434398 entry.kind === "preview-annotation" ? (
435399 <ContextChip
436 icon={
437 <MousePointerClickIcon
438 className={cn(
439 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
440 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES["preview-annotation"],
441 "size-3.5",
442 )}
443 />
444 }
400 icon={<MousePointerClickIcon />}
445401 label={previewAnnotationContextLabel(entry.record)}
446402 kindLabel="Preview annotation"
447403 details={<ComposerPreviewAnnotationDetails annotation={entry.record} />}
448404 detailsMode={definition.capabilities.details}
449 toneClassName={CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES["preview-annotation"]}
405 kind="preview-annotation"
450406 />
451407 ) : (
452408 <UnresolvedContextChip label={context.label} />
apps/web/src/components/composerInlineChip.ts-78
......@@ -1,83 +1,5 @@
1// Chip metrics are in em so the pills scale with the text they sit in (the
2// composer honors the prompt font-size preference). The chat variant pins the
3// original 12px, where every em value resolves to the same pixels as before.
4const INLINE_CHIP_GEOMETRY_CLASS_NAME =
5 "inline-flex h-[1.41em] max-w-full items-center gap-[0.33em] rounded-[0.5em] px-[0.5em] font-medium leading-none align-middle";
6
7const INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_GEOMETRY_CLASS_NAME} border border-border/70 bg-accent/40 text-foreground`;
8
9const CONTEXT_INLINE_CHIP_TONE_CLASS_NAME =
10 "border-[color-mix(in_oklab,var(--context-chip-accent)_34%,var(--contrast-border))] bg-[color-mix(in_oklab,var(--context-chip-accent)_11%,transparent)] text-[color-mix(in_oklab,var(--context-chip-accent)_22%,var(--contrast-foreground))]";
11
12export const CHAT_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[12px]`;
13
14export const COMPOSER_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[0.86em] select-none`;
15
16export const COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME =
17 "relative inline-flex 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-[6px] data-[composer-chip-selected]:after:bg-[Highlight] data-[composer-chip-selected]:after:opacity-30 data-[composer-chip-selected]:after:content-['']";
18
19export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME =
20 "block size-[1.17em] shrink-0 self-center [&>svg]:block";
21
22export const CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME =
23 "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--contrast-foreground)]";
24
25export const CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME = `transition-colors hover:border-[color-mix(in_oklab,var(--context-chip-accent)_48%,var(--contrast-border))] hover:bg-[color-mix(in_oklab,var(--context-chip-accent)_17%,transparent)] motion-reduce:transition-none ${CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME}`;
26
27export const CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES = {
28 image: "text-current",
29 video: "text-current",
30 file: "text-current",
31 mention: "text-current",
32 terminal: "text-current",
33 element: "text-current",
34 "preview-annotation": "text-current",
35 "review-comment": "text-current",
36 "pull-request": "text-current",
37 skill: "text-current",
38 citation: "text-current",
39} as const;
40
41/**
42 * Context kinds keep one restrained color identity in both composer and sent messages.
43 * Every accent shares a lightness so no kind reads heavier than another; only hue carries
44 * identity. Chroma is capped, then clamped to what each hue can hold in sRGB, which keeps
45 * the set inside the range the themes use for their own accents rather than above it.
46 */
47export const CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES = {
48 image: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_16)]`,
49 video: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_48)]`,
50 file: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.136_237)]`,
51 mention: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.11_215)]`,
52 terminal: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.134_163)]`,
53 element: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.134_70)]`,
54 "preview-annotation": `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.134_70)]`,
55 "review-comment": `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_292)]`,
56 "pull-request": `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_277)]`,
57 skill: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_322)]`,
58 citation: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_259)]`,
59} as const;
60
61export const PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES = {
62 open: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.134_163)]`,
63 draft: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.02_259)]`,
64 merged: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_292)]`,
65 closed: `${CONTEXT_INLINE_CHIP_TONE_CLASS_NAME} [--context-chip-accent:oklch(0.62_0.16_16)]`,
66 unknown: CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES["pull-request"],
67} as const;
68
69export const CHAT_INLINE_CHIP_LABEL_CLASS_NAME = "truncate leading-tight";
70
71export const COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME =
72 "block self-center truncate leading-tight select-none";
73
74export const COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME = `${INLINE_CHIP_GEOMETRY_CLASS_NAME} select-none border text-[0.86em] ${CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.skill}`;
75
761export const SKILL_CHIP_ICON_SVG = `<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>`;
772
78export const COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME =
79 "ml-[0.17em] inline-flex size-[1.17em] shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground/72 transition-colors hover:bg-foreground/6 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring";
80
813/** Keeps both the recognizable beginning and the extension/end of a long attachment name. */
824export function middleTruncateAttachmentName(name: string, maxCharacters = 36): string {
835 const characters = Array.from(name);
apps/web/src/components/contextChipParts.tsx+67-123
......@@ -10,75 +10,66 @@ import {
1010
1111import { cn } from "~/lib/utils";
1212import { PULL_REQUEST_STATE_PRESENTATION } from "~/components/pullRequest/pullRequestIcons";
13import type { PullRequestContextDisplayState } from "~/lib/composerContextRecords";
1314import { PierreEntryIcon } from "./chat/PierreEntryIcon";
14import {
15 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
16 CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME,
17 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES,
18 CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
19 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES,
20 middleTruncateAttachmentName,
21} from "./composerInlineChip";
15import { middleTruncateAttachmentName } from "./composerInlineChip";
2216import { PullRequestContextDetails } from "./PullRequestContextDetails";
23import { Button } from "./ui/button";
17import { ContextChip, ContextChipLabel, type ContextChipKind } from "./ContextChip";
2418import { Popover, PopoverPopup, PopoverTitle, PopoverTrigger } from "./ui/popover";
2519import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
2620import { PullRequestLinkPreview } from "./pullRequest/PullRequestLinkPreview";
2721import { usePullRequestPreviewTarget } from "~/lib/openPullRequestLink";
2822
29/** Shared visual slots; each surface keeps ownership of payload lookup and actions. */
23/** ContextChip kind for a pull request context in each display state. */
24export const PULL_REQUEST_CHIP_KINDS = {
25 open: "pr-open",
26 draft: "pr-draft",
27 merged: "pr-merged",
28 closed: "pr-closed",
29 unknown: "pull-request",
30} as const satisfies Record<PullRequestContextDisplayState | "unknown", ContextChipKind>;
31
32/** A static chip with an optional tooltip; each surface keeps ownership of payload lookup. */
3033export function ContextChipShell({
3134 icon,
3235 label,
33 labelClassName,
3436 tooltip,
35 tooltipClassName = "max-w-96 whitespace-pre-wrap leading-tight",
36 interactive,
37 unresolved,
38 className,
3937 ...props
40}: ComponentProps<"span"> & {
38}: Omit<ComponentProps<typeof ContextChip>, "className" | "render"> & {
4139 icon: ReactNode;
4240 label: string;
43 labelClassName: string;
41 /** Newlines in the tooltip are kept. */
4442 tooltip?: ReactNode;
45 tooltipClassName?: string;
46 interactive?: boolean;
47 unresolved?: boolean;
4843}) {
4944 const chip = (
50 <span
51 className={cn(
52 className,
53 interactive && CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
54 tooltip && CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME,
55 unresolved && "border-dashed text-foreground",
56 )}
57 data-context-unresolved={unresolved ? "true" : undefined}
45 <ContextChip
46 data-context-unresolved={props.state === "unresolved" ? "true" : undefined}
5847 tabIndex={tooltip ? 0 : undefined}
5948 {...props}
6049 >
6150 {icon}
62 <span className={labelClassName}>{label}</span>
63 </span>
51 <ContextChipLabel>{label}</ContextChipLabel>
52 </ContextChip>
6453 );
6554 if (!tooltip) return chip;
6655 return (
6756 <Tooltip>
6857 <TooltipTrigger render={chip} />
69 <TooltipPopup side="top" className={tooltipClassName}>
58 <TooltipPopup side="top" className="whitespace-pre-wrap">
7059 {tooltip}
7160 </TooltipPopup>
7261 </Tooltip>
7362 );
7463}
7564
65/** A chip that opens its details in a popover; the chip itself is the trigger. */
7666export function ContextChipPopover(props: {
67 kind: ContextChipKind;
68 icon: ReactNode;
69 label: string;
7770 copyMarkdown?: string;
7871 accessibleLabel: string;
79 chip: ReactNode;
8072 children: ReactNode;
81 triggerClassName?: string;
8273 popupClassName?: string;
8374 viewportClassName?: string;
8475}) {
......@@ -86,19 +77,16 @@ export function ContextChipPopover(props: {
8677 <Popover>
8778 <PopoverTrigger
8879 render={
89 <Button
90 variant="chip"
91 className={cn(
92 "inline-flex max-w-full cursor-pointer items-center rounded-[0.5em] align-middle",
93 CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME,
94 props.triggerClassName,
95 )}
80 <ContextChip
81 kind={props.kind}
82 render={<button type="button" />}
9683 aria-label={`${props.accessibleLabel}. Show details`}
9784 data-markdown-copy={props.copyMarkdown}
9885 />
9986 }
10087 >
101 {props.chip}
88 {props.icon}
89 <ContextChipLabel>{props.label}</ContextChipLabel>
10290 </PopoverTrigger>
10391 <PopoverPopup
10492 side="top"
......@@ -118,8 +106,7 @@ export function PullRequestChip(props: {
118106 environmentId: EnvironmentId | null;
119107 label: string;
120108 kindLabel: string;
121 className: string;
122 labelClassName: string;
109 kind: ContextChipKind;
123110 copyMarkdown?: string;
124111 onOpen: (event: MouseEvent<HTMLElement>, url: string) => void;
125112}) {
......@@ -128,21 +115,16 @@ export function PullRequestChip(props: {
128115 props.metadata.state === "open" && props.metadata.isDraft ? "draft" : props.metadata.state;
129116 const StateIcon = PULL_REQUEST_STATE_PRESENTATION[displayState].Icon;
130117 const button = (
131 <Button
132 variant="chip"
133 className={cn(
134 props.className,
135 CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME,
136 CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
137 "cursor-pointer",
138 )}
118 <ContextChip
119 kind={props.kind}
120 render={<button type="button" />}
139121 aria-label={`Open ${props.kindLabel} ${props.label}: ${props.metadata.title}`}
140122 data-markdown-copy={props.copyMarkdown}
141123 onClick={(event) => props.onOpen(event, props.metadata.url)}
142124 >
143 <StateIcon className={cn(COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, "size-3.5")} />
144 <span className={props.labelClassName}>{props.label}</span>
145 </Button>
125 <StateIcon />
126 <ContextChipLabel>{props.label}</ContextChipLabel>
127 </ContextChip>
146128 );
147129 if (previewTarget !== null) {
148130 return (
......@@ -196,16 +178,13 @@ function averageImageColor(image: HTMLImageElement): string | undefined {
196178export function ImageChipButton({
197179 name,
198180 previewUrl,
199 className,
200 labelClassName,
201181 size,
202182 suffix,
203183 style,
204184 ...props
205}: ComponentProps<"button"> & {
185}: Omit<ComponentProps<typeof ContextChip>, "kind" | "render"> & {
206186 name: string;
207187 previewUrl: string | undefined;
208 labelClassName: string;
209188 /** Every attachment chip reports its size; images are no exception. */
210189 size: string;
211190 suffix?: string | null;
......@@ -214,14 +193,9 @@ export function ImageChipButton({
214193 const [corsFailedUrl, setCorsFailedUrl] = useState<string>();
215194 const accent = sample?.url === previewUrl ? sample?.color : undefined;
216195 return (
217 <Button
218 variant="chip"
219 className={cn(
220 className,
221 CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.image,
222 CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
223 "cursor-zoom-in",
224 )}
196 <ContextChip
197 kind="image"
198 render={<button type="button" />}
225199 aria-label={`Image attachment, ${name}, ${size}`}
226200 style={{ ...style, ...(accent ? { "--context-chip-accent": accent } : {}) } as CSSProperties}
227201 {...props}
......@@ -232,25 +206,19 @@ export function ImageChipButton({
232206 crossOrigin={corsFailedUrl === previewUrl ? undefined : "anonymous"}
233207 src={previewUrl}
234208 alt=""
235 className="size-3.5 shrink-0 rounded-sm object-cover"
209 className="size-[1.17em] shrink-0 rounded-sm object-cover"
236210 onError={() => setCorsFailedUrl(previewUrl)}
237211 onLoad={(event) =>
238212 setSample({ url: previewUrl, color: averageImageColor(event.currentTarget) })
239213 }
240214 />
241215 ) : (
242 <ImageIcon
243 className={cn(
244 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
245 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES.image,
246 "size-3.5",
247 )}
248 />
216 <ImageIcon />
249217 )}
250 <span className={cn(labelClassName, "max-w-72")}>{middleTruncateAttachmentName(name)}</span>
218 <ContextChipLabel className="max-w-72">{middleTruncateAttachmentName(name)}</ContextChipLabel>
251219 <span className="shrink-0 text-[10px] text-current">{size}</span>
252220 {suffix ? <span className="text-[10px] text-current">{suffix}</span> : null}
253 </Button>
221 </ContextChip>
254222 );
255223}
256224
......@@ -259,8 +227,6 @@ export function FileChip(props: {
259227 size: string;
260228 isVideo: boolean;
261229 theme: "light" | "dark";
262 className: string;
263 labelClassName: string;
264230 accessibleLabel: string;
265231 tooltip: string;
266232 suffix?: string | null;
......@@ -270,35 +236,35 @@ export function FileChip(props: {
270236 unresolved?: boolean;
271237 onOpen?: (() => void) | undefined;
272238}) {
273 const className = cn(
274 props.className,
275 props.isVideo
276 ? CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.video
277 : CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.file,
278 props.onOpen && !props.disabled && CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME,
279 props.onOpen && !props.disabled && (props.isVideo ? "cursor-zoom-in" : "cursor-pointer"),
280 props.unresolved && "border-dashed text-foreground",
281 props.error && "border-destructive/35 bg-destructive/8 text-destructive",
282 );
283239 const content = <FileChipContent {...props} />;
240 const state = props.unresolved
241 ? ("unresolved" as const)
242 : props.error
243 ? ("invalid" as const)
244 : null;
284245 const attributes = {
285 className,
246 kind: props.isVideo ? "video" : "file",
247 ...(state ? { state } : {}),
286248 "aria-label": [props.accessibleLabel, props.suffix].filter(Boolean).join(", "),
287249 "data-markdown-copy": props.copyMarkdown,
288250 "data-context-unresolved": props.unresolved ? "true" : undefined,
289 };
251 } as const;
290252 return (
291253 <Tooltip>
292254 <TooltipTrigger
293255 render={
294256 props.onOpen ? (
295 <Button variant="chip" disabled={props.disabled} onClick={props.onOpen} {...attributes}>
257 <ContextChip
258 render={<button type="button" disabled={props.disabled} />}
259 onClick={props.onOpen}
260 {...attributes}
261 >
296262 {content}
297 </Button>
263 </ContextChip>
298264 ) : (
299 <span tabIndex={0} {...attributes}>
265 <ContextChip tabIndex={0} {...attributes}>
300266 {content}
301 </span>
267 </ContextChip>
302268 )
303269 }
304270 />
......@@ -314,55 +280,33 @@ function FileChipContent(props: {
314280 size: string;
315281 isVideo: boolean;
316282 theme: "light" | "dark";
317 labelClassName: string;
318283 suffix?: string | null;
319284}) {
320285 return (
321286 <>
322287 {props.isVideo ? (
323 <FilmIcon
324 className={cn(
325 COMPOSER_INLINE_CHIP_ICON_CLASS_NAME,
326 CONTEXT_INLINE_CHIP_ICON_TONE_CLASS_NAMES.video,
327 "size-3.5",
328 )}
329 />
288 <FilmIcon />
330289 ) : (
331 <PierreEntryIcon
332 pathValue={props.name}
333 kind="file"
334 theme={props.theme}
335 className="size-3.5"
336 />
290 <PierreEntryIcon pathValue={props.name} kind="file" theme={props.theme} />
337291 )}
338 <span className={cn(props.labelClassName, "max-w-72")}>
292 <ContextChipLabel className="max-w-72">
339293 {middleTruncateAttachmentName(props.name)}
340 </span>
294 </ContextChipLabel>
341295 <span className="shrink-0 text-[10px] text-current">{props.size}</span>
342296 {props.suffix ? <span className="text-[10px] text-current">{props.suffix}</span> : null}
343297 </>
344298 );
345299}
346300
347export function UnresolvedChip(props: {
348 label: string;
349 className: string;
350 labelClassName: string;
351 tooltip: string;
352 tooltipClassName: string;
353 copyMarkdown?: string;
354}) {
301export function UnresolvedChip(props: { label: string; tooltip: string; copyMarkdown?: string }) {
355302 return (
356303 <ContextChipShell
357 icon={<CircleDashedIcon className={cn(COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, "size-3.5")} />}
304 icon={<CircleDashedIcon />}
358305 label={props.label}
359 className={props.className}
360 labelClassName={props.labelClassName}
306 state="unresolved"
361307 aria-label={`Unavailable context, ${props.label}`}
362308 data-markdown-copy={props.copyMarkdown}
363309 tooltip={props.tooltip}
364 tooltipClassName={props.tooltipClassName}
365 unresolved
366310 />
367311 );
368312}
apps/web/src/components/ui/button.tsx+1-3
......@@ -39,7 +39,6 @@ const buttonVariants = cva(
3939 xs: "h-7 gap-1 px-[calc(--spacing(2)-1px)] text-sm sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5",
4040 },
4141 variant: {
42 chip: "",
4342 default:
4443 "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-primary/90",
4544 destructive:
......@@ -85,8 +84,7 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) {
8584 : "button";
8685
8786 const defaultProps = {
88 className:
89 variant === "chip" ? cn(className) : cn(buttonVariants({ className, size, variant })),
87 className: cn(buttonVariants({ className, size, variant })),
9088 "data-slot": "button",
9189 type: typeValue,
9290 };
apps/web/src/index.css+5-12
......@@ -1770,10 +1770,14 @@ code {
17701770}
17711771
17721772.chat-markdown a {
1773 color: var(--info-foreground);
17741773 text-decoration: none;
17751774}
17761775
1776/* File links are Chips and own their color and focus ring. */
1777.chat-markdown a:not(.chat-markdown-file-link) {
1778 color: var(--info-foreground);
1779}
1780
17771781.chat-markdown a:not(.chat-markdown-file-link):hover,
17781782.chat-markdown a:not(.chat-markdown-file-link):focus-visible {
17791783 background-image: radial-gradient(circle, currentcolor 0.75px, transparent 1px);
......@@ -1825,17 +1829,6 @@ code {
18251829 font-size: 0.75rem;
18261830}
18271831
1828.chat-markdown .chat-markdown-file-link,
1829.chat-markdown .chat-markdown-file-link:hover {
1830 color: var(--contrast-foreground);
1831 text-decoration: none;
1832}
1833
1834.chat-markdown .chat-markdown-file-link:focus-visible {
1835 outline: none;
1836 box-shadow: 0 0 0 2px color-mix(in srgb, var(--ring) 70%, transparent);
1837}
1838
18391832.chat-markdown pre {
18401833 max-width: 100%;
18411834 overflow-x: auto;
scripts/lint-restyle-ceiling.ts+1-1
......@@ -8,7 +8,7 @@ import * as NodeURL from "node:url";
88// components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the
99// count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you
1010// migrate a file, and delete this script when the rule becomes an error.
11export const RESTYLE_CEILING = 623;
11export const RESTYLE_CEILING = 603;
1212
1313const RULE = "shadcn(no-restyle)";
1414