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
This commit is contained in:
Bohdan Triapitsyn
2026-04-26 20:20:23 +03:00
parent 799998c5f8
commit 177631f123
3 changed files with 115 additions and 19 deletions
@@ -4,7 +4,7 @@ import { SessionSidebar } from '@/components/session/SessionSidebar';
import { ChatView } from '@/components/views'; import { ChatView } from '@/components/views';
import { useSessionUIStore } from '@/sync/session-ui-store'; import { useSessionUIStore } from '@/sync/session-ui-store';
import { useViewportStore } from '@/sync/viewport-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 { useConfigStore } from '@/stores/useConfigStore';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { McpDropdown } from '@/components/mcp/McpDropdown'; import { McpDropdown } from '@/components/mcp/McpDropdown';
@@ -27,6 +27,7 @@ import { useQuotaAutoRefresh, useQuotaStore } from '@/stores/useQuotaStore';
import { updateDesktopSettings } from '@/lib/persistence'; import { updateDesktopSettings } from '@/lib/persistence';
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
import type { UsageWindow } from '@/types'; import type { UsageWindow } from '@/types';
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
import { RiAddLine, RiArrowLeftLine, RiRefreshLine, RiRobot2Line, RiSettings3Line, RiTimerLine } from '@remixicon/react'; import { RiAddLine, RiArrowLeftLine, RiRefreshLine, RiRobot2Line, RiSettings3Line, RiTimerLine } from '@remixicon/react';
const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView }))); 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')} title={sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')}
showMcp showMcp
showContextUsage showContextUsage
showRateLimits
/> />
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<ErrorBoundary> <ErrorBoundary>
@@ -435,6 +437,7 @@ export const VSCodeLayout: React.FC = () => {
: sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')} : sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')}
showMcp showMcp
showContextUsage showContextUsage
showRateLimits
/> />
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<ErrorBoundary> <ErrorBoundary>
@@ -499,8 +502,11 @@ interface VSCodeHeaderProps {
const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits }) => { const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits }) => {
const { t } = useI18n(); const { t } = useI18n();
const getCurrentModel = useConfigStore((s) => s.getCurrentModel); const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
const getContextUsage = useSessionUIStore((state) => state.getContextUsage); 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 quotaResults = useQuotaStore((state) => state.results);
const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas); const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas);
const isQuotaLoading = useQuotaStore((state) => state.isLoading); const isQuotaLoading = useQuotaStore((state) => state.isLoading);
@@ -517,12 +523,97 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
}, [loadQuotaSettings]); }, [loadQuotaSettings]);
const currentModel = getCurrentModel(); const currentModel = getCurrentModel();
const limits = (currentModel?.limit && typeof currentModel.limit === 'object' const latestAssistantModel = React.useMemo(() => {
? currentModel.limit for (let i = currentSessionMessages.length - 1; i >= 0; i -= 1) {
: null) as { context?: number; output?: number } | null; const message = currentSessionMessages[i] as { role?: unknown; providerID?: unknown; modelID?: unknown };
const contextLimit = typeof limits?.context === 'number' ? limits.context : 0; if (message.role !== 'assistant') continue;
const outputLimit = typeof limits?.output === 'number' ? limits.output : 0; if (typeof message.providerID !== 'string' || typeof message.modelID !== 'string') continue;
const contextUsage = getContextUsage(contextLimit, outputLimit); 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<string, unknown>)
: 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<SessionContextUsage | null>(() => {
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<SessionContextUsage | null>(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 rateLimitGroups = React.useMemo(() => {
const groups: Array<{ const groups: Array<{
@@ -743,13 +834,17 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<RiSettings3Line className="h-5 w-5" /> <RiSettings3Line className="h-5 w-5" />
</button> </button>
)} )}
{showContextUsage && contextUsage && contextUsage.totalTokens > 0 && ( {showContextUsage && stableContextUsage && stableContextUsage.totalTokens > 0 && (
<ContextUsageDisplay <ContextUsageDisplay
totalTokens={contextUsage.totalTokens} totalTokens={stableContextUsage.totalTokens}
percentage={contextUsage.percentage} percentage={stableContextUsage.percentage}
contextLimit={contextUsage.contextLimit} contextLimit={stableContextUsage.contextLimit}
outputLimit={contextUsage.outputLimit ?? 0} outputLimit={stableContextUsage.outputLimit ?? 0}
size="compact" className="h-9 shrink-0 pl-1 pr-1 typography-ui-label"
valueClassName="font-semibold leading-none"
hideIcon
showPercentIcon
percentIconClassName="h-5 w-5"
/> />
)} )}
</div> </div>
@@ -449,9 +449,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
}; };
const isVSCode = isVSCodeRuntime(); const isVSCode = isVSCodeRuntime();
const hasAppearanceSettings = (shouldShow('theme') || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart')) && !isVSCode;
const hasThemeSettings = shouldShow('theme') && !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 hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('spacing') || shouldShow('inputBarOffset');
const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile; const hasNavigationSettings = shouldShow('terminalQuickKeys') && !isMobile;
const hasBehaviorSettings = shouldShow('mermaidRendering') const hasBehaviorSettings = shouldShow('mermaidRendering')
@@ -712,7 +714,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</Select> </Select>
</div> </div>
{(shouldShow('timeFormat') || shouldShow('weekStart')) && ( {!isVSCode && (shouldShow('timeFormat') || shouldShow('weekStart')) && (
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2"> <div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
{shouldShow('timeFormat') && ( {shouldShow('timeFormat') && (
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
-1
View File
@@ -555,7 +555,6 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
const messages = getSyncMessages(sessionId) const messages = getSyncMessages(sessionId)
if (messages.length === 0) return null 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 } } type AssistantTokens = { input: number; output: number; reasoning: number; cache: { read: number; write: number } }
let lastTokens: AssistantTokens | undefined let lastTokens: AssistantTokens | undefined
let lastMessageId: string | undefined let lastMessageId: string | undefined