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