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
-
-
- {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
+
+
+
+ {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}
+
+ ))}
+
+);