Skip to content
Closed
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 @@ -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";
Expand Down Expand Up @@ -1340,7 +1344,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}
settled={thread.settledOverride === "settled"}
/>
<ThreadRowTrailingStatus thread={thread} />
</>
),
renderDescription: (thread, { projectTitle }) => {
const modelInstanceId =
thread.session?.providerInstanceId ?? thread.modelSelection.instanceId;
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 @@ -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";
Expand Down Expand Up @@ -2019,6 +2020,7 @@ function latestTurnDiff(
}

const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
isSettled: boolean;
thread: SidebarThreadSummary;
project: EnvironmentProject | null;
projectDisplayName: string | null;
Expand Down Expand Up @@ -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",
Expand All @@ -2133,12 +2136,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 @@ -4623,6 +4654,7 @@ export default function Sidebar() {
);
return (
<SidebarSearchResultRow
isSettled={settledThreadKeys.has(threadKey)}
key={threadKey}
thread={thread}
project={
Expand Down
89 changes: 88 additions & 1 deletion apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ import {
resolveThreadPullRequestChains,
visibleThreadPullRequests,
type ThreadPullRequestBadge,
threadPullRequestSearchTerms,
} from "@t3tools/shared/threadPullRequests";
import { FolderGit2Icon, TerminalIcon } from "lucide-react";
import { useRender } from "@base-ui/react/use-render";
import { useMemo, type AnimationEvent, type MouseEvent, type ReactElement } from "react";
import {
useMemo,
type AnimationEvent,
type MouseEvent,
type ReactElement,
type ComponentProps,
} from "react";
import { cn } from "../lib/utils";
import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments";
import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
Expand Down Expand Up @@ -621,3 +628,83 @@ 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 normalizedQuery = query.trim().toLowerCase();
const matches = (candidate: Parameters<typeof threadPullRequestSearchTerms>[0]) =>
normalizedQuery.length > 0 &&
threadPullRequestSearchTerms(candidate).some((term) =>
term.toLowerCase().includes(normalizedQuery),
);
const matchedLink = visibleThreadPullRequests(thread.pullRequests).find((pr) =>
matches({ pullRequests: [pr] }),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 ? (
<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 @@ -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: [
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 @@ -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<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;
}
Loading