fix(app): find starting refs by change request descriptions (#6558)

Preserve returned change request bodies as search-only option text so client filtering keeps description matches. Keep title and ID matches ranked ahead of secondary text.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-11 06:53:37 +02:00
committed by GitHub
parent 7f9b65d464
commit cfb18f16d4
4 changed files with 79 additions and 7 deletions
@@ -160,3 +160,16 @@ describe("combobox above-search ordering", () => {
expect(getComboboxFallbackIndex(ordered.length, "below-search")).toBe(0);
});
});
describe("search-only option text", () => {
it("finds hidden search text while keeping title matches first", () => {
const bodyMatch = { id: "pr:7", label: "Fix parsing", searchText: "Release notes for 2024" };
const titleMatch = { id: "pr:8", label: "2024 release" };
expect(filterAndRankComboboxOptions([bodyMatch, titleMatch], "2024")).toEqual([
titleMatch,
bodyMatch,
]);
expect(filterAndRankComboboxOptions([bodyMatch], "unrelated")).toEqual([]);
expect(filterAndRankComboboxOptions([bodyMatch], "parsing 2024")).toEqual([bodyMatch]);
});
});
@@ -10,18 +10,22 @@ export interface ComboboxOptionModel {
id: string;
label: string;
description?: string;
/** Additional text to match without displaying it in the option row. */
searchText?: string;
kind?: ComboboxOptionKind;
}
const DESCRIPTION_FALLBACK_TIER = 99;
const SECONDARY_TEXT_FALLBACK_TIER = 99;
function scoreOption(opt: ComboboxOptionModel, search: string): MatchScore | null {
const best = scoreTextFields(search, [opt.label, opt.id]);
if (best) return best;
if (!opt.description) return null;
const descriptionScore = scoreTextFields(search, [opt.description]);
if (!descriptionScore) return null;
return { ...descriptionScore, tier: descriptionScore.tier + DESCRIPTION_FALLBACK_TIER };
const secondaryFields = opt.searchText
? [opt.label, opt.id, opt.description ?? "", opt.searchText]
: [opt.description ?? ""];
const secondaryScore = scoreTextFields(search, secondaryFields);
if (!secondaryScore) return null;
return { ...secondaryScore, tier: secondaryScore.tier + SECONDARY_TEXT_FALLBACK_TIER };
}
export interface BuildVisibleComboboxOptionsInput {
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildVisibleComboboxOptions } from "@/components/ui/combobox-options";
import type { ForgeSearchItem } from "@getpaseo/protocol/messages";
import {
type BranchPickerDetail,
@@ -340,3 +341,48 @@ describe("defaultBasePickerItem", () => {
expect(defaultBasePickerItem({ currentBranch: null })).toBeNull();
});
});
describe("starting-ref search", () => {
it.each(["gitlab", "github"] as const)(
"keeps a %s change request returned for a body-only match",
(forge) => {
const item: ForgeSearchItem = {
...prItem,
forge,
number: 3896,
title: "fix(config): send environment credentials only to trusted hosts",
body: "This mirrors the fix shipped for CVE-2024-53858.",
headRefName: "8545-duo-test",
};
const { options } = buildPickerOptionData({
branchDetails: [],
prItems: [item],
baseItem: null,
});
const visible = buildVisibleComboboxOptions({
options,
searchQuery: "2024",
searchable: true,
allowCustomValue: false,
customValuePrefix: "Use",
});
expect(visible.map((option) => option.label)).toEqual([
`${forge === "gitlab" ? "!" : "#"}3896 fix(config): send environment credentials only to trusted hosts`,
]);
const selected = buildPickerOptionData({
branchDetails: [],
prItems: [],
baseItem: { kind: "github-pr", item },
});
expect(
buildVisibleComboboxOptions({
options: selected.options,
searchQuery: "2024",
searchable: true,
allowCustomValue: false,
customValuePrefix: "Use",
}),
).toEqual(visible);
},
);
});
@@ -264,7 +264,10 @@ export function buildPickerOptionData(input: {
itemById.set(id, { kind: "github-pr", item: pr });
const updatedAtMs = pr.updatedAt ? Date.parse(pr.updatedAt) : 0;
const timestamp = Number.isNaN(updatedAtMs) ? 0 : Math.floor(updatedAtMs / 1000);
timedOptions.push({ option: { id, label: formatPrLabel(pr) }, timestamp });
timedOptions.push({
option: { id, label: formatPrLabel(pr), searchText: pr.body ?? undefined },
timestamp,
});
}
const baseItem = input.baseItem;
@@ -279,7 +282,13 @@ export function buildPickerOptionData(input: {
const labeledItem = disambiguate(baseItem, timedOptions);
itemById.set(selectedOptionId, labeledItem);
timedOptions.push({
option: { id: selectedOptionId, label: pickerItemLabel(labeledItem) },
option: {
id: selectedOptionId,
label: pickerItemLabel(labeledItem),
...(labeledItem.kind === "github-pr"
? { searchText: labeledItem.item.body ?? undefined }
: {}),
},
// The base sorts first: it is what a workspace is created from unless you pick something.
timestamp: Number.POSITIVE_INFINITY,
});