Compare commits

...

3 Commits

Author SHA1 Message Date
Aiden Cline e2fe96eefb refactor(core): drive questions through forms 2026-07-02 00:33:31 -05:00
Aiden Cline dda0d82298 Merge remote-tracking branch 'origin/v2' into form-service
# Conflicts:
#	packages/protocol/src/api.ts
#	packages/schema/src/event-manifest.ts
#	packages/schema/src/index.ts
#	packages/schema/test/event-manifest.test.ts
2026-07-02 00:00:31 -05:00
Aiden Cline 23699db968 feat(core): add session form service 2026-07-01 23:49:14 -05:00
54 changed files with 3875 additions and 1598 deletions
@@ -5,7 +5,6 @@ import type {
PermissionRequest,
Project,
ProviderAuthResponse,
QuestionRequest,
ReferenceInfo,
Session,
} from "@opencode-ai/sdk/v2/client"
@@ -22,6 +21,7 @@ import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
import { isQuestionForm, type QuestionForm } from "@/utils/question-form"
type GlobalStore = {
ready: boolean
@@ -319,9 +319,10 @@ export async function bootstrapDirectory(input: {
),
() =>
retry(() =>
input.sdk.question.list().then((x) => {
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
input.sdk.v2.form.request.list().then((x) => {
const forms: QuestionForm[] = (x.data?.data ?? []).flatMap((form) => (isQuestionForm(form) ? [form] : []))
const ids = forms.map((question) => question.sessionID)
const grouped = groupBySession(forms)
const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk })
@@ -1,8 +1,9 @@
import { describe, expect, test } from "bun:test"
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, PermissionRequest, Project, Session } from "@opencode-ai/sdk/v2/client"
import { createStore } from "solid-js/store"
import type { State } from "./types"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
import type { QuestionForm } from "@/utils/question-form"
const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
({
@@ -48,14 +49,18 @@ const questionRequest = (id: string, sessionID: string, title = id) =>
({
id,
sessionID,
questions: [
mode: "form",
metadata: { kind: "question" },
fields: [
{
question: title,
header: title,
options: [{ label: title, description: title }],
key: "question_0",
title,
description: title,
type: "string",
options: [{ value: title, label: title, description: title }],
},
],
}) as QuestionRequest
}) as QuestionForm
const baseState = (input: Partial<State> = {}) =>
({
@@ -505,7 +510,7 @@ describe("applyDirectoryEvent", () => {
expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"])
applyDirectoryEvent({
event: { type: "question.asked", properties: questionRequest("q_2", sessionID) },
event: { type: "form.created", properties: { form: questionRequest("q_2", sessionID) } },
store,
setStore,
push() {},
@@ -515,17 +520,17 @@ describe("applyDirectoryEvent", () => {
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"])
applyDirectoryEvent({
event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") },
event: { type: "form.created", properties: { form: questionRequest("q_2", sessionID, "updated") } },
store,
setStore,
push() {},
directory: "/tmp",
loadLsp() {},
})
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated")
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.fields[0]?.description).toBe("updated")
applyDirectoryEvent({
event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } },
event: { type: "form.cancelled", properties: { sessionID, id: "q_2" } },
store,
setStore,
push() {},
@@ -5,7 +5,6 @@ import type {
Part,
PermissionRequest,
Project,
QuestionRequest,
Session,
SessionStatus,
SnapshotFileDiff,
@@ -15,6 +14,7 @@ import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"
import { isQuestionForm } from "@/utils/question-form"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const SESSION_CONTENT_EVENTS = new Set([
@@ -28,9 +28,9 @@ const SESSION_CONTENT_EVENTS = new Set([
"message.part.delta",
"permission.asked",
"permission.replied",
"question.asked",
"question.replied",
"question.rejected",
"form.created",
"form.replied",
"form.cancelled",
])
export function applyGlobalEvent(input: {
@@ -364,8 +364,10 @@ export function applyDirectoryEvent(input: {
)
break
}
case "question.asked": {
const question = event.properties as QuestionRequest
case "form.created": {
const properties = event.properties as { form?: unknown }
if (!isQuestionForm(properties.form)) break
const question = properties.form
const questions = input.store.question[question.sessionID]
if (!questions) {
input.setStore("question", question.sessionID, [question])
@@ -385,12 +387,12 @@ export function applyDirectoryEvent(input: {
)
break
}
case "question.replied":
case "question.rejected": {
const props = event.properties as { sessionID: string; requestID: string }
case "form.replied":
case "form.cancelled": {
const props = event.properties as { sessionID: string; id: string }
const questions = input.store.question[props.sessionID]
if (!questions) break
const result = Binary.search(questions, props.requestID, (q) => q.id)
const result = Binary.search(questions, props.id, (q) => q.id)
if (!result.found) break
input.setStore(
"question",
@@ -3,12 +3,12 @@ import type {
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
import type { QuestionForm } from "@/utils/question-form"
const msg = (id: string, sessionID: string) =>
({
@@ -38,7 +38,7 @@ describe("app session cache", () => {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
question: Record<string, QuestionForm[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: { ses_1: { type: "busy" } as SessionStatus },
@@ -47,7 +47,7 @@ describe("app session cache", () => {
message: {},
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
permission: { ses_1: [] as PermissionRequest[] },
question: { ses_1: [] as QuestionRequest[] },
question: { ses_1: [] as QuestionForm[] },
part_text_accum_delta: { prt_1: "streamed text" },
}
@@ -72,7 +72,7 @@ describe("app session cache", () => {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
question: Record<string, QuestionForm[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: {},
@@ -2,11 +2,11 @@ import type {
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { QuestionForm } from "@/utils/question-form"
export const SESSION_CACHE_LIMIT = 40
@@ -17,7 +17,7 @@ type SessionCache = {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
question: Record<string, QuestionForm[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
}
@@ -9,7 +9,6 @@ import type {
Part,
Path,
PermissionRequest,
QuestionRequest,
ReferenceInfo,
Session,
SessionStatus,
@@ -17,6 +16,7 @@ import type {
Todo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
import type { QuestionForm } from "@/utils/question-form"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import type { Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
@@ -60,7 +60,7 @@ export type State = {
[sessionID: string]: PermissionRequest[]
}
question: {
[sessionID: string]: QuestionRequest[]
[sessionID: string]: QuestionForm[]
}
mcp_ready: boolean
mcp: {
+10 -8
View File
@@ -5,12 +5,12 @@ import type {
OpencodeClient,
Part,
PermissionRequest,
QuestionRequest,
Session,
SessionStatus,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { isQuestionForm, type QuestionForm } from "@/utils/question-form"
import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs"
@@ -136,7 +136,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
session_diff: {} as Record<string, SnapshotFileDiff[]>,
todo: {} as Record<string, Todo[]>,
permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionRequest[]>,
question: {} as Record<string, QuestionForm[]>,
message: {} as Record<string, Message[]>,
part: {} as Record<string, Part[]>,
part_text_accum_delta: {} as Record<string, string>,
@@ -932,8 +932,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
)
return
}
case "question.asked": {
const question = event.properties as QuestionRequest
case "form.created": {
const properties = event.properties as { form?: unknown }
if (!isQuestionForm(properties.form)) return
const question = properties.form
const questions = data.question[question.sessionID]
if (!questions) {
setData("question", question.sessionID, [question])
@@ -949,15 +951,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
)
return
}
case "question.replied":
case "question.rejected": {
const props = event.properties as { sessionID: string; requestID: string }
case "form.replied":
case "form.cancelled": {
const props = event.properties as { sessionID: string; id: string }
setData(
"question",
props.sessionID,
produce((draft) => {
if (!draft) return
const result = Binary.search(draft, props.requestID, (item) => item.id)
const result = Binary.search(draft, props.id, (item) => item.id)
if (result.found) draft.splice(result.index, 1)
}),
)
+6 -8
View File
@@ -48,6 +48,7 @@ import { setNavigate } from "@/utils/notification-click"
import { Worktree as WorktreeState } from "@/utils/worktree"
import { setSessionHandoff } from "@/pages/session/handoff"
import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope"
import { isQuestionForm } from "@/utils/question-form"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context"
@@ -403,25 +404,22 @@ export default function LegacyLayout(props: ParentProps) {
return
}
if (
e.details?.type === "question.replied" ||
e.details?.type === "question.rejected" ||
e.details?.type === "permission.replied"
) {
if (e.details?.type === "form.replied" || e.details?.type === "form.cancelled" || e.details?.type === "permission.replied") {
const props = e.details.properties as { sessionID: string }
const sessionKey = `${e.name}:${props.sessionID}`
dismissSessionAlert(sessionKey)
return
}
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
const questionForm = e.details?.type === "form.created" && isQuestionForm(e.details.properties?.form) ? e.details.properties.form : undefined
if (e.details?.type !== "permission.asked" && !questionForm) return
const title =
e.details.type === "permission.asked"
? language.t("notification.permission.title")
: language.t("notification.question.title")
const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const)
const directory = e.name
const props = e.details.properties
const props = questionForm ?? (e.details.properties as { sessionID: string })
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
const [store] = serverSync().child(directory, { bootstrap: false })
@@ -450,7 +448,7 @@ export default function LegacyLayout(props: ParentProps) {
}
}
if (e.details.type === "question.asked") {
if (questionForm) {
if (settings.notifications.agent()) {
void platform.notify(title, description, href)
}
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { QuestionForm } from "@/utils/question-form"
import { todoDockAtBoundary, todoState } from "./session-composer-state"
import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
@@ -19,8 +20,10 @@ const question = (id: string, sessionID: string) =>
({
id,
sessionID,
questions: [],
}) as QuestionRequest
mode: "form",
metadata: { kind: "question" },
fields: [],
}) as QuestionForm
describe("sessionPermissionRequest", () => {
test("prefers the current session permission", () => {
@@ -1,6 +1,7 @@
import { createEffect, createMemo, on, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { PermissionRequest, QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
import type { PermissionRequest, Todo } from "@opencode-ai/sdk/v2"
import type { QuestionForm } from "@/utils/question-form"
import { useParams } from "@solidjs/router"
import { showToast } from "@/utils/toast"
import { useServerSync } from "@/context/server-sync"
@@ -33,7 +34,7 @@ export function createSessionComposerController(options?: { closeMs?: number | (
const language = useLanguage()
const permission = usePermission()
const questionRequest = createMemo((): QuestionRequest | undefined => {
const questionRequest = createMemo((): QuestionForm | undefined => {
return sessionQuestionRequest(sync().data.session, sync().data.question, params.id)
})
@@ -6,13 +6,13 @@ import { DockPrompt } from "@opencode-ai/session-ui/dock-prompt"
import { Icon } from "@opencode-ai/ui/icon"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { showToast } from "@/utils/toast"
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { useServerSDK } from "@/context/server-sdk"
import { ScopedKey } from "@/utils/server-scope"
import { questionAnswer, type QuestionAnswer, type QuestionForm } from "@/utils/question-form"
const cache = new Map<string, { tab: number; answers: QuestionAnswer[]; custom: string[]; customOn: boolean[] }>()
@@ -61,13 +61,13 @@ function Option(props: {
)
}
export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit: () => void }> = (props) => {
export const SessionQuestionDock: Component<{ request: QuestionForm; onSubmit: () => void }> = (props) => {
const sdk = useSDK()
const serverSDK = useServerSDK()
const language = useLanguage()
const cacheKey = ScopedKey.from(serverSDK().scope, props.request.id)
const questions = createMemo(() => props.request.questions)
const questions = createMemo(() => props.request.fields)
const total = createMemo(() => questions().length)
const cached = cache.get(cacheKey)
@@ -93,7 +93,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
const options = createMemo(() => question()?.options ?? [])
const input = createMemo(() => store.custom[store.tab] ?? "")
const on = createMemo(() => store.customOn[store.tab] === true)
const multi = createMemo(() => question()?.multiple === true)
const multi = createMemo(() => question()?.type === "multiselect")
const count = createMemo(() => options().length + 1)
const summary = createMemo(() => {
@@ -223,7 +223,12 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
}
const replyMutation = useMutation(() => ({
mutationFn: (answers: QuestionAnswer[]) => sdk().client.question.reply({ requestID: props.request.id, answers }),
mutationFn: (answers: QuestionAnswer[]) =>
sdk().client.v2.session.form.reply({
sessionID: props.request.sessionID,
formID: props.request.id,
formReply: { answer: questionAnswer(props.request.fields, answers) },
}),
onMutate: () => {
props.onSubmit()
},
@@ -235,7 +240,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
}))
const rejectMutation = useMutation(() => ({
mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }),
mutationFn: () => sdk().client.v2.session.form.cancel({ sessionID: props.request.sessionID, formID: props.request.id }),
onMutate: () => {
props.onSubmit()
},
@@ -526,7 +531,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
overflow: store.minimized ? "hidden" : undefined,
}}
>
{question()?.question}
{question()?.title}
</div>
<Show when={!store.minimized}>
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
@@ -1,4 +1,5 @@
import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client"
import type { QuestionForm } from "@/utils/question-form"
function sessionTreeRequest<T>(
session: Session[],
@@ -44,9 +45,9 @@ export function sessionPermissionRequest(
export function sessionQuestionRequest(
session: Session[],
request: Record<string, QuestionRequest[] | undefined>,
request: Record<string, QuestionForm[] | undefined>,
sessionID?: string,
include?: (item: QuestionRequest) => boolean,
include?: (item: QuestionForm) => boolean,
) {
return sessionTreeRequest(session, request, sessionID, include)
}
+49
View File
@@ -0,0 +1,49 @@
export type QuestionOption = {
value: string
label: string
description?: string
}
export type QuestionField = {
key: string
title?: string
description?: string
type: "string" | "multiselect"
options?: QuestionOption[]
custom?: boolean
}
export type QuestionForm = {
id: string
sessionID: string
mode: "form"
metadata?: { [key: string]: unknown }
fields: QuestionField[]
}
export type QuestionAnswer = string[]
export function isQuestionForm(value: unknown): value is QuestionForm {
if (typeof value !== "object" || value === null) return false
const form = value as { mode?: unknown; metadata?: unknown; fields?: unknown }
if (form.mode !== "form") return false
if (typeof form.metadata !== "object" || form.metadata === null) return false
if ((form.metadata as { kind?: unknown }).kind !== "question") return false
return Array.isArray(form.fields) && form.fields.every(isQuestionField)
}
function isQuestionField(value: unknown): value is QuestionField {
if (typeof value !== "object" || value === null) return false
const field = value as { type?: unknown }
return field.type === "string" || field.type === "multiselect"
}
export function questionAnswer(fields: ReadonlyArray<QuestionField>, answers: ReadonlyArray<QuestionAnswer>) {
const entries = fields.flatMap((field, index): ReadonlyArray<readonly [string, string | string[]]> => {
const answer = answers[index] ?? []
if (answer.length === 0) return []
if (field.type === "multiselect") return [[field.key, answer]]
return [[field.key, answer[0] ?? ""]]
})
return Object.fromEntries(entries)
}
-1
View File
@@ -14,7 +14,6 @@ export { Project } from "@opencode-ai/schema/project"
export { ProjectCopy } from "@opencode-ai/schema/project-copy"
export { Provider } from "@opencode-ai/schema/provider"
export { Pty } from "@opencode-ai/schema/pty"
export { Question } from "@opencode-ai/schema/question"
export { Reference } from "@opencode-ai/schema/reference"
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
export { Session } from "@opencode-ai/schema/session"
+242 -184
View File
@@ -491,36 +491,136 @@ const adaptGroup12 = (raw: RawClient["server.project"]) => ({
directories: Endpoint12_1(raw),
})
type Endpoint13_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
const Endpoint13_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint13_0Input) =>
const Endpoint13_0 = (raw: RawClient["server.form"]) => (input?: Endpoint13_0Input) =>
raw["form.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
type Endpoint13_1Input = {
readonly sessionID: Endpoint13_1Request["params"]["sessionID"]
readonly location?: Endpoint13_1Request["query"]["location"]
}
const Endpoint13_1 = (raw: RawClient["server.form"]) => (input: Endpoint13_1Input) =>
raw["session.form.list"]({ params: { sessionID: input["sessionID"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.create"]>[0]
type Endpoint13_2Input = {
readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
readonly location?: Endpoint13_2Request["query"]["location"]
readonly id?: Endpoint13_2Request["payload"]["id"]
readonly title?: Endpoint13_2Request["payload"]["title"]
readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
readonly mode: Endpoint13_2Request["payload"]["mode"]
readonly fields?: Endpoint13_2Request["payload"]["fields"]
readonly url?: Endpoint13_2Request["payload"]["url"]
}
const Endpoint13_2 = (raw: RawClient["server.form"]) => (input: Endpoint13_2Input) =>
raw["session.form.create"]({
params: { sessionID: input["sessionID"] },
query: { location: input["location"] },
payload: {
id: input["id"],
title: input["title"],
metadata: input["metadata"],
mode: input["mode"],
fields: input["fields"],
url: input["url"],
},
}).pipe(Effect.mapError(mapClientError))
type Endpoint13_3Request = Parameters<RawClient["server.form"]["session.form.get"]>[0]
type Endpoint13_3Input = {
readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
readonly formID: Endpoint13_3Request["params"]["formID"]
readonly location?: Endpoint13_3Request["query"]["location"]
}
const Endpoint13_3 = (raw: RawClient["server.form"]) => (input: Endpoint13_3Input) =>
raw["session.form.get"]({
params: { sessionID: input["sessionID"], formID: input["formID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint13_4Request = Parameters<RawClient["server.form"]["session.form.state"]>[0]
type Endpoint13_4Input = {
readonly sessionID: Endpoint13_4Request["params"]["sessionID"]
readonly formID: Endpoint13_4Request["params"]["formID"]
readonly location?: Endpoint13_4Request["query"]["location"]
}
const Endpoint13_4 = (raw: RawClient["server.form"]) => (input: Endpoint13_4Input) =>
raw["session.form.state"]({
params: { sessionID: input["sessionID"], formID: input["formID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint13_5Request = Parameters<RawClient["server.form"]["session.form.reply"]>[0]
type Endpoint13_5Input = {
readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
readonly formID: Endpoint13_5Request["params"]["formID"]
readonly location?: Endpoint13_5Request["query"]["location"]
readonly answer: Endpoint13_5Request["payload"]["answer"]
}
const Endpoint13_5 = (raw: RawClient["server.form"]) => (input: Endpoint13_5Input) =>
raw["session.form.reply"]({
params: { sessionID: input["sessionID"], formID: input["formID"] },
query: { location: input["location"] },
payload: { answer: input["answer"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint13_6Request = Parameters<RawClient["server.form"]["session.form.cancel"]>[0]
type Endpoint13_6Input = {
readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
readonly formID: Endpoint13_6Request["params"]["formID"]
readonly location?: Endpoint13_6Request["query"]["location"]
}
const Endpoint13_6 = (raw: RawClient["server.form"]) => (input: Endpoint13_6Input) =>
raw["session.form.cancel"]({
params: { sessionID: input["sessionID"], formID: input["formID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup13 = (raw: RawClient["server.form"]) => ({
listRequests: Endpoint13_0(raw),
list: Endpoint13_1(raw),
create: Endpoint13_2(raw),
get: Endpoint13_3(raw),
state: Endpoint13_4(raw),
reply: Endpoint13_5(raw),
cancel: Endpoint13_6(raw),
})
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
const Endpoint14_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_0Input) =>
raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint13_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
type Endpoint13_1Input = { readonly projectID?: Endpoint13_1Request["query"]["projectID"] }
const Endpoint13_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint13_1Input) =>
type Endpoint14_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["query"]["projectID"] }
const Endpoint14_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint14_1Input) =>
raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint13_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
type Endpoint13_2Input = { readonly id: Endpoint13_2Request["params"]["id"] }
const Endpoint13_2 = (raw: RawClient["server.permission"]) => (input: Endpoint13_2Input) =>
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
const Endpoint14_2 = (raw: RawClient["server.permission"]) => (input: Endpoint14_2Input) =>
raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint13_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
type Endpoint13_3Input = {
readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
readonly id?: Endpoint13_3Request["payload"]["id"]
readonly action: Endpoint13_3Request["payload"]["action"]
readonly resources: Endpoint13_3Request["payload"]["resources"]
readonly save?: Endpoint13_3Request["payload"]["save"]
readonly metadata?: Endpoint13_3Request["payload"]["metadata"]
readonly source?: Endpoint13_3Request["payload"]["source"]
readonly agent?: Endpoint13_3Request["payload"]["agent"]
type Endpoint14_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
type Endpoint14_3Input = {
readonly sessionID: Endpoint14_3Request["params"]["sessionID"]
readonly id?: Endpoint14_3Request["payload"]["id"]
readonly action: Endpoint14_3Request["payload"]["action"]
readonly resources: Endpoint14_3Request["payload"]["resources"]
readonly save?: Endpoint14_3Request["payload"]["save"]
readonly metadata?: Endpoint14_3Request["payload"]["metadata"]
readonly source?: Endpoint14_3Request["payload"]["source"]
readonly agent?: Endpoint14_3Request["payload"]["agent"]
}
const Endpoint13_3 = (raw: RawClient["server.permission"]) => (input: Endpoint13_3Input) =>
const Endpoint14_3 = (raw: RawClient["server.permission"]) => (input: Endpoint14_3Input) =>
raw["session.permission.create"]({
params: { sessionID: input["sessionID"] },
payload: {
@@ -537,87 +637,87 @@ const Endpoint13_3 = (raw: RawClient["server.permission"]) => (input: Endpoint13
Effect.map((value) => value.data),
)
type Endpoint13_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
type Endpoint13_4Input = { readonly sessionID: Endpoint13_4Request["params"]["sessionID"] }
const Endpoint13_4 = (raw: RawClient["server.permission"]) => (input: Endpoint13_4Input) =>
type Endpoint14_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
type Endpoint14_4Input = { readonly sessionID: Endpoint14_4Request["params"]["sessionID"] }
const Endpoint14_4 = (raw: RawClient["server.permission"]) => (input: Endpoint14_4Input) =>
raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint13_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
type Endpoint13_5Input = {
readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
readonly requestID: Endpoint13_5Request["params"]["requestID"]
type Endpoint14_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
type Endpoint14_5Input = {
readonly sessionID: Endpoint14_5Request["params"]["sessionID"]
readonly requestID: Endpoint14_5Request["params"]["requestID"]
}
const Endpoint13_5 = (raw: RawClient["server.permission"]) => (input: Endpoint13_5Input) =>
const Endpoint14_5 = (raw: RawClient["server.permission"]) => (input: Endpoint14_5Input) =>
raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint13_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
type Endpoint13_6Input = {
readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
readonly requestID: Endpoint13_6Request["params"]["requestID"]
readonly reply: Endpoint13_6Request["payload"]["reply"]
readonly message?: Endpoint13_6Request["payload"]["message"]
type Endpoint14_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
type Endpoint14_6Input = {
readonly sessionID: Endpoint14_6Request["params"]["sessionID"]
readonly requestID: Endpoint14_6Request["params"]["requestID"]
readonly reply: Endpoint14_6Request["payload"]["reply"]
readonly message?: Endpoint14_6Request["payload"]["message"]
}
const Endpoint13_6 = (raw: RawClient["server.permission"]) => (input: Endpoint13_6Input) =>
const Endpoint14_6 = (raw: RawClient["server.permission"]) => (input: Endpoint14_6Input) =>
raw["session.permission.reply"]({
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
payload: { reply: input["reply"], message: input["message"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup13 = (raw: RawClient["server.permission"]) => ({
listRequests: Endpoint13_0(raw),
listSaved: Endpoint13_1(raw),
removeSaved: Endpoint13_2(raw),
create: Endpoint13_3(raw),
list: Endpoint13_4(raw),
get: Endpoint13_5(raw),
reply: Endpoint13_6(raw),
const adaptGroup14 = (raw: RawClient["server.permission"]) => ({
listRequests: Endpoint14_0(raw),
listSaved: Endpoint14_1(raw),
removeSaved: Endpoint14_2(raw),
create: Endpoint14_3(raw),
list: Endpoint14_4(raw),
get: Endpoint14_5(raw),
reply: Endpoint14_6(raw),
})
type Endpoint14_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
type Endpoint14_0Input = {
readonly location?: Endpoint14_0Request["query"]["location"]
readonly path?: Endpoint14_0Request["query"]["path"]
type Endpoint15_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
type Endpoint15_0Input = {
readonly location?: Endpoint15_0Request["query"]["location"]
readonly path?: Endpoint15_0Request["query"]["path"]
}
const Endpoint14_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint14_0Input) =>
const Endpoint15_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint15_0Input) =>
raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint14_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
type Endpoint14_1Input = {
readonly location?: Endpoint14_1Request["query"]["location"]
readonly query: Endpoint14_1Request["query"]["query"]
readonly type?: Endpoint14_1Request["query"]["type"]
readonly limit?: Endpoint14_1Request["query"]["limit"]
type Endpoint15_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
type Endpoint15_1Input = {
readonly location?: Endpoint15_1Request["query"]["location"]
readonly query: Endpoint15_1Request["query"]["query"]
readonly type?: Endpoint15_1Request["query"]["type"]
readonly limit?: Endpoint15_1Request["query"]["limit"]
}
const Endpoint14_1 = (raw: RawClient["server.fs"]) => (input: Endpoint14_1Input) =>
const Endpoint15_1 = (raw: RawClient["server.fs"]) => (input: Endpoint15_1Input) =>
raw["fs.find"]({
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
}).pipe(Effect.mapError(mapClientError))
const adaptGroup14 = (raw: RawClient["server.fs"]) => ({ list: Endpoint14_0(raw), find: Endpoint14_1(raw) })
const adaptGroup15 = (raw: RawClient["server.fs"]) => ({ list: Endpoint15_0(raw), find: Endpoint15_1(raw) })
type Endpoint15_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
const Endpoint15_0 = (raw: RawClient["server.command"]) => (input?: Endpoint15_0Input) =>
type Endpoint16_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
const Endpoint16_0 = (raw: RawClient["server.command"]) => (input?: Endpoint16_0Input) =>
raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup15 = (raw: RawClient["server.command"]) => ({ list: Endpoint15_0(raw) })
const adaptGroup16 = (raw: RawClient["server.command"]) => ({ list: Endpoint16_0(raw) })
type Endpoint16_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
const Endpoint16_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint16_0Input) =>
type Endpoint17_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
const Endpoint17_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint17_0Input) =>
raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
const adaptGroup16 = (raw: RawClient["server.skill"]) => ({ list: Endpoint16_0(raw) })
const adaptGroup17 = (raw: RawClient["server.skill"]) => ({ list: Endpoint17_0(raw) })
const Endpoint17_0 = (raw: RawClient["server.event"]) => () =>
const Endpoint18_0 = (raw: RawClient["server.event"]) => () =>
Stream.unwrap(
raw["event.subscribe"]({}).pipe(
Effect.mapError(mapClientError),
@@ -625,23 +725,23 @@ const Endpoint17_0 = (raw: RawClient["server.event"]) => () =>
),
)
const adaptGroup17 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint17_0(raw) })
const adaptGroup18 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint18_0(raw) })
type Endpoint18_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
const Endpoint18_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_0Input) =>
type Endpoint19_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
const Endpoint19_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_0Input) =>
raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint18_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
type Endpoint18_1Input = {
readonly location?: Endpoint18_1Request["query"]["location"]
readonly command?: Endpoint18_1Request["payload"]["command"]
readonly args?: Endpoint18_1Request["payload"]["args"]
readonly cwd?: Endpoint18_1Request["payload"]["cwd"]
readonly title?: Endpoint18_1Request["payload"]["title"]
readonly env?: Endpoint18_1Request["payload"]["env"]
type Endpoint19_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
type Endpoint19_1Input = {
readonly location?: Endpoint19_1Request["query"]["location"]
readonly command?: Endpoint19_1Request["payload"]["command"]
readonly args?: Endpoint19_1Request["payload"]["args"]
readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
readonly title?: Endpoint19_1Request["payload"]["title"]
readonly env?: Endpoint19_1Request["payload"]["env"]
}
const Endpoint18_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_1Input) =>
const Endpoint19_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint19_1Input) =>
raw["pty.create"]({
query: { location: input?.["location"] },
payload: {
@@ -653,148 +753,106 @@ const Endpoint18_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint18_1Inpu
},
}).pipe(Effect.mapError(mapClientError))
type Endpoint18_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
type Endpoint18_2Input = {
readonly ptyID: Endpoint18_2Request["params"]["ptyID"]
readonly location?: Endpoint18_2Request["query"]["location"]
type Endpoint19_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
type Endpoint19_2Input = {
readonly ptyID: Endpoint19_2Request["params"]["ptyID"]
readonly location?: Endpoint19_2Request["query"]["location"]
}
const Endpoint18_2 = (raw: RawClient["server.pty"]) => (input: Endpoint18_2Input) =>
const Endpoint19_2 = (raw: RawClient["server.pty"]) => (input: Endpoint19_2Input) =>
raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint18_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
type Endpoint18_3Input = {
readonly ptyID: Endpoint18_3Request["params"]["ptyID"]
readonly location?: Endpoint18_3Request["query"]["location"]
readonly title?: Endpoint18_3Request["payload"]["title"]
readonly size?: Endpoint18_3Request["payload"]["size"]
type Endpoint19_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
type Endpoint19_3Input = {
readonly ptyID: Endpoint19_3Request["params"]["ptyID"]
readonly location?: Endpoint19_3Request["query"]["location"]
readonly title?: Endpoint19_3Request["payload"]["title"]
readonly size?: Endpoint19_3Request["payload"]["size"]
}
const Endpoint18_3 = (raw: RawClient["server.pty"]) => (input: Endpoint18_3Input) =>
const Endpoint19_3 = (raw: RawClient["server.pty"]) => (input: Endpoint19_3Input) =>
raw["pty.update"]({
params: { ptyID: input["ptyID"] },
query: { location: input["location"] },
payload: { title: input["title"], size: input["size"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint18_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
type Endpoint18_4Input = {
readonly ptyID: Endpoint18_4Request["params"]["ptyID"]
readonly location?: Endpoint18_4Request["query"]["location"]
type Endpoint19_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
type Endpoint19_4Input = {
readonly ptyID: Endpoint19_4Request["params"]["ptyID"]
readonly location?: Endpoint19_4Request["query"]["location"]
}
const Endpoint18_4 = (raw: RawClient["server.pty"]) => (input: Endpoint18_4Input) =>
const Endpoint19_4 = (raw: RawClient["server.pty"]) => (input: Endpoint19_4Input) =>
raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
const adaptGroup18 = (raw: RawClient["server.pty"]) => ({
list: Endpoint18_0(raw),
create: Endpoint18_1(raw),
get: Endpoint18_2(raw),
update: Endpoint18_3(raw),
remove: Endpoint18_4(raw),
const adaptGroup19 = (raw: RawClient["server.pty"]) => ({
list: Endpoint19_0(raw),
create: Endpoint19_1(raw),
get: Endpoint19_2(raw),
update: Endpoint19_3(raw),
remove: Endpoint19_4(raw),
})
type Endpoint19_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
const Endpoint19_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint19_0Input) =>
type Endpoint20_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
const Endpoint20_0 = (raw: RawClient["server.shell"]) => (input?: Endpoint20_0Input) =>
raw["shell.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint19_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
type Endpoint19_1Input = {
readonly location?: Endpoint19_1Request["query"]["location"]
readonly command: Endpoint19_1Request["payload"]["command"]
readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
readonly timeout?: Endpoint19_1Request["payload"]["timeout"]
readonly metadata?: Endpoint19_1Request["payload"]["metadata"]
type Endpoint20_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
type Endpoint20_1Input = {
readonly location?: Endpoint20_1Request["query"]["location"]
readonly command: Endpoint20_1Request["payload"]["command"]
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
}
const Endpoint19_1 = (raw: RawClient["server.shell"]) => (input: Endpoint19_1Input) =>
const Endpoint20_1 = (raw: RawClient["server.shell"]) => (input: Endpoint20_1Input) =>
raw["shell.create"]({
query: { location: input["location"] },
payload: { command: input["command"], cwd: input["cwd"], timeout: input["timeout"], metadata: input["metadata"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint19_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
type Endpoint19_2Input = {
readonly id: Endpoint19_2Request["params"]["id"]
readonly location?: Endpoint19_2Request["query"]["location"]
type Endpoint20_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
type Endpoint20_2Input = {
readonly id: Endpoint20_2Request["params"]["id"]
readonly location?: Endpoint20_2Request["query"]["location"]
}
const Endpoint19_2 = (raw: RawClient["server.shell"]) => (input: Endpoint19_2Input) =>
const Endpoint20_2 = (raw: RawClient["server.shell"]) => (input: Endpoint20_2Input) =>
raw["shell.get"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
type Endpoint19_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
type Endpoint19_3Input = {
readonly id: Endpoint19_3Request["params"]["id"]
readonly location?: Endpoint19_3Request["query"]["location"]
readonly cursor?: Endpoint19_3Request["query"]["cursor"]
readonly limit?: Endpoint19_3Request["query"]["limit"]
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
type Endpoint20_3Input = {
readonly id: Endpoint20_3Request["params"]["id"]
readonly location?: Endpoint20_3Request["query"]["location"]
readonly cursor?: Endpoint20_3Request["query"]["cursor"]
readonly limit?: Endpoint20_3Request["query"]["limit"]
}
const Endpoint19_3 = (raw: RawClient["server.shell"]) => (input: Endpoint19_3Input) =>
const Endpoint20_3 = (raw: RawClient["server.shell"]) => (input: Endpoint20_3Input) =>
raw["shell.output"]({
params: { id: input["id"] },
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint19_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
type Endpoint19_4Input = {
readonly id: Endpoint19_4Request["params"]["id"]
readonly location?: Endpoint19_4Request["query"]["location"]
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
type Endpoint20_4Input = {
readonly id: Endpoint20_4Request["params"]["id"]
readonly location?: Endpoint20_4Request["query"]["location"]
}
const Endpoint19_4 = (raw: RawClient["server.shell"]) => (input: Endpoint19_4Input) =>
const Endpoint20_4 = (raw: RawClient["server.shell"]) => (input: Endpoint20_4Input) =>
raw["shell.remove"]({ params: { id: input["id"] }, query: { location: input["location"] } }).pipe(
Effect.mapError(mapClientError),
)
const adaptGroup19 = (raw: RawClient["server.shell"]) => ({
list: Endpoint19_0(raw),
create: Endpoint19_1(raw),
get: Endpoint19_2(raw),
output: Endpoint19_3(raw),
remove: Endpoint19_4(raw),
})
type Endpoint20_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
const Endpoint20_0 = (raw: RawClient["server.question"]) => (input?: Endpoint20_0Input) =>
raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
type Endpoint20_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
type Endpoint20_1Input = { readonly sessionID: Endpoint20_1Request["params"]["sessionID"] }
const Endpoint20_1 = (raw: RawClient["server.question"]) => (input: Endpoint20_1Input) =>
raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
)
type Endpoint20_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
type Endpoint20_2Input = {
readonly sessionID: Endpoint20_2Request["params"]["sessionID"]
readonly requestID: Endpoint20_2Request["params"]["requestID"]
readonly answers: Endpoint20_2Request["payload"]["answers"]
}
const Endpoint20_2 = (raw: RawClient["server.question"]) => (input: Endpoint20_2Input) =>
raw["session.question.reply"]({
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
payload: { answers: input["answers"] },
}).pipe(Effect.mapError(mapClientError))
type Endpoint20_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
type Endpoint20_3Input = {
readonly sessionID: Endpoint20_3Request["params"]["sessionID"]
readonly requestID: Endpoint20_3Request["params"]["requestID"]
}
const Endpoint20_3 = (raw: RawClient["server.question"]) => (input: Endpoint20_3Input) =>
raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
Effect.mapError(mapClientError),
)
const adaptGroup20 = (raw: RawClient["server.question"]) => ({
listRequests: Endpoint20_0(raw),
list: Endpoint20_1(raw),
reply: Endpoint20_2(raw),
reject: Endpoint20_3(raw),
const adaptGroup20 = (raw: RawClient["server.shell"]) => ({
list: Endpoint20_0(raw),
create: Endpoint20_1(raw),
get: Endpoint20_2(raw),
output: Endpoint20_3(raw),
remove: Endpoint20_4(raw),
})
type Endpoint21_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
@@ -864,14 +922,14 @@ const adaptClient = (raw: RawClient) => ({
"server.mcp": adaptGroup10(raw["server.mcp"]),
credential: adaptGroup11(raw["server.credential"]),
project: adaptGroup12(raw["server.project"]),
permission: adaptGroup13(raw["server.permission"]),
file: adaptGroup14(raw["server.fs"]),
command: adaptGroup15(raw["server.command"]),
skill: adaptGroup16(raw["server.skill"]),
event: adaptGroup17(raw["server.event"]),
pty: adaptGroup18(raw["server.pty"]),
shell: adaptGroup19(raw["server.shell"]),
question: adaptGroup20(raw["server.question"]),
form: adaptGroup13(raw["server.form"]),
permission: adaptGroup14(raw["server.permission"]),
file: adaptGroup15(raw["server.fs"]),
command: adaptGroup16(raw["server.command"]),
skill: adaptGroup17(raw["server.skill"]),
event: adaptGroup18(raw["server.event"]),
pty: adaptGroup19(raw["server.pty"]),
shell: adaptGroup20(raw["server.shell"]),
reference: adaptGroup21(raw["server.reference"]),
projectCopy: adaptGroup22(raw["server.projectCopy"]),
})
+109 -56
View File
@@ -83,6 +83,20 @@ import type {
ProjectCurrentOutput,
ProjectDirectoriesInput,
ProjectDirectoriesOutput,
FormListRequestsInput,
FormListRequestsOutput,
FormListInput,
FormListOutput,
FormCreateInput,
FormCreateOutput,
FormGetInput,
FormGetOutput,
FormStateInput,
FormStateOutput,
FormReplyInput,
FormReplyOutput,
FormCancelInput,
FormCancelOutput,
PermissionListRequestsInput,
PermissionListRequestsOutput,
PermissionListSavedInput,
@@ -128,14 +142,6 @@ import type {
ShellOutputOutput,
ShellRemoveInput,
ShellRemoveOutput,
QuestionListRequestsInput,
QuestionListRequestsOutput,
QuestionListInput,
QuestionListOutput,
QuestionReplyInput,
QuestionReplyOutput,
QuestionRejectInput,
QuestionRejectOutput,
ReferenceListInput,
ReferenceListOutput,
ProjectCopyCreateInput,
@@ -825,6 +831,101 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
form: {
listRequests: (input?: FormListRequestsInput, requestOptions?: RequestOptions) =>
request<FormListRequestsOutput>(
{
method: "GET",
path: `/api/form/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
list: (input: FormListInput, requestOptions?: RequestOptions) =>
request<FormListOutput>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 404, 401],
empty: false,
},
requestOptions,
),
create: (input: FormCreateInput, requestOptions?: RequestOptions) =>
request<FormCreateOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
query: { location: input["location"] },
body: {
id: input["id"],
title: input["title"],
metadata: input["metadata"],
mode: input["mode"],
fields: input["fields"],
url: input["url"],
},
successStatus: 200,
declaredStatuses: [409, 400, 404, 401],
empty: false,
},
requestOptions,
),
get: (input: FormGetInput, requestOptions?: RequestOptions) =>
request<FormGetOutput>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [404, 400, 401],
empty: false,
},
requestOptions,
),
state: (input: FormStateInput, requestOptions?: RequestOptions) =>
request<FormStateOutput>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/state`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [404, 400, 401],
empty: false,
},
requestOptions,
),
reply: (input: FormReplyInput, requestOptions?: RequestOptions) =>
request<FormReplyOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`,
query: { location: input["location"] },
body: { answer: input["answer"] },
successStatus: 204,
declaredStatuses: [409, 400, 404, 401],
empty: true,
},
requestOptions,
),
cancel: (input: FormCancelInput, requestOptions?: RequestOptions) =>
request<FormCancelOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/cancel`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [409, 404, 400, 401],
empty: true,
},
requestOptions,
),
},
permission: {
listRequests: (input?: PermissionListRequestsInput, requestOptions?: RequestOptions) =>
request<PermissionListRequestsOutput>(
@@ -1128,54 +1229,6 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
question: {
listRequests: (input?: QuestionListRequestsInput, requestOptions?: RequestOptions) =>
request<QuestionListRequestsOutput>(
{
method: "GET",
path: `/api/question/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
list: (input: QuestionListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: QuestionListOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/question`,
successStatus: 200,
declaredStatuses: [404, 400, 401],
empty: false,
},
requestOptions,
).then((value) => value.data),
reply: (input: QuestionReplyInput, requestOptions?: RequestOptions) =>
request<QuestionReplyOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`,
body: { answers: input["answers"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
reject: (input: QuestionRejectInput, requestOptions?: RequestOptions) =>
request<QuestionRejectOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`,
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
},
requestOptions,
),
},
reference: {
list: (input?: ReferenceListInput, requestOptions?: RequestOptions) =>
request<ReferenceListOutput>(
+1035 -89
View File
@@ -90,6 +90,26 @@ export type ProviderNotFoundError = {
export const isProviderNotFoundError = (value: unknown): value is ProviderNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ProviderNotFoundError"
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 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 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 PermissionNotFoundError = {
readonly _tag: "PermissionNotFoundError"
readonly requestID: string
@@ -106,14 +126,6 @@ export type ShellNotFoundError = { readonly _tag: "ShellNotFoundError"; readonly
export const isShellNotFoundError = (value: unknown): value is ShellNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError"
export type QuestionNotFoundError = {
readonly _tag: "QuestionNotFoundError"
readonly requestID: string
readonly message: string
}
export const isQuestionNotFoundError = (value: unknown): value is QuestionNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "QuestionNotFoundError"
export type ProjectCopyError = {
readonly name: "ProjectCopyError"
readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined }
@@ -2574,6 +2586,922 @@ export type ProjectDirectoriesInput = {
export type ProjectDirectoriesOutput = ReadonlyArray<{ readonly directory: string; readonly strategy?: string }>
export type FormListRequestsInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type FormListRequestsOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: ReadonlyArray<
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form"
readonly fields: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
>
}
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "url"
readonly url: string
}
>
}
export type FormListInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type FormListOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: ReadonlyArray<
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form"
readonly fields: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
>
}
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "url"
readonly url: string
}
>
}
export type FormCreateInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly id?: {
readonly id?: string | null
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form" | "url"
readonly fields?: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
> | null
readonly url?: string | null
}["id"]
readonly title?: {
readonly id?: string | null
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form" | "url"
readonly fields?: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
> | null
readonly url?: string | null
}["title"]
readonly metadata?: {
readonly id?: string | null
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form" | "url"
readonly fields?: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
> | null
readonly url?: string | null
}["metadata"]
readonly mode: {
readonly id?: string | null
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form" | "url"
readonly fields?: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
> | null
readonly url?: string | null
}["mode"]
readonly fields?: {
readonly id?: string | null
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form" | "url"
readonly fields?: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
> | null
readonly url?: string | null
}["fields"]
readonly url?: {
readonly id?: string | null
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form" | "url"
readonly fields?: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
> | null
readonly url?: string | null
}["url"]
}
export type FormCreateOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data:
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form"
readonly fields: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
>
}
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "url"
readonly url: string
}
}
export type FormGetInput = {
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type FormGetOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data:
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "form"
readonly fields: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number | "Infinity" | "-Infinity" | "NaN"
readonly maximum?: number | "Infinity" | "-Infinity" | "NaN"
readonly default?: number | "Infinity" | "-Infinity" | "NaN"
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
>
}
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: JsonValue }
readonly mode: "url"
readonly url: string
}
}
export type FormStateInput = {
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type FormStateOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data:
| { readonly status: "pending" }
| {
readonly status: "answered"
readonly answer: {
readonly [x: string]: string | number | "Infinity" | "-Infinity" | "NaN" | boolean | ReadonlyArray<string>
}
}
| { readonly status: "cancelled" }
}
export type FormReplyInput = {
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly answer: {
readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
}["answer"]
}
export type FormReplyOutput = void
export type FormCancelInput = {
readonly sessionID: { readonly sessionID: string; readonly formID: string }["sessionID"]
readonly formID: { readonly sessionID: string; readonly formID: string }["formID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type FormCancelOutput = void
export type PermissionListRequestsInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -4128,41 +5056,117 @@ export type EventSubscribeOutput =
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.v2.asked"
readonly type: "form.created"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly form:
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: unknown }
readonly mode: "form"
readonly fields: ReadonlyArray<
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "string"
readonly format?: "email" | "uri" | "date" | "date-time"
readonly minLength?: number
readonly maxLength?: number
readonly pattern?: string
readonly placeholder?: string
readonly default?: string
readonly options?: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly custom?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "number"
readonly minimum?: number
readonly maximum?: number
readonly default?: number
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "integer"
readonly minimum?: number
readonly maximum?: number
readonly default?: number
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "boolean"
readonly default?: boolean
}
| {
readonly key: string
readonly title?: string
readonly description?: string
readonly required?: boolean
readonly when?: { readonly key: string; readonly op: "eq" | "neq"; readonly value: string }
readonly type: "multiselect"
readonly options: ReadonlyArray<{
readonly value: string
readonly label: string
readonly description?: string
}>
readonly minItems?: number
readonly maxItems?: number
readonly custom?: boolean
readonly default?: ReadonlyArray<string>
}
>
}
| {
readonly id: string
readonly sessionID: string
readonly title?: string
readonly metadata?: { readonly [x: string]: unknown }
readonly mode: "url"
readonly url: string
}
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "form.replied"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly id: string
readonly sessionID: string
readonly questions: ReadonlyArray<{
readonly question: string
readonly header: string
readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }>
readonly multiple?: boolean
readonly custom?: boolean
}>
readonly tool?: { readonly messageID: string; readonly callID: string }
readonly answer: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.v2.replied"
readonly type: "form.cancelled"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: {
readonly sessionID: string
readonly requestID: string
readonly answers: ReadonlyArray<ReadonlyArray<string>>
}
}
| {
readonly id: string
readonly metadata?: { readonly [x: string]: unknown }
readonly type: "question.v2.rejected"
readonly durable?: { readonly aggregateID: string; readonly seq: number; readonly version: number }
readonly location?: { readonly directory: string; readonly workspaceID?: string }
readonly data: { readonly sessionID: string; readonly requestID: string }
readonly data: { readonly id: string; readonly sessionID: string }
}
| {
readonly id: string
@@ -4485,64 +5489,6 @@ export type ShellRemoveInput = {
export type ShellRemoveOutput = void
export type QuestionListRequestsInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type QuestionListRequestsOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: ReadonlyArray<{
readonly id: string
readonly sessionID: string
readonly questions: ReadonlyArray<{
readonly question: string
readonly header: string
readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }>
readonly multiple?: boolean
readonly custom?: boolean
}>
readonly tool?: { readonly messageID: string; readonly callID: string }
}>
}
export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type QuestionListOutput = {
readonly data: ReadonlyArray<{
readonly id: string
readonly sessionID: string
readonly questions: ReadonlyArray<{
readonly question: string
readonly header: string
readonly options: ReadonlyArray<{ readonly label: string; readonly description: string }>
readonly multiple?: boolean
readonly custom?: boolean
}>
readonly tool?: { readonly messageID: string; readonly callID: string }
}>
}["data"]
export type QuestionReplyInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"]
readonly answers: { readonly answers: ReadonlyArray<ReadonlyArray<string>> }["answers"]
}
export type QuestionReplyOutput = void
export type QuestionRejectInput = {
readonly sessionID: { readonly sessionID: string; readonly requestID: string }["sessionID"]
readonly requestID: { readonly sessionID: string; readonly requestID: string }["requestID"]
}
export type QuestionRejectOutput = void
export type ReferenceListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
+308
View File
@@ -0,0 +1,308 @@
export * as Form from "./form"
import { makeLocationNode } from "./effect/app-node"
import { Cache, Context, Deferred, Duration, Effect, Exit, Layer, Option, Schema } from "effect"
import { Form } from "@opencode-ai/schema/form"
import { EventV2 } from "./event"
const RETENTION = Duration.minutes(10)
export const ID = Form.ID
export type ID = typeof ID.Type
export const Info = Form.Info
export type Info = typeof Info.Type
export const Field = Form.Field
export type Field = Form.Field
export const State = Form.State
export type State = typeof State.Type
export const Answer = Form.Answer
export type Answer = typeof Answer.Type
export const Reply = Form.Reply
export type Reply = typeof Reply.Type
export const Event = Form.Event
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Form.NotFoundError", {
id: ID,
}) {
override get message() {
return `Form not found: ${this.id}`
}
}
export class AlreadySettledError extends Schema.TaggedErrorClass<AlreadySettledError>()("Form.AlreadySettledError", {
id: ID,
}) {
override get message() {
return `Form already settled: ${this.id}`
}
}
export class AlreadyExistsError extends Schema.TaggedErrorClass<AlreadyExistsError>()("Form.AlreadyExistsError", {
id: ID,
}) {
override get message() {
return `Form already exists: ${this.id}`
}
}
export class InvalidAnswerError extends Schema.TaggedErrorClass<InvalidAnswerError>()("Form.InvalidAnswerError", {
id: ID,
message: Schema.String,
}) {}
export type CreateInput =
| (Omit<Form.FormInfo, "id"> & { readonly id?: ID })
| (Omit<Form.UrlInfo, "id"> & { readonly id?: ID })
export interface ReplyInput {
readonly id: ID
readonly answer: Answer
}
export interface ListInput {
readonly sessionID?: string
}
export interface Interface {
readonly create: (input: CreateInput) => Effect.Effect<Info, AlreadyExistsError>
readonly ask: (input: CreateInput) => Effect.Effect<State, AlreadyExistsError>
readonly get: (id: ID) => Effect.Effect<Info, NotFoundError>
readonly list: (input?: ListInput) => Effect.Effect<ReadonlyArray<Info>>
readonly state: (id: ID) => Effect.Effect<State, NotFoundError>
readonly reply: (input: ReplyInput) => Effect.Effect<void, AlreadySettledError | InvalidAnswerError | NotFoundError>
readonly cancel: (id: ID) => Effect.Effect<void, AlreadySettledError | NotFoundError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Form") {}
interface Entry {
readonly form: Info
readonly state: State
readonly deferred: Deferred.Deferred<State>
}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const forms = yield* Cache.makeWith<ID, Entry>(() => Effect.die("Form cache must be used via set/getSuccess, never get"), {
capacity: Number.MAX_SAFE_INTEGER,
timeToLive: (exit) => (Exit.isSuccess(exit) && exit.value.state.status === "pending" ? Duration.infinity : RETENTION),
})
const find = Effect.fn("Form.find")(function* (id: ID) {
return yield* Cache.getSuccess(forms, id).pipe(
Effect.flatMap((entry) =>
Option.match(entry, {
onNone: () => Effect.fail(new NotFoundError({ id })),
onSome: Effect.succeed,
}),
),
)
})
const create = Effect.fn("Form.create")((input: CreateInput) =>
Effect.uninterruptible(
Effect.gen(function* () {
const id = input.id ?? ID.create()
const existing = yield* Cache.getSuccess(forms, id)
if (Option.isSome(existing)) return yield* new AlreadyExistsError({ id })
const base = {
id,
sessionID: input.sessionID,
title: input.title,
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
}
const form: Info =
input.mode === "form"
? { ...base, mode: "form", fields: input.fields }
: {
...base,
mode: "url",
url: input.url,
}
const entry: Entry = {
form,
state: { status: "pending" },
deferred: yield* Deferred.make<State>(),
}
yield* Cache.set(forms, id, entry)
yield* events.publish(Event.Created, { form }).pipe(Effect.onError(() => Cache.invalidate(forms, id)))
return form
}),
),
)
const ask = Effect.fn("Form.ask")((input: CreateInput) =>
Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const form = yield* create(input)
const entry = yield* find(form.id).pipe(Effect.orDie)
return yield* restore(Deferred.await(entry.deferred)).pipe(Effect.onInterrupt(() => Effect.ignore(cancel(form.id))))
}),
),
)
const get = Effect.fn("Form.get")(function* (id: ID) {
return (yield* find(id)).form
})
const list = Effect.fn("Form.list")(function* (input?: ListInput) {
const entries = yield* Cache.values(forms)
return Array.from(entries)
.filter((entry) => entry.state.status === "pending")
.filter((entry) => input?.sessionID === undefined || entry.form.sessionID === input.sessionID)
.map((entry) => entry.form)
})
const state = Effect.fn("Form.state")(function* (id: ID) {
return (yield* find(id)).state
})
const reply = Effect.fn("Form.reply")((input: ReplyInput) =>
Effect.uninterruptible(
Effect.gen(function* () {
const entry = yield* find(input.id)
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id: input.id })
const invalid = validateAnswer(entry.form, input.answer)
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
const next: State = { status: "answered", answer: input.answer }
yield* events.publish(Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
yield* Cache.set(forms, input.id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next)
}),
),
)
const cancel = Effect.fn("Form.cancel")((id: ID) =>
Effect.uninterruptible(
Effect.gen(function* () {
const entry = yield* find(id)
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id })
const next: State = { status: "cancelled" }
yield* events.publish(Event.Cancelled, { id, sessionID: entry.form.sessionID })
yield* Cache.set(forms, id, { ...entry, state: next })
yield* Deferred.succeed(entry.deferred, next)
}),
),
)
yield* Effect.addFinalizer(() =>
Cache.values(forms).pipe(
Effect.flatMap((entries) =>
Effect.forEach(
Array.from(entries).filter((entry) => entry.state.status === "pending"),
(entry) => cancel(entry.form.id).pipe(Effect.ignore),
{ discard: true },
),
),
),
)
return Service.of({ create, ask, get, list, state, reply, cancel })
}),
)
export const locationLayer = layer
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
function validateAnswer(form: Info, answer: Answer) {
if (form.mode === "url") {
if (Object.keys(answer).length === 0) return
return "URL forms must be answered with an empty answer"
}
const fields = new Map(form.fields.map((field) => [field.key, field]))
for (const key of Object.keys(answer)) {
if (!fields.has(key)) return `Unknown form field: ${key}`
}
for (const field of form.fields) {
const value = answer[field.key]
if (value === undefined) {
if (field.required && isActive(field, answer)) return `Missing required form field: ${field.key}`
continue
}
const invalid = validateField(field, value)
if (invalid) return invalid
}
}
function isActive(field: Form.Field, answer: Answer) {
if (!field.when) return true
const value = answer[field.when.key]
if (field.when.op === "eq") return value === field.when.value
return value !== field.when.value
}
function validateField(field: Form.Field, value: Form.Value): string | undefined {
if (field.type === "string") {
if (typeof value !== "string") return `Expected string for form field: ${field.key}`
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
if (field.minLength !== undefined && value.length < field.minLength) return `Form field is too short: ${field.key}`
if (field.maxLength !== undefined && value.length > field.maxLength) return `Form field is too long: ${field.key}`
if (field.pattern !== undefined) {
try {
if (!new RegExp(field.pattern).test(value)) return `Form field does not match pattern: ${field.key}`
} catch {
return `Form field has invalid pattern: ${field.key}`
}
}
if (field.format === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `Expected email for form field: ${field.key}`
if (field.format === "uri" && !isUri(value)) return `Expected URI for form field: ${field.key}`
if (field.format === "date" && !isDate(value)) return `Expected date for form field: ${field.key}`
if (field.format === "date-time" && !isDateTime(value)) return `Expected date-time for form field: ${field.key}`
if (field.options && !field.custom && !field.options.some((option) => option.value === value)) {
return `Invalid option for form field: ${field.key}`
}
return
}
if (field.type === "number" || field.type === "integer") {
if (typeof value !== "number" || !Number.isFinite(value)) return `Expected number for form field: ${field.key}`
if (field.type === "integer" && !Number.isInteger(value)) return `Expected integer for form field: ${field.key}`
if (field.minimum !== undefined && value < field.minimum) return `Form field is too small: ${field.key}`
if (field.maximum !== undefined && value > field.maximum) return `Form field is too large: ${field.key}`
return
}
if (field.type === "boolean") {
if (typeof value !== "boolean") return `Expected boolean for form field: ${field.key}`
return
}
if (field.type === "multiselect") {
if (!isStringArray(value)) return `Expected string array for form field: ${field.key}`
if (field.required && value.length === 0) return `Missing required form field: ${field.key}`
if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}`
if (field.maxItems !== undefined && value.length > field.maxItems) return `Too many selections for form field: ${field.key}`
if (!field.custom && value.some((item) => !field.options.some((option) => option.value === item))) {
return `Invalid option for form field: ${field.key}`
}
}
}
function isStringArray(value: Form.Value): value is ReadonlyArray<string> {
return Array.isArray(value) && value.every((item): item is string => typeof item === "string")
}
function isUri(value: string) {
try {
new URL(value)
return true
} catch {
return false
}
}
function isDate(value: string) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false
const date = new Date(`${value}T00:00:00.000Z`)
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
}
function isDateTime(value: string) {
return !Number.isNaN(new Date(value).getTime())
}
+2 -2
View File
@@ -9,6 +9,7 @@ import { Node } from "./effect/app-node"
import { FileMutation } from "./file-mutation"
import { FileSystem } from "./filesystem"
import { FileSystemSearch } from "./filesystem/search"
import { Form } from "./form"
import { Generate } from "./generate"
import { Watcher } from "./filesystem/watcher"
import { Image } from "./image"
@@ -24,7 +25,6 @@ import { Policy } from "./policy"
import { Project } from "./project"
import { ProjectCopy } from "./project/copy"
import { Pty } from "./pty"
import { QuestionV2 } from "./question"
import { Shell } from "./shell"
import { Reference } from "./reference"
import { ReferenceGuidance } from "./reference/guidance"
@@ -81,7 +81,7 @@ export const locationServices = LayerNode.group([
SkillGuidance.node,
ReferenceGuidance.node,
SessionTodo.node,
QuestionV2.node,
Form.node,
Generate.node,
ReadToolFileSystem.node,
BuiltInTools.node,
-151
View File
@@ -1,151 +0,0 @@
export * as QuestionV2 from "./question"
import { makeLocationNode } from "./effect/app-node"
import { Context, Deferred, Effect, Layer, Schema } from "effect"
import { Question } from "@opencode-ai/schema/question"
import { EventV2 } from "./event"
import { SessionSchema } from "./session/schema"
export const ID = Question.ID
export type ID = typeof ID.Type
export const Option = Question.Option
export type Option = typeof Option.Type
export const Info = Question.Info
export type Info = typeof Info.Type
export const Prompt = Question.Prompt
export type Prompt = typeof Prompt.Type
export const Tool = Question.Tool
export type Tool = typeof Tool.Type
export const Request = Question.Request
export type Request = typeof Request.Type
export const Answer = Question.Answer
export type Answer = typeof Answer.Type
export const Reply = Question.Reply
export type Reply = typeof Reply.Type
export const Event = Question.Event
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionV2.RejectedError", {}) {
override get message() {
return "The user dismissed this question"
}
}
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("QuestionV2.NotFoundError", {
requestID: ID,
}) {}
export interface AskInput {
readonly sessionID: SessionSchema.ID
readonly questions: ReadonlyArray<Info>
readonly tool?: Tool
}
export interface ReplyInput {
readonly requestID: ID
readonly answers: ReadonlyArray<Answer>
}
export interface Interface {
readonly ask: (input: AskInput) => Effect.Effect<ReadonlyArray<Answer>, RejectedError>
readonly reply: (input: ReplyInput) => Effect.Effect<void, NotFoundError>
readonly reject: (requestID: ID) => Effect.Effect<void, NotFoundError>
readonly list: () => Effect.Effect<ReadonlyArray<Request>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Question") {}
interface Pending {
readonly request: Request
readonly deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError>
}
/**
* Location-owned pending prompts. The Location layer map must materialize this
* layer once per embedded Location so replies cannot settle another Location's
* deferred request.
*/
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2.Service
const pending = new Map<ID, Pending>()
yield* Effect.addFinalizer(() =>
Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
discard: true,
}).pipe(
Effect.ensuring(
Effect.sync(() => {
pending.clear()
}),
),
),
)
const ask = Effect.fn("QuestionV2.ask")((input: AskInput) =>
Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const id = ID.ascending()
const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>()
const request: Request = { id, ...input }
pending.set(id, { request, deferred })
return yield* events.publish(Event.Asked, request).pipe(
Effect.andThen(restore(Deferred.await(deferred))),
Effect.ensuring(
Effect.sync(() => {
pending.delete(id)
}),
),
)
}),
),
)
const reply = Effect.fn("QuestionV2.reply")((input: ReplyInput) =>
Effect.uninterruptible(
Effect.gen(function* () {
const existing = pending.get(input.requestID)
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
yield* events.publish(Event.Replied, {
sessionID: existing.request.sessionID,
requestID: existing.request.id,
answers: input.answers.map((answer) => [...answer]),
})
yield* Deferred.succeed(existing.deferred, input.answers)
pending.delete(input.requestID)
}),
),
)
const reject = Effect.fn("QuestionV2.reject")((requestID: ID) =>
Effect.uninterruptible(
Effect.gen(function* () {
const existing = pending.get(requestID)
if (!existing) return yield* new NotFoundError({ requestID })
yield* events.publish(Event.Rejected, {
sessionID: existing.request.sessionID,
requestID: existing.request.id,
})
yield* Deferred.fail(existing.deferred, new RejectedError())
pending.delete(requestID)
}),
),
)
const list = Effect.fn("QuestionV2.list")(function* () {
return Array.from(pending.values(), (item) => item.request)
})
return Service.of({ ask, reply, reject, list })
}),
)
export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] })
+2 -2
View File
@@ -16,7 +16,7 @@ import { EventV2 } from "../../event"
import { Location } from "../../location"
import { ModelV2 } from "../../model"
import { ProviderV2 } from "../../provider"
import { QuestionV2 } from "../../question"
import { QuestionTool } from "../../tool/question"
import { SystemContext } from "../../system-context/index"
import { SystemContextRegistry } from "../../system-context/registry"
import { SkillGuidance } from "../../skill/guidance"
@@ -151,7 +151,7 @@ const layer = Layer.effect(
// Match V1: dismissing a question halts the loop instead of becoming model-facing tool output.
const isQuestionRejected = (cause: Cause.Cause<unknown>) =>
cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionV2.RejectedError)
cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionTool.RejectedError)
type TurnTransition =
// Automatic compaction completed; rebuild the request from compacted history.
+70 -13
View File
@@ -2,9 +2,10 @@ export * as QuestionTool from "./question"
import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect"
import { optional } from "@opencode-ai/schema/schema"
import { Form } from "../form"
import { makeLocationNode } from "../effect/app-node"
import { PermissionV2 } from "../permission"
import { QuestionV2 } from "../question"
import { ToolRegistry } from "./registry"
import { Tool } from "./tool"
import { Tools } from "./tools"
@@ -22,19 +23,40 @@ Usage notes:
- Answers are returned as arrays of labels; set \`multiple: true\` to allow selecting more than one
- If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label`
export const Option = Schema.Struct({
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
description: Schema.String.annotate({ description: "Explanation of choice" }),
}).annotate({ identifier: "QuestionTool.Option" })
export interface Option extends Schema.Schema.Type<typeof Option> {}
export const Prompt = Schema.Struct({
question: Schema.String.annotate({ description: "Complete question" }),
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
options: Schema.Array(Option).annotate({ description: "Available choices" }),
multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }),
custom: Schema.Boolean.pipe(optional).annotate({ description: "Allow typing a custom answer (default: true)" }),
}).annotate({ identifier: "QuestionTool.Prompt" })
export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionTool.Answer" })
export type Answer = typeof Answer.Type
export const Input = Schema.Struct({
questions: Schema.Array(QuestionV2.Prompt).annotate({ description: "Questions to ask" }),
questions: Schema.Array(Prompt).annotate({ description: "Questions to ask" }),
})
export const Output = Schema.Struct({
answers: Schema.Array(QuestionV2.Answer),
answers: Schema.Array(Answer),
})
export type Output = typeof Output.Type
export const toModelOutput = (
questions: ReadonlyArray<QuestionV2.Prompt>,
answers: ReadonlyArray<QuestionV2.Answer>,
) => {
export class RejectedError extends Error {
constructor() {
super("The user dismissed this question")
}
}
export const toModelOutput = (questions: ReadonlyArray<Prompt>, answers: ReadonlyArray<Answer>) => {
const formatted = questions
.map(
(question, index) =>
@@ -47,7 +69,7 @@ export const toModelOutput = (
const layer = Layer.effectDiscard(
Effect.gen(function* () {
const tools = yield* Tools.Service
const question = yield* QuestionV2.Service
const form = yield* Form.Service
const permission = yield* PermissionV2.Service
yield* tools
@@ -71,15 +93,22 @@ const layer = Layer.effectDiscard(
.pipe(
Effect.mapError(() => new ToolFailure({ message: "Permission denied: question" })),
Effect.andThen(
question
form
.ask({
sessionID: context.sessionID,
questions: input.questions,
tool: { messageID: context.assistantMessageID, callID: context.toolCallID },
title: input.questions.length === 1 ? input.questions[0]?.header : "Questions",
metadata: {
kind: "question",
tool: { messageID: context.assistantMessageID, callID: context.toolCallID },
},
mode: "form",
fields: input.questions.map(questionToField),
})
.pipe(Effect.orDie),
),
Effect.map((answers) => ({ answers })),
Effect.flatMap((state) =>
state.status === "answered" ? Effect.succeed({ answers: formToAnswers(input.questions, state.answer) }) : Effect.die(new RejectedError()),
),
),
}),
})
@@ -90,5 +119,33 @@ const layer = Layer.effectDiscard(
export const node = makeLocationNode({
name: "tool/question",
layer,
deps: [ToolRegistry.node, PermissionV2.node, QuestionV2.node],
deps: [ToolRegistry.node, PermissionV2.node, Form.node],
})
function questionToField(question: Prompt, index: number): Form.Field {
const base = {
key: key(index),
title: question.question,
description: question.header,
}
const options = question.options.map((option) => ({
value: option.label,
label: option.label,
description: option.description,
}))
if (question.multiple) return { ...base, type: "multiselect", options, custom: question.custom ?? true }
return { ...base, type: "string", options, custom: question.custom ?? true }
}
function formToAnswers(questions: ReadonlyArray<Prompt>, answer: Form.Answer): ReadonlyArray<Answer> {
return questions.map((_, index) => {
const value = answer[key(index)]
if (Array.isArray(value)) return value
if (typeof value === "string" && value.length > 0) return [value]
return []
})
}
function key(index: number) {
return `question_${index}`
}
+58
View File
@@ -0,0 +1,58 @@
import { describe, expect } from "bun:test"
import { Effect, Exit } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Form } from "@opencode-ai/core/form"
import { testEffect } from "./lib/effect"
const forms = AppNodeBuilder.build(LayerNode.group([EventV2.node, Form.node]))
const it = testEffect(forms)
const formID = Form.ID.create("frm_test")
const input = {
id: formID,
sessionID: "ses_test",
mode: "form",
fields: [{ key: "name", type: "string", required: true }],
} satisfies Form.CreateInput
describe("Form", () => {
it.effect("cleans up created forms when event publication fails", () =>
Effect.gen(function* () {
const service = yield* Form.Service
const events = yield* EventV2.Service
const unsubscribe = yield* events.listen((event) =>
event.type === Form.Event.Created.type ? Effect.die("create listener failed") : Effect.void,
)
yield* Effect.addFinalizer(() => unsubscribe)
expect(Exit.isFailure(yield* Effect.exit(service.create(input)))).toBe(true)
expect(yield* service.get(formID).pipe(Effect.flip)).toEqual(new Form.NotFoundError({ id: formID }))
yield* unsubscribe
expect(yield* service.create(input)).toMatchObject({ id: formID })
}),
)
it.effect("keeps forms pending when reply event publication fails", () =>
Effect.gen(function* () {
const service = yield* Form.Service
const events = yield* EventV2.Service
yield* service.create(input)
const unsubscribe = yield* events.listen((event) =>
event.type === Form.Event.Replied.type ? Effect.die("reply listener failed") : Effect.void,
)
yield* Effect.addFinalizer(() => unsubscribe)
expect(
Exit.isFailure(yield* Effect.exit(service.reply({ id: formID, answer: { name: "Ava" } }))),
).toBe(true)
expect(yield* service.state(formID)).toEqual({ status: "pending" })
yield* unsubscribe
yield* service.reply({ id: formID, answer: { name: "Ava" } })
expect(yield* service.state(formID)).toEqual({ status: "answered", answer: { name: "Ava" } })
}),
)
})
-114
View File
@@ -1,114 +0,0 @@
import { describe, expect } from "bun:test"
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { EventV2 } from "@opencode-ai/core/event"
import { QuestionV2 } from "@opencode-ai/core/question"
import { SessionV2 } from "@opencode-ai/core/session"
import { testEffect } from "./lib/effect"
const questions = AppNodeBuilder.build(LayerNode.group([EventV2.node, QuestionV2.node]))
const it = testEffect(questions)
const sessionID = SessionV2.ID.make("ses_question_test")
const question: QuestionV2.Info = {
question: "Which option?",
header: "Option",
options: [{ label: "One", description: "First option" }],
}
const waitForAsk = Effect.fn("QuestionV2Test.waitForAsk")(function* (
service: QuestionV2.Interface,
input: QuestionV2.AskInput,
) {
const events = yield* EventV2.Service
const asked = yield* Deferred.make<QuestionV2.Request>()
const unsubscribe = yield* events.listen((event) =>
event.type === QuestionV2.Event.Asked.type
? Deferred.succeed(asked, event.data as QuestionV2.Request).pipe(Effect.asVoid)
: Effect.void,
)
yield* Effect.addFinalizer(() => unsubscribe)
const fiber = yield* service.ask(input).pipe(Effect.forkScoped)
return { fiber, request: yield* Deferred.await(asked) }
})
describe("QuestionV2", () => {
it.effect("publishes lifecycle events and settles a pending reply", () =>
Effect.gen(function* () {
const service = yield* QuestionV2.Service
const events = yield* EventV2.Service
const published: EventV2.Payload[] = []
const unsubscribe = yield* events.listen((event) =>
Effect.sync(() => {
if (event.type.startsWith("question.v2.")) published.push(event)
}),
)
yield* Effect.addFinalizer(() => unsubscribe)
const { fiber, request } = yield* waitForAsk(service, { sessionID, questions: [question] })
expect(request.id).toMatch(/^que_/)
expect(yield* service.list()).toEqual([request])
yield* service.reply({ requestID: request.id, answers: [["One"]] })
expect(yield* Fiber.join(fiber)).toEqual([["One"]])
expect(yield* service.list()).toEqual([])
expect(published.map((event) => [event.type, event.data])).toEqual([
[QuestionV2.Event.Asked.type, request],
[QuestionV2.Event.Replied.type, { sessionID, requestID: request.id, answers: [["One"]] }],
])
}),
)
it.effect("publishes rejection, fails the ask, and rejects unknown IDs", () =>
Effect.gen(function* () {
const service = yield* QuestionV2.Service
const events = yield* EventV2.Service
const published: EventV2.Payload[] = []
const unsubscribe = yield* events.listen((event) =>
Effect.sync(() => {
if (event.type === QuestionV2.Event.Rejected.type) published.push(event)
}),
)
yield* Effect.addFinalizer(() => unsubscribe)
const { fiber, request } = yield* waitForAsk(service, { sessionID, questions: [question] })
yield* service.reject(request.id)
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError")
expect(published.map((event) => event.data)).toEqual([{ sessionID, requestID: request.id }])
const unknown = QuestionV2.ID.ascending("que_unknown")
expect(yield* service.reply({ requestID: unknown, answers: [] }).pipe(Effect.flip)).toEqual(
new QuestionV2.NotFoundError({ requestID: unknown }),
)
expect(yield* service.reject(unknown).pipe(Effect.flip)).toEqual(
new QuestionV2.NotFoundError({ requestID: unknown }),
)
}),
)
it.effect("isolates pending requests by location-layer instance and rejects them on finalization", () =>
Effect.gen(function* () {
const firstScope = yield* Scope.make()
const secondScope = yield* Scope.make()
const first = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), firstScope), QuestionV2.Service)
const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), QuestionV2.Service)
const fiber = yield* first.ask({ sessionID, questions: [question] }).pipe(Effect.forkScoped)
yield* Effect.yieldNow
const request = (yield* first.list())[0]!
expect(yield* second.list()).toEqual([])
expect(yield* second.reply({ requestID: request.id, answers: [["One"]] }).pipe(Effect.flip)).toEqual(
new QuestionV2.NotFoundError({ requestID: request.id }),
)
yield* Scope.close(firstScope, Exit.void)
const exit = yield* Fiber.await(fiber)
expect(Exit.isFailure(exit)).toBe(true)
if (Exit.isFailure(exit)) expect(exit.cause.toString()).toContain("QuestionV2.RejectedError")
yield* Scope.close(secondScope, Exit.void)
}),
)
})
+15 -7
View File
@@ -16,12 +16,12 @@ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNodePlatform } from "@opencode-ai/core/effect/app-node-platform"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { EventV2 } from "@opencode-ai/core/event"
import { Form } from "@opencode-ai/core/form"
import { Job } from "@opencode-ai/core/job"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { EventTable } from "@opencode-ai/core/event/sql"
import { Project } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { QuestionV2 } from "@opencode-ai/core/question"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { Snapshot } from "@opencode-ai/core/snapshot"
@@ -40,6 +40,7 @@ import * as SessionRunnerLLM from "@opencode-ai/core/session/runner/llm"
import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
import { SessionRunnerSystemPrompt } from "@opencode-ai/core/session/runner/system-prompt"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { QuestionTool } from "@opencode-ai/core/tool/question"
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
@@ -266,7 +267,7 @@ const it = testEffect(
LayerNode.group([
Database.node,
EventV2.node,
QuestionV2.node,
Form.node,
SessionProjector.node,
SessionStore.node,
AgentV2.node,
@@ -2705,14 +2706,21 @@ describe("SessionRunnerLLM", () => {
yield* setup
const session = yield* SessionV2.Service
const registry = yield* ToolRegistry.Service
const questions = yield* QuestionV2.Service
const forms = yield* Form.Service
yield* registry.register({
question: Tool.make({
description: "Ask the user",
input: Schema.Struct({}),
output: Schema.Struct({}),
execute: (_, context) =>
questions.ask({ sessionID: context.sessionID, questions: [] }).pipe(Effect.as({}), Effect.orDie),
forms
.ask({ sessionID: context.sessionID, mode: "form", fields: [] })
.pipe(
Effect.orDie,
Effect.flatMap((state) =>
state.status === "answered" ? Effect.succeed({}) : Effect.die(new QuestionTool.RejectedError()),
),
),
}),
})
yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Ask then stop" }), resume: false })
@@ -2729,12 +2737,12 @@ describe("SessionRunnerLLM", () => {
]
const run = yield* session.resume(sessionID).pipe(Effect.exit, Effect.forkChild)
let pending = yield* questions.list()
let pending = yield* forms.list({ sessionID })
while (pending.length === 0) {
yield* Effect.yieldNow
pending = yield* questions.list()
pending = yield* forms.list({ sessionID })
}
yield* questions.reject(pending[0]!.id)
yield* forms.cancel(pending[0]!.id)
const exit = yield* Fiber.join(run)
expect(exit._tag).toBe("Failure")
+41 -15
View File
@@ -2,8 +2,8 @@ import { describe, expect } from "bun:test"
import { Effect, Exit, Fiber, Layer } from "effect"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Form } from "@opencode-ai/core/form"
import { PermissionV2 } from "@opencode-ai/core/permission"
import { QuestionV2 } from "@opencode-ai/core/question"
import { SessionV2 } from "@opencode-ai/core/session"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { QuestionTool } from "@opencode-ai/core/tool/question"
@@ -13,7 +13,7 @@ import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/to
const sessionID = SessionV2.ID.make("ses_question_tool_test")
const assertions: PermissionV2.AssertInput[] = []
let captured: QuestionV2.AskInput | undefined
let captured: Form.CreateInput | undefined
let reject = false
let deny = false
const capturedInput = () => captured
@@ -31,22 +31,27 @@ const permission = Layer.succeed(
list: () => Effect.die("unused"),
}),
)
const question = Layer.succeed(
QuestionV2.Service,
QuestionV2.Service.of({
ask: (input: QuestionV2.AskInput) =>
Effect.sync(() => {
const form = Layer.succeed(
Form.Service,
Form.Service.of({
create: () => Effect.die("unused"),
ask: (input: Form.CreateInput) =>
Effect.gen(function* () {
captured = input
}).pipe(Effect.andThen(reject ? Effect.fail(new QuestionV2.RejectedError()) : Effect.succeed([["Build"], []]))),
reply: () => Effect.die("unused"),
reject: () => Effect.die("unused"),
if (reject) return { status: "cancelled" } as const
return { status: "answered", answer: { question_0: "Build" } } as const
}),
get: () => Effect.die("unused"),
list: () => Effect.die("unused"),
state: () => Effect.die("unused"),
reply: () => Effect.die("unused"),
cancel: () => Effect.die("unused"),
}),
)
const it = testEffect(
AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, QuestionTool.node]), [
[PermissionV2.node, permission],
[QuestionV2.node, question],
[Form.node, form],
[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
]),
)
@@ -117,8 +122,27 @@ describe("QuestionTool", () => {
expect(assertions).toMatchObject([{ sessionID, action: "question", resources: ["*"] }])
expect(capturedInput()).toEqual({
sessionID,
questions,
tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" },
title: "Questions",
metadata: { kind: "question", tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" } },
mode: "form",
fields: [
{
key: "question_0",
title: "What should happen?",
description: "Action",
type: "string",
options: [{ value: "Build", label: "Build", description: "Build it" }],
custom: true,
},
{
key: "question_1",
title: "Which environment?",
description: "Environment",
type: "string",
options: [{ value: "Dev", label: "Dev", description: "Development" }],
custom: true,
},
],
})
}),
)
@@ -137,8 +161,10 @@ describe("QuestionTool", () => {
})
expect(capturedInput()).toEqual({
sessionID,
questions: [],
tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" },
title: "Questions",
metadata: { kind: "question", tool: { messageID: toolIdentity.assistantMessageID, callID: "call-question" } },
mode: "form",
fields: [],
})
}),
)
@@ -26,6 +26,7 @@ import { SyncApi } from "./groups/sync"
import { TuiApi } from "./groups/tui"
import { WorkspaceApi } from "./groups/workspace"
import { makeApi } from "@opencode-ai/protocol/api"
import { FormLocationMiddleware } from "@opencode-ai/server/middleware/form-location"
import { LocationMiddleware } from "@opencode-ai/server/location"
import { SessionLocationMiddleware } from "@opencode-ai/server/middleware/session-location"
import { GlobalApi } from "./groups/global"
@@ -48,6 +49,7 @@ const EventSchema = Schema.Union([
export const ServerApi = makeApi({
definitions: EventManifest.Latest.values().toArray(),
locationMiddleware: LocationMiddleware,
formLocationMiddleware: FormLocationMiddleware,
sessionLocationMiddleware: SessionLocationMiddleware,
})
@@ -102,6 +102,7 @@ import { tuiHandlers } from "./handlers/tui"
import { handlers } from "@opencode-ai/server/handlers"
import { buildLocationServiceMap, LocationServiceMap } from "@opencode-ai/core/location-services"
import { layer as locationLayer } from "@opencode-ai/server/location"
import { formLocationLayer } from "@opencode-ai/server/middleware/form-location"
import { sessionLocationLayer } from "@opencode-ai/server/middleware/session-location"
import { PtyEnvironment } from "@opencode-ai/server/pty-environment"
import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error"
@@ -177,6 +178,7 @@ const instanceRoutes = instanceApiRoutes.pipe(
const serverRoutes = HttpApiBuilder.layer(Api).pipe(
Layer.provide(handlers),
Layer.provide(PluginPtyEnvironment.layer),
Layer.provide(formLocationLayer),
Layer.provide([serverHttpApiAuthLayer, v2SchemaErrorLayer]),
)
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { SessionEvent } from "@opencode-ai/core/session/event"
import { Agent } from "@opencode-ai/schema"
import { Agent, Form } from "@opencode-ai/schema"
import { EventManifest as SchemaEventManifest } from "@opencode-ai/schema/event-manifest"
import { Todo } from "@/session/todo"
import { EventManifest } from "@/event-manifest"
@@ -10,9 +10,12 @@ describe("public event manifest", () => {
expect(EventManifest.Definitions).toBe(SchemaEventManifest.Definitions)
expect(EventManifest.Latest).toBe(SchemaEventManifest.Latest)
expect(EventManifest.Durable).toBe(SchemaEventManifest.Durable)
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(93)
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(96)
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("form.created")).toBe(Form.Event.Created)
expect(EventManifest.Latest.get("form.replied")).toBe(Form.Event.Replied)
expect(EventManifest.Latest.get("form.cancelled")).toBe(Form.Event.Cancelled)
expect(EventManifest.Latest.get("todo.updated")).toBe(Todo.Event.Updated)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(EventManifest.Latest.has("server.connected")).toBe(true)
+211 -173
View File
@@ -410,71 +410,145 @@ export interface ProjectApi<E = never> {
readonly directories: ProjectDirectoriesOperation<E>
}
type Endpoint13_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
type Endpoint13_0Request = Parameters<RawClient["server.form"]["form.request.list"]>[0]
export type Endpoint13_0Input = { readonly location?: Endpoint13_0Request["query"]["location"] }
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
export type PermissionListRequestsOperation<E = never> = (
input?: Endpoint13_0Input,
) => Effect.Effect<Endpoint13_0Output, E>
export type Endpoint13_0Output = EffectValue<ReturnType<RawClient["server.form"]["form.request.list"]>>
export type FormListRequestsOperation<E = never> = (input?: Endpoint13_0Input) => Effect.Effect<Endpoint13_0Output, E>
type Endpoint13_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
export type Endpoint13_1Input = { readonly projectID?: Endpoint13_1Request["query"]["projectID"] }
export type Endpoint13_1Output = EffectValue<
type Endpoint13_1Request = Parameters<RawClient["server.form"]["session.form.list"]>[0]
export type Endpoint13_1Input = {
readonly sessionID: Endpoint13_1Request["params"]["sessionID"]
readonly location?: Endpoint13_1Request["query"]["location"]
}
export type Endpoint13_1Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.list"]>>
export type FormListOperation<E = never> = (input: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
type Endpoint13_2Request = Parameters<RawClient["server.form"]["session.form.create"]>[0]
export type Endpoint13_2Input = {
readonly sessionID: Endpoint13_2Request["params"]["sessionID"]
readonly location?: Endpoint13_2Request["query"]["location"]
readonly id?: Endpoint13_2Request["payload"]["id"]
readonly title?: Endpoint13_2Request["payload"]["title"]
readonly metadata?: Endpoint13_2Request["payload"]["metadata"]
readonly mode: Endpoint13_2Request["payload"]["mode"]
readonly fields?: Endpoint13_2Request["payload"]["fields"]
readonly url?: Endpoint13_2Request["payload"]["url"]
}
export type Endpoint13_2Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.create"]>>
export type FormCreateOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
type Endpoint13_3Request = Parameters<RawClient["server.form"]["session.form.get"]>[0]
export type Endpoint13_3Input = {
readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
readonly formID: Endpoint13_3Request["params"]["formID"]
readonly location?: Endpoint13_3Request["query"]["location"]
}
export type Endpoint13_3Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.get"]>>
export type FormGetOperation<E = never> = (input: Endpoint13_3Input) => Effect.Effect<Endpoint13_3Output, E>
type Endpoint13_4Request = Parameters<RawClient["server.form"]["session.form.state"]>[0]
export type Endpoint13_4Input = {
readonly sessionID: Endpoint13_4Request["params"]["sessionID"]
readonly formID: Endpoint13_4Request["params"]["formID"]
readonly location?: Endpoint13_4Request["query"]["location"]
}
export type Endpoint13_4Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.state"]>>
export type FormStateOperation<E = never> = (input: Endpoint13_4Input) => Effect.Effect<Endpoint13_4Output, E>
type Endpoint13_5Request = Parameters<RawClient["server.form"]["session.form.reply"]>[0]
export type Endpoint13_5Input = {
readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
readonly formID: Endpoint13_5Request["params"]["formID"]
readonly location?: Endpoint13_5Request["query"]["location"]
readonly answer: Endpoint13_5Request["payload"]["answer"]
}
export type Endpoint13_5Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.reply"]>>
export type FormReplyOperation<E = never> = (input: Endpoint13_5Input) => Effect.Effect<Endpoint13_5Output, E>
type Endpoint13_6Request = Parameters<RawClient["server.form"]["session.form.cancel"]>[0]
export type Endpoint13_6Input = {
readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
readonly formID: Endpoint13_6Request["params"]["formID"]
readonly location?: Endpoint13_6Request["query"]["location"]
}
export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.form"]["session.form.cancel"]>>
export type FormCancelOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
export interface FormApi<E = never> {
readonly listRequests: FormListRequestsOperation<E>
readonly list: FormListOperation<E>
readonly create: FormCreateOperation<E>
readonly get: FormGetOperation<E>
readonly state: FormStateOperation<E>
readonly reply: FormReplyOperation<E>
readonly cancel: FormCancelOperation<E>
}
type Endpoint14_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
export type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.request.list"]>>
export type PermissionListRequestsOperation<E = never> = (
input?: Endpoint14_0Input,
) => Effect.Effect<Endpoint14_0Output, E>
type Endpoint14_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
export type Endpoint14_1Input = { readonly projectID?: Endpoint14_1Request["query"]["projectID"] }
export type Endpoint14_1Output = EffectValue<
ReturnType<RawClient["server.permission"]["permission.saved.list"]>
>["data"]
export type PermissionListSavedOperation<E = never> = (
input?: Endpoint13_1Input,
) => Effect.Effect<Endpoint13_1Output, E>
input?: Endpoint14_1Input,
) => Effect.Effect<Endpoint14_1Output, E>
type Endpoint13_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
export type Endpoint13_2Input = { readonly id: Endpoint13_2Request["params"]["id"] }
export type Endpoint13_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
type Endpoint14_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
export type Endpoint14_2Input = { readonly id: Endpoint14_2Request["params"]["id"] }
export type Endpoint14_2Output = EffectValue<ReturnType<RawClient["server.permission"]["permission.saved.remove"]>>
export type PermissionRemoveSavedOperation<E = never> = (
input: Endpoint13_2Input,
) => Effect.Effect<Endpoint13_2Output, E>
input: Endpoint14_2Input,
) => Effect.Effect<Endpoint14_2Output, E>
type Endpoint13_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
export type Endpoint13_3Input = {
readonly sessionID: Endpoint13_3Request["params"]["sessionID"]
readonly id?: Endpoint13_3Request["payload"]["id"]
readonly action: Endpoint13_3Request["payload"]["action"]
readonly resources: Endpoint13_3Request["payload"]["resources"]
readonly save?: Endpoint13_3Request["payload"]["save"]
readonly metadata?: Endpoint13_3Request["payload"]["metadata"]
readonly source?: Endpoint13_3Request["payload"]["source"]
readonly agent?: Endpoint13_3Request["payload"]["agent"]
type Endpoint14_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
export type Endpoint14_3Input = {
readonly sessionID: Endpoint14_3Request["params"]["sessionID"]
readonly id?: Endpoint14_3Request["payload"]["id"]
readonly action: Endpoint14_3Request["payload"]["action"]
readonly resources: Endpoint14_3Request["payload"]["resources"]
readonly save?: Endpoint14_3Request["payload"]["save"]
readonly metadata?: Endpoint14_3Request["payload"]["metadata"]
readonly source?: Endpoint14_3Request["payload"]["source"]
readonly agent?: Endpoint14_3Request["payload"]["agent"]
}
export type Endpoint13_3Output = EffectValue<
export type Endpoint14_3Output = EffectValue<
ReturnType<RawClient["server.permission"]["session.permission.create"]>
>["data"]
export type PermissionCreateOperation<E = never> = (input: Endpoint13_3Input) => Effect.Effect<Endpoint13_3Output, E>
export type PermissionCreateOperation<E = never> = (input: Endpoint14_3Input) => Effect.Effect<Endpoint14_3Output, E>
type Endpoint13_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
export type Endpoint13_4Input = { readonly sessionID: Endpoint13_4Request["params"]["sessionID"] }
export type Endpoint13_4Output = EffectValue<
type Endpoint14_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
export type Endpoint14_4Input = { readonly sessionID: Endpoint14_4Request["params"]["sessionID"] }
export type Endpoint14_4Output = EffectValue<
ReturnType<RawClient["server.permission"]["session.permission.list"]>
>["data"]
export type PermissionListOperation<E = never> = (input: Endpoint13_4Input) => Effect.Effect<Endpoint13_4Output, E>
export type PermissionListOperation<E = never> = (input: Endpoint14_4Input) => Effect.Effect<Endpoint14_4Output, E>
type Endpoint13_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
export type Endpoint13_5Input = {
readonly sessionID: Endpoint13_5Request["params"]["sessionID"]
readonly requestID: Endpoint13_5Request["params"]["requestID"]
type Endpoint14_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
export type Endpoint14_5Input = {
readonly sessionID: Endpoint14_5Request["params"]["sessionID"]
readonly requestID: Endpoint14_5Request["params"]["requestID"]
}
export type Endpoint13_5Output = EffectValue<
export type Endpoint14_5Output = EffectValue<
ReturnType<RawClient["server.permission"]["session.permission.get"]>
>["data"]
export type PermissionGetOperation<E = never> = (input: Endpoint13_5Input) => Effect.Effect<Endpoint13_5Output, E>
export type PermissionGetOperation<E = never> = (input: Endpoint14_5Input) => Effect.Effect<Endpoint14_5Output, E>
type Endpoint13_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
export type Endpoint13_6Input = {
readonly sessionID: Endpoint13_6Request["params"]["sessionID"]
readonly requestID: Endpoint13_6Request["params"]["requestID"]
readonly reply: Endpoint13_6Request["payload"]["reply"]
readonly message?: Endpoint13_6Request["payload"]["message"]
type Endpoint14_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
export type Endpoint14_6Input = {
readonly sessionID: Endpoint14_6Request["params"]["sessionID"]
readonly requestID: Endpoint14_6Request["params"]["requestID"]
readonly reply: Endpoint14_6Request["payload"]["reply"]
readonly message?: Endpoint14_6Request["payload"]["message"]
}
export type Endpoint13_6Output = EffectValue<ReturnType<RawClient["server.permission"]["session.permission.reply"]>>
export type PermissionReplyOperation<E = never> = (input: Endpoint13_6Input) => Effect.Effect<Endpoint13_6Output, E>
export type Endpoint14_6Output = EffectValue<ReturnType<RawClient["server.permission"]["session.permission.reply"]>>
export type PermissionReplyOperation<E = never> = (input: Endpoint14_6Input) => Effect.Effect<Endpoint14_6Output, E>
export interface PermissionApi<E = never> {
readonly listRequests: PermissionListRequestsOperation<E>
@@ -486,96 +560,96 @@ export interface PermissionApi<E = never> {
readonly reply: PermissionReplyOperation<E>
}
type Endpoint14_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
export type Endpoint14_0Input = {
readonly location?: Endpoint14_0Request["query"]["location"]
readonly path?: Endpoint14_0Request["query"]["path"]
type Endpoint15_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
export type Endpoint15_0Input = {
readonly location?: Endpoint15_0Request["query"]["location"]
readonly path?: Endpoint15_0Request["query"]["path"]
}
export type Endpoint14_0Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.list"]>>
export type FileListOperation<E = never> = (input?: Endpoint14_0Input) => Effect.Effect<Endpoint14_0Output, E>
export type Endpoint15_0Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.list"]>>
export type FileListOperation<E = never> = (input?: Endpoint15_0Input) => Effect.Effect<Endpoint15_0Output, E>
type Endpoint14_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
export type Endpoint14_1Input = {
readonly location?: Endpoint14_1Request["query"]["location"]
readonly query: Endpoint14_1Request["query"]["query"]
readonly type?: Endpoint14_1Request["query"]["type"]
readonly limit?: Endpoint14_1Request["query"]["limit"]
type Endpoint15_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
export type Endpoint15_1Input = {
readonly location?: Endpoint15_1Request["query"]["location"]
readonly query: Endpoint15_1Request["query"]["query"]
readonly type?: Endpoint15_1Request["query"]["type"]
readonly limit?: Endpoint15_1Request["query"]["limit"]
}
export type Endpoint14_1Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.find"]>>
export type FileFindOperation<E = never> = (input: Endpoint14_1Input) => Effect.Effect<Endpoint14_1Output, E>
export type Endpoint15_1Output = EffectValue<ReturnType<RawClient["server.fs"]["fs.find"]>>
export type FileFindOperation<E = never> = (input: Endpoint15_1Input) => Effect.Effect<Endpoint15_1Output, E>
export interface FileApi<E = never> {
readonly list: FileListOperation<E>
readonly find: FileFindOperation<E>
}
type Endpoint15_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
export type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
export type Endpoint15_0Output = EffectValue<ReturnType<RawClient["server.command"]["command.list"]>>
export type CommandListOperation<E = never> = (input?: Endpoint15_0Input) => Effect.Effect<Endpoint15_0Output, E>
type Endpoint16_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
export type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
export type Endpoint16_0Output = EffectValue<ReturnType<RawClient["server.command"]["command.list"]>>
export type CommandListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
export interface CommandApi<E = never> {
readonly list: CommandListOperation<E>
}
type Endpoint16_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
export type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
export type Endpoint16_0Output = EffectValue<ReturnType<RawClient["server.skill"]["skill.list"]>>
export type SkillListOperation<E = never> = (input?: Endpoint16_0Input) => Effect.Effect<Endpoint16_0Output, E>
type Endpoint17_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
export type Endpoint17_0Input = { readonly location?: Endpoint17_0Request["query"]["location"] }
export type Endpoint17_0Output = EffectValue<ReturnType<RawClient["server.skill"]["skill.list"]>>
export type SkillListOperation<E = never> = (input?: Endpoint17_0Input) => Effect.Effect<Endpoint17_0Output, E>
export interface SkillApi<E = never> {
readonly list: SkillListOperation<E>
}
export type Endpoint17_0Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.subscribe"]>>>
export type EventSubscribeOperation<E = never> = () => Stream.Stream<Endpoint17_0Output, E>
export type Endpoint18_0Output = StreamValue<EffectValue<ReturnType<RawClient["server.event"]["event.subscribe"]>>>
export type EventSubscribeOperation<E = never> = () => Stream.Stream<Endpoint18_0Output, E>
export interface EventApi<E = never> {
readonly subscribe: EventSubscribeOperation<E>
}
type Endpoint18_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
export type Endpoint18_0Input = { readonly location?: Endpoint18_0Request["query"]["location"] }
export type Endpoint18_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
export type PtyListOperation<E = never> = (input?: Endpoint18_0Input) => Effect.Effect<Endpoint18_0Output, E>
type Endpoint19_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
export type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
export type Endpoint19_0Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.list"]>>
export type PtyListOperation<E = never> = (input?: Endpoint19_0Input) => Effect.Effect<Endpoint19_0Output, E>
type Endpoint18_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
export type Endpoint18_1Input = {
readonly location?: Endpoint18_1Request["query"]["location"]
readonly command?: Endpoint18_1Request["payload"]["command"]
readonly args?: Endpoint18_1Request["payload"]["args"]
readonly cwd?: Endpoint18_1Request["payload"]["cwd"]
readonly title?: Endpoint18_1Request["payload"]["title"]
readonly env?: Endpoint18_1Request["payload"]["env"]
type Endpoint19_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
export type Endpoint19_1Input = {
readonly location?: Endpoint19_1Request["query"]["location"]
readonly command?: Endpoint19_1Request["payload"]["command"]
readonly args?: Endpoint19_1Request["payload"]["args"]
readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
readonly title?: Endpoint19_1Request["payload"]["title"]
readonly env?: Endpoint19_1Request["payload"]["env"]
}
export type Endpoint18_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
export type PtyCreateOperation<E = never> = (input?: Endpoint18_1Input) => Effect.Effect<Endpoint18_1Output, E>
export type Endpoint19_1Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.create"]>>
export type PtyCreateOperation<E = never> = (input?: Endpoint19_1Input) => Effect.Effect<Endpoint19_1Output, E>
type Endpoint18_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
export type Endpoint18_2Input = {
readonly ptyID: Endpoint18_2Request["params"]["ptyID"]
readonly location?: Endpoint18_2Request["query"]["location"]
type Endpoint19_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
export type Endpoint19_2Input = {
readonly ptyID: Endpoint19_2Request["params"]["ptyID"]
readonly location?: Endpoint19_2Request["query"]["location"]
}
export type Endpoint18_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
export type PtyGetOperation<E = never> = (input: Endpoint18_2Input) => Effect.Effect<Endpoint18_2Output, E>
export type Endpoint19_2Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.get"]>>
export type PtyGetOperation<E = never> = (input: Endpoint19_2Input) => Effect.Effect<Endpoint19_2Output, E>
type Endpoint18_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
export type Endpoint18_3Input = {
readonly ptyID: Endpoint18_3Request["params"]["ptyID"]
readonly location?: Endpoint18_3Request["query"]["location"]
readonly title?: Endpoint18_3Request["payload"]["title"]
readonly size?: Endpoint18_3Request["payload"]["size"]
type Endpoint19_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
export type Endpoint19_3Input = {
readonly ptyID: Endpoint19_3Request["params"]["ptyID"]
readonly location?: Endpoint19_3Request["query"]["location"]
readonly title?: Endpoint19_3Request["payload"]["title"]
readonly size?: Endpoint19_3Request["payload"]["size"]
}
export type Endpoint18_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
export type PtyUpdateOperation<E = never> = (input: Endpoint18_3Input) => Effect.Effect<Endpoint18_3Output, E>
export type Endpoint19_3Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.update"]>>
export type PtyUpdateOperation<E = never> = (input: Endpoint19_3Input) => Effect.Effect<Endpoint19_3Output, E>
type Endpoint18_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
export type Endpoint18_4Input = {
readonly ptyID: Endpoint18_4Request["params"]["ptyID"]
readonly location?: Endpoint18_4Request["query"]["location"]
type Endpoint19_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
export type Endpoint19_4Input = {
readonly ptyID: Endpoint19_4Request["params"]["ptyID"]
readonly location?: Endpoint19_4Request["query"]["location"]
}
export type Endpoint18_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
export type PtyRemoveOperation<E = never> = (input: Endpoint18_4Input) => Effect.Effect<Endpoint18_4Output, E>
export type Endpoint19_4Output = EffectValue<ReturnType<RawClient["server.pty"]["pty.remove"]>>
export type PtyRemoveOperation<E = never> = (input: Endpoint19_4Input) => Effect.Effect<Endpoint19_4Output, E>
export interface PtyApi<E = never> {
readonly list: PtyListOperation<E>
@@ -585,47 +659,47 @@ export interface PtyApi<E = never> {
readonly remove: PtyRemoveOperation<E>
}
type Endpoint19_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
export type Endpoint19_0Input = { readonly location?: Endpoint19_0Request["query"]["location"] }
export type Endpoint19_0Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.list"]>>
export type ShellListOperation<E = never> = (input?: Endpoint19_0Input) => Effect.Effect<Endpoint19_0Output, E>
type Endpoint20_0Request = Parameters<RawClient["server.shell"]["shell.list"]>[0]
export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.list"]>>
export type ShellListOperation<E = never> = (input?: Endpoint20_0Input) => Effect.Effect<Endpoint20_0Output, E>
type Endpoint19_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
export type Endpoint19_1Input = {
readonly location?: Endpoint19_1Request["query"]["location"]
readonly command: Endpoint19_1Request["payload"]["command"]
readonly cwd?: Endpoint19_1Request["payload"]["cwd"]
readonly timeout?: Endpoint19_1Request["payload"]["timeout"]
readonly metadata?: Endpoint19_1Request["payload"]["metadata"]
type Endpoint20_1Request = Parameters<RawClient["server.shell"]["shell.create"]>[0]
export type Endpoint20_1Input = {
readonly location?: Endpoint20_1Request["query"]["location"]
readonly command: Endpoint20_1Request["payload"]["command"]
readonly cwd?: Endpoint20_1Request["payload"]["cwd"]
readonly timeout?: Endpoint20_1Request["payload"]["timeout"]
readonly metadata?: Endpoint20_1Request["payload"]["metadata"]
}
export type Endpoint19_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
export type ShellCreateOperation<E = never> = (input: Endpoint19_1Input) => Effect.Effect<Endpoint19_1Output, E>
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.create"]>>
export type ShellCreateOperation<E = never> = (input: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
type Endpoint19_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
export type Endpoint19_2Input = {
readonly id: Endpoint19_2Request["params"]["id"]
readonly location?: Endpoint19_2Request["query"]["location"]
type Endpoint20_2Request = Parameters<RawClient["server.shell"]["shell.get"]>[0]
export type Endpoint20_2Input = {
readonly id: Endpoint20_2Request["params"]["id"]
readonly location?: Endpoint20_2Request["query"]["location"]
}
export type Endpoint19_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
export type ShellGetOperation<E = never> = (input: Endpoint19_2Input) => Effect.Effect<Endpoint19_2Output, E>
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.get"]>>
export type ShellGetOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
type Endpoint19_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
export type Endpoint19_3Input = {
readonly id: Endpoint19_3Request["params"]["id"]
readonly location?: Endpoint19_3Request["query"]["location"]
readonly cursor?: Endpoint19_3Request["query"]["cursor"]
readonly limit?: Endpoint19_3Request["query"]["limit"]
type Endpoint20_3Request = Parameters<RawClient["server.shell"]["shell.output"]>[0]
export type Endpoint20_3Input = {
readonly id: Endpoint20_3Request["params"]["id"]
readonly location?: Endpoint20_3Request["query"]["location"]
readonly cursor?: Endpoint20_3Request["query"]["cursor"]
readonly limit?: Endpoint20_3Request["query"]["limit"]
}
export type Endpoint19_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
export type ShellOutputOperation<E = never> = (input: Endpoint19_3Input) => Effect.Effect<Endpoint19_3Output, E>
export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
export type ShellOutputOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
type Endpoint19_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
export type Endpoint19_4Input = {
readonly id: Endpoint19_4Request["params"]["id"]
readonly location?: Endpoint19_4Request["query"]["location"]
type Endpoint20_4Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
export type Endpoint20_4Input = {
readonly id: Endpoint20_4Request["params"]["id"]
readonly location?: Endpoint20_4Request["query"]["location"]
}
export type Endpoint19_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
export type ShellRemoveOperation<E = never> = (input: Endpoint19_4Input) => Effect.Effect<Endpoint19_4Output, E>
export type Endpoint20_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.remove"]>>
export type ShellRemoveOperation<E = never> = (input: Endpoint20_4Input) => Effect.Effect<Endpoint20_4Output, E>
export interface ShellApi<E = never> {
readonly list: ShellListOperation<E>
@@ -635,42 +709,6 @@ export interface ShellApi<E = never> {
readonly remove: ShellRemoveOperation<E>
}
type Endpoint20_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] }
export type Endpoint20_0Output = EffectValue<ReturnType<RawClient["server.question"]["question.request.list"]>>
export type QuestionListRequestsOperation<E = never> = (
input?: Endpoint20_0Input,
) => Effect.Effect<Endpoint20_0Output, E>
type Endpoint20_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
export type Endpoint20_1Input = { readonly sessionID: Endpoint20_1Request["params"]["sessionID"] }
export type Endpoint20_1Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.list"]>>["data"]
export type QuestionListOperation<E = never> = (input: Endpoint20_1Input) => Effect.Effect<Endpoint20_1Output, E>
type Endpoint20_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
export type Endpoint20_2Input = {
readonly sessionID: Endpoint20_2Request["params"]["sessionID"]
readonly requestID: Endpoint20_2Request["params"]["requestID"]
readonly answers: Endpoint20_2Request["payload"]["answers"]
}
export type Endpoint20_2Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reply"]>>
export type QuestionReplyOperation<E = never> = (input: Endpoint20_2Input) => Effect.Effect<Endpoint20_2Output, E>
type Endpoint20_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
export type Endpoint20_3Input = {
readonly sessionID: Endpoint20_3Request["params"]["sessionID"]
readonly requestID: Endpoint20_3Request["params"]["requestID"]
}
export type Endpoint20_3Output = EffectValue<ReturnType<RawClient["server.question"]["session.question.reject"]>>
export type QuestionRejectOperation<E = never> = (input: Endpoint20_3Input) => Effect.Effect<Endpoint20_3Output, E>
export interface QuestionApi<E = never> {
readonly listRequests: QuestionListRequestsOperation<E>
readonly list: QuestionListOperation<E>
readonly reply: QuestionReplyOperation<E>
readonly reject: QuestionRejectOperation<E>
}
type Endpoint21_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
export type Endpoint21_0Input = { readonly location?: Endpoint21_0Request["query"]["location"] }
export type Endpoint21_0Output = EffectValue<ReturnType<RawClient["server.reference"]["reference.list"]>>
@@ -729,6 +767,7 @@ export interface AppApi<E = never> {
readonly "server.mcp": ServerMcpApi<E>
readonly credential: CredentialApi<E>
readonly project: ProjectApi<E>
readonly form: FormApi<E>
readonly permission: PermissionApi<E>
readonly file: FileApi<E>
readonly command: CommandApi<E>
@@ -736,7 +775,6 @@ export interface AppApi<E = never> {
readonly event: EventApi<E>
readonly pty: PtyApi<E>
readonly shell: ShellApi<E>
readonly question: QuestionApi<E>
readonly reference: ReferenceApi<E>
readonly projectCopy: ProjectCopyApi<E>
}
+52 -12
View File
@@ -8,6 +8,7 @@ import { ProviderGroup } from "./groups/provider.js"
import { makeSessionGroup } from "./groups/session.js"
import { makePermissionGroup } from "./groups/permission.js"
import { FileSystemGroup } from "./groups/fs.js"
import { makeFormGroup } from "./groups/form.js"
import { CommandGroup } from "./groups/command.js"
import { SkillGroup } from "./groups/skill.js"
import { EventGroup, makeEventGroup } from "./groups/event.js"
@@ -17,7 +18,6 @@ import { PluginGroup } from "./groups/plugin.js"
import { HealthGroup } from "./groups/health.js"
import { PtyGroup } from "./groups/pty.js"
import { ShellGroup } from "./groups/shell.js"
import { makeQuestionGroup } from "./groups/question.js"
import { ReferenceGroup } from "./groups/reference.js"
import { Authorization } from "./middleware/authorization.js"
import { LocationGroup } from "./groups/location.js"
@@ -50,26 +50,35 @@ type SessionGroups<SessionLocationId extends HttpApiMiddleware.AnyId, SessionLoc
| ReturnType<typeof makeSessionGroup<SessionLocationId, SessionLocationService>>
| HttpApiGroup.AddMiddleware<typeof MessageGroup, SessionLocationId>
type FormGroups<
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
FormLocationId extends HttpApiMiddleware.AnyId,
FormLocationService,
> = ReturnType<
typeof makeFormGroup<LocationId, LocationService, FormLocationId, FormLocationService>
>
type MixedMiddlewareGroups<
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
> =
| ReturnType<
typeof makePermissionGroup<LocationId, LocationService, SessionLocationId, SessionLocationService>
>
| ReturnType<typeof makeQuestionGroup<LocationId, LocationService, SessionLocationId, SessionLocationService>>
ReturnType<typeof makePermissionGroup<LocationId, LocationService, SessionLocationId, SessionLocationService>>
type ApiGroups<
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
FormLocationId extends HttpApiMiddleware.AnyId,
FormLocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
Event extends HttpApiGroup.Any,
> =
| typeof HealthGroup
| LocationGroups<LocationId>
| FormGroups<LocationId, LocationService, FormLocationId, FormLocationService>
| SessionGroups<SessionLocationId, SessionLocationService>
| MixedMiddlewareGroups<LocationId, LocationService, SessionLocationId, SessionLocationService>
| Event
@@ -79,6 +88,8 @@ type EventGroupFor<Definitions extends ReadonlyArray<Definition>> = ReturnType<t
export type Api<
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
FormLocationId extends HttpApiMiddleware.AnyId,
FormLocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
Event extends HttpApiGroup.Any,
@@ -86,7 +97,7 @@ export type Api<
"server",
HttpApiGroup.AddMiddleware<
HttpApiGroup.AddMiddleware<
ApiGroups<LocationId, LocationService, SessionLocationId, SessionLocationService, Event>,
ApiGroups<LocationId, LocationService, FormLocationId, FormLocationService, SessionLocationId, SessionLocationService, Event>,
Authorization
>,
SchemaErrorMiddleware
@@ -98,13 +109,16 @@ const makeApiFromGroup = <
const Group extends HttpApiGroup.Any,
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
FormLocationId extends HttpApiMiddleware.AnyId,
FormLocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
>(
eventGroup: Group,
locationMiddleware: Context.Key<LocationId, LocationService>,
formLocationMiddleware: Context.Key<FormLocationId, FormLocationService>,
sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>,
): Api<LocationId, LocationService, SessionLocationId, SessionLocationService, Group> =>
): Api<LocationId, LocationService, FormLocationId, FormLocationService, SessionLocationId, SessionLocationService, Group> =>
HttpApi.make("server")
.add(HealthGroup)
.add(LocationGroup.middleware(locationMiddleware))
@@ -119,6 +133,7 @@ const makeApiFromGroup = <
.add(McpGroup.middleware(locationMiddleware))
.add(CredentialGroup.middleware(locationMiddleware))
.add(ProjectGroup.middleware(locationMiddleware))
.add(makeFormGroup(locationMiddleware, formLocationMiddleware))
.add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware))
.add(FileSystemGroup.middleware(locationMiddleware))
.add(CommandGroup.middleware(locationMiddleware))
@@ -126,7 +141,6 @@ const makeApiFromGroup = <
.add(eventGroup)
.add(PtyGroup.middleware(locationMiddleware))
.add(ShellGroup.middleware(locationMiddleware))
.add(makeQuestionGroup(locationMiddleware, sessionLocationMiddleware))
.add(ReferenceGroup.middleware(locationMiddleware))
.add(ProjectCopyGroup.middleware(locationMiddleware))
.annotateMerge(
@@ -143,22 +157,48 @@ export const makeApi = <
const Definitions extends ReadonlyArray<Definition>,
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
FormLocationId extends HttpApiMiddleware.AnyId,
FormLocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
>(options: {
readonly definitions: Definitions
readonly locationMiddleware: Context.Key<LocationId, LocationService>
readonly formLocationMiddleware: Context.Key<FormLocationId, FormLocationService>
readonly sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>
}): Api<LocationId, LocationService, SessionLocationId, SessionLocationService, EventGroupFor<Definitions>> =>
makeApiFromGroup(makeEventGroup(options.definitions), options.locationMiddleware, options.sessionLocationMiddleware)
}): Api<
LocationId,
LocationService,
FormLocationId,
FormLocationService,
SessionLocationId,
SessionLocationService,
EventGroupFor<Definitions>
> =>
makeApiFromGroup(
makeEventGroup(options.definitions),
options.locationMiddleware,
options.formLocationMiddleware,
options.sessionLocationMiddleware,
)
export const makeDefaultApi = <
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
FormLocationId extends HttpApiMiddleware.AnyId,
FormLocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
>(options: {
readonly locationMiddleware: Context.Key<LocationId, LocationService>
readonly formLocationMiddleware: Context.Key<FormLocationId, FormLocationService>
readonly sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>
}): Api<LocationId, LocationService, SessionLocationId, SessionLocationService, typeof EventGroup> =>
makeApiFromGroup(EventGroup, options.locationMiddleware, options.sessionLocationMiddleware)
}): Api<
LocationId,
LocationService,
FormLocationId,
FormLocationService,
SessionLocationId,
SessionLocationService,
typeof EventGroup
> => makeApiFromGroup(EventGroup, options.locationMiddleware, options.formLocationMiddleware, options.sessionLocationMiddleware)
+7 -2
View File
@@ -19,11 +19,16 @@ type ClientApiShape = Api<
Context.Service.Shape<typeof LocationMiddleware>,
Context.Service.Identifier<typeof SessionLocationMiddleware>,
Context.Service.Shape<typeof SessionLocationMiddleware>,
Context.Service.Identifier<typeof SessionLocationMiddleware>,
Context.Service.Shape<typeof SessionLocationMiddleware>,
typeof EventGroup
>
export const ClientApi: ClientApiShape = makeDefaultApi({
locationMiddleware: LocationMiddleware,
// The real server uses a form-specific middleware with an undocumented `global` sentinel branch.
// The generated client only needs a middleware identity for API typing.
formLocationMiddleware: SessionLocationMiddleware,
sessionLocationMiddleware: SessionLocationMiddleware,
})
@@ -40,13 +45,13 @@ export const groupNames = {
"server.integration": "integration",
"server.credential": "credential",
"server.permission": "permission",
"server.form": "form",
"server.fs": "file",
"server.command": "command",
"server.skill": "skill",
"server.event": "event",
"server.pty": "pty",
"server.shell": "shell",
"server.question": "question",
"server.reference": "reference",
"server.project": "project",
"server.projectCopy": "projectCopy",
@@ -65,7 +70,7 @@ export const endpointNames = {
"permission.request.list": "listRequests",
"permission.saved.list": "listSaved",
"permission.saved.remove": "removeSaved",
"question.request.list": "listRequests",
"form.request.list": "listRequests",
} as const
export const promiseOmitEndpoints = new Set(["pty.connect", "pty.connectToken"])
+21 -3
View File
@@ -104,15 +104,33 @@ export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionN
{ httpApiStatus: 404 },
) {}
export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFoundError>()(
"QuestionNotFoundError",
export class FormNotFoundError extends Schema.TaggedErrorClass<FormNotFoundError>()(
"FormNotFoundError",
{
requestID: Schema.String,
id: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 404 },
) {}
export class FormAlreadySettledError extends Schema.TaggedErrorClass<FormAlreadySettledError>()(
"FormAlreadySettledError",
{
id: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 409 },
) {}
export class FormInvalidAnswerError extends Schema.TaggedErrorClass<FormInvalidAnswerError>()(
"FormInvalidAnswerError",
{
id: Schema.String,
message: Schema.String,
},
{ httpApiStatus: 400 },
) {}
export class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()(
"ForbiddenError",
{ message: Schema.String },
+151
View File
@@ -0,0 +1,151 @@
import { Form } from "@opencode-ai/schema/form"
import { Location } from "@opencode-ai/schema/location"
import { Context, Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { ConflictError, FormAlreadySettledError, FormInvalidAnswerError, FormNotFoundError, InvalidRequestError } from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
const CreatePayload = Schema.Struct({
id: Form.ID.pipe(Schema.optional),
title: Form.FormInfo.fields.title,
metadata: Form.FormInfo.fields.metadata,
mode: Schema.Literals(["form", "url"]),
fields: Form.FormInfo.fields.fields.pipe(Schema.optional),
url: Form.UrlInfo.fields.url.pipe(Schema.optional),
}).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,
>(
locationMiddleware: Context.Key<LocationId, LocationService>,
formLocationMiddleware: Context.Key<FormLocationId, FormLocationService>,
) =>
HttpApiGroup.make("server.form")
.add(
HttpApiEndpoint.get("form.request.list", "/api/form/request", {
query: LocationQuery,
success: Location.response(Schema.Array(Form.Info)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.form.request.list",
summary: "List pending form requests",
description: "Retrieve pending forms for a location.",
}),
),
)
.middleware(locationMiddleware)
.add(
HttpApiEndpoint.get("session.form.list", "/api/session/:sessionID/form", {
params: { sessionID: Schema.String },
query: LocationQuery,
success: Location.response(Schema.Array(Form.Info)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.form.list",
summary: "List session forms",
description: "Retrieve pending forms for a session.",
}),
)
.middleware(formLocationMiddleware),
)
.add(
HttpApiEndpoint.post("session.form.create", "/api/session/:sessionID/form", {
params: { sessionID: Schema.String },
query: LocationQuery,
payload: CreatePayload,
success: Location.response(Form.Info),
error: [ConflictError, InvalidRequestError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.form.create",
summary: "Create session form",
description: "Create a form for a session.",
}),
)
.middleware(formLocationMiddleware),
)
.add(
HttpApiEndpoint.get("session.form.get", "/api/session/:sessionID/form/:formID", {
params: { sessionID: Schema.String, formID: Form.ID },
query: LocationQuery,
success: Location.response(Form.Info),
error: FormNotFoundError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.form.get",
summary: "Get session form",
description: "Retrieve a form for a session.",
}),
)
.middleware(formLocationMiddleware),
)
.add(
HttpApiEndpoint.get("session.form.state", "/api/session/:sessionID/form/:formID/state", {
params: { sessionID: Schema.String, formID: Form.ID },
query: LocationQuery,
success: Location.response(Form.State),
error: FormNotFoundError,
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.form.state",
summary: "Get form state",
description: "Retrieve the current state for a form.",
}),
)
.middleware(formLocationMiddleware),
)
.add(
HttpApiEndpoint.post("session.form.reply", "/api/session/:sessionID/form/:formID/reply", {
params: { sessionID: Schema.String, formID: Form.ID },
query: LocationQuery,
payload: Form.Reply,
success: HttpApiSchema.NoContent,
error: [FormAlreadySettledError, FormInvalidAnswerError, FormNotFoundError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.form.reply",
summary: "Reply to form",
description: "Submit an answer to a pending form.",
}),
)
.middleware(formLocationMiddleware),
)
.add(
HttpApiEndpoint.post("session.form.cancel", "/api/session/:sessionID/form/:formID/cancel", {
params: { sessionID: Schema.String, formID: Form.ID },
query: LocationQuery,
success: HttpApiSchema.NoContent,
error: [FormAlreadySettledError, FormNotFoundError],
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.form.cancel",
summary: "Cancel form",
description: "Cancel a pending form.",
}),
)
.middleware(formLocationMiddleware),
)
.annotateMerge(OpenApi.annotations({ title: "forms", description: "Session form routes." }))
-84
View File
@@ -1,84 +0,0 @@
import { Question } from "@opencode-ai/schema/question"
import { Location } from "@opencode-ai/schema/location"
import { Session } from "@opencode-ai/schema/session"
import { Context, Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { QuestionNotFoundError, SessionNotFoundError } from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const makeQuestionGroup = <
LocationId extends HttpApiMiddleware.AnyId,
LocationService,
SessionLocationId extends HttpApiMiddleware.AnyId,
SessionLocationService,
>(
locationMiddleware: Context.Key<LocationId, LocationService>,
sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>,
) =>
HttpApiGroup.make("server.question")
.add(
HttpApiEndpoint.get("question.request.list", "/api/question/request", {
query: LocationQuery,
success: Location.response(Schema.Array(Question.Request)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.question.request.list",
summary: "List pending question requests",
description: "Retrieve pending question requests for a location.",
}),
),
)
.annotateMerge(OpenApi.annotations({ title: "questions", description: "Experimental question routes." }))
// Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
.middleware(locationMiddleware)
.add(
HttpApiEndpoint.get("session.question.list", "/api/session/:sessionID/question", {
params: { sessionID: Session.ID },
success: Schema.Struct({ data: Schema.Array(Question.Request) }),
error: SessionNotFoundError,
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.list",
summary: "List session question requests",
description: "Retrieve pending question requests owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("session.question.reply", "/api/session/:sessionID/question/:requestID/reply", {
params: { sessionID: Session.ID, requestID: Question.ID },
payload: Question.Reply,
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reply",
summary: "Reply to pending question request",
description: "Answer a pending question request owned by a session.",
}),
),
)
.add(
HttpApiEndpoint.post("session.question.reject", "/api/session/:sessionID/question/:requestID/reject", {
params: { sessionID: Session.ID, requestID: Question.ID },
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, QuestionNotFoundError],
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.session.question.reject",
summary: "Reject pending question request",
description: "Reject a pending question request owned by a session.",
}),
),
)
.annotateMerge(
OpenApi.annotations({ title: "session questions", description: "Experimental session question routes." }),
)
+2 -2
View File
@@ -6,6 +6,7 @@ import { Durable } from "./durable-event-manifest.js"
import { Event } from "./event.js"
import { FileSystem } from "./filesystem.js"
import { FileSystemWatcher } from "./filesystem-watcher.js"
import { Form } from "./form.js"
import { InstallationEvent } from "./installation-event.js"
import { Integration } from "./integration.js"
import { LegacyEvent } from "./legacy-event.js"
@@ -18,7 +19,6 @@ import { Plugin } from "./plugin.js"
import { Project } from "./project.js"
import { ProjectDirectories } from "./project-directories.js"
import { Pty } from "./pty.js"
import { Question } from "./question.js"
import { QuestionV1 } from "./question-v1.js"
import { Reference } from "./reference.js"
import { ServerEvent } from "./server-event.js"
@@ -57,7 +57,7 @@ const featureDefinitions = Event.inventory(
...FileSystemWatcher.Event.Definitions,
...Pty.Event.Definitions,
...Shell.Event.Definitions,
...Question.Event.Definitions,
...Form.Event.Definitions,
)
export const ServerDefinitions = Event.inventory(
+149
View File
@@ -0,0 +1,149 @@
export * as Form from "./form.js"
import { Schema } from "effect"
import { define, inventory } from "./event.js"
import { ascending } from "./identifier.js"
import { NonNegativeInt, optional, statics } from "./schema.js"
const IDSchema = Schema.String.check(Schema.isStartsWith("frm_")).pipe(Schema.brand("Form.ID"))
export const ID = IDSchema.pipe(
statics((schema: typeof IDSchema) => ({ create: (id?: string) => schema.make(id ?? "frm_" + ascending()) })),
)
export type ID = typeof ID.Type
export const Metadata = Schema.Record(Schema.String, Schema.Unknown).annotate({ identifier: "Form.Metadata" })
export type Metadata = typeof Metadata.Type
export const Option = Schema.Struct({
value: Schema.String,
label: Schema.String,
description: Schema.String.pipe(optional),
}).annotate({ identifier: "Form.Option" })
export interface Option extends Schema.Schema.Type<typeof Option> {}
export const When = Schema.Struct({
key: Schema.String,
op: Schema.Literals(["eq", "neq"]),
value: Schema.String,
}).annotate({ identifier: "Form.When" })
export interface When extends Schema.Schema.Type<typeof When> {}
const FieldBase = {
key: Schema.String,
title: Schema.String.pipe(optional),
description: Schema.String.pipe(optional),
required: Schema.Boolean.pipe(optional),
when: When.pipe(optional),
}
export const StringField = Schema.Struct({
...FieldBase,
type: Schema.Literal("string"),
format: Schema.Literals(["email", "uri", "date", "date-time"]).pipe(optional),
minLength: NonNegativeInt.pipe(optional),
maxLength: NonNegativeInt.pipe(optional),
pattern: Schema.String.pipe(optional),
placeholder: Schema.String.pipe(optional),
default: Schema.String.pipe(optional),
options: Schema.Array(Option).pipe(optional),
custom: Schema.Boolean.pipe(optional),
}).annotate({ identifier: "Form.StringField" })
export interface StringField extends Schema.Schema.Type<typeof StringField> {}
export const NumberField = Schema.Struct({
...FieldBase,
type: Schema.Literal("number"),
minimum: Schema.Number.pipe(optional),
maximum: Schema.Number.pipe(optional),
default: Schema.Number.pipe(optional),
}).annotate({ identifier: "Form.NumberField" })
export interface NumberField extends Schema.Schema.Type<typeof NumberField> {}
export const IntegerField = Schema.Struct({
...FieldBase,
type: Schema.Literal("integer"),
minimum: Schema.Number.pipe(optional),
maximum: Schema.Number.pipe(optional),
default: Schema.Number.pipe(optional),
}).annotate({ identifier: "Form.IntegerField" })
export interface IntegerField extends Schema.Schema.Type<typeof IntegerField> {}
export const BooleanField = Schema.Struct({
...FieldBase,
type: Schema.Literal("boolean"),
default: Schema.Boolean.pipe(optional),
}).annotate({ identifier: "Form.BooleanField" })
export interface BooleanField extends Schema.Schema.Type<typeof BooleanField> {}
export const MultiselectField = Schema.Struct({
...FieldBase,
type: Schema.Literal("multiselect"),
options: Schema.Array(Option),
minItems: NonNegativeInt.pipe(optional),
maxItems: NonNegativeInt.pipe(optional),
custom: Schema.Boolean.pipe(optional),
default: Schema.Array(Schema.String).pipe(optional),
}).annotate({ identifier: "Form.MultiselectField" })
export interface MultiselectField extends Schema.Schema.Type<typeof MultiselectField> {}
export const Field = Schema.Union([StringField, NumberField, IntegerField, BooleanField, MultiselectField]).pipe(
Schema.toTaggedUnion("type"),
)
export type Field = StringField | NumberField | IntegerField | BooleanField | MultiselectField
const InfoBase = {
id: ID,
// Public form flows are session-owned. This is intentionally `string` because the server
// currently accepts an undocumented `global` sentinel for MCP elicitation forms that cannot
// be attributed to a concrete session yet. Do not document or rely on `global` outside our
// own clients; remove the sentinel path once MCP elicitation can carry session ownership.
sessionID: Schema.String,
title: Schema.String.pipe(optional),
metadata: Metadata.pipe(optional),
}
export const FormInfo = Schema.Struct({
...InfoBase,
mode: Schema.Literal("form"),
fields: Schema.Array(Field),
}).annotate({ identifier: "Form.FormInfo" })
export interface FormInfo extends Schema.Schema.Type<typeof FormInfo> {}
export const UrlInfo = Schema.Struct({
...InfoBase,
mode: Schema.Literal("url"),
url: Schema.String,
}).annotate({ identifier: "Form.UrlInfo" })
export interface UrlInfo extends Schema.Schema.Type<typeof UrlInfo> {}
export const Info = Schema.Union([FormInfo, UrlInfo]).pipe(Schema.toTaggedUnion("mode"))
export type Info = FormInfo | UrlInfo
export const Value = Schema.Union([Schema.String, Schema.Number, Schema.Boolean, Schema.Array(Schema.String)]).annotate({
identifier: "Form.Value",
})
export type Value = typeof Value.Type
export const Answer = Schema.Record(Schema.String, Value).annotate({ identifier: "Form.Answer" })
export type Answer = typeof Answer.Type
export const State = Schema.Union([
Schema.Struct({ status: Schema.Literal("pending") }),
Schema.Struct({ status: Schema.Literal("answered"), answer: Answer }),
Schema.Struct({ status: Schema.Literal("cancelled") }),
])
.pipe(Schema.toTaggedUnion("status"))
.annotate({ identifier: "Form.State" })
export type State = typeof State.Type
export const Reply = Schema.Struct({
answer: Answer,
}).annotate({ identifier: "Form.Reply" })
export interface Reply extends Schema.Schema.Type<typeof Reply> {}
const Created = define({ type: "form.created", schema: { form: Info } })
const Replied = define({ type: "form.replied", schema: { id: ID, sessionID: Schema.String, answer: Answer } })
const Cancelled = define({ type: "form.cancelled", schema: { id: ID, sessionID: Schema.String } })
export const Event = { Created, Replied, Cancelled, Definitions: inventory(Created, Replied, Cancelled) }
+1 -1
View File
@@ -4,6 +4,7 @@ export { Connection } from "./connection.js"
export { Credential } from "./credential.js"
export { Event } from "./event.js"
export { FileSystem } from "./filesystem.js"
export { Form } from "./form.js"
export { Integration } from "./integration.js"
export { LLM } from "./llm.js"
export { Location } from "./location.js"
@@ -22,7 +23,6 @@ export { Shell } from "./shell.js"
export { Skill } from "./skill.js"
export { Pty } from "./pty.js"
export { PtyTicket } from "./pty-ticket.js"
export { Question } from "./question.js"
export { Workspace } from "./workspace.js"
export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt.js"
export { PromptInput } from "./prompt-input.js"
-86
View File
@@ -1,86 +0,0 @@
export * as Question from "./question.js"
import { Schema } from "effect"
import { optional } from "./schema.js"
import { define, inventory } from "./event.js"
import { ascending } from "./identifier.js"
import { SessionID } from "./session-id.js"
import { statics } from "./schema.js"
export const ID = Schema.String.check(Schema.isStartsWith("que")).pipe(
Schema.brand("QuestionV2.ID"),
statics((schema) => {
const create = () => schema.make("que_" + ascending())
return {
create,
ascending: (id?: string) => (id === undefined ? create() : schema.make(id)),
}
}),
)
export type ID = typeof ID.Type
export const Option = Schema.Struct({
label: Schema.String.annotate({ description: "Display text (1-5 words, concise)" }),
description: Schema.String.annotate({ description: "Explanation of choice" }),
}).annotate({ identifier: "QuestionV2.Option" })
export interface Option extends Schema.Schema.Type<typeof Option> {}
const base = {
question: Schema.String.annotate({ description: "Complete question" }),
header: Schema.String.annotate({ description: "Very short label (max 30 chars)" }),
options: Schema.Array(Option).annotate({ description: "Available choices" }),
multiple: Schema.Boolean.pipe(optional).annotate({ description: "Allow selecting multiple choices" }),
}
export const Info = Schema.Struct({
...base,
custom: Schema.Boolean.pipe(optional).annotate({
description: "Allow typing a custom answer (default: true)",
}),
}).annotate({ identifier: "QuestionV2.Info" })
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Prompt = Schema.Struct(base).annotate({ identifier: "QuestionV2.Prompt" })
export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
export const Tool = Schema.Struct({
messageID: Schema.String,
callID: Schema.String,
}).annotate({ identifier: "QuestionV2.Tool" })
export interface Tool extends Schema.Schema.Type<typeof Tool> {}
export const Request = Schema.Struct({
id: ID,
sessionID: SessionID,
questions: Schema.Array(Info).annotate({ description: "Questions to ask" }),
tool: Tool.pipe(optional),
}).annotate({ identifier: "QuestionV2.Request" })
export interface Request extends Schema.Schema.Type<typeof Request> {}
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "QuestionV2.Answer" })
export type Answer = typeof Answer.Type
export const Reply = Schema.Struct({
answers: Schema.Array(Answer).annotate({
description: "User answers in order of questions (each answer is an array of selected labels)",
}),
}).annotate({ identifier: "QuestionV2.Reply" })
export interface Reply extends Schema.Schema.Type<typeof Reply> {}
const Asked = define({ type: "question.v2.asked", schema: Request.fields })
const Replied = define({
type: "question.v2.replied",
schema: {
sessionID: SessionID,
requestID: ID,
answers: Schema.Array(Answer),
},
})
const Rejected = define({
type: "question.v2.rejected",
schema: {
sessionID: SessionID,
requestID: ID,
},
})
export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) }
@@ -2,10 +2,10 @@ import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { Agent } from "../src/agent.js"
import { FileSystem } from "../src/filesystem.js"
import { Form } from "../src/form.js"
import { Model } from "../src/model.js"
import { Project } from "../src/project.js"
import { Pty } from "../src/pty.js"
import { Question } from "../src/question.js"
import { Session } from "../src/session.js"
import { SessionTodo } from "../src/session-todo.js"
import { optional } from "../src/schema.js"
@@ -28,7 +28,7 @@ describe("contract hygiene", () => {
})
test("current ID constructors expose create", () => {
expect(Question.ID.create()).toStartWith("que_")
expect(Form.ID.create()).toStartWith("frm_")
expect(Pty.ID.create()).toStartWith("pty_")
})
+8 -4
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { Agent, FileSystem, Integration, Permission, Project, Reference, Session, Workspace } from "../src/index.js"
import { Agent, FileSystem, Form, Integration, Permission, Project, Reference, Session, Workspace } from "../src/index.js"
import { EventManifest } from "../src/event-manifest.js"
import { IdeEvent } from "../src/ide-event.js"
import { SessionEvent } from "../src/session-event.js"
@@ -9,11 +9,11 @@ import { WorkspaceEvent } from "../src/workspace-event.js"
describe("public event manifest", () => {
test("owns the complete public event surface", () => {
expect(EventManifest.ServerDefinitions.filter((definition) => definition.type !== "agent.updated").length).toBe(63)
expect(EventManifest.ServerDefinitions.filter((definition) => definition.type !== "agent.updated").length).toBe(65)
expect(EventManifest.ServerDefinitions.filter((definition) => definition.type === "agent.updated")).toEqual([
Agent.Event.Updated,
])
expect(EventManifest.Definitions.filter((definition) => definition.type !== "agent.updated").length).toBe(93)
expect(EventManifest.Definitions.filter((definition) => definition.type !== "agent.updated").length).toBe(96)
expect(EventManifest.Definitions.filter((definition) => definition.type === "agent.updated")).toEqual([
Agent.Event.Updated,
])
@@ -29,7 +29,7 @@ describe("public event manifest", () => {
SessionV1.Event.Diff,
SessionV1.Event.Error,
])
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(93)
expect(Array.from(EventManifest.Latest.keys()).filter((type) => type !== "agent.updated").length).toBe(96)
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(Agent.Event.Updated.durable).toBeUndefined()
expect(EventManifest.Durable.has("agent.updated")).toBe(false)
@@ -43,12 +43,16 @@ describe("public event manifest", () => {
expect(EventManifest.Latest.get("session.next.step.ended")).toBe(SessionEvent.Step.Ended)
expect(EventManifest.Latest.get("todo.updated")).toBe(SessionTodo.Event.Updated)
expect(EventManifest.Latest.get("agent.updated")).toBe(Agent.Event.Updated)
expect(EventManifest.Latest.get("form.created")).toBe(Form.Event.Created)
expect(EventManifest.Latest.get("form.replied")).toBe(Form.Event.Replied)
expect(EventManifest.Latest.get("form.cancelled")).toBe(Form.Event.Cancelled)
expect(EventManifest.Latest.get("project.updated")).toBe(Project.Event.Updated)
expect(Agent.Event.Definitions).toEqual([Agent.Event.Updated])
expect(Project.Event.Definitions).toEqual([Project.Event.Updated])
expect(FileSystem.Event.Definitions).toEqual([FileSystem.Event.Edited])
expect(Integration.Event.Definitions).toEqual([Integration.Event.Updated, Integration.Event.ConnectionUpdated])
expect(Permission.Event.Definitions).toEqual([Permission.Event.Asked, Permission.Event.Replied])
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])
+329 -157
View File
@@ -76,6 +76,8 @@ import type {
FindTextResponses,
FormatterStatusErrors,
FormatterStatusResponses,
FormCreatePayload,
FormReply,
GlobalConfigGetErrors,
GlobalConfigGetResponses,
GlobalConfigUpdateErrors,
@@ -174,7 +176,6 @@ import type {
QuestionRejectResponses,
QuestionReplyErrors,
QuestionReplyResponses,
QuestionV2Reply,
SessionAbortErrors,
SessionAbortResponses,
SessionChildrenErrors,
@@ -273,6 +274,8 @@ import type {
V2CredentialUpdateResponses,
V2EventSubscribeErrors,
V2EventSubscribeResponses,
V2FormRequestListErrors,
V2FormRequestListResponses,
V2FsFindErrors,
V2FsFindResponses,
V2FsListErrors,
@@ -339,8 +342,6 @@ import type {
V2PtyRemoveResponses,
V2PtyUpdateErrors,
V2PtyUpdateResponses,
V2QuestionRequestListErrors,
V2QuestionRequestListResponses,
V2ReferenceListErrors,
V2ReferenceListResponses,
V2SessionActiveErrors,
@@ -357,6 +358,18 @@ import type {
V2SessionEventsResponses,
V2SessionForkErrors,
V2SessionForkResponses,
V2SessionFormCancelErrors,
V2SessionFormCancelResponses,
V2SessionFormCreateErrors,
V2SessionFormCreateResponses,
V2SessionFormGetErrors,
V2SessionFormGetResponses,
V2SessionFormListErrors,
V2SessionFormListResponses,
V2SessionFormReplyErrors,
V2SessionFormReplyResponses,
V2SessionFormStateErrors,
V2SessionFormStateResponses,
V2SessionGetErrors,
V2SessionGetResponses,
V2SessionHistoryErrors,
@@ -379,12 +392,6 @@ import type {
V2SessionPermissionReplyResponses,
V2SessionPromptErrors,
V2SessionPromptResponses,
V2SessionQuestionListErrors,
V2SessionQuestionListResponses,
V2SessionQuestionRejectErrors,
V2SessionQuestionRejectResponses,
V2SessionQuestionReplyErrors,
V2SessionQuestionReplyResponses,
V2SessionRenameErrors,
V2SessionRenameResponses,
V2SessionRevertClearErrors,
@@ -399,6 +406,8 @@ import type {
V2SessionSwitchAgentResponses,
V2SessionSwitchModelErrors,
V2SessionSwitchModelResponses,
V2SessionSyntheticErrors,
V2SessionSyntheticResponses,
V2SessionWaitErrors,
V2SessionWaitResponses,
V2ShellCreateErrors,
@@ -5220,6 +5229,232 @@ export class Revert extends HeyApiClient {
}
}
export class Form extends HeyApiClient {
/**
* List session forms
*
* Retrieve pending forms for a session.
*/
public list<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
location?: {
directory?: string
workspace?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "query", key: "location" },
],
},
],
)
return (options?.client ?? this.client).get<V2SessionFormListResponses, V2SessionFormListErrors, ThrowOnError>({
url: "/api/session/{sessionID}/form",
...options,
...params,
})
}
/**
* Create session form
*
* Create a form for a session.
*/
public create<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
location?: {
directory?: string
workspace?: string
}
formCreatePayload: FormCreatePayload
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "query", key: "location" },
{ key: "formCreatePayload", map: "body" },
],
},
],
)
return (options?.client ?? this.client).post<V2SessionFormCreateResponses, V2SessionFormCreateErrors, ThrowOnError>(
{
url: "/api/session/{sessionID}/form",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
},
)
}
/**
* Get session form
*
* Retrieve a form for a session.
*/
public get<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
formID: string
location?: {
directory?: string
workspace?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "formID" },
{ in: "query", key: "location" },
],
},
],
)
return (options?.client ?? this.client).get<V2SessionFormGetResponses, V2SessionFormGetErrors, ThrowOnError>({
url: "/api/session/{sessionID}/form/{formID}",
...options,
...params,
})
}
/**
* Get form state
*
* Retrieve the current state for a form.
*/
public state<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
formID: string
location?: {
directory?: string
workspace?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "formID" },
{ in: "query", key: "location" },
],
},
],
)
return (options?.client ?? this.client).get<V2SessionFormStateResponses, V2SessionFormStateErrors, ThrowOnError>({
url: "/api/session/{sessionID}/form/{formID}/state",
...options,
...params,
})
}
/**
* Reply to form
*
* Submit an answer to a pending form.
*/
public reply<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
formID: string
location?: {
directory?: string
workspace?: string
}
formReply: FormReply
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "formID" },
{ in: "query", key: "location" },
{ key: "formReply", map: "body" },
],
},
],
)
return (options?.client ?? this.client).post<V2SessionFormReplyResponses, V2SessionFormReplyErrors, ThrowOnError>({
url: "/api/session/{sessionID}/form/{formID}/reply",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Cancel form
*
* Cancel a pending form.
*/
public cancel<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
formID: string
location?: {
directory?: string
workspace?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "formID" },
{ in: "query", key: "location" },
],
},
],
)
return (options?.client ?? this.client).post<V2SessionFormCancelResponses, V2SessionFormCancelErrors, ThrowOnError>(
{
url: "/api/session/{sessionID}/form/{formID}/cancel",
...options,
...params,
},
)
}
}
export class Permission2 extends HeyApiClient {
/**
* List session permission requests
@@ -5375,106 +5610,6 @@ export class Permission2 extends HeyApiClient {
}
}
export class Question2 extends HeyApiClient {
/**
* List session question requests
*
* Retrieve pending question requests owned by a session.
*/
public list<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }])
return (options?.client ?? this.client).get<
V2SessionQuestionListResponses,
V2SessionQuestionListErrors,
ThrowOnError
>({
url: "/api/session/{sessionID}/question",
...options,
...params,
})
}
/**
* Reply to pending question request
*
* Answer a pending question request owned by a session.
*/
public reply<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
requestID: string
questionV2Reply: QuestionV2Reply
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "requestID" },
{ key: "questionV2Reply", map: "body" },
],
},
],
)
return (options?.client ?? this.client).post<
V2SessionQuestionReplyResponses,
V2SessionQuestionReplyErrors,
ThrowOnError
>({
url: "/api/session/{sessionID}/question/{requestID}/reply",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Reject pending question request
*
* Reject a pending question request owned by a session.
*/
public reject<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
requestID: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "path", key: "requestID" },
],
},
],
)
return (options?.client ?? this.client).post<
V2SessionQuestionRejectResponses,
V2SessionQuestionRejectErrors,
ThrowOnError
>({
url: "/api/session/{sessionID}/question/{requestID}/reject",
...options,
...params,
})
}
}
export class Session3 extends HeyApiClient {
/**
* List sessions
@@ -5816,6 +5951,47 @@ export class Session3 extends HeyApiClient {
})
}
/**
* Add synthetic message
*
* Append a synthetic message to a session and resume execution.
*/
public synthetic<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
text?: string
description?: string
metadata?: {
[key: string]: unknown
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "body", key: "text" },
{ in: "body", key: "description" },
{ in: "body", key: "metadata" },
],
},
],
)
return (options?.client ?? this.client).post<V2SessionSyntheticResponses, V2SessionSyntheticErrors, ThrowOnError>({
url: "/api/session/{sessionID}/synthetic",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Compact session
*
@@ -6044,15 +6220,15 @@ export class Session3 extends HeyApiClient {
return (this._revert ??= new Revert({ client: this.client }))
}
private _form?: Form
get form(): Form {
return (this._form ??= new Form({ client: this.client }))
}
private _permission?: Permission2
get permission(): Permission2 {
return (this._permission ??= new Permission2({ client: this.client }))
}
private _question?: Question2
get question(): Question2 {
return (this._question ??= new Question2({ client: this.client }))
}
}
export class Model extends HeyApiClient {
@@ -6626,6 +6802,37 @@ export class Project2 extends HeyApiClient {
}
export class Request extends HeyApiClient {
/**
* List pending form requests
*
* Retrieve pending forms for a location.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
location?: {
directory?: string
workspace?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
return (options?.client ?? this.client).get<V2FormRequestListResponses, V2FormRequestListErrors, ThrowOnError>({
url: "/api/form/request",
...options,
...params,
})
}
}
export class Form2 extends HeyApiClient {
private _request?: Request
get request(): Request {
return (this._request ??= new Request({ client: this.client }))
}
}
export class Request2 extends HeyApiClient {
/**
* List pending permission requests
*
@@ -6702,9 +6909,9 @@ export class Saved extends HeyApiClient {
}
export class Permission3 extends HeyApiClient {
private _request?: Request
get request(): Request {
return (this._request ??= new Request({ client: this.client }))
private _request?: Request2
get request(): Request2 {
return (this._request ??= new Request2({ client: this.client }))
}
private _saved?: Saved
@@ -7294,41 +7501,6 @@ export class Shell extends HeyApiClient {
}
}
export class Request2 extends HeyApiClient {
/**
* List pending question requests
*
* Retrieve pending question requests for a location.
*/
public list<ThrowOnError extends boolean = false>(
parameters?: {
location?: {
directory?: string
workspace?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }])
return (options?.client ?? this.client).get<
V2QuestionRequestListResponses,
V2QuestionRequestListErrors,
ThrowOnError
>({
url: "/api/question/request",
...options,
...params,
})
}
}
export class Question3 extends HeyApiClient {
private _request?: Request2
get request(): Request2 {
return (this._request ??= new Request2({ client: this.client }))
}
}
export class Reference extends HeyApiClient {
/**
* List references
@@ -7530,6 +7702,11 @@ export class V2 extends HeyApiClient {
return (this._project ??= new Project2({ client: this.client }))
}
private _form?: Form2
get form(): Form2 {
return (this._form ??= new Form2({ client: this.client }))
}
private _permission?: Permission3
get permission(): Permission3 {
return (this._permission ??= new Permission3({ client: this.client }))
@@ -7565,11 +7742,6 @@ export class V2 extends HeyApiClient {
return (this._shell ??= new Shell({ client: this.client }))
}
private _question?: Question3
get question(): Question3 {
return (this._question ??= new Question3({ client: this.client }))
}
private _reference?: Reference
get reference(): Reference {
return (this._reference ??= new Reference({ client: this.client }))
+612 -262
View File
@@ -72,9 +72,9 @@ export type Event =
| EventShellCreated
| EventShellExited
| EventShellDeleted
| EventQuestionV2Asked
| EventQuestionV2Replied
| EventQuestionV2Rejected
| EventFormCreated
| EventFormReplied
| EventFormCancelled
| EventTodoUpdated
| EventLspUpdated
| EventPermissionAsked
@@ -942,6 +942,9 @@ export type GlobalEvent = {
messageID: string
text: string
description?: string
metadata?: {
[key: string]: unknown
}
}
}
| {
@@ -1425,32 +1428,26 @@ export type GlobalEvent = {
}
| {
id: string
type: "question.v2.asked"
type: "form.created"
properties: {
form: FormFormInfo | FormUrlInfo
}
}
| {
id: string
type: "form.replied"
properties: {
id: string
sessionID: string
/**
* Questions to ask
*/
questions: Array<QuestionV2Info>
tool?: QuestionV2Tool
answer: FormAnswer
}
}
| {
id: string
type: "question.v2.replied"
type: "form.cancelled"
properties: {
id: string
sessionID: string
requestID: string
answers: Array<QuestionV2Answer>
}
}
| {
id: string
type: "question.v2.rejected"
properties: {
sessionID: string
requestID: string
}
}
| {
@@ -2904,6 +2901,24 @@ export type ProviderNotFoundError = {
message: string
}
export type FormNotFoundError = {
_tag: "FormNotFoundError"
id: string
message: string
}
export type FormAlreadySettledError = {
_tag: "FormAlreadySettledError"
id: string
message: string
}
export type FormInvalidAnswerError = {
_tag: "FormInvalidAnswerError"
id: string
message: string
}
export type OutputFormat1 =
| {
type: "text"
@@ -3055,9 +3070,9 @@ export type V2Event =
| ShellCreated
| ShellExited
| ShellDeleted
| QuestionV2Asked
| QuestionV2Replied
| QuestionV2Rejected
| FormCreated
| FormReplied
| FormCancelled
| TodoUpdated
| LspUpdated
| PermissionAsked
@@ -3296,40 +3311,120 @@ export type PermissionV2Source = {
export type PermissionV2Reply = "once" | "always" | "reject"
export type QuestionV2Option = {
/**
* Display text (1-5 words, concise)
*/
label: string
/**
* Explanation of choice
*/
description: string
export type FormMetadata = {
[key: string]: unknown
}
export type QuestionV2Info = {
/**
* Complete question
*/
question: string
/**
* Very short label (max 30 chars)
*/
header: string
/**
* Available choices
*/
options: Array<QuestionV2Option>
multiple?: boolean
export type FormWhen = {
key: string
op: "eq" | "neq"
value: string
}
export type FormOption = {
value: string
label: string
description?: string
}
export type FormStringField = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "string"
format?: "email" | "uri" | "date" | "date-time"
minLength?: number
maxLength?: number
pattern?: string
placeholder?: string
default?: string
options?: Array<FormOption>
custom?: boolean
}
export type QuestionV2Tool = {
messageID: string
callID: string
export type FormNumberField = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "number"
minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
}
export type QuestionV2Answer = Array<string>
export type FormIntegerField = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "integer"
minimum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
maximum?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
default?: number | "NaN" | "Infinity" | "-Infinity" | "Infinity" | "-Infinity" | "NaN"
}
export type FormBooleanField = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "boolean"
default?: boolean
}
export type FormMultiselectField = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "multiselect"
options: Array<FormOption>
minItems?: number
maxItems?: number
custom?: boolean
default?: Array<string>
}
export type FormFormInfo = {
id: string
sessionID: string
title?: string
metadata?: FormMetadata
mode: "form"
fields: Array<FormStringField | FormNumberField | FormIntegerField | FormBooleanField | FormMultiselectField>
}
export type FormUrlInfo = {
id: string
sessionID: string
title?: string
metadata?: FormMetadata
mode: "url"
url: string
}
export type FormValue =
| string
| number
| "NaN"
| "Infinity"
| "-Infinity"
| "Infinity"
| "-Infinity"
| "NaN"
| boolean
| Array<string>
export type FormAnswer = {
[key: string]: FormValue
}
export type ProjectVcs = "git"
@@ -3618,6 +3713,9 @@ export type SyncEventSessionNextSynthetic = {
messageID: string
text: string
description?: string
metadata?: {
[key: string]: unknown
}
}
}
}
@@ -4583,6 +4681,9 @@ export type SessionNextSynthetic = {
messageID: string
text: string
description?: string
metadata?: {
[key: string]: unknown
}
}
}
@@ -5326,6 +5427,31 @@ export type ProjectCurrent = {
directory: string
}
export type FormCreatePayload = {
id?: string
title?: string
metadata?: FormMetadata
mode: "form" | "url"
fields?: Array<FormStringField | FormNumberField | FormIntegerField | FormBooleanField | FormMultiselectField>
url?: string
}
export type FormState =
| {
status: "pending"
}
| {
status: "answered"
answer: FormAnswer
}
| {
status: "cancelled"
}
export type FormReply = {
answer: FormAnswer
}
export type PermissionV2Request = {
id: string
sessionID: string
@@ -6032,12 +6158,55 @@ export type ShellDeleted = {
}
}
export type QuestionV2Asked = {
export type FormNumberField1 = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "number"
minimum?: number | "NaN" | "Infinity" | "-Infinity"
maximum?: number | "NaN" | "Infinity" | "-Infinity"
default?: number | "NaN" | "Infinity" | "-Infinity"
}
export type FormIntegerField1 = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "integer"
minimum?: number | "NaN" | "Infinity" | "-Infinity"
maximum?: number | "NaN" | "Infinity" | "-Infinity"
default?: number | "NaN" | "Infinity" | "-Infinity"
}
export type FormCreated = {
id: string
metadata?: {
[key: string]: unknown
}
type: "question.v2.asked"
type: "form.created"
durable?: {
aggregateID: string
seq: number
version: number
}
location?: LocationRef
data: {
form: FormFormInfo | FormUrlInfo
}
}
export type FormValue1 = string | number | "NaN" | "Infinity" | "-Infinity" | boolean | Array<string>
export type FormReplied = {
id: string
metadata?: {
[key: string]: unknown
}
type: "form.replied"
durable?: {
aggregateID: string
seq: number
@@ -6047,20 +6216,16 @@ export type QuestionV2Asked = {
data: {
id: string
sessionID: string
/**
* Questions to ask
*/
questions: Array<QuestionV2Info>
tool?: QuestionV2Tool
answer: FormAnswer
}
}
export type QuestionV2Replied = {
export type FormCancelled = {
id: string
metadata?: {
[key: string]: unknown
}
type: "question.v2.replied"
type: "form.cancelled"
durable?: {
aggregateID: string
seq: number
@@ -6068,27 +6233,8 @@ export type QuestionV2Replied = {
}
location?: LocationRef
data: {
id: string
sessionID: string
requestID: string
answers: Array<QuestionV2Answer>
}
}
export type QuestionV2Rejected = {
id: string
metadata?: {
[key: string]: unknown
}
type: "question.v2.rejected"
durable?: {
aggregateID: string
seq: number
version: number
}
location?: LocationRef
data: {
sessionID: string
requestID: string
}
}
@@ -6557,23 +6703,6 @@ export type GlobalDisposed = {
}
}
export type QuestionV2Request = {
id: string
sessionID: string
/**
* Questions to ask
*/
questions: Array<QuestionV2Info>
tool?: QuestionV2Tool
}
export type QuestionV2Reply = {
/**
* User answers in order of questions (each answer is an array of selected labels)
*/
answers: Array<QuestionV2Answer>
}
export type ReferenceLocalSource = {
type: "local"
path: string
@@ -6806,6 +6935,9 @@ export type EventSessionNextSynthetic = {
messageID: string
text: string
description?: string
metadata?: {
[key: string]: unknown
}
}
}
@@ -7334,36 +7466,54 @@ export type EventShellDeleted = {
}
}
export type EventQuestionV2Asked = {
export type FormNumberField2 = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "number"
minimum?: number | "NaN" | "Infinity" | "-Infinity"
maximum?: number | "NaN" | "Infinity" | "-Infinity"
default?: number | "NaN" | "Infinity" | "-Infinity"
}
export type FormIntegerField2 = {
key: string
title?: string
description?: string
required?: boolean
when?: FormWhen
type: "integer"
minimum?: number | "NaN" | "Infinity" | "-Infinity"
maximum?: number | "NaN" | "Infinity" | "-Infinity"
default?: number | "NaN" | "Infinity" | "-Infinity"
}
export type EventFormCreated = {
id: string
type: "question.v2.asked"
type: "form.created"
properties: {
form: FormFormInfo | FormUrlInfo
}
}
export type EventFormReplied = {
id: string
type: "form.replied"
properties: {
id: string
sessionID: string
/**
* Questions to ask
*/
questions: Array<QuestionV2Info>
tool?: QuestionV2Tool
answer: FormAnswer
}
}
export type EventQuestionV2Replied = {
export type EventFormCancelled = {
id: string
type: "question.v2.replied"
type: "form.cancelled"
properties: {
id: string
sessionID: string
requestID: string
answers: Array<QuestionV2Answer>
}
}
export type EventQuestionV2Rejected = {
id: string
type: "question.v2.rejected"
properties: {
sessionID: string
requestID: string
}
}
@@ -12284,6 +12434,47 @@ export type V2SessionSkillResponses = {
export type V2SessionSkillResponse = V2SessionSkillResponses[keyof V2SessionSkillResponses]
export type V2SessionSyntheticData = {
body: {
text: string
description?: string
metadata?: {
[key: string]: unknown
}
}
path: {
sessionID: string
}
query?: never
url: "/api/session/{sessionID}/synthetic"
}
export type V2SessionSyntheticErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionSyntheticError = V2SessionSyntheticErrors[keyof V2SessionSyntheticErrors]
export type V2SessionSyntheticResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionSyntheticResponse = V2SessionSyntheticResponses[keyof V2SessionSyntheticResponses]
export type V2SessionCompactData = {
body?: never
path: {
@@ -13403,6 +13594,311 @@ export type V2ProjectDirectoriesResponses = {
export type V2ProjectDirectoriesResponse = V2ProjectDirectoriesResponses[keyof V2ProjectDirectoriesResponses]
export type V2FormRequestListData = {
body?: never
path?: never
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/form/request"
}
export type V2FormRequestListErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
}
export type V2FormRequestListError = V2FormRequestListErrors[keyof V2FormRequestListErrors]
export type V2FormRequestListResponses = {
/**
* Success
*/
200: {
location: LocationInfo
data: Array<FormFormInfo | FormUrlInfo>
}
}
export type V2FormRequestListResponse = V2FormRequestListResponses[keyof V2FormRequestListResponses]
export type V2SessionFormListData = {
body?: never
path: {
sessionID: string
}
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/session/{sessionID}/form"
}
export type V2SessionFormListErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionFormListError = V2SessionFormListErrors[keyof V2SessionFormListErrors]
export type V2SessionFormListResponses = {
/**
* Success
*/
200: {
location: LocationInfo
data: Array<FormFormInfo | FormUrlInfo>
}
}
export type V2SessionFormListResponse = V2SessionFormListResponses[keyof V2SessionFormListResponses]
export type V2SessionFormCreateData = {
body: FormCreatePayload
path: {
sessionID: string
}
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/session/{sessionID}/form"
}
export type V2SessionFormCreateErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
/**
* ConflictError
*/
409: ConflictError
}
export type V2SessionFormCreateError = V2SessionFormCreateErrors[keyof V2SessionFormCreateErrors]
export type V2SessionFormCreateResponses = {
/**
* Success
*/
200: {
location: LocationInfo
data: FormFormInfo | FormUrlInfo
}
}
export type V2SessionFormCreateResponse = V2SessionFormCreateResponses[keyof V2SessionFormCreateResponses]
export type V2SessionFormGetData = {
body?: never
path: {
sessionID: string
formID: string
}
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/session/{sessionID}/form/{formID}"
}
export type V2SessionFormGetErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* FormNotFoundError | SessionNotFoundError
*/
404: FormNotFoundError | SessionNotFoundError
}
export type V2SessionFormGetError = V2SessionFormGetErrors[keyof V2SessionFormGetErrors]
export type V2SessionFormGetResponses = {
/**
* Success
*/
200: {
location: LocationInfo
data: FormFormInfo | FormUrlInfo
}
}
export type V2SessionFormGetResponse = V2SessionFormGetResponses[keyof V2SessionFormGetResponses]
export type V2SessionFormStateData = {
body?: never
path: {
sessionID: string
formID: string
}
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/session/{sessionID}/form/{formID}/state"
}
export type V2SessionFormStateErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* FormNotFoundError | SessionNotFoundError
*/
404: FormNotFoundError | SessionNotFoundError
}
export type V2SessionFormStateError = V2SessionFormStateErrors[keyof V2SessionFormStateErrors]
export type V2SessionFormStateResponses = {
/**
* Success
*/
200: {
location: LocationInfo
data: FormState
}
}
export type V2SessionFormStateResponse = V2SessionFormStateResponses[keyof V2SessionFormStateResponses]
export type V2SessionFormReplyData = {
body: FormReply
path: {
sessionID: string
formID: string
}
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/session/{sessionID}/form/{formID}/reply"
}
export type V2SessionFormReplyErrors = {
/**
* FormInvalidAnswerError | InvalidRequestError
*/
400: FormInvalidAnswerError | InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* FormNotFoundError | SessionNotFoundError
*/
404: FormNotFoundError | SessionNotFoundError
/**
* FormAlreadySettledError
*/
409: FormAlreadySettledError
}
export type V2SessionFormReplyError = V2SessionFormReplyErrors[keyof V2SessionFormReplyErrors]
export type V2SessionFormReplyResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionFormReplyResponse = V2SessionFormReplyResponses[keyof V2SessionFormReplyResponses]
export type V2SessionFormCancelData = {
body?: never
path: {
sessionID: string
formID: string
}
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/session/{sessionID}/form/{formID}/cancel"
}
export type V2SessionFormCancelErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* FormNotFoundError | SessionNotFoundError
*/
404: FormNotFoundError | SessionNotFoundError
/**
* FormAlreadySettledError
*/
409: FormAlreadySettledError
}
export type V2SessionFormCancelError = V2SessionFormCancelErrors[keyof V2SessionFormCancelErrors]
export type V2SessionFormCancelResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionFormCancelResponse = V2SessionFormCancelResponses[keyof V2SessionFormCancelResponses]
export type V2PermissionRequestListData = {
body?: never
path?: never
@@ -14404,152 +14900,6 @@ export type V2ShellOutputResponses = {
export type V2ShellOutputResponse = V2ShellOutputResponses[keyof V2ShellOutputResponses]
export type V2QuestionRequestListData = {
body?: never
path?: never
query?: {
location?: {
directory?: string
workspace?: string
}
}
url: "/api/question/request"
}
export type V2QuestionRequestListErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
}
export type V2QuestionRequestListError = V2QuestionRequestListErrors[keyof V2QuestionRequestListErrors]
export type V2QuestionRequestListResponses = {
/**
* Success
*/
200: {
location: LocationInfo
data: Array<QuestionV2Request>
}
}
export type V2QuestionRequestListResponse = V2QuestionRequestListResponses[keyof V2QuestionRequestListResponses]
export type V2SessionQuestionListData = {
body?: never
path: {
sessionID: string
}
query?: never
url: "/api/session/{sessionID}/question"
}
export type V2SessionQuestionListErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError
*/
404: SessionNotFoundError
}
export type V2SessionQuestionListError = V2SessionQuestionListErrors[keyof V2SessionQuestionListErrors]
export type V2SessionQuestionListResponses = {
/**
* Success
*/
200: {
data: Array<QuestionV2Request>
}
}
export type V2SessionQuestionListResponse = V2SessionQuestionListResponses[keyof V2SessionQuestionListResponses]
export type V2SessionQuestionReplyData = {
body: QuestionV2Reply
path: {
sessionID: string
requestID: string
}
query?: never
url: "/api/session/{sessionID}/question/{requestID}/reply"
}
export type V2SessionQuestionReplyErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError | QuestionNotFoundError
*/
404: QuestionNotFoundError | SessionNotFoundError
}
export type V2SessionQuestionReplyError = V2SessionQuestionReplyErrors[keyof V2SessionQuestionReplyErrors]
export type V2SessionQuestionReplyResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionQuestionReplyResponse = V2SessionQuestionReplyResponses[keyof V2SessionQuestionReplyResponses]
export type V2SessionQuestionRejectData = {
body?: never
path: {
sessionID: string
requestID: string
}
query?: never
url: "/api/session/{sessionID}/question/{requestID}/reject"
}
export type V2SessionQuestionRejectErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestError
/**
* UnauthorizedError
*/
401: UnauthorizedError
/**
* SessionNotFoundError | QuestionNotFoundError
*/
404: QuestionNotFoundError | SessionNotFoundError
}
export type V2SessionQuestionRejectError = V2SessionQuestionRejectErrors[keyof V2SessionQuestionRejectErrors]
export type V2SessionQuestionRejectResponses = {
/**
* <No Content>
*/
204: void
}
export type V2SessionQuestionRejectResponse = V2SessionQuestionRejectResponses[keyof V2SessionQuestionRejectResponses]
export type V2ReferenceListData = {
body?: never
path?: never
+4
View File
@@ -1,8 +1,12 @@
import { makeDefaultApi } from "@opencode-ai/protocol/api"
import { LocationMiddleware } from "./location"
import { FormLocationMiddleware } from "./middleware/form-location"
import { SessionLocationMiddleware } from "./middleware/session-location"
export const Api = makeDefaultApi({
locationMiddleware: LocationMiddleware,
// FormLocationMiddleware contains the temporary `sessionID === "global"` MCP elicitation hack.
// Do not use that sentinel with general session APIs.
formLocationMiddleware: FormLocationMiddleware,
sessionLocationMiddleware: SessionLocationMiddleware,
})
+2 -2
View File
@@ -6,6 +6,7 @@ import { ProviderHandler } from "./handlers/provider"
import { SessionHandler } from "./handlers/session"
import { PermissionHandler } from "./handlers/permission"
import { FileSystemHandler } from "./handlers/fs"
import { FormHandler } from "./handlers/form"
import { CommandHandler } from "./handlers/command"
import { SkillHandler } from "./handlers/skill"
import { EventHandler } from "./handlers/event"
@@ -14,7 +15,6 @@ import { PluginHandler } from "./handlers/plugin"
import { HealthHandler } from "./handlers/health"
import { PtyHandler } from "./handlers/pty"
import { ShellHandler } from "./handlers/shell"
import { QuestionHandler } from "./handlers/question"
import { ReferenceHandler } from "./handlers/reference"
import { LocationHandler } from "./handlers/location"
import { IntegrationHandler } from "./handlers/integration"
@@ -37,6 +37,7 @@ export const handlers = Layer.mergeAll(
McpHandler,
CredentialHandler,
ProjectHandler,
FormHandler,
PermissionHandler,
FileSystemHandler,
CommandHandler,
@@ -44,7 +45,6 @@ export const handlers = Layer.mergeAll(
EventHandler,
PtyHandler,
ShellHandler,
QuestionHandler,
ReferenceHandler,
ProjectCopyHandler,
)
+134
View File
@@ -0,0 +1,134 @@
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: (form: 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* () {
return yield* response((yield* Form.Service).list())
}),
)
.handle(
"session.form.list",
Effect.fn(function* (ctx) {
return yield* response((yield* Form.Service).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()
}),
)
}),
)
-62
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()
}),
)
}),
)
+2 -2
View File
@@ -26,7 +26,7 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
})
}
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
export function requestRef(request: HttpServerRequest.HttpServerRequest): Location.Ref {
const query = new URL(request.url, "http://localhost").searchParams
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
const directory =
@@ -53,7 +53,7 @@ export const layer = Layer.effect(
return LocationMiddleware.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
return yield* effect.pipe(Effect.provide(locations.get(ref(request))))
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
}),
)
}),
@@ -0,0 +1,76 @@
import { Database } from "@opencode-ai/core/database/database"
import { Location } from "@opencode-ai/core/location"
import { LocationServiceMap } from "@opencode-ai/core/location-services"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionV2 } from "@opencode-ai/core/session"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
import { eq } from "drizzle-orm"
import { Effect, Layer, Schema } from "effect"
import { HttpRouter, HttpServerRequest } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import { requestRef, type LocationServices } from "../location"
export class FormLocationMiddleware extends HttpApiMiddleware.Service<
FormLocationMiddleware,
{ provides: LocationServices }
>()("@opencode/HttpApiFormLocation", {
error: [InvalidRequestError, SessionNotFoundError],
}) {}
const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID)
export const formLocationLayer = Layer.effect(
FormLocationMiddleware,
Effect.gen(function* () {
const { db } = yield* Database.Service
const locations = yield* LocationServiceMap.Service
return FormLocationMiddleware.of((effect) =>
Effect.gen(function* () {
const route = yield* HttpRouter.RouteContext
if (route.params.sessionID === "global") {
// Temporary MCP elicitation escape hatch. This is still Location-scoped; it only bypasses
// the session row lookup because some MCP elicitations cannot currently be attributed to
// a real session. Keep this undocumented and remove once elicitations carry session ownership.
const request = yield* HttpServerRequest.HttpServerRequest
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
}
const sessionID = yield* decodeSessionID(route.params.sessionID).pipe(
Effect.mapError(
() =>
new InvalidRequestError({
message: "Invalid session ID",
field: "sessionID",
}),
),
)
const row = yield* db
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
.from(SessionTable)
.where(eq(SessionTable.id, sessionID))
.get()
.pipe(Effect.orDie)
if (!row) {
return yield* new SessionNotFoundError({
sessionID,
message: `Session not found: ${sessionID}`,
})
}
return yield* effect.pipe(
Effect.provide(
locations.get(
Location.Ref.make({
directory: AbsolutePath.make(row.directory),
workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined,
}),
),
),
)
}),
)
}),
)
+2
View File
@@ -24,6 +24,7 @@ import { authorizationLayer } from "./middleware/authorization"
import { schemaErrorLayer } from "./middleware/schema-error"
import { PtyEnvironment } from "./pty-environment"
import { layer as locationLayer } from "./location"
import { formLocationLayer } from "./middleware/form-location"
import { sessionLocationLayer } from "./middleware/session-location"
const applicationServices = LayerNode.group([
@@ -70,6 +71,7 @@ function makeRoutes<AuthError, AuthServices>(
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(locationLayer),
Layer.provide(authorizationLayer),
+17 -11
View File
@@ -8,7 +8,6 @@ import type {
PermissionSavedInfo,
PermissionV2Request,
ProviderV2Info,
QuestionV2Request,
ReferenceInfo,
SessionMessage,
SessionMessageAssistant,
@@ -24,6 +23,7 @@ import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "./helper"
import { useSDK } from "./sdk"
import { createSignal, onCleanup } from "solid-js"
import { isQuestionForm, type QuestionForm } from "../util/question-form"
export type DataSessionStatus = "idle" | "running"
@@ -51,7 +51,7 @@ type Data = {
status: Record<string, DataSessionStatus>
message: Record<string, SessionMessage[]>
permission: Record<string, PermissionV2Request[]>
question: Record<string, QuestionV2Request[]>
question: Record<string, QuestionForm[]>
}
project: {
permission: Record<string, PermissionSavedInfo[]>
@@ -564,21 +564,22 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
),
)
break
case "question.v2.asked":
if (store.session.question[event.data.sessionID]?.some((request) => request.id === event.data.id)) break
setStore("session", "question", event.data.sessionID, [
...(store.session.question[event.data.sessionID] ?? []),
event.data,
case "form.created":
if (!isQuestionForm(event.data.form)) break
if (store.session.question[event.data.form.sessionID]?.some((request) => request.id === event.data.form.id)) break
setStore("session", "question", event.data.form.sessionID, [
...(store.session.question[event.data.form.sessionID] ?? []),
event.data.form,
])
break
case "question.v2.replied":
case "question.v2.rejected":
case "form.replied":
case "form.cancelled":
setStore(
"session",
"question",
event.data.sessionID,
(store.session.question[event.data.sessionID] ?? []).filter(
(request) => request.id !== event.data.requestID,
(request) => request.id !== event.data.id,
),
)
break
@@ -703,7 +704,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return store.session.question[sessionID]
},
async refresh(sessionID: string) {
setStore("session", "question", sessionID, mutable(await sdk.api.question.list({ sessionID })))
setStore(
"session",
"question",
sessionID,
mutable((await sdk.api.form.list({ sessionID })).data.flatMap((form) => (isQuestionForm(form) ? [form] : []))),
)
},
},
},
+17 -17
View File
@@ -3,28 +3,28 @@ import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-j
import { useRenderer } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core"
import { selectedForeground, tint, useTheme } from "../../context/theme"
import type { QuestionV2Answer, QuestionV2Request } from "@opencode-ai/sdk/v2"
import { useSDK } from "../../context/sdk"
import { SplitBorder } from "../../ui/border"
import { useTuiConfig } from "../../config"
import { useBindings, useOpencodeModeStack } from "../../keymap"
import { questionAnswer, type QuestionAnswer, type QuestionForm } from "../../util/question-form"
const QUESTION_MODE = "question"
export function QuestionPrompt(props: { request: QuestionV2Request; directory?: string }) {
export function QuestionPrompt(props: { request: QuestionForm; directory?: string }) {
const sdk = useSDK()
const { theme } = useTheme()
const renderer = useRenderer()
const tuiConfig = useTuiConfig()
const modeStack = useOpencodeModeStack()
const questions = createMemo(() => props.request.questions)
const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
const questions = createMemo(() => props.request.fields)
const single = createMemo(() => questions().length === 1 && questions()[0]?.type !== "multiselect")
const tabs = createMemo(() => (single() ? 1 : questions().length + 1)) // questions + confirm tab (no confirm for single select)
const [tabHover, setTabHover] = createSignal<number | "confirm" | null>(null)
const [store, setStore] = createStore({
tab: 0,
answers: [] as QuestionV2Answer[],
answers: [] as QuestionAnswer[],
custom: [] as string[],
selected: 0,
editing: false,
@@ -38,7 +38,7 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
const custom = createMemo(() => question()?.custom !== false)
const other = createMemo(() => custom() && store.selected === options().length)
const input = createMemo(() => store.custom[store.tab] ?? "")
const multi = createMemo(() => question()?.multiple === true)
const multi = createMemo(() => question()?.type === "multiselect")
const customPicked = createMemo(() => {
const value = input()
if (!value) return false
@@ -47,17 +47,17 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
function submit() {
const answers = questions().map((_, i) => store.answers[i] ?? [])
void sdk.api.question.reply({
void sdk.api.form.reply({
sessionID: props.request.sessionID,
requestID: props.request.id,
answers,
formID: props.request.id,
answer: questionAnswer(questions(), answers),
})
}
function reject() {
void sdk.api.question.reject({
void sdk.api.form.cancel({
sessionID: props.request.sessionID,
requestID: props.request.id,
formID: props.request.id,
})
}
@@ -71,10 +71,10 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
setStore("custom", inputs)
}
if (single()) {
void sdk.api.question.reply({
void sdk.api.form.reply({
sessionID: props.request.sessionID,
requestID: props.request.id,
answers: [[answer]],
formID: props.request.id,
answer: questionAnswer(questions(), [[answer]]),
})
return
}
@@ -328,7 +328,7 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
: theme.textMuted
}
>
{q.header}
{q.description ?? q.title}
</text>
</box>
)
@@ -356,7 +356,7 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
<box paddingLeft={1} gap={1}>
<box>
<text fg={theme.text}>
{question()?.question}
{question()?.title}
{multi() ? " (select all that apply)" : ""}
</text>
</box>
@@ -466,7 +466,7 @@ export function QuestionPrompt(props: { request: QuestionV2Request; directory?:
return (
<box paddingLeft={1}>
<text>
<span style={{ fg: theme.textMuted }}>{q.header}:</span>{" "}
<span style={{ fg: theme.textMuted }}>{q.description ?? q.title}:</span>{" "}
<span style={{ fg: answered() ? theme.text : theme.error }}>
{answered() ? value() : "(not answered)"}
</span>
+30
View File
@@ -0,0 +1,30 @@
import type { FormAnswer, FormFormInfo } from "@opencode-ai/sdk/v2"
export type QuestionField = Extract<FormFormInfo["fields"][number], { type: "string" | "multiselect" }>
export type QuestionForm = Omit<FormFormInfo, "fields"> & { fields: QuestionField[] }
export type QuestionAnswer = string[]
export function isQuestionForm(value: unknown): value is QuestionForm {
if (typeof value !== "object" || value === null) return false
const form = value as { mode?: unknown; metadata?: unknown; fields?: unknown }
if (form.mode !== "form") return false
if (typeof form.metadata !== "object" || form.metadata === null) return false
if ((form.metadata as { kind?: unknown }).kind !== "question") return false
return Array.isArray(form.fields) && form.fields.every(isQuestionField)
}
export function questionAnswer(fields: QuestionField[], answers: QuestionAnswer[]): FormAnswer {
const entries = fields.flatMap((field, index): Array<[string, string | string[]]> => {
const answer = answers[index] ?? []
if (answer.length === 0) return []
if (field.type === "multiselect") return [[field.key, answer]]
return [[field.key, answer[0] ?? ""]]
})
return Object.fromEntries(entries)
}
function isQuestionField(value: unknown): value is QuestionField {
if (typeof value !== "object" || value === null) return false
const field = value as { type?: unknown }
return field.type === "string" || field.type === "multiselect"
}
+25 -17
View File
@@ -611,37 +611,45 @@ test("adds and dismisses question requests from live events", async () => {
try {
await wait(() => data.connection.status() === "connected")
emitEvent(events, {
id: "evt_question_asked_1",
type: "question.v2.asked",
id: "evt_form_created_1",
type: "form.created",
data: {
id: "que_1",
sessionID: "ses_1",
questions: [{ question: "Which option?", header: "Option", options: [], multiple: false }],
form: {
id: "frm_1",
sessionID: "ses_1",
mode: "form",
metadata: { kind: "question" },
fields: [{ key: "question_0", title: "Which option?", description: "Option", type: "string", options: [] }],
},
},
})
emitEvent(events, {
id: "evt_question_asked_2",
type: "question.v2.asked",
id: "evt_form_created_2",
type: "form.created",
data: {
id: "que_2",
sessionID: "ses_1",
questions: [{ question: "Which environment?", header: "Environment", options: [], multiple: false }],
form: {
id: "frm_2",
sessionID: "ses_1",
mode: "form",
metadata: { kind: "question" },
fields: [{ key: "question_0", title: "Which environment?", description: "Environment", type: "string", options: [] }],
},
},
})
await wait(() => data.session.question.list("ses_1")?.length === 2)
emitEvent(events, {
id: "evt_question_replied_1",
type: "question.v2.replied",
data: { sessionID: "ses_1", requestID: "que_1", answers: [["First"]] },
id: "evt_form_replied_1",
type: "form.replied",
data: { id: "frm_1", sessionID: "ses_1", answer: { question_0: "First" } },
})
await wait(() => data.session.question.list("ses_1")?.length === 1)
expect(data.session.question.list("ses_1")?.[0]?.id).toBe("que_2")
expect(data.session.question.list("ses_1")?.[0]?.id).toBe("frm_2")
emitEvent(events, {
id: "evt_question_rejected_2",
type: "question.v2.rejected",
data: { sessionID: "ses_1", requestID: "que_2" },
id: "evt_form_cancelled_2",
type: "form.cancelled",
data: { id: "frm_2", sessionID: "ses_1" },
})
await wait(() => data.session.question.list("ses_1")?.length === 0)
} finally {