459 lines
20 KiB
TypeScript
459 lines
20 KiB
TypeScript
import { Permission } from "@/permission"
|
|
import { PermissionID } from "@/permission/schema"
|
|
import { ModelID, ProviderID } from "@/provider/schema"
|
|
import { Session } from "@/session/session"
|
|
import { MessageV2 } from "@/session/message-v2"
|
|
import { SessionPrompt } from "@/session/prompt"
|
|
import { SessionRevert } from "@/session/revert"
|
|
import { SessionStatus } from "@/session/status"
|
|
import { SessionSummary } from "@/session/summary"
|
|
import { Todo } from "@/session/todo"
|
|
import { MessageID, PartID, SessionID } from "@/session/schema"
|
|
import { Snapshot } from "@/snapshot"
|
|
import { Schema, Struct } from "effect"
|
|
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
|
import { Authorization } from "../middleware/authorization"
|
|
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
|
import {
|
|
WorkspaceRoutingMiddleware,
|
|
WorkspaceRoutingQuery,
|
|
WorkspaceRoutingQueryFields,
|
|
} from "../middleware/workspace-routing"
|
|
import { ApiNotFoundError, PermissionNotFoundError, SessionBusyError } from "../errors"
|
|
import { described } from "./metadata"
|
|
import { QueryBoolean } from "./query"
|
|
|
|
const root = "/session"
|
|
export const ListQuery = Schema.Struct({
|
|
...WorkspaceRoutingQueryFields,
|
|
scope: Schema.optional(Schema.Literals(["project"])),
|
|
path: Schema.optional(Schema.String),
|
|
roots: Schema.optional(QueryBoolean),
|
|
start: Schema.optional(Schema.NumberFromString),
|
|
search: Schema.optional(Schema.String),
|
|
limit: Schema.optional(Schema.NumberFromString),
|
|
})
|
|
export const DiffQuery = Schema.Struct({
|
|
...WorkspaceRoutingQueryFields,
|
|
...Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]),
|
|
})
|
|
export const MessagesQuery = Schema.Struct({
|
|
...WorkspaceRoutingQueryFields,
|
|
limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
|
|
before: Schema.optional(Schema.String),
|
|
})
|
|
export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
|
|
export const UpdatePayload = Schema.Struct({
|
|
title: Schema.optional(Schema.String),
|
|
permission: Schema.optional(Permission.Ruleset),
|
|
time: Schema.optional(
|
|
Schema.Struct({
|
|
archived: Schema.optional(Session.ArchivedTimestamp),
|
|
}),
|
|
),
|
|
})
|
|
export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
|
|
export const InitPayload = Schema.Struct({
|
|
modelID: ModelID,
|
|
providerID: ProviderID,
|
|
messageID: MessageID,
|
|
})
|
|
export const SummarizePayload = Schema.Struct({
|
|
providerID: ProviderID,
|
|
modelID: ModelID,
|
|
auto: Schema.optional(Schema.Boolean),
|
|
})
|
|
export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"]))
|
|
export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInput.fields, ["sessionID"]))
|
|
export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"]))
|
|
export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"]))
|
|
export const PermissionResponsePayload = Schema.Struct({
|
|
response: Permission.Reply,
|
|
})
|
|
|
|
export const SessionPaths = {
|
|
list: root,
|
|
status: `${root}/status`,
|
|
get: `${root}/:sessionID`,
|
|
children: `${root}/:sessionID/children`,
|
|
todo: `${root}/:sessionID/todo`,
|
|
diff: `${root}/:sessionID/diff`,
|
|
messages: `${root}/:sessionID/message`,
|
|
message: `${root}/:sessionID/message/:messageID`,
|
|
create: root,
|
|
remove: `${root}/:sessionID`,
|
|
update: `${root}/:sessionID`,
|
|
fork: `${root}/:sessionID/fork`,
|
|
abort: `${root}/:sessionID/abort`,
|
|
share: `${root}/:sessionID/share`,
|
|
init: `${root}/:sessionID/init`,
|
|
summarize: `${root}/:sessionID/summarize`,
|
|
prompt: `${root}/:sessionID/message`,
|
|
promptAsync: `${root}/:sessionID/prompt_async`,
|
|
command: `${root}/:sessionID/command`,
|
|
shell: `${root}/:sessionID/shell`,
|
|
revert: `${root}/:sessionID/revert`,
|
|
unrevert: `${root}/:sessionID/unrevert`,
|
|
permissions: `${root}/:sessionID/permissions/:permissionID`,
|
|
deleteMessage: `${root}/:sessionID/message/:messageID`,
|
|
deletePart: `${root}/:sessionID/message/:messageID/part/:partID`,
|
|
updatePart: `${root}/:sessionID/message/:messageID/part/:partID`,
|
|
} as const
|
|
|
|
export const SessionApi = HttpApi.make("session")
|
|
.add(
|
|
HttpApiGroup.make("session")
|
|
.add(
|
|
HttpApiEndpoint.get("list", SessionPaths.list, {
|
|
query: ListQuery,
|
|
success: described(Schema.Array(Session.Info), "List of sessions"),
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.list",
|
|
summary: "List sessions",
|
|
description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("status", SessionPaths.status, {
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(StatusMap, "Get session status"),
|
|
error: HttpApiError.BadRequest,
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.status",
|
|
summary: "Get session status",
|
|
description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("get", SessionPaths.get, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Session.Info, "Get session"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.get",
|
|
summary: "Get session",
|
|
description: "Retrieve detailed information about a specific OpenCode session.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("children", SessionPaths.children, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Schema.Array(Session.Info), "List of children"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.children",
|
|
summary: "Get session children",
|
|
description: "Retrieve all child sessions that were forked from the specified parent session.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Schema.Array(Todo.Info), "Todo list"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.todo",
|
|
summary: "Get session todos",
|
|
description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("diff", SessionPaths.diff, {
|
|
params: { sessionID: SessionID },
|
|
query: DiffQuery,
|
|
success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"),
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.diff",
|
|
summary: "Get message diff",
|
|
description: "Get the file changes (diff) that resulted from a specific user message in the session.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("messages", SessionPaths.messages, {
|
|
params: { sessionID: SessionID },
|
|
query: MessagesQuery,
|
|
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.messages",
|
|
summary: "Get session messages",
|
|
description: "Retrieve all messages in a session, including user prompts and AI responses.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.get("message", SessionPaths.message, {
|
|
params: { sessionID: SessionID, messageID: MessageID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(MessageV2.WithParts, "Message"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.message",
|
|
summary: "Get message",
|
|
description: "Retrieve a specific message from a session by its message ID.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("create", SessionPaths.create, {
|
|
query: WorkspaceRoutingQuery,
|
|
payload: [HttpApiSchema.NoContent, Session.CreateInput],
|
|
success: described(Session.Info, "Successfully created session"),
|
|
error: HttpApiError.BadRequest,
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.create",
|
|
summary: "Create session",
|
|
description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Schema.Boolean, "Successfully deleted session"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.delete",
|
|
summary: "Delete session",
|
|
description: "Delete a session and permanently remove all associated data, including messages and history.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.patch("update", SessionPaths.update, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: UpdatePayload,
|
|
success: described(Session.Info, "Successfully updated session"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.update",
|
|
summary: "Update session",
|
|
description: "Update properties of an existing session, such as title or other metadata.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("fork", SessionPaths.fork, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: Schema.optional(ForkPayload),
|
|
success: described(Session.Info, "200"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.fork",
|
|
summary: "Fork session",
|
|
description: "Create a new session by forking an existing session at a specific message point.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Schema.Boolean, "Aborted session"),
|
|
error: HttpApiError.BadRequest,
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.abort",
|
|
summary: "Abort session",
|
|
description: "Abort an active session and stop any ongoing AI processing or command execution.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("init", SessionPaths.init, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: InitPayload,
|
|
success: described(Schema.Boolean, "200"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.init",
|
|
summary: "Initialize session",
|
|
description:
|
|
"Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("share", SessionPaths.share, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Session.Info, "Successfully shared session"),
|
|
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.share",
|
|
summary: "Share session",
|
|
description: "Create a shareable link for a session, allowing others to view the conversation.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Session.Info, "Successfully unshared session"),
|
|
error: [HttpApiError.InternalServerError, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.unshare",
|
|
summary: "Unshare session",
|
|
description: "Remove the shareable link for a session, making it private again.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: SummarizePayload,
|
|
success: described(Schema.Boolean, "Summarized session"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.summarize",
|
|
summary: "Summarize session",
|
|
description: "Generate a concise summary of the session using AI compaction to preserve key information.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: PromptPayload,
|
|
success: described(MessageV2.WithParts, "Created message"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.prompt",
|
|
summary: "Send message",
|
|
description: "Create and send a new message to a session, streaming the AI response.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: PromptPayload,
|
|
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.prompt_async",
|
|
summary: "Send async message",
|
|
description:
|
|
"Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("command", SessionPaths.command, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: CommandPayload,
|
|
success: described(MessageV2.WithParts, "Created message"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.command",
|
|
summary: "Send command",
|
|
description: "Send a new command to a session for execution by the AI assistant.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("shell", SessionPaths.shell, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: ShellPayload,
|
|
success: described(MessageV2.WithParts, "Created message"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.shell",
|
|
summary: "Run shell command",
|
|
description: "Execute a shell command within the session context and return the AI's response.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("revert", SessionPaths.revert, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: RevertPayload,
|
|
success: described(Session.Info, "Updated session"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.revert",
|
|
summary: "Revert message",
|
|
description:
|
|
"Revert a specific message in a session, undoing its effects and restoring the previous state.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
|
params: { sessionID: SessionID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Session.Info, "Updated session"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.unrevert",
|
|
summary: "Restore reverted messages",
|
|
description: "Restore all previously reverted messages in a session.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
|
params: { sessionID: SessionID, permissionID: PermissionID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: PermissionResponsePayload,
|
|
success: described(Schema.Boolean, "Permission processed successfully"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError, PermissionNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "permission.respond",
|
|
summary: "Respond to permission",
|
|
description: "Approve or deny a permission request from the AI assistant.",
|
|
deprecated: true,
|
|
}),
|
|
),
|
|
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
|
params: { sessionID: SessionID, messageID: MessageID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Schema.Boolean, "Successfully deleted message"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError, SessionBusyError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "session.deleteMessage",
|
|
summary: "Delete message",
|
|
description:
|
|
"Permanently delete a specific message and all of its parts from a session without reverting file changes.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
|
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
|
query: WorkspaceRoutingQuery,
|
|
success: described(Schema.Boolean, "Successfully deleted part"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "part.delete",
|
|
description: "Delete a part from a message.",
|
|
}),
|
|
),
|
|
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
|
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
|
query: WorkspaceRoutingQuery,
|
|
payload: MessageV2.Part,
|
|
success: described(MessageV2.Part, "Successfully updated part"),
|
|
error: [HttpApiError.BadRequest, ApiNotFoundError],
|
|
}).annotateMerge(
|
|
OpenApi.annotations({
|
|
identifier: "part.update",
|
|
description: "Update a part in a message.",
|
|
}),
|
|
),
|
|
)
|
|
.annotateMerge(
|
|
OpenApi.annotations({
|
|
title: "session",
|
|
description: "Experimental HttpApi session routes.",
|
|
}),
|
|
)
|
|
.middleware(InstanceContextMiddleware)
|
|
.middleware(WorkspaceRoutingMiddleware)
|
|
.middleware(Authorization),
|
|
)
|
|
.annotateMerge(
|
|
OpenApi.annotations({
|
|
title: "opencode experimental HttpApi",
|
|
version: "0.0.1",
|
|
description: "Experimental HttpApi surface for selected instance routes.",
|
|
}),
|
|
)
|