feat(ui): 添加 Token 消耗统计显示

- 状态栏显示当前会话 Token 消耗总量,悬停显示详情
- AI 消息底部显示本次响应的输入/输出 Token
- 会话列表顶部显示项目 Token 消耗总量
- 会话列表每项显示该会话的 Token 消耗
- 新增 Token 统计 API 客户端函数
- Server done 事件携带 usage 信息
This commit is contained in:
2025-12-18 17:01:09 +08:00
parent bac32fe8f6
commit 3ff489fbc0
8 changed files with 394 additions and 73 deletions
+8 -1
View File
@@ -421,7 +421,7 @@ export async function processMessage(
}); });
}); });
// 发送完成消息 // 发送完成消息(包含 token 使用信息)
broadcastToSession(sessionId, { broadcastToSession(sessionId, {
type: 'done', type: 'done',
sessionId, sessionId,
@@ -430,6 +430,13 @@ export async function processMessage(
hasToolCalls, hasToolCalls,
messageCount: result.messages.length, messageCount: result.messages.length,
agentName: options?.agentMode || 'build', agentName: options?.agentMode || 'build',
usage: result.usage ? {
inputTokens: result.usage.promptTokens,
outputTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cacheReadTokens: result.usage.cacheReadInputTokens,
cacheWriteTokens: result.usage.cacheCreationInputTokens,
} : undefined,
}, },
}); });
+59
View File
@@ -56,6 +56,10 @@ import type {
// System Commands types // System Commands types
SystemCommandListResponse, SystemCommandListResponse,
SystemCommandInfo, SystemCommandInfo,
// Token Stats types
SessionTokenStats,
ProjectTokenStats,
TokenStatsSummary,
} from './types.js'; } from './types.js';
// Re-export types // Re-export types
@@ -155,6 +159,11 @@ export type {
ActiveFileInfo, ActiveFileInfo,
// File diff types // File diff types
FileDiffInfo, FileDiffInfo,
// Token Stats types
TokenCount,
SessionTokenStats,
ProjectTokenStats,
TokenStatsSummary,
} from './types.js'; } from './types.js';
// API Configuration // API Configuration
@@ -1156,3 +1165,53 @@ export async function getLSPDiagnostics(file?: string): Promise<{
const params = file ? `?file=${encodeURIComponent(file)}` : ''; const params = file ? `?file=${encodeURIComponent(file)}` : '';
return request('GET', `/lsp/diagnostics${params}`); return request('GET', `/lsp/diagnostics${params}`);
} }
// ============ Token Stats API ============
/**
* 获取会话 Token 统计
*/
export async function getSessionTokenStats(sessionId: string): Promise<{
success: boolean;
data?: SessionTokenStats;
error?: string;
}> {
return request('GET', `/stats/sessions/${encodeURIComponent(sessionId)}`);
}
/**
* 获取项目 Token 统计
*/
export async function getProjectTokenStats(
projectId: string,
refresh?: boolean
): Promise<{
success: boolean;
data?: ProjectTokenStats;
error?: string;
}> {
const params = refresh ? '?refresh=true' : '';
return request('GET', `/stats/projects/${encodeURIComponent(projectId)}${params}`);
}
/**
* 获取 Token 统计摘要(当前会话和项目)
*/
export async function getTokenStatsSummary(): Promise<{
success: boolean;
data?: TokenStatsSummary;
error?: string;
}> {
return request('GET', '/stats/summary');
}
/**
* 刷新项目 Token 统计
*/
export async function refreshProjectTokenStats(projectId: string): Promise<{
success: boolean;
data?: ProjectTokenStats;
error?: string;
}> {
return request('POST', `/stats/projects/${encodeURIComponent(projectId)}/refresh`);
}
+62
View File
@@ -184,6 +184,10 @@ export interface Message {
model?: string; model?: string;
stepCount?: number; stepCount?: number;
totalTokens?: number; totalTokens?: number;
/** 输入 Token 数 */
inputTokens?: number;
/** 输出 Token 数 */
outputTokens?: number;
/** 生成此消息的 Agent 名称 */ /** 生成此消息的 Agent 名称 */
agentName?: string; agentName?: string;
}; };
@@ -1223,3 +1227,61 @@ export interface FileDiffInfo {
toolCallId?: string; toolCallId?: string;
} }
// ============ Token 统计相关 ============
/** Token 统计数值 */
export interface TokenCount {
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
/** 会话 Token 统计 */
export interface SessionTokenStats {
/** 会话 ID */
sessionId?: string;
/** 本会话自身统计 */
self: TokenCount & {
cacheReadTokens?: number;
cacheWriteTokens?: number;
};
/** 子会话统计汇总 */
children: TokenCount;
/** 总计(自身 + 子会话) */
total: TokenCount;
/** 消息数量 */
messageCount: number;
/** 最后更新时间 */
updatedAt?: number;
}
/** 项目 Token 统计 */
export interface ProjectTokenStats {
/** 项目 ID */
projectId: string;
/** 总输入 Token */
totalInputTokens: number;
/** 总输出 Token */
totalOutputTokens: number;
/** 总 Token */
totalTokens: number;
/** 会话数量 */
sessionCount: number;
/** 最后更新时间 */
updatedAt?: number;
}
/** Token 统计摘要 */
export interface TokenStatsSummary {
/** 是否有活跃会话 */
hasActiveSession: boolean;
/** 当前会话 ID */
sessionId?: string;
/** 当前项目 ID */
projectId?: string;
/** 会话统计 */
session: SessionTokenStats | null;
/** 项目统计 */
project: ProjectTokenStats | null;
}
@@ -15,6 +15,7 @@ import {
CheckCircle2, CheckCircle2,
Loader2, Loader2,
GitCompare, GitCompare,
Coins,
} from 'lucide-react'; } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useState, forwardRef } from 'react'; import { useState, forwardRef } from 'react';
@@ -41,11 +42,28 @@ interface ChatMessageProps {
onDenyPermission?: (requestId: string, remember: boolean) => void; onDenyPermission?: (requestId: string, remember: boolean) => void;
} }
// 格式化 Token 数量
const formatTokens = (tokens: number): string => {
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(2)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}k`;
}
return `${tokens}`;
};
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>( export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
({ message, isStreaming = false, onAnswerQuestion, onViewDiff, onAllowPermission, onDenyPermission }, ref) => { ({ message, isStreaming = false, onAnswerQuestion, onViewDiff, onAllowPermission, onDenyPermission }, ref) => {
const isUser = message.role === 'user'; const isUser = message.role === 'user';
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
// Token 信息
const hasTokenInfo = !isUser && message.metadata?.inputTokens !== undefined;
const inputTokens = message.metadata?.inputTokens ?? 0;
const outputTokens = message.metadata?.outputTokens ?? 0;
const totalTokens = message.metadata?.totalTokens ?? (inputTokens + outputTokens);
const handleCopy = async () => { const handleCopy = async () => {
await navigator.clipboard.writeText(message.content ?? ''); await navigator.clipboard.writeText(message.content ?? '');
setCopied(true); setCopied(true);
@@ -184,6 +202,22 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
</button> </button>
</div> </div>
{renderContent()} {renderContent()}
{/* Token 统计显示(仅 AI 消息,非流式输出时) */}
{hasTokenInfo && !isStreaming && totalTokens > 0 && (
<div className="mt-2 pt-2 border-t border-line/50 flex items-center gap-3 text-xs text-fg-subtle">
<span className="flex items-center gap-1" title="Token 消耗">
<Coins size={12} className="text-fg-muted" />
<span>{formatTokens(totalTokens)}</span>
</span>
<span className="text-fg-muted/40">|</span>
<span title="输入 Token">
{formatTokens(inputTokens)}
</span>
<span title="输出 Token">
{formatTokens(outputTokens)}
</span>
</div>
)}
</div> </div>
</motion.div> </motion.div>
); );
+151 -62
View File
@@ -5,14 +5,23 @@
*/ */
import { useState, useEffect, useCallback, forwardRef } from 'react'; import { useState, useEffect, useCallback, forwardRef } from 'react';
import { X, Plus, MessageSquare, Trash2, MessageCircle } from 'lucide-react'; import { X, Plus, MessageSquare, Trash2, MessageCircle, Coins } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { cn } from '../utils/cn'; import { cn } from '../utils/cn';
import { modalOverlay, modalContent, smoothTransition, fadeInUp } from '../utils/animations'; import { modalOverlay, modalContent, smoothTransition, fadeInUp } from '../utils/animations';
import { Button } from '../primitives/Button'; import { Button } from '../primitives/Button';
import { SessionSkeleton } from './Skeleton'; import { SessionSkeleton } from './Skeleton';
import { listSessions, createSession, deleteSession, type Session } from '../api/client.js'; import {
listSessions,
createSession,
deleteSession,
getTokenStatsSummary,
getSessionTokenStats,
type Session,
type ProjectTokenStats,
type SessionTokenStats,
} from '../api/client.js';
interface SessionPanelProps { interface SessionPanelProps {
onClose: () => void; onClose: () => void;
@@ -28,6 +37,17 @@ interface SessionPanelProps {
responsive?: boolean; responsive?: boolean;
} }
// 格式化 Token 数量
const formatTokens = (tokens: number): string => {
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(2)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}k`;
}
return `${tokens}`;
};
export function SessionPanel({ export function SessionPanel({
onClose, onClose,
currentSessionId, currentSessionId,
@@ -38,13 +58,45 @@ export function SessionPanel({
}: SessionPanelProps) { }: SessionPanelProps) {
const [sessions, setSessions] = useState<Session[]>([]); const [sessions, setSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [projectStats, setProjectStats] = useState<ProjectTokenStats | null>(null);
const [sessionStats, setSessionStats] = useState<Record<string, SessionTokenStats>>({});
// 加载会话列表 // 加载会话列表和 Token 统计
const loadSessions = useCallback(async () => { const loadSessions = useCallback(async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const { data } = await listSessions(); const [sessionsResult, summaryResult] = await Promise.all([
setSessions(data); listSessions(),
getTokenStatsSummary(),
]);
setSessions(sessionsResult.data);
// 设置项目统计
if (summaryResult.success && summaryResult.data?.project) {
setProjectStats(summaryResult.data.project);
}
// 加载每个会话的 Token 统计
const statsPromises = sessionsResult.data.map(async (session) => {
try {
const result = await getSessionTokenStats(session.id);
if (result.success && result.data) {
return { id: session.id, stats: result.data };
}
} catch {
// 忽略单个会话统计加载失败
}
return null;
});
const statsResults = await Promise.all(statsPromises);
const newStats: Record<string, SessionTokenStats> = {};
for (const result of statsResults) {
if (result) {
newStats[result.id] = result.stats;
}
}
setSessionStats(newStats);
} catch (error) { } catch (error) {
console.error('Failed to load sessions:', error); console.error('Failed to load sessions:', error);
toast.error('Failed to load sessions'); toast.error('Failed to load sessions');
@@ -109,41 +161,60 @@ export function SessionPanel({
}, [sessionTitleUpdate]); }, [sessionTitleUpdate]);
// 会话列表项 // 会话列表项
const SessionItem = forwardRef<HTMLDivElement, { session: Session }>(({ session }, ref) => ( const SessionItem = forwardRef<HTMLDivElement, { session: Session }>(({ session }, ref) => {
<motion.div const stats = sessionStats[session.id];
ref={ref} const totalTokens = stats?.total.totalTokens ?? 0;
layout
variants={fadeInUp} return (
initial="initial" <motion.div
animate="animate" ref={ref}
exit="exit" layout
transition={smoothTransition} variants={fadeInUp}
onClick={() => handleSelectSession(session.id)} initial="initial"
className={cn( animate="animate"
'flex items-center gap-3 p-3 rounded-lg cursor-pointer group', exit="exit"
'hover:bg-surface-muted transition-colors', transition={smoothTransition}
'active:bg-surface-emphasis', onClick={() => handleSelectSession(session.id)}
currentSessionId === session.id && 'bg-primary-500/20 border border-primary-500/30' className={cn(
)} 'flex items-center gap-3 p-3 rounded-lg cursor-pointer group',
> 'hover:bg-surface-muted transition-colors',
<MessageSquare size={18} className="text-fg-muted flex-shrink-0" /> 'active:bg-surface-emphasis',
<div className="flex-1 min-w-0"> currentSessionId === session.id && 'bg-primary-500/20 border border-primary-500/30'
<div className="text-sm truncate text-fg"> )}
{session.name || `Chat ${session.id.slice(0, 8)}`}
</div>
<div className="text-xs text-fg-subtle">{session.messageCount} messages</div>
</div>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={(e) => handleDelete(session.id, e)}
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-red-500/20 rounded transition-all"
aria-label="Delete session"
> >
<Trash2 size={14} className="text-red-400" /> <MessageSquare size={18} className="text-fg-muted flex-shrink-0" />
</motion.button> <div className="flex-1 min-w-0">
</motion.div> <div className="text-sm truncate text-fg">
)); {session.name || `Chat ${session.id.slice(0, 8)}`}
</div>
<div className="flex items-center gap-2 text-xs text-fg-subtle">
<span>{session.messageCount} messages</span>
{totalTokens > 0 && (
<>
<span className="text-fg-muted/40"></span>
<span
className="flex items-center gap-0.5"
title={`输入: ${formatTokens(stats.total.inputTokens)} | 输出: ${formatTokens(stats.total.outputTokens)}`}
>
<Coins size={10} />
{formatTokens(totalTokens)}
</span>
</>
)}
</div>
</div>
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={(e) => handleDelete(session.id, e)}
className="opacity-0 group-hover:opacity-100 p-1.5 hover:bg-red-500/20 rounded transition-all"
aria-label="Delete session"
>
<Trash2 size={14} className="text-red-400" />
</motion.button>
</motion.div>
);
});
// 空状态 // 空状态
const EmptyState = () => ( const EmptyState = () => (
@@ -189,30 +260,48 @@ export function SessionPanel({
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-line"> <div className="border-b border-line">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between p-4">
<MessageSquare size={20} className="text-primary-400" /> <div className="flex items-center gap-2">
<h2 className="text-lg font-semibold text-fg">Sessions</h2> <MessageSquare size={20} className="text-primary-400" />
{!isLoading && ( <h2 className="text-lg font-semibold text-fg">Sessions</h2>
<span className="text-xs text-fg-muted bg-surface-muted px-2 py-0.5 rounded-full"> {!isLoading && (
{sessions.length} <span className="text-xs text-fg-muted bg-surface-muted px-2 py-0.5 rounded-full">
</span> {sessions.length}
)} </span>
</div> )}
<div className="flex items-center gap-2"> </div>
<Button onClick={handleCreate} variant="default" size="sm"> <div className="flex items-center gap-2">
<Plus size={16} /> <Button onClick={handleCreate} variant="default" size="sm">
New <Plus size={16} />
</Button> New
<motion.button </Button>
whileHover={{ scale: 1.1 }} <motion.button
whileTap={{ scale: 0.9 }} whileHover={{ scale: 1.1 }}
onClick={onClose} whileTap={{ scale: 0.9 }}
className="p-1.5 hover:bg-surface-muted rounded-lg transition-colors" onClick={onClose}
> className="p-1.5 hover:bg-surface-muted rounded-lg transition-colors"
<X size={20} className="text-fg-muted" /> >
</motion.button> <X size={20} className="text-fg-muted" />
</motion.button>
</div>
</div> </div>
{/* 项目 Token 统计 */}
{projectStats && projectStats.totalTokens > 0 && (
<div className="px-4 pb-3 flex items-center gap-4 text-xs text-fg-muted">
<div
className="flex items-center gap-1"
title={`总输入: ${formatTokens(projectStats.totalInputTokens)} | 总输出: ${formatTokens(projectStats.totalOutputTokens)}`}
>
<Coins size={12} className="text-primary-400" />
<span>:</span>
<span className="text-fg-secondary font-medium">{formatTokens(projectStats.totalTokens)}</span>
</div>
<div className="text-fg-subtle">
{projectStats.sessionCount}
</div>
</div>
)}
</div> </div>
{/* Session List */} {/* Session List */}
+57 -7
View File
@@ -1,18 +1,28 @@
/** /**
* StatusBar Component * StatusBar Component
* *
* 底部状态栏,显示 Git 分支、诊断信息、连接状态等 * 底部状态栏,显示 Git 分支、诊断信息、连接状态、Token 统计
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { GitBranch, AlertTriangle, AlertCircle, WifiOff, RefreshCw, CheckCircle } from 'lucide-react'; import { GitBranch, AlertTriangle, AlertCircle, WifiOff, RefreshCw, CheckCircle, Coins } from 'lucide-react';
import { cn } from '../utils/cn.js'; import { cn } from '../utils/cn.js';
import { getLSPDiagnostics, getGitInfo, getHealth, type DiagnosticsSummary, type GitInfo } from '../api/client.js'; import {
getLSPDiagnostics,
getGitInfo,
getHealth,
getSessionTokenStats,
type DiagnosticsSummary,
type GitInfo,
type SessionTokenStats,
} from '../api/client.js';
interface StatusBarProps { interface StatusBarProps {
className?: string; className?: string;
/** 是否连接到服务器 */ /** 是否连接到服务器 */
isConnected?: boolean; isConnected?: boolean;
/** 当前会话 ID */
sessionId?: string | null;
/** 点击诊断信息回调 */ /** 点击诊断信息回调 */
onDiagnosticsClick?: () => void; onDiagnosticsClick?: () => void;
/** 刷新间隔 (ms) */ /** 刷新间隔 (ms) */
@@ -22,25 +32,39 @@ interface StatusBarProps {
export function StatusBar({ export function StatusBar({
className, className,
isConnected: isConnectedProp, isConnected: isConnectedProp,
sessionId,
onDiagnosticsClick, onDiagnosticsClick,
refreshInterval = 30000, refreshInterval = 30000,
}: StatusBarProps) { }: StatusBarProps) {
const [diagnostics, setDiagnostics] = useState<DiagnosticsSummary | null>(null); const [diagnostics, setDiagnostics] = useState<DiagnosticsSummary | null>(null);
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null); const [gitInfo, setGitInfo] = useState<GitInfo | null>(null);
const [tokenStats, setTokenStats] = useState<SessionTokenStats | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [connectionStatus, setConnectionStatus] = useState(true); const [connectionStatus, setConnectionStatus] = useState(true);
// 如果外部传入了 isConnected,使用外部值;否则使用内部检测 // 如果外部传入了 isConnected,使用外部值;否则使用内部检测
const isConnected = isConnectedProp ?? connectionStatus; const isConnected = isConnectedProp ?? connectionStatus;
// 加载诊断信息Git 信息 // 加载诊断信息Git 信息和 Token 统计
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const [diagResult, gitResult] = await Promise.all([ const promises: Promise<unknown>[] = [
getLSPDiagnostics(), getLSPDiagnostics(),
getGitInfo(), getGitInfo(),
]); ];
// 如果有 sessionId,加载 Token 统计
if (sessionId) {
promises.push(getSessionTokenStats(sessionId));
}
const results = await Promise.all(promises);
const [diagResult, gitResult, tokenResult] = results as [
Awaited<ReturnType<typeof getLSPDiagnostics>>,
Awaited<ReturnType<typeof getGitInfo>>,
Awaited<ReturnType<typeof getSessionTokenStats>> | undefined,
];
if (diagResult.success && diagResult.data.summary) { if (diagResult.success && diagResult.data.summary) {
setDiagnostics(diagResult.data.summary); setDiagnostics(diagResult.data.summary);
@@ -50,6 +74,10 @@ export function StatusBar({
setGitInfo(gitResult.data); setGitInfo(gitResult.data);
} }
if (tokenResult?.success && tokenResult.data) {
setTokenStats(tokenResult.data);
}
// 如果没有外部传入连接状态,内部检测 // 如果没有外部传入连接状态,内部检测
if (isConnectedProp === undefined) { if (isConnectedProp === undefined) {
setConnectionStatus(true); setConnectionStatus(true);
@@ -62,7 +90,7 @@ export function StatusBar({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [isConnectedProp]); }, [isConnectedProp, sessionId]);
// 检测连接状态(如果没有外部传入) // 检测连接状态(如果没有外部传入)
const checkConnection = useCallback(async () => { const checkConnection = useCallback(async () => {
@@ -92,6 +120,17 @@ export function StatusBar({
const warningCount = diagnostics?.totalWarnings ?? 0; const warningCount = diagnostics?.totalWarnings ?? 0;
const hasIssues = errorCount > 0 || warningCount > 0; const hasIssues = errorCount > 0 || warningCount > 0;
// 格式化 Token 数量
const formatTokens = (tokens: number): string => {
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(2)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}k`;
}
return `${tokens}`;
};
return ( return (
<div <div
className={cn( className={cn(
@@ -149,6 +188,17 @@ export function StatusBar({
{/* 右侧 */} {/* 右侧 */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Token 统计 */}
{tokenStats && tokenStats.total.totalTokens > 0 && (
<div
className="flex items-center gap-1 text-fg-muted hover:text-fg-secondary cursor-default"
title={`输入: ${formatTokens(tokenStats.total.inputTokens)} | 输出: ${formatTokens(tokenStats.total.outputTokens)} | 消息: ${tokenStats.messageCount}${tokenStats.children.totalTokens > 0 ? ` | 子任务: ${formatTokens(tokenStats.children.totalTokens)}` : ''}`}
>
<Coins size={12} />
<span>{formatTokens(tokenStats.total.totalTokens)}</span>
</div>
)}
{/* 诊断信息 */} {/* 诊断信息 */}
<button <button
onClick={onDiagnosticsClick} onClick={onDiagnosticsClick}
+19 -2
View File
@@ -415,6 +415,12 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
const content = message.payload?.content || streaming?.content || ''; const content = message.payload?.content || streaming?.content || '';
// 从服务器 payload 获取 agentName,或使用当前 agentMode // 从服务器 payload 获取 agentName,或使用当前 agentMode
const agentName = message.payload?.agentName || prev.agentMode; const agentName = message.payload?.agentName || prev.agentMode;
// 获取 token usage 信息
const usage = message.payload?.usage as {
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
} | undefined;
const newMessage: Message = streaming const newMessage: Message = streaming
? { ? {
@@ -422,7 +428,13 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
id: message.payload?.id || streaming.id, id: message.payload?.id || streaming.id,
timestamp: message.payload?.timestamp || streaming.timestamp, timestamp: message.payload?.timestamp || streaming.timestamp,
content, content,
metadata: { ...streaming.metadata, agentName }, metadata: {
...streaming.metadata,
agentName,
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
totalTokens: usage?.totalTokens,
},
} }
: { : {
id: message.payload?.id || `assistant-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, id: message.payload?.id || `assistant-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
@@ -430,7 +442,12 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
timestamp: message.payload?.timestamp || new Date().toISOString(), timestamp: message.payload?.timestamp || new Date().toISOString(),
parts: [{ type: 'text', id: `text-${Date.now()}`, text: content }], parts: [{ type: 'text', id: `text-${Date.now()}`, text: content }],
content, content,
metadata: { agentName }, metadata: {
agentName,
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
totalTokens: usage?.totalTokens,
},
}; };
return { return {
+4 -1
View File
@@ -222,7 +222,10 @@ export function App() {
</div> </div>
{/* 底部状态栏 */} {/* 底部状态栏 */}
<StatusBar onDiagnosticsClick={() => setShowDiagnostics(true)} /> <StatusBar
sessionId={currentSessionId}
onDiagnosticsClick={() => setShowDiagnostics(true)}
/>
{/* 命令面板 */} {/* 命令面板 */}
{showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />} {showCommands && <CommandPanel onClose={() => setShowCommands(false)} responsive />}