Skip to content
Merged
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
4 changes: 4 additions & 0 deletions apps/mobile/src/features/home/homeThreadList.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
import {
buildProjectGroups,
derivePhysicalProjectKey,
Expand Down Expand Up @@ -311,6 +312,9 @@ export function buildHomeThreadGroups(input: {
: group.threads.filter(
(thread) =>
thread.title.toLocaleLowerCase().includes(query) ||
threadPullRequestSearchTerms(thread).some((term) =>
term.toLocaleLowerCase().includes(query),
) ||
input.matchedThreadKeys?.has(
threadSearchMatchKey({
environmentId: thread.environmentId,
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/src/features/threads/threadListV2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
import {
effectiveSnoozed,
hasQueuedTurnStart,
Expand Down Expand Up @@ -400,6 +401,9 @@ export function buildThreadListV2Items(input: {
if (
query.length > 0 &&
!thread.title.toLocaleLowerCase().includes(query) &&
!threadPullRequestSearchTerms(thread).some((term) =>
term.toLocaleLowerCase().includes(query),
) &&
input.matchedThreadKeys?.has(
threadSearchMatchKey({
environmentId: thread.environmentId,
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/components/CommandPalette.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,3 +508,45 @@ describe("filterPinnedBrowseEntries", () => {
});
});
});

it.each([
"#10839",
"10839",
"pingdotgg/t3code#10839",
"https://github.com/pingdotgg/t3code/pull/10839",
])("finds linked threads from PR query %s", (query) => {
const items = buildThreadActionItems({
threads: [
makeThread({
title: "Implementation",
pullRequests: [
{
host: "github.com",
repository: "pingdotgg/t3code",
number: 10839,
url: "https://github.com/pingdotgg/t3code/pull/10839",
source: "manual",
linkedAt: "2026-09-08T00:00:00Z",
snapshot: null,
stack: null,
},
],
}),
makeThread({ id: ThreadId.make("unrelated"), title: "Other work" }),
],
projectTitleById: new Map(),
sortOrder: "updated_at",
icon: null,
runThread: async () => undefined,
});
const groups = filterCommandPaletteGroups({
activeGroups: [],
query,
isInSubmenu: false,
projectSearchItems: [],
threadSearchItems: items,
});
expect(groups.flatMap((group) => group.items.map((item) => item.title))).toEqual([
"Implementation",
]);
});
3 changes: 3 additions & 0 deletions apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
import {
type FilesystemBrowseEntry,
type KeybindingCommand,
Expand Down Expand Up @@ -186,6 +187,7 @@ export type BuildThreadActionItemsThread = Pick<
| "title"
| "worktreePath"
> & {
pullRequests?: SidebarThreadSummary["pullRequests"];
updatedAt: string;
latestUserMessageAt?: string | null;
};
Expand Down Expand Up @@ -240,6 +242,7 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr
value: `thread:${thread.id}`,
searchTerms: [
thread.title,
...threadPullRequestSearchTerms(thread),
projectTitle ?? ``,
thread.branch ?? ``,
contentMatch?.snippet ?? ``,
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
resolveSidebarThreadStatus,
resolveThreadStatusPill,
resolveWorkingStartedAt,
searchSidebarThreadsByTitle,
searchSidebarThreads,
formatWorkingDurationLabel,
shouldClearThreadSelectionOnMouseDown,
shouldRecedeSidebarThread,
Expand Down Expand Up @@ -802,23 +802,23 @@ describe("resolveSidebarThreadStatus", () => {
});
});

describe("searchSidebarThreadsByTitle", () => {
describe("searchSidebarThreads", () => {
const threads = [
{ id: "thread-1", title: "Fix workspace search", project: "Alpha" },
{ id: "thread-2", title: "Review providers", project: "Workspace" },
{ id: "thread-3", title: "WORKTREE cleanup", project: "Beta" },
];

it("matches thread titles case-insensitively and preserves their order", () => {
expect(searchSidebarThreadsByTitle(threads, "work")).toEqual([threads[0], threads[2]]);
expect(searchSidebarThreads(threads, "work")).toEqual([threads[0], threads[2]]);
});

it("does not match project metadata", () => {
expect(searchSidebarThreadsByTitle(threads, "workspace")).toEqual([threads[0]]);
expect(searchSidebarThreads(threads, "workspace")).toEqual([threads[0]]);
});

it("returns no results for an empty query", () => {
expect(searchSidebarThreadsByTitle(threads, " ")).toEqual([]);
expect(searchSidebarThreads(threads, " ")).toEqual([]);
});
});

Expand Down
16 changes: 10 additions & 6 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests";
import * as React from "react";
import { defaultAnimateLayoutChanges, type AnimateLayoutChanges } from "@dnd-kit/sortable";
import {
Expand Down Expand Up @@ -875,17 +876,20 @@ export { pinOrderKeyBetween, planPinnedReorder } from "@t3tools/client-runtime/s
export { sortPinnedThreadsByOrderKey as sortPinnedThreadsForSidebar } from "@t3tools/client-runtime/state/thread-sort";

/**
* Search the already-ordered sidebar thread collection by title only.
* Search the already-ordered sidebar thread collection by title or linked PR.
* Keeping the input order means lifecycle ordering (active, snoozed, settled)
* remains stable while the user narrows the list.
*/
export function searchSidebarThreadsByTitle<T extends { readonly title: string }>(
threads: readonly T[],
query: string,
): T[] {
export function searchSidebarThreads<
T extends { readonly title: string } & Parameters<typeof threadPullRequestSearchTerms>[0],
>(threads: readonly T[], query: string): T[] {
const normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.length === 0) return [];
return threads.filter((thread) => thread.title.toLowerCase().includes(normalizedQuery));
return threads.filter((thread) =>
[thread.title, ...threadPullRequestSearchTerms(thread)].some((term) =>
term.toLowerCase().includes(normalizedQuery),
),
);
}

export function filterSidebarProjectScopeItems<TItem extends { readonly value: string }>(input: {
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ import {
resolveSidebarDropVerb,
type SidebarDropVerb,
resolveSidebarThreadStatus,
searchSidebarThreadsByTitle,
searchSidebarThreads,
shouldCreateNewThreadInCurrentProject,
shouldRecedeSidebarThread,
resolveWorkingStartedAt,
Expand Down Expand Up @@ -2675,7 +2675,7 @@ export default function Sidebar() {
[activeThreads, pinnedThreads, settledThreads, snoozedThreads],
);
const threadSearchResults = useMemo(
() => searchSidebarThreadsByTitle(searchableThreads, threadSearchQuery),
() => searchSidebarThreads(searchableThreads, threadSearchQuery),
[searchableThreads, threadSearchQuery],
);
const threadSearchResultOrderKey = threadSearchResults
Expand Down Expand Up @@ -4407,7 +4407,7 @@ export default function Sidebar() {
setActiveSearchResultIndex(0);
}}
onKeyDown={handleThreadSearchKeyDown}
placeholder="Search"
placeholder="Search threads or PRs"
aria-label="Search threads"
role="combobox"
aria-autocomplete="list"
Expand Down
38 changes: 37 additions & 1 deletion packages/shared/src/threadPullRequests.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { ThreadPullRequestLink, ThreadPullRequestSnapshot } from "@t3tools/contracts";
import {
ProjectId,
type ThreadPullRequestLink,
type ThreadPullRequestSnapshot,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import {
legacyLinkedPullRequestOf,
legacyThreadPullRequestKey,
threadPullRequestSearchTerms,
resolveThreadCurrentPullRequest,
resolveThreadPullRequestChains,
resolveThreadPullRequestBadge,
Expand Down Expand Up @@ -349,3 +354,34 @@ describe("chain selection and badge state", () => {
);
});
});

describe("threadPullRequestSearchTerms", () => {
it("includes completed and unsynced links but excludes dismissed links", () => {
const terms = threadPullRequestSearchTerms({
pullRequests: [
link(12, { snapshot: snapshot({ title: "Fix login", state: "merged" }) }),
link(34),
link(56, { source: "stack-dismissed" }),
],
});
expect(terms).toContain("#12");
expect(terms).toContain("pingdotgg/t3code#12");
expect(terms).toContain("https://github.com/pingdotgg/t3code/pull/12");
expect(terms).toContain("Fix login");
expect(terms).toContain("#34");
expect(terms.join(" ")).not.toContain("56");
});
});

it("searches the legacy projection when old environments decode to an empty links list", () => {
const linkedPullRequest = {
projectId: ProjectId.make("project"),
repository: "pingdotgg/t3code",
number: 12,
url: "https://github.com/pingdotgg/t3code/pull/12",
};
expect(threadPullRequestSearchTerms({ pullRequests: [], linkedPullRequest })).toContain("#12");
expect(
threadPullRequestSearchTerms({ pullRequests: [link(34)], linkedPullRequest }),
).not.toContain("#12");
});
17 changes: 17 additions & 0 deletions packages/shared/src/threadPullRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,20 @@ export function resolveThreadPullRequestBadge(
}
return { kind: "pull-request", others: visible.length - 1 };
}

/** Search terms for visible PR links, including the legacy single-link projection. */
export function threadPullRequestSearchTerms(thread: {
readonly pullRequests?: ReadonlyArray<ThreadPullRequestLink> | undefined;
readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;
}): string[] {
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 ?? "",
]);
}
const legacy = thread.linkedPullRequest;
return legacy ? [`#${legacy.number}`, `${legacy.repository}#${legacy.number}`, legacy.url] : [];
}
Loading