{
+ 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": "在恢复会话之前,请先打开一个工作区。",