Skip to content

Commit fe55faf

Browse files
fix(clients): # suggestions put linked pull requests first for text queries (#26269)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c2fbf0a commit fe55faf

4 files changed

Lines changed: 106 additions & 27 deletions

File tree

‎apps/mobile/src/state/queries.ts‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
composerPullRequestsFromLinks,
33
filterComposerPullRequestMatches,
4+
withLinkedComposerPullRequestsFirst,
45
} from "@t3tools/shared/composerPullRequestMatches";
56
import { resolveFilesystemReadAccess } from "@t3tools/client-runtime/state/filesystem";
67
import { environmentSession } from "./session";
@@ -148,14 +149,22 @@ export function useComposerPullRequestSearch(input: {
148149
: null,
149150
);
150151
const entries = (() => {
151-
if (!ready) return [];
152-
if (numeric) {
152+
if (input.query === null || input.projectId === null || input.repository === null) return [];
153+
if (/^\d*$/.test(input.query)) {
153154
return filterComposerPullRequestMatches({
154-
entries: [...(exact.data ? [exact.data] : []), ...(list.data?.entries ?? [])],
155+
entries: ready ? [...(exact.data ? [exact.data] : []), ...(list.data?.entries ?? [])] : [],
155156
linked,
156-
projectId: input.projectId!,
157-
repository: input.repository!,
158-
query: query ?? "",
157+
projectId: input.projectId,
158+
repository: input.repository,
159+
query: input.query,
160+
limit: 20,
161+
});
162+
}
163+
if (!ready) {
164+
return withLinkedComposerPullRequestsFirst({
165+
matches: [],
166+
linked,
167+
query: input.query,
159168
limit: 20,
160169
});
161170
}
@@ -170,9 +179,14 @@ export function useComposerPullRequestSearch(input: {
170179
);
171180
const unique = new Map<number, (typeof found)[number]>();
172181
for (const entry of found) if (!unique.has(entry.number)) unique.set(entry.number, entry);
173-
return [...unique.values()]
174-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
175-
.slice(0, 20);
182+
return withLinkedComposerPullRequestsFirst({
183+
matches: [...unique.values()].sort((left, right) =>
184+
right.updatedAt.localeCompare(left.updatedAt),
185+
),
186+
linked,
187+
query: query ?? "",
188+
limit: 20,
189+
});
176190
})();
177191
return {
178192
entries,

‎apps/web/src/components/chat/useComposerMenuItems.ts‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
import {
1717
composerPullRequestsFromLinks,
1818
filterComposerPullRequestMatches,
19+
withLinkedComposerPullRequestsFirst,
1920
} from "@t3tools/shared/composerPullRequestMatches";
2021
import { formatProviderSkillDisplayName } from "@t3tools/shared/inlineSkills";
2122

@@ -303,24 +304,30 @@ export function useComposerMenuItems({
303304
query: composerTrigger.query,
304305
limit: COMPOSER_PULL_REQUEST_RESULT_LIMIT,
305306
})
306-
: rankPullRequestMatches(
307-
(pullRequestLookup.data?.entries ?? []).filter((entry) => {
308-
if (
309-
entry.projectId !== pullRequestProjectId ||
310-
entry.repository.trim().toLowerCase() !== pullRequestRepository.trim().toLowerCase()
311-
) {
312-
return false;
313-
}
314-
const provider = pullRequestLookup.data?.providers.find(
315-
(candidate) => candidate.host === entry.host,
316-
);
317-
return (
318-
provider?.searchesOnHost === true ||
319-
matchesPullRequestQuery(entry, composerTrigger.query)
320-
);
321-
}),
322-
composerTrigger.query,
323-
).slice(0, COMPOSER_PULL_REQUEST_RESULT_LIMIT);
307+
: withLinkedComposerPullRequestsFirst({
308+
matches: rankPullRequestMatches(
309+
(pullRequestLookup.data?.entries ?? []).filter((entry) => {
310+
if (
311+
entry.projectId !== pullRequestProjectId ||
312+
entry.repository.trim().toLowerCase() !==
313+
pullRequestRepository.trim().toLowerCase()
314+
) {
315+
return false;
316+
}
317+
const provider = pullRequestLookup.data?.providers.find(
318+
(candidate) => candidate.host === entry.host,
319+
);
320+
return (
321+
provider?.searchesOnHost === true ||
322+
matchesPullRequestQuery(entry, composerTrigger.query)
323+
);
324+
}),
325+
composerTrigger.query,
326+
),
327+
linked: linkedPullRequests,
328+
query: composerTrigger.query,
329+
limit: COMPOSER_PULL_REQUEST_RESULT_LIMIT,
330+
});
324331
return matches.map((pullRequest) => ({
325332
id: `pull-request:${pullRequest.projectId}:${pullRequest.repository}:${pullRequest.number}`,
326333
type: "pull-request",

‎packages/shared/src/composerPullRequestMatches.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ThreadPullRequestLink } from "@t3tools/contracts";
55
import {
66
composerPullRequestsFromLinks,
77
filterComposerPullRequestMatches,
8+
withLinkedComposerPullRequestsFirst,
89
} from "./composerPullRequestMatches.ts";
910

1011
const entry = (number: number, updatedAt: string) => ({
@@ -78,4 +79,30 @@ describe("linked pull requests", () => {
7879
});
7980
expect(result).toEqual([entry(4, "2026-01-03")]);
8081
});
82+
83+
it("puts linked pull requests ahead of a text search, before and after it returns", () => {
84+
const linked = composerPullRequestsFromLinks(
85+
[
86+
link("owner/repo", 7, {
87+
snapshot: { ...link("owner/repo", 7).snapshot!, title: "Fix login" },
88+
}),
89+
link("owner/repo", 8),
90+
],
91+
"p1",
92+
);
93+
const searching = withLinkedComposerPullRequestsFirst({
94+
matches: [],
95+
linked,
96+
query: "fix",
97+
limit: 10,
98+
});
99+
expect(searching.map((match) => match.number)).toEqual([7]);
100+
const searched = withLinkedComposerPullRequestsFirst({
101+
matches: [entry(9, "2026-01-05"), entry(8, "2026-01-03")],
102+
linked,
103+
query: "fix",
104+
limit: 10,
105+
});
106+
expect(searched).toEqual([linked[0], entry(8, "2026-01-03"), entry(9, "2026-01-05")]);
107+
});
81108
});

‎packages/shared/src/composerPullRequestMatches.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,34 @@ export function filterComposerPullRequestMatches<
7373
)
7474
.slice(0, input.limit);
7575
}
76+
77+
/** Puts the thread's linked pull requests that match a text query ahead of the searched ones. */
78+
export function withLinkedComposerPullRequestsFirst<
79+
Entry extends ComposerPullRequestMatch,
80+
Linked extends ComposerPullRequestMatch & {
81+
readonly title: string;
82+
readonly headBranch: string;
83+
},
84+
>(input: {
85+
readonly matches: ReadonlyArray<Entry>;
86+
readonly linked: ReadonlyArray<Linked>;
87+
readonly query: string;
88+
readonly limit: number;
89+
}): ReadonlyArray<Entry | Linked> {
90+
const keyOf = (entry: ComposerPullRequestMatch) =>
91+
`${entry.repository.trim().toLowerCase()}#${entry.number}`;
92+
const matchesByKey = new Map(input.matches.map((entry) => [keyOf(entry), entry]));
93+
const words = input.query.toLowerCase().split(/\s+/u).filter(Boolean);
94+
const linked = input.linked.flatMap((entry): Array<Entry | Linked> => {
95+
const match = matchesByKey.get(keyOf(entry));
96+
if (match !== undefined) return [match];
97+
const text =
98+
`#${entry.number} ${entry.title} ${entry.repository} ${entry.headBranch}`.toLowerCase();
99+
return words.every((word) => text.includes(word)) ? [entry] : [];
100+
});
101+
const linkedKeys = new Set(linked.map(keyOf));
102+
return [...linked, ...input.matches.filter((entry) => !linkedKeys.has(keyOf(entry)))].slice(
103+
0,
104+
input.limit,
105+
);
106+
}

0 commit comments

Comments
 (0)