mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-14 15:32:52 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5040e022e | |||
| 9872fb8a54 | |||
| baf5ce3b7e | |||
| 70efd3fb38 |
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@opencode-ai/core": minor
|
||||
"@opencode-ai/schema": minor
|
||||
"@opencode-ai/protocol": minor
|
||||
"@opencode-ai/client": minor
|
||||
---
|
||||
|
||||
Remove the unused question request API and use session forms for question tool interactions.
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
ReferenceListInput,
|
||||
ReferenceListOutput,
|
||||
ReferenceInfo,
|
||||
QuestionRequest,
|
||||
SessionApi,
|
||||
SessionInfo,
|
||||
} from "@opencode-ai/client/promise"
|
||||
@@ -112,7 +111,6 @@ type LocationApi = { readonly get: (input?: LocationGetInput) => Promise<Locatio
|
||||
|
||||
type McpApi = ServerApi["mcp"]
|
||||
type PermissionApi = ServerApi["permission"]
|
||||
type QuestionApi = ServerApi["question"]
|
||||
type VcsApi = ServerApi["vcs"]
|
||||
|
||||
export const loadProjectsQuery = (scope: ServerScope, projects: ProjectApi, worktrees: WorktreeApi) =>
|
||||
@@ -303,7 +301,6 @@ export async function bootstrapDirectory(input: {
|
||||
readonly mcp: McpApi
|
||||
readonly permission: PermissionApi
|
||||
readonly project: ProjectApi
|
||||
readonly question: QuestionApi
|
||||
readonly reference: ReferenceListApi
|
||||
readonly session: SessionApi
|
||||
readonly vcs: VcsApi
|
||||
@@ -394,40 +391,6 @@ export async function bootstrapDirectory(input: {
|
||||
)
|
||||
}),
|
||||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.api.question.request
|
||||
.list({ location: { directory: input.directory } })
|
||||
.then((result) => result.data)
|
||||
.then((questions) => {
|
||||
const ids = questions.map((question) => question.sessionID)
|
||||
const grouped = groupBySession(
|
||||
questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[],
|
||||
)
|
||||
const warm = input.session
|
||||
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
|
||||
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
|
||||
return warm.then(() =>
|
||||
batch(() => {
|
||||
const current = input.session?.data.question ?? input.store.question
|
||||
for (const sessionID of Object.keys(current)) {
|
||||
if (grouped[sessionID]) continue
|
||||
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
|
||||
if (input.session) input.session.set("question", sessionID, [])
|
||||
if (!input.session) input.setStore("question", sessionID, [])
|
||||
}
|
||||
for (const [sessionID, questions] of Object.entries(grouped)) {
|
||||
const value = reconcile(
|
||||
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
|
||||
{ key: "id" },
|
||||
)
|
||||
if (input.session) input.session.set("question", sessionID, value)
|
||||
if (!input.session) input.setStore("question", sessionID, value)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
() => Promise.resolve(input.loadSessions(input.directory)),
|
||||
input.mcp &&
|
||||
(() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, directoryKey(input.directory), input.api.mcp))),
|
||||
|
||||
@@ -250,7 +250,6 @@ export function createChildStoreManager(input: {
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
get mcp_ready() {
|
||||
return !mcpQuery.isLoading
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Message, Part, Project } from "@/types"
|
||||
import type { PermissionRequest, QuestionRequest, SessionInfo } from "@opencode-ai/client/promise"
|
||||
import type { PermissionRequest, SessionInfo } from "@opencode-ai/client/promise"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { State } from "./types"
|
||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
|
||||
@@ -45,19 +45,6 @@ const permissionRequest = (id: string, sessionID: string, title = id) =>
|
||||
save: [],
|
||||
}) as PermissionRequest
|
||||
|
||||
const questionRequest = (id: string, sessionID: string, title = id) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
questions: [
|
||||
{
|
||||
question: title,
|
||||
header: title,
|
||||
options: [{ label: title, description: title }],
|
||||
},
|
||||
],
|
||||
}) as QuestionRequest
|
||||
|
||||
const baseState = (input: Partial<State> = {}) =>
|
||||
({
|
||||
status: "complete",
|
||||
@@ -75,7 +62,6 @@ const baseState = (input: Partial<State> = {}) =>
|
||||
session_diff: {},
|
||||
todo: {},
|
||||
permission: {},
|
||||
question: {},
|
||||
mcp: {},
|
||||
lsp: [],
|
||||
vcs: undefined,
|
||||
@@ -220,7 +206,6 @@ describe("applyDirectoryEvent", () => {
|
||||
session_diff: { ses_1: [] },
|
||||
todo: { ses_1: [] },
|
||||
permission: { ses_1: [] },
|
||||
question: { ses_1: [] },
|
||||
session_status: { ses_1: { type: "busy" } },
|
||||
}),
|
||||
)
|
||||
@@ -241,7 +226,6 @@ describe("applyDirectoryEvent", () => {
|
||||
expect(store.session_diff.ses_1).toBeUndefined()
|
||||
expect(store.todo.ses_1).toBeUndefined()
|
||||
expect(store.permission.ses_1).toBeUndefined()
|
||||
expect(store.question.ses_1).toBeUndefined()
|
||||
expect(store.session_status.ses_1).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -282,7 +266,6 @@ describe("applyDirectoryEvent", () => {
|
||||
session_diff: { [item.info.id]: [] },
|
||||
todo: { [item.info.id]: [] },
|
||||
permission: { [item.info.id]: [] },
|
||||
question: { [item.info.id]: [] },
|
||||
session_status: { [item.info.id]: { type: "busy" } },
|
||||
}),
|
||||
)
|
||||
@@ -306,7 +289,6 @@ describe("applyDirectoryEvent", () => {
|
||||
expect(store.session_diff[item.info.id]).toBeUndefined()
|
||||
expect(store.todo[item.info.id]).toBeUndefined()
|
||||
expect(store.permission[item.info.id]).toBeUndefined()
|
||||
expect(store.question[item.info.id]).toBeUndefined()
|
||||
expect(store.session_status[item.info.id]).toBeUndefined()
|
||||
}
|
||||
})
|
||||
@@ -325,7 +307,6 @@ describe("applyDirectoryEvent", () => {
|
||||
session_diff: { [dropped.id]: [] },
|
||||
todo: { [dropped.id]: [] },
|
||||
permission: { [dropped.id]: [] },
|
||||
question: { [dropped.id]: [] },
|
||||
session_status: { [dropped.id]: { type: "busy" } },
|
||||
}),
|
||||
)
|
||||
@@ -349,7 +330,6 @@ describe("applyDirectoryEvent", () => {
|
||||
expect(store.session_diff[dropped.id]).toBeUndefined()
|
||||
expect(store.todo[dropped.id]).toBeUndefined()
|
||||
expect(store.permission[dropped.id]).toBeUndefined()
|
||||
expect(store.question[dropped.id]).toBeUndefined()
|
||||
expect(store.session_status[dropped.id]).toBeUndefined()
|
||||
expect(todos).toEqual([dropped.id])
|
||||
})
|
||||
@@ -486,12 +466,11 @@ describe("applyDirectoryEvent", () => {
|
||||
expect(store.part[messageID]).toBeUndefined()
|
||||
})
|
||||
|
||||
test("tracks permission and question request lifecycles", () => {
|
||||
test("tracks permission request lifecycles", () => {
|
||||
const sessionID = "ses_1"
|
||||
const [store, setStore] = createStore(
|
||||
baseState({
|
||||
permission: { [sessionID]: [permissionRequest("perm_1", sessionID), permissionRequest("perm_3", sessionID)] },
|
||||
question: { [sessionID]: [questionRequest("q_1", sessionID), questionRequest("q_3", sessionID)] },
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -524,36 +503,6 @@ describe("applyDirectoryEvent", () => {
|
||||
loadLsp() {},
|
||||
})
|
||||
expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"])
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "question.asked", properties: questionRequest("q_2", sessionID) },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
directory: "/tmp",
|
||||
loadLsp() {},
|
||||
})
|
||||
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") },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
directory: "/tmp",
|
||||
loadLsp() {},
|
||||
})
|
||||
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated")
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
directory: "/tmp",
|
||||
loadLsp() {},
|
||||
})
|
||||
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_3"])
|
||||
})
|
||||
|
||||
test("updates vcs branch in store and cache", () => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { Message, Part, Project, Todo } from "@/types"
|
||||
import type {
|
||||
FileDiffInfo,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
SessionInfo,
|
||||
SessionStatus,
|
||||
} from "@opencode-ai/client/promise"
|
||||
@@ -27,9 +26,6 @@ const SESSION_CONTENT_EVENTS = new Set([
|
||||
"message.part.delta",
|
||||
"permission.asked",
|
||||
"permission.replied",
|
||||
"question.asked",
|
||||
"question.replied",
|
||||
"question.rejected",
|
||||
])
|
||||
|
||||
export function applyGlobalEvent(input: {
|
||||
@@ -86,7 +82,6 @@ export function cleanupDroppedSessionCaches(
|
||||
...Object.keys(store.session_diff),
|
||||
...Object.keys(store.todo),
|
||||
...Object.keys(store.permission),
|
||||
...Object.keys(store.question),
|
||||
...Object.keys(store.session_status),
|
||||
...Object.values(store.part)
|
||||
.map((parts) => parts?.find((part) => !!part?.sessionID)?.sessionID)
|
||||
@@ -438,43 +433,6 @@ export function applyDirectoryEvent(input: {
|
||||
)
|
||||
break
|
||||
}
|
||||
case "question.asked": {
|
||||
const question = event.properties as QuestionRequest
|
||||
const questions = input.store.question[question.sessionID]
|
||||
if (!questions) {
|
||||
input.setStore("question", question.sessionID, [question])
|
||||
break
|
||||
}
|
||||
const result = Binary.search(questions, question.id, (q) => q.id)
|
||||
if (result.found) {
|
||||
input.setStore("question", question.sessionID, result.index, reconcile(question))
|
||||
break
|
||||
}
|
||||
input.setStore(
|
||||
"question",
|
||||
question.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 0, question)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "question.replied":
|
||||
case "question.rejected": {
|
||||
const props = event.properties as { sessionID: string; requestID: string }
|
||||
const questions = input.store.question[props.sessionID]
|
||||
if (!questions) break
|
||||
const result = Binary.search(questions, props.requestID, (q) => q.id)
|
||||
if (!result.found) break
|
||||
input.setStore(
|
||||
"question",
|
||||
props.sessionID,
|
||||
produce((draft) => {
|
||||
draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
break
|
||||
}
|
||||
case "lsp.updated": {
|
||||
input.loadLsp()
|
||||
break
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Message, Part, Todo } from "@/types"
|
||||
import type { FormInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||
import type { FormInfo, PermissionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
||||
|
||||
@@ -33,7 +33,6 @@ describe("app session cache", () => {
|
||||
session_message: Record<string, never[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
form: Record<string, FormInfo[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
} = {
|
||||
@@ -44,7 +43,6 @@ describe("app session cache", () => {
|
||||
session_message: {},
|
||||
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
|
||||
permission: { ses_1: [] as PermissionRequest[] },
|
||||
question: { ses_1: [] as QuestionRequest[] },
|
||||
form: { ses_1: [] as FormInfo[] },
|
||||
part_text_accum_delta: { prt_1: "streamed text" },
|
||||
}
|
||||
@@ -58,7 +56,6 @@ describe("app session cache", () => {
|
||||
expect(store.session_diff.ses_1).toBeUndefined()
|
||||
expect(store.session_status.ses_1).toBeUndefined()
|
||||
expect(store.permission.ses_1).toBeUndefined()
|
||||
expect(store.question.ses_1).toBeUndefined()
|
||||
expect(store.form.ses_1).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -72,7 +69,6 @@ describe("app session cache", () => {
|
||||
session_message: Record<string, never[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
form: Record<string, FormInfo[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
} = {
|
||||
@@ -83,7 +79,6 @@ describe("app session cache", () => {
|
||||
session_message: {},
|
||||
part: { [m.id]: [part("prt_1", "ses_1", m.id)] },
|
||||
permission: {},
|
||||
question: {},
|
||||
form: {},
|
||||
part_text_accum_delta: {},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Message, Part, Todo } from "@/types"
|
||||
import type { FormInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||
import type { FormInfo, PermissionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||
import type { FileDiffInfo } from "@opencode-ai/client/promise"
|
||||
import type { SessionMessageInfo } from "@opencode-ai/client/promise"
|
||||
|
||||
@@ -13,7 +13,6 @@ type SessionCache = {
|
||||
session_message: Record<string, SessionMessageInfo[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
form?: Record<string, FormInfo[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
}
|
||||
@@ -38,7 +37,6 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable<stri
|
||||
delete store.session_diff[sessionID]
|
||||
delete store.session_status[sessionID]
|
||||
delete store.permission[sessionID]
|
||||
delete store.question[sessionID]
|
||||
if (store.form) delete store.form[sessionID]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Agent, Config, LspStatus, Message, Part, Path, Todo, VcsInfo } fro
|
||||
import type {
|
||||
FileDiffInfo,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
ReferenceInfo,
|
||||
SessionInfo,
|
||||
SessionStatus,
|
||||
@@ -50,9 +49,6 @@ export type State = {
|
||||
permission: {
|
||||
[sessionID: string]: PermissionRequest[]
|
||||
}
|
||||
question: {
|
||||
[sessionID: string]: QuestionRequest[]
|
||||
}
|
||||
mcp_ready: boolean
|
||||
mcp: {
|
||||
[name: string]: McpServer["status"]
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
SessionMessageInfo,
|
||||
} from "@opencode-ai/client/promise"
|
||||
import type { Message, Part, Todo } from "@/types"
|
||||
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||
import type { FileDiffInfo, PermissionRequest, SessionStatus } from "@opencode-ai/client/promise"
|
||||
import { batch } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { rootSession } from "@/utils/session-route"
|
||||
@@ -198,7 +198,6 @@ export function createServerSession(
|
||||
session_diff: {} as Record<string, FileDiffInfo[]>,
|
||||
todo: {} as Record<string, Todo[]>,
|
||||
permission: {} as Record<string, PermissionRequest[]>,
|
||||
question: {} as Record<string, QuestionRequest[]>,
|
||||
form: {} as Record<string, FormInfo[]>,
|
||||
pending: {} as Record<string, SessionInboxInfo[]>,
|
||||
input: {} as Record<string, string[]>,
|
||||
@@ -281,9 +280,6 @@ export function createServerSession(
|
||||
...Object.entries(data.permission)
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([sessionID]) => sessionID),
|
||||
...Object.entries(data.question)
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([sessionID]) => sessionID),
|
||||
...Object.entries(data.form)
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([sessionID]) => sessionID),
|
||||
@@ -529,9 +525,6 @@ export function createServerSession(
|
||||
...Object.entries(data.permission)
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([sessionID]) => sessionID),
|
||||
...Object.entries(data.question)
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([sessionID]) => sessionID),
|
||||
...Object.entries(data.form)
|
||||
.filter(([, items]) => items.length > 0)
|
||||
.map(([sessionID]) => sessionID),
|
||||
@@ -1339,36 +1332,6 @@ export function createServerSession(
|
||||
)
|
||||
return
|
||||
}
|
||||
case "question.asked": {
|
||||
const question = event.properties as QuestionRequest
|
||||
const questions = data.question[question.sessionID]
|
||||
if (!questions) {
|
||||
setData("question", question.sessionID, [question])
|
||||
return
|
||||
}
|
||||
const result = Binary.search(questions, question.id, (item) => item.id)
|
||||
if (result.found) setData("question", question.sessionID, result.index, reconcile(question))
|
||||
if (!result.found)
|
||||
setData(
|
||||
"question",
|
||||
question.sessionID,
|
||||
produce((draft) => void draft.splice(result.index, 0, question)),
|
||||
)
|
||||
return
|
||||
}
|
||||
case "question.replied":
|
||||
case "question.rejected": {
|
||||
const props = event.properties as { sessionID: string; requestID: string }
|
||||
setData(
|
||||
"question",
|
||||
props.sessionID,
|
||||
produce((draft) => {
|
||||
if (!draft) return
|
||||
const result = Binary.search(draft, props.requestID, (item) => item.id)
|
||||
if (result.found) draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
export * as ServerProcess from "./server-process"
|
||||
|
||||
import { NodeServices } from "@effect/platform-node"
|
||||
import { Service, type DiscoverOptions } from "@opencode-ai/client/effect/service"
|
||||
import { Service, type DiscoverOptions, type Info } from "@opencode-ai/client/effect/service"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { OPENCODE_CHANNEL, OPENCODE_VERSION } from "./version"
|
||||
import { AppProcess } from "@opencode-ai/util/process"
|
||||
import { randomBytes, randomUUID } from "node:crypto"
|
||||
import { Effect, Option, Redacted, Schedule } from "effect"
|
||||
import path from "node:path"
|
||||
import { Effect, FileSystem, Option, Redacted, Schedule, Schema } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { Env } from "./env"
|
||||
import { ServiceConfig } from "./services/service-config"
|
||||
import { ServiceRegistration } from "./services/service-registration"
|
||||
import { Updater } from "./services/updater"
|
||||
import { WebUi } from "./services/web-ui"
|
||||
|
||||
@@ -121,13 +121,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
onListen: (address, shutdown) =>
|
||||
Effect.gen(function* () {
|
||||
if (!config.password) yield* ServiceConfig.password(password)
|
||||
return yield* ServiceRegistration.register(
|
||||
address,
|
||||
password,
|
||||
instanceID,
|
||||
serviceOptions.file,
|
||||
shutdown,
|
||||
)
|
||||
return yield* register(address, password, instanceID, serviceOptions.file, shutdown)
|
||||
}),
|
||||
},
|
||||
transform,
|
||||
@@ -164,6 +158,52 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||
)
|
||||
})
|
||||
|
||||
const infoJson = Schema.fromJsonString(Service.Info)
|
||||
const encodeInfo = Schema.encodeEffect(infoJson)
|
||||
const decodeInfo = Schema.decodeUnknownEffect(infoJson)
|
||||
|
||||
const register = Effect.fnUntraced(function* (
|
||||
address: HttpServer.Address,
|
||||
password: string,
|
||||
id: string,
|
||||
file: string,
|
||||
shutdown: Effect.Effect<void>,
|
||||
) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const temp = file + "." + id + ".tmp"
|
||||
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||
const info = {
|
||||
id,
|
||||
version: OPENCODE_VERSION,
|
||||
url: HttpServer.formatAddress(address),
|
||||
pid: process.pid,
|
||||
password,
|
||||
}
|
||||
const encoded = yield* encodeInfo(info)
|
||||
const current = fs.readFileString(file).pipe(
|
||||
Effect.flatMap(decodeInfo),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
const owns = (found: Info | undefined) =>
|
||||
found?.id === info.id &&
|
||||
found.version === info.version &&
|
||||
found.url === info.url &&
|
||||
found.pid === info.pid &&
|
||||
found.password === info.password
|
||||
yield* fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, file)))
|
||||
yield* current.pipe(
|
||||
Effect.filterOrFail(owns),
|
||||
Effect.repeat(Schedule.spaced("5 seconds")),
|
||||
Effect.ignore,
|
||||
Effect.andThen(shutdown),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
return current.pipe(
|
||||
Effect.flatMap((found) => (owns(found) ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
)
|
||||
})
|
||||
|
||||
const recognizeIncumbent = Effect.fnUntraced(function* (options: DiscoverOptions, hostname: string, port: number) {
|
||||
const found = yield* Service.incumbent({ ...options, url: serviceURL(hostname, port) }).pipe(
|
||||
Effect.filterOrFail((value) => value !== undefined),
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
export * as ServiceRegistration from "./service-registration"
|
||||
|
||||
import { Service, type Info } from "@opencode-ai/client/effect/service"
|
||||
import path from "node:path"
|
||||
import { Effect, FileSystem, Schedule, Schema } from "effect"
|
||||
import { HttpServer } from "effect/unstable/http"
|
||||
import { OPENCODE_VERSION } from "../version"
|
||||
|
||||
const infoJson = Schema.fromJsonString(Service.Info)
|
||||
const encodeInfo = Schema.encodeEffect(infoJson)
|
||||
const decodeInfo = Schema.decodeUnknownEffect(infoJson)
|
||||
|
||||
export const register = Effect.fnUntraced(function* (
|
||||
address: HttpServer.Address,
|
||||
password: string,
|
||||
id: string,
|
||||
file: string,
|
||||
shutdown: Effect.Effect<void>,
|
||||
) {
|
||||
const fs = yield* FileSystem.FileSystem
|
||||
const temp = file + "." + id + ".tmp"
|
||||
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
|
||||
const info = {
|
||||
id,
|
||||
version: OPENCODE_VERSION,
|
||||
url: HttpServer.formatAddress(address),
|
||||
pid: process.pid,
|
||||
password,
|
||||
}
|
||||
const encoded = yield* encodeInfo(info)
|
||||
const current = fs.readFileString(file).pipe(
|
||||
Effect.flatMap(decodeInfo),
|
||||
Effect.orElseSucceed(() => undefined),
|
||||
)
|
||||
const owns = (found: Info | undefined) =>
|
||||
found?.id === info.id &&
|
||||
found.version === info.version &&
|
||||
found.url === info.url &&
|
||||
found.pid === info.pid &&
|
||||
found.password === info.password
|
||||
yield* fs.writeFileString(temp, encoded, { mode: 0o600 }).pipe(Effect.andThen(fs.rename(temp, file)))
|
||||
yield* current.pipe(
|
||||
Effect.filterOrFail(owns),
|
||||
Effect.repeat(Schedule.spaced("5 seconds")),
|
||||
Effect.ignore,
|
||||
Effect.andThen(shutdown),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
return current.pipe(
|
||||
Effect.flatMap((found) => (owns(found) ? fs.remove(file) : Effect.void)),
|
||||
Effect.ignore,
|
||||
)
|
||||
})
|
||||
@@ -240,8 +240,6 @@ async function run(input: {
|
||||
})()
|
||||
spyOn(sdk.event, "subscribe").mockImplementation(() => stream)
|
||||
spyOn(sdk.permission, "list").mockImplementation(() => ok([]) as never)
|
||||
spyOn(sdk.question, "list").mockImplementation(() => ok([]) as never)
|
||||
spyOn(sdk.question, "reject").mockImplementation(() => ok(undefined) as never)
|
||||
spyOn(sdk.form, "list").mockImplementation(
|
||||
(request) => ok(input.pendingForms?.filter((item) => item.sessionID === request.sessionID) ?? []) as never,
|
||||
)
|
||||
@@ -435,8 +433,6 @@ describe("runNonInteractivePrompt", () => {
|
||||
expect(sdk.form.request.list).toHaveBeenCalledWith({
|
||||
location: { directory: "/work tree", workspace: "wrk_1" },
|
||||
})
|
||||
expect(sdk.question.list).not.toHaveBeenCalled()
|
||||
expect(sdk.question.reject).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("attach mode cancels only session-owned forms", async () => {
|
||||
|
||||
@@ -8,7 +8,6 @@ import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { ServiceConfig } from "../src/services/service-config"
|
||||
import { ServiceRegistration } from "../src/services/service-registration"
|
||||
|
||||
test("managed service ports are stable per installation channel", () => {
|
||||
expect(ServiceConfig.defaultPort("latest")).toBe(0xc0de)
|
||||
@@ -414,37 +413,34 @@ test("port contender recognizes an incumbent registered during the bind race", a
|
||||
}
|
||||
}, 45_000)
|
||||
|
||||
test("service registration replaces a stale owner with the bound address", async () => {
|
||||
test("stale dead registration is replaced after binding the selected port", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-stale-"))
|
||||
const port = await availablePort()
|
||||
const registration = path.join(root, "state", "opencode", "service-local.json")
|
||||
await fs.mkdir(path.join(root, "config", "opencode"), { recursive: true })
|
||||
await fs.mkdir(path.dirname(registration), { recursive: true })
|
||||
await fs.writeFile(path.join(root, "config", "opencode", "service-local.json"), JSON.stringify({ port }))
|
||||
await fs.writeFile(
|
||||
registration,
|
||||
JSON.stringify({ id: "dead", version: "dead", url: "http://127.0.0.1:4321", pid: 2_147_483_647 }),
|
||||
JSON.stringify({ id: "dead", version: "dead", url: `http://127.0.0.1:${port}`, pid: 2_147_483_647 }),
|
||||
)
|
||||
const owner = Bun.spawn([process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"], {
|
||||
env: serviceEnv(root),
|
||||
stderr: "pipe",
|
||||
stdout: "ignore",
|
||||
})
|
||||
try {
|
||||
const cleanup = await Effect.runPromise(
|
||||
ServiceRegistration.register(
|
||||
{ _tag: "TcpAddress", hostname: "127.0.0.1", port: 4321 },
|
||||
"secret",
|
||||
"owner",
|
||||
registration,
|
||||
Effect.never,
|
||||
).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)),
|
||||
)
|
||||
expect(await Bun.file(registration).json()).toEqual({
|
||||
id: "owner",
|
||||
version: OPENCODE_VERSION,
|
||||
url: "http://127.0.0.1:4321",
|
||||
pid: process.pid,
|
||||
password: "secret",
|
||||
})
|
||||
await Effect.runPromise(cleanup.pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
expect(await Bun.file(registration).exists()).toBe(false)
|
||||
const info = await waitForInfo(registration, (value) => value.id !== "dead")
|
||||
expect(new URL(info.url).port).toBe(String(port))
|
||||
expect(info.pid).toBe(owner.pid)
|
||||
await Effect.runPromise(Service.stop({ file: registration }).pipe(Effect.provide(NodeFileSystem.layer)))
|
||||
await owner.exited
|
||||
} finally {
|
||||
owner.kill("SIGTERM")
|
||||
await owner.exited
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
}, 30_000)
|
||||
|
||||
test("a failed service stays registered and owns the selected port until stopped", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-failed-"))
|
||||
|
||||
@@ -32,7 +32,6 @@ import type { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
import type { Command } from "@opencode-ai/schema/command"
|
||||
import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
|
||||
import type { Pty } from "@opencode-ai/schema/pty"
|
||||
import type { Question } from "@opencode-ai/schema/question"
|
||||
import type { Reference } from "@opencode-ai/schema/reference"
|
||||
import type { Worktree } from "@opencode-ai/schema/worktree"
|
||||
import type { Vcs } from "@opencode-ai/schema/vcs"
|
||||
@@ -1503,69 +1502,38 @@ export interface ShellApi<E = never> {
|
||||
export type Endpoint22_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Question.Request> }
|
||||
export type QuestionRequestListOperation<E = never> = (
|
||||
input?: Endpoint22_0Input,
|
||||
) => Effect.Effect<Endpoint22_0Output, E>
|
||||
|
||||
export type Endpoint22_1Input = { readonly sessionID: Session.ID }
|
||||
export type Endpoint22_1Output = ReadonlyArray<Question.Request>
|
||||
export type QuestionListOperation<E = never> = (input: Endpoint22_1Input) => Effect.Effect<Endpoint22_1Output, E>
|
||||
|
||||
export type Endpoint22_2Input = {
|
||||
readonly sessionID: Session.ID
|
||||
readonly requestID: Question.ID
|
||||
readonly answers: ReadonlyArray<Question.Answer>
|
||||
}
|
||||
export type Endpoint22_2Output = void
|
||||
export type QuestionReplyOperation<E = never> = (input: Endpoint22_2Input) => Effect.Effect<Endpoint22_2Output, E>
|
||||
|
||||
export type Endpoint22_3Input = { readonly sessionID: Session.ID; readonly requestID: Question.ID }
|
||||
export type Endpoint22_3Output = void
|
||||
export type QuestionRejectOperation<E = never> = (input: Endpoint22_3Input) => Effect.Effect<Endpoint22_3Output, E>
|
||||
|
||||
export interface QuestionApi<E = never> {
|
||||
readonly request: { readonly list: QuestionRequestListOperation<E> }
|
||||
readonly list: QuestionListOperation<E>
|
||||
readonly reply: QuestionReplyOperation<E>
|
||||
readonly reject: QuestionRejectOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint23_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint23_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Reference.Info> }
|
||||
export type ReferenceListOperation<E = never> = (input?: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E>
|
||||
export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Reference.Info> }
|
||||
export type ReferenceListOperation<E = never> = (input?: Endpoint22_0Input) => Effect.Effect<Endpoint22_0Output, E>
|
||||
|
||||
export interface ReferenceApi<E = never> {
|
||||
readonly list: ReferenceListOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint24_0Input = { readonly projectID: Project.ID }
|
||||
export type Endpoint24_0Output = Worktree.List
|
||||
export type WorktreeListOperation<E = never> = (input: Endpoint24_0Input) => Effect.Effect<Endpoint24_0Output, E>
|
||||
export type Endpoint23_0Input = { readonly projectID: Project.ID }
|
||||
export type Endpoint23_0Output = Worktree.List
|
||||
export type WorktreeListOperation<E = never> = (input: Endpoint23_0Input) => Effect.Effect<Endpoint23_0Output, E>
|
||||
|
||||
export type Endpoint24_1Input = {
|
||||
export type Endpoint23_1Input = {
|
||||
readonly projectID: Project.ID
|
||||
readonly strategy: Worktree.StrategyID
|
||||
readonly from?: AbsolutePath | undefined
|
||||
readonly directory: AbsolutePath
|
||||
readonly name?: string | undefined
|
||||
}
|
||||
export type Endpoint24_1Output = Worktree.Info
|
||||
export type WorktreeCreateOperation<E = never> = (input: Endpoint24_1Input) => Effect.Effect<Endpoint24_1Output, E>
|
||||
export type Endpoint23_1Output = Worktree.Info
|
||||
export type WorktreeCreateOperation<E = never> = (input: Endpoint23_1Input) => Effect.Effect<Endpoint23_1Output, E>
|
||||
|
||||
export type Endpoint24_2Input = {
|
||||
export type Endpoint23_2Input = {
|
||||
readonly projectID: Project.ID
|
||||
readonly directory: AbsolutePath
|
||||
readonly force: boolean
|
||||
}
|
||||
export type Endpoint24_2Output = void
|
||||
export type WorktreeRemoveOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E>
|
||||
export type Endpoint23_2Output = void
|
||||
export type WorktreeRemoveOperation<E = never> = (input: Endpoint23_2Input) => Effect.Effect<Endpoint23_2Output, E>
|
||||
|
||||
export type Endpoint24_3Input = { readonly projectID: Project.ID }
|
||||
export type Endpoint24_3Output = void
|
||||
export type WorktreeRefreshOperation<E = never> = (input: Endpoint24_3Input) => Effect.Effect<Endpoint24_3Output, E>
|
||||
export type Endpoint23_3Input = { readonly projectID: Project.ID }
|
||||
export type Endpoint23_3Output = void
|
||||
export type WorktreeRefreshOperation<E = never> = (input: Endpoint23_3Input) => Effect.Effect<Endpoint23_3Output, E>
|
||||
|
||||
export interface WorktreeApi<E = never> {
|
||||
readonly list: WorktreeListOperation<E>
|
||||
@@ -1574,25 +1542,25 @@ export interface WorktreeApi<E = never> {
|
||||
readonly refresh: WorktreeRefreshOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint25_0Input = {
|
||||
export type Endpoint24_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint25_0Output = { readonly location: Location.Info; readonly data: Vcs.Info }
|
||||
export type VcsGetOperation<E = never> = (input?: Endpoint25_0Input) => Effect.Effect<Endpoint25_0Output, E>
|
||||
export type Endpoint24_0Output = { readonly location: Location.Info; readonly data: Vcs.Info }
|
||||
export type VcsGetOperation<E = never> = (input?: Endpoint24_0Input) => Effect.Effect<Endpoint24_0Output, E>
|
||||
|
||||
export type Endpoint25_1Input = {
|
||||
export type Endpoint24_1Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint25_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
|
||||
export type VcsStatusOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||
export type Endpoint24_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray<Vcs.FileStatus> }
|
||||
export type VcsStatusOperation<E = never> = (input?: Endpoint24_1Input) => Effect.Effect<Endpoint24_1Output, E>
|
||||
|
||||
export type Endpoint25_2Input = {
|
||||
export type Endpoint24_2Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
readonly mode: Vcs.Mode
|
||||
readonly context?: number | undefined
|
||||
}
|
||||
export type Endpoint25_2Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileDiff.Info> }
|
||||
export type VcsDiffOperation<E = never> = (input: Endpoint25_2Input) => Effect.Effect<Endpoint25_2Output, E>
|
||||
export type Endpoint24_2Output = { readonly location: Location.Info; readonly data: ReadonlyArray<FileDiff.Info> }
|
||||
export type VcsDiffOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E>
|
||||
|
||||
export interface VcsApi<E = never> {
|
||||
readonly get: VcsGetOperation<E>
|
||||
@@ -1600,20 +1568,20 @@ export interface VcsApi<E = never> {
|
||||
readonly diff: VcsDiffOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint26_0Output = ReadonlyArray<Location.Ref>
|
||||
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E>
|
||||
export type Endpoint25_0Output = ReadonlyArray<Location.Ref>
|
||||
export type DebugLocationListOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
||||
|
||||
export type Endpoint26_1Input = {
|
||||
export type Endpoint25_1Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint26_1Output = void
|
||||
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint26_1Input) => Effect.Effect<Endpoint26_1Output, E>
|
||||
export type Endpoint25_1Output = void
|
||||
export type DebugLocationEvictOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||
|
||||
export interface DebugApi<E = never> {
|
||||
readonly location: { readonly list: DebugLocationListOperation<E>; readonly evict: DebugLocationEvictOperation<E> }
|
||||
}
|
||||
|
||||
export type Endpoint27_0Output =
|
||||
export type Endpoint26_0Output =
|
||||
| { readonly status: "required" | "completed" }
|
||||
| {
|
||||
readonly status: "running"
|
||||
@@ -1624,36 +1592,36 @@ export type Endpoint27_0Output =
|
||||
}
|
||||
}
|
||||
| { readonly status: "error"; readonly error: string }
|
||||
export type MigrationV1StatusOperation<E = never> = () => Effect.Effect<Endpoint27_0Output, E>
|
||||
export type MigrationV1StatusOperation<E = never> = () => Effect.Effect<Endpoint26_0Output, E>
|
||||
|
||||
export interface MigrationApi<E = never> {
|
||||
readonly v1: { readonly status: MigrationV1StatusOperation<E> }
|
||||
}
|
||||
|
||||
export type Endpoint28_0Input = {
|
||||
export type Endpoint27_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint28_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> }
|
||||
export type WebsearchProvidersOperation<E = never> = (input?: Endpoint28_0Input) => Effect.Effect<Endpoint28_0Output, E>
|
||||
export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray<WebSearch.Provider> }
|
||||
export type WebsearchProvidersOperation<E = never> = (input?: Endpoint27_0Input) => Effect.Effect<Endpoint27_0Output, E>
|
||||
|
||||
export type Endpoint28_1Input = {
|
||||
export type Endpoint27_1Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
readonly query: string
|
||||
readonly providerID?: WebSearch.ID | undefined
|
||||
}
|
||||
export type Endpoint28_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response }
|
||||
export type WebsearchQueryOperation<E = never> = (input: Endpoint28_1Input) => Effect.Effect<Endpoint28_1Output, E>
|
||||
export type Endpoint27_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response }
|
||||
export type WebsearchQueryOperation<E = never> = (input: Endpoint27_1Input) => Effect.Effect<Endpoint27_1Output, E>
|
||||
|
||||
export interface WebsearchApi<E = never> {
|
||||
readonly providers: WebsearchProvidersOperation<E>
|
||||
readonly query: WebsearchQueryOperation<E>
|
||||
}
|
||||
|
||||
export type Endpoint29_0Input = {
|
||||
export type Endpoint28_0Input = {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}
|
||||
export type Endpoint29_0Output = ReadonlyArray<Config.Entry>
|
||||
export type ConfigGetOperation<E = never> = (input?: Endpoint29_0Input) => Effect.Effect<Endpoint29_0Output, E>
|
||||
export type Endpoint28_0Output = ReadonlyArray<Config.Entry>
|
||||
export type ConfigGetOperation<E = never> = (input?: Endpoint28_0Input) => Effect.Effect<Endpoint28_0Output, E>
|
||||
|
||||
export interface ConfigApi<E = never> {
|
||||
readonly get: ConfigGetOperation<E>
|
||||
@@ -1682,7 +1650,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 worktree: WorktreeApi<E>
|
||||
readonly vcs: VcsApi<E>
|
||||
|
||||
@@ -200,38 +200,30 @@ import type {
|
||||
Endpoint21_5Output,
|
||||
Endpoint22_0Input,
|
||||
Endpoint22_0Output,
|
||||
Endpoint22_1Input,
|
||||
Endpoint22_1Output,
|
||||
Endpoint22_2Input,
|
||||
Endpoint22_2Output,
|
||||
Endpoint22_3Input,
|
||||
Endpoint22_3Output,
|
||||
Endpoint23_0Input,
|
||||
Endpoint23_0Output,
|
||||
Endpoint23_1Input,
|
||||
Endpoint23_1Output,
|
||||
Endpoint23_2Input,
|
||||
Endpoint23_2Output,
|
||||
Endpoint23_3Input,
|
||||
Endpoint23_3Output,
|
||||
Endpoint24_0Input,
|
||||
Endpoint24_0Output,
|
||||
Endpoint24_1Input,
|
||||
Endpoint24_1Output,
|
||||
Endpoint24_2Input,
|
||||
Endpoint24_2Output,
|
||||
Endpoint24_3Input,
|
||||
Endpoint24_3Output,
|
||||
Endpoint25_0Input,
|
||||
Endpoint25_0Output,
|
||||
Endpoint25_1Input,
|
||||
Endpoint25_1Output,
|
||||
Endpoint25_2Input,
|
||||
Endpoint25_2Output,
|
||||
Endpoint26_0Output,
|
||||
Endpoint26_1Input,
|
||||
Endpoint26_1Output,
|
||||
Endpoint27_0Input,
|
||||
Endpoint27_0Output,
|
||||
Endpoint27_1Input,
|
||||
Endpoint27_1Output,
|
||||
Endpoint28_0Input,
|
||||
Endpoint28_0Output,
|
||||
Endpoint28_1Input,
|
||||
Endpoint28_1Output,
|
||||
Endpoint29_0Input,
|
||||
Endpoint29_0Output,
|
||||
} from "../api/api.js"
|
||||
import { ClientError } from "./client-error.js"
|
||||
|
||||
@@ -1158,145 +1150,110 @@ const adaptGroup21 = (raw: RawClient["server.shell"]) => ({
|
||||
remove: Endpoint21_5(raw),
|
||||
})
|
||||
|
||||
const Endpoint22_0 = (raw: RawClient["server.question"]) => (input?: Endpoint22_0Input) =>
|
||||
const Endpoint22_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint22_0Input) =>
|
||||
preserveEffect<Endpoint22_0Output>()(
|
||||
raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) =>
|
||||
preserveEffect<Endpoint22_1Output>()(
|
||||
raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
)
|
||||
|
||||
const Endpoint22_2 = (raw: RawClient["server.question"]) => (input: Endpoint22_2Input) =>
|
||||
preserveEffect<Endpoint22_2Output>()(
|
||||
raw["session.question.reply"]({
|
||||
params: { sessionID: input["sessionID"], requestID: input["requestID"] },
|
||||
payload: { answers: input["answers"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint22_3 = (raw: RawClient["server.question"]) => (input: Endpoint22_3Input) =>
|
||||
preserveEffect<Endpoint22_3Output>()(
|
||||
raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const adaptGroup22 = (raw: RawClient["server.question"]) => ({
|
||||
request: { list: Endpoint22_0(raw) },
|
||||
list: Endpoint22_1(raw),
|
||||
reply: Endpoint22_2(raw),
|
||||
reject: Endpoint22_3(raw),
|
||||
})
|
||||
|
||||
const Endpoint23_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint23_0Input) =>
|
||||
preserveEffect<Endpoint23_0Output>()(
|
||||
raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup23 = (raw: RawClient["server.reference"]) => ({ list: Endpoint23_0(raw) })
|
||||
const adaptGroup22 = (raw: RawClient["server.reference"]) => ({ list: Endpoint22_0(raw) })
|
||||
|
||||
const Endpoint24_0 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_0Input) =>
|
||||
preserveEffect<Endpoint24_0Output>()(
|
||||
const Endpoint23_0 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_0Input) =>
|
||||
preserveEffect<Endpoint23_0Output>()(
|
||||
raw["worktree.list"]({ params: { projectID: input["projectID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint24_1 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_1Input) =>
|
||||
preserveEffect<Endpoint24_1Output>()(
|
||||
const Endpoint23_1 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_1Input) =>
|
||||
preserveEffect<Endpoint23_1Output>()(
|
||||
raw["worktree.create"]({
|
||||
params: { projectID: input["projectID"] },
|
||||
payload: { strategy: input["strategy"], from: input["from"], directory: input["directory"], name: input["name"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint24_2 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_2Input) =>
|
||||
preserveEffect<Endpoint24_2Output>()(
|
||||
const Endpoint23_2 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_2Input) =>
|
||||
preserveEffect<Endpoint23_2Output>()(
|
||||
raw["worktree.remove"]({
|
||||
params: { projectID: input["projectID"] },
|
||||
payload: { directory: input["directory"], force: input["force"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint24_3 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_3Input) =>
|
||||
preserveEffect<Endpoint24_3Output>()(
|
||||
const Endpoint23_3 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_3Input) =>
|
||||
preserveEffect<Endpoint23_3Output>()(
|
||||
raw["worktree.refresh"]({ params: { projectID: input["projectID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup24 = (raw: RawClient["server.worktree"]) => ({
|
||||
list: Endpoint24_0(raw),
|
||||
create: Endpoint24_1(raw),
|
||||
remove: Endpoint24_2(raw),
|
||||
refresh: Endpoint24_3(raw),
|
||||
const adaptGroup23 = (raw: RawClient["server.worktree"]) => ({
|
||||
list: Endpoint23_0(raw),
|
||||
create: Endpoint23_1(raw),
|
||||
remove: Endpoint23_2(raw),
|
||||
refresh: Endpoint23_3(raw),
|
||||
})
|
||||
|
||||
const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) =>
|
||||
preserveEffect<Endpoint25_0Output>()(
|
||||
const Endpoint24_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint24_0Input) =>
|
||||
preserveEffect<Endpoint24_0Output>()(
|
||||
raw["vcs.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_1Input) =>
|
||||
preserveEffect<Endpoint25_1Output>()(
|
||||
const Endpoint24_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint24_1Input) =>
|
||||
preserveEffect<Endpoint24_1Output>()(
|
||||
raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint25_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_2Input) =>
|
||||
preserveEffect<Endpoint25_2Output>()(
|
||||
const Endpoint24_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint24_2Input) =>
|
||||
preserveEffect<Endpoint24_2Output>()(
|
||||
raw["vcs.diff"]({ query: { location: input["location"], mode: input["mode"], context: input["context"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const adaptGroup25 = (raw: RawClient["server.vcs"]) => ({
|
||||
get: Endpoint25_0(raw),
|
||||
status: Endpoint25_1(raw),
|
||||
diff: Endpoint25_2(raw),
|
||||
const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({
|
||||
get: Endpoint24_0(raw),
|
||||
status: Endpoint24_1(raw),
|
||||
diff: Endpoint24_2(raw),
|
||||
})
|
||||
|
||||
const Endpoint26_0 = (raw: RawClient["server.debug"]) => () =>
|
||||
preserveEffect<Endpoint26_0Output>()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)))
|
||||
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
||||
preserveEffect<Endpoint25_0Output>()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError)))
|
||||
|
||||
const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) =>
|
||||
preserveEffect<Endpoint26_1Output>()(
|
||||
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
|
||||
preserveEffect<Endpoint25_1Output>()(
|
||||
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup26 = (raw: RawClient["server.debug"]) => ({
|
||||
location: { list: Endpoint26_0(raw), evict: Endpoint26_1(raw) },
|
||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
|
||||
location: { list: Endpoint25_0(raw), evict: Endpoint25_1(raw) },
|
||||
})
|
||||
|
||||
const Endpoint27_0 = (raw: RawClient["server.migration"]) => () =>
|
||||
preserveEffect<Endpoint27_0Output>()(raw["migration.v1.status"]({}).pipe(Effect.mapError(mapClientError)))
|
||||
const Endpoint26_0 = (raw: RawClient["server.migration"]) => () =>
|
||||
preserveEffect<Endpoint26_0Output>()(raw["migration.v1.status"]({}).pipe(Effect.mapError(mapClientError)))
|
||||
|
||||
const adaptGroup27 = (raw: RawClient["server.migration"]) => ({ v1: { status: Endpoint27_0(raw) } })
|
||||
const adaptGroup26 = (raw: RawClient["server.migration"]) => ({ v1: { status: Endpoint26_0(raw) } })
|
||||
|
||||
const Endpoint28_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint28_0Input) =>
|
||||
preserveEffect<Endpoint28_0Output>()(
|
||||
const Endpoint27_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint27_0Input) =>
|
||||
preserveEffect<Endpoint27_0Output>()(
|
||||
raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const Endpoint28_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint28_1Input) =>
|
||||
preserveEffect<Endpoint28_1Output>()(
|
||||
const Endpoint27_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint27_1Input) =>
|
||||
preserveEffect<Endpoint27_1Output>()(
|
||||
raw["websearch.query"]({
|
||||
query: { location: input["location"] },
|
||||
payload: { query: input["query"], providerID: input["providerID"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup28 = (raw: RawClient["server.websearch"]) => ({
|
||||
providers: Endpoint28_0(raw),
|
||||
query: Endpoint28_1(raw),
|
||||
const adaptGroup27 = (raw: RawClient["server.websearch"]) => ({
|
||||
providers: Endpoint27_0(raw),
|
||||
query: Endpoint27_1(raw),
|
||||
})
|
||||
|
||||
const Endpoint29_0 = (raw: RawClient["server.config"]) => (input?: Endpoint29_0Input) =>
|
||||
preserveEffect<Endpoint29_0Output>()(
|
||||
const Endpoint28_0 = (raw: RawClient["server.config"]) => (input?: Endpoint28_0Input) =>
|
||||
preserveEffect<Endpoint28_0Output>()(
|
||||
raw["config.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroup29 = (raw: RawClient["server.config"]) => ({ get: Endpoint29_0(raw) })
|
||||
const adaptGroup28 = (raw: RawClient["server.config"]) => ({ get: Endpoint28_0(raw) })
|
||||
|
||||
const adaptClient = (raw: RawClient) => ({
|
||||
health: adaptGroup0(raw["server.health"]),
|
||||
@@ -1321,14 +1278,13 @@ const adaptClient = (raw: RawClient) => ({
|
||||
event: adaptGroup19(raw["server.event"]),
|
||||
pty: adaptGroup20(raw["server.pty"]),
|
||||
shell: adaptGroup21(raw["server.shell"]),
|
||||
question: adaptGroup22(raw["server.question"]),
|
||||
reference: adaptGroup23(raw["server.reference"]),
|
||||
worktree: adaptGroup24(raw["server.worktree"]),
|
||||
vcs: adaptGroup25(raw["server.vcs"]),
|
||||
debug: adaptGroup26(raw["server.debug"]),
|
||||
migration: adaptGroup27(raw["server.migration"]),
|
||||
websearch: adaptGroup28(raw["server.websearch"]),
|
||||
config: adaptGroup29(raw["server.config"]),
|
||||
reference: adaptGroup22(raw["server.reference"]),
|
||||
worktree: adaptGroup23(raw["server.worktree"]),
|
||||
vcs: adaptGroup24(raw["server.vcs"]),
|
||||
debug: adaptGroup25(raw["server.debug"]),
|
||||
migration: adaptGroup26(raw["server.migration"]),
|
||||
websearch: adaptGroup27(raw["server.websearch"]),
|
||||
config: adaptGroup28(raw["server.config"]),
|
||||
})
|
||||
|
||||
export const make = (options?: { readonly baseUrl?: URL | string }) =>
|
||||
|
||||
@@ -194,14 +194,6 @@ import type {
|
||||
ShellOutputOutput,
|
||||
ShellRemoveInput,
|
||||
ShellRemoveOutput,
|
||||
QuestionRequestListInput,
|
||||
QuestionRequestListOutput,
|
||||
QuestionListInput,
|
||||
QuestionListOutput,
|
||||
QuestionReplyInput,
|
||||
QuestionReplyOutput,
|
||||
QuestionRejectInput,
|
||||
QuestionRejectOutput,
|
||||
ReferenceListInput,
|
||||
ReferenceListOutput,
|
||||
WorktreeListInput,
|
||||
@@ -1660,56 +1652,6 @@ export function make(options: ClientOptions) {
|
||||
requestOptions,
|
||||
),
|
||||
},
|
||||
question: {
|
||||
request: {
|
||||
list: (input?: QuestionRequestListInput, requestOptions?: RequestOptions) =>
|
||||
request<QuestionRequestListOutput>(
|
||||
{
|
||||
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>(
|
||||
|
||||
@@ -324,12 +324,6 @@ export type Pty = {
|
||||
exitCode?: number
|
||||
}
|
||||
|
||||
export type QuestionOption = { label: string; description: string }
|
||||
|
||||
export type QuestionTool = { messageID: string; id: string }
|
||||
|
||||
export type QuestionAnswer = Array<string>
|
||||
|
||||
export type FormMetadata1 = { [x: string]: any }
|
||||
|
||||
export type FormWhen1 = { key: string; op: "eq" | "neq"; value: string | number | boolean }
|
||||
@@ -910,15 +904,6 @@ export type ShellDeleted = {
|
||||
data: { id: string }
|
||||
}
|
||||
|
||||
export type QuestionRejected = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "question.rejected"
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; requestID: string }
|
||||
}
|
||||
|
||||
export type FormCancelled = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -1425,23 +1410,6 @@ export type PtyUpdated = {
|
||||
data: { info: Pty }
|
||||
}
|
||||
|
||||
export type QuestionInfo = {
|
||||
question: string
|
||||
header: string
|
||||
options: Array<QuestionOption>
|
||||
multiple?: boolean
|
||||
custom?: boolean
|
||||
}
|
||||
|
||||
export type QuestionReplied = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "question.replied"
|
||||
location?: LocationRef
|
||||
data: { sessionID: string; requestID: string; answers: Array<QuestionAnswer> }
|
||||
}
|
||||
|
||||
export type FormStringField1 = {
|
||||
key: string
|
||||
title?: string
|
||||
@@ -1682,17 +1650,6 @@ export type FormReplied = {
|
||||
data: { id: string; sessionID: string; answer: FormAnswer }
|
||||
}
|
||||
|
||||
export type QuestionAsked = {
|
||||
id: string
|
||||
created: number
|
||||
metadata?: { [x: string]: any }
|
||||
type: "question.asked"
|
||||
location?: LocationRef
|
||||
data: { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool }
|
||||
}
|
||||
|
||||
export type QuestionRequest = { id: string; sessionID: string; questions: Array<QuestionInfo>; tool?: QuestionTool }
|
||||
|
||||
export type FormField1 =
|
||||
| FormStringField1
|
||||
| FormNumberField1
|
||||
@@ -2114,9 +2071,6 @@ export type V2Event =
|
||||
| ShellCreated
|
||||
| ShellExited
|
||||
| ShellDeleted
|
||||
| QuestionAsked
|
||||
| QuestionReplied
|
||||
| QuestionRejected
|
||||
| FormCreated
|
||||
| FormReplied
|
||||
| FormCancelled
|
||||
@@ -2296,14 +2250,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 WorktreeError = {
|
||||
readonly name: "WorktreeError"
|
||||
readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined }
|
||||
@@ -5592,36 +5538,6 @@ export type ShellRemoveInput = {
|
||||
|
||||
export type ShellRemoveOutput = void
|
||||
|
||||
export type QuestionRequestListInput = {
|
||||
readonly location?: {
|
||||
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||
}["location"]
|
||||
}
|
||||
|
||||
export type QuestionRequestListOutput = {
|
||||
location: { directory: string; workspaceID?: string; project: { id: string; directory: string; canonical: string } }
|
||||
data: Array<QuestionRequest>
|
||||
}
|
||||
|
||||
export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
|
||||
|
||||
export type QuestionListOutput = { data: Array<QuestionRequest> }["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
|
||||
|
||||
@@ -27,7 +27,6 @@ import { Plugin } from "./plugin.js"
|
||||
import { PluginSupervisor } from "./plugin/supervisor.js"
|
||||
import { Worktree } from "./worktree.js"
|
||||
import { Pty } from "./pty.js"
|
||||
import { Question } from "./question.js"
|
||||
import { Shell } from "./shell.js"
|
||||
import { Reference } from "./reference.js"
|
||||
import { WebSearch } from "./websearch.js"
|
||||
@@ -86,7 +85,6 @@ const locationServiceNodes = [
|
||||
ReferenceInstructions.node,
|
||||
InstructionEntry.node,
|
||||
Form.node,
|
||||
Question.node,
|
||||
Generate.node,
|
||||
SessionGenerateNode.node,
|
||||
ReadToolFileSystem.node,
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
export * as Question from "./question.js"
|
||||
|
||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Context, Deferred, Effect, Layer, Schema } from "effect"
|
||||
import { Question } from "@opencode-ai/schema/question"
|
||||
import { Bus } from "./bus.js"
|
||||
import { SessionSchema } from "./session/schema.js"
|
||||
|
||||
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 { Event } from "@opencode-ai/schema/question"
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("Question.RejectedError", {}) {
|
||||
override get message() {
|
||||
return "The user dismissed this question"
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Question.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/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 bus = yield* Bus.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("Question.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* bus.publish(Question.Event.Asked, request).pipe(
|
||||
Effect.andThen(restore(Deferred.await(deferred))),
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
pending.delete(id)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const reply = Effect.fn("Question.reply")((input: ReplyInput) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const existing = pending.get(input.requestID)
|
||||
if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
|
||||
yield* bus.publish(Question.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("Question.reject")((requestID: ID) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const existing = pending.get(requestID)
|
||||
if (!existing) return yield* new NotFoundError({ requestID })
|
||||
yield* bus.publish(Question.Event.Rejected, {
|
||||
sessionID: existing.request.sessionID,
|
||||
requestID: existing.request.id,
|
||||
})
|
||||
yield* Deferred.fail(existing.deferred, new RejectedError())
|
||||
pending.delete(requestID)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const list = Effect.fn("Question.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: [Bus.node] })
|
||||
@@ -2,7 +2,6 @@ import { AIError, ToolFailure } from "@opencode-ai/ai"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { Permission } from "../permission.js"
|
||||
import { Question } from "../question.js"
|
||||
import { Integration } from "../integration.js"
|
||||
import { AgentNotFoundError, StepFailedError, UserInterruptedError } from "./error.js"
|
||||
import { SessionRunnerModel } from "./runner/model.js"
|
||||
@@ -37,7 +36,6 @@ export function toSessionError(cause: unknown): SessionError.Error {
|
||||
}
|
||||
}
|
||||
if (cause instanceof Permission.BlockedError) return { type: "permission.rejected", message: cause.message }
|
||||
if (cause instanceof Question.RejectedError) return { type: "aborted", message: cause.message }
|
||||
if (cause instanceof ToolFailure || cause instanceof Tool.Error) {
|
||||
if (cause.error === undefined) return { type: "tool.execution", message: cause.message }
|
||||
// The canonical error is the sole model-visible representation, so a cause
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ToolFailure } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { Form } from "../../form.js"
|
||||
import { Permission } from "../../permission.js"
|
||||
import { Question } from "../../question.js"
|
||||
import { Question } from "@opencode-ai/schema/question"
|
||||
|
||||
export const name = "question"
|
||||
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { Question } from "@opencode-ai/core/question"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const questions = AppNodeBuilder.build(LayerNode.group([Bus.node, Question.node]))
|
||||
const it = testEffect(questions)
|
||||
|
||||
const sessionID = Session.ID.make("ses_question_test")
|
||||
const question: Question.Info = {
|
||||
question: "Which option?",
|
||||
header: "Option",
|
||||
options: [{ label: "One", description: "First option" }],
|
||||
}
|
||||
|
||||
const waitForAsk = Effect.fn("QuestionTest.waitForAsk")(function* (
|
||||
service: Question.Interface,
|
||||
input: Question.AskInput,
|
||||
) {
|
||||
const bus = yield* Bus.Service
|
||||
const asked = yield* Deferred.make<Question.Request>()
|
||||
const unsubscribe = yield* bus.listen((event) =>
|
||||
event.type === Question.Event.Asked.type
|
||||
? Deferred.succeed(asked, event.data as Question.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("Question", () => {
|
||||
it.effect("publishes lifecycle events and settles a pending reply", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Question.Service
|
||||
const bus = yield* Bus.Service
|
||||
const published: Event.Payload[] = []
|
||||
const unsubscribe = yield* bus.listen((event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.type.startsWith("question.")) 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([
|
||||
[Question.Event.Asked.type, request],
|
||||
[Question.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* Question.Service
|
||||
const bus = yield* Bus.Service
|
||||
const published: Event.Payload[] = []
|
||||
const unsubscribe = yield* bus.listen((event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.type === Question.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("Question.RejectedError")
|
||||
expect(published.map((event) => event.data)).toEqual([{ sessionID, requestID: request.id }])
|
||||
|
||||
const unknown = Question.ID.ascending("que_unknown")
|
||||
expect(yield* service.reply({ requestID: unknown, answers: [] }).pipe(Effect.flip)).toEqual(
|
||||
new Question.NotFoundError({ requestID: unknown }),
|
||||
)
|
||||
expect(yield* service.reject(unknown).pipe(Effect.flip)).toEqual(
|
||||
new Question.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), Question.Service)
|
||||
const second = Context.get(yield* Layer.buildWithScope(Layer.fresh(questions), secondScope), Question.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 Question.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("Question.RejectedError")
|
||||
yield* Scope.close(secondScope, Exit.void)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -36,6 +36,16 @@ export function CaptureContextMenu({ identifier, deepLink, issueLink, children }
|
||||
setPosition(undefined)
|
||||
}
|
||||
|
||||
const copyImage = () => {
|
||||
const canvas = targetRef.current?.querySelector("canvas")
|
||||
if (!canvas) return
|
||||
const image = new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("Failed to encode capture image"))), "image/png")
|
||||
})
|
||||
void navigator.clipboard.write([new ClipboardItem({ "image/png": image })])
|
||||
setPosition(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={targetRef}
|
||||
@@ -58,13 +68,16 @@ export function CaptureContextMenu({ identifier, deepLink, issueLink, children }
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
left: Math.max(8, Math.min(position.x, window.innerWidth - 176)),
|
||||
top: Math.max(8, Math.min(position.y, window.innerHeight - 84)),
|
||||
top: Math.max(8, Math.min(position.y, window.innerHeight - 160)),
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Escape") setPosition(undefined)
|
||||
}}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<button type="button" role="menuitem" onClick={copyImage}>
|
||||
Copy image
|
||||
</button>
|
||||
<button type="button" role="menuitem" onClick={() => copy(identifier)}>
|
||||
Copy ID
|
||||
</button>
|
||||
|
||||
@@ -10438,359 +10438,6 @@
|
||||
"summary": "Read shell output"
|
||||
}
|
||||
},
|
||||
"/api/question/request": {
|
||||
"get": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.question.request.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Info"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["location", "data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve pending question requests for a location.",
|
||||
"summary": "List pending question requests"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question": {
|
||||
"get": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve pending question requests owned by a session.",
|
||||
"summary": "List session question requests"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question/{requestID}/reply": {
|
||||
"post": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.reply",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "requestID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "<No Content>"
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError | QuestionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QuestionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Answer a pending question request owned by a session.",
|
||||
"summary": "Reply to pending question request",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Question.Reply"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question/{requestID}/reject": {
|
||||
"post": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.reject",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "requestID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "<No Content>"
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError | QuestionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QuestionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Reject a pending question request owned by a session.",
|
||||
"summary": "Reject pending question request"
|
||||
}
|
||||
},
|
||||
"/api/reference": {
|
||||
"get": {
|
||||
"tags": ["reference"],
|
||||
@@ -21720,237 +21367,6 @@
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Option": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Display text (1-5 words, concise)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of choice"
|
||||
}
|
||||
},
|
||||
"required": ["label", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "Complete question"
|
||||
},
|
||||
"header": {
|
||||
"type": "string",
|
||||
"description": "Very short label (max 30 chars)"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Option"
|
||||
},
|
||||
"description": "Available choices"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"custom": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["question", "header", "options"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Tool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messageID": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["messageID", "id"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"question.asked": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.asked"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Info"
|
||||
},
|
||||
"description": "Questions to ask"
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/components/schemas/Question.Tool"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "questions"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Answer": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"question.replied": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.replied"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Answer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "requestID", "answers"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"question.rejected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.rejected"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "requestID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Form.Metadata1": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -23421,15 +22837,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/shell.deleted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.asked"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.replied"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.rejected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/form.created"
|
||||
},
|
||||
@@ -23611,70 +23018,6 @@
|
||||
"required": ["_tag", "id", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Request": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Info"
|
||||
},
|
||||
"description": "Questions to ask"
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/components/schemas/Question.Tool"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "questions"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Reply": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Answer"
|
||||
},
|
||||
"description": "User answers in order of questions (each answer is an array of selected labels)"
|
||||
}
|
||||
},
|
||||
"required": ["answers"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"QuestionNotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["QuestionNotFoundError"]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "requestID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Reference.LocalSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24948,10 +24291,6 @@
|
||||
"name": "shell",
|
||||
"description": "Experimental location-scoped shell command routes."
|
||||
},
|
||||
{
|
||||
"name": "question",
|
||||
"description": "Experimental session question routes."
|
||||
},
|
||||
{
|
||||
"name": "reference",
|
||||
"description": "Location-scoped project references."
|
||||
|
||||
@@ -20,7 +20,6 @@ import { ServerGroup } from "./groups/server.js"
|
||||
import { DebugGroup } from "./groups/debug.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"
|
||||
@@ -71,9 +70,7 @@ type MixedMiddlewareGroups<
|
||||
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,
|
||||
@@ -170,7 +167,6 @@ const makeApiFromGroup = <
|
||||
.add(eventGroup)
|
||||
.add(PtyGroup.middleware(locationMiddleware))
|
||||
.add(ShellGroup.middleware(locationMiddleware))
|
||||
.add(makeQuestionGroup(locationMiddleware, sessionLocationMiddleware))
|
||||
.add(ReferenceGroup.middleware(locationMiddleware))
|
||||
.add(WorktreeGroup)
|
||||
.add(VcsGroup.middleware(locationMiddleware))
|
||||
|
||||
@@ -57,7 +57,6 @@ export const groupNames = {
|
||||
"server.pty": "pty",
|
||||
"server.shell": "shell",
|
||||
"server.mcp": "mcp",
|
||||
"server.question": "question",
|
||||
"server.reference": "reference",
|
||||
"server.project": "project",
|
||||
"server.worktree": "worktree",
|
||||
|
||||
@@ -141,15 +141,6 @@ export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionN
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFoundError>()(
|
||||
"QuestionNotFoundError",
|
||||
{
|
||||
requestID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class FormNotFoundError extends Schema.TaggedErrorClass<FormNotFoundError>()(
|
||||
"FormNotFoundError",
|
||||
{
|
||||
|
||||
@@ -1,82 +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: "question", 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: "question", description: "Experimental session question routes." }))
|
||||
@@ -21,7 +21,6 @@ import { Plugin } from "./plugin.js"
|
||||
import { Project } from "./project.js"
|
||||
import { Worktree } from "./worktree.js"
|
||||
import { Pty } from "./pty.js"
|
||||
import { Question } from "./question.js"
|
||||
import { Reference } from "./reference.js"
|
||||
import { ServerEvent } from "./server-event.js"
|
||||
import { Shell } from "./shell.js"
|
||||
@@ -56,7 +55,6 @@ const featureDefinitions = Event.inventory(
|
||||
...Skill.Event.Definitions,
|
||||
...Pty.Event.Definitions,
|
||||
...Shell.Event.Definitions,
|
||||
...Question.Event.Definitions,
|
||||
...Form.Event.Definitions,
|
||||
...WebSearch.Event.Definitions,
|
||||
)
|
||||
|
||||
@@ -2,28 +2,11 @@ export * as Question from "./question.js"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import { optional } from "./schema.js"
|
||||
import { ephemeral, 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("Question.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({
|
||||
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: "Question.Option" })
|
||||
export interface Option extends Schema.Schema.Type<typeof Option> {}
|
||||
})
|
||||
|
||||
const base = {
|
||||
question: Schema.String.annotate({ description: "Complete question" }),
|
||||
@@ -32,55 +15,8 @@ const base = {
|
||||
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: "Question.Info" })
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
|
||||
export const Prompt = Schema.Struct(base).annotate({ identifier: "Question.Prompt" })
|
||||
export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
|
||||
|
||||
export const Tool = Schema.Struct({
|
||||
messageID: Schema.String,
|
||||
id: Schema.String,
|
||||
}).annotate({ identifier: "Question.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: "Question.Request" })
|
||||
export interface Request extends Schema.Schema.Type<typeof Request> {}
|
||||
|
||||
export const Answer = Schema.Array(Schema.String).annotate({ identifier: "Question.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: "Question.Reply" })
|
||||
export interface Reply extends Schema.Schema.Type<typeof Reply> {}
|
||||
|
||||
const Asked = ephemeral({ type: "question.asked", schema: Request.fields })
|
||||
const Replied = ephemeral({
|
||||
type: "question.replied",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
requestID: ID,
|
||||
answers: Schema.Array(Answer),
|
||||
},
|
||||
})
|
||||
const Rejected = ephemeral({
|
||||
type: "question.rejected",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
requestID: ID,
|
||||
},
|
||||
})
|
||||
export const Event = { Asked, Replied, Rejected, Definitions: inventory(Asked, Replied, Rejected) }
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Model } from "../src/model.js"
|
||||
import { Project } from "../src/project.js"
|
||||
import { Provider } from "../src/provider.js"
|
||||
import { Pty } from "../src/pty.js"
|
||||
import { Question } from "../src/question.js"
|
||||
import { Session } from "../src/session.js"
|
||||
import { SessionMessage } from "../src/session-message.js"
|
||||
import { SessionInbox } from "../src/session-inbox.js"
|
||||
@@ -132,7 +131,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_")
|
||||
})
|
||||
|
||||
|
||||
@@ -41,6 +41,9 @@ describe("public event manifest", () => {
|
||||
expect(EventManifest.Server.get("session.created")).toBe(SessionEvent.Created)
|
||||
expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted)
|
||||
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
|
||||
expect(EventManifest.Server.has("question.asked")).toBe(false)
|
||||
expect(EventManifest.Server.has("question.replied")).toBe(false)
|
||||
expect(EventManifest.Server.has("question.rejected")).toBe(false)
|
||||
expect(Agent.Event.Updated.durable).toBeUndefined()
|
||||
expect(EventManifest.Durable.has("agent.updated")).toBe(false)
|
||||
})
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ServerHandler } from "./handlers/server"
|
||||
import { DebugHandler } from "./handlers/debug"
|
||||
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"
|
||||
@@ -57,7 +56,6 @@ export const handlers = Layer.mergeAll(
|
||||
EventHandler.pipe(Layer.provide(EventFeed.layer)),
|
||||
PtyHandler,
|
||||
ShellHandler,
|
||||
QuestionHandler,
|
||||
ReferenceHandler,
|
||||
WorktreeHandler,
|
||||
VcsHandler,
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Question } 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: Question.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: Question.Request["sessionID"],
|
||||
requestID: Question.ID,
|
||||
use: (question: Question.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const question = yield* Question.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* () {
|
||||
const question = yield* Question.Service
|
||||
return yield* response(question.list())
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.question.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const question = yield* Question.Service
|
||||
const requests = yield* question.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("Question.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("Question.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -5,11 +5,11 @@ describe("inlineCodeKind", () => {
|
||||
test("leaves code expressions as normal inline code", () => {
|
||||
expect(
|
||||
inlineCodeKind(
|
||||
`case "question.asked": ... input.setStore("question", question.sessionID, [question]) / splice/insert`,
|
||||
`case "form.created": ... input.setStore("form", form.sessionID, [form]) / splice/insert`,
|
||||
),
|
||||
).toBeUndefined()
|
||||
expect(inlineCodeKind(`<SessionQuestionDock request={request} ... />`)).toBeUndefined()
|
||||
expect(inlineCodeKind(`from sync.data.question + sync.data.session.`)).toBeUndefined()
|
||||
expect(inlineCodeKind(`from sync.data.form + sync.data.session.`)).toBeUndefined()
|
||||
expect(inlineCodeKind(`@opencode-ai/app <StatusPopover />)`)).toBeUndefined()
|
||||
expect(inlineCodeKind(`sync.data.session`)).toBeUndefined()
|
||||
expect(inlineCodeKind(`window.api`)).toBeUndefined()
|
||||
|
||||
@@ -110,10 +110,19 @@ function createStorage(root: string, channel: string) {
|
||||
},
|
||||
}
|
||||
|
||||
const watcher = watch(directory, () => entries.forEach((entry) => entry.reload()))
|
||||
let reload: ReturnType<typeof setTimeout> | undefined
|
||||
const watcher = watch(directory, () => {
|
||||
clearTimeout(reload)
|
||||
// Atomic writes notify for the temporary file before its final rename, and some
|
||||
// platforms coalesce the rename event. Reload after the event burst has settled.
|
||||
reload = setTimeout(() => entries.forEach((entry) => entry.reload()), 50)
|
||||
})
|
||||
return {
|
||||
storage,
|
||||
close: () => watcher.close(),
|
||||
close: () => {
|
||||
clearTimeout(reload)
|
||||
watcher.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ export default Plugin.define({
|
||||
const errored = new Set<string>()
|
||||
const terminal = new Set<string>()
|
||||
const forms = new Set<string>()
|
||||
const questions = new Set<string>()
|
||||
const permissions = new Set<string>()
|
||||
|
||||
const started = (sessionID: string) => {
|
||||
@@ -50,13 +49,6 @@ export default Plugin.define({
|
||||
}),
|
||||
context.data.on("form.replied", (event) => forms.delete(event.data.id)),
|
||||
context.data.on("form.cancelled", (event) => forms.delete(event.data.id)),
|
||||
context.data.on("question.asked", (event) => {
|
||||
if (questions.has(event.data.id)) return
|
||||
questions.add(event.data.id)
|
||||
notify(context, event.data.sessionID, "Question needs input", "question")
|
||||
}),
|
||||
context.data.on("question.replied", (event) => questions.delete(event.data.requestID)),
|
||||
context.data.on("question.rejected", (event) => questions.delete(event.data.requestID)),
|
||||
context.data.on("permission.asked", (event) => {
|
||||
if (permissions.has(event.data.id)) return
|
||||
permissions.add(event.data.id)
|
||||
|
||||
@@ -1530,9 +1530,13 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
|
||||
return name === "shell" || name === "subagent"
|
||||
})
|
||||
if (!current || !part) return
|
||||
return `${current.id}:${part.id}`
|
||||
return { key: `${current.id}:${part.id}`, started: part.time.ran ?? part.time.created }
|
||||
})
|
||||
const visible = createDelayedPresence(running, BACKGROUND_TOOL_HINT_DELAY)
|
||||
const visible = createDelayedPresence(
|
||||
running,
|
||||
(tool) => Math.max(0, BACKGROUND_TOOL_HINT_DELAY - (Date.now() - tool.started)),
|
||||
(previous, next) => previous.key === next.key && previous.started === next.started,
|
||||
)
|
||||
return (
|
||||
<Show when={visible() && shortcut()}>
|
||||
{(value) => (
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal, onCleanup, type Accessor } from "solid-js"
|
||||
|
||||
export function createDelayedPresence<T>(source: Accessor<T | undefined>, delay: number) {
|
||||
export function createDelayedPresence<T>(
|
||||
source: Accessor<T | undefined>,
|
||||
delay: number | ((value: T) => number),
|
||||
equals?: (previous: T, next: T) => boolean,
|
||||
) {
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
const value = equals
|
||||
? createMemo(source, undefined, {
|
||||
equals: (previous, next) => {
|
||||
if (previous === undefined || next === undefined) return previous === next
|
||||
return equals(previous, next)
|
||||
},
|
||||
})
|
||||
: source
|
||||
|
||||
createEffect(() => {
|
||||
const value = source()
|
||||
const current = value()
|
||||
setVisible(false)
|
||||
if (value === undefined) return
|
||||
if (current === undefined) return
|
||||
|
||||
const timer = setTimeout(() => setVisible(true), delay)
|
||||
const remaining = typeof delay === "function" ? delay(current) : delay
|
||||
if (remaining <= 0) {
|
||||
setVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => setVisible(true), remaining)
|
||||
onCleanup(() => clearTimeout(timer))
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import Notifications from "../../../../src/feature-plugins/system/notifications"
|
||||
import type { OpenCodeEvent, PermissionAsked, QuestionAsked } from "@opencode-ai/client"
|
||||
import type { OpenCodeEvent, PermissionAsked } from "@opencode-ai/client"
|
||||
import type { AttentionNotifyOptions, Context } from "@opencode-ai/plugin/tui/context"
|
||||
|
||||
type Session = { id: string; title: string; parentID?: string }
|
||||
@@ -58,14 +58,6 @@ async function setup() {
|
||||
}
|
||||
}
|
||||
|
||||
function question(id: string, sessionID = "session"): QuestionAsked["data"] {
|
||||
return {
|
||||
id,
|
||||
sessionID,
|
||||
questions: [],
|
||||
}
|
||||
}
|
||||
|
||||
function form(id: string, sessionID = "session"): Extract<OpenCodeEvent, { type: "form.created" }>["data"]["form"] {
|
||||
return {
|
||||
id,
|
||||
@@ -123,13 +115,6 @@ function executionFailed(id: string, sessionID = "session"): OpenCodeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
const questionNotification: AttentionNotifyOptions = {
|
||||
title: "Demo session",
|
||||
message: "Question needs input",
|
||||
notification: { when: "blurred" },
|
||||
sound: { name: "question", when: "always" },
|
||||
}
|
||||
|
||||
const formNotification: AttentionNotifyOptions = {
|
||||
title: "Input requested",
|
||||
message: "Input needs response",
|
||||
@@ -155,7 +140,7 @@ const permissionNotification: AttentionNotifyOptions = {
|
||||
}
|
||||
|
||||
describe("internal notifications TUI plugin", () => {
|
||||
test("notifies for form, question, and permission requests with blurred notifications and always-on sounds", async () => {
|
||||
test("notifies for form and permission requests with blurred notifications and always-on sounds", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({
|
||||
@@ -164,10 +149,9 @@ describe("internal notifications TUI plugin", () => {
|
||||
type: "form.created",
|
||||
data: { form: { ...form("form-1"), title: "Confirm deployment" } },
|
||||
})
|
||||
harness.emit({ id: "event-2", created: 0, type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-3", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
|
||||
expect(harness.notifications).toEqual([titledFormNotification, questionNotification, permissionNotification])
|
||||
expect(harness.notifications).toEqual([titledFormNotification, permissionNotification])
|
||||
})
|
||||
|
||||
test("notifies for global forms once the TUI can render them", async () => {
|
||||
@@ -183,7 +167,7 @@ describe("internal notifications TUI plugin", () => {
|
||||
expect(harness.notifications).toEqual([globalFormNotification])
|
||||
})
|
||||
|
||||
test("dedupes pending forms, questions, and permissions until they are resolved", async () => {
|
||||
test("dedupes pending forms and permissions until they are resolved", async () => {
|
||||
const harness = await setup()
|
||||
|
||||
harness.emit({ id: "event-1", created: 0, type: "form.created", data: { form: form("form-1") } })
|
||||
@@ -196,16 +180,6 @@ describe("internal notifications TUI plugin", () => {
|
||||
})
|
||||
harness.emit({ id: "event-4", created: 0, type: "form.created", data: { form: form("form-1") } })
|
||||
|
||||
harness.emit({ id: "event-5", created: 0, type: "question.asked", data: question("question-1") })
|
||||
harness.emit({ id: "event-6", created: 0, type: "question.asked", data: question("question-1") })
|
||||
harness.emit({
|
||||
id: "event-7",
|
||||
created: 0,
|
||||
type: "question.replied",
|
||||
data: { sessionID: "session", requestID: "question-1", answers: [] },
|
||||
})
|
||||
harness.emit({ id: "event-8", created: 0, type: "question.asked", data: question("question-1") })
|
||||
|
||||
harness.emit({ id: "event-9", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({ id: "event-10", created: 0, type: "permission.asked", data: permission("permission-1") })
|
||||
harness.emit({
|
||||
@@ -219,8 +193,6 @@ describe("internal notifications TUI plugin", () => {
|
||||
expect(harness.notifications).toEqual([
|
||||
formNotification,
|
||||
formNotification,
|
||||
questionNotification,
|
||||
questionNotification,
|
||||
permissionNotification,
|
||||
permissionNotification,
|
||||
])
|
||||
|
||||
@@ -18,10 +18,10 @@ import { TestTuiContexts } from "../fixture/tui-environment"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { createTuiResolvedConfig } from "../fixture/tui-runtime"
|
||||
|
||||
async function wait(fn: () => boolean | Promise<boolean>, timeout = 2_000) {
|
||||
async function wait(fn: () => boolean | Promise<boolean>, timeout = 2_000, label = "condition") {
|
||||
const start = Date.now()
|
||||
while (!(await fn())) {
|
||||
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
||||
if (Date.now() - start > timeout) throw new Error(`timed out waiting for ${label}`)
|
||||
await Bun.sleep(10)
|
||||
}
|
||||
}
|
||||
@@ -141,15 +141,7 @@ async function renderSessionTabs(
|
||||
locations,
|
||||
vcsLocations,
|
||||
state,
|
||||
emit: (event: OpenCodeEvent) =>
|
||||
new Promise<void>((resolve) => {
|
||||
const off = client.event.listen((delivered) => {
|
||||
if (delivered.details.id !== event.id) return
|
||||
off()
|
||||
resolve()
|
||||
})
|
||||
events.emit({ ...event, location: { directory } })
|
||||
}),
|
||||
emit: (event: OpenCodeEvent) => events.emit({ ...event, location: { directory } }),
|
||||
focus: () => app.renderer.emit("focus"),
|
||||
blur: () => app.renderer.emit("blur"),
|
||||
flush: () => storage.flush(),
|
||||
@@ -222,31 +214,43 @@ test("stores session tabs for the current working directory by default", async (
|
||||
})
|
||||
|
||||
test("only the foreground TUI mutates unread state", async () => {
|
||||
await using temporary = await tmpdir()
|
||||
let foreground: Awaited<ReturnType<typeof renderSessionTabs>> | undefined
|
||||
let background: Awaited<ReturnType<typeof renderSessionTabs>> | undefined
|
||||
|
||||
try {
|
||||
foreground = await renderSessionTabs("first", { persisted: ["first", "second"] })
|
||||
background = await renderSessionTabs("first", { persisted: ["first", "second"] })
|
||||
foreground = await renderSessionTabs("first", { state: temporary.path, persisted: ["first", "second"] })
|
||||
background = await renderSessionTabs("second", { state: temporary.path })
|
||||
foreground.focus()
|
||||
background.blur()
|
||||
await wait(() => foreground?.tabs.tabs().length === 2 && background?.tabs.tabs().length === 2, 2_000, "shared tabs")
|
||||
|
||||
const firstDone = executionSucceeded("first")
|
||||
await Promise.all([foreground.emit(firstDone), background.emit(firstDone)])
|
||||
foreground.emit(firstDone)
|
||||
background.emit(firstDone)
|
||||
await Promise.all([foreground.flush(), background.flush()])
|
||||
expect(foreground.tabs.status("first").unread).toBeUndefined()
|
||||
expect(background.tabs.status("first").unread).toBeUndefined()
|
||||
|
||||
const secondDone = executionSucceeded("second")
|
||||
await Promise.all([foreground.emit(secondDone), background.emit(secondDone)])
|
||||
await Promise.all([foreground.flush(), background.flush()])
|
||||
expect(foreground.tabs.status("second").unread).toBe("activity")
|
||||
expect(background.tabs.status("second").unread).toBeUndefined()
|
||||
foreground.emit(secondDone)
|
||||
background.emit(secondDone)
|
||||
await wait(
|
||||
() =>
|
||||
foreground?.tabs.status("second").unread === "activity" &&
|
||||
background?.tabs.status("second").unread === "activity",
|
||||
10_000,
|
||||
"shared unread activity",
|
||||
)
|
||||
|
||||
foreground.tabs.select("second")
|
||||
await foreground.flush()
|
||||
expect(foreground.tabs.status("second").unread).toBeUndefined()
|
||||
expect(background.tabs.status("second").unread).toBeUndefined()
|
||||
await wait(
|
||||
() =>
|
||||
foreground?.tabs.status("second").unread === undefined &&
|
||||
background?.tabs.status("second").unread === undefined,
|
||||
10_000,
|
||||
"shared unread clearing",
|
||||
)
|
||||
} finally {
|
||||
if (foreground) await foreground.destroy()
|
||||
if (background) await background.destroy()
|
||||
|
||||
@@ -53,3 +53,62 @@ test("cancels the delay when the value disappears or the owner is disposed", asy
|
||||
jest.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
test("uses the remaining delay for the current value", async () => {
|
||||
jest.useFakeTimers()
|
||||
const scope = createRoot((dispose) => {
|
||||
const [value, setValue] = createSignal<{ age: number }>()
|
||||
return {
|
||||
dispose,
|
||||
setValue,
|
||||
visible: createDelayedPresence(value, (current) => Math.max(0, 1_000 - current.age)),
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
scope.setValue({ age: 400 })
|
||||
await Promise.resolve()
|
||||
jest.advanceTimersByTime(599)
|
||||
expect(scope.visible()).toBe(false)
|
||||
jest.advanceTimersByTime(1)
|
||||
expect(scope.visible()).toBe(true)
|
||||
|
||||
scope.setValue({ age: 1_000 })
|
||||
await Promise.resolve()
|
||||
expect(scope.visible()).toBe(true)
|
||||
} finally {
|
||||
scope.dispose()
|
||||
jest.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
test("does not restart the delay for an equivalent value", async () => {
|
||||
jest.useFakeTimers()
|
||||
const scope = createRoot((dispose) => {
|
||||
const [value, setValue] = createSignal<{ id: string }>()
|
||||
return {
|
||||
dispose,
|
||||
setValue,
|
||||
visible: createDelayedPresence(value, 1_000, (previous, next) => previous.id === next.id),
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
scope.setValue({ id: "first" })
|
||||
await Promise.resolve()
|
||||
jest.advanceTimersByTime(500)
|
||||
scope.setValue({ id: "first" })
|
||||
await Promise.resolve()
|
||||
jest.advanceTimersByTime(499)
|
||||
expect(scope.visible()).toBe(false)
|
||||
jest.advanceTimersByTime(1)
|
||||
expect(scope.visible()).toBe(true)
|
||||
|
||||
scope.setValue({ id: "second" })
|
||||
await Promise.resolve()
|
||||
expect(scope.visible()).toBe(false)
|
||||
} finally {
|
||||
scope.dispose()
|
||||
jest.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10438,359 +10438,6 @@
|
||||
"summary": "Read shell output"
|
||||
}
|
||||
},
|
||||
"/api/question/request": {
|
||||
"get": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.question.request.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Info"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["location", "data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve pending question requests for a location.",
|
||||
"summary": "List pending question requests"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question": {
|
||||
"get": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve pending question requests owned by a session.",
|
||||
"summary": "List session question requests"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question/{requestID}/reply": {
|
||||
"post": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.reply",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "requestID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "<No Content>"
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError | QuestionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QuestionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Answer a pending question request owned by a session.",
|
||||
"summary": "Reply to pending question request",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Question.Reply"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question/{requestID}/reject": {
|
||||
"post": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.reject",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "requestID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "<No Content>"
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError | QuestionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QuestionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Reject a pending question request owned by a session.",
|
||||
"summary": "Reject pending question request"
|
||||
}
|
||||
},
|
||||
"/api/reference": {
|
||||
"get": {
|
||||
"tags": ["reference"],
|
||||
@@ -21720,237 +21367,6 @@
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Option": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Display text (1-5 words, concise)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of choice"
|
||||
}
|
||||
},
|
||||
"required": ["label", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "Complete question"
|
||||
},
|
||||
"header": {
|
||||
"type": "string",
|
||||
"description": "Very short label (max 30 chars)"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Option"
|
||||
},
|
||||
"description": "Available choices"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"custom": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["question", "header", "options"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Tool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messageID": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["messageID", "id"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"question.asked": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.asked"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Info"
|
||||
},
|
||||
"description": "Questions to ask"
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/components/schemas/Question.Tool"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "questions"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Answer": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"question.replied": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.replied"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Answer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "requestID", "answers"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"question.rejected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.rejected"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "requestID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Form.Metadata1": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -23421,15 +22837,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/shell.deleted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.asked"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.replied"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.rejected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/form.created"
|
||||
},
|
||||
@@ -23611,70 +23018,6 @@
|
||||
"required": ["_tag", "id", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Request": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Info"
|
||||
},
|
||||
"description": "Questions to ask"
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/components/schemas/Question.Tool"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "questions"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Reply": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Answer"
|
||||
},
|
||||
"description": "User answers in order of questions (each answer is an array of selected labels)"
|
||||
}
|
||||
},
|
||||
"required": ["answers"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"QuestionNotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["QuestionNotFoundError"]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "requestID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Reference.LocalSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24948,10 +24291,6 @@
|
||||
"name": "shell",
|
||||
"description": "Experimental location-scoped shell command routes."
|
||||
},
|
||||
{
|
||||
"name": "question",
|
||||
"description": "Experimental session question routes."
|
||||
},
|
||||
{
|
||||
"name": "reference",
|
||||
"description": "Location-scoped project references."
|
||||
|
||||
@@ -10438,359 +10438,6 @@
|
||||
"summary": "Read shell output"
|
||||
}
|
||||
},
|
||||
"/api/question/request": {
|
||||
"get": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.question.request.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workspace": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Info"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["location", "data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve pending question requests for a location.",
|
||||
"summary": "List pending question requests"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question": {
|
||||
"get": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Retrieve pending question requests owned by a session.",
|
||||
"summary": "List session question requests"
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question/{requestID}/reply": {
|
||||
"post": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.reply",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "requestID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "<No Content>"
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError | QuestionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QuestionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Answer a pending question request owned by a session.",
|
||||
"summary": "Reply to pending question request",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Question.Reply"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/{sessionID}/question/{requestID}/reject": {
|
||||
"post": {
|
||||
"tags": ["question"],
|
||||
"operationId": "v2.session.question.reject",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "sessionID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "requestID",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "<No Content>"
|
||||
},
|
||||
"400": {
|
||||
"description": "InvalidRequestError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidRequestError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "UnauthorizedError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UnauthorizedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "SessionNotFoundError | QuestionNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/QuestionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionNotFoundError"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Reject a pending question request owned by a session.",
|
||||
"summary": "Reject pending question request"
|
||||
}
|
||||
},
|
||||
"/api/reference": {
|
||||
"get": {
|
||||
"tags": ["reference"],
|
||||
@@ -21720,237 +21367,6 @@
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Option": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Display text (1-5 words, concise)"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of choice"
|
||||
}
|
||||
},
|
||||
"required": ["label", "description"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "Complete question"
|
||||
},
|
||||
"header": {
|
||||
"type": "string",
|
||||
"description": "Very short label (max 30 chars)"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Option"
|
||||
},
|
||||
"description": "Available choices"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"custom": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["question", "header", "options"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Tool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messageID": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["messageID", "id"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"question.asked": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.asked"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Info"
|
||||
},
|
||||
"description": "Questions to ask"
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/components/schemas/Question.Tool"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "questions"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Answer": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"question.replied": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.replied"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Answer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "requestID", "answers"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"question.rejected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^evt_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"created": {
|
||||
"type": "number"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["question.rejected"]
|
||||
},
|
||||
"location": {
|
||||
"$ref": "#/components/schemas/Location.Ref"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["sessionID", "requestID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "created", "type", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Form.Metadata1": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -23421,15 +22837,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/shell.deleted"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.asked"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.replied"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/question.rejected"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/form.created"
|
||||
},
|
||||
@@ -23611,70 +23018,6 @@
|
||||
"required": ["_tag", "id", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Request": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^que"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"allOf": [
|
||||
{
|
||||
"pattern": "^ses"
|
||||
}
|
||||
]
|
||||
},
|
||||
"questions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Info"
|
||||
},
|
||||
"description": "Questions to ask"
|
||||
},
|
||||
"tool": {
|
||||
"$ref": "#/components/schemas/Question.Tool"
|
||||
}
|
||||
},
|
||||
"required": ["id", "sessionID", "questions"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Question.Reply": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"answers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Question.Answer"
|
||||
},
|
||||
"description": "User answers in order of questions (each answer is an array of selected labels)"
|
||||
}
|
||||
},
|
||||
"required": ["answers"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"QuestionNotFoundError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["QuestionNotFoundError"]
|
||||
},
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "requestID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Reference.LocalSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24948,10 +24291,6 @@
|
||||
"name": "shell",
|
||||
"description": "Experimental location-scoped shell command routes."
|
||||
},
|
||||
{
|
||||
"name": "question",
|
||||
"description": "Experimental session question routes."
|
||||
},
|
||||
{
|
||||
"name": "reference",
|
||||
"description": "Location-scoped project references."
|
||||
|
||||
Reference in New Issue
Block a user