-
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 6 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 |
|---|---|---|
|
|
@@ -15,6 +15,8 @@ import { resolveStableLaneBaseBranch } from "../../../desktop/src/shared/laneBas | |
| import { LAUNCH_PROFILE_TITLE, LAUNCH_PROFILE_TOOL_TYPE, resolveClaudeCliModelForLaunch } from "../../../desktop/src/shared/cliLaunch"; | ||
| import { getAgentSkillRootCandidates } from "../../../desktop/src/shared/agentSkillRoots"; | ||
| import { | ||
| composerFileSearchQuery, | ||
| composerTriggerForSelection, | ||
| composerTriggerSpansWholeDraft, | ||
| detectComposerTrigger, | ||
| findConfirmedComposerTokens, | ||
|
|
@@ -2512,6 +2514,10 @@ export const MENTION_MAX_ROWS = 10; | |
| export const MENTION_FILE_ROWS = 5; | ||
| const STARTUP_RECONNECT_DELAY_MS = 3_000; | ||
|
|
||
| function matchesMentionTarget(target: string, query: string): boolean { | ||
| return target.includes(query) || query.startsWith(`${target} `); | ||
|
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 target labels overlap, this predicate treats every confirmed prefix equally. For example, with a lane named Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| type MentionRemoteCacheEntry = { | ||
| filesByQuery: Map<string, Array<{ path: string }>>; | ||
| commits: Array<Record<string, unknown>> | null; | ||
|
|
@@ -7339,7 +7345,17 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath, | |
| return; | ||
| } | ||
| let cancelled = false; | ||
| const query = range.query.toLowerCase(); | ||
| const query = range.query.trim().toLowerCase(); | ||
| const fileQuery = composerFileSearchQuery(range.query).toLowerCase(); | ||
| const matchesMentionQuery = (suggestion: MentionSuggestion): boolean => { | ||
| if (!query) return true; | ||
| const label = suggestion.label.toLowerCase(); | ||
| return ( | ||
| matchesMentionTarget(label, query) | ||
| || suggestion.insertText.toLowerCase().includes(query) | ||
| || Boolean(suggestion.detail?.toLowerCase().includes(query)) | ||
| ); | ||
| }; | ||
| const localSuggestions = (): MentionSuggestion[] => [ | ||
| ...lanes.map((lane) => ({ | ||
| kind: "lane" as const, | ||
|
|
@@ -7353,20 +7369,10 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath, | |
| insertText: `@chat:${session.sessionId}`, | ||
| detail: session.laneId, | ||
| })), | ||
| ].filter((suggestion) => ( | ||
| !query | ||
| || suggestion.label.toLowerCase().includes(query) | ||
| || suggestion.insertText.toLowerCase().includes(query) | ||
| || suggestion.detail?.toLowerCase().includes(query) | ||
| )); | ||
| ].filter(matchesMentionQuery); | ||
| const attachedSuggestions = (): MentionSuggestion[] => selectedMentions | ||
| .filter((suggestion) => suggestion.attachment && suggestion.filePath) | ||
| .filter((suggestion) => ( | ||
| !query | ||
| || suggestion.label.toLowerCase().includes(query) | ||
| || suggestion.insertText.toLowerCase().includes(query) | ||
| || suggestion.detail?.toLowerCase().includes(query) | ||
| )); | ||
| .filter(matchesMentionQuery); | ||
|
|
||
| const publishSuggestions = (remote: MentionSuggestion[] = []) => { | ||
| if (cancelled) return; | ||
|
|
@@ -7390,16 +7396,16 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath, | |
| // (shallowest paths first) instead of returning nothing, matching the | ||
| // desktop composer's `@` behavior. The cache keys on the query string, | ||
| // so "" caches like any typed query. | ||
| const filesPromise = cache.filesByQuery.get(query) | ||
| ? Promise.resolve(cache.filesByQuery.get(query)!) | ||
| const filesPromise = cache.filesByQuery.get(fileQuery) | ||
| ? Promise.resolve(cache.filesByQuery.get(fileQuery)!) | ||
| : Promise.resolve(conn.action<Array<{ path: string }>>("file", "quickOpen", { | ||
| workspaceId: laneId, | ||
| query, | ||
| query: fileQuery, | ||
| limit: MENTION_FILE_ROWS, | ||
| })) | ||
| .then((files) => { | ||
| const safeFiles = Array.isArray(files) ? files : []; | ||
| cache.filesByQuery.set(query, safeFiles); | ||
| cache.filesByQuery.set(fileQuery, safeFiles); | ||
| return safeFiles; | ||
| }) | ||
| .catch(() => []); | ||
|
|
@@ -7452,7 +7458,8 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath, | |
| .filter((pr) => { | ||
| const title = String(pr.title ?? ""); | ||
| const number = String(pr.number ?? pr.prNumber ?? ""); | ||
| return !query || title.toLowerCase().includes(query) || number.includes(query); | ||
| const loweredTitle = title.toLowerCase(); | ||
| return !query || matchesMentionTarget(loweredTitle, query) || number.includes(query); | ||
| }) | ||
| .slice(0, 5) | ||
| .map((pr) => { | ||
|
|
@@ -12331,8 +12338,12 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath, | |
| }, [addNotice, chatRowBudget, lanes, models, refreshState, registerOptimisticTerminalSession, selectedMentions, setChatScrollOffset, setDraftChatMode, terminalPaneWidth]); | ||
|
|
||
| const insertMention = useCallback((suggestion: MentionSuggestion) => { | ||
| const trigger = detectComposerTrigger(prompt, promptCursorRef.current); | ||
| if (trigger?.type !== "at") return; | ||
| const detectedTrigger = detectComposerTrigger(prompt, promptCursorRef.current); | ||
| if (detectedTrigger?.type !== "at") return; | ||
| const trigger = composerTriggerForSelection( | ||
| detectedTrigger, | ||
| suggestion.kind === "file" ? suggestion.filePath ?? suggestion.label : suggestion.label, | ||
| ); | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| const next = replaceComposerTriggerSpan(prompt, trigger, `${suggestion.insertText} `); | ||
| setPromptValue(next.text, next.caret); | ||
| setSelectedMentions((prev) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,15 +85,38 @@ function scoreBrowseDepth(normalizedPath: string): number { | |
| return Math.max(1, BROWSE_BASE_SCORE - depth); | ||
| } | ||
|
|
||
| function scorePathForNeedle(normalized: string, needle: string): number { | ||
| if (normalized === needle) return 1000; | ||
| if (normalized.endsWith(`/${needle}`) || normalized.endsWith(`\\${needle}`)) return 900; | ||
| const idx = normalized.indexOf(needle); | ||
| return idx < 0 ? -1 : 600 - idx; | ||
| } | ||
|
|
||
| function scorePath(pathValue: string, query: string): number { | ||
| const normalized = pathValue.toLowerCase(); | ||
| const needle = query.toLowerCase().trim(); | ||
| if (!needle) return scoreBrowseDepth(normalized); | ||
| if (normalized === needle) return 1000; | ||
| if (normalized.endsWith(`/${needle}`) || normalized.endsWith(`\\${needle}`)) return 900; | ||
| const idx = normalized.indexOf(needle); | ||
| if (idx < 0) return -1; | ||
| return 600 - idx; | ||
| const directScore = scorePathForNeedle(normalized, needle); | ||
| if (directScore >= 0) return directScore; | ||
|
|
||
| // Composer @-file queries can contain ordinary prose after an extensionless | ||
| // path whose filename or directory contains spaces. A path index cannot | ||
| // know that boundary from the string alone, so try progressively shorter | ||
| // space-delimited prefixes and keep the longest matching one. Restrict this | ||
| // fallback to path-like queries so ordinary multiword quick-open searches | ||
| // keep their existing whole-query semantics. | ||
| if (!needle.includes("/") && !needle.includes("\\")) return -1; | ||
|
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 a root-level extensionless file is followed by prose, such as Useful? React with 👍 / 👎. |
||
| const words = needle.split(/[ \t]+/); | ||
| let best = -1; | ||
| for (let end = words.length - 1; end > 0; end -= 1) { | ||
| const prefix = words.slice(0, end).join(" "); | ||
| const score = scorePathForNeedle(normalized, prefix); | ||
| if (score < 0) continue; | ||
| // Prefer a longer path prefix when multiple indexed paths share the same | ||
| // beginning. The tiny fractional tie-break preserves existing score tiers. | ||
| best = Math.max(best, score + Math.min(prefix.length, 999) / 1000); | ||
|
Comment on lines
+119
to
+121
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 an extensionless path is itself a prefix of another valid path containing spaces—for example files Useful? React with 👍 / 👎. |
||
| } | ||
| return best; | ||
| } | ||
|
|
||
| async function cooperativeYield(): Promise<void> { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In ADE Code, the new keep-alive matching is applied to lane/chat suggestions and PR titles, but the recent-commit filter still checks
subject.includes(query). Consequently, after typing an exact commit subject followed by prose, such as@Fix parser please inspect, that commit disappears before Enter can select it, unlike the other newly supported multiword targets. Use this helper for commit subjects as well so selection can preserve the trailing prose.Useful? React with 👍 / 👎.