From 266d70cc4d887b7459ec6557c430ec7bb92bd3fc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 22 Sep 2026 21:33:01 -0700 Subject: [PATCH] refactor(web): context chips render through one ContextChip component (#13192) Co-authored-by: Claude Opus 5.5 (1M context) --- apps/web/src/components/ChatMarkdown.tsx | 52 ++--- .../components/ComposerPromptEditorTiptap.tsx | 48 ++--- apps/web/src/components/ContextChip.tsx | 131 ++++++++++++ .../components/chat/AssistantCitationChip.tsx | 36 +--- apps/web/src/components/chat/FileTagChip.tsx | 25 +-- .../src/components/chat/MessagesTimeline.tsx | 144 +++---------- .../src/components/chat/SkillInlineText.tsx | 37 ++-- .../chat/TerminalContextInlineChip.tsx | 50 +---- .../composerContextPresentation.tsx | 98 +++------ apps/web/src/components/composerInlineChip.ts | 78 ------- apps/web/src/components/contextChipParts.tsx | 190 ++++++------------ apps/web/src/components/ui/button.tsx | 4 +- apps/web/src/index.css | 17 +- scripts/lint-restyle-ceiling.ts | 2 +- 14 files changed, 333 insertions(+), 579 deletions(-) create mode 100644 apps/web/src/components/ContextChip.tsx diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 52aed78e66635045e3de791c40fe8474b5f2d7e4..ef833e0e679489d78bb53bb0bdd7bed626bc4a77 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -105,7 +105,7 @@ import { markdownImageGallery, markdownImageItems } from "./chat/markdownImageGa import { MediaVideoPlayer } from "./media/MediaVideoPlayer"; import { MediaActions, type MediaActionSource } from "./media/MediaActions"; import { resolveProtocolRelativeMediaUrl } from "./media/mediaContent"; -import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip"; +import { FileTagChipContent } from "./chat/FileTagChip"; import { PierreEntryIcon } from "./chat/PierreEntryIcon"; import { revealInFileExplorerLabelForKind, @@ -118,6 +118,7 @@ import { import { hasSpecificPierreIconForFileName, syntheticFileNameForLanguageId } from "../pierre-icons"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { Button } from "./ui/button"; +import { ContextChip } from "./ContextChip"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./ui/collapsible"; import { ScrollArea } from "./ui/scroll-area"; import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu"; @@ -1156,11 +1157,9 @@ interface MarkdownFileLinkProps { /** Platform-specific menu label ("Reveal in Finder", ...); required for the reveal item to show. */ revealLabel?: string | undefined; - className?: string | undefined; } -const MARKDOWN_FILE_CHIP_CLASS_NAME = "chat-markdown-file-link"; -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`; +const MARKDOWN_FILE_LINK_CLASS_NAME = "chat-markdown-file-link"; function pathParentSegments(path: string): string[] { const normalized = path.replaceAll("\\", "/"); @@ -1895,7 +1894,6 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ onOpenMedia, onReveal, revealLabel, - className, }: MarkdownFileLinkProps) { const handleOpenInEditor = useCallback(() => { if (!onOpen) { @@ -2165,13 +2163,10 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ } + className={MARKDOWN_FILE_LINK_CLASS_NAME} data-markdown-copy={copyMarkdown} onClick={(event) => { event.preventDefault(); @@ -2188,25 +2183,21 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ }} onContextMenu={handleContextMenu} > - - + + ) : ( - + + ) } /> @@ -2242,8 +2233,7 @@ function areMarkdownFileLinkPropsEqual( previous.onOpenInBrowser === next.onOpenInBrowser && previous.onOpenMedia === next.onOpenMedia && previous.onReveal === next.onReveal && - previous.revealLabel === next.revealLabel && - previous.className === next.className + previous.revealLabel === next.revealLabel ); } @@ -2571,12 +2561,7 @@ function useChatMarkdownState({ [cwd, findWorkspaceBasenameMatch, revealFileInFileManager], ); const fileLinkChip = useCallback( - ( - fileLinkMeta: MarkdownFileLinkMeta, - copyMarkdown: string, - className?: string, - mediaSource?: string, - ) => { + (fileLinkMeta: MarkdownFileLinkMeta, copyMarkdown: string, mediaSource?: string) => { const parentSuffix = fileLinkParentSuffixByPath.get( fileLinkMeta.filePath.replaceAll("\\", "/"), ); @@ -2633,7 +2618,6 @@ function useChatMarkdownState({ ? () => openMarkdownFileInPreview(fileLinkMeta.filePath) : undefined } - className={className} /> ); }, @@ -3068,7 +3052,6 @@ const CHAT_MARKDOWN_COMPONENTS = { return fileLinkChip( fileLinkMeta, `[${fileLinkMeta.basename}](${normalizedHref})`, - props.className, normalizedHref, ); }, @@ -3085,7 +3068,6 @@ const CHAT_MARKDOWN_COMPONENTS = { return fileLinkChip( fileLinkMeta, `\`${codeText}\``, - undefined, inlineCodeFilePathCandidate(codeText) ?? codeText.trim(), ); } diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index ec9b9e07e2fbc03ccc6e394256c971c4557e498f..8af2d18b310e7bbf5513040bfe5bb208bd789d15 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -58,18 +58,13 @@ import { import { collectInlineContextIds } from "~/lib/composerContextReferences"; import { cn, isMacPlatform } from "~/lib/utils"; import { basenameOfPath } from "~/pierre-icons"; -import { - COMPOSER_INLINE_CHIP_DECORATOR_CLASS_NAME, - COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, - COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, - COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME, - SKILL_CHIP_ICON_SVG, -} from "./composerInlineChip"; -import { FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip"; +import { FileTagChipContent } from "./chat/FileTagChip"; +import { SkillChipIcon } from "./chat/SkillInlineText"; import { AssistantCitationChip } from "./chat/AssistantCitationChip"; import { getTimelinePageScrollKey } from "./chat/pageScrollController"; import { ContextChipPopover } from "./contextChipParts"; import { Button } from "./ui/button"; +import { ContextChip } from "./ContextChip"; import { ComposerContextActionsContext, ComposerContextReferenceChip, @@ -190,6 +185,13 @@ function resolvedThemeFromDocument(): "light" | "dark" { // ── Inline atom nodes (chips) ───────────────────────────────────────────── +/** + * Wraps an inline chip node view: keeps the caret and text selection out of the chip and + * paints the editor's node selection over it. + */ +const CHIP_NODE_SELECTION_CLASS_NAME = + "relative inline-flex select-none items-center align-middle leading-none data-[composer-chip-selected]:after:pointer-events-none data-[composer-chip-selected]:after:absolute data-[composer-chip-selected]:after:inset-0 data-[composer-chip-selected]:after:rounded-[6px] data-[composer-chip-selected]:after:bg-[Highlight] data-[composer-chip-selected]:after:opacity-30 data-[composer-chip-selected]:after:content-['']"; + const ComposerMentionExtension = Node.create({ name: "composer-mention", group: "inline", @@ -217,11 +219,11 @@ function ComposerMentionNodeView({ node }: NodeViewProps) { const actions = use(ComposerContextActionsContext); const path = (node.attrs.path as string) ?? ""; const chip = ( - + ); return ( - + {path} @@ -275,20 +277,12 @@ function ComposerSkillNodeView({ node }: NodeViewProps) { const skillDescription = (node.attrs.skillDescription as string | null) ?? null; const skill = skills.find((candidate) => candidate.name === skillName); return ( - + } + label={skillLabel} accessibleLabel={`Skill ${skillLabel}`} - triggerClassName={COMPOSER_INLINE_SKILL_CHIP_CLASS_NAME} - chip={ - <> -