| author | |
| committer | |
| log | b21c5456548f17190e7f3688f9883b98cbf5221d |
| tree | b35b0ab4dac1afa29cb5b5fa7b6c05ccfed42bfc |
| parent | 829af7b73a960d390a0b32becd37ad6483627aa4 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 56 insertions(+), 1 deletions(-)
apps/web/src/components/CommandPalette.logic.test.ts+48| ... | ... | @@ -440,6 +440,54 @@ describe("buildThreadActionItems", () => { |
| 440 | 440 | ]); |
| 441 | 441 | }); |
| 442 | 442 | |
| 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 | 491 | it("preserves thread project-name matches when there is no stronger title match", () => { |
| 444 | 492 | const group: CommandPaletteGroup = { |
| 445 | 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 | 11 | import * as Arr from "effect/Array"; |
| 12 | 12 | import * as Result from "effect/Result"; |
| 13 | 13 | import { type ReactNode } from "react"; |
| 14 | import { sortThreads } from "../lib/threadSort"; | |
| 14 | import { getThreadSortTimestamp, sortThreads } from "../lib/threadSort"; | |
| 15 | 15 | import { normalizeSearchText } from "../lib/utils"; |
| 16 | 16 | import { formatRelativeTimeLabel } from "../timestampFormat"; |
| 17 | 17 | import { type Project, type SidebarThreadSummary, type Thread } from "../types"; |
| ... | ... | @@ -135,6 +135,7 @@ export interface CommandPaletteItem { |
| 135 | 135 | readonly description?: ReactNode; |
| 136 | 136 | readonly threadContentMatch?: CommandPaletteThreadContentMatch; |
| 137 | 137 | readonly timestamp?: string; |
| 138 | readonly searchRecency?: number; | |
| 138 | 139 | readonly icon: ReactNode; |
| 139 | 140 | readonly disabled?: boolean; |
| 140 | 141 | /** Optional content rendered inline before the title text. */ |
| ... | ... | @@ -315,6 +316,7 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr |
| 315 | 316 | timestamp: formatRelativeTimeLabel( |
| 316 | 317 | thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt, |
| 317 | 318 | ), |
| 319 | searchRecency: getThreadSortTimestamp(thread, "updated_at"), | |
| 318 | 320 | icon: input.icon, |
| 319 | 321 | }, |
| 320 | 322 | leadingContent ? { titleLeadingContent: leadingContent } : {}, |
| ... | ... | @@ -366,6 +368,10 @@ function rankCommandPaletteItemMatch( |
| 366 | 368 | for (const [index, field] of terms.entries()) { |
| 367 | 369 | const fieldRank = rankSearchFieldMatch(field, normalizedQuery, queryTokens); |
| 368 | 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 | 375 | return 1_000 - index * 100 + fieldRank; |
| 370 | 376 | } |
| 371 | 377 | } |
| ... | ... | @@ -442,6 +448,7 @@ export function filterCommandPaletteGroups(input: { |
| 442 | 448 | (left, right) => |
| 443 | 449 | Number(left.item.secondary ?? false) - Number(right.item.secondary ?? false) || |
| 444 | 450 | right.rank - left.rank || |
| 451 | (right.item.searchRecency ?? 0) - (left.item.searchRecency ?? 0) || | |
| 445 | 452 | left.index - right.index, |
| 446 | 453 | ) |
| 447 | 454 | .map((entry) => entry.item); |