feat: display context usage in UI for Codex and Claude Code (#1775)
* feat: display context usage in UI for Codex executor - Extract token usage data from Codex execution logs. - Track usage state within the `EntriesContext`. - Display a context usage progress bar in the next action card. * Add claude-code context token usage * fix type issue --------- Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
This commit is contained in:
@@ -338,6 +338,9 @@ pub enum HistoryStrategy {
|
||||
AmpResume,
|
||||
}
|
||||
|
||||
/// Default context window for models (used until we get actual value from result)
|
||||
const DEFAULT_CLAUDE_CONTEXT_WINDOW: u32 = 200_000;
|
||||
|
||||
/// Handles log processing and interpretation for Claude executor
|
||||
pub struct ClaudeLogProcessor {
|
||||
model_name: Option<String>,
|
||||
@@ -347,6 +350,10 @@ pub struct ClaudeLogProcessor {
|
||||
strategy: HistoryStrategy,
|
||||
streaming_messages: HashMap<String, StreamingMessageState>,
|
||||
streaming_message_id: Option<String>,
|
||||
// Main model name (excluding subagents). Only used internally for context window tracking.
|
||||
main_model_name: Option<String>,
|
||||
main_model_context_window: u32,
|
||||
context_tokens_used: u32,
|
||||
}
|
||||
|
||||
impl ClaudeLogProcessor {
|
||||
@@ -358,10 +365,13 @@ impl ClaudeLogProcessor {
|
||||
fn new_with_strategy(strategy: HistoryStrategy) -> Self {
|
||||
Self {
|
||||
model_name: None,
|
||||
main_model_name: None,
|
||||
tool_map: HashMap::new(),
|
||||
strategy,
|
||||
streaming_messages: HashMap::new(),
|
||||
streaming_message_id: None,
|
||||
main_model_context_window: DEFAULT_CLAUDE_CONTEXT_WINDOW,
|
||||
context_tokens_used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,6 +779,7 @@ impl ClaudeLogProcessor {
|
||||
ClaudeJson::System {
|
||||
subtype,
|
||||
api_key_source,
|
||||
model,
|
||||
..
|
||||
} => {
|
||||
// emit billing warning if required
|
||||
@@ -780,6 +791,12 @@ impl ClaudeLogProcessor {
|
||||
// keep the existing behaviour for the normal system message
|
||||
match subtype.as_deref() {
|
||||
Some("init") => {
|
||||
if self.main_model_name.is_none() {
|
||||
// this name matches the model names in the usage report in the result message
|
||||
if let Some(model) = model {
|
||||
self.main_model_name = Some(model.clone());
|
||||
}
|
||||
}
|
||||
// Skip system init messages because it doesn't contain the actual model that will be used in assistant messages in case of claude-code-router.
|
||||
// We'll send system initialized message with first assistant message that has a model field.
|
||||
}
|
||||
@@ -1103,7 +1120,11 @@ impl ClaudeLogProcessor {
|
||||
ClaudeJson::ToolResult { .. } => {
|
||||
// Add proper ToolResult support to NormalizedEntry when the type system supports it
|
||||
}
|
||||
ClaudeJson::StreamEvent { event, .. } => match event {
|
||||
ClaudeJson::StreamEvent {
|
||||
event,
|
||||
parent_tool_use_id,
|
||||
..
|
||||
} => match event {
|
||||
ClaudeStreamEvent::MessageStart { message } => {
|
||||
if message.role == "assistant" {
|
||||
if let Some(patch) = extract_model_name(self, message, entry_index_provider)
|
||||
@@ -1152,7 +1173,21 @@ impl ClaudeLogProcessor {
|
||||
}
|
||||
}
|
||||
ClaudeStreamEvent::ContentBlockStop { .. } => {}
|
||||
ClaudeStreamEvent::MessageDelta { .. } => {}
|
||||
ClaudeStreamEvent::MessageDelta { usage, .. } => {
|
||||
// do not report context token usage for subagents
|
||||
if parent_tool_use_id.is_none()
|
||||
&& let Some(usage) = usage
|
||||
{
|
||||
let input_tokens = usage.input_tokens.unwrap_or(0)
|
||||
+ usage.cache_creation_input_tokens.unwrap_or(0)
|
||||
+ usage.cache_read_input_tokens.unwrap_or(0);
|
||||
let output_tokens = usage.output_tokens.unwrap_or(0);
|
||||
let total_tokens = input_tokens + output_tokens;
|
||||
self.context_tokens_used = total_tokens as u32;
|
||||
|
||||
patches.push(self.add_token_usage_entry(entry_index_provider));
|
||||
}
|
||||
}
|
||||
ClaudeStreamEvent::MessageStop => {
|
||||
if let Some(message_id) = self.streaming_message_id.take() {
|
||||
let _ = self.streaming_messages.remove(&message_id);
|
||||
@@ -1160,7 +1195,22 @@ impl ClaudeLogProcessor {
|
||||
}
|
||||
ClaudeStreamEvent::Unknown => {}
|
||||
},
|
||||
ClaudeJson::Result { is_error, .. } => {
|
||||
ClaudeJson::Result {
|
||||
is_error,
|
||||
model_usage,
|
||||
..
|
||||
} => {
|
||||
// get the real model context window and correct the context usage entry
|
||||
if let Some(context_window) = model_usage.as_ref().and_then(|model_usage| {
|
||||
self.main_model_name
|
||||
.as_ref()
|
||||
.and_then(|name| model_usage.get(name))
|
||||
.and_then(|usage| usage.context_window)
|
||||
}) {
|
||||
self.main_model_context_window = context_window;
|
||||
patches.push(self.add_token_usage_entry(entry_index_provider));
|
||||
}
|
||||
|
||||
if matches!(self.strategy, HistoryStrategy::AmpResume) && is_error.unwrap_or(false)
|
||||
{
|
||||
let entry = NormalizedEntry {
|
||||
@@ -1325,6 +1375,26 @@ impl ClaudeLogProcessor {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn add_token_usage_entry(
|
||||
&mut self,
|
||||
entry_index_provider: &EntryIndexProvider,
|
||||
) -> json_patch::Patch {
|
||||
let entry = NormalizedEntry {
|
||||
timestamp: None,
|
||||
entry_type: NormalizedEntryType::TokenUsageInfo(crate::logs::TokenUsageInfo {
|
||||
total_tokens: self.context_tokens_used,
|
||||
model_context_window: self.main_model_context_window,
|
||||
}),
|
||||
content: format!(
|
||||
"Tokens used: {} / Context window: {}",
|
||||
self.context_tokens_used, self.main_model_context_window
|
||||
),
|
||||
metadata: None,
|
||||
};
|
||||
let idx = entry_index_provider.next();
|
||||
ConversationPatch::add_normalized_entry(idx, entry)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_model_name(
|
||||
@@ -1542,6 +1612,10 @@ pub enum ClaudeJson {
|
||||
num_turns: Option<u32>,
|
||||
#[serde(default, alias = "sessionId")]
|
||||
session_id: Option<String>,
|
||||
#[serde(default, alias = "modelUsage")]
|
||||
model_usage: Option<HashMap<String, ClaudeModelUsage>>,
|
||||
#[serde(default)]
|
||||
usage: Option<ClaudeUsage>,
|
||||
},
|
||||
ApprovalResponse {
|
||||
call_id: String,
|
||||
@@ -1661,6 +1735,14 @@ pub struct ClaudeUsage {
|
||||
pub service_tier: Option<String>,
|
||||
}
|
||||
|
||||
/// Per-model usage statistics from result message
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ClaudeModelUsage {
|
||||
#[serde(default)]
|
||||
pub context_window: Option<u32>,
|
||||
}
|
||||
|
||||
/// Structured tool data for Claude tools based on real samples
|
||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
||||
#[serde(tag = "name", content = "input")]
|
||||
|
||||
@@ -17,7 +17,7 @@ use codex_protocol::{
|
||||
ErrorEvent, EventMsg, ExecApprovalRequestEvent, ExecCommandBeginEvent, ExecCommandEndEvent,
|
||||
ExecCommandOutputDeltaEvent, ExecOutputStream, FileChange as CodexProtoFileChange,
|
||||
McpInvocation, McpToolCallBeginEvent, McpToolCallEndEvent, PatchApplyBeginEvent,
|
||||
PatchApplyEndEvent, StreamErrorEvent, TokenUsageInfo, ViewImageToolCallEvent, WarningEvent,
|
||||
PatchApplyEndEvent, StreamErrorEvent, ViewImageToolCallEvent, WarningEvent,
|
||||
WebSearchBeginEvent, WebSearchEndEvent,
|
||||
},
|
||||
};
|
||||
@@ -215,7 +215,6 @@ struct LogState {
|
||||
mcp_tools: HashMap<String, McpToolState>,
|
||||
patches: HashMap<String, PatchState>,
|
||||
web_searches: HashMap<String, WebSearchState>,
|
||||
token_usage_info: Option<TokenUsageInfo>,
|
||||
}
|
||||
|
||||
enum StreamingTextKind {
|
||||
@@ -233,7 +232,6 @@ impl LogState {
|
||||
mcp_tools: HashMap::new(),
|
||||
patches: HashMap::new(),
|
||||
web_searches: HashMap::new(),
|
||||
token_usage_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,7 +969,28 @@ pub fn normalize_logs(msg_store: Arc<MsgStore>, worktree_path: &Path) {
|
||||
}
|
||||
EventMsg::TokenCount(payload) => {
|
||||
if let Some(info) = payload.info {
|
||||
state.token_usage_info = Some(info);
|
||||
add_normalized_entry(
|
||||
&msg_store,
|
||||
&entry_index,
|
||||
NormalizedEntry {
|
||||
timestamp: None,
|
||||
entry_type: NormalizedEntryType::TokenUsageInfo(
|
||||
crate::logs::TokenUsageInfo {
|
||||
total_tokens: info.last_token_usage.total_tokens as u32,
|
||||
model_context_window: info
|
||||
.model_context_window
|
||||
.unwrap_or_default()
|
||||
as u32,
|
||||
},
|
||||
),
|
||||
content: format!(
|
||||
"Tokens used: {} / Context window: {}",
|
||||
info.last_token_usage.total_tokens,
|
||||
info.model_context_window.unwrap_or_default()
|
||||
),
|
||||
metadata: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
EventMsg::ContextCompacted(..) => {
|
||||
|
||||
@@ -95,6 +95,13 @@ pub enum NormalizedEntryType {
|
||||
execution_processes: usize,
|
||||
needs_setup: bool,
|
||||
},
|
||||
TokenUsageInfo(TokenUsageInfo),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
pub struct TokenUsageInfo {
|
||||
pub total_tokens: u32,
|
||||
pub model_context_window: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
|
||||
@@ -207,6 +207,7 @@ fn generate_types_content() -> String {
|
||||
executors::logs::CommandRunResult::decl(),
|
||||
executors::logs::NormalizedEntry::decl(),
|
||||
executors::logs::NormalizedEntryType::decl(),
|
||||
executors::logs::TokenUsageInfo::decl(),
|
||||
executors::logs::FileChange::decl(),
|
||||
executors::logs::ActionType::decl(),
|
||||
executors::logs::TodoItem::decl(),
|
||||
|
||||
@@ -733,9 +733,14 @@ function DisplayConversationEntry({
|
||||
const isUserMessage = entryType.type === 'user_message';
|
||||
const isUserFeedback = entryType.type === 'user_feedback';
|
||||
const isLoading = entryType.type === 'loading';
|
||||
const isTokenUsage = entryType.type === 'token_usage_info';
|
||||
const isFileEdit = (a: ActionType): a is FileEditAction =>
|
||||
a.action === 'file_edit';
|
||||
|
||||
if (isTokenUsage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isUserMessage) {
|
||||
return (
|
||||
<UserMessage
|
||||
|
||||
@@ -313,6 +313,10 @@ function NewDisplayConversationEntry(props: Props) {
|
||||
// The new design doesn't need the next action bar
|
||||
return null;
|
||||
|
||||
case 'token_usage_info':
|
||||
// Displayed in the chat header as the context-usage gauge
|
||||
return null;
|
||||
|
||||
case 'user_feedback':
|
||||
case 'loading':
|
||||
// Fallback to legacy component for these entry types
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useExecutionProcesses } from '@/hooks/useExecutionProcesses';
|
||||
import { useUserSystem } from '@/components/ConfigProvider';
|
||||
import { useApprovalFeedbackOptional } from '@/contexts/ApprovalFeedbackContext';
|
||||
import { useMessageEditContext } from '@/contexts/MessageEditContext';
|
||||
import { useEntries } from '@/contexts/EntriesContext';
|
||||
import { useEntries, useTokenUsage } from '@/contexts/EntriesContext';
|
||||
import { useReviewOptional } from '@/contexts/ReviewProvider';
|
||||
import { useActions } from '@/contexts/ActionsContext';
|
||||
import { useTodos } from '@/hooks/useTodos';
|
||||
@@ -115,6 +115,7 @@ export function SessionChatBoxContainer({
|
||||
|
||||
// Get entries early to extract pending approval for scratch key
|
||||
const { entries } = useEntries();
|
||||
const tokenUsageInfo = useTokenUsage();
|
||||
|
||||
// Extract pending approval metadata from entries (needed for scratchId)
|
||||
const pendingApproval = useMemo(() => {
|
||||
@@ -567,6 +568,7 @@ export function SessionChatBoxContainer({
|
||||
status="idle"
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
tokenUsageInfo={tokenUsageInfo}
|
||||
editor={{
|
||||
value: '',
|
||||
onChange: () => {},
|
||||
@@ -601,6 +603,7 @@ export function SessionChatBoxContainer({
|
||||
onViewCode={handleViewCode}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
tokenUsageInfo={tokenUsageInfo}
|
||||
editor={{
|
||||
value: editorValue,
|
||||
onChange: handleEditorChange,
|
||||
|
||||
136
frontend/src/components/ui-new/primitives/ContextUsageGauge.tsx
Normal file
136
frontend/src/components/ui-new/primitives/ContextUsageGauge.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { TokenUsageInfo } from 'shared/types';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export interface ContextUsageGaugeProps {
|
||||
tokenUsageInfo?: TokenUsageInfo | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ContextUsageGauge({
|
||||
tokenUsageInfo,
|
||||
className,
|
||||
}: ContextUsageGaugeProps) {
|
||||
const { t } = useTranslation('common');
|
||||
const { percentage, formattedUsed, formattedTotal, status } = useMemo(() => {
|
||||
if (!tokenUsageInfo || tokenUsageInfo.model_context_window === 0) {
|
||||
return {
|
||||
percentage: 0,
|
||||
formattedUsed: '0',
|
||||
formattedTotal: '0',
|
||||
status: 'empty' as const,
|
||||
};
|
||||
}
|
||||
|
||||
const pct = Math.min(
|
||||
100,
|
||||
(tokenUsageInfo.total_tokens / tokenUsageInfo.model_context_window) * 100
|
||||
);
|
||||
|
||||
const formatTokens = (n: number) => {
|
||||
if (n >= 1_000_000) {
|
||||
const m = n / 1_000_000;
|
||||
return m % 1 === 0 ? `${m}M` : `${m.toFixed(1)}M`;
|
||||
}
|
||||
if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
|
||||
return n.toString();
|
||||
};
|
||||
|
||||
let statusValue: 'low' | 'medium' | 'high' | 'critical' | 'empty';
|
||||
if (pct < 50) statusValue = 'low';
|
||||
else if (pct < 75) statusValue = 'medium';
|
||||
else if (pct < 90) statusValue = 'high';
|
||||
else statusValue = 'critical';
|
||||
|
||||
return {
|
||||
percentage: pct,
|
||||
formattedUsed: formatTokens(tokenUsageInfo.total_tokens),
|
||||
formattedTotal: formatTokens(tokenUsageInfo.model_context_window),
|
||||
status: statusValue,
|
||||
};
|
||||
}, [tokenUsageInfo]);
|
||||
|
||||
const progress = clamp(percentage / 100, 0, 1);
|
||||
|
||||
const tooltip =
|
||||
status === 'empty'
|
||||
? t('contextUsage.emptyTooltip')
|
||||
: t('contextUsage.tooltip', {
|
||||
percentage: Math.round(percentage),
|
||||
used: formattedUsed,
|
||||
total: formattedTotal,
|
||||
});
|
||||
|
||||
const progressColor =
|
||||
status === 'empty'
|
||||
? 'text-low/40'
|
||||
: status === 'critical'
|
||||
? 'text-error'
|
||||
: status === 'high'
|
||||
? 'text-brand-secondary'
|
||||
: status === 'medium'
|
||||
? 'text-normal'
|
||||
: 'text-low';
|
||||
|
||||
const radius = 8;
|
||||
const strokeWidth = 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const dashOffset = circumference * (1 - progress);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} side="bottom">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-sm p-half',
|
||||
'hover:bg-panel transition-colors cursor-help',
|
||||
className
|
||||
)}
|
||||
aria-label={
|
||||
status === 'empty'
|
||||
? t('contextUsage.label')
|
||||
: t('contextUsage.ariaLabel', {
|
||||
percentage: Math.round(percentage),
|
||||
})
|
||||
}
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
className="size-icon-base -rotate-90"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
className="text-border/60"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${circumference} ${circumference}`}
|
||||
strokeDashoffset={dashOffset}
|
||||
className={cn(
|
||||
progressColor,
|
||||
'transition-all duration-500 ease-out'
|
||||
)}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,12 @@ import {
|
||||
WarningIcon,
|
||||
} from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Session, BaseCodingAgent, TodoItem } from 'shared/types';
|
||||
import type {
|
||||
BaseCodingAgent,
|
||||
Session,
|
||||
TodoItem,
|
||||
TokenUsageInfo,
|
||||
} from 'shared/types';
|
||||
import type { LocalImageMetadata } from '@/components/ui/wysiwyg/context/task-attempt-context';
|
||||
import { formatDateShortWithTime } from '@/utils/date';
|
||||
import { toPrettyCase } from '@/utils/string';
|
||||
@@ -36,6 +41,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
} from './Dropdown';
|
||||
import { type ExecutorProps } from './CreateChatBox';
|
||||
import { ContextUsageGauge } from './ContextUsageGauge';
|
||||
|
||||
// Re-export shared types
|
||||
export type { EditorProps, VariantProps } from './ChatBoxBase';
|
||||
@@ -135,6 +141,7 @@ interface SessionChatBoxProps {
|
||||
inProgressTodo?: TodoItem | null;
|
||||
localImages?: LocalImageMetadata[];
|
||||
onViewCode?: () => void;
|
||||
tokenUsageInfo?: TokenUsageInfo | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +168,7 @@ export function SessionChatBox({
|
||||
inProgressTodo,
|
||||
localImages,
|
||||
onViewCode,
|
||||
tokenUsageInfo,
|
||||
}: SessionChatBoxProps) {
|
||||
const { t } = useTranslation('tasks');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -585,6 +593,7 @@ export function SessionChatBox({
|
||||
{!isNewSessionMode && (
|
||||
<AgentIcon agent={agent} className="size-icon-xl" />
|
||||
)}
|
||||
<ContextUsageGauge tokenUsageInfo={tokenUsageInfo} />
|
||||
<ToolbarDropdown
|
||||
label={sessionLabel}
|
||||
disabled={isInFeedbackMode || isInEditMode || isInApprovalMode}
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import type { PatchTypeWithKey } from '@/hooks/useConversationHistory';
|
||||
import { NormalizedEntry, TokenUsageInfo } from 'shared/types';
|
||||
|
||||
interface EntriesContextType {
|
||||
entries: PatchTypeWithKey[];
|
||||
setEntries: (entries: PatchTypeWithKey[]) => void;
|
||||
reset: () => void;
|
||||
tokenUsageInfo: TokenUsageInfo | null;
|
||||
}
|
||||
|
||||
const EntriesContext = createContext<EntriesContextType | null>(null);
|
||||
@@ -22,13 +24,29 @@ interface EntriesProviderProps {
|
||||
|
||||
export const EntriesProvider = ({ children }: EntriesProviderProps) => {
|
||||
const [entries, setEntriesState] = useState<PatchTypeWithKey[]>([]);
|
||||
const [tokenUsageInfo, setTokenUsageInfo] = useState<TokenUsageInfo | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const extractTokenUsageInfo = (
|
||||
entries: PatchTypeWithKey[]
|
||||
): TokenUsageInfo | null => {
|
||||
const latest = entries.findLast(
|
||||
(e) =>
|
||||
e.type === 'NORMALIZED_ENTRY' &&
|
||||
e.content.entry_type.type === 'token_usage_info'
|
||||
)?.content as NormalizedEntry | undefined;
|
||||
return (latest?.entry_type as TokenUsageInfo) ?? null;
|
||||
};
|
||||
|
||||
const setEntries = useCallback((newEntries: PatchTypeWithKey[]) => {
|
||||
setEntriesState(newEntries);
|
||||
setTokenUsageInfo(extractTokenUsageInfo(newEntries));
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setEntriesState([]);
|
||||
setTokenUsageInfo(null);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
@@ -36,8 +54,9 @@ export const EntriesProvider = ({ children }: EntriesProviderProps) => {
|
||||
entries,
|
||||
setEntries,
|
||||
reset,
|
||||
tokenUsageInfo,
|
||||
}),
|
||||
[entries, setEntries, reset]
|
||||
[entries, setEntries, reset, tokenUsageInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -52,3 +71,11 @@ export const useEntries = (): EntriesContextType => {
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useTokenUsage = () => {
|
||||
const context = useContext(EntriesContext);
|
||||
if (!context) {
|
||||
throw new Error('useTokenUsage must be used within an EntriesProvider');
|
||||
}
|
||||
return context.tokenUsageInfo;
|
||||
};
|
||||
|
||||
@@ -279,5 +279,11 @@
|
||||
"search": {
|
||||
"matchCount": "{{current}} of {{total}}",
|
||||
"noMatches": "No matches"
|
||||
},
|
||||
"contextUsage": {
|
||||
"label": "Context usage",
|
||||
"emptyTooltip": "Context usage appears after the next reply",
|
||||
"tooltip": "Context: {{percentage}}% · {{used}} / {{total}} tokens",
|
||||
"ariaLabel": "Context usage: {{percentage}}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,5 +279,11 @@
|
||||
"search": {
|
||||
"matchCount": "{{current}} de {{total}}",
|
||||
"noMatches": "Sin coincidencias"
|
||||
},
|
||||
"contextUsage": {
|
||||
"label": "Uso del contexto",
|
||||
"emptyTooltip": "El uso del contexto aparece después de la próxima respuesta",
|
||||
"tooltip": "Contexto: {{percentage}}% · {{used}} / {{total}} tokens",
|
||||
"ariaLabel": "Uso del contexto: {{percentage}}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,5 +279,11 @@
|
||||
"search": {
|
||||
"matchCount": "{{current}} / {{total}}",
|
||||
"noMatches": "一致なし"
|
||||
},
|
||||
"contextUsage": {
|
||||
"label": "コンテキスト使用量",
|
||||
"emptyTooltip": "コンテキスト使用量は次の返信後に表示されます",
|
||||
"tooltip": "コンテキスト: {{percentage}}% · {{used}} / {{total}} tokens",
|
||||
"ariaLabel": "コンテキスト使用量: {{percentage}}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,5 +279,11 @@
|
||||
"search": {
|
||||
"matchCount": "{{current}} / {{total}}",
|
||||
"noMatches": "일치 항목 없음"
|
||||
},
|
||||
"contextUsage": {
|
||||
"label": "컨텍스트 사용량",
|
||||
"emptyTooltip": "컨텍스트 사용량은 다음 응답 후에 표시됩니다",
|
||||
"tooltip": "컨텍스트: {{percentage}}% · {{used}} / {{total}} 토큰",
|
||||
"ariaLabel": "컨텍스트 사용량: {{percentage}}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,5 +279,11 @@
|
||||
"search": {
|
||||
"matchCount": "{{current}} / {{total}}",
|
||||
"noMatches": "无匹配项"
|
||||
},
|
||||
"contextUsage": {
|
||||
"label": "上下文使用量",
|
||||
"emptyTooltip": "上下文使用量将在下一次回复后显示",
|
||||
"tooltip": "上下文:{{percentage}}% · {{used}} / {{total}} tokens",
|
||||
"ariaLabel": "上下文使用量:{{percentage}}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,5 +279,11 @@
|
||||
"search": {
|
||||
"matchCount": "{{current}} / {{total}}",
|
||||
"noMatches": "無匹配項"
|
||||
},
|
||||
"contextUsage": {
|
||||
"label": "上下文使用量",
|
||||
"emptyTooltip": "上下文使用量將在下一次回覆後顯示",
|
||||
"tooltip": "上下文:{{percentage}}% · {{used}} / {{total}} tokens",
|
||||
"ariaLabel": "上下文使用量:{{percentage}}%"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,7 +982,7 @@ export function ProjectTasks() {
|
||||
actions={
|
||||
<Button
|
||||
variant="icon"
|
||||
aria-label={t('common:buttons.close', { defaultValue: 'Close' })}
|
||||
aria-label={t('common:buttons.close')}
|
||||
onClick={() => {
|
||||
setSelectedSharedTaskId(null);
|
||||
if (projectId) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -543,7 +543,9 @@ export type CommandRunResult = { exit_status: CommandExitStatus | null, output:
|
||||
|
||||
export type NormalizedEntry = { timestamp: string | null, entry_type: NormalizedEntryType, content: string, };
|
||||
|
||||
export type NormalizedEntryType = { "type": "user_message" } | { "type": "user_feedback", denied_tool: string, } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, status: ToolStatus, } | { "type": "system_message" } | { "type": "error_message", error_type: NormalizedEntryError, } | { "type": "thinking" } | { "type": "loading" } | { "type": "next_action", failed: boolean, execution_processes: number, needs_setup: boolean, };
|
||||
export type NormalizedEntryType = { "type": "user_message" } | { "type": "user_feedback", denied_tool: string, } | { "type": "assistant_message" } | { "type": "tool_use", tool_name: string, action_type: ActionType, status: ToolStatus, } | { "type": "system_message" } | { "type": "error_message", error_type: NormalizedEntryError, } | { "type": "thinking" } | { "type": "loading" } | { "type": "next_action", failed: boolean, execution_processes: number, needs_setup: boolean, } | { "type": "token_usage_info" } & TokenUsageInfo;
|
||||
|
||||
export type TokenUsageInfo = { total_tokens: number, model_context_window: number, };
|
||||
|
||||
export type FileChange = { "action": "write", content: string, } | { "action": "delete" } | { "action": "rename", new_path: string, } | { "action": "edit",
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user