Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/client/src/promise/generated/client.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/client/src/promise/generated/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/client/src/promise/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ export type {
SkillApi,
} from "./api.js"
export type { EventSubscribeOutput as OpenCodeEvent } from "./generated/types.js"
export type {
MediaFileAttachment as PromptMediaFileAttachment,
Attachment as PromptAttachment,
} from "@opencode/schema/prompt"
export type OpenCodeClient = ReturnType<typeof OpenCode.make>
3 changes: 2 additions & 1 deletion packages/client/test/promise.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.vcs)).toEqual(["init", "get", "base", "status", "branch", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove", "connect"])
expect(Object.keys(client.pty.connect)).toEqual(["token"])
expect(Object.keys(client.experimental)).toEqual(["persistentPty"])
expect(Object.keys(client.experimental).sort()).toEqual(["media", "persistentPty"])
expect(Object.keys(client.experimental.media)).toEqual(["get"])
expect(client.experimental.persistentPty.read).toBeFunction()
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "output", "remove"])
expect(Object.keys(client.project)).toEqual(["list", "update"])
Expand Down
41 changes: 40 additions & 1 deletion packages/core/src/session.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * as Session from "./session.js"
export * from "./session/schema.js"

import { DateTime, Effect, Layer, Context, Stream } from "effect"
import { DateTime, Effect, Layer, Context, Option, Stream } from "effect"
import { LLMClient } from "@opencode/ai/effect"
import { ListAnchor, ListCursor, ListFilter } from "@opencode/schema/session"
import { and, desc, eq } from "drizzle-orm"
Expand All @@ -16,6 +16,7 @@ import { Instance } from "./instance/service.js"
import { Database } from "./database/database.js"
import { SessionProjector } from "./session/projector.js"
import { SessionMessageTable } from "./session/sql.js"
import { SessionBlob } from "./session/blob.js"
import { SessionSchema } from "./session/schema.js"
import { RelativePath } from "./schema.js"
import { Agent } from "@opencode/schema/agent"
Expand Down Expand Up @@ -147,6 +148,15 @@ export interface Interface {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
}) => Effect.Effect<SessionMessage.Info | undefined>
/** Experimental: {@link Interface.messages} with files stored in `blob` left as `media:` refs when the session's location enables `experimental.media_blob`. */
readonly messagesWithRefs: (
input: SessionStore.MessagesInput,
) => Effect.Effect<SessionMessage.MediaInfo[], NotFoundError | MessageDecodeError>
/** Experimental: {@link Interface.message} with files stored in `blob` left as `media:` refs when the session's location enables `experimental.media_blob`. */
readonly messageWithRefs: (input: {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
}) => Effect.Effect<SessionMessage.MediaInfo | undefined>
readonly context: (
sessionID: SessionSchema.ID,
) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
Expand All @@ -166,6 +176,8 @@ export interface Interface {
* unhandled compaction barriers.
*/
readonly inbox: (sessionID: SessionSchema.ID) => Effect.Effect<SessionInbox.Info[], NotFoundError>
/** Experimental: {@link Interface.inbox} with files stored in `blob` left as `media:` refs when the session's location enables `experimental.media_blob`. */
readonly inboxWithRefs: (sessionID: SessionSchema.ID) => Effect.Effect<SessionInbox.MediaInfo[], NotFoundError>
readonly cancelInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
readonly steerInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
readonly queueInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
Expand Down Expand Up @@ -268,6 +280,16 @@ const layer = Layer.effect(
const environments = yield* SessionEnvironment.Service
const locations = yield* LocationServiceMap.Service
const sessions = yield* Session.make()
// `experimental.media_blob` as the session's own location configures it, where its media is written.
const keepsMediaRefs = (session: SessionSchema.Info) =>
SessionBlob.enabled.pipe(
instances.provide(session),
Effect.catch((cause) =>
Effect.logWarning("could not resolve experimental.media_blob; serving media inline", { cause }).pipe(
Effect.as(false),
),
),
)
const isDurableSessionEvent = (item: Bus.LogItem): item is SessionEvent.DurableEvent =>
Object.hasOwn(SessionEvent.Durable.cases, item.type)

Expand Down Expand Up @@ -413,6 +435,18 @@ const layer = Layer.effect(
return yield* store.messages(input)
}),
message: (input) => sessions.forSession(input.sessionID).message(input.messageID),
messagesWithRefs: Effect.fn("Session.messagesWithRefs")(function* (input) {
const session = yield* result.get(input.sessionID)
if (!(yield* keepsMediaRefs(session))) return yield* store.messages(input)
return yield* store.messagesWithRefs(input)
}),
messageWithRefs: Effect.fn("Session.messageWithRefs")(function* (input) {
const session = yield* result.get(input.sessionID).pipe(Effect.option)
if (Option.isNone(session)) return undefined
if (!(yield* keepsMediaRefs(session.value))) return yield* result.message(input)
const stored = yield* store.messageWithRefs(input.messageID)
return stored?.sessionID === input.sessionID ? stored.message : undefined
}),
context: Effect.fn("Session.context")(function* (sessionID) {
yield* result.get(sessionID)
return yield* store.context(sessionID)
Expand All @@ -429,6 +463,11 @@ const layer = Layer.effect(
})
}),
inbox: (sessionID) => sessions.forSession(sessionID).inbox(),
inboxWithRefs: Effect.fn("Session.inboxWithRefs")(function* (sessionID) {
const session = yield* result.get(sessionID)
if (!(yield* keepsMediaRefs(session))) return yield* result.inbox(sessionID)
return yield* SessionInbox.listWithRefs(db, sessionID)
}),
cancelInbox: (input) => sessions.forSession(input.sessionID).cancelInbox(input.inboxID),
steerInbox: (input) => sessions.forSession(input.sessionID).steerInbox(input.inboxID),
queueInbox: (input) => sessions.forSession(input.sessionID).queueInbox(input.inboxID),
Expand Down
Loading
Loading