diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 0825c5cb92cc..600d0ce8efbf 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -175,7 +175,11 @@ import { CommandPaletteMetaDot, ThreadCommandSubtitle, } from "./ThreadCommandSubtitle"; -import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators"; +import { + ThreadRowLeadingStatus, + ThreadRowTrailingStatus, + ThreadSearchPullRequestNumber, +} from "./ThreadStatusIndicators"; import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server"; import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances"; import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings"; @@ -1340,7 +1344,16 @@ function OpenCommandPaletteDialog(props: { sortOrder: clientSettings.sidebarThreadSortOrder, icon: , renderLeadingContent: (thread) => , - renderTrailingContent: (thread) => , + renderTrailingContent: (thread) => ( + <> + + + + ), renderDescription: (thread, { projectTitle }) => { const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 90f65ea2b941..6d416456935a 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -211,6 +211,7 @@ import { synchronizeTerminalPulse, type TerminalStatusIndicator, useLinkedThreadPullRequest, + ThreadSearchPullRequestNumber, } from "./ThreadStatusIndicators"; import { resolveSnoozePresets, snoozeWakeLabel, type SnoozePreset } from "./Sidebar.snooze"; import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon"; @@ -2019,6 +2020,7 @@ function latestTurnDiff( } const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { + isSettled: boolean; thread: SidebarThreadSummary; project: EnvironmentProject | null; projectDisplayName: string | null; @@ -2118,11 +2120,12 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { tabIndex={-1} aria-selected={props.isHighlighted} aria-current={accessibility.current} + aria-describedby={`${props.resultId}-pr`} aria-label={accessibility.label} onMouseMove={props.onHighlight} onClick={props.onSelect} className={cn( - "flex min-h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1 text-left text-sm outline-none", + "group/sidebar-row group/v2-row flex min-h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 py-1 text-left text-sm outline-none", props.isHighlighted || props.isRouteActive ? "bg-sidebar-row-active text-sidebar-foreground" : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground", @@ -2133,12 +2136,40 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { } > {props.project ? ( - + ) : null} - {thread.title} - + + {thread.title} + + + + + {threadTimeLabel(thread)} @@ -4623,6 +4654,7 @@ export default function Sidebar() { ); return ( ); } + +/** Number presentation for compact thread search results. */ +function ThreadPullRequestNumber({ + number, + className, + ...props +}: ComponentProps<"span"> & { number: number }) { + return ( + + #{number} + + ); +} + +function settledPrHoverColorClass(state: NonNullable["state"], isDraft = false): string { + switch (state) { + case "open": + if (isDraft) { + return "group-hover/sidebar-row:text-zinc-500 dark:group-hover/sidebar-row:text-zinc-400/80"; + } + return "group-hover/sidebar-row:text-emerald-600 dark:group-hover/sidebar-row:text-emerald-300/90"; + case "merged": + return "group-hover/sidebar-row:text-violet-600 dark:group-hover/sidebar-row:text-violet-300/90"; + case "closed": + return "group-hover/sidebar-row:text-red-600 dark:group-hover/sidebar-row:text-red-300/90"; + } +} + +export function ThreadSearchPullRequestNumber({ + thread, + query, + enabled = true, + settled = false, +}: { + thread: SidebarThreadSummary; + query: string; + enabled?: boolean; + settled?: boolean; +}) { + const normalizedQuery = query.trim().toLowerCase(); + const matches = (candidate: Parameters[0]) => + normalizedQuery.length > 0 && + threadPullRequestSearchTerms(candidate).some((term) => + term.toLowerCase().includes(normalizedQuery), + ); + const matchedLink = visibleThreadPullRequests(thread.pullRequests).find((pr) => + matches({ pullRequests: [pr] }), + ); + const fallbackCandidates = + thread.pullRequests.length === 0 + ? [thread.linkedPullRequest, thread.branchPullRequest] + : [thread.branchPullRequest]; + const legacyReference = matchedLink + ? undefined + : fallbackCandidates.find((pr) => pr != null && matches({ linkedPullRequest: pr })); + const legacyStatus = useLinkedThreadPullRequest( + thread.environmentId, + legacyReference, + enabled, + undefined, + legacyReference, + ); + const reference = matchedLink ?? legacyReference; + const linked = matchedLink ? linkedPullRequestSnapshotStatus(matchedLink) : legacyStatus; + const status = prStatusIndicator(linked?.pr ?? null, linked?.sourceControlProvider); + return reference ? ( + + ) : null; +} diff --git a/packages/shared/src/threadPullRequests.test.ts b/packages/shared/src/threadPullRequests.test.ts index 4844df8f7786..a0db9e6830b0 100644 --- a/packages/shared/src/threadPullRequests.test.ts +++ b/packages/shared/src/threadPullRequests.test.ts @@ -433,6 +433,31 @@ describe("chain selection and badge state", () => { }); describe("threadPullRequestSearchTerms", () => { + it.each([undefined, [], [link(34)]])( + "searches the branch PR alongside existing links (%j)", + (pullRequests) => { + const branchPullRequest = { + projectId: ProjectId.make("project"), + repository: "pingdotgg/t3code", + number: 11809, + url: "https://github.com/pingdotgg/t3code/pull/11809", + }; + const terms = threadPullRequestSearchTerms({ + pullRequests, + branchPullRequest, + linkedPullRequest: { + ...branchPullRequest, + number: 12, + url: "https://github.com/pingdotgg/t3code/pull/12", + }, + }); + for (const query of ["11809", "#11809", "pingdotgg/t3code#11809", branchPullRequest.url]) { + expect(terms.some((term) => term.includes(query))).toBe(true); + } + expect(terms).toContain(pullRequests?.length ? "#34" : "#12"); + }, + ); + it("includes completed and unsynced links but excludes dismissed links", () => { const terms = threadPullRequestSearchTerms({ pullRequests: [ diff --git a/packages/shared/src/threadPullRequests.ts b/packages/shared/src/threadPullRequests.ts index e747c151bd1c..e58ddfce24f8 100644 --- a/packages/shared/src/threadPullRequests.ts +++ b/packages/shared/src/threadPullRequests.ts @@ -306,19 +306,28 @@ export function resolveThreadPullRequestBadge( return { kind: "pull-request", others: visible.length - 1, state }; } -/** Search terms for visible PR links, including the legacy single-link projection. */ +/** Search terms for visible PR links, the legacy projection, and the branch PR. */ export function threadPullRequestSearchTerms(thread: { readonly pullRequests?: ReadonlyArray | undefined; readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined; + readonly branchPullRequest?: ThreadLinkedPullRequest | null | undefined; }): string[] { + const branch = thread.branchPullRequest; + const terms = branch + ? [`#${branch.number}`, `${branch.repository}#${branch.number}`, branch.url] + : []; if (thread.pullRequests !== undefined && thread.pullRequests.length > 0) { - return visibleThreadPullRequests(thread.pullRequests).flatMap((link) => [ - `#${link.number}`, - `${link.repository}#${link.number}`, - link.url, - link.snapshot?.title ?? "", - ]); + return terms.concat( + visibleThreadPullRequests(thread.pullRequests).flatMap((link) => [ + `#${link.number}`, + `${link.repository}#${link.number}`, + link.url, + link.snapshot?.title ?? "", + ]), + ); } const legacy = thread.linkedPullRequest; - return legacy ? [`#${legacy.number}`, `${legacy.repository}#${legacy.number}`, legacy.url] : []; + return legacy + ? terms.concat([`#${legacy.number}`, `${legacy.repository}#${legacy.number}`, legacy.url]) + : terms; }