From 4793bd2ea202cbd983355060aaeaa69f2a89a37f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 21 Sep 2026 03:25:24 -0400 Subject: [PATCH] feat(session-search): count sessions plainly and offer Show more when the scan fills its depth --- .../AiVaultPanel.legacy-filter.test.tsx | 2 +- .../components/right-sidebar/AiVaultPanel.tsx | 28 ++++---- .../AiVaultSessionLimitMenu.test.tsx | 34 ++++++++++ .../right-sidebar/AiVaultSessionLimitMenu.tsx | 30 +++++++++ .../AiVaultSessionListBar.test.tsx | 43 ++++++------ .../right-sidebar/AiVaultSessionListBar.tsx | 52 +++++++------- .../right-sidebar/ai-vault-session-limit.ts | 5 ++ .../right-sidebar/ai-vault-sort-options.ts | 67 +++++++++++-------- src/renderer/src/i18n/locales/en.json | 8 ++- src/renderer/src/i18n/locales/es.json | 2 - src/renderer/src/i18n/locales/fr.json | 2 - src/renderer/src/i18n/locales/ja.json | 2 - src/renderer/src/i18n/locales/ko.json | 2 - src/renderer/src/i18n/locales/zh.json | 2 - 14 files changed, 175 insertions(+), 104 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.test.tsx diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx index 5a7bb3fb115..6b2411521f3 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.legacy-filter.test.tsx @@ -173,7 +173,7 @@ it('shows the whole history and no offer while the box is empty', async () => { expect(screen.getByText('Rename the bar widget')).toBeTruthy() expect(searchSessions).not.toHaveBeenCalled() // The same bar serves both modes; browsing it reports the history counts and its own sort. - expect(screen.getByText('2 shown · 2 recent')).toBeTruthy() + expect(screen.getByText('2 sessions')).toBeTruthy() expect(screen.getByRole('button', { name: 'Sort sessions: Last updated' })).toBeTruthy() expect(screen.queryByText('2 results')).toBeNull() expect(screen.queryByRole('button', { name: /^Sort results:/ })).toBeNull() diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx index 22c19c4c515..6b892f7a674 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx @@ -39,14 +39,10 @@ import { AiVaultPanelHeader } from './AiVaultPanelHeader' import { AiVaultResultCountLabel, AiVaultSessionListBar, - AiVaultShownCountLabel + AiVaultSessionCountLabel } from './AiVaultSessionListBar' -import { - aiVaultBrowseSortAriaLabel, - aiVaultBrowseSortOptions, - aiVaultSearchSortAriaLabel, - aiVaultSearchSortOptions -} from './ai-vault-sort-options' +import { aiVaultBrowseSortMenu, aiVaultSearchSortMenu } from './ai-vault-sort-options' +import { AiVaultShowMoreSessionsRow } from './AiVaultSessionLimitMenu' import { AiVaultSessionVirtualList } from './AiVaultSessionVirtualList' import { useAiVaultSessionRefresh } from './ai-vault-session-refresh' import { @@ -342,18 +338,19 @@ export default function AiVaultPanel(): React.JSX.Element { } value={searchSort} - options={aiVaultSearchSortOptions()} - sortAriaLabel={aiVaultSearchSortAriaLabel} + menu={aiVaultSearchSortMenu()} onChange={setSearchSort} /> ) : ( + } value={sort} - options={aiVaultBrowseSortOptions()} - sortAriaLabel={aiVaultBrowseSortAriaLabel} + menu={aiVaultBrowseSortMenu()} onChange={setSort} /> ))} @@ -408,6 +405,13 @@ export default function AiVaultPanel(): React.JSX.Element { onRequestDelete={(session) => void requestDelete(session)} /> )} + {!searching && ( + + )} {launchActions.continuationRequest && ( { + const onSessionLimitChange = vi.fn() + render( + + ) + await userEvent.setup().click(screen.getByRole('button', { name: 'Show more sessions' })) + expect(onSessionLimitChange).toHaveBeenCalledWith(500) +}) + +it('stays hidden while the scan has room or is already unlimited', () => { + render( + + ) + render( + + ) + expect(screen.queryByRole('button')).toBeNull() +}) diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx index fc242fbcddd..1b5b1b56ea2 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx @@ -7,13 +7,43 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/components/ui/dropdown-menu' +import { Button } from '@/components/ui/button' import { translate } from '@/i18n/i18n' import { AI_VAULT_SESSION_LIMITS, DEFAULT_AI_VAULT_SESSION_LIMIT, + nextAiVaultSessionLimit, type AiVaultSessionLimit } from './ai-vault-session-limit' +/** Footer row once the scan filled its History depth; steps the same setting the menu edits. */ +export function AiVaultShowMoreSessionsRow({ + loaded, + sessionLimit, + onSessionLimitChange +}: { + loaded: number + sessionLimit: AiVaultSessionLimit + onSessionLimitChange: (limit: AiVaultSessionLimit) => void +}): React.JSX.Element | null { + const next = nextAiVaultSessionLimit(sessionLimit) + if (next === null || typeof sessionLimit !== 'number' || loaded < sessionLimit) { + return null + } + return ( +
+ +
+ ) +} + export function AiVaultSessionLimitMenu({ sessionLimit, onSessionLimitChange diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx index f029a975cd6..3e18209665c 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.test.tsx @@ -2,16 +2,11 @@ import { cleanup, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { afterEach, expect, it, vi } from 'vitest' -import { - aiVaultBrowseSortAriaLabel, - aiVaultBrowseSortOptions, - aiVaultSearchSortAriaLabel, - aiVaultSearchSortOptions -} from './ai-vault-sort-options' +import { aiVaultBrowseSortMenu, aiVaultSearchSortMenu } from './ai-vault-sort-options' import { AiVaultResultCountLabel, AiVaultSessionListBar, - AiVaultShownCountLabel + AiVaultSessionCountLabel } from './AiVaultSessionListBar' afterEach(cleanup) @@ -21,8 +16,7 @@ it('reports how many hits are shown and which order produced them', () => { } value="relevance" - options={aiVaultSearchSortOptions()} - sortAriaLabel={aiVaultSearchSortAriaLabel} + menu={aiVaultSearchSortMenu()} onChange={vi.fn()} /> ) @@ -33,8 +27,7 @@ it('reports how many hits are shown and which order produced them', () => { } value="newest" - options={aiVaultSearchSortOptions()} - sortAriaLabel={aiVaultSearchSortAriaLabel} + menu={aiVaultSearchSortMenu()} onChange={vi.fn()} /> ) @@ -45,18 +38,28 @@ it('reports how many hits are shown and which order produced them', () => { it('reports how much of the browsed history is shown and its order', () => { render( } + label={} value="created" - options={aiVaultBrowseSortOptions()} - sortAriaLabel={aiVaultBrowseSortAriaLabel} + menu={aiVaultBrowseSortMenu()} onChange={vi.fn()} /> ) - expect(screen.getByText('4 shown · 12 recent')).toBeTruthy() - expect(screen.getByText('4 shown')).toBeTruthy() + expect(screen.getByText('4 of 12 sessions')).toBeTruthy() expect(screen.getByRole('button', { name: 'Sort sessions: Created' })).toBeTruthy() }) +it('counts plainly when filters hide nothing', () => { + render( + } + value="updated" + menu={aiVaultBrowseSortMenu()} + onChange={vi.fn()} + /> + ) + expect(screen.getByText('12 sessions')).toBeTruthy() +}) + it('hands the picked search order back to the caller', async () => { const onChange = vi.fn() const user = userEvent.setup({ pointerEventsCheck: 0 }) @@ -64,8 +67,7 @@ it('hands the picked search order back to the caller', async () => { } value="relevance" - options={aiVaultSearchSortOptions()} - sortAriaLabel={aiVaultSearchSortAriaLabel} + menu={aiVaultSearchSortMenu()} onChange={onChange} /> ) @@ -81,10 +83,9 @@ it('hands the picked browse order back to the caller', async () => { const user = userEvent.setup({ pointerEventsCheck: 0 }) render( } + label={} value="updated" - options={aiVaultBrowseSortOptions()} - sortAriaLabel={aiVaultBrowseSortAriaLabel} + menu={aiVaultBrowseSortMenu()} onChange={onChange} /> ) diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.tsx index e5b85860c20..30b3314a5e9 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionListBar.tsx @@ -10,7 +10,7 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { translate } from '@/i18n/i18n' -import type { AiVaultSortOption } from './ai-vault-sort-options' +import type { AiVaultSortMenu } from './ai-vault-sort-options' /** Left-hand label while searching: how many hits the list is showing. */ export function AiVaultResultCountLabel({ count }: { count: number }): React.JSX.Element { @@ -23,31 +23,29 @@ export function AiVaultResultCountLabel({ count }: { count: number }): React.JSX ) } -/** Left-hand label while browsing: how much of the scanned history the list is showing. */ -export function AiVaultShownCountLabel({ +/** Left-hand label while browsing: the count, and how much of the scan filters hid. */ +export function AiVaultSessionCountLabel({ shown, - recent + loaded }: { shown: number - recent: number + loaded: number }): React.JSX.Element { + if (shown !== loaded) { + return ( + <> + {translate('sessionSearch.panel.sessionsOfLoaded', '{{value0}} of {{value1}} sessions', { + value0: shown, + value1: loaded + })} + + ) + } return ( <> - {/* Why: below 300px the bar competes with the sort menu, so compact copy prevents overlap. */} - - {translate( - 'auto.components.right.sidebar.AiVaultPanel.shownRecent', - '{{value0}} shown · {{value1}} recent', - { value0: shown, value1: recent } - )} - - - {translate( - 'auto.components.right.sidebar.AiVaultPanel.sessionsShownCompact', - '{{value0}} shown', - { value0: shown } - )} - + {shown === 1 + ? translate('sessionSearch.panel.sessionsOne', '{{count}} session', { count: shown }) + : translate('sessionSearch.panel.sessionsOther', '{{count}} sessions', { count: shown })} ) } @@ -59,17 +57,15 @@ export function AiVaultShownCountLabel({ export function AiVaultSessionListBar({ label, value, - options, - sortAriaLabel, + menu, onChange }: { label: ReactNode value: Value - options: readonly AiVaultSortOption[] - sortAriaLabel: (selectedLabel: string) => string + menu: AiVaultSortMenu onChange: (value: Value) => void }): React.JSX.Element { - const selected = options.find((option) => option.value === value) + const selected = menu.options.find((option) => option.value === value) return (
@@ -81,7 +77,7 @@ export function AiVaultSessionListBar({ variant="ghost" size="xs" className="shrink-0" - aria-label={sortAriaLabel(selected?.label ?? '')} + aria-label={menu.ariaLabel(selected?.label ?? '')} > {selected?.label} @@ -92,13 +88,13 @@ export function AiVaultSessionListBar({ value={value} // Radix hands back a bare string; the option list is what narrows it. onValueChange={(next) => { - const picked = options.find((option) => option.value === next) + const picked = menu.options.find((option) => option.value === next) if (picked) { onChange(picked.value) } }} > - {options.map((option) => ( + {menu.options.map((option) => ( {option.label} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-limit.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-limit.ts index 8d857cd00d0..3a869b9062d 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-limit.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-limit.ts @@ -9,3 +9,8 @@ export function normalizeAiVaultSessionLimit(value: unknown): AiVaultSessionLimi ? (value as AiVaultSessionLimit) : DEFAULT_AI_VAULT_SESSION_LIMIT } + +/** The next History depth step, or null once the scan is already unlimited. */ +export function nextAiVaultSessionLimit(limit: AiVaultSessionLimit): AiVaultSessionLimit | null { + return AI_VAULT_SESSION_LIMITS[AI_VAULT_SESSION_LIMITS.indexOf(limit) + 1] ?? null +} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-sort-options.ts b/src/renderer/src/components/right-sidebar/ai-vault-sort-options.ts index 416061bc810..0bf9d9e48ef 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-sort-options.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-sort-options.ts @@ -3,37 +3,46 @@ import type { AiVaultSearchSort, AiVaultSort } from '../../../../shared/ai-vault export type AiVaultSortOption = { value: Value; label: string } -export function aiVaultBrowseSortOptions(): readonly AiVaultSortOption[] { - return [ - { - value: 'updated', - label: translate( - 'auto.components.right.sidebar.AiVaultPanelControls.lastUpdated', - 'Last updated' - ) - }, - { - value: 'created', - label: translate('auto.components.right.sidebar.AiVaultPanelControls.created', 'Created') - } - ] +/** One mode's sort menu: its choices, and the accessible name of the trigger showing one. */ +export type AiVaultSortMenu = { + options: readonly AiVaultSortOption[] + ariaLabel: (selectedLabel: string) => string } -export function aiVaultSearchSortOptions(): readonly AiVaultSortOption[] { - return [ - { value: 'relevance', label: translate('sessionSearch.panel.sortRelevance', 'Most relevant') }, - { value: 'newest', label: translate('sessionSearch.panel.sortNewest', 'Newest') } - ] +export function aiVaultBrowseSortMenu(): AiVaultSortMenu { + return { + options: [ + { + value: 'updated', + label: translate( + 'auto.components.right.sidebar.AiVaultPanelControls.lastUpdated', + 'Last updated' + ) + }, + { + value: 'created', + label: translate('auto.components.right.sidebar.AiVaultPanelControls.created', 'Created') + } + ], + ariaLabel: (selectedLabel) => + translate('sessionSearch.panel.sortSessionsAriaLabel', 'Sort sessions: {{value0}}', { + value0: selectedLabel + }) + } } -export function aiVaultBrowseSortAriaLabel(selectedLabel: string): string { - return translate('sessionSearch.panel.sortSessionsAriaLabel', 'Sort sessions: {{value0}}', { - value0: selectedLabel - }) -} - -export function aiVaultSearchSortAriaLabel(selectedLabel: string): string { - return translate('sessionSearch.panel.sortResultsAriaLabel', 'Sort results: {{value0}}', { - value0: selectedLabel - }) +export function aiVaultSearchSortMenu(): AiVaultSortMenu { + return { + options: [ + { + value: 'relevance', + label: translate('sessionSearch.panel.sortRelevance', 'Most relevant') + }, + { value: 'newest', label: translate('sessionSearch.panel.sortNewest', 'Newest') } + ], + ariaLabel: (selectedLabel) => + translate('sessionSearch.panel.sortResultsAriaLabel', 'Sort results: {{value0}}', { + value0: selectedLabel + }) + } } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 094299d2a58..ece5febd020 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -13201,8 +13201,6 @@ "agentSessionQueued": "{{value0}} session queued", "sessionHistory": "Agent Session History", "agents": "Agents", - "shownRecent": "{{value0}} shown · {{value1}} recent", - "sessionsShownCompact": "{{value0}} shown", "resumePastSessions": "Resume past sessions", "refreshSessionHistory": "Refresh Session History", "searchSessions": "Search sessions", @@ -18021,7 +18019,11 @@ "enable": "Enable", "retry": "Try again", "scopeUnknown": "This computer does not have this workspace or project. Switch the scope to All to search everything on it.", - "sortSessionsAriaLabel": "Sort sessions: {{value0}}" + "sortSessionsAriaLabel": "Sort sessions: {{value0}}", + "sessionsOne": "{{count}} session", + "sessionsOther": "{{count}} sessions", + "sessionsOfLoaded": "{{value0}} of {{value1}} sessions", + "showMoreSessions": "Show more sessions" } }, "sessionHistory": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 4d4df5925c2..e2231a6707c 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -11376,7 +11376,6 @@ "localWorkspacesOnly": "Reanudar desde el historial solo está disponible en espacios de trabajo locales.", "agentSessionQueued": "Sesión de {{value0}} en cola", "sessionHistory": "Historial de sesiones de agentes", - "shownRecent": "{{value0}} mostradas · {{value1}} recientes", "resumePastSessions": "Reanudar sesiones anteriores", "refreshSessionHistory": "Actualizar historial de sesiones", "searchSessions": "Buscar sesiones", @@ -11389,7 +11388,6 @@ "sessionId": "ID de sesión", "logPath": "Ruta del log", "agents": "Agentes", - "sessionsShownCompact": "{{value0}} mostradas", "originalPaneUnavailable": "El panel original ya no está disponible.", "worktreeUnavailable": "El worktree ya no está disponible.", "openSupportedWorkspace": "Abre un workspace antes de reanudar una sesión.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index d7281df89b2..9e4f221980a 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -12503,8 +12503,6 @@ "agentSessionQueued": "Session {{value0}} en file d'attente", "sessionHistory": "Historique des sessions d'agent", "agents": "Agents", - "shownRecent": "{{value0}} affichées · {{value1}} récentes", - "sessionsShownCompact": "{{value0}} affichées", "resumePastSessions": "Reprendre les sessions passées", "refreshSessionHistory": "Actualiser l'historique des sessions", "searchSessions": "Rechercher des sessions", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index c9ebcf53129..378fdc8bf75 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -11376,7 +11376,6 @@ "localWorkspacesOnly": "履歴からの再開はローカルワークスペースでのみ利用できます。", "agentSessionQueued": "{{value0}} 件のセッションが順番待ち", "sessionHistory": "Agent セッション履歴", - "shownRecent": "{{value0}}表示 · {{value1}}最近", "resumePastSessions": "過去のセッションを再開", "refreshSessionHistory": "セッション履歴を更新", "searchSessions": "セッションを検索", @@ -11389,7 +11388,6 @@ "sessionId": "セッション ID", "logPath": "ログパス", "agents": "Agent", - "sessionsShownCompact": "{{value0}} 件表示", "originalPaneUnavailable": "元のペインは利用できなくなりました。", "worktreeUnavailable": "ワークツリーは利用できなくなりました。", "openSupportedWorkspace": "セッションを再開する前に、ワークスペースを開いてください。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index f495de99b11..c05e94a0eb0 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -11418,7 +11418,6 @@ "localWorkspacesOnly": "기록에서 재개하기는 로컬 워크스페이스에서만 사용할 수 있습니다.", "agentSessionQueued": "{{value0}} 세션이 대기열에 추가됨", "sessionHistory": "Agent 세션 기록", - "shownRecent": "{{value0}}개 표시 · 최근 {{value1}}개", "resumePastSessions": "이전 세션 재개", "refreshSessionHistory": "세션 기록 새로고침", "searchSessions": "세션 검색", @@ -11431,7 +11430,6 @@ "sessionId": "세션 ID", "logPath": "로그 경로", "agents": "에이전트", - "sessionsShownCompact": "{{value0}} 표시됨", "originalPaneUnavailable": "원래 창은 더 이상 사용할 수 없습니다.", "worktreeUnavailable": "워크트리는 더 이상 사용할 수 없습니다.", "openSupportedWorkspace": "세션을 재개하기 전에 워크스페이스를 여세요.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 3ff613da7b7..7c6370ff46f 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -11456,7 +11456,6 @@ "localWorkspacesOnly": "从历史记录恢复仅适用于本地工作区。", "agentSessionQueued": "{{value0}} 会话已排队", "sessionHistory": "智能体会话历史", - "shownRecent": "已显示 {{value0}} 项 · 最近 {{value1}} 项", "resumePastSessions": "恢复过往会话", "refreshSessionHistory": "刷新会话历史", "searchSessions": "搜索会话", @@ -11469,7 +11468,6 @@ "sessionId": "会话 ID", "logPath": "日志路径", "agents": "智能体", - "sessionsShownCompact": "已显示 {{value0}} 个", "originalPaneUnavailable": "原始面板不再可用。", "worktreeUnavailable": "工作树不再可用。", "openSupportedWorkspace": "在恢复会话之前,请先打开一个工作区。",