diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx index ab8ce1b1f49..9751d52dd84 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx @@ -284,7 +284,8 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
{currentTab === 'Local' && !showLocalWizard && ( - - ) - })} -
- )} + { 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', () => {