diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.performance.test.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.performance.test.tsx index 3e423f59..3151033c 100644 --- a/packages/ui/src/components/chat/MarkdownRendererImpl.performance.test.tsx +++ b/packages/ui/src/components/chat/MarkdownRendererImpl.performance.test.tsx @@ -445,6 +445,52 @@ describe('MarkdownRenderer DOM mount performance contract', () => { clearDetachedMarkdownDomCache(); }); + test('does not detach Markdown DOM that intersects the active selection', async () => { + clearDetachedMarkdownDomCache(); + const content = 'selected content'; + const host = document.createElement('div'); + document.body.replaceChildren(host); + const root = createRoot(host); + await act(async () => { + root.render( + , + ); + await waitForSettledEffects(); + }); + const markdown = host.querySelector('[data-markdown-content]'); + if (!markdown) throw new Error('Expected mounted Markdown content'); + const originalGetSelection = window.getSelection; + Object.defineProperty(window, 'getSelection', { + configurable: true, + value: () => ({ + rangeCount: 1, + isCollapsed: false, + getRangeAt: () => ({ intersectsNode: (node: Node) => node === markdown }), + }), + }); + + try { + await act(async () => root.unmount()); + expect(detachedMarkdownDomCacheStats().entries).toBe(0); + } finally { + Object.defineProperty(window, 'getSelection', { configurable: true, value: originalGetSelection }); + clearDetachedMarkdownDomCache(); + } + }); + test('defers and batches Mermaid controller initialization after Markdown mount', async () => { const mounted = await mountFixture(fixtureWorkload.rendererCount); const critical = mounted.counts; diff --git a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx index 545ccdff..456afa5f 100644 --- a/packages/ui/src/components/chat/MarkdownRendererImpl.tsx +++ b/packages/ui/src/components/chat/MarkdownRendererImpl.tsx @@ -843,6 +843,8 @@ const useMorphdomMarkdown = ({ if (target.childNodes.length === 0 || shouldRefreshMermaidViewers(target)) return; if (Array.from(target.children).some((block) => !block.hasAttribute('data-md-id'))) return; if (target.querySelector('[data-md-copy-pending]')) return; + const selection = window.getSelection(); + if (selection?.rangeCount && !selection.isCollapsed && selection.getRangeAt(0).intersectsNode(target)) return; const openMenu = target.querySelector('[data-md-menu]:not(.hidden)'); const copiedButton = Array.from(target.querySelectorAll('[data-md-action]')) .some((button) => button.getAttribute('title') === mountedDom.copiedLabel); diff --git a/packages/ui/src/components/session/sidebar/projects/SessionGroupSection.test.ts b/packages/ui/src/components/session/sidebar/projects/SessionGroupSection.test.ts index a3edd2d7..6641ff6c 100644 --- a/packages/ui/src/components/session/sidebar/projects/SessionGroupSection.test.ts +++ b/packages/ui/src/components/session/sidebar/projects/SessionGroupSection.test.ts @@ -55,4 +55,14 @@ describe('selectFolderIdsForProjection', () => { expect([...selectFolderIdsForProjection(folders, { archivedBucket: true, searchQuery: 'matching' })]) .toEqual(['root', 'child']); }); + + test('keeps a fuzzy folder match and its ancestor', () => { + const folders = [ + { id: 'root', name: 'Root', parentId: null, nodeCount: 0 }, + { id: 'child', name: 'Release Notes', parentId: 'root', nodeCount: 0 }, + ]; + + expect([...selectFolderIdsForProjection(folders, { archivedBucket: false, searchQuery: 'release-notes' })]) + .toEqual(['root', 'child']); + }); }); diff --git a/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts b/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts index 064681cb..f2c40e5c 100644 --- a/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts +++ b/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts @@ -1,4 +1,5 @@ import { getRuntimeKey } from '@/lib/runtime-switch'; +import { matchesRankQuery } from '@/lib/search/fuzzySearch'; import { normalizePath } from '@/lib/pathNormalization'; import { resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore'; import { getPinnedSessionKey } from '@/stores/useSessionPinnedStore'; @@ -227,7 +228,7 @@ export const selectFolderIdsForProjection = ( keep = (childIdsByParentId.get(folderId) ?? []).some(shouldKeep); } else { if (!keep && !options.searchQuery) keep = true; - if (!keep && (entry.nodeCount > 0 || entry.name.toLowerCase().includes(options.searchQuery))) keep = true; + if (!keep && (entry.nodeCount > 0 || matchesRankQuery([entry.name], options.searchQuery))) keep = true; if (!keep) keep = (childIdsByParentId.get(folderId) ?? []).some(shouldKeep); }