feat(core): add session form service (#34855)
This commit is contained in:
parent
460cdc5aec
commit
7ebd344fa2
67 changed files with 7862 additions and 5223 deletions
|
|
@ -69,7 +69,10 @@ describe("bootstrapDirectory", () => {
|
|||
},
|
||||
permission: { list: async () => ({ data: [] }) },
|
||||
question: { list: async () => ({ data: [] }) },
|
||||
v2: { reference: { list: async () => ({ data: { data: [] } }) } },
|
||||
v2: {
|
||||
form: { request: { list: async () => ({ data: { data: [] } }) } },
|
||||
reference: { list: async () => ({ data: { data: [] } }) },
|
||||
},
|
||||
mcp: {
|
||||
status: async () => {
|
||||
mcpReads.push("status")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type {
|
|||
PermissionRequest,
|
||||
Project,
|
||||
ProviderAuthResponse,
|
||||
QuestionRequest,
|
||||
ReferenceInfo,
|
||||
Session,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
|
|
@ -22,6 +21,7 @@ import { QueryClient, queryOptions } from "@tanstack/solid-query"
|
|||
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||
import { ScopedKey, type ServerScope } from "@/utils/server-scope"
|
||||
import { isQuestionForm, type QuestionForm } from "@/utils/question-form"
|
||||
|
||||
type GlobalStore = {
|
||||
ready: boolean
|
||||
|
|
@ -319,9 +319,10 @@ export async function bootstrapDirectory(input: {
|
|||
),
|
||||
() =>
|
||||
retry(() =>
|
||||
input.sdk.question.list().then((x) => {
|
||||
const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id)
|
||||
const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
|
||||
input.sdk.v2.form.request.list().then((x) => {
|
||||
const forms: QuestionForm[] = (x.data?.data ?? []).flatMap((form) => (isQuestionForm(form) ? [form] : []))
|
||||
const ids = forms.map((question) => question.sessionID)
|
||||
const grouped = groupBySession(forms)
|
||||
const warm = input.session
|
||||
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
|
||||
: warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk })
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import type { Message, Part, PermissionRequest, Project, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
|
||||
import type { Message, Part, PermissionRequest, Project, Session } from "@opencode-ai/sdk/v2/client"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { State } from "./types"
|
||||
import { applyDirectoryEvent, applyGlobalEvent, cleanupDroppedSessionCaches } from "./event-reducer"
|
||||
import type { QuestionForm } from "@/utils/question-form"
|
||||
|
||||
const rootSession = (input: { id: string; parentID?: string; archived?: number }) =>
|
||||
({
|
||||
|
|
@ -48,14 +49,18 @@ const questionRequest = (id: string, sessionID: string, title = id) =>
|
|||
({
|
||||
id,
|
||||
sessionID,
|
||||
questions: [
|
||||
mode: "form",
|
||||
metadata: { kind: "question" },
|
||||
fields: [
|
||||
{
|
||||
question: title,
|
||||
header: title,
|
||||
options: [{ label: title, description: title }],
|
||||
key: "question_0",
|
||||
title,
|
||||
description: title,
|
||||
type: "string",
|
||||
options: [{ value: title, label: title, description: title }],
|
||||
},
|
||||
],
|
||||
}) as QuestionRequest
|
||||
}) as QuestionForm
|
||||
|
||||
const baseState = (input: Partial<State> = {}) =>
|
||||
({
|
||||
|
|
@ -505,7 +510,7 @@ describe("applyDirectoryEvent", () => {
|
|||
expect(store.permission[sessionID]?.map((x) => x.id)).toEqual(["perm_1", "perm_3"])
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "question.asked", properties: questionRequest("q_2", sessionID) },
|
||||
event: { type: "form.created", properties: { form: questionRequest("q_2", sessionID) } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
|
|
@ -515,17 +520,17 @@ describe("applyDirectoryEvent", () => {
|
|||
expect(store.question[sessionID]?.map((x) => x.id)).toEqual(["q_1", "q_2", "q_3"])
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "question.asked", properties: questionRequest("q_2", sessionID, "updated") },
|
||||
event: { type: "form.created", properties: { form: questionRequest("q_2", sessionID, "updated") } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
directory: "/tmp",
|
||||
loadLsp() {},
|
||||
})
|
||||
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.questions[0]?.header).toBe("updated")
|
||||
expect(store.question[sessionID]?.find((x) => x.id === "q_2")?.fields[0]?.description).toBe("updated")
|
||||
|
||||
applyDirectoryEvent({
|
||||
event: { type: "question.rejected", properties: { sessionID, requestID: "q_2" } },
|
||||
event: { type: "form.cancelled", properties: { sessionID, id: "q_2" } },
|
||||
store,
|
||||
setStore,
|
||||
push() {},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type {
|
|||
Part,
|
||||
PermissionRequest,
|
||||
Project,
|
||||
QuestionRequest,
|
||||
Session,
|
||||
SessionStatus,
|
||||
SnapshotFileDiff,
|
||||
|
|
@ -15,6 +14,7 @@ import type { State, VcsCache } from "./types"
|
|||
import { trimSessions } from "./session-trim"
|
||||
import { dropSessionCaches } from "./session-cache"
|
||||
import { diffs as list, message as clean } from "@/utils/diffs"
|
||||
import { isQuestionForm } from "@/utils/question-form"
|
||||
|
||||
const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"])
|
||||
const SESSION_CONTENT_EVENTS = new Set([
|
||||
|
|
@ -28,9 +28,9 @@ const SESSION_CONTENT_EVENTS = new Set([
|
|||
"message.part.delta",
|
||||
"permission.asked",
|
||||
"permission.replied",
|
||||
"question.asked",
|
||||
"question.replied",
|
||||
"question.rejected",
|
||||
"form.created",
|
||||
"form.replied",
|
||||
"form.cancelled",
|
||||
])
|
||||
|
||||
export function applyGlobalEvent(input: {
|
||||
|
|
@ -364,8 +364,10 @@ export function applyDirectoryEvent(input: {
|
|||
)
|
||||
break
|
||||
}
|
||||
case "question.asked": {
|
||||
const question = event.properties as QuestionRequest
|
||||
case "form.created": {
|
||||
const properties = event.properties as { form?: unknown }
|
||||
if (!isQuestionForm(properties.form)) break
|
||||
const question = properties.form
|
||||
const questions = input.store.question[question.sessionID]
|
||||
if (!questions) {
|
||||
input.setStore("question", question.sessionID, [question])
|
||||
|
|
@ -385,12 +387,12 @@ export function applyDirectoryEvent(input: {
|
|||
)
|
||||
break
|
||||
}
|
||||
case "question.replied":
|
||||
case "question.rejected": {
|
||||
const props = event.properties as { sessionID: string; requestID: string }
|
||||
case "form.replied":
|
||||
case "form.cancelled": {
|
||||
const props = event.properties as { sessionID: string; id: string }
|
||||
const questions = input.store.question[props.sessionID]
|
||||
if (!questions) break
|
||||
const result = Binary.search(questions, props.requestID, (q) => q.id)
|
||||
const result = Binary.search(questions, props.id, (q) => q.id)
|
||||
if (!result.found) break
|
||||
input.setStore(
|
||||
"question",
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import type {
|
|||
Message,
|
||||
Part,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
SessionStatus,
|
||||
SnapshotFileDiff,
|
||||
Todo,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache"
|
||||
import type { QuestionForm } from "@/utils/question-form"
|
||||
|
||||
const msg = (id: string, sessionID: string) =>
|
||||
({
|
||||
|
|
@ -38,7 +38,7 @@ describe("app session cache", () => {
|
|||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
question: Record<string, QuestionForm[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
} = {
|
||||
session_status: { ses_1: { type: "busy" } as SessionStatus },
|
||||
|
|
@ -47,7 +47,7 @@ describe("app session cache", () => {
|
|||
message: {},
|
||||
part: { msg_1: [part("prt_1", "ses_1", "msg_1")] },
|
||||
permission: { ses_1: [] as PermissionRequest[] },
|
||||
question: { ses_1: [] as QuestionRequest[] },
|
||||
question: { ses_1: [] as QuestionForm[] },
|
||||
part_text_accum_delta: { prt_1: "streamed text" },
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ describe("app session cache", () => {
|
|||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
question: Record<string, QuestionForm[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
} = {
|
||||
session_status: {},
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import type {
|
|||
Message,
|
||||
Part,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
SessionStatus,
|
||||
SnapshotFileDiff,
|
||||
Todo,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import type { QuestionForm } from "@/utils/question-form"
|
||||
|
||||
export const SESSION_CACHE_LIMIT = 40
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ type SessionCache = {
|
|||
message: Record<string, Message[] | undefined>
|
||||
part: Record<string, Part[] | undefined>
|
||||
permission: Record<string, PermissionRequest[] | undefined>
|
||||
question: Record<string, QuestionRequest[] | undefined>
|
||||
question: Record<string, QuestionForm[] | undefined>
|
||||
part_text_accum_delta: Record<string, string | undefined>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import type {
|
|||
Part,
|
||||
Path,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
ReferenceInfo,
|
||||
Session,
|
||||
SessionStatus,
|
||||
|
|
@ -17,6 +16,7 @@ import type {
|
|||
Todo,
|
||||
VcsInfo,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import type { QuestionForm } from "@/utils/question-form"
|
||||
import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
|
||||
import type { Accessor } from "solid-js"
|
||||
import type { SetStoreFunction, Store } from "solid-js/store"
|
||||
|
|
@ -60,7 +60,7 @@ export type State = {
|
|||
[sessionID: string]: PermissionRequest[]
|
||||
}
|
||||
question: {
|
||||
[sessionID: string]: QuestionRequest[]
|
||||
[sessionID: string]: QuestionForm[]
|
||||
}
|
||||
mcp_ready: boolean
|
||||
mcp: {
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import type {
|
|||
OpencodeClient,
|
||||
Part,
|
||||
PermissionRequest,
|
||||
QuestionRequest,
|
||||
Session,
|
||||
SessionStatus,
|
||||
SnapshotFileDiff,
|
||||
Todo,
|
||||
} from "@opencode-ai/sdk/v2/client"
|
||||
import { isQuestionForm, type QuestionForm } from "@/utils/question-form"
|
||||
import { batch } from "solid-js"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs"
|
||||
|
|
@ -136,7 +136,7 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||
session_diff: {} as Record<string, SnapshotFileDiff[]>,
|
||||
todo: {} as Record<string, Todo[]>,
|
||||
permission: {} as Record<string, PermissionRequest[]>,
|
||||
question: {} as Record<string, QuestionRequest[]>,
|
||||
question: {} as Record<string, QuestionForm[]>,
|
||||
message: {} as Record<string, Message[]>,
|
||||
part: {} as Record<string, Part[]>,
|
||||
part_text_accum_delta: {} as Record<string, string>,
|
||||
|
|
@ -932,8 +932,10 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||
)
|
||||
return
|
||||
}
|
||||
case "question.asked": {
|
||||
const question = event.properties as QuestionRequest
|
||||
case "form.created": {
|
||||
const properties = event.properties as { form?: unknown }
|
||||
if (!isQuestionForm(properties.form)) return
|
||||
const question = properties.form
|
||||
const questions = data.question[question.sessionID]
|
||||
if (!questions) {
|
||||
setData("question", question.sessionID, [question])
|
||||
|
|
@ -949,15 +951,15 @@ export function createServerSession(client: OpencodeClient, options?: { retry?:
|
|||
)
|
||||
return
|
||||
}
|
||||
case "question.replied":
|
||||
case "question.rejected": {
|
||||
const props = event.properties as { sessionID: string; requestID: string }
|
||||
case "form.replied":
|
||||
case "form.cancelled": {
|
||||
const props = event.properties as { sessionID: string; id: string }
|
||||
setData(
|
||||
"question",
|
||||
props.sessionID,
|
||||
produce((draft) => {
|
||||
if (!draft) return
|
||||
const result = Binary.search(draft, props.requestID, (item) => item.id)
|
||||
const result = Binary.search(draft, props.id, (item) => item.id)
|
||||
if (result.found) draft.splice(result.index, 1)
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue