ソースを参照

update httpapi routes

James Long 3 ヶ月 前
コミット
1a2b1837e8

+ 1 - 1
packages/opencode/src/server/backend.ts

@@ -11,7 +11,7 @@ export type Selection = {
 export type Attributes = ReturnType<typeof attributes>
 
 export function select(): Selection {
-  // if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { backend: "effect-httpapi", reason: "env" }
+  if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) return { backend: "effect-httpapi", reason: "env" }
   return { backend: "hono", reason: "stable" }
 }
 

+ 28 - 0
packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts

@@ -1,4 +1,5 @@
 import { NonNegativeInt } from "@/util/schema"
+import { SessionID } from "@/session/schema"
 import { Schema } from "effect"
 import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "../middleware/authorization"
@@ -21,6 +22,9 @@ export const ReplayPayload = Schema.Struct({
 export const ReplayResponse = Schema.Struct({
   sessionID: Schema.String,
 })
+export const SessionPayload = Schema.Struct({
+  sessionID: SessionID,
+})
 export const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt)
 export const HistoryEvent = Schema.Struct({
   id: Schema.String,
@@ -33,6 +37,8 @@ export const HistoryEvent = Schema.Struct({
 export const SyncPaths = {
   start: `${root}/start`,
   replay: `${root}/replay`,
+  erase: `${root}/erase`,
+  steal: `${root}/steal`,
   history: `${root}/history`,
 } as const
 
@@ -60,6 +66,28 @@ export const SyncApi = HttpApi.make("sync")
             description: "Validate and replay a complete sync event history.",
           }),
         ),
+        HttpApiEndpoint.post("erase", SyncPaths.erase, {
+          payload: SessionPayload,
+          success: described(SessionPayload, "Erased session sync events"),
+          error: HttpApiError.BadRequest,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "sync.erase",
+            summary: "Erase session sync events",
+            description: "Erase all locally stored sync events for a session aggregate.",
+          }),
+        ),
+        HttpApiEndpoint.post("steal", SyncPaths.steal, {
+          payload: SessionPayload,
+          success: described(SessionPayload, "Session stolen into workspace"),
+          error: HttpApiError.BadRequest,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "sync.steal",
+            summary: "Steal session into workspace",
+            description: "Update a session to belong to the current workspace through the sync event system.",
+          }),
+        ),
         HttpApiEndpoint.post("history", SyncPaths.history, {
           payload: HistoryPayload,
           success: described(Schema.Array(HistoryEvent), "Sync events"),

+ 15 - 1
packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts

@@ -1,7 +1,7 @@
 import { Workspace } from "@/control-plane/workspace"
 import { WorkspaceAdaptorEntry } from "@/control-plane/types"
 import { Schema, Struct } from "effect"
-import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
+import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
 import { Authorization } from "../middleware/authorization"
 import { InstanceContextMiddleware } from "../middleware/instance-context"
 import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
@@ -9,12 +9,14 @@ import { described } from "./metadata"
 
 const root = "/experimental/workspace"
 export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"]))
+export const WarpPayload = Schema.Struct(Struct.omit(Workspace.SessionWarpInput.fields, ["workspaceID"]))
 
 export const WorkspacePaths = {
   adaptors: `${root}/adaptor`,
   list: root,
   status: `${root}/status`,
   remove: `${root}/:id`,
+  warp: `${root}/:id/warp`,
 } as const
 
 export const WorkspaceApi = HttpApi.make("workspace")
@@ -70,6 +72,18 @@ export const WorkspaceApi = HttpApi.make("workspace")
             description: "Remove an existing workspace.",
           }),
         ),
+        HttpApiEndpoint.post("warp", WorkspacePaths.warp, {
+          params: { id: Workspace.Info.fields.id },
+          payload: WarpPayload,
+          success: described(HttpApiSchema.NoContent, "Session warped"),
+          error: HttpApiError.BadRequest,
+        }).annotateMerge(
+          OpenApi.annotations({
+            identifier: "experimental.workspace.warp",
+            summary: "Warp session into workspace",
+            description: "Move a session's sync history into the target workspace.",
+          }),
+        ),
       )
       .annotateMerge(OpenApi.annotations({ title: "workspace", description: "Experimental HttpApi workspace routes." }))
       .middleware(InstanceContextMiddleware)

+ 38 - 2
packages/opencode/src/server/routes/instance/httpapi/handlers/sync.ts

@@ -1,5 +1,8 @@
 import { startWorkspaceSyncing } from "@/control-plane/workspace"
+import { WorkspaceContext } from "@/control-plane/workspace-context"
 import * as InstanceState from "@/effect/instance-state"
+import { Instance } from "@/project/instance"
+import { Session } from "@/session/session"
 import { Database } from "@/storage/db"
 import { SyncEvent } from "@/sync"
 import { EventTable } from "@/sync/event.sql"
@@ -12,7 +15,7 @@ import { or } from "drizzle-orm"
 import { Effect } from "effect"
 import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
-import { HistoryPayload, ReplayPayload } from "../groups/sync"
+import { HistoryPayload, ReplayPayload, SessionPayload } from "../groups/sync"
 
 export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handlers) =>
   Effect.gen(function* () {
@@ -33,6 +36,34 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
       return { sessionID: events[0].aggregateID }
     })
 
+    const erase = Effect.fn("SyncHttpApi.erase")(function* (ctx: { payload: typeof SessionPayload.Type }) {
+      SyncEvent.remove(ctx.payload.sessionID)
+      return { sessionID: ctx.payload.sessionID }
+    })
+
+    const steal = Effect.fn("SyncHttpApi.steal")(function* (ctx: { payload: typeof SessionPayload.Type }) {
+      const instance = yield* InstanceState.context
+      const workspaceID = yield* InstanceState.workspaceID
+      if (!workspaceID) throw new Error("Cannot steal session without workspace context")
+
+      yield* Effect.sync(() =>
+        WorkspaceContext.provide({
+          workspaceID,
+          fn: () =>
+            Instance.restore(instance, () =>
+              SyncEvent.run(Session.Event.Updated, {
+                sessionID: ctx.payload.sessionID,
+                info: {
+                  workspaceID,
+                },
+              }),
+            ),
+        }),
+      )
+
+      return { sessionID: ctx.payload.sessionID }
+    })
+
     const history = Effect.fn("SyncHttpApi.history")(function* (ctx: { payload: typeof HistoryPayload.Type }) {
       const exclude = Object.entries(ctx.payload)
       return Database.use((db) =>
@@ -49,6 +80,11 @@ export const syncHandlers = HttpApiBuilder.group(InstanceHttpApi, "sync", (handl
       )
     })
 
-    return handlers.handle("start", start).handle("replay", replay).handle("history", history)
+    return handlers
+      .handle("start", start)
+      .handle("replay", replay)
+      .handle("erase", erase)
+      .handle("steal", steal)
+      .handle("history", history)
   }),
 )

+ 19 - 2
packages/opencode/src/server/routes/instance/httpapi/handlers/workspace.ts

@@ -3,9 +3,9 @@ import { Workspace } from "@/control-plane/workspace"
 import * as InstanceState from "@/effect/instance-state"
 import { Instance } from "@/project/instance"
 import { Effect } from "effect"
-import { HttpApiBuilder } from "effect/unstable/httpapi"
+import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
 import { InstanceHttpApi } from "../api"
-import { CreatePayload } from "../groups/workspace"
+import { CreatePayload, WarpPayload } from "../groups/workspace"
 
 export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) =>
   Effect.gen(function* () {
@@ -40,11 +40,28 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
       return yield* Effect.promise(() => Instance.restore(instance, () => Workspace.remove(ctx.params.id)))
     })
 
+    const warp = Effect.fn("WorkspaceHttpApi.warp")(function* (ctx: {
+      params: { id: Workspace.Info["id"] }
+      payload: typeof WarpPayload.Type
+    }) {
+      const instance = yield* InstanceState.context
+      yield* Effect.promise(() =>
+        Instance.restore(instance, () =>
+          Workspace.sessionWarp({
+            workspaceID: ctx.params.id,
+            ...ctx.payload,
+          }),
+        ),
+      )
+      return HttpApiSchema.NoContent.make()
+    })
+
     return handlers
       .handle("adaptors", adaptors)
       .handle("list", list)
       .handle("create", create)
       .handle("status", status)
       .handle("remove", remove)
+      .handle("warp", warp)
   }),
 )