diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index d1f726d077..ea99fd1cd4 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -544,9 +544,7 @@ export type Endpoint14_2Input = { readonly id?: Endpoint14_2Request["payload"]["id"] readonly title: Endpoint14_2Request["payload"]["title"] readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly mode: Endpoint14_2Request["payload"]["mode"] - readonly fields?: Endpoint14_2Request["payload"]["fields"] - readonly url?: Endpoint14_2Request["payload"]["url"] + readonly fields: Endpoint14_2Request["payload"]["fields"] } export type Endpoint14_2Output = EffectValue>["data"] export type FormCreateOperation = (input: Endpoint14_2Input) => Effect.Effect diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 3b39da28a0..d50624c1ef 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -646,21 +646,12 @@ type Endpoint14_2Input = { readonly id?: Endpoint14_2Request["payload"]["id"] readonly title: Endpoint14_2Request["payload"]["title"] readonly metadata?: Endpoint14_2Request["payload"]["metadata"] - readonly mode: Endpoint14_2Request["payload"]["mode"] - readonly fields?: Endpoint14_2Request["payload"]["fields"] - readonly url?: Endpoint14_2Request["payload"]["url"] + readonly fields: Endpoint14_2Request["payload"]["fields"] } const Endpoint14_2 = (raw: RawClient["server.form"]) => (input: Endpoint14_2Input) => raw["session.form.create"]({ params: { sessionID: input["sessionID"] }, - payload: { - id: input["id"], - title: input["title"], - metadata: input["metadata"], - mode: input["mode"], - fields: input["fields"], - url: input["url"], - }, + payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, }).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 606673944c..5223e38083 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -1041,14 +1041,7 @@ export function make(options: ClientOptions) { { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, - body: { - id: input["id"], - title: input["title"], - metadata: input["metadata"], - mode: input["mode"], - fields: input["fields"], - url: input["url"], - }, + body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, successStatus: 200, declaredStatuses: [404, 409, 400, 401], empty: false, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index c61fdb5eea..bbca02f0c1 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -2700,225 +2700,205 @@ export type FormRequestListOutput = { readonly workspaceID?: string readonly project: { readonly id: string; readonly directory: string } } - readonly data: ReadonlyArray< - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" - readonly fields: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > - } - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "url" - readonly url: string - } - > + readonly data: ReadonlyArray<{ + readonly id: string + readonly sessionID: string + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: ReadonlyArray< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > + }> } export type FormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } export type FormListOutput = { - readonly data: ReadonlyArray< - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" - readonly fields: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > - } - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "url" - readonly url: string - } - > + readonly data: ReadonlyArray<{ + readonly id: string + readonly sessionID: string + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: ReadonlyArray< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > + }> }["data"] export type FormCreateInput = { @@ -2927,8 +2907,7 @@ export type FormCreateInput = { readonly id?: string | null readonly title: string readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" | "url" - readonly fields?: ReadonlyArray< + readonly fields: ReadonlyArray< | { readonly key: string readonly title?: string @@ -3017,15 +2996,14 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - > | null - readonly url?: string | null + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > }["id"] readonly title: { readonly id?: string | null readonly title: string readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" | "url" - readonly fields?: ReadonlyArray< + readonly fields: ReadonlyArray< | { readonly key: string readonly title?: string @@ -3114,15 +3092,14 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - > | null - readonly url?: string | null + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > }["title"] readonly metadata?: { readonly id?: string | null readonly title: string readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" | "url" - readonly fields?: ReadonlyArray< + readonly fields: ReadonlyArray< | { readonly key: string readonly title?: string @@ -3211,15 +3188,14 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - > | null - readonly url?: string | null + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > }["metadata"] - readonly mode: { + readonly fields: { readonly id?: string | null readonly title: string readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" | "url" - readonly fields?: ReadonlyArray< + readonly fields: ReadonlyArray< | { readonly key: string readonly title?: string @@ -3308,312 +3284,109 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - > | null - readonly url?: string | null - }["mode"] - readonly fields?: { - readonly id?: string | null - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" | "url" - readonly fields?: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > | null - readonly url?: string | null + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > }["fields"] - readonly url?: { - readonly id?: string | null - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" | "url" - readonly fields?: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > | null - readonly url?: string | null - }["url"] } export type FormCreateOutput = { - readonly data: - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" - readonly fields: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > - } - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "url" - readonly url: string - } + readonly data: { + readonly id: string + readonly sessionID: string + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: ReadonlyArray< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > + } }["data"] export type FormGetInput = { @@ -3622,112 +3395,103 @@ export type FormGetInput = { } export type FormGetOutput = { - readonly data: - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "form" - readonly fields: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "number" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "integer" - readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" - readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" - readonly default?: number | "Infinity" | "-Infinity" | "NaN" - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > - } - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly mode: "url" - readonly url: string - } + readonly data: { + readonly id: string + readonly sessionID: string + readonly title: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly fields: ReadonlyArray< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "number" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "integer" + readonly minimum?: number | "Infinity" | "-Infinity" | "NaN" + readonly maximum?: number | "Infinity" | "-Infinity" | "NaN" + readonly default?: number | "Infinity" | "-Infinity" | "NaN" + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > + } }["data"] export type FormStateInput = { @@ -5455,112 +5219,103 @@ export type EventSubscribeOutput = readonly type: "form.created" readonly location?: { readonly directory: string; readonly workspaceID?: string } readonly data: { - readonly form: - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: unknown } - readonly mode: "form" - readonly fields: ReadonlyArray< - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | boolean - }> - readonly type: "string" - readonly format?: "email" | "uri" | "date" | "date-time" - readonly minLength?: number - readonly maxLength?: number - readonly pattern?: string - readonly placeholder?: string - readonly default?: string - readonly options?: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly custom?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | boolean - }> - readonly type: "number" - readonly minimum?: number - readonly maximum?: number - readonly default?: number - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | boolean - }> - readonly type: "integer" - readonly minimum?: number - readonly maximum?: number - readonly default?: number - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | boolean - }> - readonly type: "boolean" - readonly default?: boolean - } - | { - readonly key: string - readonly title?: string - readonly description?: string - readonly required?: boolean - readonly when?: ReadonlyArray<{ - readonly key: string - readonly op: "eq" | "neq" - readonly value: string | number | boolean - }> - readonly type: "multiselect" - readonly options: ReadonlyArray<{ - readonly value: string - readonly label: string - readonly description?: string - }> - readonly minItems?: number - readonly maxItems?: number - readonly custom?: boolean - readonly default?: ReadonlyArray - } - > - } - | { - readonly id: string - readonly sessionID: string - readonly title: string - readonly metadata?: { readonly [x: string]: unknown } - readonly mode: "url" - readonly url: string - } + readonly form: { + readonly id: string + readonly sessionID: string + readonly title: string + readonly metadata?: { readonly [x: string]: unknown } + readonly fields: ReadonlyArray< + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | boolean + }> + readonly type: "string" + readonly format?: "email" | "uri" | "date" | "date-time" + readonly minLength?: number + readonly maxLength?: number + readonly pattern?: string + readonly placeholder?: string + readonly default?: string + readonly options?: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly custom?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | boolean + }> + readonly type: "number" + readonly minimum?: number + readonly maximum?: number + readonly default?: number + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | boolean + }> + readonly type: "integer" + readonly minimum?: number + readonly maximum?: number + readonly default?: number + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | boolean + }> + readonly type: "boolean" + readonly default?: boolean + } + | { + readonly key: string + readonly title?: string + readonly description?: string + readonly required?: boolean + readonly when?: ReadonlyArray<{ + readonly key: string + readonly op: "eq" | "neq" + readonly value: string | number | boolean + }> + readonly type: "multiselect" + readonly options: ReadonlyArray<{ + readonly value: string + readonly label: string + readonly description?: string + }> + readonly minItems?: number + readonly maxItems?: number + readonly custom?: boolean + readonly default?: ReadonlyArray + } + | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + > + } } } | { diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index 7cdda87b4c..979b7b5709 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -64,9 +64,7 @@ export class InvalidFormError extends Schema.TaggedErrorClass( message: Schema.String, }) {} -export type CreateInput = - | (Omit & { readonly id?: ID }) - | (Omit & { readonly id?: ID }) +export type CreateInput = Omit & { readonly id?: ID } export interface ReplyInput { readonly id: ID @@ -74,7 +72,7 @@ export interface ReplyInput { } export interface ListInput { - readonly sessionID?: Form.FormInfo["sessionID"] + readonly sessionID?: Form.Info["sessionID"] } export interface Interface { @@ -125,20 +123,15 @@ export const layer = Layer.effect( const id = input.id ?? ID.create() const existing = yield* Cache.getSuccess(forms, id) if (Option.isSome(existing)) return yield* new AlreadyExistsError({ id }) - if (input.mode === "form") { - const invalid = validateFields(input.fields) - if (invalid) return yield* new InvalidFormError({ message: invalid }) - } - const base = { + const invalid = validateFields(input.fields) + if (invalid) return yield* new InvalidFormError({ message: invalid }) + const form: Info = { id, sessionID: input.sessionID, title: input.title, ...(input.metadata === undefined ? {} : { metadata: input.metadata }), + fields: input.fields, } - const form: Info = - input.mode === "form" - ? { ...base, mode: "form", fields: input.fields } - : { ...base, mode: "url", url: input.url } const entry: Entry = { form, state: { status: "pending" }, @@ -228,15 +221,12 @@ export const locationLayer = layer export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) function validateAnswer(form: Info, answer: Answer) { - if (form.mode === "url") { - if (Object.keys(answer).length === 0) return - return "URL forms must be answered with an empty answer" - } - const fields = new Map(form.fields.map((field) => [field.key, field])) + const fields = new Map(form.fields.flatMap((field) => (field.type === "link" ? [] : [[field.key, field] as const]))) for (const key of Object.keys(answer)) { if (!fields.has(key)) return `Unknown form field: ${key}` } for (const field of form.fields) { + if (field.type === "link") continue const value = answer[field.key] const active = isActive(field, answer) if (value === undefined) { @@ -249,7 +239,9 @@ function validateAnswer(form: Info, answer: Answer) { } } -function isActive(field: Form.Field, answer: Answer) { +type InputField = Exclude + +function isActive(field: InputField, answer: Answer) { if (!field.when) return true return field.when.every((when) => matches(when, answer[when.key])) } @@ -267,8 +259,9 @@ function matches(when: Form.When, value: Form.Value | undefined) { // are closed. Rejecting these at creation surfaces authoring mistakes to the caller instead of // silently never matching. function validateFields(fields: ReadonlyArray) { - const earlier = new Map() + const earlier = new Map() for (const field of fields) { + if (field.type === "link") continue if (earlier.has(field.key)) return `Duplicate form field key: ${field.key}` for (const when of field.when ?? []) { const target = earlier.get(when.key) @@ -280,7 +273,7 @@ function validateFields(fields: ReadonlyArray) { } } -function validateWhen(when: Form.When, target: Form.Field) { +function validateWhen(when: Form.When, target: InputField) { if (target.type === "boolean") { if (typeof when.value !== "boolean") return "Form field condition value must be a boolean" return @@ -297,7 +290,7 @@ function validateWhen(when: Form.When, target: Form.Field) { } } -function validateField(field: Form.Field, value: Form.Value): string | undefined { +function validateField(field: InputField, value: Form.Value): string | undefined { if (field.type === "string") { if (typeof value !== "string") return `Expected string for form field: ${field.key}` if (field.required && value.length === 0) return `Missing required form field: ${field.key}` diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 7a99c15982..5cc9d8a24c 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -311,8 +311,7 @@ export const layer = Layer.effect( elicitationID: input.params.elicitationId, message: input.params.message, }, - mode: "url", - url: input.params.url, + fields: [{ type: "link", url: input.params.url }], }) .pipe( Effect.raceFirst(waitForAbort(input.signal)), @@ -330,7 +329,6 @@ export const layer = Layer.effect( sessionID: GLOBAL_ELICITATION_SESSION_ID, title: `${input.server} is requesting input`, metadata: { kind: "mcp-elicitation", server: input.server, message: params.message }, - mode: "form", fields: Object.entries(params.requestedSchema.properties).map(([key, property]) => toElicitationField(key, property, params.requestedSchema.required?.includes(key) === true), ), diff --git a/packages/core/src/tool/question.ts b/packages/core/src/tool/question.ts index 646efd4b3b..91a6d02afe 100644 --- a/packages/core/src/tool/question.ts +++ b/packages/core/src/tool/question.ts @@ -86,7 +86,6 @@ export const Plugin = { kind: "question", tool: { messageID: context.assistantMessageID, callID: context.toolCallID }, }, - mode: "form", fields: input.questions.map( (question, index): Form.Field => ({ key: `q${index}`, diff --git a/packages/core/test/form.test.ts b/packages/core/test/form.test.ts index f5cf16e159..4ee3c70b32 100644 --- a/packages/core/test/form.test.ts +++ b/packages/core/test/form.test.ts @@ -15,7 +15,6 @@ const input = { id: formID, sessionID: SessionSchema.ID.make("ses_test"), title: "Test form", - mode: "form", fields: [{ key: "name", type: "string", required: true }], } satisfies Form.CreateInput @@ -47,7 +46,6 @@ describe("Form", () => { const created = yield* service.create({ sessionID: "global", title: "MCP input", - mode: "form", fields: [{ key: "name", type: "string", required: true }], }) expect(created.sessionID).toBe("global") @@ -59,6 +57,14 @@ describe("Form", () => { yield* service.reply({ id: created.id, answer: { name: "Ava" } }) expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { name: "Ava" } }) + + const linkOnly = yield* service.create({ + sessionID: "global", + title: "External setup", + fields: [{ type: "link", url: "https://example.com/setup" }], + }) + yield* service.reply({ id: linkOnly.id, answer: {} }) + expect(yield* service.state(linkOnly.id)).toEqual({ status: "answered", answer: {} }) }), ) @@ -68,15 +74,18 @@ describe("Form", () => { const created = yield* service.create({ sessionID: "global", title: "Conditional form", - mode: "form", fields: [ { key: "confirm", type: "boolean", required: true }, { key: "reason", type: "string", required: true, when: [{ key: "confirm", op: "eq", value: false }] }, ], }) - const inactive = yield* service.reply({ id: created.id, answer: { confirm: true, reason: "x" } }).pipe(Effect.flip) - expect(inactive).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: reason" })) + const inactive = yield* service + .reply({ id: created.id, answer: { confirm: true, reason: "x" } }) + .pipe(Effect.flip) + expect(inactive).toEqual( + new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: reason" }), + ) const missing = yield* service.reply({ id: created.id, answer: { confirm: false } }).pipe(Effect.flip) expect(missing).toEqual( @@ -101,7 +110,6 @@ describe("Form", () => { const created = yield* service.create({ sessionID: "global", title: "Multiselect form", - mode: "form", fields: [ { key: "langs", type: "multiselect", options }, { key: "goVersion", type: "string", required: true, when: [{ key: "langs", op: "eq", value: "go" }] }, @@ -124,7 +132,6 @@ describe("Form", () => { const created = yield* service.create({ sessionID: "global", title: "Dependent form", - mode: "form", fields: [ { key: "a", type: "boolean" }, { key: "b", type: "boolean" }, @@ -142,7 +149,9 @@ describe("Form", () => { }) const missingX = yield* service.reply({ id: created.id, answer: { a: true, b: true, z: "ok" } }).pipe(Effect.flip) - expect(missingX).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: x" })) + expect(missingX).toEqual( + new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: x" }), + ) const inactiveX = yield* service .reply({ id: created.id, answer: { a: true, b: false, x: "nope", z: "ok" } }) @@ -150,7 +159,9 @@ describe("Form", () => { expect(inactiveX).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: x" })) const missingZ = yield* service.reply({ id: created.id, answer: { a: true, b: false } }).pipe(Effect.flip) - expect(missingZ).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: z" })) + expect(missingZ).toEqual( + new Form.InvalidAnswerError({ id: created.id, message: "Missing required form field: z" }), + ) yield* service.reply({ id: created.id, answer: { a: true, b: false, z: "ok" } }) expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { a: true, b: false, z: "ok" } }) @@ -167,7 +178,6 @@ describe("Form", () => { const created = yield* service.create({ sessionID: "global", title: "Selection form", - mode: "form", fields: [ { key: "langs", type: "multiselect", options }, { key: "note", type: "string", required: true, when: [{ key: "langs", op: "neq", value: "go" }] }, @@ -186,7 +196,9 @@ describe("Form", () => { ) const inactive = yield* service.reply({ id: created.id, answer: { langs: ["go"], note: "x" } }).pipe(Effect.flip) - expect(inactive).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: note" })) + expect(inactive).toEqual( + new Form.InvalidAnswerError({ id: created.id, message: "Form field is not active: note" }), + ) yield* service.reply({ id: created.id, answer: { langs: ["go"] } }) expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { langs: ["go"] } }) @@ -199,7 +211,6 @@ describe("Form", () => { const created = yield* service.create({ sessionID: "global", title: "Cascading form", - mode: "form", fields: [ { key: "a", type: "boolean" }, { key: "b", type: "string", when: [{ key: "a", op: "eq", value: true }] }, @@ -221,13 +232,13 @@ describe("Form", () => { Effect.gen(function* () { const service = yield* Form.Service const flipCreate = (fields: ReadonlyArray) => - service.create({ sessionID: "global", title: "Invalid form", mode: "form", fields }).pipe(Effect.flip) + service.create({ sessionID: "global", title: "Invalid form", fields }).pipe(Effect.flip) expect( - yield* flipCreate([ - { key: "b", type: "string", when: [{ key: "missing", op: "eq", value: "x" }] }, - ]), - ).toEqual(new Form.InvalidFormError({ message: "Form field condition must reference an earlier field: b -> missing" })) + yield* flipCreate([{ key: "b", type: "string", when: [{ key: "missing", op: "eq", value: "x" }] }]), + ).toEqual( + new Form.InvalidFormError({ message: "Form field condition must reference an earlier field: b -> missing" }), + ) expect( yield* flipCreate([ @@ -241,9 +252,7 @@ describe("Form", () => { { key: "a", type: "boolean" }, { key: "b", type: "string", when: [{ key: "a", op: "eq", value: "yes" }] }, ]), - ).toEqual( - new Form.InvalidFormError({ message: "Form field condition value must be a boolean: b -> a" }), - ) + ).toEqual(new Form.InvalidFormError({ message: "Form field condition value must be a boolean: b -> a" })) expect( yield* flipCreate([ @@ -258,6 +267,28 @@ describe("Form", () => { }), ) + it.effect("treats link fields as non-answerable", () => + Effect.gen(function* () { + const service = yield* Form.Service + const created = yield* service.create({ + sessionID: "global", + title: "External setup", + fields: [ + { type: "link", url: "https://example.com/setup", title: "Open setup" }, + { key: "name", type: "string", required: true }, + ], + }) + + const invalid = yield* service + .reply({ id: created.id, answer: { link: "opened", name: "Ava" } }) + .pipe(Effect.flip) + expect(invalid).toEqual(new Form.InvalidAnswerError({ id: created.id, message: "Unknown form field: link" })) + + yield* service.reply({ id: created.id, answer: { name: "Ava" } }) + expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { name: "Ava" } }) + }), + ) + it.effect("cleans up created forms when event publication fails", () => Effect.gen(function* () { const service = yield* Form.Service diff --git a/packages/core/test/tool-question.test.ts b/packages/core/test/tool-question.test.ts index 63b6f3a912..a763ac59e4 100644 --- a/packages/core/test/tool-question.test.ts +++ b/packages/core/test/tool-question.test.ts @@ -158,7 +158,6 @@ describe("QuestionTool", () => { sessionID, title: "Questions", metadata: { kind: "question", tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" } }, - mode: "form", fields: [ { key: "q0", @@ -205,7 +204,6 @@ describe("QuestionTool", () => { sessionID, title: "Questions", metadata: { kind: "question", tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" } }, - mode: "form", fields: [], }) }), diff --git a/packages/protocol/src/groups/form.ts b/packages/protocol/src/groups/form.ts index b3fe2f12e8..f9160e5dc9 100644 --- a/packages/protocol/src/groups/form.ts +++ b/packages/protocol/src/groups/form.ts @@ -14,11 +14,9 @@ import { LocationQuery, locationQueryOpenApi } from "./location.js" const CreatePayload = Schema.Struct({ id: Form.ID.pipe(Schema.optional), - title: Form.FormInfo.fields.title, - metadata: Form.FormInfo.fields.metadata, - mode: Schema.Literals(["form", "url"]), - fields: Form.FormInfo.fields.fields.pipe(Schema.optional), - url: Form.UrlInfo.fields.url.pipe(Schema.optional), + title: Form.Info.fields.title, + metadata: Form.Info.fields.metadata, + fields: Form.Info.fields.fields, }).annotate({ identifier: "Form.CreatePayload" }) export type CreatePayload = typeof CreatePayload.Type diff --git a/packages/schema/src/form.ts b/packages/schema/src/form.ts index 98f863f7fa..1fd6ad4466 100644 --- a/packages/schema/src/form.ts +++ b/packages/schema/src/form.ts @@ -94,10 +94,23 @@ export const MultiselectField = Schema.Struct({ }).annotate({ identifier: "Form.MultiselectField" }) export interface MultiselectField extends Schema.Schema.Type {} -export const Field = Schema.Union([StringField, NumberField, IntegerField, BooleanField, MultiselectField]).pipe( - Schema.toTaggedUnion("type"), -) -export type Field = StringField | NumberField | IntegerField | BooleanField | MultiselectField +export const LinkField = Schema.Struct({ + type: Schema.Literal("link"), + url: Schema.String, + title: Schema.String.pipe(optional), + description: Schema.String.pipe(optional), +}).annotate({ identifier: "Form.LinkField" }) +export interface LinkField extends Schema.Schema.Type {} + +export const Field = Schema.Union([ + StringField, + NumberField, + IntegerField, + BooleanField, + MultiselectField, + LinkField, +]).pipe(Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "Form.Field" })) +export type Field = StringField | NumberField | IntegerField | BooleanField | MultiselectField | LinkField const InfoBase = { id: ID, @@ -110,22 +123,11 @@ const InfoBase = { metadata: Metadata.pipe(optional), } -export const FormInfo = Schema.Struct({ +export const Info = Schema.Struct({ ...InfoBase, - mode: Schema.Literal("form"), fields: Schema.Array(Field), -}).annotate({ identifier: "Form.FormInfo" }) -export interface FormInfo extends Schema.Schema.Type {} - -export const UrlInfo = Schema.Struct({ - ...InfoBase, - mode: Schema.Literal("url"), - url: Schema.String, -}).annotate({ identifier: "Form.UrlInfo" }) -export interface UrlInfo extends Schema.Schema.Type {} - -export const Info = Schema.Union([FormInfo, UrlInfo]).pipe(Schema.toTaggedUnion("mode")) -export type Info = FormInfo | UrlInfo +}).annotate({ identifier: "Form.Info" }) +export interface Info extends Schema.Schema.Type {} export const Value = Schema.Union([Schema.String, Schema.Number, Schema.Boolean, Schema.Array(Schema.String)]).annotate( { diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index 140be20cba..6b3daaf261 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test" import { DateTime, Schema } from "effect" import { Agent } from "../src/agent.js" import { FileSystem } from "../src/filesystem.js" +import { Form } from "../src/form.js" import { Mcp } from "../src/mcp.js" import { Model } from "../src/model.js" import { Project } from "../src/project.js" @@ -69,6 +70,9 @@ describe("contract hygiene", () => { const identifiers = [ Agent.Color, FileSystem.Submatch, + Form.Field, + Form.Info, + Form.LinkField, Mcp.Resource, Mcp.ResourceTemplate, Mcp.ResourceCatalog, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 92b9514db5..23a55908f0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1408,7 +1408,7 @@ export type GlobalEvent = { id: string type: "form.created" properties: { - form: FormFormInfo | FormUrlInfo + form: FormInfo } } | { @@ -3545,22 +3545,27 @@ export type FormMultiselectField = { default?: Array } -export type FormFormInfo = { - id: string - sessionID: string - title: string - metadata?: FormMetadata - mode: "form" - fields: Array +export type FormLinkField = { + type: "link" + url: string + title?: string + description?: string } -export type FormUrlInfo = { +export type FormField = + | FormStringField + | FormNumberField + | FormIntegerField + | FormBooleanField + | FormMultiselectField + | FormLinkField + +export type FormInfo = { id: string sessionID: string title: string metadata?: FormMetadata - mode: "url" - url: string + fields: Array } export type FormValue = @@ -5836,9 +5841,7 @@ export type FormCreatePayload = { id?: string title: string metadata?: FormMetadata - mode: "form" | "url" - fields?: Array - url?: string + fields: Array } export type FormState = @@ -6559,7 +6562,7 @@ export type FormCreated = { type: "form.created" location?: LocationRef data: { - form: FormFormInfo | FormUrlInfo + form: FormInfo } } @@ -7831,7 +7834,7 @@ export type EventFormCreated = { id: string type: "form.created" properties: { - form: FormFormInfo | FormUrlInfo + form: FormInfo } } @@ -9892,33 +9895,19 @@ export type FormMultiselectFieldV2 = { default?: Array } -export type FormFormInfoV2 = { +export type FormInfoV2 = { id: string sessionID: string title: string metadata?: FormMetadata - mode: "form" - fields: Array -} - -export type FormUrlInfoV2 = { - id: string - sessionID: string - title: string - metadata?: FormMetadata - mode: "url" - url: string + fields: Array } export type FormCreatePayloadV2 = { id?: string | null title: string metadata?: FormMetadata - mode: "form" | "url" - fields?: Array< - FormStringFieldV2 | FormNumberField | FormIntegerField | FormBooleanField | FormMultiselectFieldV2 - > | null - url?: string | null + fields: Array } export type FormValueV2 = @@ -10627,22 +10616,20 @@ export type FormMultiselectField1 = { default?: Array } -export type FormFormInfo1 = { - id: string - sessionID: string - title: string - metadata?: FormMetadata1 - mode: "form" - fields: Array -} +export type FormField1 = + | FormStringField1 + | FormNumberField1 + | FormIntegerField1 + | FormBooleanField1 + | FormMultiselectField1 + | FormLinkField -export type FormUrlInfo1 = { +export type FormInfo1 = { id: string sessionID: string title: string metadata?: FormMetadata1 - mode: "url" - url: string + fields: Array } export type FormCreatedV2 = { @@ -10654,7 +10641,7 @@ export type FormCreatedV2 = { type: "form.created" location?: LocationRefV2 data: { - form: FormFormInfo1 | FormUrlInfo1 + form: FormInfo1 } } @@ -17207,7 +17194,7 @@ export type V2FormRequestListResponses = { */ 200: { location: LocationInfoV2 - data: Array + data: Array } } @@ -17244,7 +17231,7 @@ export type V2SessionFormListResponses = { * Success */ 200: { - data: Array + data: Array } } @@ -17285,7 +17272,7 @@ export type V2SessionFormCreateResponses = { * Success */ 200: { - data: FormFormInfoV2 | FormUrlInfoV2 + data: FormInfoV2 } } @@ -17323,7 +17310,7 @@ export type V2SessionFormGetResponses = { * Success */ 200: { - data: FormFormInfoV2 | FormUrlInfoV2 + data: FormInfoV2 } } diff --git a/packages/server/src/handlers/form.ts b/packages/server/src/handlers/form.ts index db910464af..e561153bb0 100644 --- a/packages/server/src/handlers/form.ts +++ b/packages/server/src/handlers/form.ts @@ -44,29 +44,21 @@ export const FormHandler = HttpApiBuilder.group(Api, "server.form", (handlers) = "session.form.create", Effect.fn(function* (ctx) { const form = yield* Form.Service - const common = { - id: ctx.payload.id, - sessionID: ctx.params.sessionID, - title: ctx.payload.title, - metadata: ctx.payload.metadata, - } - const input = yield* (() => { - if (ctx.payload.mode === "form") { - if (!ctx.payload.fields) { - return new InvalidRequestError({ message: "Form fields are required", field: "fields" }) - } - return Effect.succeed({ ...common, mode: "form" as const, fields: ctx.payload.fields }) - } - if (!ctx.payload.url) return new InvalidRequestError({ message: "Form URL is required", field: "url" }) - return Effect.succeed({ ...common, mode: "url" as const, url: ctx.payload.url }) - })() - - const created = yield* form.create(input).pipe( - Effect.catchTags({ - "Form.AlreadyExistsError": (error) => new ConflictError({ resource: error.id, message: error.message }), - "Form.InvalidFormError": (error) => new InvalidRequestError({ message: error.message, field: "fields" }), - }), - ) + const created = yield* form + .create({ + id: ctx.payload.id, + sessionID: ctx.params.sessionID, + title: ctx.payload.title, + metadata: ctx.payload.metadata, + fields: ctx.payload.fields, + }) + .pipe( + Effect.catchTags({ + "Form.AlreadyExistsError": (error) => new ConflictError({ resource: error.id, message: error.message }), + "Form.InvalidFormError": (error) => + new InvalidRequestError({ message: error.message, field: "fields" }), + }), + ) return { data: created } }), ) diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 76d24a796d..99ad580dd4 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -6,8 +6,6 @@ import type { AgentInfo, CommandInfo, - FormFormInfo, - FormUrlInfo, IntegrationInfo, LocationRef, McpServer, @@ -38,7 +36,7 @@ const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_") // Global MCP elicitations temporarily use "global" instead of a real session ID, so the // server cannot recover their Location when settling them. Preserve the event Location // until MCP elicitations carry session ownership. -export type FormInfo = (FormFormInfo | FormUrlInfo) & { readonly location?: LocationRef } +export type FormInfo = import("@opencode-ai/sdk/v2").FormInfo & { readonly location?: LocationRef } type LocationData = { agent?: AgentInfo[] diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx index 5f143370ce..40ed0bfe13 100644 --- a/packages/tui/src/routes/session/form.tsx +++ b/packages/tui/src/routes/session/form.tsx @@ -4,7 +4,7 @@ import { useRenderer, useTerminalDimensions } from "@opentui/solid" import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core" import open from "open" import { selectedForeground, tint, useTheme } from "../../context/theme" -import type { FormFormInfo, FormValue } from "@opencode-ai/sdk/v2" +import type { FormField, FormValue } from "@opencode-ai/sdk/v2" import type { FormInfo } from "../../context/data" import { useSDK } from "../../context/sdk" import { SplitBorder } from "../../ui/border" @@ -13,7 +13,16 @@ import { useBindings, useOpencodeModeStack } from "../../keymap" const FORM_MODE = "form" -type Field = FormFormInfo["fields"][number] +type Field = Exclude +type LinkField = Extract + +function isField(field: FormField): field is Field { + return field.type !== "link" +} + +function isLink(field: FormField): field is LinkField { + return field.type === "link" +} function fieldLabel(field: Field) { return field.title ?? field.key @@ -141,10 +150,15 @@ function requestOptions(form: FormInfo) { } export function FormPrompt(props: { form: FormInfo }) { - return props.form.mode === "url" ? : + const links = () => props.form.fields.filter(isLink) + return links().length > 0 && links().length === props.form.fields.length ? ( + + ) : ( + + ) } -function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) { +function LinkPrompt(props: { form: FormInfo; links: LinkField[] }) { const sdk = useSDK() const { theme } = useTheme() const modeStack = useOpencodeModeStack() @@ -177,7 +191,8 @@ function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) { desc: "Open link", group: "Form", cmd: () => { - void open(props.form.url) + const link = props.links[0] + if (link) void open(link.url) }, }, { @@ -206,7 +221,19 @@ function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) { {message()} - {props.form.url} + + {(link) => ( + + + {link.title} + + + {link.description} + + {link.url} + + )} + @@ -220,27 +247,28 @@ function UrlPrompt(props: { form: FormInfo & { mode: "url" } }) { ) } -function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) { +function FieldsPrompt(props: { form: FormInfo }) { const sdk = useSDK() const { theme } = useTheme() const renderer = useRenderer() const dimensions = useTerminalDimensions() const tuiConfig = useTuiConfig() const modeStack = useOpencodeModeStack() + const configuredFields = () => props.form.fields.filter(isField) const [tabHover, setTabHover] = createSignal(null) const [store, setStore] = createStore({ tab: 0, answers: Object.fromEntries( - props.form.fields.flatMap((field) => (field.default === undefined ? [] : [[field.key, field.default]])), + configuredFields().flatMap((field) => (field.default === undefined ? [] : [[field.key, field.default]])), ) as Record, custom: Object.fromEntries( - props.form.fields.flatMap((field) => { + configuredFields().flatMap((field) => { const value = customDefault(field) return value === undefined ? [] : [[field.key, value]] }), ) as Record, - selected: selectedRow(props.form.fields[0], props.form.fields[0]?.default), + selected: selectedRow(configuredFields()[0], configuredFields()[0]?.default), editing: false, error: "", }) @@ -248,9 +276,10 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) { let textarea: TextareaRenderable | undefined let review: ScrollBoxRenderable | undefined + const links = createMemo(() => props.form.fields.filter(isLink)) const fields = createMemo(() => { const answers: Record = {} - return props.form.fields.filter((field) => { + return configuredFields().filter((field) => { const active = (field.when ?? []).every((when) => { const value = answers[when.key] if (value === undefined) return false @@ -263,7 +292,6 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) { }) const single = createMemo(() => { const list = fields() - if (props.form.fields.length !== 1) return false if (list.length !== 1) return false const field = list[0]! return field.type === "boolean" || (field.type === "string" && field.options !== undefined) @@ -769,6 +797,26 @@ function FieldsPrompt(props: { form: FormInfo & { mode: "form" } }) { {props.form.title} + + {(link) => ( + { + if (renderer.getSelection()?.getSelectedText()) return + void open(link.url) + }} + > + + {link.title} + + + {link.description} + + {link.url} + + )} + diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index adacd106b9..2a1c7e09e8 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -78,7 +78,6 @@ function form(id: string, sessionID = "session"): Extract { const calls = createFetch((url) => { if (url.pathname !== "/api/session/ses_1/form") return return json({ - data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] }], + data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", fields: [] }], }) }, events) let data!: ReturnType @@ -1540,13 +1540,13 @@ test("adds, dismisses, and refreshes form requests", async () => { id: "evt_form_created_1", created: 0, type: "form.created", - data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } }, + data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: [] } }, }) emitEvent(events, { id: "evt_form_created_duplicate", created: 1, type: "form.created", - data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } }, + data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: [] } }, }) await wait(() => data.session.form.list("ses_1")?.length === 1) @@ -1562,7 +1562,7 @@ test("adds, dismisses, and refreshes form requests", async () => { id: "evt_form_created_2", created: 3, type: "form.created", - data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", mode: "form", fields: [] } }, + data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", fields: [] } }, }) emitEvent(events, { id: "evt_form_cancelled_2", @@ -1612,7 +1612,7 @@ test("tracks global forms by location", async () => { location: other, type: "form.created", data: { - form: { id: "frm_other", sessionID: "global", title: "Input requested", mode: "form", fields: [] }, + form: { id: "frm_other", sessionID: "global", title: "Input requested", fields: [] }, }, }) @@ -1625,7 +1625,7 @@ test("tracks global forms by location", async () => { location: { directory }, type: "form.created", data: { - form: { id: "frm_default", sessionID: "global", title: "Input requested", mode: "form", fields: [] }, + form: { id: "frm_default", sessionID: "global", title: "Input requested", fields: [] }, }, }) await wait(() => data.session.form.list("global", { directory })?.length === 1) @@ -1664,7 +1664,6 @@ test("refreshes global forms for the requested location", async () => { id: requestedDirectory === other.directory ? "frm_other" : "frm_default", sessionID: "global", title: "Input requested", - mode: "form", fields: [], }, ], @@ -1743,7 +1742,6 @@ test("refreshes global forms once per loaded location after reconnect", async () id: `frm_${requestedDirectory === other.directory ? "other" : "default"}_${count}`, sessionID: "global", title: "Input requested", - mode: "form", fields: [], }, ], @@ -1803,13 +1801,12 @@ test("refreshes global forms once per loaded location after reconnect", async () test("reconciles all pending form requests when the event stream reconnects", async () => { const events = createEventStream() let requests = [ - { id: "frm_old", sessionID: "ses_old", title: "Input requested", mode: "form" as const, fields: [] }, + { id: "frm_old", sessionID: "ses_old", title: "Input requested", fields: [] }, { id: "frm_keep", sessionID: "ses_keep", title: "Input requested", - mode: "url" as const, - url: "https://example.com", + fields: [{ type: "link" as const, url: "https://example.com" }], }, ] let calls = 0 @@ -1841,7 +1838,7 @@ test("reconciles all pending form requests when the event stream reconnects", as await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old") expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep") - requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", mode: "form" as const, fields: [] }] + requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", fields: [] }] events.disconnect() await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")