diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx
index 27ea474d192..f7d75a74229 100644
--- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarChat.tsx
@@ -3,57 +3,36 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
-import React, { FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import React, { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState } from '../util/services.js';
-import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useFullChatThreadsStreamState } from '../util/services.js';
-
-import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js';
-import { URI } from '../../../../../../../base/common/uri.js';
-import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
-import { ErrorDisplay } from './ErrorDisplay.js';
-import { BlockCode, TextAreaFns, VoidInputBox2, VoidDiffEditor } from '../util/inputs.js';
+import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { PastThreadsList } from './SidebarThreadSelector.js';
import { VoidChatArea, ButtonSubmit, ButtonStop } from './composer/VoidChatArea.js';
import { SelectedFiles } from './composer/SelectedFiles.js';
-import { ScrollToBottomContainer } from './composer/ScrollToBottomContainer.js';
+import { scrollToBottom } from './composer/ScrollToBottomContainer.js';
import { ContextUsageBar } from './composer/ContextUsageBar.js';
import { CommandBarInChat } from './composer/CommandBarInChat.js';
+import { ChatMessageList } from './composer/ChatMessageList.js';
+import { StagingContextChips } from './composer/StagingContextChips.js';
+import { useContextUsage } from './composer/useContextUsage.js';
import { LandingPage } from './landing/LandingPage.js';
import { ComposerTabs } from './chrome/ComposerTabs.js';
import { ThreadHeader } from './chrome/ThreadHeader.js';
-import { CORTEXIDE_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { CORTEXIDE_OPEN_SETTINGS_ACTION_ID } from '../../../cortexideSettingsPane.js';
-import { displayInfoOfProviderName, FeatureName, isFeatureNameDisabled, isValidProviderModelSelection } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js';
-import { ICommandService } from '../../../../../../../platform/commands/common/commands.js';
-import { WarningBox } from '../settings/WarningBox.js';
-import { getModelCapabilities, getIsReasoningEnabledState, getReservedOutputTokenSpace } from '../../../../common/modelCapabilities.js';
-import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
-import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage, PlanMessage, ReviewMessage, PlanStep, StepStatus, PlanApprovalState } from '../../../../common/chatThreadServiceTypes.js';
-import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js';
-import { CopyButton, EditToolAcceptRejectButtonsHTML, JumpToFileButton, JumpToTerminalButton, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js';
-import { IsRunningType } from '../../../chatThreadService.js';
-import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
-import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js';
-import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
+import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js';
+import { StagingSelectionItem, ChatImageAttachment, ChatPDFAttachment } from '../../../../common/chatThreadServiceTypes.js';
import ErrorBoundary from './ErrorBoundary.js';
-import { ToolApprovalTypeSwitch } from '../settings/Settings.js';
-
-import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
-import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js';
import { useImageAttachments } from '../util/useImageAttachments.js';
import { usePDFAttachments } from '../util/usePDFAttachments.js';
import { useTranslation } from '../util/useTranslation.js';
import { PDFAttachmentList } from '../util/PDFAttachmentList.js';
import { ImageAttachmentList } from '../util/ImageAttachmentList.js';
-import { ChatImageAttachment, ChatPDFAttachment } from '../../../../common/chatThreadServiceTypes.js';
-import { ImageMessageRenderer } from '../util/ImageMessageRenderer.js';
-import { PDFMessageRenderer } from '../util/PDFMessageRenderer.js';
import { IconX, IconWarning, IconLoading, TypingCursor } from './shared/icons.js';
import { getBasename, getFolderName, getRelative, voidOpenFileFn } from './shared/pathUtils.js';
import { ToolChildrenWrapper, CodeChildren, ListableToolItem } from './tools/ToolPrimitives.js';
import { ChatBubble } from './chat/ChatBubble.js';
-import { EditToolSoFar } from './tools/ToolRenderers.js';
// Re-export shared modules for existing consumers (will migrate imports over time).
export { IconX, IconWarning, IconLoading, TypingCursor } from './shared/icons.js';
@@ -94,9 +73,6 @@ export const SidebarChat = () => {
const latestError = currThreadStreamState?.error
const { displayContentSoFar, toolCallSoFar, reasoningSoFar } = currThreadStreamState?.llmInfo ?? {}
- // this is just if it's currently being generated, NOT if it's currently running
- const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone // show loading for slow tools (right now just edit)
-
// ----- SIDEBAR CHAT state (local) -----
// state of current message
@@ -226,6 +202,8 @@ export const SidebarChat = () => {
if (isDisabled && !_forceSubmit) return
if (isRunning) return
+ const notificationService = accessor.get('INotificationService')
+
// use subscribed state - currentThread.id is already from subscribed state
const threadId = currentThread.id
@@ -238,7 +216,6 @@ export const SidebarChat = () => {
const trimmed = userMessage.trim()
if (trimmed.startsWith('/')) {
const [cmd, ...rest] = trimmed.slice(1).split(/\s+/)
- const notificationService = accessor.get('INotificationService')
const clearInput = () => {
if (textAreaFnsRef.current) textAreaFnsRef.current.setValue('')
textAreaRef.current?.focus()
@@ -295,7 +272,6 @@ export const SidebarChat = () => {
const editorService = accessor.get('IEditorService')
const languageService = accessor.get('ILanguageService')
const historyService = accessor.get('IHistoryService')
- const notificationService = accessor.get('INotificationService')
let outlineService: any = undefined
try { outlineService = accessor.get('IOutlineModelService') } catch {}
@@ -570,15 +546,13 @@ export const SidebarChat = () => {
console.error('Error while sending message in chat:', e)
}
- }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState, imageAttachments, pdfAttachments, clearImages, clearPDFs, currentThread.id])
+ }, [accessor, chatThreadsService, commandService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState, imageAttachments, pdfAttachments, clearImages, clearPDFs, currentThread.id, chatThreadsState])
const onAbort = async () => {
const threadId = currentThread.id
await chatThreadsService.abortRunning(threadId)
}
- const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(CORTEXIDE_CTRL_L_ACTION_ID)?.getLabel()
-
const threadId = currentThread.id
const currCheckpointIdx = chatThreadsState.allThreads[threadId]?.state?.currCheckpointIdx ?? undefined // if not exist, treat like checkpoint is last message (infinity)
@@ -600,142 +574,11 @@ export const SidebarChat = () => {
- const previousMessagesHTML = useMemo(() => {
- // const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
- // tool request shows up as Editing... if in progress
- // Use stable keys based on message ID or index for better React reconciliation
- return previousMessages.map((message, i) => {
- // Use message ID if available, otherwise fall back to index
- const messageKey = (message as any).id || `msg-${i}`
- return
- })
- }, [previousMessages, threadId, currCheckpointIdx, isRunning, scrollToBottomCallback])
-
- const streamingChatIdx = previousMessagesHTML.length
- // Memoize chatMessage object to avoid recreating on every render
- const streamingChatMessage = useMemo(() => ({
- role: 'assistant' as const,
- displayContent: displayContentSoFar ?? '',
- reasoning: reasoningSoFar ?? '',
- anthropicReasoning: null,
- }), [displayContentSoFar, reasoningSoFar])
-
- // Only show streaming message when actively streaming (LLM, tool, or preparing)
- // Don't show when idle/undefined to prevent duplicate messages and never-ending loading
- // Only show stop button when actively running (LLM, tool, preparing), not when idle
- const isActivelyStreaming = isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'preparing'
- const currStreamingMessageHTML = isActivelyStreaming && (reasoningSoFar || displayContentSoFar) ?
-
: null
-
-
- // the tool currently being generated
- const generatingTool = toolIsGenerating ?
- toolCallSoFar.name === 'edit_file' || toolCallSoFar.name === 'rewrite_file' ?
- : null
- : null
-
- const messagesHTML =
- {/* previous messages */}
- {previousMessagesHTML}
- {currStreamingMessageHTML}
-
- {/* Generating tool */}
- {generatingTool}
-
- {/* loading indicator with status message - only show when no content is streaming yet */}
- {(isRunning === 'LLM' || isRunning === 'preparing') && !displayContentSoFar && !reasoningSoFar ? (
-
-
-
- {isRunning === 'preparing' && currThreadStreamState?.llmInfo?.displayContentSoFar ? (
- <>
- {currThreadStreamState.llmInfo.displayContentSoFar}
-
- >
- ) : isRunning === 'preparing' ? (
- <>
- Preparing request
-
- >
- ) : (
- <>
- Generating response
-
- >
- )}
-
-
Press Escape to cancel
-
-
- ) : null}
-
- {/* Escape hint when streaming (e.g. "Waiting for model response...") */}
- {(isRunning === 'LLM' || isRunning === 'preparing') && (displayContentSoFar || reasoningSoFar) ? (
- Press Escape to cancel
- ) : null}
-
-
- {/* error message */}
- {latestError === undefined ? null :
-
-
{ chatThreadsService.dismissStreamError(currentThread.id) }}
- showDismiss={true}
- />
-
- You can try again or open settings to change the model.
-
- { commandService.executeCommand(CORTEXIDE_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
-
- }
-
-
-
const onChangeText = useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
}, [setInstructionsAreEmpty])
const onKeyDown = useCallback((e: KeyboardEvent
) => {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
- // Check isDisabled again at the time of key press (not closure value)
if (!isDisabled && !isRunning) {
onSubmit()
}
@@ -744,44 +587,25 @@ export const SidebarChat = () => {
}
}, [onSubmit, onAbort, isRunning, isDisabled])
- // Context usage calculation + warning (partially memoized - draft tokens calculated on each render)
- const [ctxWarned, setCtxWarned] = useState(false)
- const estimateTokens = useCallback((s: string) => Math.ceil((s || '').length / 4), [])
- const modelSel = settingsState.modelSelectionOfFeature['Chat']
-
- // Memoize context budget and messages tokens (only recalculate when messages or model changes)
- const { contextBudget, messagesTokens } = useMemo(() => {
- let budget = 0
- let tokens = 0
- if (modelSel && isValidProviderModelSelection(modelSel)) {
- const { providerName, modelName } = modelSel
- const caps = getModelCapabilities(providerName, modelName, settingsState.overridesOfModel)
- const contextWindow = caps.contextWindow
- const msOpts = settingsState.optionsOfModelSelection['Chat'][providerName]?.[modelName]
- const isReasoningEnabled2 = getIsReasoningEnabledState('Chat', providerName, modelName, msOpts, settingsState.overridesOfModel)
- const rot = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled: isReasoningEnabled2, overridesOfModel: settingsState.overridesOfModel }) || 0
- budget = Math.max(256, Math.floor(contextWindow * 0.8) - rot)
- tokens = previousMessages.reduce((acc, m) => {
- if (m.role === 'user') return acc + estimateTokens(m.content || '')
- if (m.role === 'assistant') return acc + estimateTokens((m.displayContent as string) || (m.content || '') || '')
- return acc
- }, 0)
- }
- return { contextBudget: budget, messagesTokens: tokens }
- }, [modelSel, previousMessages, settingsState.overridesOfModel, estimateTokens])
+ const isActivelyStreaming = isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'preparing'
- // Calculate draft tokens and total on each render (draft changes frequently)
- const draftTokens = estimateTokens(textAreaRef.current?.value || '')
- const contextTotal = messagesTokens + draftTokens
- const contextPct = contextBudget > 0 ? contextTotal / contextBudget : 0
+ const { modelSel, contextTotal, contextBudget, contextPct } = useContextUsage(
+ previousMessages,
+ textAreaRef.current?.value || '',
+ )
- useEffect(() => {
- if (contextPct > 0.8 && contextPct < 1 && !ctxWarned) {
- try { accessor.get('INotificationService').info(`Context nearing limit: ~${contextTotal} / ${contextBudget} tokens. Older messages may be summarized.`) } catch {}
- setCtxWarned(true)
- }
- if (contextPct < 0.6 && ctxWarned) setCtxWarned(false)
- }, [contextPct, ctxWarned, contextTotal, contextBudget, accessor])
+ const messagesHTML =
const inputChatArea = {
/>
{/* Context chips for current selections */}
- {selections.length > 0 && (
-
- {selections.map((sel, idx) => {
- const name = sel.type === 'Folder'
- ? (sel.uri?.path?.split('/').filter(Boolean).pop() || 'folder')
- : (sel.uri?.path?.split('/').pop() || 'file')
- const fullPath = sel.uri?.fsPath || sel.uri?.path || name
- const rangeLabel = (sel as any).range ? ` • ${(sel as any).range.startLineNumber}-${(sel as any).range.endLineNumber}` : ''
- const tooltipText = (sel as any).range
- ? `${fullPath} (lines ${(sel as any).range.startLineNumber}-${(sel as any).range.endLineNumber})`
- : fullPath
- return (
-
- {sel.type === 'Folder' ? 'Folder' : 'File'}
- {name}
- {rangeLabel && {rangeLabel}}
-
-
- )
- })}
-
- )}
+ { chatThreadsService.popStagingSelections(1) }}
+ />
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ChatMessageList.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ChatMessageList.tsx
new file mode 100644
index 00000000000..97878eb7498
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ChatMessageList.tsx
@@ -0,0 +1,163 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import React, { RefObject, useMemo } from 'react';
+import { useAccessor, useChatThreadsStreamState } from '../../util/services.js';
+import { ChatMessage } from '../../../../common/chatThreadServiceTypes.js';
+import { IsRunningType } from '../../../chatThreadService.js';
+import { ScrollToBottomContainer } from './ScrollToBottomContainer.js';
+import { ChatBubble } from '../chat/ChatBubble.js';
+import { EditToolSoFar } from '../tools/ToolRenderers.js';
+import { ProseWrapper } from '../chat/proseWrappers.js';
+import { IconLoading } from '../shared/icons.js';
+import { ErrorDisplay } from '../ErrorDisplay.js';
+import { WarningBox } from '../../settings/WarningBox.js';
+import { CORTEXIDE_OPEN_SETTINGS_ACTION_ID } from '../../../cortexideSettingsPane.js';
+
+type StreamError = { message: string; fullError?: string } | undefined;
+
+type ChatMessageListProps = {
+ threadId: string;
+ previousMessages: ChatMessage[];
+ currCheckpointIdx: number | undefined;
+ isRunning: IsRunningType | undefined;
+ displayContentSoFar: string | undefined;
+ reasoningSoFar: string | undefined;
+ toolCallSoFar: { name: string; isDone?: boolean; rawParams?: string } | undefined;
+ latestError: StreamError;
+ scrollContainerRef: RefObject;
+ scrollToBottomCallback: (() => void) | null;
+};
+
+export const ChatMessageList = ({
+ threadId,
+ previousMessages,
+ currCheckpointIdx,
+ isRunning,
+ displayContentSoFar,
+ reasoningSoFar,
+ toolCallSoFar,
+ latestError,
+ scrollContainerRef,
+ scrollToBottomCallback,
+}: ChatMessageListProps) => {
+ const accessor = useAccessor();
+ const commandService = accessor.get('ICommandService');
+ const chatThreadsService = accessor.get('IChatThreadService');
+ const currThreadStreamState = useChatThreadsStreamState(threadId);
+
+ const previousMessagesHTML = useMemo(() => {
+ return previousMessages.map((message, i) => {
+ const messageKey = (message as { id?: string }).id || `msg-${i}`;
+ return ;
+ });
+ }, [previousMessages, threadId, currCheckpointIdx, isRunning, scrollToBottomCallback]);
+
+ const streamingChatIdx = previousMessagesHTML.length;
+ const streamingChatMessage = useMemo(() => ({
+ role: 'assistant' as const,
+ displayContent: displayContentSoFar ?? '',
+ reasoning: reasoningSoFar ?? '',
+ anthropicReasoning: null,
+ }), [displayContentSoFar, reasoningSoFar]);
+
+ const isActivelyStreaming = isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'preparing';
+ const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone;
+
+ const currStreamingMessageHTML = isActivelyStreaming && (reasoningSoFar || displayContentSoFar)
+ ?
+ : null;
+
+ const generatingTool = toolIsGenerating && (toolCallSoFar.name === 'edit_file' || toolCallSoFar.name === 'rewrite_file')
+ ?
+ : null;
+
+ return (
+
+ {previousMessagesHTML}
+ {currStreamingMessageHTML}
+ {generatingTool}
+
+ {(isRunning === 'LLM' || isRunning === 'preparing') && !displayContentSoFar && !reasoningSoFar ? (
+
+
+
+ {isRunning === 'preparing' && currThreadStreamState?.llmInfo?.displayContentSoFar ? (
+ <>
+ {currThreadStreamState.llmInfo.displayContentSoFar}
+
+ >
+ ) : isRunning === 'preparing' ? (
+ <>
+ Preparing request
+
+ >
+ ) : (
+ <>
+ Generating response
+
+ >
+ )}
+
+
Press Escape to cancel
+
+
+ ) : null}
+
+ {(isRunning === 'LLM' || isRunning === 'preparing') && (displayContentSoFar || reasoningSoFar) ? (
+ Press Escape to cancel
+ ) : null}
+
+ {latestError === undefined ? null : (
+
+
{ chatThreadsService.dismissStreamError(threadId); }}
+ showDismiss={true}
+ />
+
+ You can try again or open settings to change the model.
+
+ { commandService.executeCommand(CORTEXIDE_OPEN_SETTINGS_ACTION_ID); }} text='Open settings' />
+
+ )}
+
+ );
+};
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ScrollToBottomContainer.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ScrollToBottomContainer.tsx
index a5a63334037..210d3755364 100644
--- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ScrollToBottomContainer.tsx
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ScrollToBottomContainer.tsx
@@ -5,7 +5,7 @@
import React, { useEffect, useState } from 'react';
-const scrollToBottom = (divRef: { current: HTMLElement | null }) => {
+export const scrollToBottom = (divRef: { current: HTMLElement | null }) => {
if (divRef.current) {
divRef.current.scrollTop = divRef.current.scrollHeight;
}
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/StagingContextChips.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/StagingContextChips.tsx
new file mode 100644
index 00000000000..63b96b042e3
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/StagingContextChips.tsx
@@ -0,0 +1,55 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import React from 'react';
+import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
+
+type StagingContextChipsProps = {
+ selections: StagingSelectionItem[];
+ onRemoveLast: () => void;
+};
+
+export const StagingContextChips = ({ selections, onRemoveLast }: StagingContextChipsProps) => {
+ if (selections.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {selections.map((sel, idx) => {
+ const name = sel.type === 'Folder'
+ ? (sel.uri?.path?.split('/').filter(Boolean).pop() || 'folder')
+ : (sel.uri?.path?.split('/').pop() || 'file');
+ const fullPath = sel.uri?.fsPath || sel.uri?.path || name;
+ const rangeLabel = (sel as { range?: { startLineNumber: number; endLineNumber: number } }).range
+ ? ` • ${(sel as { range: { startLineNumber: number; endLineNumber: number } }).range.startLineNumber}-${(sel as { range: { startLineNumber: number; endLineNumber: number } }).range.endLineNumber}`
+ : '';
+ const tooltipText = (sel as { range?: { startLineNumber: number; endLineNumber: number } }).range
+ ? `${fullPath} (lines ${(sel as { range: { startLineNumber: number; endLineNumber: number } }).range.startLineNumber}-${(sel as { range: { startLineNumber: number; endLineNumber: number } }).range.endLineNumber})`
+ : fullPath;
+ return (
+
+ {sel.type === 'Folder' ? 'Folder' : 'File'}
+ {name}
+ {rangeLabel && {rangeLabel}}
+
+
+ );
+ })}
+
+ );
+};
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/useContextUsage.ts b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/useContextUsage.ts
new file mode 100644
index 00000000000..bf909eb5e46
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/useContextUsage.ts
@@ -0,0 +1,54 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useAccessor, useSettingsState } from '../../util/services.js';
+import { ChatMessage } from '../../../../common/chatThreadServiceTypes.js';
+import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../../../../common/modelCapabilities.js';
+import { isValidProviderModelSelection } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js';
+
+export const useContextUsage = (previousMessages: ChatMessage[], draftText: string) => {
+ const accessor = useAccessor();
+ const settingsState = useSettingsState();
+ const [ctxWarned, setCtxWarned] = useState(false);
+ const estimateTokens = useCallback((s: string) => Math.ceil((s || '').length / 4), []);
+ const modelSel = settingsState.modelSelectionOfFeature['Chat'];
+
+ const { contextBudget, messagesTokens } = useMemo(() => {
+ let budget = 0;
+ let tokens = 0;
+ if (modelSel && isValidProviderModelSelection(modelSel)) {
+ const { providerName, modelName } = modelSel;
+ const caps = getModelCapabilities(providerName, modelName, settingsState.overridesOfModel);
+ const contextWindow = caps.contextWindow;
+ const msOpts = settingsState.optionsOfModelSelection['Chat'][providerName]?.[modelName];
+ const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, msOpts, settingsState.overridesOfModel);
+ const rot = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel: settingsState.overridesOfModel }) || 0;
+ budget = Math.max(256, Math.floor(contextWindow * 0.8) - rot);
+ tokens = previousMessages.reduce((acc, m) => {
+ if (m.role === 'user') return acc + estimateTokens(m.content || '');
+ if (m.role === 'assistant') return acc + estimateTokens((m.displayContent as string) || (m.content || '') || '');
+ return acc;
+ }, 0);
+ }
+ return { contextBudget: budget, messagesTokens: tokens };
+ }, [modelSel, previousMessages, settingsState.overridesOfModel, settingsState.optionsOfModelSelection, estimateTokens]);
+
+ const draftTokens = estimateTokens(draftText);
+ const contextTotal = messagesTokens + draftTokens;
+ const contextPct = contextBudget > 0 ? contextTotal / contextBudget : 0;
+
+ useEffect(() => {
+ if (contextPct > 0.8 && contextPct < 1 && !ctxWarned) {
+ try {
+ accessor.get('INotificationService').info(`Context nearing limit: ~${contextTotal} / ${contextBudget} tokens. Older messages may be summarized.`);
+ } catch { /* noop */ }
+ setCtxWarned(true);
+ }
+ if (contextPct < 0.6 && ctxWarned) setCtxWarned(false);
+ }, [contextPct, ctxWarned, contextTotal, contextBudget, accessor]);
+
+ return { modelSel, contextTotal, contextBudget, contextPct };
+};
diff --git a/src/vs/workbench/contrib/cortexide/test/common/designSystem.test.ts b/src/vs/workbench/contrib/cortexide/test/common/designSystem.test.ts
index 766027126bb..b0ee7f375af 100644
--- a/src/vs/workbench/contrib/cortexide/test/common/designSystem.test.ts
+++ b/src/vs/workbench/contrib/cortexide/test/common/designSystem.test.ts
@@ -74,8 +74,18 @@ suite('designSystem (Phase 1 — onboarding adoption)', () => {
});
});
+const voidOnboardingPath = join(dirname(fileURLToPath(import.meta.url)), '../../browser/react/src/onboarding/VoidOnboarding.tsx');
const settingsPath = join(dirname(fileURLToPath(import.meta.url)), '../../browser/react/src/settings/Settings.tsx');
+suite('designSystem (Phase 1 — void onboarding adoption)', () => {
+
+ test('Void onboarding welcome CTAs use design-system buttons', () => {
+ const src = readFileSync(voidOnboardingPath, 'utf8');
+ assert.ok(src.includes('btn btn-primary'), 'expected btn-primary in void onboarding');
+ assert.ok(src.includes('btn btn-secondary'), 'expected btn-secondary in void onboarding');
+ });
+});
+
suite('designSystem (Phase 1 — settings adoption)', () => {
test('Settings pane uses design-system button classes', () => {