Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions resources/profiles/agent-sessions.code-profile
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@
"name": "Agent Sessions",
"icon": "vscode",
"settings": {
"workbench.statusBar.visible": false,
"workbench.activityBar.location": "hidden",
"workbench.sideBar.location": "right",
"workbench.secondarySideBar.defaultVisibility": "maximized",
"workbench.secondarySideBar.restoreMaximized": true,
"chat.restoreLastPanelSession": true,
"workbench.startupEditor": "none",
"workbench.editor.restoreEditors": false,
"chat.agentsControl.enabled": true,
"chat.unifiedAgentsBar.enabled": true,
"files.autoSave": "afterDelay",
"window.title": "Agent Sessions",
"workbench.editor.showTabs": "single",
"workbench.colorTheme": "2026-dark-experimental",
"chat.agentsControl.triStateToggle": true,
"workbench.tips.enabled": false,
"chat.agent.maxRequests": 1000,
"workbench.layoutControl.type": "toggles",
"inlineChat.affordance": "editor",
"inlineChat.renderMode": "hover",
"github.copilot.chat.claudeCode.enabled": true,
"github.copilot.chat.languageContext.typescript.enabled": true
"workbench.statusBar.visible": false,
"workbench.activityBar.location": "hidden",
"workbench.sideBar.location": "right",
"workbench.secondarySideBar.defaultVisibility": "maximized",
"workbench.secondarySideBar.restoreMaximized": true,
"chat.restoreLastPanelSession": true,
"workbench.startupEditor": "none",
"workbench.editor.restoreEditors": false,
"chat.agentsControl.enabled": true,
"chat.unifiedAgentsBar.enabled": true,
"files.autoSave": "afterDelay",
"window.title": "Agent Sessions",
"workbench.editor.showTabs": "single",
"workbench.colorTheme": "2026-dark-experimental",
"chat.agentsControl.triStateToggle": true,
"workbench.tips.enabled": false,
"chat.agent.maxRequests": 1000,
"workbench.layoutControl.type": "toggles",
"inlineChat.affordance": "editor",
"inlineChat.renderMode": "hover",
"github.copilot.chat.claudeCode.enabled": true,
"github.copilot.chat.languageContext.typescript.enabled": true,
"diffEditor.renderSideBySide": false,
"diffEditor.hideUnchangedRegions.enabled": true
}
}
11 changes: 7 additions & 4 deletions src/vs/workbench/api/browser/mainThreadChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,14 +348,15 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat

this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions);

this._chatSessionsService.setOptionsChangeCallback(async (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>) => {
this._register(this._chatSessionsService.onRequestNotifyExtension(({ sessionResource, updates, waitUntil }) => {
const handle = this._getHandleForSessionType(sessionResource.scheme);
this._logService.trace(`[MainThreadChatSessions] onRequestNotifyExtension received: scheme '${sessionResource.scheme}', handle ${handle}, ${updates.length} update(s)`);
if (handle !== undefined) {
await this.notifyOptionsChange(handle, sessionResource, updates);
waitUntil(this.notifyOptionsChange(handle, sessionResource, updates));
} else {
this._logService.warn(`[MainThreadChatSessions] Cannot notify option change for scheme '${sessionResource.scheme}': no provider registered. Registered schemes: [${Array.from(this._sessionTypeToHandle.keys()).join(', ')}]`);
}
});
}));

this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => {
for (const [handle, { provider }] of this._itemProvidersRegistrations) {
Expand Down Expand Up @@ -709,10 +710,12 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
* Notify the extension about option changes for a session
*/
async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>): Promise<void> {
this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: starting proxy call for handle ${handle}, sessionResource ${sessionResource}`);
try {
await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None);
this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: proxy call completed for handle ${handle}, sessionResource ${sessionResource}`);
} catch (error) {
this._logService.error(`Error notifying extension about options change for handle ${handle}, sessionResource ${sessionResource}:`, error);
this._logService.error(`[MainThreadChatSessions] notifyOptionsChange: error for handle ${handle}, sessionResource ${sessionResource}:`, error);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { sep } from '../../../../../base/common/path.js';
import { raceCancellationError } from '../../../../../base/common/async.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { AsyncEmitter, Emitter, Event } from '../../../../../base/common/event.js';
import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../../base/common/map.js';
import { Schemas } from '../../../../../base/common/network.js';
Expand All @@ -31,7 +31,7 @@ import { ExtensionsRegistry } from '../../../../services/extensions/common/exten
import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js';
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';
Expand Down Expand Up @@ -277,6 +277,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; }
private readonly _onDidChangeOptionGroups = this._register(new Emitter<string>());
public get onDidChangeOptionGroups() { return this._onDidChangeOptionGroups.event; }
private readonly _onRequestNotifyExtension = this._register(new AsyncEmitter<IChatSessionOptionsWillNotifyExtensionEvent>());
public get onRequestNotifyExtension() { return this._onRequestNotifyExtension.event; }

private readonly inProgressMap: Map<string, number> = new Map();
private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();
Expand Down Expand Up @@ -1030,29 +1032,23 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
return this._sessionTypeOptions.get(chatSessionType);
}

private _optionsChangeCallback?: SessionOptionsChangedCallback;

/**
* Set the callback for notifying extensions about option changes
*/
public setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void {
this._optionsChangeCallback = callback;
}

/**
* Notify extension about option changes for a session
*/
public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void> {
if (!updates.length) {
return;
}
if (this._optionsChangeCallback) {
await this._optionsChangeCallback(sessionResource, updates);
}
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: starting for ${sessionResource}, ${updates.length} update(s): [${updates.map(u => u.optionId).join(', ')}]`);
// Fire event to notify MainThreadChatSessions (which forwards to extension host)
// Uses fireAsync to properly await async listener work via waitUntil pattern
await this._onRequestNotifyExtension.fireAsync({ sessionResource, updates }, CancellationToken.None);
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: fireAsync completed for ${sessionResource}`);
for (const u of updates) {
this.setSessionOption(sessionResource, u.optionId, u.value);
}
this._onDidChangeSessionOptions.fire(sessionResource);
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: finished for ${sessionResource}`);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as dom from '../../../../../../base/browser/dom.js';
import { $, AnimationFrameScheduler, DisposableResizeObserver } from '../../../../../../base/browser/dom.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { IDisposable } from '../../../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { rcut } from '../../../../../../base/common/strings.js';
import { localize } from '../../../../../../nls.js';
Expand All @@ -17,7 +18,7 @@ import { ChatTreeItem } from '../../chat.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js';
import { ChatCollapsibleMarkdownContentPart } from './chatCollapsibleMarkdownContentPart.js';
import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js';
import { IChatMarkdownContent, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService/chatService.js';
import { IRunSubagentToolInputParams, RunSubagentTool } from '../../../common/tools/builtinTools/runSubagentTool.js';
import { autorun } from '../../../../../../base/common/observable.js';
import { Lazy } from '../../../../../../base/common/lazy.js';
Expand All @@ -36,11 +37,22 @@ const MAX_TITLE_LENGTH = 100;
* Represents a lazy tool item that will be created when the subagent section is expanded.
*/
interface ILazyToolItem {
kind: 'tool';
lazy: Lazy<ChatToolInvocationPart>;
toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized;
codeBlockStartIndex: number;
}

/**
* Represents a lazy markdown item (e.g., edit pill) that will be rendered when expanded.
*/
interface ILazyMarkdownItem {
kind: 'markdown';
lazy: Lazy<{ domNode: HTMLElement; disposable?: IDisposable }>;
}

type ILazyItem = ILazyToolItem | ILazyMarkdownItem;

/**
* This is generally copied from ChatThinkingContentPart. We are still experimenting with both UIs so I'm not
* trying to refactor to share code. Both could probably be simplified when stable.
Expand All @@ -59,7 +71,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
private prompt: string | undefined;

// Lazy rendering support
private readonly lazyItems: ILazyToolItem[] = [];
private readonly lazyItems: ILazyItem[] = [];
private hasExpandedOnce: boolean = false;
private pendingPromptRender: boolean = false;
private pendingResultText: string | undefined;
Expand Down Expand Up @@ -500,6 +512,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
} else {
// Defer rendering until expanded
const item: ILazyToolItem = {
kind: 'tool',
lazy: new Lazy(() => this.createToolPart(toolInvocation, codeBlockStartIndex)),
toolInvocation,
codeBlockStartIndex,
Expand All @@ -508,6 +521,61 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}
}

/**
* Appends a markdown item (e.g., an edit pill) to the subagent content part.
* This is used to route codeblockUri parts with subAgentInvocationId to this subagent's container.
*/
public appendMarkdownItem(
factory: () => { domNode: HTMLElement; disposable?: IDisposable },
_codeblocksPartId: string | undefined,
_markdown: IChatMarkdownContent,
_originalParent?: HTMLElement
): void {
// If expanded or has been expanded once, render immediately
if (this.isExpanded() || this.hasExpandedOnce) {
const result = factory();
this.appendMarkdownItemToDOM(result.domNode);
if (result.disposable) {
this._register(result.disposable);
}
} else {
// Defer rendering until expanded
const item: ILazyMarkdownItem = {
kind: 'markdown',
lazy: new Lazy(factory),
};
this.lazyItems.push(item);
}
}

/**
* Appends a markdown item's DOM node to the wrapper.
*/
private appendMarkdownItemToDOM(domNode: HTMLElement): void {
if (!domNode.hasChildNodes() || domNode.textContent?.trim() === '') {
return;
}

// Wrap with icon like other items
const itemWrapper = $('.chat-thinking-tool-wrapper');
const iconElement = createThinkingIcon(Codicon.edit);
itemWrapper.appendChild(domNode);
itemWrapper.insertBefore(iconElement, itemWrapper.firstChild);

// Insert before result container if it exists, otherwise append
if (this.wrapper) {
if (this.resultContainer) {
this.wrapper.insertBefore(itemWrapper, this.resultContainer);
} else {
this.wrapper.appendChild(itemWrapper);
}
}
this.lastItemWrapper = itemWrapper;

// Schedule layout to measure last item and scroll
this.layoutScheduler.schedule();
}

protected override shouldInitEarly(): boolean {
// Never init early - subagent is collapsed while running, content only shown on expand
return false;
Expand Down Expand Up @@ -582,15 +650,23 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}

/**
* Materializes a lazy tool item by creating the tool part and adding it to the DOM.
* Materializes a lazy item by creating the content and adding it to the DOM.
*/
private materializeLazyItem(item: ILazyToolItem): void {
private materializeLazyItem(item: ILazyItem): void {
if (item.lazy.hasValue) {
return; // Already materialized
}

const part = item.lazy.value;
this.appendToolPartToDOM(part, item.toolInvocation);
if (item.kind === 'tool') {
const part = item.lazy.value;
this.appendToolPartToDOM(part, item.toolInvocation);
} else if (item.kind === 'markdown') {
const result = item.lazy.value;
this.appendMarkdownItemToDOM(result.domNode);
if (result.disposable) {
this._register(result.disposable);
}
}
}

/**
Expand Down Expand Up @@ -640,6 +716,10 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
}

hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean {
if (other.kind === 'markdownContent') {
return true;
}

// Match subagent tool invocations with the same subAgentInvocationId to keep them grouped
if ((other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && (other.subAgentInvocationId || other.toolId === RunSubagentTool.Id)) {
// For runSubagent tool, use toolCallId as the effective ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,5 @@
.interactive-session .interactive-response .value .chat-thinking-box .chat-used-context-label code {
display: inline;
line-height: inherit;
padding: 0 4px;
padding: 1px 3px;
}
17 changes: 16 additions & 1 deletion src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { IThemeService } from '../../../../../platform/theme/common/themeService
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
import { IWorkbenchIssueService } from '../../../issue/common/issue.js';
import { CodiconActionViewItem } from '../../../notebook/browser/view/cellParts/cellActionView.js';
import { annotateSpecialMarkdownContent, hasCodeblockUriTag } from '../../common/widget/annotations.js';
import { annotateSpecialMarkdownContent, extractSubAgentInvocationIdFromText, hasCodeblockUriTag } from '../../common/widget/annotations.js';
import { checkModeOption } from '../../common/chat.js';
import { IChatAgentMetadata } from '../../common/participants/chatAgents.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
Expand Down Expand Up @@ -1939,6 +1939,21 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
// append to thinking part when the codeblock is complete
const isComplete = this.isCodeblockComplete(markdown, context.element);

// Check if this markdown should be routed to a subagent content part
const subAgentInvocationId = extractSubAgentInvocationIdFromText(markdown.content.value);
if (subAgentInvocationId) {
const subagentPart = this.getSubagentPart(templateData.renderedParts, subAgentInvocationId);
if (subagentPart && markdownPart?.domNode && isComplete) {
subagentPart.appendMarkdownItem(
() => ({ domNode: markdownPart.domNode, disposable: markdownPart }),
markdownPart.codeblocksPartId,
markdown,
templateData.value
);
return subagentPart;
}
}

// create thinking part if it doesn't exist yet
const lastThinking = this.getLastThinkingPart(templateData.renderedParts);
if (!lastThinking && markdownPart?.domNode && this.shouldPinPart(markdown, context.element) && collapsedToolsMode === CollapsedToolsDisplayMode.Always && isComplete) {
Expand Down
8 changes: 6 additions & 2 deletions src/vs/workbench/contrib/chat/browser/widget/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
display: flex;
flex-direction: column;
gap: 2px;
line-height: 1.5em;
}

.interactive-response-progress-tree,
Expand Down Expand Up @@ -2173,6 +2174,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
.interactive-session .chat-used-context-label .monaco-button {
padding: 2px 6px 2px 2px;
font-size: var(--vscode-chat-font-size-body-m);
line-height: unset;
}

.interactive-session .chat-file-changes-label .monaco-button:hover {
Expand Down Expand Up @@ -2204,8 +2206,8 @@ have to be updated for changes to the rules above, or to support more deeply nes
.interactive-item-container .progress-container {
display: flex;
align-items: center;
gap: 7px;
margin: 0 0 6px 4px;
gap: 4px;
margin: 0 0 6px 2px;

/* Tool calls transition from a progress to a collapsible list part, which needs to have this top padding.
The working progress also can be replaced by a tool progress part. So align this padding so the text doesn't appear to shift. */
Expand All @@ -2222,6 +2224,8 @@ have to be updated for changes to the rules above, or to support more deeply nes
.codicon {
/* Very aggressive list styles try to apply focus colors to every codicon in a list row. */
color: var(--vscode-icon-foreground) !important;
/* Matching the margin on all .monaco-text-button .codicon */
margin: 0 .2em;

&.codicon-error {
color: var(--vscode-editorError-foreground) !important;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export interface IChatResponseCodeblockUriPart {
uri: URI;
isEdit?: boolean;
undoStopId?: string;
subAgentInvocationId?: string;
}

export interface IChatAgentMarkdownContentWithVulnerability {
Expand Down
Loading
Loading