feat(desktop): render session usage footer at transcript end

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m4
2026-09-18 17:08:49 +08:00
parent 917290bec2
commit 71b37dd42d
3 changed files with 20 additions and 2 deletions
+12 -1
View File
@@ -70,9 +70,9 @@ import { ProfileTag } from './profile-tag'
import { isRouteSessionMismatch } from './route-session-state'
import { useRuntimeMessageRepository } from './runtime-repository'
import { ScrollToBottomButton } from './scroll-to-bottom-button'
import { SessionUsageFooter } from './session-usage-footer'
import { useSessionView } from './session-view'
import { SessionActionsMenu } from './sidebar/session-actions-menu'
import { WorkspaceBar } from './workspace-bar'
import { routedSessionIsLoading, threadLoadingState } from './thread-loading'
import {
backfillOlderTranscriptPage,
@@ -80,6 +80,7 @@ import {
transcriptBackfillAvailable
} from './transcript-backfill'
import { advanceSessionTranscriptWindow, type SessionWindowMemo } from './transcript-window'
import { WorkspaceBar } from './workspace-bar'
interface ChatViewProps extends Omit<React.ComponentProps<'div'>, 'onSubmit'> {
gateway: HermesGateway | null
@@ -692,6 +693,15 @@ const ChatViewContent = memo(function ChatViewContent({
const overlayKind: DragKind = dragKind === 'files' ? 'files' : sessionDragging && !sessionEdgeHover ? 'session' : null
// Identity must be stable across ChatView re-renders: this is a prop of the
// 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.
const threadFooter = useMemo(
() => (activeSessionId ? <SessionUsageFooter sessionId={activeSessionId} /> : undefined),
[activeSessionId]
)
return (
<div
className={cn(
@@ -739,6 +749,7 @@ const ChatViewContent = memo(function ChatViewContent({
<Thread
clampToComposer={showChatBar}
cwd={currentCwd}
footer={threadFooter}
gateway={gateway}
intro={showIntro ? { personality: introPersonality, seed: introSeed } : undefined}
loading={threadLoading}
@@ -1,4 +1,4 @@
import { createContext, memo, useCallback, useContext, useMemo, useRef, useState } from 'react'
import { createContext, memo, type ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react'
import { ChatEmptySlot } from '@/components/assistant-ui/chat-empty-slot'
import { AssistantMessage } from '@/components/assistant-ui/thread/assistant-message'
@@ -36,6 +36,7 @@ const ThreadEditContext = createContext<ThreadEditContextValue>({ cwd: null, gat
interface ThreadProps {
clampToComposer?: boolean
cwd?: string | null
footer?: ReactNode
gateway?: HermesGateway | null
intro?: IntroProps
loading?: ThreadLoadingState
@@ -58,6 +59,7 @@ interface ThreadProps {
export const Thread = memo(function Thread({
clampToComposer = false,
cwd = null,
footer,
gateway = null,
intro,
loading,
@@ -176,6 +178,7 @@ export const Thread = memo(function Thread({
clampToComposer={clampToComposer}
components={messageComponents}
emptyPlaceholder={emptyPlaceholder}
footer={footer}
loadingIndicator={loadingIndicator}
scrollProfile={scrollProfile}
sessionId={sessionId}
@@ -231,6 +231,7 @@ interface ThreadMessageListProps {
clampToComposer: boolean
components: ThreadMessageComponents
emptyPlaceholder?: ReactNode
footer?: ReactNode
loadingIndicator?: ReactNode
sessionId?: string | null
sessionKey?: string | null
@@ -421,6 +422,7 @@ const ThreadMessageListInner: FC<ThreadMessageListProps> = ({
clampToComposer,
components,
emptyPlaceholder,
footer,
loadingIndicator,
sessionId = null,
scrollProfile,
@@ -1064,6 +1066,7 @@ const ThreadMessageListInner: FC<ThreadMessageListProps> = ({
if (contentRef.current) {
resizeObserver.observe(contentRef.current)
}
void scrollToBottom('instant')
}
@@ -1365,6 +1368,7 @@ const ThreadMessageListInner: FC<ThreadMessageListProps> = ({
)}
{rows}
{loadingIndicator}
{footer}
{clampToComposer && (
<div
aria-hidden="true"