diff --git a/packages/client/src/generated/types.ts b/packages/client/src/generated/types.ts index f426a50f87..bfd965ae35 100644 --- a/packages/client/src/generated/types.ts +++ b/packages/client/src/generated/types.ts @@ -305,7 +305,6 @@ export type SessionsPromptInput = { readonly text: string readonly files?: ReadonlyArray<{ readonly uri: string - readonly mime: string readonly name?: string readonly description?: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } @@ -324,7 +323,6 @@ export type SessionsPromptInput = { readonly text: string readonly files?: ReadonlyArray<{ readonly uri: string - readonly mime: string readonly name?: string readonly description?: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } @@ -343,7 +341,6 @@ export type SessionsPromptInput = { readonly text: string readonly files?: ReadonlyArray<{ readonly uri: string - readonly mime: string readonly name?: string readonly description?: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } @@ -362,7 +359,6 @@ export type SessionsPromptInput = { readonly text: string readonly files?: ReadonlyArray<{ readonly uri: string - readonly mime: string readonly name?: string readonly description?: string readonly source?: { readonly start: number; readonly end: number; readonly text: string } diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 97745b41bf..4ce97e133a 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -10,6 +10,7 @@ import { ModelV2 } from "./model" import { Location } from "./location" import { SessionMessage } from "./session/message" import { Prompt } from "./session/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" import { EventV2 } from "./event" import { Database } from "./database/database" import { SessionProjector } from "./session/projector" @@ -32,6 +33,7 @@ import { SessionInput } from "./session/input" import { Snapshot } from "./snapshot" import { SessionRevert } from "./session/revert" import { Revert } from "@opencode-ai/schema/revert" +import { FSUtil } from "./fs-util" export const RevertState = Revert.State export type RevertState = Revert.State @@ -137,7 +139,7 @@ export interface Interface { readonly prompt: (input: { id?: SessionMessage.ID sessionID: SessionSchema.ID - prompt: Prompt + prompt: PromptInput.Prompt delivery?: SessionInput.Delivery resume?: boolean }) => Effect.Effect @@ -349,13 +351,14 @@ export const layer = Layer.unwrap( Effect.uninterruptible( Effect.gen(function* () { yield* result.get(input.sessionID) + const prompt = resolvePrompt(input.prompt) const messageID = input.id ?? SessionMessage.ID.create() const delivery = input.delivery ?? "steer" - const expected = { sessionID: input.sessionID, messageID, prompt: input.prompt, delivery } + const expected = { sessionID: input.sessionID, messageID, prompt, delivery } const admitted = yield* SessionInput.admit(db, events, { id: messageID, sessionID: input.sessionID, - prompt: input.prompt, + prompt, delivery, }).pipe( Effect.catchDefect((defect) => @@ -452,3 +455,17 @@ export const defaultLayer = layer.pipe( Layer.provide(ProjectV2.defaultLayer), Layer.orDie, ) + +const resolvePrompt = (input: PromptInput.Prompt) => + Prompt.make({ + text: input.text, + agents: input.agents, + files: input.files?.map((file) => { + const dataMime = file.uri.match(/^data:([^;,]+)[;,]/i)?.[1] + const target = URL.canParse(file.uri) ? new URL(file.uri).pathname : (file.name ?? file.uri) + return { + ...file, + mime: dataMime ?? (target.endsWith("/") ? "application/x-directory" : FSUtil.mimeType(target)), + } + }), + }) diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 9ff0e22490..d60f9f1ccf 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -173,6 +173,27 @@ describe("SessionV2.prompt", () => { }), ) + it.effect("resolves attachment MIME before admission", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + + const message = yield* session.prompt({ + sessionID, + prompt: { + text: "Inspect this image", + files: [{ uri: "data:image/png;base64,aGVsbG8=", name: "image.png" }], + }, + resume: false, + }) + + expect(message.prompt.files).toEqual([ + { uri: "data:image/png;base64,aGVsbG8=", name: "image.png", mime: "image/png" }, + ]) + expect((yield* admitted(message.id))?.prompt.files).toEqual(message.prompt.files) + }), + ) + it.effect("streams durable Session events after an aggregate sequence", () => Effect.gen(function* () { yield* setup diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index f1f908e6ac..057e08d7c7 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -1,6 +1,6 @@ import { SessionMessage } from "@opencode-ai/schema/session-message" import { SessionInput } from "@opencode-ai/schema/session-input" -import { Prompt } from "@opencode-ai/schema/prompt" +import { PromptInput } from "@opencode-ai/schema/prompt-input" import { Session } from "@opencode-ai/schema/session" import { Project } from "@opencode-ai/schema/project" import { AbsolutePath, NonNegativeInt, PositiveInt, RelativePath, statics } from "@opencode-ai/schema/schema" @@ -192,7 +192,7 @@ export const makeSessionGroup = (sessionLo params: { sessionID: Session.ID }, payload: Schema.Struct({ id: SessionMessage.ID.pipe(Schema.optional), - prompt: Prompt, + prompt: PromptInput.Prompt, delivery: SessionInput.Delivery.pipe(Schema.optional), resume: Schema.Boolean.pipe(Schema.optional), }), diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 3cde8b8a02..b7c8e5110f 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -24,4 +24,5 @@ export { PtyTicket } from "./pty-ticket" export { Question } from "./question" export { Workspace } from "./workspace" export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt" +export { PromptInput } from "./prompt-input" export * from "./schema" diff --git a/packages/schema/src/prompt-input.ts b/packages/schema/src/prompt-input.ts new file mode 100644 index 0000000000..f2a0d460df --- /dev/null +++ b/packages/schema/src/prompt-input.ts @@ -0,0 +1,26 @@ +export * as PromptInput from "./prompt-input" + +import { Schema } from "effect" +import { AgentAttachment, Source } from "./prompt" +import { optional, statics } from "./schema" + +export interface FileAttachment extends Schema.Schema.Type {} +export const FileAttachment = Schema.Struct({ + uri: Schema.String, + name: Schema.String.pipe(optional), + description: Schema.String.pipe(optional), + source: Source.pipe(optional), +}) + .annotate({ identifier: "PromptInput.FileAttachment" }) + .pipe( + statics((schema) => ({ + create: (input: FileAttachment) => schema.make(input), + })), + ) + +export interface Prompt extends Schema.Schema.Type {} +export const Prompt = Schema.Struct({ + text: Schema.String, + files: Schema.Array(FileAttachment).pipe(optional), + agents: Schema.Array(AgentAttachment).pipe(optional), +}).annotate({ identifier: "PromptInput" }) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9beb809922..73fb662271 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -142,7 +142,7 @@ import type { ProjectListResponses, ProjectUpdateErrors, ProjectUpdateResponses, - Prompt, + PromptInput, ProviderAuthErrors, ProviderAuthResponses, ProviderListErrors, @@ -5621,7 +5621,7 @@ export class Session3 extends HeyApiClient { parameters: { sessionID: string id?: string - prompt?: Prompt + prompt?: PromptInput delivery?: "steer" | "queue" resume?: boolean }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ae610a3f12..98335fcab0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2700,6 +2700,12 @@ export type SessionNotFoundError = { message: string } +export type PromptInput = { + text: string + files?: Array + agents?: Array +} + export type ConflictError = { _tag: "ConflictError" message: string @@ -3814,6 +3820,13 @@ export type SessionV2Info = { revert?: RevertState } +export type PromptInputFileAttachment = { + uri: string + name?: string + description?: string + source?: PromptSource +} + export type SessionInputAdmitted = { admittedSeq: number id: string @@ -12091,7 +12104,7 @@ export type V2SessionSwitchModelResponse = V2SessionSwitchModelResponses[keyof V export type V2SessionPromptData = { body: { id?: string - prompt: Prompt + prompt: PromptInput delivery?: "steer" | "queue" resume?: boolean }