feat(pi): bridge extension UI dialogs (#1134)
This commit is contained in:
@@ -24,6 +24,8 @@ Pi MCP support depends on the open-source `pi-mcp-adapter` extension being loade
|
|||||||
|
|
||||||
Pi import discovery reads Pi's persisted JSONL session files because Pi RPC does not expose a recent-session listing command. Resume and full history hydration still go through `pi --mode rpc` using the session file as `nativeHandle`.
|
Pi import discovery reads Pi's persisted JSONL session files because Pi RPC does not expose a recent-session listing command. Resume and full history hydration still go through `pi --mode rpc` using the session file as `nativeHandle`.
|
||||||
|
|
||||||
|
Pi RPC extension UI dialog requests (`select`, `input`, `editor`, `confirm`) are bridged into Paseo question permissions and answered with `extension_ui_response`. Fire-and-forget extension UI requests such as notifications are intentionally ignored by the provider adapter unless Paseo grows first-class UI for them.
|
||||||
|
|
||||||
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.listModels`, `listModes`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs.
|
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.listModels`, `listModes`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ import { runDesktopStartup } from "./desktop-startup.js";
|
|||||||
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
||||||
const APP_SCHEME = "paseo";
|
const APP_SCHEME = "paseo";
|
||||||
const PASEO_DEBUG = process.env.PASEO_DEBUG === "1";
|
const PASEO_DEBUG = process.env.PASEO_DEBUG === "1";
|
||||||
|
const DISABLE_SINGLE_INSTANCE_LOCK = process.env.PASEO_DISABLE_SINGLE_INSTANCE_LOCK === "1";
|
||||||
|
const APP_NAME = process.env.PASEO_TEST_APP_NAME?.trim() || "Paseo";
|
||||||
|
|
||||||
function isAllowedBrowserWebviewUrl(value: string | undefined): boolean {
|
function isAllowedBrowserWebviewUrl(value: string | undefined): boolean {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -108,7 +110,7 @@ const FORWARDED_PASEO_SHORTCUT_KEYS = new Set([
|
|||||||
]);
|
]);
|
||||||
const DESKTOP_SMOKE_ENV = "PASEO_DESKTOP_SMOKE";
|
const DESKTOP_SMOKE_ENV = "PASEO_DESKTOP_SMOKE";
|
||||||
const DESKTOP_SMOKE_STOP_REQUEST = "paseo-smoke-stop";
|
const DESKTOP_SMOKE_STOP_REQUEST = "paseo-smoke-stop";
|
||||||
app.setName("Paseo");
|
app.setName(APP_NAME);
|
||||||
|
|
||||||
function getBrowserIdFromWebviewPartition(partition: string | undefined): string | null {
|
function getBrowserIdFromWebviewPartition(partition: string | undefined): string | null {
|
||||||
const prefix = "persist:paseo-browser-";
|
const prefix = "persist:paseo-browser-";
|
||||||
@@ -374,7 +376,7 @@ async function createMainWindow(): Promise<void> {
|
|||||||
const iconPath = getWindowIconPath();
|
const iconPath = getWindowIconPath();
|
||||||
const systemTheme = resolveSystemWindowTheme();
|
const systemTheme = resolveSystemWindowTheme();
|
||||||
|
|
||||||
const title = devWorktreeName ? `Paseo (${devWorktreeName})` : "Paseo";
|
const title = devWorktreeName ? `${APP_NAME} (${devWorktreeName})` : APP_NAME;
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
title,
|
title,
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -522,6 +524,11 @@ function sendOpenProjectEvent(win: BrowserWindow, projectPath: string): void {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function setupSingleInstanceLock(): boolean {
|
function setupSingleInstanceLock(): boolean {
|
||||||
|
if (DISABLE_SINGLE_INSTANCE_LOCK) {
|
||||||
|
log.info("[single-instance] disabled by PASEO_DISABLE_SINGLE_INSTANCE_LOCK");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const gotLock = app.requestSingleInstanceLock();
|
const gotLock = app.requestSingleInstanceLock();
|
||||||
if (!gotLock) {
|
if (!gotLock) {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
|||||||
@@ -102,6 +102,20 @@ class SessionEvents {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextPermissionRequest(): Promise<Extract<AgentStreamEvent, { type: "permission_requested" }>> {
|
||||||
|
return this.nextEvent(
|
||||||
|
(event): event is Extract<AgentStreamEvent, { type: "permission_requested" }> =>
|
||||||
|
event.type === "permission_requested",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPermissionResolution(): Promise<Extract<AgentStreamEvent, { type: "permission_resolved" }>> {
|
||||||
|
return this.nextEvent(
|
||||||
|
(event): event is Extract<AgentStreamEvent, { type: "permission_resolved" }> =>
|
||||||
|
event.type === "permission_resolved",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private nextEvent<T extends AgentStreamEvent>(
|
private nextEvent<T extends AgentStreamEvent>(
|
||||||
predicate: (event: AgentStreamEvent) => event is T,
|
predicate: (event: AgentStreamEvent) => event is T,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@@ -119,6 +133,125 @@ class SessionEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("PiRpcAgentSession", () => {
|
describe("PiRpcAgentSession", () => {
|
||||||
|
test("bridges Pi RPC select extension UI requests through question permissions", async () => {
|
||||||
|
const { pi, session, events } = await createSession();
|
||||||
|
const fakeSession = pi.latestSession();
|
||||||
|
|
||||||
|
await session.startTurn("ask");
|
||||||
|
fakeSession.emit({
|
||||||
|
type: "extension_ui_request",
|
||||||
|
id: "ui-1",
|
||||||
|
method: "select",
|
||||||
|
title: "Pick one",
|
||||||
|
options: ["A", "B"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const permission = await events.nextPermissionRequest();
|
||||||
|
expect(permission.request).toMatchObject({
|
||||||
|
id: "ui-1",
|
||||||
|
provider: "pi",
|
||||||
|
kind: "question",
|
||||||
|
title: "Pick one",
|
||||||
|
input: {
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: "Pick one",
|
||||||
|
header: "Response",
|
||||||
|
options: [{ label: "A" }, { label: "B" }],
|
||||||
|
multiSelect: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
metadata: { extensionUiMethod: "select" },
|
||||||
|
});
|
||||||
|
expect(session.getPendingPermissions()).toHaveLength(1);
|
||||||
|
|
||||||
|
await session.respondToPermission("ui-1", {
|
||||||
|
behavior: "allow",
|
||||||
|
updatedInput: { answers: { Response: "B" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fakeSession.extensionUiResponses).toEqual([{ id: "ui-1", response: { value: "B" } }]);
|
||||||
|
expect(session.getPendingPermissions()).toEqual([]);
|
||||||
|
await expect(events.nextPermissionResolution()).resolves.toMatchObject({
|
||||||
|
requestId: "ui-1",
|
||||||
|
resolution: { behavior: "allow" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bridges Pi RPC input and confirm extension UI responses", async () => {
|
||||||
|
const { pi, session, events } = await createSession();
|
||||||
|
const fakeSession = pi.latestSession();
|
||||||
|
|
||||||
|
fakeSession.emit({
|
||||||
|
type: "extension_ui_request",
|
||||||
|
id: "input-1",
|
||||||
|
method: "input",
|
||||||
|
title: "Your name",
|
||||||
|
placeholder: "name",
|
||||||
|
});
|
||||||
|
await events.nextPermissionRequest();
|
||||||
|
await session.respondToPermission("input-1", {
|
||||||
|
behavior: "allow",
|
||||||
|
updatedInput: { answers: { Response: "Ada" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
fakeSession.emit({
|
||||||
|
type: "extension_ui_request",
|
||||||
|
id: "confirm-1",
|
||||||
|
method: "confirm",
|
||||||
|
title: "Proceed?",
|
||||||
|
});
|
||||||
|
await events.nextPermissionRequest();
|
||||||
|
await session.respondToPermission("confirm-1", {
|
||||||
|
behavior: "allow",
|
||||||
|
updatedInput: { answers: { Response: "No" } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fakeSession.extensionUiResponses).toEqual([
|
||||||
|
{ id: "input-1", response: { value: "Ada" } },
|
||||||
|
{ id: "confirm-1", response: { confirmed: false } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancels Pi RPC extension UI dialogs when question permission is denied", async () => {
|
||||||
|
const { pi, session, events } = await createSession();
|
||||||
|
const fakeSession = pi.latestSession();
|
||||||
|
|
||||||
|
fakeSession.emit({
|
||||||
|
type: "extension_ui_request",
|
||||||
|
id: "ui-cancel",
|
||||||
|
method: "select",
|
||||||
|
title: "Pick one",
|
||||||
|
options: ["A", "B"],
|
||||||
|
});
|
||||||
|
await events.nextPermissionRequest();
|
||||||
|
|
||||||
|
await session.respondToPermission("ui-cancel", {
|
||||||
|
behavior: "deny",
|
||||||
|
message: "Dismissed by user",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fakeSession.extensionUiResponses).toEqual([
|
||||||
|
{ id: "ui-cancel", response: { cancelled: true } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores Pi RPC fire-and-forget extension UI requests", async () => {
|
||||||
|
const { pi } = await createSession();
|
||||||
|
const fakeSession = pi.latestSession();
|
||||||
|
|
||||||
|
fakeSession.emit({
|
||||||
|
type: "extension_ui_request",
|
||||||
|
id: "notify-1",
|
||||||
|
method: "notify",
|
||||||
|
message: "hello",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fakeSession.extensionUiResponses).toEqual([]);
|
||||||
|
expect(fakeSession.canceledExtensionUiRequests).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("streams assistant text, reasoning, and tool calls from Pi events", async () => {
|
test("streams assistant text, reasoning, and tool calls from Pi events", async () => {
|
||||||
const { pi, session, events } = await createSession();
|
const { pi, session, events } = await createSession();
|
||||||
const fakeSession = pi.latestSession();
|
const fakeSession = pi.latestSession();
|
||||||
|
|||||||
@@ -403,6 +403,108 @@ function latestPiErrorMessage(messages: PiAgentMessage[]): string | null {
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalString(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExtensionUiRequestToPermission(
|
||||||
|
event: Extract<PiRuntimeEvent, { type: "extension_ui_request" }>,
|
||||||
|
): AgentPermissionRequest | null {
|
||||||
|
switch (event.method) {
|
||||||
|
case "select":
|
||||||
|
return buildExtensionUiQuestionPermission(event, {
|
||||||
|
question: optionalString(event.title) ?? "Select an option",
|
||||||
|
options: Array.isArray(event.options)
|
||||||
|
? event.options.filter((option): option is string => typeof option === "string")
|
||||||
|
: [],
|
||||||
|
multiSelect: false,
|
||||||
|
});
|
||||||
|
case "input":
|
||||||
|
return buildExtensionUiQuestionPermission(event, {
|
||||||
|
question: optionalString(event.title) ?? "Enter a value",
|
||||||
|
options: [],
|
||||||
|
multiSelect: false,
|
||||||
|
});
|
||||||
|
case "editor":
|
||||||
|
return buildExtensionUiQuestionPermission(event, {
|
||||||
|
question: optionalString(event.title) ?? "Edit text",
|
||||||
|
options: [],
|
||||||
|
multiSelect: false,
|
||||||
|
});
|
||||||
|
case "confirm":
|
||||||
|
return buildExtensionUiQuestionPermission(event, {
|
||||||
|
question: [optionalString(event.title), optionalString(event.message)]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n\n"),
|
||||||
|
options: ["Yes", "No"],
|
||||||
|
multiSelect: false,
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExtensionUiQuestionPermission(
|
||||||
|
event: Extract<PiRuntimeEvent, { type: "extension_ui_request" }>,
|
||||||
|
input: { question: string; options: string[]; multiSelect: boolean },
|
||||||
|
): AgentPermissionRequest {
|
||||||
|
const header = "Response";
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
provider: PI_PROVIDER,
|
||||||
|
name: `Pi ${event.method}`,
|
||||||
|
kind: "question",
|
||||||
|
title: input.question,
|
||||||
|
input: {
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
question: input.question,
|
||||||
|
header,
|
||||||
|
options: input.options.map((label) => ({ label })),
|
||||||
|
multiSelect: input.multiSelect,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
extensionUiMethod: event.method,
|
||||||
|
answerHeader: header,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstPermissionAnswer(input: AgentMetadata | undefined): string | null {
|
||||||
|
const answers = isRecord(input?.answers) ? input.answers : null;
|
||||||
|
if (!answers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const first = Object.values(answers).find((value) => typeof value === "string");
|
||||||
|
return typeof first === "string" ? first : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExtensionUiResponse(
|
||||||
|
request: AgentPermissionRequest,
|
||||||
|
response: AgentPermissionResponse,
|
||||||
|
): { value?: string; confirmed?: boolean; cancelled?: boolean } {
|
||||||
|
if (response.behavior === "deny") {
|
||||||
|
return { cancelled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = optionalString(request.metadata?.extensionUiMethod);
|
||||||
|
const answer = firstPermissionAnswer(response.updatedInput);
|
||||||
|
if (answer === null) {
|
||||||
|
return { cancelled: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === "confirm") {
|
||||||
|
return { confirmed: /^yes$/i.test(answer.trim()) };
|
||||||
|
}
|
||||||
|
return { value: answer };
|
||||||
|
}
|
||||||
|
|
||||||
function mapPiModel(model: PiModel): AgentModelDefinition {
|
function mapPiModel(model: PiModel): AgentModelDefinition {
|
||||||
return {
|
return {
|
||||||
provider: PI_PROVIDER,
|
provider: PI_PROVIDER,
|
||||||
@@ -428,6 +530,7 @@ export class PiRpcAgentSession implements AgentSession {
|
|||||||
|
|
||||||
private readonly subscribers = new Set<(event: AgentStreamEvent) => void>();
|
private readonly subscribers = new Set<(event: AgentStreamEvent) => void>();
|
||||||
private readonly activeToolCalls = new Map<string, PiTrackedToolCall>();
|
private readonly activeToolCalls = new Map<string, PiTrackedToolCall>();
|
||||||
|
private readonly pendingExtensionUiRequests = new Map<string, AgentPermissionRequest>();
|
||||||
private activeTurnId: string | null = null;
|
private activeTurnId: string | null = null;
|
||||||
private lastKnownThinkingOptionId: string | null;
|
private lastKnownThinkingOptionId: string | null;
|
||||||
private state: PiSessionState;
|
private state: PiSessionState;
|
||||||
@@ -540,12 +643,27 @@ export class PiRpcAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPendingPermissions(): AgentPermissionRequest[] {
|
getPendingPermissions(): AgentPermissionRequest[] {
|
||||||
return [];
|
return [...this.pendingExtensionUiRequests.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
async respondToPermission(requestId: string, response: AgentPermissionResponse): Promise<void> {
|
async respondToPermission(requestId: string, response: AgentPermissionResponse): Promise<void> {
|
||||||
void requestId;
|
const request = this.pendingExtensionUiRequests.get(requestId);
|
||||||
void response;
|
if (!request) {
|
||||||
|
throw new Error(`No pending permission request with id '${requestId}'`);
|
||||||
|
}
|
||||||
|
this.pendingExtensionUiRequests.delete(requestId);
|
||||||
|
|
||||||
|
this.runtimeSession.respondToExtensionUiRequest(
|
||||||
|
requestId,
|
||||||
|
buildExtensionUiResponse(request, response),
|
||||||
|
);
|
||||||
|
this.emit({
|
||||||
|
type: "permission_resolved",
|
||||||
|
provider: PI_PROVIDER,
|
||||||
|
requestId,
|
||||||
|
resolution: response,
|
||||||
|
turnId: this.currentTurnIdForEvent(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describePersistence(): AgentPersistenceHandle | null {
|
describePersistence(): AgentPersistenceHandle | null {
|
||||||
@@ -624,9 +742,26 @@ export class PiRpcAgentSession implements AgentSession {
|
|||||||
return this.activeTurnId ?? undefined;
|
return this.activeTurnId ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleExtensionUiRequest(
|
||||||
|
event: Extract<PiRuntimeEvent, { type: "extension_ui_request" }>,
|
||||||
|
): void {
|
||||||
|
const request = mapExtensionUiRequestToPermission(event);
|
||||||
|
if (!request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingExtensionUiRequests.set(request.id, request);
|
||||||
|
this.emit({
|
||||||
|
type: "permission_requested",
|
||||||
|
provider: PI_PROVIDER,
|
||||||
|
request,
|
||||||
|
turnId: this.currentTurnIdForEvent(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private handleRuntimeEvent(event: PiRuntimeEvent): void {
|
private handleRuntimeEvent(event: PiRuntimeEvent): void {
|
||||||
if (event.type === "extension_ui_request") {
|
if (event.type === "extension_ui_request") {
|
||||||
this.runtimeSession.cancelExtensionUiRequest(event.id);
|
this.handleExtensionUiRequest(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.type === "process_exit") {
|
if (event.type === "process_exit") {
|
||||||
|
|||||||
@@ -166,8 +166,15 @@ class PiCliRuntimeSession implements PiRuntimeSession {
|
|||||||
return data.commands ?? [];
|
return data.commands ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respondToExtensionUiRequest(
|
||||||
|
id: string,
|
||||||
|
response: { value?: string; confirmed?: boolean; cancelled?: boolean },
|
||||||
|
): void {
|
||||||
|
this.writeJsonLine({ type: "extension_ui_response", id, ...response });
|
||||||
|
}
|
||||||
|
|
||||||
cancelExtensionUiRequest(id: string): void {
|
cancelExtensionUiRequest(id: string): void {
|
||||||
this.writeJsonLine({ type: "extension_ui_response", id, cancelled: true });
|
this.respondToExtensionUiRequest(id, { cancelled: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export interface PiRuntimeSession {
|
|||||||
setThinkingLevel(level: string): Promise<void>;
|
setThinkingLevel(level: string): Promise<void>;
|
||||||
getSessionStats(): Promise<PiSessionStats>;
|
getSessionStats(): Promise<PiSessionStats>;
|
||||||
getCommands(): Promise<PiRpcSlashCommand[]>;
|
getCommands(): Promise<PiRpcSlashCommand[]>;
|
||||||
|
respondToExtensionUiRequest(
|
||||||
|
id: string,
|
||||||
|
response: { value?: string; confirmed?: boolean; cancelled?: boolean },
|
||||||
|
): void;
|
||||||
cancelExtensionUiRequest(id: string): void;
|
cancelExtensionUiRequest(id: string): void;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export class FakePiSession implements PiRuntimeSession {
|
|||||||
readonly setThinkingLevelRequests: string[] = [];
|
readonly setThinkingLevelRequests: string[] = [];
|
||||||
abortRequested = false;
|
abortRequested = false;
|
||||||
readonly canceledExtensionUiRequests: string[] = [];
|
readonly canceledExtensionUiRequests: string[] = [];
|
||||||
|
readonly extensionUiResponses: Array<{
|
||||||
|
id: string;
|
||||||
|
response: { value?: string; confirmed?: boolean; cancelled?: boolean };
|
||||||
|
}> = [];
|
||||||
setModelResult: PiModel | null = null;
|
setModelResult: PiModel | null = null;
|
||||||
models: PiModel[] = [];
|
models: PiModel[] = [];
|
||||||
messages: PiAgentMessage[] = [];
|
messages: PiAgentMessage[] = [];
|
||||||
@@ -130,8 +134,16 @@ export class FakePiSession implements PiRuntimeSession {
|
|||||||
return this.commands;
|
return this.commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respondToExtensionUiRequest(
|
||||||
|
id: string,
|
||||||
|
response: { value?: string; confirmed?: boolean; cancelled?: boolean },
|
||||||
|
): void {
|
||||||
|
this.extensionUiResponses.push({ id, response });
|
||||||
|
}
|
||||||
|
|
||||||
cancelExtensionUiRequest(id: string): void {
|
cancelExtensionUiRequest(id: string): void {
|
||||||
this.canceledExtensionUiRequests.push(id);
|
this.canceledExtensionUiRequests.push(id);
|
||||||
|
this.respondToExtensionUiRequest(id, { cancelled: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {}
|
async close(): Promise<void> {}
|
||||||
|
|||||||
Reference in New Issue
Block a user