authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-23 01:25:41-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-23 01:25:41-05:00
logb21c5456548f17190e7f3688f9883b98cbf5221d
treeb35b0ab4dac1afa29cb5b5fa7b6c05ccfed42bfc
parent829af7b73a960d390a0b32becd37ad6483627aa4
signature Signed by PGP key B5690EEEBB952194

fix(web): sort title matches by recent activity (#13219)


2 files changed, 56 insertions(+), 1 deletions(-)

apps/web/src/components/CommandPalette.logic.test.ts+48
...@@ -440,6 +440,54 @@ describe("buildThreadActionItems", () => {...@@ -440,6 +440,54 @@ describe("buildThreadActionItems", () => {
440 ]);440 ]);
441 });441 });
442442
443 it("orders title matches by recent activity before older prefix matches", () => {
444 const threads = [
445 makeThread({
446 id: ThreadId.make("old-prefix"),
447 title: "Convex InvalidCursor in Convex threads query",
448 createdAt: "2026-01-01T00:00:00.000Z",
449 updatedAt: "2026-01-02T00:00:00.000Z",
450 }),
451 makeThread({
452 id: ThreadId.make("recent-title"),
453 title: "Disable Convex schema validation",
454 createdAt: "2025-12-01T00:00:00.000Z",
455 updatedAt: "2026-03-24T00:00:00.000Z",
456 }),
457 makeThread({
458 id: ThreadId.make("recent-content"),
459 title: "Fix schema validation",
460 createdAt: "2026-03-25T00:00:00.000Z",
461 updatedAt: "2026-03-25T00:00:00.000Z",
462 }),
463 ];
464 const items = buildThreadActionItems({
465 threads,
466 projectTitleById: new Map([[PROJECT_ID, "T3 Code"]]),
467 sortOrder: "created_at",
468 icon: null,
469 getContentMatch: (thread) =>
470 thread.id === ThreadId.make("recent-content")
471 ? { source: "user", snippet: "Please check Convex", query: "convex" }
472 : undefined,
473 runThread: async () => undefined,
474 });
475
476 const groups = filterCommandPaletteGroups({
477 activeGroups: [],
478 query: "convex",
479 isInSubmenu: false,
480 projectSearchItems: [],
481 threadSearchItems: items,
482 });
483
484 expect(groups[0]?.items.map((item) => item.value)).toEqual([
485 "thread:recent-title",
486 "thread:old-prefix",
487 "thread:recent-content",
488 ]);
489 });
490
443 it("preserves thread project-name matches when there is no stronger title match", () => {491 it("preserves thread project-name matches when there is no stronger title match", () => {
444 const group: CommandPaletteGroup = {492 const group: CommandPaletteGroup = {
445 value: "threads-search",493 value: "threads-search",
apps/web/src/components/CommandPalette.logic.ts+8-1
...@@ -11,7 +11,7 @@ import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";...@@ -11,7 +11,7 @@ import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
11import * as Arr from "effect/Array";11import * as Arr from "effect/Array";
12import * as Result from "effect/Result";12import * as Result from "effect/Result";
13import { type ReactNode } from "react";13import { type ReactNode } from "react";
14import { sortThreads } from "../lib/threadSort";14import { getThreadSortTimestamp, sortThreads } from "../lib/threadSort";
15import { normalizeSearchText } from "../lib/utils";15import { normalizeSearchText } from "../lib/utils";
16import { formatRelativeTimeLabel } from "../timestampFormat";16import { formatRelativeTimeLabel } from "../timestampFormat";
17import { type Project, type SidebarThreadSummary, type Thread } from "../types";17import { type Project, type SidebarThreadSummary, type Thread } from "../types";
...@@ -135,6 +135,7 @@ export interface CommandPaletteItem {...@@ -135,6 +135,7 @@ export interface CommandPaletteItem {
135 readonly description?: ReactNode;135 readonly description?: ReactNode;
136 readonly threadContentMatch?: CommandPaletteThreadContentMatch;136 readonly threadContentMatch?: CommandPaletteThreadContentMatch;
137 readonly timestamp?: string;137 readonly timestamp?: string;
138 readonly searchRecency?: number;
138 readonly icon: ReactNode;139 readonly icon: ReactNode;
139 readonly disabled?: boolean;140 readonly disabled?: boolean;
140 /** Optional content rendered inline before the title text. */141 /** Optional content rendered inline before the title text. */
...@@ -315,6 +316,7 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr...@@ -315,6 +316,7 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr
315 timestamp: formatRelativeTimeLabel(316 timestamp: formatRelativeTimeLabel(
316 thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,317 thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,
317 ),318 ),
319 searchRecency: getThreadSortTimestamp(thread, "updated_at"),
318 icon: input.icon,320 icon: input.icon,
319 },321 },
320 leadingContent ? { titleLeadingContent: leadingContent } : {},322 leadingContent ? { titleLeadingContent: leadingContent } : {},
...@@ -366,6 +368,10 @@ function rankCommandPaletteItemMatch(...@@ -366,6 +368,10 @@ function rankCommandPaletteItemMatch(
366 for (const [index, field] of terms.entries()) {368 for (const [index, field] of terms.entries()) {
367 const fieldRank = rankSearchFieldMatch(field, normalizedQuery, queryTokens);369 const fieldRank = rankSearchFieldMatch(field, normalizedQuery, queryTokens);
368 if (fieldRank !== Number.NEGATIVE_INFINITY) {370 if (fieldRank !== Number.NEGATIVE_INFINITY) {
371 if (index === 0 && item.searchRecency !== undefined) {
372 // All non-exact thread title matches share a tier so recency breaks the tie.
373 return 1_000 + Number(fieldRank === 3);
374 }
369 return 1_000 - index * 100 + fieldRank;375 return 1_000 - index * 100 + fieldRank;
370 }376 }
371 }377 }
...@@ -442,6 +448,7 @@ export function filterCommandPaletteGroups(input: {...@@ -442,6 +448,7 @@ export function filterCommandPaletteGroups(input: {
442 (left, right) =>448 (left, right) =>
443 Number(left.item.secondary ?? false) - Number(right.item.secondary ?? false) ||449 Number(left.item.secondary ?? false) - Number(right.item.secondary ?? false) ||
444 right.rank - left.rank ||450 right.rank - left.rank ||
451 (right.item.searchRecency ?? 0) - (left.item.searchRecency ?? 0) ||
445 left.index - right.index,452 left.index - right.index,
446 )453 )
447 .map((entry) => entry.item);454 .map((entry) => entry.item);