| author | |
| committer | |
| log | 266d70cc4d887b7459ec6557c430ec7bb92bd3fc |
| tree | 8ca54b8437862c97047cd42b4599210d7d52729c |
| parent | 29931dd3a9a4996a5282f59564a64e6be496d5c6 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 |
| 105 | 105 | import { MediaVideoPlayer } from "./media/MediaVideoPlayer"; |
| 106 | 106 | import { MediaActions, type MediaActionSource } from "./media/MediaActions"; |
| 107 | 107 | import { resolveProtocolRelativeMediaUrl } from "./media/mediaContent"; |
| 108 | import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip"; | |
| 108 | import { FileTagChipContent } from "./chat/FileTagChip"; | |
| 109 | 109 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; |
| 110 | 110 | import { |
| 111 | 111 | revealInFileExplorerLabelForKind, |
| ... | ... | @@ -118,6 +118,7 @@ import { |
| 118 | 118 | import { hasSpecificPierreIconForFileName, syntheticFileNameForLanguageId } from "../pierre-icons"; |
| 119 | 119 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 120 | 120 | import { Button } from "./ui/button"; |
| 121 | import { ContextChip } from "./ContextChip"; | |
| 121 | 122 | import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./ui/collapsible"; |
| 122 | 123 | import { ScrollArea } from "./ui/scroll-area"; |
| 123 | 124 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu"; |
| ... | ... | @@ -1156,11 +1157,9 @@ interface MarkdownFileLinkProps { |
| 1156 | 1157 | /** Platform-specific menu label ("Reveal in Finder", ...); required for the |
| 1157 | 1158 | reveal item to show. */ |
| 1158 | 1159 | revealLabel?: string | undefined; |
| 1159 | className?: string | undefined; | |
| 1160 | 1160 | } |
| 1161 | 1161 | |
| 1162 | const MARKDOWN_FILE_CHIP_CLASS_NAME = "chat-markdown-file-link"; | |
| 1163 | const 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`; | |
| 1162 | const MARKDOWN_FILE_LINK_CLASS_NAME = "chat-markdown-file-link"; | |
| 1164 | 1163 | |
| 1165 | 1164 | function pathParentSegments(path: string): string[] { |
| 1166 | 1165 | const normalized = path.replaceAll("\\", "/"); |
| ... | ... | @@ -1895,7 +1894,6 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 1895 | 1894 | onOpenMedia, |
| 1896 | 1895 | onReveal, |
| 1897 | 1896 | revealLabel, |
| 1898 | className, | |
| 1899 | 1897 | }: MarkdownFileLinkProps) { |
| 1900 | 1898 | const handleOpenInEditor = useCallback(() => { |
| 1901 | 1899 | if (!onOpen) { |
| ... | ... | @@ -2165,13 +2163,10 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 2165 | 2163 | <TooltipTrigger |
| 2166 | 2164 | render={ |
| 2167 | 2165 | 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} | |
| 2175 | 2170 | data-markdown-copy={copyMarkdown} |
| 2176 | 2171 | onClick={(event) => { |
| 2177 | 2172 | event.preventDefault(); |
| ... | ... | @@ -2188,25 +2183,21 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 2188 | 2183 | }} |
| 2189 | 2184 | onContextMenu={handleContextMenu} |
| 2190 | 2185 | > |
| 2191 | <FileTagChipContent path={iconPath} label={label} theme={theme} selectable /> | |
| 2192 | </a> | |
| 2186 | <FileTagChipContent path={iconPath} label={label} theme={theme} /> | |
| 2187 | </ContextChip> | |
| 2193 | 2188 | ) : ( |
| 2194 | <button | |
| 2195 | type="button" | |
| 2189 | <ContextChip | |
| 2190 | kind="mention" | |
| 2191 | render={<button type="button" />} | |
| 2196 | 2192 | aria-label={`File options for ${label}`} |
| 2197 | 2193 | 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")} | |
| 2204 | 2195 | data-markdown-copy={copyMarkdown} |
| 2205 | 2196 | onClick={handleContextMenu} |
| 2206 | 2197 | onContextMenu={handleContextMenu} |
| 2207 | 2198 | > |
| 2208 | <FileTagChipContent path={iconPath} label={label} theme={theme} selectable /> | |
| 2209 | </button> | |
| 2199 | <FileTagChipContent path={iconPath} label={label} theme={theme} /> | |
| 2200 | </ContextChip> | |
| 2210 | 2201 | ) |
| 2211 | 2202 | } |
| 2212 | 2203 | /> |
| ... | ... | @@ -2242,8 +2233,7 @@ function areMarkdownFileLinkPropsEqual( |
| 2242 | 2233 | previous.onOpenInBrowser === next.onOpenInBrowser && |
| 2243 | 2234 | previous.onOpenMedia === next.onOpenMedia && |
| 2244 | 2235 | previous.onReveal === next.onReveal && |
| 2245 | previous.revealLabel === next.revealLabel && | |
| 2246 | previous.className === next.className | |
| 2236 | previous.revealLabel === next.revealLabel | |
| 2247 | 2237 | ); |
| 2248 | 2238 | } |
| 2249 | 2239 | |
| ... | ... | @@ -2571,12 +2561,7 @@ function useChatMarkdownState({ |
| 2571 | 2561 | [cwd, findWorkspaceBasenameMatch, revealFileInFileManager], |
| 2572 | 2562 | ); |
| 2573 | 2563 | const fileLinkChip = useCallback( |
| 2574 | ( | |
| 2575 | fileLinkMeta: MarkdownFileLinkMeta, | |
| 2576 | copyMarkdown: string, | |
| 2577 | className?: string, | |
| 2578 | mediaSource?: string, | |
| 2579 | ) => { | |
| 2564 | (fileLinkMeta: MarkdownFileLinkMeta, copyMarkdown: string, mediaSource?: string) => { | |
| 2580 | 2565 | const parentSuffix = fileLinkParentSuffixByPath.get( |
| 2581 | 2566 | fileLinkMeta.filePath.replaceAll("\\", "/"), |
| 2582 | 2567 | ); |
| ... | ... | @@ -2633,7 +2618,6 @@ function useChatMarkdownState({ |
| 2633 | 2618 | ? () => openMarkdownFileInPreview(fileLinkMeta.filePath) |
| 2634 | 2619 | : undefined |
| 2635 | 2620 | } |
| 2636 | className={className} | |
| 2637 | 2621 | /> |
| 2638 | 2622 | ); |
| 2639 | 2623 | }, |
| ... | ... | @@ -3068,7 +3052,6 @@ const CHAT_MARKDOWN_COMPONENTS = { |
| 3068 | 3052 | return fileLinkChip( |
| 3069 | 3053 | fileLinkMeta, |
| 3070 | 3054 | `[${fileLinkMeta.basename}](${normalizedHref})`, |
| 3071 | props.className, | |
| 3072 | 3055 | normalizedHref, |
| 3073 | 3056 | ); |
| 3074 | 3057 | }, |
| ... | ... | @@ -3085,7 +3068,6 @@ const CHAT_MARKDOWN_COMPONENTS = { |
| 3085 | 3068 | return fileLinkChip( |
| 3086 | 3069 | fileLinkMeta, |
| 3087 | 3070 | `\`${codeText}\``, |
| 3088 | undefined, | |
| 3089 | 3071 | inlineCodeFilePathCandidate(codeText) ?? codeText.trim(), |
| 3090 | 3072 | ); |
| 3091 | 3073 | } |
apps/web/src/components/ComposerPromptEditorTiptap.tsx+21-27| ... | ... | @@ -58,18 +58,13 @@ import { |
| 58 | 58 | import { collectInlineContextIds } from "~/lib/composerContextReferences"; |
| 59 | 59 | import { cn, isMacPlatform } from "~/lib/utils"; |
| 60 | 60 | import { basenameOfPath } from "~/pierre-icons"; |
| 61 | import { | |
| 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"; | |
| 68 | import { FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip"; | |
| 61 | import { FileTagChipContent } from "./chat/FileTagChip"; | |
| 62 | import { SkillChipIcon } from "./chat/SkillInlineText"; | |
| 69 | 63 | import { AssistantCitationChip } from "./chat/AssistantCitationChip"; |
| 70 | 64 | import { getTimelinePageScrollKey } from "./chat/pageScrollController"; |
| 71 | 65 | import { ContextChipPopover } from "./contextChipParts"; |
| 72 | 66 | import { Button } from "./ui/button"; |
| 67 | import { ContextChip } from "./ContextChip"; | |
| 73 | 68 | import { |
| 74 | 69 | ComposerContextActionsContext, |
| 75 | 70 | ComposerContextReferenceChip, |
| ... | ... | @@ -190,6 +185,13 @@ function resolvedThemeFromDocument(): "light" | "dark" { |
| 190 | 185 | |
| 191 | 186 | // ── Inline atom nodes (chips) ───────────────────────────────────────────── |
| 192 | 187 | |
| 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 | */ | |
| 192 | const 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 | ||
| 193 | 195 | const ComposerMentionExtension = Node.create({ |
| 194 | 196 | name: "composer-mention", |
| 195 | 197 | group: "inline", |
| ... | ... | @@ -217,11 +219,11 @@ function ComposerMentionNodeView({ node }: NodeViewProps) { |
| 217 | 219 | const actions = use(ComposerContextActionsContext); |
| 218 | 220 | const path = (node.attrs.path as string) ?? ""; |
| 219 | 221 | const chip = ( |
| 220 | <Button | |
| 221 | variant="chip" | |
| 222 | <ContextChip | |
| 223 | kind="mention" | |
| 224 | render={<button type="button" />} | |
| 222 | 225 | onClick={() => actions.openMention(path)} |
| 223 | 226 | aria-label={`Preview ${path}`} |
| 224 | className={`${FILE_TAG_CHIP_CLASS_NAME} cursor-pointer focus-visible:outline-2`} | |
| 225 | 227 | contentEditable={false} |
| 226 | 228 | spellCheck={false} |
| 227 | 229 | data-composer-mention-chip="true" |
| ... | ... | @@ -231,10 +233,10 @@ function ComposerMentionNodeView({ node }: NodeViewProps) { |
| 231 | 233 | label={basenameOfPath(path)} |
| 232 | 234 | theme={resolvedThemeFromDocument()} |
| 233 | 235 | /> |
| 234 | </Button> | |
| 236 | </ContextChip> | |
| 235 | 237 | ); |
| 236 | 238 | return ( |
| 237 | <NodeViewWrapper as="span" className={COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME}> | |
| 239 | <NodeViewWrapper as="span" className={CHIP_NODE_SELECTION_CLASS_NAME}> | |
| 238 | 240 | <Tooltip> |
| 239 | 241 | <TooltipTrigger render={chip} /> |
| 240 | 242 | <TooltipPopup side="top">{path}</TooltipPopup> |
| ... | ... | @@ -275,20 +277,12 @@ function ComposerSkillNodeView({ node }: NodeViewProps) { |
| 275 | 277 | const skillDescription = (node.attrs.skillDescription as string | null) ?? null; |
| 276 | 278 | const skill = skills.find((candidate) => candidate.name === skillName); |
| 277 | 279 | return ( |
| 278 | <NodeViewWrapper as="span" className={COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME}> | |
| 280 | <NodeViewWrapper as="span" className={CHIP_NODE_SELECTION_CLASS_NAME}> | |
| 279 | 281 | <ContextChipPopover |
| 282 | kind="skill" | |
| 283 | icon={<SkillChipIcon />} | |
| 284 | label={skillLabel} | |
| 280 | 285 | 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 | } | |
| 292 | 286 | > |
| 293 | 287 | <div className="space-y-3 p-2 text-sm"> |
| 294 | 288 | <p className="font-medium">{skillLabel}</p> |
| ... | ... | @@ -380,7 +374,7 @@ function ComposerCitationNodeView({ node, editor, getPos }: NodeViewProps) { |
| 380 | 374 | return ( |
| 381 | 375 | <NodeViewWrapper |
| 382 | 376 | as="span" |
| 383 | className="inline-flex min-w-0 max-w-full" | |
| 377 | className="inline-flex min-w-0 max-w-full select-none" | |
| 384 | 378 | contentEditable={false} |
| 385 | 379 | spellCheck={false} |
| 386 | 380 | data-composer-citation-chip="true" |
| ... | ... | @@ -435,7 +429,7 @@ const ComposerContextReferenceExtension = Node.create({ |
| 435 | 429 | |
| 436 | 430 | function ComposerContextReferenceNodeView({ node }: NodeViewProps) { |
| 437 | 431 | return ( |
| 438 | <NodeViewWrapper as="span" className={COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME}> | |
| 432 | <NodeViewWrapper as="span" className={CHIP_NODE_SELECTION_CLASS_NAME}> | |
| 439 | 433 | <ComposerContextReferenceChip |
| 440 | 434 | kind={(node.attrs.kind as string) ?? ""} |
| 441 | 435 | contextId={(node.attrs.contextId as string) ?? ""} |
apps/web/src/components/ContextChip.tsx created+131| ... | ... | @@ -0,0 +1,131 @@ |
| 1 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 2 | import { useRender } from "@base-ui/react/use-render"; | |
| 3 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 4 | import type * as React from "react"; | |
| 5 | ||
| 6 | import { 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 | */ | |
| 25 | const 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 | ||
| 83 | type ContextChipKind = NonNullable<VariantProps<typeof contextChipVariants>["kind"]>; | |
| 84 | ||
| 85 | interface ContextChipProps extends useRender.ComponentProps<"span"> { | |
| 86 | kind?: ContextChipKind; | |
| 87 | state?: "unresolved" | "invalid"; | |
| 88 | } | |
| 89 | ||
| 90 | function 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. */ | |
| 104 | function 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. */ | |
| 115 | function 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 | ||
| 131 | export { ContextChip, ContextChipAction, ContextChipLabel, type ContextChipKind }; |
apps/web/src/components/chat/AssistantCitationChip.tsx+10-26| ... | ... | @@ -13,19 +13,11 @@ import { |
| 13 | 13 | findAssistantCitationSourceAnchor, |
| 14 | 14 | type AssistantCitationSourceAnchor, |
| 15 | 15 | } from "~/lib/assistantTextSelection"; |
| 16 | import { cn } from "~/lib/utils"; | |
| 17 | 16 | import { |
| 18 | 17 | assistantCitationHash, |
| 19 | 18 | assistantCitationNavigation, |
| 20 | 19 | } from "../../lib/assistantCitationNavigation"; |
| 21 | import { | |
| 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"; | |
| 20 | import { ContextChip, ContextChipAction, ContextChipLabel } from "../ContextChip"; | |
| 29 | 21 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 30 | 22 | import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 31 | 23 | import { AssistantCitationCommentEditor } from "./AssistantCitationCommentEditor"; |
| ... | ... | @@ -33,11 +25,6 @@ import { resolveAssistantCitationCommentDismissal } from "./assistantCitationCom |
| 33 | 25 | import { observeAssistantCitationCommentSource } from "./AssistantCitationSource"; |
| 34 | 26 | import { composerFloatingLayerProps } from "./composerEventScope"; |
| 35 | 27 | |
| 36 | const 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 | ||
| 41 | 28 | export function AssistantCitationChip({ |
| 42 | 29 | citation, |
| 43 | 30 | composer = false, |
| ... | ... | @@ -126,8 +113,8 @@ export function AssistantCitationChip({ |
| 126 | 113 | 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)]" |
| 127 | 114 | aria-label={`View cited assistant text: ${label}`} |
| 128 | 115 | > |
| 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> | |
| 131 | 118 | </Link> |
| 132 | 119 | ); |
| 133 | 120 | const chatSourceLink = ( |
| ... | ... | @@ -136,16 +123,13 @@ export function AssistantCitationChip({ |
| 136 | 123 | 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)]" |
| 137 | 124 | aria-label={`View cited assistant text: ${label}`} |
| 138 | 125 | > |
| 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> | |
| 141 | 128 | </Link> |
| 142 | 129 | ); |
| 143 | 130 | 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" | |
| 149 | 133 | contentEditable={false} |
| 150 | 134 | data-assistant-citation-chip="true" |
| 151 | 135 | data-markdown-copy={serializeAssistantCitation(citation)} |
| ... | ... | @@ -171,9 +155,9 @@ export function AssistantCitationChip({ |
| 171 | 155 | > |
| 172 | 156 | <PopoverTrigger |
| 173 | 157 | aria-label={citation.comment ? "Edit citation comment" : "Add comment to citation"} |
| 174 | className={CITATION_ACTION_BUTTON_CLASS_NAME} | |
| 158 | render={<ContextChipAction />} | |
| 175 | 159 | > |
| 176 | <PencilIcon aria-hidden="true" className="size-[0.85em]" /> | |
| 160 | <PencilIcon aria-hidden="true" /> | |
| 177 | 161 | </PopoverTrigger> |
| 178 | 162 | {commentEditor.open ? ( |
| 179 | 163 | <PopoverPopup |
| ... | ... | @@ -223,6 +207,6 @@ export function AssistantCitationChip({ |
| 223 | 207 | ) : null} |
| 224 | 208 | </Popover> |
| 225 | 209 | ) : null} |
| 226 | </span> | |
| 210 | </ContextChip> | |
| 227 | 211 | ); |
| 228 | 212 | } |
apps/web/src/components/chat/FileTagChip.tsx+3-22| ... | ... | @@ -1,22 +1,12 @@ |
| 1 | 1 | import { inferEntryKindFromPath } from "../../pierre-icons"; |
| 2 | import { | |
| 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"; | |
| 2 | import { ContextChipLabel } from "../ContextChip"; | |
| 10 | 3 | import { PierreEntryIcon } from "./PierreEntryIcon"; |
| 11 | 4 | |
| 12 | export const FILE_TAG_CHIP_CLASS_NAME = `${COMPOSER_INLINE_CHIP_CLASS_NAME} ${CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.mention}`; | |
| 13 | export 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">`. */ | |
| 15 | 6 | export function FileTagChipContent(props: { |
| 16 | 7 | path: string; |
| 17 | 8 | label: string; |
| 18 | 9 | theme: "light" | "dark"; |
| 19 | selectable?: boolean; | |
| 20 | 10 | }) { |
| 21 | 11 | return ( |
| 22 | 12 | <> |
| ... | ... | @@ -24,17 +14,8 @@ export function FileTagChipContent(props: { |
| 24 | 14 | pathValue={props.path} |
| 25 | 15 | kind={inferEntryKindFromPath(props.path)} |
| 26 | 16 | theme={props.theme} |
| 27 | className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} | |
| 28 | 17 | /> |
| 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> | |
| 38 | 19 | </> |
| 39 | 20 | ); |
| 40 | 21 | } |
apps/web/src/components/chat/MessagesTimeline.tsx+27-117| ... | ... | @@ -214,6 +214,7 @@ import { |
| 214 | 214 | ContextChipShell, |
| 215 | 215 | FileChip, |
| 216 | 216 | ImageChipButton, |
| 217 | PULL_REQUEST_CHIP_KINDS, | |
| 217 | 218 | PullRequestChip, |
| 218 | 219 | UnresolvedChip, |
| 219 | 220 | } from "../contextChipParts"; |
| ... | ... | @@ -236,15 +237,7 @@ import { |
| 236 | 237 | encodeComposerContextFragment, |
| 237 | 238 | } from "@t3tools/shared/composerContextClipboard"; |
| 238 | 239 | import { chatMarkdownClipboardPayload } from "../../markdown-clipboard"; |
| 239 | import { | |
| 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"; | |
| 240 | import { ContextChip, ContextChipLabel, type ContextChipKind } from "../ContextChip"; | |
| 248 | 241 | import { createContextPresentationRegistry } from "../contextPresentationRegistry"; |
| 249 | 242 | import { useOpenPrLink } from "~/lib/openPullRequestLink"; |
| 250 | 243 | import type { ChatMarkdownContextReference } from "../ChatMarkdown"; |
| ... | ... | @@ -254,7 +247,7 @@ import { useUiStateStore } from "~/uiStateStore"; |
| 254 | 247 | import { type TimestampFormat } from "@t3tools/contracts/settings"; |
| 255 | 248 | import { formatChatTimestampTooltip, formatDayAwareTimestamp } from "../../timestampFormat"; |
| 256 | 249 | |
| 257 | import { SkillInlineText } from "./SkillInlineText"; | |
| 250 | import { SkillChipIcon, SkillInlineText } from "./SkillInlineText"; | |
| 258 | 251 | import { deriveAgentSpawnSummary } from "./agentSpawnSummary"; |
| 259 | 252 | import { formatWorkspaceRelativePath } from "../../filePathDisplay"; |
| 260 | 253 | import { |
| ... | ... | @@ -2559,8 +2552,9 @@ function BackgroundWorktreeSetupChip({ snapshot }: { snapshot: WorktreeSetupSnap |
| 2559 | 2552 | <PopoverTrigger |
| 2560 | 2553 | render={ |
| 2561 | 2554 | <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" | |
| 2564 | 2558 | aria-label={`${scriptName} is still running. Show setup progress.`} |
| 2565 | 2559 | /> |
| 2566 | 2560 | } |
| ... | ... | @@ -3453,14 +3447,10 @@ function UserMessageMentionChip(props: { |
| 3453 | 3447 | <Tooltip> |
| 3454 | 3448 | <TooltipTrigger |
| 3455 | 3449 | render={ |
| 3456 | <button | |
| 3457 | type="button" | |
| 3450 | <ContextChip | |
| 3451 | kind="mention" | |
| 3452 | render={<button type="button" />} | |
| 3458 | 3453 | 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 | )} | |
| 3464 | 3454 | data-markdown-copy={props.copyMarkdown} |
| 3465 | 3455 | onClick={() => { |
| 3466 | 3456 | if (ctx.threadRef) |
| ... | ... | @@ -3471,10 +3461,9 @@ function UserMessageMentionChip(props: { |
| 3471 | 3461 | pathValue={props.record.path} |
| 3472 | 3462 | kind={inferEntryKindFromPath(props.record.path)} |
| 3473 | 3463 | theme={ctx.resolvedTheme} |
| 3474 | className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} | |
| 3475 | 3464 | /> |
| 3476 | <span className={CHAT_INLINE_CHIP_LABEL_CLASS_NAME}>{props.record.label}</span> | |
| 3477 | </button> | |
| 3465 | <ContextChipLabel>{props.record.label}</ContextChipLabel> | |
| 3466 | </ContextChip> | |
| 3478 | 3467 | } |
| 3479 | 3468 | /> |
| 3480 | 3469 | <TooltipPopup>{props.record.path}</TooltipPopup> |
| ... | ... | @@ -3488,21 +3477,16 @@ function UserMessageContextChip(props: { |
| 3488 | 3477 | kindLabel?: string; |
| 3489 | 3478 | copyMarkdown: string; |
| 3490 | 3479 | tooltip?: string; |
| 3491 | toneClassName?: string; | |
| 3492 | interactive?: boolean; | |
| 3493 | unresolved?: boolean; | |
| 3480 | kind: ContextChipKind; | |
| 3494 | 3481 | }) { |
| 3495 | 3482 | return ( |
| 3496 | 3483 | <ContextChipShell |
| 3484 | kind={props.kind} | |
| 3497 | 3485 | icon={props.icon} |
| 3498 | 3486 | label={props.label} |
| 3499 | className={cn(CHAT_INLINE_CHIP_CLASS_NAME, props.toneClassName)} | |
| 3500 | labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 3501 | 3487 | aria-label={props.kindLabel ? `${props.kindLabel}, ${props.label}` : undefined} |
| 3502 | 3488 | data-markdown-copy={props.copyMarkdown} |
| 3503 | 3489 | tooltip={props.tooltip} |
| 3504 | interactive={props.interactive === true} | |
| 3505 | unresolved={props.unresolved === true} | |
| 3506 | 3490 | /> |
| 3507 | 3491 | ); |
| 3508 | 3492 | } |
| ... | ... | @@ -3510,7 +3494,7 @@ function UserMessageContextChip(props: { |
| 3510 | 3494 | function UserMessagePullRequestContextChip(props: { |
| 3511 | 3495 | record: Extract<KnownComposerContextRecord, { kind: "review-comment" }>; |
| 3512 | 3496 | copyMarkdown: string; |
| 3513 | toneClassName: string; | |
| 3497 | kind: ContextChipKind; | |
| 3514 | 3498 | }) { |
| 3515 | 3499 | const { activeThreadEnvironmentId, openPullRequest } = use(TimelineRowCtx); |
| 3516 | 3500 | const metadata = props.record.pullRequest; |
| ... | ... | @@ -3521,8 +3505,7 @@ function UserMessagePullRequestContextChip(props: { |
| 3521 | 3505 | environmentId={activeThreadEnvironmentId} |
| 3522 | 3506 | label={reviewCommentContextLabel(props.record)} |
| 3523 | 3507 | 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} | |
| 3526 | 3509 | copyMarkdown={props.copyMarkdown} |
| 3527 | 3510 | onOpen={openPullRequest} |
| 3528 | 3511 | /> |
| ... | ... | @@ -3675,11 +3658,8 @@ function UnavailableUserMessageContextChip(props: UserMessageContextRenderContex |
| 3675 | 3658 | return ( |
| 3676 | 3659 | <UnresolvedChip |
| 3677 | 3660 | label={props.reference.label} |
| 3678 | className={CHAT_INLINE_CHIP_CLASS_NAME} | |
| 3679 | labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 3680 | 3661 | copyMarkdown={props.copyMarkdown} |
| 3681 | 3662 | tooltip="This context is no longer available." |
| 3682 | tooltipClassName="max-w-96 whitespace-pre-wrap leading-tight" | |
| 3683 | 3663 | /> |
| 3684 | 3664 | ); |
| 3685 | 3665 | } |
| ... | ... | @@ -3707,18 +3687,12 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3707 | 3687 | render: (record, context) => |
| 3708 | 3688 | record.kind === "skill" ? ( |
| 3709 | 3689 | <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 />} | |
| 3717 | 3691 | label={record.label || record.name} |
| 3718 | 3692 | kindLabel="Skill" |
| 3719 | 3693 | tooltip={`$${record.name}`} |
| 3720 | 3694 | copyMarkdown={context.copyMarkdown} |
| 3721 | toneClassName={CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.skill} | |
| 3695 | kind="skill" | |
| 3722 | 3696 | /> |
| 3723 | 3697 | ) : ( |
| 3724 | 3698 | <UnavailableUserMessageContextChip {...context} /> |
| ... | ... | @@ -3743,8 +3717,6 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3743 | 3717 | <ImageChipButton |
| 3744 | 3718 | name={record.name} |
| 3745 | 3719 | previewUrl={attachment.previewUrl} |
| 3746 | className={CHAT_INLINE_CHIP_CLASS_NAME} | |
| 3747 | labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 3748 | 3720 | size={formatAttachmentSize(record.sizeBytes)} |
| 3749 | 3721 | data-markdown-copy={context.copyMarkdown} |
| 3750 | 3722 | onClick={() => context.onExpandImage(attachment)} |
| ... | ... | @@ -3777,8 +3749,6 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3777 | 3749 | size={size} |
| 3778 | 3750 | isVideo={isVideo} |
| 3779 | 3751 | theme={context.resolvedTheme} |
| 3780 | className={CHAT_INLINE_CHIP_CLASS_NAME} | |
| 3781 | labelClassName={CHAT_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 3782 | 3752 | disabled={disabled} |
| 3783 | 3753 | accessibleLabel={`${isVideo ? "Video" : "File"} attachment, ${record.name}, ${size}`} |
| 3784 | 3754 | copyMarkdown={context.copyMarkdown} |
| ... | ... | @@ -3797,7 +3767,6 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3797 | 3767 | record.kind === "terminal" ? ( |
| 3798 | 3768 | <span data-markdown-copy={context.copyMarkdown}> |
| 3799 | 3769 | <TerminalContextInlineChip |
| 3800 | surface="transcript" | |
| 3801 | 3770 | label={record.label} |
| 3802 | 3771 | terminalLabel={record.terminalLabel} |
| 3803 | 3772 | lineStart={record.lineStart} |
| ... | ... | @@ -3818,24 +3787,9 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3818 | 3787 | <UserMessageContextPopover |
| 3819 | 3788 | copyMarkdown={context.copyMarkdown} |
| 3820 | 3789 | 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} | |
| 3839 | 3793 | > |
| 3840 | 3794 | <UserMessageElementDetails record={record} /> |
| 3841 | 3795 | </UserMessageContextPopover> |
| ... | ... | @@ -3859,7 +3813,7 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3859 | 3813 | <UserMessagePullRequestContextChip |
| 3860 | 3814 | record={record} |
| 3861 | 3815 | copyMarkdown={context.copyMarkdown} |
| 3862 | toneClassName={PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES[pullRequestState]} | |
| 3816 | kind={PULL_REQUEST_CHIP_KINDS[pullRequestState]} | |
| 3863 | 3817 | /> |
| 3864 | 3818 | ); |
| 3865 | 3819 | } |
| ... | ... | @@ -3867,38 +3821,9 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3867 | 3821 | <UserMessageContextPopover |
| 3868 | 3822 | copyMarkdown={context.copyMarkdown} |
| 3869 | 3823 | 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} | |
| 3902 | 3827 | > |
| 3903 | 3828 | <UserMessageReviewCommentCard |
| 3904 | 3829 | comment={{ |
| ... | ... | @@ -3929,24 +3854,9 @@ const userMessageContextPresentationRegistry = createContextPresentationRegistry |
| 3929 | 3854 | <UserMessageContextPopover |
| 3930 | 3855 | copyMarkdown={context.copyMarkdown} |
| 3931 | 3856 | 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} | |
| 3950 | 3860 | > |
| 3951 | 3861 | <UserMessagePreviewAnnotationDetails record={record} image={context.annotationImage} /> |
| 3952 | 3862 | </UserMessageContextPopover> |
apps/web/src/components/chat/SkillInlineText.tsx+17-20| ... | ... | @@ -2,14 +2,8 @@ import { Children, cloneElement, isValidElement, type ReactNode } from "react"; |
| 2 | 2 | import type { ServerProviderSkill } from "@t3tools/contracts"; |
| 3 | 3 | import { formatProviderSkillDisplayName } from "@t3tools/client-runtime/providerSkills"; |
| 4 | 4 | |
| 5 | import { | |
| 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"; | |
| 12 | import { cn } from "~/lib/utils"; | |
| 5 | import { SKILL_CHIP_ICON_SVG } from "../composerInlineChip"; | |
| 6 | import { ContextChip, ContextChipLabel } from "../ContextChip"; | |
| 13 | 7 | |
| 14 | 8 | const SKILL_TOKEN_REGEX = |
| 15 | 9 | /(^|\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( |
| 76 | 70 | |
| 77 | 71 | function SkillChip(props: { skill: InlineSkill; rawText: string }) { |
| 78 | 72 | 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. */ | |
| 81 | export function SkillChipIcon() { | |
| 82 | return ( | |
| 83 | <span | |
| 84 | aria-hidden="true" | |
| 85 | className="contents" | |
| 86 | dangerouslySetInnerHTML={{ __html: SKILL_CHIP_ICON_SVG }} | |
| 87 | /> | |
| 91 | 88 | ); |
| 92 | 89 | } |
apps/web/src/components/chat/TerminalContextInlineChip.tsx+6-44| ... | ... | @@ -1,16 +1,6 @@ |
| 1 | 1 | import { TerminalIcon } from "lucide-react"; |
| 2 | 2 | |
| 3 | import { cn } from "~/lib/utils"; | |
| 4 | 3 | import type { ContextPresentationCapability } from "../contextPresentationRegistry"; |
| 5 | import { | |
| 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"; | |
| 14 | 4 | import { ContextChipPopover, ContextChipShell } from "../contextChipParts"; |
| 15 | 5 | |
| 16 | 6 | interface TerminalContextInlineChipProps { |
| ... | ... | @@ -21,41 +11,18 @@ interface TerminalContextInlineChipProps { |
| 21 | 11 | text: string; |
| 22 | 12 | detailsMode: ContextPresentationCapability["details"]; |
| 23 | 13 | expired?: boolean; |
| 24 | surface?: "composer" | "transcript"; | |
| 25 | 14 | } |
| 26 | 15 | |
| 27 | 16 | export function TerminalContextInlineChip(props: TerminalContextInlineChipProps) { |
| 28 | 17 | 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 | ); | |
| 42 | 18 | |
| 43 | 19 | if (!expired && text.length > 0 && detailsMode === "popover") { |
| 44 | 20 | return ( |
| 45 | 21 | <ContextChipPopover |
| 22 | kind="terminal" | |
| 23 | icon={<TerminalIcon />} | |
| 24 | label={label} | |
| 46 | 25 | 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 | )} | |
| 59 | 26 | popupClassName="w-[min(40rem,calc(100vw-2rem))]" |
| 60 | 27 | viewportClassName="overflow-hidden p-2" |
| 61 | 28 | > |
| ... | ... | @@ -83,17 +50,12 @@ export function TerminalContextInlineChip(props: TerminalContextInlineChipProps) |
| 83 | 50 | |
| 84 | 51 | return ( |
| 85 | 52 | <ContextChipShell |
| 86 | icon={icon} | |
| 53 | kind="terminal" | |
| 54 | {...(expired ? { state: "invalid" as const } : {})} | |
| 55 | icon={<TerminalIcon />} | |
| 87 | 56 | 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} | |
| 94 | 57 | aria-label={`Terminal excerpt, ${label}${expired ? ", expired" : ""}`} |
| 95 | 58 | data-terminal-context-expired={expired ? "true" : undefined} |
| 96 | tooltipClassName="max-w-80 whitespace-pre-wrap leading-tight" | |
| 97 | 59 | tooltip={ |
| 98 | 60 | expired |
| 99 | 61 | ? `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 { |
| 14 | 14 | formatAttachmentUploadProgress, |
| 15 | 15 | type AttachmentUploadState, |
| 16 | 16 | } from "~/lib/attachmentUploadState"; |
| 17 | import { cn } from "~/lib/utils"; | |
| 18 | 17 | import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; |
| 19 | 18 | import { |
| 20 | 19 | fileContextReference, |
| ... | ... | @@ -36,20 +35,14 @@ import { |
| 36 | 35 | createContextPresentationRegistry, |
| 37 | 36 | type ContextPresentationCapability, |
| 38 | 37 | } from "./contextPresentationRegistry"; |
| 39 | import { | |
| 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"; | |
| 38 | import type { ContextChipKind } from "./ContextChip"; | |
| 47 | 39 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 48 | 40 | import { |
| 49 | 41 | ContextChipPopover, |
| 50 | 42 | ContextChipShell, |
| 51 | 43 | FileChip, |
| 52 | 44 | ImageChipButton, |
| 45 | PULL_REQUEST_CHIP_KINDS, | |
| 53 | 46 | PullRequestChip, |
| 54 | 47 | UnresolvedChip, |
| 55 | 48 | } from "./contextChipParts"; |
| ... | ... | @@ -138,30 +131,31 @@ function ContextChip(props: { |
| 138 | 131 | kindLabel: string; |
| 139 | 132 | details: ReactNode; |
| 140 | 133 | detailsMode: ContextPresentationCapability["details"]; |
| 141 | toneClassName: string; | |
| 134 | kind: ContextChipKind; | |
| 142 | 135 | }) { |
| 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 ( | |
| 144 | 149 | <ContextChipShell |
| 150 | kind={props.kind} | |
| 145 | 151 | icon={props.icon} |
| 146 | 152 | 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} | |
| 151 | 153 | aria-label={ |
| 152 | 154 | props.detailsMode === "tooltip" ? `${props.kindLabel}, ${props.label}` : undefined |
| 153 | 155 | } |
| 154 | 156 | tooltip={props.detailsMode === "tooltip" ? props.details : undefined} |
| 155 | 157 | /> |
| 156 | 158 | ); |
| 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; | |
| 165 | 159 | } |
| 166 | 160 | |
| 167 | 161 | function uploadStatusSuffix(upload: AttachmentUploadState | undefined): string | null { |
| ... | ... | @@ -191,8 +185,6 @@ function ImageContextChip(props: { |
| 191 | 185 | <ImageChipButton |
| 192 | 186 | name={props.record.name} |
| 193 | 187 | previewUrl={props.record.previewUrl} |
| 194 | className={COMPOSER_INLINE_CHIP_CLASS_NAME} | |
| 195 | labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 196 | 188 | size={formatAttachmentSize(props.record.sizeBytes)} |
| 197 | 189 | suffix={uploadStatusSuffix(props.upload)} |
| 198 | 190 | onClick={() => actions.expandImage(props.record.id)} |
| ... | ... | @@ -222,8 +214,6 @@ function FileContextChip(props: { |
| 222 | 214 | size={size} |
| 223 | 215 | isVideo={isVideo} |
| 224 | 216 | theme={resolvedTheme} |
| 225 | className={COMPOSER_INLINE_CHIP_CLASS_NAME} | |
| 226 | labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 227 | 217 | error={props.upload?.status === "failed"} |
| 228 | 218 | unresolved={needsReattach} |
| 229 | 219 | suffix={suffix} |
| ... | ... | @@ -243,7 +233,7 @@ function FileContextChip(props: { |
| 243 | 233 | ); |
| 244 | 234 | } |
| 245 | 235 | |
| 246 | function PullRequestContextChip(props: { record: ReviewCommentContext; toneClassName: string }) { | |
| 236 | function PullRequestContextChip(props: { record: ReviewCommentContext; kind: ContextChipKind }) { | |
| 247 | 237 | const actions = use(ComposerContextActionsContext); |
| 248 | 238 | const metadata = props.record.pullRequest; |
| 249 | 239 | if (metadata === undefined) return null; |
| ... | ... | @@ -253,8 +243,7 @@ function PullRequestContextChip(props: { record: ReviewCommentContext; toneClass |
| 253 | 243 | environmentId={actions.environmentId} |
| 254 | 244 | label={reviewCommentContextLabel(props.record)} |
| 255 | 245 | 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} | |
| 258 | 247 | onOpen={actions.openPullRequest} |
| 259 | 248 | /> |
| 260 | 249 | ); |
| ... | ... | @@ -324,10 +313,7 @@ function UnresolvedContextChip(props: { label: string }) { |
| 324 | 313 | return ( |
| 325 | 314 | <UnresolvedChip |
| 326 | 315 | label={props.label} |
| 327 | className={COMPOSER_INLINE_CHIP_CLASS_NAME} | |
| 328 | labelClassName={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME} | |
| 329 | 316 | tooltip="This context is no longer available. Remove it or attach it again." |
| 330 | tooltipClassName="max-w-80 leading-tight" | |
| 331 | 317 | /> |
| 332 | 318 | ); |
| 333 | 319 | } |
| ... | ... | @@ -389,40 +375,18 @@ const composerContextPresentationRegistry = createContextPresentationRegistry< |
| 389 | 375 | return ( |
| 390 | 376 | <PullRequestContextChip |
| 391 | 377 | record={entry.record} |
| 392 | toneClassName={PULL_REQUEST_INLINE_CHIP_TONE_CLASS_NAMES[pullRequestState]} | |
| 378 | kind={PULL_REQUEST_CHIP_KINDS[pullRequestState]} | |
| 393 | 379 | /> |
| 394 | 380 | ); |
| 395 | 381 | } |
| 396 | 382 | return ( |
| 397 | 383 | <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 />} | |
| 417 | 385 | label={reviewCommentContextLabel(entry.record)} |
| 418 | 386 | kindLabel={isPullRequest ? pullRequestContextKindLabel(entry.record) : "Review comment"} |
| 419 | 387 | details={<ComposerReviewCommentDetails comment={entry.record} />} |
| 420 | 388 | 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"} | |
| 426 | 390 | /> |
| 427 | 391 | ); |
| 428 | 392 | }, |
| ... | ... | @@ -433,20 +397,12 @@ const composerContextPresentationRegistry = createContextPresentationRegistry< |
| 433 | 397 | render: (entry, context, definition) => |
| 434 | 398 | entry.kind === "preview-annotation" ? ( |
| 435 | 399 | <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 />} | |
| 445 | 401 | label={previewAnnotationContextLabel(entry.record)} |
| 446 | 402 | kindLabel="Preview annotation" |
| 447 | 403 | details={<ComposerPreviewAnnotationDetails annotation={entry.record} />} |
| 448 | 404 | detailsMode={definition.capabilities.details} |
| 449 | toneClassName={CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES["preview-annotation"]} | |
| 405 | kind="preview-annotation" | |
| 450 | 406 | /> |
| 451 | 407 | ) : ( |
| 452 | 408 | <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. | |
| 4 | const 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 | ||
| 7 | const INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_GEOMETRY_CLASS_NAME} border border-border/70 bg-accent/40 text-foreground`; | |
| 8 | ||
| 9 | const 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 | ||
| 12 | export const CHAT_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[12px]`; | |
| 13 | ||
| 14 | export const COMPOSER_INLINE_CHIP_CLASS_NAME = `${INLINE_CHIP_CLASS_NAME} text-[0.86em] select-none`; | |
| 15 | ||
| 16 | export 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 | ||
| 19 | export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME = | |
| 20 | "block size-[1.17em] shrink-0 self-center [&>svg]:block"; | |
| 21 | ||
| 22 | export const CONTEXT_INLINE_CHIP_FOCUS_CLASS_NAME = | |
| 23 | "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--contrast-foreground)]"; | |
| 24 | ||
| 25 | export 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 | ||
| 27 | export 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 | */ | |
| 47 | export 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 | ||
| 61 | export 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 | ||
| 69 | export const CHAT_INLINE_CHIP_LABEL_CLASS_NAME = "truncate leading-tight"; | |
| 70 | ||
| 71 | export const COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME = | |
| 72 | "block self-center truncate leading-tight select-none"; | |
| 73 | ||
| 74 | export 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 | ||
| 76 | 1 | export 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>`; |
| 77 | 2 | |
| 78 | export 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 | ||
| 81 | 3 | /** Keeps both the recognizable beginning and the extension/end of a long attachment name. */ |
| 82 | 4 | export function middleTruncateAttachmentName(name: string, maxCharacters = 36): string { |
| 83 | 5 | const characters = Array.from(name); |
apps/web/src/components/contextChipParts.tsx+67-123| ... | ... | @@ -10,75 +10,66 @@ import { |
| 10 | 10 | |
| 11 | 11 | import { cn } from "~/lib/utils"; |
| 12 | 12 | import { PULL_REQUEST_STATE_PRESENTATION } from "~/components/pullRequest/pullRequestIcons"; |
| 13 | import type { PullRequestContextDisplayState } from "~/lib/composerContextRecords"; | |
| 13 | 14 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; |
| 14 | import { | |
| 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"; | |
| 15 | import { middleTruncateAttachmentName } from "./composerInlineChip"; | |
| 22 | 16 | import { PullRequestContextDetails } from "./PullRequestContextDetails"; |
| 23 | import { Button } from "./ui/button"; | |
| 17 | import { ContextChip, ContextChipLabel, type ContextChipKind } from "./ContextChip"; | |
| 24 | 18 | import { Popover, PopoverPopup, PopoverTitle, PopoverTrigger } from "./ui/popover"; |
| 25 | 19 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; |
| 26 | 20 | import { PullRequestLinkPreview } from "./pullRequest/PullRequestLinkPreview"; |
| 27 | 21 | import { usePullRequestPreviewTarget } from "~/lib/openPullRequestLink"; |
| 28 | 22 | |
| 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. */ | |
| 24 | export 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. */ | |
| 30 | 33 | export function ContextChipShell({ |
| 31 | 34 | icon, |
| 32 | 35 | label, |
| 33 | labelClassName, | |
| 34 | 36 | tooltip, |
| 35 | tooltipClassName = "max-w-96 whitespace-pre-wrap leading-tight", | |
| 36 | interactive, | |
| 37 | unresolved, | |
| 38 | className, | |
| 39 | 37 | ...props |
| 40 | }: ComponentProps<"span"> & { | |
| 38 | }: Omit<ComponentProps<typeof ContextChip>, "className" | "render"> & { | |
| 41 | 39 | icon: ReactNode; |
| 42 | 40 | label: string; |
| 43 | labelClassName: string; | |
| 41 | /** Newlines in the tooltip are kept. */ | |
| 44 | 42 | tooltip?: ReactNode; |
| 45 | tooltipClassName?: string; | |
| 46 | interactive?: boolean; | |
| 47 | unresolved?: boolean; | |
| 48 | 43 | }) { |
| 49 | 44 | 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} | |
| 58 | 47 | tabIndex={tooltip ? 0 : undefined} |
| 59 | 48 | {...props} |
| 60 | 49 | > |
| 61 | 50 | {icon} |
| 62 | <span className={labelClassName}>{label}</span> | |
| 63 | </span> | |
| 51 | <ContextChipLabel>{label}</ContextChipLabel> | |
| 52 | </ContextChip> | |
| 64 | 53 | ); |
| 65 | 54 | if (!tooltip) return chip; |
| 66 | 55 | return ( |
| 67 | 56 | <Tooltip> |
| 68 | 57 | <TooltipTrigger render={chip} /> |
| 69 | <TooltipPopup side="top" className={tooltipClassName}> | |
| 58 | <TooltipPopup side="top" className="whitespace-pre-wrap"> | |
| 70 | 59 | {tooltip} |
| 71 | 60 | </TooltipPopup> |
| 72 | 61 | </Tooltip> |
| 73 | 62 | ); |
| 74 | 63 | } |
| 75 | 64 | |
| 65 | /** A chip that opens its details in a popover; the chip itself is the trigger. */ | |
| 76 | 66 | export function ContextChipPopover(props: { |
| 67 | kind: ContextChipKind; | |
| 68 | icon: ReactNode; | |
| 69 | label: string; | |
| 77 | 70 | copyMarkdown?: string; |
| 78 | 71 | accessibleLabel: string; |
| 79 | chip: ReactNode; | |
| 80 | 72 | children: ReactNode; |
| 81 | triggerClassName?: string; | |
| 82 | 73 | popupClassName?: string; |
| 83 | 74 | viewportClassName?: string; |
| 84 | 75 | }) { |
| ... | ... | @@ -86,19 +77,16 @@ export function ContextChipPopover(props: { |
| 86 | 77 | <Popover> |
| 87 | 78 | <PopoverTrigger |
| 88 | 79 | 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" />} | |
| 96 | 83 | aria-label={`${props.accessibleLabel}. Show details`} |
| 97 | 84 | data-markdown-copy={props.copyMarkdown} |
| 98 | 85 | /> |
| 99 | 86 | } |
| 100 | 87 | > |
| 101 | {props.chip} | |
| 88 | {props.icon} | |
| 89 | <ContextChipLabel>{props.label}</ContextChipLabel> | |
| 102 | 90 | </PopoverTrigger> |
| 103 | 91 | <PopoverPopup |
| 104 | 92 | side="top" |
| ... | ... | @@ -118,8 +106,7 @@ export function PullRequestChip(props: { |
| 118 | 106 | environmentId: EnvironmentId | null; |
| 119 | 107 | label: string; |
| 120 | 108 | kindLabel: string; |
| 121 | className: string; | |
| 122 | labelClassName: string; | |
| 109 | kind: ContextChipKind; | |
| 123 | 110 | copyMarkdown?: string; |
| 124 | 111 | onOpen: (event: MouseEvent<HTMLElement>, url: string) => void; |
| 125 | 112 | }) { |
| ... | ... | @@ -128,21 +115,16 @@ export function PullRequestChip(props: { |
| 128 | 115 | props.metadata.state === "open" && props.metadata.isDraft ? "draft" : props.metadata.state; |
| 129 | 116 | const StateIcon = PULL_REQUEST_STATE_PRESENTATION[displayState].Icon; |
| 130 | 117 | 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" />} | |
| 139 | 121 | aria-label={`Open ${props.kindLabel} ${props.label}: ${props.metadata.title}`} |
| 140 | 122 | data-markdown-copy={props.copyMarkdown} |
| 141 | 123 | onClick={(event) => props.onOpen(event, props.metadata.url)} |
| 142 | 124 | > |
| 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> | |
| 146 | 128 | ); |
| 147 | 129 | if (previewTarget !== null) { |
| 148 | 130 | return ( |
| ... | ... | @@ -196,16 +178,13 @@ function averageImageColor(image: HTMLImageElement): string | undefined { |
| 196 | 178 | export function ImageChipButton({ |
| 197 | 179 | name, |
| 198 | 180 | previewUrl, |
| 199 | className, | |
| 200 | labelClassName, | |
| 201 | 181 | size, |
| 202 | 182 | suffix, |
| 203 | 183 | style, |
| 204 | 184 | ...props |
| 205 | }: ComponentProps<"button"> & { | |
| 185 | }: Omit<ComponentProps<typeof ContextChip>, "kind" | "render"> & { | |
| 206 | 186 | name: string; |
| 207 | 187 | previewUrl: string | undefined; |
| 208 | labelClassName: string; | |
| 209 | 188 | /** Every attachment chip reports its size; images are no exception. */ |
| 210 | 189 | size: string; |
| 211 | 190 | suffix?: string | null; |
| ... | ... | @@ -214,14 +193,9 @@ export function ImageChipButton({ |
| 214 | 193 | const [corsFailedUrl, setCorsFailedUrl] = useState<string>(); |
| 215 | 194 | const accent = sample?.url === previewUrl ? sample?.color : undefined; |
| 216 | 195 | 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" />} | |
| 225 | 199 | aria-label={`Image attachment, ${name}, ${size}`} |
| 226 | 200 | style={{ ...style, ...(accent ? { "--context-chip-accent": accent } : {}) } as CSSProperties} |
| 227 | 201 | {...props} |
| ... | ... | @@ -232,25 +206,19 @@ export function ImageChipButton({ |
| 232 | 206 | crossOrigin={corsFailedUrl === previewUrl ? undefined : "anonymous"} |
| 233 | 207 | src={previewUrl} |
| 234 | 208 | alt="" |
| 235 | className="size-3.5 shrink-0 rounded-sm object-cover" | |
| 209 | className="size-[1.17em] shrink-0 rounded-sm object-cover" | |
| 236 | 210 | onError={() => setCorsFailedUrl(previewUrl)} |
| 237 | 211 | onLoad={(event) => |
| 238 | 212 | setSample({ url: previewUrl, color: averageImageColor(event.currentTarget) }) |
| 239 | 213 | } |
| 240 | 214 | /> |
| 241 | 215 | ) : ( |
| 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 /> | |
| 249 | 217 | )} |
| 250 | <span className={cn(labelClassName, "max-w-72")}>{middleTruncateAttachmentName(name)}</span> | |
| 218 | <ContextChipLabel className="max-w-72">{middleTruncateAttachmentName(name)}</ContextChipLabel> | |
| 251 | 219 | <span className="shrink-0 text-[10px] text-current">{size}</span> |
| 252 | 220 | {suffix ? <span className="text-[10px] text-current">{suffix}</span> : null} |
| 253 | </Button> | |
| 221 | </ContextChip> | |
| 254 | 222 | ); |
| 255 | 223 | } |
| 256 | 224 | |
| ... | ... | @@ -259,8 +227,6 @@ export function FileChip(props: { |
| 259 | 227 | size: string; |
| 260 | 228 | isVideo: boolean; |
| 261 | 229 | theme: "light" | "dark"; |
| 262 | className: string; | |
| 263 | labelClassName: string; | |
| 264 | 230 | accessibleLabel: string; |
| 265 | 231 | tooltip: string; |
| 266 | 232 | suffix?: string | null; |
| ... | ... | @@ -270,35 +236,35 @@ export function FileChip(props: { |
| 270 | 236 | unresolved?: boolean; |
| 271 | 237 | onOpen?: (() => void) | undefined; |
| 272 | 238 | }) { |
| 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 | ); | |
| 283 | 239 | const content = <FileChipContent {...props} />; |
| 240 | const state = props.unresolved | |
| 241 | ? ("unresolved" as const) | |
| 242 | : props.error | |
| 243 | ? ("invalid" as const) | |
| 244 | : null; | |
| 284 | 245 | const attributes = { |
| 285 | className, | |
| 246 | kind: props.isVideo ? "video" : "file", | |
| 247 | ...(state ? { state } : {}), | |
| 286 | 248 | "aria-label": [props.accessibleLabel, props.suffix].filter(Boolean).join(", "), |
| 287 | 249 | "data-markdown-copy": props.copyMarkdown, |
| 288 | 250 | "data-context-unresolved": props.unresolved ? "true" : undefined, |
| 289 | }; | |
| 251 | } as const; | |
| 290 | 252 | return ( |
| 291 | 253 | <Tooltip> |
| 292 | 254 | <TooltipTrigger |
| 293 | 255 | render={ |
| 294 | 256 | 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 | > | |
| 296 | 262 | {content} |
| 297 | </Button> | |
| 263 | </ContextChip> | |
| 298 | 264 | ) : ( |
| 299 | <span tabIndex={0} {...attributes}> | |
| 265 | <ContextChip tabIndex={0} {...attributes}> | |
| 300 | 266 | {content} |
| 301 | </span> | |
| 267 | </ContextChip> | |
| 302 | 268 | ) |
| 303 | 269 | } |
| 304 | 270 | /> |
| ... | ... | @@ -314,55 +280,33 @@ function FileChipContent(props: { |
| 314 | 280 | size: string; |
| 315 | 281 | isVideo: boolean; |
| 316 | 282 | theme: "light" | "dark"; |
| 317 | labelClassName: string; | |
| 318 | 283 | suffix?: string | null; |
| 319 | 284 | }) { |
| 320 | 285 | return ( |
| 321 | 286 | <> |
| 322 | 287 | {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 /> | |
| 330 | 289 | ) : ( |
| 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} /> | |
| 337 | 291 | )} |
| 338 | <span className={cn(props.labelClassName, "max-w-72")}> | |
| 292 | <ContextChipLabel className="max-w-72"> | |
| 339 | 293 | {middleTruncateAttachmentName(props.name)} |
| 340 | </span> | |
| 294 | </ContextChipLabel> | |
| 341 | 295 | <span className="shrink-0 text-[10px] text-current">{props.size}</span> |
| 342 | 296 | {props.suffix ? <span className="text-[10px] text-current">{props.suffix}</span> : null} |
| 343 | 297 | </> |
| 344 | 298 | ); |
| 345 | 299 | } |
| 346 | 300 | |
| 347 | export function UnresolvedChip(props: { | |
| 348 | label: string; | |
| 349 | className: string; | |
| 350 | labelClassName: string; | |
| 351 | tooltip: string; | |
| 352 | tooltipClassName: string; | |
| 353 | copyMarkdown?: string; | |
| 354 | }) { | |
| 301 | export function UnresolvedChip(props: { label: string; tooltip: string; copyMarkdown?: string }) { | |
| 355 | 302 | return ( |
| 356 | 303 | <ContextChipShell |
| 357 | icon={<CircleDashedIcon className={cn(COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, "size-3.5")} />} | |
| 304 | icon={<CircleDashedIcon />} | |
| 358 | 305 | label={props.label} |
| 359 | className={props.className} | |
| 360 | labelClassName={props.labelClassName} | |
| 306 | state="unresolved" | |
| 361 | 307 | aria-label={`Unavailable context, ${props.label}`} |
| 362 | 308 | data-markdown-copy={props.copyMarkdown} |
| 363 | 309 | tooltip={props.tooltip} |
| 364 | tooltipClassName={props.tooltipClassName} | |
| 365 | unresolved | |
| 366 | 310 | /> |
| 367 | 311 | ); |
| 368 | 312 | } |
apps/web/src/components/ui/button.tsx+1-3| ... | ... | @@ -39,7 +39,6 @@ const buttonVariants = cva( |
| 39 | 39 | 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", |
| 40 | 40 | }, |
| 41 | 41 | variant: { |
| 42 | chip: "", | |
| 43 | 42 | default: |
| 44 | 43 | "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", |
| 45 | 44 | destructive: |
| ... | ... | @@ -85,8 +84,7 @@ function Button({ className, variant, size, render, ...props }: ButtonProps) { |
| 85 | 84 | : "button"; |
| 86 | 85 | |
| 87 | 86 | const defaultProps = { |
| 88 | className: | |
| 89 | variant === "chip" ? cn(className) : cn(buttonVariants({ className, size, variant })), | |
| 87 | className: cn(buttonVariants({ className, size, variant })), | |
| 90 | 88 | "data-slot": "button", |
| 91 | 89 | type: typeValue, |
| 92 | 90 | }; |
apps/web/src/index.css+5-12| ... | ... | @@ -1770,10 +1770,14 @@ code { |
| 1770 | 1770 | } |
| 1771 | 1771 | |
| 1772 | 1772 | .chat-markdown a { |
| 1773 | color: var(--info-foreground); | |
| 1774 | 1773 | text-decoration: none; |
| 1775 | 1774 | } |
| 1776 | 1775 | |
| 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 | ||
| 1777 | 1781 | .chat-markdown a:not(.chat-markdown-file-link):hover, |
| 1778 | 1782 | .chat-markdown a:not(.chat-markdown-file-link):focus-visible { |
| 1779 | 1783 | background-image: radial-gradient(circle, currentcolor 0.75px, transparent 1px); |
| ... | ... | @@ -1825,17 +1829,6 @@ code { |
| 1825 | 1829 | font-size: 0.75rem; |
| 1826 | 1830 | } |
| 1827 | 1831 | |
| 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 | ||
| 1839 | 1832 | .chat-markdown pre { |
| 1840 | 1833 | max-width: 100%; |
| 1841 | 1834 | overflow-x: auto; |
scripts/lint-restyle-ceiling.ts+1-1| ... | ... | @@ -8,7 +8,7 @@ import * as NodeURL from "node:url"; |
| 8 | 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the |
| 9 | 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you |
| 10 | 10 | // migrate a file, and delete this script when the rule becomes an error. |
| 11 | export const RESTYLE_CEILING = 623; | |
| 11 | export const RESTYLE_CEILING = 603; | |
| 12 | 12 | |
| 13 | 13 | const RULE = "shadcn(no-restyle)"; |
| 14 | 14 |