Merge pull request #13 from robbyczgw-cla/feat/context-meter-upstream

feat: Context Window Meter
This commit is contained in:
Julien Thibeaut
2026-02-08 11:05:01 +01:00
committed by GitHub
5 changed files with 86 additions and 1 deletions
@@ -88,6 +88,7 @@ export function ChatScreen({
const {
sessionsQuery,
sessions,
activeSession,
activeExists,
activeSessionKey,
activeTitle,
@@ -161,7 +162,8 @@ export function ChatScreen({
streamStop()
setPendingGeneration(false)
setWaitingForResponse(false)
}, [streamStop])
void queryClient.invalidateQueries({ queryKey: chatQueryKeys.sessions })
}, [queryClient, streamStop])
const streamStart = useCallback(() => {
if (!activeFriendlyId || isNewChat) return
if (streamTimer.current) window.clearInterval(streamTimer.current)
@@ -608,6 +610,8 @@ export function ChatScreen({
wrapperRef={headerRef}
showSidebarButton={isMobile}
onOpenSidebar={handleOpenSidebar}
usedTokens={activeSession?.totalTokens}
maxTokens={activeSession?.contextTokens}
/>
{hideUi ? null : (
@@ -2,12 +2,15 @@ import { memo } from 'react'
import { HugeiconsIcon } from '@hugeicons/react'
import { Menu01Icon } from '@hugeicons/core-free-icons'
import { Button } from '@/components/ui/button'
import { ContextMeter } from './context-meter'
type ChatHeaderProps = {
activeTitle: string
wrapperRef?: React.Ref<HTMLDivElement>
showSidebarButton?: boolean
onOpenSidebar?: () => void
usedTokens?: number
maxTokens?: number
}
function ChatHeaderComponent({
@@ -15,6 +18,8 @@ function ChatHeaderComponent({
wrapperRef,
showSidebarButton = false,
onOpenSidebar,
usedTokens,
maxTokens,
}: ChatHeaderProps) {
return (
<div
@@ -33,6 +38,11 @@ function ChatHeaderComponent({
</Button>
) : null}
<div className="text-sm font-medium truncate">{activeTitle}</div>
<ContextMeter
usedTokens={usedTokens}
maxTokens={maxTokens}
className="ml-3 hidden sm:flex"
/>
</div>
)
}
@@ -0,0 +1,59 @@
import { memo, useMemo } from 'react'
import { cn } from '@/lib/utils'
type ContextMeterProps = {
usedTokens?: number
maxTokens?: number
className?: string
}
function ContextMeterComponent({
usedTokens,
maxTokens,
className,
}: ContextMeterProps) {
const { percentage, color, label } = useMemo(() => {
if (!usedTokens || !maxTokens) return { percentage: 0, color: '', label: '' }
const pct = Math.min((usedTokens / maxTokens) * 100, 100)
const fmt = (n: number) =>
n >= 1000 ? `${(n / 1000).toFixed(0)}K` : String(n)
return {
percentage: pct,
color:
pct >= 90
? 'bg-red-500'
: pct >= 70
? 'bg-yellow-500'
: 'bg-green-500',
label: `${fmt(usedTokens)} / ${fmt(maxTokens)} tokens (${pct.toFixed(0)}%)`,
}
}, [usedTokens, maxTokens])
if (!usedTokens || !maxTokens || percentage === 0) return null
return (
<div
className={cn(
'flex items-center gap-2 text-xs text-primary-500',
className,
)}
title={label}
>
<div className="w-20 h-1.5 bg-primary-100 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full transition-all duration-500',
color,
percentage >= 95 && 'animate-pulse',
)}
style={{ width: `${percentage}%` }}
/>
</div>
<span className="tabular-nums whitespace-nowrap">
{percentage.toFixed(0)}%
</span>
</div>
)
}
export const ContextMeter = memo(ContextMeterComponent)
+4
View File
@@ -49,6 +49,8 @@ export type SessionSummary = {
updatedAt?: number
lastMessage?: GatewayMessage | null
friendlyId?: string
totalTokens?: number
contextTokens?: number
}
export type SessionListResponse = {
@@ -69,6 +71,8 @@ export type SessionMeta = {
label?: string
updatedAt?: number
lastMessage?: GatewayMessage | null
totalTokens?: number
contextTokens?: number
}
export type PathsPayload = {
+8
View File
@@ -97,6 +97,14 @@ export function normalizeSessions(
updatedAt:
typeof session.updatedAt === 'number' ? session.updatedAt : undefined,
lastMessage: session.lastMessage ?? null,
totalTokens:
typeof session.totalTokens === 'number'
? session.totalTokens
: undefined,
contextTokens:
typeof session.contextTokens === 'number'
? session.contextTokens
: undefined,
}
})
}