refactor(search): session sidebar and project-context filters use the shared matcher
Sidebar session/folder/group search and the Todos, Memory, Plans and Notes filters required the whole query as one literal substring; they now match tokens in any order and ignore punctuation via matchesRankQuery, keeping their own list order and tree structure.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
@@ -186,13 +187,10 @@ export const MemorySection: React.FC<{
|
|||||||
};
|
};
|
||||||
}, [markViewed, viewKey]);
|
}, [markViewed, viewKey]);
|
||||||
|
|
||||||
const visibleEntries = React.useMemo(() => {
|
const visibleEntries = React.useMemo(
|
||||||
const needle = query.trim().toLowerCase();
|
() => entries.filter((entry) => matchesRankQuery([entry.title, entry.body], query)),
|
||||||
if (!needle) return entries;
|
[entries, query],
|
||||||
return entries.filter((entry) => (
|
);
|
||||||
entry.title.toLowerCase().includes(needle) || entry.body.toLowerCase().includes(needle)
|
|
||||||
));
|
|
||||||
}, [entries, query]);
|
|
||||||
|
|
||||||
const handleDelete = React.useCallback(async (memoryId: string) => {
|
const handleDelete = React.useCallback(async (memoryId: string) => {
|
||||||
if (!await deleteEntry(scope, memoryId)) {
|
if (!await deleteEntry(scope, memoryId)) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
@@ -178,11 +179,10 @@ export const NotesSection: React.FC<{
|
|||||||
const saveNoteBody = useProjectContextStore((state) => state.saveNoteBody);
|
const saveNoteBody = useProjectContextStore((state) => state.saveNoteBody);
|
||||||
const deleteNote = useProjectContextStore((state) => state.deleteNote);
|
const deleteNote = useProjectContextStore((state) => state.deleteNote);
|
||||||
|
|
||||||
const visibleNotes = React.useMemo(() => {
|
const visibleNotes = React.useMemo(
|
||||||
const needle = query.trim().toLowerCase();
|
() => notes.filter((note) => matchesRankQuery([note.body], query)),
|
||||||
if (!needle) return notes;
|
[notes, query],
|
||||||
return notes.filter((note) => note.body.toLowerCase().includes(needle));
|
);
|
||||||
}, [notes, query]);
|
|
||||||
|
|
||||||
// The store keeps the failure reason; without passing it through, every
|
// The store keeps the failure reason; without passing it through, every
|
||||||
// failure looks identical to the user and tells them nothing about the cause.
|
// failure looks identical to the user and tells them nothing about the cause.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
@@ -146,11 +147,10 @@ export const PlansSection: React.FC<{
|
|||||||
[onTogglePinned, projectRef, t]
|
[onTogglePinned, projectRef, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const visiblePlans = React.useMemo(() => {
|
const visiblePlans = React.useMemo(
|
||||||
const needle = query.trim().toLowerCase();
|
() => plans.filter((plan) => matchesRankQuery([plan.title], query)),
|
||||||
if (!needle) return plans;
|
[plans, query],
|
||||||
return plans.filter((plan) => plan.title.toLowerCase().includes(needle));
|
);
|
||||||
}, [plans, query]);
|
|
||||||
|
|
||||||
const handleOpenPlan = React.useCallback(
|
const handleOpenPlan = React.useCallback(
|
||||||
(plan: ProjectPlanLink) => {
|
(plan: ProjectPlanLink) => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -183,11 +184,10 @@ export const TodosSection: React.FC<{
|
|||||||
const completedTodoCount = todos.reduce((count, todo) => count + (todo.completed ? 1 : 0), 0);
|
const completedTodoCount = todos.reduce((count, todo) => count + (todo.completed ? 1 : 0), 0);
|
||||||
// Filtering is display-only: every handler above still edits the full list,
|
// Filtering is display-only: every handler above still edits the full list,
|
||||||
// so reordering or clearing while a filter is active cannot drop hidden items.
|
// so reordering or clearing while a filter is active cannot drop hidden items.
|
||||||
const visibleTodos = React.useMemo(() => {
|
const visibleTodos = React.useMemo(
|
||||||
const needle = query.trim().toLowerCase();
|
() => todos.filter((todo) => matchesRankQuery([todo.text], query)),
|
||||||
if (!needle) return todos;
|
[query, todos],
|
||||||
return todos.filter((todo) => todo.text.toLowerCase().includes(needle));
|
);
|
||||||
}, [query, todos]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
import type { Session } from '@opencode-ai/sdk/v2';
|
||||||
@@ -483,7 +484,7 @@ function SessionGroupSectionBase(props: Props): React.ReactNode {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderMatches = entry.folder.name.toLowerCase().includes(normalizedSessionSearchQuery);
|
const folderMatches = matchesRankQuery([entry.folder.name], normalizedSessionSearchQuery);
|
||||||
if (folderMatches || entry.nodes.length > 0) {
|
if (folderMatches || entry.nodes.length > 0) {
|
||||||
keepByFolderId.set(folderId, true);
|
keepByFolderId.set(folderId, true);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
import type { Session } from '@opencode-ai/sdk/v2';
|
||||||
import type { WorktreeMetadata } from '@/types/worktree';
|
import type { WorktreeMetadata } from '@/types/worktree';
|
||||||
@@ -44,7 +45,7 @@ export const useSessionGrouping = (args: Args) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nodes.flatMap((node) => {
|
return nodes.flatMap((node) => {
|
||||||
const nodeMatches = buildSessionSearchText(node.session).includes(query);
|
const nodeMatches = matchesRankQuery([buildSessionSearchText(node.session)], query);
|
||||||
if (nodeMatches) {
|
if (nodeMatches) {
|
||||||
return [node];
|
return [node];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { matchesRankQuery } from '@/lib/search/fuzzySearch';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
import type { Session } from '@opencode-ai/sdk/v2';
|
||||||
import type { SessionGroup, SessionNode, GroupSearchData } from '../types';
|
import type { SessionGroup, SessionNode, GroupSearchData } from '../types';
|
||||||
@@ -161,10 +162,10 @@ export const useSessionSidebarSections = (args: Args) => {
|
|||||||
section.groups.forEach((group) => {
|
section.groups.forEach((group) => {
|
||||||
const filteredNodes = filterSessionNodesForSearch(group.sessions, normalizedSessionSearchQuery);
|
const filteredNodes = filterSessionNodesForSearch(group.sessions, normalizedSessionSearchQuery);
|
||||||
const matchedSessionCount = countNodes(filteredNodes);
|
const matchedSessionCount = countNodes(filteredNodes);
|
||||||
const groupMatches = buildGroupSearchText(group).includes(normalizedSessionSearchQuery);
|
const groupMatches = matchesRankQuery([buildGroupSearchText(group)], normalizedSessionSearchQuery);
|
||||||
const scopeKey = normalizePath(group.directory ?? null);
|
const scopeKey = normalizePath(group.directory ?? null);
|
||||||
const scopeFolders = scopeKey ? (foldersMap[scopeKey] ?? []) : [];
|
const scopeFolders = scopeKey ? (foldersMap[scopeKey] ?? []) : [];
|
||||||
const folderNameMatchCount = scopeFolders.filter((folder) => folder.name.toLowerCase().includes(normalizedSessionSearchQuery)).length;
|
const folderNameMatchCount = scopeFolders.filter((folder) => matchesRankQuery([folder.name], normalizedSessionSearchQuery)).length;
|
||||||
|
|
||||||
result.set(group, {
|
result.set(group, {
|
||||||
filteredNodes,
|
filteredNodes,
|
||||||
|
|||||||
Reference in New Issue
Block a user