Merge pull request #214 from multica-ai/codex/chat-context-window-indicator
feat(chat): add context window usage indicator
This commit is contained in:
commit
fc8a813120
12 changed files with 284 additions and 23 deletions
24
apps/desktop/src/main/electron-env.d.ts
vendored
24
apps/desktop/src/main/electron-env.d.ts
vendored
|
|
@ -247,18 +247,18 @@ interface ElectronAPI {
|
|||
setEnabled: (enabled: boolean) => Promise<{ ok: boolean; enabled?: boolean; error?: string }>
|
||||
wake: (reason?: string) => Promise<{ ok: boolean; result?: unknown; error?: string }>
|
||||
}
|
||||
localChat: {
|
||||
subscribe: (agentId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }>
|
||||
unsubscribe: (agentId: string) => Promise<{ ok: boolean }>
|
||||
getHistory: (agentId: string, options?: { offset?: number; limit?: number }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number }>
|
||||
send: (agentId: string, content: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
abort: (agentId: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
resolveExecApproval: (approvalId: string, decision: string) => Promise<{ ok: boolean }>
|
||||
onEvent: (callback: (event: LocalChatEvent) => void) => void
|
||||
offEvent: () => void
|
||||
onApproval: (callback: (approval: LocalChatApproval) => void) => void
|
||||
offApproval: () => void
|
||||
}
|
||||
localChat: {
|
||||
subscribe: (agentId: string) => Promise<{ ok?: boolean; error?: string; alreadySubscribed?: boolean }>
|
||||
unsubscribe: (agentId: string) => Promise<{ ok: boolean }>
|
||||
getHistory: (agentId: string, options?: { offset?: number; limit?: number }) => Promise<{ messages: unknown[]; total: number; offset: number; limit: number; contextWindowTokens?: number }>
|
||||
send: (agentId: string, content: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
abort: (agentId: string) => Promise<{ ok?: boolean; error?: string }>
|
||||
resolveExecApproval: (approvalId: string, decision: string) => Promise<{ ok: boolean }>
|
||||
onEvent: (callback: (event: LocalChatEvent) => void) => void
|
||||
offEvent: () => void
|
||||
onApproval: (callback: (approval: LocalChatApproval) => void) => void
|
||||
offApproval: () => void
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer process, expose in `preload.ts`
|
||||
|
|
|
|||
|
|
@ -354,20 +354,21 @@ export function registerHubIpcHandlers(): void {
|
|||
const h = getHub()
|
||||
const agent = h.getAgent(agentId)
|
||||
if (!agent) {
|
||||
return { messages: [], total: 0, offset: 0, limit: 0 }
|
||||
return { messages: [], total: 0, offset: 0, limit: 0, contextWindowTokens: undefined }
|
||||
}
|
||||
|
||||
try {
|
||||
await agent.ensureInitialized()
|
||||
const allMessages = agent.loadSessionMessagesForDisplay()
|
||||
const contextWindowTokens = agent.getContextWindowTokens()
|
||||
const total = allMessages.length
|
||||
// Must match DEFAULT_MESSAGES_LIMIT from @multica/sdk/actions/rpc
|
||||
const limit = options?.limit ?? 200
|
||||
const offset = options?.offset ?? Math.max(0, total - limit)
|
||||
const sliced = allMessages.slice(offset, offset + limit)
|
||||
return { messages: sliced, total, offset, limit }
|
||||
return { messages: sliced, total, offset, limit, contextWindowTokens }
|
||||
} catch {
|
||||
return { messages: [], total: 0, offset: 0, limit: 0 }
|
||||
return { messages: [], total: 0, offset: 0, limit: 0, contextWindowTokens: undefined }
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export function LocalChat({ initialPrompt }: LocalChatProps) {
|
|||
isLoadingHistory,
|
||||
isLoadingMore,
|
||||
hasMore,
|
||||
contextWindowTokens,
|
||||
error,
|
||||
pendingApprovals,
|
||||
sendMessage,
|
||||
|
|
@ -110,6 +111,7 @@ export function LocalChat({ initialPrompt }: LocalChatProps) {
|
|||
isLoadingHistory={isLoadingHistory}
|
||||
isLoadingMore={isLoadingMore}
|
||||
hasMore={hasMore}
|
||||
contextWindowTokens={contextWindowTokens}
|
||||
error={error}
|
||||
pendingApprovals={pendingApprovals}
|
||||
sendMessage={sendMessage}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export function useLocalChat() {
|
|||
chatRef.current.setHistory(result.messages as AgentMessageItem[], agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
})
|
||||
offsetRef.current = result.offset
|
||||
}
|
||||
|
|
@ -140,6 +141,7 @@ export function useLocalChat() {
|
|||
chatRef.current.prependHistory(result.messages as AgentMessageItem[], agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
})
|
||||
offsetRef.current = result.offset
|
||||
}
|
||||
|
|
@ -172,6 +174,7 @@ export function useLocalChat() {
|
|||
isLoadingHistory,
|
||||
isLoadingMore,
|
||||
hasMore: chat.hasMore,
|
||||
contextWindowTokens: chat.contextWindowTokens,
|
||||
error: chat.error,
|
||||
pendingApprovals: chat.pendingApprovals,
|
||||
sendMessage,
|
||||
|
|
|
|||
|
|
@ -553,6 +553,20 @@ export class AsyncAgent {
|
|||
return this.agent.getProviderInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get persisted session metadata.
|
||||
*/
|
||||
getSessionMeta(): import("./session/types.js").SessionMeta | undefined {
|
||||
return this.agent.getSessionMeta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective context window token limit for this session.
|
||||
*/
|
||||
getContextWindowTokens(): number {
|
||||
return this.agent.getContextWindowTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different provider and/or model.
|
||||
* This updates the agent's model without recreating the session.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
getDefaultModel,
|
||||
} from "./providers/index.js";
|
||||
import { SessionManager } from "./session/session-manager.js";
|
||||
import type { SessionMeta } from "./session/types.js";
|
||||
import { ProfileManager } from "./profile/index.js";
|
||||
import { SkillManager } from "./skills/index.js";
|
||||
import { credentialManager, getCredentialsPath } from "./credentials.js";
|
||||
|
|
@ -1199,6 +1200,20 @@ export class Agent {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get persisted session metadata.
|
||||
*/
|
||||
getSessionMeta(): SessionMeta | undefined {
|
||||
return this.session.getMeta();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective context window token limit for this session.
|
||||
*/
|
||||
getContextWindowTokens(): number {
|
||||
return this.session.getMeta()?.contextWindowTokens ?? this.session.getContextWindowTokens();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different provider and/or model.
|
||||
* This updates the agent's model without recreating the session.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export function createGetAgentMessagesHandler(): RpcHandler {
|
|||
const session = new SessionManager({ sessionId: agentId });
|
||||
const allMessages = session.loadMessagesForDisplay();
|
||||
const total = allMessages.length;
|
||||
const contextWindowTokens = session.getMeta()?.contextWindowTokens ?? session.getContextWindowTokens();
|
||||
|
||||
// When offset is not provided, return the latest messages
|
||||
if (offset == null) {
|
||||
|
|
@ -39,6 +40,6 @@ export function createGetAgentMessagesHandler(): RpcHandler {
|
|||
|
||||
const sliced = allMessages.slice(offset, offset + limit);
|
||||
|
||||
return { messages: sliced, total, offset, limit };
|
||||
return { messages: sliced, total, offset, limit, contextWindowTokens };
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export function useChat() {
|
|||
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>([]);
|
||||
const [error, setError] = useState<ChatError | null>(null);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [contextWindowTokens, setContextWindowTokens] = useState<number | undefined>(undefined);
|
||||
|
||||
const isStreaming = streamingIds.size > 0;
|
||||
|
||||
|
|
@ -125,19 +126,33 @@ export function useChat() {
|
|||
}, []);
|
||||
|
||||
/** Load initial history (replaces all messages) */
|
||||
const setHistory = useCallback((raw: AgentMessageItem[], agentId: string, meta?: { total: number; offset: number }) => {
|
||||
const setHistory = useCallback((
|
||||
raw: AgentMessageItem[],
|
||||
agentId: string,
|
||||
meta?: { total: number; offset: number; contextWindowTokens?: number },
|
||||
) => {
|
||||
const loaded = convertMessages(raw, agentId);
|
||||
setMessages(loaded);
|
||||
if (meta) {
|
||||
setHasMore(meta.offset > 0);
|
||||
if (meta.contextWindowTokens !== undefined) {
|
||||
setContextWindowTokens(meta.contextWindowTokens);
|
||||
}
|
||||
}
|
||||
}, [convertMessages]);
|
||||
|
||||
/** Prepend older messages (for "load more" pagination) */
|
||||
const prependHistory = useCallback((raw: AgentMessageItem[], agentId: string, meta: { total: number; offset: number }) => {
|
||||
const prependHistory = useCallback((
|
||||
raw: AgentMessageItem[],
|
||||
agentId: string,
|
||||
meta: { total: number; offset: number; contextWindowTokens?: number },
|
||||
) => {
|
||||
const older = convertMessages(raw, agentId);
|
||||
setMessages((prev) => [...older, ...prev]);
|
||||
setHasMore(meta.offset > 0);
|
||||
if (meta.contextWindowTokens !== undefined) {
|
||||
setContextWindowTokens(meta.contextWindowTokens);
|
||||
}
|
||||
}, [convertMessages]);
|
||||
|
||||
/** Add a user message */
|
||||
|
|
@ -274,6 +289,7 @@ export function useChat() {
|
|||
streamingIds,
|
||||
isStreaming,
|
||||
hasMore,
|
||||
contextWindowTokens,
|
||||
pendingApprovals,
|
||||
error,
|
||||
// State control (for transport layer to call)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
|
|||
chat.setHistory(result.messages, agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
});
|
||||
offsetRef.current = result.offset;
|
||||
})
|
||||
|
|
@ -100,6 +101,7 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
|
|||
chat.prependHistory(result.messages, agentId, {
|
||||
total: result.total,
|
||||
offset: result.offset,
|
||||
contextWindowTokens: result.contextWindowTokens,
|
||||
});
|
||||
offsetRef.current = result.offset;
|
||||
} catch {
|
||||
|
|
@ -125,6 +127,7 @@ export function useGatewayChat({ client, hubId, agentId }: UseGatewayChatOptions
|
|||
isLoadingHistory,
|
||||
isLoadingMore,
|
||||
hasMore: chat.hasMore,
|
||||
contextWindowTokens: chat.contextWindowTokens,
|
||||
error: chat.error,
|
||||
pendingApprovals: chat.pendingApprovals,
|
||||
sendMessage,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ export interface GetAgentMessagesResult {
|
|||
total: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
/** Context window size (tokens) used by this session */
|
||||
contextWindowTokens?: number;
|
||||
}
|
||||
|
||||
/** getHubInfo - no params needed */
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { useEditor, EditorContent, type Editor } from "@tiptap/react";
|
|||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { ArrowUp, Square } from "lucide-react";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@multica/ui/components/ui/hover-card";
|
||||
import { ArrowUp, Gauge, Square, TriangleAlert } from "lucide-react";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
import "./chat-input.css";
|
||||
|
||||
|
|
@ -15,6 +16,22 @@ export interface ChatInputRef {
|
|||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface ContextWindowUsage {
|
||||
usedTokens: number;
|
||||
totalTokens: number;
|
||||
availableTokens: number;
|
||||
usageRatio: number;
|
||||
usagePercent: number;
|
||||
isEstimated?: boolean;
|
||||
lastCompaction?: {
|
||||
removed: number;
|
||||
kept: number;
|
||||
tokensRemoved?: number;
|
||||
tokensKept?: number;
|
||||
reason: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChatInputProps {
|
||||
onSubmit?: (value: string) => void;
|
||||
onAbort?: () => void;
|
||||
|
|
@ -23,10 +40,95 @@ interface ChatInputProps {
|
|||
placeholder?: string;
|
||||
/** Initial value to pre-fill the input */
|
||||
defaultValue?: string;
|
||||
/** Context usage stats shown in the input footer */
|
||||
contextWindowUsage?: ContextWindowUsage;
|
||||
}
|
||||
|
||||
function formatTokenCount(tokens: number): string {
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
if (tokens >= 10_000) return `${Math.round(tokens / 1000)}k`;
|
||||
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
|
||||
return `${tokens}`;
|
||||
}
|
||||
|
||||
function resolveUsageTone(ratio: number): {
|
||||
dotClass: string;
|
||||
textClass: string;
|
||||
} {
|
||||
if (ratio >= 0.9) {
|
||||
return { dotClass: "bg-destructive", textClass: "text-destructive" };
|
||||
}
|
||||
if (ratio >= 0.75) {
|
||||
return { dotClass: "bg-foreground/80", textClass: "text-foreground" };
|
||||
}
|
||||
return { dotClass: "bg-muted-foreground/60", textClass: "text-muted-foreground" };
|
||||
}
|
||||
|
||||
function ContextWindowIndicator({ usage }: { usage: ContextWindowUsage }) {
|
||||
const ratio = Math.max(0, usage.usageRatio);
|
||||
const usagePercent = Math.max(0, usage.usagePercent);
|
||||
const clampedPercent = Math.min(100, usagePercent);
|
||||
const tone = resolveUsageTone(ratio);
|
||||
const usedTokens = formatTokenCount(usage.usedTokens);
|
||||
const totalTokens = formatTokenCount(usage.totalTokens);
|
||||
const availableTokens = formatTokenCount(Math.max(0, usage.availableTokens));
|
||||
const compactionFreed = usage.lastCompaction?.tokensRemoved;
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger
|
||||
className={cn(
|
||||
"inline-flex h-8 items-center gap-1.5 rounded-md border border-border/70 px-2 text-[11px] font-medium transition-colors hover:bg-muted/50",
|
||||
tone.textClass,
|
||||
)}
|
||||
>
|
||||
<span className={cn("size-1.5 rounded-full", tone.dotClass)} />
|
||||
<Gauge className="size-3.5" />
|
||||
<span>{clampedPercent}%</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="top" align="start" className="w-72 space-y-3 rounded-xl p-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">Context window</p>
|
||||
<p className={cn("text-2xl font-semibold leading-none", ratio >= 0.9 && "text-destructive")}>
|
||||
{clampedPercent}% full
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{usedTokens} / {totalTokens} tokens used{usage.isEstimated ? " (est.)" : ""}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-1.5 overflow-hidden rounded-full bg-muted/80">
|
||||
<div
|
||||
className={cn("h-full rounded-full transition-[width]", tone.dotClass)}
|
||||
style={{ width: `${Math.min(100, Math.max(1, clampedPercent))}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{availableTokens} tokens left</span>
|
||||
<span>
|
||||
{compactionFreed != null
|
||||
? `Last compaction: -${formatTokenCount(compactionFreed)}`
|
||||
: "Auto-compaction enabled"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{ratio > 1 && (
|
||||
<div className="flex items-start gap-1.5 rounded-md border border-destructive/30 bg-destructive/10 px-2 py-1.5 text-xs text-destructive">
|
||||
<TriangleAlert className="size-3.5 shrink-0" />
|
||||
<span>Context is over capacity. The next run will compact history.</span>
|
||||
</div>
|
||||
)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
||||
function ChatInput({ onSubmit, onAbort, isLoading, disabled, placeholder = "Type a message...", defaultValue }, ref) {
|
||||
function ChatInput(
|
||||
{ onSubmit, onAbort, isLoading, disabled, placeholder = "Type a message...", defaultValue, contextWindowUsage },
|
||||
ref,
|
||||
) {
|
||||
// Use refs to avoid stale closures in Tiptap keydown handler
|
||||
const onSubmitRef = useRef(onSubmit);
|
||||
onSubmitRef.current = onSubmit;
|
||||
|
|
@ -137,7 +239,12 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|||
disabled && "is-disabled cursor-not-allowed opacity-60",
|
||||
)}>
|
||||
<EditorContent className="min-h-12" editor={editor} />
|
||||
<div className="flex items-center justify-end pt-2">
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
{contextWindowUsage ? (
|
||||
<ContextWindowIndicator usage={contextWindowUsage} />
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Button size="icon" onClick={handleButtonClick} disabled={disabled && !showStop}>
|
||||
{showStop ? <Square className="size-4 fill-current" /> : <ArrowUp />}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useRef, useEffect, useCallback } from "react";
|
||||
import { useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Skeleton } from "@multica/ui/components/ui/skeleton";
|
||||
import { ChatInput } from "@multica/ui/components/chat-input";
|
||||
import { ChatInput, type ContextWindowUsage } from "@multica/ui/components/chat-input";
|
||||
import { MessageList } from "@multica/ui/components/message-list";
|
||||
import { MemoizedMarkdown } from "@multica/ui/components/markdown";
|
||||
import { MulticaIcon } from "@multica/ui/components/multica-icon";
|
||||
|
|
@ -33,6 +33,7 @@ export interface ChatViewProps {
|
|||
isLoadingHistory: boolean;
|
||||
isLoadingMore?: boolean;
|
||||
hasMore?: boolean;
|
||||
contextWindowTokens?: number;
|
||||
error: ChatViewError | null;
|
||||
pendingApprovals: ChatViewApproval[];
|
||||
sendMessage: (text: string) => void;
|
||||
|
|
@ -48,6 +49,96 @@ export interface ChatViewProps {
|
|||
bottomSlot?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;
|
||||
const CHARS_PER_TOKEN = 4;
|
||||
const ESTIMATION_SAFETY_MARGIN = 1.2;
|
||||
const MESSAGE_OVERHEAD_TOKENS = 12;
|
||||
const RESPONSE_RESERVE_TOKENS = 1024;
|
||||
|
||||
function safeJsonLength(value: unknown): number {
|
||||
try {
|
||||
return JSON.stringify(value)?.length ?? 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function estimateMessageChars(message: Message): number {
|
||||
let chars = 0;
|
||||
|
||||
for (const block of message.content) {
|
||||
if (block.type === "text") {
|
||||
chars += block.text?.length ?? 0;
|
||||
continue;
|
||||
}
|
||||
if (block.type === "thinking") {
|
||||
chars += block.thinking?.length ?? 0;
|
||||
continue;
|
||||
}
|
||||
if (block.type === "toolCall") {
|
||||
chars += (block.name?.length ?? 0) + safeJsonLength(block.arguments) + 32;
|
||||
continue;
|
||||
}
|
||||
if (block.type === "image") {
|
||||
// Image blocks add prompt/metadata overhead even without inline text.
|
||||
chars += 512;
|
||||
continue;
|
||||
}
|
||||
chars += safeJsonLength(block);
|
||||
}
|
||||
|
||||
if (message.toolArgs) {
|
||||
chars += safeJsonLength(message.toolArgs);
|
||||
}
|
||||
if (message.toolName) {
|
||||
chars += message.toolName.length;
|
||||
}
|
||||
|
||||
return chars;
|
||||
}
|
||||
|
||||
function deriveContextWindowUsage(
|
||||
messages: Message[],
|
||||
contextWindowTokens?: number,
|
||||
): ContextWindowUsage {
|
||||
const totalTokens = Math.max(1, contextWindowTokens ?? DEFAULT_CONTEXT_WINDOW_TOKENS);
|
||||
const contextMessages = messages.filter((message) => message.role !== "system");
|
||||
const baseReserve = contextMessages.length > 0 ? RESPONSE_RESERVE_TOKENS : 0;
|
||||
|
||||
let estimatedUsedTokens = baseReserve;
|
||||
for (const message of contextMessages) {
|
||||
const chars = estimateMessageChars(message);
|
||||
const tokenEstimate = Math.ceil((chars / CHARS_PER_TOKEN) * ESTIMATION_SAFETY_MARGIN);
|
||||
estimatedUsedTokens += tokenEstimate + MESSAGE_OVERHEAD_TOKENS;
|
||||
}
|
||||
|
||||
let lastCompaction: ContextWindowUsage["lastCompaction"];
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (message?.systemType === "compaction" && message.compaction) {
|
||||
lastCompaction = {
|
||||
removed: message.compaction.removed,
|
||||
kept: message.compaction.kept,
|
||||
tokensRemoved: message.compaction.tokensRemoved,
|
||||
tokensKept: message.compaction.tokensKept,
|
||||
reason: message.compaction.reason,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const usageRatio = estimatedUsedTokens / totalTokens;
|
||||
return {
|
||||
usedTokens: estimatedUsedTokens,
|
||||
totalTokens,
|
||||
availableTokens: Math.max(0, totalTokens - estimatedUsedTokens),
|
||||
usageRatio,
|
||||
usagePercent: Math.round(usageRatio * 100),
|
||||
isEstimated: true,
|
||||
lastCompaction,
|
||||
};
|
||||
}
|
||||
|
||||
export function ChatView({
|
||||
messages,
|
||||
streamingIds,
|
||||
|
|
@ -55,6 +146,7 @@ export function ChatView({
|
|||
isLoadingHistory,
|
||||
isLoadingMore = false,
|
||||
hasMore = false,
|
||||
contextWindowTokens,
|
||||
error,
|
||||
pendingApprovals,
|
||||
sendMessage,
|
||||
|
|
@ -70,6 +162,10 @@ export function ChatView({
|
|||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const fadeStyle = useScrollFade(mainRef);
|
||||
const { suppressAutoScroll } = useAutoScroll(mainRef);
|
||||
const contextWindowUsage = useMemo(
|
||||
() => deriveContextWindowUsage(messages, contextWindowTokens),
|
||||
[messages, contextWindowTokens],
|
||||
);
|
||||
|
||||
// scrollHeight compensation for prepended messages
|
||||
const prevScrollHeightRef = useRef(0);
|
||||
|
|
@ -277,6 +373,7 @@ export function ChatView({
|
|||
disabled={!!error && error.code !== 'AGENT_ERROR'}
|
||||
placeholder={error && error.code !== 'AGENT_ERROR' ? "Connection error" : "Ask your Agent..."}
|
||||
defaultValue={initialPrompt}
|
||||
contextWindowUsage={contextWindowUsage}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue