Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ import {
CommandPaletteMetaDot,
ThreadCommandSubtitle,
} from "./ThreadCommandSubtitle";
import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";
import {
ThreadRowLeadingStatus,
ThreadRowTrailingStatus,
ThreadSearchPullRequestNumber,
} from "./ThreadStatusIndicators";
import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server";
import {
applyProviderInstanceSettings,
Expand Down Expand Up @@ -1387,7 +1391,16 @@ function OpenCommandPaletteDialog(props: {
sortOrder: clientSettings.sidebarThreadSortOrder,
icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,
renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />,
renderTrailingContent: (thread) => <ThreadRowTrailingStatus thread={thread} />,
renderTrailingContent: (thread) => (
<>
<ThreadSearchPullRequestNumber
thread={thread}
query={threadSearchQuery}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/CommandPalette.tsx:1415

Multi-token searches such as owner/repo #123 return the matching thread but render no PR badge. ThreadSearchPullRequestNumber receives the entire threadSearchQuery and requires it as one contiguous substring, unlike filterCommandPaletteGroups, which matches each whitespace-separated token independently. Make the badge use the same token-based matching predicate so it identifies PRs for valid command-palette searches.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/CommandPalette.tsx around line 1415:

Multi-token searches such as `owner/repo #123` return the matching thread but render no PR badge. `ThreadSearchPullRequestNumber` receives the entire `threadSearchQuery` and requires it as one contiguous substring, unlike `filterCommandPaletteGroups`, which matches each whitespace-separated token independently. Make the badge use the same token-based matching predicate so it identifies PRs for valid command-palette searches.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Claude Opus 5.5 responding on behalf of @tris203

Valid, fixed in a5fc6c5. The PR number now uses the same per-token matching as the palette filter: a PR is named when every whitespace-separated token of the query appears in that PR's search terms, so owner/repo #123 shows #123. The sidebar uses the same predicate. Covered by a new pullRequestMatchesSearchQuery test in Sidebar.logic.test.ts.

A mixed query such as rebase 123, where one token comes from the title, still shows no number. Naming a PR on any single matching token would label every result whenever a token is the repository name, so I kept the stricter rule.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

settled={thread.settledOverride === "settled"}
/>
<ThreadRowTrailingStatus thread={thread} />
</>
),
renderDescription: (thread, { projectTitle }) => {
const modelInstanceId =
thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId;
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
resolveThreadRowClassName,
resolveThreadStatusPill,
resolveWorkingStartedAt,
pullRequestMatchesSearchQuery,
searchSidebarThreads,
shouldClearThreadSelectionOnMouseDown,
shouldShowSidebarV2Duration,
Expand Down Expand Up @@ -991,6 +992,27 @@ describe("resolveSidebarThreadStatus", () => {
});
});

describe("pullRequestMatchesSearchQuery", () => {
const branchPullRequest = {
projectId: ProjectId.make("project"),
repository: "pingdotgg/t3code",
number: 123,
url: "https://github.com/pingdotgg/t3code/pull/123",
};

it("matches when every query token appears in the PR's terms", () => {
expect(pullRequestMatchesSearchQuery({ branchPullRequest }, "123")).toBe(true);
expect(pullRequestMatchesSearchQuery({ branchPullRequest }, "pingdotgg/t3code #123")).toBe(
true,
);
});

it("rejects queries with a token the PR does not carry, and blank queries", () => {
expect(pullRequestMatchesSearchQuery({ branchPullRequest }, "rebase 123")).toBe(false);
expect(pullRequestMatchesSearchQuery({ branchPullRequest }, " ")).toBe(false);
});
});

describe("searchSidebarThreads", () => {
const searchThread = (id: string, title: string, project: string) => ({
environmentId: localEnvironmentId,
Expand Down
17 changes: 16 additions & 1 deletion apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
type ThreadSortInput,
} from "../lib/threadSort";
import type { SidebarThreadSummary, Thread } from "../types";
import { cn } from "../lib/utils";
import { cn, normalizeSearchText } from "../lib/utils";
import { isLatestRunSettled } from "../session-logic";
import { resolveServerBackedAppStageLabel } from "../branding.logic";

Expand Down Expand Up @@ -1083,6 +1083,21 @@ export function searchSidebarThreads<
return [...titleMatches, ...contentMatches];
}

/**
* Whether a PR's search terms contain every whitespace-separated token of the
* query. Search results use this to pick the PR number to show, so it has to
* accept the multi-token queries the command palette matches on.
*/
export function pullRequestMatchesSearchQuery(
pullRequest: Parameters<typeof threadPullRequestSearchTerms>[0],
query: string,
): boolean {
const normalizedQuery = normalizeSearchText(query);
if (normalizedQuery.length === 0) return false;
const haystack = normalizeSearchText(threadPullRequestSearchTerms(pullRequest).join(" "));
return normalizedQuery.split(" ").every((token) => haystack.includes(token));
}

export function filterSidebarProjectScopeItems<TItem extends { readonly value: string }>(input: {
items: readonly TItem[];
query: string;
Expand Down
40 changes: 36 additions & 4 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ import {
synchronizeTerminalPulse,
type TerminalStatusIndicator,
useLinkedThreadPullRequest,
ThreadSearchPullRequestNumber,
} from "./ThreadStatusIndicators";
import { resolveSnoozePresets, snoozeWakeLabel, type SnoozePreset } from "./Sidebar.snooze";
import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon";
Expand Down Expand Up @@ -2129,6 +2130,7 @@ function latestRunDiff(
}

const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
isSettled: boolean;
thread: SidebarThreadSummary;
project: EnvironmentProject | null;
projectDisplayName: string | null;
Expand Down Expand Up @@ -2228,11 +2230,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",
Expand All @@ -2243,12 +2246,40 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
}
>
{props.project ? (
<ProjectFavicon project={props.project} className="size-4 shrink-0" />
<ProjectFavicon
project={props.project}
className={cn(
"size-4 shrink-0 transition-opacity",
props.isSettled &&
!props.isRouteActive &&
!props.isHighlighted &&
"opacity-40 grayscale group-hover/sidebar-row:opacity-100 group-hover/sidebar-row:grayscale-0",
)}
/>
) : null}
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex min-w-0 items-center gap-2.5">
<span className="min-w-0 flex-1 truncate">{thread.title}</span>
<span className="shrink-0 text-xs text-muted-foreground/55 tabular-nums">
<span
className={cn(
"min-w-0 flex-1 truncate",
props.isSettled
? "font-normal text-secondary-label/70 group-hover/sidebar-row:text-foreground"
: "font-medium text-foreground",
(props.isHighlighted || props.isRouteActive) && "text-foreground",
thread.titleRegeneration != null && "opacity-55",
)}
>
{thread.title}
</span>
<span id={`${props.resultId}-pr`} className="contents">
<ThreadSearchPullRequestNumber
thread={thread}
query={props.searchQuery}
enabled={leaseLiveStatus}
settled={props.isSettled}
/>
</span>
<span className="w-8 shrink-0 text-right text-xs text-muted-foreground/55 tabular-nums">
{threadTimeLabel(thread)}
</span>
</span>
Expand Down Expand Up @@ -4857,6 +4888,7 @@ export default function Sidebar() {
);
return (
<SidebarSearchResultRow
isSettled={settledThreadKeys.has(threadKey)}
key={threadKey}
thread={thread}
project={
Expand Down
85 changes: 84 additions & 1 deletion apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ import {
type ThreadPullRequestBadge,
} from "@t3tools/shared/threadPullRequests";
import { useRender } from "@base-ui/react/use-render";
import { type ReactNode, type AnimationEvent, type MouseEvent, type ReactElement } from "react";
import {
type ReactNode,
type AnimationEvent,
type MouseEvent,
type ReactElement,
type ComponentProps,
} from "react";
import { cn } from "../lib/utils";

import { parseChangeRequestUrl } from "../lib/openPullRequestLink";
Expand All @@ -38,6 +44,7 @@ import { vcsEnvironment } from "../state/vcs";
import { useUiStateStore } from "../uiStateStore";
import { resolveChangeRequestPresentation } from "../sourceControlPresentation";
import {
pullRequestMatchesSearchQuery,
resolveThreadLastVisitedAt,
resolveThreadStatusPill,
type ThreadStatusPill,
Expand Down Expand Up @@ -1040,3 +1047,79 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma
</span>
);
}

/** Number presentation for compact thread search results. */
function ThreadPullRequestNumber({
number,
className,
...props
}: ComponentProps<"span"> & { number: number }) {
return (
<span {...props} className={cn("shrink-0 text-xs tabular-nums", className)}>
#{number}
</span>
);
}

function settledPrHoverColorClass(state: NonNullable<ThreadPr>["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 matchedLink = visibleThreadPullRequests(thread.pullRequests).find((pr) =>
pullRequestMatchesSearchQuery({ pullRequests: [pr] }, query),
);
const fallbackCandidates =
thread.pullRequests.length === 0
? [thread.linkedPullRequest, thread.branchPullRequest]
: [thread.branchPullRequest];
const legacyReference = matchedLink
? undefined
: fallbackCandidates.find(
(pr) => pr != null && pullRequestMatchesSearchQuery({ linkedPullRequest: pr }, query),
);
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 ? (
<ThreadPullRequestNumber
number={reference.number}
className={
settled
? cn(
"text-secondary-label transition-colors",
linked?.pr && settledPrHoverColorClass(linked.pr.state, linked.pr.isDraft),
)
: (status?.colorClass ?? "text-secondary-label")
}
aria-label={status?.tooltip ?? `Pull request #${reference.number}`}
/>
) : null;
}
25 changes: 25 additions & 0 deletions packages/shared/src/threadPullRequests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,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: [
Expand Down
25 changes: 17 additions & 8 deletions packages/shared/src/threadPullRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,21 +308,30 @@ 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<ThreadPullRequestLink> | 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;
}

/** Older V2 event payloads stored one link; an explicit empty array means it was unlinked. */
Expand Down
Loading