mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Project pending requests into domain models
This commit is contained in:
parent
380587e2be
commit
2e9d74167d
23 changed files with 363 additions and 245 deletions
|
|
@ -1,159 +1,42 @@
|
|||
import type { RequestId } from "../../generated/app-server/RequestId";
|
||||
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
|
||||
|
||||
type SimpleApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
|
||||
|
||||
type AppServerCommandApprovalDecision =
|
||||
| SimpleApprovalDecision
|
||||
| { acceptWithExecpolicyAmendment: unknown }
|
||||
| { applyNetworkPolicyAmendment: { network_policy_amendment: { action: "allow" | "deny"; [key: string]: unknown } } };
|
||||
|
||||
type AppServerApprovalAction = "accept" | "accept-session" | "decline" | "cancel" | AppServerCommandApprovalDecisionAction;
|
||||
|
||||
interface AppServerCommandApprovalDecisionAction {
|
||||
kind: "command-decision";
|
||||
decision: AppServerCommandApprovalDecision;
|
||||
}
|
||||
import type {
|
||||
ApprovalAction,
|
||||
CommandApprovalDecision,
|
||||
McpElicitationAction,
|
||||
McpElicitationContentValue,
|
||||
PendingApproval,
|
||||
PendingMcpElicitation,
|
||||
PendingMcpElicitationField,
|
||||
PendingMcpElicitationOption,
|
||||
PendingUserInput,
|
||||
} from "../../domain/pending-requests/model";
|
||||
|
||||
type AppServerRequestByMethod<Method extends ServerRequest["method"]> = Extract<ServerRequest, { method: Method }>;
|
||||
type AppServerRequestParams<Method extends ServerRequest["method"]> = AppServerRequestByMethod<Method>["params"];
|
||||
|
||||
type AppServerCommandApprovalParams = AppServerRequestParams<"item/commandExecution/requestApproval">;
|
||||
|
||||
type AppServerFileChangeApprovalParams = Omit<AppServerRequestParams<"item/fileChange/requestApproval">, "reason" | "grantRoot"> & {
|
||||
reason: string | null;
|
||||
grantRoot: string | null;
|
||||
};
|
||||
|
||||
type AppServerPermissionsApprovalParams = AppServerRequestParams<"item/permissions/requestApproval">;
|
||||
|
||||
export type AppServerApproval =
|
||||
| {
|
||||
requestId: RequestId;
|
||||
method: "item/commandExecution/requestApproval";
|
||||
params: AppServerCommandApprovalParams;
|
||||
}
|
||||
| {
|
||||
requestId: RequestId;
|
||||
method: "item/fileChange/requestApproval";
|
||||
params: AppServerFileChangeApprovalParams;
|
||||
}
|
||||
| {
|
||||
requestId: RequestId;
|
||||
method: "item/permissions/requestApproval";
|
||||
params: AppServerPermissionsApprovalParams;
|
||||
};
|
||||
|
||||
interface AppServerGrantedPermissionProfile {
|
||||
network?: unknown;
|
||||
fileSystem?: unknown;
|
||||
}
|
||||
|
||||
type AppServerApprovalResponseSource =
|
||||
| { method: "item/commandExecution/requestApproval" }
|
||||
| { method: "item/fileChange/requestApproval" }
|
||||
| { method: "item/permissions/requestApproval"; params: { permissions: { network?: unknown; fileSystem?: unknown } } };
|
||||
|
||||
export type AppServerApprovalResponse =
|
||||
| { decision: AppServerCommandApprovalDecision }
|
||||
| { decision: SimpleApprovalDecision }
|
||||
| { decision: CommandApprovalDecision }
|
||||
| { scope: "session" | "turn"; permissions: AppServerGrantedPermissionProfile };
|
||||
|
||||
type AppServerUserInputParams = AppServerRequestParams<"item/tool/requestUserInput">;
|
||||
|
||||
export interface AppServerUserInput {
|
||||
requestId: RequestId;
|
||||
method: "item/tool/requestUserInput";
|
||||
params: AppServerUserInputParams;
|
||||
}
|
||||
|
||||
export interface AppServerUserInputResponse {
|
||||
answers: Record<string, { answers: string[] }>;
|
||||
}
|
||||
|
||||
type AppServerMcpElicitationAction = "accept" | "decline" | "cancel";
|
||||
type AppServerMcpElicitationContentValue = string | number | boolean | readonly string[] | null;
|
||||
type AppServerMcpElicitationRequest = AppServerRequestByMethod<"mcpServer/elicitation/request">;
|
||||
type AppServerMcpElicitationFormParams = Extract<AppServerMcpElicitationRequest["params"], { mode: "form" }>;
|
||||
type AppServerMcpElicitationPrimitiveSchema = NonNullable<AppServerMcpElicitationFormParams["requestedSchema"]["properties"][string]>;
|
||||
|
||||
interface AppServerMcpElicitationOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface AppServerMcpElicitationFieldBase {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
type AppServerMcpElicitationField =
|
||||
| (AppServerMcpElicitationFieldBase & {
|
||||
type: "string";
|
||||
format: string | null;
|
||||
minLength: number | null;
|
||||
maxLength: number | null;
|
||||
defaultValue: string;
|
||||
})
|
||||
| (AppServerMcpElicitationFieldBase & {
|
||||
type: "number" | "integer";
|
||||
minimum: number | null;
|
||||
maximum: number | null;
|
||||
defaultValue: number | null;
|
||||
})
|
||||
| (AppServerMcpElicitationFieldBase & {
|
||||
type: "boolean";
|
||||
defaultValue: boolean;
|
||||
})
|
||||
| (AppServerMcpElicitationFieldBase & {
|
||||
type: "single-select";
|
||||
options: readonly AppServerMcpElicitationOption[];
|
||||
defaultValue: string;
|
||||
})
|
||||
| (AppServerMcpElicitationFieldBase & {
|
||||
type: "multi-select";
|
||||
options: readonly AppServerMcpElicitationOption[];
|
||||
minItems: number | null;
|
||||
maxItems: number | null;
|
||||
defaultValue: readonly string[];
|
||||
});
|
||||
|
||||
interface AppServerMcpElicitationNormalizedFormParams {
|
||||
threadId: string;
|
||||
turnId: string | null;
|
||||
serverName: string;
|
||||
mode: "form";
|
||||
message: string;
|
||||
meta: unknown;
|
||||
fields: readonly AppServerMcpElicitationField[];
|
||||
}
|
||||
|
||||
interface AppServerMcpElicitationNormalizedUrlParams {
|
||||
threadId: string;
|
||||
turnId: string | null;
|
||||
serverName: string;
|
||||
mode: "url";
|
||||
message: string;
|
||||
meta: unknown;
|
||||
url: string;
|
||||
elicitationId: string;
|
||||
}
|
||||
|
||||
export interface AppServerMcpElicitation {
|
||||
requestId: RequestId;
|
||||
method: "mcpServer/elicitation/request";
|
||||
params: AppServerMcpElicitationNormalizedFormParams | AppServerMcpElicitationNormalizedUrlParams;
|
||||
}
|
||||
|
||||
export interface AppServerMcpElicitationResponse {
|
||||
action: AppServerMcpElicitationAction;
|
||||
action: McpElicitationAction;
|
||||
content: unknown;
|
||||
_meta: unknown;
|
||||
}
|
||||
|
||||
export function appServerApprovalRequest(request: ServerRequest): AppServerApproval | null {
|
||||
export function appServerApprovalRequest(request: ServerRequest): PendingApproval | null {
|
||||
switch (request.method) {
|
||||
case "item/commandExecution/requestApproval":
|
||||
return {
|
||||
|
|
@ -182,13 +65,10 @@ export function appServerApprovalRequest(request: ServerRequest): AppServerAppro
|
|||
}
|
||||
}
|
||||
|
||||
export function appServerApprovalResponse(
|
||||
approval: AppServerApprovalResponseSource,
|
||||
action: AppServerApprovalAction,
|
||||
): AppServerApprovalResponse {
|
||||
export function appServerApprovalResponse(approval: PendingApproval, action: ApprovalAction): AppServerApprovalResponse {
|
||||
if (approval.method === "item/commandExecution/requestApproval") {
|
||||
return {
|
||||
decision: isAppServerCommandDecisionAction(action) ? action.decision : commandDecision(action),
|
||||
decision: isCommandDecisionAction(action) ? action.decision : commandDecision(action),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -204,7 +84,7 @@ export function appServerApprovalResponse(
|
|||
};
|
||||
}
|
||||
|
||||
export function appServerUserInputRequest(request: ServerRequest): AppServerUserInput | null {
|
||||
export function appServerUserInputRequest(request: ServerRequest): PendingUserInput | null {
|
||||
if (request.method !== "item/tool/requestUserInput") return null;
|
||||
return {
|
||||
requestId: request.id,
|
||||
|
|
@ -229,7 +109,7 @@ export function appServerUserInputResponse(
|
|||
};
|
||||
}
|
||||
|
||||
export function appServerMcpElicitationRequest(request: ServerRequest): AppServerMcpElicitation | null {
|
||||
export function appServerMcpElicitationRequest(request: ServerRequest): PendingMcpElicitation | null {
|
||||
if (request.method !== "mcpServer/elicitation/request") return null;
|
||||
const params = request.params;
|
||||
if (params.mode === "url") {
|
||||
|
|
@ -264,8 +144,8 @@ export function appServerMcpElicitationRequest(request: ServerRequest): AppServe
|
|||
}
|
||||
|
||||
export function appServerMcpElicitationResponse(
|
||||
action: AppServerMcpElicitationAction,
|
||||
content: Record<string, AppServerMcpElicitationContentValue> | null,
|
||||
action: McpElicitationAction,
|
||||
content: Record<string, McpElicitationContentValue> | null,
|
||||
): AppServerMcpElicitationResponse {
|
||||
return {
|
||||
action,
|
||||
|
|
@ -274,18 +154,18 @@ export function appServerMcpElicitationResponse(
|
|||
};
|
||||
}
|
||||
|
||||
function isAppServerCommandDecisionAction(action: AppServerApprovalAction): action is AppServerCommandApprovalDecisionAction {
|
||||
function isCommandDecisionAction(action: ApprovalAction): action is Extract<ApprovalAction, { kind: "command-decision" }> {
|
||||
return typeof action === "object";
|
||||
}
|
||||
|
||||
function commandDecision(action: AppServerApprovalAction): AppServerCommandApprovalDecision {
|
||||
function commandDecision(action: ApprovalAction): CommandApprovalDecision {
|
||||
if (action === "accept") return "accept";
|
||||
if (action === "accept-session") return "acceptForSession";
|
||||
if (action === "cancel") return "cancel";
|
||||
return "decline";
|
||||
}
|
||||
|
||||
function fileChangeDecision(action: AppServerApprovalAction): SimpleApprovalDecision {
|
||||
function fileChangeDecision(action: ApprovalAction): CommandApprovalDecision {
|
||||
if (action === "accept") return "accept";
|
||||
if (action === "accept-session") return "acceptForSession";
|
||||
if (action === "cancel") return "cancel";
|
||||
|
|
@ -302,7 +182,7 @@ function grantedPermissions(requested: { network?: unknown; fileSystem?: unknown
|
|||
function mcpElicitationFieldsFromSchema(
|
||||
properties: Record<string, AppServerMcpElicitationPrimitiveSchema | undefined>,
|
||||
required: ReadonlySet<string>,
|
||||
): AppServerMcpElicitationField[] {
|
||||
): PendingMcpElicitationField[] {
|
||||
return Object.entries(properties).flatMap(([id, schema]) => {
|
||||
if (!schema) return [];
|
||||
return [mcpElicitationFieldFromSchema(id, schema, required.has(id))];
|
||||
|
|
@ -313,7 +193,7 @@ function mcpElicitationFieldFromSchema(
|
|||
id: string,
|
||||
schema: AppServerMcpElicitationPrimitiveSchema,
|
||||
required: boolean,
|
||||
): AppServerMcpElicitationField {
|
||||
): PendingMcpElicitationField {
|
||||
const base = {
|
||||
id,
|
||||
title: schema.title ?? id,
|
||||
|
|
@ -362,7 +242,7 @@ function mcpElicitationFieldFromSchema(
|
|||
};
|
||||
}
|
||||
|
||||
function singleSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSchema, { type: "string" }>): AppServerMcpElicitationOption[] {
|
||||
function singleSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSchema, { type: "string" }>): PendingMcpElicitationOption[] {
|
||||
if ("oneOf" in schema) return schema.oneOf.map((option) => ({ value: option.const, label: option.title }));
|
||||
if ("enum" in schema) {
|
||||
const enumNames = "enumNames" in schema ? schema.enumNames : undefined;
|
||||
|
|
@ -372,7 +252,7 @@ function singleSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSch
|
|||
return [];
|
||||
}
|
||||
|
||||
function multiSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSchema, { type: "array" }>): AppServerMcpElicitationOption[] {
|
||||
function multiSelectOptions(schema: Extract<AppServerMcpElicitationPrimitiveSchema, { type: "array" }>): PendingMcpElicitationOption[] {
|
||||
if ("anyOf" in schema.items) return schema.items.anyOf.map((option) => ({ value: option.const, label: option.title }));
|
||||
return schema.items.enum.map((value) => ({ value, label: value }));
|
||||
}
|
||||
|
|
@ -382,16 +262,16 @@ function bigintToNumberOrNull(value: bigint | number | undefined): number | null
|
|||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function toJsonContent(content: Record<string, AppServerMcpElicitationContentValue> | null): unknown {
|
||||
function toJsonContent(content: Record<string, McpElicitationContentValue> | null): unknown {
|
||||
if (!content) return null;
|
||||
return Object.fromEntries(Object.entries(content).map(([key, value]) => [key, toJsonValue(value)]));
|
||||
}
|
||||
|
||||
function toJsonValue(value: AppServerMcpElicitationContentValue): unknown {
|
||||
function toJsonValue(value: McpElicitationContentValue): unknown {
|
||||
if (isReadonlyStringArray(value)) return [...value];
|
||||
return value;
|
||||
}
|
||||
|
||||
function isReadonlyStringArray(value: AppServerMcpElicitationContentValue): value is readonly string[] {
|
||||
function isReadonlyStringArray(value: McpElicitationContentValue): value is readonly string[] {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
|
|
|||
308
src/domain/pending-requests/model.ts
Normal file
308
src/domain/pending-requests/model.ts
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
export type PendingRequestId = string | number;
|
||||
|
||||
type SimpleApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
|
||||
|
||||
export type CommandApprovalDecision =
|
||||
| SimpleApprovalDecision
|
||||
| { acceptWithExecpolicyAmendment: unknown }
|
||||
| { applyNetworkPolicyAmendment: { network_policy_amendment: { action: "allow" | "deny"; [key: string]: unknown } } };
|
||||
|
||||
interface CommandApprovalParams {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
startedAtMs: number;
|
||||
approvalId?: string | null;
|
||||
reason?: string | null;
|
||||
networkApprovalContext?: unknown;
|
||||
command?: string | null;
|
||||
cwd?: string | null;
|
||||
commandActions?: unknown[] | null;
|
||||
additionalPermissions?: unknown;
|
||||
proposedExecpolicyAmendment?: unknown;
|
||||
proposedNetworkPolicyAmendments?: unknown[] | null;
|
||||
availableDecisions?: CommandApprovalDecision[] | null;
|
||||
}
|
||||
|
||||
interface FileChangeApprovalParams {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
startedAtMs: number;
|
||||
reason: string | null;
|
||||
grantRoot: string | null;
|
||||
}
|
||||
|
||||
interface PermissionProfile {
|
||||
network?: { enabled?: boolean | null } | null;
|
||||
fileSystem?: {
|
||||
entries?: readonly { path: unknown; access?: unknown }[] | null;
|
||||
read?: unknown;
|
||||
write?: unknown;
|
||||
globScanMaxDepth?: unknown;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface PermissionsApprovalParams {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
startedAtMs: number;
|
||||
reason: string | null;
|
||||
cwd: string;
|
||||
environmentId: string | null;
|
||||
permissions: PermissionProfile;
|
||||
}
|
||||
|
||||
export type ApprovalAction = "accept" | "accept-session" | "decline" | "cancel" | CommandApprovalDecisionAction;
|
||||
|
||||
interface CommandApprovalDecisionAction {
|
||||
kind: "command-decision";
|
||||
decision: CommandApprovalDecision;
|
||||
}
|
||||
|
||||
export type PendingApproval =
|
||||
| {
|
||||
requestId: PendingRequestId;
|
||||
method: "item/commandExecution/requestApproval";
|
||||
params: CommandApprovalParams;
|
||||
}
|
||||
| {
|
||||
requestId: PendingRequestId;
|
||||
method: "item/fileChange/requestApproval";
|
||||
params: FileChangeApprovalParams;
|
||||
}
|
||||
| {
|
||||
requestId: PendingRequestId;
|
||||
method: "item/permissions/requestApproval";
|
||||
params: PermissionsApprovalParams;
|
||||
};
|
||||
|
||||
interface PendingUserInputOption {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface PendingUserInputQuestion {
|
||||
id: string;
|
||||
header: string;
|
||||
question: string;
|
||||
isOther: boolean;
|
||||
isSecret: boolean;
|
||||
options: PendingUserInputOption[] | null;
|
||||
}
|
||||
|
||||
interface PendingUserInputParams {
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
questions: PendingUserInputQuestion[];
|
||||
autoResolutionMs: number | null;
|
||||
}
|
||||
|
||||
export interface PendingUserInput {
|
||||
requestId: PendingRequestId;
|
||||
method: "item/tool/requestUserInput";
|
||||
params: PendingUserInputParams;
|
||||
}
|
||||
|
||||
export type McpElicitationAction = "accept" | "decline" | "cancel";
|
||||
|
||||
export type McpElicitationContentValue = string | number | boolean | readonly string[] | null;
|
||||
|
||||
export interface PendingMcpElicitationOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface PendingMcpElicitationFieldBase {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export type PendingMcpElicitationField =
|
||||
| (PendingMcpElicitationFieldBase & {
|
||||
type: "string";
|
||||
format: string | null;
|
||||
minLength: number | null;
|
||||
maxLength: number | null;
|
||||
defaultValue: string;
|
||||
})
|
||||
| (PendingMcpElicitationFieldBase & {
|
||||
type: "number" | "integer";
|
||||
minimum: number | null;
|
||||
maximum: number | null;
|
||||
defaultValue: number | null;
|
||||
})
|
||||
| (PendingMcpElicitationFieldBase & {
|
||||
type: "boolean";
|
||||
defaultValue: boolean;
|
||||
})
|
||||
| (PendingMcpElicitationFieldBase & {
|
||||
type: "single-select";
|
||||
options: readonly PendingMcpElicitationOption[];
|
||||
defaultValue: string;
|
||||
})
|
||||
| (PendingMcpElicitationFieldBase & {
|
||||
type: "multi-select";
|
||||
options: readonly PendingMcpElicitationOption[];
|
||||
minItems: number | null;
|
||||
maxItems: number | null;
|
||||
defaultValue: readonly string[];
|
||||
});
|
||||
|
||||
interface PendingMcpElicitationFormParams {
|
||||
threadId: string;
|
||||
turnId: string | null;
|
||||
serverName: string;
|
||||
mode: "form";
|
||||
message: string;
|
||||
meta: unknown;
|
||||
fields: readonly PendingMcpElicitationField[];
|
||||
}
|
||||
|
||||
interface PendingMcpElicitationUrlParams {
|
||||
threadId: string;
|
||||
turnId: string | null;
|
||||
serverName: string;
|
||||
mode: "url";
|
||||
message: string;
|
||||
meta: unknown;
|
||||
url: string;
|
||||
elicitationId: string;
|
||||
}
|
||||
|
||||
export interface PendingMcpElicitation {
|
||||
requestId: PendingRequestId;
|
||||
method: "mcpServer/elicitation/request";
|
||||
params: PendingMcpElicitationFormParams | PendingMcpElicitationUrlParams;
|
||||
}
|
||||
|
||||
export function approvalActionKind(action: ApprovalAction): "accept" | "accept-session" | "decline" | "cancel" {
|
||||
if (!isCommandDecisionAction(action)) return action;
|
||||
const decision = action.decision;
|
||||
if (typeof decision === "string") return simpleApprovalActionKind(decision);
|
||||
if ("acceptWithExecpolicyAmendment" in decision) return "accept-session";
|
||||
if ("applyNetworkPolicyAmendment" in decision) {
|
||||
return decision.applyNetworkPolicyAmendment.network_policy_amendment.action === "allow" ? "accept-session" : "decline";
|
||||
}
|
||||
return "decline";
|
||||
}
|
||||
|
||||
function simpleApprovalActionKind(decision: string): "accept" | "accept-session" | "decline" | "cancel" {
|
||||
if (decision === "accept") return "accept";
|
||||
if (decision === "acceptForSession") return "accept-session";
|
||||
if (decision === "cancel") return "cancel";
|
||||
return "decline";
|
||||
}
|
||||
|
||||
function isCommandDecisionAction(action: ApprovalAction): action is CommandApprovalDecisionAction {
|
||||
return typeof action === "object";
|
||||
}
|
||||
|
||||
export function questionDefaultAnswer(question: PendingUserInputQuestion): string {
|
||||
return question.options?.[0]?.label ?? "";
|
||||
}
|
||||
|
||||
export function userInputDraftKey(requestId: PendingRequestId, questionId: string): string {
|
||||
return pendingRequestDerivedKey(requestId, questionId);
|
||||
}
|
||||
|
||||
export function userInputOtherDraftKey(requestId: PendingRequestId, questionId: string): string {
|
||||
return pendingRequestDerivedKey(requestId, `${questionId}:other`);
|
||||
}
|
||||
|
||||
export function mcpElicitationDraftKey(requestId: PendingRequestId, fieldId: string): string {
|
||||
return pendingRequestDerivedKey(requestId, `mcp:${fieldId}`);
|
||||
}
|
||||
|
||||
export function approvalDetailsDisclosureId(requestId: PendingRequestId): string {
|
||||
return pendingRequestDerivedKey(requestId, "details");
|
||||
}
|
||||
|
||||
export function pendingRequestDerivedKeyPrefix(requestId: PendingRequestId): string {
|
||||
return `${String(requestId)}:`;
|
||||
}
|
||||
|
||||
function pendingRequestDerivedKey(requestId: PendingRequestId, suffix: string): string {
|
||||
return `${pendingRequestDerivedKeyPrefix(requestId)}${suffix}`;
|
||||
}
|
||||
|
||||
export function answersForPendingUserInput(input: PendingUserInput, drafts: ReadonlyMap<string, string>): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
input.params.questions.map((question) => [
|
||||
question.id,
|
||||
drafts.get(userInputDraftKey(input.requestId, question.id)) ?? questionDefaultAnswer(question),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function contentForPendingMcpElicitation(
|
||||
elicitation: PendingMcpElicitation,
|
||||
drafts: ReadonlyMap<string, string>,
|
||||
): Record<string, McpElicitationContentValue> | null {
|
||||
if (elicitation.params.mode !== "form") return null;
|
||||
return Object.fromEntries(
|
||||
elicitation.params.fields.map((field) => [
|
||||
field.id,
|
||||
mcpElicitationFieldValue(field, drafts.get(mcpElicitationDraftKey(elicitation.requestId, field.id))),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
export function mcpElicitationFieldDefaultDraft(field: PendingMcpElicitationField): string {
|
||||
switch (field.type) {
|
||||
case "boolean":
|
||||
return field.defaultValue ? "true" : "false";
|
||||
case "multi-select":
|
||||
return JSON.stringify(field.defaultValue);
|
||||
case "number":
|
||||
case "integer":
|
||||
return field.defaultValue === null ? "" : String(field.defaultValue);
|
||||
default:
|
||||
return field.defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function mcpElicitationFieldValue(field: PendingMcpElicitationField, draftValue: string | undefined): McpElicitationContentValue {
|
||||
const draft = draftValue ?? mcpElicitationFieldDefaultDraft(field);
|
||||
switch (field.type) {
|
||||
case "boolean":
|
||||
return draft === "true";
|
||||
case "number":
|
||||
case "integer":
|
||||
return numericMcpElicitationFieldValue(field, draft);
|
||||
case "multi-select":
|
||||
return multiSelectMcpElicitationFieldValue(field, draft);
|
||||
default:
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
|
||||
function numericMcpElicitationFieldValue(
|
||||
field: Extract<PendingMcpElicitationField, { type: "number" | "integer" }>,
|
||||
draft: string,
|
||||
): number | null {
|
||||
if (draft.trim() === "") return null;
|
||||
const parsed = field.type === "integer" ? Number.parseInt(draft, 10) : Number(draft);
|
||||
if (Number.isFinite(parsed)) return parsed;
|
||||
return field.defaultValue;
|
||||
}
|
||||
|
||||
function multiSelectMcpElicitationFieldValue(
|
||||
field: Extract<PendingMcpElicitationField, { type: "multi-select" }>,
|
||||
draft: string,
|
||||
): readonly string[] {
|
||||
try {
|
||||
const parsed = JSON.parse(draft) as unknown;
|
||||
if (Array.isArray(parsed)) {
|
||||
const allowed = new Set(field.options.map((option) => option.value));
|
||||
return parsed.filter((value): value is string => typeof value === "string" && allowed.has(value));
|
||||
}
|
||||
} catch {
|
||||
// Fall through to schema default when a stale draft cannot be parsed.
|
||||
}
|
||||
return field.defaultValue;
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ import {
|
|||
type PendingMcpElicitation,
|
||||
type PendingRequestId,
|
||||
type PendingUserInput,
|
||||
} from "../../domain/pending-requests/model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
import { approvalResponse } from "../requests/approval";
|
||||
import { mcpElicitationResponse } from "../requests/mcp-elicitation";
|
||||
import { userInputResponse } from "../requests/user-input";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { ServerNotification, ServerRequest } from "../../../../app-server/connection/rpc-messages";
|
||||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../domain/pending-requests/model";
|
||||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../../../domain/pending-requests/model";
|
||||
import { toPendingApproval } from "../requests/approval";
|
||||
import { toPendingMcpElicitation } from "../requests/mcp-elicitation";
|
||||
import { toPendingUserInput } from "../requests/user-input";
|
||||
|
|
|
|||
|
|
@ -2,54 +2,12 @@ import type { ServerRequest } from "../../../../app-server/connection/rpc-messag
|
|||
import {
|
||||
appServerApprovalRequest,
|
||||
appServerApprovalResponse,
|
||||
type AppServerApproval,
|
||||
type AppServerApprovalResponse,
|
||||
} from "../../../../app-server/protocol/server-requests";
|
||||
import type { ApprovalAction, PendingApproval } from "../../domain/pending-requests/model";
|
||||
import type { ApprovalAction, PendingApproval } from "../../../../domain/pending-requests/model";
|
||||
|
||||
export function toPendingApproval(request: ServerRequest): PendingApproval | null {
|
||||
const approval = appServerApprovalRequest(request);
|
||||
if (!approval) return null;
|
||||
switch (approval.method) {
|
||||
case "item/commandExecution/requestApproval":
|
||||
return pendingApproval(approval);
|
||||
case "item/fileChange/requestApproval":
|
||||
return pendingApproval(approval);
|
||||
case "item/permissions/requestApproval":
|
||||
return pendingApproval(approval);
|
||||
}
|
||||
}
|
||||
|
||||
function pendingApproval(
|
||||
request: Extract<AppServerApproval, { method: "item/commandExecution/requestApproval" }>,
|
||||
): Extract<PendingApproval, { method: "item/commandExecution/requestApproval" }>;
|
||||
function pendingApproval(
|
||||
request: Extract<AppServerApproval, { method: "item/fileChange/requestApproval" }>,
|
||||
): Extract<PendingApproval, { method: "item/fileChange/requestApproval" }>;
|
||||
function pendingApproval(
|
||||
request: Extract<AppServerApproval, { method: "item/permissions/requestApproval" }>,
|
||||
): Extract<PendingApproval, { method: "item/permissions/requestApproval" }>;
|
||||
function pendingApproval(request: AppServerApproval): PendingApproval {
|
||||
switch (request.method) {
|
||||
case "item/commandExecution/requestApproval":
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
method: request.method,
|
||||
params: request.params,
|
||||
};
|
||||
case "item/fileChange/requestApproval":
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
method: request.method,
|
||||
params: request.params,
|
||||
};
|
||||
case "item/permissions/requestApproval":
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
method: request.method,
|
||||
params: request.params,
|
||||
};
|
||||
}
|
||||
return appServerApprovalRequest(request);
|
||||
}
|
||||
|
||||
export function approvalResponse(approval: PendingApproval, action: ApprovalAction): AppServerApprovalResponse {
|
||||
|
|
|
|||
|
|
@ -4,28 +4,10 @@ import {
|
|||
appServerMcpElicitationResponse,
|
||||
type AppServerMcpElicitationResponse,
|
||||
} from "../../../../app-server/protocol/server-requests";
|
||||
import type { McpElicitationAction, McpElicitationContentValue, PendingMcpElicitation } from "../../domain/pending-requests/model";
|
||||
import type { McpElicitationAction, McpElicitationContentValue, PendingMcpElicitation } from "../../../../domain/pending-requests/model";
|
||||
|
||||
export function toPendingMcpElicitation(request: ServerRequest): PendingMcpElicitation | null {
|
||||
const elicitation = appServerMcpElicitationRequest(request);
|
||||
if (!elicitation) return null;
|
||||
if (elicitation.params.mode === "url") {
|
||||
return {
|
||||
requestId: elicitation.requestId,
|
||||
method: elicitation.method,
|
||||
params: {
|
||||
...elicitation.params,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
requestId: elicitation.requestId,
|
||||
method: elicitation.method,
|
||||
params: {
|
||||
...elicitation.params,
|
||||
fields: elicitation.params.fields,
|
||||
},
|
||||
};
|
||||
return appServerMcpElicitationRequest(request);
|
||||
}
|
||||
|
||||
export function mcpElicitationResponse(
|
||||
|
|
|
|||
|
|
@ -4,16 +4,10 @@ import {
|
|||
appServerUserInputResponse,
|
||||
type AppServerUserInputResponse,
|
||||
} from "../../../../app-server/protocol/server-requests";
|
||||
import type { PendingUserInput } from "../../domain/pending-requests/model";
|
||||
import type { PendingUserInput } from "../../../../domain/pending-requests/model";
|
||||
|
||||
export function toPendingUserInput(request: ServerRequest): PendingUserInput | null {
|
||||
const userInputRequest = appServerUserInputRequest(request);
|
||||
if (!userInputRequest) return null;
|
||||
return {
|
||||
requestId: userInputRequest.requestId,
|
||||
method: userInputRequest.method,
|
||||
params: userInputRequest.params,
|
||||
};
|
||||
return appServerUserInputRequest(request);
|
||||
}
|
||||
|
||||
export function userInputResponse(input: PendingUserInput, answers: Record<string, string>): AppServerUserInputResponse {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import type {
|
|||
PendingMcpElicitation,
|
||||
PendingRequestId,
|
||||
PendingUserInput,
|
||||
} from "../../domain/pending-requests/model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
|
||||
export type { PendingRequestId } from "../../domain/pending-requests/model";
|
||||
export type { PendingRequestId } from "../../../../domain/pending-requests/model";
|
||||
|
||||
export interface PendingRequestBlockState {
|
||||
approvals: readonly PendingApproval[];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
type PendingApproval,
|
||||
type PendingMcpElicitation,
|
||||
type PendingUserInput,
|
||||
} from "../../domain/pending-requests/model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
import type { PendingRequestBlockActions, PendingRequestBlockState, PendingRequestId } from "./block";
|
||||
import type { ChatState } from "../state/root-reducer";
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type PendingMcpElicitation,
|
||||
type PendingRequestId,
|
||||
type PendingUserInput,
|
||||
} from "../../domain/pending-requests/model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
|
||||
export interface ChatRequestState {
|
||||
readonly approvals: readonly PendingApproval[];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { jsonPreview } from "../../../../utils";
|
||||
import { pathRelativeToRoot } from "../message-stream/format/path-labels";
|
||||
import { permissionRows, type MessageStreamPermissionProfile } from "../message-stream/format/permission-rows";
|
||||
import type { PendingApproval } from "./model";
|
||||
import type { PendingApproval } from "../../../../domain/pending-requests/model";
|
||||
|
||||
interface ApprovalSummaryParts {
|
||||
reason: string | null;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type PendingApproval,
|
||||
type PendingMcpElicitation,
|
||||
type PendingUserInput,
|
||||
} from "./model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
import { approvalDetails, approvalResultSummary, approvalTitle } from "./approval";
|
||||
import type { MessageStreamItem, MessageStreamUserInputQuestionResult } from "../message-stream/items";
|
||||
import { definedProp } from "../../../../utils";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "./model";
|
||||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../../../domain/pending-requests/model";
|
||||
|
||||
export function pendingRequestsSignature(
|
||||
approvals: readonly PendingApproval[],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
type ApprovalAction,
|
||||
type CommandApprovalDecision,
|
||||
type PendingApproval,
|
||||
} from "../../domain/pending-requests/model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
|
||||
export interface ApprovalActionOption {
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../domain/pending-requests/model";
|
||||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../../../domain/pending-requests/model";
|
||||
import {
|
||||
pendingApprovalViewModel,
|
||||
pendingMcpElicitationViewModel,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
questionDefaultAnswer,
|
||||
userInputDraftKey,
|
||||
userInputOtherDraftKey,
|
||||
} from "../../domain/pending-requests/model";
|
||||
} from "../../../../domain/pending-requests/model";
|
||||
|
||||
type PendingRequestId = DomainPendingRequestId;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
|
||||
import type { ApprovalAction, McpElicitationAction, PendingRequestId } from "../../domain/pending-requests/model";
|
||||
import type { ApprovalAction, McpElicitationAction, PendingRequestId } from "../../../../domain/pending-requests/model";
|
||||
import type { PendingRequestBlockSnapshot } from "../../presentation/pending-requests/snapshot";
|
||||
import type { MessageStreamForkTarget } from "../../presentation/message-stream/text-view";
|
||||
import type { PlanImplementationTarget } from "../../domain/message-stream/selectors";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { approvalDetailsDisclosureId } from "../../domain/pending-requests/model";
|
||||
import { approvalDetailsDisclosureId } from "../../../../domain/pending-requests/model";
|
||||
import {
|
||||
type PendingApprovalViewModel,
|
||||
type PendingMcpElicitationFieldViewModel,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import { approvalResponse, toPendingApproval } from "../../../../../src/features/chat/app-server/requests/approval";
|
||||
import { approvalDetails, approvalSummary, approvalTitle } from "../../../../../src/features/chat/domain/pending-requests/approval";
|
||||
import type { CommandApprovalDecision } from "../../../../../src/features/chat/domain/pending-requests/model";
|
||||
import type { CommandApprovalDecision } from "../../../../../src/domain/pending-requests/model";
|
||||
import { approvalActionOptions } from "../../../../../src/features/chat/presentation/pending-requests/approval-view";
|
||||
import type { ServerRequest } from "../../../../../src/app-server/connection/rpc-messages";
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import type { ServerRequest } from "../../../../../src/app-server/connection/rpc-messages";
|
||||
import { toPendingMcpElicitation, mcpElicitationResponse } from "../../../../../src/features/chat/app-server/requests/mcp-elicitation";
|
||||
import { contentForPendingMcpElicitation, mcpElicitationDraftKey } from "../../../../../src/features/chat/domain/pending-requests/model";
|
||||
import { contentForPendingMcpElicitation, mcpElicitationDraftKey } from "../../../../../src/domain/pending-requests/model";
|
||||
|
||||
function expectPresent<T>(value: T | null | undefined): T {
|
||||
if (value === null || value === undefined) throw new Error("Expected value to be present");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { answersForPendingUserInput, questionDefaultAnswer } from "../../../../../src/features/chat/domain/pending-requests/model";
|
||||
import { answersForPendingUserInput, questionDefaultAnswer } from "../../../../../src/domain/pending-requests/model";
|
||||
import { toPendingUserInput, userInputResponse } from "../../../../../src/features/chat/app-server/requests/user-input";
|
||||
import {
|
||||
pendingRequestFocusSignature,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import {
|
|||
pendingApprovalViewModel,
|
||||
pendingMcpElicitationViewModel,
|
||||
} from "../../../../../src/features/chat/presentation/pending-requests/view-model";
|
||||
import type {
|
||||
PendingApproval,
|
||||
PendingMcpElicitation,
|
||||
PendingUserInput,
|
||||
} from "../../../../../src/features/chat/domain/pending-requests/model";
|
||||
import type { PendingApproval, PendingMcpElicitation, PendingUserInput } from "../../../../../src/domain/pending-requests/model";
|
||||
import type { PendingRequestBlockContext } from "../../../../../src/features/chat/ui/message-stream/context";
|
||||
import type { MessageStreamItem } from "../../../../../src/features/chat/domain/message-stream/items";
|
||||
import { changeInputValue } from "../../../../support/dom";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { vi } from "vitest";
|
|||
import type { ComponentChild as UiNode } from "preact";
|
||||
import { act } from "preact/test-utils";
|
||||
|
||||
import type { PendingApproval, PendingUserInput } from "../../../../../src/features/chat/domain/pending-requests/model";
|
||||
import type { PendingApproval, PendingUserInput } from "../../../../../src/domain/pending-requests/model";
|
||||
import { pendingRequestBlockSnapshotFromState } from "../../../../../src/features/chat/presentation/pending-requests/snapshot";
|
||||
import { pendingRequestBlockNode } from "../../../../../src/features/chat/ui/message-stream/pending-request-block";
|
||||
import { messageStreamBlocks as rawMessageStreamBlocks } from "../../../../../src/features/chat/ui/message-stream/stream-blocks";
|
||||
|
|
|
|||
Loading…
Reference in a new issue