diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index e43e9d833875..5bdaf8b7d086 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useSettingsState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; -import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; +import { VoidChatArea } from '../sidebar-tsx/composer/VoidChatArea.js'; import { CORTEXIDE_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; import { useRefState } from '../util/helpers.js'; import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js'; 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 578728748760..e7cb62bebb8e 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,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState, useFullChatThreadsStreamState } from '../util/services.js'; @@ -12,18 +12,21 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd import { URI } from '../../../../../../../base/common/uri.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; -import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js'; -import { ModelDropdown, } from '../settings/ModelDropdown.js'; +import { BlockCode, TextAreaFns, VoidInputBox2, VoidDiffEditor } 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 { 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 { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled, isValidProviderModelSelection } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.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, Image as ImageIcon, FileText } from 'lucide-react'; +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, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js'; @@ -39,13 +42,12 @@ 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 { FileAccess } from '../../../../../../../base/common/network.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, IconArrowUp, IconSquare, IconWarning, IconLoading, TypingCursor } from './shared/icons.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'; @@ -53,777 +55,11 @@ import { ToolChildrenWrapper, CodeChildren, ListableToolItem } from './tools/Too export { IconX, IconWarning, IconLoading, TypingCursor } from './shared/icons.js'; export { getBasename, getFolderName, getRelative, voidOpenFileFn } from './shared/pathUtils.js'; export { ToolChildrenWrapper, CodeChildren, ListableToolItem } from './tools/ToolPrimitives.js'; - -// SLIDER ONLY: -const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => { - const accessor = useAccessor() - - const cortexideSettingsService = accessor.get('ICortexideSettingsService') - const voidSettingsState = useSettingsState() - - const modelSelection = voidSettingsState.modelSelectionOfFeature[featureName] - const overridesOfModel = voidSettingsState.overridesOfModel - - if (!modelSelection) return null - - // Skip "auto" - it's not a real provider - if (!isValidProviderModelSelection(modelSelection)) { - return null; - } - - const { modelName, providerName } = modelSelection - const { reasoningCapabilities } = getModelCapabilities(providerName, modelName, overridesOfModel) - const { canTurnOffReasoning, reasoningSlider: reasoningBudgetSlider } = reasoningCapabilities || {} - - const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[modelName] - const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel) - - if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider - return
- Thinking - { - const isOff = canTurnOffReasoning && !newVal - cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff }) - }} - /> -
- } - - if (reasoningBudgetSlider?.type === 'budget_slider') { // if it's a slider - const { min: min_, max, default: defaultVal } = reasoningBudgetSlider - - const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters - const stepSize = Math.round((max - min_) / nSteps) - - const valueIfOff = min_ - stepSize - const min = canTurnOffReasoning ? valueIfOff : min_ - const value = isReasoningEnabled ? voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal - : valueIfOff - - return
- Thinking - { - if (modelSelection.providerName === 'auto' && modelSelection.modelName === 'auto') return; - const isOff = canTurnOffReasoning && newVal === valueIfOff - cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningBudget: newVal }) - }} - /> - {isReasoningEnabled ? `${value} tokens` : 'Thinking disabled'} -
- } - - if (reasoningBudgetSlider?.type === 'effort_slider') { - - const { values, default: defaultVal } = reasoningBudgetSlider - - const min = canTurnOffReasoning ? -1 : 0 - const max = values.length - 1 - - const currentEffort = voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEffort ?? defaultVal - const valueIfOff = -1 - const value = isReasoningEnabled && currentEffort ? values.indexOf(currentEffort) : valueIfOff - - const currentEffortCapitalized = currentEffort.charAt(0).toUpperCase() + currentEffort.slice(1, Infinity) - - return
- Thinking - { - if (modelSelection.providerName === 'auto' && modelSelection.modelName === 'auto') return; - const isOff = canTurnOffReasoning && newVal === valueIfOff - cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningEffort: values[newVal] ?? undefined }) - }} - /> - {isReasoningEnabled ? `${currentEffortCapitalized}` : 'Thinking disabled'} -
- } - - return null -} - - - -const nameOfChatMode: Record = { - 'normal': 'Ask', - 'gather': 'Gather', - 'plan': 'Plan', - 'agent': 'Agent', -} - -const detailOfChatMode: Record = { - 'normal': 'Ask questions about your code', - 'gather': 'Reads files, no edits', - 'plan': 'Plans first, then executes', - 'agent': 'Edits files and uses tools', -} - - -const ChatModeDropdown = ({ className }: { className: string }) => { - const accessor = useAccessor() - - const cortexideSettingsService = accessor.get('ICortexideSettingsService') - const settingsState = useSettingsState() - - const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'plan', 'agent'], []) - - const onChangeOption = useCallback((newVal: ChatMode) => { - cortexideSettingsService.setGlobalSetting('chatMode', newVal) - }, [cortexideSettingsService]) - - const getModeDisplayName = (val: ChatMode) => nameOfChatMode[val] ?? val - - return detailOfChatMode[val]} - getOptionsEqual={(a, b) => a === b} - /> - -} +export { VoidChatArea, ButtonSubmit, ButtonStop } from './composer/VoidChatArea.js'; +export { SelectedFiles } from './composer/SelectedFiles.js'; - - -interface CortexideChatAreaProps { - // Required - children: React.ReactNode; // This will be the input component - - // Form controls - onSubmit: () => void; - onAbort: () => void; - isStreaming: boolean; - isDisabled?: boolean; - divRef?: React.RefObject; - - // UI customization - className?: string; - showModelDropdown?: boolean; - showSelections?: boolean; - showProspectiveSelections?: boolean; - loadingIcon?: React.ReactNode; - - selections?: StagingSelectionItem[] - setSelections?: (s: StagingSelectionItem[]) => void - // selections?: any[]; - // onSelectionsChange?: (selections: any[]) => void; - - onClickAnywhere?: () => void; - // Optional close button - onClose?: () => void; - - // Image attachments - imageAttachments?: React.ReactNode; - onImagePaste?: (files: File[]) => void; - onImageDrop?: (files: File[]) => void; - onImageUpload?: () => void; - onPDFDrop?: (files: File[]) => void; - pdfAttachments?: React.ReactNode; - - featureName: FeatureName; -} - -export const VoidChatArea: React.FC = ({ - children, - onSubmit, - onAbort, - onClose, - onClickAnywhere, - divRef, - isStreaming = false, - isDisabled = false, - className = '', - showModelDropdown = true, - showSelections = false, - showProspectiveSelections = false, - selections, - setSelections, - imageAttachments, - onImagePaste, - onImageDrop, - onImageUpload, - onPDFDrop, - pdfAttachments, - featureName, - loadingIcon, -}) => { - const [isDragOver, setIsDragOver] = React.useState(false); - const imageInputRef = React.useRef(null); - const pdfInputRef = React.useRef(null); - const containerRef = React.useRef(null); - - // allow-any-unicode-next-line - // Handle paste — listens on container (bubbles from textarea) AND document level - // (document listener catches Ctrl+V when the sidebar panel is focused but textarea isn't) - React.useEffect(() => { - const handlePaste = (e: ClipboardEvent) => { - const items = Array.from(e.clipboardData?.items || []); - const imageFiles: File[] = []; - const pdfFiles: File[] = []; - - for (const item of items) { - if (item.type.startsWith('image/')) { - const file = item.getAsFile(); - if (file) imageFiles.push(file); - } else if (item.type === 'application/pdf') { - const file = item.getAsFile(); - if (file) pdfFiles.push(file); - } - } - - if (imageFiles.length > 0 && onImagePaste) { - e.preventDefault(); - onImagePaste(imageFiles); - } - if (pdfFiles.length > 0 && onPDFDrop) { - e.preventDefault(); - onPDFDrop(pdfFiles); - } - }; - - // Primary: attach to container so it catches events bubbling from the textarea - const container = containerRef.current || divRef?.current; - if (container) { - container.addEventListener('paste', handlePaste); - } - - // Fallback: document-level listener catches paste when chat area is visible but - // the textarea isn't focused (e.g. user focuses sidebar panel then Ctrl+V) - const handleDocumentPaste = (e: ClipboardEvent) => { - // Only intercept if the target is not already inside our container - // and our container is mounted in the DOM - const cont = containerRef.current || divRef?.current; - if (!cont) return; - if (cont.contains(e.target as Node)) return; // already handled by container listener - // Only fire if the paste comes from a non-input element (avoid stealing from other inputs) - const target = e.target as HTMLElement; - const tag = target?.tagName?.toLowerCase(); - if (tag === 'input' || tag === 'textarea') return; - - const items = Array.from(e.clipboardData?.items || []); - const imageFiles = items - .filter(i => i.type.startsWith('image/')) - .map(i => i.getAsFile()) - .filter((f): f is File => f !== null); - - if (imageFiles.length > 0 && onImagePaste) { - e.preventDefault(); - onImagePaste(imageFiles); - } - }; - document.addEventListener('paste', handleDocumentPaste); - - return () => { - if (container) container.removeEventListener('paste', handlePaste); - document.removeEventListener('paste', handleDocumentPaste); - }; - }, [divRef, onImagePaste, onPDFDrop]); - - // Throttle drag over events to prevent jank - const lastDragOverTimeRef = React.useRef(0); - const DRAG_THROTTLE_MS = 50; // Update at most every 50ms - - // Handle drag and drop - const handleDragOver = React.useCallback((e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const now = Date.now(); - if (now - lastDragOverTimeRef.current < DRAG_THROTTLE_MS) { - return; - } - lastDragOverTimeRef.current = now; - - const hasFiles = Array.from(e.dataTransfer.items).some(item => - item.type.startsWith('image/') || item.type === 'application/pdf' - ); - if (hasFiles) { - setIsDragOver(true); - } - }, []); - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - - const imageFiles = Array.from(e.dataTransfer.files).filter(file => - file.type.startsWith('image/') - ); - const pdfFiles = Array.from(e.dataTransfer.files).filter(file => - file.type === 'application/pdf' - ); - - if (imageFiles.length > 0 && onImageDrop) { - onImageDrop(imageFiles); - } - if (pdfFiles.length > 0 && onPDFDrop) { - onPDFDrop(pdfFiles); - } - }; - - const handleImageUploadClick = () => { - imageInputRef.current?.click(); - }; - - const handlePDFUploadClick = () => { - pdfInputRef.current?.click(); - }; - - const handleImageInputChange = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []).filter(file => - file.type.startsWith('image/') - ); - if (files.length > 0 && onImageDrop) { - onImageDrop(files); - } - e.target.value = ''; // Reset input - }; - - const handlePDFInputChange = (e: React.ChangeEvent) => { - const files = Array.from(e.target.files || []).filter(file => - file.type === 'application/pdf' - ); - if (files.length > 0 && onPDFDrop) { - onPDFDrop(files); - } - e.target.value = ''; // Reset input - }; - - return ( -
{ - if (divRef) { - if (typeof divRef === 'function') { - divRef(node); - } else { - divRef.current = node; - } - } - containerRef.current = node; - }} - className={` - gap-x-1 - flex flex-col p-2.5 relative input text-left shrink-0 - rounded-2xl - bg-[#030304] - transition-all duration-200 - border border-[rgba(255,255,255,0.08)] focus-within:border-[rgba(255,255,255,0.12)] hover:border-[rgba(255,255,255,0.12)] - ${isDragOver ? 'border-blue-500 bg-blue-500/10' : ''} - max-h-[80vh] overflow-y-auto - ${className} - `} - onClick={(e) => { - onClickAnywhere?.() - }} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - > - {/* Hidden file inputs - separate for images and PDFs */} - - - - {/* Image attachments section */} - {imageAttachments} - - {/* PDF attachments section */} - {pdfAttachments} - - {/* Selections section */} - {showSelections && selections && setSelections && ( - - )} - - {/* Input section - Modern Cursor-style layout */} -
-
- {children} -
- - {/* Right-side icon bar - Cursor style */} -
- {/* Image upload button */} - - - {/* PDF upload button */} - - - {/* Submit button */} - {isStreaming ? ( - - ) : ( - - )} -
- - {/* Close button (X) if onClose is provided */} - {onClose && ( -
- -
- )} -
- - {/* Bottom row - Model selector and settings */} -
- {showModelDropdown && ( -
- {featureName === 'Chat' && } - - -
- )} - - {/* Loading indicator */} - {isStreaming && loadingIcon && ( -
- {loadingIcon} -
- )} -
-
- ); -}; - - - - -type ButtonProps = ButtonHTMLAttributes -const DEFAULT_BUTTON_SIZE = 22; -export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { - - return -} - -export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => { - return -} - - - -const scrollToBottom = (divRef: { current: HTMLElement | null }) => { - if (divRef.current) { - divRef.current.scrollTop = divRef.current.scrollHeight; - } -}; - - - -const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject }) => { - const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom - - const divRef = scrollContainerRef - - const onScroll = () => { - const div = divRef.current; - if (!div) return; - - const isBottom = Math.abs( - div.scrollHeight - div.clientHeight - div.scrollTop - ) < 4; - - setIsAtBottom(isBottom); - }; - - // When children change (new messages added) - useEffect(() => { - if (isAtBottom) { - scrollToBottom(divRef); - } - }, [children, isAtBottom]); // Dependency on children to detect new messages - - // Initial scroll to bottom - useEffect(() => { - scrollToBottom(divRef); - }, []); - - return ( -
- {children} -
- ); -}; - -export const SelectedFiles = ( - { type, selections, setSelections, showProspectiveSelections, messageIdx, }: - | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, } - | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean, messageIdx?: number } -) => { - - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const modelReferenceService = accessor.get('ICortexideModelService') - - - - - // state for tracking prospective files - const { uri: currentURI } = useActiveURI() - const [recentUris, setRecentUris] = useState([]) - const maxRecentUris = 10 - const maxProspectiveFiles = 3 - useEffect(() => { // handle recent files - if (!currentURI) return - setRecentUris(prev => { - const withoutCurrent = prev.filter(uri => uri.fsPath !== currentURI.fsPath) // remove duplicates - const withCurrent = [currentURI, ...withoutCurrent] - return withCurrent.slice(0, maxRecentUris) - }) - }, [currentURI]) - const [prospectiveSelections, setProspectiveSelections] = useState([]) - - - // handle prospective files - useEffect(() => { - const computeRecents = async () => { - const prospectiveURIs = recentUris - .filter(uri => !selections.find(s => s.type === 'File' && s.uri.fsPath === uri.fsPath)) - .slice(0, maxProspectiveFiles) - - const answer: StagingSelectionItem[] = [] - for (const uri of prospectiveURIs) { - answer.push({ - type: 'File', - uri: uri, - language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext', - state: { wasAddedAsCurrentFile: false }, - }) - } - return answer - } - - // add a prospective file if type === 'staging' and if the user is in a file, and if the file is not selected yet - if (type === 'staging' && showProspectiveSelections) { - computeRecents().then((a) => setProspectiveSelections(a)) - } - else { - setProspectiveSelections([]) - } - }, [recentUris, selections, type, showProspectiveSelections]) - - - const allSelections = [...selections, ...prospectiveSelections] - - if (allSelections.length === 0) { - return null - } - - return ( -
- - {allSelections.map((selection, i) => { - - const isThisSelectionProspective = i > selections.length - 1 - - const thisKey = selection.type === 'CodeSelection' ? selection.type + selection.language + selection.range + selection.state.wasAddedAsCurrentFile + selection.uri.fsPath - : selection.type === 'File' ? selection.type + selection.language + selection.state.wasAddedAsCurrentFile + selection.uri.fsPath - : selection.type === 'Folder' ? selection.type + selection.language + selection.state + selection.uri.fsPath - : i - - const SelectionIcon = ( - selection.type === 'File' ? File - : selection.type === 'Folder' ? Folder - : selection.type === 'CodeSelection' ? Text - : (undefined as never) - ) - - return
- {/* tooltip for file path */} - - {/* summarybox */} -
{ - if (type !== 'staging') return; // (never) - if (isThisSelectionProspective) { // add prospective selection to selections - setSelections([...selections, selection]) - } - else if (selection.type === 'File') { // open files - voidOpenFileFn(selection.uri, accessor); - - const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile - if (wasAddedAsCurrentFile) { - // make it so the file is added permanently, not just as the current file - const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } } - setSelections([ - ...selections.slice(0, i), - newSelection, - ...selections.slice(i + 1) - ]) - } - } - else if (selection.type === 'CodeSelection') { - voidOpenFileFn(selection.uri, accessor, selection.range); - } - else if (selection.type === 'Folder') { - // TODO!!! reveal in tree - } - }} - > - {} - - { // file name and range - getBasename(selection.uri.fsPath) - + (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '') - } - - {selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ? - - {`(Current File)`} - - : null - } - - {type === 'staging' && !isThisSelectionProspective ? // X button -
{ - e.stopPropagation(); // don't open/close selection - if (type !== 'staging') return; - setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) - }} - > - -
- : <> - } -
-
-
- - })} - - -
- - ) -} - - type ToolHeaderParams = { icon?: React.ReactNode; title: React.ReactNode; @@ -4574,24 +3810,6 @@ export const SidebarChat = () => { const isLandingPage = previousMessages.length === 0 - const initiallySuggestedPromptsHTML =
- {[ - 'Summarize my codebase', - 'How do types work in Rust?', - 'Create a .voidrules file for me' - ].map((text, index) => ( -
onSubmit(text)} - > - {text} -
- ))} -
- - - const threadPageInput =
@@ -4635,107 +3853,14 @@ export const SidebarChat = () => {
- const keybindingService = accessor.get('IKeybindingService') - const quickActions: { id: string, label: string }[] = [ - { id: 'void.explainCode', label: 'Explain' }, - { id: 'void.refactorCode', label: 'Refactor' }, - { id: 'void.addTests', label: 'Add Tests' }, - { id: 'void.fixTests', label: 'Fix Tests' }, - { id: 'void.writeDocstring', label: 'Docstring' }, - { id: 'void.optimizeCode', label: 'Optimize' }, - { id: 'void.debugCode', label: 'Debug' }, - ] - - const QuickActionsBar = () => ( -
- {quickActions.map(({ id, label }) => { - const kb = keybindingService.lookupKeybinding(id)?.getLabel() - return ( - - ) - })} -
- ) - - // Lightweight context chips: active file and model - const ContextChipsBar = () => { - const editorService = accessor.get('IEditorService') - const activeEditor = editorService?.activeEditor - // Try best-effort file label - const activeResource = activeEditor?.resource - const activeFileLabel = activeResource ? activeResource.path?.split('/').pop() : undefined - const modelSel = settingsState.modelSelectionOfFeature['Chat'] - const modelLabel = modelSel ? `${modelSel.providerName}:${modelSel.modelName}` : undefined - if (!activeFileLabel && !modelLabel) return null - return ( -
- {activeFileLabel && ( - - File - {activeFileLabel} - - )} - {modelLabel && ( - - Model - {modelLabel} - - )} -
- ) - } - - const logoUri = useMemo(() => FileAccess.asBrowserUri('vs/workbench/browser/media/cortexide-main.png').toString(true), []) - - const landingPageContent =
- {/* CortexIDE logo */} -
-
- CortexIDE -
- CortexIDE -
- - {landingPageInput} - - - {/* Context chips */} - - - - - {/* Quick Actions shortcuts */} - - - - - {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads - -
{t('chat.previousThreads')}
- -
- : - -
{t('chat.suggestions')}
- {initiallySuggestedPromptsHTML} -
- } -
+ const landingPageContent = 1} + suggestionsLabel={t('chat.suggestions')} + previousThreadsLabel={t('chat.previousThreads')} + onSubmitPrompt={onSubmit} + /> // const threadPageContent =
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 new file mode 100644 index 000000000000..a5a63334037d --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/ScrollToBottomContainer.tsx @@ -0,0 +1,50 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useEffect, useState } from 'react'; + +const scrollToBottom = (divRef: { current: HTMLElement | null }) => { + if (divRef.current) { + divRef.current.scrollTop = divRef.current.scrollHeight; + } +}; + +export const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject }) => { + const [isAtBottom, setIsAtBottom] = useState(true); + + const divRef = scrollContainerRef; + + const onScroll = () => { + const div = divRef.current; + if (!div) return; + + const isBottom = Math.abs( + div.scrollHeight - div.clientHeight - div.scrollTop + ) < 4; + + setIsAtBottom(isBottom); + }; + + useEffect(() => { + if (isAtBottom) { + scrollToBottom(divRef); + } + }, [children, isAtBottom, divRef]); + + useEffect(() => { + scrollToBottom(divRef); + }, [divRef]); + + return ( +
+ {children} +
+ ); +}; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/SelectedFiles.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/SelectedFiles.tsx new file mode 100644 index 000000000000..75aa10320e08 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/SelectedFiles.tsx @@ -0,0 +1,180 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { useEffect, useState } from 'react'; +import { File, Folder, Text } from 'lucide-react'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { useAccessor, useActiveURI } from '../../util/services.js'; +import { StagingSelectionItem } from '../../../../../common/chatThreadServiceTypes.js'; +import { IconX } from '../shared/icons.js'; +import { getBasename, getRelative, voidOpenFileFn } from '../shared/pathUtils.js'; + +export const SelectedFiles = ( + { type, selections, setSelections, showProspectiveSelections, messageIdx, }: + | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, } + | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean, messageIdx?: number } +) => { + + const accessor = useAccessor() + const modelReferenceService = accessor.get('ICortexideModelService') + + const { uri: currentURI } = useActiveURI() + const [recentUris, setRecentUris] = useState([]) + const maxRecentUris = 10 + const maxProspectiveFiles = 3 + useEffect(() => { + if (!currentURI) return + setRecentUris(prev => { + const withoutCurrent = prev.filter(uri => uri.fsPath !== currentURI.fsPath) + const withCurrent = [currentURI, ...withoutCurrent] + return withCurrent.slice(0, maxRecentUris) + }) + }, [currentURI]) + const [prospectiveSelections, setProspectiveSelections] = useState([]) + + useEffect(() => { + const computeRecents = async () => { + const prospectiveURIs = recentUris + .filter(uri => !selections.find(s => s.type === 'File' && s.uri.fsPath === uri.fsPath)) + .slice(0, maxProspectiveFiles) + + const answer: StagingSelectionItem[] = [] + for (const uri of prospectiveURIs) { + answer.push({ + type: 'File', + uri: uri, + language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext', + state: { wasAddedAsCurrentFile: false }, + }) + } + return answer + } + + if (type === 'staging' && showProspectiveSelections) { + computeRecents().then((a) => setProspectiveSelections(a)) + } + else { + setProspectiveSelections([]) + } + }, [recentUris, selections, type, showProspectiveSelections, modelReferenceService]) + + + const allSelections = [...selections, ...prospectiveSelections] + + if (allSelections.length === 0) { + return null + } + + return ( +
+ + {allSelections.map((selection, i) => { + + const isThisSelectionProspective = i > selections.length - 1 + + const thisKey = selection.type === 'CodeSelection' ? selection.type + selection.language + selection.range + selection.state.wasAddedAsCurrentFile + selection.uri.fsPath + : selection.type === 'File' ? selection.type + selection.language + selection.state.wasAddedAsCurrentFile + selection.uri.fsPath + : selection.type === 'Folder' ? selection.type + selection.language + selection.state + selection.uri.fsPath + : i + + const SelectionIcon = ( + selection.type === 'File' ? File + : selection.type === 'Folder' ? Folder + : selection.type === 'CodeSelection' ? Text + : (undefined as never) + ) + + return
+ +
{ + if (type !== 'staging') return; + if (isThisSelectionProspective) { + setSelections([...selections, selection]) + } + else if (selection.type === 'File') { + voidOpenFileFn(selection.uri, accessor); + + const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile + if (wasAddedAsCurrentFile) { + const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } } + setSelections([ + ...selections.slice(0, i), + newSelection, + ...selections.slice(i + 1) + ]) + } + } + else if (selection.type === 'CodeSelection') { + voidOpenFileFn(selection.uri, accessor, selection.range); + } + else if (selection.type === 'Folder') { + // TODO!!! reveal in tree + } + }} + > + {} + + {getBasename(selection.uri.fsPath) + + (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '') + } + + {selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ? + + {`(Current File)`} + + : null + } + + {type === 'staging' && !isThisSelectionProspective ? +
{ + e.stopPropagation(); + if (type !== 'staging') return; + setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) + }} + > + +
+ : <> + } +
+
+
+ + })} + + +
+ + ) +} diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/VoidChatArea.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/VoidChatArea.tsx new file mode 100644 index 000000000000..13bb4daad813 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/composer/VoidChatArea.tsx @@ -0,0 +1,554 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { ButtonHTMLAttributes, useCallback, useMemo } from 'react'; +import { FileText, Image as ImageIcon } from 'lucide-react'; +import { useAccessor, useSettingsState } from '../../util/services.js'; +import { VoidCustomDropdownBox, VoidSlider, VoidSwitch } from '../../util/inputs.js'; +import { ModelDropdown } from '../../settings/ModelDropdown.js'; +import { ChatMode, FeatureName, isValidProviderModelSelection } from '../../../../../../../workbench/contrib/cortexide/common/cortexideSettingsTypes.js'; +import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; +import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js'; +import { IconX, IconArrowUp, IconSquare } from '../shared/icons.js'; +import { SelectedFiles } from './SelectedFiles.js'; + +// SLIDER ONLY: +const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => { + const accessor = useAccessor() + + const cortexideSettingsService = accessor.get('ICortexideSettingsService') + const voidSettingsState = useSettingsState() + + const modelSelection = voidSettingsState.modelSelectionOfFeature[featureName] + const overridesOfModel = voidSettingsState.overridesOfModel + + if (!modelSelection) return null + + // Skip "auto" - it's not a real provider + if (!isValidProviderModelSelection(modelSelection)) { + return null; + } + + const { modelName, providerName } = modelSelection + const { reasoningCapabilities } = getModelCapabilities(providerName, modelName, overridesOfModel) + const { canTurnOffReasoning, reasoningSlider: reasoningBudgetSlider } = reasoningCapabilities || {} + + const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[modelName] + const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel) + + if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider + return
+ Thinking + { + const isOff = canTurnOffReasoning && !newVal + cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff }) + }} + /> +
+ } + + if (reasoningBudgetSlider?.type === 'budget_slider') { // if it's a slider + const { min: min_, max, default: defaultVal } = reasoningBudgetSlider + + const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters + const stepSize = Math.round((max - min_) / nSteps) + + const valueIfOff = min_ - stepSize + const min = canTurnOffReasoning ? valueIfOff : min_ + const value = isReasoningEnabled ? voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal + : valueIfOff + + return
+ Thinking + { + if (modelSelection.providerName === 'auto' && modelSelection.modelName === 'auto') return; + const isOff = canTurnOffReasoning && newVal === valueIfOff + cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningBudget: newVal }) + }} + /> + {isReasoningEnabled ? `${value} tokens` : 'Thinking disabled'} +
+ } + + if (reasoningBudgetSlider?.type === 'effort_slider') { + + const { values, default: defaultVal } = reasoningBudgetSlider + + const min = canTurnOffReasoning ? -1 : 0 + const max = values.length - 1 + + const currentEffort = voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEffort ?? defaultVal + const valueIfOff = -1 + const value = isReasoningEnabled && currentEffort ? values.indexOf(currentEffort) : valueIfOff + + const currentEffortCapitalized = currentEffort.charAt(0).toUpperCase() + currentEffort.slice(1, Infinity) + + return
+ Thinking + { + if (modelSelection.providerName === 'auto' && modelSelection.modelName === 'auto') return; + const isOff = canTurnOffReasoning && newVal === valueIfOff + cortexideSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningEffort: values[newVal] ?? undefined }) + }} + /> + {isReasoningEnabled ? `${currentEffortCapitalized}` : 'Thinking disabled'} +
+ } + + return null +} + + + +const nameOfChatMode: Record = { + 'normal': 'Ask', + 'gather': 'Gather', + 'plan': 'Plan', + 'agent': 'Agent', +} + +const detailOfChatMode: Record = { + 'normal': 'Ask questions about your code', + 'gather': 'Reads files, no edits', + 'plan': 'Plans first, then executes', + 'agent': 'Edits files and uses tools', +} + + +const ChatModeDropdown = ({ className }: { className: string }) => { + const accessor = useAccessor() + + const cortexideSettingsService = accessor.get('ICortexideSettingsService') + const settingsState = useSettingsState() + + const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'plan', 'agent'], []) + + const onChangeOption = useCallback((newVal: ChatMode) => { + cortexideSettingsService.setGlobalSetting('chatMode', newVal) + }, [cortexideSettingsService]) + + const getModeDisplayName = (val: ChatMode) => nameOfChatMode[val] ?? val + + return detailOfChatMode[val]} + getOptionsEqual={(a, b) => a === b} + /> + +} + + + + + +interface CortexideChatAreaProps { + // Required + children: React.ReactNode; // This will be the input component + + // Form controls + onSubmit: () => void; + onAbort: () => void; + isStreaming: boolean; + isDisabled?: boolean; + divRef?: React.RefObject; + + // UI customization + className?: string; + showModelDropdown?: boolean; + showSelections?: boolean; + showProspectiveSelections?: boolean; + loadingIcon?: React.ReactNode; + + selections?: StagingSelectionItem[] + setSelections?: (s: StagingSelectionItem[]) => void + // selections?: any[]; + // onSelectionsChange?: (selections: any[]) => void; + + onClickAnywhere?: () => void; + // Optional close button + onClose?: () => void; + + // Image attachments + imageAttachments?: React.ReactNode; + onImagePaste?: (files: File[]) => void; + onImageDrop?: (files: File[]) => void; + onImageUpload?: () => void; + onPDFDrop?: (files: File[]) => void; + pdfAttachments?: React.ReactNode; + + featureName: FeatureName; +} + +export const VoidChatArea: React.FC = ({ + children, + onSubmit, + onAbort, + onClose, + onClickAnywhere, + divRef, + isStreaming = false, + isDisabled = false, + className = '', + showModelDropdown = true, + showSelections = false, + showProspectiveSelections = false, + selections, + setSelections, + imageAttachments, + onImagePaste, + onImageDrop, + onImageUpload, + onPDFDrop, + pdfAttachments, + featureName, + loadingIcon, +}) => { + const [isDragOver, setIsDragOver] = React.useState(false); + const imageInputRef = React.useRef(null); + const pdfInputRef = React.useRef(null); + const containerRef = React.useRef(null); + + // allow-any-unicode-next-line + // Handle paste — listens on container (bubbles from textarea) AND document level + // (document listener catches Ctrl+V when the sidebar panel is focused but textarea isn't) + React.useEffect(() => { + const handlePaste = (e: ClipboardEvent) => { + const items = Array.from(e.clipboardData?.items || []); + const imageFiles: File[] = []; + const pdfFiles: File[] = []; + + for (const item of items) { + if (item.type.startsWith('image/')) { + const file = item.getAsFile(); + if (file) imageFiles.push(file); + } else if (item.type === 'application/pdf') { + const file = item.getAsFile(); + if (file) pdfFiles.push(file); + } + } + + if (imageFiles.length > 0 && onImagePaste) { + e.preventDefault(); + onImagePaste(imageFiles); + } + if (pdfFiles.length > 0 && onPDFDrop) { + e.preventDefault(); + onPDFDrop(pdfFiles); + } + }; + + // Primary: attach to container so it catches events bubbling from the textarea + const container = containerRef.current || divRef?.current; + if (container) { + container.addEventListener('paste', handlePaste); + } + + // Fallback: document-level listener catches paste when chat area is visible but + // the textarea isn't focused (e.g. user focuses sidebar panel then Ctrl+V) + const handleDocumentPaste = (e: ClipboardEvent) => { + // Only intercept if the target is not already inside our container + // and our container is mounted in the DOM + const cont = containerRef.current || divRef?.current; + if (!cont) return; + if (cont.contains(e.target as Node)) return; // already handled by container listener + // Only fire if the paste comes from a non-input element (avoid stealing from other inputs) + const target = e.target as HTMLElement; + const tag = target?.tagName?.toLowerCase(); + if (tag === 'input' || tag === 'textarea') return; + + const items = Array.from(e.clipboardData?.items || []); + const imageFiles = items + .filter(i => i.type.startsWith('image/')) + .map(i => i.getAsFile()) + .filter((f): f is File => f !== null); + + if (imageFiles.length > 0 && onImagePaste) { + e.preventDefault(); + onImagePaste(imageFiles); + } + }; + document.addEventListener('paste', handleDocumentPaste); + + return () => { + if (container) container.removeEventListener('paste', handlePaste); + document.removeEventListener('paste', handleDocumentPaste); + }; + }, [divRef, onImagePaste, onPDFDrop]); + + // Throttle drag over events to prevent jank + const lastDragOverTimeRef = React.useRef(0); + const DRAG_THROTTLE_MS = 50; // Update at most every 50ms + + // Handle drag and drop + const handleDragOver = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const now = Date.now(); + if (now - lastDragOverTimeRef.current < DRAG_THROTTLE_MS) { + return; + } + lastDragOverTimeRef.current = now; + + const hasFiles = Array.from(e.dataTransfer.items).some(item => + item.type.startsWith('image/') || item.type === 'application/pdf' + ); + if (hasFiles) { + setIsDragOver(true); + } + }, []); + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + + const imageFiles = Array.from(e.dataTransfer.files).filter(file => + file.type.startsWith('image/') + ); + const pdfFiles = Array.from(e.dataTransfer.files).filter(file => + file.type === 'application/pdf' + ); + + if (imageFiles.length > 0 && onImageDrop) { + onImageDrop(imageFiles); + } + if (pdfFiles.length > 0 && onPDFDrop) { + onPDFDrop(pdfFiles); + } + }; + + const handleImageUploadClick = () => { + imageInputRef.current?.click(); + }; + + const handlePDFUploadClick = () => { + pdfInputRef.current?.click(); + }; + + const handleImageInputChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []).filter(file => + file.type.startsWith('image/') + ); + if (files.length > 0 && onImageDrop) { + onImageDrop(files); + } + e.target.value = ''; // Reset input + }; + + const handlePDFInputChange = (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []).filter(file => + file.type === 'application/pdf' + ); + if (files.length > 0 && onPDFDrop) { + onPDFDrop(files); + } + e.target.value = ''; // Reset input + }; + + return ( +
{ + if (divRef) { + if (typeof divRef === 'function') { + divRef(node); + } else { + divRef.current = node; + } + } + containerRef.current = node; + }} + className={` + gap-x-1 + flex flex-col p-2.5 relative input text-left shrink-0 + rounded-2xl + bg-[#030304] + transition-all duration-200 + border border-[rgba(255,255,255,0.08)] focus-within:border-[rgba(255,255,255,0.12)] hover:border-[rgba(255,255,255,0.12)] + ${isDragOver ? 'border-blue-500 bg-blue-500/10' : ''} + max-h-[80vh] overflow-y-auto + ${className} + `} + onClick={(e) => { + onClickAnywhere?.() + }} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + > + {/* Hidden file inputs - separate for images and PDFs */} + + + + {/* Image attachments section */} + {imageAttachments} + + {/* PDF attachments section */} + {pdfAttachments} + + {/* Selections section */} + {showSelections && selections && setSelections && ( + + )} + + {/* Input section - Modern Cursor-style layout */} +
+
+ {children} +
+ + {/* Right-side icon bar - Cursor style */} +
+ {/* Image upload button */} + + + {/* PDF upload button */} + + + {/* Submit button */} + {isStreaming ? ( + + ) : ( + + )} +
+ + {/* Close button (X) if onClose is provided */} + {onClose && ( +
+ +
+ )} +
+ + {/* Bottom row - Model selector and settings */} +
+ {showModelDropdown && ( +
+ {featureName === 'Chat' && } + + +
+ )} + + {/* Loading indicator */} + {isStreaming && loadingIcon && ( +
+ {loadingIcon} +
+ )} +
+
+ ); +}; + + + + +type ButtonProps = ButtonHTMLAttributes +const DEFAULT_BUTTON_SIZE = 22; +export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { + + return +} + +export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => { + return +} diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/ContextChipsBar.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/ContextChipsBar.tsx new file mode 100644 index 000000000000..ec8819733f13 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/ContextChipsBar.tsx @@ -0,0 +1,39 @@ +/*-------------------------------------------------------------------------------------- + * 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 { useAccessor, useSettingsState } from '../../util/services.js'; + +export const ContextChipsBar = () => { + const accessor = useAccessor(); + const settingsState = useSettingsState(); + const editorService = accessor.get('IEditorService'); + const activeEditor = editorService?.activeEditor; + const activeResource = activeEditor?.resource; + const activeFileLabel = activeResource ? activeResource.path?.split('/').pop() : undefined; + const modelSel = settingsState.modelSelectionOfFeature['Chat']; + const modelLabel = modelSel ? `${modelSel.providerName}:${modelSel.modelName}` : undefined; + + if (!activeFileLabel && !modelLabel) { + return null; + } + + return ( +
+ {activeFileLabel && ( + + File + {activeFileLabel} + + )} + {modelLabel && ( + + Model + {modelLabel} + + )} +
+ ); +}; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/LandingPage.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/LandingPage.tsx new file mode 100644 index 000000000000..7a757b992124 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/LandingPage.tsx @@ -0,0 +1,78 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import React, { ReactNode, RefObject } from 'react'; +import { useMemo } from 'react'; +import { FileAccess } from '../../../../../../../../base/common/network.js'; +import ErrorBoundary from '../ErrorBoundary.js'; +import { PastThreadsList } from '../SidebarThreadSelector.js'; +import { ContextChipsBar } from './ContextChipsBar.js'; +import { QuickActionsBar } from './QuickActionsBar.js'; +import { SuggestedPrompts } from './SuggestedPrompts.js'; + +export type LandingPageProps = { + sidebarRef: RefObject; + inputSection: ReactNode; + showPreviousThreads: boolean; + suggestionsLabel: string; + previousThreadsLabel: string; + onSubmitPrompt: (text: string) => void; +}; + +export const LandingPage = ({ + sidebarRef, + inputSection, + showPreviousThreads, + suggestionsLabel, + previousThreadsLabel, + onSubmitPrompt, +}: LandingPageProps) => { + const logoUri = useMemo( + () => FileAccess.asBrowserUri('vs/workbench/browser/media/cortexide-main.png').toString(true), + [], + ); + + return ( +
+
+
+ CortexIDE +
+ CortexIDE +
+ + + {inputSection} + + + + + + + + + + + {showPreviousThreads ? ( + +
{previousThreadsLabel}
+ +
+ ) : ( + +
{suggestionsLabel}
+ +
+ )} +
+ ); +}; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/QuickActionsBar.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/QuickActionsBar.tsx new file mode 100644 index 000000000000..c44bad7a9af0 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/QuickActionsBar.tsx @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------------------- + * 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 { useAccessor } from '../../util/services.js'; +import { ICommandService } from '../../../../../../../../platform/commands/common/commands.js'; + +const QUICK_ACTIONS: { id: string; label: string }[] = [ + { id: 'void.explainCode', label: 'Explain' }, + { id: 'void.refactorCode', label: 'Refactor' }, + { id: 'void.addTests', label: 'Add Tests' }, + { id: 'void.fixTests', label: 'Fix Tests' }, + { id: 'void.writeDocstring', label: 'Docstring' }, + { id: 'void.optimizeCode', label: 'Optimize' }, + { id: 'void.debugCode', label: 'Debug' }, +]; + +export const QuickActionsBar = () => { + const accessor = useAccessor(); + const commandService = accessor.get('ICommandService') as ICommandService; + const keybindingService = accessor.get('IKeybindingService'); + + return ( +
+ {QUICK_ACTIONS.map(({ id, label }) => { + const kb = keybindingService.lookupKeybinding(id)?.getLabel(); + return ( + + ); + })} +
+ ); +}; diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/SuggestedPrompts.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/SuggestedPrompts.tsx new file mode 100644 index 000000000000..1afe1e408370 --- /dev/null +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/landing/SuggestedPrompts.tsx @@ -0,0 +1,26 @@ +/*-------------------------------------------------------------------------------------- + * 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'; + +const DEFAULT_PROMPTS = [ + 'Summarize my codebase', + 'How do types work in Rust?', + 'Create a .voidrules file for me', +]; + +export const SuggestedPrompts = ({ onSubmit }: { onSubmit: (text: string) => void }) => ( +
+ {DEFAULT_PROMPTS.map((text, index) => ( +
onSubmit(text)} + > + {text} +
+ ))} +
+);