diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx
index 45db6caa9c7..39b929732b2 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.tsx
@@ -7,56 +7,13 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger
} from '@/components/ui/dropdown-menu'
-import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import {
- AI_VAULT_SESSION_LIMIT_STEP,
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,
- loading,
- sessionLimit,
- onSessionLimitChange
-}: {
- loaded: number
- loading: boolean
- sessionLimit: AiVaultSessionLimit
- onSessionLimitChange: (limit: AiVaultSessionLimit) => void
-}): React.JSX.Element | null {
- const next = nextAiVaultSessionLimit(sessionLimit)
- if (next === null || sessionLimit === 'unlimited' || loaded === 0) {
- return null
- }
- // Why: a step raises the depth before the rescan lands, so the row must survive
- // that gap in a loading state instead of vanishing until the new rows paint.
- const filled = loaded >= sessionLimit
- const stepping = loading && loaded >= sessionLimit - AI_VAULT_SESSION_LIMIT_STEP
- if (!filled && !stepping) {
- return null
- }
- return (
-
-
-
- )
-}
-
export function AiVaultSessionLimitMenu({
sessionLimit,
onSessionLimitChange
diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.test.tsx b/src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx
similarity index 59%
rename from src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.test.tsx
rename to src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx
index 11cbfd8d05c..105c021d5d2 100644
--- a/src/renderer/src/components/right-sidebar/AiVaultSessionLimitMenu.test.tsx
+++ b/src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.test.tsx
@@ -2,7 +2,7 @@
import { cleanup, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, expect, it, vi } from 'vitest'
-import { AiVaultShowMoreSessionsRow } from './AiVaultSessionLimitMenu'
+import { AiVaultShowMoreSessionsRow } from './AiVaultShowMoreSessionsRow'
afterEach(cleanup)
@@ -11,6 +11,7 @@ it('steps the history depth up by one page once the scan filled it', async () =>
render(
{
render(
{
expect(button.hasAttribute('disabled')).toBe(true)
})
+// Why: the old rule inferred "stepping" from the selected depth minus a page, which at the
+// default depth is zero, so every foreground rescan claimed more history was coming.
+it('stays hidden during a foreground rescan the scan had room for', () => {
+ render(
+
+ )
+ expect(screen.queryByRole('button')).toBeNull()
+})
+
it('stays hidden while the scan has room or is already unlimited', () => {
render(
{
render(
{
)
expect(screen.queryByRole('button')).toBeNull()
})
+
+it('stays hidden until the first scan reports the depth it ran at', () => {
+ render(
+
+ )
+ expect(screen.queryByRole('button')).toBeNull()
+})
diff --git a/src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.tsx
new file mode 100644
index 00000000000..c994fa30af6
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/AiVaultShowMoreSessionsRow.tsx
@@ -0,0 +1,45 @@
+import { Button } from '@/components/ui/button'
+import { translate } from '@/i18n/i18n'
+import { AI_VAULT_SESSION_LIMIT_STEP, 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,
+ loadedSessionLimit,
+ loading,
+ sessionLimit,
+ onSessionLimitChange
+}: {
+ loaded: number
+ /** The depth those rows came from: still the old one while a deeper rescan runs. */
+ loadedSessionLimit: AiVaultSessionLimit | null
+ loading: boolean
+ sessionLimit: AiVaultSessionLimit
+ onSessionLimitChange: (limit: AiVaultSessionLimit) => void
+}): React.JSX.Element | null {
+ if (sessionLimit === 'unlimited' || loaded === 0) {
+ return null
+ }
+ if (
+ loadedSessionLimit === null ||
+ loadedSessionLimit === 'unlimited' ||
+ loaded < loadedSessionLimit
+ ) {
+ return null
+ }
+ return (
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-applied-scan.ts b/src/renderer/src/components/right-sidebar/ai-vault-applied-scan.ts
new file mode 100644
index 00000000000..4702b924b39
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/ai-vault-applied-scan.ts
@@ -0,0 +1,27 @@
+import { useCallback, useState } from 'react'
+import type { AiVaultListResult } from '../../../../shared/ai-vault-types'
+import { applyPublishedAiVaultList } from './ai-vault-session-identity'
+import type { AiVaultSessionLimit } from './ai-vault-session-limit'
+
+// One object so a session count is never paired with a depth its scan never ran at.
+export type AiVaultAppliedScan = { result: AiVaultListResult; limit: AiVaultSessionLimit }
+
+/** The scan the panel is showing, together with the History depth it ran at. */
+export function useAppliedAiVaultScan(): {
+ scan: AiVaultAppliedScan | null
+ applyScan: (published: AiVaultListResult, limit: AiVaultSessionLimit) => void
+} {
+ const [scan, setScan] = useState(null)
+ // Identity-preserving like the plain setter was, so an unchanged republish still bails out.
+ const applyScan = useCallback((published: AiVaultListResult, limit: AiVaultSessionLimit) => {
+ applyPublishedAiVaultList(published, (update) =>
+ setScan((prev) => {
+ const result = update(prev?.result ?? null)
+ return prev !== null && prev.result === result && prev.limit === limit
+ ? prev
+ : { result, limit }
+ })
+ )
+ }, [])
+ return { scan, applyScan }
+}
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 b874ad6835f..7cc212114a2 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
@@ -14,8 +14,3 @@ export function normalizeAiVaultSessionLimit(value: unknown): AiVaultSessionLimi
? value
: DEFAULT_AI_VAULT_SESSION_LIMIT
}
-
-/** The next History depth, or null once the scan is already unlimited. */
-export function nextAiVaultSessionLimit(limit: AiVaultSessionLimit): number | null {
- return limit === 'unlimited' ? null : limit + AI_VAULT_SESSION_LIMIT_STEP
-}
diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts
index 389acac8579..b8787af49f9 100644
--- a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts
+++ b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts
@@ -13,7 +13,8 @@ import {
import { useAppStore } from '@/store'
import type { AiVaultSessionLimit } from './ai-vault-session-limit'
import { AiVaultSessionPublicationGate } from './ai-vault-session-publication-gate'
-import { applyPublishedAiVaultList, EMPTY_AI_VAULT_SESSIONS } from './ai-vault-session-identity'
+import { EMPTY_AI_VAULT_SESSIONS } from './ai-vault-session-identity'
+import { useAppliedAiVaultScan } from './ai-vault-applied-scan'
import {
aiVaultSessionResultCacheKey,
cacheAiVaultSessionResult,
@@ -84,8 +85,11 @@ export function useAiVaultSessionRefresh(
refresh: (args?: AiVaultRefreshArgs) => Promise
scanResult: AiVaultListResult | null
sessions: readonly AiVaultSession[]
+ /** The depth the sessions on screen came from, which trails the selected one during a rescan. */
+ loadedSessionLimit: AiVaultSessionLimit | null
} {
- const [scanResult, setScanResult] = useState(null)
+ const { scan, applyScan } = useAppliedAiVaultScan()
+ const scanResult = scan?.result ?? null
const sessions = scanResult?.sessions ?? EMPTY_AI_VAULT_SESSIONS
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
@@ -118,7 +122,6 @@ export function useAiVaultSessionRefresh(
)}\n${sessionLimitRef.current}`,
[]
)
-
const refresh = useCallback(
async (args: AiVaultRefreshArgs = {}): Promise => {
const hostScope = executionHostScopeRef.current
@@ -137,7 +140,7 @@ export function useAiVaultSessionRefresh(
lastAppliedScanRef.current = { scopeKey: scanKey, scannedAt: cachedResult.scannedAt }
setError(null)
publicationGateRef.current.publish(cachedResult, (published) => {
- applyPublishedAiVaultList(published, setScanResult)
+ applyScan(published, selectedLimit)
})
setLoading(false)
return
@@ -211,7 +214,7 @@ export function useAiVaultSessionRefresh(
})
publicationGateRef.current.publish(result, (published) => {
if (mountedRef.current && scanKey === currentScanScopeKey()) {
- applyPublishedAiVaultList(published, setScanResult)
+ applyScan(published, selectedLimit)
}
})
} catch (err) {
@@ -244,7 +247,7 @@ export function useAiVaultSessionRefresh(
// Deps intentionally avoid changing scope values: refresh reads them
// through refs and recurses on itself, so its identity must stay stable.
},
- [currentScanScopeKey]
+ [applyScan, currentScanScopeKey]
)
// Forced rescans triggered by new agent sessions run
@@ -359,5 +362,5 @@ export function useAiVaultSessionRefresh(
requestForcedRescan()
}, [agentSessionIdsKey, requestForcedRescan])
- return { error, loading, refresh, scanResult, sessions }
+ return { error, loading, refresh, scanResult, sessions, loadedSessionLimit: scan?.limit ?? null }
}