فهرست منبع

refactor(session): extract sharing orchestration (#21759)

Kit Langton 6 روز پیش
والد
کامیت
16c60c9ee7

+ 2 - 1
packages/opencode/src/cli/cmd/github.ts

@@ -21,6 +21,7 @@ import { cmd } from "./cmd"
 import { ModelsDev } from "../../provider/models"
 import { Instance } from "@/project/instance"
 import { bootstrap } from "../bootstrap"
+import { SessionShare } from "@/share/session"
 import { Session } from "../../session"
 import type { SessionID } from "../../session/schema"
 import { MessageID, PartID } from "../../session/schema"
@@ -559,7 +560,7 @@ export const GithubRunCommand = cmd({
         shareId = await (async () => {
           if (share === false) return
           if (!share && repoData.data.private) return
-          await Session.share(session.id)
+          await SessionShare.share(session.id)
           return session.id.slice(-8)
         })()
         console.log("opencode session", session.id)

+ 6 - 5
packages/opencode/src/server/routes/session.ts

@@ -9,6 +9,7 @@ import { SessionPrompt } from "../../session/prompt"
 import { SessionRunState } from "@/session/run-state"
 import { SessionCompaction } from "../../session/compaction"
 import { SessionRevert } from "../../session/revert"
+import { SessionShare } from "@/share/session"
 import { SessionStatus } from "@/session/status"
 import { SessionSummary } from "@/session/summary"
 import { Todo } from "../../session/todo"
@@ -206,10 +207,10 @@ export const SessionRoutes = lazy(() =>
           },
         },
       }),
-      validator("json", Session.create.schema.optional()),
+      validator("json", Session.create.schema),
       async (c) => {
         const body = c.req.valid("json") ?? {}
-        const session = await Session.create(body)
+        const session = await SessionShare.create(body)
         return c.json(session)
       },
     )
@@ -426,7 +427,7 @@ export const SessionRoutes = lazy(() =>
       ),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
-        await Session.share(sessionID)
+        await SessionShare.share(sessionID)
         const session = await Session.get(sessionID)
         return c.json(session)
       },
@@ -491,12 +492,12 @@ export const SessionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          sessionID: Session.unshare.schema,
+          sessionID: SessionID.zod,
         }),
       ),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
-        await Session.unshare(sessionID)
+        await SessionShare.unshare(sessionID)
         const session = await Session.get(sessionID)
         return c.json(session)
       },

+ 3 - 37
packages/opencode/src/session/index.ts

@@ -5,7 +5,6 @@ import { Bus } from "@/bus"
 import { Decimal } from "decimal.js"
 import z from "zod"
 import { type ProviderMetadata } from "ai"
-import { Config } from "../config/config"
 import { Flag } from "../flag/flag"
 import { Installation } from "../installation"
 
@@ -30,7 +29,7 @@ import type { Provider } from "@/provider/provider"
 import { Permission } from "@/permission"
 import { Global } from "@/global"
 import type { LanguageModelV2Usage } from "@ai-sdk/provider"
-import { Effect, Layer, Scope, ServiceMap } from "effect"
+import { Effect, Layer, ServiceMap } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 
 export namespace Session {
@@ -319,8 +318,6 @@ export namespace Session {
     readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info>
     readonly touch: (sessionID: SessionID) => Effect.Effect<void>
     readonly get: (id: SessionID) => Effect.Effect<Info>
-    readonly share: (id: SessionID) => Effect.Effect<{ url: string }>
-    readonly unshare: (id: SessionID) => Effect.Effect<void>
     readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
     readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
     readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect<void>
@@ -364,12 +361,10 @@ export namespace Session {
   const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
     Effect.sync(() => Database.use(fn))
 
-  export const layer: Layer.Layer<Service, never, Bus.Service | Config.Service> = Layer.effect(
+  export const layer: Layer.Layer<Service, never, Bus.Service> = Layer.effect(
     Service,
     Effect.gen(function* () {
       const bus = yield* Bus.Service
-      const config = yield* Config.Service
-      const scope = yield* Scope.Scope
 
       const createNext = Effect.fn("Session.createNext")(function* (input: {
         id?: SessionID
@@ -399,11 +394,6 @@ export namespace Session {
 
         yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result }))
 
-        const cfg = yield* config.get()
-        if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) {
-          yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope))
-        }
-
         if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
           // This only exist for backwards compatibility. We should not be
           // manually publishing this event; it is a sync event now
@@ -422,25 +412,6 @@ export namespace Session {
         return fromRow(row)
       })
 
-      const share = Effect.fn("Session.share")(function* (id: SessionID) {
-        const cfg = yield* config.get()
-        if (cfg.share === "disabled") throw new Error("Sharing is disabled in configuration")
-        const result = yield* Effect.promise(async () => {
-          const { ShareNext } = await import("@/share/share-next")
-          return ShareNext.create(id)
-        })
-        yield* Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: result.url } } }))
-        return result
-      })
-
-      const unshare = Effect.fn("Session.unshare")(function* (id: SessionID) {
-        yield* Effect.promise(async () => {
-          const { ShareNext } = await import("@/share/share-next")
-          await ShareNext.remove(id)
-        })
-        yield* Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: null } } }))
-      })
-
       const children = Effect.fn("Session.children")(function* (parentID: SessionID) {
         const ctx = yield* InstanceState.context
         const rows = yield* db((d) =>
@@ -460,7 +431,6 @@ export namespace Session {
           for (const child of kids) {
             yield* remove(child.id)
           }
-          yield* unshare(sessionID).pipe(Effect.ignore)
           yield* Effect.sync(() => {
             SyncEvent.run(Event.Deleted, { sessionID, info: session })
             SyncEvent.remove(sessionID)
@@ -661,8 +631,6 @@ export namespace Session {
         fork,
         touch,
         get,
-        share,
-        unshare,
         setTitle,
         setArchived,
         setPermission,
@@ -683,7 +651,7 @@ export namespace Session {
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer))
+  export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
 
   const { runPromise } = makeRuntime(Service, defaultLayer)
 
@@ -704,8 +672,6 @@ export namespace Session {
   )
 
   export const get = fn(SessionID.zod, (id) => runPromise((svc) => svc.get(id)))
-  export const share = fn(SessionID.zod, (id) => runPromise((svc) => svc.share(id)))
-  export const unshare = fn(SessionID.zod, (id) => runPromise((svc) => svc.unshare(id)))
 
   export const setTitle = fn(z.object({ sessionID: SessionID.zod, title: z.string() }), (input) =>
     runPromise((svc) => svc.setTitle(input)),

+ 67 - 0
packages/opencode/src/share/session.ts

@@ -0,0 +1,67 @@
+import { makeRuntime } from "@/effect/run-service"
+import { Session } from "@/session"
+import { SessionID } from "@/session/schema"
+import { SyncEvent } from "@/sync"
+import { fn } from "@/util/fn"
+import { Effect, Layer, Scope, ServiceMap } from "effect"
+import { Config } from "../config/config"
+import { Flag } from "../flag/flag"
+import { ShareNext } from "./share-next"
+
+export namespace SessionShare {
+  export interface Interface {
+    readonly create: (input?: Parameters<typeof Session.create>[0]) => Effect.Effect<Session.Info>
+    readonly share: (sessionID: SessionID) => Effect.Effect<{ url: string }, unknown>
+    readonly unshare: (sessionID: SessionID) => Effect.Effect<void, unknown>
+  }
+
+  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionShare") {}
+
+  export const layer = Layer.effect(
+    Service,
+    Effect.gen(function* () {
+      const cfg = yield* Config.Service
+      const session = yield* Session.Service
+      const shareNext = yield* ShareNext.Service
+      const scope = yield* Scope.Scope
+
+      const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
+        const conf = yield* cfg.get()
+        if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
+        const result = yield* shareNext.create(sessionID)
+        yield* Effect.sync(() =>
+          SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } }),
+        )
+        return result
+      })
+
+      const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
+        yield* shareNext.remove(sessionID)
+        yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } }))
+      })
+
+      const create = Effect.fn("SessionShare.create")(function* (input?: Parameters<typeof Session.create>[0]) {
+        const result = yield* session.create(input)
+        if (result.parentID) return result
+        const conf = yield* cfg.get()
+        if (!(Flag.OPENCODE_AUTO_SHARE || conf.share === "auto")) return result
+        yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope))
+        return result
+      })
+
+      return Service.of({ create, share, unshare })
+    }),
+  )
+
+  export const defaultLayer = layer.pipe(
+    Layer.provide(ShareNext.defaultLayer),
+    Layer.provide(Session.defaultLayer),
+    Layer.provide(Config.defaultLayer),
+  )
+
+  const { runPromise } = makeRuntime(Service, defaultLayer)
+
+  export const create = fn(Session.create.schema, (input) => runPromise((svc) => svc.create(input)))
+  export const share = fn(SessionID.zod, (sessionID) => runPromise((svc) => svc.share(sessionID)))
+  export const unshare = fn(SessionID.zod, (sessionID) => runPromise((svc) => svc.unshare(sessionID)))
+}

+ 5 - 1
packages/opencode/src/share/share-next.ts

@@ -159,7 +159,10 @@ export namespace ShareNext {
 
           if (disabled) return cache
 
-          const watch = <D extends { type: string }>(def: D, fn: (evt: { properties: any }) => Effect.Effect<void>) =>
+          const watch = <D extends { type: string }>(
+            def: D,
+            fn: (evt: { properties: any }) => Effect.Effect<void, unknown>,
+          ) =>
             bus.subscribe(def as never).pipe(
               Stream.runForEach((evt) =>
                 fn(evt).pipe(
@@ -194,6 +197,7 @@ export namespace ShareNext {
           yield* watch(Session.Event.Diff, (evt) =>
             sync(evt.properties.sessionID, [{ type: "session_diff", data: evt.properties.diff }]),
           )
+          yield* watch(Session.Event.Deleted, (evt) => remove(evt.properties.sessionID))
 
           return cache
         }),

+ 17 - 0
specs/v2/session.md

@@ -0,0 +1,17 @@
+# Session API
+
+## Remove Dedicated `session.init` Route
+
+The dedicated `POST /session/:sessionID/init` endpoint exists only as a compatibility wrapper around the normal `/init` command flow.
+
+Current behavior:
+
+- the route calls `SessionPrompt.command(...)`
+- it sends `Command.Default.INIT`
+- it does not provide distinct session-core behavior beyond running the existing init command in an existing session
+
+V2 plan:
+
+- remove the dedicated `session.init` endpoint
+- rely on the normal `/init` command flow instead
+- avoid reintroducing `Session.initialize`-style special cases in the session service layer