-
Notifications
You must be signed in to change notification settings - Fork 12
Chat Mention Tags -> Primary #1068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
0da7089
f8ef13d
431fa99
c6ba626
e1567fc
1e4610c
0db18fe
48130ce
6ed0084
484b962
328240b
3aa444f
8a0a6e1
c144a5e
337a5f3
ae5201d
61ce273
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ import type { | |
| } from "../../../shared/types/orchestration"; | ||
| import { getModelById, modelSupportsFastMode, type ProviderFamily } from "../../../shared/modelRegistry"; | ||
| import { | ||
| composerTriggerForSelection, | ||
| composerTriggerSpansWholeDraft, | ||
| detectComposerTrigger, | ||
| findConfirmedComposerTokens, | ||
|
|
@@ -1853,6 +1854,12 @@ export function AgentChatComposer({ | |
| const richEditorRef = useRef<HTMLDivElement | null>(null); | ||
| const richSelectionRef = useRef<Range | null>(null); | ||
| const richInitializedRef = useRef(false); | ||
| // Plain textarea chips are painted by an overlay, while the serialized | ||
| // draft intentionally stores only the opaque mention pointer. Keep the | ||
| // selected row's title separately so the visible chip stays user-facing. | ||
| // This is a presentation cache only; send-time parsing still uses the | ||
| // canonical @chat:<id> token in `draft`. | ||
| const mentionLabelsRef = useRef<Map<string, string>>(new Map()); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| const lastSerializedDraftRef = useRef<string>(""); | ||
| const lastPlainSelectionRef = useRef<number | null>(null); | ||
| const fileAddInProgressRef = useRef(false); | ||
|
|
@@ -2004,12 +2011,30 @@ export function AgentChatComposer({ | |
| let pos = 0; | ||
| plainComposerTokens.forEach((token, index) => { | ||
| if (token.start > pos) segments.push(draft.slice(pos, token.start)); | ||
| const tokenText = draft.slice(token.start, token.end); | ||
| const displayText = token.kind === "mention" | ||
| ? mentionLabelsRef.current.get(tokenText)?.trim() || tokenText | ||
| : tokenText; | ||
|
Comment on lines
+2025
to
+2027
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the default plain-text composer, the textarea becomes transparent and this overlay supplies every visible glyph, so replacing the serialized Useful? React with 👍 / 👎. |
||
| const isLabeledMention = token.kind === "mention" && displayText !== tokenText; | ||
| segments.push( | ||
| <span | ||
| key={`chip-${index}-${token.start}`} | ||
| className="rounded-[4px] bg-violet-500/14 text-violet-100/92 shadow-[inset_0_0_0_1px_rgba(167,139,250,0.18)]" | ||
| > | ||
| {draft.slice(token.start, token.end)} | ||
| {isLabeledMention ? ( | ||
| // Keep the textarea's canonical token as an invisible layout slot. | ||
| // The visible title is positioned inside that slot so a longer or | ||
| // shorter label cannot move the caret or following prose out of | ||
| // alignment with the real textarea value. | ||
| <span className="relative inline-block align-baseline" title={displayText}> | ||
| <span className="invisible whitespace-pre" data-composer-mention-layout> | ||
| {tokenText} | ||
| </span> | ||
| <span className="absolute inset-0 overflow-hidden text-ellipsis whitespace-nowrap" data-composer-mention-display> | ||
| {displayText} | ||
| </span> | ||
| </span> | ||
| ) : displayText} | ||
| </span>, | ||
| ); | ||
| pos = token.end; | ||
|
|
@@ -2713,7 +2738,7 @@ export function AgentChatComposer({ | |
| // and flattens chips, so serialized indices cannot be mapped back onto DOM | ||
| // positions. Chips, <br>, and block edges terminate the run and act as | ||
| // word boundaries. | ||
| const getRichTriggerContext = useCallback((): { trigger: ComposerTrigger; range: Range } | null => { | ||
| const getRichTriggerContext = useCallback((queryOverride?: string): { trigger: ComposerTrigger; range: Range } | null => { | ||
| const editor = richEditorRef.current; | ||
| if (!editor) return null; | ||
| const selection = window.getSelection(); | ||
|
|
@@ -2741,8 +2766,11 @@ export function AgentChatComposer({ | |
| walker = walker.previousSibling; | ||
| } | ||
|
|
||
| const trigger = detectComposerTrigger(runText, runText.length); | ||
| if (!trigger) return null; | ||
| const detectedTrigger = detectComposerTrigger(runText, runText.length); | ||
| if (!detectedTrigger) return null; | ||
| const trigger = queryOverride == null | ||
| ? detectedTrigger | ||
| : { ...detectedTrigger, query: queryOverride }; | ||
|
|
||
| let remaining = trigger.start; | ||
| let startNode: Text = caretNode; | ||
|
|
@@ -2757,9 +2785,22 @@ export function AgentChatComposer({ | |
| remaining -= length; | ||
| } | ||
|
|
||
| let endRemaining = trigger.start + 1 + trigger.query.length; | ||
| let endNode: Text = caretNode; | ||
| let endOffset = caretOffset; | ||
| for (const node of runNodes) { | ||
| const length = node === caretNode ? caretOffset : (node.textContent ?? "").length; | ||
| if (endRemaining <= length) { | ||
| endNode = node; | ||
| endOffset = endRemaining; | ||
| break; | ||
| } | ||
| endRemaining -= length; | ||
| } | ||
|
|
||
| const range = document.createRange(); | ||
| range.setStart(startNode, startOffset); | ||
| range.setEnd(caretNode, caretOffset); | ||
| range.setEnd(endNode, endOffset); | ||
| return { trigger, range }; | ||
| }, []); | ||
|
|
||
|
|
@@ -2768,7 +2809,7 @@ export function AgentChatComposer({ | |
| // no trigger span can be located (caller falls back to caret insertion). | ||
| const replaceRichTriggerWith = useCallback((insertion: | ||
| | { text: string } | ||
| | { chipKind: "file" | "command" | "mention"; chipText: string; chipLabel?: string } | ||
| | { chipKind: "file" | "command" | "mention"; chipText: string; chipLabel?: string; triggerLabel?: string } | ||
| ): boolean => { | ||
| const editor = richEditorRef.current; | ||
| if (!editor) return false; | ||
|
|
@@ -2779,7 +2820,14 @@ export function AgentChatComposer({ | |
| selection?.removeAllRanges(); | ||
| selection?.addRange(saved); | ||
| } | ||
| const context = getRichTriggerContext(); | ||
| const detectedContext = getRichTriggerContext(); | ||
| if (!detectedContext) return false; | ||
| const trigger = "triggerLabel" in insertion | ||
| ? composerTriggerForSelection(detectedContext.trigger, insertion.triggerLabel ?? "") | ||
| : detectedContext.trigger; | ||
| const context = trigger.query === detectedContext.trigger.query | ||
| ? detectedContext | ||
| : getRichTriggerContext(trigger.query); | ||
| if (!context) return false; | ||
| selection?.removeAllRanges(); | ||
| selection?.addRange(context.range); | ||
|
|
@@ -3873,11 +3921,16 @@ export function AgentChatComposer({ | |
| } | ||
| // Replace exactly the @query trigger span with the confirmed token. | ||
| if (useRichComposer) { | ||
| if (!replaceRichTriggerWith({ chipKind: "file", chipText: `@${item.path}` })) { | ||
| if (!replaceRichTriggerWith({ | ||
| chipKind: "file", | ||
| chipText: `@${item.path}`, | ||
| triggerLabel: item.path, | ||
| })) { | ||
| insertTextIntoRichEditor(`@${item.path} `); | ||
| } | ||
| } else { | ||
| const next = replaceComposerTriggerSpan(draft, commandMenuTrigger, `@${item.path} `); | ||
| const trigger = composerTriggerForSelection(commandMenuTrigger, item.path); | ||
| const next = replaceComposerTriggerSpan(draft, trigger, `@${item.path} `); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the selected path contains spaces, this inserts it verbatim (for example, AGENTS.md reference: AGENTS.md:L33-L35 Useful? React with 👍 / 👎. |
||
| onDraftChange(next.text); | ||
| restoreTextareaCaret(next.caret); | ||
| } | ||
|
|
@@ -3886,16 +3939,19 @@ export function AgentChatComposer({ | |
| // A mention is a pointer, not an attachment: nothing is resolved or read | ||
| // now. The token is expanded into an <ade-mention> block at send time. | ||
| const token = formatChatMentionToken(item.mention.kind, item.mention.id); | ||
| mentionLabelsRef.current.set(token, item.mention.title); | ||
| if (useRichComposer) { | ||
| if (!replaceRichTriggerWith({ | ||
| chipKind: "mention", | ||
| chipText: token, | ||
| chipLabel: item.mention.title, | ||
| triggerLabel: item.mention.title, | ||
| })) { | ||
| insertTextIntoRichEditor(`${token} `); | ||
| } | ||
| } else { | ||
| const next = replaceComposerTriggerSpan(draft, commandMenuTrigger, `${token} `); | ||
| const trigger = composerTriggerForSelection(commandMenuTrigger, item.mention.title); | ||
| const next = replaceComposerTriggerSpan(draft, trigger, `${token} `); | ||
| onDraftChange(next.text); | ||
| restoreTextareaCaret(next.caret); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.