mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-20 21:00:29 +08:00
refactor(session): extract sharing orchestration (#21759)
This commit is contained in:
@@ -21,6 +21,7 @@ import { cmd } from "./cmd"
|
|||||||
import { ModelsDev } from "../../provider/models"
|
import { ModelsDev } from "../../provider/models"
|
||||||
import { Instance } from "@/project/instance"
|
import { Instance } from "@/project/instance"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
|
import { SessionShare } from "@/share/session"
|
||||||
import { Session } from "../../session"
|
import { Session } from "../../session"
|
||||||
import type { SessionID } from "../../session/schema"
|
import type { SessionID } from "../../session/schema"
|
||||||
import { MessageID, PartID } from "../../session/schema"
|
import { MessageID, PartID } from "../../session/schema"
|
||||||
@@ -559,7 +560,7 @@ export const GithubRunCommand = cmd({
|
|||||||
shareId = await (async () => {
|
shareId = await (async () => {
|
||||||
if (share === false) return
|
if (share === false) return
|
||||||
if (!share && repoData.data.private) return
|
if (!share && repoData.data.private) return
|
||||||
await Session.share(session.id)
|
await SessionShare.share(session.id)
|
||||||
return session.id.slice(-8)
|
return session.id.slice(-8)
|
||||||
})()
|
})()
|
||||||
console.log("opencode session", session.id)
|
console.log("opencode session", session.id)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { SessionPrompt } from "../../session/prompt"
|
|||||||
import { SessionRunState } from "@/session/run-state"
|
import { SessionRunState } from "@/session/run-state"
|
||||||
import { SessionCompaction } from "../../session/compaction"
|
import { SessionCompaction } from "../../session/compaction"
|
||||||
import { SessionRevert } from "../../session/revert"
|
import { SessionRevert } from "../../session/revert"
|
||||||
|
import { SessionShare } from "@/share/session"
|
||||||
import { SessionStatus } from "@/session/status"
|
import { SessionStatus } from "@/session/status"
|
||||||
import { SessionSummary } from "@/session/summary"
|
import { SessionSummary } from "@/session/summary"
|
||||||
import { Todo } from "../../session/todo"
|
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) => {
|
async (c) => {
|
||||||
const body = c.req.valid("json") ?? {}
|
const body = c.req.valid("json") ?? {}
|
||||||
const session = await Session.create(body)
|
const session = await SessionShare.create(body)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -426,7 +427,7 @@ export const SessionRoutes = lazy(() =>
|
|||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
await Session.share(sessionID)
|
await SessionShare.share(sessionID)
|
||||||
const session = await Session.get(sessionID)
|
const session = await Session.get(sessionID)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
@@ -491,12 +492,12 @@ export const SessionRoutes = lazy(() =>
|
|||||||
validator(
|
validator(
|
||||||
"param",
|
"param",
|
||||||
z.object({
|
z.object({
|
||||||
sessionID: Session.unshare.schema,
|
sessionID: SessionID.zod,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
const sessionID = c.req.valid("param").sessionID
|
||||||
await Session.unshare(sessionID)
|
await SessionShare.unshare(sessionID)
|
||||||
const session = await Session.get(sessionID)
|
const session = await Session.get(sessionID)
|
||||||
return c.json(session)
|
return c.json(session)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Bus } from "@/bus"
|
|||||||
import { Decimal } from "decimal.js"
|
import { Decimal } from "decimal.js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { type ProviderMetadata } from "ai"
|
import { type ProviderMetadata } from "ai"
|
||||||
import { Config } from "../config/config"
|
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { Installation } from "../installation"
|
import { Installation } from "../installation"
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ import type { Provider } from "@/provider/provider"
|
|||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Global } from "@/global"
|
import { Global } from "@/global"
|
||||||
import type { LanguageModelV2Usage } from "@ai-sdk/provider"
|
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"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
|
|
||||||
export namespace Session {
|
export namespace Session {
|
||||||
@@ -319,8 +318,6 @@ export namespace Session {
|
|||||||
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info>
|
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info>
|
||||||
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
|
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
|
||||||
readonly get: (id: SessionID) => Effect.Effect<Info>
|
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 setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
|
||||||
readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
|
readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
|
||||||
readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => 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) =>
|
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
||||||
Effect.sync(() => Database.use(fn))
|
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,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const bus = yield* Bus.Service
|
const bus = yield* Bus.Service
|
||||||
const config = yield* Config.Service
|
|
||||||
const scope = yield* Scope.Scope
|
|
||||||
|
|
||||||
const createNext = Effect.fn("Session.createNext")(function* (input: {
|
const createNext = Effect.fn("Session.createNext")(function* (input: {
|
||||||
id?: SessionID
|
id?: SessionID
|
||||||
@@ -399,11 +394,6 @@ export namespace Session {
|
|||||||
|
|
||||||
yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result }))
|
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) {
|
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||||
// This only exist for backwards compatibility. We should not be
|
// This only exist for backwards compatibility. We should not be
|
||||||
// manually publishing this event; it is a sync event now
|
// manually publishing this event; it is a sync event now
|
||||||
@@ -422,25 +412,6 @@ export namespace Session {
|
|||||||
return fromRow(row)
|
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 children = Effect.fn("Session.children")(function* (parentID: SessionID) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
const rows = yield* db((d) =>
|
const rows = yield* db((d) =>
|
||||||
@@ -460,7 +431,6 @@ export namespace Session {
|
|||||||
for (const child of kids) {
|
for (const child of kids) {
|
||||||
yield* remove(child.id)
|
yield* remove(child.id)
|
||||||
}
|
}
|
||||||
yield* unshare(sessionID).pipe(Effect.ignore)
|
|
||||||
yield* Effect.sync(() => {
|
yield* Effect.sync(() => {
|
||||||
SyncEvent.run(Event.Deleted, { sessionID, info: session })
|
SyncEvent.run(Event.Deleted, { sessionID, info: session })
|
||||||
SyncEvent.remove(sessionID)
|
SyncEvent.remove(sessionID)
|
||||||
@@ -661,8 +631,6 @@ export namespace Session {
|
|||||||
fork,
|
fork,
|
||||||
touch,
|
touch,
|
||||||
get,
|
get,
|
||||||
share,
|
|
||||||
unshare,
|
|
||||||
setTitle,
|
setTitle,
|
||||||
setArchived,
|
setArchived,
|
||||||
setPermission,
|
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)
|
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 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) =>
|
export const setTitle = fn(z.object({ sessionID: SessionID.zod, title: z.string() }), (input) =>
|
||||||
runPromise((svc) => svc.setTitle(input)),
|
runPromise((svc) => svc.setTitle(input)),
|
||||||
|
|||||||
67
packages/opencode/src/share/session.ts
Normal file
67
packages/opencode/src/share/session.ts
Normal file
@@ -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)))
|
||||||
|
}
|
||||||
@@ -159,7 +159,10 @@ export namespace ShareNext {
|
|||||||
|
|
||||||
if (disabled) return cache
|
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(
|
bus.subscribe(def as never).pipe(
|
||||||
Stream.runForEach((evt) =>
|
Stream.runForEach((evt) =>
|
||||||
fn(evt).pipe(
|
fn(evt).pipe(
|
||||||
@@ -194,6 +197,7 @@ export namespace ShareNext {
|
|||||||
yield* watch(Session.Event.Diff, (evt) =>
|
yield* watch(Session.Event.Diff, (evt) =>
|
||||||
sync(evt.properties.sessionID, [{ type: "session_diff", data: evt.properties.diff }]),
|
sync(evt.properties.sessionID, [{ type: "session_diff", data: evt.properties.diff }]),
|
||||||
)
|
)
|
||||||
|
yield* watch(Session.Event.Deleted, (evt) => remove(evt.properties.sessionID))
|
||||||
|
|
||||||
return cache
|
return cache
|
||||||
}),
|
}),
|
||||||
|
|||||||
17
specs/v2/session.md
Normal file
17
specs/v2/session.md
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user