From 177631f1234daeee9b8f79e1c8b30c27d94267ac Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 26 Apr 2026 20:20:23 +0300 Subject: [PATCH] fix: improve VS Code header context and settings visibility Show language selection in VS Code settings Display context usage in the VS Code chat header Keep VS Code header usage controls visible in expanded layouts --- .../ui/src/components/layout/VSCodeLayout.tsx | 125 +++++++++++++++--- .../openchamber/OpenChamberVisualSettings.tsx | 8 +- packages/ui/src/sync/session-ui-store.ts | 1 - 3 files changed, 115 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/components/layout/VSCodeLayout.tsx b/packages/ui/src/components/layout/VSCodeLayout.tsx index e56fdee9..53f604a5 100644 --- a/packages/ui/src/components/layout/VSCodeLayout.tsx +++ b/packages/ui/src/components/layout/VSCodeLayout.tsx @@ -4,7 +4,7 @@ import { SessionSidebar } from '@/components/session/SessionSidebar'; import { ChatView } from '@/components/views'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useViewportStore } from '@/sync/viewport-store'; -import { useSessions, useDirectorySync } from '@/sync/sync-context'; +import { useSessions, useDirectorySync, useSessionMessages, useSessionMessagesResolved } from '@/sync/sync-context'; import { useConfigStore } from '@/stores/useConfigStore'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { McpDropdown } from '@/components/mcp/McpDropdown'; @@ -27,6 +27,7 @@ import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore'; import { updateDesktopSettings } from '@/lib/persistence'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; import type { UsageWindow } from '@/types'; +import type { SessionContextUsage } from '@/stores/types/sessionTypes'; import { RiAddLine, RiArrowLeftLine, RiRefreshLine, RiRobot2Line, RiSettings3Line, RiTimerLine } from '@remixicon/react'; const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView }))); @@ -384,6 +385,7 @@ export const VSCodeLayout: React.FC = () => { title={sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')} showMcp showContextUsage + showRateLimits />
@@ -435,6 +437,7 @@ export const VSCodeLayout: React.FC = () => { : sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')} showMcp showContextUsage + showRateLimits />
@@ -499,8 +502,11 @@ interface VSCodeHeaderProps { const VSCodeHeader: React.FC = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits }) => { const { t } = useI18n(); - const getCurrentModel = useConfigStore((s) => s.getCurrentModel); - const getContextUsage = useSessionUIStore((state) => state.getContextUsage); + const getCurrentModel = useConfigStore((state) => state.getCurrentModel); + const providers = useConfigStore((state) => state.providers); + const currentSessionId = useSessionUIStore((state) => state.currentSessionId); + const currentSessionMessages = useSessionMessages(currentSessionId ?? ''); + const currentSessionMessagesResolved = useSessionMessagesResolved(currentSessionId ?? ''); const quotaResults = useQuotaStore((state) => state.results); const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas); const isQuotaLoading = useQuotaStore((state) => state.isLoading); @@ -517,12 +523,97 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on }, [loadQuotaSettings]); const currentModel = getCurrentModel(); - const limits = (currentModel?.limit && typeof currentModel.limit === 'object' - ? currentModel.limit - : null) as { context?: number; output?: number } | null; - const contextLimit = typeof limits?.context === 'number' ? limits.context : 0; - const outputLimit = typeof limits?.output === 'number' ? limits.output : 0; - const contextUsage = getContextUsage(contextLimit, outputLimit); + const latestAssistantModel = React.useMemo(() => { + for (let i = currentSessionMessages.length - 1; i >= 0; i -= 1) { + const message = currentSessionMessages[i] as { role?: unknown; providerID?: unknown; modelID?: unknown }; + if (message.role !== 'assistant') continue; + if (typeof message.providerID !== 'string' || typeof message.modelID !== 'string') continue; + const provider = providers.find((entry) => entry.id === message.providerID); + const model = provider?.models.find((entry) => entry.id === message.modelID); + if (model) return model; + } + return undefined; + }, [currentSessionMessages, providers]); + const modelForLimits = currentModel?.limit ? currentModel : latestAssistantModel; + const limit = modelForLimits && typeof modelForLimits.limit === 'object' && modelForLimits.limit !== null + ? (modelForLimits.limit as Record) + : null; + const contextLimit = limit && typeof limit.context === 'number' ? limit.context : 0; + const outputLimit = limit && typeof limit.output === 'number' ? limit.output : 0; + + const contextUsage = React.useMemo(() => { + if (!currentSessionId || currentSessionMessages.length === 0) { + return null; + } + + type AssistantTokens = { input: number; output: number; reasoning: number; cache: { read: number; write: number } }; + let lastTokens: AssistantTokens | undefined; + let lastMessageId: string | undefined; + + for (let i = currentSessionMessages.length - 1; i >= 0; i -= 1) { + const message = currentSessionMessages[i]; + if (message.role !== 'assistant') continue; + const tokens = (message as { tokens?: AssistantTokens }).tokens; + if (!tokens) continue; + const total = tokens.input + tokens.output + tokens.reasoning + (tokens.cache?.read ?? 0) + (tokens.cache?.write ?? 0); + if (total > 0) { + lastTokens = tokens; + lastMessageId = message.id; + break; + } + } + + if (!lastTokens) { + return null; + } + + const totalTokens = lastTokens.input + lastTokens.output + lastTokens.reasoning + (lastTokens.cache?.read ?? 0) + (lastTokens.cache?.write ?? 0); + const thresholdLimit = contextLimit > 0 ? contextLimit : 200000; + const percentage = contextLimit > 0 ? Math.round((totalTokens / contextLimit) * 100) : 0; + const normalizedOutput = outputLimit > 0 ? Math.round((lastTokens.output / outputLimit) * 100) : undefined; + + return { + totalTokens, + percentage, + contextLimit: contextLimit || 0, + outputLimit: outputLimit || undefined, + normalizedOutput, + thresholdLimit, + lastMessageId, + }; + }, [contextLimit, currentSessionId, currentSessionMessages, outputLimit]); + const [stableContextUsage, setStableContextUsage] = React.useState(null); + const isContextUsageResolvedForSession = !currentSessionId || currentSessionMessagesResolved; + + React.useEffect(() => { + if (!currentSessionId) { + setStableContextUsage((prev) => (prev === null ? prev : null)); + return; + } + + if (contextUsage && contextUsage.totalTokens > 0) { + setStableContextUsage((prev) => { + if ( + prev + && prev.totalTokens === contextUsage.totalTokens + && prev.percentage === contextUsage.percentage + && prev.contextLimit === contextUsage.contextLimit + && (prev.outputLimit ?? 0) === (contextUsage.outputLimit ?? 0) + && (prev.normalizedOutput ?? 0) === (contextUsage.normalizedOutput ?? 0) + && prev.thresholdLimit === contextUsage.thresholdLimit + && prev.lastMessageId === contextUsage.lastMessageId + ) { + return prev; + } + return contextUsage; + }); + return; + } + + if (isContextUsageResolvedForSession) { + setStableContextUsage((prev) => (prev === null ? prev : null)); + } + }, [contextUsage, currentSessionId, isContextUsageResolvedForSession]); const rateLimitGroups = React.useMemo(() => { const groups: Array<{ @@ -743,13 +834,17 @@ const VSCodeHeader: React.FC = ({ title, showBack, onBack, on )} - {showContextUsage && contextUsage && contextUsage.totalTokens > 0 && ( + {showContextUsage && stableContextUsage && stableContextUsage.totalTokens > 0 && ( )}
diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index c313c414..1b348b55 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -449,9 +449,11 @@ export const OpenChamberVisualSettings: React.FC }; const isVSCode = isVSCodeRuntime(); - const hasAppearanceSettings = (shouldShow('theme') || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode; const hasThemeSettings = shouldShow('theme') && !isVSCode; - const hasLocalizationSettings = (shouldShow('theme') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode; + const hasLocalizationSettings = shouldShow('theme') || shouldShow('timeFormat') || shouldShow('weekStart'); + const hasAppearanceSettings = isVSCode + ? hasLocalizationSettings + : (shouldShow('theme') || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart')); const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset'); const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile; const hasBehaviorSettings = shouldShow('mermaidRendering') @@ -712,7 +714,7 @@ export const OpenChamberVisualSettings: React.FC
- {(shouldShow('timeFormat') || shouldShow('weekStart')) && ( + {!isVSCode && (shouldShow('timeFormat') || shouldShow('weekStart')) && (
{shouldShow('timeFormat') && (
diff --git a/packages/ui/src/sync/session-ui-store.ts b/packages/ui/src/sync/session-ui-store.ts index 67f1896b..ab242946 100644 --- a/packages/ui/src/sync/session-ui-store.ts +++ b/packages/ui/src/sync/session-ui-store.ts @@ -555,7 +555,6 @@ export const useSessionUIStore = create()((set, get) => ({ const messages = getSyncMessages(sessionId) if (messages.length === 0) return null - // Find last assistant message with token data type AssistantTokens = { input: number; output: number; reasoning: number; cache: { read: number; write: number } } let lastTokens: AssistantTokens | undefined let lastMessageId: string | undefined