Skip to content

Commit bcce021

Browse files
authored
Add confirmation prompt (#2251)
Whenever the copilot is going to do something that might cost money or lose data, it's a good idea to confirm with the user first. Added this for job submission for now.
1 parent e4bff4f commit bcce021

File tree

5 files changed

+104
-7
lines changed

5 files changed

+104
-7
lines changed

vscode/src/copilot/azqTools.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getJobFiles, submitJob } from "../azure/workspaceActions.js";
1818
import { HistogramData } from "./shared.js";
1919
import { getQirForVisibleQs } from "../qirGeneration.js";
2020
import { CopilotToolError, ToolResult, ToolState } from "./tools.js";
21+
import { CopilotWebviewViewProvider as CopilotView } from "./webviewViewProvider.js";
2122

2223
/**
2324
* These tool definitions correspond to the ones declared
@@ -410,6 +411,13 @@ async function submitToTarget(
410411

411412
const quantumUris = new QuantumUris(workspace.endpointUri, workspace.id);
412413

414+
const confirmed = await CopilotView.getConfirmation(
415+
`Submit job "${jobName}" to ${target.id} for ${numberOfShots} shots?`,
416+
);
417+
if (!confirmed) {
418+
return { result: "Job submission was cancelled by the user" };
419+
}
420+
413421
try {
414422
const token = await getTokenForWorkspace(workspace);
415423
if (!token) {

vscode/src/copilot/shared.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export type CopilotCommand =
99
command: "submitUserMessage";
1010
request: string;
1111
}
12+
| {
13+
command: "confirmation";
14+
confirmed: boolean;
15+
}
1216
| {
1317
command: "restartChat";
1418
history: ChatElement[];
@@ -38,7 +42,8 @@ export type CopilotUpdate =
3842
| {
3943
kind: "appendDelta";
4044
payload: { delta: string; status: Status };
41-
};
45+
}
46+
| { kind: "showConfirmation"; payload: { confirmText: string } };
4247

4348
export type QuantumChatMessage = UserMessage | AssistantMessage | ToolMessage;
4449

@@ -86,6 +91,10 @@ type Status =
8691
status: "executingTool";
8792
toolName: string;
8893
}
94+
| {
95+
status: "awaitingConfirmation";
96+
confirmText: string;
97+
}
8998
| {
9099
status: "assistantConnectionError";
91100
};

vscode/src/copilot/webview/copilot.css

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ h1 {
4242
justify-content: flex-end;
4343
flex-wrap: wrap;
4444
align-content: center;
45-
height: 28px;
45+
align-items: center;
46+
margin: 0.6em 0;
4647
font-style: italic;
4748
font-size: 0.9em;
4849
}
@@ -94,7 +95,7 @@ h1 {
9495
.user-message,
9596
.assistant-message {
9697
padding: 0 16px;
97-
margin-bottom: 16px;
98+
margin-top: 16px;
9899
border: 1px solid #ccc;
99100
border-radius: 0.5em;
100101
}
@@ -168,7 +169,7 @@ pre code {
168169

169170
.histogram-container {
170171
width: 100%;
171-
margin-bottom: 10px;
172+
margin-top: 1em;
172173
}
173174

174175
/* If not specified it defaults to the 'pre code' default monospace font */
@@ -341,3 +342,16 @@ code.hljs {
341342
color: gray;
342343
margin-top: 0.8em;
343344
}
345+
346+
.confirm-button {
347+
background-color: var(--vscode-button-background);
348+
color: var(--vscode-button-foreground);
349+
border: none;
350+
border-radius: 4px;
351+
padding: 0.3em 0.6em;
352+
margin-top: 0.4em;
353+
margin-bottom: 0.2em;
354+
margin-left: 0.6em;
355+
margin-right: 0;
356+
cursor: pointer;
357+
}

vscode/src/copilot/webview/copilot.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,26 @@ function StatusIndicator({ status }: { status: Status }) {
293293
</>
294294
) : status.status === "assistantConnectionError" ? (
295295
"There was an error communicating with Azure Quantum Copilot. Please check your Internet connection and try again."
296+
) : status.status === "awaitingConfirmation" ? (
297+
<>
298+
<div>{status.confirmText}</div>
299+
<div>
300+
<button
301+
type="button"
302+
class="confirm-button"
303+
onClick={() => submitConfirmation(true)}
304+
>
305+
Yes
306+
</button>
307+
<button
308+
type="button"
309+
class="confirm-button"
310+
onClick={() => submitConfirmation(false)}
311+
>
312+
No
313+
</button>
314+
</div>
315+
</>
296316
) : (
297317
""
298318
)}
@@ -464,6 +484,10 @@ function submitUserMessage(content: string) {
464484
});
465485
}
466486

487+
function submitConfirmation(confirmed: boolean) {
488+
postMessageToExtension({ command: "confirmation", confirmed });
489+
}
490+
467491
/**
468492
* Copilot command to restart the chat with a new history.
469493
* The service backend can be changed here as well.
@@ -515,6 +539,12 @@ function onMessage(event: MessageEvent<CopilotUpdate>) {
515539
model.debugUi = message.payload.debugUi;
516540
vscodeApi.setState(message.payload.history);
517541
break;
542+
case "showConfirmation":
543+
model.status = {
544+
status: "awaitingConfirmation",
545+
confirmText: message.payload.confirmText,
546+
};
547+
break;
518548
default:
519549
console.error("Unknown message kind: ", (message as any).kind);
520550
return;

vscode/src/copilot/webviewViewProvider.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
window,
1414
} from "vscode";
1515
import { Copilot, CopilotUpdateHandler } from "./copilot";
16-
import { CopilotCommand } from "./shared";
16+
import { CopilotCommand, CopilotUpdate } from "./shared";
1717
import { qsharpExtensionId } from "../common";
1818

1919
export function registerCopilotPanel(context: ExtensionContext): void {
@@ -34,12 +34,40 @@ export function registerCopilotPanel(context: ExtensionContext): void {
3434
);
3535
}
3636

37-
class CopilotWebviewViewProvider implements WebviewViewProvider {
37+
export class CopilotWebviewViewProvider implements WebviewViewProvider {
3838
public static readonly viewType = "quantum-copilot";
3939

40+
// The below two static properties are for tools to ask the current panel to show
41+
// a confirmation prompt, and to call the tool back with the result.
42+
// The Copilot webview is a singleton, and only one confirmation prompt can be shown at a time,
43+
// so this avoid having to traffic state for the current webview etc. around to the tools.
44+
public static current: CopilotWebviewViewProvider | undefined;
45+
public static confirmationResolver:
46+
| undefined
47+
| ((confirmed: boolean) => void);
48+
4049
private view?: WebviewView;
4150

42-
constructor(private readonly extensionUri: Uri) {}
51+
constructor(private readonly extensionUri: Uri) {
52+
CopilotWebviewViewProvider.current = this;
53+
}
54+
55+
public static async getConfirmation(confirmText: string): Promise<boolean> {
56+
const view = CopilotWebviewViewProvider.current?.view;
57+
if (!view) throw "Quantum Copilot panel is unavailable";
58+
59+
const confirmationPromise = new Promise<boolean>((resolver) => {
60+
CopilotWebviewViewProvider.confirmationResolver = resolver;
61+
const msg: CopilotUpdate = {
62+
kind: "showConfirmation",
63+
payload: { confirmText },
64+
};
65+
66+
view.webview.postMessage(msg);
67+
});
68+
69+
return confirmationPromise;
70+
}
4371

4472
private copilot: Copilot | undefined;
4573
private _streamCallback: CopilotUpdateHandler | undefined;
@@ -124,6 +152,14 @@ class CopilotWebviewViewProvider implements WebviewViewProvider {
124152
this.copilot.restartChat(message.history, message.service);
125153
break;
126154
}
155+
case "confirmation": {
156+
CopilotWebviewViewProvider.confirmationResolver?.(message.confirmed);
157+
CopilotWebviewViewProvider.confirmationResolver = undefined;
158+
break;
159+
}
160+
default:
161+
log.error("Unknown message from webview: ", message);
162+
break;
127163
}
128164
}
129165
}

0 commit comments

Comments
 (0)