v2.2-paseo-providers: Paseo provider stack + v2.2.1 pane-scoped chat fixes
Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
113
apps/coder/src/services/permission-waiter.ts
Normal file
113
apps/coder/src/services/permission-waiter.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Blocks ACP dispatch on permission prompts until the user responds via API.
|
||||
*/
|
||||
import type { RequestPermissionRequest, RequestPermissionResponse } from '@agentclientprotocol/sdk';
|
||||
import { isUnattendedMode } from './provider-manifest.js';
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 120_000;
|
||||
|
||||
interface PendingPermission {
|
||||
request: RequestPermissionRequest;
|
||||
sessionId: string;
|
||||
resolve: (response: RequestPermissionResponse) => void;
|
||||
reject: (err: Error) => void;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
const pendingByTask = new Map<string, PendingPermission>();
|
||||
|
||||
export interface PermissionPrompt {
|
||||
taskId: string;
|
||||
toolTitle?: string;
|
||||
options: Array<{ optionId: string; label: string }>;
|
||||
}
|
||||
|
||||
export interface PermissionHooks {
|
||||
onPrompt?: (prompt: PermissionPrompt & { sessionId: string }) => void | Promise<void>;
|
||||
onResolved?: (taskId: string, sessionId: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
let hooks: PermissionHooks = {};
|
||||
|
||||
export function setPermissionHooks(next: PermissionHooks): void {
|
||||
hooks = next;
|
||||
}
|
||||
|
||||
function toPrompt(taskId: string, params: RequestPermissionRequest): PermissionPrompt {
|
||||
return {
|
||||
taskId,
|
||||
toolTitle: params.toolCall?.title ?? undefined,
|
||||
options: params.options.map((o) => ({
|
||||
optionId: o.optionId,
|
||||
label: o.name,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function waitForPermissionResponse(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
provider: string,
|
||||
modeId: string | undefined,
|
||||
params: RequestPermissionRequest,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
if (isUnattendedMode(provider, modeId)) {
|
||||
const first = params.options[0];
|
||||
if (first) {
|
||||
return Promise.resolve({ outcome: { outcome: 'selected', optionId: first.optionId } });
|
||||
}
|
||||
return Promise.resolve({ outcome: { outcome: 'cancelled' } });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const existing = pendingByTask.get(taskId);
|
||||
if (existing) {
|
||||
clearTimeout(existing.timer);
|
||||
existing.reject(new Error('superseded by newer permission request'));
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
pendingByTask.delete(taskId);
|
||||
void hooks.onResolved?.(taskId, sessionId);
|
||||
resolve({ outcome: { outcome: 'cancelled' } });
|
||||
}, timeoutMs);
|
||||
|
||||
pendingByTask.set(taskId, { request: params, sessionId, resolve, reject, timer });
|
||||
|
||||
const prompt = toPrompt(taskId, params);
|
||||
void hooks.onPrompt?.({ ...prompt, sessionId });
|
||||
});
|
||||
}
|
||||
|
||||
export function respondToPermission(taskId: string, optionId: string | null): boolean {
|
||||
const pending = pendingByTask.get(taskId);
|
||||
if (!pending) return false;
|
||||
|
||||
clearTimeout(pending.timer);
|
||||
pendingByTask.delete(taskId);
|
||||
|
||||
if (optionId) {
|
||||
pending.resolve({ outcome: { outcome: 'selected', optionId } });
|
||||
} else {
|
||||
pending.resolve({ outcome: { outcome: 'cancelled' } });
|
||||
}
|
||||
|
||||
void hooks.onResolved?.(taskId, pending.sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getPendingPermission(taskId: string): PermissionPrompt | null {
|
||||
const pending = pendingByTask.get(taskId);
|
||||
if (!pending) return null;
|
||||
return toPrompt(taskId, pending.request);
|
||||
}
|
||||
|
||||
export function cancelPendingPermission(taskId: string): void {
|
||||
const pending = pendingByTask.get(taskId);
|
||||
if (!pending) return;
|
||||
clearTimeout(pending.timer);
|
||||
pendingByTask.delete(taskId);
|
||||
pending.resolve({ outcome: { outcome: 'cancelled' } });
|
||||
void hooks.onResolved?.(taskId, pending.sessionId);
|
||||
}
|
||||
Reference in New Issue
Block a user