diff --git a/extensions/theme-defaults/package.json b/extensions/theme-defaults/package.json index 2458ef317da..2c74765fc5a 100644 --- a/extensions/theme-defaults/package.json +++ b/extensions/theme-defaults/package.json @@ -60,6 +60,12 @@ "label": "%lightHcColorThemeLabel%", "uiTheme": "hc-light", "path": "./themes/hc_light.json" + }, + { + "id": "CortexIDE Dark", + "label": "%cortexideDarkColorThemeLabel%", + "uiTheme": "vs-dark", + "path": "./themes/cortexide_dark.json" } ], "iconThemes": [ diff --git a/extensions/theme-defaults/package.nls.json b/extensions/theme-defaults/package.nls.json index cacbd6b8d9a..7b591616dac 100644 --- a/extensions/theme-defaults/package.nls.json +++ b/extensions/theme-defaults/package.nls.json @@ -9,5 +9,6 @@ "lightColorThemeLabel": "Light (Visual Studio)", "hcColorThemeLabel": "Dark High Contrast", "lightHcColorThemeLabel": "Light High Contrast", + "cortexideDarkColorThemeLabel": "CortexIDE Dark", "minimalIconThemeLabel": "Minimal (Visual Studio Code)" } diff --git a/extensions/theme-defaults/themes/cortexide_dark.json b/extensions/theme-defaults/themes/cortexide_dark.json new file mode 100644 index 00000000000..e903f1b2bec --- /dev/null +++ b/extensions/theme-defaults/themes/cortexide_dark.json @@ -0,0 +1,38 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "CortexIDE Dark", + "include": "./dark_vs.json", + "colors": { + "editor.background": "#010101", + "editor.foreground": "#e2e3f0", + "sideBar.background": "#040404", + "sideBar.foreground": "#e2e3f0", + "sideBarTitle.foreground": "#a5a7c4", + "input.background": "#080808", + "input.foreground": "#e2e3f0", + "input.placeholderForeground": "#6d708d", + "input.border": "#1f1f1f", + "focusBorder": "#7b5cff", + "checkbox.border": "#1f1f1f", + "editor.inactiveSelectionBackground": "#0d0d0d", + "editorIndentGuide.background1": "#161616", + "editorIndentGuide.activeBackground1": "#1f1f1f", + "editor.selectionHighlightBackground": "#7b5cff26", + "list.dropBackground": "#101010", + "activityBarBadge.background": "#7b5cff", + "menu.background": "#080808", + "menu.foreground": "#e2e3f0", + "menu.separatorBackground": "#1f1f1f", + "menu.border": "#1f1f1f", + "menu.selectionBackground": "#101010", + "sideBarSectionHeader.background": "#0000", + "sideBarSectionHeader.border": "#1f1f1f", + "tab.selectedBackground": "#080808", + "tab.selectedForeground": "#e2e3f0", + "tab.lastPinnedBorder": "#1f1f1f", + "list.activeSelectionIconForeground": "#e2e3f0", + "terminal.inactiveSelectionBackground": "#0d0d0d", + "widget.border": "#1f1f1f", + "actionBar.toggledBackground": "#101010" + } +} diff --git a/product.json b/product.json index 6de76a55917..f590d285658 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "CortexIDE", "nameLong": "CortexIDE", - "cortexVersion": "0.0.9", + "cortexVersion": "0.0.10", "cortexRelease": "0001", "applicationName": "cortexide", "dataFolderName": ".cortexide", diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 65a9a0e06af..10af5107892 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -227,7 +227,8 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun this.availableUpdate.updateFilePath = path.join(cachePath, `CodeSetup-${this.productService.quality}-${update.version}.flag`); await pfs.Promises.writeFile(this.availableUpdate.updateFilePath, 'flag'); - const child = spawn(this.availableUpdate.packagePath, ['/verysilent', '/log', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + // Do not pass /mergetasks so the installer preserves the user's previous task choices (desktop icon, quick launch, etc.) from the registry instead of forcing our own. + const child = spawn(this.availableUpdate.packagePath, ['/verysilent', '/log', `/update="${this.availableUpdate.updateFilePath}"`, '/nocloseapplications'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'], windowsVerbatimArguments: true @@ -256,7 +257,8 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun if (this.availableUpdate.updateFilePath) { fs.unlinkSync(this.availableUpdate.updateFilePath); } else { - spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], { + // Do not pass /mergetasks so the installer preserves the user's previous task choices (desktop icon, quick launch, etc.). + spawn(this.availableUpdate.packagePath, ['/silent', '/log'], { detached: true, stdio: ['ignore', 'ignore', 'ignore'] }); diff --git a/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css b/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css index 931701be728..7fbad81f289 100644 --- a/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css +++ b/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css @@ -109,43 +109,18 @@ /* ---------------------------------------------------------------------------------------- * Workbench chrome & layout scaffolding + * Do NOT override body/part background and color or --vscode-* so the user's selected + * theme (e.g. Solarized Dark, Light+) applies. CortexIDE variables (--cortex-*, --void-*) + * remain in :root for use inside CortexIDE-specific UI (e.g. .void-scope, chat panel). * ------------------------------------------------------------------------------------- */ body.monaco-workbench, .monaco-workbench { - background: var(--cortex-surface-0) !important; - color: var(--cortex-text-base) !important; font-size: var(--cortex-font-body); } -body.monaco-workbench { - --vscode-activityBar-background: var(--cortex-surface-2); - --vscode-activityBar-activeBorder: var(--cortex-brand); - --vscode-activityBarBadge-background: var(--cortex-brand); - --vscode-button-background: #0f0f0f; - --vscode-button-foreground: var(--cortex-text-base); - --vscode-button-hoverBackground: #1a1a1a; - --vscode-focusBorder: var(--cortex-brand); - --vscode-input-background: #080808; - --vscode-input-border: var(--cortex-border-strong); - --vscode-input-foreground: var(--cortex-text-base); - --vscode-list-activeSelectionBackground: #111111; - --vscode-menu-selectionBackground: #141414; - --vscode-panel-background: var(--cortex-surface-1); - --vscode-panel-border: var(--cortex-border-strong); - --vscode-sideBar-background: var(--cortex-surface-1); - --vscode-sideBarSectionHeader-background: var(--cortex-surface-1); - --vscode-statusBar-background: var(--cortex-surface-0); - --vscode-statusBar-border: var(--cortex-border-strong); - --vscode-tab-activeBackground: var(--cortex-surface-1); - --vscode-tab-inactiveBackground: var(--cortex-surface-0); - --vscode-titleBar-activeBackground: var(--cortex-surface-0); - --vscode-titleBar-border: #101010; -} - .monaco-workbench .part.titlebar, .monaco-workbench .part.statusbar { backdrop-filter: blur(16px); - background: var(--cortex-surface-0) !important; box-shadow: var(--cortex-shadow-hairline); } @@ -155,20 +130,6 @@ body.monaco-workbench { backdrop-filter: none; } -.monaco-workbench .part.activitybar { - background: var(--cortex-surface-3) !important; - border-right: 1px solid var(--cortex-border-strong) !important; - box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.02); -} - -.monaco-workbench .part.sidebar, -.monaco-workbench .part.auxiliarybar, -.monaco-workbench .part.panel { - background: var(--cortex-surface-1) !important; - border-color: var(--cortex-border-strong) !important; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); -} - .monaco-workbench .part.sidebar > .content .pane-header, .monaco-workbench .part.auxiliarybar > .content .pane-header { padding: var(--cortex-space-sm) var(--cortex-space-md) !important; @@ -194,26 +155,6 @@ body.monaco-workbench { letter-spacing: 0.01em; } -.monaco-workbench .part.editor, -.monaco-workbench .editor-container, -.monaco-workbench .part.editor>.content .editor-group-container, -.monaco-workbench .auxiliarybar-part { - background-color: var(--cortex-surface-1) !important; - color: var(--cortex-text-base) !important; -} - -.monaco-workbench .tab, -.monaco-workbench .monaco-list, -.monaco-workbench .split-view-view, -.monaco-workbench .pane-body, -.monaco-editor, -.monaco-editor .margin, -.monaco-editor-background, -.monaco-workbench .part.editor>.content .editor-group-container { - background-color: var(--cortex-surface-1) !important; - color: var(--cortex-text-base) !important; -} - .monaco-editor .void-sweepIdxBG { background-color: var(--vscode-void-sweepIdxBG); } @@ -305,108 +246,64 @@ body.monaco-workbench { /* ---------------------------------------------------------------------------------------- * Explorer / Search / Extensions surfaces * ------------------------------------------------------------------------------------- */ -.monaco-workbench .part.sidebar .composite, -.monaco-workbench .part.sidebar .pane-body, -.monaco-workbench .part.sidebar .monaco-list, -.monaco-workbench .part.sidebar .monaco-list-rows, -.monaco-workbench .part.sidebar .monaco-tree, -.monaco-workbench .part.sidebar .split-view-view, -.monaco-workbench .part.sidebar .pane-body .monaco-list-row, -.monaco-workbench .part.sidebar .pane-body .monaco-list-row:hover { - background-color: var(--cortex-surface-1) !important; - color: var(--cortex-text-base) !important; -} - -.monaco-workbench .part.sidebar .pane-header, -.monaco-workbench .part.sidebar .pane-header .title { - background-color: var(--cortex-surface-0) !important; - color: var(--cortex-text-base) !important; -} - -.monaco-workbench .extensions-viewlet .extensions-list .monaco-list-row, -.monaco-workbench .extensions-viewlet .extension-editor, -.monaco-workbench .extensions-viewlet .extension-editor .body { - background-color: var(--cortex-surface-1) !important; - color: var(--cortex-text-base) !important; -} - -.monaco-workbench .search-view .search-widget, -.monaco-workbench .search-view .query-details, -.monaco-workbench .search-view .monaco-split-view2, -.monaco-workbench .search-view .results .monaco-list { - background-color: var(--cortex-surface-1) !important; - color: var(--cortex-text-base) !important; - border-color: var(--cortex-border-strong) !important; -} +/* Sidebar, extensions, search: use user theme (no cortex overrides) */ /* ---------------------------------------------------------------------------------------- - * Chat / message input experience + * Chat / message input experience - use user theme so chat panel respects selected theme * ------------------------------------------------------------------------------------- */ .monaco-workbench .interactive-session .chat-input-container { - background-color: var(--cortex-surface-2) !important; - border: 1px solid var(--cortex-border-strong) !important; + background-color: var(--vscode-input-background) !important; + border: 1px solid var(--vscode-input-border) !important; box-shadow: none !important; } .monaco-workbench .interactive-session .chat-input-container:focus-within { - border-color: var(--cortex-brand) !important; - box-shadow: 0 0 0 1px var(--cortex-brand) inset; + border-color: var(--vscode-focusBorder) !important; + box-shadow: 0 0 0 1px var(--vscode-focusBorder) inset; } .monaco-workbench .interactive-session .chat-input-container .chat-editor-container .monaco-editor, .monaco-workbench .interactive-session .chat-input-container .chat-editor-container .monaco-editor .monaco-editor-background, .monaco-workbench .interactive-session .chat-input-container .chat-editor-container .monaco-editor .margin, .monaco-workbench .interactive-session .chat-input-container .chat-editor-container .monaco-editor .inputarea.ime-input { - background-color: var(--cortex-surface-2) !important; - color: var(--cortex-text-base) !important; + background-color: var(--vscode-input-background) !important; + color: var(--vscode-input-foreground) !important; } .monaco-workbench .interactive-session .chat-input-container .chat-editor-container .monaco-editor .cursor { - border-color: var(--cortex-text-base) !important; + border-color: var(--vscode-input-foreground) !important; } .monaco-workbench .interactive-session .chat-input-toolbars, .monaco-workbench .interactive-session .chat-input-toolbars .monaco-toolbar .action-item, .monaco-workbench .interactive-session .chat-modelPicker-item { - background-color: var(--cortex-surface-0) !important; - color: var(--cortex-text-base) !important; - border-color: var(--cortex-border-strong) !important; + background-color: var(--vscode-sideBar-background) !important; + color: var(--vscode-sideBar-foreground) !important; + border-color: var(--vscode-sideBar-border) !important; +} + +/* Chat panel (auxiliary bar): override void/cortex variables so React chat UI uses theme */ +.monaco-workbench .part.auxiliarybar > .content { + --void-bg-1: var(--vscode-sideBar-background); + --void-bg-1-alt: var(--vscode-sideBar-background); + --void-bg-2: var(--vscode-sideBar-background); + --void-bg-2-alt: var(--vscode-sideBar-background); + --void-bg-2-hover: var(--vscode-list-hoverBackground, var(--vscode-sideBar-background)); + --void-bg-3: var(--vscode-sideBar-background); + --void-fg-0: var(--vscode-sideBar-foreground); + --void-fg-1: var(--vscode-sideBar-foreground); + --void-fg-2: var(--vscode-sideBar-foreground); + --void-fg-3: var(--vscode-descriptionForeground, var(--vscode-sideBar-foreground)); + --void-fg-4: var(--vscode-descriptionForeground, var(--vscode-sideBar-foreground)); + --void-border-1: var(--vscode-sideBar-border, transparent); + --void-border-2: var(--vscode-sideBar-border, transparent); + --void-border-3: var(--vscode-sideBar-border, transparent); + --void-border-4: var(--vscode-sideBar-border, transparent); } /* ---------------------------------------------------------------------------------------- - * Settings experience + * Settings experience: use user theme (no cortex background/color overrides) * ------------------------------------------------------------------------------------- */ -.monaco-workbench .settings-editor, -.monaco-workbench .settings-editor .settings-body, -.monaco-workbench .settings-editor .settings-tree-container, -.monaco-workbench .settings-editor .settings-toc-container { - background: var(--cortex-surface-1) !important; - color: var(--cortex-text-base); -} - -.monaco-workbench .settings-editor .settings-header { - background: var(--cortex-surface-0) !important; - border-bottom: 1px solid var(--cortex-border-strong); -} - -.monaco-workbench .settings-editor .settings-target-container .settings-target-button, -.monaco-workbench .settings-editor .settings-header-controls .monaco-dropdown, -.monaco-workbench .settings-editor .settings-target-container .monaco-select-box { - background: var(--cortex-surface-2) !important; - border-color: var(--cortex-border-strong) !important; - color: var(--cortex-text-base) !important; -} - -.monaco-workbench .settings-editor .monaco-list.settings-toc-tree, -.monaco-workbench .settings-editor .monaco-list.settings-toc-tree .monaco-list-row { - background: transparent !important; -} - -.monaco-workbench .settings-editor .setting-item { - background: var(--cortex-surface-2) !important; - border: 1px solid var(--cortex-border-weak) !important; - border-radius: var(--cortex-radius-md); -} /* styles for all containers used by void */ .void-scope { @@ -530,12 +427,13 @@ body.monaco-workbench { background-size: contain; background-repeat: no-repeat; background-position: center; + border-radius: 50%; -webkit-mask: none; mask: none; } .monaco-workbench .editor-group-watermark .void-void-icon { - border-radius: 0 !important; + border-radius: 50% !important; background-color: transparent !important; mask: none !important; -webkit-mask: none !important; @@ -646,67 +544,4 @@ body.monaco-workbench { color: var(--cortex-text-muted) !important; } -/* ---------------------------------------------------------------------------------------- - * Settings / preferences - * ------------------------------------------------------------------------------------- */ -.monaco-workbench .settings-editor, -.monaco-workbench .settings-editor > .settings-header, -.monaco-workbench .settings-editor > .settings-body, -.monaco-workbench .settings-editor .settings-tree-container, -.monaco-workbench .settings-editor .settings-toc-container, -.monaco-workbench .settings-editor .settings-body > .monaco-split-view2 { - background: color-mix(in srgb, var(--void-bg-2-alt) 92%, #020204 8%) !important; - color: var(--void-fg-1) !important; - border-color: var(--void-border-3) !important; -} - -.monaco-workbench .settings-editor .settings-header, -.monaco-workbench .settings-editor .settings-body { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); -} - -.monaco-workbench .settings-editor .suggest-input-container, -.monaco-workbench .settings-editor .monaco-inputbox { - background: color-mix(in srgb, var(--void-bg-1) 85%, #050505 15%) !important; - border: 1px solid var(--void-border-2) !important; - border-radius: 14px !important; -} - -.monaco-workbench .settings-editor .monaco-inputbox input { - background: transparent !important; - color: var(--void-fg-1) !important; -} - -.monaco-workbench .settings-editor .monaco-list-row, -.monaco-workbench .settings-editor .settings-tree-container .setting-item, -.monaco-workbench .settings-editor .settings-tree-container .setting-item-contents { - background: transparent !important; - color: var(--void-fg-1) !important; - border-radius: 16px; -} - -.monaco-workbench .settings-editor .monaco-list-row.focused, -.monaco-workbench .settings-editor .monaco-list-row:hover, -.monaco-workbench .settings-editor .setting-item.focused, -.monaco-workbench .settings-editor .setting-item:hover { - background: color-mix(in srgb, var(--void-bg-1-alt) 70%, #0a0c16 30%) !important; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03); -} - -.monaco-workbench .settings-editor .settings-toc-container { - border-right: 1px solid var(--void-border-3) !important; -} - -.monaco-workbench .settings-editor .settings-group-title-label, -.monaco-workbench .settings-editor .setting-item-label, -.monaco-workbench .settings-editor .setting-item-description { - color: var(--void-fg-1) !important; -} - -.monaco-workbench .settings-editor .setting-item-title.is-overridden::before { - color: var(--void-warning) !important; -} - -.monaco-workbench .settings-editor .settings-toc-container .monaco-list-row.focused .monaco-tl-contents { - color: var(--void-fg-0) !important; -} +/* Settings / preferences: use user theme (no void/cortex background overrides) */ diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/util/services.tsx index 501798177fd..1c42c906baa 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/util/services.tsx @@ -76,7 +76,8 @@ let refreshModelState: RefreshModelStateOfProvider const refreshModelStateListeners: Set<(s: RefreshModelStateOfProvider) => void> = new Set() const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: RefreshModelStateOfProvider) => void> = new Set() -let colorThemeState: ColorScheme +// Default to LIGHT so useIsDark() is never undefined before _registerServices runs +let colorThemeState: ColorScheme = ColorScheme.LIGHT const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() @@ -145,10 +146,15 @@ export const _registerServices = (accessor: ServicesAccessor) => { ) colorThemeState = themeService.getColorTheme().type + // Notify any already-mounted components so they get correct initial theme + colorThemeStateListeners.forEach(l => l(colorThemeState)) disposables.push( themeService.onDidColorThemeChange(({ type }) => { colorThemeState = type - colorThemeStateListeners.forEach(l => l(colorThemeState)) + // Defer to next frame so we don't call React setState during theme application (avoids "update while rendering" when switching theme) + requestAnimationFrame(() => { + colorThemeStateListeners.forEach(l => l(colorThemeState)) + }) }) ) diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/void-settings-tsx/Settings.tsx index f0085e17819..3c989114d05 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/void-settings-tsx/Settings.tsx @@ -1083,7 +1083,7 @@ export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?: /> Use headless browsing - (ℹ️) + (i) @@ -1530,7 +1530,7 @@ const MCPServersList = () => { export const Settings = () => { const isDark = useIsDark() - // ─── sidebar nav ────────────────────────── + // --- sidebar nav --- const [selectedSection, setSelectedSection] = useState('models'); @@ -1622,9 +1622,17 @@ export const Settings = () => { return ( -
+
- {/* ────────────── SIDEBAR ────────────── */} + {/* --- SIDEBAR --- */}
- {/* ───────────── MAIN PANE ───────────── */} + {/* --- MAIN PANE --- */}
diff --git a/src/vs/workbench/contrib/cortexide/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/cortexide/electron-main/llmMessage/sendLLMMessage.impl.ts index e79ad9c9a25..5df1fdec6f8 100644 --- a/src/vs/workbench/contrib/cortexide/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/cortexide/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -610,6 +610,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE let toolId = '' let toolParamsStr = '' let isRetrying = false // Flag to prevent processing streaming chunks during retry + let timeoutDeliveredPartial = false // Set when stall timeout fires with partial; outer catch skips onError // Detect if this is a local provider for timeout optimization const isExplicitLocalProviderChat = providerName === 'ollama' || providerName === 'vLLM' || providerName === 'lmStudio' @@ -632,36 +633,47 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE const processStreamingResponse = async (response: any) => { _setAborter(() => response.controller.abort()) - // For local models, add hard timeout with partial results - const overallTimeout = isLocalChat ? 20_000 : 120_000 // 20s for local, 120s for remote + // For local models: rolling stall timeout (reset on each chunk) so we only fire after no chunk for stallWindow. + // This prevents premature onFinalMessage(partial) which would freeze the UI while the model keeps streaming. + const stallWindowMs = isLocalChat ? 60_000 : 0 // 60s of no chunks = stall for local; remote uses one-shot below + const oneShotTimeoutMs = isLocalChat ? 0 : 120_000 // remote: 120s from start const firstTokenTimeout = isLocalChat ? 10_000 : 30_000 // 10s for first token on local let firstTokenReceived = false + let overallTimeoutId: ReturnType | null = null + let timeoutFired = false + + const scheduleOverallTimeout = () => { + if (overallTimeoutId) clearTimeout(overallTimeoutId) + const delay = isLocalChat ? stallWindowMs : oneShotTimeoutMs + if (delay <= 0) return + overallTimeoutId = setTimeout(() => { + timeoutFired = true + if (fullTextSoFar || fullReasoningSoFar || toolName) { + timeoutDeliveredPartial = true + const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId) + const toolCallObj = toolCall ? { toolCall } : {} + onFinalMessage({ + fullText: fullTextSoFar, + fullReasoning: fullReasoningSoFar, + anthropicReasoning: null, + ...toolCallObj + }) + response.controller?.abort() + } else { + response.controller?.abort() + onError({ + message: isLocalChat + ? 'Local model timed out (no response for 60s). Try a smaller model or use a cloud model.' + : 'Request timed out.', + fullError: null + }) + } + }, delay) + } - // Set up overall timeout - const timeoutId = setTimeout(() => { - if (fullTextSoFar || fullReasoningSoFar || toolName) { - // We have partial results - commit them - const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId) - const toolCallObj = toolCall ? { toolCall } : {} - onFinalMessage({ - fullText: fullTextSoFar, - fullReasoning: fullReasoningSoFar, - anthropicReasoning: null, - ...toolCallObj - }) - // Note: We don't call onError here since we have partial results - } else { - // No tokens received - abort - response.controller?.abort() - onError({ - message: isLocalChat - ? 'Local model timed out. Try a smaller model or use a cloud model for this task.' - : 'Request timed out.', - fullError: null - }) - } - }, overallTimeout) + // Start overall timeout: rolling for local (reset on each chunk), one-shot for remote + scheduleOverallTimeout() // Set up first token timeout (only for local models) let firstTokenTimeoutId: ReturnType | null = null @@ -682,11 +694,14 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE for await (const chunk of response) { // Check if we're retrying (another response is being processed) if (isRetrying) { - clearTimeout(timeoutId) + if (overallTimeoutId) clearTimeout(overallTimeoutId) if (firstTokenTimeoutId) clearTimeout(firstTokenTimeoutId) return // Stop processing this streaming response, retry is in progress } + // If timeout already fired with partial, stop processing (avoid double onFinalMessage) + if (timeoutFired) break + // Mark first token received if (!firstTokenReceived) { firstTokenReceived = true @@ -696,6 +711,9 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE } } + // Rolling timeout: reset on each chunk for local so we only fire on real stall + if (isLocalChat) scheduleOverallTimeout() + // message const newText = chunk.choices[0]?.delta?.content ?? '' @@ -748,10 +766,11 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE } // Clear timeouts on successful completion - clearTimeout(timeoutId) + if (overallTimeoutId) clearTimeout(overallTimeoutId) if (firstTokenTimeoutId) clearTimeout(firstTokenTimeoutId) - // on final + // on final (skip if timeout already fired and committed partial) + if (timeoutFired) return if (!fullTextSoFar && !fullReasoningSoFar && !toolName) { onError({ message: 'CortexIDE: Response from model was empty.', fullError: null }) } @@ -761,7 +780,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } } catch (streamError) { - clearTimeout(timeoutId) + if (overallTimeoutId) clearTimeout(overallTimeoutId) if (firstTokenTimeoutId) clearTimeout(firstTokenTimeoutId) // If error occurs during streaming, re-throw to be caught by outer catch handler throw streamError @@ -832,6 +851,9 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE }) // when error/fail - this catches errors of both .create() and .then(for await) .catch(async error => { + // Stall timeout already delivered partial and aborted; don't show error + if (timeoutDeliveredPartial) return + // Abort streaming response if it's still running if (streamingResponse) { try {