v1.12.2: live tok/s + ctx display next to status indicator
ChatThroughput renders inline beside StatusDot while streaming or tool_running. Subscribes to existing usage frames via sessionEvents. Hides when status drops to idle/error or data is older than 10s. Addresses the 2026-05-21 spike's UX gap where slow streams looked identical to dead streams — now there's a live token velocity readout that immediately distinguishes the two. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { Bot, History, MessageSquare, Plus, Terminal, X } from 'lucide-react';
|
||||
import type { Chat, WorkspacePane } from '@/api/types';
|
||||
import { StatusDot } from '@/components/StatusDot';
|
||||
import { ChatThroughput } from '@/components/ChatThroughput';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
@@ -99,6 +100,7 @@ export function ChatTabBar({
|
||||
>
|
||||
<MessageSquare size={12} className="shrink-0" />
|
||||
<StatusDot chatId={chat.id} />
|
||||
<ChatThroughput chatId={chat.id} />
|
||||
{renamingId === chat.id ? (
|
||||
<input
|
||||
autoFocus
|
||||
|
||||
28
apps/web/src/components/ChatThroughput.tsx
Normal file
28
apps/web/src/components/ChatThroughput.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useChatStatus } from '@/hooks/useChatStatus';
|
||||
import { useChatThroughput } from '@/hooks/useChatThroughput';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface Props {
|
||||
chatId: string | null | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// v1.12.2: inline throughput readout. Renders next to StatusDot while the
|
||||
// chat is streaming or running a tool. Hidden in idle/error/waiting states
|
||||
// — the dot already communicates those.
|
||||
export function ChatThroughput({ chatId, className }: Props) {
|
||||
const status = useChatStatus(chatId);
|
||||
const t = useChatThroughput(chatId);
|
||||
if (!chatId || !t) return null;
|
||||
if (status !== 'streaming' && status !== 'tool_running') return null;
|
||||
const tps = t.tps != null && t.tps > 0 ? Math.round(t.tps) : null;
|
||||
const showCtx = t.ctx_used != null && t.ctx_max != null;
|
||||
if (tps === null && !showCtx) return null;
|
||||
return (
|
||||
<span className={cn('text-xs text-muted-foreground tabular-nums', className)}>
|
||||
{tps !== null && `${tps} tok/s`}
|
||||
{tps !== null && showCtx && ' · '}
|
||||
{showCtx && `${t.ctx_used!.toLocaleString()}/${t.ctx_max!.toLocaleString()}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { toast } from 'sonner';
|
||||
import type { Chat, WorkspacePane } from '@/api/types';
|
||||
import { BottomSheet } from '@/components/BottomSheet';
|
||||
import { StatusDot } from '@/components/StatusDot';
|
||||
import { ChatThroughput } from '@/components/ChatThroughput';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -206,6 +207,7 @@ export function MobileTabSwitcher({
|
||||
>
|
||||
<span className="shrink-0 text-muted-foreground">{paneIcon(active?.kind ?? 'chat')}</span>
|
||||
<StatusDot chatId={activeChatId} />
|
||||
<ChatThroughput chatId={activeChatId} />
|
||||
<span className="truncate flex-1 text-left">{activeLabel}</span>
|
||||
<ChevronDown size={14} className="opacity-60 shrink-0" />
|
||||
</button>
|
||||
@@ -237,6 +239,7 @@ export function MobileTabSwitcher({
|
||||
>
|
||||
<span className="shrink-0 text-muted-foreground">{paneIcon(pane.kind)}</span>
|
||||
<StatusDot chatId={cid ?? null} />
|
||||
<ChatThroughput chatId={cid ?? null} />
|
||||
{renamingChatId === cid && cid ? (
|
||||
<input
|
||||
autoFocus
|
||||
|
||||
Reference in New Issue
Block a user