import { Session } from "@opencode/schema/session" import { SessionMessage } from "@opencode/schema/session-message" import { Location } from "@opencode/schema/location" import { Schema, Struct } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors.js" export const SessionMessagesQuery = Schema.Struct({ limit: Schema.optional( Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)), ).annotate({ description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size.", }), order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({ description: "Message order for the first page. Use desc for newest first or asc for oldest first.", }), cursor: Schema.optional( Schema.String.annotate({ description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.", }), ), type: Schema.optional( Schema.Literals([ "agent-switched", "model-switched", "location-switched", "user", "synthetic", "system", "skill", "shell", "assistant", "compaction", ] satisfies ReadonlyArray), ).annotate({ description: "Filter by message type before pagination. When omitted, all message types are returned. Pass the same type when following cursors.", }), }).annotate({ identifier: "SessionMessagesQuery" }) const PublicLocationSwitched = Schema.Struct({ ...Struct.omit(SessionMessage.LocationSwitched.fields, ["location", "previous"]), location: Location.PublicRef, previous: Schema.Struct({ location: Location.PublicRef, projectID: SessionMessage.LocationSwitched.fields.projectID, subpath: SessionMessage.LocationSwitched.fields.subpath, }).pipe(Schema.optional), }).annotate({ identifier: "Session.Message.LocationSwitched" }) export const PublicSessionMessage = Schema.Union([ SessionMessage.AgentSelected, SessionMessage.ModelSelected, PublicLocationSwitched, SessionMessage.User, SessionMessage.Synthetic, SessionMessage.System, SessionMessage.Skill, SessionMessage.Shell, SessionMessage.Assistant, SessionMessage.Compaction, SessionMessage.Idle, ]).annotate({ identifier: "Session.Message.Info" }) export const MessageGroup = HttpApiGroup.make("server.message") .add( HttpApiEndpoint.get("session.messages", "/api/session/:sessionID/message", { params: { sessionID: Session.ID }, query: SessionMessagesQuery, success: Schema.Struct({ data: Schema.Array(PublicSessionMessage), cursor: Schema.Struct({ previous: Schema.String.pipe(Schema.optional), next: Schema.String.pipe(Schema.optional), }), }).annotate({ identifier: "SessionMessagesResponse" }), error: [InvalidCursorError, SessionNotFoundError, UnknownError], }).annotateMerge( OpenApi.annotations({ identifier: "session.message.list", summary: "Get session messages", description: "Retrieve projected messages for a session, optionally filtered by type. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline, passing the same type filter on each page.", }), ), ) .annotateMerge( OpenApi.annotations({ title: "session", description: "Experimental message routes.", }), )