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:
@@ -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
|
||||
/>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ErrorBoundary>
|
||||
@@ -435,6 +437,7 @@ export const VSCodeLayout: React.FC = () => {
|
||||
: sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')}
|
||||
showMcp
|
||||
showContextUsage
|
||||
showRateLimits
|
||||
/>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ErrorBoundary>
|
||||
@@ -499,8 +502,11 @@ interface VSCodeHeaderProps {
|
||||
|
||||
const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ 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<VSCodeHeaderProps> = ({ 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<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 groups: Array<{
|
||||
@@ -743,13 +834,17 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
<RiSettings3Line className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
{showContextUsage && contextUsage && contextUsage.totalTokens > 0 && (
|
||||
{showContextUsage && stableContextUsage && stableContextUsage.totalTokens > 0 && (
|
||||
<ContextUsageDisplay
|
||||
totalTokens={contextUsage.totalTokens}
|
||||
percentage={contextUsage.percentage}
|
||||
contextLimit={contextUsage.contextLimit}
|
||||
outputLimit={contextUsage.outputLimit ?? 0}
|
||||
size="compact"
|
||||
totalTokens={stableContextUsage.totalTokens}
|
||||
percentage={stableContextUsage.percentage}
|
||||
contextLimit={stableContextUsage.contextLimit}
|
||||
outputLimit={stableContextUsage.outputLimit ?? 0}
|
||||
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>
|
||||
|
||||
@@ -449,9 +449,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
};
|
||||
|
||||
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<OpenChamberVisualSettingsProps>
|
||||
</Select>
|
||||
</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">
|
||||
{shouldShow('timeFormat') && (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
|
||||
@@ -555,7 +555,6 @@ export const useSessionUIStore = create<SessionUIState>()((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
|
||||
|
||||
Reference in New Issue
Block a user