diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9a007f7a8631c..c95ef3058ef01 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,6 +51,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; +import { IInteractiveSessionResponseCommandFollowup } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -1100,6 +1101,7 @@ export interface IInteractiveRequestDto { export interface IInteractiveResponseDto { followups?: string[]; + commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; } export interface IInteractiveResponseProgressDto { diff --git a/src/vs/workbench/api/common/extHostInteractiveSession.ts b/src/vs/workbench/api/common/extHostInteractiveSession.ts index 47541e6692053..b61bbfe1f6346 100644 --- a/src/vs/workbench/api/common/extHostInteractiveSession.ts +++ b/src/vs/workbench/api/common/extHostInteractiveSession.ts @@ -157,7 +157,7 @@ export class ExtHostInteractiveSession implements ExtHostInteractiveSessionShape return; } - return { followups: res.followups }; + return { followups: res.followups, commandFollowups: res.commands }; } throw new Error('provider must implement either provideResponse or provideResponseWithProgress'); diff --git a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts index 0e4f63edf9459..e49ad49c6000a 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts +++ b/src/vs/workbench/contrib/interactiveSession/browser/interactiveSessionListRenderer.ts @@ -28,6 +28,7 @@ import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/contrib/markd import { ViewportSemanticTokensContribution } from 'vs/editor/contrib/semanticTokens/browser/viewportSemanticTokens'; import { SmartSelectController } from 'vs/editor/contrib/smartSelect/browser/smartSelect'; import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -37,6 +38,7 @@ import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreve import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { InteractiveSessionEditorOptions } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionOptions'; +import { IInteractiveSessionResponseCommandFollowup } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; import { IInteractiveRequestViewModel, IInteractiveResponseViewModel, isRequestVM, isResponseVM } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel'; import { getNWords } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter'; @@ -82,6 +84,7 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configService: IConfigurationService, @ILogService private readonly logService: ILogService, + @ICommandService private readonly commandService: ICommandService, ) { super(); this.renderer = this.instantiationService.createInstance(MarkdownRenderer, {}); @@ -193,13 +196,24 @@ export class InteractiveListItemRenderer extends Disposable implements ITreeRend templateData.value.appendChild(result.element); templateData.elementDisposables.add(result); - if (isResponseVM(element) && element.followups?.length && index === this.delegate.getListLength() - 1) { + if (isResponseVM(element) && index === this.delegate.getListLength() - 1) { const followupsContainer = dom.append(templateData.value, $('.interactive-response-followups')); - element.followups.forEach(q => { - const button = templateData.elementDisposables.add(new Button(followupsContainer, defaultButtonStyles)); - button.label = `"${q}"`; - templateData.elementDisposables.add(button.onDidClick(() => this._onDidSelectFollowup.fire(q))); - }); + const followups = element.commandFollowups ?? element.followups ?? []; + followups.forEach(q => this.renderFollowup(followupsContainer, templateData, q)); + } + } + + private renderFollowup(container: HTMLElement, templateData: IInteractiveListItemTemplate, followup: string | IInteractiveSessionResponseCommandFollowup): void { + const button = templateData.elementDisposables.add(new Button(container, { ...defaultButtonStyles, supportIcons: typeof followup !== 'string' })); + const label = typeof followup === 'string' ? `"${followup}"` : followup.title; + button.label = label; + if (typeof followup === 'string') { + // This should probably be a command as well? + templateData.elementDisposables.add(button.onDidClick(() => this._onDidSelectFollowup.fire(followup))); + } else { + templateData.elementDisposables.add(button.onDidClick(() => { + this.commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); + })); } } diff --git a/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css b/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css index 5024ecf582b5a..b8123e6aff326 100644 --- a/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css +++ b/src/vs/workbench/contrib/interactiveSession/browser/media/interactiveSession.css @@ -170,7 +170,7 @@ margin: 0; } -.interactive-session .interactive-response .interactive-session-response-followups { +.interactive-session .interactive-response .interactive-response-followups { display: flex; flex-direction: column; gap: 8px; @@ -178,7 +178,7 @@ margin-bottom: 1em; /* This is matching the margin on rendered markdown */ } -.interactive-session .interactive-response .interactive-session-response-followups .monaco-button { +.interactive-session .interactive-response .interactive-response-followups .monaco-button { width: 100%; padding: 2px 8px; font-size: 11px; diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts index 782aa82806cf7..e522e7b95ea33 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionModel.ts @@ -18,6 +18,12 @@ export interface IInteractiveRequestModel { readonly response: IInteractiveResponseModel | undefined; } +export interface IInteractiveSessionResponseCommandFollowup { + commandId: string; + args: any[]; + title: string; // supports codicon strings +} + export interface IInteractiveResponseModel { readonly onDidChange: Event; readonly id: string; @@ -26,6 +32,7 @@ export interface IInteractiveResponseModel { readonly response: IMarkdownString; readonly isComplete: boolean; readonly followups?: string[]; + readonly commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; } export function isRequest(item: unknown): item is IInteractiveRequestModel { @@ -72,6 +79,11 @@ export class InteractiveResponseModel extends Disposable implements IInteractive return this._followups; } + private _commandFollowups: IInteractiveSessionResponseCommandFollowup[] | undefined; + public get commandFollowups(): IInteractiveSessionResponseCommandFollowup[] | undefined { + return this._commandFollowups; + } + private _response: IMarkdownString; public get response(): IMarkdownString { return this._response; @@ -90,9 +102,10 @@ export class InteractiveResponseModel extends Disposable implements IInteractive this._onDidChange.fire(); } - complete(followups: string[] | undefined): void { + complete(followups: string[] | undefined, commandFollowups: IInteractiveSessionResponseCommandFollowup[] | undefined): void { this._isComplete = true; this._followups = followups; + this._commandFollowups = commandFollowups; this._onDidChange.fire(); } } @@ -211,8 +224,8 @@ export class InteractiveSessionModel extends Disposable implements IInteractiveS } } - completeResponse(request: InteractiveRequestModel, followups?: string[]): void { - request.response!.complete(followups); + completeResponse(request: InteractiveRequestModel, followups?: string[], commandFollowups?: IInteractiveSessionResponseCommandFollowup[]): void { + request.response!.complete(followups, commandFollowups); } setResponse(request: InteractiveRequestModel, response: InteractiveResponseModel): void { diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts index 309c3add9c750..1f337e7d7bf60 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionService.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { InteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; +import { IInteractiveSessionResponseCommandFollowup, InteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; export interface IInteractiveSession { id: number; @@ -27,6 +27,7 @@ export interface IInteractiveRequest { export interface IInteractiveResponse { session: IInteractiveSession; followups?: string[]; + commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; } export interface IInteractiveProgress { diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts index 45ca810f929b1..d6a295fcabeb4 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionServiceImpl.ts @@ -140,7 +140,7 @@ export class InteractiveSessionService extends Disposable implements IInteractiv return; } - model.completeResponse(request, rawResponse.followups); + model.completeResponse(request, rawResponse.followups, rawResponse.commandFollowups); this.trace('sendRequest', `Provider returned response for session ${sessionId} with ${rawResponse.followups} followups`); } finally { this._pendingRequestSessions.delete(sessionId); diff --git a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts index 0ac746f65d36a..4e96a75f10025 100644 --- a/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts +++ b/src/vs/workbench/contrib/interactiveSession/common/interactiveSessionViewModel.ts @@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; -import { IInteractiveRequestModel, IInteractiveResponseModel, IInteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; +import { IInteractiveRequestModel, IInteractiveResponseModel, IInteractiveSessionModel, IInteractiveSessionResponseCommandFollowup } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel'; import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService'; import { countWords } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWordCounter'; @@ -57,6 +57,7 @@ export interface IInteractiveResponseViewModel { readonly response: IMarkdownString; readonly isComplete: boolean; readonly followups?: string[]; + readonly commandFollowups?: IInteractiveSessionResponseCommandFollowup[]; readonly progressiveResponseRenderingEnabled: boolean; readonly contentUpdateTimings?: IInteractiveSessionLiveUpdateData; renderData?: IInteractiveResponseRenderData; @@ -191,6 +192,10 @@ export class InteractiveResponseViewModel extends Disposable implements IInterac return this._model.followups; } + get commandFollowups() { + return this._model.commandFollowups; + } + renderData: IInteractiveResponseRenderData | undefined = undefined; currentRenderedHeight: number | undefined; diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index fd00e63f1eb69..df8ce2a6a8949 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -78,12 +78,19 @@ declare module 'vscode' { export interface InteractiveResponseForProgress { followups?: string[]; + commands?: InteractiveResponseCommand[]; } export interface InteractiveProgress { content: string; } + export interface InteractiveResponseCommand { + commandId: string; + args: any[]; + title: string; // supports codicon strings + } + export interface InteractiveSessionProvider { provideInitialSuggestions?(token: CancellationToken): ProviderResult; prepareSession(initialState: InteractiveSessionState | undefined, token: CancellationToken): ProviderResult;