fix(core): revert form service and mcp elicitation (#35080)

This commit is contained in:
Aiden Cline 2026-07-03 00:32:10 -05:00 committed by GitHub
commit ef2140d121
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 6587 additions and 6492 deletions

View file

@ -30,13 +30,11 @@ export const createDirSyncContext = (
const data = new Proxy({} as State, {
get(_, property: keyof State) {
if (property === "session_working") return serverSync.session.data.session_working.bind(serverSync.session.data)
if (property === "question") return { ...serverSync.session.data.question, global: current()[0].question.global ?? [] }
if (sessionFields.has(property)) return serverSync.session.data[property as keyof typeof serverSync.session.data]
return current()[0][property]
},
})
const set = ((...input: unknown[]) => {
if (input[0] === "question" && input[1] === "global") return (current()[1] as (...args: unknown[]) => unknown)(...input)
if (typeof input[0] === "string" && sessionFields.has(input[0])) {
return (serverSync.session.set as (...args: unknown[]) => unknown)(...input)
}

View file

@ -69,10 +69,7 @@ describe("bootstrapDirectory", () => {
},
permission: { list: async () => ({ data: [] }) },
question: { list: async () => ({ data: [] }) },
v2: {
form: { request: { list: async () => ({ data: { data: [] } }) } },
reference: { list: async () => ({ data: { data: [] } }) },
},
v2: { reference: { list: async () => ({ data: { data: [] } }) } },
mcp: {
status: async () => {
mcpReads.push("status")

View file

@ -5,6 +5,7 @@ import type {
PermissionRequest,
Project,
ProviderAuthResponse,
QuestionRequest,
ReferenceInfo,
Session,
} from "@opencode-ai/sdk/v2/client"
@ -21,7 +22,6 @@ import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
import { isQuestionForm, type QuestionForm } from "@/utils/question-form"
type GlobalStore = {
ready: boolean
@ -319,11 +319,9 @@ export async function bootstrapDirectory(input: {
),
() =>
retry(() =>
input.sdk.v2.form.request.list().then((x) => {
const forms: QuestionForm[] = (x.data?.data ?? []).flatMap((form) => (isQuestionForm(form) ? [form] : []))
const global = forms.filter((question) => question.sessionID === "global")
const grouped = groupBySession(forms.filter((question) => question.sessionID !== "global"))
const ids = Object.keys(grouped)
input.sdk.question.list().then((x) => {
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk })
@ -331,22 +329,11 @@ export async function bootstrapDirectory(input: {
batch(() => {
const current = input.session?.data.question ?? input.store.question
for (const sessionID of Object.keys(current)) {
if (sessionID === "global") continue
if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.directory !== input.directory) continue
if (input.session) input.session.set("question", sessionID, [])
if (!input.session) input.setStore("question", sessionID, [])
}
if (global.length > 0 || input.store.question.global) {
input.setStore(
"question",
"global",
reconcile(
global.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
),
)
}
for (const [sessionID, questions] of Object.entries(grouped)) {
const value = reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),

View file

@ -1,9 +1,8 @@
import { describe, expect, test } from "bun:test"
import type { Message, Part, PermissionRequest, Project, Session } from "@opencode-ai/sdk/v2/client"
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
import { createStore } from "solid-js/store"
import type { State } from "./types"
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
import type { QuestionForm } from "@/utils/question-form"
const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
({
@ -49,18 +48,14 @@ const questionRequest = (id: string, sessionID: string, title = id) =>
({
id,
sessionID,
mode: "form",
metadata: { kind: "question" },
fields: [
questions: [
{
key: "question_0",
title,
description: title,
type: "string",
options: [{ value: title, label: title, description: title }],
question: title,
header: title,
options: [{ label: title, description: title }],
},
],
}) as QuestionForm
}) as QuestionRequest
const baseState = (input: Partial<State> = {}) =>
({
@ -510,7 +505,7 @@ describe("applyDirectoryEvent", () => {
expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"])
applyDirectoryEvent({
event: { type: "form.created", properties: { form: questionRequest("q_2", sessionID) } },
event: { type: "question.asked", properties: questionRequest("q_2", sessionID) },
store,
setStore,
push() {},
@ -520,18 +515,17 @@ describe("applyDirectoryEvent", () => {
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"])
applyDirectoryEvent({
event: { type: "form.created", properties: { form: questionRequest("q_2", sessionID, "updated") } },
event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") },
store,
setStore,
push() {},
directory: "/tmp",
loadLsp() {},
})
const form = store.question[sessionID]?.find((x) => x.id === "q_2")
expect(form?.mode === "form" ? form.fields[0]?.description : undefined).toBe("updated")
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated")
applyDirectoryEvent({
event: { type: "form.cancelled", properties: { sessionID, id: "q_2" } },
event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } },
store,
setStore,
push() {},
@ -541,34 +535,6 @@ describe("applyDirectoryEvent", () => {
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_3"])
})
test("tracks global form lifecycles when session content is delegated", () => {
const [store, setStore] = createStore(baseState())
applyDirectoryEvent({
event: { type: "form.created", properties: { form: questionRequest("q_1", "global") } },
store,
setStore,
push() {},
directory: "/tmp",
loadLsp() {},
sessionContent: false,
})
expect(store.question.global?.map((x) => x.id)).toEqual(["q_1"])
applyDirectoryEvent({
event: { type: "form.cancelled", properties: { sessionID: "global", id: "q_1" } },
store,
setStore,
push() {},
directory: "/tmp",
loadLsp() {},
sessionContent: false,
})
expect(store.question.global).toEqual([])
})
test("updates vcs branch in store and cache", () => {
const [store, setStore] = createStore(baseState({ vcs: { branch: "main", default_branch: "main" } }))
const [cacheStore, setCacheStore] = createStore({

View file

@ -5,6 +5,7 @@ import type {
Part,
PermissionRequest,
Project,
QuestionRequest,
Session,
SessionStatus,
SnapshotFileDiff,
@ -14,7 +15,6 @@ import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"
import { diffs as list, message as clean } from "@/utils/diffs"
import { isQuestionForm } from "@/utils/question-form"
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
const SESSION_CONTENT_EVENTS = new Set([
@ -28,9 +28,9 @@ const SESSION_CONTENT_EVENTS = new Set([
"message.part.delta",
"permission.asked",
"permission.replied",
"form.created",
"form.replied",
"form.cancelled",
"question.asked",
"question.replied",
"question.rejected",
])
export function applyGlobalEvent(input: {
@ -120,7 +120,7 @@ export function applyDirectoryEvent(input: {
permission?: State["permission"]
}) {
const event = input.event
if (input.sessionContent === false && SESSION_CONTENT_EVENTS.has(event.type) && !isGlobalQuestionEvent(event)) return
if (input.sessionContent === false && SESSION_CONTENT_EVENTS.has(event.type)) return
const limit = Math.max(input.store.limit, input.retainedLimit ?? 0)
switch (event.type) {
case "server.instance.disposed": {
@ -364,10 +364,8 @@ export function applyDirectoryEvent(input: {
)
break
}
case "form.created": {
const properties = event.properties as { form?: unknown }
if (!isQuestionForm(properties.form)) break
const question = properties.form
case "question.asked": {
const question = event.properties as QuestionRequest
const questions = input.store.question[question.sessionID]
if (!questions) {
input.setStore("question", question.sessionID, [question])
@ -387,12 +385,12 @@ export function applyDirectoryEvent(input: {
)
break
}
case "form.replied":
case "form.cancelled": {
const props = event.properties as { sessionID: string; id: string }
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.id, (q) => q.id)
const result = Binary.search(questions, props.requestID, (q) => q.id)
if (!result.found) break
input.setStore(
"question",
@ -413,13 +411,3 @@ export function applyDirectoryEvent(input: {
}
}
}
export function isGlobalQuestionEvent(event: { type: string; properties?: unknown }) {
if (event.type === "form.created") {
const form = (event.properties as { form?: unknown } | undefined)?.form
return isQuestionForm(form) && form.sessionID === "global"
}
if (event.type !== "form.replied" && event.type !== "form.cancelled") return false
const properties = event.properties as { sessionID?: unknown } | undefined
return properties?.sessionID === "global"
}

View file

@ -3,12 +3,12 @@ import type {
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
import type { QuestionForm } from "@/utils/question-form"
const msg = (id: string, sessionID: string) =>
({
@ -38,7 +38,7 @@ describe("app session cache", () => {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionForm[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: { ses_1: { type: "busy" } as SessionStatus },
@ -47,7 +47,7 @@ describe("app session cache", () => {
message: {},
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
permission: { ses_1: [] as PermissionRequest[] },
question: { ses_1: [] as QuestionForm[] },
question: { ses_1: [] as QuestionRequest[] },
part_text_accum_delta: { prt_1: "streamed text" },
}
@ -72,7 +72,7 @@ describe("app session cache", () => {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionForm[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
} = {
session_status: {},

View file

@ -2,11 +2,11 @@ import type {
Message,
Part,
PermissionRequest,
QuestionRequest,
SessionStatus,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { QuestionForm } from "@/utils/question-form"
export const SESSION_CACHE_LIMIT = 40
@ -17,7 +17,7 @@ type SessionCache = {
message: Record<string, Message[] | undefined>
part: Record<string, Part[] | undefined>
permission: Record<string, PermissionRequest[] | undefined>
question: Record<string, QuestionForm[] | undefined>
question: Record<string, QuestionRequest[] | undefined>
part_text_accum_delta: Record<string, string | undefined>
}

View file

@ -9,6 +9,7 @@ import type {
Part,
Path,
PermissionRequest,
QuestionRequest,
ReferenceInfo,
Session,
SessionStatus,
@ -16,7 +17,6 @@ import type {
Todo,
VcsInfo,
} from "@opencode-ai/sdk/v2/client"
import type { QuestionForm } from "@/utils/question-form"
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
import type { Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
@ -60,7 +60,7 @@ export type State = {
[sessionID: string]: PermissionRequest[]
}
question: {
[sessionID: string]: QuestionForm[]
[sessionID: string]: QuestionRequest[]
}
mcp_ready: boolean
mcp: {

View file

@ -5,12 +5,12 @@ import type {
OpencodeClient,
Part,
PermissionRequest,
QuestionRequest,
Session,
SessionStatus,
SnapshotFileDiff,
Todo,
} from "@opencode-ai/sdk/v2/client"
import { isQuestionForm, type QuestionForm } from "@/utils/question-form"
import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs"
@ -136,7 +136,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
session_diff: {} as Record<string, SnapshotFileDiff[]>,
todo: {} as Record<string, Todo[]>,
permission: {} as Record<string, PermissionRequest[]>,
question: {} as Record<string, QuestionForm[]>,
question: {} as Record<string, QuestionRequest[]>,
message: {} as Record<string, Message[]>,
part: {} as Record<string, Part[]>,
part_text_accum_delta: {} as Record<string, string>,
@ -932,10 +932,8 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
)
return
}
case "form.created": {
const properties = event.properties as { form?: unknown }
if (!isQuestionForm(properties.form)) return
const question = properties.form
case "question.asked": {
const question = event.properties as QuestionRequest
const questions = data.question[question.sessionID]
if (!questions) {
setData("question", question.sessionID, [question])
@ -951,15 +949,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
)
return
}
case "form.replied":
case "form.cancelled": {
const props = event.properties as { sessionID: string; id: string }
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.id, (item) => item.id)
const result = Binary.search(draft, props.requestID, (item) => item.id)
if (result.found) draft.splice(result.index, 1)
}),
)

View file

@ -25,7 +25,7 @@ import {
loadReferencesQuery,
} from "./global-sync/bootstrap"
import { createChildStoreManager } from "./global-sync/child-store"
import { applyDirectoryEvent, applyGlobalEvent, isGlobalQuestionEvent } from "./global-sync/event-reducer"
import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer"
import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load"
import { trimSessions } from "./global-sync/session-trim"
import type { ProjectMeta } from "./global-sync/types"
@ -375,7 +375,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
const event = e.details
const recent = bootingRoot || Date.now() - bootedAt < 1500
if (!isGlobalQuestionEvent(event)) session.apply(event)
session.apply(event)
if (directory === "global") {
applyGlobalEvent({