microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
164.39k stars 29.32k forks source link

Add support for async stream.button() in copilot #225516

Closed alenakhineika closed 3 months ago

alenakhineika commented 3 months ago

It would be handy if the stream.button() could wait for the attached command to be resolved.

When generating a participant answer for a particular command, we might encounter that a user is not connected yet. We want to expand the command palette, so users can select one of the existing connections, and then the extension would proceed with the answer generation.

Currently, we do it without the button, and just open the command palette directly, but this has proven to be confusing and difficult to notice for users.

I tried to pass vscode.ChatRequest and vscode.ChatResponseStream to my connect function and after connecting call the this._participant.requestHandler() with the same request and response. But it looks like the stream lives within one iteration user prompt -> the handler response and when I invoke the handler again the stream is already closed.

if (!isConnected()) {
  stream.markdown(
    "Looks like you aren't currently connected, first let's get you connected to the cluster we'd like to create this query to run against.\n\n"
  );
  stream.button({
    command: EXTENSION_COMMANDS.MDB_CHANGE_ACTIVE_CONNECTION_FROM_COPILOT,
    title: vscode.l10n.t('Connect'),
    arguments: [request, context, stream, token]
  });
  return { metadata: { command: '' } };
}

I also tried to solve it with await vscode.commands.executeCommand('workbench.action.chat.open', { query: chatHandlerArgs.request.prompt }), but it prints the initial user prompt the second time to the chat, what is expected, but not ideal experience for our use case. Something like await vscode.commands.executeCommand('workbench.action.chat.open', { response }) would do the job, so we could print messages to the chat at any time, without requiring a user prompt.

Please let me know if there is an existing way to handle this. Thank you!

alenakhineika commented 3 months ago

cc @isidorn

isidorn commented 3 months ago

Sounds similar to https://github.com/microsoft/vscode/issues/211136

roblourens commented 3 months ago

Do you want the current request to wait on the user clicking the button, or send a new request to call back to the participant? You can do either.

You can wait on the command yourself by setting up some callback to do await somePromiseThatIsResolvedWhenTheCommandIsInvoked inside your chat handler, after sending the button.

Or, you can do the workbench.action.chat.open trick, with some other query that you want to show, even an empty string. I'm guessing the first is what you'd prefer though. Does that help?

alenakhineika commented 3 months ago

For workbench.action.chat.open we would need to print the command that we want to return control to, right? Like:

@MongoDB /query

Otherwise, it won't know which action to take.

To await somePromiseThatIsResolvedWhenTheCommandIsInvoked, I thought about Promise + setInterval with a condition and timeout as a workaround. That would work, but wanted to enquire if there is no better more native copilot way to do so, e.g. awaing stream buttons.

roblourens commented 3 months ago

I'd recommend awaiting a Promise, not polling with a setInterval, especially because the user might never click the button. But yeah, either of those work, but we don't have anything better for you right now.

alenakhineika commented 3 months ago

After experimenting a bit more, I see how this would be useful if you supported the following command:

await vscode.commands.executeCommand('workbench.action.chat.requestHandler', { command: 'query' })

This would call the required handler directly, without printing the command itself to the chat.

Here is the use case:

list

Now we need somehow to tell the copilot to proceed with query generation. With workbench.action.chat.open, we would need to print @MongoDB /query find all docs by name lena again.

With workbench.action.chat.requestHandler it would not print anything and just returned a generated query.

I find it better than awaiting a promise, because as you said, what if the user didn't click anything, what if they want to call another command, what will happen with all unresolved promises? We kind of block the flow while awaiting. With workbench.action.chat.requestHandler, the user could also scroll the history and click those links again to change the selection.

What do you think?

alenakhineika commented 3 months ago

Another command that could be useful is something like:

await vscode.commands.executeCommand('workbench.action.chat.userPrompt', { query: '@MongoDB /query' })

This could be similar to workbench.action.chat.open, with the difference that it won't send the message to chat, but only insert the text into the input, suggesting to the user the next step.

isidorn commented 3 months ago

with the difference that it won't send the message to chat, but only insert the text into the input, suggesting to the user the next step.

I like this suggestion and see the need for this. Especially since we want to put the user in control to review the query before it is run. @roblourens could it be an argument sent to the open command - send: boolean? And maybe even false is the default?

roblourens commented 3 months ago

You can use the isPartialQuery argument to fill in the input but not send the query.

I wonder whether you actually want the confirmation API though, this shows a confirmation box inline, the response ends, and it makes another request when the user picks an option: https://github.com/microsoft/vscode/blob/24da11d618d76e825966709b3dd8d40de399a3d5/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts#L151

alenakhineika commented 3 months ago

The isPartialQuery is exactly what I wanted, thank you!

What about supporting something like await vscode.commands.executeCommand('workbench.action.chat.requestHandler', { command: 'query' })? So instead of printing and sending a query, or partially printing without sending it, we would send a request to a handler without printing to chat.

coll

I currently hide these commands in the package.json with the where condition to prevent users from running them directly in chat, because those are a sequence of actions, at it might lead to unexpected flow if they call them at any time. The conversation would look cleaner without these auxiliary hystory in the chat.

Or maybe there is also some cool flag that I haven't found in your codebase? :)

roblourens commented 3 months ago

I don't think we should do that, because if there's a response in chat, there should be a request that shows you why that response is showing up. Remember that the user could run some other chat request with a different participant before they go back and click that button.

alenakhineika commented 3 months ago

I see your point. Feel free to close the issue. I will be printing to chat then with workbench.action.chat.open and keep an eye on new features that you deliver. Maybe in the future, there will be something to improve this flow.

And thank you so much for your quick and detailed responses. They’ve been incredibly helpful in moving our participant's progress forward!

roblourens commented 3 months ago

Thanks, I appreciate the feedback!