From 71b37dd42d80ecfb3ac8dc1cf4b785928ac7ed09 Mon Sep 17 00:00:00 2001 From: m4 Date: Fri, 18 Sep 2026 17:08:49 +0800 Subject: [PATCH] feat(desktop): render session usage footer at transcript end Co-Authored-By: Claude Opus 4.7 --- apps/desktop/src/app/chat/index.tsx | 13 ++++++++++++- .../src/components/assistant-ui/thread/index.tsx | 5 ++++- .../src/components/assistant-ui/thread/list.tsx | 4 ++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/chat/index.tsx b/apps/desktop/src/app/chat/index.tsx index 1c93423173..02f9186203 100644 --- a/apps/desktop/src/app/chat/index.tsx +++ b/apps/desktop/src/app/chat/index.tsx @@ -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, '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 ? : undefined), + [activeSessionId] + ) + return (
({ 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} diff --git a/apps/desktop/src/components/assistant-ui/thread/list.tsx b/apps/desktop/src/components/assistant-ui/thread/list.tsx index f6efe8b0c2..8f94883606 100644 --- a/apps/desktop/src/components/assistant-ui/thread/list.tsx +++ b/apps/desktop/src/components/assistant-ui/thread/list.tsx @@ -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 = ({ clampToComposer, components, emptyPlaceholder, + footer, loadingIndicator, sessionId = null, scrollProfile, @@ -1064,6 +1066,7 @@ const ThreadMessageListInner: FC = ({ if (contentRef.current) { resizeObserver.observe(contentRef.current) } + void scrollToBottom('instant') } @@ -1365,6 +1368,7 @@ const ThreadMessageListInner: FC = ({ )} {rows} {loadingIndicator} + {footer} {clampToComposer && (