diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index 02f9186203..17d563cb37 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -697,9 +697,12 @@ const ChatViewContent = memo(function ChatViewContent({ // memo'd Thread/ThreadMessageList, and a fresh element per render would // defeat their bail-out. Stats changes re-render inside SessionUsageFooter // via its own useStore($sessions) subscription, not through these props. + // The footer needs a STORED id ($sessions rows key off it): activeSessionId + // is the runtime id, so fall back through the routed id for fresh resumes. + const footerSessionId = selectedSessionId || routedSessionId const threadFooter = useMemo( - () => (activeSessionId ? : undefined), - [activeSessionId] + () => (footerSessionId ? : undefined), + [footerSessionId] ) return ( diff --git a/apps/desktop/src/app/chat/session-usage-footer.test.tsx b/apps/desktop/src/app/chat/session-usage-footer.test.tsx index 70f3ef0592..b95680d584 100644 --- a/apps/desktop/src/app/chat/session-usage-footer.test.tsx +++ b/apps/desktop/src/app/chat/session-usage-footer.test.tsx @@ -58,6 +58,16 @@ describe('SessionUsageFooter', () => { expect(container.querySelector('[data-slot="session-usage-footer"]')).toBeNull() }) + it('matches a live lineage id to the stored row, like the header does', () => { + // Auto-compression rotates the session id: the view holds a chain tip while + // the list row still carries its own id plus the lineage ids. + setSessions([makeSessionInfo({ _lineage_ids: ['root', 'tip'], id: 'root', input_tokens: 2_000, output_tokens: 400 })]) + + const { container } = render() + + expect(container.querySelector('[data-slot="session-usage-footer"]')?.textContent).toContain('2.4k tok') + }) + it('re-renders when the sessions atom refreshes after a turn', () => { setSessions([makeSessionInfo({ id: 's1', input_tokens: 1_000, output_tokens: 200 })]) diff --git a/apps/desktop/src/app/chat/session-usage-footer.tsx b/apps/desktop/src/app/chat/session-usage-footer.tsx index 1543eb49bd..9fde23a6af 100644 --- a/apps/desktop/src/app/chat/session-usage-footer.tsx +++ b/apps/desktop/src/app/chat/session-usage-footer.tsx @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react' import { useI18n } from '@/i18n' import { compactNumber } from '@/lib/format' -import { $sessions } from '@/store/session' +import { $sessions, sessionMatchesStoredId } from '@/store/session' import { sessionCostUsd } from '@/store/sidebar-archive' /** Cumulative token/cost totals for one session, rendered at the end of the @@ -13,7 +13,9 @@ import { sessionCostUsd } from '@/store/sidebar-archive' export function SessionUsageFooter({ sessionId }: { sessionId: string }) { const { t } = useI18n() const sessions = useStore($sessions) - const session = sessions.find(candidate => candidate.id === sessionId) + // Match like ChatHeader does: a compression chain hands the live view a + // lineage id, not necessarily the row's own id. + const session = sessions.find(candidate => sessionMatchesStoredId(candidate, sessionId)) if (!session) { return null