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 { 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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user