Restyle /remote success banner as native info notification (#311516)

Add a new 'info' chat content kind that mirrors the existing 'warning'
path and renders via ChatErrorContentPart with ChatErrorLevel.Info, which
already has blue notification styling (.chat-info-codicon). Expose this
via a new ChatResponseInfoPart proposed API and stream.info() method.

Use the new API in copilotcli's /remote success path so the banner
renders as a native blue info notification card with a persistent
'Open on GitHub' button (vscode.open, no model roundtrip).
This commit is contained in:
Elijah King
2026-04-20 15:29:19 -07:00
committed by GitHub
parent 5b72325eb2
commit 87ce530566
10 changed files with 76 additions and 19 deletions
@@ -27,7 +27,7 @@ import { DisposableStore, IDisposable, toDisposable } from '../../../../util/vs/
import { truncate } from '../../../../util/vs/base/common/strings';
import { ThemeIcon } from '../../../../util/vs/base/common/themables';
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
import { ChatResponseMarkdownPart, ChatResponseThinkingProgressPart, ChatSessionStatus, ChatToolInvocationPart, EventEmitter, Uri } from '../../../../vscodeTypes';
import { ChatResponseMarkdownPart, ChatResponseThinkingProgressPart, ChatSessionStatus, ChatToolInvocationPart, EventEmitter, MarkdownString, Uri } from '../../../../vscodeTypes';
import { IToolsService } from '../../../tools/common/toolsService';
import { IChatSessionMetadataStore } from '../../common/chatSessionMetadataStore';
import { ExternalEditTracker } from '../../common/externalEditTracker';
@@ -1191,24 +1191,22 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
const frontendUrl = `https://github.com/${nwo.owner}/${nwo.repo}/tasks/${taskId}`;
this.logService.info(`[CopilotCLISession] MC session created, URL: ${frontendUrl}`);
// Show a notification popup with an "Open" button
const vsCodeApi = require('vscode') as typeof vscode;
const openAction = l10n.t('Open in Browser');
vsCodeApi.window.showInformationMessage(
l10n.t('Remote control enabled. View this session on GitHub.'),
openAction
).then(selection => {
if (selection === openAction) {
vsCodeApi.env.openExternal(vsCodeApi.Uri.parse(frontendUrl));
}
// Render a persistent inline info banner using the proposed
// `stream.info()` API (blue background + blue info icon, matches
// the native chat info notification style). The button uses
// `vscode.open` so it opens the URL externally without invoking
// the model, and the banner stays visible after click.
const banner = new MarkdownString(
`**${l10n.t('Remote control is enabled.')}** ` +
l10n.t('You can open this session from any device.')
);
this._stream?.info(banner);
this._stream?.button({
command: 'vscode.open',
arguments: [Uri.parse(frontendUrl)],
title: l10n.t('Open on GitHub'),
});
// Also show the link inline in the chat stream
this._stream?.markdown(l10n.t(
'Remote control enabled. Open this session from any device:\n\n[{0}]({1})',
frontendUrl, frontendUrl
));
// Step 9: Start continuous event exporter and command poller
this._startMcEventExporter();
this._startMcCommandPoller();
@@ -2132,6 +2132,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart,
ChatResponseCodeblockUriPart: extHostTypes.ChatResponseCodeblockUriPart,
ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart,
ChatResponseInfoPart: extHostTypes.ChatResponseInfoPart,
ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart,
ChatResponseNotebookEditPart: extHostTypes.ChatResponseNotebookEditPart,
ChatResponseWorkspaceEditPart: extHostTypes.ChatResponseWorkspaceEditPart,
@@ -224,6 +224,14 @@ export class ChatAgentResponseStream {
_report(dto);
return this;
},
info(value) {
throwIfDone(this.progress);
checkProposedApiEnabled(that._extension, 'chatParticipantAdditions');
const part = new extHostTypes.ChatResponseInfoPart(value);
const dto = typeConvert.ChatResponseInfoPart.from(part);
_report(dto);
return this;
},
reference(value, iconPath) {
return this.reference2(value, iconPath);
},
@@ -43,7 +43,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js';
import { IViewBadge } from '../../common/views.js';
import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js';
import { IChatRequestModeInstructions } from '../../contrib/chat/common/model/chatModel.js';
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatExternalToolInvocationUpdate, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js';
import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatExternalToolInvocationUpdate, IChatFollowup, IChatHookPart, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffDataSerialized, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTerminalToolInvocationData, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage, IChatInfoMessage, IChatWorkspaceEdit } from '../../contrib/chat/common/chatService/chatService.js';
import { LocalChatSessionUri } from '../../contrib/chat/common/model/chatUri.js';
import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js';
import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js';
@@ -2852,6 +2852,18 @@ export namespace ChatResponseWarningPart {
}
}
export namespace ChatResponseInfoPart {
export function from(part: vscode.ChatResponseInfoPart): Dto<IChatInfoMessage> {
return {
kind: 'info',
content: MarkdownString.from(part.value)
};
}
export function to(part: Dto<IChatInfoMessage>): vscode.ChatResponseInfoPart {
return new types.ChatResponseInfoPart(part.content.value);
}
}
export namespace ChatResponseExtensionsPart {
export function from(part: vscode.ChatResponseExtensionsPart): Dto<IChatExtensionsContent> {
return {
@@ -3354,6 +3366,8 @@ export namespace ChatResponsePart {
return ChatResponseCodeblockUriPart.from(part);
} else if (part instanceof types.ChatResponseWarningPart) {
return ChatResponseWarningPart.from(part);
} else if (part instanceof types.ChatResponseInfoPart) {
return ChatResponseInfoPart.from(part);
} else if (part instanceof types.ChatResponseConfirmationPart) {
return ChatResponseConfirmationPart.from(part);
} else if (part instanceof types.ChatResponseQuestionCarouselPart) {
@@ -3273,6 +3273,17 @@ export class ChatResponseWarningPart {
}
}
export class ChatResponseInfoPart {
value: vscode.MarkdownString;
constructor(value: string | vscode.MarkdownString) {
if (typeof value !== 'string' && value.isTrusted === true) {
throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.');
}
this.value = typeof value === 'string' ? new MarkdownString(value) : value;
}
}
export class ChatResponseCommandButtonPart {
value: vscode.Command;
constructor(value: vscode.Command) {
@@ -2194,6 +2194,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return this.renderConfirmation(context, content, templateData);
} else if (content.kind === 'warning') {
return this.instantiationService.createInstance(ChatErrorContentPart, ChatErrorLevel.Warning, content.content, content, this.chatContentMarkdownRenderer);
} else if (content.kind === 'info') {
return this.instantiationService.createInstance(ChatErrorContentPart, ChatErrorLevel.Info, content.content, content, this.chatContentMarkdownRenderer);
} else if (content.kind === 'hook') {
return this.renderHookPart(content, context, templateData);
} else if (content.kind === 'markdownContent') {
@@ -289,6 +289,11 @@ export interface IChatWarningMessage {
kind: 'warning';
}
export interface IChatInfoMessage {
content: IMarkdownString;
kind: 'info';
}
export interface IChatAgentVulnerabilityDetails {
title: string;
description: string;
@@ -1105,6 +1110,7 @@ export type IChatProgress =
| IChatTaskResult
| IChatCommandButton
| IChatWarningMessage
| IChatInfoMessage
| IChatTextEdit
| IChatNotebookEdit
| IChatWorkspaceEdit
@@ -30,7 +30,7 @@ import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCo
import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js';
import { migrateLegacyTerminalToolSpecificData } from '../chat.js';
import { ChatPerfMark, markChat } from '../chatPerf.js';
import { ChatAgentVoteDirection, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatPlanReview, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, ToolConfirmKind, isIUsedContext } from '../chatService/chatService.js';
import { ChatAgentVoteDirection, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatPlanReview, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatInfoMessage, IChatWorkspaceEdit, ResponseModelState, ToolConfirmKind, isIUsedContext } from '../chatService/chatService.js';
import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js';
import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js';
import { ToolDataSource, IToolData } from '../tools/languageModelToolsService.js';
@@ -189,6 +189,7 @@ export type IChatProgressHistoryResponseContent =
| IChatProgressMessage
| IChatCommandButton
| IChatWarningMessage
| IChatInfoMessage
| IChatTask
| IChatTaskSerialized
| IChatTextEditGroup
@@ -637,6 +638,7 @@ class AbstractResponse implements IResponse {
case 'progressTask':
case 'progressTaskSerialized':
case 'warning':
case 'info':
segment = { text: part.content.value };
break;
default:
@@ -81,6 +81,7 @@ const responsePartSchema = Adapt.v<IChatProgressResponseContent, SerializedChatR
case 'planReview':
case 'undoStop':
case 'warning':
case 'info':
case 'treeData':
case 'workspaceEdit':
case 'disabledClaudeHooks':
@@ -453,6 +453,11 @@ declare module 'vscode' {
constructor(value: string | MarkdownString);
}
export class ChatResponseInfoPart {
value: MarkdownString;
constructor(value: string | MarkdownString);
}
export class ChatResponseProgressPart2 extends ChatResponseProgressPart {
value: string;
task?: (progress: Progress<ChatResponseWarningPart | ChatResponseReferencePart>) => Thenable<string | void>;
@@ -633,6 +638,15 @@ declare module 'vscode' {
*/
warning(message: string | MarkdownString): void;
/**
* Push an info banner to this stream. Short-hand for
* `push(new ChatResponseInfoPart(message))`.
*
* @param message An informational message
* @returns This stream.
*/
info(message: string | MarkdownString): void;
reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void;
reference2(value: Uri | Location | string | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }): void;