feat(core): add session form service (#34855)

This commit is contained in:
Aiden Cline 2026-07-02 17:15:18 -05:00 committed by GitHub
commit 7ebd344fa2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 7862 additions and 5223 deletions

View file

@ -0,0 +1,136 @@
import { Form } from "@opencode-ai/core/form"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import {
ConflictError,
FormAlreadySettledError,
FormInvalidAnswerError,
FormNotFoundError,
InvalidRequestError,
} from "@opencode-ai/protocol/errors"
import { Api } from "../api"
import { response } from "../location"
function missingForm(id: Form.ID) {
return new FormNotFoundError({ id, message: `Form not found: ${id}` })
}
function alreadySettled(error: Form.AlreadySettledError) {
return new FormAlreadySettledError({ id: error.id, message: error.message })
}
function alreadyExists(error: Form.AlreadyExistsError) {
return new ConflictError({ resource: error.id, message: error.message })
}
function invalidAnswer(error: Form.InvalidAnswerError) {
return new FormInvalidAnswerError({ id: error.id, message: error.message })
}
export const FormHandler = HttpApiBuilder.group(Api, "server.form", (handlers) =>
Effect.gen(function* () {
const withOwnedForm = Effect.fnUntraced(function* <A, E>(
sessionID: string,
formID: Form.ID,
use: (service: Form.Interface, info: Form.Info) => Effect.Effect<A, E>,
) {
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 yield* use(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 form = yield* Form.Service
return yield* response(form.list({ sessionID: ctx.params.sessionID }))
}),
)
.handle(
"session.form.create",
Effect.fn(function* (ctx) {
const form = yield* Form.Service
if (ctx.payload.mode === "form") {
if (!ctx.payload.fields) {
return yield* new InvalidRequestError({ message: "Form fields are required", field: "fields" })
}
return yield* response(
form.create({
id: ctx.payload.id,
sessionID: ctx.params.sessionID,
title: ctx.payload.title,
metadata: ctx.payload.metadata,
mode: "form",
fields: ctx.payload.fields,
}).pipe(Effect.catchTag("Form.AlreadyExistsError", alreadyExists)),
)
}
if (!ctx.payload.url) return yield* new InvalidRequestError({ message: "Form URL is required", field: "url" })
return yield* response(
form.create({
id: ctx.payload.id,
sessionID: ctx.params.sessionID,
title: ctx.payload.title,
metadata: ctx.payload.metadata,
mode: "url",
url: ctx.payload.url,
}).pipe(Effect.catchTag("Form.AlreadyExistsError", alreadyExists)),
)
}),
)
.handle(
"session.form.get",
Effect.fn(function* (ctx) {
return yield* response(withOwnedForm(ctx.params.sessionID, ctx.params.formID, (_, info) => Effect.succeed(info)))
}),
)
.handle(
"session.form.state",
Effect.fn(function* (ctx) {
return yield* response(
withOwnedForm(ctx.params.sessionID, ctx.params.formID, (form) =>
form.state(ctx.params.formID).pipe(Effect.catchTag("Form.NotFoundError", () => missingForm(ctx.params.formID))),
),
)
}),
)
.handle(
"session.form.reply",
Effect.fn(function* (ctx) {
yield* withOwnedForm(ctx.params.sessionID, ctx.params.formID, (form) =>
form.reply({ id: ctx.params.formID, answer: ctx.payload.answer }).pipe(
Effect.catchTags({
"Form.AlreadySettledError": alreadySettled,
"Form.InvalidAnswerError": invalidAnswer,
"Form.NotFoundError": () => missingForm(ctx.params.formID),
}),
),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.form.cancel",
Effect.fn(function* (ctx) {
yield* withOwnedForm(ctx.params.sessionID, ctx.params.formID, (form) =>
form.cancel(ctx.params.formID).pipe(
Effect.catchTags({
"Form.AlreadySettledError": alreadySettled,
"Form.NotFoundError": () => missingForm(ctx.params.formID),
}),
),
)
return HttpApiSchema.NoContent.make()
}),
)
}),
)

View file

@ -1,62 +0,0 @@
import { QuestionV2 } from "@opencode-ai/core/question"
import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { QuestionNotFoundError } from "@opencode-ai/protocol/errors"
import { response } from "../location"
function missingRequest(id: QuestionV2.ID) {
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
}
export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (handlers) =>
Effect.gen(function* () {
const withOwnedQuestion = Effect.fnUntraced(function* <A, E>(
sessionID: QuestionV2.Request["sessionID"],
requestID: QuestionV2.ID,
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
) {
const question = yield* QuestionV2.Service
const request = (yield* question.list()).find((request) => request.id === requestID)
if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID)
return yield* use(question)
})
return handlers
.handle(
"question.request.list",
Effect.fn(function* () {
return yield* response((yield* QuestionV2.Service).list())
}),
)
.handle(
"session.question.list",
Effect.fn(function* (ctx) {
const requests = yield* (yield* QuestionV2.Service).list()
return { data: requests.filter((request) => request.sessionID === ctx.params.sessionID) }
}),
)
.handle(
"session.question.reply",
Effect.fn(function* (ctx) {
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
question
.reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers })
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
)
return HttpApiSchema.NoContent.make()
}),
)
.handle(
"session.question.reject",
Effect.fn(function* (ctx) {
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
question
.reject(ctx.params.requestID)
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
)
return HttpApiSchema.NoContent.make()
}),
)
}),
)