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 f170a61f8d6..dbc0fea4407 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
@@ -16,6 +16,8 @@ import { ErrorDisplay } from './ErrorDisplay.js';
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js';
import { ModelDropdown, } from '../settings/ModelDropdown.js';
import { PastThreadsList } from './SidebarThreadSelector.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';
@@ -4051,6 +4053,7 @@ export const SidebarChat = () => {
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
// Image attachments management
+ const [showHistory, setShowHistory] = useState(false);
const {
attachments: imageAttachments,
addImages: addImagesRaw,
@@ -5031,11 +5034,22 @@ export const SidebarChat = () => {
return (
-
- {isLandingPage ?
- landingPageContent
- : threadPageContent}
-
+
+
+
setShowHistory(v => !v)}
+ />
+ {showHistory && (
+
+
+
+ )}
+
+ {isLandingPage ? landingPageContent : threadPageContent}
+
+
)
}
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
index 8ef6cf93427..de9767eba09 100644
--- a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx
@@ -238,7 +238,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
group px-3 py-2 rounded-xl border border-void-border-3/70 bg-void-bg-1/40 hover:bg-void-bg-2/70 cursor-pointer text-sm text-void-fg-1 transition-all duration-150 ease-out shadow-[0_8px_20px_rgba(0,0,0,0.35)] hover:-translate-y-0.5
`}
onClick={() => {
- chatThreadsService.switchToThread(pastThread.id);
+ chatThreadsService.switchToTab(pastThread.id);
}}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chrome/ComposerTabs.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chrome/ComposerTabs.tsx
new file mode 100644
index 00000000000..ab4c8e82edf
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chrome/ComposerTabs.tsx
@@ -0,0 +1,82 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { LoaderCircle, Plus, X } from 'lucide-react';
+import { useAccessor, useChatThreadsState, useFullChatThreadsStreamState } from '../../util/services.js';
+import type { IsRunningType } from '../../../../chatThreadService.js';
+import { getThreadTabLabel } from '../../../../common/threadTitle.js';
+
+const MAX_VISIBLE_TABS = 12;
+
+export const ComposerTabs = () => {
+ const accessor = useAccessor();
+ const chatThreadsService = accessor.get('IChatThreadService');
+ const { openTabs, currentThreadId, allThreads } = useChatThreadsState();
+ const streamState = useFullChatThreadsStreamState();
+
+ const tabs = openTabs.slice(0, MAX_VISIBLE_TABS);
+
+ const isRunning = (threadId: string): IsRunningType | undefined =>
+ streamState[threadId]?.isRunning;
+
+ return (
+
+
+ {tabs.map(threadId => {
+ const thread = allThreads[threadId];
+ const active = threadId === currentThreadId;
+ const label = getThreadTabLabel(thread);
+ const running = isRunning(threadId);
+
+ return (
+
chatThreadsService.switchToTab(threadId)}
+ title={label}
+ >
+ {(running === 'LLM' || running === 'tool' || running === 'preparing') && (
+
+ )}
+ {label}
+
+
+ );
+ })}
+
+
+
+ );
+};
diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chrome/ThreadHeader.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chrome/ThreadHeader.tsx
new file mode 100644
index 00000000000..52300bb8b2f
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/browser/react/src/sidebar-tsx/chrome/ThreadHeader.tsx
@@ -0,0 +1,65 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import { History, Settings2 } from 'lucide-react';
+import { useAccessor, useChatThreadsState } from '../../util/services.js';
+import { CORTEXIDE_OPEN_SETTINGS_ACTION_ID } from '../../../../cortexideSettingsPane.js';
+import { getThreadTabLabel } from '../../../../common/threadTitle.js';
+
+type ThreadHeaderProps = {
+ showHistory: boolean;
+ onToggleHistory: () => void;
+};
+
+export const ThreadHeader = ({ showHistory, onToggleHistory }: ThreadHeaderProps) => {
+ const accessor = useAccessor();
+ const commandService = accessor.get('ICommandService');
+ const chatThreadsService = accessor.get('IChatThreadService');
+ const { currentThreadId, allThreads } = useChatThreadsState();
+
+ const thread = allThreads[currentThreadId];
+ const title = getThreadTabLabel(thread);
+ const hasHistory = Object.keys(allThreads).some(
+ id => (allThreads[id]?.messages.length ?? 0) > 0,
+ );
+
+ return (
+
+
+ {title}
+
+
+ {hasHistory && (
+
+ )}
+
+
+
+
+ );
+};
diff --git a/src/vs/workbench/contrib/cortexide/common/threadTitle.ts b/src/vs/workbench/contrib/cortexide/common/threadTitle.ts
new file mode 100644
index 00000000000..c6320c4d69b
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/common/threadTitle.ts
@@ -0,0 +1,34 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+const MAX_TAB_LABEL = 32;
+
+/** Minimal thread shape for tab labels (no browser service import). */
+export type ThreadLabelSource = {
+ messages: { role: string; displayContent?: string }[];
+};
+
+/** Short label for composer tabs and headers (first user message or "New chat"). */
+export const getThreadTabLabel = (thread: ThreadLabelSource | undefined): string => {
+ if (!thread) {
+ return 'New chat';
+ }
+ const firstUserIdx = thread.messages.findIndex(m => m.role === 'user');
+ if (firstUserIdx === -1) {
+ return 'New chat';
+ }
+ const msg = thread.messages[firstUserIdx];
+ if (msg.role !== 'user') {
+ return 'New chat';
+ }
+ const text = (msg.displayContent || '').trim().replace(/\s+/g, ' ');
+ if (!text) {
+ return 'New chat';
+ }
+ if (text.length <= MAX_TAB_LABEL) {
+ return text;
+ }
+ return `${text.slice(0, MAX_TAB_LABEL - 1)}…`;
+};
diff --git a/src/vs/workbench/contrib/cortexide/test/common/threadTitle.test.ts b/src/vs/workbench/contrib/cortexide/test/common/threadTitle.test.ts
new file mode 100644
index 00000000000..464d3800d32
--- /dev/null
+++ b/src/vs/workbench/contrib/cortexide/test/common/threadTitle.test.ts
@@ -0,0 +1,27 @@
+/*--------------------------------------------------------------------------------------
+ * Copyright 2025 Glass Devtools, Inc. All rights reserved.
+ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
+ *--------------------------------------------------------------------------------------*/
+
+import * as assert from 'assert';
+import { suite, test } from 'mocha';
+import { getThreadTabLabel } from '../../common/threadTitle.js';
+
+suite('getThreadTabLabel', () => {
+ test('empty thread is "New chat"', () => {
+ assert.strictEqual(getThreadTabLabel({ messages: [] } as any), 'New chat');
+ });
+
+ test('uses first user message', () => {
+ assert.strictEqual(getThreadTabLabel({
+ messages: [{ role: 'user', displayContent: 'Fix the menubar bug' }],
+ } as any), 'Fix the menubar bug');
+ });
+
+ test('truncates long titles', () => {
+ const long = 'a'.repeat(40);
+ const label = getThreadTabLabel({ messages: [{ role: 'user', displayContent: long }] } as any);
+ assert.ok(label.endsWith('…'));
+ assert.ok(label.length <= 32);
+ });
+});