From 12e53ee6d45962f90a4a4c21360d4a61d0a30382 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 15 Sep 2026 18:23:35 -0400 Subject: [PATCH] refactor(protocol): simplify interactive resources --- V2_HTTP_API_AUDIT.md | 36 +- .../remote-session-settings.spec.ts | 12 +- .../app/e2e/regression/session-queue.spec.ts | 6 +- .../regression/session-request-docks.spec.ts | 7 +- .../regression/tab-navigate-mousedown.spec.ts | 2 +- packages/app/e2e/utils/mock-api.ts | 7 +- packages/app/e2e/utils/mock-server.ts | 10 +- packages/app/src/session/composer/queue.ts | 4 +- .../app/src/session/requests/auto-approve.ts | 2 +- packages/app/src/session/requests/model.ts | 2 +- .../requests/session-question-dock.tsx | 4 +- .../src/session/requests/websearch.test.ts | 6 +- .../app/src/session/requests/websearch.ts | 6 +- packages/cli/src/acp/event.ts | 2 +- packages/cli/src/acp/permission.ts | 2 +- packages/cli/src/run/noninteractive.ts | 8 +- packages/cli/test/acp/event-behavior.test.ts | 4 +- packages/cli/test/run/noninteractive.test.ts | 22 +- packages/client/src/effect/api/api.ts | 116 +- .../client/src/effect/generated/client.ts | 164 ++- .../client/src/promise/generated/client.ts | 193 ++- .../client/src/promise/generated/types.ts | 974 +++++++------- packages/client/src/solid/data.ts | 18 +- packages/client/test/promise.test.ts | 8 +- packages/core/src/permission/saved.ts | 6 +- packages/core/src/plugin/host.ts | 2 +- packages/core/test/permission.test.ts | 4 +- packages/plugin/src/tui/context.ts | 8 +- packages/protocol/openapi.json | 1148 ++++++++--------- packages/protocol/src/api.ts | 28 +- packages/protocol/src/groups/form.ts | 129 +- packages/protocol/src/groups/permission.ts | 2 +- packages/protocol/src/groups/session.ts | 125 +- packages/schema/src/form.ts | 6 + packages/schema/src/permission-saved.ts | 6 +- packages/sdk/test/instances.test.ts | 2 +- packages/server/src/handlers/form.ts | 124 +- packages/server/src/handlers/permission.ts | 2 +- packages/server/src/handlers/session.ts | 107 +- .../server/test/session-instances.test.ts | 4 +- packages/tui/src/mini/demo.ts | 4 +- packages/tui/src/mini/permission.shared.ts | 4 +- packages/tui/src/mini/runtime.ts | 10 +- packages/tui/src/mini/stream-v2.subagent.ts | 2 +- packages/tui/src/mini/stream-v2.transport.ts | 4 +- packages/tui/src/routes/session/index.tsx | 6 +- .../tui/src/routes/session/permission.tsx | 2 +- packages/tui/test/cli/tui/data.test.tsx | 6 +- packages/tui/test/cli/tui/form.test.tsx | 2 +- .../tui/test/cli/tui/input-scope.test.tsx | 4 +- packages/tui/test/fixture/tui-client.ts | 2 +- .../tui/test/mini/blocker.responsive.test.tsx | 2 +- .../tui/test/mini/permission.shared.test.ts | 6 +- packages/tui/test/mini/runtime.test.ts | 2 +- .../tui/test/mini/stream-v2.transport.test.ts | 4 +- services/www/openapi.json | 1148 ++++++++--------- services/www/public/openapi.json | 1148 ++++++++--------- 57 files changed, 2639 insertions(+), 3035 deletions(-) diff --git a/V2_HTTP_API_AUDIT.md b/V2_HTTP_API_AUDIT.md index 58a0f68007a..cce010545ed 100644 --- a/V2_HTTP_API_AUDIT.md +++ b/V2_HTTP_API_AUDIT.md @@ -166,7 +166,7 @@ Review endpoints in document order. For each endpoint, select one disposition an | [x] 065 | `POST` | `/api/experimental/session/{sessionID}/wait` | `experimental.session.wait` | Experimental-only | Race-free idle barrier retained outside the stable API. | | [x] 066 | `POST` | `/api/session/{sessionID}/generate` | `session.generate` | Keep | Transient generation from session context retained. | | [x] 067 | `POST` | `/api/session/{sessionID}/interrupt` | `session.interrupt` | Change | Renamed `continue` to `resume` across public and internal interruption APIs. | -| [x] 068 | `PUT` | `/api/experimental/session/{sessionID}/environment` | `experimental.session.environment` | Experimental-only | Process-local environment replacement retained outside the stable API. | +| [x] 068 | `PUT` | `/api/session/{sessionID}/environment` | `session.environment` | Keep | Process-local environment replacement retained in the stable API. | | [x] 069 | `POST` | `/api/session/{sessionID}/view` | `session.view` | Change | Idle watermark now uses the standard epoch-millisecond timestamp schema. | ## Group 6: Session history and recovery @@ -196,23 +196,23 @@ Review endpoints in document order. For each endpoint, select one disposition an | Done | Method | Path | Operation ID | Decision | Notes | |---|---|---|---|---|---| | [x] 083 | `GET` | `/api/session/{sessionID}/inbox` | `session.inbox.list` | Change | Inbox timestamps now use the standard nested `time.created` shape. | -| [ ] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | | | -| [ ] 085 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/steer` | `session.inbox.steer` | | | -| [ ] 086 | `POST` | `/api/session/{sessionID}/inbox/{inboxID}/queue` | `session.inbox.queue` | | | -| [ ] 087 | `GET` | `/api/form/request` | `form.request.list` | | | -| [ ] 088 | `GET` | `/api/session/{sessionID}/form` | `session.form.list` | | | -| [ ] 089 | `POST` | `/api/session/{sessionID}/form` | `session.form.create` | | | -| [ ] 090 | `GET` | `/api/session/{sessionID}/form/{formID}` | `session.form.get` | | | -| [ ] 091 | `GET` | `/api/session/{sessionID}/form/{formID}/state` | `session.form.state` | | | -| [ ] 092 | `POST` | `/api/session/{sessionID}/form/{formID}/reply` | `session.form.reply` | | | -| [ ] 093 | `POST` | `/api/session/{sessionID}/form/{formID}/cancel` | `session.form.cancel` | | | -| [ ] 094 | `GET` | `/api/permission/request` | `permission.request.list` | | | -| [ ] 095 | `GET` | `/api/permission/saved` | `permission.saved.list` | | | -| [ ] 096 | `DELETE` | `/api/permission/saved/{id}` | `permission.saved.remove` | | | -| [ ] 097 | `POST` | `/api/session/{sessionID}/permission` | `session.permission.create` | | | -| [ ] 098 | `GET` | `/api/session/{sessionID}/permission` | `session.permission.list` | | | -| [ ] 099 | `GET` | `/api/session/{sessionID}/permission/{requestID}` | `session.permission.get` | | | -| [ ] 100 | `POST` | `/api/session/{sessionID}/permission/{requestID}/reply` | `session.permission.reply` | | | +| [x] 084 | `DELETE` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.cancel` | Change | Cancellation is idempotent and returns `204` when the session exists. | +| [x] 085 | `PATCH` | `/api/session/{sessionID}/inbox/{inboxID}` | `session.inbox.update` | Change | Consolidated delivery mutation with `delivery: "steer" | "queue"`. | +| [x] 086 | — | — | — | Remove | Replaced by `session.inbox.update`. | +| [x] 087 | `GET` | `/api/form` | `form.list` | Change | Removed redundant `request` path and operation namespace. | +| [x] 088 | `GET` | `/api/session/{sessionID}/form` | `session.form.list` | Keep | Pending session form list retained with temporary MCP sentinel compatibility. | +| [x] 089 | `POST` | `/api/session/{sessionID}/form` | `session.form.create` | Keep | External form creation and temporary MCP sentinel ownership retained. | +| [x] 090 | `GET` | `/api/session/{sessionID}/form/{formID}` | `session.form.get` | Change | Form definition and lifecycle state are now returned together. | +| [x] 091 | — | — | — | Remove | State is included by `session.form.get`. | +| [x] 092 | `POST` | `/api/session/{sessionID}/form/{formID}/reply` | `session.form.reply` | Keep | One-shot validated form reply retained. | +| [x] 093 | `DELETE` | `/api/session/{sessionID}/form/{formID}` | `session.form.cancel` | Change | Form cancellation now deletes the pending form resource. | +| [x] 094 | `GET` | `/api/permission/request` | `permission.request.list` | Keep | Pending-request namespace retained alongside saved permissions. | +| [x] 095 | `GET` | `/api/permission/saved` | `permission.saved.list` | Change | Added persisted creation and update timestamps under `time`. | +| [x] 096 | `DELETE` | `/api/permission/saved/{id}` | `permission.saved.remove` | Keep | Idempotent saved-permission deletion retained. | +| [x] 097 | `POST` | `/api/session/{sessionID}/permission` | `session.permission.create` | Keep | Non-blocking permission evaluation and pending-request creation retained. | +| [x] 098 | `GET` | `/api/session/{sessionID}/permission` | `session.permission.list` | Keep | Pending session permission list retained. | +| [x] 099 | `GET` | `/api/session/{sessionID}/permission/{requestID}` | `session.permission.get` | Keep | Specific pending permission read with ownership validation retained. | +| [x] 100 | `POST` | `/api/session/{sessionID}/permission/{requestID}/reply` | `session.permission.reply` | Change | Renamed request field from `reply` to `decision`. | | [ ] 101 | `PUT` | `/api/session/{sessionID}/permission/rules` | `session.permission.rules` | | | ## Group 8: Filesystem, worktrees, and VCS diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index 316e1d3edf2..982029fbf56 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -61,7 +61,7 @@ test("session settings use the remote server context", async ({ page }) => { directory: undefined, sessionID: sessionA.id, permissionID: "permission-pending-a", - body: { reply: "once" }, + body: { decision: "once" }, }, ]) @@ -153,7 +153,7 @@ test("auto-accept responds for an unfocused server session", async ({ page }) => directory: undefined, sessionID: sessionA.id, permissionID: "permission-background-a", - body: { reply: "once" }, + body: { decision: "once" }, }, ]) @@ -180,14 +180,14 @@ test("auto-accept responds for an unfocused server session", async ({ page }) => directory: undefined, sessionID: sessionA.id, permissionID: "permission-background-a", - body: { reply: "once" }, + body: { decision: "once" }, }, { origin: serverA, directory: undefined, sessionID: childSessionA.id, permissionID: "permission-background-a-child", - body: { reply: "once" }, + body: { decision: "once" }, }, ]) }) @@ -243,7 +243,7 @@ test("auto-accept sweeps again after a reconnect", async ({ page }) => { directory: undefined, sessionID: sessionA.id, permissionID: "permission-offline-a", - body: { reply: "once" }, + body: { decision: "once" }, }, ]) // The reconnect sweep must resync active sessions instead of trusting @@ -280,7 +280,7 @@ test("auto-accept approves a request discovered by opening a session", async ({ directory: undefined, sessionID: sessionA.id, permissionID: "permission-synced-a", - body: { reply: "once" }, + body: { decision: "once" }, }, ]) }) diff --git a/packages/app/e2e/regression/session-queue.spec.ts b/packages/app/e2e/regression/session-queue.spec.ts index 11ccedac3d9..b70e1f76ea6 100644 --- a/packages/app/e2e/regression/session-queue.spec.ts +++ b/packages/app/e2e/regression/session-queue.spec.ts @@ -74,7 +74,7 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) { item: { type: "user", payload: row.payload, delivery: row.delivery }, }) }, - onInboxChange: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" }) => { + onInboxChange: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" | "queue" }) => { changes.push({ inboxID: input.inboxID, action: input.action }) log.push(`${input.action}:${input.inboxID}`) const index = rows.findIndex((row) => row.id === input.inboxID) @@ -85,11 +85,11 @@ function createQueueMock(seed: string[], messages: SessionMessageInfo[] = []) { emit("session.inbox.cancelled", { sessionID: input.sessionID, inboxID: input.inboxID }) return } - row.delivery = "steer" + row.delivery = input.action emit("session.inbox.delivery.changed", { sessionID: input.sessionID, inboxID: input.inboxID, - delivery: "steer", + delivery: input.action, }) }, } diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index 04e634dc48b..dc82f35a14c 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -48,7 +48,10 @@ test("shows a pending question dock", async ({ page }) => { const rejectRequests: string[] = [] page.on("request", (request) => { if (request.method() !== "POST") return - if (new URL(request.url()).pathname === `/api/session/${sessionID}/form/frm_question_request/cancel`) + if ( + request.method() === "DELETE" && + new URL(request.url()).pathname === `/api/session/${sessionID}/form/frm_question_request` + ) rejectRequests.push(request.url()) }) @@ -107,7 +110,7 @@ test("shows a pending permission dock", async ({ page }) => { await permission.getByRole("button", { name: "Allow once" }).click() const request = await reply expect(new URL(request.url()).pathname).toBe(`/api/session/${sessionID}/permission/permission-request/reply`) - expect(request.postDataJSON()).toEqual({ reply: "once" }) + expect(request.postDataJSON()).toEqual({ decision: "once" }) }) test("restores the draft caret before typing after a request dock closes", async ({ page }) => { diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index f9bc5c8e16f..3a352c0927a 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -587,7 +587,7 @@ async function mockServer(page: Page) { return json(route, { location: { directory: sessionA.directory }, data: [] }) if (url.pathname === "/api/model/default") return json(route, { location: { directory: sessionA.directory }, data: null }) - if (url.pathname === "/api/permission/request" || url.pathname === "/api/form/request") + if (url.pathname === "/api/permission/request" || url.pathname === "/api/form") return json(route, { location: { directory: sessionA.directory }, data: [] }) if (url.pathname === "/api/mcp") return json(route, { location: { directory: sessionA.directory }, data: [] }) if (url.pathname === "/api/mcp/resource") diff --git a/packages/app/e2e/utils/mock-api.ts b/packages/app/e2e/utils/mock-api.ts index e69bc3544e1..dd72eafd20d 100644 --- a/packages/app/e2e/utils/mock-api.ts +++ b/packages/app/e2e/utils/mock-api.ts @@ -112,7 +112,7 @@ const Group = HttpApiGroup.make("mock") ) .add(HttpApiEndpoint.get("location", "/api/location", { success: Json })) .add(HttpApiEndpoint.get("permissionRequests", "/api/permission/request", { success: Json })) - .add(HttpApiEndpoint.get("formRequests", "/api/form/request", { success: Json })) + .add(HttpApiEndpoint.get("formRequests", "/api/form", { success: Json })) .add(HttpApiEndpoint.get("vcs", "/api/vcs", { success: Json })) .add(HttpApiEndpoint.get("vcsStatus", "/api/vcs/status", { success: Json })) .add(HttpApiEndpoint.get("vcsBranches", "/api/vcs/branches", { success: Json })) @@ -173,7 +173,7 @@ const Group = HttpApiGroup.make("mock") }), ) .add( - HttpApiEndpoint.post("sessionFormCancel", "/api/session/:sessionID/form/:formID/cancel", { + HttpApiEndpoint.delete("sessionFormCancel", "/api/session/:sessionID/form/:formID", { params: { ...SessionParams, formID: Schema.String }, success: NoContent, }), @@ -218,8 +218,9 @@ const Group = HttpApiGroup.make("mock") }), ) .add( - HttpApiEndpoint.post("sessionInboxSteer", "/api/session/:sessionID/inbox/:inboxID/steer", { + HttpApiEndpoint.patch("sessionInboxUpdate", "/api/session/:sessionID/inbox/:inboxID", { params: { ...SessionParams, inboxID: Schema.String }, + payload: Schema.Struct({ delivery: Schema.Literals(["steer", "queue"]) }), success: NoContent, }), ) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index ef4ebc1e910..6e5983f8bde 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -42,7 +42,7 @@ export interface MockServerConfig { sessionStatus?: Record | (() => Record) inbox?: unknown[] | (() => unknown[]) onPrompt?: (input: { sessionID: string; body: Record }) => void - onInboxChange?: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" }) => void + onInboxChange?: (input: { sessionID: string; inboxID: string; action: "cancel" | "steer" | "queue" }) => void } type MockStreamWindow = Window & { @@ -454,9 +454,13 @@ function mockHandlers(config: MockServerConfig, state: { cursors: Map config.onInboxChange?.({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID, action: "cancel" }), ).pipe(Effect.andThen(noContent)), - sessionInboxSteer: (ctx) => + sessionInboxUpdate: (ctx) => Effect.sync(() => - config.onInboxChange?.({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID, action: "steer" }), + config.onInboxChange?.({ + sessionID: ctx.params.sessionID, + inboxID: ctx.params.inboxID, + action: ctx.payload.delivery, + }), ).pipe(Effect.andThen(noContent)), sessionSwitchAgent: () => noContent, sessionSwitchModel: () => noContent, diff --git a/packages/app/src/session/composer/queue.ts b/packages/app/src/session/composer/queue.ts index 6d8310a86d2..b7fb22c5afc 100644 --- a/packages/app/src/session/composer/queue.ts +++ b/packages/app/src/session/composer/queue.ts @@ -130,7 +130,9 @@ export function createSessionQueue(input: { } const steer = (id: string) => { if (state.editing?.id === id) cancelEdit() - return server.api.session.inbox.steer({ sessionID: input.sessionID, inboxID: id }).catch(() => notify()) + return server.api.session.inbox + .update({ sessionID: input.sessionID, inboxID: id, delivery: "steer" }) + .catch(() => notify()) } const remove = (id: string) => { if (state.editing?.id === id) cancelEdit() diff --git a/packages/app/src/session/requests/auto-approve.ts b/packages/app/src/session/requests/auto-approve.ts index 88a725e67ad..b0a9e306f81 100644 --- a/packages/app/src/session/requests/auto-approve.ts +++ b/packages/app/src/session/requests/auto-approve.ts @@ -115,7 +115,7 @@ export function createPermissionAutoApprover(input: { sdk: ServerSDK; data: Data if (state.disposed || !enabled() || state.responded.has(permission.id)) return remember(permission.id) input.sdk.api.permission - .reply({ sessionID: permission.sessionID, requestID: permission.id, reply: "once" }) + .reply({ sessionID: permission.sessionID, requestID: permission.id, decision: "once" }) .catch(() => { // A reply failure leaves the request pending but invisible (the UI // hides prompts while auto-approve is on), so retry a bounded number diff --git a/packages/app/src/session/requests/model.ts b/packages/app/src/session/requests/model.ts index 2f5c4cebda8..db640fe80f8 100644 --- a/packages/app/src/session/requests/model.ts +++ b/packages/app/src/session/requests/model.ts @@ -112,7 +112,7 @@ export function createSessionRequestModel() { setStore("responding", perm.id) serverSDK.api.permission - .reply({ sessionID: perm.sessionID, requestID: perm.id, reply: response }) + .reply({ sessionID: perm.sessionID, requestID: perm.id, decision: response }) .catch((err: unknown) => { const description = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description }) diff --git a/packages/app/src/session/requests/session-question-dock.tsx b/packages/app/src/session/requests/session-question-dock.tsx index 24b76e28c7c..5b97d8f5f54 100644 --- a/packages/app/src/session/requests/session-question-dock.tsx +++ b/packages/app/src/session/requests/session-question-dock.tsx @@ -237,7 +237,7 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () => const replyMutation = useMutation(() => ({ mutationFn: (answer: FormAnswer) => - serverSDK.api.form.reply({ sessionID: props.request.sessionID, formID: props.request.id, answer }), + serverSDK.api.session.form.reply({ sessionID: props.request.sessionID, formID: props.request.id, answer }), onMutate: () => { props.onSubmit() }, @@ -249,7 +249,7 @@ export const SessionQuestionDock: Component<{ request: FormInfo; onSubmit: () => })) const rejectMutation = useMutation(() => ({ - mutationFn: () => serverSDK.api.form.cancel({ sessionID: props.request.sessionID, formID: props.request.id }), + mutationFn: () => serverSDK.api.session.form.cancel({ sessionID: props.request.sessionID, formID: props.request.id }), onMutate: () => { props.onSubmit() }, diff --git a/packages/app/src/session/requests/websearch.test.ts b/packages/app/src/session/requests/websearch.test.ts index 4385289919b..411424661e2 100644 --- a/packages/app/src/session/requests/websearch.test.ts +++ b/packages/app/src/session/requests/websearch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { FormAnswer, FormCreated, FormReplyInput, OpenCodeEvent } from "@opencode/client/promise" +import type { FormAnswer, FormCreated, SessionFormReplyInput, OpenCodeEvent } from "@opencode/client/promise" import { replyWebSearch } from "./websearch" const consent: FormCreated["data"]["form"] = { @@ -28,7 +28,7 @@ const provider: FormCreated["data"]["form"] = { function fixture() { const listeners = new Set<(event: OpenCodeEvent) => void>() - const replies: FormReplyInput[] = [] + const replies: SessionFormReplyInput[] = [] const abort = new AbortController() const emit = (event: OpenCodeEvent) => listeners.forEach((listener) => listener(event)) return { @@ -45,7 +45,7 @@ function fixture() { } }, }, - reply: async (input: FormReplyInput) => { + reply: async (input: SessionFormReplyInput) => { replies.push(input) }, create: (form = provider) => emit({ id: "evt_create", created: 0, type: "form.created", data: { form } }), diff --git a/packages/app/src/session/requests/websearch.ts b/packages/app/src/session/requests/websearch.ts index 78b437487f0..38b8c30e4c5 100644 --- a/packages/app/src/session/requests/websearch.ts +++ b/packages/app/src/session/requests/websearch.ts @@ -1,4 +1,4 @@ -import type { FormInfo, FormOption, FormReplyInput, FormStringField } from "@opencode/client/promise" +import type { FormInfo, FormOption, SessionFormReplyInput, FormStringField } from "@opencode/client/promise" import { createEffect, createMemo, createResource, on, onCleanup } from "solid-js" import { createStore } from "solid-js/store" import type { OpenCodeEventStream } from "@/runtime/server/client" @@ -14,7 +14,7 @@ export function createWebSearchRequest(input: { connected: () => boolean request: () => FormInfo | undefined providers: (sessionID: string) => Promise - reply: (input: FormReplyInput) => Promise + reply: (input: SessionFormReplyInput) => Promise events: Pick }) { const [store, setStore] = createStore({ @@ -93,7 +93,7 @@ export async function replyWebSearch(input: { form: FormInfo selection: string | false signal: AbortSignal - reply: (input: FormReplyInput) => Promise + reply: (input: SessionFormReplyInput) => Promise events: Pick }) { if (input.signal.aborted) return diff --git a/packages/cli/src/acp/event.ts b/packages/cli/src/acp/event.ts index d237dc9597a..ef6bea22e27 100644 --- a/packages/cli/src/acp/event.ts +++ b/packages/cli/src/acp/event.ts @@ -165,7 +165,7 @@ export async function streamTurn(input: { continue } if (event.type === "form.created" && (event.data.form.sessionID === input.sessionID || child)) { - await input.client.form + await input.client.session.form .cancel({ sessionID: event.data.form.sessionID, formID: event.data.form.id }) .catch(() => input.client.session.interrupt({ sessionID: event.data.form.sessionID }).catch(() => {})) continue diff --git a/packages/cli/src/acp/permission.ts b/packages/cli/src/acp/permission.ts index 0ae41f04130..826afd867fa 100644 --- a/packages/cli/src/acp/permission.ts +++ b/packages/cli/src/acp/permission.ts @@ -55,7 +55,7 @@ export async function replyPermission(input: { await input.client.permission.reply({ sessionID: input.sessionID, requestID: input.event.data.id, - reply, + decision: reply, }) } diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index c4a89e483d9..df9bc81ad89 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -145,7 +145,7 @@ export async function runNonInteractivePrompt(input: Input) { .reply({ sessionID: input.sessionID, requestID: request.id, - reply: input.auto ? "once" : "reject", + decision: input.auto ? "once" : "reject", }) .catch(() => {}) if (!input.auto) { @@ -155,7 +155,7 @@ export async function runNonInteractivePrompt(input: Input) { const cancelForm = async (request: Pick) => { try { - await input.client.form.cancel( + await input.client.session.form.cancel( { sessionID: request.sessionID, formID: request.id }, ...formRequestOptions(request.sessionID === GLOBAL_FORM_SESSION_ID ? input.location : undefined), ) @@ -695,10 +695,10 @@ export async function runNonInteractivePrompt(input: Input) { const [permissions, forms, globals] = await Promise.all([ input.client.permission.list({ sessionID: input.sessionID }).catch(() => undefined), - input.client.form.list({ sessionID: input.sessionID }).catch(() => undefined), + input.client.session.form.list({ sessionID: input.sessionID }).catch(() => undefined), input.attached ? Promise.resolve(undefined) - : input.client.form.request + : input.client.form .list({ location: { directory: input.location.directory }, }) diff --git a/packages/cli/test/acp/event-behavior.test.ts b/packages/cli/test/acp/event-behavior.test.ts index 5c3b56b4403..4a23b5bde6b 100644 --- a/packages/cli/test/acp/event-behavior.test.ts +++ b/packages/cli/test/acp/event-behavior.test.ts @@ -748,7 +748,9 @@ describe("acp event behavior", () => { expect(response.stopReason).toBe("end_turn") expect( - fixture.requests.some((request) => request.path === "/api/session/ses_form/form/frm_question/cancel"), + fixture.requests.some( + (request) => request.method === "DELETE" && request.path === "/api/session/ses_form/form/frm_question", + ), ).toBe(true) } finally { await fixture.stop() diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index 867d317007b..bc6a39dcf63 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -240,17 +240,17 @@ async function run(input: { })() spyOn(sdk.event, "subscribe").mockImplementation(() => stream) spyOn(sdk.permission, "list").mockImplementation(() => ok([]) as never) - spyOn(sdk.form, "list").mockImplementation( + spyOn(sdk.session.form, "list").mockImplementation( (request) => ok(input.pendingForms?.filter((item) => item.sessionID === request.sessionID) ?? []) as never, ) - spyOn(sdk.form.request, "list").mockImplementation( + spyOn(sdk.form, "list").mockImplementation( () => ok({ location: { ...location, project: { id: "proj_1", directory: location.directory } }, data: input.pendingForms?.filter((item) => item.sessionID === "global") ?? [], }) as never, ) - spyOn(sdk.form, "cancel").mockImplementation((request) => (input.cancel?.(request) ?? ok(undefined)) as never) + spyOn(sdk.session.form, "cancel").mockImplementation((request) => (input.cancel?.(request) ?? ok(undefined)) as never) let promptID = "msg_prompt" spyOn(sdk.session, "wait").mockImplementation(() => input.wait?.() ?? wait.promise) spyOn(sdk.message, "list").mockImplementation(() => @@ -426,10 +426,10 @@ describe("runNonInteractivePrompt", () => { "x-opencode-directory": "%2Fwork%20tree", }, } - expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, globalOptions) - expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" }) - expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" }, globalOptions) - expect(sdk.form.request.list).toHaveBeenCalledWith({ + expect(sdk.session.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, globalOptions) + expect(sdk.session.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" }) + expect(sdk.session.form.cancel).toHaveBeenCalledWith({ sessionID: "global", formID: "frm_pending_global" }, globalOptions) + expect(sdk.form.list).toHaveBeenCalledWith({ location: { directory: "/work tree" }, }) }) @@ -440,10 +440,10 @@ describe("runNonInteractivePrompt", () => { pendingForms: [form("frm_pending", "ses_1"), form("frm_pending_global", "global")], turn: (messageID) => [formCreated(form("frm_live", "global")), prompted(messageID), settled()], }) - expect(sdk.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" }) - expect(sdk.form.request.list).not.toHaveBeenCalled() - expect(sdk.form.cancel).not.toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, expect.anything()) - expect(sdk.form.cancel).not.toHaveBeenCalledWith( + expect(sdk.session.form.cancel).toHaveBeenCalledWith({ sessionID: "ses_1", formID: "frm_pending" }) + expect(sdk.form.list).not.toHaveBeenCalled() + expect(sdk.session.form.cancel).not.toHaveBeenCalledWith({ sessionID: "global", formID: "frm_live" }, expect.anything()) + expect(sdk.session.form.cancel).not.toHaveBeenCalledWith( { sessionID: "global", formID: "frm_pending_global" }, expect.anything(), ) diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 1226d343ec1..3eac1e5a1c1 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -23,8 +23,8 @@ import type { Event } from "@opencode/schema/event" import type { EventLog } from "@opencode/schema/event-log" import type { Shell } from "@opencode/schema/shell" import type { Provider } from "@opencode/schema/provider" -import type { Integration } from "@opencode/schema/integration" import type { Form } from "@opencode/schema/form" +import type { Integration } from "@opencode/schema/integration" import type { Mcp } from "@opencode/schema/mcp" import type { Credential } from "@opencode/schema/credential" import type { PermissionSaved } from "@opencode/schema/permission-saved" @@ -360,17 +360,15 @@ export type SessionInboxCancelOperation = ( input: SessionInboxCancelInput, ) => Effect.Effect -export type SessionInboxSteerInput = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID } -export type SessionInboxSteerOutput = void -export type SessionInboxSteerOperation = ( - input: SessionInboxSteerInput, -) => Effect.Effect - -export type SessionInboxQueueInput = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID } -export type SessionInboxQueueOutput = void -export type SessionInboxQueueOperation = ( - input: SessionInboxQueueInput, -) => Effect.Effect +export type SessionInboxUpdateInput = { + readonly sessionID: Session.ID + readonly inboxID: SessionMessage.ID + readonly delivery: SessionInbox.Delivery +} +export type SessionInboxUpdateOutput = void +export type SessionInboxUpdateOperation = ( + input: SessionInboxUpdateInput, +) => Effect.Effect export type SessionInstructionsEntryListInput = { readonly sessionID: Session.ID } export type SessionInstructionsEntryListOutput = ReadonlyArray @@ -1325,6 +1323,44 @@ export type SessionMessageGetOperation = ( input: SessionMessageGetInput, ) => Effect.Effect +export type SessionFormListInput = { readonly sessionID: string } +export type SessionFormListOutput = ReadonlyArray +export type SessionFormListOperation = ( + input: SessionFormListInput, +) => Effect.Effect + +export type SessionFormCreateInput = { + readonly sessionID: string + readonly id?: Form.ID | undefined + readonly title: string + readonly metadata?: Form.Metadata | undefined + readonly fields: Form.Fields +} +export type SessionFormCreateOutput = Form.Info +export type SessionFormCreateOperation = ( + input: SessionFormCreateInput, +) => Effect.Effect + +export type SessionFormGetInput = { readonly sessionID: string; readonly formID: Form.ID } +export type SessionFormGetOutput = Form.Detail +export type SessionFormGetOperation = (input: SessionFormGetInput) => Effect.Effect + +export type SessionFormReplyInput = { + readonly sessionID: string + readonly formID: Form.ID + readonly answer: Form.Answer +} +export type SessionFormReplyOutput = void +export type SessionFormReplyOperation = ( + input: SessionFormReplyInput, +) => Effect.Effect + +export type SessionFormCancelInput = { readonly sessionID: string; readonly formID: Form.ID } +export type SessionFormCancelOutput = void +export type SessionFormCancelOperation = ( + input: SessionFormCancelInput, +) => Effect.Effect + export type SessionEnvironmentInput = { readonly sessionID: Session.ID readonly variables: { readonly [x: string]: string } @@ -1369,8 +1405,7 @@ export interface SessionApi { readonly inbox: { readonly list: SessionInboxListOperation readonly cancel: SessionInboxCancelOperation - readonly steer: SessionInboxSteerOperation - readonly queue: SessionInboxQueueOperation + readonly update: SessionInboxUpdateOperation } readonly instructions: { readonly entry: { @@ -1384,6 +1419,13 @@ export interface SessionApi { readonly interrupt: SessionInterruptOperation readonly background: SessionBackgroundOperation readonly message: { readonly get: SessionMessageGetOperation } + readonly form: { + readonly list: SessionFormListOperation + readonly create: SessionFormCreateOperation + readonly get: SessionFormGetOperation + readonly reply: SessionFormReplyOperation + readonly cancel: SessionFormCancelOperation + } readonly environment: SessionEnvironmentOperation readonly view: SessionViewOperation } @@ -1684,50 +1726,12 @@ export interface ProjectApi { readonly update: ProjectUpdateOperation } -export type FormRequestListInput = { readonly location?: { readonly directory?: string | undefined } | undefined } -export type FormRequestListOutput = { readonly location: Location.PublicRef; readonly data: ReadonlyArray } -export type FormRequestListOperation = ( - input?: FormRequestListInput, -) => Effect.Effect - -export type FormListInput = { readonly sessionID: string } -export type FormListOutput = ReadonlyArray -export type FormListOperation = (input: FormListInput) => Effect.Effect - -export type FormCreateInput = { - readonly sessionID: string - readonly id?: Form.ID | undefined - readonly title: string - readonly metadata?: Form.Metadata | undefined - readonly fields: Form.Fields -} -export type FormCreateOutput = Form.Info -export type FormCreateOperation = (input: FormCreateInput) => Effect.Effect - -export type FormGetInput = { readonly sessionID: string; readonly formID: Form.ID } -export type FormGetOutput = Form.Info -export type FormGetOperation = (input: FormGetInput) => Effect.Effect - -export type FormStateInput = { readonly sessionID: string; readonly formID: Form.ID } -export type FormStateOutput = Form.State -export type FormStateOperation = (input: FormStateInput) => Effect.Effect - -export type FormReplyInput = { readonly sessionID: string; readonly formID: Form.ID; readonly answer: Form.Answer } -export type FormReplyOutput = void -export type FormReplyOperation = (input: FormReplyInput) => Effect.Effect - -export type FormCancelInput = { readonly sessionID: string; readonly formID: Form.ID } -export type FormCancelOutput = void -export type FormCancelOperation = (input: FormCancelInput) => Effect.Effect +export type FormListInput = { readonly location?: { readonly directory?: string | undefined } | undefined } +export type FormListOutput = { readonly location: Location.PublicRef; readonly data: ReadonlyArray } +export type FormListOperation = (input?: FormListInput) => Effect.Effect export interface FormApi { - readonly request: { readonly list: FormRequestListOperation } readonly list: FormListOperation - readonly create: FormCreateOperation - readonly get: FormGetOperation - readonly state: FormStateOperation - readonly reply: FormReplyOperation - readonly cancel: FormCancelOperation } export type PermissionRequestListInput = { readonly location?: { readonly directory?: string | undefined } | undefined } @@ -1777,7 +1781,7 @@ export type PermissionGetOperation = (input: PermissionGetInput) => E export type PermissionReplyInput = { readonly sessionID: Session.ID readonly requestID: Permission.ID - readonly reply: Permission.Reply + readonly decision: Permission.Reply readonly message?: string | undefined } export type PermissionReplyOutput = void diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index cc00d84a7cd..fe838b4a723 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -71,10 +71,8 @@ import type { SessionInboxListOutput, SessionInboxCancelInput, SessionInboxCancelOutput, - SessionInboxSteerInput, - SessionInboxSteerOutput, - SessionInboxQueueInput, - SessionInboxQueueOutput, + SessionInboxUpdateInput, + SessionInboxUpdateOutput, SessionInstructionsEntryListInput, SessionInstructionsEntryListOutput, SessionInstructionsEntryPutInput, @@ -91,6 +89,16 @@ import type { SessionBackgroundOutput, SessionMessageGetInput, SessionMessageGetOutput, + SessionFormListInput, + SessionFormListOutput, + SessionFormCreateInput, + SessionFormCreateOutput, + SessionFormGetInput, + SessionFormGetOutput, + SessionFormReplyInput, + SessionFormReplyOutput, + SessionFormCancelInput, + SessionFormCancelOutput, SessionEnvironmentInput, SessionEnvironmentOutput, SessionViewInput, @@ -150,20 +158,8 @@ import type { ProjectListOutput, ProjectUpdateInput, ProjectUpdateOutput, - FormRequestListInput, - FormRequestListOutput, FormListInput, FormListOutput, - FormCreateInput, - FormCreateOutput, - FormGetInput, - FormGetOutput, - FormStateInput, - FormStateOutput, - FormReplyInput, - FormReplyOutput, - FormCancelInput, - FormCancelOutput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, @@ -605,18 +601,12 @@ const EndpointSessionInboxCancel = (raw: RawClient["server.session"]) => (input: ), ) -const EndpointSessionInboxSteer = (raw: RawClient["server.session"]) => (input: SessionInboxSteerInput) => - preserveEffect()( - raw["session.inbox.steer"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe( - Effect.mapError(mapClientError), - ), - ) - -const EndpointSessionInboxQueue = (raw: RawClient["server.session"]) => (input: SessionInboxQueueInput) => - preserveEffect()( - raw["session.inbox.queue"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe( - Effect.mapError(mapClientError), - ), +const EndpointSessionInboxUpdate = (raw: RawClient["server.session"]) => (input: SessionInboxUpdateInput) => + preserveEffect()( + raw["session.inbox.update"]({ + params: { sessionID: input["sessionID"], inboxID: input["inboxID"] }, + payload: { delivery: input["delivery"] }, + }).pipe(Effect.mapError(mapClientError)), ) const EndpointSessionInstructionsEntryList = @@ -686,6 +676,48 @@ const EndpointSessionMessageGet = (raw: RawClient["server.session"]) => (input: ), ) +const EndpointSessionFormList = (raw: RawClient["server.session"]) => (input: SessionFormListInput) => + preserveEffect()( + raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointSessionFormCreate = (raw: RawClient["server.session"]) => (input: SessionFormCreateInput) => + preserveEffect()( + raw["session.form.create"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointSessionFormGet = (raw: RawClient["server.session"]) => (input: SessionFormGetInput) => + preserveEffect()( + raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointSessionFormReply = (raw: RawClient["server.session"]) => (input: SessionFormReplyInput) => + preserveEffect()( + raw["session.form.reply"]({ + params: { sessionID: input["sessionID"], formID: input["formID"] }, + payload: { answer: input["answer"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const EndpointSessionFormCancel = (raw: RawClient["server.session"]) => (input: SessionFormCancelInput) => + preserveEffect()( + raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + const EndpointSessionEnvironment = (raw: RawClient["server.session"]) => (input: SessionEnvironmentInput) => preserveEffect()( raw["session.environment"]({ @@ -732,8 +764,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({ inbox: { list: EndpointSessionInboxList(raw), cancel: EndpointSessionInboxCancel(raw), - steer: EndpointSessionInboxSteer(raw), - queue: EndpointSessionInboxQueue(raw), + update: EndpointSessionInboxUpdate(raw), }, instructions: { entry: { @@ -747,6 +778,13 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({ interrupt: EndpointSessionInterrupt(raw), background: EndpointSessionBackground(raw), message: { get: EndpointSessionMessageGet(raw) }, + form: { + list: EndpointSessionFormList(raw), + create: EndpointSessionFormCreate(raw), + get: EndpointSessionFormGet(raw), + reply: EndpointSessionFormReply(raw), + cancel: EndpointSessionFormCancel(raw), + }, environment: EndpointSessionEnvironment(raw), view: EndpointSessionView(raw), }) @@ -1006,70 +1044,12 @@ const adaptGroupProject = (raw: RawClient["server.project"]) => ({ update: EndpointProjectUpdate(raw), }) -const EndpointFormRequestList = (raw: RawClient["server.form"]) => (input?: FormRequestListInput) => - preserveEffect()( - raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), - ) - -const EndpointFormList = (raw: RawClient["server.form"]) => (input: FormListInput) => +const EndpointFormList = (raw: RawClient["server.form"]) => (input?: FormListInput) => preserveEffect()( - raw["session.form.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), + raw["form.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const EndpointFormCreate = (raw: RawClient["server.form"]) => (input: FormCreateInput) => - preserveEffect()( - raw["session.form.create"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - -const EndpointFormGet = (raw: RawClient["server.form"]) => (input: FormGetInput) => - preserveEffect()( - raw["session.form.get"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - -const EndpointFormState = (raw: RawClient["server.form"]) => (input: FormStateInput) => - preserveEffect()( - raw["session.form.state"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - -const EndpointFormReply = (raw: RawClient["server.form"]) => (input: FormReplyInput) => - preserveEffect()( - raw["session.form.reply"]({ - params: { sessionID: input["sessionID"], formID: input["formID"] }, - payload: { answer: input["answer"] }, - }).pipe(Effect.mapError(mapClientError)), - ) - -const EndpointFormCancel = (raw: RawClient["server.form"]) => (input: FormCancelInput) => - preserveEffect()( - raw["session.form.cancel"]({ params: { sessionID: input["sessionID"], formID: input["formID"] } }).pipe( - Effect.mapError(mapClientError), - ), - ) - -const adaptGroupForm = (raw: RawClient["server.form"]) => ({ - request: { list: EndpointFormRequestList(raw) }, - list: EndpointFormList(raw), - create: EndpointFormCreate(raw), - get: EndpointFormGet(raw), - state: EndpointFormState(raw), - reply: EndpointFormReply(raw), - cancel: EndpointFormCancel(raw), -}) +const adaptGroupForm = (raw: RawClient["server.form"]) => ({ list: EndpointFormList(raw) }) const EndpointPermissionRequestList = (raw: RawClient["server.permission"]) => (input?: PermissionRequestListInput) => preserveEffect()( @@ -1128,7 +1108,7 @@ const EndpointPermissionReply = (raw: RawClient["server.permission"]) => (input: preserveEffect()( raw["session.permission.reply"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] }, - payload: { reply: input["reply"], message: input["message"] }, + payload: { decision: input["decision"], message: input["message"] }, }).pipe(Effect.mapError(mapClientError)), ) diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 11d669aa0a2..8069434a6fa 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -65,10 +65,8 @@ import type { SessionInboxListOutput, SessionInboxCancelInput, SessionInboxCancelOutput, - SessionInboxSteerInput, - SessionInboxSteerOutput, - SessionInboxQueueInput, - SessionInboxQueueOutput, + SessionInboxUpdateInput, + SessionInboxUpdateOutput, SessionInstructionsEntryListInput, SessionInstructionsEntryListOutput, SessionInstructionsEntryPutInput, @@ -85,6 +83,16 @@ import type { SessionBackgroundOutput, SessionMessageGetInput, SessionMessageGetOutput, + SessionFormListInput, + SessionFormListOutput, + SessionFormCreateInput, + SessionFormCreateOutput, + SessionFormGetInput, + SessionFormGetOutput, + SessionFormReplyInput, + SessionFormReplyOutput, + SessionFormCancelInput, + SessionFormCancelOutput, SessionEnvironmentInput, SessionEnvironmentOutput, SessionViewInput, @@ -144,20 +152,8 @@ import type { ProjectListOutput, ProjectUpdateInput, ProjectUpdateOutput, - FormRequestListInput, - FormRequestListOutput, FormListInput, FormListOutput, - FormCreateInput, - FormCreateOutput, - FormGetInput, - FormGetOutput, - FormStateInput, - FormStateOutput, - FormReplyInput, - FormReplyOutput, - FormCancelInput, - FormCancelOutput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, @@ -851,27 +847,17 @@ export function make(options: ClientOptions) { method: "DELETE", path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}`, successStatus: 204, - declaredStatuses: [400, 401, 404, 409], + declaredStatuses: [400, 401, 404], empty: true, }, requestOptions, ), - steer: (input: SessionInboxSteerInput, requestOptions?: RequestOptions) => - request( + update: (input: SessionInboxUpdateInput, requestOptions?: RequestOptions) => + request( { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/steer`, - successStatus: 204, - declaredStatuses: [400, 401, 404, 409], - empty: true, - }, - requestOptions, - ), - queue: (input: SessionInboxQueueInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/queue`, + method: "PATCH", + path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}`, + body: { delivery: input["delivery"] }, successStatus: 204, declaredStatuses: [400, 401, 404, 409], empty: true, @@ -977,11 +963,70 @@ export function make(options: ClientOptions) { requestOptions, ).then((value) => value.data), }, + form: { + list: (input: SessionFormListInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionFormListOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, + successStatus: 200, + declaredStatuses: [400, 401, 404], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + create: (input: SessionFormCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionFormCreateOutput }>( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, + body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 409], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + get: (input: SessionFormGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: SessionFormGetOutput }>( + { + method: "GET", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`, + successStatus: 200, + declaredStatuses: [400, 401, 404], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + reply: (input: SessionFormReplyInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`, + body: { answer: input["answer"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404, 409], + empty: true, + }, + requestOptions, + ), + cancel: (input: SessionFormCancelInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`, + successStatus: 204, + declaredStatuses: [400, 401, 404, 409], + empty: true, + }, + requestOptions, + ), + }, environment: (input: SessionEnvironmentInput, requestOptions?: RequestOptions) => request( { method: "PUT", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/environment`, + path: `/api/session/${encodeURIComponent(input.sessionID)}/environment`, body: { variables: input["variables"] }, successStatus: 204, declaredStatuses: [400, 401, 404], @@ -1367,87 +1412,17 @@ export function make(options: ClientOptions) { ), }, form: { - request: { - list: (input?: FormRequestListInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/form/request`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - }, - list: (input: FormListInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormListOutput }>( + list: (input?: FormListInput, requestOptions?: RequestOptions) => + request( { method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, + path: `/api/form`, + query: { location: input?.["location"] }, successStatus: 200, - declaredStatuses: [400, 401, 404], + declaredStatuses: [400, 401], empty: false, }, requestOptions, - ).then((value) => value.data), - create: (input: FormCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormCreateOutput }>( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form`, - body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 409], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - get: (input: FormGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormGetOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`, - successStatus: 200, - declaredStatuses: [400, 401, 404], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - state: (input: FormStateInput, requestOptions?: RequestOptions) => - request<{ readonly data: FormStateOutput }>( - { - method: "GET", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/state`, - successStatus: 200, - declaredStatuses: [400, 401, 404], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - reply: (input: FormReplyInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`, - body: { answer: input["answer"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404, 409], - empty: true, - }, - requestOptions, - ), - cancel: (input: FormCancelInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/cancel`, - successStatus: 204, - declaredStatuses: [400, 401, 404, 409], - empty: true, - }, - requestOptions, ), }, permission: { @@ -1537,7 +1512,7 @@ export function make(options: ClientOptions) { { method: "POST", path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`, - body: { reply: input["reply"], message: input["message"] }, + body: { decision: input["decision"], message: input["message"] }, successStatus: 204, declaredStatuses: [400, 401, 404], empty: true, diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index ac7a2d92e39..b789e5c804c 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -200,6 +200,20 @@ export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: nu export type SessionInterruptResponse = { interrupted: boolean } +export type FormMetadata = { [x: string]: JsonValue } + +export type FormWhen = { + key: string + op: "eq" | "neq" + value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean +} + +export type FormOption = { value: string; label: string; description?: string } + +export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string } + +export type FormValue = string | number | "Infinity" | "-Infinity" | "NaN" | boolean | Array + export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {}) export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens" @@ -221,16 +235,6 @@ export type MoneyUSDPerMillionTokens = number export type GenerateTextResponse = { data: { text: string } } -export type FormWhen = { - key: string - op: "eq" | "neq" - value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean -} - -export type FormOption = { value: string; label: string; description?: string } - -export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string } - export type IntegrationCommandMethod = { id: string; type: "command"; label: string; command: Array } export type IntegrationEnvMethod = { type: "env"; names: Array } @@ -303,13 +307,15 @@ export type ProjectCommands = { start?: string } export type ProjectTime = { created: number; updated: number } -export type FormMetadata = { [x: string]: JsonValue } - -export type FormValue = string | number | boolean | Array - export type PermissionSource = { type: "tool"; messageID: string; id: string } -export type PermissionSavedInfo = { id: string; projectID: string; action: string; resource: string } +export type PermissionSavedInfo = { + id: string + projectID: string + action: string + resource: string + time: { created: number; updated: number } +} export type FileSystemEntry = { path: string; type: "file" | "directory" } @@ -358,6 +364,8 @@ export type FormMetadata1 = { [x: string]: any } export type FormWhen1 = { key: string; op: "eq" | "neq"; value: string | number | boolean } +export type FormValue1 = string | number | boolean | Array + export type SessionStatus = | { type: "idle" } | { @@ -1336,36 +1344,6 @@ export type SessionMessageAssistantReasoning1 = { export type ToolContent1 = ToolTextContent | ToolFileContent1 -export type ModelCompatibility = { - reasoningField?: ModelReasoningField - requireReasoning?: boolean - maxTokensField?: ModelMaxTokensField - requireFinishReason?: boolean - requireAssistantAfterTool?: boolean - supportsPromptCacheKey?: boolean -} - -export type ProviderInfo = { - id: string - canonical?: string - integrationID?: string - name: string - activation: "auto" | "enabled" | "disabled" - package: string - compaction?: ProviderCompaction - transport?: ProviderTransport - settings?: { [x: string]: any } - headers?: { [x: string]: string } - body?: { [x: string]: any } -} - -export type ModelCost = { - tier?: { type: "context"; size: number } - input: MoneyUSDPerMillionTokens - output: MoneyUSDPerMillionTokens - cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens } -} - export type FormNumberField = { key: string title?: string @@ -1431,6 +1409,38 @@ export type FormMultiselectField = { default?: Array } +export type FormAnswer = { [x: string]: FormValue } + +export type ModelCompatibility = { + reasoningField?: ModelReasoningField + requireReasoning?: boolean + maxTokensField?: ModelMaxTokensField + requireFinishReason?: boolean + requireAssistantAfterTool?: boolean + supportsPromptCacheKey?: boolean +} + +export type ProviderInfo = { + id: string + canonical?: string + integrationID?: string + name: string + activation: "auto" | "enabled" | "disabled" + package: string + compaction?: ProviderCompaction + transport?: ProviderTransport + settings?: { [x: string]: any } + headers?: { [x: string]: string } + body?: { [x: string]: any } +} + +export type ModelCost = { + tier?: { type: "context"; size: number } + input: MoneyUSDPerMillionTokens + output: MoneyUSDPerMillionTokens + cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens } +} + export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo export type McpServer = { @@ -1470,8 +1480,6 @@ export type ProjectUpdated = { } } -export type FormAnswer = { [x: string]: FormValue } - export type PermissionRequest = { id: string sessionID: string @@ -1609,6 +1617,8 @@ export type FormMultiselectField1 = { default?: Array } +export type FormAnswer1 = { [x: string]: FormValue1 } + export type SessionStatusUpdated = { id: string created: number @@ -1806,6 +1816,16 @@ export type SessionMessageToolStateError1 = { metadata?: { [x: string]: JsonValue } } +export type FormField = + | FormStringField + | FormNumberField + | FormIntegerField + | FormBooleanField + | FormMultiselectField + | FormExternalField + +export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" } + export type ModelInfo = { id: string modelID: string @@ -1829,25 +1849,6 @@ export type ModelInfo = { limit: { context: number; input?: number; output: number } } -export type FormField = - | FormStringField - | FormNumberField - | FormIntegerField - | FormBooleanField - | FormMultiselectField - | FormExternalField - -export type FormState = { status: "pending" } | { status: "answered"; answer: FormAnswer } | { status: "cancelled" } - -export type FormReplied = { - id: string - created: number - metadata?: { [x: string]: any } - type: "form.replied" - location?: LocationRef - data: { id: string; sessionID: string; answer: FormAnswer } -} - export type FormField1 = | FormStringField1 | FormNumberField1 @@ -1856,6 +1857,15 @@ export type FormField1 = | FormMultiselectField1 | FormExternalField +export type FormReplied = { + id: string + created: number + metadata?: { [x: string]: any } + type: "form.replied" + location?: LocationRef + data: { id: string; sessionID: string; answer: FormAnswer1 } +} + export type ReferenceInfo = { name: string path: string @@ -2194,12 +2204,21 @@ export type SessionMessageAssistantContentEncoded = | SessionMessageAssistantReasoning1 | SessionMessageAssistantTool1 +export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields } + +export type FormDetail = { + id: string + sessionID: string + title: string + metadata?: FormMetadata + fields: FormFields + state: FormState +} + export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string; form?: FormFields } export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFields } -export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields } - export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields2 } export type SessionMessageInfo = @@ -2507,6 +2526,26 @@ export const isInstructionEntryValueTooLargeError = (value: unknown): value is I "_tag" in value && value["_tag"] === "InstructionEntryValueTooLargeError" +export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string } +export const isFormNotFoundError = (value: unknown): value is FormNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError" + +export type FormInvalidAnswerError = { + readonly _tag: "FormInvalidAnswerError" + readonly id: string + readonly message: string +} +export const isFormInvalidAnswerError = (value: unknown): value is FormInvalidAnswerError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormInvalidAnswerError" + +export type FormAlreadySettledError = { + readonly _tag: "FormAlreadySettledError" + readonly id: string + readonly message: string +} +export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError" + export type ProviderNotFoundError = { readonly _tag: "ProviderNotFoundError" readonly providerID: string @@ -2557,26 +2596,6 @@ export type ProjectNotFoundError = { export const isProjectNotFoundError = (value: unknown): value is ProjectNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProjectNotFoundError" -export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string } -export const isFormNotFoundError = (value: unknown): value is FormNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormNotFoundError" - -export type FormInvalidAnswerError = { - readonly _tag: "FormInvalidAnswerError" - readonly id: string - readonly message: string -} -export const isFormInvalidAnswerError = (value: unknown): value is FormInvalidAnswerError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormInvalidAnswerError" - -export type FormAlreadySettledError = { - readonly _tag: "FormAlreadySettledError" - readonly id: string - readonly message: string -} -export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError" - export type PermissionNotFoundError = { readonly _tag: "PermissionNotFoundError" readonly requestID: string @@ -4330,19 +4349,13 @@ export type SessionInboxCancelInput = { export type SessionInboxCancelOutput = void -export type SessionInboxSteerInput = { +export type SessionInboxUpdateInput = { readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"] readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"] + readonly delivery: { readonly delivery: "steer" | "queue" }["delivery"] } -export type SessionInboxSteerOutput = void - -export type SessionInboxQueueInput = { - readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"] - readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"] -} - -export type SessionInboxQueueOutput = void +export type SessionInboxUpdateOutput = void export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } @@ -4396,376 +4409,11 @@ export type SessionMessageGetInput = { export type SessionMessageGetOutput = { data: SessionMessageInfo }["data"] -export type SessionEnvironmentInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly variables: { readonly variables: { readonly [x: string]: string } }["variables"] -} +export type SessionFormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } -export type SessionEnvironmentOutput = void +export type SessionFormListOutput = { data: Array }["data"] -export type SessionViewInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly idle: { readonly idle: number }["idle"] -} - -export type SessionViewOutput = void - -export type MessageListInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly limit?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - readonly type?: - | "agent-switched" - | "model-switched" - | "location-switched" - | "user" - | "synthetic" - | "system" - | "skill" - | "shell" - | "assistant" - | "compaction" - | undefined - }["limit"] - readonly order?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - readonly type?: - | "agent-switched" - | "model-switched" - | "location-switched" - | "user" - | "synthetic" - | "system" - | "skill" - | "shell" - | "assistant" - | "compaction" - | undefined - }["order"] - readonly cursor?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - readonly type?: - | "agent-switched" - | "model-switched" - | "location-switched" - | "user" - | "synthetic" - | "system" - | "skill" - | "shell" - | "assistant" - | "compaction" - | undefined - }["cursor"] - readonly type?: { - readonly limit?: number | undefined - readonly order?: "asc" | "desc" | undefined - readonly cursor?: string | undefined - readonly type?: - | "agent-switched" - | "model-switched" - | "location-switched" - | "user" - | "synthetic" - | "system" - | "skill" - | "shell" - | "assistant" - | "compaction" - | undefined - }["type"] -} - -export type MessageListOutput = SessionMessagesResponse - -export type ModelListInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type ModelListOutput = { location: LocationPublicRef; data: Array } - -export type ModelDefaultInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type ModelDefaultOutput = { location: LocationPublicRef; data: ModelInfo | null } - -export type GenerateTextInput = { - readonly prompt: { - readonly prompt: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - }["prompt"] - readonly model?: { - readonly prompt: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null - }["model"] -} - -export type GenerateTextOutput = GenerateTextResponse["data"] - -export type ProviderListInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type ProviderListOutput = { location: LocationPublicRef; data: Array } - -export type ProviderGetInput = { - readonly providerID: { readonly providerID: string }["providerID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type ProviderGetOutput = { location: LocationPublicRef; data: ProviderInfo } - -export type IntegrationListInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type IntegrationListOutput = { location: LocationPublicRef; data: Array } - -export type IntegrationGetInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo } - -export type IntegrationWellknownAddInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly url: { readonly url: string }["url"] -} - -export type IntegrationWellknownAddOutput = void - -export type IntegrationConnectKeyInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly key: { - readonly key: string - readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined - readonly label?: string | undefined - }["key"] - readonly answer?: { - readonly key: string - readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined - readonly label?: string | undefined - }["answer"] - readonly label?: { - readonly key: string - readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined - readonly label?: string | undefined - }["label"] -} - -export type IntegrationConnectKeyOutput = void - -export type IntegrationOauthConnectInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly methodID: { - readonly methodID: string - readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined - readonly label?: string | undefined - }["methodID"] - readonly answer?: { - readonly methodID: string - readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined - readonly label?: string | undefined - }["answer"] - readonly label?: { - readonly methodID: string - readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined - readonly label?: string | undefined - }["label"] -} - -export type IntegrationOauthConnectOutput = { location: LocationPublicRef; data: IntegrationAttempt } - -export type IntegrationOauthStatusInput = { - readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] - readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type IntegrationOauthStatusOutput = { location: LocationPublicRef; data: IntegrationAttemptStatus } - -export type IntegrationOauthCompleteInput = { - readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] - readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly code?: { readonly code?: string | undefined }["code"] -} - -export type IntegrationOauthCompleteOutput = void - -export type IntegrationOauthCancelInput = { - readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] - readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type IntegrationOauthCancelOutput = void - -export type IntegrationCommandConnectInput = { - readonly integrationID: { readonly integrationID: string }["integrationID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"] - readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"] -} - -export type IntegrationCommandConnectOutput = { location: LocationPublicRef; data: IntegrationCommandAttempt } - -export type IntegrationCommandStatusInput = { - readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] - readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type IntegrationCommandStatusOutput = { location: LocationPublicRef; data: IntegrationCommandAttemptStatus } - -export type IntegrationCommandCancelInput = { - readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] - readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type IntegrationCommandCancelOutput = void - -export type McpListInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type McpListOutput = { location: LocationPublicRef; data: Array } - -export type McpAddInput = { - readonly server: { readonly server: string }["server"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly config: { - readonly config: - | { - readonly type: "local" - readonly command: ReadonlyArray - readonly cwd?: string - readonly environment?: { readonly [x: string]: string } - readonly disabled?: boolean - readonly codemode?: boolean - readonly timeout?: { readonly startup?: number; readonly catalog?: number; readonly execution?: number } - readonly protocol?: "legacy" | "auto" | "2026-07-28" - } - | { - readonly type: "remote" - readonly url: string - readonly headers?: { readonly [x: string]: string } - readonly oauth?: - | { - readonly client_id?: string - readonly client_secret?: string - readonly scope?: string - readonly callback_port?: number - readonly redirect_uri?: string - readonly auth_server_metadata_url?: string - } - | false - readonly disabled?: boolean - readonly codemode?: boolean - readonly timeout?: { readonly startup?: number; readonly catalog?: number; readonly execution?: number } - readonly protocol?: "legacy" | "auto" | "2026-07-28" - } - }["config"] -} - -export type McpAddOutput = void - -export type McpRemoveInput = { - readonly server: { readonly server: string }["server"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type McpRemoveOutput = void - -export type McpConnectInput = { - readonly server: { readonly server: string }["server"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type McpConnectOutput = void - -export type McpDisconnectInput = { - readonly server: { readonly server: string }["server"] - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type McpDisconnectOutput = void - -export type McpResourceCatalogInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpResourceCatalog } - -export type CredentialUpdateInput = { - readonly credentialID: { readonly credentialID: string }["credentialID"] - readonly label: { readonly label: string }["label"] -} - -export type CredentialUpdateOutput = void - -export type CredentialActivateInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] } - -export type CredentialActivateOutput = void - -export type CredentialRemoveInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] } - -export type CredentialRemoveOutput = void - -export type ProjectListOutput = Array - -export type ProjectUpdateInput = { - readonly projectID: { readonly projectID: string }["projectID"] - readonly canonical?: { - readonly canonical?: string - readonly name?: string - readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } - readonly commands?: { readonly start?: string } - }["canonical"] - readonly name?: { - readonly canonical?: string - readonly name?: string - readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } - readonly commands?: { readonly start?: string } - }["name"] - readonly icon?: { - readonly canonical?: string - readonly name?: string - readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } - readonly commands?: { readonly start?: string } - }["icon"] - readonly commands?: { - readonly canonical?: string - readonly name?: string - readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } - readonly commands?: { readonly start?: string } - }["commands"] -} - -export type ProjectUpdateOutput = Project - -export type FormRequestListInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type FormRequestListOutput = { location: LocationPublicRef; data: Array } - -export type FormListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type FormListOutput = { data: Array }["data"] - -export type FormCreateInput = { +export type SessionFormCreateInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly id?: { readonly id?: string | null @@ -5573,23 +5221,16 @@ export type FormCreateInput = { }["fields"] } -export type FormCreateOutput = { data: FormInfo }["data"] +export type SessionFormCreateOutput = { data: FormInfo }["data"] -export type FormGetInput = { +export type SessionFormGetInput = { readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] } -export type FormGetOutput = { data: FormInfo }["data"] +export type SessionFormGetOutput = { data: FormDetail }["data"] -export type FormStateInput = { - readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] - readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] -} - -export type FormStateOutput = { data: FormState }["data"] - -export type FormReplyInput = { +export type SessionFormReplyInput = { readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] readonly answer: { @@ -5597,14 +5238,379 @@ export type FormReplyInput = { }["answer"] } -export type FormReplyOutput = void +export type SessionFormReplyOutput = void -export type FormCancelInput = { +export type SessionFormCancelInput = { readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"] readonly formID: { readonly sessionID: string; readonly formID: string }["formID"] } -export type FormCancelOutput = void +export type SessionFormCancelOutput = void + +export type SessionEnvironmentInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly variables: { readonly variables: { readonly [x: string]: string } }["variables"] +} + +export type SessionEnvironmentOutput = void + +export type SessionViewInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly idle: { readonly idle: number }["idle"] +} + +export type SessionViewOutput = void + +export type MessageListInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly limit?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + readonly type?: + | "agent-switched" + | "model-switched" + | "location-switched" + | "user" + | "synthetic" + | "system" + | "skill" + | "shell" + | "assistant" + | "compaction" + | undefined + }["limit"] + readonly order?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + readonly type?: + | "agent-switched" + | "model-switched" + | "location-switched" + | "user" + | "synthetic" + | "system" + | "skill" + | "shell" + | "assistant" + | "compaction" + | undefined + }["order"] + readonly cursor?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + readonly type?: + | "agent-switched" + | "model-switched" + | "location-switched" + | "user" + | "synthetic" + | "system" + | "skill" + | "shell" + | "assistant" + | "compaction" + | undefined + }["cursor"] + readonly type?: { + readonly limit?: number | undefined + readonly order?: "asc" | "desc" | undefined + readonly cursor?: string | undefined + readonly type?: + | "agent-switched" + | "model-switched" + | "location-switched" + | "user" + | "synthetic" + | "system" + | "skill" + | "shell" + | "assistant" + | "compaction" + | undefined + }["type"] +} + +export type MessageListOutput = SessionMessagesResponse + +export type ModelListInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type ModelListOutput = { location: LocationPublicRef; data: Array } + +export type ModelDefaultInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type ModelDefaultOutput = { location: LocationPublicRef; data: ModelInfo | null } + +export type GenerateTextInput = { + readonly prompt: { + readonly prompt: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + }["prompt"] + readonly model?: { + readonly prompt: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null + }["model"] +} + +export type GenerateTextOutput = GenerateTextResponse["data"] + +export type ProviderListInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type ProviderListOutput = { location: LocationPublicRef; data: Array } + +export type ProviderGetInput = { + readonly providerID: { readonly providerID: string }["providerID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type ProviderGetOutput = { location: LocationPublicRef; data: ProviderInfo } + +export type IntegrationListInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type IntegrationListOutput = { location: LocationPublicRef; data: Array } + +export type IntegrationGetInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo } + +export type IntegrationWellknownAddInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly url: { readonly url: string }["url"] +} + +export type IntegrationWellknownAddOutput = void + +export type IntegrationConnectKeyInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly key: { + readonly key: string + readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined + readonly label?: string | undefined + }["key"] + readonly answer?: { + readonly key: string + readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined + readonly label?: string | undefined + }["answer"] + readonly label?: { + readonly key: string + readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined + readonly label?: string | undefined + }["label"] +} + +export type IntegrationConnectKeyOutput = void + +export type IntegrationOauthConnectInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly methodID: { + readonly methodID: string + readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined + readonly label?: string | undefined + }["methodID"] + readonly answer?: { + readonly methodID: string + readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined + readonly label?: string | undefined + }["answer"] + readonly label?: { + readonly methodID: string + readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray } | undefined + readonly label?: string | undefined + }["label"] +} + +export type IntegrationOauthConnectOutput = { location: LocationPublicRef; data: IntegrationAttempt } + +export type IntegrationOauthStatusInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type IntegrationOauthStatusOutput = { location: LocationPublicRef; data: IntegrationAttemptStatus } + +export type IntegrationOauthCompleteInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly code?: { readonly code?: string | undefined }["code"] +} + +export type IntegrationOauthCompleteOutput = void + +export type IntegrationOauthCancelInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type IntegrationOauthCancelOutput = void + +export type IntegrationCommandConnectInput = { + readonly integrationID: { readonly integrationID: string }["integrationID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly methodID: { readonly methodID: string; readonly label?: string | undefined }["methodID"] + readonly label?: { readonly methodID: string; readonly label?: string | undefined }["label"] +} + +export type IntegrationCommandConnectOutput = { location: LocationPublicRef; data: IntegrationCommandAttempt } + +export type IntegrationCommandStatusInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type IntegrationCommandStatusOutput = { location: LocationPublicRef; data: IntegrationCommandAttemptStatus } + +export type IntegrationCommandCancelInput = { + readonly integrationID: { readonly integrationID: string; readonly attemptID: string }["integrationID"] + readonly attemptID: { readonly integrationID: string; readonly attemptID: string }["attemptID"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type IntegrationCommandCancelOutput = void + +export type McpListInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type McpListOutput = { location: LocationPublicRef; data: Array } + +export type McpAddInput = { + readonly server: { readonly server: string }["server"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly config: { + readonly config: + | { + readonly type: "local" + readonly command: ReadonlyArray + readonly cwd?: string + readonly environment?: { readonly [x: string]: string } + readonly disabled?: boolean + readonly codemode?: boolean + readonly timeout?: { readonly startup?: number; readonly catalog?: number; readonly execution?: number } + readonly protocol?: "legacy" | "auto" | "2026-07-28" + } + | { + readonly type: "remote" + readonly url: string + readonly headers?: { readonly [x: string]: string } + readonly oauth?: + | { + readonly client_id?: string + readonly client_secret?: string + readonly scope?: string + readonly callback_port?: number + readonly redirect_uri?: string + readonly auth_server_metadata_url?: string + } + | false + readonly disabled?: boolean + readonly codemode?: boolean + readonly timeout?: { readonly startup?: number; readonly catalog?: number; readonly execution?: number } + readonly protocol?: "legacy" | "auto" | "2026-07-28" + } + }["config"] +} + +export type McpAddOutput = void + +export type McpRemoveInput = { + readonly server: { readonly server: string }["server"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type McpRemoveOutput = void + +export type McpConnectInput = { + readonly server: { readonly server: string }["server"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type McpConnectOutput = void + +export type McpDisconnectInput = { + readonly server: { readonly server: string }["server"] + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type McpDisconnectOutput = void + +export type McpResourceCatalogInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpResourceCatalog } + +export type CredentialUpdateInput = { + readonly credentialID: { readonly credentialID: string }["credentialID"] + readonly label: { readonly label: string }["label"] +} + +export type CredentialUpdateOutput = void + +export type CredentialActivateInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] } + +export type CredentialActivateOutput = void + +export type CredentialRemoveInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] } + +export type CredentialRemoveOutput = void + +export type ProjectListOutput = Array + +export type ProjectUpdateInput = { + readonly projectID: { readonly projectID: string }["projectID"] + readonly canonical?: { + readonly canonical?: string + readonly name?: string + readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } + readonly commands?: { readonly start?: string } + }["canonical"] + readonly name?: { + readonly canonical?: string + readonly name?: string + readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } + readonly commands?: { readonly start?: string } + }["name"] + readonly icon?: { + readonly canonical?: string + readonly name?: string + readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } + readonly commands?: { readonly start?: string } + }["icon"] + readonly commands?: { + readonly canonical?: string + readonly name?: string + readonly icon?: { readonly url?: string; readonly override?: string; readonly color?: string } + readonly commands?: { readonly start?: string } + }["commands"] +} + +export type ProjectUpdateOutput = Project + +export type FormListInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type FormListOutput = { location: LocationPublicRef; data: Array } export type PermissionRequestListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] @@ -5703,8 +5709,14 @@ export type PermissionGetOutput = { data: PermissionRequest }["data"] export type PermissionReplyInput = { readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"] readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"] - readonly reply: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["reply"] - readonly message?: { readonly reply: "once" | "always" | "reject"; readonly message?: string | undefined }["message"] + readonly decision: { + readonly decision: "once" | "always" | "reject" + readonly message?: string | undefined + }["decision"] + readonly message?: { + readonly decision: "once" | "always" | "reject" + readonly message?: string | undefined + }["message"] } export type PermissionReplyOutput = void diff --git a/packages/client/src/solid/data.ts b/packages/client/src/solid/data.ts index 1378d2e7680..a33a8b1cc19 100644 --- a/packages/client/src/solid/data.ts +++ b/packages/client/src/solid/data.ts @@ -7,9 +7,9 @@ import type { AgentInfo, CommandInfo, ConfigEntry, - FormCancelInput, + SessionFormCancelInput, FormInfo, - FormReplyInput, + SessionFormReplyInput, IntegrationInfo, LocationRef, LocationGetOutput, @@ -296,7 +296,7 @@ export function createData(config: CreateDataInput) { return true } - function settleForm(input: FormCancelInput, ref: LocationRef | undefined, request: Promise) { + function settleForm(input: SessionFormCancelInput, ref: LocationRef | undefined, request: Promise) { return request .catch((error: unknown) => { if ((!isFormNotFoundError(error) && !isFormAlreadySettledError(error)) || error.id !== input.formID) throw error @@ -1742,7 +1742,7 @@ export function createData(config: CreateDataInput) { const key = `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}` return sync.run(key, async () => { if (sessionID === "global") { - const response = await api().form.request.list({ + const response = await api().form.list({ location: locationQuery(ref ?? defaultLocation()), }) const location = { @@ -1757,7 +1757,7 @@ export function createData(config: CreateDataInput) { ]) return } - setStore("session", "form", sessionID, await api().form.list({ sessionID })) + setStore("session", "form", sessionID, await api().session.form.list({ sessionID })) }) }, invalidate(sessionID: string, ref?: LocationRef) { @@ -1765,11 +1765,11 @@ export function createData(config: CreateDataInput) { `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`, ) }, - reply(input: FormReplyInput, ref?: LocationRef) { - return settleForm(input, ref, api().form.reply(input, formRequestOptions(input.sessionID, ref))) + reply(input: SessionFormReplyInput, ref?: LocationRef) { + return settleForm(input, ref, api().session.form.reply(input, formRequestOptions(input.sessionID, ref))) }, - cancel(input: FormCancelInput, ref?: LocationRef) { - return settleForm(input, ref, api().form.cancel(input, formRequestOptions(input.sessionID, ref))) + cancel(input: SessionFormCancelInput, ref?: LocationRef) { + return settleForm(input, ref, api().session.form.cancel(input, formRequestOptions(input.sessionID, ref))) }, }, }, diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 9262b8eb43f..ba601a76b26 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -546,13 +546,13 @@ test("session.inbox mutations use the public HTTP contract", async () => { }) await client.session.inbox.cancel({ sessionID: "ses_test", inboxID: "msg_cancel" }) - await client.session.inbox.steer({ sessionID: "ses_test", inboxID: "msg_steer" }) - await client.session.inbox.queue({ sessionID: "ses_test", inboxID: "msg_queue" }) + await client.session.inbox.update({ sessionID: "ses_test", inboxID: "msg_steer", delivery: "steer" }) + await client.session.inbox.update({ sessionID: "ses_test", inboxID: "msg_queue", delivery: "queue" }) expect(requests).toEqual([ { method: "DELETE", url: "http://localhost:3000/api/session/ses_test/inbox/msg_cancel" }, - { method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_steer/steer" }, - { method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_queue/queue" }, + { method: "PATCH", url: "http://localhost:3000/api/session/ses_test/inbox/msg_steer" }, + { method: "PATCH", url: "http://localhost:3000/api/session/ses_test/inbox/msg_queue" }, ]) }) diff --git a/packages/core/src/permission/saved.ts b/packages/core/src/permission/saved.ts index 2d2b76206f8..8ba50b16beb 100644 --- a/packages/core/src/permission/saved.ts +++ b/packages/core/src/permission/saved.ts @@ -1,7 +1,7 @@ export * as PermissionSaved from "./saved.js" import { eq } from "drizzle-orm" -import { Context, Effect, Layer, Schema } from "effect" +import { Context, DateTime, Effect, Layer, Schema } from "effect" import { Project } from "@opencode/schema/project" import { Database } from "../database/database.js" import { makeGlobalNode } from "@opencode/util/effect/app-node" @@ -52,6 +52,10 @@ const layer = Layer.effect( projectID: row.project_id, action: row.action, resource: row.resource, + time: { + created: DateTime.makeUnsafe(row.time_created), + updated: DateTime.makeUnsafe(row.time_updated), + }, }), ) }) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 245d96925cb..a5bfac8df5a 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -411,7 +411,7 @@ export const make = Effect.fn("PluginHost.make")(function* ( .pipe( Effect.flatMap((request) => request?.sessionID === input.sessionID - ? permission.reply({ requestID: input.requestID, reply: input.reply, message: input.message }) + ? permission.reply({ requestID: input.requestID, reply: input.decision, message: input.message }) : Effect.fail(new Error(`Permission request not found: ${input.requestID}`)), ), ), diff --git a/packages/core/test/permission.test.ts b/packages/core/test/permission.test.ts index d7298cfdd85..3690b39f1a2 100644 --- a/packages/core/test/permission.test.ts +++ b/packages/core/test/permission.test.ts @@ -329,7 +329,9 @@ describe("Permission", () => { ).toMatchObject([{ action: "read", resource: "src/*" }]) const saved = yield* PermissionSaved.Service const id = (yield* saved.list())[0]!.id - expect(yield* saved.list()).toEqual([{ id, projectID: Project.ID.global, action: "read", resource: "src/*" }]) + expect(yield* saved.list()).toMatchObject([ + { id, projectID: Project.ID.global, action: "read", resource: "src/*" }, + ]) yield* service.assert(assertion({ id: Permission.ID.create("per_next"), resources: ["src/next.ts"] })) yield* saved.remove(id) expect(yield* saved.list()).toEqual([]) diff --git a/packages/plugin/src/tui/context.ts b/packages/plugin/src/tui/context.ts index 17fb7ecf7fc..1860294fdcc 100644 --- a/packages/plugin/src/tui/context.ts +++ b/packages/plugin/src/tui/context.ts @@ -1,9 +1,9 @@ import type { AgentInfo, CommandInfo, - FormCancelInput, + SessionFormCancelInput, FormInfo, - FormReplyInput, + SessionFormReplyInput, IntegrationInfo, LocationRef, McpResource, @@ -95,8 +95,8 @@ export interface Data { list(sessionID: string, location?: LocationRef): Array | undefined sync(sessionID: string, location?: LocationRef): Promise invalidate(sessionID: string, location?: LocationRef): void - reply(input: FormReplyInput, location?: LocationRef): Promise - cancel(input: FormCancelInput, location?: LocationRef): Promise + reply(input: SessionFormReplyInput, location?: LocationRef): Promise + cancel(input: SessionFormCancelInput, location?: LocationRef): Promise } } readonly project: { diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index f9c08e4bb08..630ff83a66d 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -3314,26 +3314,14 @@ } } } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } } }, - "description": "Cancel an inbox item that has not yet been delivered.", + "description": "Cancel an inbox item that has not yet been delivered. Unavailable items are a no-op.", "summary": "Cancel inbox input" - } - }, - "/api/session/{sessionID}/inbox/{inboxID}/steer": { - "post": { + }, + "patch": { "tags": ["session"], - "operationId": "session.inbox.steer", + "operationId": "session.inbox.update", "parameters": [ { "name": "sessionID", @@ -3400,82 +3388,25 @@ } } }, - "description": "Change a queued inbox item to steer delivery and wake session execution.", - "summary": "Steer queued item" - } - }, - "/api/session/{sessionID}/inbox/{inboxID}/queue": { - "post": { - "tags": ["session"], - "operationId": "session.inbox.queue", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "inboxID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } + "description": "Change a pending inbox item's delivery mode. Steering wakes session execution.", + "summary": "Update inbox item", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["delivery"], + "additionalProperties": false } } }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } - } - }, - "description": "Change a steered inbox item to queued delivery.", - "summary": "Queue steered item" + "required": true + } } }, "/api/experimental/session/{sessionID}/instructions/entries": { @@ -4222,10 +4153,456 @@ "summary": "Get session message" } }, - "/api/experimental/session/{sessionID}/environment": { + "/api/session/{sessionID}/form": { + "get": { + "tags": ["session"], + "operationId": "session.form.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, + "post": { + "tags": ["session"], + "operationId": "session.form.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + }, + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorEncoded" + } + } + } + } + }, + "description": "Create a form for a session.", + "summary": "Create session form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.CreatePayload" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": ["session"], + "operationId": "session.form.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Detail" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Retrieve a form and its current state for a session.", + "summary": "Get session form" + }, + "delete": { + "tags": ["session"], + "operationId": "session.form.cancel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" + } + } + } + } + }, + "description": "Cancel a pending form.", + "summary": "Cancel form" + } + }, + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": ["session"], + "operationId": "session.form.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerErrorEncoded" + }, + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" + } + } + } + } + }, + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/environment": { "put": { "tags": ["session"], - "operationId": "experimental.session.environment", + "operationId": "session.environment", "parameters": [ { "name": "sessionID", @@ -7012,10 +7389,10 @@ } } }, - "/api/form/request": { + "/api/form": { "get": { "tags": ["form"], - "operationId": "form.request.list", + "operationId": "form.list", "parameters": [ { "name": "location", @@ -7095,535 +7472,7 @@ } }, "description": "Retrieve pending forms for a location.", - "summary": "List pending form requests" - } - }, - "/api/session/{sessionID}/form": { - "get": { - "tags": ["form"], - "operationId": "session.form.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Form.Info" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - } - } - } - }, - "description": "Retrieve pending forms for a session.", - "summary": "List session forms" - }, - "post": { - "tags": ["form"], - "operationId": "session.form.create", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } - } - }, - "description": "Create a form for a session.", - "summary": "Create session form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.CreatePayload" - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/form/{formID}": { - "get": { - "tags": ["form"], - "operationId": "session.form.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Retrieve a form for a session.", - "summary": "Get session form" - } - }, - "/api/session/{sessionID}/form/{formID}/state": { - "get": { - "tags": ["form"], - "operationId": "session.form.state", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.State" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Retrieve the current state for a form.", - "summary": "Get form state" - } - }, - "/api/session/{sessionID}/form/{formID}/reply": { - "post": { - "tags": ["form"], - "operationId": "session.form.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "FormInvalidAnswerError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormInvalidAnswerErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" - } - } - } - } - }, - "description": "Submit an answer to a pending form.", - "summary": "Reply to form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.Reply" - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/form/{formID}/cancel": { - "post": { - "tags": ["form"], - "operationId": "session.form.cancel", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" - } - } - } - } - }, - "description": "Cancel a pending form.", - "summary": "Cancel form" + "summary": "List pending forms" } }, "/api/permission/request": { @@ -8203,7 +8052,7 @@ "schema": { "type": "object", "properties": { - "reply": { + "decision": { "$ref": "#/components/schemas/Permission.Reply" }, "message": { @@ -8217,7 +8066,7 @@ ] } }, - "required": ["reply"], + "required": ["decision"], "additionalProperties": false } } @@ -13909,12 +13758,38 @@ "$ref": "#/components/schemas/Form.Metadata" }, "fields": { - "$ref": "#/components/schemas/Form.Fields_2" + "$ref": "#/components/schemas/Form.Fields" } }, "required": ["title", "fields"], "additionalProperties": false }, + "Form.Detail": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^frm_" + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields" + }, + "state": { + "$ref": "#/components/schemas/Form.State" + } + }, + "required": ["id", "sessionID", "title", "fields", "state"], + "additionalProperties": false + }, "Form.ExternalField": { "type": "object", "properties": { @@ -14013,7 +13888,7 @@ "$ref": "#/components/schemas/Form.Metadata" }, "fields": { - "$ref": "#/components/schemas/Form.Fields_2" + "$ref": "#/components/schemas/Form.Fields" } }, "required": ["id", "sessionID", "title", "fields"], @@ -14995,7 +14870,7 @@ "type": "string" }, "form": { - "$ref": "#/components/schemas/Form.Fields_1" + "$ref": "#/components/schemas/Form.Fields_2" } }, "required": ["type"], @@ -15031,7 +14906,7 @@ "type": "string" }, "form": { - "$ref": "#/components/schemas/Form.Fields" + "$ref": "#/components/schemas/Form.Fields_1" } }, "required": ["id", "type", "label"], @@ -15888,9 +15763,22 @@ }, "resource": { "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + } + }, + "required": ["created", "updated"], + "additionalProperties": false } }, - "required": ["id", "projectID", "action", "resource"], + "required": ["id", "projectID", "action", "resource", "time"], "additionalProperties": false }, "PersistentPty.CreateInput": { @@ -19145,7 +19033,7 @@ }, { "name": "form", - "description": "Session form routes." + "description": "Location form routes." }, { "name": "permission", diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index 18dac0ac5ad..3285c9ff416 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -54,16 +54,20 @@ type LocationGroups = | HttpApiGroup.AddMiddleware | HttpApiGroup.AddMiddleware -type SessionGroups = - | ReturnType> - | typeof MessageGroup - -type FormGroups< - LocationId extends HttpApiMiddleware.AnyId, - LocationService, +type SessionGroups< + SessionLocationId extends HttpApiMiddleware.AnyId, + SessionLocationService, FormLocationId extends HttpApiMiddleware.AnyId, FormLocationService, -> = ReturnType> +> = + | ReturnType< + typeof makeSessionGroup + > + | typeof MessageGroup + +type FormGroups = ReturnType< + typeof makeFormGroup +> type MixedMiddlewareGroups< LocationId extends HttpApiMiddleware.AnyId, @@ -89,8 +93,8 @@ type ApiGroups< | typeof PersistentPtyGroup | typeof CredentialGroup | LocationGroups - | FormGroups - | SessionGroups + | FormGroups + | SessionGroups | MixedMiddlewareGroups | Event @@ -151,7 +155,7 @@ const makeApiFromGroup = < .add(LocationGroup.middleware(locationMiddleware)) .add(AgentGroup.middleware(locationMiddleware)) .add(PluginGroup.middleware(locationMiddleware)) - .add(makeSessionGroup(sessionLocationMiddleware)) + .add(makeSessionGroup(sessionLocationMiddleware, formLocationMiddleware)) .add(MessageGroup) .add(ModelGroup.middleware(locationMiddleware)) .add(GenerateGroup) @@ -160,7 +164,7 @@ const makeApiFromGroup = < .add(McpGroup.middleware(locationMiddleware)) .add(CredentialGroup) .add(ProjectGroup.middleware(locationMiddleware)) - .add(makeFormGroup(locationMiddleware, formLocationMiddleware)) + .add(makeFormGroup(locationMiddleware)) .add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware)) .add(FileSystemGroup.middleware(locationMiddleware)) .add(CommandGroup.middleware(locationMiddleware)) diff --git a/packages/protocol/src/groups/form.ts b/packages/protocol/src/groups/form.ts index 2c75df9a294..16abef80ff9 100644 --- a/packages/protocol/src/groups/form.ts +++ b/packages/protocol/src/groups/form.ts @@ -1,143 +1,26 @@ import { Form } from "@opencode/schema/form" import { Location } from "@opencode/schema/location" import { Context, Schema } from "effect" -import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" -import { - ConflictError, - FormAlreadySettledError, - FormInvalidAnswerError, - FormNotFoundError, - InvalidRequestError, - SessionNotFoundError, -} from "../errors.js" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi" import { LocationQuery, locationQueryOpenApi } from "./location.js" -const CreatePayload = Schema.Struct({ - id: Form.ID.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 - -// Form routes intentionally look session-scoped, but use a form-specific middleware instead of -// SessionLocationMiddleware. The middleware treats real session IDs normally and has an -// undocumented `global` sentinel branch for MCP elicitation forms that are still Location-scoped -// but not session-owned. This is temporary and should disappear once elicitations are attributable. -export const makeFormGroup = < - LocationId extends HttpApiMiddleware.AnyId, - LocationService, - FormLocationId extends HttpApiMiddleware.AnyId, - FormLocationService, ->( +export const makeFormGroup = ( locationMiddleware: Context.Key, - formLocationMiddleware: Context.Key, ) => HttpApiGroup.make("server.form") .add( - HttpApiEndpoint.get("form.request.list", "/api/form/request", { + HttpApiEndpoint.get("form.list", "/api/form", { query: LocationQuery, success: Location.response(Schema.Array(Form.Info)), }) .annotateMerge(locationQueryOpenApi) .annotateMerge( OpenApi.annotations({ - identifier: "form.request.list", - summary: "List pending form requests", + identifier: "form.list", + summary: "List pending forms", description: "Retrieve pending forms for a location.", }), ), ) .middleware(locationMiddleware) - .add( - HttpApiEndpoint.get("session.form.list", "/api/session/:sessionID/form", { - params: { sessionID: Schema.String }, - success: Schema.Struct({ data: Schema.Array(Form.Info) }), - error: SessionNotFoundError, - }).annotateMerge( - OpenApi.annotations({ - identifier: "session.form.list", - summary: "List session forms", - description: "Retrieve pending forms for a session.", - }), - ), - ) - .add( - HttpApiEndpoint.post("session.form.create", "/api/session/:sessionID/form", { - params: { sessionID: Schema.String }, - payload: CreatePayload, - success: Schema.Struct({ data: Form.Info }), - error: [SessionNotFoundError, ConflictError, InvalidRequestError], - }) - .middleware(formLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "session.form.create", - summary: "Create session form", - description: "Create a form for a session.", - }), - ), - ) - .add( - HttpApiEndpoint.get("session.form.get", "/api/session/:sessionID/form/:formID", { - params: { sessionID: Schema.String, formID: Form.ID }, - success: Schema.Struct({ data: Form.Info }), - error: [SessionNotFoundError, FormNotFoundError], - }) - .middleware(formLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "session.form.get", - summary: "Get session form", - description: "Retrieve a form for a session.", - }), - ), - ) - .add( - HttpApiEndpoint.get("session.form.state", "/api/session/:sessionID/form/:formID/state", { - params: { sessionID: Schema.String, formID: Form.ID }, - success: Schema.Struct({ data: Form.State }), - error: [SessionNotFoundError, FormNotFoundError], - }) - .middleware(formLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "session.form.state", - summary: "Get form state", - description: "Retrieve the current state for a form.", - }), - ), - ) - .add( - HttpApiEndpoint.post("session.form.reply", "/api/session/:sessionID/form/:formID/reply", { - params: { sessionID: Schema.String, formID: Form.ID }, - payload: Form.Reply, - success: HttpApiSchema.NoContent, - error: [SessionNotFoundError, FormAlreadySettledError, FormInvalidAnswerError, FormNotFoundError], - }) - .middleware(formLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "session.form.reply", - summary: "Reply to form", - description: "Submit an answer to a pending form.", - }), - ), - ) - .add( - HttpApiEndpoint.post("session.form.cancel", "/api/session/:sessionID/form/:formID/cancel", { - params: { sessionID: Schema.String, formID: Form.ID }, - success: HttpApiSchema.NoContent, - error: [SessionNotFoundError, FormAlreadySettledError, FormNotFoundError], - }) - .middleware(formLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "session.form.cancel", - summary: "Cancel form", - description: "Cancel a pending form.", - }), - ), - ) - .annotateMerge(OpenApi.annotations({ title: "form", description: "Session form routes." })) + .annotateMerge(OpenApi.annotations({ title: "form", description: "Location form routes." })) diff --git a/packages/protocol/src/groups/permission.ts b/packages/protocol/src/groups/permission.ts index 0dfcd90a448..bd2d6f05a23 100644 --- a/packages/protocol/src/groups/permission.ts +++ b/packages/protocol/src/groups/permission.ts @@ -117,7 +117,7 @@ export const makePermissionGroup = < HttpApiEndpoint.post("session.permission.reply", "/api/session/:sessionID/permission/:requestID/reply", { params: { sessionID: Session.ID, requestID: Permission.ID }, payload: Schema.Struct({ - reply: Permission.Reply, + decision: Permission.Reply, message: Schema.String.pipe(Schema.optional), }), success: HttpApiSchema.NoContent, diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index a0c5752be2e..2b16ec51b79 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -20,6 +20,9 @@ import { ConflictError, CommandExecutionError, CommandNotFoundError, + FormAlreadySettledError, + FormInvalidAnswerError, + FormNotFoundError, InvalidCursorError, InvalidRequestError, MessageNotFoundError, @@ -37,6 +40,7 @@ import { Location } from "@opencode/schema/location" import { SessionEvent } from "@opencode/schema/session-event" import { EventLog } from "@opencode/schema/event-log" import { FileDiff } from "@opencode/schema/file-diff" +import { Form } from "@opencode/schema/form" import { PublicSessionMessage } from "./message.js" const ParentIDFilter = Schema.Union([ @@ -140,6 +144,13 @@ const PublicInboxInfo = Schema.Union([ PublicMove, ]).annotate({ identifier: "Session.Inbox.Info" }) +const FormCreatePayload = Schema.Struct({ + id: Form.ID.pipe(Schema.optional), + title: Form.Info.fields.title, + metadata: Form.Info.fields.metadata, + fields: Form.Info.fields.fields, +}).annotate({ identifier: "Form.CreatePayload" }) + const BooleanFromString = Schema.Literals(["true", "false"]).pipe( Schema.decodeTo(Schema.Boolean, { decode: SchemaGetter.transform((value) => value === "true"), @@ -159,7 +170,12 @@ export const SessionsQuery = Schema.Struct({ cursor: SessionsQueryCursor.pipe(Schema.optional), }).annotate({ identifier: "SessionsQuery" }) -export const makeSessionGroup = (sessionLocationMiddleware: Context.Key) => +export const makeSessionGroup = < + I extends HttpApiMiddleware.AnyId, + S, + FormI extends HttpApiMiddleware.AnyId, + FormS, +>(sessionLocationMiddleware: Context.Key, formLocationMiddleware: Context.Key) => HttpApiGroup.make("server.session") .add( HttpApiEndpoint.get("session.list", "/api/session", { @@ -599,38 +615,26 @@ export const makeSessionGroup = (sessionLo HttpApiEndpoint.delete("session.inbox.cancel", "/api/session/:sessionID/inbox/:inboxID", { params: { sessionID: Session.ID, inboxID: SessionMessage.ID }, success: HttpApiSchema.NoContent, - error: [ConflictError, SessionNotFoundError], + error: SessionNotFoundError, }).annotateMerge( OpenApi.annotations({ identifier: "session.inbox.cancel", summary: "Cancel inbox input", - description: "Cancel an inbox item that has not yet been delivered.", + description: "Cancel an inbox item that has not yet been delivered. Unavailable items are a no-op.", }), ), ) .add( - HttpApiEndpoint.post("session.inbox.steer", "/api/session/:sessionID/inbox/:inboxID/steer", { + HttpApiEndpoint.patch("session.inbox.update", "/api/session/:sessionID/inbox/:inboxID", { params: { sessionID: Session.ID, inboxID: SessionMessage.ID }, + payload: Schema.Struct({ delivery: SessionInbox.Delivery }), success: HttpApiSchema.NoContent, error: [ConflictError, SessionNotFoundError], }).annotateMerge( OpenApi.annotations({ - identifier: "session.inbox.steer", - summary: "Steer queued item", - description: "Change a queued inbox item to steer delivery and wake session execution.", - }), - ), - ) - .add( - HttpApiEndpoint.post("session.inbox.queue", "/api/session/:sessionID/inbox/:inboxID/queue", { - params: { sessionID: Session.ID, inboxID: SessionMessage.ID }, - success: HttpApiSchema.NoContent, - error: [ConflictError, SessionNotFoundError], - }).annotateMerge( - OpenApi.annotations({ - identifier: "session.inbox.queue", - summary: "Queue steered item", - description: "Change a steered inbox item to queued delivery.", + identifier: "session.inbox.update", + summary: "Update inbox item", + description: "Change a pending inbox item's delivery mode. Steering wakes session execution.", }), ), ) @@ -783,14 +787,91 @@ export const makeSessionGroup = (sessionLo ), ) .add( - HttpApiEndpoint.put("session.environment", "/api/experimental/session/:sessionID/environment", { + HttpApiEndpoint.get("session.form.list", "/api/session/:sessionID/form", { + params: { sessionID: Schema.String }, + success: Schema.Struct({ data: Schema.Array(Form.Info) }), + error: SessionNotFoundError, + }) + .middleware(formLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "session.form.list", + summary: "List session forms", + description: "Retrieve pending forms for a session.", + }), + ), + ) + .add( + HttpApiEndpoint.post("session.form.create", "/api/session/:sessionID/form", { + params: { sessionID: Schema.String }, + payload: FormCreatePayload, + success: Schema.Struct({ data: Form.Info }), + error: [SessionNotFoundError, ConflictError, InvalidRequestError], + }) + .middleware(formLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "session.form.create", + summary: "Create session form", + description: "Create a form for a session.", + }), + ), + ) + .add( + HttpApiEndpoint.get("session.form.get", "/api/session/:sessionID/form/:formID", { + params: { sessionID: Schema.String, formID: Form.ID }, + success: Schema.Struct({ data: Form.Detail }), + error: [SessionNotFoundError, FormNotFoundError], + }) + .middleware(formLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "session.form.get", + summary: "Get session form", + description: "Retrieve a form and its current state for a session.", + }), + ), + ) + .add( + HttpApiEndpoint.post("session.form.reply", "/api/session/:sessionID/form/:formID/reply", { + params: { sessionID: Schema.String, formID: Form.ID }, + payload: Form.Reply, + success: HttpApiSchema.NoContent, + error: [SessionNotFoundError, FormAlreadySettledError, FormInvalidAnswerError, FormNotFoundError], + }) + .middleware(formLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "session.form.reply", + summary: "Reply to form", + description: "Submit an answer to a pending form.", + }), + ), + ) + .add( + HttpApiEndpoint.delete("session.form.cancel", "/api/session/:sessionID/form/:formID", { + params: { sessionID: Schema.String, formID: Form.ID }, + success: HttpApiSchema.NoContent, + error: [SessionNotFoundError, FormAlreadySettledError, FormNotFoundError], + }) + .middleware(formLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "session.form.cancel", + summary: "Cancel form", + description: "Cancel a pending form.", + }), + ), + ) + .add( + HttpApiEndpoint.put("session.environment", "/api/session/:sessionID/environment", { params: { sessionID: Session.ID }, payload: Schema.Struct({ variables: Schema.Record(Schema.String, Schema.String) }), success: HttpApiSchema.NoContent, error: SessionNotFoundError, }).annotateMerge( OpenApi.annotations({ - identifier: "experimental.session.environment", + identifier: "session.environment", summary: "Set session environment", description: "Replace the process environment used by local shell commands for this session.", }), diff --git a/packages/schema/src/form.ts b/packages/schema/src/form.ts index 6c2fdc29e80..599f30b8e41 100644 --- a/packages/schema/src/form.ts +++ b/packages/schema/src/form.ts @@ -152,6 +152,12 @@ export const State = Schema.Union([ .annotate({ identifier: "Form.State" }) export type State = typeof State.Type +export const Detail = Schema.Struct({ + ...Info.fields, + state: State, +}).annotate({ identifier: "Form.Detail" }) +export interface Detail extends Schema.Schema.Type {} + export const Reply = Schema.Struct({ answer: Answer, }).annotate({ identifier: "Form.Reply" }) diff --git a/packages/schema/src/permission-saved.ts b/packages/schema/src/permission-saved.ts index 9432b1d7775..301036e0c57 100644 --- a/packages/schema/src/permission-saved.ts +++ b/packages/schema/src/permission-saved.ts @@ -3,7 +3,7 @@ export * as PermissionSaved from "./permission-saved.js" import { Schema } from "effect" import { ascending } from "./identifier.js" import { ProjectID } from "./project-id.js" -import { statics } from "./schema.js" +import { DateTimeUtcFromMillis, statics } from "./schema.js" export const ID = Schema.String.pipe( Schema.brand("PermissionSaved.ID"), @@ -16,5 +16,9 @@ export const Info = Schema.Struct({ projectID: ProjectID, action: Schema.String, resource: Schema.String, + time: Schema.Struct({ + created: DateTimeUtcFromMillis, + updated: DateTimeUtcFromMillis, + }), }).annotate({ identifier: "PermissionSaved.Info" }) export interface Info extends Schema.Schema.Type {} diff --git a/packages/sdk/test/instances.test.ts b/packages/sdk/test/instances.test.ts index fe6fe005c63..87857bd0838 100644 --- a/packages/sdk/test/instances.test.ts +++ b/packages/sdk/test/instances.test.ts @@ -90,7 +90,7 @@ test("Promise instances are lazy, share by key and Location, and stay isolated b expect(setups).toEqual([]) // Permission and form lists read instance services, so they acquire the Session's instance. expect(await first.permission.list({ sessionID })).toEqual([]) - expect(await first.form.list({ sessionID })).toEqual([]) + expect(await first.session.form.list({ sessionID })).toEqual([]) expect(configured).toEqual(["first:alpha"]) await Promise.all( diff --git a/packages/server/src/handlers/form.ts b/packages/server/src/handlers/form.ts index 9be44d32f20..15f2714c056 100644 --- a/packages/server/src/handlers/form.ts +++ b/packages/server/src/handlers/form.ts @@ -1,123 +1,15 @@ import { Form } from "@opencode/core/form" -import { Instance } from "@opencode/core/instance/service" -import { LocationServiceMap } from "@opencode/core/location-services" -import { Session } from "@opencode/core/session" -import { - ConflictError, - FormAlreadySettledError, - FormInvalidAnswerError, - FormNotFoundError, - InvalidRequestError, -} from "@opencode/protocol/errors" import { Effect } from "effect" -import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" -import { requestRef, response, sessionInfo } from "../location" - -function missingForm(id: Form.ID) { - return new FormNotFoundError({ id, message: `Form not found: ${id}` }) -} +import { response } from "../location" export const FormHandler = HttpApiBuilder.group(Api, "server.form", (handlers) => - Effect.gen(function* () { - const locations = yield* LocationServiceMap.Service - const instances = yield* Instance.Service - const sessions = yield* Session.Service - const requireOwnedForm = Effect.fnUntraced(function* (sessionID: Form.Info["sessionID"], formID: Form.ID) { + handlers.handle( + "form.list", + Effect.fn(function* () { const form = yield* Form.Service - const info = yield* form.get(formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(formID))) - if (info.sessionID !== sessionID) return yield* missingForm(formID) - return { form, info } - }) - - return handlers - .handle( - "form.request.list", - Effect.fn(function* () { - const form = yield* Form.Service - return yield* response(form.list()) - }), - ) - .handle( - "session.form.list", - Effect.fn(function* (ctx) { - const session = - ctx.params.sessionID === "global" ? undefined : yield* sessionInfo(sessions, ctx.params.sessionID) - const read = Form.Service.use((form) => form.list({ sessionID: ctx.params.sessionID })) - const forms = yield* session - ? read.pipe(instances.provide(session)) - : read.pipe(Effect.provide(locations.get(requestRef(ctx.request)))) - return { data: forms } - }), - ) - .handle( - "session.form.create", - Effect.fn(function* (ctx) { - const form = yield* Form.Service - 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 } - }), - ) - .handle( - "session.form.get", - Effect.fn(function* (ctx) { - const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) - return { data: owned.info } - }), - ) - .handle( - "session.form.state", - Effect.fn(function* (ctx) { - const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) - const data = yield* owned.form - .state(ctx.params.formID) - .pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(ctx.params.formID))) - return { data } - }), - ) - .handle( - "session.form.reply", - Effect.fn(function* (ctx) { - const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) - yield* owned.form.reply({ id: ctx.params.formID, answer: ctx.payload.answer }).pipe( - Effect.catchTags({ - "Form.AlreadySettledError": (error) => - new FormAlreadySettledError({ id: error.id, message: error.message }), - "Form.InvalidAnswerError": (error) => - new FormInvalidAnswerError({ id: error.id, message: error.message }), - "Form.NotFoundError": () => missingForm(ctx.params.formID), - }), - ) - return HttpApiSchema.NoContent.make() - }), - ) - .handle( - "session.form.cancel", - Effect.fn(function* (ctx) { - const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) - yield* owned.form.cancel(ctx.params.formID).pipe( - Effect.catchTags({ - "Form.AlreadySettledError": (error) => - new FormAlreadySettledError({ id: error.id, message: error.message }), - "Form.NotFoundError": () => missingForm(ctx.params.formID), - }), - ) - return HttpApiSchema.NoContent.make() - }), - ) - }), + return yield* response(form.list()) + }), + ), ) diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index bd2bc6b09c3..401b5059dcd 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -78,7 +78,7 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", Effect.fn(function* (ctx) { const owned = yield* requireOwnedRequest(ctx.params.sessionID, ctx.params.requestID) yield* owned.permission - .reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message }) + .reply({ requestID: ctx.params.requestID, reply: ctx.payload.decision, message: ctx.payload.message }) .pipe(Effect.catchTag("Permission.NotFoundError", () => missingRequest(ctx.params.requestID))) return HttpApiSchema.NoContent.make() }), diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 59b19e0bd5d..346a2602a53 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -3,6 +3,7 @@ import { SessionStats } from "@opencode/core/session/stats" import { SessionTitle } from "@opencode/core/session/title" import { SessionTransfer } from "@opencode/core/session/transfer" import { InstructionEntry } from "@opencode/core/session/instruction-entry" +import { Form } from "@opencode/core/form" import { DateTime, Effect, Stream } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" import { Api } from "../api" @@ -11,6 +12,9 @@ import { ConflictError, CommandExecutionError, CommandNotFoundError, + FormAlreadySettledError, + FormInvalidAnswerError, + FormNotFoundError, InvalidRequestError, InvalidCursorError, MessageNotFoundError, @@ -23,10 +27,20 @@ import { failedMessageDecode, failedSnapshot, missingMessage, missingSession } f const DefaultSessionsLimit = 50 +function missingForm(id: Form.ID) { + return new FormNotFoundError({ id, message: `Form not found: ${id}` }) +} + export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) => Effect.gen(function* () { const session = yield* Session.Service const transfer = yield* SessionTransfer.Service + const requireOwnedForm = Effect.fnUntraced(function* (sessionID: Form.Info["sessionID"], formID: Form.ID) { + const form = yield* Form.Service + const info = yield* form.get(formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(formID))) + if (info.sessionID !== sessionID) return yield* missingForm(formID) + return { form, info } + }) const busySession = (error: Session.BusyError) => new SessionBusyError({ sessionID: error.sessionID, @@ -517,27 +531,21 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl .handle( "session.inbox.cancel", Effect.fn(function* (ctx) { - return yield* pendingMutation( - session.cancelInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), - "Pending input can no longer be cancelled", + yield* session.cancelInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }).pipe( + Effect.catchTag("Session.NotFoundError", missingSession), + Effect.catchTag("Session.InboxConflictError", () => Effect.void), ) + return HttpApiSchema.NoContent.make() }), ) .handle( - "session.inbox.steer", + "session.inbox.update", Effect.fn(function* (ctx) { return yield* pendingMutation( - session.steerInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), - "Pending input is no longer queued", - ) - }), - ) - .handle( - "session.inbox.queue", - Effect.fn(function* (ctx) { - return yield* pendingMutation( - session.queueInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), - "Pending input is no longer a steer", + ctx.payload.delivery === "steer" + ? session.steerInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }) + : session.queueInbox({ sessionID: ctx.params.sessionID, inboxID: ctx.params.inboxID }), + `Pending input cannot change to ${ctx.payload.delivery}`, ) }), ) @@ -614,5 +622,74 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl }) }), ) + .handle( + "session.form.list", + Effect.fn(function* (ctx) { + const form = yield* Form.Service + return { data: yield* form.list({ sessionID: ctx.params.sessionID }) } + }), + ) + .handle( + "session.form.create", + Effect.fn(function* (ctx) { + const form = yield* Form.Service + 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 } + }), + ) + .handle( + "session.form.get", + Effect.fn(function* (ctx) { + const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) + const state = yield* owned.form + .state(ctx.params.formID) + .pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(ctx.params.formID))) + return { data: { ...owned.info, state } } + }), + ) + .handle( + "session.form.reply", + Effect.fn(function* (ctx) { + const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) + yield* owned.form.reply({ id: ctx.params.formID, answer: ctx.payload.answer }).pipe( + Effect.catchTags({ + "Form.AlreadySettledError": (error) => + new FormAlreadySettledError({ id: error.id, message: error.message }), + "Form.InvalidAnswerError": (error) => + new FormInvalidAnswerError({ id: error.id, message: error.message }), + "Form.NotFoundError": () => missingForm(ctx.params.formID), + }), + ) + return HttpApiSchema.NoContent.make() + }), + ) + .handle( + "session.form.cancel", + Effect.fn(function* (ctx) { + const owned = yield* requireOwnedForm(ctx.params.sessionID, ctx.params.formID) + yield* owned.form.cancel(ctx.params.formID).pipe( + Effect.catchTags({ + "Form.AlreadySettledError": (error) => + new FormAlreadySettledError({ id: error.id, message: error.message }), + "Form.NotFoundError": () => missingForm(ctx.params.formID), + }), + ) + return HttpApiSchema.NoContent.make() + }), + ) }), ) diff --git a/packages/server/test/session-instances.test.ts b/packages/server/test/session-instances.test.ts index 60f21771feb..0cea57e4aff 100644 --- a/packages/server/test/session-instances.test.ts +++ b/packages/server/test/session-instances.test.ts @@ -295,7 +295,7 @@ it.live( ).toBe(404) expect( (yield* request(`/api/session/${entry.session.id}/permission/${entry.foreignPermission.id}/reply`, { - reply: "once", + decision: "once", })).status, ).toBe(404) yield* Effect.gen(function* () { @@ -315,7 +315,7 @@ it.live( ).toBe(204) expect( (yield* request(`/api/session/${entry.session.id}/permission/${entry.permission.id}/reply`, { - reply: "once", + decision: "once", })).status, ).toBe(204) yield* Effect.gen(function* () { diff --git a/packages/tui/src/mini/demo.ts b/packages/tui/src/mini/demo.ts index 757706aca37..8fe7181bb47 100644 --- a/packages/tui/src/mini/demo.ts +++ b/packages/tui/src/mini/demo.ts @@ -1006,14 +1006,14 @@ export function createRunDemo(input: Input) { const permission = (input: PermissionReply): boolean => { const item = state.perms.get(input.requestID) - if (!item || !input.reply) { + if (!item || !input.decision) { return false } state.perms.delete(input.requestID) clearBlocker(state) - if (input.reply === "reject") { + if (input.decision === "reject") { failTool(state, item.ref, input.message || "permission rejected") return true } diff --git a/packages/tui/src/mini/permission.shared.ts b/packages/tui/src/mini/permission.shared.ts index 8294a5abe50..38d2de31354 100644 --- a/packages/tui/src/mini/permission.shared.ts +++ b/packages/tui/src/mini/permission.shared.ts @@ -79,13 +79,13 @@ export { permissionAlwaysLines } function permissionReply( sessionID: string, requestID: string, - reply: PermissionReply["reply"], + decision: PermissionReply["decision"], message?: string, ): PermissionReply { return { sessionID, requestID, - reply, + decision, ...(message && message.trim() ? { message: message.trim() } : {}), } } diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index 53ae4e2ee50..f01c8e70032 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -276,7 +276,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep onFormReply: async (next) => { if (state.demo?.formReply(next)) return try { - await state.sdk.form.reply(next, formRequestOptions(next.sessionID === "global" ? next.location : undefined)) + await state.sdk.session.form.reply(next, formRequestOptions(next.sessionID === "global" ? next.location : undefined)) } catch (error) { if (!formAlreadySettled(error)) throw error } @@ -285,7 +285,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep onFormCancel: async (next) => { if (state.demo?.formCancel(next)) return try { - await state.sdk.form.cancel(next, formRequestOptions(next.sessionID === "global" ? next.location : undefined)) + await state.sdk.session.form.cancel(next, formRequestOptions(next.sessionID === "global" ? next.location : undefined)) } catch (error) { if (!formAlreadySettled(error)) throw error } @@ -395,7 +395,11 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep onQueuedPromptAction: async (action, inboxID) => { if (!state.sessionID) return log?.write(`send.pending.${action}`, { sessionID: state.sessionID, inboxID }) - await state.sdk.session.inbox[action]({ sessionID: state.sessionID, inboxID }) + if (action === "cancel") { + await state.sdk.session.inbox.cancel({ sessionID: state.sessionID, inboxID }) + return + } + await state.sdk.session.inbox.update({ sessionID: state.sessionID, inboxID, delivery: action }) }, onSubagentInterrupt: (sessionID) => { log?.write("send.subagent.interrupt", { sessionID }) diff --git a/packages/tui/src/mini/stream-v2.subagent.ts b/packages/tui/src/mini/stream-v2.subagent.ts index cee7d4128a8..87d151a7df0 100644 --- a/packages/tui/src/mini/stream-v2.subagent.ts +++ b/packages/tui/src/mini/stream-v2.subagent.ts @@ -541,7 +541,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac replayBlockerEvents(child, "permission") input.emit() }), - sdk.form.list({ sessionID: child.sessionID }, { signal }).then((forms) => { + sdk.session.form.list({ sessionID: child.sessionID }, { signal }).then((forms) => { if (!blockerCurrent(child, epoch, signal)) return child.forms = forms replayBlockerEvents(child, "form") diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index f860cf125ef..f4a0e72cc35 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -948,9 +948,9 @@ export async function createSessionTransport(input: StreamInput): Promise { @@ -545,9 +545,9 @@ export function Session(props: { const result = await runPendingAction(inboxID, async () => { const request = action === "steer" - ? client.api.session.inbox.steer({ sessionID: route.sessionID, inboxID }) + ? client.api.session.inbox.update({ sessionID: route.sessionID, inboxID, delivery: "steer" }) : action === "queue" - ? client.api.session.inbox.queue({ sessionID: route.sessionID, inboxID }) + ? client.api.session.inbox.update({ sessionID: route.sessionID, inboxID, delivery: "queue" }) : client.api.session.inbox.cancel({ sessionID: route.sessionID, inboxID }) const error = await request.then( () => undefined, diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx index fd71965112d..f6f7279ee78 100644 --- a/packages/tui/src/routes/session/permission.tsx +++ b/packages/tui/src/routes/session/permission.tsx @@ -135,7 +135,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory? function reply(value: PermissionReply, message?: string) { void data.session.permission - .reply({ sessionID: props.request.sessionID, requestID: props.request.id, reply: value, message }) + .reply({ sessionID: props.request.sessionID, requestID: props.request.id, decision: value, message }) .catch((error: unknown) => toast.error(error)) } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 4810323a807..2e8ab527d3e 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -2347,7 +2347,7 @@ test("dismisses a permission that expired before its reply", async () => { await data.session.permission.reply({ sessionID: request.sessionID, requestID: request.id, - reply: "once", + decision: "once", }) expect(replies).toBe(1) @@ -2501,7 +2501,7 @@ test("syncs global forms once for each requested location", async () => { const requests: URL[] = [] const other = { directory: "/tmp/opencode-other" } const calls = createFetch((url) => { - if (url.pathname !== "/api/form/request") return + if (url.pathname !== "/api/form") return requests.push(url) const requestedDirectory = url.searchParams.get("location[directory]") ?? directory return json({ @@ -2579,7 +2579,7 @@ test("resyncs global forms only for the active location after reconnect", async ], cursor: {}, }) - if (url.pathname !== "/api/form/request") return + if (url.pathname !== "/api/form") return requests.push(url) const requestedDirectory = url.searchParams.get("location[directory]") ?? home.directory const count = (counts.get(requestedDirectory) ?? 0) + 1 diff --git a/packages/tui/test/cli/tui/form.test.tsx b/packages/tui/test/cli/tui/form.test.tsx index f1b02894882..b839850d89d 100644 --- a/packages/tui/test/cli/tui/form.test.tsx +++ b/packages/tui/test/cli/tui/form.test.tsx @@ -67,7 +67,7 @@ async function mountForm( replies.push(answer) return response?.reply ? failure(response.reply) : new Response(null, { status: 204 }) }) - if (url.pathname === "/api/session/ses_test/form/frm_test/cancel") { + if (url.pathname === "/api/session/ses_test/form/frm_test" && request.method === "DELETE") { cancellations.push(true) return response?.cancel ? failure(response.cancel) : new Response(null, { status: 204 }) } diff --git a/packages/tui/test/cli/tui/input-scope.test.tsx b/packages/tui/test/cli/tui/input-scope.test.tsx index 39d6b87524d..3364368458e 100644 --- a/packages/tui/test/cli/tui/input-scope.test.tsx +++ b/packages/tui/test/cli/tui/input-scope.test.tsx @@ -243,7 +243,7 @@ test("permission layers leave the focused peer's Enter and navigation alone unti panes.setActive(true) panes.app.mockInput.pressEnter() await panes.app.waitFor(() => panes.replies.length === 1) - expect(panes.replies).toEqual([{ reply: "once" }]) + expect(panes.replies).toEqual([{ decision: "once" }]) expect(panes.submissions).toHaveLength(1) } finally { panes.app.renderer.destroy() @@ -272,7 +272,7 @@ test("permission rejection text keeps its draft and regains focus when its scope expect(panes.app.renderer.currentFocusedEditor?.id).toBe(input?.id) panes.app.mockInput.pressEnter() await panes.app.waitFor(() => panes.replies.length === 1) - expect(panes.replies).toEqual([{ reply: "reject", message: "choose another command" }]) + expect(panes.replies).toEqual([{ decision: "reject", message: "choose another command" }]) } finally { panes.app.renderer.destroy() } diff --git a/packages/tui/test/fixture/tui-client.ts b/packages/tui/test/fixture/tui-client.ts index 98986adb1f7..50e175e3d2b 100644 --- a/packages/tui/test/fixture/tui-client.ts +++ b/packages/tui/test/fixture/tui-client.ts @@ -142,7 +142,7 @@ export function createFetch(override?: FetchHandler, events?: ReturnType { expect(out.reply).toEqual({ sessionID: "session-1", requestID: "perm-1", - reply: "once", + decision: "once", }) }) @@ -47,7 +47,7 @@ describe("run permission shared", () => { expect(permissionRun(next.state, "perm-1", "confirm").reply).toEqual({ sessionID: "session-1", requestID: "perm-1", - reply: "always", + decision: "always", }) expect(permissionRun(next.state, "perm-1", "cancel").state).toMatchObject({ @@ -64,7 +64,7 @@ describe("run permission shared", () => { expect(out).toEqual({ sessionID: "session-1", requestID: "perm-1", - reply: "reject", + decision: "reject", message: "use rg", }) diff --git a/packages/tui/test/mini/runtime.test.ts b/packages/tui/test/mini/runtime.test.ts index f51ffcb7ba5..80ce772153e 100644 --- a/packages/tui/test/mini/runtime.test.ts +++ b/packages/tui/test/mini/runtime.test.ts @@ -173,7 +173,7 @@ describe("run interactive runtime", () => { let lifecycle!: LifecycleInput const settled: Array<{ sessionID: string; formID: string }> = [] stubCatalogLists(sdk) - const reply = spyOn(sdk.form, "reply").mockImplementation(() => ok(undefined)) + const reply = spyOn(sdk.session.form, "reply").mockImplementation(() => ok(undefined)) const task = runInteractiveDeferredMode( { diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index af8137965a1..a5c9c1d5158 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -174,8 +174,8 @@ function sdk(input: { }), ) spyOn(client.permission, "list").mockImplementation((request) => ok(input.permissions?.[request.sessionID] ?? [])) - spyOn(client.form, "list").mockImplementation((request) => ok(input.forms?.[request.sessionID] ?? [])) - spyOn(client.form.request, "list").mockImplementation(() => + spyOn(client.session.form, "list").mockImplementation((request) => ok(input.forms?.[request.sessionID] ?? [])) + spyOn(client.form, "list").mockImplementation(() => ok({ location: { directory: input.globalLocation?.directory ?? "/tmp", diff --git a/services/www/openapi.json b/services/www/openapi.json index f9c08e4bb08..630ff83a66d 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -3314,26 +3314,14 @@ } } } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } } }, - "description": "Cancel an inbox item that has not yet been delivered.", + "description": "Cancel an inbox item that has not yet been delivered. Unavailable items are a no-op.", "summary": "Cancel inbox input" - } - }, - "/api/session/{sessionID}/inbox/{inboxID}/steer": { - "post": { + }, + "patch": { "tags": ["session"], - "operationId": "session.inbox.steer", + "operationId": "session.inbox.update", "parameters": [ { "name": "sessionID", @@ -3400,82 +3388,25 @@ } } }, - "description": "Change a queued inbox item to steer delivery and wake session execution.", - "summary": "Steer queued item" - } - }, - "/api/session/{sessionID}/inbox/{inboxID}/queue": { - "post": { - "tags": ["session"], - "operationId": "session.inbox.queue", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "inboxID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } + "description": "Change a pending inbox item's delivery mode. Steering wakes session execution.", + "summary": "Update inbox item", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["delivery"], + "additionalProperties": false } } }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } - } - }, - "description": "Change a steered inbox item to queued delivery.", - "summary": "Queue steered item" + "required": true + } } }, "/api/experimental/session/{sessionID}/instructions/entries": { @@ -4222,10 +4153,456 @@ "summary": "Get session message" } }, - "/api/experimental/session/{sessionID}/environment": { + "/api/session/{sessionID}/form": { + "get": { + "tags": ["session"], + "operationId": "session.form.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, + "post": { + "tags": ["session"], + "operationId": "session.form.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + }, + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorEncoded" + } + } + } + } + }, + "description": "Create a form for a session.", + "summary": "Create session form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.CreatePayload" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": ["session"], + "operationId": "session.form.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Detail" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Retrieve a form and its current state for a session.", + "summary": "Get session form" + }, + "delete": { + "tags": ["session"], + "operationId": "session.form.cancel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" + } + } + } + } + }, + "description": "Cancel a pending form.", + "summary": "Cancel form" + } + }, + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": ["session"], + "operationId": "session.form.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerErrorEncoded" + }, + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" + } + } + } + } + }, + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/environment": { "put": { "tags": ["session"], - "operationId": "experimental.session.environment", + "operationId": "session.environment", "parameters": [ { "name": "sessionID", @@ -7012,10 +7389,10 @@ } } }, - "/api/form/request": { + "/api/form": { "get": { "tags": ["form"], - "operationId": "form.request.list", + "operationId": "form.list", "parameters": [ { "name": "location", @@ -7095,535 +7472,7 @@ } }, "description": "Retrieve pending forms for a location.", - "summary": "List pending form requests" - } - }, - "/api/session/{sessionID}/form": { - "get": { - "tags": ["form"], - "operationId": "session.form.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Form.Info" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - } - } - } - }, - "description": "Retrieve pending forms for a session.", - "summary": "List session forms" - }, - "post": { - "tags": ["form"], - "operationId": "session.form.create", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } - } - }, - "description": "Create a form for a session.", - "summary": "Create session form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.CreatePayload" - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/form/{formID}": { - "get": { - "tags": ["form"], - "operationId": "session.form.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Retrieve a form for a session.", - "summary": "Get session form" - } - }, - "/api/session/{sessionID}/form/{formID}/state": { - "get": { - "tags": ["form"], - "operationId": "session.form.state", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.State" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Retrieve the current state for a form.", - "summary": "Get form state" - } - }, - "/api/session/{sessionID}/form/{formID}/reply": { - "post": { - "tags": ["form"], - "operationId": "session.form.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "FormInvalidAnswerError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormInvalidAnswerErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" - } - } - } - } - }, - "description": "Submit an answer to a pending form.", - "summary": "Reply to form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.Reply" - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/form/{formID}/cancel": { - "post": { - "tags": ["form"], - "operationId": "session.form.cancel", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" - } - } - } - } - }, - "description": "Cancel a pending form.", - "summary": "Cancel form" + "summary": "List pending forms" } }, "/api/permission/request": { @@ -8203,7 +8052,7 @@ "schema": { "type": "object", "properties": { - "reply": { + "decision": { "$ref": "#/components/schemas/Permission.Reply" }, "message": { @@ -8217,7 +8066,7 @@ ] } }, - "required": ["reply"], + "required": ["decision"], "additionalProperties": false } } @@ -13909,12 +13758,38 @@ "$ref": "#/components/schemas/Form.Metadata" }, "fields": { - "$ref": "#/components/schemas/Form.Fields_2" + "$ref": "#/components/schemas/Form.Fields" } }, "required": ["title", "fields"], "additionalProperties": false }, + "Form.Detail": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^frm_" + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields" + }, + "state": { + "$ref": "#/components/schemas/Form.State" + } + }, + "required": ["id", "sessionID", "title", "fields", "state"], + "additionalProperties": false + }, "Form.ExternalField": { "type": "object", "properties": { @@ -14013,7 +13888,7 @@ "$ref": "#/components/schemas/Form.Metadata" }, "fields": { - "$ref": "#/components/schemas/Form.Fields_2" + "$ref": "#/components/schemas/Form.Fields" } }, "required": ["id", "sessionID", "title", "fields"], @@ -14995,7 +14870,7 @@ "type": "string" }, "form": { - "$ref": "#/components/schemas/Form.Fields_1" + "$ref": "#/components/schemas/Form.Fields_2" } }, "required": ["type"], @@ -15031,7 +14906,7 @@ "type": "string" }, "form": { - "$ref": "#/components/schemas/Form.Fields" + "$ref": "#/components/schemas/Form.Fields_1" } }, "required": ["id", "type", "label"], @@ -15888,9 +15763,22 @@ }, "resource": { "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + } + }, + "required": ["created", "updated"], + "additionalProperties": false } }, - "required": ["id", "projectID", "action", "resource"], + "required": ["id", "projectID", "action", "resource", "time"], "additionalProperties": false }, "PersistentPty.CreateInput": { @@ -19145,7 +19033,7 @@ }, { "name": "form", - "description": "Session form routes." + "description": "Location form routes." }, { "name": "permission", diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index f9c08e4bb08..630ff83a66d 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -3314,26 +3314,14 @@ } } } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } } }, - "description": "Cancel an inbox item that has not yet been delivered.", + "description": "Cancel an inbox item that has not yet been delivered. Unavailable items are a no-op.", "summary": "Cancel inbox input" - } - }, - "/api/session/{sessionID}/inbox/{inboxID}/steer": { - "post": { + }, + "patch": { "tags": ["session"], - "operationId": "session.inbox.steer", + "operationId": "session.inbox.update", "parameters": [ { "name": "sessionID", @@ -3400,82 +3388,25 @@ } } }, - "description": "Change a queued inbox item to steer delivery and wake session execution.", - "summary": "Steer queued item" - } - }, - "/api/session/{sessionID}/inbox/{inboxID}/queue": { - "post": { - "tags": ["session"], - "operationId": "session.inbox.queue", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - }, - { - "name": "inboxID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^msg_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } + "description": "Change a pending inbox item's delivery mode. Steering wakes session execution.", + "summary": "Update inbox item", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Session.Inbox.Delivery" + } + }, + "required": ["delivery"], + "additionalProperties": false } } }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } - } - }, - "description": "Change a steered inbox item to queued delivery.", - "summary": "Queue steered item" + "required": true + } } }, "/api/experimental/session/{sessionID}/instructions/entries": { @@ -4222,10 +4153,456 @@ "summary": "Get session message" } }, - "/api/experimental/session/{sessionID}/environment": { + "/api/session/{sessionID}/form": { + "get": { + "tags": ["session"], + "operationId": "session.form.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Form.Info" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Retrieve pending forms for a session.", + "summary": "List session forms" + }, + "post": { + "tags": ["session"], + "operationId": "session.form.create", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + }, + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictErrorEncoded" + } + } + } + } + }, + "description": "Create a form for a session.", + "summary": "Create session form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.CreatePayload" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/form/{formID}": { + "get": { + "tags": ["session"], + "operationId": "session.form.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Form.Detail" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + } + }, + "description": "Retrieve a form and its current state for a session.", + "summary": "Get session form" + }, + "delete": { + "tags": ["session"], + "operationId": "session.form.cancel", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" + } + } + } + } + }, + "description": "Cancel a pending form.", + "summary": "Cancel form" + } + }, + "/api/session/{sessionID}/form/{formID}/reply": { + "post": { + "tags": ["session"], + "operationId": "session.form.reply", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "formID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^frm_" + }, + "required": true + } + ], + "security": [], + "responses": { + "204": { + "description": "" + }, + "400": { + "description": "FormInvalidAnswerError | InvalidRequestError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormInvalidAnswerErrorEncoded" + }, + { + "$ref": "#/components/schemas/InvalidRequestErrorEncoded" + } + ] + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedErrorEncoded" + } + } + } + }, + "404": { + "description": "SessionNotFoundError | FormNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/FormNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + }, + { + "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" + } + ] + } + } + } + }, + "409": { + "description": "FormAlreadySettledError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" + } + } + } + } + }, + "description": "Submit an answer to a pending form.", + "summary": "Reply to form", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Form.Reply" + } + } + }, + "required": true + } + } + }, + "/api/session/{sessionID}/environment": { "put": { "tags": ["session"], - "operationId": "experimental.session.environment", + "operationId": "session.environment", "parameters": [ { "name": "sessionID", @@ -7012,10 +7389,10 @@ } } }, - "/api/form/request": { + "/api/form": { "get": { "tags": ["form"], - "operationId": "form.request.list", + "operationId": "form.list", "parameters": [ { "name": "location", @@ -7095,535 +7472,7 @@ } }, "description": "Retrieve pending forms for a location.", - "summary": "List pending form requests" - } - }, - "/api/session/{sessionID}/form": { - "get": { - "tags": ["form"], - "operationId": "session.form.list", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Form.Info" - } - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - } - } - } - }, - "description": "Retrieve pending forms for a session.", - "summary": "List session forms" - }, - "post": { - "tags": ["form"], - "operationId": "session.form.create", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "ConflictError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConflictErrorEncoded" - } - } - } - } - }, - "description": "Create a form for a session.", - "summary": "Create session form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.CreatePayload" - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/form/{formID}": { - "get": { - "tags": ["form"], - "operationId": "session.form.get", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.Info" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Retrieve a form for a session.", - "summary": "Get session form" - } - }, - "/api/session/{sessionID}/form/{formID}/state": { - "get": { - "tags": ["form"], - "operationId": "session.form.state", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Form.State" - } - }, - "required": ["data"], - "additionalProperties": false - } - } - } - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Retrieve the current state for a form.", - "summary": "Get form state" - } - }, - "/api/session/{sessionID}/form/{formID}/reply": { - "post": { - "tags": ["form"], - "operationId": "session.form.reply", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "FormInvalidAnswerError | InvalidRequestError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormInvalidAnswerErrorEncoded" - }, - { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - ] - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" - } - } - } - } - }, - "description": "Submit an answer to a pending form.", - "summary": "Reply to form", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Form.Reply" - } - } - }, - "required": true - } - } - }, - "/api/session/{sessionID}/form/{formID}/cancel": { - "post": { - "tags": ["form"], - "operationId": "session.form.cancel", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "formID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^frm_" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError | FormNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FormNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - }, - "409": { - "description": "FormAlreadySettledError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FormAlreadySettledErrorEncoded" - } - } - } - } - }, - "description": "Cancel a pending form.", - "summary": "Cancel form" + "summary": "List pending forms" } }, "/api/permission/request": { @@ -8203,7 +8052,7 @@ "schema": { "type": "object", "properties": { - "reply": { + "decision": { "$ref": "#/components/schemas/Permission.Reply" }, "message": { @@ -8217,7 +8066,7 @@ ] } }, - "required": ["reply"], + "required": ["decision"], "additionalProperties": false } } @@ -13909,12 +13758,38 @@ "$ref": "#/components/schemas/Form.Metadata" }, "fields": { - "$ref": "#/components/schemas/Form.Fields_2" + "$ref": "#/components/schemas/Form.Fields" } }, "required": ["title", "fields"], "additionalProperties": false }, + "Form.Detail": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^frm_" + }, + "sessionID": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/Form.Metadata" + }, + "fields": { + "$ref": "#/components/schemas/Form.Fields" + }, + "state": { + "$ref": "#/components/schemas/Form.State" + } + }, + "required": ["id", "sessionID", "title", "fields", "state"], + "additionalProperties": false + }, "Form.ExternalField": { "type": "object", "properties": { @@ -14013,7 +13888,7 @@ "$ref": "#/components/schemas/Form.Metadata" }, "fields": { - "$ref": "#/components/schemas/Form.Fields_2" + "$ref": "#/components/schemas/Form.Fields" } }, "required": ["id", "sessionID", "title", "fields"], @@ -14995,7 +14870,7 @@ "type": "string" }, "form": { - "$ref": "#/components/schemas/Form.Fields_1" + "$ref": "#/components/schemas/Form.Fields_2" } }, "required": ["type"], @@ -15031,7 +14906,7 @@ "type": "string" }, "form": { - "$ref": "#/components/schemas/Form.Fields" + "$ref": "#/components/schemas/Form.Fields_1" } }, "required": ["id", "type", "label"], @@ -15888,9 +15763,22 @@ }, "resource": { "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + } + }, + "required": ["created", "updated"], + "additionalProperties": false } }, - "required": ["id", "projectID", "action", "resource"], + "required": ["id", "projectID", "action", "resource", "time"], "additionalProperties": false }, "PersistentPty.CreateInput": { @@ -19145,7 +19033,7 @@ }, { "name": "form", - "description": "Session form routes." + "description": "Location form routes." }, { "name": "permission",