Skip to content

Commit 16c60c9

Browse files
authored
refactor(session): extract sharing orchestration (#21759)
1 parent 0970b10 commit 16c60c9

6 files changed

Lines changed: 100 additions & 44 deletions

File tree

packages/opencode/src/cli/cmd/github.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { cmd } from "./cmd"
2121
import { ModelsDev } from "../../provider/models"
2222
import { Instance } from "@/project/instance"
2323
import { bootstrap } from "../bootstrap"
24+
import { SessionShare } from "@/share/session"
2425
import { Session } from "../../session"
2526
import type { SessionID } from "../../session/schema"
2627
import { MessageID, PartID } from "../../session/schema"
@@ -559,7 +560,7 @@ export const GithubRunCommand = cmd({
559560
shareId = await (async () => {
560561
if (share === false) return
561562
if (!share && repoData.data.private) return
562-
await Session.share(session.id)
563+
await SessionShare.share(session.id)
563564
return session.id.slice(-8)
564565
})()
565566
console.log("opencode session", session.id)

packages/opencode/src/server/routes/session.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SessionPrompt } from "../../session/prompt"
99
import { SessionRunState } from "@/session/run-state"
1010
import { SessionCompaction } from "../../session/compaction"
1111
import { SessionRevert } from "../../session/revert"
12+
import { SessionShare } from "@/share/session"
1213
import { SessionStatus } from "@/session/status"
1314
import { SessionSummary } from "@/session/summary"
1415
import { Todo } from "../../session/todo"
@@ -206,10 +207,10 @@ export const SessionRoutes = lazy(() =>
206207
},
207208
},
208209
}),
209-
validator("json", Session.create.schema.optional()),
210+
validator("json", Session.create.schema),
210211
async (c) => {
211212
const body = c.req.valid("json") ?? {}
212-
const session = await Session.create(body)
213+
const session = await SessionShare.create(body)
213214
return c.json(session)
214215
},
215216
)
@@ -426,7 +427,7 @@ export const SessionRoutes = lazy(() =>
426427
),
427428
async (c) => {
428429
const sessionID = c.req.valid("param").sessionID
429-
await Session.share(sessionID)
430+
await SessionShare.share(sessionID)
430431
const session = await Session.get(sessionID)
431432
return c.json(session)
432433
},
@@ -491,12 +492,12 @@ export const SessionRoutes = lazy(() =>
491492
validator(
492493
"param",
493494
z.object({
494-
sessionID: Session.unshare.schema,
495+
sessionID: SessionID.zod,
495496
}),
496497
),
497498
async (c) => {
498499
const sessionID = c.req.valid("param").sessionID
499-
await Session.unshare(sessionID)
500+
await SessionShare.unshare(sessionID)
500501
const session = await Session.get(sessionID)
501502
return c.json(session)
502503
},

packages/opencode/src/session/index.ts

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Bus } from "@/bus"
55
import { Decimal } from "decimal.js"
66
import z from "zod"
77
import { type ProviderMetadata } from "ai"
8-
import { Config } from "../config/config"
98
import { Flag } from "../flag/flag"
109
import { Installation } from "../installation"
1110

@@ -30,7 +29,7 @@ import type { Provider } from "@/provider/provider"
3029
import { Permission } from "@/permission"
3130
import { Global } from "@/global"
3231
import type { LanguageModelV2Usage } from "@ai-sdk/provider"
33-
import { Effect, Layer, Scope, ServiceMap } from "effect"
32+
import { Effect, Layer, ServiceMap } from "effect"
3433
import { makeRuntime } from "@/effect/run-service"
3534

3635
export namespace Session {
@@ -319,8 +318,6 @@ export namespace Session {
319318
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info>
320319
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
321320
readonly get: (id: SessionID) => Effect.Effect<Info>
322-
readonly share: (id: SessionID) => Effect.Effect<{ url: string }>
323-
readonly unshare: (id: SessionID) => Effect.Effect<void>
324321
readonly setTitle: (input: { sessionID: SessionID; title: string }) => Effect.Effect<void>
325322
readonly setArchived: (input: { sessionID: SessionID; time?: number }) => Effect.Effect<void>
326323
readonly setPermission: (input: { sessionID: SessionID; permission: Permission.Ruleset }) => Effect.Effect<void>
@@ -364,12 +361,10 @@ export namespace Session {
364361
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
365362
Effect.sync(() => Database.use(fn))
366363

367-
export const layer: Layer.Layer<Service, never, Bus.Service | Config.Service> = Layer.effect(
364+
export const layer: Layer.Layer<Service, never, Bus.Service> = Layer.effect(
368365
Service,
369366
Effect.gen(function* () {
370367
const bus = yield* Bus.Service
371-
const config = yield* Config.Service
372-
const scope = yield* Scope.Scope
373368

374369
const createNext = Effect.fn("Session.createNext")(function* (input: {
375370
id?: SessionID
@@ -399,11 +394,6 @@ export namespace Session {
399394

400395
yield* Effect.sync(() => SyncEvent.run(Event.Created, { sessionID: result.id, info: result }))
401396

402-
const cfg = yield* config.get()
403-
if (!result.parentID && (Flag.OPENCODE_AUTO_SHARE || cfg.share === "auto")) {
404-
yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope))
405-
}
406-
407397
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
408398
// This only exist for backwards compatibility. We should not be
409399
// manually publishing this event; it is a sync event now
@@ -422,25 +412,6 @@ export namespace Session {
422412
return fromRow(row)
423413
})
424414

425-
const share = Effect.fn("Session.share")(function* (id: SessionID) {
426-
const cfg = yield* config.get()
427-
if (cfg.share === "disabled") throw new Error("Sharing is disabled in configuration")
428-
const result = yield* Effect.promise(async () => {
429-
const { ShareNext } = await import("@/share/share-next")
430-
return ShareNext.create(id)
431-
})
432-
yield* Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: result.url } } }))
433-
return result
434-
})
435-
436-
const unshare = Effect.fn("Session.unshare")(function* (id: SessionID) {
437-
yield* Effect.promise(async () => {
438-
const { ShareNext } = await import("@/share/share-next")
439-
await ShareNext.remove(id)
440-
})
441-
yield* Effect.sync(() => SyncEvent.run(Event.Updated, { sessionID: id, info: { share: { url: null } } }))
442-
})
443-
444415
const children = Effect.fn("Session.children")(function* (parentID: SessionID) {
445416
const ctx = yield* InstanceState.context
446417
const rows = yield* db((d) =>
@@ -460,7 +431,6 @@ export namespace Session {
460431
for (const child of kids) {
461432
yield* remove(child.id)
462433
}
463-
yield* unshare(sessionID).pipe(Effect.ignore)
464434
yield* Effect.sync(() => {
465435
SyncEvent.run(Event.Deleted, { sessionID, info: session })
466436
SyncEvent.remove(sessionID)
@@ -661,8 +631,6 @@ export namespace Session {
661631
fork,
662632
touch,
663633
get,
664-
share,
665-
unshare,
666634
setTitle,
667635
setArchived,
668636
setPermission,
@@ -683,7 +651,7 @@ export namespace Session {
683651
}),
684652
)
685653

686-
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer))
654+
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
687655

688656
const { runPromise } = makeRuntime(Service, defaultLayer)
689657

@@ -704,8 +672,6 @@ export namespace Session {
704672
)
705673

706674
export const get = fn(SessionID.zod, (id) => runPromise((svc) => svc.get(id)))
707-
export const share = fn(SessionID.zod, (id) => runPromise((svc) => svc.share(id)))
708-
export const unshare = fn(SessionID.zod, (id) => runPromise((svc) => svc.unshare(id)))
709675

710676
export const setTitle = fn(z.object({ sessionID: SessionID.zod, title: z.string() }), (input) =>
711677
runPromise((svc) => svc.setTitle(input)),
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { makeRuntime } from "@/effect/run-service"
2+
import { Session } from "@/session"
3+
import { SessionID } from "@/session/schema"
4+
import { SyncEvent } from "@/sync"
5+
import { fn } from "@/util/fn"
6+
import { Effect, Layer, Scope, ServiceMap } from "effect"
7+
import { Config } from "../config/config"
8+
import { Flag } from "../flag/flag"
9+
import { ShareNext } from "./share-next"
10+
11+
export namespace SessionShare {
12+
export interface Interface {
13+
readonly create: (input?: Parameters<typeof Session.create>[0]) => Effect.Effect<Session.Info>
14+
readonly share: (sessionID: SessionID) => Effect.Effect<{ url: string }, unknown>
15+
readonly unshare: (sessionID: SessionID) => Effect.Effect<void, unknown>
16+
}
17+
18+
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionShare") {}
19+
20+
export const layer = Layer.effect(
21+
Service,
22+
Effect.gen(function* () {
23+
const cfg = yield* Config.Service
24+
const session = yield* Session.Service
25+
const shareNext = yield* ShareNext.Service
26+
const scope = yield* Scope.Scope
27+
28+
const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
29+
const conf = yield* cfg.get()
30+
if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
31+
const result = yield* shareNext.create(sessionID)
32+
yield* Effect.sync(() =>
33+
SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: result.url } } }),
34+
)
35+
return result
36+
})
37+
38+
const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
39+
yield* shareNext.remove(sessionID)
40+
yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } }))
41+
})
42+
43+
const create = Effect.fn("SessionShare.create")(function* (input?: Parameters<typeof Session.create>[0]) {
44+
const result = yield* session.create(input)
45+
if (result.parentID) return result
46+
const conf = yield* cfg.get()
47+
if (!(Flag.OPENCODE_AUTO_SHARE || conf.share === "auto")) return result
48+
yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope))
49+
return result
50+
})
51+
52+
return Service.of({ create, share, unshare })
53+
}),
54+
)
55+
56+
export const defaultLayer = layer.pipe(
57+
Layer.provide(ShareNext.defaultLayer),
58+
Layer.provide(Session.defaultLayer),
59+
Layer.provide(Config.defaultLayer),
60+
)
61+
62+
const { runPromise } = makeRuntime(Service, defaultLayer)
63+
64+
export const create = fn(Session.create.schema, (input) => runPromise((svc) => svc.create(input)))
65+
export const share = fn(SessionID.zod, (sessionID) => runPromise((svc) => svc.share(sessionID)))
66+
export const unshare = fn(SessionID.zod, (sessionID) => runPromise((svc) => svc.unshare(sessionID)))
67+
}

packages/opencode/src/share/share-next.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ export namespace ShareNext {
159159

160160
if (disabled) return cache
161161

162-
const watch = <D extends { type: string }>(def: D, fn: (evt: { properties: any }) => Effect.Effect<void>) =>
162+
const watch = <D extends { type: string }>(
163+
def: D,
164+
fn: (evt: { properties: any }) => Effect.Effect<void, unknown>,
165+
) =>
163166
bus.subscribe(def as never).pipe(
164167
Stream.runForEach((evt) =>
165168
fn(evt).pipe(
@@ -194,6 +197,7 @@ export namespace ShareNext {
194197
yield* watch(Session.Event.Diff, (evt) =>
195198
sync(evt.properties.sessionID, [{ type: "session_diff", data: evt.properties.diff }]),
196199
)
200+
yield* watch(Session.Event.Deleted, (evt) => remove(evt.properties.sessionID))
197201

198202
return cache
199203
}),

specs/v2/session.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Session API
2+
3+
## Remove Dedicated `session.init` Route
4+
5+
The dedicated `POST /session/:sessionID/init` endpoint exists only as a compatibility wrapper around the normal `/init` command flow.
6+
7+
Current behavior:
8+
9+
- the route calls `SessionPrompt.command(...)`
10+
- it sends `Command.Default.INIT`
11+
- it does not provide distinct session-core behavior beyond running the existing init command in an existing session
12+
13+
V2 plan:
14+
15+
- remove the dedicated `session.init` endpoint
16+
- rely on the normal `/init` command flow instead
17+
- avoid reintroducing `Session.initialize`-style special cases in the session service layer

0 commit comments

Comments
 (0)