authorgravatar for 46866023+GuilhermeVieiraDev@users.noreply.github.comGuilherme Vieira <46866023+GuilhermeVieiraDev@users.noreply.github.com> 2026-04-15 21:29:13+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-15 13:29:13-07:00
log28cb9db20bc78fdb5a767ff9661e500076938fbe
tree1dcd8d62bcf10d4c05f50f728b7b91a69fe6ad68
parent7968f278ee54202cc3ae63f8e52d224b13e0265f
signature Signed by PGP key B5690EEEBB952194

feat(web): add tooltip to composer file mention pill (#1944)


1 files changed, 48 insertions(+), 62 deletions(-)

apps/web/src/components/ComposerPromptEditor.tsx+48-62
...@@ -32,11 +32,8 @@ import {...@@ -32,11 +32,8 @@ import {
32 type ElementNode,32 type ElementNode,
33 type LexicalNode,33 type LexicalNode,
34 type SerializedLexicalNode,34 type SerializedLexicalNode,
35 TextNode,
36 type EditorConfig,
37 type EditorState,35 type EditorState,
38 type NodeKey,36 type NodeKey,
39 type SerializedTextNode,
40 type Spread,37 type Spread,
41} from "lexical";38} from "lexical";
42import {39import {
...@@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread<...@@ -103,7 +100,7 @@ type SerializedComposerMentionNode = Spread<
103 type: "composer-mention";100 type: "composer-mention";
104 version: 1;101 version: 1;
105 },102 },
106 SerializedTextNode103 SerializedLexicalNode
107>;104>;
108105
109type SerializedComposerSkillNode = Spread<106type SerializedComposerSkillNode = Spread<
...@@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{...@@ -132,7 +129,40 @@ const ComposerTerminalContextActionsContext = createContext<{
132 onRemoveTerminalContext: () => {},129 onRemoveTerminalContext: () => {},
133});130});
134131
135class ComposerMentionNode extends TextNode {132function ComposerMentionDecorator(props: { path: string }) {
133 const theme = resolvedThemeFromDocument();
134 const chip = (
135 <span
136 className={COMPOSER_INLINE_CHIP_CLASS_NAME}
137 contentEditable={false}
138 spellCheck={false}
139 data-composer-mention-chip="true"
140 >
141 <img
142 alt=""
143 aria-hidden="true"
144 className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME}
145 loading="lazy"
146 src={getVscodeIconUrlForEntry(props.path, inferEntryKindFromPath(props.path), theme)}
147 />
148 <span className={COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME}>{basenameOfPath(props.path)}</span>
149 </span>
150 );
151
152 return (
153 <Tooltip>
154 <TooltipTrigger render={chip} />
155 <TooltipPopup
156 side="top"
157 className="max-w-[30rem] whitespace-normal leading-tight wrap-anywhere"
158 >
159 {props.path}
160 </TooltipPopup>
161 </Tooltip>
162 );
163}
164
165class ComposerMentionNode extends DecoratorNode<ReactElement> {
136 __path: string;166 __path: string;
137167
138 static override getType(): string {168 static override getType(): string {
...@@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode {...@@ -144,12 +174,12 @@ class ComposerMentionNode extends TextNode {
144 }174 }
145175
146 static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode {176 static override importJSON(serializedNode: SerializedComposerMentionNode): ComposerMentionNode {
147 return $createComposerMentionNode(serializedNode.path);177 return $createComposerMentionNode(serializedNode.path).updateFromJSON(serializedNode);
148 }178 }
149179
150 constructor(path: string, key?: NodeKey) {180 constructor(path: string, key?: NodeKey) {
181 super(key);
151 const normalizedPath = path.startsWith("@") ? path.slice(1) : path;182 const normalizedPath = path.startsWith("@") ? path.slice(1) : path;
152 super(`@${normalizedPath}`, key);
153 this.__path = normalizedPath;183 this.__path = normalizedPath;
154 }184 }
155185
...@@ -162,41 +192,26 @@ class ComposerMentionNode extends TextNode {...@@ -162,41 +192,26 @@ class ComposerMentionNode extends TextNode {
162 };192 };
163 }193 }
164194
165 override createDOM(_config: EditorConfig): HTMLElement {195 override createDOM(): HTMLElement {
166 const dom = document.createElement("span");196 const dom = document.createElement("span");
167 dom.className = COMPOSER_INLINE_CHIP_CLASS_NAME;197 dom.className = "inline-flex align-middle leading-none";
168 dom.contentEditable = "false";
169 dom.setAttribute("spellcheck", "false");
170 renderMentionChipDom(dom, this.__path);
171 return dom;198 return dom;
172 }199 }
173200
174 override updateDOM(201 override updateDOM(): false {
175 prevNode: ComposerMentionNode,
176 dom: HTMLElement,
177 _config: EditorConfig,
178 ): boolean {
179 dom.contentEditable = "false";
180 if (prevNode.__text !== this.__text || prevNode.__path !== this.__path) {
181 renderMentionChipDom(dom, this.__path);
182 }
183 return false;
184 }
185
186 override canInsertTextBefore(): false {
187 return false;202 return false;
188 }203 }
189204
190 override canInsertTextAfter(): false {205 override getTextContent(): string {
191 return false;206 return `@${this.__path}`;
192 }207 }
193208
194 override isTextEntity(): true {209 override isInline(): true {
195 return true;210 return true;
196 }211 }
197212
198 override isToken(): true {213 override decorate(): ReactElement {
199 return true;214 return <ComposerMentionDecorator path={this.__path} />;
200 }215 }
201}216}
202217
...@@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" {...@@ -434,26 +449,6 @@ function resolvedThemeFromDocument(): "light" | "dark" {
434 return document.documentElement.classList.contains("dark") ? "dark" : "light";449 return document.documentElement.classList.contains("dark") ? "dark" : "light";
435}450}
436451
437function renderMentionChipDom(container: HTMLElement, pathValue: string): void {
438 container.textContent = "";
439 container.style.setProperty("user-select", "none");
440 container.style.setProperty("-webkit-user-select", "none");
441
442 const theme = resolvedThemeFromDocument();
443 const icon = document.createElement("img");
444 icon.alt = "";
445 icon.ariaHidden = "true";
446 icon.className = COMPOSER_INLINE_CHIP_ICON_CLASS_NAME;
447 icon.loading = "lazy";
448 icon.src = getVscodeIconUrlForEntry(pathValue, inferEntryKindFromPath(pathValue), theme);
449
450 const label = document.createElement("span");
451 label.className = COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME;
452 label.textContent = basenameOfPath(pathValue);
453
454 container.append(icon, label);
455}
456
457function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {452function terminalContextSignature(contexts: ReadonlyArray<TerminalContextDraft>): string {
458 return contexts453 return contexts
459 .map((context) =>454 .map((context) =>
...@@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb...@@ -595,12 +590,9 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb
595 }590 }
596591
597 if ($isTextNode(node)) {592 if ($isTextNode(node)) {
598 if (node instanceof ComposerMentionNode) {
599 return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
600 }
601 return offset + Math.min(pointOffset, node.getTextContentSize());593 return offset + Math.min(pointOffset, node.getTextContentSize());
602 }594 }
603 if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) {595 if (isComposerInlineTokenNode(node)) {
604 return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);596 return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
605 }597 }
606598
...@@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe...@@ -642,12 +634,9 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe
642 }634 }
643635
644 if ($isTextNode(node)) {636 if ($isTextNode(node)) {
645 if (node instanceof ComposerMentionNode) {
646 return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
647 }
648 return offset + Math.min(pointOffset, node.getTextContentSize());637 return offset + Math.min(pointOffset, node.getTextContentSize());
649 }638 }
650 if (node instanceof ComposerSkillNode || node instanceof ComposerTerminalContextNode) {639 if (isComposerInlineTokenNode(node)) {
651 return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);640 return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset);
652 }641 }
653642
...@@ -673,10 +662,7 @@ function findSelectionPointAtOffset(...@@ -673,10 +662,7 @@ function findSelectionPointAtOffset(
673 node: LexicalNode,662 node: LexicalNode,
674 remainingRef: { value: number },663 remainingRef: { value: number },
675): { key: string; offset: number; type: "text" | "element" } | null {664): { key: string; offset: number; type: "text" | "element" } | null {
676 if (node instanceof ComposerMentionNode || node instanceof ComposerSkillNode) {665 if (isComposerInlineTokenNode(node)) {
677 return findSelectionPointForInlineToken(node, remainingRef);
678 }
679 if (node instanceof ComposerTerminalContextNode) {
680 return findSelectionPointForInlineToken(node, remainingRef);666 return findSelectionPointForInlineToken(node, remainingRef);
681 }667 }
682668