From 9872fb8a543a1234e0e6564e8d09cd5806e987a8 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:38:32 +0000 Subject: [PATCH] refactor(api): remove legacy question service (#42562) Co-authored-by: Filip Hejmowski Co-authored-by: neriousy Co-authored-by: Kit Langton --- .changeset/quiet-forms-replace-questions.md | 8 + .../app/src/context/global-sync/bootstrap.ts | 37 - .../src/context/global-sync/child-store.ts | 1 - .../context/global-sync/event-reducer.test.ts | 55 +- .../src/context/global-sync/event-reducer.ts | 42 -- .../context/global-sync/session-cache.test.ts | 7 +- .../src/context/global-sync/session-cache.ts | 4 +- packages/app/src/context/global-sync/types.ts | 4 - packages/app/src/context/server-session.ts | 39 +- packages/cli/test/run/noninteractive.test.ts | 4 - packages/client/src/effect/api/api.ts | 111 ++- .../client/src/effect/generated/client.ts | 164 ++--- .../client/src/promise/generated/client.ts | 58 -- .../client/src/promise/generated/types.ts | 84 --- packages/core/src/location-services.ts | 2 - packages/core/src/question.ts | 151 ---- packages/core/src/session/to-session-error.ts | 2 - packages/core/src/tool/plugin/question.ts | 2 +- packages/core/test/question.test.ts | 115 --- packages/protocol/openapi.json | 661 ------------------ packages/protocol/src/api.ts | 6 +- packages/protocol/src/client.ts | 1 - packages/protocol/src/errors.ts | 9 - packages/protocol/src/groups/question.ts | 82 --- packages/schema/src/event-manifest.ts | 2 - packages/schema/src/question.ts | 68 +- packages/schema/test/contract-hygiene.test.ts | 3 +- packages/schema/test/event-manifest.test.ts | 3 + packages/server/src/handlers.ts | 2 - packages/server/src/handlers/question.ts | 64 -- .../markdown-inline-code-kind.test.ts | 4 +- .../feature-plugins/system/notifications.ts | 8 - .../test/cli/cmd/tui/notifications.test.ts | 36 +- packages/www/openapi.json | 661 ------------------ packages/www/public/openapi.json | 661 ------------------ 35 files changed, 126 insertions(+), 3035 deletions(-) create mode 100644 .changeset/quiet-forms-replace-questions.md delete mode 100644 packages/core/src/question.ts delete mode 100644 packages/core/test/question.test.ts delete mode 100644 packages/protocol/src/groups/question.ts delete mode 100644 packages/server/src/handlers/question.ts diff --git a/.changeset/quiet-forms-replace-questions.md b/.changeset/quiet-forms-replace-questions.md new file mode 100644 index 00000000000..f672c0a2e3f --- /dev/null +++ b/.changeset/quiet-forms-replace-questions.md @@ -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. diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 82ed5a637c1..ac7a560bff4 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -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 @@ -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))), diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 2149857baa1..78e00ab9590 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -250,7 +250,6 @@ export function createChildStoreManager(input: { session_diff: {}, todo: {}, permission: {}, - question: {}, get mcp_ready() { return !mcpQuery.isLoading }, diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts index 804fa2e354e..a0865e57e8c 100644 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/packages/app/src/context/global-sync/event-reducer.test.ts @@ -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 = {}) => ({ status: "complete", @@ -75,7 +62,6 @@ const baseState = (input: Partial = {}) => 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", () => { diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 38c0d7be751..a346e2d0a29 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -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 diff --git a/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app/src/context/global-sync/session-cache.test.ts index 28e21056dd9..20bdc1eb1ee 100644 --- a/packages/app/src/context/global-sync/session-cache.test.ts +++ b/packages/app/src/context/global-sync/session-cache.test.ts @@ -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 part: Record permission: Record - question: Record form: Record part_text_accum_delta: Record } = { @@ -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 part: Record permission: Record - question: Record form: Record part_text_accum_delta: Record } = { @@ -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: {}, } diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts index 9187bd9a45c..5f51cfe1e49 100644 --- a/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app/src/context/global-sync/session-cache.ts @@ -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 part: Record permission: Record - question: Record form?: Record part_text_accum_delta: Record } @@ -38,7 +37,6 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable, todo: {} as Record, permission: {} as Record, - question: {} as Record, form: {} as Record, pending: {} as Record, input: {} as Record, @@ -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) - }), - ) - } } } diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index e45311e8b22..8da880f1f5a 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -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 () => { diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 7b5919d024b..ecbb63c2339 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -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 { 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 } -export type QuestionRequestListOperation = ( - input?: Endpoint22_0Input, -) => Effect.Effect - -export type Endpoint22_1Input = { readonly sessionID: Session.ID } -export type Endpoint22_1Output = ReadonlyArray -export type QuestionListOperation = (input: Endpoint22_1Input) => Effect.Effect - -export type Endpoint22_2Input = { - readonly sessionID: Session.ID - readonly requestID: Question.ID - readonly answers: ReadonlyArray -} -export type Endpoint22_2Output = void -export type QuestionReplyOperation = (input: Endpoint22_2Input) => Effect.Effect - -export type Endpoint22_3Input = { readonly sessionID: Session.ID; readonly requestID: Question.ID } -export type Endpoint22_3Output = void -export type QuestionRejectOperation = (input: Endpoint22_3Input) => Effect.Effect - -export interface QuestionApi { - readonly request: { readonly list: QuestionRequestListOperation } - readonly list: QuestionListOperation - readonly reply: QuestionReplyOperation - readonly reject: QuestionRejectOperation -} - -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 } -export type ReferenceListOperation = (input?: Endpoint23_0Input) => Effect.Effect +export type Endpoint22_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } +export type ReferenceListOperation = (input?: Endpoint22_0Input) => Effect.Effect export interface ReferenceApi { readonly list: ReferenceListOperation } -export type Endpoint24_0Input = { readonly projectID: Project.ID } -export type Endpoint24_0Output = Worktree.List -export type WorktreeListOperation = (input: Endpoint24_0Input) => Effect.Effect +export type Endpoint23_0Input = { readonly projectID: Project.ID } +export type Endpoint23_0Output = Worktree.List +export type WorktreeListOperation = (input: Endpoint23_0Input) => Effect.Effect -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 = (input: Endpoint24_1Input) => Effect.Effect +export type Endpoint23_1Output = Worktree.Info +export type WorktreeCreateOperation = (input: Endpoint23_1Input) => Effect.Effect -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 = (input: Endpoint24_2Input) => Effect.Effect +export type Endpoint23_2Output = void +export type WorktreeRemoveOperation = (input: Endpoint23_2Input) => Effect.Effect -export type Endpoint24_3Input = { readonly projectID: Project.ID } -export type Endpoint24_3Output = void -export type WorktreeRefreshOperation = (input: Endpoint24_3Input) => Effect.Effect +export type Endpoint23_3Input = { readonly projectID: Project.ID } +export type Endpoint23_3Output = void +export type WorktreeRefreshOperation = (input: Endpoint23_3Input) => Effect.Effect export interface WorktreeApi { readonly list: WorktreeListOperation @@ -1574,25 +1542,25 @@ export interface WorktreeApi { readonly refresh: WorktreeRefreshOperation } -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 = (input?: Endpoint25_0Input) => Effect.Effect +export type Endpoint24_0Output = { readonly location: Location.Info; readonly data: Vcs.Info } +export type VcsGetOperation = (input?: Endpoint24_0Input) => Effect.Effect -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 } -export type VcsStatusOperation = (input?: Endpoint25_1Input) => Effect.Effect +export type Endpoint24_1Output = { readonly location: Location.Info; readonly data: ReadonlyArray } +export type VcsStatusOperation = (input?: Endpoint24_1Input) => Effect.Effect -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 } -export type VcsDiffOperation = (input: Endpoint25_2Input) => Effect.Effect +export type Endpoint24_2Output = { readonly location: Location.Info; readonly data: ReadonlyArray } +export type VcsDiffOperation = (input: Endpoint24_2Input) => Effect.Effect export interface VcsApi { readonly get: VcsGetOperation @@ -1600,20 +1568,20 @@ export interface VcsApi { readonly diff: VcsDiffOperation } -export type Endpoint26_0Output = ReadonlyArray -export type DebugLocationListOperation = () => Effect.Effect +export type Endpoint25_0Output = ReadonlyArray +export type DebugLocationListOperation = () => Effect.Effect -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 = (input?: Endpoint26_1Input) => Effect.Effect +export type Endpoint25_1Output = void +export type DebugLocationEvictOperation = (input?: Endpoint25_1Input) => Effect.Effect export interface DebugApi { readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } } -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 = () => Effect.Effect +export type MigrationV1StatusOperation = () => Effect.Effect export interface MigrationApi { readonly v1: { readonly status: MigrationV1StatusOperation } } -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 } -export type WebsearchProvidersOperation = (input?: Endpoint28_0Input) => Effect.Effect +export type Endpoint27_0Output = { readonly location: Location.Info; readonly data: ReadonlyArray } +export type WebsearchProvidersOperation = (input?: Endpoint27_0Input) => Effect.Effect -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 = (input: Endpoint28_1Input) => Effect.Effect +export type Endpoint27_1Output = { readonly location: Location.Info; readonly data: WebSearch.Response } +export type WebsearchQueryOperation = (input: Endpoint27_1Input) => Effect.Effect export interface WebsearchApi { readonly providers: WebsearchProvidersOperation readonly query: WebsearchQueryOperation } -export type Endpoint29_0Input = { +export type Endpoint28_0Input = { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined } -export type Endpoint29_0Output = ReadonlyArray -export type ConfigGetOperation = (input?: Endpoint29_0Input) => Effect.Effect +export type Endpoint28_0Output = ReadonlyArray +export type ConfigGetOperation = (input?: Endpoint28_0Input) => Effect.Effect export interface ConfigApi { readonly get: ConfigGetOperation @@ -1682,7 +1650,6 @@ export interface AppApi { readonly event: EventApi readonly pty: PtyApi readonly shell: ShellApi - readonly question: QuestionApi readonly reference: ReferenceApi readonly worktree: WorktreeApi readonly vcs: VcsApi diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 1599fc244a8..eee41f22980 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -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()( - raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), - ) - -const Endpoint22_1 = (raw: RawClient["server.question"]) => (input: Endpoint22_1Input) => - preserveEffect()( - 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()( - 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()( - 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()( 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()( +const Endpoint23_0 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_0Input) => + preserveEffect()( raw["worktree.list"]({ params: { projectID: input["projectID"] } }).pipe(Effect.mapError(mapClientError)), ) -const Endpoint24_1 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_1Input) => - preserveEffect()( +const Endpoint23_1 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_1Input) => + preserveEffect()( 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()( +const Endpoint23_2 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_2Input) => + preserveEffect()( 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()( +const Endpoint23_3 = (raw: RawClient["server.worktree"]) => (input: Endpoint23_3Input) => + preserveEffect()( 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()( +const Endpoint24_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint24_0Input) => + preserveEffect()( raw["vcs.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const Endpoint25_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_1Input) => - preserveEffect()( +const Endpoint24_1 = (raw: RawClient["server.vcs"]) => (input?: Endpoint24_1Input) => + preserveEffect()( raw["vcs.status"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const Endpoint25_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint25_2Input) => - preserveEffect()( +const Endpoint24_2 = (raw: RawClient["server.vcs"]) => (input: Endpoint24_2Input) => + preserveEffect()( 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()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))) +const Endpoint25_0 = (raw: RawClient["server.debug"]) => () => + preserveEffect()(raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))) -const Endpoint26_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint26_1Input) => - preserveEffect()( +const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) => + preserveEffect()( 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()(raw["migration.v1.status"]({}).pipe(Effect.mapError(mapClientError))) +const Endpoint26_0 = (raw: RawClient["server.migration"]) => () => + preserveEffect()(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()( +const Endpoint27_0 = (raw: RawClient["server.websearch"]) => (input?: Endpoint27_0Input) => + preserveEffect()( raw["websearch.providers"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const Endpoint28_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint28_1Input) => - preserveEffect()( +const Endpoint27_1 = (raw: RawClient["server.websearch"]) => (input: Endpoint27_1Input) => + preserveEffect()( 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()( +const Endpoint28_0 = (raw: RawClient["server.config"]) => (input?: Endpoint28_0Input) => + preserveEffect()( 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 }) => diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index b3f64ca0c76..593388d5dbb 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -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( - { - 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( - { - 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( - { - 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( diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 242a3e46fbc..49667a8d838 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -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 - 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 - 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 } -} - 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; tool?: QuestionTool } -} - -export type QuestionRequest = { id: string; sessionID: string; questions: Array; 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 -} - -export type QuestionListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type QuestionListOutput = { data: Array }["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> }["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 diff --git a/packages/core/src/location-services.ts b/packages/core/src/location-services.ts index 02db84a829f..764694f5459 100644 --- a/packages/core/src/location-services.ts +++ b/packages/core/src/location-services.ts @@ -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, diff --git a/packages/core/src/question.ts b/packages/core/src/question.ts deleted file mode 100644 index ef56fc22881..00000000000 --- a/packages/core/src/question.ts +++ /dev/null @@ -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()("Question.RejectedError", {}) { - override get message() { - return "The user dismissed this question" - } -} - -export class NotFoundError extends Schema.TaggedErrorClass()("Question.NotFoundError", { - requestID: ID, -}) {} - -export interface AskInput { - readonly sessionID: SessionSchema.ID - readonly questions: ReadonlyArray - readonly tool?: Tool -} - -export interface ReplyInput { - readonly requestID: ID - readonly answers: ReadonlyArray -} - -export interface Interface { - readonly ask: (input: AskInput) => Effect.Effect, RejectedError> - readonly reply: (input: ReplyInput) => Effect.Effect - readonly reject: (requestID: ID) => Effect.Effect - readonly list: () => Effect.Effect> -} - -export class Service extends Context.Service()("@opencode/Question") {} - -interface Pending { - readonly request: Request - readonly deferred: Deferred.Deferred, 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() - - 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, 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] }) diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index c5ee9e68579..bbc2010a4a1 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -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 diff --git a/packages/core/src/tool/plugin/question.ts b/packages/core/src/tool/plugin/question.ts index 7e16745c1b1..26e82741523 100644 --- a/packages/core/src/tool/plugin/question.ts +++ b/packages/core/src/tool/plugin/question.ts @@ -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" diff --git a/packages/core/test/question.test.ts b/packages/core/test/question.test.ts deleted file mode 100644 index cd3e0369b2a..00000000000 --- a/packages/core/test/question.test.ts +++ /dev/null @@ -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() - 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) - }), - ) -}) diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 0efd338c300..d524b5784d4 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -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": "" - }, - "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": "" - }, - "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." diff --git a/packages/protocol/src/api.ts b/packages/protocol/src/api.ts index 3732b839fb4..e6ca152bfda 100644 --- a/packages/protocol/src/api.ts +++ b/packages/protocol/src/api.ts @@ -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> - | ReturnType> +> = ReturnType> 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)) diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index 93ba6b386e1..50d3003a946 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -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", diff --git a/packages/protocol/src/errors.ts b/packages/protocol/src/errors.ts index 7b7cde045b6..fea0b498b6d 100644 --- a/packages/protocol/src/errors.ts +++ b/packages/protocol/src/errors.ts @@ -141,15 +141,6 @@ export class PermissionNotFoundError extends Schema.TaggedErrorClass()( - "QuestionNotFoundError", - { - requestID: Schema.String, - message: Schema.String, - }, - { httpApiStatus: 404 }, -) {} - export class FormNotFoundError extends Schema.TaggedErrorClass()( "FormNotFoundError", { diff --git a/packages/protocol/src/groups/question.ts b/packages/protocol/src/groups/question.ts deleted file mode 100644 index 6a7cf0588d8..00000000000 --- a/packages/protocol/src/groups/question.ts +++ /dev/null @@ -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, - sessionLocationMiddleware: Context.Key, -) => - 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." })) diff --git a/packages/schema/src/event-manifest.ts b/packages/schema/src/event-manifest.ts index 842e9287d1d..156a3df1b80 100644 --- a/packages/schema/src/event-manifest.ts +++ b/packages/schema/src/event-manifest.ts @@ -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, ) diff --git a/packages/schema/src/question.ts b/packages/schema/src/question.ts index beb634d15b0..61dedfaa370 100644 --- a/packages/schema/src/question.ts +++ b/packages/schema/src/question.ts @@ -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 {} +}) 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 {} - export const Prompt = Schema.Struct(base).annotate({ identifier: "Question.Prompt" }) export interface Prompt extends Schema.Schema.Type {} -export const Tool = Schema.Struct({ - messageID: Schema.String, - id: Schema.String, -}).annotate({ identifier: "Question.Tool" }) -export interface Tool extends Schema.Schema.Type {} - -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 {} - 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 {} - -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) } diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index a1ef01fea8b..7f9627df53b 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -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_") }) diff --git a/packages/schema/test/event-manifest.test.ts b/packages/schema/test/event-manifest.test.ts index 754ac9eb4c5..d61334c5518 100644 --- a/packages/schema/test/event-manifest.test.ts +++ b/packages/schema/test/event-manifest.test.ts @@ -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) }) diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index cd2c4d4a7a4..e0233ba64a8 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -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, diff --git a/packages/server/src/handlers/question.ts b/packages/server/src/handlers/question.ts deleted file mode 100644 index 7229de0ae58..00000000000 --- a/packages/server/src/handlers/question.ts +++ /dev/null @@ -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* ( - sessionID: Question.Request["sessionID"], - requestID: Question.ID, - use: (question: Question.Interface) => Effect.Effect, - ) { - 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() - }), - ) - }), -) diff --git a/packages/session-ui/src/components/markdown-inline-code-kind.test.ts b/packages/session-ui/src/components/markdown-inline-code-kind.test.ts index 5b7c00855b6..26059377f27 100644 --- a/packages/session-ui/src/components/markdown-inline-code-kind.test.ts +++ b/packages/session-ui/src/components/markdown-inline-code-kind.test.ts @@ -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(``)).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 )`)).toBeUndefined() expect(inlineCodeKind(`sync.data.session`)).toBeUndefined() expect(inlineCodeKind(`window.api`)).toBeUndefined() diff --git a/packages/tui/src/feature-plugins/system/notifications.ts b/packages/tui/src/feature-plugins/system/notifications.ts index 180b345ca0f..03b6b626515 100644 --- a/packages/tui/src/feature-plugins/system/notifications.ts +++ b/packages/tui/src/feature-plugins/system/notifications.ts @@ -24,7 +24,6 @@ export default Plugin.define({ const errored = new Set() const terminal = new Set() const forms = new Set() - const questions = new Set() const permissions = new Set() 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) diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index 1b98d9e7881..a0a246105ce 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -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["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, ]) diff --git a/packages/www/openapi.json b/packages/www/openapi.json index 0efd338c300..d524b5784d4 100644 --- a/packages/www/openapi.json +++ b/packages/www/openapi.json @@ -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": "" - }, - "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": "" - }, - "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." diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 0efd338c300..d524b5784d4 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -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": "" - }, - "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": "" - }, - "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."