| author | |
| committer | |
| log | 0425c32571e033c068b8caebfd984acce9f8735b |
| tree | bfa1bd874fe8d54f7650d221692c7ec72193316b |
| parent | 3417bc3856a25fa772f969faeaf8428a58010d47 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>8 files changed, 186 insertions(+), 22 deletions(-)
apps/mobile/src/features/threads/ThreadComposer.tsx+1| ... | ... | @@ -502,6 +502,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 502 | 502 | ? (project?.id ?? null) |
| 503 | 503 | : null, |
| 504 | 504 | pullRequestRepository: project?.repositoryIdentity?.displayName ?? null, |
| 505 | threadPullRequests: props.selectedThread.pullRequests, | |
| 505 | 506 | selectedProviderStatus, |
| 506 | 507 | hasThread: true, |
| 507 | 508 | hasCompactableConversation: props.hasCompactableConversation, |
apps/mobile/src/features/threads/use-composer-command-menu.ts+10-1| ... | ... | @@ -4,6 +4,7 @@ import type { |
| 4 | 4 | ProviderInteractionMode, |
| 5 | 5 | ServerProvider, |
| 6 | 6 | ThreadId, |
| 7 | ThreadPullRequestLink, | |
| 7 | 8 | } from "@t3tools/contracts"; |
| 8 | 9 | import { matchComposerThreadItems } from "@t3tools/client-runtime/composerThreadItems"; |
| 9 | 10 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/models"; |
| ... | ... | @@ -179,6 +180,7 @@ export function useComposerCommandMenu({ |
| 179 | 180 | projectCwd, |
| 180 | 181 | pullRequestProjectId = null, |
| 181 | 182 | pullRequestRepository = null, |
| 183 | threadPullRequests, | |
| 182 | 184 | selectedProviderStatus, |
| 183 | 185 | hasThread, |
| 184 | 186 | hasCompactableConversation, |
| ... | ... | @@ -198,6 +200,8 @@ export function useComposerCommandMenu({ |
| 198 | 200 | readonly projectCwd: string | null; |
| 199 | 201 | readonly pullRequestProjectId?: ProjectId | null; |
| 200 | 202 | readonly pullRequestRepository?: string | null; |
| 203 | /** The thread's linked pull requests, offered in `#` suggestions from any repository. */ | |
| 204 | readonly threadPullRequests?: ReadonlyArray<ThreadPullRequestLink>; | |
| 201 | 205 | readonly selectedProviderStatus: ServerProvider | null; |
| 202 | 206 | readonly hasThread: boolean; |
| 203 | 207 | readonly hasCompactableConversation: boolean; |
| ... | ... | @@ -357,6 +361,7 @@ export function useComposerCommandMenu({ |
| 357 | 361 | environmentId, |
| 358 | 362 | projectId: pullRequestProjectId, |
| 359 | 363 | repository: pullRequestRepository, |
| 364 | threadPullRequests, | |
| 360 | 365 | query: trigger?.kind === "pull-request" ? trigger.query : null, |
| 361 | 366 | }); |
| 362 | 367 | |
| ... | ... | @@ -377,7 +382,11 @@ export function useComposerCommandMenu({ |
| 377 | 382 | isDraft: entry.isDraft, |
| 378 | 383 | }, |
| 379 | 384 | label: `#${entry.number}`, |
| 380 | description: `${entry.isDraft ? "Draft" : entry.state} · ${entry.title}`, | |
| 385 | description: `${entry.isDraft ? "Draft" : entry.state} · ${ | |
| 386 | entry.repository.toLowerCase() === pullRequestRepository?.toLowerCase() | |
| 387 | ? "" | |
| 388 | : `${entry.repository} · ` | |
| 389 | }${entry.title}`, | |
| 381 | 390 | })); |
| 382 | 391 | } |
| 383 | 392 |
apps/mobile/src/state/queries.ts+16-2| ... | ... | @@ -1,4 +1,7 @@ |
| 1 | import { filterComposerPullRequestMatches } from "@t3tools/shared/composerPullRequestMatches"; | |
| 1 | import { | |
| 2 | composerPullRequestsFromLinks, | |
| 3 | filterComposerPullRequestMatches, | |
| 4 | } from "@t3tools/shared/composerPullRequestMatches"; | |
| 2 | 5 | import { resolveFilesystemReadAccess } from "@t3tools/client-runtime/state/filesystem"; |
| 3 | 6 | import { environmentSession } from "./session"; |
| 4 | 7 | import { useEnvironmentPresentation } from "./presentation"; |
| ... | ... | @@ -8,6 +11,7 @@ import type { |
| 8 | 11 | OrchestrationV2ProjectedTurnItem, |
| 9 | 12 | ProjectId, |
| 10 | 13 | ThreadId, |
| 14 | ThreadPullRequestLink, | |
| 11 | 15 | VcsListRefsResult, |
| 12 | 16 | VcsRef, |
| 13 | 17 | } from "@t3tools/contracts"; |
| ... | ... | @@ -81,9 +85,18 @@ export function useComposerPullRequestSearch(input: { |
| 81 | 85 | environmentId: EnvironmentId | null; |
| 82 | 86 | projectId: ProjectId | null; |
| 83 | 87 | repository: string | null; |
| 88 | /** The thread's linked pull requests, offered in numeric searches from any repository. */ | |
| 89 | threadPullRequests?: ReadonlyArray<ThreadPullRequestLink> | undefined; | |
| 84 | 90 | query: string | null; |
| 85 | 91 | }) { |
| 86 | 92 | const query = useDebouncedValue(input.query, 180); |
| 93 | const linked = useMemo( | |
| 94 | () => | |
| 95 | input.projectId === null | |
| 96 | ? [] | |
| 97 | : composerPullRequestsFromLinks(input.threadPullRequests ?? [], input.projectId), | |
| 98 | [input.projectId, input.threadPullRequests], | |
| 99 | ); | |
| 87 | 100 | const ready = |
| 88 | 101 | query === input.query && |
| 89 | 102 | query !== null && |
| ... | ... | @@ -122,6 +135,7 @@ export function useComposerPullRequestSearch(input: { |
| 122 | 135 | if (numeric) { |
| 123 | 136 | return filterComposerPullRequestMatches({ |
| 124 | 137 | entries: [...(exact.data ? [exact.data] : []), ...(list.data?.entries ?? [])], |
| 138 | linked, | |
| 125 | 139 | projectId: input.projectId!, |
| 126 | 140 | repository: input.repository!, |
| 127 | 141 | query: query ?? "", |
| ... | ... | @@ -142,7 +156,7 @@ export function useComposerPullRequestSearch(input: { |
| 142 | 156 | return [...unique.values()] |
| 143 | 157 | .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)) |
| 144 | 158 | .slice(0, 20); |
| 145 | }, [ready, exact.data, list.data, input.projectId, input.repository, numeric, query]); | |
| 159 | }, [ready, exact.data, list.data, linked, input.projectId, input.repository, numeric, query]); | |
| 146 | 160 | return { |
| 147 | 161 | entries, |
| 148 | 162 | isPending: input.query !== null && (query !== input.query || list.isPending || exact.isPending), |
apps/web/src/components/ChatView.tsx+1| ... | ... | @@ -11876,6 +11876,7 @@ export default function ChatView(props: ChatViewProps) { |
| 11876 | 11876 | pullRequestRepository={ |
| 11877 | 11877 | supportsPullRequests ? activeProjectRepository : null |
| 11878 | 11878 | } |
| 11879 | threadPullRequests={(activeThreadShell ?? activeThread)?.pullRequests} | |
| 11879 | 11880 | restingControlsHost={restingComposerControlsHost} |
| 11880 | 11881 | restingControlsHaveLeadingContext={ |
| 11881 | 11882 | mountComposerContextStrip && |
apps/web/src/components/chat/ChatComposer.tsx+26-3| ... | ... | @@ -7,7 +7,10 @@ import { usePrimaryEnvironmentId } from "../../state/environments"; |
| 7 | 7 | import { useRightPanelStore } from "~/rightPanelStore"; |
| 8 | 8 | import { AttachmentFilePreview } from "../files/AttachmentFilePreview"; |
| 9 | 9 | import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog"; |
| 10 | import { filterComposerPullRequestMatches } from "@t3tools/shared/composerPullRequestMatches"; | |
| 10 | import { | |
| 11 | composerPullRequestsFromLinks, | |
| 12 | filterComposerPullRequestMatches, | |
| 13 | } from "@t3tools/shared/composerPullRequestMatches"; | |
| 11 | 14 | import { importPastedComposerText, readPastedComposerContext } from "../composerInlineTokenPaste"; |
| 12 | 15 | import { elementContextToPreviewAnnotation } from "../../lib/elementContext"; |
| 13 | 16 | import { RefreshIcon } from "~/components/ui/refresh-icon"; |
| ... | ... | @@ -248,7 +251,11 @@ import { THREAD_CONTEXT_DROP_EVENT, threadContextDropTargetProps } from "./threa |
| 248 | 251 | import { readThreadShell, useThreadShells } from "~/state/entities"; |
| 249 | 252 | import { requestConfirmDialog } from "~/confirmDialog"; |
| 250 | 253 | import { encodeComposerContextFragment } from "@t3tools/shared/composerContextClipboard"; |
| 251 | import type { ComposerContextClipboardFragment, ComposerContextRecord } from "@t3tools/contracts"; | |
| 254 | import type { | |
| 255 | ComposerContextClipboardFragment, | |
| 256 | ComposerContextRecord, | |
| 257 | ThreadPullRequestLink, | |
| 258 | } from "@t3tools/contracts"; | |
| 252 | 259 | import { resolveAssetUrl } from "~/assets/assetUrls"; |
| 253 | 260 | import { assetEnvironment } from "~/state/assets"; |
| 254 | 261 | import { readPreparedConnection } from "~/state/session"; |
| ... | ... | @@ -1629,6 +1636,8 @@ export interface ChatComposerProps { |
| 1629 | 1636 | gitCwd: string | null; |
| 1630 | 1637 | pullRequestProjectId: ProjectId | null; |
| 1631 | 1638 | pullRequestRepository: string | null; |
| 1639 | /** The thread's linked pull requests, offered in `#` suggestions from any repository. */ | |
| 1640 | threadPullRequests: ReadonlyArray<ThreadPullRequestLink> | undefined; | |
| 1632 | 1641 | restingControlsHost: HTMLDivElement | null; |
| 1633 | 1642 | restingControlsHaveLeadingContext: boolean; |
| 1634 | 1643 | onRestingControlsVisibilityChange: (visible: boolean) => void; |
| ... | ... | @@ -1775,6 +1784,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 1775 | 1784 | gitCwd, |
| 1776 | 1785 | pullRequestProjectId, |
| 1777 | 1786 | pullRequestRepository, |
| 1787 | threadPullRequests, | |
| 1778 | 1788 | restingControlsHost, |
| 1779 | 1789 | restingControlsHaveLeadingContext, |
| 1780 | 1790 | onRestingControlsVisibilityChange, |
| ... | ... | @@ -2688,6 +2698,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2688 | 2698 | }), |
| 2689 | 2699 | ); |
| 2690 | 2700 | |
| 2701 | const linkedPullRequests = useMemo( | |
| 2702 | () => | |
| 2703 | pullRequestProjectId === null | |
| 2704 | ? [] | |
| 2705 | : composerPullRequestsFromLinks(threadPullRequests ?? [], pullRequestProjectId), | |
| 2706 | [pullRequestProjectId, threadPullRequests], | |
| 2707 | ); | |
| 2708 | ||
| 2691 | 2709 | const composerMenuItems = useMemo<ComposerCommandItem[]>(() => { |
| 2692 | 2710 | if (!composerTrigger) return []; |
| 2693 | 2711 | if (composerTrigger.kind === "path") { |
| ... | ... | @@ -2799,6 +2817,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2799 | 2817 | const matches = /^\d*$/u.test(composerTrigger.query) |
| 2800 | 2818 | ? filterComposerPullRequestMatches({ |
| 2801 | 2819 | entries: [...exactPullRequest, ...(pullRequestLookup.data?.entries ?? [])], |
| 2820 | linked: linkedPullRequests, | |
| 2802 | 2821 | projectId: pullRequestProjectId, |
| 2803 | 2822 | repository: pullRequestRepository, |
| 2804 | 2823 | query: composerTrigger.query, |
| ... | ... | @@ -2835,7 +2854,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2835 | 2854 | isDraft: pullRequest.isDraft, |
| 2836 | 2855 | }, |
| 2837 | 2856 | label: `#${pullRequest.number}`, |
| 2838 | description: pullRequest.title, | |
| 2857 | description: | |
| 2858 | pullRequest.repository.trim().toLowerCase() === pullRequestRepository.trim().toLowerCase() | |
| 2859 | ? pullRequest.title | |
| 2860 | : `${pullRequest.repository} · ${pullRequest.title}`, | |
| 2839 | 2861 | })); |
| 2840 | 2862 | } |
| 2841 | 2863 | return []; |
| ... | ... | @@ -2846,6 +2868,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) |
| 2846 | 2868 | environmentId, |
| 2847 | 2869 | environmentThreadShells, |
| 2848 | 2870 | exactPullRequestLookup.data, |
| 2871 | linkedPullRequests, | |
| 2849 | 2872 | planModeUiEnabled, |
| 2850 | 2873 | pullRequestLookup.data, |
| 2851 | 2874 | pullRequestProjectId, |
docs/user/composer.md+2-1| ... | ... | @@ -257,7 +257,8 @@ excerpt, Terminal 1 lines 3-4" and similar to screen readers. |
| 257 | 257 | A pull request appears as its icon and number. Its color reflects whether it was open, draft, |
| 258 | 258 | merged, or closed when it was attached. Select it to inspect the captured title and branches, |
| 259 | 259 | then choose **Open pull request** to visit the pull request. On web and desktop, type `#` to browse the newest |
| 260 | pull requests in the current project's repository. Continue typing digits to filter the recent list | |
| 260 | pull requests in the current project's repository. Pull requests linked to the thread are included | |
| 261 | from any repository and listed first, after any exact number match. Continue typing digits to filter the recent list | |
| 261 | 262 | by any part of its pull request numbers. A complete number is also resolved directly, even when that |
| 262 | 263 | pull request is older than the recent list. Type a single word after `#` to search pull requests in |
| 263 | 264 | the repository by text. Choose a result to insert it as a chip. |
packages/shared/src/composerPullRequestMatches.test.ts+71-1| ... | ... | @@ -1,6 +1,11 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | import { filterComposerPullRequestMatches } from "./composerPullRequestMatches.ts"; | |
| 3 | import type { ThreadPullRequestLink } from "@t3tools/contracts"; | |
| 4 | ||
| 5 | import { | |
| 6 | composerPullRequestsFromLinks, | |
| 7 | filterComposerPullRequestMatches, | |
| 8 | } from "./composerPullRequestMatches.ts"; | |
| 4 | 9 | |
| 5 | 10 | const entry = (number: number, updatedAt: string) => ({ |
| 6 | 11 | number, |
| ... | ... | @@ -46,3 +51,68 @@ describe("filterComposerPullRequestMatches", () => { |
| 46 | 51 | expect(result).toHaveLength(1); |
| 47 | 52 | }); |
| 48 | 53 | }); |
| 54 | ||
| 55 | describe("linked pull requests", () => { | |
| 56 | const link = ( | |
| 57 | repository: string, | |
| 58 | number: number, | |
| 59 | overrides: Partial<ThreadPullRequestLink> = {}, | |
| 60 | ): ThreadPullRequestLink => ({ | |
| 61 | host: "github.com", | |
| 62 | repository, | |
| 63 | number, | |
| 64 | url: `https://github.com/${repository}/pull/${number}`, | |
| 65 | source: "agent", | |
| 66 | linkedAt: "2026-01-01T00:00:00.000Z", | |
| 67 | snapshot: { | |
| 68 | state: "open", | |
| 69 | title: `${repository} ${number}`, | |
| 70 | headBranch: "feature", | |
| 71 | baseBranch: "main", | |
| 72 | isDraft: false, | |
| 73 | updatedAt: "2026-01-02T00:00:00.000Z", | |
| 74 | syncedAt: "2026-01-02T00:00:00.000Z", | |
| 75 | }, | |
| 76 | stack: null, | |
| 77 | ...overrides, | |
| 78 | }); | |
| 79 | ||
| 80 | it("offers a linked pull request from another repository above the project's own", () => { | |
| 81 | const result = filterComposerPullRequestMatches({ | |
| 82 | entries: [entry(4, "2026-01-03"), { ...entry(4, "2026-01-04"), repository: "owner/other" }], | |
| 83 | linked: composerPullRequestsFromLinks([link("owner/repo-b", 4)], "p1"), | |
| 84 | projectId: "p1", | |
| 85 | repository: "owner/repo", | |
| 86 | query: "4", | |
| 87 | limit: 10, | |
| 88 | }); | |
| 89 | expect(result.map((match) => `${match.repository}#${match.number}`)).toEqual([ | |
| 90 | "owner/repo-b#4", | |
| 91 | "owner/repo#4", | |
| 92 | ]); | |
| 93 | }); | |
| 94 | ||
| 95 | it("keeps one row for a linked pull request the project listing already has", () => { | |
| 96 | const result = filterComposerPullRequestMatches({ | |
| 97 | entries: [entry(4, "2026-01-03")], | |
| 98 | linked: composerPullRequestsFromLinks([link("Owner/Repo", 4)], "p1"), | |
| 99 | projectId: "p1", | |
| 100 | repository: "owner/repo", | |
| 101 | query: "", | |
| 102 | limit: 10, | |
| 103 | }); | |
| 104 | expect(result).toEqual([entry(4, "2026-01-03")]); | |
| 105 | }); | |
| 106 | ||
| 107 | it("skips links that are not synced yet or were dismissed", () => { | |
| 108 | expect( | |
| 109 | composerPullRequestsFromLinks( | |
| 110 | [ | |
| 111 | link("owner/repo-b", 4, { snapshot: null }), | |
| 112 | link("owner/repo-b", 5, { source: "stack-dismissed" }), | |
| 113 | ], | |
| 114 | "p1", | |
| 115 | ), | |
| 116 | ).toEqual([]); | |
| 117 | }); | |
| 118 | }); |
packages/shared/src/composerPullRequestMatches.ts+59-14| ... | ... | @@ -1,3 +1,7 @@ |
| 1 | import type { ThreadPullRequestLink } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | import { visibleThreadPullRequests } from "./threadPullRequests.ts"; | |
| 4 | ||
| 1 | 5 | export interface ComposerPullRequestMatch { |
| 2 | 6 | readonly number: number; |
| 3 | 7 | readonly projectId: string; |
| ... | ... | @@ -6,37 +10,78 @@ export interface ComposerPullRequestMatch { |
| 6 | 10 | } |
| 7 | 11 | |
| 8 | 12 | /** |
| 9 | * Pull requests matching the numeric fragment typed after `#`, de-duplicated. An exact number | |
| 10 | * match always outranks a substring match so the result limit can never drop the pull request | |
| 11 | * the user typed in full; the rest stay newest first. | |
| 13 | * Suggestion rows for the pull requests a thread links, from any repository. Built from the | |
| 14 | * synced snapshot, so a link the server has not read yet is left out until it has one. | |
| 15 | */ | |
| 16 | export function composerPullRequestsFromLinks( | |
| 17 | links: ReadonlyArray<ThreadPullRequestLink>, | |
| 18 | projectId: string, | |
| 19 | ) { | |
| 20 | return visibleThreadPullRequests(links).flatMap((link) => | |
| 21 | link.snapshot === null | |
| 22 | ? [] | |
| 23 | : [ | |
| 24 | { | |
| 25 | projectId, | |
| 26 | repository: link.repository, | |
| 27 | number: link.number, | |
| 28 | url: link.url, | |
| 29 | title: link.snapshot.title, | |
| 30 | headBranch: link.snapshot.headBranch, | |
| 31 | baseBranch: link.snapshot.baseBranch, | |
| 32 | state: link.snapshot.state, | |
| 33 | isDraft: link.snapshot.isDraft, | |
| 34 | updatedAt: link.snapshot.updatedAt ?? link.linkedAt, | |
| 35 | }, | |
| 36 | ], | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * Pull requests matching the numeric fragment typed after `#`, de-duplicated. `entries` are kept | |
| 42 | * to the project's repository; `linked` rows (the thread's own pull requests) may come from any | |
| 43 | * repository. An exact number match always outranks a substring match so the result limit can | |
| 44 | * never drop the pull request the user typed in full; then linked rows lead, newest first. | |
| 12 | 45 | */ |
| 13 | export function filterComposerPullRequestMatches<Entry extends ComposerPullRequestMatch>(input: { | |
| 46 | export function filterComposerPullRequestMatches< | |
| 47 | Entry extends ComposerPullRequestMatch, | |
| 48 | Linked extends ComposerPullRequestMatch = never, | |
| 49 | >(input: { | |
| 14 | 50 | readonly entries: ReadonlyArray<Entry>; |
| 51 | readonly linked?: ReadonlyArray<Linked>; | |
| 15 | 52 | readonly projectId: string; |
| 16 | 53 | readonly repository: string; |
| 17 | 54 | readonly query: string; |
| 18 | 55 | readonly limit: number; |
| 19 | }): ReadonlyArray<Entry> { | |
| 56 | }): ReadonlyArray<Entry | Linked> { | |
| 20 | 57 | const repository = input.repository.trim().toLowerCase(); |
| 58 | const keyOf = (entry: ComposerPullRequestMatch) => | |
| 59 | `${entry.repository.trim().toLowerCase()}#${entry.number}`; | |
| 60 | const linked = (input.linked ?? []).filter((entry) => String(entry.number).includes(input.query)); | |
| 61 | const linkedKeys = new Set(linked.map(keyOf)); | |
| 21 | 62 | const matchingEntries = input.entries.filter( |
| 22 | 63 | (entry) => |
| 23 | 64 | entry.projectId === input.projectId && |
| 24 | 65 | entry.repository.trim().toLowerCase() === repository && |
| 25 | 66 | String(entry.number).includes(input.query), |
| 26 | 67 | ); |
| 27 | const uniqueEntries = new Map<number, Entry>(); | |
| 28 | for (const entry of matchingEntries) { | |
| 29 | if (!uniqueEntries.has(entry.number)) { | |
| 30 | uniqueEntries.set(entry.number, entry); | |
| 68 | // Project rows go first so a linked pull request the listing also has keeps the fresher row. | |
| 69 | const uniqueEntries = new Map<string, Entry | Linked>(); | |
| 70 | for (const entry of [...matchingEntries, ...linked]) { | |
| 71 | if (!uniqueEntries.has(keyOf(entry))) { | |
| 72 | uniqueEntries.set(keyOf(entry), entry); | |
| 31 | 73 | } |
| 32 | 74 | } |
| 33 | const isExactMatch = (entry: Entry) => String(entry.number) === input.query; | |
| 75 | const isExactMatch = (entry: ComposerPullRequestMatch) => String(entry.number) === input.query; | |
| 76 | const isLinked = (entry: ComposerPullRequestMatch) => linkedKeys.has(keyOf(entry)); | |
| 34 | 77 | // `.sort()` on a copy, not `.toSorted()`: this runs on Hermes, which has no ES2023 array |
| 35 | 78 | // methods, and reaching for one here crashed the composer as the suggestions loaded. |
| 36 | 79 | return [...uniqueEntries.values()] |
| 37 | .sort((left, right) => { | |
| 38 | const exactness = Number(isExactMatch(right)) - Number(isExactMatch(left)); | |
| 39 | return exactness !== 0 ? exactness : right.updatedAt.localeCompare(left.updatedAt); | |
| 40 | }) | |
| 80 | .sort( | |
| 81 | (left, right) => | |
| 82 | Number(isExactMatch(right)) - Number(isExactMatch(left)) || | |
| 83 | Number(isLinked(right)) - Number(isLinked(left)) || | |
| 84 | right.updatedAt.localeCompare(left.updatedAt), | |
| 85 | ) | |
| 41 | 86 | .slice(0, input.limit); |
| 42 | 87 | } |