feat: persistent context-window tracker in ChatPane

Adds a floating popover above the chat input showing current
context-window usage. Modeled on Paseo's tracker.

- New hook useChatContextStats(chatId, messages) finds the latest
  assistant message in the chat with both ctx_used and ctx_max set,
  computes percent, and returns null when data unavailable.
- New component ChatContextPopover renders a small card with the
  "Context window" label, big percent, and "used / max tokens"
  subline. Hidden when stats is null.
- Color thresholds: <60% muted, 60-85 amber, >85 destructive.
- Not a portal — absolutely positioned inside a new relative
  wrapper around ChatInput in ChatPane.tsx, so it's pane-local
  (multi-pane safe).
- Live updates via the existing messages-array dependency.
- No API / schema / WS changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 04:36:08 +00:00
parent 7f0fd1281b
commit 782c2b183d
3 changed files with 99 additions and 1 deletions

View File

@@ -3,8 +3,10 @@ import { ChevronDown, Square, X } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api/client';
import { useSessionStream } from '@/hooks/useSessionStream';
import { useChatContextStats } from '@/hooks/useChatContextStats';
import { MessageList } from '@/components/MessageList';
import { ChatInput } from '@/components/ChatInput';
import { ChatContextPopover } from '@/components/ChatContextPopover';
import {
DropdownMenu,
DropdownMenuContent,
@@ -37,6 +39,7 @@ export function ChatPane({ sessionId, chatId, projectId, sessionChats }: Props)
const chatMessages = stream.messages.filter((m) => m.chat_id === chatId);
const streaming = chatMessages.some((m) => m.status === 'streaming');
const contextStats = useChatContextStats(chatId, chatMessages);
// Auto-send next queued message when streaming completes
useEffect(() => {
@@ -162,7 +165,10 @@ export function ChatPane({ sessionId, chatId, projectId, sessionChats }: Props)
</div>
)}
<ChatInput disabled={false} projectId={projectId} onSend={handleSend} onForceSend={streaming ? handleForceSend : undefined} />
<div className="relative">
<ChatContextPopover stats={contextStats} />
<ChatInput disabled={false} projectId={projectId} onSend={handleSend} onForceSend={streaming ? handleForceSend : undefined} />
</div>
</div>
);
}